Skip to content

Commit d65a5b5

Browse files
authored
refactor: move LSP balance calculations to BitkitCore (#265)
* refactor: move LSP balance calculations to BitkitCore and improve transfer to spending flow * Fix comments
1 parent 4907e05 commit d65a5b5

File tree

5 files changed

+123
-169
lines changed

5 files changed

+123
-169
lines changed

Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Bitkit/ViewModels/BlocktankViewModel.swift

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -209,44 +209,27 @@ class BlocktankViewModel: ObservableObject {
209209
)
210210
}
211211

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

218-
// Get current rates
219218
guard let rates = currencyService.loadCachedRates(),
220219
let eurRate = currencyService.getCurrentRate(for: "EUR", from: rates)
221220
else {
222221
Logger.error("Failed to get EUR rate for lspBalance calculation")
223222
throw CustomServiceError.currencyRateUnavailable
224223
}
225224

226-
// Calculate thresholds in sats
227-
let threshold1 = currencyService.convertFiatToSats(fiatValue: 225, rate: eurRate)
228-
let threshold2 = currencyService.convertFiatToSats(fiatValue: 495, rate: eurRate)
229-
let defaultLspBalance = currencyService.convertFiatToSats(fiatValue: 450, rate: eurRate)
230-
231-
Logger.debug("getDefaultLspBalance - clientBalance: \(clientBalance)")
232-
Logger.debug("getDefaultLspBalance - maxLspBalance: \(maxLspBalance)")
233-
Logger.debug("getDefaultLspBalance - defaultLspBalance: \(defaultLspBalance)")
234-
235-
// Safely calculate lspBalance to avoid arithmetic overflow
236-
var lspBalance: UInt64 = 0
237-
if defaultLspBalance > clientBalance {
238-
lspBalance = defaultLspBalance - clientBalance
239-
}
240-
241-
if clientBalance > threshold1 {
242-
lspBalance = clientBalance
243-
}
244-
245-
if clientBalance > threshold2 {
246-
lspBalance = maxLspBalance
247-
}
225+
let satsPerEur = currencyService.convertFiatToSats(fiatValue: 1, rate: eurRate)
226+
let params = DefaultLspBalanceParams(
227+
clientBalanceSat: clientBalance,
228+
maxChannelSizeSat: info?.options.maxChannelSizeSat ?? 0,
229+
satsPerEur: satsPerEur
230+
)
248231

249-
return min(lspBalance, maxLspBalance)
232+
return BitkitCore.getDefaultLspBalance(params: params)
250233
}
251234

