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/address_from_output.dart';
7
import 'package:cw_bitcoin/bitcoin_address_record.dart';
8
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
8
-import 'package:cw_bitcoin/psbt_transaction_builder.dart';
9
-import 'package:cw_core/encryption_file_utils.dart';
10
-import 'package:cw_bitcoin/electrum_derivations.dart';
9
+import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
10
import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart';
11
import 'package:cw_bitcoin/electrum_balance.dart';
12
+import 'package:cw_bitcoin/electrum_derivations.dart';
13
import 'package:cw_bitcoin/electrum_wallet.dart';
14
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
15
+import 'package:cw_bitcoin/payjoin/manager.dart';
16
+import 'package:cw_bitcoin/payjoin/storage.dart';
17
+import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
18
+import 'package:cw_bitcoin/psbt/signer.dart';
19
+import 'package:cw_bitcoin/psbt/transaction_builder.dart';
20
+import 'package:cw_bitcoin/psbt/v0_deserialize.dart';
21
+import 'package:cw_bitcoin/psbt/v0_finalizer.dart';
22
import 'package:cw_core/crypto_currency.dart';
23
+import 'package:cw_core/encryption_file_utils.dart';
24
+import 'package:cw_core/payjoin_session.dart';
25
+import 'package:cw_core/pending_transaction.dart';
26
import 'package:cw_core/unspent_coins_info.dart';
27
import 'package:cw_core/wallet_info.dart';
28
import 'package:cw_core/wallet_keys_file.dart';
29
import 'package:flutter/foundation.dart';
30
import 'package:hive/hive.dart';
31
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
32
+import 'package:ledger_bitcoin/psbt.dart';
33
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
34
import 'package:mobx/mobx.dart';
35
42
required String password,
43
required WalletInfo walletInfo,
44
required Box<UnspentCoinsInfo> unspentCoinsInfo,
45
+ required Box<PayjoinSession> payjoinBox,
46
required EncryptionFileUtils encryptionFileUtils,
47
Uint8List? seedBytes,
48
String? mnemonic,
83
// String derivationPath = walletInfo.derivationInfo!.derivationPath!;
84
// String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
85
// final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
74
- walletAddresses = BitcoinWalletAddresses(
75
- walletInfo,
76
- initialAddresses: initialAddresses,
77
- initialRegularAddressIndex: initialRegularAddressIndex,
78
- initialChangeAddressIndex: initialChangeAddressIndex,
79
- initialSilentAddresses: initialSilentAddresses,
80
- initialSilentAddressIndex: initialSilentAddressIndex,
81
- mainHd: hd,
82
- sideHd: accountHD.childKey(Bip32KeyIndex(1)),
83
- network: networkParam ?? network,
84
- masterHd:
85
- seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
86
- isHardwareWallet: walletInfo.isHardwareWallet,
87
- );
86
+
87
+ payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
88
+ walletAddresses = BitcoinWalletAddresses(walletInfo,
89
+ initialAddresses: initialAddresses,
90
+ initialRegularAddressIndex: initialRegularAddressIndex,
91
+ initialChangeAddressIndex: initialChangeAddressIndex,
92
+ initialSilentAddresses: initialSilentAddresses,
93
+ initialSilentAddressIndex: initialSilentAddressIndex,
94
+ mainHd: hd,
95
+ sideHd: accountHD.childKey(Bip32KeyIndex(1)),
96
+ network: networkParam ?? network,
97
+ masterHd:
98
+ seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null,
99
+ isHardwareWallet: walletInfo.isHardwareWallet,
100
+ payjoinManager: payjoinManager);
101
102
autorun((_) {
103
this.walletAddresses.isEnabledAutoGenerateSubaddress =
113
required String password,
114
required WalletInfo walletInfo,
115
required Box<UnspentCoinsInfo> unspentCoinsInfo,
116
+ required Box<PayjoinSession> payjoinBox,
117
required EncryptionFileUtils encryptionFileUtils,
118
String? passphrase,
119
String? addressPageType,
136
break;
137
case DerivationType.electrum:
138
default:
125
- seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
139
+ seedBytes =
140
+ await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
141
break;
142
}
143
+
144
return BitcoinWallet(
145
mnemonic: mnemonic,
146
passphrase: passphrase ?? "",
157
initialChangeAddressIndex: initialChangeAddressIndex,
158
addressPageType: addressPageType,
159
networkParam: network,
160
+ payjoinBox: payjoinBox,
161
);
162
}
163
165
required String name,
166
required WalletInfo walletInfo,
167
required Box<UnspentCoinsInfo> unspentCoinsInfo,
168
+ required Box<PayjoinSession> payjoinBox,
169
required String password,
170
required EncryptionFileUtils encryptionFileUtils,
171
required bool alwaysScan,
222
if (mnemonic != null) {
223
switch (walletInfo.derivationInfo!.derivationType) {
224
case DerivationType.electrum:
207
- seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
225
+ seedBytes =
226
+ await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? "");
227
break;
228
case DerivationType.bip39:
229
default:
236
}
237
238
return BitcoinWallet(
220
- mnemonic: mnemonic,
221
- xpub: keysData.xPub,
222
- password: password,
223
- passphrase: passphrase,
224
- walletInfo: walletInfo,
225
- unspentCoinsInfo: unspentCoinsInfo,
226
- initialAddresses: snp?.addresses,
227
- initialSilentAddresses: snp?.silentAddresses,
228
- initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
229
- initialBalance: snp?.balance,
230
- encryptionFileUtils: encryptionFileUtils,
231
- seedBytes: seedBytes,
232
- initialRegularAddressIndex: snp?.regularAddressIndex,
233
- initialChangeAddressIndex: snp?.changeAddressIndex,
234
- addressPageType: snp?.addressPageType,
235
- networkParam: network,
236
- alwaysScan: alwaysScan,
237
- );
239
+ mnemonic: mnemonic,
240
+ xpub: keysData.xPub,
241
+ password: password,
242
+ passphrase: passphrase,
243
+ walletInfo: walletInfo,
244
+ unspentCoinsInfo: unspentCoinsInfo,
245
+ initialAddresses: snp?.addresses,
246
+ initialSilentAddresses: snp?.silentAddresses,
247
+ initialSilentAddressIndex: snp?.silentAddressIndex ?? 0,
248
+ initialBalance: snp?.balance,
249
+ encryptionFileUtils: encryptionFileUtils,
250
+ seedBytes: seedBytes,
251
+ initialRegularAddressIndex: snp?.regularAddressIndex,
252
+ initialChangeAddressIndex: snp?.changeAddressIndex,
253
+ addressPageType: snp?.addressPageType,
254
+ networkParam: network,
255
+ alwaysScan: alwaysScan,
256
+ payjoinBox: payjoinBox);
257
}
258
259
LedgerConnection? _ledgerConnection;
266
derivationPath: walletInfo.derivationInfo!.derivationPath!);
267
}
268
250
- @override
251
- Future<BtcTransaction> buildHardwareWalletTransaction({
269
+ late final PayjoinManager payjoinManager;
270
+
271
+ bool get isPayjoinAvailable => unspentCoinsInfo.values
272
+ .where((element) =>
273
+ element.walletId == id && element.isSending && !element.isFrozen)
274
+ .isNotEmpty;
275
+
276
+ Future<PsbtV2> buildPsbt({
277
required List<BitcoinBaseOutput> outputs,
278
required BigInt fee,
279
required BasedUtxoNetwork network,
280
required List<UtxoWithAddress> utxos,
281
required Map<String, PublicKeyWithDerivationPath> publicKeys,
282
+ required Uint8List masterFingerprint,
283
String? memo,
284
bool enableRBF = false,
285
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
286
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
287
}) async {
262
- final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
263
-
288
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
289
for (final utxo in utxos) {
290
final rawTx =
302
));
303
}
304
281
- final psbt = PSBTTransactionBuild(
282
- inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
305
+ return PSBTTransactionBuild(
306
+ inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF)
307
+ .psbt;
308
+ }
309
+
310
+ @override
311
+ Future<BtcTransaction> buildHardwareWalletTransaction({
312
+ required List<BitcoinBaseOutput> outputs,
313
+ required BigInt fee,
314
+ required BasedUtxoNetwork network,
315
+ required List<UtxoWithAddress> utxos,
316
+ required Map<String, PublicKeyWithDerivationPath> publicKeys,
317
+ String? memo,
318
+ bool enableRBF = false,
319
+ BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
320
+ BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
321
+ }) async {
322
+ final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint();
323
+
324
+ final psbt = await buildPsbt(
325
+ outputs: outputs,
326
+ fee: fee,
327
+ network: network,
328
+ utxos: utxos,
329
+ publicKeys: publicKeys,
330
+ masterFingerprint: masterFingerprint,
331
+ memo: memo,
332
+ enableRBF: enableRBF,
333
+ inputOrdering: inputOrdering,
334
+ outputOrdering: outputOrdering,
335
+ );
336
284
- final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt);
337
+ final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt);
338
return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex));
339
}
340
341
+ @override
342
+ Future<PendingTransaction> createTransaction(Object credentials) async {
343
+ credentials = credentials as BitcoinTransactionCredentials;
344
+
345
+ final tx = (await super.createTransaction(credentials))
346
+ as PendingBitcoinTransaction;
347
+
348
+ final payjoinUri = credentials.payjoinUri;
349
+ if (payjoinUri == null) return tx;
350
+
351
+ final transaction = await buildPsbt(
352
+ utxos: tx.utxos,
353
+ outputs: tx.outputs
354
+ .map((e) => BitcoinOutput(
355
+ address: addressFromScript(e.scriptPubKey),
356
+ value: e.amount,
357
+ isSilentPayment: e.isSilentPayment,
358
+ isChange: e.isChange,
359
+ ))
360
+ .toList(),
361
+ fee: BigInt.from(tx.fee),
362
+ network: network,
363
+ memo: credentials.outputs.first.memo,
364
+ outputOrdering: BitcoinOrdering.none,
365
+ enableRBF: true,
366
+ publicKeys: tx.publicKeys!,
367
+ masterFingerprint: Uint8List(0));
368
+
369
+ final originalPsbt = await signPsbt(
370
+ base64.encode(transaction.asPsbtV0()), getUtxoWithPrivateKeys());
371
+
372
+ tx.commitOverride = () async {
373
+ final sender = await payjoinManager.initSender(
374
+ payjoinUri, originalPsbt, int.parse(tx.feeRate));
375
+ payjoinManager.spawnNewSender(
376
+ sender: sender, pjUrl: payjoinUri, amount: BigInt.from(tx.amount));
377
+ };
378
+
379
+ return tx;
380
+ }
381
+
382
+ List<UtxoWithPrivateKey> getUtxoWithPrivateKeys() => unspentCoins
383
+ .where((e) => (e.isSending && !e.isFrozen))
384
+ .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
385
+ .toList();
386
+
387
+ Future<void> commitPsbt(String finalizedPsbt) {
388
+ final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
389
+
390
+ final btcTx =
391
+ BtcTransaction.fromRaw(BytesUtils.toHexString(psbt.extract()));
392
+
393
+ return PendingBitcoinTransaction(
394
+ btcTx,
395
+ type,
396
+ electrumClient: electrumClient,
397
+ amount: 0,
398
+ fee: 0,
399
+ feeRate: "",
400
+ network: network,
401
+ hasChange: true,
402
+ ).commit();
403
+ }
404
+
405
+ Future<String> signPsbt(
406
+ String preProcessedPsbt, List<UtxoWithPrivateKey> utxos) async {
407
+ final psbt = PsbtV2()..deserializeV0(base64Decode(preProcessedPsbt));
408
+
409
+ await psbt.signWithUTXO(utxos, (txDigest, utxo, key, sighash) {
410
+ return utxo.utxo.isP2tr()
411
+ ? key.signTapRoot(
412
+ txDigest,
413
+ sighash: sighash,
414
+ tweak: utxo.utxo.isSilentPayment != true,
415
+ )
416
+ : key.signInput(txDigest, sigHash: sighash);
417
+ }, (txId, vout) async {
418
+ final txHex = await electrumClient.getTransactionHex(hash: txId);
419
+ final output = BtcTransaction.fromRaw(txHex).outputs[vout];
420
+ return TaprootAmountScriptPair(output.amount, output.scriptPubKey);
421
+ });
422
+
423
+ psbt.finalizeV0();
424
+ return base64Encode(psbt.asPsbtV0());
425
+ }
426
+
427
@override
428
Future<String> signMessage(String message, {String? address = null}) async {
429
if (walletInfo.isHardwareWallet) {