Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
@@ -1,33 +1,36 @@
import 'dart:async';
import 'dart:io';
import "dart:async";
import "dart:io";
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/wallet_type_utils.dart';
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';
import "package:bitbox_flutter/bitbox_flutter.dart" as sdk;
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
import "package:hash_wallet/wallet_type_utils.dart";
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:mobx/mobx.dart";
part 'bitbox_view_model.g.dart';
part "bitbox_view_model.g.dart";
class BitboxViewModel = BitboxViewModelBase with _$BitboxViewModel;
abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
late final sdk.BitboxManager bitboxManager;
BitboxViewModelBase() {
if (!Platform.isIOS && !isMoneroOnly) {
bitboxManager = sdk.BitboxManager();
}
}
late final sdk.BitboxManager bitboxManager;
BitboxHardwareWalletDevice? _connectedDevice;
Timer? _isConnectedTimer;
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.bitbox;
@@ -36,7 +39,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
bool isBleEnabled = false;
@override
bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => Platform.isIOS && false; // TODO(Konsti): remove when we enable bluetooth
@override
Future<void> updateBleState() async {}
@@ -46,39 +53,58 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => bitboxManager.devices
.then((devices) => devices.map((d) => BitboxHardwareWalletDevice(d)).toList());
.then((devices) => devices.map(BitboxHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is BitboxHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (device is! BitboxHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
try {
final appDocDir = await getAppDir();
await bitboxManager.connect(device.device);
printV("Got Connection", file: '${appDocDir.path}/error.txt');
printV("Got Connection", file: "${appDocDir.path}/error.txt");
await bitboxManager.initBitBox();
printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt');
printV("Bitbox initialized!", file: "${appDocDir.path}/error.txt");
await bitboxManager.channelHashVerify();
printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt');
_isConnecting = false;
printV("Bitbox channel-hash verified!", file: "${appDocDir.path}/error.txt");
_connectedDevice = device;
_isConnectedTimer = Timer.periodic(const Duration(seconds: 1), (_) => _isStillConnected());
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
_connectedDevice = null;
_isConnectedTimer?.cancel();
_isConnectedTimer = null;
return false;
}
bool _isConnecting = false;
@override
bool get isConnected => false;
bool isConnected(WalletType type) => _connectedDevice != null;
Future<void> _isStillConnected() async {
final devices = await getAllUsbDevices();
if (!devices.any((d) => d.name == _connectedDevice?.name)) {
_connectedDevice = null;
_isConnectedTimer?.cancel();
_isConnectedTimer = null;
}
}
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
@@ -96,17 +122,17 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store {
}
@override
Future<void> initWallet(WalletBase wallet) async {
Future<void> initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for bitbox');
throw Exception("Unexpected wallet type: ${wallet.type} for bitbox");
}
}
}
@@ -1,14 +1,16 @@
import 'package:hash_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';
import "package:hash_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;
bool get isConnecting;
bool isConnected(WalletType type);
Future<void> updateBleState();
@@ -16,6 +18,8 @@ abstract class HardwareWalletViewModel {
Future<List<HardwareWalletDevice>> getAllUsbDevices();
Future<List<HardwareWalletDevice>> getConnectedBleDevices() async => [];
Future<void> stopScanning();
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type);
@@ -25,4 +29,6 @@ abstract class HardwareWalletViewModel {
Future<void> initWallet(WalletBase wallet);
String? interpretErrorCode(String error) => null;
Future<void> close() async {}
}
@@ -1,58 +1,68 @@
import 'dart:async';
import 'dart:io';
import "dart:async";
import "dart:io";
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/main.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/routes.dart';
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:hash_wallet/wallet_type_utils.dart';
import 'package:cw_core/hardware/device_connection_type.dart';
import 'package:cw_core/hardware/hardware_wallet_service.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:flutter/widgets.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk;
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/main.dart";
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/src/screens/connect_device/connect_device_page.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
import "package:hash_wallet/wallet_type_utils.dart";
import "package:cw_core/hardware/device_connection_type.dart";
import "package:cw_core/hardware/hardware_wallet_service.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:flutter/widgets.dart";
import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk;
import "package:mobx/mobx.dart";
import "package:permission_handler/permission_handler.dart";
part 'ledger_view_model.g.dart';
part "ledger_view_model.g.dart";
class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel;
abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
LedgerViewModelBase(AppStore appStore) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) {
_initBLE();
}
});
reaction((_) => appStore.wallet, (wallet) async {
if (wallet != null && wallet.type != _connectingWalletType) {
await close();
}
});
if (!Platform.isIOS) {
ledgerPlusUSB = sdk.LedgerInterface.usb();
}
}
}
late final sdk.LedgerInterface ledgerPlusBLE;
late final sdk.LedgerInterface ledgerPlusUSB;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero, HardwareWalletType.ledger, Platform.isIOS)
.isNotEmpty;
WalletType.monero,
HardwareWalletType.ledger,
Platform.isIOS,
).isNotEmpty;
}
return true;
}
LedgerViewModelBase() {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) _initBLE();
});
updateBleState();
if (!Platform.isIOS) {
ledgerPlusUSB = sdk.LedgerInterface.usb();
}
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger;
@@ -60,6 +70,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
@observable
bool isBleEnabled = false;
@override
@observable
bool isConnecting = false;
@override
bool get hasBluetooth => true;
@@ -69,9 +83,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
if (isBleEnabled && !_bleIsInitialized) {
ledgerPlusBLE = sdk.LedgerInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) return true;
if (Platform.isMacOS) {
return true;
}
Map<Permission, PermissionStatus> statuses = await [
final statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
@@ -79,7 +95,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)),
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
@@ -91,30 +107,51 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) isBleEnabled = newState;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d));
ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() => ledgerPlusUSB.devices
.then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList());
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList());
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) await ledgerPlusBLE.stopScanning();
if (!Platform.isIOS) await ledgerPlusUSB.stopScanning();
Future<List<HardwareWalletDevice>> getConnectedBleDevices() async {
if (!_bleIsInitialized) {
return const [];
}
return (await ledgerPlusBLE.devices).map(LedgerHardwareWalletDevice.new).toList();
}
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) {
await ledgerPlusBLE.stopScanning();
}
if (!Platform.isIOS) {
await ledgerPlusUSB.stopScanning();
}
}
@override
@action
Future<bool> connectDevice(HardwareWalletDevice device, WalletType type) async {
if (!(device is LedgerHardwareWalletDevice)) return false;
if (_isConnecting) return false;
_isConnecting = true;
if (device is! LedgerHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
_connectingWalletType = type;
if (isConnected) {
if (isConnected(type)) {
try {
await _connection!.disconnect().catchError((_) {});
} catch (_) {}
@@ -123,33 +160,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
final ledger =
device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB;
if (_connectionChangeSubscription == null) {
_connectionChangeSubscription = ledger.deviceStateChanges.listen(_connectionChangeListener);
}
_connectionChangeSubscription ??=
ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener);
try {
_connection = await ledger.connect(device.device);
_isConnecting = false;
isConnecting = false;
return true;
} catch (e) {
printV(e);
}
_isConnecting = false;
isConnecting = false;
return false;
}
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
sdk.LedgerConnection? _connection;
bool _isConnecting = false;
WalletType? _connectingWalletType;
bool suppressMoneroReconnectPrompt = false;
void _connectionChangeListener(sdk.BleConnectionState event) {
printV('Ledger Device State Changed: $event');
if (event == sdk.BleConnectionState.disconnected && !_isConnecting) {
printV("Ledger Device State Changed: $event");
if (event == sdk.BleConnectionState.disconnected && !isConnecting) {
_connection = null;
if (_connectingWalletType == WalletType.monero) {
monero!.resetLedgerConnection();
if (suppressMoneroReconnectPrompt) {
return;
}
Navigator.of(navigatorKey.currentContext!).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
@@ -157,8 +197,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
hardwareWalletType: HardwareWalletType.ledger,
allowChangeWallet: true,
isReconnect: true,
onConnectDevice: (context, ledgerVM) async {
if (context.mounted) {
onConnectDevice: (context, ledgerVM) {
if (context.mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
@@ -169,32 +209,32 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
@override
bool get isConnected => _connection != null && !(_connection!.isDisconnected);
bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected;
sdk.LedgerConnection get connection => _connection!;
@override
Future<void> initWallet(WalletBase wallet) async {
Future<void> initWallet(WalletBase wallet) {
switch (wallet.type) {
case WalletType.monero:
return monero!.setLedgerConnection(wallet, connection);
case WalletType.bitcoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for ledger');
throw Exception("Unexpected wallet type: ${wallet.type} for ledger");
}
}
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.monero:
return monero!.getLedgerHardwareWalletService(connection);
case WalletType.bitcoin:
return bitcoin!.getLedgerHardwareWalletService(connection, true);
case WalletType.litecoin:
@@ -213,16 +253,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
return error;
}
var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString());
final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString());
String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? "";
if (errorCode.contains("6985")) {
return S.current.ledger_error_tx_rejected_by_user;
} else if (errorCode.contains("5515")) {
return S.current.ledger_error_device_locked;
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) {
} else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) {
return S.current.ledger_error_wrong_app;
}
return null;
}
@override
Future<void> close() async {
try {
printV("Resetting ledger VM");
await _connectionChangeSubscription?.cancel();
await _connection?.disconnect().catchError((_) {});
_connectingWalletType = null;
_connectionChangeSubscription = null;
_connection = null;
isConnecting = false;
await stopScanning();
} catch (_) {}
}
}
@@ -0,0 +1,521 @@
import "dart:async";
import "dart:convert";
import "dart:io";
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/core/secure_storage.dart";
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
import "package:hash_wallet/evm/evm.dart";
import "package:hash_wallet/main.dart";
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/new-ui/widgets/hardware_wallet/proceed_on_device_sheet.dart";
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
import "package:hash_wallet/wallet_type_utils.dart";
import "package:cw_core/encryption_file_utils.dart";
import "package:cw_core/hardware/device_connection_type.dart";
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:cw_core/key.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:device_info_plus/device_info_plus.dart";
import "package:flutter/material.dart";
import "package:mobx/mobx.dart";
import "package:permission_handler/permission_handler.dart";
import "package:trezor_connect/trezor_connect.dart" as connect_sdk;
import "package:trezor_flutter/trezor_flutter.dart" as sdk;
part "trezor_connect_view_model.g.dart";
const trezorUseNative = [WalletType.bitcoin, WalletType.monero];
class TrezorConnectViewModel = TrezorConnectViewModelBase with _$TrezorConnectViewModel;
abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with Store {
TrezorConnectViewModelBase(this.trezorConnect, this._secureStorage) {
if (_doesSupportHardwareWallets) {
reaction((_) => isBleEnabled, (_) {
if (isBleEnabled) {
_initBLE();
}
});
if (!Platform.isIOS) {
trezorUSB = sdk.TrezorInterface.usb();
}
}
}
StreamSubscription<sdk.BleConnectionState>? _connectionChangeSubscription;
final connect_sdk.TrezorConnect trezorConnect;
final SecureStorage _secureStorage;
late final sdk.TrezorInterface trezorBLE;
late final sdk.TrezorInterface trezorUSB;
sdk.ThpState? _state;
bool get _doesSupportHardwareWallets {
if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(
WalletType.monero,
HardwareWalletType.trezor,
Platform.isIOS,
).isNotEmpty;
}
return true;
}
bool _bleIsInitialized = false;
Future<void> _initBLE() async {
if (isBleEnabled && !_bleIsInitialized) {
trezorBLE = sdk.TrezorInterface.ble(
onPermissionRequest: (_) async {
if (Platform.isMacOS) {
return true;
}
final Map<Permission, PermissionStatus> statuses = await [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
].request();
return statuses.values.where((status) => status.isDenied).isEmpty;
},
bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)),
);
_bleIsInitialized = true;
}
}
@override
HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor;
@override
@observable
bool isConnecting = false;
@override
@observable
bool isBleEnabled = false;
@override
bool get hasBluetooth => true;
@override
Future<void> updateBleState() async {
final bleState = await sdk.UniversalBle.getBluetoothAvailabilityState();
final newState = bleState == sdk.AvailabilityState.poweredOn;
if (newState != isBleEnabled) {
isBleEnabled = newState;
}
}
@override
Stream<HardwareWalletDevice> scanForBleDevices() =>
trezorBLE.scan().map(TrezorHardwareWalletDevice.new);
@override
Future<List<HardwareWalletDevice>> getAllUsbDevices() =>
trezorUSB.devices.then((devices) => devices.map(TrezorHardwareWalletDevice.new).toList());
@override
Future<List<HardwareWalletDevice>> getConnectedBleDevices() async {
if (!_bleIsInitialized) {
return const [];
}
return (await trezorBLE.devices).map(TrezorHardwareWalletDevice.new).toList();
}
@override
Future<void> stopScanning() async {
if (_bleIsInitialized) {
await trezorBLE.stopScanning();
}
if (!Platform.isIOS) {
await trezorUSB.stopScanning();
}
}
sdk.TrezorClient? _client;
Completer<String?>? _pinCompleter;
Completer<TrezorDeviceSettings>? _settingsCompleter;
/// Settings that successfully opened a session, kept in memory (never persisted) per wallet id
/// so a reconnect during a send doesn't ask for the wallet options / passphrase again.
final Map<String, TrezorDeviceSettings> _sessionSettings = {};
TrezorDeviceSettings? _lastUsedSettings;
String? _reconnectWalletId;
/// The wallet the live session (and with it the passphrase) belongs to, null if unknown
String? _sessionWalletId;
/// Call right before opening the connect page to reconnect the device for [wallet].
/// Only then the settings of the previous session are reused; creating or restoring a wallet
/// always asks for them.
void prepareReconnect(WalletBase wallet) => _reconnectWalletId = wallet.id;
/// Call when the connect page opened after [prepareReconnect] is closed, so an attempt that was
/// backed out of can never leak its settings into a later, unrelated connect (e.g. a restore).
void cancelReconnect() => _reconnectWalletId = null;
/// Call once a wallet was created / restored with the live session
void bindSessionToWallet(WalletBase wallet) {
_sessionWalletId = wallet.id;
final settings = _lastUsedSettings;
if (settings != null) {
_sessionSettings[wallet.id] = settings;
_lastUsedSettings = null;
}
}
/// False if the live session is known to belong to another wallet, e.g. one with a different
/// passphrase. Using it would only fail when signing.
bool isSessionFor(WalletBase wallet) =>
_sessionWalletId == null || _sessionWalletId == wallet.id;
Future<void> dropSession() async {
final client = _client;
_client = null;
_sessionWalletId = null;
_lastUsedSettings = null;
try {
await client?.connection.disconnect();
} catch (_) {}
}
@observable
TrezorParingState paringState = TrezorParingState.initial;
void setParingPin(String pin) => _pinCompleter?.complete(pin);
void setDeviceSettings(TrezorDeviceSettings settings) => _settingsCompleter?.complete(settings);
@override
@action
Future<bool> connectDevice(
HardwareWalletDevice device,
WalletType type, [
bool isRetry = false,
]) async {
if (device is! TrezorHardwareWalletDevice) {
return false;
}
if (isConnecting) {
return false;
}
isConnecting = true;
paringState = TrezorParingState.initial;
final reconnectWalletId = _reconnectWalletId;
_reconnectWalletId = null;
final reusedSettings = reconnectWalletId != null ? _sessionSettings[reconnectWalletId] : null;
try {
final trezorInterface =
device.connectionType == HardwareWalletConnectionType.ble ? trezorBLE : trezorUSB;
final connection = await trezorInterface.connect(device.device);
_connectionChangeSubscription ??=
trezorInterface.deviceStateChanges.listen(_connectionChangeListener);
if (!isRetry) {
unawaited(
showModalBottomSheet(
context: navigatorKey.currentContext!,
isScrollControlled: true,
isDismissible: false,
enableDrag: false,
useSafeArea: true,
builder: (_) => HardwareWalletProceedOnDeviceSheet(
hardwareWalletType: hardwareWalletType,
trezorConnectVM: this,
onRetry: () => connectDevice(device, type, true),
),
),
);
}
Future<String> onPinCode() async {
_pinCompleter = Completer<String?>();
paringState = TrezorParingState.enterPin;
final res = await _pinCompleter!.future;
paringState = TrezorParingState.verifyingPin;
if (res == null) {
throw Exception();
}
return res;
}
final deviceInfo = await _deviceName;
_state ??= await _getState();
_client = sdk.TrezorClient.getClientForConnection(
connection,
_state!,
"Cake Wallet",
deviceInfo,
onPinCode,
);
await _client!.createChannel();
final hasAutoPairingCredentials =
_state!.pairingCredentials.any((c) => c.autoconnect == true);
final isAutoPairingAvailable = _client is sdk.TrezorClientV2 && !hasAutoPairingCredentials;
final TrezorDeviceSettings settings;
if (reusedSettings != null) {
settings = reusedSettings;
} else {
paringState =
TrezorParingState.awaitingSettings(isAutoPairingAvailable: isAutoPairingAvailable);
_settingsCompleter = Completer<TrezorDeviceSettings>();
settings = await _settingsCompleter!.future;
}
paringState = TrezorParingState.awaitingPassphrase;
if (settings.enableAutoParing && isAutoPairingAvailable) {
if (_client case final sdk.TrezorClientV2 clientV2) {
try {
final auto = await clientV2.getAutoPairingCredentials();
_state!.setPairingCredentials([auto]);
await _saveState();
} catch (_) {}
}
}
if ((settings.passphrase ?? "").isNotEmpty || settings.passphraseOnDevice) {
final passphrase = settings.passphraseOnDevice
? const sdk.TrezorPassphrase.onDevice()
: sdk.TrezorPassphrase.value(settings.passphrase ?? "");
await _client!.createSession(passphrase);
}
_lastUsedSettings = settings;
_sessionWalletId = reconnectWalletId;
paringState = TrezorParingState.success;
return true;
} catch (e) {
await _client?.connection.disconnect();
_client = null;
_sessionWalletId = null;
_lastUsedSettings = null;
// Drop the (possibly half way through a handshake) state, the next attempt reloads the
// saved pairing credentials instead of asking for the pairing code again.
_state = null;
if (reconnectWalletId != null) {
_sessionSettings.remove(reconnectWalletId);
}
// rethrow;
paringState = TrezorParingState.fail(e.toString());
printV(e);
return false;
} finally {
isConnecting = false;
_reconnectWalletId = null;
}
}
Future<String> get _deviceName async {
final deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
return (await deviceInfo.androidInfo).model;
}
if (Platform.isIOS) {
return (await deviceInfo.iosInfo).name;
}
return "Computer";
}
@override
bool isConnected(WalletType type) => trezorUseNative.contains(type)
? _client != null && _client?.connection.isDisconnected == false
: true;
@override
HardwareWalletService getHardwareWalletService(WalletType type) {
switch (type) {
case WalletType.monero:
return monero!.getTrezorHardwareWalletService(_client!);
case WalletType.bitcoin:
return bitcoin!.getTrezorHardwareWalletService(null, _client!, true);
case WalletType.litecoin:
return bitcoin!.getTrezorHardwareWalletService(trezorConnect, null, false);
case WalletType.ethereum:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
Future<void> initWallet(WalletBase wallet) async {
// Only a session that was opened for this wallet may hand its settings to it
final lastUsedSettings = _lastUsedSettings;
if (lastUsedSettings != null && _sessionWalletId == wallet.id) {
_sessionSettings[wallet.id] = lastUsedSettings;
_lastUsedSettings = null;
}
switch (wallet.type) {
case WalletType.monero:
return monero!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type));
default:
throw Exception("Unexpected wallet type: ${wallet.type} for trezor");
}
}
final EncryptionFileUtils _encryptionFileUtils = encryptionFileUtilsFor(true);
static const String _secureStorageKey = "com.cakewallet.trezor/thp_state";
Future<String> get _thpJsonFile async => "${(await getAppDir()).path}/thp_state.json.enc";
Future<sdk.ThpState> _getState() async {
final password = await _secureStorage.read(key: _secureStorageKey);
if (password == null) {
return _saveState();
}
try {
final state = await _encryptionFileUtils.read(path: await _thpJsonFile, password: password);
return sdk.ThpState.fromJson(state);
} catch (_) {
return _saveState();
}
}
Future<sdk.ThpState> _saveState() async {
try {
final password = generateKey();
await _secureStorage.write(key: _secureStorageKey, value: password);
final file = File(await _thpJsonFile);
if (!file.existsSync()) {
file.createSync();
}
final state = _state ?? sdk.ThpState();
await _encryptionFileUtils.write(
path: file.path,
password: password,
data: jsonEncode(state.toMap()),
);
_state = state;
return state;
} catch (_) {
throw Exception("Unable to save Trezor State");
}
}
Future<void> _connectionChangeListener(sdk.BleConnectionState event) async {
printV("Trezor Device State Changed: $event");
if (event != sdk.BleConnectionState.disconnected || isConnecting) {
return;
}
final client = _client;
if (client == null) {
return;
}
// The events aren't device specific, so check if it's actually our Trezor that is gone
if (client.connection.connectionType == sdk.ConnectionType.ble) {
try {
final state = await sdk.UniversalBle.getConnectionState(client.connection.device.id);
if (state == sdk.BleConnectionState.connected) {
return;
}
} catch (_) {}
}
if (identical(_client, client)) {
_client = null;
_sessionWalletId = null;
}
}
Future<bool> syncKeyImages(WalletBase wallet) async {
if (wallet.type == WalletType.monero) {
try {
await monero!.syncTrezor(wallet);
} catch (_) {
return false;
}
}
return true;
}
@override
Future<void> close() async {
try {
await _connectionChangeSubscription?.cancel();
_connectionChangeSubscription = null;
isConnecting = false;
await stopScanning();
} catch (_) {}
}
}
abstract class TrezorParingState {
static TrezorParingState initial = InitialTrezorParingState();
static TrezorParingState enterPin = EnterPinTrezorParingState();
static TrezorParingState success = SuccessTrezorParingState();
static TrezorParingState verifyingPin = VerifyingPinTrezorParingState();
static TrezorParingState awaitingPassphrase = AwaitingPassphraseTrezorParingState();
static TrezorParingState awaitingSettings({required bool isAutoPairingAvailable}) =>
AwaitingSettingsTrezorParingState(isAutoPairingAvailable: isAutoPairingAvailable);
static TrezorParingState fail(String message) => FailTrezorParingState(message);
}
class InitialTrezorParingState extends TrezorParingState {}
class EnterPinTrezorParingState extends TrezorParingState {}
class VerifyingPinTrezorParingState extends TrezorParingState {}
class AwaitingSettingsTrezorParingState extends TrezorParingState {
AwaitingSettingsTrezorParingState({required this.isAutoPairingAvailable});
final bool isAutoPairingAvailable;
}
class AwaitingPassphraseTrezorParingState extends TrezorParingState {}
class SuccessTrezorParingState extends TrezorParingState {}
class FailTrezorParingState extends TrezorParingState {
FailTrezorParingState(this.message);
final String message;
}
class TrezorDeviceSettings {
const TrezorDeviceSettings({
required this.enableAutoParing,
required this.passphraseOnDevice,
this.passphrase,
});
final bool enableAutoParing;
final bool passphraseOnDevice;
final String? passphrase;
}
@@ -1,80 +0,0 @@
import 'dart:async';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
import 'package:hash_wallet/evm/evm.dart';
import 'package:hash_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:
case WalletType.polygon:
return evm!.getTrezorHardwareWalletService(trezorConnect);
default:
throw UnimplementedError();
}
}
@override
Future<void> initWallet(WalletBase wallet) async {
switch (wallet.type) {
case WalletType.bitcoin:
case WalletType.litecoin:
return bitcoin!
.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
case WalletType.ethereum:
case WalletType.polygon:
return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type));
default:
throw Exception('Unexpected wallet type: ${wallet.type} for trezor');
}
}
}