252235
func refreshMinCjitSats() async throws {

Bitkit/ViewModels/TransferViewModel.swift

Lines changed: 28 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -294,91 +294,52 @@ class TransferViewModel: ObservableObject {
294294
selectedChannelIds = []
295295
}
296296

297-
// MARK: - Balance Calculation Functions
297+
// MARK: - Balance Calculation
298+
299+
/// Calculates channel liquidity options using bitkit-core
300+
func calculateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) -> TransferValues {
301+
guard let blocktankInfo else {
302+
return TransferValues()
303+
}
298304

299-
func getDefaultLspBalance(clientBalanceSat: UInt64, maxLspBalance: UInt64) -> UInt64 {
300-
// Get current rates
301305
guard let rates = currencyService.loadCachedRates(),
302306
let eurRate = currencyService.getCurrentRate(for: "EUR", from: rates)
303307
else {
304-
Logger.error("Failed to get rates for getDefaultLspBalance", context: "TransferViewModel")
305-
return 0
308+
Logger.error("Failed to get rates for calculateTransferValues", context: "TransferViewModel")
309+
return TransferValues()
306310
}
307311

308-
// Calculate thresholds in sats
309-
let threshold1 = currencyService.convertFiatToSats(fiatValue: 225, rate: eurRate)
310-
let threshold2 = currencyService.convertFiatToSats(fiatValue: 495, rate: eurRate)
311-
let defaultLspBalanceSats = currencyService.convertFiatToSats(fiatValue: 450, rate: eurRate)
312-
313-
var lspBalance = Int64(defaultLspBalanceSats) - Int64(clientBalanceSat)
314-
315-
// Ensure non-negative result
316-
if lspBalance < 0 {
317-
lspBalance = 0
318-
}
312+
let satsPerEur = currencyService.convertFiatToSats(fiatValue: 1, rate: eurRate)
313+
let existingChannelsTotalSat = totalBtChannelsValueSats(blocktankInfo: blocktankInfo)
319314

320-
if clientBalanceSat > threshold1 {
321-
lspBalance = Int64(clientBalanceSat)
322-
}
315+
let params = ChannelLiquidityParams(
316+
clientBalanceSat: clientBalanceSat,
317+
existingChannelsTotalSat: existingChannelsTotalSat,
318+
minChannelSizeSat: blocktankInfo.options.minChannelSizeSat,
319+
maxChannelSizeSat: blocktankInfo.options.maxChannelSizeSat,
320+
satsPerEur: satsPerEur
321+
)
323322

324-
if clientBalanceSat > threshold2 {
325-
lspBalance = Int64(maxLspBalance)
326-
}
323+
let options = BitkitCore.calculateChannelLiquidityOptions(params: params)
327324

328-
return min(UInt64(lspBalance), maxLspBalance)
325+
return TransferValues(
326+
defaultLspBalance: options.defaultLspBalanceSat,
327+
minLspBalance: options.minLspBalanceSat,
328+
maxLspBalance: options.maxLspBalanceSat,
329+
maxClientBalance: options.maxClientBalanceSat
330+
)
329331
}
330332

331-
func getMinLspBalance(clientBalance: UInt64, minChannelSize: UInt64) -> UInt64 {
332-
// LSP balance must be at least 2.5% of the channel size for LDK to accept (reserve balance)
333-
let ldkMinimum = UInt64(Double(clientBalance) * 0.025)
334-
// Channel size must be at least minChannelSize
335-
let lspMinimum = clientBalance < minChannelSize ? minChannelSize - clientBalance : 0
336-
337-
return max(ldkMinimum, lspMinimum)
333+
func updateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) {
334+
transferValues = calculateTransferValues(clientBalanceSat: clientBalanceSat, blocktankInfo: blocktankInfo)
338335
}
339336

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

346-
func updateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) {
347-
transferValues = calculateTransferValues(clientBalanceSat: clientBalanceSat, blocktankInfo: blocktankInfo)
348-
}
349-
350-
func calculateTransferValues(clientBalanceSat: UInt64, blocktankInfo: IBtInfo?) -> TransferValues {
351-
guard let blocktankInfo else {
352-
return TransferValues()
353-
}
354-
355-
// Calculate the total value of existing Blocktank channels
356-
let channelsSize = totalBtChannelsValueSats(blocktankInfo: blocktankInfo)
357-
358-
let minChannelSizeSat = UInt64(blocktankInfo.options.minChannelSizeSat)
359-
let maxChannelSizeSat = UInt64(blocktankInfo.options.maxChannelSizeSat)
360-
361-
// Because LSP limits constantly change depending on network fees
362-
// Add a 2% buffer to avoid fluctuations while making the order
363-
let maxChannelSize1 = UInt64(Double(maxChannelSizeSat) * 0.98)
364-
365-
// The maximum channel size the user can open including existing channels
366-
let maxChannelSize2 = maxChannelSize1 > channelsSize ? maxChannelSize1 - channelsSize : 0
367-
let maxChannelSize = min(maxChannelSize1, maxChannelSize2)
368-
369-
let minLspBalance = getMinLspBalance(clientBalance: clientBalanceSat, minChannelSize: minChannelSizeSat)
370-
let maxLspBalance = maxChannelSize > clientBalanceSat ? maxChannelSize - clientBalanceSat : 0
371-
let defaultLspBalance = getDefaultLspBalance(clientBalanceSat: clientBalanceSat, maxLspBalance: maxLspBalance)
372-
let maxClientBalance = getMaxClientBalance(maxChannelSize: maxChannelSize)
373-
374-
return TransferValues(
375-
defaultLspBalance: defaultLspBalance,
376-
minLspBalance: minLspBalance,
377-
maxLspBalance: maxLspBalance,
378-
maxClientBalance: maxClientBalance
379-
)
380-
}
381-
382343
// MARK: - Manual Channel Opening
383344

