8
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
9
import 'package:collection/collection.dart';
10
import 'package:cw_bitcoin/bitcoin_address_record.dart';
11
+import 'package:cw_bitcoin/bitcoin_amount_format.dart';
12
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
12
-import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
13
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
14
-import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
14
import 'package:cw_bitcoin/bitcoin_unspent.dart';
15
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
16
import 'package:cw_bitcoin/electrum.dart';
18
import 'package:cw_bitcoin/electrum_transaction_history.dart';
19
import 'package:cw_bitcoin/electrum_transaction_info.dart';
20
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
21
+import 'package:cw_bitcoin/exceptions.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';
188
}
189
}
190
191
- Future<EstimatedTxResult> estimateTxFeeAndInputsToUse(
192
- int credentialsAmount,
193
- bool sendAll,
194
- List<BitcoinBaseAddress> outputAddresses,
195
- List<BitcoinOutput> outputs,
196
- int? feeRate,
197
- BitcoinTransactionPriority? priority,
198
- {int? inputsCount,
199
- String? memo}) async {
191
+ int _getDustAmount() {
192
+ return 546;
193
+ }
194
+
195
+ bool _isBelowDust(int amount) => amount <= _getDustAmount() && network != BitcoinNetwork.testnet;
196
+
197
+ Future<EstimatedTxResult> estimateSendAllTx(
198
+ List<BitcoinOutput> outputs,
199
+ int feeRate, {
200
+ String? memo,
201
+ int credentialsAmount = 0,
202
+ }) async {
203
final utxos = <UtxoWithAddress>[];
204
List<ECPrivate> privateKeys = [];
202
-
203
- var leftAmount = credentialsAmount;
204
- var allInputsAmount = 0;
205
+ int allInputsAmount = 0;
206
207
for (int i = 0; i < unspentCoins.length; i++) {
208
final utx = unspentCoins[i];
209
210
if (utx.isSending) {
211
allInputsAmount += utx.value;
211
- leftAmount = leftAmount - utx.value;
212
213
final address = addressTypeFromStr(utx.address, network);
214
final privkey = generateECPrivate(
226
vout: utx.vout,
227
scriptType: _getScriptType(address),
228
),
229
- ownerDetails:
230
- UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
229
+ ownerDetails: UtxoAddressDetails(
230
+ publicKey: privkey.getPublic().toHex(),
231
+ address: address,
232
+ ),
233
),
234
);
233
-
234
- bool amountIsAcquired = !sendAll && leftAmount <= 0;
235
- if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
236
- break;
237
- }
235
}
236
}
237
239
throw BitcoinTransactionNoInputsException();
240
}
241
245
- var changeValue = allInputsAmount - credentialsAmount;
242
+ int estimatedSize;
243
+ if (network is BitcoinCashNetwork) {
244
+ estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
245
+ utxos: utxos,
246
+ outputs: outputs,
247
+ network: network as BitcoinCashNetwork,
248
+ memo: memo,
249
+ );
250
+ } else {
251
+ estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
252
+ utxos: utxos,
253
+ outputs: outputs,
254
+ network: network,
255
+ memo: memo,
256
+ );
257
+ }
258
+
259
+ int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
260
+
261
+ if (fee == 0) {
262
+ throw BitcoinTransactionNoFeeException();
263
+ }
264
247
- if (!sendAll) {
248
- if (changeValue > 0) {
249
- final changeAddress = await walletAddresses.getChangeAddress();
250
- final address = addressTypeFromStr(changeAddress, network);
251
- outputAddresses.add(address);
252
- outputs.add(BitcoinOutput(address: address, value: BigInt.from(changeValue)));
265
+ // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
266
+ int amount = allInputsAmount - fee;
267
+
268
+ // Attempting to send less than the dust limit
269
+ if (_isBelowDust(amount)) {
270
+ throw BitcoinTransactionNoDustException();
271
+ }
272
+
273
+ if (credentialsAmount > 0) {
274
+ final amountLeftForFee = amount - credentialsAmount;
275
+ if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) {
276
+ amount -= amountLeftForFee;
277
+ fee += amountLeftForFee;
278
}
279
}
280
256
- final estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
281
+ outputs[outputs.length - 1] =
282
+ BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
283
+
284
+ return EstimatedTxResult(
285
utxos: utxos,
258
- outputs: outputs,
259
- network: network,
286
+ privateKeys: privateKeys,
287
+ fee: fee,
288
+ amount: amount,
289
+ isSendAll: true,
290
+ hasChange: false,
291
memo: memo,
292
);
293
+ }
294
263
- int fee = feeRate != null
264
- ? feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize)
265
- : feeAmountForPriority(priority!, 0, 0, size: estimatedSize);
295
+ Future<EstimatedTxResult> estimateTxForAmount(
296
+ int credentialsAmount,
297
+ List<BitcoinOutput> outputs,
298
+ int feeRate, {
299
+ int? inputsCount,
300
+ String? memo,
301
+ }) async {
302
+ final utxos = <UtxoWithAddress>[];
303
+ List<ECPrivate> privateKeys = [];
304
+ int allInputsAmount = 0;
305
267
- if (fee == 0) {
268
- throw BitcoinTransactionWrongBalanceException(currency);
269
- }
306
+ int leftAmount = credentialsAmount;
307
+ final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList();
308
271
- var amount = credentialsAmount;
309
+ for (int i = 0; i < sendingCoins.length; i++) {
310
+ final utx = sendingCoins[i];
311
273
- final lastOutput = outputs.last;
274
- if (!sendAll) {
275
- if (changeValue > fee) {
276
- // Here, lastOutput is change, deduct the fee from it
277
- outputs[outputs.length - 1] =
278
- BitcoinOutput(address: lastOutput.address, value: lastOutput.value - BigInt.from(fee));
312
+ allInputsAmount += utx.value;
313
+ leftAmount = leftAmount - utx.value;
314
+
315
+ final address = addressTypeFromStr(utx.address, network);
316
+ final privkey = generateECPrivate(
317
+ hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
318
+ index: utx.bitcoinAddressRecord.index,
319
+ network: network);
320
+
321
+ privateKeys.add(privkey);
322
+
323
+ utxos.add(
324
+ UtxoWithAddress(
325
+ utxo: BitcoinUtxo(
326
+ txHash: utx.hash,
327
+ value: BigInt.from(utx.value),
328
+ vout: utx.vout,
329
+ scriptType: _getScriptType(address),
330
+ ),
331
+ ownerDetails: UtxoAddressDetails(
332
+ publicKey: privkey.getPublic().toHex(),
333
+ address: address,
334
+ ),
335
+ ),
336
+ );
337
+
338
+ bool amountIsAcquired = leftAmount <= 0;
339
+ if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
340
+ break;
341
}
342
+ }
343
+
344
+ if (utxos.isEmpty) {
345
+ throw BitcoinTransactionNoInputsException();
346
+ }
347
+
348
+ final spendingAllCoins = sendingCoins.length == utxos.length;
349
+
350
+ // How much is being spent - how much is being sent
351
+ int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
352
+
353
+ if (amountLeftForChangeAndFee <= 0) {
354
+ throw BitcoinTransactionWrongBalanceException();
355
+ }
356
+
357
+ final changeAddress = await walletAddresses.getChangeAddress();
358
+ final address = addressTypeFromStr(changeAddress, network);
359
+ outputs.add(BitcoinOutput(
360
+ address: address,
361
+ value: BigInt.from(amountLeftForChangeAndFee),
362
+ ));
363
+
364
+ int estimatedSize;
365
+ if (network is BitcoinCashNetwork) {
366
+ estimatedSize = ForkedTransactionBuilder.estimateTransactionSize(
367
+ utxos: utxos,
368
+ outputs: outputs,
369
+ network: network as BitcoinCashNetwork,
370
+ memo: memo,
371
+ );
372
} else {
281
- // 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
282
- amount = allInputsAmount - fee;
373
+ estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize(
374
+ utxos: utxos,
375
+ outputs: outputs,
376
+ network: network,
377
+ memo: memo,
378
+ );
379
+ }
380
+
381
+ int fee = feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize);
382
+
383
+ if (fee == 0) {
384
+ throw BitcoinTransactionNoFeeException();
385
+ }
386
+
387
+ int amount = credentialsAmount;
388
+ final lastOutput = outputs.last;
389
+ final amountLeftForChange = amountLeftForChangeAndFee - fee;
390
+
391
+ if (!_isBelowDust(amountLeftForChange)) {
392
+ // Here, lastOutput already is change, return the amount left without the fee to the user's address.
393
outputs[outputs.length - 1] =
284
- BitcoinOutput(address: lastOutput.address, value: BigInt.from(amount));
394
+ BitcoinOutput(address: lastOutput.address, value: BigInt.from(amountLeftForChange));
395
+ } else {
396
+ // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change
397
+ outputs.removeLast();
398
+
399
+ // Still has inputs to spend before failing
400
+ if (!spendingAllCoins) {
401
+ return estimateTxForAmount(
402
+ credentialsAmount,
403
+ outputs,
404
+ feeRate,
405
+ inputsCount: utxos.length + 1,
406
+ memo: memo,
407
+ );
408
+ }
409
+
410
+ final estimatedSendAll = await estimateSendAllTx(
411
+ outputs,
412
+ feeRate,
413
+ memo: memo,
414
+ );
415
+
416
+ if (estimatedSendAll.amount == credentialsAmount) {
417
+ return estimatedSendAll;
418
+ }
419
+
420
+ // Estimate to user how much is needed to send to cover the fee
421
+ final maxAmountWithReturningChange = allInputsAmount - _getDustAmount() - fee - 1;
422
+ throw BitcoinTransactionNoDustOnChangeException(
423
+ bitcoinAmountToString(amount: maxAmountWithReturningChange),
424
+ bitcoinAmountToString(amount: estimatedSendAll.amount),
425
+ );
426
+ }
427
+
428
+ // Attempting to send less than the dust limit
429
+ if (_isBelowDust(amount)) {
430
+ throw BitcoinTransactionNoDustException();
431
}
432
433
final totalAmount = amount + fee;
434
435
if (totalAmount > balance[currency]!.confirmed) {
290
- throw BitcoinTransactionWrongBalanceException(currency);
436
+ throw BitcoinTransactionWrongBalanceException();
437
}
438
439
if (totalAmount > allInputsAmount) {
294
- if (unspentCoins.where((utx) => utx.isSending).length == utxos.length) {
295
- throw BitcoinTransactionWrongBalanceException(currency);
440
+ if (spendingAllCoins) {
441
+ throw BitcoinTransactionWrongBalanceException();
442
} else {
297
- if (changeValue > fee) {
298
- outputAddresses.removeLast();
443
+ if (amountLeftForChangeAndFee > fee) {
444
outputs.removeLast();
445
}
446
302
- return estimateTxFeeAndInputsToUse(
303
- credentialsAmount, sendAll, outputAddresses, outputs, feeRate, priority,
304
- inputsCount: utxos.length + 1);
447
+ return estimateTxForAmount(
448
+ credentialsAmount,
449
+ outputs,
450
+ feeRate,
451
+ inputsCount: utxos.length + 1,
452
+ memo: memo,
453
+ );
454
}
455
}
456
459
privateKeys: privateKeys,
460
fee: fee,
461
amount: amount,
462
+ hasChange: true,
463
+ isSendAll: false,
464
memo: memo,
465
);
466
}
469
Future<PendingTransaction> createTransaction(Object credentials) async {
470
try {
471
final outputs = <BitcoinOutput>[];
321
- final outputAddresses = <BitcoinBaseAddress>[];
472
final transactionCredentials = credentials as BitcoinTransactionCredentials;
473
final hasMultiDestination = transactionCredentials.outputs.length > 1;
474
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
475
+ final memo = transactionCredentials.outputs.first.memo;
476
326
- var credentialsAmount = 0;
477
+ int credentialsAmount = 0;
478
479
for (final out in transactionCredentials.outputs) {
329
- final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
330
- final address = addressTypeFromStr(outputAddress, network);
480
+ final outputAmount = out.formattedCryptoAmount!;
481
332
- outputAddresses.add(address);
482
+ if (!sendAll && _isBelowDust(outputAmount)) {
483
+ throw BitcoinTransactionNoDustException();
484
+ }
485
486
if (hasMultiDestination) {
335
- if (out.sendAll || out.formattedCryptoAmount! <= 0) {
336
- throw BitcoinTransactionWrongBalanceException(currency);
487
+ if (out.sendAll) {
488
+ throw BitcoinTransactionWrongBalanceException();
489
}
490
+ }
491
339
- final outputAmount = out.formattedCryptoAmount!;
340
- credentialsAmount += outputAmount;
492
+ credentialsAmount += outputAmount;
493
342
- outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
494
+ final address =
495
+ addressTypeFromStr(out.isParsedAddress ? out.extractedAddress! : out.address, network);
496
+
497
+ if (sendAll) {
498
+ // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
499
+ outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
500
} else {
344
- if (!sendAll) {
345
- final outputAmount = out.formattedCryptoAmount!;
346
- credentialsAmount += outputAmount;
347
- outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
348
- } else {
349
- // The value will be changed after estimating the Tx size and deducting the fee from the total
350
- outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
351
- }
501
+ outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
502
}
503
}
504
355
- final estimatedTx = await estimateTxFeeAndInputsToUse(
356
- credentialsAmount,
357
- sendAll,
358
- outputAddresses,
359
- outputs,
360
- transactionCredentials.feeRate,
361
- transactionCredentials.priority,
362
- memo: transactionCredentials.outputs.first.memo,
363
- );
505
+ final feeRateInt = transactionCredentials.feeRate != null
506
+ ? transactionCredentials.feeRate!
507
+ : feeRate(transactionCredentials.priority!);
508
+
509
+ EstimatedTxResult estimatedTx;
510
+ if (sendAll) {
511
+ estimatedTx = await estimateSendAllTx(
512
+ outputs,
513
+ feeRateInt,
514
+ memo: memo,
515
+ credentialsAmount: credentialsAmount,
516
+ );
517
+ } else {
518
+ estimatedTx = await estimateTxForAmount(
519
+ credentialsAmount,
520
+ outputs,
521
+ feeRateInt,
522
+ memo: memo,
523
+ );
524
+ }
525
365
- final txb = BitcoinTransactionBuilder(
366
- utxos: estimatedTx.utxos,
367
- outputs: outputs,
368
- fee: BigInt.from(estimatedTx.fee),
369
- network: network,
370
- memo: estimatedTx.memo,
371
- outputOrdering: BitcoinOrdering.none,
372
- );
526
+ BasedBitcoinTransacationBuilder txb;
527
+ if (network is BitcoinCashNetwork) {
528
+ txb = ForkedTransactionBuilder(
529
+ utxos: estimatedTx.utxos,
530
+ outputs: outputs,
531
+ fee: BigInt.from(estimatedTx.fee),
532
+ network: network,
533
+ memo: estimatedTx.memo,
534
+ outputOrdering: BitcoinOrdering.none,
535
+ );
536
+ } else {
537
+ txb = BitcoinTransactionBuilder(
538
+ utxos: estimatedTx.utxos,
539
+ outputs: outputs,
540
+ fee: BigInt.from(estimatedTx.fee),
541
+ network: network,
542
+ memo: estimatedTx.memo,
543
+ outputOrdering: BitcoinOrdering.none,
544
+ );
545
+ }
546
547
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
548
final key = estimatedTx.privateKeys
563
electrumClient: electrumClient,
564
amount: estimatedTx.amount,
565
fee: estimatedTx.fee,
393
- network: network)
566
+ feeRate: feeRateInt.toString(),
567
+ network: network,
568
+ hasChange: estimatedTx.hasChange,
569
+ isSendAll: estimatedTx.isSendAll)
570
..addListener((transaction) async {
571
transactionHistory.addOne(transaction);
572
await updateBalance();
599
}
600
}
601
426
- int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
602
+ int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
603
{int? size}) =>
604
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
605
1084
required this.privateKeys,
1085
required this.fee,
1086
required this.amount,
1087
+ required this.hasChange,
1088
+ required this.isSendAll,
1089
this.memo,
1090
});
1091
1093
final List<ECPrivate> privateKeys;
1094
final int fee;
1095
final int amount;
1096
+ final bool hasChange;
1097
+ final bool isSendAll;
1098
final String? memo;
1099
}
1100
1101
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
1102
+ if (network is BitcoinCashNetwork) {
1103
+ if (!address.startsWith("bitcoincash:") &&
1104
+ (address.startsWith("q") || address.startsWith("p"))) {
1105
+ address = "bitcoincash:$address";
1106
+ }
1107
+
1108
+ return BitcoinCashAddress(address).baseAddress;
1109
+ }
1110
+
1111
if (P2pkhAddress.regex.hasMatch(address)) {
1112
return P2pkhAddress.fromAddress(address: address, network: network);
1113
} else if (P2shAddress.regex.hasMatch(address)) {