feat: transfer to spending from HW wallet#616
Conversation
|
waiting for upstream branch merge |
…s correctly set later by markOnchainActivityAsTransfer)
Greptile SummaryThis PR adds hardware-wallet funding for Transfer to Spending. The main changes are:
Confidence Score: 4/5The hardware transfer flow has two changed paths that can produce incorrect state.
Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift and Bitkit/ViewModels/HwFundingSigner.swift
|
| Filename | Overview |
|---|---|
| Bitkit/ViewModels/HwFundingSigner.swift | Adds hardware signing orchestration with reconnect, compose, sign, broadcast, and timeouts. |
| Bitkit/ViewModels/TransferViewModel.swift | Adds hardware transfer state, order funding coordination, and signing error handling. |
| Bitkit/Managers/HwWalletManager.swift | Adds funding account lookup, max-spendable estimation, compose/sign/broadcast helpers, and watcher sync. |
| Bitkit/Managers/BalanceManager.swift | Updates transfer-to-spending balance calculations to resolve channels from funding transactions. |
| Bitkit/Services/CoreService.swift | Adds helpers to sync hardware on-chain activity and mark funding transactions as transfers. |
| Bitkit/Services/TransferService.swift | Adds pending hardware transfer activity creation and channel association for funding transactions. |
| Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift | Adds the hardware amount screen and hardware-specific limit handling. |
| Bitkit/Views/Transfer/Hardware/SpendingHwSign.swift | Adds the hardware signing screen with fee display and advanced controls. |
| Bitkit/Views/Wallets/HardwareWalletScreen.swift | Routes eligible hardware wallets into the new transfer flow. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant Amount as SpendingAmountHw
participant VM as TransferViewModel
participant BT as Blocktank
participant Signer as HwFundingSigner
participant HW as HwWalletManager/Trezor
participant TS as TransferService
User->>Amount: Enter amount and continue
Amount->>BT: Create order
BT-->>Amount: Order
Amount->>VM: Store order
User->>VM: Open Trezor Connect
VM->>Signer: Sign order
Signer->>HW: Ensure connected
Signer->>HW: Compose funding tx
Signer->>HW: Sign and broadcast
HW-->>Signer: Broadcast result
Signer-->>VM: txId and fees
VM->>TS: Create transfer and pending activity
VM-->>User: Transaction signed
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant Amount as SpendingAmountHw
participant VM as TransferViewModel
participant BT as Blocktank
participant Signer as HwFundingSigner
participant HW as HwWalletManager/Trezor
participant TS as TransferService
User->>Amount: Enter amount and continue
Amount->>BT: Create order
BT-->>Amount: Order
Amount->>VM: Store order
User->>VM: Open Trezor Connect
VM->>Signer: Sign order
Signer->>HW: Ensure connected
Signer->>HW: Compose funding tx
Signer->>HW: Sign and broadcast
HW-->>Signer: Broadcast result
Signer-->>VM: txId and fees
VM->>TS: Create transfer and pending activity
VM-->>User: Transaction signed
Reviews (1): Last reviewed commit: "fix: improve send marx calc for hardware..." | Re-trigger Greptile
| private func signAndBroadcast( | ||
| deviceId: String, | ||
| funding tx: HwFundingTransaction | ||
| ) async throws -> HwFundingBroadcastResult { | ||
| do { | ||
| return try await withTimeout(timeouts.sign) { |
There was a problem hiding this comment.
Timed-Out Broadcast Loses Tracking
The timeout wraps signing and broadcasting as one operation, but the caller only records the transfer after this function returns successfully. If the timeout fires after the raw transaction has been handed to the broadcaster but before the result returns, the user sees a timeout while the funding tx can still be accepted on-chain, leaving the paid order without fundPaidOrder, order watching, or the pending transfer activity.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 102f858661
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| do { | ||
| let values = transfer.calculateTransferValues(clientBalanceSat: amountSats, blocktankInfo: blocktank.info) | ||
| let lspBalance = max(values.defaultLspBalance, values.minLspBalance) | ||
| let order = try await blocktank.createOrder(clientBalance: amountSats, lspBalance: lspBalance) |
There was a problem hiding this comment.
Block geoblocked hardware orders before signing
When GeoService.shared.isGeoBlocked is true, the existing software entry point disables the Blocktank-backed transfer button in FundingOptions, and TransferService.createTransfer will reject an LSP transfer. This hardware-only path still calls blocktank.createOrder and then proceeds to the signing screen; on confirm the geo check happens only after the Trezor funding transaction has been signed/broadcast, so a blocked user can send funds to the LSP before the app refuses to track the transfer. Guard this path before creating the order/signing.
Useful? React with 👍 / 👎.
| private var isValidAmount: Bool { | ||
| maxAllowed > 0 && amountSats <= maxAllowed |
There was a problem hiding this comment.
Disable continue while hardware limits are stale
hwSpending lives on the shared TransferViewModel, so maxAllowed can still contain the previous hardware wallet's limit when this view is recreated or while updateHwLimits(deviceId:) is loading. Because this predicate ignores transfer.hwSpending.isLoading, Continue can stay enabled against stale limits (including the initial zero amount) and create an order for a different/insufficient device before the fresh cap arrives. Clear the hardware spending state when starting a calculation or include the loading state in this validity check.
Useful? React with 👍 / 👎.
Refs #589
This PR adds the watch-only Transfer to Spending flow for funding a Lightning channel from a paired Trezor, with the on-chain funding transaction signed on the device. It builds on the connect-hardware flow (#614) and is an iOS port of bitkit-android's transfer-to-spending flow (synonymdev/bitkit-android#1039).
Description
Because iOS is Bluetooth-only in production, the Android USB/Bridge stale-session handling is limited to what the dev/E2E Bridge needs (the longer signing read timeout). The device-signing orchestration is extracted into a small
HwFundingSignercollaborator so the transfer view model keeps only the coordination that reuses the shared machinery. v1 funds from the native-segwit account only; multi-address-type spend is out of scope.QA Notes
OBS: driven on the Trezor Bridge emulator on regtest; please re-verify the full happy path and confirmation on a physical device.
Manual Tests
regression:Total Balance during paid-order → pending-channel → ready-channel does not double count the transfer amount while the pending channel appears.Automated Checks
BitkitTests/HwFundingSignerTests.swift(9),BitkitTests/TransferViewModelHwTests.swift(5),BitkitTests/HwWalletManagerFundingTests.swift(4), and sharedBitkitTests/HwTransferMocks.swift— cover the fee-reserve math (rate + both fallbacks), the reconnect/compose/sign/timeout orchestration and stale-session cleanup, the view model's error mapping and signing re-entrancy guard, and the native-segwit funding balance + funding-account resolution. 18/18 pass on the iPhone 16 simulator.xcodebuild -workspace Bitkit.xcodeproj/project.xcworkspace -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,id=<iPhone 16>' ONLY_ACTIVE_ARCH=YES build(concrete simulator UDID +ONLY_ACTIVE_ARCH=YESbecause the Rust xcframeworks are arm64-only).node scripts/validate-translations.js: 0 errors (run withglobresolved locally, since this checkout has nopackage.json).BalanceManagerdouble-count fix orTransferServicechannel-resolution/activity marking: iOS'sLightningService/BlocktankService/CoreServicelack the protocol seams Android's repos have, so these are covered by the regtest E2E rather than isolated unit tests.Linked Issues/Tasks
Screenshot / Video
transfer-with-emulator.mov
close-channel.mov