384345
/// Opens a manual channel and tracks the transfer

Bitkit/Views/Transfer/SpendingAdvancedView.swift

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ struct SpendingAdvancedView: View {
88
@EnvironmentObject var blocktank: BlocktankViewModel
99
@EnvironmentObject var currency: CurrencyViewModel
1010
@EnvironmentObject var transfer: TransferViewModel
11-
@EnvironmentObject var wallet: WalletViewModel
1211
@Environment(\.dismiss) var dismiss
1312

1413
@StateObject private var amountViewModel = AmountInputViewModel()
1514
@State private var feeEstimate: UInt64?
15+
@State private var isLoading = false
16+
@State private var feeEstimateTask: Task<Void, Never>?
1617

1718
var lspBalance: UInt64 {
1819
amountViewModel.amountSats
1920
}
2021

2122
private var isValid: Bool {
22-
let isAboveMin = lspBalance >= transfer.transferValues.minLspBalance
23-
let isBelowMax = lspBalance <= transfer.transferValues.maxLspBalance
24-
return isAboveMin && isBelowMax
23+
let values = transfer.transferValues
24+
guard lspBalance > 0, values.maxLspBalance > 0 else { return false }
25+
return lspBalance >= values.minLspBalance && lspBalance <= values.maxLspBalance
2526
}
2627

2728
var body: some View {
@@ -73,15 +74,17 @@ struct SpendingAdvancedView: View {
7374

7475
CustomButton(
7576
title: t("common__continue"),
76-
isDisabled: !isValid
77+
isDisabled: !isValid,
78+
isLoading: isLoading
7779
) {
80+
isLoading = true
81+
defer { isLoading = false }
82+
7883
do {
79-
// Create a new order with the specified receiving capacity
8084
let newOrder = try await blocktank.createOrder(
8185
clientBalance: order.clientBalanceSat,
8286
lspBalance: lspBalance
8387
)
84-
8588
transfer.onAdvancedOrderCreated(order: newOrder)
8689
dismiss()
8790
} catch {
@@ -102,7 +105,11 @@ struct SpendingAdvancedView: View {
102105
updateFeeEstimate()
103106
}
104107
.onChange(of: lspBalance) { _ in
105-
updateFeeEstimate()
108+
if isValid {
109+
updateFeeEstimate()
110+
} else {
111+
feeEstimate = nil
112+
}
106113
}
107114
}
108115

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

132-
Task {
139+
feeEstimateTask?.cancel()
140+
feeEstimate = nil
141+
142+
feeEstimateTask = Task {
133143
do {
134-
feeEstimate = nil
135144
let estimate = try await blocktank.estimateOrderFee(
136145
clientBalance: order.clientBalanceSat,
137146
lspBalance: lspBalance
138147
)
139-
feeEstimate = estimate.feeSat
148+
guard !Task.isCancelled else { return }
149+
await MainActor.run {
150+
feeEstimate = estimate.feeSat
151+
}
140152
} catch {
141-
feeEstimate = nil
142-
Logger.error("Failed to estimate fee: \(error.localizedDescription)")
153+
guard !Task.isCancelled else { return }
154+
Logger.debug("Fee estimation failed: \(error.localizedDescription)")
143155
}
144156
}
145157
}
@@ -150,7 +162,6 @@ struct SpendingAdvancedView: View {
150162
SpendingAdvancedView(
151163
order: IBtOrder.mock(lspBalanceSat: 100_000, clientBalanceSat: 50000)
152164
)
153-
.environmentObject(WalletViewModel())
154165
.environmentObject(AppViewModel())
155166
.environmentObject(CurrencyViewModel())
156167
.environmentObject(BlocktankViewModel())

0 commit comments

Comments
 (0)