feat: trezor integration - #2468
Conversation
# Conflicts: # lib/features/receive/ui/screens/receive_qr_screen.dart # lib/features/send/ui/screens/send_screen.dart
Resolved conflict in lib/main.dart: kept TrezorDeeplinkListener wrap (Trezor callback delivery) and upstream's beta-banner builder (BETA corner banner for beta-channel builds). Both changes are non-overlapping.
trezor-connect
…ources/ AGENTS.md rule #14 ("Folders justify their existence — files don't justify folders"): one entity → <feature>/domain/<noun>.dart, not domain/entities/<noun>.dart; one datasource → data/<noun>_datasource.dart, not data/datasources/<noun>_datasource.dart. The Trezor slice has exactly one of each today (trezor_account, trezor_connect_datasource), so the wrapping folders aren't justified yet. Recreate them when a second file of the same kind lands.
feat: add Trezor hardware wallet support
The @trezorErrorUnknown metadata object in app_en.arb was missing its closing brace, merging it with the next key and breaking ARB/JSON parsing (flutter gen-l10n: FormatException at the bitcoinPrice... key).
|
Claude finished @ethicnology's task in 8m 5s —— View job PR Review: Trezor integration (trezor → develop)
🔴 Blockers1. Compile error — The Trezor menu entry was inserted inside the BitBox TabMenuVerticalButton(
title: Platform.isAndroid
? context.loc.importWalletBitBox
: context.loc.importWalletBitBoxNova,
onTap: () => context.pushNamed(
BitBoxRoute.importBitBox.name,
extra: const BitBoxRouteParams(
requestedDeviceType: SignerDeviceEntity.bitbox02,
),
),
), // ← close the BitBox button
const Gap(16),
TabMenuVerticalButton(
title: context.loc.importWalletTrezor,
onTap: () => context.pushNamed(TrezorRoute.importTrezor.name),
),
],2. Regression — duplicated verify buttons and Lightning leak, The three new rows were added above the pre-existing
Fix: drop the two new Ledger/BitBox lines and move the Trezor entry inside the guarded block, extending the guard: final showAddressVerification = !isLightning && (isLedger || isBitBox || isTrezor);
...
if (showAddressVerification) ...[
if (isLedger) const Column(children: [VerifyAddressOnLedgerButton()]),
if (isBitBox) const Column(children: [VerifyAddressOnBitBoxButton()]),
if (isTrezor) const Column(children: [VerifyAddressOnTrezorButton()]),
Gap(gap),
],🟠 Security3. Signed transaction from the deeplink callback is broadcast without validation — The signing round-trip goes out to Trezor Suite and comes back through the 4. Supply chain —
🟡 Worth verifying5. Possible double-handling of callback URIs —
6. Send-to-self outputs are marked as change — Any output carrying Notes (non-blocking)
SummaryThe feature architecture is clean (datasource → repository → usecase → cubit, typed errors, good test coverage of the tricky lifecycle races). However, the branch currently cannot compile due to the malformed widget tree in (Inline review comments could not be posted: this workflow only grants comment-update and CI-read tools, no PR review API.) |
The Trezor menu button was inserted inside the BitBox button's constructor argument list instead of as a sibling, leaving the brackets unbalanced and breaking compilation. Also drop the unused settings_cubit.dart import.
The Trezor verify-address row was added unconditionally above the existing showAddressVerification block, which still rendered the Ledger/BitBox buttons. This duplicated the verify button for Ledger and BitBox wallets and leaked all three verify buttons onto the Lightning receive screen, where there is no on-chain address to verify. Fold Trezor into the guarded block instead.
trezor_connect_datasource.dart imported package:bdk_dart directly, tripping the depend_on_referenced_packages lint since bdk_dart is only a transitive dependency. Every other BDK call site in the repo (including the closest analog, ledger_device_datasource.dart) goes through package:bull_sdk/bdk.dart, which re-exports the identical API so the app pins a single, SDK-managed bdk_dart version instead of a second parallel one.
Fixes CI format-check failures on the trezor operation base cubit and the three trezor UI screens.
BullishNode
left a comment
There was a problem hiding this comment.
Request changes — two findings
1. [P1] A previously signed transaction survives edits and can be broadcast instead of the transaction currently shown
SendCubit.updateSignedBitcoinTx stores the Trezor-signed raw transaction, but neither backClicked nor the amount-change path clears it.
Reproduction:
- Build and sign transaction A with Trezor.
- Navigate back and change the amount or recipient, producing transaction B.
- Return to confirmation. The screen displays the state for B, but
signedBitcoinTxstill contains A. onConfirmTransactionClickedsees a non-null signed transaction, skips rebuilding and signing, and broadcasts A.
This creates normal-flow wrong-payment potential. It is P1 rather than P0 because transaction A was previously reviewed and approved on the device, but the app must never broadcast it while showing the user the details of B.
Fix: invalidate signedBitcoinTx and all related finalized artifacts whenever any transaction-defining input changes, and again when rebuilding the unsigned PSBT. Add a regression test covering sign A → back/edit → review B → confirm, asserting that A cannot be broadcast.
2. [P1 security, medium confidence] The import callback uses predictable, unauthenticated request correlation
The app accepts any URI matching bullbitcoin://trezor-callback and forwards it to the connector after only checking the scheme and host (TrezorDeeplinkListener). The pinned connector generates the callback ID from the current millisecond timestamp and accepts the matching payload without authenticating its origin (connector source). During import, the returned xpub and descriptor are then used to construct the wallet (repository mapping).
A malicious local app that can observe or estimate when the request was created could race a forged callback containing its own xpub and descriptor. If successful, Bull Bitcoin would import an attacker-controlled watch-only wallet and later show receive addresses controlled by that attacker.
Confidence is medium because this requires a hostile local app plus successful timing observation/guessing, and no end-to-end exploit was demonstrated. The underlying trust-boundary weakness is nevertheless concrete and should be fixed before shipping xpub import.
Fix: use a cryptographically random, single-use state value; bind it to the expected operation and request; reject missing, mismatched, expired, or replayed state; strictly validate the response schema and account data; and prefer an app-owned verified universal/App Link where feasible.
signedBitcoinTx/signedBitcoinPsbt/signedLiquidTx survived backClicked() and every input-changing path (amount, recipient, UTXO selection), so onConfirmTransactionClicked's `signedBitcoinTx == null` check could see a signature finalized for a previously-built transaction and skip straight to broadcasting it while the confirm screen showed a different, edited transaction. createTransaction() is the single choke point every path back to the confirm step runs through (onAmountConfirmed, swap creation, utxo/fee edits) to rebuild the unsigned PSBT, so clear the three fields there before rebuilding; also clear them in backClicked() for defense in depth on the exact path called out in review. Adds a regression test that seeds a Trezor-signed tx, calls createTransaction() as if the user edited and re-confirmed, and asserts the stale signature cannot survive the rebuild.
|
Merge blocked upstream dependency We need either
Also registerCallback() uses a predictable timestamp as the sole correlation token for Trezor Suite deeplink callbacks. This must be reviewed and patched before this branch merges. Finding #1 (stale signed tx) is fixed and tested in 541a5bb; finding #2 remains open and gates the merge. |
Testing and finalizing @anipy1 contribution to be merged in develop