1+ import BitkitCore
12import LDKNode
23import SwiftUI
34
@@ -12,13 +13,31 @@ struct SpendingAmount: View {
1213
1314 @State private var amountViewModel = AmountInputViewModel ( )
1415 @State private var isLoading = false
16+ @State private var isCalculatingMax = true
1517 @State private var availableAmount : UInt64 ?
1618 @State private var maxTransferAmount : UInt64 ?
1719
1820 private var amountSats : UInt64 {
1921 amountViewModel. amountSats
2022 }
2123
24+ /// Inputs the max calculation depends on. A single `.task(id:)` keyed on this restarts the
25+ /// calculation (with structured cancellation) whenever either input changes, so concurrent
26+ /// reloads can neither overlap nor leak the loading flag.
27+ private struct MaxCalcInputs : Equatable {
28+ let maxChannelSizeSat : UInt64 ?
29+ let maxClientBalanceSat : UInt64 ?
30+ let spendableOnchainBalanceSats : Int
31+ }
32+
33+ private var maxCalcInputs : MaxCalcInputs {
34+ MaxCalcInputs (
35+ maxChannelSizeSat: blocktank. info? . options. maxChannelSizeSat,
36+ maxClientBalanceSat: blocktank. info? . options. maxClientBalanceSat,
37+ spendableOnchainBalanceSats: wallet. spendableOnchainBalanceSats
38+ )
39+ }
40+
2241 private var isValidAmount : Bool {
2342 guard let max = maxTransferAmount else { return false }
2443 return amountSats <= max
@@ -61,7 +80,8 @@ struct SpendingAmount: View {
6180
6281 NumberPad (
6382 type: amountViewModel. getNumberPadType ( currency: currency) ,
64- errorKey: amountViewModel. errorKey
83+ errorKey: amountViewModel. errorKey,
84+ isDisabled: isCalculatingMax
6585 ) { key in
6686 amountViewModel. handleNumberPadInput ( key, currency: currency)
6787 }
@@ -79,22 +99,28 @@ struct SpendingAmount: View {
7999 . padding ( . horizontal, 16 )
80100 . bottomSafeAreaPadding ( )
81101 . offlineOverlay ( title: t ( " lightning__transfer__nav_title " ) )
82- . task ( id: blocktank. info? . options. maxChannelSizeSat) {
102+ . task ( id: maxCalcInputs) {
103+ await MainActor . run { isCalculatingMax = true }
83104 await calculateMaxTransferAmount ( )
84- }
85- . onChange ( of: wallet. spendableOnchainBalanceSats) {
86- Task {
87- await calculateMaxTransferAmount ( )
105+ if !Task. isCancelled {
106+ await MainActor . run { isCalculatingMax = false }
88107 }
89108 }
90109 . onChange ( of: maxTransferAmount) { updateInputCap ( ) }
91- . onChange ( of: amountViewModel. maxExceededCount) { showMaxExceededToast ( ) }
110+ . onChange ( of: amountViewModel. maxExceededCount) { onMaxExceeded ( ) }
92111 }
93112
94113 private func updateInputCap( ) {
95114 amountViewModel. maxAmountOverride = ( maxTransferAmount ?? 0 ) > 0 ? maxTransferAmount : nil
96115 }
97116
117+ private func onMaxExceeded( ) {
118+ if let max = maxTransferAmount {
119+ amountViewModel. updateFromSats ( max, currency: currency)
120+ }
121+ showMaxExceededToast ( )
122+ }
123+
98124 private func showMaxExceededToast( ) {
99125 app. toast (
100126 type: . warning,
@@ -178,10 +204,9 @@ struct SpendingAmount: View {
178204
179205 guard let feeEstimates = await feeEstimatesManager. getEstimates ( refresh: true ) else {
180206 await MainActor . run {
181- let balance = UInt64 ( wallet. spendableOnchainBalanceSats)
182- availableAmount = balance
183- let values = transfer. calculateTransferValues ( clientBalanceSat: balance, blocktankInfo: info)
184- maxTransferAmount = min ( values. maxClientBalance, balance)
207+ let fallback = fallbackMaxTransferAmount ( info: info)
208+ availableAmount = fallback
209+ maxTransferAmount = fallback
185210 }
186211 return
187212 }
@@ -193,40 +218,37 @@ struct SpendingAmount: View {
193218 satsPerVByte: fastFeeRate
194219 )
195220
196- // First pass: estimate with calculatedAvailableAmount to get approximate clientBalance
197- let values1 = transfer. calculateTransferValues ( clientBalanceSat: calculatedAvailableAmount, blocktankInfo: info)
198- let lspBalance1 = max ( values1. defaultLspBalance, values1. minLspBalance)
199- let feeEstimate1 = try await blocktank. estimateOrderFee (
200- clientBalance: calculatedAvailableAmount,
201- lspBalance: lspBalance1
202- )
203- let lspFees1 = feeEstimate1. networkFeeSat + feeEstimate1. serviceFeeSat
204- let approxClientBalance = UInt64 ( max ( 0 , Int64 ( calculatedAvailableAmount) - Int64( lspFees1) ) )
205-
206- // Second pass: recalculate lspBalance with actual clientBalance (same as onContinue will use)
207- // This ensures fee estimation matches the actual order creation
208- let values2 = transfer. calculateTransferValues ( clientBalanceSat: approxClientBalance, blocktankInfo: info)
209- let lspBalance2 = max ( values2. defaultLspBalance, values2. minLspBalance)
210- let feeEstimate2 = try await blocktank. estimateOrderFee (
211- clientBalance: approxClientBalance,
212- lspBalance: lspBalance2
221+ let ( available, maxAmount) = try await transfer. calculateSpendingLimits (
222+ onchainAvailable: calculatedAvailableAmount,
223+ lspMaxClientBalance: info. options. maxClientBalanceSat,
224+ transferValues: { transfer. calculateTransferValues ( clientBalanceSat: $0, blocktankInfo: info) } ,
225+ estimateOrderFee: { clientBalance, lspBalance in
226+ let estimate = try await blocktank. estimateOrderFee ( clientBalance: clientBalance, lspBalance: lspBalance)
227+ return ( estimate. networkFeeSat, estimate. serviceFeeSat)
228+ }
213229 )
214- let lspFees = feeEstimate2. networkFeeSat + feeEstimate2. serviceFeeSat
215- let maxClientBalance = UInt64 ( max ( 0 , Int64 ( calculatedAvailableAmount) - Int64( lspFees) ) )
216- let result = min ( values2. maxClientBalance, maxClientBalance)
217230
218231 await MainActor . run {
219- availableAmount = calculatedAvailableAmount
220- maxTransferAmount = result
232+ availableAmount = available
233+ maxTransferAmount = maxAmount
221234 }
222235 } catch {
223236 Logger . error ( " Failed to calculate max transfer amount: \( error) " )
224237 await MainActor . run {
225- let balance = UInt64 ( wallet. spendableOnchainBalanceSats)
226- availableAmount = balance
227- let values = transfer. calculateTransferValues ( clientBalanceSat: balance, blocktankInfo: info)
228- maxTransferAmount = min ( values. maxClientBalance, balance)
238+ let fallback = fallbackMaxTransferAmount ( info: info)
239+ availableAmount = fallback
240+ maxTransferAmount = fallback
229241 }
230242 }
231243 }
244+
245+ /// Fallback max when fee estimates are unavailable: clamp the client balance to the LSP's max
246+ /// client balance so the liquidity calculation doesn't collapse to zero on a saturating balance.
247+ private func fallbackMaxTransferAmount( info: IBtInfo ) -> UInt64 {
248+ let balance = UInt64 ( wallet. spendableOnchainBalanceSats)
249+ let lspMaxClientBalance = info. options. maxClientBalanceSat
250+ let clientBalance = lspMaxClientBalance > 0 ? min ( balance, lspMaxClientBalance) : balance
251+ let values = transfer. calculateTransferValues ( clientBalanceSat: clientBalance, blocktankInfo: info)
252+ return min ( values. maxClientBalance, balance)
253+ }
232254}
0 commit comments