From a5b6ae6475cea72003204ce56bd57bd59cd97faf Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 13 May 2025 10:10:32 +0200 Subject: [PATCH 01/19] feat: bitcoin view-only --- .../bitcoin_wallet_creation_credentials.dart | 11 ++++ cw_bitcoin/lib/bitcoin_wallet_keys.dart | 10 +++- cw_bitcoin/lib/bitcoin_wallet_service.dart | 23 ++++++-- cw_bitcoin/lib/electrum_wallet.dart | 25 +++++++-- .../lib/pending_bitcoin_transaction.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 14 +++-- .../wallet_restore_from_keys_form.dart | 19 ++++--- .../screens/restore/wallet_restore_page.dart | 10 ++-- .../screens/wallet_keys/wallet_keys_page.dart | 13 +++-- lib/view_model/wallet_keys_view_model.dart | 24 ++++---- lib/view_model/wallet_restore_view_model.dart | 55 +++++++++++++------ tool/configure.dart | 1 + 12 files changed, 145 insertions(+), 62 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index 177d61e87..3d71a0c39 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -54,6 +54,17 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { final String wif; } +class BitcoinWalletFromKeysCredentials extends WalletCredentials { + BitcoinWalletFromKeysCredentials({ + required String name, + required String password, + required this.xpub, + WalletInfo? walletInfo, + }) : super(name: name, password: password, walletInfo: walletInfo); + + final String xpub; +} + class BitcoinRestoreWalletFromHardware extends WalletCredentials { BitcoinRestoreWalletFromHardware({ required String name, diff --git a/cw_bitcoin/lib/bitcoin_wallet_keys.dart b/cw_bitcoin/lib/bitcoin_wallet_keys.dart index 0a4afc10d..4ed0da49c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_keys.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_keys.dart @@ -1,7 +1,15 @@ class BitcoinWalletKeys { - const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey}); + const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey, required this.xpub}); final String wif; final String privateKey; final String publicKey; + final String xpub; + + Map toJson() => { + 'wif': wif, + 'privateKey': privateKey, + 'publicKey': publicKey, + 'xpub': xpub + }; } \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 317b25bcd..74be5e231 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -20,7 +20,7 @@ import 'package:bip39/bip39.dart' as bip39; class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, - BitcoinRestoreWalletFromWIFCredentials, + BitcoinWalletFromKeysCredentials, BitcoinRestoreWalletFromHardware> { BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.payjoinSessionSource, this.alwaysScan, this.isDirect); @@ -169,9 +169,24 @@ class BitcoinWalletService extends WalletService< } @override - Future restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, - {bool? isTestnet}) async => - throw UnimplementedError(); + Future restoreFromKeys(BitcoinWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { + final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; + credentials.walletInfo?.network = network.value; + + final wallet = await BitcoinWallet( + password: credentials.password!, + xpub: credentials.xpub, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + networkParam: network, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.save(); + await wallet.init(); + return wallet; + } @override Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index bb9cea1bc..a29efa48c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -280,11 +280,26 @@ abstract class ElectrumWalletBase } @override - BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), - privateKey: hd.privateKey.toHex(), - publicKey: hd.publicKey.toHex(), - ); + BitcoinWalletKeys get keys { + String? wif; + String? privateKey; + String? publicKey; + try { + wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer); + } catch (_) {} + try { + privateKey = hd.privateKey.toHex(); + } catch (_) {} + try { + publicKey = hd.publicKey.toHex(); + } catch (_) {} + return BitcoinWalletKeys( + wif: wif ?? '', + privateKey: privateKey ?? '', + publicKey: publicKey ?? '', + xpub: xpub, + ); + } String _password; List unspentCoins; diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 6930524eb..c95d4e1d7 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -165,6 +165,6 @@ class PendingBitcoinTransaction with PendingTransaction { @override Future commitUR() { - throw UnimplementedError(); + return Future.value("test"); } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 131bc3a02..11434b64b 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin { passphrase: passphrase, ); + @override + WalletCredentials createBitcoinWalletFromKeys({ + required String name, + required String password, + required String xpub, + }) => + BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub); + @override WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( {required String name, @@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin { final bitcoinWallet = wallet as ElectrumWallet; final keys = bitcoinWallet.keys; - return { - 'wif': keys.wif, - 'privateKey': keys.privateKey, - 'publicKey': keys.publicKey - }; + return bitcoinWallet.keys.toJson(); } @override diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 9ca5c2508..5713ad7d0 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -198,14 +198,14 @@ class WalletRestoreFromKeysFormState extends State { Widget _restoreFromKeysFormFields() { // Decred can only restore a view only wallet with an account pubkey. Other // fields are not used. - if (widget.walletRestoreViewModel.type == WalletType.decred) { + if (widget.walletRestoreViewModel.onlyViewKeyRestore) { return Column( children: [ BaseTextFormField( controller: viewKeyController, hintText: S.of(context).view_key_public, maxLines: null, - ) + ), ], ); } @@ -253,13 +253,14 @@ class WalletRestoreFromKeysFormState extends State { maxLines: null, ), ), - BlockchainHeightWidget( - key: blockchainHeightKey, - hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, - onHeightChange: (_) => null, - onHeightOrDateEntered: widget.onHeightOrDateEntered, - walletType: widget.walletRestoreViewModel.type, - ), + if (widget.walletRestoreViewModel.hasBlockchainHeightSelector) + BlockchainHeightWidget( + key: blockchainHeightKey, + hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, + onHeightChange: (_) => null, + onHeightOrDateEntered: widget.onHeightOrDateEntered, + walletType: widget.walletRestoreViewModel.type, + ), ], ); } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index dd4a284be..411178c83 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -199,7 +199,7 @@ class WalletRestorePage extends BasePage { credentials['seed'] = walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; - if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { + if (walletRestoreViewModel.hasBlockchainHeightSelector) { credentials['height'] = walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ?? -1; @@ -219,7 +219,7 @@ class WalletRestorePage extends BasePage { credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - if (walletRestoreViewModel.type != WalletType.decred) { + if (!walletRestoreViewModel.onlyViewKeyRestore) { credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; credentials['spendKey'] = @@ -519,7 +519,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } }, onViewKeyEntered: (bool entered) { - if (walletRestoreViewModel.type == WalletType.decred) { + if (widget.walletRestoreViewModel.onlyViewKeyRestore) { walletRestoreViewModel.isButtonEnabled = entered; } }, @@ -536,7 +536,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> key: widget.walletRestoreFromSeedFormKey, restoredWallet: walletRestoreViewModel.restoredWallet, seedSettingsViewModel: widget.seedSettingsViewModel, - displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector, + displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightSelector, displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector, type: widget.walletRestoreViewModel.type, blockHeightFocusNode: widget.blockHeightFocusNode, @@ -563,7 +563,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> } void _validateOnChange({bool isPolyseed = false}) { - if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { + if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightSelector) { final hasHeight = walletRestoreFromSeedFormKey .currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty; diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index 917891854..6bffd5f12 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -82,7 +82,7 @@ class _WalletKeysPageBodyState extends State showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty; isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly; - final totalTabs = 1 + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); + final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); _tabController = TabController(length: totalTabs, vsync: this); } @@ -126,7 +126,7 @@ class _WalletKeysPageBodyState extends State dividerColor: Colors.transparent, padding: EdgeInsets.zero, tabs: [ - Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), + if (_hasSeeds) Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),), if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')), ], @@ -137,10 +137,11 @@ class _WalletKeysPageBodyState extends State child: TabBarView( controller: _tabController, children: [ - Padding( - padding: const EdgeInsets.only(left: 22, right: 22), - child: _buildSeedTab(context, false), - ), + if (_hasSeeds) + Padding( + padding: const EdgeInsets.only(left: 22, right: 22), + child: _buildSeedTab(context, false), + ), if (showKeyTab) Padding( padding: const EdgeInsets.only(left: 22, right: 22), diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index da5d04f59..bf181a2ea 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; @@ -162,18 +163,21 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: + final keys = bitcoin!.getWalletKeys(_appStore.wallet!); + + items.addAll([ + if ((keys['wif']??'').isNotEmpty) + StandartListItem(title: "WIF", value: keys['wif']!), + if ((keys['privateKey']??'').isNotEmpty) + StandartListItem(title: S.current.private_key, value: keys['privateKey']!), + if (keys['publicKey'] != null) + StandartListItem(title: S.current.public_key, value: keys['publicKey']!), + if (keys['xpub'] != null) + StandartListItem(title: "xPub", value: keys['xpub']!), + ]); + break; case WalletType.none: case WalletType.haven: - // final keys = bitcoin!.getWalletKeys(_appStore.wallet!); - // - // items.addAll([ - // if (keys['wif'] != null) - // StandartListItem(title: "WIF", value: keys['wif']!), - // if (keys['privateKey'] != null) - // StandartListItem(title: S.current.private_key, value: keys['privateKey']!), - // if (keys['publicKey'] != null) - // StandartListItem(title: S.current.public_key, value: keys['publicKey']!), - // ]); break; } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 6e00ba4cc..9497f27e0 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -32,17 +32,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, {required WalletType type, this.restoredWallet}) - : hasSeedLanguageSelector = - type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, - hasBlockchainHeightLanguageSelector = - type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, - hasRestoreFromPrivateKey = type == WalletType.ethereum || - type == WalletType.polygon || - type == WalletType.nano || - type == WalletType.banano || - type == WalletType.solana || - type == WalletType.tron, - isButtonEnabled = false, + : isButtonEnabled = false, hasPassphrase = false, mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, @@ -60,9 +50,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.decred: + case WalletType.bitcoin: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; - case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: case WalletType.zano: @@ -82,9 +72,34 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { static const decredSeedMnemonicLength = 15; late List availableModes; - final bool hasSeedLanguageSelector; - final bool hasBlockchainHeightLanguageSelector; - final bool hasRestoreFromPrivateKey; + late final bool hasSeedLanguageSelector = [ + WalletType.monero, + WalletType.haven, + WalletType.wownero + ].contains(type); + + late final bool hasBlockchainHeightSelector = [ + WalletType.monero, + WalletType.haven, + WalletType.wownero + ].contains(type); + + late final bool hasRestoreFromPrivateKey = [ + WalletType.ethereum, + WalletType.polygon, + WalletType.nano, + WalletType.banano, + WalletType.solana, + WalletType.tron + ].contains(type); + + late final bool onlyViewKeyRestore = [ + WalletType.bitcoin, + WalletType.litecoin, + WalletType.bitcoinCash, + WalletType.decred + ].contains(type); + final RestoredWallet? restoredWallet; @observable @@ -198,6 +213,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final address = options['address'] as String?; switch (type) { + case WalletType.bitcoin: + return bitcoin!.createBitcoinWalletFromKeys( + name: name, + password: password, + xpub: viewKey!, + ); + case WalletType.monero: return monero!.createMoneroRestoreWalletFromKeysCredentials( name: name, @@ -276,8 +298,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.litecoin: String? mnemonic = credentials['seed'] as String?; String? passphrase = credentials['passphrase'] as String?; + if (mnemonic == null) break; return bitcoin!.getDerivationsFromMnemonic( - mnemonic: mnemonic!, + mnemonic: mnemonic, node: node, passphrase: passphrase, ); diff --git a/tool/configure.dart b/tool/configure.dart index f6e1496de..42f137375 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -163,6 +163,7 @@ abstract class Bitcoin { String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); + WalletCredentials createBitcoinWalletFromKeys({required String name, required String password, required String xpub}); WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); From 355218753d373921c242c77eae8f06c4c56caaf5 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 13 May 2025 10:31:43 +0200 Subject: [PATCH 02/19] fix: add UR to cw_bitcoin, fix payjoin issue --- cw_bitcoin/lib/bitcoin_wallet_service.dart | 1 + cw_bitcoin/lib/pending_bitcoin_transaction.dart | 13 ++++++++++++- cw_bitcoin/pubspec.lock | 17 +++++++++++++++++ cw_bitcoin/pubspec.yaml | 4 ++++ pubspec_base.yaml | 4 ++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 74be5e231..0e30c4319 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -181,6 +181,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + payjoinBox: payjoinSessionSource, ); await wallet.save(); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index c95d4e1d7..e07786f76 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; @@ -11,6 +14,9 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:cw_mweb/mwebd.pb.dart'; +import 'package:ur/cbor_lite.dart'; +import 'package:ur/ur.dart'; +import 'package:ur/ur_encoder.dart'; class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( @@ -165,6 +171,11 @@ class PendingBitcoinTransaction with PendingTransaction { @override Future commitUR() { - return Future.value("test"); + var sourceBytes = Uint8List.fromList(utf8.encode(hex)); + var cborEncoder = CBOREncoder(); + cborEncoder.encodeBytes(sourceBytes); + var ur = UR("psbt", cborEncoder.getBytes()); + var encoded = UREncoder.encode(ur); + return Future.value(encoded); } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c2987894c..fe3f87e17 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -1101,6 +1101,23 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + ur: + dependency: "direct main" + description: + path: "." + ref: "5738f70d0ec3d50977ac3dd01fed62939600238b" + resolved-ref: "5738f70d0ec3d50977ac3dd01fed62939600238b" + url: "https://github.com/bukata-sa/bc-ur-dart" + source: git + version: "0.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index c24732c3a..7b82cd61b 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -58,6 +58,10 @@ dependencies: git: url: https://github.com/sneurlax/socks_socket ref: e6232c53c1595469931ababa878759a067c02e94 + ur: + git: + url: https://github.com/bukata-sa/bc-ur-dart + ref: 5738f70d0ec3d50977ac3dd01fed62939600238b dev_dependencies: flutter_test: diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 9f7f17ae4..78eff5f95 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -130,6 +130,10 @@ dependencies: url: https://github.com/MrCyjaneK/flutter_daemon ref: c24ee99f2f3070ea02d8108bbdd7727d73f7e5f1 flutter_local_notifications: ^19.0.0 + ur: + git: + url: https://github.com/bukata-sa/bc-ur-dart + ref: 5738f70d0ec3d50977ac3dd01fed62939600238b dev_dependencies: flutter_test: From a9df2ee285e7fedda06f239a48cf4994609c137e Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Wed, 28 May 2025 13:19:33 +0200 Subject: [PATCH 03/19] feat: add unsignedPsbt to PendingBitcoinTransaction --- cw_bitcoin/lib/bitcoin_wallet.dart | 10 ++++-- cw_bitcoin/lib/electrum_wallet.dart | 31 +++++++++++-------- .../lib/pending_bitcoin_transaction.dart | 24 +++++++++----- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 9231022f6..54d7e5a51 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -372,8 +372,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { publicKeys: tx.publicKeys!, masterFingerprint: Uint8List(0)); - final originalPsbt = await signPsbt( - base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); + if (tx.isViewOnly) { + tx.unsignedPsbt = transaction.serialize(); + return tx; + } + + final originalPsbt = + await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); tx.commitOverride = () async { final sender = await payjoinManager.initSender( @@ -405,6 +410,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { feeRate: "", network: network, hasChange: true, + isViewOnly: false, ).commit(); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a29efa48c..70e20813e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1119,7 +1119,8 @@ abstract class ElectrumWalletBase network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot + hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot, + isViewOnly: false, )..addListener((transaction) async { transactionHistory.addOne(transaction); await updateBalance(); @@ -1190,18 +1191,21 @@ abstract class ElectrumWalletBase } }); - return PendingBitcoinTransaction(transaction, type, - electrumClient: electrumClient, - amount: estimatedTx.amount, - fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), - network: network, - hasChange: estimatedTx.hasChange, - isSendAll: estimatedTx.isSendAll, - hasTaprootInputs: hasTaprootInputs, - utxos: estimatedTx.utxos, - publicKeys: estimatedTx.publicKeys) - ..addListener((transaction) async { + return PendingBitcoinTransaction( + transaction, + type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys, + isViewOnly: keys.privateKey.isEmpty, + )..addListener((transaction) async { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { transactionHistory.transactions.values.forEach((tx) { @@ -1862,6 +1866,7 @@ abstract class ElectrumWalletBase network: network, hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), + isViewOnly: keys.privateKey.isEmpty, )..addListener((transaction) async { transactionHistory.transactions.values.forEach((tx) { if (tx.id == hash) { diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index e07786f76..b5906ae97 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -34,6 +34,8 @@ class PendingBitcoinTransaction with PendingTransaction { this.utxos = const [], this.publicKeys, this.commitOverride, + this.unsignedPsbt, + required this.isViewOnly, }) : _listeners = []; final WalletType type; @@ -46,6 +48,7 @@ class PendingBitcoinTransaction with PendingTransaction { final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; + final bool isViewOnly; List utxos; bool isMweb; String? changeAddressOverride; @@ -55,6 +58,8 @@ class PendingBitcoinTransaction with PendingTransaction { final Map? publicKeys; Future Function()? commitOverride; + Uint8List? unsignedPsbt; + @override String get id => idOverride ?? _tx.txId(); @@ -78,9 +83,11 @@ class PendingBitcoinTransaction with PendingTransaction { try { final change = _tx.outputs.firstWhere((out) => out.isChange); if (changeAddressOverride != null) { - return PendingChange(changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); + return PendingChange( + changeAddressOverride!, BtcUtils.fromSatoshi(change.amount)); } - return PendingChange(change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); + return PendingChange( + change.scriptPubKey.toAddress(), BtcUtils.fromSatoshi(change.amount)); } catch (_) { return null; } @@ -129,12 +136,14 @@ class PendingBitcoinTransaction with PendingTransaction { Future _ltcCommit() async { try { - final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); + final resp = await CwMweb.broadcast( + BroadcastRequest(rawTx: BytesUtils.fromHexString(hex))); idOverride = resp.txid; } on GrpcError catch (e) { throw BitcoinTransactionCommitFailed(errorMessage: e.message); } catch (e) { - throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}"); + throw BitcoinTransactionCommitFailed( + errorMessage: "Unknown error: ${e.toString()}"); } } @@ -153,7 +162,8 @@ class PendingBitcoinTransaction with PendingTransaction { _listeners.forEach((listener) => listener(transactionInfo())); } - void addListener(void Function(ElectrumTransactionInfo transaction) listener) => + void addListener( + void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, @@ -168,10 +178,10 @@ class PendingBitcoinTransaction with PendingTransaction { inputAddresses: _tx.inputs.map((input) => input.txId).toList(), outputAddresses: outputAddresses, fee: fee); - + @override Future commitUR() { - var sourceBytes = Uint8List.fromList(utf8.encode(hex)); + var sourceBytes = unsignedPsbt!; var cborEncoder = CBOREncoder(); cborEncoder.encodeBytes(sourceBytes); var ur = UR("psbt", cborEncoder.getBytes()); From cde1274def08575ec69f7648cfe94daa30e34e49 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 29 May 2025 08:06:42 +0200 Subject: [PATCH 04/19] wip --- cw_bitcoin/lib/bitcoin_wallet.dart | 8 ++++---- cw_bitcoin/lib/electrum_wallet.dart | 3 ++- cw_bitcoin/lib/pending_bitcoin_transaction.dart | 14 +++++++++++--- cw_monero/lib/pending_monero_transaction.dart | 1 + 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 54d7e5a51..1c9ad8220 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -352,7 +352,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { as PendingBitcoinTransaction; final payjoinUri = credentials.payjoinUri; - if (payjoinUri == null) return tx; + if (payjoinUri == null && !tx.shouldCommitUR()) return tx; final transaction = await buildPsbt( utxos: tx.utxos, @@ -372,7 +372,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { publicKeys: tx.publicKeys!, masterFingerprint: Uint8List(0)); - if (tx.isViewOnly) { + if (tx.shouldCommitUR()) { tx.unsignedPsbt = transaction.serialize(); return tx; } @@ -381,8 +381,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys()); tx.commitOverride = () async { - final sender = await payjoinManager.initSender( - payjoinUri, originalPsbt, int.parse(tx.feeRate)); + final sender = + await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate)); payjoinManager.spawnNewSender( sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount)); }; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 70e20813e..ff075d916 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -681,7 +681,7 @@ abstract class ElectrumWalletBase ); spendsSilentPayment = true; isSilentPayment = true; - } else if (!isHardwareWallet) { + } else if (!isHardwareWallet && false) { privkey = generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); } @@ -1154,6 +1154,7 @@ abstract class ElectrumWalletBase bool hasTaprootInputs = false; final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + return ""; String error = "Cannot find private key."; ECPrivateInfo? key; diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index b5906ae97..500c4776f 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -179,13 +179,21 @@ class PendingBitcoinTransaction with PendingTransaction { outputAddresses: outputAddresses, fee: fee); + @override + bool shouldCommitUR() => isViewOnly; + @override Future commitUR() { var sourceBytes = unsignedPsbt!; var cborEncoder = CBOREncoder(); cborEncoder.encodeBytes(sourceBytes); - var ur = UR("psbt", cborEncoder.getBytes()); - var encoded = UREncoder.encode(ur); - return Future.value(encoded); + var ur = UR("psbt", cborEncoder.getBytes()); + // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256))); + var encoded = UREncoder(ur, 120); + List values = []; + while (!encoded.isComplete) { + values.add(encoded.nextPart()); + } + return Future.value(values.join("\n")); } } diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index 9909a3021..f2459b225 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -39,6 +39,7 @@ class PendingMoneroTransaction with PendingTransaction { String get feeFormatted => AmountConverter.amountIntToString( CryptoCurrency.xmr, pendingTransactionDescription.fee); + @override bool shouldCommitUR() => isViewOnly; @override From a4a40356a33fe8cbd1d627af78715c888026b31c Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 3 Jun 2025 12:13:47 +0200 Subject: [PATCH 05/19] feat: xpub restore from bitcoin URI QR --- lib/view_model/restore/restore_wallet.dart | 5 +++-- lib/view_model/restore/wallet_restore_from_qr_code.dart | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/view_model/restore/restore_wallet.dart b/lib/view_model/restore/restore_wallet.dart index cc3ad4123..153a9960a 100644 --- a/lib/view_model/restore/restore_wallet.dart +++ b/lib/view_model/restore/restore_wallet.dart @@ -35,7 +35,7 @@ class RestoredWallet { factory RestoredWallet.fromKey(Map json) { try { - final codeParsed = jsonDecode(json['raw_qr'].toString()); + final codeParsed = jsonDecode(json['raw_qr'].toString()); if (codeParsed["version"] == 0) { json['address'] = codeParsed["primaryAddress"]; json['view_key'] = codeParsed["privateViewKey"]; @@ -44,6 +44,7 @@ class RestoredWallet { } catch (e) { // fine, we don't care, it is only for monero anyway } + json['view_key'] ??= json['xpub']; final height = json['height'] as String?; return RestoredWallet( restoreMode: json['mode'] as WalletRestoreMode, @@ -51,7 +52,7 @@ class RestoredWallet { address: json['address'] as String?, spendKey: json['spend_key'] as String?, viewKey: json['view_key'] as String?, - height: height != null ? int.tryParse(height)??0 : 0, + height: height != null ? int.tryParse(height) ?? 0 : 0, privateKey: json['private_key'] as String?, ); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 530ba1725..bc959d349 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -158,6 +158,10 @@ class WalletRestoreFromQRCode { throw Exception('Unexpected restore mode: tx_payment_id is invalid'); } + if (credentials.containsKey("xpub")) { + return WalletRestoreMode.keys; + } + if (credentials['seed'] != null) { final seedValue = credentials['seed'] as String; final words = SeedValidator.getWordList(type: type, language: 'english'); From 9539327e170284265a2e6af989311e7db7dd8cbb Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 3 Jun 2025 15:11:24 +0200 Subject: [PATCH 06/19] feat(dev): debug UR codes [skip ci] --- cw_bitcoin/lib/bitcoin_wallet.dart | 24 ++++++++++++++++++- lib/bitcoin/cw_bitcoin.dart | 6 +++++ lib/src/screens/ur/animated_ur_page.dart | 30 ++++++++++++++++++++---- tool/configure.dart | 1 + 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 1c9ad8220..ac59197f6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -24,6 +24,7 @@ import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/payjoin_session.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; @@ -32,6 +33,9 @@ import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_bitcoin/psbt.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; +import 'package:ur/cbor_lite.dart'; +import 'package:ur/ur.dart'; +import 'package:ur/ur_decoder.dart'; part 'bitcoin_wallet.g.dart'; @@ -373,7 +377,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { masterFingerprint: Uint8List(0)); if (tx.shouldCommitUR()) { - tx.unsignedPsbt = transaction.serialize(); + tx.unsignedPsbt = transaction.asPsbtV0(); return tx; } @@ -436,6 +440,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return base64Encode(psbt.asPsbtV0()); } + Future commitPsbtUR(List urCodes) async { + final ur = URDecoder(); + for (final inp in urCodes) { + ur.receivePart(inp); + } + final result = (ur.result as UR); + final cbor = result.cbor; + final cborDecoder = CBORDecoder(cbor); + final out = cborDecoder.decodeBytes(); + final bytes = out.$1; + final base64psbt = base64Encode(bytes); + final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt)); + + // psbt.finalize(); + final finalized = base64Encode(psbt.serialize()); + await commitPsbt(finalized); + } + @override Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 11434b64b..263fe7e84 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -723,6 +723,12 @@ class CWBitcoin extends Bitcoin { } } + @override + Future commitPsbtUR(Object wallet, List urCodes) { + final _wallet = wallet as BitcoinWalletBase; + return _wallet.commitPsbtUR(urCodes); + } + @override String getPayjoinEndpoint(Object wallet) { final _wallet = wallet as ElectrumWallet; diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index a4ef3517f..5f63b4ac9 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -7,9 +8,12 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/utils/clipboard_util.dart'; +import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/animated_ur_model.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; // ur:xmr-txunsigned - unsigned transaction // should show a scanner afterwards. @@ -46,8 +50,8 @@ class AnimatedURPage extends BasePage { frames: urQr.trim().split("\n"), ), ), + if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt"].contains(urQrType)) ...{ SizedBox(height: 32), - if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output") Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( @@ -60,8 +64,10 @@ class AnimatedURPage extends BasePage { ), ), ), - SizedBox(height: 32), - if (urQrType == "ur:xmr-output" && !isAll) Padding( + }, + if (urQrType == "ur:xmr-output" && !isAll) ...{ + SizedBox(height: 32), + Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: SizedBox( width: double.maxFinite, @@ -73,6 +79,7 @@ class AnimatedURPage extends BasePage { ), ), ), + }, ], ); } @@ -106,6 +113,10 @@ class AnimatedURPage extends BasePage { Navigator.of(context).pop(true); } break; + case "ur:psbt": // psbt + final ur = await presentQRScanner(context); + if (ur == null) return; + await bitcoin!.commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n")); default: throw UnimplementedError("unable to handle UR: ${urQrType}"); } @@ -168,10 +179,21 @@ class _URQRState extends State { children: [ Center( child: QrImage( - data: widget.frames[frame % widget.frames.length], version: -1, + data: widget.frames[frame % widget.frames.length], + version: -1, size: 400, ), ), + if (FeatureFlag.hasDevOptions) ...{ + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: """Current frame (${frame % widget.frames.length}): ${widget.frames[frame % widget.frames.length]}, +All frames: + - ${widget.frames.join("\n - ")}""")); + }, + child: Text(widget.frames[frame % widget.frames.length]), + ), + } ], ); } diff --git a/tool/configure.dart b/tool/configure.dart index 42f137375..05568e146 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -245,6 +245,7 @@ abstract class Bitcoin { bool getMwebEnabled(Object wallet); String? getUnusedMwebAddress(Object wallet); String? getUnusedSegwitAddress(Object wallet); + Future commitPsbtUR(Object wallet, List urCodes); void updatePayjoinState(Object wallet, bool state); String getPayjoinEndpoint(Object wallet); From 8e147967adda0ec9c33787a252374933fcf245e3 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 5 Jun 2025 11:40:17 +0200 Subject: [PATCH 07/19] fix: master fingerprint default in create bitcoin transaction --- cw_bitcoin/lib/bitcoin_wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ac59197f6..609276c95 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -374,7 +374,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { outputOrdering: BitcoinOrdering.none, enableRBF: true, publicKeys: tx.publicKeys!, - masterFingerprint: Uint8List(0)); + masterFingerprint: Uint8List.fromList([0, 0, 0, 0])); if (tx.shouldCommitUR()) { tx.unsignedPsbt = transaction.asPsbtV0(); From bfec91dc02a51f2e56db1af88be853dd027c6364 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Thu, 5 Jun 2025 12:17:02 +0200 Subject: [PATCH 08/19] fix: handle empty private key in bitcoin transaction building --- cw_bitcoin/lib/electrum_wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ff075d916..2dd8d698c 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1154,7 +1154,7 @@ abstract class ElectrumWalletBase bool hasTaprootInputs = false; final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { - return ""; + if (keys.privateKey.isEmpty) return ""; String error = "Cannot find private key."; ECPrivateInfo? key; From da1e467f0a9c1016a07d3a2cd254deccd23aa308 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Fri, 6 Jun 2025 09:39:44 +0200 Subject: [PATCH 09/19] fix: show success dialog after btc ur transaction --- lib/src/screens/ur/animated_ur_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index 5f63b4ac9..92325d8eb 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -117,6 +117,7 @@ class AnimatedURPage extends BasePage { final ur = await presentQRScanner(context); if (ur == null) return; await bitcoin!.commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n")); + Navigator.of(context).pop(true); default: throw UnimplementedError("unable to handle UR: ${urQrType}"); } From 7fba63712d82259a81f628ca0a73aaef9895eee1 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Fri, 13 Jun 2025 13:02:10 +0200 Subject: [PATCH 10/19] fix(ui): remove SP from UI in view only wallet --- cw_bitcoin/lib/bitcoin_receive_page_option.dart | 8 ++++++++ cw_bitcoin/pubspec.lock | 8 -------- lib/bitcoin/cw_bitcoin.dart | 9 ++++++++- lib/view_model/dashboard/receive_option_view_model.dart | 2 +- tool/configure.dart | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 07083e111..c4b175590 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -28,6 +28,14 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinReceivePageOption.p2pkh ]; + static const allViewOnly = [ + BitcoinReceivePageOption.p2wpkh, + BitcoinReceivePageOption.p2tr, + BitcoinReceivePageOption.p2wsh, + BitcoinReceivePageOption.p2sh, + BitcoinReceivePageOption.p2pkh + ]; + static const allLitecoin = [ BitcoinReceivePageOption.p2wpkh, BitcoinReceivePageOption.mweb, diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index fe3f87e17..4a77b31ca 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -1110,14 +1110,6 @@ packages: url: "https://github.com/bukata-sa/bc-ur-dart" source: git version: "0.1.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" vector_math: dependency: transitive description: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 263fe7e84..ae285ab36 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -280,7 +280,14 @@ class CWBitcoin extends Bitcoin { } @override - List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + List getBitcoinReceivePageOptions(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + final keys = bitcoinWallet.keys; + if (keys.privateKey.isEmpty) { + return BitcoinReceivePageOption.allViewOnly; + } + return BitcoinReceivePageOption.all; + } @override List getLitecoinReceivePageOptions() { diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index f15d7dad6..1ae6851a0 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -22,7 +22,7 @@ abstract class ReceiveOptionViewModelBase with Store { switch (walletType) { case WalletType.bitcoin: _options = [ - ...bitcoin!.getBitcoinReceivePageOptions(), + ...bitcoin!.getBitcoinReceivePageOptions(_wallet), ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) ]; break; diff --git a/tool/configure.dart b/tool/configure.dart index 05568e146..45d7cbba3 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -207,7 +207,7 @@ abstract class Bitcoin { Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); - List getBitcoinReceivePageOptions(); + List getBitcoinReceivePageOptions(Object wallet); List getLitecoinReceivePageOptions(); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); bool isPayjoinAvailable(Object wallet); From dc51ad138523269c4ea1fc41b4c6b2488ec67c6e Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Fri, 13 Jun 2025 13:52:37 +0200 Subject: [PATCH 11/19] fix(cw_bitcoin): tx sending on non-airgap --- cw_bitcoin/lib/electrum_wallet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 2dd8d698c..01197daa0 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -681,7 +681,7 @@ abstract class ElectrumWalletBase ); spendsSilentPayment = true; isSilentPayment = true; - } else if (!isHardwareWallet && false) { + } else if (!isHardwareWallet && keys.privateKey.isNotEmpty) { privkey = generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); } From eeaa8b434deec0cc215adc14ef3806c4e5fb4643 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Sat, 14 Jun 2025 19:07:39 +0200 Subject: [PATCH 12/19] fix(cw_bitcoin): disable all non-segwit addresses --- cw_bitcoin/lib/bitcoin_receive_page_option.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index c4b175590..8491ae8e3 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -30,10 +30,12 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const allViewOnly = [ BitcoinReceivePageOption.p2wpkh, - BitcoinReceivePageOption.p2tr, - BitcoinReceivePageOption.p2wsh, - BitcoinReceivePageOption.p2sh, - BitcoinReceivePageOption.p2pkh + // TODO: uncomment this after we properly derive keys and not use m/84 for + // all of them (as this breaks cupcake) + // BitcoinReceivePageOption.p2tr, + // BitcoinReceivePageOption.p2wsh, + // BitcoinReceivePageOption.p2sh, + // BitcoinReceivePageOption.p2pkh ]; static const allLitecoin = [ From 43f4dfa6f0dcbea1601e04f9831cefb3a710f5cf Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 17 Jun 2025 19:42:43 +0200 Subject: [PATCH 13/19] feat(ur): support BBQR for coldcard qr --- .../lib/pending_bitcoin_transaction.dart | 18 ++++- cw_bitcoin/pubspec.lock | 11 ++- cw_bitcoin/pubspec.yaml | 4 + .../src/pending_bitcoin_cash_transaction.dart | 2 +- cw_core/lib/pending_transaction.dart | 2 +- cw_decred/lib/pending_transaction.dart | 2 +- cw_evm/lib/pending_evm_chain_transaction.dart | 2 +- cw_monero/lib/pending_monero_transaction.dart | 7 +- cw_nano/lib/pending_nano_transaction.dart | 2 +- cw_solana/lib/pending_solana_transaction.dart | 2 +- cw_tron/lib/pending_tron_transaction.dart | 2 +- .../lib/pending_wownero_transaction.dart | 2 +- .../lib/model/pending_zano_transaction.dart | 2 +- lib/di.dart | 2 +- lib/src/screens/ur/animated_ur_page.dart | 81 ++++++++++++++----- pubspec_base.yaml | 4 + 16 files changed, 110 insertions(+), 35 deletions(-) diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 500c4776f..7c726944c 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:bbqrdart/bbqrdart.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; @@ -183,7 +184,7 @@ class PendingBitcoinTransaction with PendingTransaction { bool shouldCommitUR() => isViewOnly; @override - Future commitUR() { + Future> commitUR() { var sourceBytes = unsignedPsbt!; var cborEncoder = CBOREncoder(); cborEncoder.encodeBytes(sourceBytes); @@ -194,6 +195,19 @@ class PendingBitcoinTransaction with PendingTransaction { while (!encoded.isComplete) { values.add(encoded.nextPart()); } - return Future.value(values.join("\n")); + + final bbqrObj = BBQRPsbt.fromUint8List(sourceBytes); + List bbqr = [ + bbqrObj.asString(), + ]; + while (!bbqrObj.isDone) { + bbqrObj.next(); + bbqr.add(bbqrObj.asString()); + } + + return Future.value({ + "PSBT (bcur)": values.join("\n"), + "PSBT (bbqr)": bbqr.join("\n"), + }); } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4a77b31ca..bbe6c9312 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bbqrdart: + dependency: "direct main" + description: + path: "." + ref: b0f3f72911221bafed0ab77359fccd64bc79d524 + resolved-ref: b0f3f72911221bafed0ab77359fccd64bc79d524 + url: "https://github.com/mrcyjanek/bbqrdart" + source: git + version: "1.0.0" bech32: dependency: "direct main" description: @@ -1191,5 +1200,5 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.6.2 <4.0.0" flutter: ">=3.27.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 7b82cd61b..d9a28d576 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -62,6 +62,10 @@ dependencies: git: url: https://github.com/bukata-sa/bc-ur-dart ref: 5738f70d0ec3d50977ac3dd01fed62939600238b + bbqrdart: + git: + url: https://github.com/mrcyjanek/bbqrdart + ref: b0f3f72911221bafed0ab77359fccd64bc79d524 dev_dependencies: flutter_test: diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index 05483ce54..27cbc7e3a 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -86,7 +86,7 @@ class PendingBitcoinCashTransaction with PendingTransaction { isReplaced: false, ); @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 78eba68a3..cd3eaa60c 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -17,5 +17,5 @@ mixin PendingTransaction { bool shouldCommitUR() => false; Future commit(); - Future commitUR(); + Future> commitUR(); } diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart index 63162bf40..7bdf54652 100644 --- a/cw_decred/lib/pending_transaction.dart +++ b/cw_decred/lib/pending_transaction.dart @@ -33,7 +33,7 @@ class DecredPendingTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_evm/lib/pending_evm_chain_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart index 61b406470..e5c819c06 100644 --- a/cw_evm/lib/pending_evm_chain_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -52,7 +52,7 @@ class PendingEVMChainTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_monero/lib/pending_monero_transaction.dart b/cw_monero/lib/pending_monero_transaction.dart index f2459b225..5eb9d873f 100644 --- a/cw_monero/lib/pending_monero_transaction.dart +++ b/cw_monero/lib/pending_monero_transaction.dart @@ -65,7 +65,7 @@ class PendingMoneroTransaction with PendingTransaction { } @override - Future commitUR() async { + Future> commitUR() async { try { final ret = await monero_transaction_history.commitTransactionFromPointerAddress( address: pendingTransactionDescription.pointerAddress, @@ -75,7 +75,10 @@ class PendingMoneroTransaction with PendingTransaction { await Future.delayed(const Duration(milliseconds: 250)); await wallet.fetchTransactions(); }()); - return ret; + if (ret == null) return {}; + return { + "xmr-txsigned": ret, + }; } catch (e) { final message = e.toString(); diff --git a/cw_nano/lib/pending_nano_transaction.dart b/cw_nano/lib/pending_nano_transaction.dart index 51a4ef6c1..32d965288 100644 --- a/cw_nano/lib/pending_nano_transaction.dart +++ b/cw_nano/lib/pending_nano_transaction.dart @@ -39,7 +39,7 @@ class PendingNanoTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index 5102ea51f..b06813212 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -41,7 +41,7 @@ class PendingSolanaTransaction with PendingTransaction { String get id => ''; @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart index 2420f083b..c56f90eb5 100644 --- a/cw_tron/lib/pending_tron_transaction.dart +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -32,7 +32,7 @@ class PendingTronTransaction with PendingTransaction { String get id => ''; @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart index 967f63756..2171598e0 100644 --- a/cw_wownero/lib/pending_wownero_transaction.dart +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -52,7 +52,7 @@ class PendingWowneroTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/cw_zano/lib/model/pending_zano_transaction.dart b/cw_zano/lib/model/pending_zano_transaction.dart index 719c370a1..3d63b0377 100644 --- a/cw_zano/lib/model/pending_zano_transaction.dart +++ b/cw_zano/lib/model/pending_zano_transaction.dart @@ -46,7 +46,7 @@ class PendingZanoTransaction with PendingTransaction { } @override - Future commitUR() { + Future> commitUR() { throw UnimplementedError(); } } diff --git a/lib/di.dart b/lib/di.dart index 5a7be0f1f..d4f7da99e 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -971,7 +971,7 @@ Future setup({ getIt.registerFactory(() => AnimatedURModel(getIt.get())); - getIt.registerFactoryParam((String urQr, _) => + getIt.registerFactoryParam, void>((Map urQr, _) => AnimatedURPage(getIt.get(), urQr: urQr)); getIt.registerFactoryParam( diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index 92325d8eb..fdad8d3b7 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:bbqrdart/bbqrdart.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -20,22 +21,21 @@ import 'package:flutter/services.dart'; class AnimatedURPage extends BasePage { final bool isAll; - AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) { - if (urQr == "export-outputs") { - this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false); - } else if (urQr == "export-outputs-all") { - this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true); - } else { - this.urQr = urQr; - } - } + AnimatedURPage(this.animatedURmodel, { + required this.urQr, + this.isAll = false, + }); - late String urQr; + late Map urQr; final AnimatedURModel animatedURmodel; String get urQrType { - final first = urQr.trim().split("\n")[0]; + if (urQr.values.first.trim().substring(0, 2) == BBQR.header) { + return BBQR.header; + } + if (urQr.isEmpty) return "unknown"; + final first = urQr.values.first.trim().split("\n")[0]; return first.split('/')[0]; } @@ -47,10 +47,10 @@ class AnimatedURPage extends BasePage { Padding( padding: const EdgeInsets.only(top: 64.0), child: URQR( - frames: urQr.trim().split("\n"), + urqr: urQr, ), ), - if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt"].contains(urQrType)) ...{ + if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header].contains(urQrType)) ...{ SizedBox(height: 32), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -65,6 +65,7 @@ class AnimatedURPage extends BasePage { ), ), }, + if (urQrType == "ur:xmr-output" && !isAll) ...{ SizedBox(height: 32), Padding( @@ -88,7 +89,13 @@ class AnimatedURPage extends BasePage { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) { - return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true); + return AnimatedURPage( + animatedURmodel, + urQr: { + "export-outputs-all": "export-outputs-all", + }, + isAll: true, + ); }, ), ); @@ -136,9 +143,9 @@ class AnimatedURPage extends BasePage { } class URQR extends StatefulWidget { - URQR({super.key, required this.frames}); + URQR({super.key, required this.urqr}); - List frames; + final Map urqr; @override // ignore: library_private_types_in_public_api @@ -172,6 +179,24 @@ class _URQRState extends State { super.dispose(); } + late String selected = (widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first; + int selectedInt = 0; + + List get frames { + return widget.urqr[selected]?.split("\n") ?? []; + } + + late String nextLabel = widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length] ; + + void next() { + final keys = widget.urqr.keys.toList(); + selectedInt++; + setState(() { + nextLabel = keys[(selectedInt + 1) % keys.length]; + selected = keys[(selectedInt + 1) % keys.length]; + }); + } + @override Widget build(BuildContext context) { return Column( @@ -180,19 +205,35 @@ class _URQRState extends State { children: [ Center( child: QrImage( - data: widget.frames[frame % widget.frames.length], + data: frames[frame % frames.length], version: -1, size: 400, ), ), + if (widget.urqr.values.length > 1) + SizedBox( + width: double.maxFinite, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.maxFinite, + child: PrimaryButton( + onPressed: next, + text: nextLabel, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ), if (FeatureFlag.hasDevOptions) ...{ TextButton( onPressed: () { - Clipboard.setData(ClipboardData(text: """Current frame (${frame % widget.frames.length}): ${widget.frames[frame % widget.frames.length]}, + Clipboard.setData(ClipboardData(text: """Current frame (${frame % frames.length}): ${frames[frame % frames.length]}, All frames: - - ${widget.frames.join("\n - ")}""")); + - ${frames.join("\n - ")}""")); }, - child: Text(widget.frames[frame % widget.frames.length]), + child: Text(frames[frame % frames.length]), ), } ], diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 78eff5f95..d01eff6a5 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -134,6 +134,10 @@ dependencies: git: url: https://github.com/bukata-sa/bc-ur-dart ref: 5738f70d0ec3d50977ac3dd01fed62939600238b + bbqrdart: + git: + url: https://github.com/mrcyjanek/bbqrdart + ref: b0f3f72911221bafed0ab77359fccd64bc79d524 dev_dependencies: flutter_test: From 78f5e45d9b9b2e3ac439cfb6e1d64a5b76fc1822 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 19 Jun 2025 06:02:55 +0200 Subject: [PATCH 14/19] flutter 3.27.0 -> 3.27.4 --- .github/workflows/automated_integration_test.yml | 2 +- .github/workflows/pr_test_build_android.yml | 2 +- .github/workflows/pr_test_build_linux.yml | 2 +- Dockerfile | 4 ++-- cw_bitcoin/pubspec.lock | 2 +- cw_core/pubspec.lock | 2 +- cw_nano/pubspec.lock | 2 +- docs/builds/ANDROID.md | 4 ++-- docs/builds/IOS.md | 8 ++++---- docs/builds/LINUX.md | 4 ++-- docs/builds/MACOS.md | 8 ++++---- docs/builds/WINDOWS.md | 6 +++--- scripts/linux/build_cake_release.sh | 2 +- scripts/windows/Dockerfile.windows | 2 +- 14 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index a26e3645d..84c680dda 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -55,7 +55,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: "3.27.0" + flutter-version: "3.27.4" channel: stable - name: Install package dependencies diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index cdbd7ca37..f6102e0ae 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 119cd7530..9c3ab91b2 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -9,7 +9,7 @@ jobs: PR_test_build: runs-on: linux-amd64 container: - image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly + image: ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/Dockerfile b/Dockerfile index 151b7af20..58fec4613 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly +# docker buildx build --push --pull --platform linux/amd64,linux/arm64 . -f Dockerfile -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly # Heavily inspired by cirrusci images # https://github.com/cirruslabs/docker-images-android/blob/master/sdk/tools/Dockerfile @@ -15,7 +15,7 @@ LABEL org.opencontainers.image.source=https://github.com/cake-tech/cake_wallet ENV GOLANG_VERSION=1.24.1 # Pin Flutter version to latest known-working version -ENV FLUTTER_VERSION=3.27.0 +ENV FLUTTER_VERSION=3.27.4 # Pin Android Studio, platform, and build tools versions to latest known-working version # Comes from https://developer.android.com/studio/#command-tools diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index bbe6c9312..0171f2793 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -1201,4 +1201,4 @@ packages: version: "2.2.2" sdks: dart: ">=3.6.2 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.27.4" diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index d7bfdbe2a..c53c38a00 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -829,4 +829,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.27.4" diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index aa85d6dde..f8498065a 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -978,4 +978,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.27.4" diff --git a/docs/builds/ANDROID.md b/docs/builds/ANDROID.md index d7bc584ad..522735042 100644 --- a/docs/builds/ANDROID.md +++ b/docs/builds/ANDROID.md @@ -18,8 +18,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts/android source ./app_env.sh cakewallet diff --git a/docs/builds/IOS.md b/docs/builds/IOS.md index 1d23b917c..44abaa805 100644 --- a/docs/builds/IOS.md +++ b/docs/builds/IOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your iOS devi ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.0 +Flutter 3.27.4 ``` NOTE: Newer versions of macOS and Xcode may also work, but have not been confirmed to work by the Cake team. @@ -43,9 +43,9 @@ To enable iOS build support for Xcode, perform the following: ### 3. Installing Flutter -Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -65,7 +65,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ``` diff --git a/docs/builds/LINUX.md b/docs/builds/LINUX.md index 0f438a1d6..d22f42ec1 100644 --- a/docs/builds/LINUX.md +++ b/docs/builds/LINUX.md @@ -20,8 +20,8 @@ In order to build the latest version of Cake Wallet, simply run the following: git clone --branch main https://github.com/cake-tech/cake_wallet.git # NOTE: Replace `main` with the latest release tag available at https://github.com/cake-tech/cake_wallet/releases/latest. cd cake_wallet -# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry -docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly bash -x << EOF +# docker build -t ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly . # Uncomment to build the docker image yourself instead of pulling it from the registry +docker run --privileged -v$(pwd):$(pwd) -w $(pwd) -i --rm ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly bash -x << EOF set -x -e pushd scripts ./gen_android_manifest.sh diff --git a/docs/builds/MACOS.md b/docs/builds/MACOS.md index 7e0f39aab..46a9842a4 100644 --- a/docs/builds/MACOS.md +++ b/docs/builds/MACOS.md @@ -7,7 +7,7 @@ The following are the system requirements to build Cake Wallet for your macOS de ```txt macOS 15.3.1 Xcode 16.2 -Flutter 3.27.0 +Flutter 3.27.4 ``` ### 1. Installing dependencies @@ -34,9 +34,9 @@ sudo xcodebuild -runFirstLaunch ### 3. Installing Flutter -Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). -NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 4. Installing Rust @@ -56,7 +56,7 @@ The output of this command should appear like this, indicating successful instal ```zsh Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.27.0, on macOS 15.x.x) +[✓] Flutter (Channel stable, 3.27.4, on macOS 15.x.x) ... [✓] Xcode - develop for iOS and macOS (Xcode 16.2) ... diff --git a/docs/builds/WINDOWS.md b/docs/builds/WINDOWS.md index 7a86dac49..4fec78dc0 100644 --- a/docs/builds/WINDOWS.md +++ b/docs/builds/WINDOWS.md @@ -6,18 +6,18 @@ The following are the system requirements to build Cake Wallet for your Windows ```txt Windows 10 or later (64-bit), x86-64 based -Flutter 3.27.0 +Flutter 3.27.4 ``` ### 1. Installing Flutter -Install Flutter, specifically version `3.27.0` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). +Install Flutter, specifically version `3.27.4` by following the [official docs](https://docs.flutter.dev/get-started/install/windows). In order for Flutter to function, you'll also need to enable Developer Mode: Start Menu > search for "Run" > type `ms-settings:developers`, and turn on Developer Mode. -NOTE: as `3.27.0` is not the latest version, you'll need to download it from instead of the link in the docs above. +NOTE: as `3.27.4` is not the latest version, you'll need to download it from instead of the link in the docs above. ### 2. Install Development Tools diff --git a/scripts/linux/build_cake_release.sh b/scripts/linux/build_cake_release.sh index 16f228d2e..e73898e4c 100755 --- a/scripts/linux/build_cake_release.sh +++ b/scripts/linux/build_cake_release.sh @@ -14,7 +14,7 @@ cd "$SCRIPT_DIR" BUILD_AMD64=false BUILD_ARM64=false APP_TYPE="cakewallet" -DOCKER_IMAGE="ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.0-go1.24.1-ruststablenightly" +DOCKER_IMAGE="ghcr.io/cake-tech/cake_wallet:debian12-flutter3.27.4-go1.24.1-ruststablenightly" # Parse arguments for arg in "$@" diff --git a/scripts/windows/Dockerfile.windows b/scripts/windows/Dockerfile.windows index a534be5e4..893009b8f 100644 --- a/scripts/windows/Dockerfile.windows +++ b/scripts/windows/Dockerfile.windows @@ -4,7 +4,7 @@ FROM mcr.microsoft.com/windows/servercore:ltsc2022 -ENV FLUTTER_VERSION=3.27.0 +ENV FLUTTER_VERSION=3.27.4 ENV GIT_VERSION=2.47.1 ENV VS_INSTALLED_DIR="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" ENV PATH="C:\Users\ContainerAdministrator\.cargo\bin;C:\ProgramData\chocolatey\bin;C:\flutter\flutter\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\ContainerAdministrator\AppData\Local\Microsoft\WindowsApps" From 1833c0804af262763585111c06424ec0110bbb50 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 24 Jun 2025 12:50:00 +0200 Subject: [PATCH 15/19] feat: raw xpub qr support for coldcard q --- .../restore/wallet_restore_from_qr_code.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index bc959d349..e522a20b2 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -54,6 +54,8 @@ class WalletRestoreFromQRCode { final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key)); + if (code.startsWith("xpub")) return WalletType.bitcoin; + if (extracted == null) { // Special case for view-only monero wallet try { @@ -117,11 +119,15 @@ class WalletRestoreFromQRCode { formattedUri = seedPhrase != null ? '$walletType:?seed=$seedPhrase' - : throw Exception('Failed to determine valid seed phrase'); + : code.startsWith('xpub') + ? '$walletType:?xpub=$code' + : throw Exception('Failed to determine valid seed phrase'); } else { final index = code.indexOf(':'); final query = code.substring(index + 1).replaceAll('?', '&'); - formattedUri = '$walletType:?$query'; + formattedUri = code.startsWith('xpub') + ? '$walletType:?xpub=$code' + :'$walletType:?$query'; } final uri = Uri.parse(formattedUri); From e1bdc1db09d13cc4d52ebebc071533f0287cb86e Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 26 Jun 2025 08:18:42 +0200 Subject: [PATCH 16/19] fix: update ledger_bitcoin fix: update switching button --- cw_bitcoin/pubspec.lock | 6 +++--- cw_bitcoin/pubspec.yaml | 2 +- lib/src/screens/ur/animated_ur_page.dart | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 0171f2793..78a36632f 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -597,8 +597,8 @@ packages: dependency: "direct main" description: path: "packages/ledger-bitcoin" - ref: trunk - resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 + ref: eab179d487cddda3f647f6608115a89662facde4 + resolved-ref: eab179d487cddda3f647f6608115a89662facde4 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" @@ -1201,4 +1201,4 @@ packages: version: "2.2.2" sdks: dart: ">=3.6.2 <4.0.0" - flutter: ">=3.27.4" + flutter: ">=3.27.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index d9a28d576..ef6fb1a7a 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins path: packages/ledger-bitcoin - ref: trunk + ref: eab179d487cddda3f647f6608115a89662facde4 ledger_litecoin: git: url: https://github.com/cake-tech/ledger-flutter-plus-plugins diff --git a/lib/src/screens/ur/animated_ur_page.dart b/lib/src/screens/ur/animated_ur_page.dart index fdad8d3b7..358afa4b1 100644 --- a/lib/src/screens/ur/animated_ur_page.dart +++ b/lib/src/screens/ur/animated_ur_page.dart @@ -192,8 +192,8 @@ class _URQRState extends State { final keys = widget.urqr.keys.toList(); selectedInt++; setState(() { - nextLabel = keys[(selectedInt + 1) % keys.length]; - selected = keys[(selectedInt + 1) % keys.length]; + nextLabel = keys[(selectedInt) % keys.length]; + selected = keys[(selectedInt) % keys.length]; }); } From ddeef70d0538fc855e909fbbab54ef7af0fa1980 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 26 Jun 2025 09:02:31 +0200 Subject: [PATCH 17/19] fix: restore button not showing up after scanning --- lib/view_model/wallet_restore_view_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 9497f27e0..9ec0500b1 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -32,7 +32,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, {required WalletType type, this.restoredWallet}) - : isButtonEnabled = false, + : isButtonEnabled = restoredWallet != null, hasPassphrase = false, mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, From d34b5b9112cfc695f7b913165faf3b6025876372 Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Thu, 26 Jun 2025 10:54:48 +0200 Subject: [PATCH 18/19] feat: commit raw tx (from coldcard Q) --- cw_bitcoin/lib/bitcoin_wallet.dart | 48 +++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 609276c95..73aa46458 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -441,21 +441,41 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } Future commitPsbtUR(List urCodes) async { - final ur = URDecoder(); - for (final inp in urCodes) { - ur.receivePart(inp); - } - final result = (ur.result as UR); - final cbor = result.cbor; - final cborDecoder = CBORDecoder(cbor); - final out = cborDecoder.decodeBytes(); - final bytes = out.$1; - final base64psbt = base64Encode(bytes); - final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt)); + if (urCodes.isEmpty) throw Exception("No QR code got scanned"); + bool isUr = urCodes.any((str) { + return str.startsWith("ur:psbt/"); + }); + if (isUr) { + final ur = URDecoder(); + for (final inp in urCodes) { + ur.receivePart(inp); + } + final result = (ur.result as UR); + final cbor = result.cbor; + final cborDecoder = CBORDecoder(cbor); + final out = cborDecoder.decodeBytes(); + final bytes = out.$1; + final base64psbt = base64Encode(bytes); + final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt)); - // psbt.finalize(); - final finalized = base64Encode(psbt.serialize()); - await commitPsbt(finalized); + // psbt.finalize(); + final finalized = base64Encode(psbt.serialize()); + await commitPsbt(finalized); + } else { + final btcTx = BtcTransaction.fromRaw(urCodes.first); + + return PendingBitcoinTransaction( + btcTx, + type, + electrumClient: electrumClient, + amount: 0, + fee: 0, + feeRate: "", + network: network, + hasChange: true, + isViewOnly: false, + ).commit(); + } } @override From 8a12e2c2c7bd44a4a945fabcbf3b2bb98acc139e Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Fri, 27 Jun 2025 13:40:25 +0200 Subject: [PATCH 19/19] feat: legacy crypto-psbt support for seed signer --- cw_bitcoin/lib/pending_bitcoin_transaction.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 7c726944c..e14095bf8 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -188,12 +188,16 @@ class PendingBitcoinTransaction with PendingTransaction { var sourceBytes = unsignedPsbt!; var cborEncoder = CBOREncoder(); cborEncoder.encodeBytes(sourceBytes); - var ur = UR("psbt", cborEncoder.getBytes()); + var ur = UR("psbt", cborEncoder.getBytes()); + var urLegacy = UR("crypto-psbt", cborEncoder.getBytes()); // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256))); var encoded = UREncoder(ur, 120); + var encodedLegacy = UREncoder(urLegacy, 120); List values = []; + List valuesLegacy = []; while (!encoded.isComplete) { values.add(encoded.nextPart()); + valuesLegacy.add(encodedLegacy.nextPart()); } final bbqrObj = BBQRPsbt.fromUint8List(sourceBytes); @@ -208,6 +212,7 @@ class PendingBitcoinTransaction with PendingTransaction { return Future.value({ "PSBT (bcur)": values.join("\n"), "PSBT (bbqr)": bbqr.join("\n"), + "PSBT (bcur legacy)": valuesLegacy.join("\n"), }); } }