3
import 'dart:io';
4
import 'dart:math';
5
6
+import 'package:bitcoin_base/bitcoin_base.dart';
7
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
8
+import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
9
import 'package:collection/collection.dart';
8
-import 'package:cw_bitcoin/address_to_output_script.dart';
10
import 'package:cw_bitcoin/bitcoin_address_record.dart';
11
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
12
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
19
import 'package:cw_bitcoin/electrum_transaction_history.dart';
20
import 'package:cw_bitcoin/electrum_transaction_info.dart';
21
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
22
+import 'package:cw_bitcoin/litecoin_network.dart';
23
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
24
import 'package:cw_bitcoin/script_hash.dart';
25
import 'package:cw_bitcoin/utils.dart';
39
import 'package:hive/hive.dart';
40
import 'package:mobx/mobx.dart';
41
import 'package:rxdart/subjects.dart';
42
+import 'package:http/http.dart' as http;
43
44
part 'electrum_wallet.g.dart';
45
76
}
77
: {}),
78
this.unspentCoinsInfo = unspentCoinsInfo,
79
+ this.network = networkType == bitcoin.bitcoin
80
+ ? BitcoinNetwork.mainnet
81
+ : networkType == litecoinNetwork
82
+ ? LitecoinNetwork.mainnet
83
+ : BitcoinNetwork.testnet,
84
+ this.isTestnet = networkType == bitcoin.testnet,
85
super(walletInfo) {
86
this.electrumClient = electrumClient ?? ElectrumClient();
87
this.walletInfo = walletInfo;
115
@observable
116
SyncStatus syncStatus;
117
109
- List<String> get scriptHashes => walletAddresses.addresses
110
- .map((addr) => scriptHash(addr.address, networkType: networkType))
118
+ List<String> get scriptHashes => walletAddresses.addressesByReceiveType
119
+ .map((addr) => scriptHash(addr.address, network: network))
120
.toList();
121
113
- List<String> get publicScriptHashes => walletAddresses.addresses
122
+ List<String> get publicScriptHashes => walletAddresses.allAddresses
123
.where((addr) => !addr.isHidden)
115
- .map((addr) => scriptHash(addr.address, networkType: networkType))
124
+ .map((addr) => scriptHash(addr.address, network: network))
125
.toList();
126
127
String get xpub => hd.base58!;
130
String get seed => mnemonic;
131
132
bitcoin.NetworkType networkType;
133
+ BasedUtxoNetwork network;
134
+
135
+ @override
136
+ bool? isTestnet;
137
138
@override
139
BitcoinWalletKeys get keys =>
158
Future<void> startSync() async {
159
try {
160
syncStatus = AttemptingSyncStatus();
148
- await walletAddresses.discoverAddresses();
161
await updateTransactions();
162
_subscribeForUpdates();
163
await updateUnspent();
164
await updateBalance();
153
- _feeRates = await electrumClient.feeRates();
165
+ _feeRates = await electrumClient.feeRates(network: network);
166
167
Timer.periodic(
168
const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
193
}
194
}
195
184
- @override
185
- Future<PendingTransaction> createTransaction(Object credentials) async {
186
- const minAmount = 546;
187
- final transactionCredentials = credentials as BitcoinTransactionCredentials;
188
- final inputs = <BitcoinUnspent>[];
189
- final outputs = transactionCredentials.outputs;
190
- final hasMultiDestination = outputs.length > 1;
196
+ Future<EstimatedTxResult> _estimateTxFeeAndInputsToUse(
197
+ int credentialsAmount,
198
+ bool sendAll,
199
+ List<BitcoinBaseAddress> outputAddresses,
200
+ List<BitcoinOutput> outputs,
201
+ BitcoinTransactionCredentials transactionCredentials,
202
+ {int? inputsCount}) async {
203
+ final utxos = <UtxoWithAddress>[];
204
+ List<ECPrivate> privateKeys = [];
205
+
206
+ var leftAmount = credentialsAmount;
207
var allInputsAmount = 0;
208
193
- if (unspentCoins.isEmpty) {
194
- await updateUnspent();
195
- }
209
+ for (int i = 0; i < unspentCoins.length; i++) {
210
+ final utx = unspentCoins[i];
211
197
- for (final utx in unspentCoins) {
212
if (utx.isSending) {
213
allInputsAmount += utx.value;
200
- inputs.add(utx);
214
+ leftAmount = leftAmount - utx.value;
215
+
216
+ final address = _addressTypeFromStr(utx.address, network);
217
+ final privkey = generateECPrivate(
218
+ hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
219
+ index: utx.bitcoinAddressRecord.index,
220
+ network: network);
221
+
222
+ privateKeys.add(privkey);
223
+
224
+ utxos.add(
225
+ UtxoWithAddress(
226
+ utxo: BitcoinUtxo(
227
+ txHash: utx.hash,
228
+ value: BigInt.from(utx.value),
229
+ vout: utx.vout,
230
+ scriptType: _getScriptType(address),
231
+ ),
232
+ ownerDetails:
233
+ UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
234
+ ),
235
+ );
236
+
237
+ bool amountIsAcquired = !sendAll && leftAmount <= 0;
238
+ if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
239
+ break;
240
+ }
241
}
242
}
243
204
- if (inputs.isEmpty) {
244
+ if (utxos.isEmpty) {
245
throw BitcoinTransactionNoInputsException();
246
}
247
208
- final allAmountFee = transactionCredentials.feeRate != null
209
- ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
210
- : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
211
-
212
- final allAmount = allInputsAmount - allAmountFee;
213
-
214
- var credentialsAmount = 0;
215
- var amount = 0;
216
- var fee = 0;
217
-
218
- if (hasMultiDestination) {
219
- if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
220
- throw BitcoinTransactionWrongBalanceException(currency);
221
- }
222
-
223
- credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
248
+ var changeValue = allInputsAmount - credentialsAmount;
249
225
- if (allAmount - credentialsAmount < minAmount) {
226
- throw BitcoinTransactionWrongBalanceException(currency);
250
+ if (!sendAll) {
251
+ if (changeValue > 0) {
252
+ final changeAddress = await walletAddresses.getChangeAddress();
253
+ final address = _addressTypeFromStr(changeAddress, network);
254
+ outputAddresses.add(address);
255
+ outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue)));
256
}
257
+ }
258
229
- amount = credentialsAmount;
259
+ final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
260
+ utxos: utxos, outputs: outputs, network: network);
261
231
- if (transactionCredentials.feeRate != null) {
232
- fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
233
- outputsCount: outputs.length + 1);
234
- } else {
235
- fee = calculateEstimatedFee(transactionCredentials.priority, amount,
236
- outputsCount: outputs.length + 1);
237
- }
238
- } else {
239
- final output = outputs.first;
240
- credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
262
+ final fee = transactionCredentials.feeRate != null
263
+ ? feeAmountWithFeeRate(transactionCredentials.feeRate!, 0, 0, size: estimatedSize)
264
+ : feeAmountForPriority(transactionCredentials.priority!, 0, 0, size: estimatedSize);
265
242
- if (credentialsAmount > allAmount) {
243
- throw BitcoinTransactionWrongBalanceException(currency);
244
- }
266
+ if (fee == 0) {
267
+ throw BitcoinTransactionWrongBalanceException(currency);
268
+ }
269
246
- amount = output.sendAll || allAmount - credentialsAmount < minAmount
247
- ? allAmount
248
- : credentialsAmount;
270
+ var amount = credentialsAmount;
271
250
- if (output.sendAll || amount == allAmount) {
251
- fee = allAmountFee;
252
- } else if (transactionCredentials.feeRate != null) {
253
- fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
254
- } else {
255
- fee = calculateEstimatedFee(transactionCredentials.priority, amount);
272
+ final lastOutput = outputs.last;
273
+ if (!sendAll) {
274
+ if (changeValue > fee) {
275
+ // Here, lastOutput is change, deduct the fee from it
276
+ outputs[outputs.length - 1] =
277
+ BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
278
}
257
- }
258
-
259
- if (fee == 0) {
260
- throw BitcoinTransactionWrongBalanceException(currency);
279
+ } else {
280
+ // Here, if sendAll, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount for change
281
+ amount = allInputsAmount - fee;
282
+ outputs[outputs.length - 1] =
283
+ BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount));
284
}
285
286
final totalAmount = amount + fee;
287
265
- if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
288
+ if (totalAmount > balance[currency]!.confirmed) {
289
throw BitcoinTransactionWrongBalanceException(currency);
290
}
291
269
- final txb = bitcoin.TransactionBuilder(network: networkType);
270
- final changeAddress = await walletAddresses.getChangeAddress();
271
- var leftAmount = totalAmount;
272
- var totalInputAmount = 0;
273
-
274
- inputs.clear();
275
-
276
- for (final utx in unspentCoins) {
277
- if (utx.isSending) {
278
- leftAmount = leftAmount - utx.value;
279
- totalInputAmount += utx.value;
280
- inputs.add(utx);
281
-
282
- if (leftAmount <= 0) {
283
- break;
292
+ if (totalAmount > allInputsAmount) {
293
+ if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) {
294
+ throw BitcoinTransactionWrongBalanceException(currency);
295
+ } else {
296
+ if (changeValue > fee) {
297
+ outputAddresses.removeLast();
298
+ outputs.removeLast();
299
}
300
+
301
+ return _estimateTxFeeAndInputsToUse(
302
+ credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials,
303
+ inputsCount: utxos.length + 1);
304
}
305
}
306
288
- if (inputs.isEmpty) {
289
- throw BitcoinTransactionNoInputsException();
290
- }
307
+ return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount);
308
+ }
309
292
- if (amount <= 0 || totalInputAmount < totalAmount) {
293
- throw BitcoinTransactionWrongBalanceException(currency);
294
- }
310
+ @override
311
+ Future<PendingTransaction> createTransaction(Object credentials) async {
312
+ try {
313
+ final outputs = <BitcoinOutput>[];
314
+ final outputAddresses = <BitcoinBaseAddress>[];
315
+ final transactionCredentials = credentials as BitcoinTransactionCredentials;
316
+ final hasMultiDestination = transactionCredentials.outputs.length > 1;
317
+ final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
318
296
- txb.setVersion(1);
297
- inputs.forEach((input) {
298
- if (input.isP2wpkh) {
299
- final p2wpkh = bitcoin
300
- .P2WPKH(
301
- data: generatePaymentData(
302
- hd: input.bitcoinAddressRecord.isHidden
303
- ? walletAddresses.sideHd
304
- : walletAddresses.mainHd,
305
- index: input.bitcoinAddressRecord.index),
306
- network: networkType)
307
- .data;
308
-
309
- txb.addInput(input.hash, input.vout, null, p2wpkh.output);
310
- } else {
311
- txb.addInput(input.hash, input.vout);
312
- }
313
- });
319
+ var credentialsAmount = 0;
320
315
- outputs.forEach((item) {
316
- final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
317
- final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
318
- txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!);
319
- });
321
+ for (final out in transactionCredentials.outputs) {
322
+ final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
323
+ final address = _addressTypeFromStr(outputAddress, network);
324
321
- final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1);
322
- var feeAmount = 0;
325
+ outputAddresses.add(address);
326
324
- if (transactionCredentials.feeRate != null) {
325
- feeAmount = transactionCredentials.feeRate! * estimatedSize;
326
- } else {
327
- feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
328
- }
327
+ if (hasMultiDestination) {
328
+ if (out.sendAll || out.formattedCryptoAmount! <= 0) {
329
+ throw BitcoinTransactionWrongBalanceException(currency);
330
+ }
331
330
- final changeValue = totalInputAmount - amount - feeAmount;
332
+ final outputAmount = out.formattedCryptoAmount!;
333
+ credentialsAmount += outputAmount;
334
332
- if (changeValue > minAmount) {
333
- txb.addOutput(changeAddress, changeValue);
334
- }
335
+ outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
336
+ } else {
337
+ if (!sendAll) {
338
+ final outputAmount = out.formattedCryptoAmount!;
339
+ credentialsAmount += outputAmount;
340
+ outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
341
+ } else {
342
+ // The value will be changed after estimating the Tx size and deducting the fee from the total
343
+ outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
344
+ }
345
+ }
346
+ }
347
336
- for (var i = 0; i < inputs.length; i++) {
337
- final input = inputs[i];
338
- final keyPair = generateKeyPair(
339
- hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
340
- index: input.bitcoinAddressRecord.index,
341
- network: networkType);
342
- final witnessValue = input.isP2wpkh ? input.value : null;
348
+ final estimatedTx = await _estimateTxFeeAndInputsToUse(
349
+ credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials);
350
344
- txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
345
- }
351
+ final txb = BitcoinTransactionBuilder(
352
+ utxos: estimatedTx.utxos,
353
+ outputs: outputs,
354
+ fee: BigInt.from(estimatedTx.fee),
355
+ network: network);
356
+
357
+ final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
358
+ final key = estimatedTx.privateKeys
359
+ .firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
360
347
- return PendingBitcoinTransaction(txb.build(), type,
348
- electrumClient: electrumClient, amount: amount, fee: fee)
349
- ..addListener((transaction) async {
350
- transactionHistory.addOne(transaction);
351
- await updateBalance();
361
+ if (key == null) {
362
+ throw Exception("Cannot find private key");
363
+ }
364
+
365
+ if (utxo.utxo.isP2tr()) {
366
+ return key.signTapRoot(txDigest, sighash: sighash);
367
+ } else {
368
+ return key.signInput(txDigest, sigHash: sighash);
369
+ }
370
});
371
+
372
+ return PendingBitcoinTransaction(transaction, type,
373
+ electrumClient: electrumClient,
374
+ amount: estimatedTx.amount,
375
+ fee: estimatedTx.fee,
376
+ network: network)
377
+ ..addListener((transaction) async {
378
+ transactionHistory.addOne(transaction);
379
+ await updateBalance();
380
+ });
381
+ } catch (e) {
382
+ throw e;
383
+ }
384
}
385
386
String toJSON() => json.encode({
387
'mnemonic': mnemonic,
357
- 'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
358
- 'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
359
- 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
360
- 'balance': balance[currency]?.toJSON()
388
+ 'account_index': walletAddresses.currentReceiveAddressIndexByType,
389
+ 'change_address_index': walletAddresses.currentChangeAddressIndexByType,
390
+ 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
391
+ 'address_page_type': walletInfo.addressPageType == null
392
+ ? SegwitAddresType.p2wpkh.toString()
393
+ : walletInfo.addressPageType.toString(),
394
+ 'balance': balance[currency]?.toJSON(),
395
+ 'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
396
});
397
398
int feeRate(TransactionPriority priority) {
407
}
408
}
409
375
- int feeAmountForPriority(
376
- BitcoinTransactionPriority priority, int inputsCount, int outputsCount) =>
377
- feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
410
+ int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
411
+ {int? size}) =>
412
+ feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
413
379
- int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) =>
380
- feeRate * estimatedTransactionSize(inputsCount, outputsCount);
414
+ int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
415
+ feeRate * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
416
417
@override
383
- int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount}) {
418
+ int calculateEstimatedFee(TransactionPriority? priority, int? amount,
419
+ {int? outputsCount, int? size}) {
420
if (priority is BitcoinTransactionPriority) {
421
return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount,
386
- outputsCount: outputsCount);
422
+ outputsCount: outputsCount, size: size);
423
}
424
425
return 0;
426
}
427
392
- int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) {
428
+ int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
429
+ if (size != null) {
430
+ return feeAmountWithFeeRate(feeRate, 0, 0, size: size);
431
+ }
432
+
433
int inputsCount = 0;
434
435
if (amount != null) {
497
await transactionHistory.changePassword(password);
498
}
499
460
- bitcoin.ECPair keyPairFor({required int index}) =>
461
- generateKeyPair(hd: hd, index: index, network: networkType);
462
-
500
@override
501
Future<void> rescan({required int height}) async => throw UnimplementedError();
502
510
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
511
512
Future<void> updateUnspent() async {
476
- final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
477
- .getListUnspentWithAddress(address.address, networkType)
478
- .then((unspent) => unspent.map((unspent) {
513
+ List<BitcoinUnspent> updatedUnspentCoins = [];
514
+
515
+ final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
516
+
517
+ await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
518
+ .getListUnspentWithAddress(address.address, network)
519
+ .then((unspent) => Future.forEach<Map<String, dynamic>>(unspent, (unspent) async {
520
try {
480
- return BitcoinUnspent.fromJSON(address, unspent);
481
- } catch (_) {
482
- return null;
483
- }
484
- }).whereNotNull())));
485
- unspentCoins = unspent.expand((e) => e).toList();
486
- unspentCoins.forEach((coin) async {
487
- final tx = await fetchTransactionInfo(hash: coin.hash, height: 0);
488
- coin.isChange = tx?.direction == TransactionDirection.outgoing;
489
- });
521
+ final coin = BitcoinUnspent.fromJSON(address, unspent);
522
+ final tx = await fetchTransactionInfo(
523
+ hash: coin.hash, height: 0, myAddresses: addressesSet);
524
+ coin.isChange = tx?.direction == TransactionDirection.outgoing;
525
+ updatedUnspentCoins.add(coin);
526
+ } catch (_) {}
527
+ }))));
528
+
529
+ unspentCoins = updatedUnspentCoins;
530
531
if (unspentCoinsInfo.isEmpty) {
532
unspentCoins.forEach((coin) => _addCoinInfo(coin));
535
536
if (unspentCoins.isNotEmpty) {
537
unspentCoins.forEach((coin) {
498
- final coinInfoList = unspentCoinsInfo.values
499
- .where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
538
+ final coinInfoList = unspentCoinsInfo.values.where((element) =>
539
+ element.walletId.contains(id) &&
540
+ element.hash.contains(coin.hash) &&
541
+ element.vout == coin.vout);
542
543
if (coinInfoList.isNotEmpty) {
544
final coinInfo = coinInfoList.first;
579
580
if (currentWalletUnspentCoins.isNotEmpty) {
581
currentWalletUnspentCoins.forEach((element) {
540
- final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash));
582
+ final existUnspentCoins = unspentCoins
583
+ .where((coin) => element.hash.contains(coin.hash) && element.vout == coin.vout);
584
585
if (existUnspentCoins.isEmpty) {
586
keys.add(element.key);
598
599
Future<ElectrumTransactionBundle> getTransactionExpanded(
600
{required String hash, required int height}) async {
558
- final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
559
- final transactionHex = verboseTransaction['hex'] as String;
560
- final original = bitcoin.Transaction.fromHex(transactionHex);
561
- final ins = <bitcoin.Transaction>[];
562
- final time = verboseTransaction['time'] as int?;
563
- final confirmations = verboseTransaction['confirmations'] as int? ?? 0;
564
-
565
- for (final vin in original.ins) {
566
- final id = HEX.encode(vin.hash!.reversed.toList());
567
- final txHex = await electrumClient.getTransactionHex(hash: id);
568
- final tx = bitcoin.Transaction.fromHex(txHex);
569
- ins.add(tx);
601
+ String transactionHex;
602
+ int? time;
603
+ int confirmations = 0;
604
+ if (network == BitcoinNetwork.testnet) {
605
+ // Testnet public electrum server does not support verbose transaction fetching
606
+ transactionHex = await electrumClient.getTransactionHex(hash: hash);
607
+
608
+ final status = json.decode(
609
+ (await http.get(Uri.parse("https://blockstream.info/testnet/api/tx/$hash/status"))).body);
610
+
611
+ time = status["block_time"] as int?;
612
+ final tip = await electrumClient.getCurrentBlockChainTip() ?? 0;
613
+ confirmations = tip - (status["block_height"] as int? ?? 0);
614
+ } else {
615
+ final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash);
616
+
617
+ transactionHex = verboseTransaction['hex'] as String;
618
+ time = verboseTransaction['time'] as int?;
619
+ confirmations = verboseTransaction['confirmations'] as int? ?? 0;
620
+ }
621
+
622
+ final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex);
623
+ final ins = <bitcoin_base.BtcTransaction>[];
624
+
625
+ for (final vin in original.inputs) {
626
+ try {
627
+ final id = HEX.encode(HEX.decode(vin.txId).reversed.toList());
628
+ final txHex = await electrumClient.getTransactionHex(hash: id);
629
+ final tx = bitcoin_base.BtcTransaction.fromRaw(txHex);
630
+ ins.add(tx);
631
+ } catch (_) {
632
+ ins.add(bitcoin_base.BtcTransaction.fromRaw(
633
+ await electrumClient.getTransactionHex(hash: vin.txId),
634
+ ));
635
+ }
636
}
637
572
- return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations);
638
+ return ElectrumTransactionBundle(original,
639
+ ins: ins, time: time, confirmations: confirmations, height: height);
640
}
641
642
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
576
- {required String hash, required int height}) async {
643
+ {required String hash,
644
+ required int height,
645
+ required Set<String> myAddresses,
646
+ bool? retryOnFailure}) async {
647
try {
578
- final tx = await getTransactionExpanded(hash: hash, height: height);
579
- final addresses = walletAddresses.addresses.map((addr) => addr.address).toSet();
580
- return ElectrumTransactionInfo.fromElectrumBundle(tx, walletInfo.type, networkType,
581
- addresses: addresses, height: height);
582
- } catch (_) {
648
+ return ElectrumTransactionInfo.fromElectrumBundle(
649
+ await getTransactionExpanded(hash: hash, height: height), walletInfo.type, network,
650
+ addresses: myAddresses, height: height);
651
+ } catch (e) {
652
+ if (e is FormatException && retryOnFailure == true) {
653
+ await Future.delayed(const Duration(seconds: 2));
654
+ return fetchTransactionInfo(hash: hash, height: height, myAddresses: myAddresses);
655
+ }
656
return null;
657
}
658
}
659
660
@override
661
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
589
- final addressHashes = <String, BitcoinAddressRecord>{};
590
- final normalizedHistories = <Map<String, dynamic>>[];
591
- final newTxCounts = <String, int>{};
592
-
593
- walletAddresses.addresses.forEach((addressRecord) {
594
- final sh = scriptHash(addressRecord.address, networkType: networkType);
595
- addressHashes[sh] = addressRecord;
596
- newTxCounts[sh] = 0;
597
- });
598
-
662
try {
600
- final histories = addressHashes.keys.map((scriptHash) =>
601
- electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history}));
602
- final historyResults = await Future.wait(histories);
603
-
604
- historyResults.forEach((history) {
605
- history.entries.forEach((historyItem) {
606
- if (historyItem.value.isNotEmpty) {
607
- final address = addressHashes[historyItem.key];
608
- address?.setAsUsed();
609
- newTxCounts[historyItem.key] = historyItem.value.length;
610
- normalizedHistories.addAll(historyItem.value);
663
+ final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
664
+ final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
665
+ final currentHeight = await electrumClient.getCurrentBlockChainTip() ?? 0;
666
+
667
+ await Future.wait(ADDRESS_TYPES.map((type) {
668
+ final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
669
+
670
+ return Future.wait(addressesByType.map((addressRecord) async {
671
+ final history = await _fetchAddressHistory(addressRecord, addressesSet, currentHeight);
672
+
673
+ if (history.isNotEmpty) {
674
+ addressRecord.txCount = history.length;
675
+ historiesWithDetails.addAll(history);
676
+
677
+ final matchedAddresses =
678
+ addressesByType.where((addr) => addr.isHidden == addressRecord.isHidden);
679
+
680
+ final isLastUsedAddress =
681
+ history.isNotEmpty && addressRecord.address == matchedAddresses.last.address;
682
+
683
+ if (isLastUsedAddress) {
684
+ await walletAddresses.discoverAddresses(
685
+ matchedAddresses.toList(),
686
+ addressRecord.isHidden,
687
+ (address, addressesSet) =>
688
+ _fetchAddressHistory(address, addressesSet, currentHeight)
689
+ .then((history) => history.isNotEmpty ? address.address : null),
690
+ type: type);
691
+ }
692
}
612
- });
613
- });
693
+ }));
694
+ }));
695
615
- for (var sh in addressHashes.keys) {
616
- var balanceData = await electrumClient.getBalance(sh);
617
- var addressRecord = addressHashes[sh];
618
- if (addressRecord != null) {
619
- addressRecord.balance = balanceData['confirmed'] as int? ?? 0;
620
- }
621
- }
696
+ return historiesWithDetails;
697
+ } catch (e) {
698
+ print(e.toString());
699
+ return {};
700
+ }
701
+ }
702
623
- addressHashes.forEach((sh, addressRecord) {
624
- addressRecord.txCount = newTxCounts[sh] ?? 0;
625
- });
703
+ Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
704
+ BitcoinAddressRecord addressRecord, Set<String> addressesSet, int currentHeight) async {
705
+ try {
706
+ final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
707
+
708
+ final history = await electrumClient
709
+ .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network));
710
+
711
+ if (history.isNotEmpty) {
712
+ addressRecord.setAsUsed();
713
+
714
+ await Future.wait(history.map((transaction) async {
715
+ final txid = transaction['tx_hash'] as String;
716
+ final height = transaction['height'] as int;
717
+ final storedTx = transactionHistory.transactions[txid];
718
+
719
+ if (storedTx != null) {
720
+ if (height > 0) {
721
+ storedTx.height = height;
722
+ // the tx's block itself is the first confirmation so add 1
723
+ storedTx.confirmations = currentHeight - height + 1;
724
+ storedTx.isPending = storedTx.confirmations == 0;
725
+ }
726
+
727
+ historiesWithDetails[txid] = storedTx;
728
+ } else {
729
+ final tx = await fetchTransactionInfo(
730
+ hash: txid, height: height, myAddresses: addressesSet, retryOnFailure: true);
731
+
732
+ if (tx != null) {
733
+ historiesWithDetails[txid] = tx;
734
+
735
+ // Got a new transaction fetched, add it to the transaction history
736
+ // instead of waiting all to finish, and next time it will be faster
737
+ transactionHistory.addOne(tx);
738
+ await transactionHistory.save();
739
+ }
740
+ }
741
627
- final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
628
- try {
629
- return fetchTransactionInfo(
630
- hash: transaction['tx_hash'] as String, height: transaction['height'] as int);
631
- } catch (_) {
742
return Future.value(null);
633
- }
634
- }));
743
+ }));
744
+ }
745
636
- return historiesWithDetails.fold<Map<String, ElectrumTransactionInfo>>(
637
- <String, ElectrumTransactionInfo>{}, (acc, tx) {
638
- if (tx == null) {
639
- return acc;
640
- }
641
- acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
642
- return acc;
643
- });
746
+ return historiesWithDetails;
747
} catch (e) {
748
print(e.toString());
749
return {};
757
}
758
759
_isTransactionUpdating = true;
657
- final transactions = await fetchTransactions();
658
- transactionHistory.addMany(transactions);
760
+ await fetchTransactions();
761
walletAddresses.updateReceiveAddresses();
660
- await transactionHistory.save();
762
_isTransactionUpdating = false;
763
} catch (e, stacktrace) {
764
print(stacktrace);
789
}
790
791
Future<ElectrumBalance> _fetchBalances() async {
691
- final addresses = walletAddresses.addresses.toList();
792
+ final addresses = walletAddresses.allAddresses.toList();
793
final balanceFutures = <Future<Map<String, dynamic>>>[];
794
for (var i = 0; i < addresses.length; i++) {
795
final addressRecord = addresses[i];
695
- final sh = scriptHash(addressRecord.address, networkType: networkType);
796
+ final sh = scriptHash(addressRecord.address, network: network);
797
final balanceFuture = electrumClient.getBalance(sh);
798
balanceFutures.add(balanceFuture);
799
}
802
unspentCoinsInfo.values.forEach((info) {
803
unspentCoins.forEach((element) {
804
if (element.hash == info.hash &&
805
+ element.vout == info.vout &&
806
info.isFrozen &&
807
element.bitcoinAddressRecord.address == info.address &&
808
element.value == info.value) {
840
String getChangeAddress() {
841
const minCountOfHiddenAddresses = 5;
842
final random = Random();
741
- var addresses = walletAddresses.addresses.where((addr) => addr.isHidden).toList();
843
+ var addresses = walletAddresses.allAddresses.where((addr) => addr.isHidden).toList();
844
845
if (addresses.length < minCountOfHiddenAddresses) {
744
- addresses = walletAddresses.addresses.toList();
846
+ addresses = walletAddresses.allAddresses.toList();
847
}
848
849
return addresses[random.nextInt(addresses.length)].address;
855
@override
856
String signMessage(String message, {String? address = null}) {
857
final index = address != null
756
- ? walletAddresses.addresses.firstWhere((element) => element.address == address).index
858
+ ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
859
: null;
860
final HD = index == null ? hd : hd.derive(index);
861
return base64Encode(HD.signMessage(message));
862
}
863
}
864
+
865
+class EstimateTxParams {
866
+ EstimateTxParams(
867
+ {required this.amount,
868
+ required this.feeRate,
869
+ required this.priority,
870
+ required this.outputsCount,
871
+ required this.size});
872
+
873
+ final int amount;
874
+ final int feeRate;
875
+ final TransactionPriority priority;
876
+ final int outputsCount;
877
+ final int size;
878
+}
879
+
880
+class EstimatedTxResult {
881
+ EstimatedTxResult(
882
+ {required this.utxos, required this.privateKeys, required this.fee, required this.amount});
883
+
884
+ final List<UtxoWithAddress> utxos;
885
+ final List<ECPrivate> privateKeys;
886
+ final int fee;
887
+ final int amount;
888
+}
889
+
890
+BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) {
891
+ if (P2pkhAddress.regex.hasMatch(address)) {
892
+ return P2pkhAddress.fromAddress(address: address, network: network);
893
+ } else if (P2shAddress.regex.hasMatch(address)) {
894
+ return P2shAddress.fromAddress(address: address, network: network);
895
+ } else if (P2wshAddress.regex.hasMatch(address)) {
896
+ return P2wshAddress.fromAddress(address: address, network: network);
897
+ } else if (P2trAddress.regex.hasMatch(address)) {
898
+ return P2trAddress.fromAddress(address: address, network: network);
899
+ } else {
900
+ return P2wpkhAddress.fromAddress(address: address, network: network);
901
+ }
902
+}
903
+
904
+BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
905
+ if (type is P2pkhAddress) {
906
+ return P2pkhAddressType.p2pkh;
907
+ } else if (type is P2shAddress) {
908
+ return P2shAddressType.p2wpkhInP2sh;
909
+ } else if (type is P2wshAddress) {
910
+ return SegwitAddresType.p2wsh;
911
+ } else if (type is P2trAddress) {
912
+ return SegwitAddresType.p2tr;
913
+ } else {
914
+ return SegwitAddresType.p2wpkh;
915
+ }
916
+}