LCW-1128-Add-support-for-Trezor (#2565)

* feat: add support for Trezor hardware wallet integration Restore Wallets

* feat: add Trezor hardware wallet support, implement RLP decoding utility, and update transaction signing logic

* feat: add bitcoin trezor signing

* chore: update Trezor Connect dependency to specific commit hash in pubspec_base.yaml

* feat: add trezor support for litecoin

* feat: add colorFilter for device manufacturer icons to align with theme styles

* refactor: optimize Trezor callback handling

* feat: add BitBox and Trezor support for Bitcoin and Litecoin, update transaction signing logic, and improve modularity in hardware wallet services

* refactor: simplify constructor assertions in `BaseBottomSheetWidget` and cleanup Trezor chainId handling logic

* feat: conditionally render device manufacturers based on platform and hide SeedSigner for now
This commit is contained in:
Konstantin Ullrich
2025-10-11 02:12:27 +03:00
committed by GitHub
parent 75ca159d2c
commit 8b76391a2e
38 changed files with 996 additions and 184 deletions
+7 -5
View File
@@ -536,11 +536,6 @@ class CWBitcoin extends Bitcoin {
return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 10).round();
}
@override
void setLedgerConnection(WalletBase wallet, ledger.LedgerConnection connection) {
(wallet as ElectrumWallet).setLedgerConnection(connection);
}
@override
void setHardwareWalletService(WalletBase wallet, HardwareWalletService service) {
(wallet as ElectrumWallet).hardwareWalletService = service;
@@ -560,6 +555,13 @@ class CWBitcoin extends Bitcoin {
return LitecoinBitboxService(manager);
}
@override
HardwareWalletService getTrezorHardwareWalletService(
trezor.TrezorConnect connect, bool isBitcoin) {
if (isBitcoin) return BitcoinTrezorService(connect);
return LitecoinTrezorService(connect);
}
@override
List<ElectrumSubAddress> getSilentPaymentAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
+7
View File
@@ -56,6 +56,7 @@ import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
import 'package:cake_wallet/view_model/hardware_wallet/bitbox_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_view_model.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
@@ -281,6 +282,7 @@ import 'package:get_it/get_it.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:trezor_connect/trezor_connect.dart';
import 'buy/kryptonim/kryptonim.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'dogecoin/dogecoin.dart';
@@ -400,6 +402,7 @@ Future<void> setup({
switch(type) {
case HardwareWalletType.bitbox: return getIt<BitboxViewModel>();
case HardwareWalletType.ledger: return getIt<LedgerViewModel>();
case HardwareWalletType.trezor: return getIt<TrezorViewModel>();
case HardwareWalletType.cupcake:
case HardwareWalletType.coldcard:
case HardwareWalletType.seedsigner:
@@ -412,6 +415,10 @@ Future<void> setup({
getIt.registerLazySingleton(() => BitboxViewModel());
getIt.registerLazySingleton(() => TrezorConnect("cakewallet://trezor_connect",
appName: "Cake Wallet"));
getIt.registerLazySingleton(() => TrezorViewModel(getIt<TrezorConnect>()));
final secretStore = await SecretStoreBase.load(getIt.get<SecureStorage>());
getIt.registerSingleton<SecretStore>(secretStore);
+7
View File
@@ -188,6 +188,9 @@ class CWEthereum extends Ethereum {
} else if (service is EVMChainBitboxService) {
((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials)
.setBitbox(service.manager, wallet.walletInfo.derivationInfo?.derivationPath);
} else if (service is EVMChainTrezorService) {
((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials)
.setTrezorConnect(service.connect, wallet.walletInfo.derivationInfo?.derivationPath);
}
}
@override
@@ -198,6 +201,10 @@ class CWEthereum extends Ethereum {
HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) =>
EVMChainBitboxService(manager);
@override
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect) =>
EVMChainTrezorService(connect);
@override
List<String> getDefaultTokenContractAddresses() {
return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList();
+2
View File
@@ -62,6 +62,7 @@ import 'package:cw_core/window_size.dart';
import 'package:logging/logging.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/core/reset_service.dart';
import 'package:trezor_connect/trezor_connect.dart';
final navigatorKey = GlobalKey<NavigatorState>();
final rootKey = GlobalKey<RootState>();
@@ -375,6 +376,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
linkViewModel: linkViewModel,
tradeMonitor: tradeMonitor,
nodeSwitchingService: nodeSwitchingService,
trezorConnect: getIt<TrezorConnect>(),
child: ThemeProvider(
themeStore: appStore.themeStore,
materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp(
+7
View File
@@ -206,6 +206,9 @@ class CWPolygon extends Polygon {
} else if (service is EVMChainBitboxService) {
((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials)
.setBitbox(service.manager, wallet.walletInfo.derivationInfo?.derivationPath);
} else if (service is EVMChainTrezorService) {
((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials)
.setTrezorConnect(service.connect, wallet.walletInfo.derivationInfo?.derivationPath);
}
}
@@ -217,6 +220,10 @@ class CWPolygon extends Polygon {
HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) =>
EVMChainBitboxService(manager, chainId: 137);
@override
HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect) =>
EVMChainTrezorService(connect, chainId: 137);
@override
List<String> getDefaultTokenContractAddresses() =>
DefaultPolygonErc20Tokens().initialPolygonErc20Tokens.map((e) => e.contractAddress).toList();
+6
View File
@@ -345,6 +345,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
(_) => getIt.get<NewWalletTypePage>(
param1: NewWalletTypeArguments(
onTypeSelected: (BuildContext context, WalletType type) {
if (hardwareWalletType == HardwareWalletType.trezor) {
Navigator.of(context).pushNamed(Routes.chooseHardwareWalletAccount,
arguments: [type, hardwareWalletType]);
return;
}
final arguments = ConnectDevicePageParams(
walletType: type,
hardwareWalletType: hardwareWalletType,
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
@@ -29,28 +31,62 @@ class SelectDeviceManufacturerPage extends BasePage {
@override
AppBarStyle get appBarStyle => AppBarStyle.regular;
ColorFilter get _colorFilter =>
ColorFilter.mode(currentTheme.colorScheme.onSurface, BlendMode.srcIn);
List<_DeviceManufacturer> get availableManufacturers => [
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/ledger_man.svg', height: 25),
image: SvgPicture.asset(
'assets/images/hardware_wallet/ledger_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.ledger,
),
// _DeviceManufacturer(
// image: SvgPicture.asset('assets/images/hardware_wallet/bitbox_man.svg', height: 25),
// hardwareWalletType: HardwareWalletType.bitbox,
// tag: S.current.new_tag,
// ),
if (Platform.isAndroid) ...[
_DeviceManufacturer(
image: SvgPicture.asset(
'assets/images/hardware_wallet/trezor_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.trezor,
tag: S.current.new_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset(
'assets/images/hardware_wallet/bitbox_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.bitbox,
tag: S.current.new_tag,
),
],
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/cupcake_man.svg', height: 25),
image: SvgPicture.asset(
'assets/images/hardware_wallet/cupcake_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.cupcake,
tag: S.current.new_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/coldcard_man.svg', height: 25),
image: SvgPicture.asset(
'assets/images/hardware_wallet/coldcard_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.coldcard,
tag: S.current.new_tag,
),
// _DeviceManufacturer(
// image: SvgPicture.asset('assets/images/hardware_wallet/seedsigner_man.svg', height: 25),
// image: SvgPicture.asset(
// 'assets/images/hardware_wallet/seedsigner_man.svg',
// height: 25,
// colorFilter: _colorFilter,
// ),
// hardwareWalletType: HardwareWalletType.seedsigner,
// tag: S.current.new_tag,
// ),
@@ -61,26 +97,40 @@ class SelectDeviceManufacturerPage extends BasePage {
.toList();
List<_DeviceManufacturer> get comingManufacturers => [
if (!Platform.isAndroid) ...[
_DeviceManufacturer(
image: SvgPicture.asset(
'assets/images/hardware_wallet/bitbox_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.bitbox,
tag: S.current.coming_soon_tag,
),
],
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/trezor_man.svg', height: 25),
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/bitbox_man.svg', height: 25),
hardwareWalletType: HardwareWalletType.bitbox,
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/seedsigner_man.svg', height: 25),
image: SvgPicture.asset(
'assets/images/hardware_wallet/seedsigner_man.svg',
height: 25,
colorFilter: _colorFilter,
),
hardwareWalletType: HardwareWalletType.seedsigner,
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/foundation_man.svg', height: 25),
image: SvgPicture.asset(
'assets/images/hardware_wallet/foundation_man.svg',
height: 25,
colorFilter: _colorFilter,
),
tag: S.current.coming_soon_tag,
),
_DeviceManufacturer(
image: SvgPicture.asset('assets/images/hardware_wallet/keystone_man.svg', height: 25),
image: SvgPicture.asset(
'assets/images/hardware_wallet/keystone_man.svg',
height: 25,
colorFilter: _colorFilter,
),
tag: S.current.coming_soon_tag,
),
];
@@ -90,48 +140,52 @@ class SelectDeviceManufacturerPage extends BasePage {
child: Center(
child: Container(
padding: EdgeInsets.only(left: 24, right: 24),
child: Column(
children: [
...availableManufacturers.map(
(manufacturer) => Padding(
padding: EdgeInsets.only(top: 10),
child: ManufacturerOptionTile(
image: manufacturer.image,
tag: manufacturer.tag,
onPressed: () {
if (onSelect != null)
return onSelect!.call(context, manufacturer.hardwareWalletType!);
if (isAirgappedWallet(manufacturer.hardwareWalletType)) {
_onScanQRCode(context, manufacturer.hardwareWalletType!);
} else if (manufacturer.hardwareWalletType != null) {
Navigator.pushNamed(context, Routes.connectHardwareWallet,
arguments: [manufacturer.hardwareWalletType]);
}
},
),
),
),
if (showUnavailable) ...[
Padding(
padding: EdgeInsets.only(top: 20, bottom: 10),
child: DottedDivider(color: Theme.of(context).colorScheme.surfaceContainer),
),
...comingManufacturers.map(
height: double.infinity,
child: SingleChildScrollView(
child: Column(
children: [
...availableManufacturers.map(
(manufacturer) => Padding(
padding: EdgeInsets.only(top: 10),
child: ManufacturerOptionTile(
image: manufacturer.image,
tag: manufacturer.tag,
onPressed: () =>
Fluttertoast.showToast(msg: 'One more tap and it might work'),
// Ester egg
isUnavailable: true,
onPressed: () {
if (onSelect != null)
return onSelect!.call(context, manufacturer.hardwareWalletType!);
if (isAirgappedWallet(manufacturer.hardwareWalletType)) {
_onScanQRCode(context, manufacturer.hardwareWalletType!);
} else if (manufacturer.hardwareWalletType != null) {
Navigator.pushNamed(context, Routes.connectHardwareWallet,
arguments: [manufacturer.hardwareWalletType]);
}
},
),
),
),
]
],
if (showUnavailable) ...[
Padding(
padding: EdgeInsets.only(top: 20, bottom: 10),
child: DottedDivider(color: Theme.of(context).colorScheme.surfaceContainer),
),
...comingManufacturers.map(
(manufacturer) => Padding(
padding: EdgeInsets.only(top: 10),
child: ManufacturerOptionTile(
image: manufacturer.image,
tag: manufacturer.tag,
onPressed: () =>
Fluttertoast.showToast(msg: 'One more tap and it might work'),
// Ester egg
isUnavailable: true,
),
),
),
]
],
),
),
),
),
@@ -33,10 +33,12 @@ class CryptoBalanceWidget extends StatelessWidget {
switch (dashboardViewModel.wallet.hardwareWalletType) {
case null:
return null;
case HardwareWalletType.ledger:
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
case HardwareWalletType.bitbox:
return "assets/images/hardware_wallet/device_bitbox.svg";
case HardwareWalletType.ledger:
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
case HardwareWalletType.trezor:
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
case HardwareWalletType.cupcake:
return "assets/images/cupcake.svg";
case HardwareWalletType.coldcard:
+17 -8
View File
@@ -1,26 +1,28 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/trade_monitor.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/socket_health_logger.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:mobx/mobx.dart';
import 'package:trezor_connect/trezor_connect.dart';
import 'package:uni_links/uni_links.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cw_core/utils/socket_health_logger.dart';
class Root extends StatefulWidget {
Root({
@@ -33,6 +35,7 @@ class Root extends StatefulWidget {
required this.linkViewModel,
required this.tradeMonitor,
required this.nodeSwitchingService,
required this.trezorConnect,
}) : super(key: key);
final AuthenticationStore authenticationStore;
@@ -43,6 +46,7 @@ class Root extends StatefulWidget {
final LinkViewModel linkViewModel;
final TradeMonitor tradeMonitor;
final NodeSwitchingService nodeSwitchingService;
final TrezorConnect trezorConnect;
@override
RootState createState() => RootState();
@@ -110,6 +114,11 @@ class RootState extends State<Root> with WidgetsBindingObserver {
widget.linkViewModel.currentLink = uri;
if (uri.toString().startsWith(widget.trezorConnect.callbackBackUri)) {
widget.trezorConnect.handleCallback(uri);
return;
}
bool requireAuth = await widget.authService.requireAuth();
if (!requireAuth && widget.authenticationStore.state == AuthenticationState.allowed) {
@@ -23,8 +23,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
this.leftActionButtonKey,
this.rightActionButtonKey,
required this.maxHeight,
}) : assert(footerType == FooterType.none,
'currentTheme is required unless footerType is none');
});
final String titleText;
final String? titleIconPath;
@@ -11,6 +11,7 @@ import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:bitbox_flutter/bitbox_flutter.dart' as sdk;
import 'package:mobx/mobx.dart';
@@ -28,6 +29,9 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.bitbox;
@override
@observable
bool isBleEnabled = false;
@@ -1,9 +1,11 @@
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
abstract class HardwareWalletViewModel {
HardwareWalletType get hardwareWalletType;
bool get isConnected;
bool get isBleEnabled;
bool get hasBluetooth;
@@ -54,6 +54,9 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
@override
@observable
bool isBleEnabled = false;
@@ -0,0 +1,82 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:trezor_connect/trezor_connect.dart' as sdk;
import 'package:mobx/mobx.dart';
part 'trezor_view_model.g.dart';
class TrezorViewModel = TrezorViewModelBase with _$TrezorViewModel;
abstract class TrezorViewModelBase extends HardwareWalletViewModel with Store {
final sdk.TrezorConnect trezorConnect;
TrezorViewModelBase(this.trezorConnect);
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isBleEnabled = false;
@override
bool get hasBluetooth => false;
@override
Future<void> updateBleState() async {}
@override
Stream<HardwareWalletDevice> scanForBleDevices() => throw UnimplementedError();
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() async => [];
@override
Future<void> stopScanning() async {}
@override
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async => true;
@override
bool get isConnected => true;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, false);
case WalletType.ethereum:
return ethereum!.getTrezorHardwareWalletService(trezorConnect);
case WalletType.polygon:
return polygon!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
void initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
return ethereum!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.polygon:
return polygon!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type}');
}
}
}
+1 -2
View File
@@ -37,7 +37,6 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
@@ -942,7 +941,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
);
if (token != null) {
selectedCryptoCurrency = token as CryptoCurrency;
selectedCryptoCurrency = token;
}
}
}
@@ -102,10 +102,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
throw Exception('Unexpected type: ${type.toString()}');
}
if (hardwareWalletVM is LedgerViewModel)
credentials.hardwareWalletType = HardwareWalletType.ledger;
else
credentials.hardwareWalletType = HardwareWalletType.bitbox;
credentials.hardwareWalletType = hardwareWalletVM.hardwareWalletType;
return credentials;
}