diff --git a/Bitkit/Utilities/SpendingConfirmTotal.swift b/Bitkit/Utilities/SpendingConfirmTotal.swift new file mode 100644 index 000000000..9e2f809b7 --- /dev/null +++ b/Bitkit/Utilities/SpendingConfirmTotal.swift @@ -0,0 +1,18 @@ +import Foundation + +/// Confirm total for the spending transfer screen: the amount of bitcoin that leaves the wallet. +enum SpendingConfirmTotal { + /// When send-all funds the order, the sweep spends `maxSendable + networkFee` (the spendable balance). + /// Otherwise the total is the order fee plus the miner fee. + static func leavingAmount( + orderFeeSat: UInt64, + networkFeeSat: UInt64, + shouldUseSendAll: Bool, + maxSendable: UInt64? + ) -> UInt64 { + if shouldUseSendAll, let maxSendable { + return maxSendable.saturatingAdd(networkFeeSat) + } + return orderFeeSat.saturatingAdd(networkFeeSat) + } +} diff --git a/Bitkit/ViewModels/TransferViewModel.swift b/Bitkit/ViewModels/TransferViewModel.swift index 9179d6659..af5caa89d 100644 --- a/Bitkit/ViewModels/TransferViewModel.swift +++ b/Bitkit/ViewModels/TransferViewModel.swift @@ -399,7 +399,12 @@ class TransferViewModel: ObservableObject { isMaxAmount: isMaxAmount ) - let txTotalSats = order.feeSat + txFee + let txTotalSats = SpendingConfirmTotal.leavingAmount( + orderFeeSat: order.feeSat, + networkFeeSat: txFee, + shouldUseSendAll: isMaxAmount, + maxSendable: maxSendableAmount + ) // Pre-activity metadata lets the LDK activity sync recognize this send as a transfer. let currentTime = UInt64(Date().timeIntervalSince1970) diff --git a/Bitkit/Views/Transfer/SpendingConfirm.swift b/Bitkit/Views/Transfer/SpendingConfirm.swift index 77cd8ce93..2a86b6af9 100644 --- a/Bitkit/Views/Transfer/SpendingConfirm.swift +++ b/Bitkit/Views/Transfer/SpendingConfirm.swift @@ -26,7 +26,12 @@ struct SpendingConfirm: View { } var total: UInt64 { - transfer.uiState.feeSat + transactionFee + SpendingConfirmTotal.leavingAmount( + orderFeeSat: transfer.uiState.feeSat, + networkFeeSat: transactionFee, + shouldUseSendAll: shouldUseSendAll, + maxSendable: maxSendableAmount + ) } var body: some View { diff --git a/BitkitTests/SpendingConfirmTotalTests.swift b/BitkitTests/SpendingConfirmTotalTests.swift new file mode 100644 index 000000000..f50165874 --- /dev/null +++ b/BitkitTests/SpendingConfirmTotalTests.swift @@ -0,0 +1,77 @@ +@testable import Bitkit +import XCTest + +final class SpendingConfirmTotalTests: XCTestCase { + func testNormalPath_UsesOrderFeePlusNetworkFee() { + XCTAssertEqual( + SpendingConfirmTotal.leavingAmount( + orderFeeSat: 50_000, + networkFeeSat: 250, + shouldUseSendAll: false, + maxSendable: nil + ), + 50_250 + ) + } + + func testNormalPath_IgnoresStaleMaxSendable() { + XCTAssertEqual( + SpendingConfirmTotal.leavingAmount( + orderFeeSat: 50_000, + networkFeeSat: 250, + shouldUseSendAll: false, + maxSendable: 99_750 + ), + 50_250 + ) + } + + func testSendAllPath_UsesMaxSendablePlusNetworkFee() { + // balance 100_000, send-all fee 250 → maxSendable 99_750; amount leaving = 100_000 + XCTAssertEqual( + SpendingConfirmTotal.leavingAmount( + orderFeeSat: 50_000, + networkFeeSat: 250, + shouldUseSendAll: true, + maxSendable: 99_750 + ), + 100_000 + ) + } + + func testNormalPath_SaturatesOnOverflow() { + XCTAssertEqual( + SpendingConfirmTotal.leavingAmount( + orderFeeSat: UInt64.max, + networkFeeSat: 1, + shouldUseSendAll: false, + maxSendable: nil + ), + UInt64.max + ) + } + + func testSendAllPath_SaturatesOnOverflow() { + XCTAssertEqual( + SpendingConfirmTotal.leavingAmount( + orderFeeSat: 1, + networkFeeSat: 1, + shouldUseSendAll: true, + maxSendable: UInt64.max + ), + UInt64.max + ) + } + + func testSendAllPath_WithoutMaxSendable_FallsBackToOrderPlusFee() { + XCTAssertEqual( + SpendingConfirmTotal.leavingAmount( + orderFeeSat: 50_000, + networkFeeSat: 250, + shouldUseSendAll: true, + maxSendable: nil + ), + 50_250 + ) + } +} diff --git a/changelog.d/next/772.fixed.md b/changelog.d/next/772.fixed.md new file mode 100644 index 000000000..5e7da60bd --- /dev/null +++ b/changelog.d/next/772.fixed.md @@ -0,0 +1 @@ +Spending confirm now shows the amount of bitcoin that leaves the wallet when a transfer uses send-all funding, and the transfer record uses that same amount.