rbf fixes issues sum utxo and fee calculation (#1625)
* total out amount issue * fix empty inputs and outputs addresses for new tx * fix sum value of utxo not spending * Update configure.dart * Update electrum_wallet.dart * receiving address * review fixes
Serhii committed
Aug 23, 2024 at 16:19 UTC
4c2d0613635a1457c2a63ba788f15dae170fcae9
6 files changed
+131
-77
cw_bitcoin/lib/electrum_transaction_info.dart
+1
-1
@@ -235,6 +235,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
235
}
236
237
String toString() {
238
- return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
238
+ return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
239
}
240
}
cw_bitcoin/lib/electrum_wallet.dart
+82
-47
@@ -132,6 +132,7 @@ abstract class ElectrumWalletBase
132
final String? _mnemonic;
133
134
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
135
+
136
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
137
138
final EncryptionFileUtils encryptionFileUtils;
@@ -1363,26 +1364,15 @@ abstract class ElectrumWalletBase
1364
}
1365
}
1366
1366
- Future<bool> canReplaceByFee(String hash) async {
1367
- final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
1368
-
1369
- final String? transactionHex;
1370
- int confirmations = 0;
1371
-
1372
- if (verboseTransaction.isEmpty) {
1373
- transactionHex = await electrumClient.getTransactionHex(hash: hash);
1374
- } else {
1375
- confirmations = verboseTransaction['confirmations'] as int? ?? 0;
1376
- transactionHex = verboseTransaction['hex'] as String?;
1377
- }
1378
-
1379
- if (confirmations > 0) return false;
1380
-
1381
- if (transactionHex == null || transactionHex.isEmpty) {
1367
+ Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
1368
+ try {
1369
+ final bundle = await getTransactionExpanded(hash: tx.txHash);
1370
+ _updateInputsAndOutputs(tx, bundle);
1371
+ if (bundle.confirmations > 0) return false;
1372
+ return bundle.originalTransaction.canReplaceByFee;
1373
+ } catch (e) {
1374
return false;
1375
}
1384
-
1385
- return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
1376
}
1377
1378
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
@@ -1458,47 +1448,59 @@ abstract class ElectrumWalletBase
1448
);
1449
}
1450
1461
- int totalOutAmount = bundle.originalTransaction.outputs
1462
- .fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
1451
+ // Create a list of available outputs
1452
+ final outputs = <BitcoinOutput>[];
1453
+ for (final out in bundle.originalTransaction.outputs) {
1454
+ final address = addressFromOutputScript(out.scriptPubKey, network);
1455
+ final btcAddress = addressTypeFromStr(address, network);
1456
+ outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
1457
+ }
1458
1464
- var currentFee = allInputsAmount - totalOutAmount;
1459
+ // Calculate the total amount and fees
1460
+ int totalOutAmount =
1461
+ outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
1462
+ int currentFee = allInputsAmount - totalOutAmount;
1463
int remainingFee = newFee - currentFee;
1464
1467
- final outputs = <BitcoinOutput>[];
1465
+ if (remainingFee <= 0) {
1466
+ throw Exception("New fee must be higher than the current fee.");
1467
+ }
1468
1469
- // Add outputs and deduct the fees from it
1470
- for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
1471
- final out = bundle.originalTransaction.outputs[i];
1472
- final address = addressFromOutputScript(out.scriptPubKey, network);
1473
- final btcAddress = addressTypeFromStr(address, network);
1469
+ // Deduct Remaining Fee from Main Outputs
1470
+ if (remainingFee > 0) {
1471
+ for (int i = outputs.length - 1; i >= 0; i--) {
1472
+ int outputAmount = outputs[i].value.toInt();
1473
1475
- int newAmount;
1476
- if (out.amount.toInt() >= remainingFee) {
1477
- newAmount = out.amount.toInt() - remainingFee;
1478
- remainingFee = 0;
1474
+ if (outputAmount > _dustAmount) {
1475
+ int deduction = (outputAmount - _dustAmount >= remainingFee)
1476
+ ? remainingFee
1477
+ : outputAmount - _dustAmount;
1478
+ outputs[i] = BitcoinOutput(
1479
+ address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
1480
+ remainingFee -= deduction;
1481
1480
- // if new amount of output is less than dust amount, then don't add this output as well
1481
- if (newAmount <= _dustAmount) {
1482
- continue;
1482
+ if (remainingFee <= 0) break;
1483
}
1484
- } else {
1485
- remainingFee -= out.amount.toInt();
1486
- continue;
1484
}
1485
+ }
1486
1489
- outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
1487
+ // Final check if the remaining fee couldn't be deducted
1488
+ if (remainingFee > 0) {
1489
+ throw Exception("Not enough funds to cover the fee.");
1490
}
1491
1492
+ // Identify all change outputs
1493
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
1494
+ final List<BitcoinOutput> changeOutputs = outputs
1495
+ .where((output) => changeAddresses
1496
+ .any((element) => element.address == output.address.toAddress(network)))
1497
+ .toList();
1498
1494
- // look for a change address in the outputs
1495
- final changeOutput = outputs.firstWhereOrNull((output) =>
1496
- changeAddresses.any((element) => element.address == output.address.toAddress(network)));
1499
+ int totalChangeAmount =
1500
+ changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
1501
1498
- // deduct the change amount from the output amount
1499
- if (changeOutput != null) {
1500
- totalOutAmount -= changeOutput.value.toInt();
1501
- }
1502
+ // The final amount that the receiver will receive
1503
+ int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
1504
1505
final txb = BitcoinTransactionBuilder(
1506
utxos: utxos,
@@ -1527,10 +1529,10 @@ abstract class ElectrumWalletBase
1529
transaction,
1530
type,
1531
electrumClient: electrumClient,
1530
- amount: totalOutAmount,
1532
+ amount: sendingAmount,
1533
fee: newFee,
1534
network: network,
1533
- hasChange: changeOutput != null,
1535
+ hasChange: changeOutputs.isNotEmpty,
1536
feeRate: newFee.toString(),
1537
)..addListener((transaction) async {
1538
transactionHistory.addOne(transaction);
@@ -2026,6 +2028,39 @@ abstract class ElectrumWalletBase
2028
});
2029
}
2030
}
2031
+
2032
+ void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) {
2033
+ tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList();
2034
+
2035
+ if (tx.inputAddresses == null ||
2036
+ tx.inputAddresses!.isEmpty ||
2037
+ tx.outputAddresses == null ||
2038
+ tx.outputAddresses!.isEmpty) {
2039
+ List<String> inputAddresses = [];
2040
+ List<String> outputAddresses = [];
2041
+
2042
+ for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
2043
+ final input = bundle.originalTransaction.inputs[i];
2044
+ final inputTransaction = bundle.ins[i];
2045
+ final vout = input.txIndex;
2046
+ final outTransaction = inputTransaction.outputs[vout];
2047
+ final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
2048
+
2049
+ if (address.isNotEmpty) inputAddresses.add(address);
2050
+ }
2051
+
2052
+ for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
2053
+ final out = bundle.originalTransaction.outputs[i];
2054
+ final address = addressFromOutputScript(out.scriptPubKey, network);
2055
+
2056
+ if (address.isNotEmpty) outputAddresses.add(address);
2057
+ }
2058
+ tx.inputAddresses = inputAddresses;
2059
+ tx.outputAddresses = outputAddresses;
2060
+
2061
+ transactionHistory.addOne(tx);
2062
+ }
2063
+ }
2064
}
2065
2066
class ScanNode {
lib/bitcoin/cw_bitcoin.dart
+3
-2
@@ -398,9 +398,10 @@ class CWBitcoin extends Bitcoin {
398
}
399
400
@override
401
- Future<bool> canReplaceByFee(Object wallet, String transactionHash) async {
401
+ Future<bool> canReplaceByFee(Object wallet, Object transactionInfo) async {
402
final bitcoinWallet = wallet as ElectrumWallet;
403
- return bitcoinWallet.canReplaceByFee(transactionHash);
403
+ final tx = transactionInfo as ElectrumTransactionInfo;
404
+ return bitcoinWallet.canReplaceByFee(tx);
405
}
406
407
@override
lib/view_model/send/send_view_model.dart
+30
-16
@@ -18,6 +18,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
18
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
19
import 'package:cake_wallet/wownero/wownero.dart';
20
import 'package:cw_core/exceptions.dart';
21
+import 'package:cw_core/transaction_info.dart';
22
import 'package:cw_core/transaction_priority.dart';
23
import 'package:cake_wallet/view_model/send/output.dart';
24
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
@@ -392,25 +393,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
393
}
394
395
@action
395
- Future<void> replaceByFee(String txId, String newFee) async {
396
+ Future<void> replaceByFee(TransactionInfo tx, String newFee) async {
397
state = IsExecutingState();
398
398
- final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee);
399
-
400
- if (!isSufficient) {
401
- state = AwaitingConfirmationState(
402
- title: S.current.confirm_fee_deduction,
403
- message: S.current.confirm_fee_deduction_content,
404
- onConfirm: () async {
405
- pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
406
- state = ExecutedSuccessfullyState();
407
- },
408
- onCancel: () {
409
- state = FailureState('Insufficient change for fee');
410
- });
411
- } else {
412
- pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
399
+ try {
400
+ final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee);
401
+
402
+ if (!isSufficient) {
403
+ state = AwaitingConfirmationState(
404
+ title: S.current.confirm_fee_deduction,
405
+ message: S.current.confirm_fee_deduction_content,
406
+ onConfirm: () async => await _executeReplaceByFee(tx, newFee),
407
+ onCancel: () => state = FailureState('Insufficient change for fee'));
408
+ } else {
409
+ await _executeReplaceByFee(tx, newFee);
410
+ }
411
+ } catch (e) {
412
+ state = FailureState(e.toString());
413
+ }
414
+ }
415
+
416
+ Future<void> _executeReplaceByFee(TransactionInfo tx, String newFee) async {
417
+
418
+
419
+ clearOutputs();
420
+ final output = outputs.first;
421
+ output.address = tx.outputAddresses?.first ?? '';
422
+
423
+ try {
424
+ pendingTransaction = await bitcoin!.replaceByFee(wallet, tx.id, newFee);
425
state = ExecutedSuccessfullyState();
426
+ } catch (e) {
427
+ state = FailureState(e.toString());
428
}
429
}
430
lib/view_model/transaction_details_view_model.dart
+13
-10
@@ -52,7 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store {
52
case WalletType.bitcoin:
53
_addElectrumListItems(tx, dateFormat);
54
_addBumpFeesListItems(tx);
55
- _checkForRBF();
55
+ _checkForRBF(tx);
56
break;
57
case WalletType.litecoin:
58
case WalletType.bitcoinCash:
@@ -349,12 +349,15 @@ abstract class TransactionDetailsViewModelBase with Store {
349
350
void _addBumpFeesListItems(TransactionInfo tx) {
351
transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
352
+ final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true)
353
+ ? 1
354
+ : transactionInfo.inputAddresses!.length;
355
+ final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true)
356
+ ? 1
357
+ : transactionInfo.outputAddresses!.length;
358
359
newFee = bitcoin!.getFeeAmountForPriority(
354
- wallet,
355
- bitcoin!.getBitcoinTransactionPriorityMedium(),
356
- transactionInfo.inputAddresses?.length ?? 1,
357
- transactionInfo.outputAddresses?.length ?? 1);
360
+ wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount);
361
362
RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0'));
363
@@ -383,12 +386,12 @@ abstract class TransactionDetailsViewModelBase with Store {
386
return setNewFee(value: sliderValue, priority: transactionPriority!);
387
}));
388
386
- if (transactionInfo.inputAddresses != null) {
389
+ if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) {
390
RBFListItems.add(StandardExpandableListItem(
391
title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!));
392
}
393
391
- if (transactionInfo.outputAddresses != null) {
394
+ if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) {
395
RBFListItems.add(StandardExpandableListItem(
396
title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!));
397
}
@@ -416,10 +419,10 @@ abstract class TransactionDetailsViewModelBase with Store {
419
}
420
421
@action
419
- Future<void> _checkForRBF() async {
422
+ Future<void> _checkForRBF(TransactionInfo tx) async {
423
if (wallet.type == WalletType.bitcoin &&
424
transactionInfo.direction == TransactionDirection.outgoing) {
422
- if (await bitcoin!.canReplaceByFee(wallet, transactionInfo.id)) {
425
+ if (await bitcoin!.canReplaceByFee(wallet, tx)) {
426
_canReplaceByFee = true;
427
}
428
}
@@ -441,7 +444,7 @@ abstract class TransactionDetailsViewModelBase with Store {
444
return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
445
}
446
444
- void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo.id, newFee);
447
+ void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee,);
448
449
@computed
450
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
tool/configure.dart
+2
-1
@@ -79,6 +79,7 @@ import 'dart:typed_data';
79
import 'package:bitcoin_base/bitcoin_base.dart';
80
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
81
import 'package:cake_wallet/view_model/send/output.dart';
82
+import 'package:cw_bitcoin/electrum_transaction_info.dart';
83
import 'package:cw_core/hardware/hardware_account_data.dart';
84
import 'package:cw_core/node.dart';
85
import 'package:cw_core/output_info.dart';
@@ -204,7 +205,7 @@ abstract class Bitcoin {
205
bool isTestnet(Object wallet);
206
207
Future<PendingTransaction> replaceByFee(Object wallet, String transactionHash, String fee);
207
- Future<bool> canReplaceByFee(Object wallet, String transactionHash);
208
+ Future<bool> canReplaceByFee(Object wallet, Object tx);
209
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee);
210
int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size});
211
int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount,