Skip to content

feat: transfer to spending from HW wallet#616

Open
jvsena42 wants to merge 24 commits into
masterfrom
feat/hw-transfer-to-spending
Open

feat: transfer to spending from HW wallet#616
jvsena42 wants to merge 24 commits into
masterfrom
feat/hw-transfer-to-spending

Conversation

@jvsena42

@jvsena42 jvsena42 commented Jul 6, 2026

Copy link
Copy Markdown
Member

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

  • Adds the Amount → Sign With Your Device → Transaction Signed flow, entered from the Hardware Wallet detail screen's Transfer To Spending button, then handing off to the existing Funds in Transfer progress screen.
  • Sources the available amount and MAX from the device's native-segwit account balance, minus the LSP fee and a composed on-chain mining-fee reserve, while respecting the LSP receiving-cap limit.
  • Composes the funding transaction, signs it on the Trezor, broadcasts it, then records the actual mining fee, fee rate, and channel on the transfer activity.
  • Confirms the hardware-wallet transfer activity from watcher data without clobbering its existing transfer metadata.
  • Prevents the total balance from double-counting during the paid-order → pending-channel → ready-channel transition by resolving each transfer's channel via its funding transaction.
  • Shows the shared Spending intro before the first hardware transfer, then continues straight to the hardware amount flow on subsequent transfers.
  • Hardens the signing path: reconnects a known Bluetooth device before signing, clears a stale session on a signing timeout, and gives the Trezor Bridge signing call a longer read timeout so on-device confirmation doesn't time out at 30s.
  • Reuses the existing transfer machinery — spending-limit calculation, Blocktank order, channel watcher, and the Funds in Transfer screen — swapping only local signing for on-device signing.

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 HwFundingSigner collaborator 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

  • 1. Hardware Wallet detail → Transfer To Spending → Amount: shows TRANSFER TO SPENDING, AVAILABLE from the device balance, and the 25% / MAX / unit-toggle controls.
  • 2. First-ever hardware transfer → Spending intro shows, Continue advances to the hardware Amount flow; subsequent transfers skip the intro.
  • 3. Amount → 25% or MAX → Continue → Sign: a Blocktank order is created and Sign shows the fee grid with the Trezor visual behind the button.
  • 4. Sign → Open Trezor Connect → approve on device: composes, signs, broadcasts, advances to Transaction Signed, then auto-forwards to Funds in Transfer; spending increases and the hardware balance drops by the signed send.
  • 5. MAX with a low LSP receiving cap: AVAILABLE is capped by the receivable amount and the wallet spends only the LSP fee plus the composed mining fee.
  • 6. After the funding tx confirms: the transfer activity confirms from watcher data and shows the actual composed mining fee.
  • 7. regression: Total Balance during paid-order → pending-channel → ready-channel does not double count the transfer amount while the pending channel appears.
  • 8a. BLE reconnect: device unlocked → Open Trezor Connect: silently reconnects, signs, and broadcasts.
    • 8b. on failure → Open Trezor Connect: shows the Reconnect Hardware Device error.
  • 9. Bridge signing: a long device-confirmation pause does not time out before approval (previously failed at 30s).
  • 10. Amount above the limit: input resets to MAX and shows the max-amount error toast.
  • 11a. Sign → Learn More: opens the liquidity info screen.
    • 11b. Sign → Advanced → adjust receiving balance: returns to Sign with updated fees.

Automated Checks

  • Unit tests added: BitkitTests/HwFundingSignerTests.swift (9), BitkitTests/TransferViewModelHwTests.swift (5), BitkitTests/HwWalletManagerFundingTests.swift (4), and shared BitkitTests/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.
  • Build passes: 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=YES because the Rust xcframeworks are arm64-only).
  • node scripts/validate-translations.js: 0 errors (run with glob resolved locally, since this checkout has no package.json).
  • SwiftFormat: clean on changed files.
  • No unit tests for the BalanceManager double-count fix or TransferService channel-resolution/activity marking: iOS's LightningService/BlocktankService/CoreService lack the protocol seams Android's repos have, so these are covered by the regtest E2E rather than isolated unit tests.
  • CI: standard build and test checks run by the PR bot.

Linked Issues/Tasks

Screenshot / Video

transfer-with-emulator.mov
close-channel.mov

@jvsena42 jvsena42 self-assigned this Jul 6, 2026
@jvsena42 jvsena42 added this to the 2.4.0 milestone Jul 6, 2026
@jvsena42 jvsena42 mentioned this pull request Jul 6, 2026
8 tasks
@jvsena42

jvsena42 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

waiting for upstream branch merge

Base automatically changed from feat/hw-wallet-connect to master July 6, 2026 18:11
@jvsena42 jvsena42 marked this pull request as ready for review July 6, 2026 21:11
@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds hardware-wallet funding for Transfer to Spending. The main changes are:

  • New hardware amount, sign, and signed screens.
  • Trezor reconnect, compose, sign, and broadcast orchestration.
  • Native-segwit funding balance and max-spendable calculation.
  • Transfer activity and balance reconciliation for hardware-funded channels.
  • Tests for signer, transfer view model, wallet funding, and activity handling.

Confidence Score: 4/5

The hardware transfer flow has two changed paths that can produce incorrect state.

  • The amount screen can create a zero-client-balance order.
  • A signing timeout can leave a broadcast funding transaction without local transfer tracking.
  • The rest of the balance and activity reconciliation changes are structured around existing transfer records.

Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift and Bitkit/ViewModels/HwFundingSigner.swift

Important Files Changed

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
Loading
%%{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
Loading

Reviews (1): Last reviewed commit: "fix: improve send marx calc for hardware..." | Re-trigger Greptile

Comment thread Bitkit/Views/Transfer/Hardware/SpendingAmountHw.swift
Comment on lines +119 to +124
private func signAndBroadcast(
deviceId: String,
funding tx: HwFundingTransaction
) async throws -> HwFundingBroadcastResult {
do {
return try await withTimeout(timeouts.sign) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +29 to +30
private var isValidAmount: Bool {
maxAllowed > 0 && amountSats <= maxAllowed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant