dev
dart 225 lines 6.43 KB
Raw
1 import 'package:bbqrdart/bbqrdart.dart';
2 import 'package:cw_bitcoin/electrum_wallet.dart';
3 import 'package:cw_core/amount/money.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:grpc/grpc.dart';
6 import 'package:cw_bitcoin/exceptions.dart';
7 import 'package:bitcoin_base/bitcoin_base.dart';
8 import 'package:blockchain_utils/blockchain_utils.dart';
9 import 'package:cw_core/pending_transaction.dart';
10 import 'package:cw_bitcoin/electrum.dart';
11 import 'package:cw_bitcoin/electrum_transaction_info.dart';
12 import 'package:cw_core/transaction_direction.dart';
13 import 'package:cw_core/wallet_type.dart';
14 import 'package:cw_mweb/cw_mweb.dart';
15 import 'package:cw_mweb/mwebd.pb.dart';
16 import 'package:ur/cbor_lite.dart';
17 import 'package:ur/ur.dart';
18 import 'package:ur/ur_encoder.dart';
19
20 class PendingBitcoinTransaction with PendingTransaction {
21 PendingBitcoinTransaction(
22 this._tx,
23 this.type, {
24 required this.electrumClient,
25 required this.amount,
26 required this.fee,
27 required this.feeRate,
28 this.network,
29 required this.hasChange,
30 this.isSendAll = false,
31 this.hasTaprootInputs = false,
32 this.isMweb = false,
33 this.utxos = const [],
34 this.derivedOutputs = const [],
35 this.publicKeys,
36 this.commitOverride,
37 this.unsignedPsbt,
38 required this.isViewOnly,
39 }) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
40
41 final WalletType type;
42 final BtcTransaction _tx;
43 final ElectrumClient electrumClient;
44 final String feeRate;
45 final BasedUtxoNetwork? network;
46 final bool isSendAll;
47 final bool hasChange;
48 final bool hasTaprootInputs;
49 final bool isViewOnly;
50 List<UtxoWithAddress> utxos;
51
52 final List<BitcoinOutput> derivedOutputs;
53
54 bool isMweb;
55 String? changeAddressOverride;
56 String? idOverride;
57 String? hexOverride;
58 List<String>? outputAddresses;
59 final Map<String, PublicKeyWithDerivationPath>? publicKeys;
60 Future<void> Function()? commitOverride;
61
62 Uint8List? unsignedPsbt;
63
64 @override
65 final Money amount;
66
67 @override
68 final Money fee;
69
70 @override
71 String get id => idOverride ?? _tx.txId();
72
73 @override
74 String get hex => hexOverride ?? _tx.serialize();
75
76 @override
77 String get amountFormatted => amount.toString();
78
79 @override
80 int? get outputCount => _tx.outputs.length;
81
82 List<TxOutput> get outputs => _tx.outputs;
83
84 bool get hasSilentPayment => _tx.hasSilentPayment;
85
86 List<String?> get stealthAddresses => derivedOutputs
87 .where((out) => !out.isChange)
88 .map((out) => out.isSilentPayment
89 ? out.address.toAddress(network ?? BitcoinNetwork.mainnet)
90 : null)
91 .toList();
92
93 PendingChange? get change {
94 try {
95 final change = _tx.outputs.firstWhere((out) => out.isChange);
96 if (changeAddressOverride != null) {
97 return PendingChange(changeAddressOverride!, change.amount);
98 }
99 return PendingChange(change.scriptPubKey.toAddress(network: network), change.amount);
100 } catch (_) {
101 return null;
102 }
103 }
104
105 final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
106
107 Future<void> _commit() async {
108 int? callId;
109
110 final result = await electrumClient.broadcastTransaction(
111 transactionRaw: hex, network: network, idCallback: (id) => callId = id);
112
113 if (result.isEmpty) {
114 if (callId != null) {
115 final error = electrumClient.getErrorMessage(callId!);
116
117 if (error.contains("dust")) {
118 if (hasChange) {
119 throw BitcoinTransactionCommitFailedDustChange();
120 } else if (!isSendAll) {
121 throw BitcoinTransactionCommitFailedDustOutput();
122 } else {
123 throw BitcoinTransactionCommitFailedDustOutputSendAll();
124 }
125 }
126
127 if (error.contains("bad-txns-vout-negative")) {
128 throw BitcoinTransactionCommitFailedVoutNegative();
129 }
130
131 if (error.contains("non-BIP68-final")) {
132 throw BitcoinTransactionCommitFailedBIP68Final();
133 }
134
135 if (error.contains("min fee not met")) {
136 throw BitcoinTransactionCommitFailedLessThanMin();
137 }
138
139 throw BitcoinTransactionCommitFailed(errorMessage: error);
140 }
141
142 throw BitcoinTransactionCommitFailed();
143 }
144 }
145
146 Future<void> _ltcCommit() async {
147 try {
148 final resp = await CwMweb.broadcast(BroadcastRequest(rawTx: BytesUtils.fromHexString(hex)));
149 idOverride = resp.txid;
150 } on GrpcError catch (e) {
151 throw BitcoinTransactionCommitFailed(errorMessage: e.message);
152 } catch (e) {
153 throw BitcoinTransactionCommitFailed(errorMessage: "Unknown error: ${e.toString()}");
154 }
155 }
156
157 @override
158 Future<void> commit() async {
159 if (commitOverride != null) {
160 return commitOverride?.call();
161 }
162
163 if (isMweb) {
164 await _ltcCommit();
165 } else {
166 await _commit();
167 }
168
169 _listeners.forEach((listener) => listener(transactionInfo()));
170 }
171
172 void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
173 _listeners.add(listener);
174
175 ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(
176 type,
177 id: id,
178 height: 0,
179 amount: amount,
180 direction: TransactionDirection.outgoing,
181 date: DateTime.now(),
182 isPending: true,
183 isReplaced: false,
184 confirmations: 0,
185 inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
186 outputAddresses: outputAddresses,
187 fee: fee,
188 );
189
190 @override
191 bool shouldCommitUR() => isViewOnly;
192
193 @override
194 Future<Map<String, String>> commitUR() {
195 var sourceBytes = unsignedPsbt!;
196 var cborEncoder = CBOREncoder();
197 cborEncoder.encodeBytes(sourceBytes);
198 var ur = UR("psbt", cborEncoder.getBytes());
199 var urLegacy = UR("crypto-psbt", cborEncoder.getBytes());
200 // var ur = UR("psbt", Uint8List.fromList(List.generate(64*1024, (int x) => x % 256)));
201 var encoded = UREncoder(ur, 120);
202 var encodedLegacy = UREncoder(urLegacy, 120);
203 List<String> values = [];
204 List<String> valuesLegacy = [];
205 while (!encoded.isComplete) {
206 values.add(encoded.nextPart());
207 valuesLegacy.add(encodedLegacy.nextPart());
208 }
209
210 final bbqrObj = BBQRPsbt.fromUint8List(sourceBytes);
211 List<String> bbqr = [
212 bbqrObj.asString(),
213 ];
214 while (!bbqrObj.isDone) {
215 bbqrObj.next();
216 bbqr.add(bbqrObj.asString());
217 }
218
219 return Future.value({
220 "Cupcake bcur": values.join("\n"),
221 "ColdCard bbqr": bbqr.join("\n"),
222 if (kDebugMode) "SeedSigner bcur legacy": valuesLegacy.join("\n"),
223 });
224 }
225 }