From 43f4dfa6f0dcbea1601e04f9831cefb3a710f5cf Mon Sep 17 00:00:00 2001 From: Czarek Nakamoto Date: Tue, 17 Jun 2025 19:42:43 +0200 Subject: [PATCH] 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: