From e59431aa06cd0cf06108b41c3f06c6f83d2882a8 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Fri, 17 Oct 2025 11:33:05 +0200 Subject: [PATCH 1/8] feat: bip39 for decred --- cw_core/lib/wallet_info.dart | 2 ++ .../lib/wallet_creation_credentials.dart | 5 ++- cw_decred/lib/wallet_service.dart | 6 ++++ cw_decred/pubspec.lock | 32 +++++++++++++++++++ cw_decred/pubspec.yaml | 2 ++ lib/core/wallet_creation_service.dart | 2 +- lib/decred/cw_decred.dart | 4 +-- lib/entities/preferences_key.dart | 1 + lib/entities/seed_type.dart | 25 +++++++++++++++ .../advanced_privacy_settings_page.dart | 17 ++++++++++ .../screens/new_wallet/new_wallet_page.dart | 11 +++---- lib/store/settings_store.dart | 22 +++++++++++++ .../advanced_privacy_settings_view_model.dart | 6 +++- lib/view_model/seed_settings_view_model.dart | 7 ++++ lib/view_model/wallet_creation_vm.dart | 14 +++++++- .../wallet_hardware_restore_view_model.dart | 4 +-- lib/view_model/wallet_new_vm.dart | 17 ++++++---- lib/view_model/wallet_restore_view_model.dart | 24 +++++++------- tool/configure.dart | 2 +- 19 files changed, 169 insertions(+), 34 deletions(-) diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 2fd18016b1..533bde0efa 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -35,6 +35,8 @@ enum DerivationType { nano, bip39, electrum, + @HiveField(5) + decred, } enum HardwareWalletType { diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index ca04514479..b1fcd6518c 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -3,8 +3,11 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; class DecredNewWalletCredentials extends WalletCredentials { - DecredNewWalletCredentials({required String name, WalletInfo? walletInfo}) + DecredNewWalletCredentials({required String name, WalletInfo? walletInfo, required this.isBip39, required this.mnemonic}) : super(name: name, walletInfo: walletInfo); + + final bool isBip39; + final String? mnemonic; } class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index afcf6d5db5..9e04fea18a 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -12,6 +12,7 @@ import 'package:path/path.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:bip39/bip39.dart' as bip39; class DecredWalletService extends WalletService< DecredNewWalletCredentials, @@ -57,6 +58,11 @@ class DecredWalletService extends WalletService< @override Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { await this.init(); + if (credentials.isBip39) { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + return restoreFromSeed(DecredRestoreWalletFromSeedCredentials(name: credentials.name, password: credentials.password!, mnemonic: mnemonic, walletInfo: credentials.walletInfo)); + } final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final network = isTestnet == true ? testnet : mainnet; final config = { diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index ab2826fa22..7a40242ee3 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -46,6 +46,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bip32: + dependency: "direct main" + description: + name: bip32 + sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + bip39: + dependency: "direct main" + description: + name: bip39 + sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc + url: "https://pub.dev" + source: hosted + version: "1.0.6" blockchain_utils: dependency: transitive description: @@ -63,6 +79,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + bs58check: + dependency: transitive + description: + name: bs58check + sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9 + url: "https://pub.dev" + source: hosted + version: "1.0.2" build: dependency: transitive description: @@ -329,6 +353,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hex: + dependency: transitive + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" hive: dependency: transitive description: diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index 989831a891..723d8d3654 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -13,6 +13,8 @@ environment: dependencies: flutter: sdk: flutter + bip39: ^1.0.6 + bip32: ^2.0.0 cw_core: path: ../cw_core diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 7f09e7bfb0..52c8b6952a 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -84,6 +84,7 @@ class WalletCreationService { case WalletType.tron: case WalletType.dogecoin: case WalletType.nano: + case WalletType.decred: return true; case WalletType.monero: case WalletType.wownero: @@ -91,7 +92,6 @@ class WalletCreationService { case WalletType.haven: case WalletType.banano: case WalletType.zano: - case WalletType.decred: return false; } } diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index 7fe2ee5c20..fd69bdb3a2 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -5,8 +5,8 @@ class CWDecred extends Decred { @override WalletCredentials createDecredNewWalletCredentials( - {required String name, WalletInfo? walletInfo}) => - DecredNewWalletCredentials(name: name, walletInfo: walletInfo); + {required String name, WalletInfo? walletInfo, required bool isBip39, required String? mnemonic}) => + DecredNewWalletCredentials(name: name, walletInfo: walletInfo, isBip39: isBip39, mnemonic: mnemonic); @override WalletCredentials createDecredRestoreWalletFromSeedCredentials( diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index a9acc2a6bf..968d3e35b7 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -108,6 +108,7 @@ class PreferencesKey { static const moneroSeedType = 'monero_seed_type'; static const bitcoinSeedType = 'bitcoin_seed_type'; static const nanoSeedType = 'nano_seed_type'; + static const decredSeedType = 'decred_seed_type'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const donationLinkWalletName = 'donation_link_wallet_name'; diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index 22f0be0452..f859bc5914 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -84,3 +84,28 @@ class NanoSeedType extends EnumerableItem with Serializable { } } } + +class DecredSeedType extends EnumerableItem with Serializable { + const DecredSeedType(this.type, {required String title, required int raw}) + : super(title: title, raw: raw); + + final DerivationType type; + + static const all = [DecredSeedType.nanoStandard, DecredSeedType.bip39]; + + static const defaultDerivationType = bip39; + + static const nanoStandard = DecredSeedType(DerivationType.nano, raw: 0, title: 'Decred'); + static const bip39 = DecredSeedType(DerivationType.bip39, raw: 1, title: 'BIP39'); + + static DecredSeedType deserialize({required int raw}) { + switch (raw) { + case 0: + return nanoStandard; + case 1: + return bip39; + default: + throw Exception('Unexpected token: $raw for SeedType deserialize'); + } + } +} diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index 44840dbed7..8ee3a979ef 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -103,6 +103,10 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo if (widget.privacySettingsViewModel.type == WalletType.nano) { widget.seedTypeViewModel.setNanoSeedType(NanoSeedType.bip39); } + + if (widget.privacySettingsViewModel.type == WalletType.decred) { + widget.seedTypeViewModel.setDecredSeedType(DecredSeedType.bip39); + } } super.initState(); } @@ -189,6 +193,19 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ), ); }), + if (widget.privacySettingsViewModel.isDecredSeedTypeOptionsEnabled) + Observer(builder: (_) { + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.seedtype, + items: DecredSeedType.all, + selectedItem: widget.seedTypeViewModel.decredSeedType, + onItemSelected: (type) { + widget.seedTypeViewModel.setDecredSeedType(type); + }, + ), + ); + }), if (!widget.isFromRestore) Observer(builder: (_) { if (widget.privacySettingsViewModel.hasSeedPhraseLengthOption) diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 2b489766c9..dacaf12740 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -367,12 +367,11 @@ class _WalletNameFormState extends State { }); } else { await _walletNewVM.create( - options: _walletNewVM.hasLanguageSelector - ? [ - _languageSelectorKey.currentState?.selected ?? defaultSeedLanguage, - widget._seedSettingsViewModel.moneroSeedType - ] - : null); + options: { + 'language': _languageSelectorKey.currentState?.selected ?? defaultSeedLanguage, + 'moneroSeedType': widget._seedSettingsViewModel.moneroSeedType, + 'decredSeedType': widget._seedSettingsViewModel.decredSeedType, + }); } } catch (e) { _formProcessing = false; diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 42809cc3e9..6519289f6a 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -65,6 +65,7 @@ abstract class SettingsStoreBase with Store { required MoneroSeedType initialMoneroSeedType, required BitcoinSeedType initialBitcoinSeedType, required NanoSeedType initialNanoSeedType, + required DecredSeedType initialDecredSeedType, required bool initialAppSecure, required bool initialDisableTrade, required bool initialDisableAutomaticExchangeStatusUpdates, @@ -159,6 +160,7 @@ abstract class SettingsStoreBase with Store { moneroSeedType = initialMoneroSeedType, bitcoinSeedType = initialBitcoinSeedType, nanoSeedType = initialNanoSeedType, + decredSeedType = initialDecredSeedType, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, enableDuressPin = initialEnableDuressPin, @@ -384,6 +386,11 @@ abstract class SettingsStoreBase with Store { (NanoSeedType nanoSeedType) => sharedPreferences.setInt(PreferencesKey.nanoSeedType, nanoSeedType.raw)); + reaction( + (_) => decredSeedType, + (DecredSeedType decredSeedType) => + sharedPreferences.setInt(PreferencesKey.decredSeedType, decredSeedType.raw)); + reaction( (_) => fiatApiMode, (FiatApiMode mode) => @@ -689,6 +696,7 @@ abstract class SettingsStoreBase with Store { static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType; static const defaultNanoSeedType = NanoSeedType.defaultDerivationType; + static const defaultDecredSeedType = DecredSeedType.defaultDerivationType; @observable FiatCurrency fiatCurrency; @@ -738,6 +746,9 @@ abstract class SettingsStoreBase with Store { @observable NanoSeedType nanoSeedType; + @observable + DecredSeedType decredSeedType; + @observable bool isAppSecure; @@ -1209,6 +1220,11 @@ abstract class SettingsStoreBase with Store { final nanoSeedType = _nanoSeedType != null ? NanoSeedType.deserialize(raw: _nanoSeedType) : defaultNanoSeedType; + final _decredSeedType = sharedPreferences.getInt(PreferencesKey.decredSeedType); + + final decredSeedType = + _decredSeedType != null ? DecredSeedType.deserialize(raw: _decredSeedType) : defaultDecredSeedType; + final nodes = {}; final powNodes = {}; @@ -1401,6 +1417,7 @@ abstract class SettingsStoreBase with Store { initialMoneroSeedType: moneroSeedType, initialBitcoinSeedType: bitcoinSeedType, initialNanoSeedType: nanoSeedType, + initialDecredSeedType: decredSeedType, initialAppSecure: isAppSecure, initialDisableTrade: disableTradeOption, initialDisableAutomaticExchangeStatusUpdates: disableAutomaticExchangeStatusUpdates, @@ -1570,6 +1587,11 @@ abstract class SettingsStoreBase with Store { nanoSeedType = _nanoSeedType != null ? NanoSeedType.deserialize(raw: _nanoSeedType) : defaultNanoSeedType; + final _decredSeedType = sharedPreferences.getInt(PreferencesKey.decredSeedType); + + decredSeedType = + _decredSeedType != null ? DecredSeedType.deserialize(raw: _decredSeedType) : defaultDecredSeedType; + balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); shouldSaveRecipientAddress = diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 429bdf0e1d..63be2d3f15 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -53,12 +53,14 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.banano: return _settingsStore.nanoSeedType == NanoSeedType.bip39; + case WalletType.decred: + return _settingsStore.decredSeedType == DecredSeedType.bip39; + case WalletType.monero: case WalletType.wownero: case WalletType.none: case WalletType.haven: case WalletType.zano: - case WalletType.decred: return false; } } @@ -75,6 +77,8 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { bool get isNanoSeedTypeOptionsEnabled => [WalletType.nano].contains(type); + bool get isDecredSeedTypeOptionsEnabled => [WalletType.decred].contains(type); + bool get hasPassphraseOption => [ WalletType.bitcoin, WalletType.litecoin, diff --git a/lib/view_model/seed_settings_view_model.dart b/lib/view_model/seed_settings_view_model.dart index 2218996761..49a23df1a7 100644 --- a/lib/view_model/seed_settings_view_model.dart +++ b/lib/view_model/seed_settings_view_model.dart @@ -30,6 +30,13 @@ abstract class SeedSettingsViewModelBase with Store { void setNanoSeedType(NanoSeedType derivationType) => _appStore.settingsStore.nanoSeedType = derivationType; + @computed + DecredSeedType get decredSeedType => _appStore.settingsStore.decredSeedType; + + @action + void setDecredSeedType(DecredSeedType derivationType) => + _appStore.settingsStore.decredSeedType = derivationType; + @computed String? get passphrase => this._seedSettingsStore.passphrase; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 60918051c4..a3506be077 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -148,12 +148,18 @@ abstract class WalletCreationVMBase with Store { DerivationInfo getDefaultCreateDerivation() { final useBip39ForBitcoin = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.bip39; final useBip39ForNano = seedSettingsViewModel.nanoSeedType.type == DerivationType.bip39; + final useBip39ForDecred = seedSettingsViewModel.decredSeedType.type == DerivationType.bip39; switch (type) { case WalletType.nano: if (useBip39ForNano) { return DerivationInfo(derivationType: DerivationType.bip39); } return DerivationInfo(derivationType: DerivationType.nano); + case WalletType.decred: + if (useBip39ForDecred) { + return DerivationInfo(derivationType: DerivationType.bip39); + } + return DerivationInfo(derivationType: DerivationType.decred); case WalletType.bitcoin: if (useBip39ForBitcoin) { return DerivationInfo( @@ -182,12 +188,18 @@ abstract class WalletCreationVMBase with Store { DerivationInfo? getCommonRestoreDerivation() { final useElectrum = seedSettingsViewModel.bitcoinSeedType.type == DerivationType.electrum; final useNanoStandard = seedSettingsViewModel.nanoSeedType.type == DerivationType.nano; + final useDecredStandard = seedSettingsViewModel.decredSeedType.type == DerivationType.decred; switch (this.type) { case WalletType.nano: if (useNanoStandard) { return DerivationInfo(derivationType: DerivationType.nano); } return DerivationInfo(derivationType: DerivationType.bip39); + case WalletType.decred: + if (useDecredStandard) { + return DerivationInfo(derivationType: DerivationType.decred); + } + return DerivationInfo(derivationType: DerivationType.bip39); case WalletType.bitcoin: if (useElectrum) { return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; @@ -244,7 +256,7 @@ abstract class WalletCreationVMBase with Store { return list; } - WalletCredentials getCredentials(dynamic options) => throw UnimplementedError(); + WalletCredentials getCredentials(Map? options) => throw UnimplementedError(); Future process(WalletCredentials credentials) => throw UnimplementedError(); diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index 58585a669c..e3321691a6 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -73,7 +73,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with } @override - WalletCredentials getCredentials(dynamic _options) { + WalletCredentials getCredentials(Map? options) { WalletCredentials credentials; switch (type) { case WalletType.bitcoin: @@ -95,7 +95,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with name: name, ledgerConnection: (hardwareWalletVM as LedgerViewModel).connection, password: password, - height: _options['height'] as int? ?? 0, + height: options?['height'] as int? ?? 0, ); default: throw Exception('Unexpected type: ${type.toString()}'); diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 79ecbdfb4e..73fc0b7293 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -59,8 +59,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { [WalletType.monero, WalletType.wownero].contains(type); @override - WalletCredentials getCredentials(dynamic _options) { - final options = _options as List?; + WalletCredentials getCredentials(Map? options) { final passphrase = seedSettingsViewModel.passphrase; seedSettingsViewModel.setPassphrase(null); @@ -68,12 +67,12 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.monero: return monero!.createMoneroNewWalletCredentials( name: name, - language: options!.first as String, + language: options?['language'] as String, password: walletPassword, passphrase: passphrase, seedType: newWalletArguments!.mnemonic != null ? MoneroSeedType.bip39.raw - : (options.last as MoneroSeedType).raw, + : (options?['moneroSeedType'] as MoneroSeedType).raw, mnemonic: newWalletArguments!.mnemonic, ); case WalletType.bitcoin: @@ -151,8 +150,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.wownero: return wownero!.createWowneroNewWalletCredentials( name: name, - language: options!.first as String, - isPolyseed: (options.last as MoneroSeedType).raw == 1, + language: options?['language'] as String, + isPolyseed: (options?['moneroSeedType'] as MoneroSeedType).raw == 1, password: walletPassword, passphrase: passphrase, ); @@ -163,7 +162,11 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { passphrase: passphrase, ); case WalletType.decred: - return decred!.createDecredNewWalletCredentials(name: name); + return decred!.createDecredNewWalletCredentials( + name: name, + isBip39: (options?['decredSeedType'] as DecredSeedType).type == DerivationType.bip39, + mnemonic: newWalletArguments!.mnemonic, + ); case WalletType.none: case WalletType.haven: throw Exception('Unexpected type: ${type.toString()}'); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index a08d582cbb..0b92257441 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -119,15 +119,15 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { bool isButtonEnabled; @override - WalletCredentials getCredentials(dynamic options) { + WalletCredentials getCredentials(Map? options) { final password = walletPassword ?? generateWalletPassword(); - String? passphrase = options['passphrase'] as String?; - final height = options['height'] as int? ?? 0; - name = options['name'] as String; - DerivationInfo? derivationInfo = options["derivationInfo"] as DerivationInfo?; + String? passphrase = options?['passphrase'] as String?; + final height = options?['height'] as int? ?? 0; + name = options?['name'] as String; + DerivationInfo? derivationInfo = options?["derivationInfo"] as DerivationInfo?; if (mode == WalletRestoreMode.seed) { - final seed = options['seed'] as String; + final seed = options?['seed'] as String; switch (type) { case WalletType.monero: return monero!.createMoneroRestoreWalletFromSeedCredentials( @@ -274,7 +274,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromPrivateKey( name: name, - privateKey: options['private_key'] as String, + privateKey: options?['private_key'] as String, password: password, ); @@ -282,20 +282,20 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { return nano!.createNanoRestoreWalletFromKeysCredentials( name: name, password: password, - seedKey: options['private_key'] as String, + seedKey: options?['private_key'] as String, derivationType: derivationInfo!.derivationType!, ); case WalletType.polygon: return polygon!.createPolygonRestoreWalletFromPrivateKey( name: name, password: password, - privateKey: options['private_key'] as String, + privateKey: options?['private_key'] as String, ); case WalletType.base: return base!.createBaseRestoreWalletFromPrivateKey( name: name, password: password, - privateKey: options['private_key'] as String, + privateKey: options?['private_key'] as String, ); case WalletType.arbitrum: return arbitrum!.createArbitrumRestoreWalletFromPrivateKey( @@ -307,13 +307,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { return solana!.createSolanaRestoreWalletFromPrivateKey( name: name, password: password, - privateKey: options['private_key'] as String, + privateKey: options?['private_key'] as String, ); case WalletType.tron: return tron!.createTronRestoreWalletFromPrivateKey( name: name, password: password, - privateKey: options['private_key'] as String, + privateKey: options?['private_key'] as String, ); case WalletType.wownero: return wownero!.createWowneroRestoreWalletFromKeysCredentials( diff --git a/tool/configure.dart b/tool/configure.dart index bbb4a1b41b..6daf70b374 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1418,7 +1418,7 @@ import 'package:cw_decred/mnemonic.dart'; abstract class Decred { WalletCredentials createDecredNewWalletCredentials( - {required String name, WalletInfo? walletInfo}); + {required String name, WalletInfo? walletInfo, required bool isBip39, required String? mnemonic}); WalletCredentials createDecredRestoreWalletFromSeedCredentials( {required String name, required String mnemonic, required String password}); WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( From 33cb21bd73dded414f433339159a6a30d779e926 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Mon, 27 Oct 2025 16:49:15 +0900 Subject: [PATCH 2/8] decred: Add support for 12 and 24 word seeds. --- cw_decred/lib/api/libdcrwallet.dart | 12 ++++++------ cw_decred/lib/wallet.dart | 5 +++-- lib/src/screens/restore/wallet_restore_page.dart | 2 +- lib/view_model/wallet_restore_view_model.dart | 2 +- scripts/android/build_decred.sh | 2 +- scripts/ios/build_decred.sh | 2 +- scripts/macos/build_decred.sh | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index a6d1e10179..0f7fa2b377 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -176,13 +176,13 @@ class Libwallet { ptrsToFree: [cName, cNumBlocks], ); break; - case "createsignedtransaction": + case "createtransaction": final name = args["name"] ?? ""; final signReq = args["signreq"] ?? ""; final cName = name.toCString(); final cSignReq = signReq.toCString(); res = executePayloadFn( - fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), + fn: () => dcrwalletApi.createTransaction(cName, cSignReq), ptrsToFree: [cName, cSignReq], ); break; @@ -490,16 +490,16 @@ class Libwallet { return res.payload; } - Future createSignedTransaction( - String walletName, String createSignedTransactionReq) async { + Future createTransaction( + String walletName, String createTransactionReq) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { - "method": "createsignedtransaction", + "method": "createtransaction", "name": walletName, - "signreq": createSignedTransactionReq, + "signreq": createTransactionReq, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 432edb4bc0..c8e42067e0 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -412,10 +412,11 @@ abstract class DecredWalletBase "feerate": creds.feeRate ?? defaultFeeRate, "password": _password, "sendall": sendAll, + "sign": true, }; - final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); + final res = await _libwallet.createTransaction(walletInfo.name, jsonEncode(signReq)); final decoded = json.decode(res); - final signedHex = decoded["signedhex"]; + final signedHex = decoded["hex"]; final send = () async { await _libwallet.sendRawTransaction(walletInfo.name, signedHex); await updateBalance(); diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index d673319e5c..f0c51cbb7a 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -610,7 +610,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } if ((walletRestoreViewModel.type == WalletType.decred) && - seedWords.length != WalletRestoreViewModelBase.decredSeedMnemonicLength) { + !WalletRestoreViewModelBase.decredSeedMnemonicLengths.contains(seedWords.length)) { return false; } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 0b92257441..dc3d425efa 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -75,7 +75,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { } static const moneroSeedMnemonicLength = 25; - static const decredSeedMnemonicLength = 15; + static const decredSeedMnemonicLengths = [12, 15, 24]; late List availableModes; late final bool hasSeedLanguageSelector = [ diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index 81b884ff1e..13d0cd817a 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" CW_DECRED_DIR=$(realpath ../..)/cw_decred LIBWALLET_PATH="${PWD}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="29c832efedd48514db9552a0c95ef02ce9cd5dc8" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH || true diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 37384f4e12..4f33c0042c 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -3,7 +3,7 @@ set -e . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="29c832efedd48514db9552a0c95ef02ce9cd5dc8" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh index b1bbfbc438..c5dd418ecd 100755 --- a/scripts/macos/build_decred.sh +++ b/scripts/macos/build_decred.sh @@ -4,7 +4,7 @@ LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="05f8d7374999400fe4d525eb365c39b77d307b14" +LIBWALLET_VERSION="29c832efedd48514db9552a0c95ef02ce9cd5dc8" echo "======================= DECRED LIBWALLET =========================" From f79904f5bed77933a652f754feaacb3af88e0af6 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 30 Oct 2025 15:44:34 +0100 Subject: [PATCH 3/8] add decred to bip39 wallets --- lib/reactions/wallet_utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index 2b48a49ea7..b8a5eee84e 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -15,11 +15,11 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.banano: case WalletType.monero: case WalletType.dogecoin: + case WalletType.decred: return true; case WalletType.wownero: case WalletType.haven: case WalletType.zano: - case WalletType.decred: case WalletType.none: return false; } From 238e5eb657f5414ab87bd74d47b3aeb8e23cbbe6 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Wed, 5 Nov 2025 18:52:47 +0100 Subject: [PATCH 4/8] fix: decred seed types, passphrase for decred bip39 wallet --- cw_core/lib/wallet_info.dart | 1 - lib/entities/seed_type.dart | 6 +++--- lib/src/screens/restore/wallet_restore_page.dart | 2 +- lib/view_model/advanced_privacy_settings_view_model.dart | 2 +- lib/view_model/wallet_restore_view_model.dart | 7 +++++++ 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 533bde0efa..97f2aadd5c 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -35,7 +35,6 @@ enum DerivationType { nano, bip39, electrum, - @HiveField(5) decred, } diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index f859bc5914..6f1ad34a1d 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -91,17 +91,17 @@ class DecredSeedType extends EnumerableItem with Serializable { final DerivationType type; - static const all = [DecredSeedType.nanoStandard, DecredSeedType.bip39]; + static const all = [DecredSeedType.decred, DecredSeedType.bip39]; static const defaultDerivationType = bip39; - static const nanoStandard = DecredSeedType(DerivationType.nano, raw: 0, title: 'Decred'); + static const decred = DecredSeedType(DerivationType.decred, raw: 0, title: 'Decred'); static const bip39 = DecredSeedType(DerivationType.bip39, raw: 1, title: 'BIP39'); static DecredSeedType deserialize({required int raw}) { switch (raw) { case 0: - return nanoStandard; + return decred; case 1: return bip39; default: diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index f0c51cbb7a..9f3d3d72fd 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -99,7 +99,7 @@ class WalletRestorePage extends BasePage { children: [ Observer( builder: (context) { - return walletRestoreViewModel.mode == WalletRestoreMode.seed + return walletRestoreViewModel.walletHasPassphrase ? StandardCheckbox( captionColor: Theme.of(context).colorScheme.onSecondaryContainer, value: walletRestoreViewModel.hasPassphrase, diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 63be2d3f15..9684f1d587 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -93,7 +93,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.wownero, WalletType.zano, WalletType.dogecoin, - ].contains(type); + ].contains(type) || (type == WalletType.decred && _settingsStore.decredSeedType != DecredSeedType.decred); @computed bool get addCustomNode => _addCustomNode; diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index dc3d425efa..69a20cebe3 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/dogecoin/dogecoin.dart'; +import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; @@ -112,6 +113,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { @observable WalletRestoreMode mode; + @computed + bool get walletHasPassphrase { + return !(type == WalletType.decred && seedSettingsViewModel.decredSeedType == DecredSeedType.decred) && + mode == WalletRestoreMode.seed; + } + @observable bool hasPassphrase; From 914a6955accaffbf321e4d280679bd855b6d57c7 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 6 Nov 2025 08:54:35 +0100 Subject: [PATCH 5/8] wip: decred passphrase --- cw_decred/lib/wallet_creation_credentials.dart | 2 ++ cw_decred/lib/wallet_service.dart | 18 ++++++++++++++++-- lib/decred/cw_decred.dart | 4 ++-- lib/view_model/wallet_restore_view_model.dart | 3 ++- tool/configure.dart | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index b1fcd6518c..fb3f71244f 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -15,9 +15,11 @@ class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { {required String name, required String password, required this.mnemonic, + required this.passphrase, WalletInfo? walletInfo}) : super(name: name, password: password, walletInfo: walletInfo); + final String? passphrase; final String mnemonic; } diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index 9e04fea18a..d1a6f796c2 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/mnemonic.dart'; import 'package:cw_decred/wallet_creation_credentials.dart'; import 'package:cw_decred/wallet.dart'; import 'package:cw_core/wallet_base.dart'; @@ -10,7 +11,6 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:path/path.dart'; import 'package:hive/hive.dart'; -import 'package:collection/collection.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:bip39/bip39.dart' as bip39; @@ -61,7 +61,13 @@ class DecredWalletService extends WalletService< if (credentials.isBip39) { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); - return restoreFromSeed(DecredRestoreWalletFromSeedCredentials(name: credentials.name, password: credentials.password!, mnemonic: mnemonic, walletInfo: credentials.walletInfo)); + return restoreFromSeed(DecredRestoreWalletFromSeedCredentials( + name: credentials.name, + password: credentials.password!, + mnemonic: mnemonic, + walletInfo: credentials.walletInfo, + passphrase: credentials.passphrase??'' + )); } final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final network = isTestnet == true ? testnet : mainnet; @@ -211,6 +217,14 @@ class DecredWalletService extends WalletService< await this.init(); final network = isTestnet == true ? testnet : mainnet; final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); + + if ([12,24].contains(credentials.seedPhraseLength!)) { + // bip39 stuff - converting from non-english to english. + final entropy = bip39.mnemonicToEntropy(credentials.mnemonic); + bip39.entropyToMnemonic(entropy); + + } + final config = { "name": credentials.walletInfo!.name, "datadir": dirPath, diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index fd69bdb3a2..e7a60a9ec0 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -10,8 +10,8 @@ class CWDecred extends Decred { @override WalletCredentials createDecredRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}) => - DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); + {required String name, required String mnemonic, required String password, required String passphrase}) => + DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); @override WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 69a20cebe3..deab666eea 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -235,6 +235,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, mnemonic: seed, password: password, + passphrase: passphrase??'', ); case WalletType.none: case WalletType.haven: @@ -308,7 +309,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { return arbitrum!.createArbitrumRestoreWalletFromPrivateKey( name: name, password: password, - privateKey: options['private_key'] as String, + privateKey: options?['private_key'] as String, ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromPrivateKey( diff --git a/tool/configure.dart b/tool/configure.dart index 6daf70b374..6e27f4bcf5 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1420,7 +1420,7 @@ abstract class Decred { WalletCredentials createDecredNewWalletCredentials( {required String name, WalletInfo? walletInfo, required bool isBip39, required String? mnemonic}); WalletCredentials createDecredRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password}); + {required String name, required String mnemonic, required String password, required String passphrase}); WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( {required String name, required String pubkey, required String password}); WalletService createDecredWalletService(Box unspentCoinSource); From 20cfafad643043b2e15eb1f4853cbbdb4cffac45 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 14 Nov 2025 16:15:53 +0900 Subject: [PATCH 6/8] decred: Add passphrase to restore from mnemonic. --- cw_decred/lib/api/libdcrwallet.dart | 3 +- cw_decred/lib/wallet.dart | 14 +++---- cw_decred/lib/wallet_addresses.dart | 1 - .../lib/wallet_creation_credentials.dart | 3 +- cw_decred/lib/wallet_service.dart | 37 +++++++++---------- cw_decred/pubspec.yaml | 5 +++ scripts/android/build_decred.sh | 2 +- scripts/ios/build_decred.sh | 2 +- scripts/macos/build_decred.sh | 2 +- 9 files changed, 35 insertions(+), 34 deletions(-) diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index 0f7fa2b377..b18b6ab222 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -490,8 +490,7 @@ class Libwallet { return res.payload; } - Future createTransaction( - String walletName, String createTransactionReq) async { + Future createTransaction(String walletName, String createTransactionReq) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index c8e42067e0..460ce705a4 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -37,8 +37,8 @@ class DecredWallet = DecredWalletBase with _$DecredWallet; abstract class DecredWalletBase extends WalletBase with Store { - DecredWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password, Box unspentCoinsInfo, - Libwallet libwallet, Function() closeLibwallet) + DecredWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password, + Box unspentCoinsInfo, Libwallet libwallet, Function() closeLibwallet) : _password = password, _libwallet = libwallet, _closeLibwallet = closeLibwallet, @@ -46,13 +46,11 @@ abstract class DecredWalletBase this.unspentCoinsInfo = unspentCoinsInfo, this.watchingOnly = derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePath || - derivationInfo.derivationPath == - DecredWalletService.pubkeyRestorePathTestnet, + derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), - this.isTestnet = derivationInfo.derivationPath == - DecredWalletService.seedRestorePathTestnet || - derivationInfo.derivationPath == - DecredWalletService.pubkeyRestorePathTestnet, + this.isTestnet = + derivationInfo.derivationPath == DecredWalletService.seedRestorePathTestnet || + derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, super(walletInfo, derivationInfo) { walletAddresses = DecredWalletAddresses(walletInfo, libwallet); transactionHistory = DecredTransactionHistory(); diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart index e4af108b9d..4550a278a3 100644 --- a/cw_decred/lib/wallet_addresses.dart +++ b/cw_decred/lib/wallet_addresses.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index fb3f71244f..0186b6eaf6 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -3,7 +3,8 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; class DecredNewWalletCredentials extends WalletCredentials { - DecredNewWalletCredentials({required String name, WalletInfo? walletInfo, required this.isBip39, required this.mnemonic}) + DecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo, required this.isBip39, required this.mnemonic}) : super(name: name, walletInfo: walletInfo); final bool isBip39; diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index d1a6f796c2..6b3bfdfe1a 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:cw_decred/api/libdcrwallet.dart'; -import 'package:cw_decred/mnemonic.dart'; import 'package:cw_decred/wallet_creation_credentials.dart'; import 'package:cw_decred/wallet.dart'; import 'package:cw_core/wallet_base.dart'; @@ -62,12 +61,11 @@ class DecredWalletService extends WalletService< final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); return restoreFromSeed(DecredRestoreWalletFromSeedCredentials( - name: credentials.name, - password: credentials.password!, - mnemonic: mnemonic, - walletInfo: credentials.walletInfo, - passphrase: credentials.passphrase??'' - )); + name: credentials.name, + password: credentials.password!, + mnemonic: mnemonic, + walletInfo: credentials.walletInfo, + passphrase: credentials.passphrase ?? '')); } final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); final network = isTestnet == true ? testnet : mainnet; @@ -164,8 +162,8 @@ class DecredWalletService extends WalletService< "unsyncedaddrs": true, }; await libwallet!.loadWallet(jsonEncode(config)); - final wallet = - DecredWallet(walletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + final wallet = DecredWallet( + walletInfo, di, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); await wallet.init(); return wallet; } @@ -187,10 +185,10 @@ class DecredWalletService extends WalletService< throw Exception('Wallet not found'); } final di = await currentWalletInfo.getDerivationInfo(); - final network = di.derivationPath == seedRestorePathTestnet || - di.derivationPath == pubkeyRestorePathTestnet - ? testnet - : mainnet; + final network = + di.derivationPath == seedRestorePathTestnet || di.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; currentWalletInfo.network = network; currentWalletInfo.save(); if (libwallet == null) { @@ -217,19 +215,20 @@ class DecredWalletService extends WalletService< await this.init(); final network = isTestnet == true ? testnet : mainnet; final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); - - if ([12,24].contains(credentials.seedPhraseLength!)) { + + var mnemonic = credentials.mnemonic; + if ([12, 24].contains(credentials.seedPhraseLength!)) { // bip39 stuff - converting from non-english to english. final entropy = bip39.mnemonicToEntropy(credentials.mnemonic); - bip39.entropyToMnemonic(entropy); - + mnemonic = bip39.entropyToMnemonic(entropy); } - + final config = { "name": credentials.walletInfo!.name, "datadir": dirPath, "pass": credentials.password!, - "mnemonic": credentials.mnemonic, + "mnemonic": mnemonic, + "seedpass": credentials.passphrase, "net": network, "unsyncedaddrs": true, }; diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index 723d8d3654..25de435c62 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -18,6 +18,11 @@ dependencies: cw_core: path: ../cw_core + mobx: any + ffi: any + path: any + hive: any + intl: any dev_dependencies: flutter_test: sdk: flutter diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh index 13d0cd817a..9bf9881bc9 100755 --- a/scripts/android/build_decred.sh +++ b/scripts/android/build_decred.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" CW_DECRED_DIR=$(realpath ../..)/cw_decred LIBWALLET_PATH="${PWD}/decred/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="29c832efedd48514db9552a0c95ef02ce9cd5dc8" +LIBWALLET_VERSION="cbfeb71a1343ce93d24e4b11fc7268f733ab600c" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH || true diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh index 4f33c0042c..1ec8f6595e 100755 --- a/scripts/ios/build_decred.sh +++ b/scripts/ios/build_decred.sh @@ -3,7 +3,7 @@ set -e . ./config.sh LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="29c832efedd48514db9552a0c95ef02ce9cd5dc8" +LIBWALLET_VERSION="cbfeb71a1343ce93d24e4b11fc7268f733ab600c" if [[ -e $LIBWALLET_PATH ]]; then rm -fr $LIBWALLET_PATH diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh index c5dd418ecd..c15011d358 100755 --- a/scripts/macos/build_decred.sh +++ b/scripts/macos/build_decred.sh @@ -4,7 +4,7 @@ LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" LIBWALLET_URL="https://github.com/decred/libwallet.git" -LIBWALLET_VERSION="29c832efedd48514db9552a0c95ef02ce9cd5dc8" +LIBWALLET_VERSION="cbfeb71a1343ce93d24e4b11fc7268f733ab600c" echo "======================= DECRED LIBWALLET =========================" From cd1a1e7a10bbaa8918527adf43705847cabdc60b Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 20 Nov 2025 05:05:05 -0300 Subject: [PATCH 7/8] fix bip39 creation for decred --- cw_decred/lib/wallet_service.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index 6b3bfdfe1a..a93af53aef 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -60,6 +60,7 @@ class DecredWalletService extends WalletService< if (credentials.isBip39) { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + credentials.seedPhraseLength = strength == 128 ? 12 : 24; return restoreFromSeed(DecredRestoreWalletFromSeedCredentials( name: credentials.name, password: credentials.password!, @@ -217,11 +218,6 @@ class DecredWalletService extends WalletService< final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); var mnemonic = credentials.mnemonic; - if ([12, 24].contains(credentials.seedPhraseLength!)) { - // bip39 stuff - converting from non-english to english. - final entropy = bip39.mnemonicToEntropy(credentials.mnemonic); - mnemonic = bip39.entropyToMnemonic(entropy); - } final config = { "name": credentials.walletInfo!.name, From 0f74d81cf9db58e97a4bbf03143f4987ff4a4bf5 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 26 Nov 2025 17:10:38 +0900 Subject: [PATCH 8/8] decred: Add ledger wallets. --- .../lib/hardware/device_connection_type.dart | 3 +- cw_decred/lib/api/libdcrwallet.dart | 159 +++- cw_decred/lib/ledger.dart | 713 ++++++++++++++++++ cw_decred/lib/wallet.dart | 77 +- .../lib/wallet_creation_credentials.dart | 4 +- cw_decred/lib/wallet_service.dart | 31 +- cw_decred/pubspec.lock | 121 ++- cw_decred/pubspec.yaml | 13 + lib/decred/cw_decred.dart | 36 +- .../hardware_wallet/ledger_view_model.dart | 5 + .../wallet_hardware_restore_view_model.dart | 5 + tool/configure.dart | 22 +- 12 files changed, 1137 insertions(+), 52 deletions(-) create mode 100644 cw_decred/lib/ledger.dart diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 76f07edf18..aa66fdc6ab 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -24,7 +24,8 @@ enum DeviceConnectionType { WalletType.bitcoin, WalletType.litecoin, WalletType.ethereum, - WalletType.polygon + WalletType.polygon, + WalletType.decred ].contains(walletType); break; case HardwareWalletType.trezor: diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index b18b6ab222..9e1b246332 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -178,12 +178,12 @@ class Libwallet { break; case "createtransaction": final name = args["name"] ?? ""; - final signReq = args["signreq"] ?? ""; + final signReq = args["req"] ?? ""; final cName = name.toCString(); - final cSignReq = signReq.toCString(); + final cReq = signReq.toCString(); res = executePayloadFn( - fn: () => dcrwalletApi.createTransaction(cName, cSignReq), - ptrsToFree: [cName, cSignReq], + fn: () => dcrwalletApi.createTransaction(cName, cReq), + ptrsToFree: [cName, cReq], ); break; case "sendrawtransaction": @@ -299,9 +299,65 @@ class Libwallet { ptrsToFree: [cName], ); break; - case "shutdown": + case "addrfromextendedkey": + final req = args["req"] ?? ""; + final cReq = req.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addrFromExtendedKey(cReq), + ptrsToFree: [cReq], + ); + break; + case "createextendedkey": + final req = args["req"] ?? ""; + final cReq = req.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.createExtendedKey(cReq), + ptrsToFree: [cReq], + ); + break; + case "decodetx": + final name = args["name"] ?? ""; + final cName = name.toCString(); + final txHex = args["txhex"] ?? ""; + final cTxHex = txHex.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.decodeTx(cName, cTxHex), + ptrsToFree: [cName, cTxHex], + ); + break; + case "gettxn": + final name = args["name"] ?? ""; + final cName = name.toCString(); + final hashes = args["hashes"] ?? ""; + final cHashes = hashes.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.getTxn(cName, cHashes), + ptrsToFree: [cName, cHashes], + ); + break; + case "validateaddr": final name = args["name"] ?? ""; - // final cName = name.toCString(); + final cName = name.toCString(); + final addr = args["addr"] ?? ""; + final cAddr = addr.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.validateAddr(cName, cAddr), + ptrsToFree: [cName, cAddr], + ); + break; + case "addsigs": + final name = args["name"] ?? ""; + final cName = name.toCString(); + final txHex = args["txhex"] ?? ""; + final cTxHex = txHex.toCString(); + final sigScripts = args["sigscripts"] ?? ""; + final cSigScripts = sigScripts.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addSigs(cName, cTxHex, cSigScripts), + ptrsToFree: [cName, cTxHex, cSigScripts], + ); + break; + case "shutdown": executePayloadFn( fn: () => dcrwalletApi.shutdown(), ptrsToFree: [], @@ -498,7 +554,7 @@ class Libwallet { final req = { "method": "createtransaction", "name": walletName, - "signreq": createTransactionReq, + "req": createTransactionReq, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; @@ -679,6 +735,95 @@ class Libwallet { return res.payload; } + Future addrFromExtendedKey(String afekReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addrfromextendedkey", + "req": afekReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future createExtendedKey(String createReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createextendedkey", + "req": createReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future decodeTx(String walletName, String txHex) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "decodetx", + "name": walletName, + "txhex": txHex, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future getTxn(String walletName, String hashes) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "gettxn", + "name": walletName, + "hashes": hashes, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future validateAddr(String walletName, String addr) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "validateaddr", + "name": walletName, + "addr": addr, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future addSigs(String walletName, String txHex, String sigScripts) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addsigs", + "name": walletName, + "txhex": txHex, + "sigscripts": sigScripts, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + Future shutdown() async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); diff --git a/cw_decred/lib/ledger.dart b/cw_decred/lib/ledger.dart new file mode 100644 index 0000000000..79575bc475 --- /dev/null +++ b/cw_decred/lib/ledger.dart @@ -0,0 +1,713 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:dart_varuint_bitcoin/dart_varuint_bitcoin.dart' as varint; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; +import 'package:ledger_litecoin/src/operations/litecoin_sign_msg_operation.dart'; +import 'package:ledger_litecoin/src/tx_utils/transaction.dart'; +import 'package:ledger_litecoin/src/litecoin_transformer.dart'; +import 'package:ledger_litecoin/src/tx_utils/finalize_input.dart'; +import 'package:ledger_litecoin/src/tx_utils/constants.dart'; +import 'package:ledger_litecoin/src/ledger/ledger_input_operation.dart'; +import 'package:ledger_litecoin/src/ledger/litecoin_instructions.dart'; +import 'package:ledger_litecoin/src/utils/bip32_path_helper.dart'; +import 'package:ledger_litecoin/src/utils/bip32_path_to_buffer.dart'; + +/// This command is used to sign a given secure hash using a private key (after +/// re-hashing it following the standard Bitcoin signing process) to finalize a +/// transaction input signing process. +/// +/// This command will be rejected if the transaction signing state is not +/// consistent or if a user validation is required and the provided user +/// validation code is not correct. +class UntrustedHashSignOperation extends LedgerInputOperation { + final String derivationPath; + + final int lockTime; + + final int sigHashType; + + final int expiryHeight; + + UntrustedHashSignOperation( + this.derivationPath, this.lockTime, this.expiryHeight, this.sigHashType) + : super(btcCLA, untrustedHashSignINS); + + @override + int get p1 => 0x00; + + @override + int get p2 => 0x00; + + @override + Future read(ByteDataReader reader) async { + final result = reader.read(reader.remainingLength); + + if (result.isNotEmpty) { + result[0] = 0x30; + return result.sublist(0, result.length - 2); + } + + return result; + } + + @override + Future writeInputData() async { + final writer = ByteDataWriter(); + + final path = BIPPath.fromString(derivationPath).toPathArray(); + writer.write(packDerivationPath(path)); + writer.writeUint32(lockTime); + writer.writeUint32(expiryHeight); + writer.write([sigHashType]); + + return writer.toBytes(); + } +} + +class HashOutputFull extends LedgerInputOperation { + final Uint8List outputScript; + + HashOutputFull(this.outputScript) : super(btcCLA, untrustedHashTransactionInputFinalizeINS); + + @override + int get p1 => 0x80; + + @override + int get p2 => 0x00; + + @override + Future read(ByteDataReader reader) async => reader.read(reader.remainingLength); + + @override + Future writeInputData() async => outputScript; +} + +class TrustedInput { + final bool trustedInput; + final Uint8List value; + final Uint8List tree; + final Uint8List sequence; + + const TrustedInput({ + required this.trustedInput, + required this.value, + required this.tree, + required this.sequence, + }); +} + +/// This command is used to sign a given secure hash using a private key (after +/// re-hashing it following the standard Bitcoin signing process) to finalize a +/// transaction input signing process. +/// +/// This command will be rejected if the transaction signing state is not +/// consistent or if a user validation is required and the provided user +/// validation code is not correct. +class UntrustedHashTxInputStartOperation extends LedgerInputOperation { + final bool firstRound; + final bool isNewTransaction; + + final Uint8List transactionData; + + UntrustedHashTxInputStartOperation(this.isNewTransaction, this.firstRound, this.transactionData) + : super(btcCLA, untrustedHashTransactionInputStartINS); + + @override + int get p1 => firstRound ? 0x00 : 0x80; + + @override + int get p2 => isNewTransaction ? 0x00 : 0x80; + + @override + Future read(ByteDataReader reader) async => reader.read(reader.remainingLength); + + @override + Future writeInputData() async => transactionData; +} + +Future startUntrustedHashTransactionInput( + LedgerConnection connection, LedgerTransformer transformer, + {required bool isNewTransaction, + required Transaction transaction, + required List inputs}) async { + var data = ByteDataWriter() + ..write(transaction.version) + ..write(varint.encode(transaction.inputs.length).buffer); + + await connection.sendOperation( + UntrustedHashTxInputStartOperation( + isNewTransaction, + true, + data.toBytes(), + ), + transformer: transformer); + + var i = 0; + + for (final input in transaction.inputs) { + late final Uint8List prefix; + if (inputs[i].trustedInput) { + prefix = Uint8List.fromList([0x01, inputs[i].value.length]); + } else { + prefix = Uint8List.fromList([0x00]); + } + + final data = Uint8List.fromList([ + ...prefix, + ...inputs[i].value, + ...inputs[i].tree, + ...varint.encode(input.script.length).buffer, + ]); + + await connection.sendOperation( + UntrustedHashTxInputStartOperation( + isNewTransaction, + false, + data, + ), + transformer: transformer); + + final scriptBlocks = []; + var offset = 0; + + if (input.script.isEmpty) { + scriptBlocks.add(input.sequence); + } else { + while (offset != input.script.length) { + final blockSize = input.script.length - offset > MAX_SCRIPT_BLOCK + ? MAX_SCRIPT_BLOCK + : input.script.length - offset; + + if (offset + blockSize != input.script.length) { + scriptBlocks.add(input.script.sublist(offset, offset + blockSize)); + } else { + scriptBlocks.add( + Uint8List.fromList( + [...input.script.sublist(offset, offset + blockSize), ...input.sequence]), + ); + } + + offset += blockSize; + } + } + + for (final scriptBlock in scriptBlocks) { + await connection.sendOperation( + UntrustedHashTxInputStartOperation( + isNewTransaction, + false, + scriptBlock, + ), + transformer: transformer); + } + + i++; + } +} + +/// This command is used to extract a Trusted Input (encrypted transaction hash, +/// output index, output amount) from a transaction. +/// +/// The transaction data to be provided should be encoded using bitcoin standard +/// raw transaction encoding. Scripts can be sent over several APDUs. +/// Other individual transaction elements split over different APDUs will be +/// rejected. 64 bits varints are rejected. +class GetTrustedInputOperation extends LedgerInputOperation { + final int? indexLookup; + + final Uint8List inputData; + + GetTrustedInputOperation(this.inputData, [this.indexLookup]) : super(btcCLA, getTrustedInputINS); + + @override + int get p1 => indexLookup != null ? 0x00 : 0x80; + + @override + int get p2 => 0x00; + + @override + Future read(ByteDataReader reader) async { + final result = reader.read(reader.remainingLength); + + return result.isNotEmpty ? result.sublist(0, result.length - 2) : result; + } + + @override + Future writeInputData() async { + final writer = ByteDataWriter(); + if (indexLookup != null) writer.writeUint32(indexLookup!, Endian.big); + + writer.write(inputData); + + return writer.toBytes(); + } +} + +Future getTrustedInput(LedgerConnection connection, LedgerTransformer transformer, + {required int indexLookup, required Transaction transaction}) async { + Future processScriptBlocks(Uint8List script, Uint8List? sequence) async { + final seq = sequence ?? Uint8List(0); + final scriptBlocks = []; + var offset = 0; + + while (offset != script.length) { + final blockSize = + script.length - offset > MAX_SCRIPT_BLOCK ? MAX_SCRIPT_BLOCK : script.length - offset; + + if (offset + blockSize != script.length) { + scriptBlocks.add(script.sublist(offset, offset + blockSize)); + } else { + scriptBlocks + .add(Uint8List.fromList([...script.sublist(offset, offset + blockSize), ...seq])); + } + + offset += blockSize; + } + + // Handle case when no script length: we still want to pass the sequence + // relatable: https://github.com/LedgerHQ/ledger-live-desktop/issues/1386 + if (script.isEmpty) scriptBlocks.add(seq); + + Uint8List res = Uint8List(0); + + for (final scriptBlock in scriptBlocks) { + res = await connection.sendOperation(GetTrustedInputOperation(scriptBlock), + transformer: transformer); + } + + return res; + } + + await connection.sendOperation( + GetTrustedInputOperation( + Uint8List.fromList( + [...transaction.version, ...varint.encode(transaction.inputs.length).buffer]), + indexLookup, + ), + transformer: transformer); + + for (final input in transaction.inputs) { + var data = Uint8List.fromList( + [...input.prevout, ...input.tree!, ...varint.encode(input.script.length).buffer]); + + await connection.sendOperation(GetTrustedInputOperation(data), transformer: transformer); + + data = Uint8List.fromList([...input.script, ...input.sequence]); + await connection.sendOperation(GetTrustedInputOperation(data), transformer: transformer); + } + + await connection.sendOperation( + GetTrustedInputOperation(varint.encode(transaction.outputs.length).buffer), + transformer: transformer); + + for (final output in transaction.outputs) { + final data = Uint8List.fromList([ + ...output.amount, + ...[0, 0], + ...varint.encode(output.script.length).buffer, + ...output.script, + ]); + await connection.sendOperation(GetTrustedInputOperation(data), transformer: transformer); + } + + final res = await processScriptBlocks(Uint8List.fromList([0, 0, 0, 0, 0, 0, 0, 0]), null); + + return hex.encode(res); +} + +class PubkeyResp { + String pubkey; + String address; + String chainCode; + + PubkeyResp(this.pubkey, this.address, this.chainCode); +} + +/// Returns an extended public key at the given derivation path, serialized as per BIP-32 +class ExtendedPublicKeyOperation extends LedgerInputOperation { + /// If [displayPublicKey] is set to true the Public Key will be shown to the user on the ledger device + final bool displayPublicKey; + + /// The [derivationPath] is a Bip32-path used to derive the public key/Address + /// If the path is not standard, an error is returned + final String derivationPath; + + ExtendedPublicKeyOperation({ + required this.displayPublicKey, + required this.derivationPath, + }) : super(0xE0, 0x40); + + @override + Future read(ByteDataReader reader) async { + final b = reader.read(reader.remainingLength); + final pubkeyLength = b[0]; + final addressLength = b[1 + pubkeyLength]; + final pubkeyB = b.sublist(1, 1 + pubkeyLength); + final addressB = b.sublist(1 + pubkeyLength + 1, 1 + pubkeyLength + 1 + addressLength); + final chainCodeB = + b.sublist(1 + pubkeyLength + 1 + addressLength, 1 + pubkeyLength + 1 + addressLength + 32); + final pubkey = uint8ListToHex(pubkeyB); + final address = ascii.decode(addressB); + final chainCode = uint8ListToHex(chainCodeB); + return PubkeyResp(pubkey, address, chainCode); + } + + @override + int get p1 => displayPublicKey ? 0x01 : 0x00; + + @override + int get p2 => 0x00; // legacy key type + + @override + Future writeInputData() async { + final path = BIPPath.fromString(derivationPath).toPathArray(); + + final writer = ByteDataWriter()..writeUint8(path.length); // Write length of the derivation path + + for (final element in path) { + writer.writeUint32(element); // Add each part of the path + } + final b = writer.toBytes(); + + return b; + } +} + +String uint8ListToHex(Uint8List data) { + final StringBuffer hexBuffer = StringBuffer(); + for (int byte in data) { + hexBuffer.write(byte.toRadixString(16).padLeft(2, '0')); + } + return hexBuffer.toString(); +} + +Uint8List hexToUint8List(String hexString) { + if (hexString.length % 2 != 0) { + throw ArgumentError('Hex string must have an even number of characters'); + } + + final List bytes = []; + for (int i = 0; i < hexString.length; i += 2) { + String hexPair = hexString.substring(i, i + 2); + int byte = int.parse(hexPair, radix: 16); + bytes.add(byte); + } + return Uint8List.fromList(bytes); +} + +Uint8List intToLittleEndianBytes(int value, int byteCount) { + final buffer = Uint8List(byteCount).buffer; + final byteData = ByteData.view(buffer); + + switch (byteCount) { + case 1: + byteData.setUint8(0, value); + break; + case 2: + byteData.setUint16(0, value, Endian.little); + break; + case 4: + byteData.setUint32(0, value, Endian.little); + break; + case 8: + byteData.setUint64(0, value, Endian.little); + break; + default: + throw ArgumentError('Unsupported byteCount: $byteCount'); + } + + return buffer.asUint8List(); +} + +Uint8List strHashToUint8List(String txid) { + if (txid.length != 64) { + throw ArgumentError('txid should be 32 bytes long'); + } + + final List bytes = []; + for (int i = 64; i > 0; i -= 2) { + String hexPair = txid.substring(i - 2, i); + int byte = int.parse(hexPair, radix: 16); + bytes.add(byte); + } + return Uint8List.fromList(bytes); +} + +class InputTx { + Transaction tx; + int vout; + String path; + String pubkey; + Uint8List sequence; + Uint8List tree; + Uint8List redeemScript; + + InputTx(this.tx, this.vout, this.path, this.pubkey, this.sequence, this.tree, this.redeemScript); +} + +Future createInputTx(Map inp, int n, String walletName, + Uint8List sequence, Uint8List tree, Libwallet libwallet) async { + List ins = []; + final vins = inp["vin"]; + for (var vin in vins) { + final sequence = intToLittleEndianBytes(vin["sequence"], 4); + final tree = intToLittleEndianBytes(vin["tree"], 1); + final prevout = strHashToUint8List(vin["txid"]); + final vout = intToLittleEndianBytes(vin["vout"], 4); + BytesBuilder bb = BytesBuilder(); + bb.add(prevout); + bb.add(vout); + final inp = TransactionInput(bb.toBytes(), Uint8List.new(25), sequence, tree); + ins.add(inp); + } + List outs = []; + final vouts = inp["vout"]; + String addr = ""; + Uint8List redeemScript = Uint8List(0); + for (var vout in vouts) { + final atoms = (vout["value"] * 100000000).toInt(); + final amount = intToLittleEndianBytes(atoms, 8); + final script = hexToUint8List(vout["scriptPubKey"]["hex"]); + final out = TransactionOutput(amount, script); + outs.add(out); + if (vout["n"] == n) { + addr = vout["scriptPubKey"]["addresses"][0]; + redeemScript = hexToUint8List(vout["scriptPubKey"]["hex"]); + } + } + final vaJSON = await libwallet.validateAddr(walletName, addr); + final va = json.decode(vaJSON.isEmpty ? "{}" : vaJSON); + final accountn = va["accountn"] ?? 0; + final branch = va["branch"] ?? 0; + final index = va["index"] ?? 0; + final path = "44'/42'/" + accountn.toString() + "'/" + branch.toString() + "/" + index.toString(); + final tx = Transaction( + version: intToLittleEndianBytes(inp["version"], 4), + inputs: ins, + outputs: outs, + locktime: intToLittleEndianBytes(inp["locktime"], 4)); + return InputTx(tx, n, path, va["pubkey"], sequence, tree, redeemScript); +} + +Uint8List serializeTransactionOutputs(List outs) { + var outputBuffer = outs.isNotEmpty ? varint.encode(outs.length).buffer : Uint8List(0); + + for (final out in outs) { + final atoms = (out["value"] * 100000000).toInt(); + outputBuffer = Uint8List.fromList([ + ...outputBuffer, + ...intToLittleEndianBytes(atoms, 8), + ...intToLittleEndianBytes(out["version"], 2), + ...varint.encode(out["scriptPubKey"]["hex"].length).buffer, + ...hexToUint8List(out["scriptPubKey"]["hex"]), + ]); + } + + return outputBuffer; +} + +Future signTransaction( + String unsignedTx, String walletName, LedgerConnection connection, Libwallet libwallet) async { + final verboseTxJSON = await libwallet.decodeTx(walletName, unsignedTx); + final verboseTx = json.decode(verboseTxJSON.isEmpty ? "{}" : verboseTxJSON); + final targetTx = Transaction(version: intToLittleEndianBytes(verboseTx["version"], 4)); + targetTx.locktime = intToLittleEndianBytes(verboseTx["locktime"], 4); + final vins = verboseTx["vin"]; + final nullScript = Uint8List(0); + final nullPrevout = Uint8List(0); + var ins = []; + List ttIns = []; + for (var vin in vins) { + final txIds = jsonEncode([vin["txid"]]); + final txHexesJSON = await libwallet.getTxn(walletName, txIds); + final txHexes = json.decode(txHexesJSON); + final verboseInJSON = await libwallet.decodeTx(walletName, txHexes[0]); + final verboseIn = json.decode(verboseInJSON.isEmpty ? "{}" : verboseInJSON); + final sequence = intToLittleEndianBytes(vin["sequence"], 4); + final tree = intToLittleEndianBytes(vin["tree"], 1); + ins.add(await createInputTx(verboseIn, vin["vout"], walletName, sequence, tree, libwallet)); + ttIns.add(TransactionInput(nullPrevout, nullScript, sequence, tree)); + } + targetTx.inputs = ttIns; + String changePath = ""; + final vouts = verboseTx["vout"]; + final prefixWriter = ByteDataWriter(); + prefixWriter.write(varint.encode(vouts.length).buffer); + for (var vout in vouts) { + final addr = vout["scriptPubKey"]["addresses"][0]; + final vaJSON = await libwallet.validateAddr(walletName, addr); + final va = json.decode(vaJSON.isEmpty ? "{}" : vaJSON); + final atoms = (vout["value"] * 100000000).toInt(); + final script = vout["scriptPubKey"]["hex"]; + prefixWriter.write(intToLittleEndianBytes(atoms, 8)); + prefixWriter.write(intToLittleEndianBytes(vout["version"], 2)); + prefixWriter.write(varint.encode((script.length / 2).toInt()).buffer); + prefixWriter.write(hexToUint8List(script)); + final branch = va["branch"] ?? 0; + // Assume the internal branch is change. + if (branch == 1) { + final accountn = va["accountn"] ?? 0; + final index = va["index"] ?? 0; + changePath = "44'/42'/" + accountn.toString() + "'/1/" + index.toString(); + } + } + final version = intToLittleEndianBytes(verboseTx["version"], 4); + final List trustedInputs = []; + final List regularOutputs = []; + final List signatures = []; + final LitecoinTransformer transformer = LitecoinTransformer(); + final outputScript = serializeTransactionOutputs(vouts); + + for (final inp in ins) { + final trustedInput = + await getTrustedInput(connection, transformer, indexLookup: inp.vout, transaction: inp.tx); + + trustedInputs.add(TrustedInput( + trustedInput: true, + value: hexToUint8List(trustedInput), + tree: inp.tree, + sequence: inp.sequence, + )); + + regularOutputs.add(inp.tx.outputs[inp.vout]); + } + var isNewTx = true; + for (var i = 0; i < ins.length; i++) { + final script = ins[i].redeemScript; + targetTx.inputs[i].script = script; + await startUntrustedHashTransactionInput( + connection, + transformer, + isNewTransaction: isNewTx, + transaction: targetTx, + inputs: trustedInputs, + ); + + await provideOutputFullChangePath(connection, transformer, path: changePath); + + await connection.sendOperation(HashOutputFull(prefixWriter.toBytes()), + transformer: transformer); + + const SIGHASH_ALL = 1; + final signature = await connection.sendOperation( + UntrustedHashSignOperation( + ins[i].path, verboseTx["locktime"], verboseTx["expiry"], SIGHASH_ALL), + transformer: transformer); + + isNewTx = false; + signatures.add(signature); + targetTx.inputs[i].script = nullScript; + } + + var sigScripts = []; + for (int i = 0; i < signatures.length; i++) { + final pk = hexToUint8List(ins[i].pubkey); + final sigBuffer = Uint8List.fromList([ + ...varint.encode(signatures[i].length).buffer, + ...signatures[i], + ...varint.encode(pk.length).buffer, + ...pk, + ]); + sigScripts.add(hex.encode(sigBuffer)); + } + final sigScriptsJSON = jsonEncode(sigScripts); + return await libwallet.addSigs(walletName, unsignedTx, sigScriptsJSON); +} + +class LedgerWalletService extends HardwareWalletService { + LedgerWalletService( + this.ledgerConnection, { + this.transformer = const LitecoinTransformer(), + this.derivPath = "m/44'/42'/0'/0/0", + }); + final LedgerConnection ledgerConnection; + final LitecoinTransformer transformer; + final String derivPath; + + // NOTE: Seems unused. We need the wallet name and libwallet for signing so + // not adding to this class. + @override + Future signTransaction({required String transaction}) => throw UnimplementedError(); + + @override + Future> getAvailableAccounts({int index = 0, int limit = 5}) async { + await DecredWalletService.initLibwallet(); + + final accounts = []; + final indexRange = List.generate(limit, (i) => i + index); + final parentPkRes = await ledgerConnection.sendOperation( + ExtendedPublicKeyOperation(displayPublicKey: false, derivationPath: "44'/42'"), + transformer: transformer); + + for (final i in indexRange) { + final derivationPath = "44'/42'/$i'"; + final pkRes = await ledgerConnection.sendOperation( + ExtendedPublicKeyOperation(displayPublicKey: false, derivationPath: derivationPath), + transformer: transformer); + + final createReq = { + "key": pkRes.pubkey, + "parentkey": parentPkRes.pubkey, + "chaincode": pkRes.chainCode, + "network": "mainnet", // TODO: Change with network. + "depth": 2, + "childn": i, + "isprivate": false, + }; + + final xpub = await DecredWalletService.libwallet!.createExtendedKey(jsonEncode(createReq)); + + // The xpub is at the account level. 0/0 will take the address from the + // external branch with index 0. + final addrReq = { + "key": xpub, + "path": "0/0", + "addrtype": "p2pkh", + }; + + final address = await DecredWalletService.libwallet!.addrFromExtendedKey(jsonEncode(addrReq)); + + accounts.add(HardwareAccountData( + address: address, + accountIndex: i, + derivationPath: derivationPath, + xpub: xpub, + )); + } + + return accounts; + } + + /// This command is used to sign message using a private key. + /// + /// The signature is performed as follows: + /// The [message] to sign is the magic "\x19Decred Signed Message:\n" - + /// followed by the length of the message to sign on 1 byte (if requested) followed by the binary content of the message + /// The signature is performed on a double SHA-256 hash of the data to sign using the selected private key + /// + /// + /// The signature is returned using the standard ASN-1 encoding. + /// To convert it to the proprietary Bitcoin-QT format, the host has to : + /// + /// Get the parity of the first byte (sequence) : P + /// Add 27 to P if the public key is not compressed, otherwise add 31 to P + /// Return the Base64 encoded version of P || r || s + @override + Future signMessage({required Uint8List message, String? derivationPath}) async { + var dp = derivationPath; + if (dp == null) { + dp = derivPath; + } + return await ledgerConnection.sendOperation( + LitecoinSignMsgOperation(message, dp), + transformer: transformer, + ); + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 460ce705a4..c1fcb8d2ec 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -7,13 +7,22 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_decred/amount_format.dart'; -import 'package:cw_decred/pending_transaction.dart'; -import 'package:cw_decred/transaction_credentials.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:hive/hive.dart'; +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_decred/pending_transaction.dart'; +import 'package:cw_decred/transaction_credentials.dart'; import 'package:cw_decred/api/libdcrwallet.dart'; import 'package:cw_decred/transaction_history.dart'; import 'package:cw_decred/wallet_addresses.dart'; @@ -21,15 +30,7 @@ import 'package:cw_decred/transaction_priority.dart'; import 'package:cw_decred/wallet_service.dart'; import 'package:cw_decred/balance.dart'; import 'package:cw_decred/transaction_info.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_decred/ledger.dart'; part 'wallet.g.dart'; @@ -44,9 +45,10 @@ abstract class DecredWalletBase _closeLibwallet = closeLibwallet, this.syncStatus = NotConnectedSyncStatus(), this.unspentCoinsInfo = unspentCoinsInfo, - this.watchingOnly = + this.isWatchingOnly = derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePath || derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet, + this.isHardware = walletInfo.hardwareWalletType != null, this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), this.isTestnet = derivationInfo.derivationPath == DecredWalletService.seedRestorePathTestnet || @@ -73,6 +75,7 @@ abstract class DecredWalletBase final Libwallet _libwallet; final Function() _closeLibwallet; final idPrefix = "decred_"; + LedgerWalletService? ledgerWalletService; // TODO: Encrypt this. var _seed = ""; @@ -81,7 +84,8 @@ abstract class DecredWalletBase // synced is used to set the syncTimer interval. bool synced = false; - bool watchingOnly; + bool isWatchingOnly; + bool isHardware; bool connecting = false; String persistantPeer = "default-spv-nodes"; FeeCache feeRateFast = FeeCache(defaultFeeRate); @@ -107,7 +111,7 @@ abstract class DecredWalletBase @override String? get seed { - if (watchingOnly) { + if (isWatchingOnly) { return null; } return _seed; @@ -128,7 +132,7 @@ abstract class DecredWalletBase Future init() async { final getSeed = () async { - if (!watchingOnly) { + if (!isWatchingOnly) { _seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? ""; } _pubkey = await _libwallet.defaultPubkey(walletInfo.name); @@ -350,7 +354,7 @@ abstract class DecredWalletBase @override Future createTransaction(Object credentials) async { - if (watchingOnly) { + if (isWatchingOnly && !isHardware) { return DecredPendingTransaction( txid: "", amount: 0, @@ -403,18 +407,27 @@ abstract class DecredWalletBase // The inputs are always used. Currently we don't have use for this // argument. sendall ingores output value and sends everything. - final signReq = { + final req = { // "inputs": inputs, "ignoreInputs": ignoreInputs, "outputs": outputs, "feerate": creds.feeRate ?? defaultFeeRate, - "password": _password, "sendall": sendAll, "sign": true, }; - final res = await _libwallet.createTransaction(walletInfo.name, jsonEncode(signReq)); + if (!isHardware) { + req["password"] = _password; + req["sign"] = true; + } + final res = await _libwallet.createTransaction(walletInfo.name, jsonEncode(req)); final decoded = json.decode(res); - final signedHex = decoded["hex"]; + var signedHex; + if (isHardware) { + signedHex = await signTransaction( + decoded["hex"], walletInfo.name, ledgerWalletService!.ledgerConnection, _libwallet); + } else { + signedHex = decoded["hex"]; + } final send = () async { await _libwallet.sendRawTransaction(walletInfo.name, signedHex); await updateBalance(); @@ -535,7 +548,7 @@ abstract class DecredWalletBase // mnemonic. As long as not private data is imported into the wallet, we // can always rescan from there. var rescanHeight = 0; - if (!watchingOnly) { + if (!isWatchingOnly) { rescanHeight = await walletBirthdayBlockHeight(); // Sync has not yet reached the birthday block. if (rescanHeight == -1) { @@ -560,7 +573,7 @@ abstract class DecredWalletBase @override Future changePassword(String password) async { - if (watchingOnly) { + if (isWatchingOnly) { return; } return () async { @@ -630,7 +643,21 @@ abstract class DecredWalletBase @override Future signMessage(String message, {String? address = null}) async { - if (watchingOnly) { + if (isHardware) { + var path = "m/44'/42'/0'/0/0"; + if (address != null) { + final vaJSON = await _libwallet.validateAddr(walletInfo.name, address); + final va = json.decode(vaJSON.isEmpty ? "{}" : vaJSON); + final accountn = va["accountn"] ?? 0; + final branch = va["branch"] ?? 0; + final index = va["index"] ?? 0; + path = "44'/42'/" + accountn.toString() + "'/" + branch.toString() + "/" + index.toString(); + } + final signature = await ledgerWalletService! + .signMessage(message: ascii.encode(message), derivationPath: path); + return base64Encode(signature); + } + if (isWatchingOnly) { throw "a watching only wallet cannot sign"; } var addr = address; @@ -765,5 +792,5 @@ abstract class DecredWalletBase String get password => _password; @override - bool canSend() => seed != null; + bool canSend() => seed != null || isHardware; } diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart index 0186b6eaf6..a5cec317b3 100644 --- a/cw_decred/lib/wallet_creation_credentials.dart +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -38,9 +38,7 @@ class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials { class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials { DecredRestoreWalletFromHardwareCredentials( {required String name, required this.hwAccountData, WalletInfo? walletInfo}) - : t = throw UnimplementedError(), - super(name: name, walletInfo: walletInfo); + : super(name: name, walletInfo: walletInfo); final HardwareAccountData hwAccountData; - final void t; } diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart index a93af53aef..768dfbdc43 100644 --- a/cw_decred/lib/wallet_service.dart +++ b/cw_decred/lib/wallet_service.dart @@ -30,6 +30,10 @@ class DecredWalletService extends WalletService< static Libwallet? libwallet; Future init() async { + return initLibwallet(); + } + + static Future initLibwallet() async { if (libwallet != null) { return; } @@ -271,6 +275,29 @@ class DecredWalletService extends WalletService< @override Future restoreFromHardwareWallet( - DecredRestoreWalletFromHardwareCredentials credentials) async => - throw UnimplementedError(); + DecredRestoreWalletFromHardwareCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final network = isTestnet == true ? testnet : mainnet; + final dirPath = await pathForWalletDir(name: credentials.walletInfo!.name, type: getType()); + final config = { + "name": credentials.walletInfo!.name, + "datadir": dirPath, + "pubkey": credentials.hwAccountData.xpub, + "net": network, + "unsyncedaddrs": true, + }; + await libwallet!.createWatchOnlyWallet(jsonEncode(config)); + final di = await credentials.walletInfo!.getDerivationInfo(); + di.derivationPath = isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath; + await di.save(); + credentials.walletInfo!.network = network; + credentials.walletInfo!.dirPath = ""; + credentials.walletInfo!.path = ""; + credentials.hardwareWalletType = HardwareWalletType.ledger; + final wallet = DecredWallet(credentials.walletInfo!, di, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } } diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock index 7a40242ee3..e9774414db 100644 --- a/cw_decred/pubspec.lock +++ b/cw_decred/pubspec.lock @@ -62,8 +62,16 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + bitcoin_base: + dependency: "direct main" + description: + name: bitcoin_base + sha256: f744eca882f501108639946e1172ab0b2e5553169dffc973cd0bfa78f25986d4 + url: "https://pub.dev" + source: hosted + version: "0.5.0" blockchain_utils: - dependency: transitive + dependency: "direct main" description: path: "." ref: cake-update-v2 @@ -71,6 +79,14 @@ packages: url: "https://github.com/cake-tech/blockchain_utils" source: git version: "3.3.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" boolean_selector: dependency: transitive description: @@ -201,7 +217,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" @@ -209,7 +225,7 @@ packages: source: hosted version: "1.19.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 @@ -255,6 +271,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dart_varuint_bitcoin: + dependency: "direct main" + description: + name: dart_varuint_bitcoin + sha256: "4f0ccc9733fb54148b9d3688eea822b7aaabf5cc00025998f8c09a1d45b31b4b" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" decimal: dependency: transitive description: @@ -280,7 +312,7 @@ packages: source: hosted version: "1.3.3" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" @@ -329,6 +361,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8 + url: "https://pub.dev" + source: hosted + version: "1.1.0" frontend_server_client: dependency: transitive description: @@ -362,7 +402,7 @@ packages: source: hosted version: "0.2.0" hive: - dependency: transitive + dependency: "direct main" description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" @@ -402,7 +442,7 @@ packages: source: hosted version: "4.0.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf @@ -457,6 +497,31 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + ledger_flutter_plus: + dependency: "direct main" + description: + name: ledger_flutter_plus + sha256: "531da5daba5731d9eca2732881ef2f039b97bf8aa3564e7098dfa99a9b07a8e6" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + ledger_litecoin: + dependency: "direct main" + description: + path: "packages/ledger-litecoin" + ref: HEAD + resolved-ref: "6e6ce17c2ba5cfe3d16a60d53f930db8e827b8f3" + url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" + source: git + version: "0.0.2" + ledger_usb_plus: + dependency: transitive + description: + name: ledger_usb_plus + sha256: "21cc5d976cf7edb3518bd2a0c4164139cbb0817d2e4f2054707fc4edfdf9ce87" + url: "https://pub.dev" + source: hosted + version: "1.0.4" logging: dependency: transitive description: @@ -506,7 +571,7 @@ packages: source: hosted version: "2.0.0" mobx: - dependency: transitive + dependency: "direct main" description: name: mobx sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 @@ -547,7 +612,7 @@ packages: source: hosted version: "2.1.1" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" @@ -602,6 +667,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -674,6 +747,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -888,6 +969,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_ble: + dependency: transitive + description: + name: universal_ble + sha256: "35d210e93a5938c6a6d1fd3c710cf4ac90b1bdd1b11c8eb2beeb32600672e6e6" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" unorm_dart: dependency: transitive description: @@ -952,6 +1049,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index 25de435c62..93f1220873 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -18,11 +18,24 @@ dependencies: cw_core: path: ../cw_core + ledger_flutter_plus: ^1.4.1 + ledger_litecoin: + git: + url: https://github.com/cake-tech/ledger-flutter-plus-plugins + path: packages/ledger-litecoin mobx: any + bitcoin_base: any + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v2 ffi: any path: any hive: any + collection: any intl: any + dart_varuint_bitcoin: any + convert: any dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart index e7a60a9ec0..3be771c771 100644 --- a/lib/decred/cw_decred.dart +++ b/lib/decred/cw_decred.dart @@ -5,19 +5,35 @@ class CWDecred extends Decred { @override WalletCredentials createDecredNewWalletCredentials( - {required String name, WalletInfo? walletInfo, required bool isBip39, required String? mnemonic}) => - DecredNewWalletCredentials(name: name, walletInfo: walletInfo, isBip39: isBip39, mnemonic: mnemonic); + {required String name, + WalletInfo? walletInfo, + required bool isBip39, + required String? mnemonic}) => + DecredNewWalletCredentials( + name: name, walletInfo: walletInfo, isBip39: isBip39, mnemonic: mnemonic); @override WalletCredentials createDecredRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password, required String passphrase}) => - DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); + {required String name, + required String mnemonic, + required String password, + required String passphrase}) => + DecredRestoreWalletFromSeedCredentials( + name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); @override WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( {required String name, required String pubkey, required String password}) => DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); + @override + WalletCredentials createDecredHardwareWalletCredentials( + {required String name, + required HardwareAccountData accountData, + WalletInfo? walletInfo}) => + DecredRestoreWalletFromHardwareCredentials( + name: name, hwAccountData: accountData, walletInfo: walletInfo); + @override WalletService createDecredWalletService(Box unspentCoinSource) { return DecredWalletService(unspentCoinSource); @@ -110,4 +126,16 @@ class CWDecred extends Decred { final decredWallet = wallet as DecredWallet; return decredWallet.pubkey; } + + @override + void setHardwareWalletService(WalletBase wallet, HardwareWalletService service) { + final decredWallet = wallet as DecredWallet; + final ledgerService = service as LedgerWalletService; + decredWallet.ledgerWalletService = ledgerService; + } + + @override + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) { + return LedgerWalletService(connection); + } } diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index da0f138130..d74701238c 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; @@ -190,6 +191,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return ethereum!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); case WalletType.polygon: return polygon!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + case WalletType.decred: + return decred!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -206,6 +209,8 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return ethereum!.getLedgerHardwareWalletService(connection); case WalletType.polygon: return polygon!.getLedgerHardwareWalletService(connection); + case WalletType.decred: + return decred!.getLedgerHardwareWalletService(connection); default: throw UnimplementedError(); } diff --git a/lib/view_model/wallet_hardware_restore_view_model.dart b/lib/view_model/wallet_hardware_restore_view_model.dart index e3321691a6..3c0775e608 100644 --- a/lib/view_model/wallet_hardware_restore_view_model.dart +++ b/lib/view_model/wallet_hardware_restore_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.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'; @@ -97,6 +98,10 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with password: password, height: options?['height'] as int? ?? 0, ); + case WalletType.decred: + credentials = + decred!.createDecredHardwareWalletCredentials(name: name, accountData: selectedAccount!); + break; default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/tool/configure.dart b/tool/configure.dart index 6e27f4bcf5..e5c9e3164a 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1401,8 +1401,14 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/hardware/hardware_account_data.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:cake_wallet/view_model/send/output.dart'; +import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; """; const decredCWHeaders = """ import 'package:cw_decred/transaction_priority.dart'; @@ -1412,17 +1418,26 @@ import 'package:cw_decred/wallet_creation_credentials.dart'; import 'package:cw_decred/amount_format.dart'; import 'package:cw_decred/transaction_credentials.dart'; import 'package:cw_decred/mnemonic.dart'; +import 'package:cw_decred/ledger.dart'; """; const decredCwPart = "part 'cw_decred.dart';"; const decredContent = """ abstract class Decred { WalletCredentials createDecredNewWalletCredentials( - {required String name, WalletInfo? walletInfo, required bool isBip39, required String? mnemonic}); + {required String name, + WalletInfo? walletInfo, + required bool isBip39, + required String? mnemonic}); WalletCredentials createDecredRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password, required String passphrase}); + {required String name, + required String mnemonic, + required String password, + required String passphrase}); WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( {required String name, required String pubkey, required String password}); + WalletCredentials createDecredHardwareWalletCredentials( + {required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); WalletService createDecredWalletService(Box unspentCoinSource); List getTransactionPriorities(); @@ -1448,6 +1463,9 @@ abstract class Decred { List getDecredWordList(); String pubkey(Object wallet); + + void setHardwareWalletService(WalletBase wallet, HardwareWalletService service); + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); } """;