dev
dart 697 lines 23.9 KB
Raw
1 import 'dart:convert';
2
3 import 'package:bip39/bip39.dart' as bip39;
4 import 'package:bitcoin_base/bitcoin_base.dart';
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 import 'package:cw_bitcoin/.secrets.g.dart' as secrets;
7 import 'package:cw_bitcoin/address_from_output.dart';
8 import 'package:cw_bitcoin/bitcoin_address_record.dart';
9 import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
10 import "package:cw_bitcoin/bitcoin_receive_page_option.dart";
11 import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
12 import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
13 import 'package:cw_bitcoin/electrum_balance.dart';
14 import 'package:cw_bitcoin/electrum_derivations.dart';
15 import 'package:cw_bitcoin/electrum_transaction_info.dart';
16 import 'package:cw_bitcoin/electrum_wallet.dart';
17 import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
18 import 'package:cw_bitcoin/locktime.dart';
19 import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
20 import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
21 import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
22 import 'package:cw_bitcoin/output_ordering.dart';
23 import 'package:cw_bitcoin/payjoin/manager.dart';
24 import 'package:cw_bitcoin/payjoin/storage.dart';
25 import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
26 import 'package:cw_bitcoin/psbt/signer.dart';
27 import 'package:cw_bitcoin/psbt/transaction_builder.dart';
28 import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
29 import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
30 import 'package:cw_core/amount/money.dart';
31 import 'package:cw_core/crypto_currency.dart';
32 import 'package:cw_core/encryption_file_utils.dart';
33 import 'package:cw_core/output_info.dart';
34 import 'package:cw_core/payjoin_session.dart';
35 import 'package:cw_core/pending_transaction.dart';
36 import 'package:cw_core/sync_status.dart';
37 import "package:cw_core/receive_page_option.dart";
38 import 'package:cw_core/unspent_coin_type.dart';
39 import 'package:cw_core/unspent_coins_info.dart';
40 import 'package:cw_core/utils/print_verbose.dart';
41 import 'package:cw_core/utils/zpub.dart';
42 import 'package:cw_core/wallet_info.dart';
43 import 'package:cw_core/wallet_keys_file.dart';
44 import 'package:flutter/foundation.dart';
45 import 'package:hive/hive.dart';
46 import 'package:ledger_bitcoin/psbt.dart';
47 import 'package:mobx/mobx.dart';
48 import 'package:ur/cbor_lite.dart';
49 import 'package:ur/ur.dart';
50 import 'package:ur/ur_decoder.dart';
51
52 part 'bitcoin_wallet.g.dart';
53
54 class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
55
56 abstract class BitcoinWalletBase extends ElectrumWallet with Store {
57 BitcoinWalletBase({
58 required String password,
59 required WalletInfo walletInfo,
60 required DerivationInfo derivationInfo,
61 required Box<UnspentCoinsInfo> unspentCoinsInfo,
62 required Box<PayjoinSession> payjoinBox,
63 required EncryptionFileUtils encryptionFileUtils,
64 Uint8List? seedBytes,
65 String? mnemonic,
66 String? xpub,
67 String? addressPageType,
68 BasedUtxoNetwork? networkParam,
69 List<BitcoinAddressRecord>? initialAddresses,
70 ElectrumBalance? initialBalance,
71 ElectrumBalance? initialLightningBalance,
72 Map<String, int>? initialRegularAddressIndex,
73 Map<String, int>? initialChangeAddressIndex,
74 String? passphrase,
75 List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
76 int initialSilentAddressIndex = 0,
77 bool? alwaysScan,
78 bool? useLightning,
79 String? cachedLightningAddress,
80 }) : super(
81 mnemonic: mnemonic,
82 passphrase: passphrase,
83 xpub: xpub,
84 password: password,
85 walletInfo: walletInfo,
86 derivationInfo: derivationInfo,
87 unspentCoinsInfo: unspentCoinsInfo,
88 network: networkParam == null
89 ? BitcoinNetwork.mainnet
90 : networkParam == BitcoinNetwork.mainnet
91 ? BitcoinNetwork.mainnet
92 : BitcoinNetwork.testnet,
93 initialAddresses: initialAddresses,
94 initialBalance: initialBalance,
95 seedBytes: seedBytes,
96 encryptionFileUtils: encryptionFileUtils,
97 currency:
98 networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
99 alwaysScan: alwaysScan,
100 useLightning: useLightning ?? true,
101 ) {
102 // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
103 // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here)
104 // String derivationPath = walletInfo.derivationInfo!.derivationPath!;
105 // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
106 // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
107
108 if (mnemonic != null && this.useLightning && LightningWallet.isAvailable) {
109 try {
110 lightningWallet = LightningWallet(
111 mnemonic: mnemonic,
112 passphrase: passphrase,
113 seedBytes: seedBytes,
114 apiKey: secrets.breezApiKey,
115 lnurlDomain: "cake.cash",
116 cachedAddress: cachedLightningAddress,
117 );
118 } catch (e) {
119 printV(e);
120 }
121 }
122
123 payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
124 walletAddresses = BitcoinWalletAddresses(
125 walletInfo,
126 initialAddresses: initialAddresses,
127 initialRegularAddressIndex: initialRegularAddressIndex,
128 initialChangeAddressIndex: initialChangeAddressIndex,
129 initialSilentAddresses: initialSilentAddresses,
130 initialSilentAddressIndex: initialSilentAddressIndex,
131 mainHdByType: mainHdByType,
132 sideHdByType: sideHdByType,
133 legacyMainHd: mainHd,
134 legacySideHd: sideHd,
135 network: networkParam ?? network,
136 masterHd: seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
137 isHardwareWallet: walletInfo.isHardwareWallet,
138 payjoinManager: payjoinManager,
139 lightningWallet: lightningWallet,
140 );
141
142 if (lightningWallet != null) {
143 walletAddresses.setLightningAddress(walletInfo.name);
144 }
145 autorun((_) {
146 this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
147 });
148
149 reaction((_) => this.useLightning, (bool useLightning) {
150 if (useLightning && LightningWallet.isAvailable) {
151 if (mnemonic != null) {
152 lightningWallet = LightningWallet(
153 mnemonic: mnemonic,
154 passphrase: passphrase,
155 seedBytes: seedBytes,
156 apiKey: secrets.breezApiKey,
157 lnurlDomain: "cake.cash",
158 cachedAddress: cachedLightningAddress,
159 );
160 walletAddresses.setLightningAddress(walletInfo.name);
161 }
162 } else {
163 lightningWallet = null;
164 }
165 });
166
167 if (initialLightningBalance != null) {
168 balance[CryptoCurrency.btcln] = initialLightningBalance;
169 }
170 }
171
172 bool get isLightningInitialized => lightningWallet?.isInitialized == true;
173
174 @override
175 bool get hasRescan => true;
176
177 static Future<BitcoinWallet> create({
178 required String mnemonic,
179 required String password,
180 required WalletInfo walletInfo,
181 required Box<UnspentCoinsInfo> unspentCoinsInfo,
182 required Box<PayjoinSession> payjoinBox,
183 required EncryptionFileUtils encryptionFileUtils,
184 String? passphrase,
185 String? addressPageType,
186 BasedUtxoNetwork? network,
187 List<BitcoinAddressRecord>? initialAddresses,
188 List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
189 ElectrumBalance? initialBalance,
190 Map<String, int>? initialRegularAddressIndex,
191 Map<String, int>? initialChangeAddressIndex,
192 int initialSilentAddressIndex = 0,
193 }) async {
194 late Uint8List seedBytes;
195
196 final derivationInfo = await walletInfo.getDerivationInfo();
197
198 switch (derivationInfo.derivationType) {
199 case DerivationType.bip39:
200 seedBytes = await bip39.mnemonicToSeed(
201 mnemonic,
202 passphrase: passphrase ?? "",
203 );
204 break;
205 case DerivationType.electrum:
206 default:
207 seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
208 break;
209 }
210
211 return BitcoinWallet(
212 mnemonic: mnemonic,
213 passphrase: passphrase ?? "",
214 password: password,
215 walletInfo: walletInfo,
216 derivationInfo: derivationInfo,
217 unspentCoinsInfo: unspentCoinsInfo,
218 initialAddresses: initialAddresses,
219 initialSilentAddresses: initialSilentAddresses,
220 initialSilentAddressIndex: initialSilentAddressIndex,
221 initialBalance: initialBalance,
222 encryptionFileUtils: encryptionFileUtils,
223 seedBytes: seedBytes,
224 initialRegularAddressIndex: initialRegularAddressIndex,
225 initialChangeAddressIndex: initialChangeAddressIndex,
226 addressPageType: addressPageType,
227 networkParam: network,
228 payjoinBox: payjoinBox,
229 useLightning: true,
230 );
231 }
232
233 static Future<BitcoinWallet> open({
234 required String name,
235 required WalletInfo walletInfo,
236 required Box<UnspentCoinsInfo> unspentCoinsInfo,
237 required Box<PayjoinSession> payjoinBox,
238 required String password,
239 required EncryptionFileUtils encryptionFileUtils,
240 }) async {
241 final network = walletInfo.network != null
242 ? BasedUtxoNetwork.fromName(walletInfo.network!)
243 : BitcoinNetwork.mainnet;
244
245 final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
246
247 ElectrumWalletSnapshot? snp = null;
248
249 try {
250 snp = await ElectrumWalletSnapshot.load(
251 encryptionFileUtils,
252 name,
253 walletInfo.type,
254 password,
255 network,
256 );
257 } catch (e) {
258 if (!hasKeysFile) rethrow;
259 }
260
261 final WalletKeysData keysData;
262 // Migrate wallet from the old scheme to then new .keys file scheme
263 if (!hasKeysFile) {
264 keysData = WalletKeysData(
265 mnemonic: snp!.mnemonic,
266 xPub: snp.xpub,
267 passphrase: snp.passphrase,
268 );
269 } else {
270 keysData = await WalletKeysFile.readKeysFile(
271 name,
272 walletInfo.type,
273 password,
274 encryptionFileUtils,
275 );
276 }
277
278 final derivationInfo = await walletInfo.getDerivationInfo();
279
280 // set the default if not present:
281 derivationInfo.derivationPath ??= snp?.derivationPath ?? electrum_path;
282 derivationInfo.derivationType ??= snp?.derivationType ?? DerivationType.electrum;
283 if (derivationInfo.derivationType == DerivationType.unknown) {
284 if (snp?.derivationPath == electrum_path || snp?.derivationType == DerivationType.electrum) {
285 derivationInfo.derivationPath = electrum_path;
286 derivationInfo.derivationType = DerivationType.electrum;
287 } else {
288 derivationInfo.derivationPath = segwit_path;
289 derivationInfo.derivationType = DerivationType.bip39;
290 }
291 }
292 await derivationInfo.save();
293
294 Uint8List? seedBytes = null;
295 final mnemonic = keysData.mnemonic;
296 final passphrase = keysData.passphrase;
297
298 if (mnemonic != null) {
299 switch (derivationInfo.derivationType) {
300 case DerivationType.electrum:
301 seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
302 break;
303 case DerivationType.bip39:
304 default:
305 seedBytes = await bip39.mnemonicToSeed(
306 mnemonic,
307 passphrase: passphrase ?? '',
308 );
309 break;
310 }
311 }
312
313 return BitcoinWallet(
314 mnemonic: mnemonic,
315 xpub: keysData.xPub != null ? convertZpubToXpub(keysData.xPub!) : null,
316 password: password,
317 passphrase: passphrase,
318 walletInfo: walletInfo,
319 derivationInfo: derivationInfo,
320 unspentCoinsInfo: unspentCoinsInfo,
321 initialAddresses: snp?.addresses,
322 initialSilentAddresses: snp?.silentAddresses,
323 initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
324 initialBalance: snp?.balance,
325 initialLightningBalance: snp?.lightningBalance,
326 encryptionFileUtils: encryptionFileUtils,
327 seedBytes: seedBytes,
328 initialRegularAddressIndex: snp?.regularAddressIndex,
329 initialChangeAddressIndex: snp?.changeAddressIndex,
330 addressPageType: snp?.addressPageType,
331 networkParam: network,
332 alwaysScan: snp?.alwaysScan,
333 useLightning: snp?.useLightning,
334 cachedLightningAddress: snp?.cachedLightningAddress,
335 payjoinBox: payjoinBox,
336 );
337 }
338
339 @override
340 Future<void> close({bool shouldCleanup = false}) async {
341 payjoinManager.cleanupSessions();
342 await lightningWallet?.close();
343 super.close(shouldCleanup: shouldCleanup);
344 }
345
346 @override
347 Future<ElectrumBalance> fetchBalances() async {
348 final balance = await super.fetchBalances();
349 if (!isLightningInitialized || lightningWallet == null) {
350 return balance;
351 }
352
353 try {
354 final lBalance = await lightningWallet!.getBalance();
355
356 this.balance[CryptoCurrency.btcln] = ElectrumBalance(
357 confirmed: lBalance,
358 unconfirmed: Money.zero(CryptoCurrency.btcln),
359 frozen: Money.zero(CryptoCurrency.btcln));
360 } catch (e) {
361 printV("Error fetching lightning balance: $e");
362 }
363
364 return ElectrumBalance(
365 confirmed: balance.confirmed,
366 unconfirmed: balance.unconfirmed,
367 frozen: balance.frozen ?? Money.zero(currency),
368 );
369 }
370
371 @override
372 @action
373 Future<void> subscribeForUpdates() async {
374 if (isLightningInitialized && lightningWallet != null) {
375 lightningWallet!.setEventListener(
376 onTransactionEvent: (tx) async {
377 if (transactionHistory.transactions[tx.id]?.isPending != tx.isPending) {
378 transactionHistory.addOne(tx);
379 await transactionHistory.save();
380 await fetchBalances();
381 }
382 },
383 onCreateDepositTransactionEvent: (txs) async {
384 if (txs.isNotEmpty) {
385 transactionHistory.addMany(txs);
386 await transactionHistory.save();
387 }
388 },
389 onUpdateDepositTransactionEvent: (txs) async {
390 if (txs.isNotEmpty) {
391 txs.forEach((tx) => transactionHistory.transactions.remove(tx.id));
392 await transactionHistory.save();
393 }
394 },
395 onBalanceChangedEvent: fetchBalances,
396 );
397 }
398
399 return super.subscribeForUpdates();
400 }
401
402 @override
403 Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
404 if (lightningWallet != null) {
405 final existingTx = transactionHistory.transactions.values
406 .where((e) => (e.additionalInfo["isLightning"] as bool?) == true)
407 .lastOrNull;
408
409 lightningWallet!.getTransactionHistory(fromDate: existingTx?.date).then((lnHistory) async {
410 transactionHistory.addMany(lnHistory);
411 await transactionHistory.save();
412 }).onError((_, __) {});
413 }
414
415 return super.fetchTransactions();
416 }
417
418 LightningWallet? lightningWallet;
419
420 late final PayjoinManager payjoinManager;
421
422 @override
423 bool get hasPayjoinSupport => keys.privateKey.isNotEmpty;
424
425 @override
426 bool get hasLightningSupport => lightningWallet?.sdk != null;
427
428 bool get isPayjoinAvailable => unspentCoinsInfo.values
429 .where((element) => element.walletId == id && element.isSending && !element.isFrozen)
430 .isNotEmpty;
431
432 Future<PsbtV2> buildPsbt({
433 required List<BitcoinBaseOutput> outputs,
434 required List<OutputInfo> cwOutputs,
435 required BigInt fee,
436 required BasedUtxoNetwork network,
437 required List<UtxoWithAddress> utxos,
438 required Map<String, PublicKeyWithDerivationPath> publicKeys,
439 required Uint8List masterFingerprint,
440 String? memo,
441 bool enableRBF = false,
442 BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
443 BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
444 }) async {
445 final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
446 for (final utxo in utxos) {
447 final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
448 final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
449
450 psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
451 utxo: utxo.utxo,
452 rawTx: rawTx,
453 ownerDetails: utxo.ownerDetails,
454 ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
455 ownerMasterFingerprint: masterFingerprint,
456 ownerPublicKey: publicKeyAndDerivationPath.publicKey,
457 ));
458 }
459
460 final locktime = antiFeeSnipingLocktime(
461 chainTip: await getCurrentChainTip(),
462 synced: syncStatus is SyncedSyncStatus,
463 );
464
465 return PSBTTransactionBuild(
466 inputs: psbtReadyInputs,
467 outputs: outputs,
468 enableRBF: enableRBF,
469 cwOutputs: cwOutputs,
470 locktime: locktime)
471 .psbt;
472 }
473
474 @override
475 Future<BtcTransaction> buildHardwareWalletTransaction({
476 required List<BitcoinBaseOutput> outputs,
477 required BigInt fee,
478 required BasedUtxoNetwork network,
479 required List<UtxoWithAddress> utxos,
480 required List<OutputInfo> cwOutputs,
481 required Map<String, PublicKeyWithDerivationPath> publicKeys,
482 String? memo,
483 bool enableRBF = false,
484 BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
485 BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
486 }) async {
487 final masterFingerprint =
488 await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
489
490 final orderedOutputs = orderOutputs(outputs, outputOrdering);
491
492 final psbt = await buildPsbt(
493 outputs: orderedOutputs,
494 fee: fee,
495 network: network,
496 utxos: utxos,
497 cwOutputs: cwOutputs,
498 publicKeys: publicKeys,
499 masterFingerprint: masterFingerprint,
500 memo: memo,
501 enableRBF: enableRBF,
502 inputOrdering: inputOrdering,
503 // Already applied above; don't reorder again.
504 outputOrdering: BitcoinOrdering.none,
505 );
506
507 final psbtStr = base64Encode(psbt.serialize());
508 if (hardwareWalletService is BitcoinLedgerService && derivationInfo.derivationPath != null) {
509 (hardwareWalletService as BitcoinLedgerService)
510 .setAccountDerivationPath(derivationInfo.derivationPath!);
511 }
512
513 final rawHex = await hardwareWalletService!.signTransaction(transaction: psbtStr);
514 return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
515 }
516
517 @override
518 Future<PendingTransaction> createTransaction(Object credentials) async {
519 credentials = credentials as BitcoinTransactionCredentials;
520 final lnAddr = credentials.outputs.first.isParsedAddress
521 ? credentials.outputs.first.extractedAddress!
522 : credentials.outputs.first.address;
523
524 final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
525 if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
526 isLNCompatible == true) {
527 Money amount;
528 if (credentials.outputs.first.sendAll) {
529 amount = await lightningWallet!.getBalance();
530 } else {
531 amount = credentials.outputs.first.cryptoAmount;
532 }
533
534 return lightningWallet!.createTransaction(
535 lnAddr,
536 amount.amount > BigInt.zero ? amount.amount : null,
537 credentials.priority,
538 credentials.outputs.first.sendAll,
539 );
540 }
541
542 final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
543
544 final payjoinUri = credentials.payjoinUri;
545 if (payjoinUri == null && !tx.shouldCommitUR()) return tx;
546
547 final transaction = await buildPsbt(
548 utxos: tx.utxos,
549 outputs: tx.outputs
550 .map((e) => BitcoinOutput(
551 address: addressFromScript(e.scriptPubKey),
552 value: e.amount,
553 isSilentPayment: e.isSilentPayment,
554 isChange: e.isChange,
555 ))
556 .toList(),
557 cwOutputs: credentials.outputs,
558 fee: tx.fee.amount,
559 network: network,
560 memo: credentials.outputs.first.memo,
561 outputOrdering: BitcoinOrdering.none,
562 enableRBF: true,
563 publicKeys: tx.publicKeys!,
564 masterFingerprint: Uint8List.fromList([0, 0, 0, 0]));
565
566 if (tx.shouldCommitUR()) {
567 tx.unsignedPsbt = transaction.asPsbtV0();
568 return tx;
569 }
570
571 final originalPsbt =
572 await signPsbt(base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
573
574 tx.commitOverride = () async {
575 final sender =
576 await payjoinManager.initSender(payjoinUri!, originalPsbt, int.parse(tx.feeRate));
577 payjoinManager.spawnNewSender(sender: sender, pjUrl: payjoinUri, amount: tx.amount.amount);
578 };
579
580 return tx;
581 }
582
583 List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
584 .where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
585 .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
586 .toList();
587
588 Future<void> commitPsbt(String finalizedPsbt) {
589 final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
590
591 final btcTx = BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
592
593 return PendingBitcoinTransaction(
594 btcTx,
595 type,
596 electrumClient: electrumClient,
597 amount: Money.zero(currency),
598 fee: Money.zero(currency),
599 feeRate: "",
600 network: network,
601 hasChange: true,
602 isViewOnly: false,
603 ).commit();
604 }
605
606 Future<String> signPsbt(String preProcessedPsbt, List<UtxoWithPrivateKey> utxos) async {
607 final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt));
608
609 await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) {
610 return utxo.utxo.isP2tr()
611 ? key.signTapRoot(
612 txDigest,
613 sighash: sighash,
614 tweak: utxo.utxo.isSilentPayment != true,
615 )
616 : key.signInput(txDigest, sigHash: sighash);
617 }, (txId, vout) async {
618 final txHex = await electrumClient.getTransactionHex(hash: txId);
619 final output = BtcTransaction.fromRaw(txHex).outputs[vout];
620 return TaprootAmountScriptPair(output.amount, output.scriptPubKey);
621 });
622
623 psbt.finalizeV0();
624 return base64Encode(psbt.asPsbtV0());
625 }
626
627 Future<void> commitPsbtUR(List<String> urCodes) async {
628 if (urCodes.isEmpty) throw Exception("No QR code got scanned");
629 bool isUr = urCodes.any((str) {
630 return str.startsWith("ur:psbt/");
631 });
632 if (isUr) {
633 final ur = URDecoder();
634 for (final inp in urCodes) {
635 ur.receivePart(inp);
636 }
637 final result = (ur.result as UR);
638 final cbor = result.cbor;
639 final cborDecoder = CBORDecoder(cbor);
640 final out = cborDecoder.decodeBytes();
641 final bytes = out.$1;
642 final base64psbt = base64Encode(bytes);
643 final psbt = PsbtV2()..deserializeV0(base64Decode(base64psbt));
644
645 // psbt.finalize();
646 final finalized = base64Encode(psbt.serialize());
647 await commitPsbt(finalized);
648 } else {
649 final btcTx = BtcTransaction.fromRaw(urCodes.first);
650
651 return PendingBitcoinTransaction(
652 btcTx,
653 type,
654 electrumClient: electrumClient,
655 amount: Money.zero(currency),
656 fee: Money.zero(currency),
657 feeRate: "",
658 network: network,
659 hasChange: true,
660 isViewOnly: false,
661 ).commit();
662 }
663 }
664
665 @override
666 Future<String> signMessage(String message, {String? address = null}) async {
667 if (walletInfo.isHardwareWallet) {
668 final addressEntry = address != null
669 ? walletAddresses.allAddresses.firstWhere((element) => element.address == address)
670 : null;
671 final index = addressEntry?.index ?? 0;
672 final isChange = addressEntry?.isHidden == true ? 1 : 0;
673 final derivationInfo = await walletInfo.getDerivationInfo();
674 final accountPath = derivationInfo.derivationPath;
675 final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null;
676
677 final signature = await hardwareWalletService!
678 .signMessage(message: ascii.encode(message), derivationPath: derivationPath);
679 return base64Encode(signature);
680 }
681
682 return super.signMessage(message, address: address);
683 }
684
685 @override
686 bool receiveOptionAvailable(ReceivePageOption option) {
687 if(option == BitcoinReceivePageOption.lightning) {
688 return hasLightningSupport;
689 }
690
691 if(option == BitcoinReceivePageOption.silent_payments) {
692 return hasSilentPaymentsScanning;
693 }
694
695 return true;
696 }
697 }