From f7fc9ae349961848a5c86c8d43f0a3c8a2c4d24f Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 1 Apr 2026 13:22:52 +0300 Subject: [PATCH 1/8] fix: avoid msat truncation when paying invoices with built-in amounts Bump bitkit-core to v0.1.56 which rounds up sub-satoshi invoice amounts. Additionally, stop overriding the amount for invoices that already have one. Pass sats: nil so LDK uses the invoice's native msat precision instead of our truncated sats value converted back to msat. Only pass sats for zero-amount invoices where the user specifies the amount. Closes #511 --- Bitkit.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 3 +-- Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift | 4 +++- Bitkit/Views/Wallets/Send/SendConfirmationView.swift | 9 +++++++-- Bitkit/Views/Wallets/Send/SendQuickpay.swift | 5 +++-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 05748e0d..c52a179b 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -937,8 +937,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/synonymdev/bitkit-core"; requirement = { - branch = master; - kind = branch; + kind = revision; + revision = 99bd86bb60c1f14e8ce8a6356cd2ab36f222fc69; }; }; 96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b1ca3574..3eca56f8 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/synonymdev/bitkit-core", "state" : { - "branch" : "master", - "revision" : "76a63a2654f717accde5268905897b73e4f7d3c4" + "revision" : "99bd86bb60c1f14e8ce8a6356cd2ab36f222fc69" } }, { diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index 5c823477..312eb2ec 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -220,9 +220,11 @@ struct LnurlPayConfirm: View { do { // Perform the Lightning payment (10s timeout → navigate to pending for hold invoices) + // LNURL server returns invoices with the amount baked in, so pass sats: nil + // to let LDK use the invoice's native millisatoshi precision. try await wallet.sendWithTimeout( bolt11: bolt11, - sats: wallet.sendAmountSats, + sats: nil, onTimeout: { app.addPendingPaymentHash(paymentHash) navigationPath.append(.pending(paymentHash: paymentHash)) diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift index d01b9330..585b1fa2 100644 --- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift +++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift @@ -480,10 +480,13 @@ struct SendConfirmationView: View { await createPreActivityMetadata(paymentId: paymentHash, paymentHash: paymentHash) // Perform the Lightning payment (10s timeout → navigate to pending for hold invoices) + // For invoices with a built-in amount, pass sats: nil so LDK uses the invoice's + // native millisatoshi precision instead of our truncated satoshi value. + let paymentSats: UInt64? = invoice.amountSatoshis == 0 ? amount : nil do { try await wallet.sendWithTimeout( bolt11: invoice.bolt11, - sats: amount, + sats: paymentSats, onTimeout: { app.addPendingPaymentHash(paymentHash) navigationPath.append(.pending(paymentHash: paymentHash)) @@ -790,7 +793,9 @@ struct SendConfirmationView: View { } if canSwitchWallet || app.selectedWalletToPayFrom == .lightning { - await wallet.refreshRoutingFeeEstimate(bolt11: bolt11, amountSats: wallet.sendAmountSats) + // For invoices with a built-in amount, pass nil so LDK uses native msat precision + let amountSats: UInt64? = app.scannedLightningInvoice?.amountSatoshis == 0 ? wallet.sendAmountSats : nil + await wallet.refreshRoutingFeeEstimate(bolt11: bolt11, amountSats: amountSats) } else { wallet.routingFeeEstimateSats = 0 } diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift index 24e0fdbc..a9311c82 100644 --- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift +++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift @@ -110,12 +110,13 @@ struct SendQuickpay: View { let parsedInvoice = try Bolt11Invoice.fromStr(invoiceStr: bolt11) let paymentHash = String(describing: parsedInvoice.paymentHash()) - let amount = wallet.sendAmountSats do { + // Quickpay only triggers for invoices with built-in amounts, so pass sats: nil + // to let LDK use the invoice's native millisatoshi precision. try await wallet.sendWithTimeout( bolt11: bolt11, - sats: amount, + sats: nil, onTimeout: { app.addPendingPaymentHash(paymentHash) navigationPath.append(.pending(paymentHash: paymentHash)) From 548dcb11ae91b91a14c3a8e97725eee17f919f5a Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 1 Apr 2026 15:10:31 +0300 Subject: [PATCH 2/8] chore: add changelog entry for msat fix Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c889ad..7219f879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Avoid msat truncation when paying invoices with built-in amounts #512 + [Unreleased]: https://github.com/synonymdev/bitkit-ios/compare/v2.1.2...HEAD From 617ecdcc71c95259ccdd718f7a48559ddc7c85ce Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 3 Apr 2026 09:25:41 +0300 Subject: [PATCH 3/8] fix: preserve msat precision for LNURL pay and withdraw callbacks LNURL protocol uses millisatoshis, but the app was normalizing msat values to sats in-place on the data structs. When those sats were later converted back to msats for callbacks, the fractional part was lost (e.g. 500500 msat -> 501 sats -> 501000 msat, rejected by server). - Stop normalizing LNURL data structs in-place; keep original msat values - Change fetchLnurlInvoice to accept amountMsats directly - Convert to sats only for display (floor) and validation (ceil for min) - For fixed-amount LNURL-pay, pass original msat value to callback - For fixed-amount LNURL-withdraw, use floor(msats/1000) for invoice amount Co-Authored-By: Claude Opus 4.6 (1M context) --- Bitkit/Utilities/Lnurl.swift | 6 +++--- Bitkit/ViewModels/AppViewModel.swift | 21 ++++--------------- .../LnurlWithdraw/LnurlWithdrawAmount.swift | 5 +++-- .../LnurlWithdraw/LnurlWithdrawConfirm.swift | 6 +++--- .../Views/Wallets/Send/LnurlPayAmount.swift | 8 +++---- .../Views/Wallets/Send/LnurlPayConfirm.swift | 10 ++++++--- Bitkit/Views/Wallets/Send/SendQuickpay.swift | 8 +++---- 7 files changed, 27 insertions(+), 37 deletions(-) diff --git a/Bitkit/Utilities/Lnurl.swift b/Bitkit/Utilities/Lnurl.swift index db72554c..bdff5059 100644 --- a/Bitkit/Utilities/Lnurl.swift +++ b/Bitkit/Utilities/Lnurl.swift @@ -123,17 +123,17 @@ struct LnurlHelper { /// Fetches a Lightning invoice from an LNURL pay callback /// - Parameters: /// - callbackUrl: The LNURL callback URL - /// - amount: The amount in satoshis to pay + /// - amountMsats: The amount in millisatoshis to pay /// - comment: Optional comment to include with the payment /// - Returns: The bolt11 invoice string /// - Throws: Network or parsing errors static func fetchLnurlInvoice( callbackUrl: String, - amount: UInt64, + amountMsats: UInt64, comment: String? = nil ) async throws -> String { var queryItems = [ - URLQueryItem(name: "amount", value: String(amount * 1000)), // Convert to millisatoshis + URLQueryItem(name: "amount", value: String(amountMsats)), ] // Add comment if provided diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 998f2036..c5421980 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -480,19 +480,14 @@ extension AppViewModel { } private func handleLnurlPayInvoice(_ data: LnurlPayData) { - // Check if lightning service is running guard lightningService.status?.isRunning == true else { toast(type: .error, title: "Lightning not running", description: "Please try again later.") return } - var normalizedData = data - normalizedData.minSendable = max(1, LightningAmountConversion.satsCeil(fromMsats: normalizedData.minSendable)) - normalizedData.maxSendable = max(normalizedData.minSendable, LightningAmountConversion.satsFloor(fromMsats: normalizedData.maxSendable)) - - // Check if user has enough lightning balance to pay the minimum amount + let minSats = max(1, LightningAmountConversion.satsCeil(fromMsats: data.minSendable)) let lightningBalance = lightningService.balances?.totalLightningBalanceSats ?? 0 - if lightningBalance < normalizedData.minSendable { + if lightningBalance < minSats { toast( type: .warning, title: t("other__lnurl_pay_error"), @@ -502,11 +497,10 @@ extension AppViewModel { } selectedWalletToPayFrom = .lightning - lnurlPayData = normalizedData + lnurlPayData = data } private func handleLnurlWithdraw(_ data: LnurlWithdrawData) { - // Check if lightning service is running guard lightningService.status?.isRunning == true else { toast(type: .error, title: "Lightning not running", description: "Please try again later.") return @@ -515,7 +509,6 @@ extension AppViewModel { let minMsats = data.minWithdrawable ?? Env.msatsPerSat let maxMsats = data.maxWithdrawable - // Check if minWithdrawable > maxWithdrawable if minMsats > maxMsats { toast( type: .warning, @@ -525,13 +518,7 @@ extension AppViewModel { return } - var normalizedData = data let minSats = max(1, LightningAmountConversion.satsCeil(fromMsats: minMsats)) - let maxSats = max(minSats, LightningAmountConversion.satsFloor(fromMsats: maxMsats)) - normalizedData.minWithdrawable = minSats - normalizedData.maxWithdrawable = maxSats - - // Check if we have enough receiving capacity let lightningBalance = lightningService.balances?.totalLightningBalanceSats ?? 0 if lightningBalance < minSats { toast( @@ -542,7 +529,7 @@ extension AppViewModel { return } - lnurlWithdrawData = normalizedData + lnurlWithdrawData = data } private func handleLnurlChannel(_ data: LnurlChannelData) { diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift index ba2c91df..65da9367 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawAmount.swift @@ -9,11 +9,12 @@ struct LnurlWithdrawAmount: View { @StateObject private var amountViewModel = AmountInputViewModel() var minAmount: Int { - Int(app.lnurlWithdrawData!.minWithdrawable ?? 1) + let minMsats = app.lnurlWithdrawData!.minWithdrawable ?? Env.msatsPerSat + return Int(max(1, LightningAmountConversion.satsCeil(fromMsats: minMsats))) } var maxAmount: Int { - Int(app.lnurlWithdrawData!.maxWithdrawable) + Int(LightningAmountConversion.satsFloor(fromMsats: app.lnurlWithdrawData!.maxWithdrawable)) } var amount: UInt64 { diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift index e90f3889..ae3c1d67 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift @@ -9,12 +9,12 @@ struct LnurlWithdrawConfirm: View { @State private var isLoading = false var amount: UInt64 { - // Fixed amount + // Fixed amount: floor ensures the invoice doesn't exceed the server's max if app.lnurlWithdrawData!.maxWithdrawable == app.lnurlWithdrawData!.minWithdrawable { - return app.lnurlWithdrawData!.maxWithdrawable + return LightningAmountConversion.satsFloor(fromMsats: app.lnurlWithdrawData!.maxWithdrawable) } - // For variable amount, use the amount from the previous screen + // For variable amount, use the amount from the previous screen (already in sats) return wallet.lnurlWithdrawAmount! } diff --git a/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift b/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift index a3d4ed33..97196d43 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayAmount.swift @@ -11,7 +11,7 @@ struct LnurlPayAmount: View { var maxAmount: UInt64 { // TODO: subtract fee - min(app.lnurlPayData!.maxSendable, UInt64(wallet.totalLightningSats)) + min(LightningAmountConversion.satsFloor(fromMsats: app.lnurlPayData!.maxSendable), UInt64(wallet.totalLightningSats)) } var amount: UInt64 { @@ -80,12 +80,12 @@ struct LnurlPayAmount: View { } private func onContinue() { - let minSendable = app.lnurlPayData!.minSendable + let minSendableSats = max(1, LightningAmountConversion.satsCeil(fromMsats: app.lnurlPayData!.minSendable)) - if amount < minSendable { + if amount < minSendableSats { app.toast( type: .error, title: t("wallet__lnurl_pay__error_min__title"), - description: t("wallet__lnurl_pay__error_min__description", variables: ["amount": "\(minSendable)"]), + description: t("wallet__lnurl_pay__error_min__description", variables: ["amount": "\(minSendableSats)"]), accessibilityIdentifier: "LnurlPayAmountTooLowToast" ) return diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index 94204286..b7de19f3 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -29,7 +29,7 @@ struct LnurlPayConfirm: View { VStack(alignment: .leading) { MoneyStack( - sats: Int(wallet.sendAmountSats ?? app.lnurlPayData!.minSendable), + sats: Int(wallet.sendAmountSats ?? LightningAmountConversion.satsFloor(fromMsats: app.lnurlPayData!.minSendable)), showSymbol: true, testIdPrefix: "ReviewAmount" ) @@ -186,12 +186,16 @@ struct LnurlPayConfirm: View { throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing LNURL pay data"]) } - let amount = wallet.sendAmountSats ?? lnurlPayData.minSendable + let amountMsats: UInt64 = if let userSats = wallet.sendAmountSats { + userSats * 1000 + } else { + lnurlPayData.minSendable + } // Fetch the Lightning invoice from LNURL let bolt11 = try await LnurlHelper.fetchLnurlInvoice( callbackUrl: lnurlPayData.callback, - amount: amount, + amountMsats: amountMsats, comment: comment.isEmpty ? nil : comment ) diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift index a9311c82..d9910bd4 100644 --- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift +++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift @@ -88,14 +88,12 @@ struct SendQuickpay: View { // Handle LNURL Pay if let lnurlPayData = app.lnurlPayData { - let amount = lnurlPayData.minSendable - - // Set the amount for the success screen - wallet.sendAmountSats = amount + // Set the amount in sats for the success screen + wallet.sendAmountSats = LightningAmountConversion.satsFloor(fromMsats: lnurlPayData.minSendable) bolt11Invoice = try await LnurlHelper.fetchLnurlInvoice( callbackUrl: lnurlPayData.callback, - amount: amount + amountMsats: lnurlPayData.minSendable ) } else if let scannedInvoice = app.scannedLightningInvoice { wallet.sendAmountSats = scannedInvoice.amountSatoshis From 0723c7e983eb344053181e43d191eaa527f30864 Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 3 Apr 2026 09:35:08 +0300 Subject: [PATCH 4/8] chore: update changelog with LNURL msat fix Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7219f879..93b28814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- Preserve msat precision for LNURL pay and withdraw callbacks #512 - Avoid msat truncation when paying invoices with built-in amounts #512 [Unreleased]: https://github.com/synonymdev/bitkit-ios/compare/v2.1.2...HEAD From ef88089ced8f89992daf0a842b85679fb5616d9a Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 3 Apr 2026 10:45:05 +0300 Subject: [PATCH 5/8] fix: use msat-precision invoices for fixed-amount LNURL withdraw For LNURL-withdraw with sub-sat precision (e.g. 222538 msat), neither floor (222 sats) nor ceiling (223 sats) matches the server's exact amount range. Add receiveMsats/createInvoiceMsats to create invoices with native msat precision, used for fixed-amount LNURL withdrawals. Co-Authored-By: Claude Opus 4.6 (1M context) --- Bitkit/Services/LightningService.swift | 8 +++-- Bitkit/ViewModels/WalletViewModel.swift | 6 ++++ .../LnurlWithdraw/LnurlWithdrawConfirm.swift | 34 ++++++++++++------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 15c9c213..899f7b1e 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -499,16 +499,20 @@ class LightningService { } func receive(amountSats: UInt64? = nil, description: String, expirySecs: UInt32 = 3600) async throws -> String { + try await receiveMsats(amountMsats: amountSats.map { $0 * 1000 }, description: description, expirySecs: expirySecs) + } + + func receiveMsats(amountMsats: UInt64? = nil, description: String, expirySecs: UInt32 = 3600) async throws -> String { guard let node else { throw AppError(serviceError: .nodeNotSetup) } let bolt11 = try await ServiceQueue.background(.ldk) { - if let amountSats { + if let amountMsats { try node .bolt11Payment() .receive( - amountMsat: amountSats * 1000, + amountMsat: amountMsats, description: Bolt11InvoiceDescription.direct(description: description), expirySecs: expirySecs ) diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index ea8b0f2f..ac3b3057 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -363,6 +363,12 @@ class WalletViewModel: ObservableObject { return invoice.lowercased() } + func createInvoiceMsats(amountMsats: UInt64, note: String, expirySecs: UInt32? = nil) async throws -> String { + let finalExpirySecs = expirySecs ?? 60 * 60 * 24 + let invoice = try await lightningService.receiveMsats(amountMsats: amountMsats, description: note, expirySecs: finalExpirySecs) + return invoice.lowercased() + } + @discardableResult func waitForNodeToRun(timeoutSeconds: Double = 10.0) async -> Bool { guard nodeLifecycleState != .running else { return true } diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift index ae3c1d67..4af80f12 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift @@ -8,13 +8,14 @@ struct LnurlWithdrawConfirm: View { let onFailure: (UInt64) -> Void @State private var isLoading = false - var amount: UInt64 { - // Fixed amount: floor ensures the invoice doesn't exceed the server's max - if app.lnurlWithdrawData!.maxWithdrawable == app.lnurlWithdrawData!.minWithdrawable { + var isFixedAmount: Bool { + app.lnurlWithdrawData!.maxWithdrawable == app.lnurlWithdrawData!.minWithdrawable + } + + var displayAmountSats: UInt64 { + if isFixedAmount { return LightningAmountConversion.satsFloor(fromMsats: app.lnurlWithdrawData!.maxWithdrawable) } - - // For variable amount, use the amount from the previous screen (already in sats) return wallet.lnurlWithdrawAmount! } @@ -22,7 +23,7 @@ struct LnurlWithdrawConfirm: View { VStack(spacing: 0) { SheetHeader(title: t("wallet__lnurl_w_title"), showBackButton: true) - MoneyStack(sats: Int(amount), showSymbol: true, testIdPrefix: "WithdrawAmount") + MoneyStack(sats: Int(displayAmountSats), showSymbol: true, testIdPrefix: "WithdrawAmount") .padding(.top, 16) .padding(.bottom, 42) @@ -58,12 +59,19 @@ struct LnurlWithdrawConfirm: View { throw NSError(domain: "LNURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing LNURL withdraw data"]) } - // Create a Lightning invoice for the withdraw - let invoice = try await wallet.createInvoice( - amountSats: amount, - note: withdrawData.defaultDescription, - expirySecs: 3600 - ) + let invoice: String = if isFixedAmount { + try await wallet.createInvoiceMsats( + amountMsats: withdrawData.maxWithdrawable, + note: withdrawData.defaultDescription, + expirySecs: 3600 + ) + } else { + try await wallet.createInvoice( + amountSats: displayAmountSats, + note: withdrawData.defaultDescription, + expirySecs: 3600 + ) + } // Perform the LNURL withdraw try await LnurlHelper.handleLnurlWithdraw( @@ -84,7 +92,7 @@ struct LnurlWithdrawConfirm: View { } catch { await MainActor.run { - onFailure(amount) + onFailure(displayAmountSats) isLoading = false } } From 851826fae556a994aade22cd3e01ad0e59615cd2 Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 3 Apr 2026 18:02:54 +0300 Subject: [PATCH 6/8] fix: use satsCeil for LNURL display amounts and revert LDK Node rev Use ceiling division for LNURL display amounts to match BOLT11 behavior. Previously LNURL-pay showed 222 sats on review but 223 after sending, and LNURL-withdraw showed 222 while BOLT11 showed 223 for the same 222222 msat amount. Also revert the LDK Node revision to the master-pinned version (c5698d00) which was inadvertently changed during package resolution. This fixes the BitkitNotification linker failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- Bitkit.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift | 2 +- Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift | 2 +- Bitkit/Views/Wallets/Send/SendQuickpay.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index c52a179b..a16a01e2 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -930,7 +930,7 @@ repositoryURL = "https://github.com/synonymdev/ldk-node"; requirement = { kind = revision; - revision = ae38eadab70fceb5dbe242bc02bf895581cb7c3f; + revision = c5698d00066e0e50f33696afc562d71023da2373; }; }; 96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3eca56f8..f3c26d48 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,7 +23,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/synonymdev/ldk-node", "state" : { - "revision" : "ae38eadab70fceb5dbe242bc02bf895581cb7c3f" + "revision" : "c5698d00066e0e50f33696afc562d71023da2373" } }, { diff --git a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift index 4af80f12..37eb0b9c 100644 --- a/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift +++ b/Bitkit/Views/Wallets/LnurlWithdraw/LnurlWithdrawConfirm.swift @@ -14,7 +14,7 @@ struct LnurlWithdrawConfirm: View { var displayAmountSats: UInt64 { if isFixedAmount { - return LightningAmountConversion.satsFloor(fromMsats: app.lnurlWithdrawData!.maxWithdrawable) + return LightningAmountConversion.satsCeil(fromMsats: app.lnurlWithdrawData!.maxWithdrawable) } return wallet.lnurlWithdrawAmount! } diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index b7de19f3..bd44c37f 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -29,7 +29,7 @@ struct LnurlPayConfirm: View { VStack(alignment: .leading) { MoneyStack( - sats: Int(wallet.sendAmountSats ?? LightningAmountConversion.satsFloor(fromMsats: app.lnurlPayData!.minSendable)), + sats: Int(wallet.sendAmountSats ?? LightningAmountConversion.satsCeil(fromMsats: app.lnurlPayData!.minSendable)), showSymbol: true, testIdPrefix: "ReviewAmount" ) diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift index d9910bd4..db782f90 100644 --- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift +++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift @@ -89,7 +89,7 @@ struct SendQuickpay: View { // Handle LNURL Pay if let lnurlPayData = app.lnurlPayData { // Set the amount in sats for the success screen - wallet.sendAmountSats = LightningAmountConversion.satsFloor(fromMsats: lnurlPayData.minSendable) + wallet.sendAmountSats = LightningAmountConversion.satsCeil(fromMsats: lnurlPayData.minSendable) bolt11Invoice = try await LnurlHelper.fetchLnurlInvoice( callbackUrl: lnurlPayData.callback, From 91a35c576a426d032b6e8beac691799d92e918dc Mon Sep 17 00:00:00 2001 From: benk10 Date: Sun, 5 Apr 2026 08:17:46 +0300 Subject: [PATCH 7/8] fix: use ceiling division for PaymentDetails.amountSats Activity list showed 222 sats while review screen showed 223 for a 222222 msat payment. The PaymentDetails extension used floor division (amountMsat / 1000). Use satsCeil to match all other display paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- Bitkit/Extensions/PaymentDetails.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Extensions/PaymentDetails.swift b/Bitkit/Extensions/PaymentDetails.swift index 9798f929..4889b76b 100644 --- a/Bitkit/Extensions/PaymentDetails.swift +++ b/Bitkit/Extensions/PaymentDetails.swift @@ -4,7 +4,7 @@ import LDKNode extension PaymentDetails { var amountSats: UInt64? { if let amountMsat { - return amountMsat / 1000 + return LightningAmountConversion.satsCeil(fromMsats: amountMsat) } return nil From 36af4dafbd9ba42a5498706b82183f76c616397b Mon Sep 17 00:00:00 2001 From: benk10 Date: Sun, 5 Apr 2026 08:36:16 +0300 Subject: [PATCH 8/8] fix: use ceiling for received payment notification amount The received transaction sheet used floor division for the msat amount. Co-Authored-By: Claude Opus 4.6 (1M context) --- Bitkit/ViewModels/AppViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index c5421980..21f94f3a 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -726,7 +726,8 @@ extension AppViewModel { } await MainActor.run { - sheetViewModel.showSheet(.receivedTx, data: ReceivedTxSheetDetails(type: .lightning, sats: amountMsat / 1000)) + let sats = LightningAmountConversion.satsCeil(fromMsats: amountMsat) + sheetViewModel.showSheet(.receivedTx, data: ReceivedTxSheetDetails(type: .lightning, sats: sats)) } } case .channelPending(channelId: _, userChannelId: _, formerTemporaryChannelId: _, counterpartyNodeId: _, fundingTxo: _):