Skip to content
Open
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
6f9fdf4
add trezor connect option
anipy1 May 28, 2026
4b44f81
feat(trezor): import wallet via Trezor connect deeplink
anipy1 May 30, 2026
f7ffbc6
feat(trezor): verify receive address on device
anipy1 May 31, 2026
7696975
feat(trezor): sign transactions via Trezor Suite
anipy1 May 31, 2026
45e758d
Merge remote-tracking branch 'upstream/develop' into trezor-connect
anipy1 May 31, 2026
8022c83
fix(trezor): verify address by comparing trezor response to passed ad…
anipy1 Jun 3, 2026
bbfb78e
fix(trezor): carry master fingreprint with TrezorAccount, drop cache
anipy1 Jun 4, 2026
71c1eab
fix(trezor): use wallet ScriptType for change-output script type
anipy1 Jun 4, 2026
f2ca3fd
fix(trezor): use anipy1/trezor_connect fork with callback error fix
anipy1 Jun 5, 2026
6ddb8c3
chore(trezor): tidy pubspec formatting + restore upstream PR link in …
anipy1 Jun 6, 2026
3a5d3df
Merge upstream/develop into trezor-connect
anipy1 Jun 6, 2026
886099b
Merge branch 'develop' into trezor-connect
anipy1 Jun 6, 2026
f7923d1
fix(trezor): surface clean error when descriptor is missing
anipy1 Jun 6, 2026
38458f7
Merge branch 'develop' into trezor-connect
anipy1 Jun 6, 2026
5dc84c3
fix(trezor): consume import_watch_only_wallet via public facade
anipy1 Jun 6, 2026
d53e965
fix(trezor): split TrezorOperationCubit into typed per-operation cubits
anipy1 Jun 6, 2026
a895d00
test(trezor): add focused tests for address verification + import paths
anipy1 Jun 6, 2026
5ff5d71
fix(trezor): thread network through import/verify/sign
anipy1 Jun 6, 2026
5e677d2
fix(trezor): route deeplink callback through application port
anipy1 Jun 6, 2026
620ab3b
fix(trezor): guard cubit emits against close during Suite round-trip
anipy1 Jun 6, 2026
0b43098
fix(trezor): map package TrezorLaunchException to suiteNotInstalled
anipy1 Jun 6, 2026
74d67c1
fix(trezor): detect Suite-unresponsive via app lifecycle
anipy1 Jun 6, 2026
6727dd0
fix(trezor): guard route params via redirect
anipy1 Jun 7, 2026
15f032d
fix(trezor): narrow Android intent-filter to host=trezor-callback
anipy1 Jun 7, 2026
d0d5073
fix(trezor): centralize Trezor capability claims
anipy1 Jun 7, 2026
990ea72
fix(trezor): point copy at the device, not suite
anipy1 Jun 7, 2026
f2fc4fb
chore(linux): regenerate plugin registrant for app_links
anipy1 Jun 7, 2026
bcfc899
fix(trezor): copy raw address from verify screen, not the spaced one
anipy1 Jun 7, 2026
7272658
fix(trezor): delete dead state and screen branches
anipy1 Jun 7, 2026
61aacad
fix(trezor): drop unused derivationPath from sign-transaction route
anipy1 Jun 7, 2026
b0e0f5c
fix(trezor): drop TrezorSignedPsbt DTO, return signed hex directly
anipy1 Jun 7, 2026
5ef0d03
fix(trezor): narrow getAccounts API to getDefaultAccount
anipy1 Jun 7, 2026
21837af
fix(trezor): migrate Trezor UX copy to ARB localization
anipy1 Jun 7, 2026
6c89ed9
fix(trezor): use PAYTOADDRESS script_type for external outputs
anipy1 Jun 7, 2026
e65c3f4
Merge remote-tracking branch 'upstream/develop' into trezor-connect
anipy1 Jun 11, 2026
7a2d013
style(trezor): use initializing formals for private fields
anipy1 Jun 11, 2026
904679c
Merge branch 'develop' into
anipy1 Jun 16, 2026
c39807c
fix(trezor): suppress stale completion after grace-expiry fallback
anipy1 Jun 16, 2026
7df5f48
chore(trezor): revert unrelated pubspec.lock churn
anipy1 Jun 16, 2026
f6e22e7
fix(trezor): localize remaining Trezor snackbars in send/receive
anipy1 Jun 16, 2026
d4a6986
chore(trezor): delete unused TrezorFacade scaffolding
anipy1 Jun 16, 2026
87a9add
refactor(trezor): converge layout and error name to target architecture
anipy1 Jun 17, 2026
75c4226
refactor(trezor): enforce repository contract with abstract interface…
anipy1 Jun 17, 2026
c3cf2ae
refactor(trezor): rename framework_errors to trezor_data_exceptions
anipy1 Jun 17, 2026
a2215c2
refactor(trezor): localize errors via TrezorError.toTranslated
anipy1 Jun 17, 2026
81d511a
refactor(trezor): flatten single-file domain/entities/ and data/datas…
anipy1 Jun 19, 2026
c18310a
Merge remote-tracking branch 'upstream/develop' into trezor-connect
anipy1 Jun 19, 2026
2bc28d7
refactor(trezor): reclassify callback dispatcher as a port
anipy1 Jun 19, 2026
f10ab46
refactor(trezor): split locator port registration from repository
anipy1 Jun 19, 2026
804d47d
Merge branch 'develop' into trezor-connect
i5hi Jun 25, 2026
ac0b083
Merge branch 'trezor' into trezor-connect
ethicnology Jul 17, 2026
6ff0616
Merge pull request #2223 from anipy1/trezor-connect
ethicnology Jul 17, 2026
ac6a604
fix(hardware-wallet): close unterminated trezorErrorUnknown arb entry
ethicnology Jul 17, 2026
608f4a2
fix(import-wallet): fix malformed widget tree breaking build
ethicnology Jul 17, 2026
19ddf60
fix(receive): dedupe verify-address buttons and gate trezor by lightning
ethicnology Jul 17, 2026
a96e7f2
fix(trezor): import bdk via bull_sdk gateway instead of bdk_dart
ethicnology Jul 17, 2026
60fce80
style(trezor): apply dart format
ethicnology Jul 17, 2026
541a5bb
fix(send): clear stale signed tx when rebuilding the unsigned PSBT
ethicnology Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
</intent-filter>
<!-- Custom-scheme callback for Trezor Connect deeplinks.
Restricted to host="trezor-callback" so only the exact
callback URI we issue (bullbitcoin://trezor-callback?...)
is claimed at the platform level — other bullbitcoin://*
URIs fall through to the system instead of being routed
into the app and dropped in Dart. TrezorDeeplinkListener
still validates scheme+host as belt-and-suspenders. -->
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bullbitcoin" android:host="trezor-callback" />
</intent-filter>
<!-- USB device attached intent filter for BitBox02 -->
<intent-filter>
<action
Expand Down
10 changes: 10 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@
<string>$(GOOGLE_DRIVE_URL_SCHEME)</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.bullbitcoin.app.trezor-callback</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bullbitcoin</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
Expand Down
5 changes: 4 additions & 1 deletion lib/core/entities/signer_device_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ enum SignerDeviceEntity {
ledgerStax,
passport,
seedsigner,
specter;
specter,
trezor;

String get displayName => switch (this) {
SignerDeviceEntity.bitbox02 => 'BitBox02',
Expand All @@ -26,6 +27,8 @@ enum SignerDeviceEntity {

bool get isBitBox => name.startsWith('bitbox');

bool get isTrezor => this == SignerDeviceEntity.trezor;

bool get supportsBluetooth =>
isLedger && this != SignerDeviceEntity.ledgerNanoSPlus;

Expand Down
5 changes: 4 additions & 1 deletion lib/core/storage/tables/wallet_metadata_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ enum SignerDevice {
ledgerStax,
passport,
seedsigner,
specter;
specter,
trezor;

static SignerDevice fromEntity(SignerDeviceEntity entity) => switch (entity) {
SignerDeviceEntity.bitbox02 => SignerDevice.bitbox02,
Expand All @@ -53,6 +54,7 @@ enum SignerDevice {
SignerDeviceEntity.passport => SignerDevice.passport,
SignerDeviceEntity.seedsigner => SignerDevice.seedsigner,
SignerDeviceEntity.specter => SignerDevice.specter,
SignerDeviceEntity.trezor => SignerDevice.trezor,
};

SignerDeviceEntity toEntity() => switch (this) {
Expand All @@ -69,6 +71,7 @@ enum SignerDevice {
SignerDevice.passport => SignerDeviceEntity.passport,
SignerDevice.seedsigner => SignerDeviceEntity.seedsigner,
SignerDevice.specter => SignerDeviceEntity.specter,
SignerDevice.trezor => SignerDeviceEntity.trezor,
};
}

Expand Down
6 changes: 6 additions & 0 deletions lib/features/import_wallet/import_wallet_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:bb_mobile/features/import_mnemonic/router.dart';
import 'package:bb_mobile/features/import_qr_device/router.dart';
import 'package:bb_mobile/features/import_watch_only_wallet/import_watch_only_router.dart';
import 'package:bb_mobile/features/ledger/ui/ledger_router.dart';
import 'package:bb_mobile/features/trezor/ui/trezor_router.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
Expand Down Expand Up @@ -119,6 +120,11 @@ class ImportWalletPage extends StatelessWidget {
),
),
),
const Gap(16),
TabMenuVerticalButton(
title: context.loc.importWalletTrezor,
onTap: () => context.pushNamed(TrezorRoute.importTrezor.name),
),
],
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export 'package:bb_mobile/features/import_watch_only_wallet/watch_only_wallet_entity.dart';

export 'package:bb_mobile/features/import_watch_only_wallet/import_watch_only_router.dart'
show ImportWatchOnlyWalletRoutes;
51 changes: 50 additions & 1 deletion lib/features/receive/ui/screens/receive_qr_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:bb_mobile/features/receive/presentation/bloc/receive_bloc.dart';
import 'package:bb_mobile/core/widgets/tiles/bordered_tappable_tile.dart';
import 'package:bb_mobile/features/labels/ui/label_entry_bottom_sheet.dart';
import 'package:bb_mobile/features/receive/ui/widgets/receive_amount_bottom_sheet.dart';
import 'package:bb_mobile/features/trezor/ui/trezor_router.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_animate/flutter_animate.dart';
Expand All @@ -43,7 +44,11 @@ class ReceiveQrPage extends StatelessWidget {
final isBitBox = context.select(
(ReceiveBloc bloc) => bloc.state.wallet?.signerDevice?.isBitBox ?? false,
);
final showAddressVerification = !isLightning && (isLedger || isBitBox);
final isTrezor = context.select(
(ReceiveBloc bloc) => bloc.state.wallet?.signerDevice?.isTrezor ?? false,
);
final showAddressVerification =
!isLightning && (isLedger || isBitBox || isTrezor);

final gap = Device.screen.height * 0.02;
return SingleChildScrollView(
Expand All @@ -62,6 +67,8 @@ class ReceiveQrPage extends StatelessWidget {
const Column(children: [VerifyAddressOnLedgerButton()]),
if (isBitBox)
const Column(children: [VerifyAddressOnBitBoxButton()]),
if (isTrezor)
const Column(children: [VerifyAddressOnTrezorButton()]),
Gap(gap),
],
if (!isLightning) const ReceiveNewAddressButton(),
Expand Down Expand Up @@ -807,3 +814,45 @@ class VerifyAddressOnBitBoxButton extends StatelessWidget {
);
}
}

class VerifyAddressOnTrezorButton extends StatelessWidget {
const VerifyAddressOnTrezorButton({super.key});

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: BBButton.big(
label: context.loc.receiveVerifyAddressTrezor,
onPressed: () {
final state = context.read<ReceiveBloc>().state;
if (state.wallet == null || state.bitcoinAddress == null) {
SnackBarUtils.showSnackBar(
context,
context.loc.receiveVerifyAddressError,
);
return;
}
final keyChainPath =
state.bitcoinAddress!.keyChain == WalletAddressKeyChain.external
? '0'
: '1';
final derivationPath =
'${state.wallet!.derivationPath}/$keyChainPath/${state.bitcoinAddress!.index}';
context.pushNamed(
TrezorRoute.trezorVerifyAddress.name,
extra: TrezorVerifyAddressRouteParams(
address: state.address,
derivationPath: derivationPath,
scriptType: state.wallet!.scriptType,
isTestnet: state.wallet!.network.isTestnet,
),
);
},
bgColor: context.appColors.primary,
textColor: context.appColors.onPrimary,
outlined: true,
),
);
}
}
50 changes: 50 additions & 0 deletions lib/features/send/ui/screens/send_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:bb_mobile/features/bitcoin_price/ui/currency_text.dart';
import 'package:bb_mobile/features/ledger/ui/ledger_router.dart';
import 'package:bb_mobile/features/ledger/ui/screens/ledger_action_screen.dart';
import 'package:bb_mobile/features/psbt_flow/psbt_router.dart';
import 'package:bb_mobile/features/trezor/ui/trezor_router.dart';
import 'package:bb_mobile/features/send/presentation/bloc/send_cubit.dart';
import 'package:bb_mobile/features/send/presentation/bloc/send_state.dart';
import 'package:bb_mobile/features/send/ui/screens/open_the_camera_widget.dart';
Expand Down Expand Up @@ -914,6 +915,8 @@ class _BottomButtons extends StatelessWidget {
? const SignLedgerButton()
: (wallet.signerDevice != null && wallet.signerDevice!.isBitBox)
? const SignBitBoxButton()
: (wallet.signerDevice != null && wallet.signerDevice!.isTrezor)
? const SignTrezorButton()
: const ShowPsbtButton()
else
const ConfirmSendButton(),
Expand Down Expand Up @@ -2059,6 +2062,52 @@ class SignBitBoxButton extends StatelessWidget {
}
}

class SignTrezorButton extends StatelessWidget {
const SignTrezorButton({super.key});

@override
Widget build(BuildContext context) {
final unsignedPsbt = context.select(
(SendCubit cubit) => cubit.state.unsignedPsbt,
);
final scriptType = context.select(
(SendCubit cubit) => cubit.state.selectedWallet?.scriptType,
);
final isTestnet = context.select(
(SendCubit cubit) =>
cubit.state.selectedWallet?.network.isTestnet ?? false,
);

return BBButton.big(
label: context.loc.sendSignWithTrezor,
onPressed: () async {
if (unsignedPsbt == null || scriptType == null) {
return;
}

final result = await context.pushNamed<String>(
TrezorRoute.trezorSignTransaction.name,
extra: TrezorSignTransactionRouteParams(
psbt: unsignedPsbt,
isTestnet: isTestnet,
scriptType: scriptType,
),
);

if (result != null && context.mounted) {
SnackBarUtils.showSnackBar(
context,
context.loc.sendTransactionSignedTrezor,
);
await context.read<SendCubit>().updateSignedBitcoinTx(result);
}
},
bgColor: context.appColors.secondary,
textColor: context.appColors.onSecondary,
);
}
}

String _swapCreationErrorMessage(
BuildContext context,
SwapCreationException error,
Expand All @@ -2072,6 +2121,7 @@ String _swapCreationErrorMessage(
return error.message;
}

/// Helper function to get localized error message for SwapLimitsException
String _getSwapLimitsErrorMessage(
BuildContext context,
SwapLimitsException error,
Expand Down
11 changes: 11 additions & 0 deletions lib/features/trezor/data/trezor_callback_port_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:bb_mobile/features/trezor/data/trezor_connect_datasource.dart';
import 'package:bb_mobile/features/trezor/domain/trezor_callback_port.dart';

class TrezorCallbackPortImpl implements TrezorCallbackPort {
final TrezorConnectDatasource _datasource;

TrezorCallbackPortImpl({required this._datasource});

@override
void handleCallback(Uri uri) => _datasource.handleCallback(uri);
}
Loading
Loading