Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 8 additions & 25 deletions Bitkit/ViewModels/BlocktankViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,44 +209,27 @@ class BlocktankViewModel: ObservableObject {
)
}

/// Calculates default LSP balance for CJIT channels using bitkit-core
private func getDefaultLspBalance(clientBalance: UInt64) async throws -> UInt64 {
if info == nil {
try await refreshInfo()
}
let maxLspBalance = info?.options.maxChannelSizeSat ?? 0

// Get current rates
guard let rates = currencyService.loadCachedRates(),
let eurRate = currencyService.getCurrentRate(for: "EUR", from: rates)
else {
Logger.error("Failed to get EUR rate for lspBalance calculation")
throw CustomServiceError.currencyRateUnavailable
}

// Calculate thresholds in sats
let threshold1 = currencyService.convertFiatToSats(fiatValue: 225, rate: eurRate)
let threshold2 = currencyService.convertFiatToSats(fiatValue: 495, rate: eurRate)
let defaultLspBalance = currencyService.convertFiatToSats(fiatValue: 450, rate: eurRate)

Logger.debug("getDefaultLspBalance - clientBalance: \(clientBalance)")
Logger.debug("getDefaultLspBalance - maxLspBalance: \(maxLspBalance)")
Logger.debug("getDefaultLspBalance - defaultLspBalance: \(defaultLspBalance)")

// Safely calculate lspBalance to avoid arithmetic overflow
var lspBalance: UInt64 = 0
if defaultLspBalance > clientBalance {
lspBalance = defaultLspBalance - clientBalance
}

if clientBalance > threshold1 {
lspBalance = clientBalance
}

if clientBalance > threshold2 {
lspBalance = maxLspBalance
}
let satsPerEur = currencyService.convertFiatToSats(fiatValue: 1, rate: eurRate)
let params = DefaultLspBalanceParams(
clientBalanceSat: clientBalance,
maxChannelSizeSat: info?.options.maxChannelSizeSat ?? 0,
satsPerEur: satsPerEur
)

return min(lspBalance, maxLspBalance)
return BitkitCore.getDefaultLspBalance(params: params)
}

func refreshMinCjitSats() async throws {
Expand Down
95 changes: 28 additions & 67 deletions Bitkit/ViewModels/TransferViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,91 +294,52 @@ class TransferViewModel: ObservableObject {
selectedChannelIds = []
}

// MARK: - Balance Calculation Functions
// MARK: - Balance Calculation

/// Calculates channel liquidity options using bitkit-core
func calculateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) -> TransferValues {
guard let blocktankInfo else {
return TransferValues()
}

func getDefaultLspBalance(clientBalanceSat: UInt64, maxLspBalance: UInt64) -> UInt64 {
// Get current rates
guard let rates = currencyService.loadCachedRates(),
let eurRate = currencyService.getCurrentRate(for: "EUR", from: rates)
else {
Logger.error("Failed to get rates for getDefaultLspBalance", context: "TransferViewModel")
return 0
Logger.error("Failed to get rates for calculateTransferValues", context: "TransferViewModel")
return TransferValues()
}

// Calculate thresholds in sats
let threshold1 = currencyService.convertFiatToSats(fiatValue: 225, rate: eurRate)
let threshold2 = currencyService.convertFiatToSats(fiatValue: 495, rate: eurRate)
let defaultLspBalanceSats = currencyService.convertFiatToSats(fiatValue: 450, rate: eurRate)

var lspBalance = Int64(defaultLspBalanceSats) - Int64(clientBalanceSat)

// Ensure non-negative result
if lspBalance < 0 {
lspBalance = 0
}
let satsPerEur = currencyService.convertFiatToSats(fiatValue: 1, rate: eurRate)
let existingChannelsTotalSat = totalBtChannelsValueSats(blocktankInfo: blocktankInfo)

if clientBalanceSat > threshold1 {
lspBalance = Int64(clientBalanceSat)
}
let params = ChannelLiquidityParams(
clientBalanceSat: clientBalanceSat,
existingChannelsTotalSat: existingChannelsTotalSat,
minChannelSizeSat: blocktankInfo.options.minChannelSizeSat,
maxChannelSizeSat: blocktankInfo.options.maxChannelSizeSat,
satsPerEur: satsPerEur
)

if clientBalanceSat > threshold2 {
lspBalance = Int64(maxLspBalance)
}
let options = BitkitCore.calculateChannelLiquidityOptions(params: params)

return min(UInt64(lspBalance), maxLspBalance)
return TransferValues(
defaultLspBalance: options.defaultLspBalanceSat,
minLspBalance: options.minLspBalanceSat,
maxLspBalance: options.maxLspBalanceSat,
maxClientBalance: options.maxClientBalanceSat
)
}

func getMinLspBalance(clientBalance: UInt64, minChannelSize: UInt64) -> UInt64 {
// LSP balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
let ldkMinimum = UInt64(Double(clientBalance) * 0.025)
// Channel size must be at least minChannelSize
let lspMinimum = clientBalance < minChannelSize ? minChannelSize - clientBalance : 0

return max(ldkMinimum, lspMinimum)
func updateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) {
transferValues = calculateTransferValues(clientBalanceSat: clientBalanceSat, blocktankInfo: blocktankInfo)
}

/// Calculates max client balance accounting for LDK reserve requirement
func getMaxClientBalance(maxChannelSize: UInt64) -> UInt64 {
// Remote balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
let minRemoteBalance = UInt64(Double(maxChannelSize) * 0.025)
return maxChannelSize - minRemoteBalance
}

func updateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) {
transferValues = calculateTransferValues(clientBalanceSat: clientBalanceSat, blocktankInfo: blocktankInfo)
}

func calculateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) -> TransferValues {
guard let blocktankInfo else {
return TransferValues()
}

// Calculate the total value of existing Blocktank channels
let channelsSize = totalBtChannelsValueSats(blocktankInfo: blocktankInfo)

let minChannelSizeSat = UInt64(blocktankInfo.options.minChannelSizeSat)
let maxChannelSizeSat = UInt64(blocktankInfo.options.maxChannelSizeSat)

// Because LSP limits constantly change depending on network fees
// Add a 2% buffer to avoid fluctuations while making the order
let maxChannelSize1 = UInt64(Double(maxChannelSizeSat) * 0.98)

// The maximum channel size the user can open including existing channels
let maxChannelSize2 = maxChannelSize1 > channelsSize ? maxChannelSize1 - channelsSize : 0
let maxChannelSize = min(maxChannelSize1, maxChannelSize2)

let minLspBalance = getMinLspBalance(clientBalance: clientBalanceSat, minChannelSize: minChannelSizeSat)
let maxLspBalance = maxChannelSize > clientBalanceSat ? maxChannelSize - clientBalanceSat : 0
let defaultLspBalance = getDefaultLspBalance(clientBalanceSat: clientBalanceSat, maxLspBalance: maxLspBalance)
let maxClientBalance = getMaxClientBalance(maxChannelSize: maxChannelSize)

return TransferValues(
defaultLspBalance: defaultLspBalance,
minLspBalance: minLspBalance,
maxLspBalance: maxLspBalance,
maxClientBalance: maxClientBalance
)
}

// MARK: - Manual Channel Opening

/// Opens a manual channel and tracks the transfer
Expand Down
39 changes: 25 additions & 14 deletions Bitkit/Views/Transfer/SpendingAdvancedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ struct SpendingAdvancedView: View {
@EnvironmentObject var blocktank: BlocktankViewModel
@EnvironmentObject var currency: CurrencyViewModel
@EnvironmentObject var transfer: TransferViewModel
@EnvironmentObject var wallet: WalletViewModel
@Environment(\.dismiss) var dismiss

@StateObject private var amountViewModel = AmountInputViewModel()
@State private var feeEstimate: UInt64?
@State private var isLoading = false
@State private var feeEstimateTask: Task<Void, Never>?

var lspBalance: UInt64 {
amountViewModel.amountSats
}

private var isValid: Bool {
let isAboveMin = lspBalance >= transfer.transferValues.minLspBalance
let isBelowMax = lspBalance <= transfer.transferValues.maxLspBalance
return isAboveMin && isBelowMax
let values = transfer.transferValues
guard lspBalance > 0, values.maxLspBalance > 0 else { return false }
return lspBalance >= values.minLspBalance && lspBalance <= values.maxLspBalance
}

var body: some View {
Expand Down Expand Up @@ -73,15 +74,17 @@ struct SpendingAdvancedView: View {

CustomButton(
title: t("common__continue"),
isDisabled: !isValid
isDisabled: !isValid,
isLoading: isLoading
) {
isLoading = true
defer { isLoading = false }

do {
// Create a new order with the specified receiving capacity
let newOrder = try await blocktank.createOrder(
clientBalance: order.clientBalanceSat,
lspBalance: lspBalance
)

transfer.onAdvancedOrderCreated(order: newOrder)
dismiss()
} catch {
Expand All @@ -102,7 +105,11 @@ struct SpendingAdvancedView: View {
updateFeeEstimate()
}
.onChange(of: lspBalance) { _ in
updateFeeEstimate()
if isValid {
updateFeeEstimate()
} else {
feeEstimate = nil
}
}
}

Expand All @@ -129,17 +136,22 @@ struct SpendingAdvancedView: View {
private func updateFeeEstimate() {
guard lspBalance > 0 else { return }

Task {
feeEstimateTask?.cancel()
feeEstimate = nil

feeEstimateTask = Task {
do {
feeEstimate = nil
let estimate = try await blocktank.estimateOrderFee(
clientBalance: order.clientBalanceSat,
lspBalance: lspBalance
)
feeEstimate = estimate.feeSat
guard !Task.isCancelled else { return }
await MainActor.run {
feeEstimate = estimate.feeSat
}
} catch {
feeEstimate = nil
Logger.error("Failed to estimate fee: \(error.localizedDescription)")
guard !Task.isCancelled else { return }
Logger.debug("Fee estimation failed: \(error.localizedDescription)")
}
}
}
Expand All @@ -150,7 +162,6 @@ struct SpendingAdvancedView: View {
SpendingAdvancedView(
order: IBtOrder.mock(lspBalanceSat: 100_000, clientBalanceSat: 50000)
)
.environmentObject(WalletViewModel())
.environmentObject(AppViewModel())
.environmentObject(CurrencyViewModel())
.environmentObject(BlocktankViewModel())
Expand Down
Loading
Loading