rbf fixes issues sum utxo and fee calculation ()

* 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
This commit is contained in:
Serhii 2024-08-23 16:19:42 +03:00 committed by GitHub
parent d01199bd04
commit 4c2d061363
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 132 additions and 78 deletions

View file

@ -235,6 +235,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
} }
String toString() { String toString() {
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)'; 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)';
} }
} }

View file

@ -132,6 +132,7 @@ abstract class ElectrumWalletBase
final String? _mnemonic; final String? _mnemonic;
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
final EncryptionFileUtils encryptionFileUtils; final EncryptionFileUtils encryptionFileUtils;
@ -1363,26 +1364,15 @@ abstract class ElectrumWalletBase
} }
} }
Future<bool> canReplaceByFee(String hash) async { Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); try {
final bundle = await getTransactionExpanded(hash: tx.txHash);
final String? transactionHex; _updateInputsAndOutputs(tx, bundle);
int confirmations = 0; if (bundle.confirmations > 0) return false;
return bundle.originalTransaction.canReplaceByFee;
if (verboseTransaction.isEmpty) { } catch (e) {
transactionHex = await electrumClient.getTransactionHex(hash: hash);
} else {
confirmations = verboseTransaction['confirmations'] as int? ?? 0;
transactionHex = verboseTransaction['hex'] as String?;
}
if (confirmations > 0) return false;
if (transactionHex == null || transactionHex.isEmpty) {
return false; return false;
} }
return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
} }
Future<bool> isChangeSufficientForFee(String txId, int newFee) async { Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
@ -1458,47 +1448,59 @@ abstract class ElectrumWalletBase
); );
} }
int totalOutAmount = bundle.originalTransaction.outputs // Create a list of available outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
var currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
final outputs = <BitcoinOutput>[]; final outputs = <BitcoinOutput>[];
for (final out in bundle.originalTransaction.outputs) {
// Add outputs and deduct the fees from it
for (int i = bundle.originalTransaction.outputs.length - 1; i >= 0; i--) {
final out = bundle.originalTransaction.outputs[i];
final address = addressFromOutputScript(out.scriptPubKey, network); final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network); final btcAddress = addressTypeFromStr(address, network);
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
}
int newAmount; // Calculate the total amount and fees
if (out.amount.toInt() >= remainingFee) { int totalOutAmount =
newAmount = out.amount.toInt() - remainingFee; outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
remainingFee = 0; int currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
// if new amount of output is less than dust amount, then don't add this output as well if (remainingFee <= 0) {
if (newAmount <= _dustAmount) { throw Exception("New fee must be higher than the current fee.");
continue; }
// Deduct Remaining Fee from Main Outputs
if (remainingFee > 0) {
for (int i = outputs.length - 1; i >= 0; i--) {
int outputAmount = outputs[i].value.toInt();
if (outputAmount > _dustAmount) {
int deduction = (outputAmount - _dustAmount >= remainingFee)
? remainingFee
: outputAmount - _dustAmount;
outputs[i] = BitcoinOutput(
address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
remainingFee -= deduction;
if (remainingFee <= 0) break;
} }
} else {
remainingFee -= out.amount.toInt();
continue;
} }
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(newAmount)));
} }
// Final check if the remaining fee couldn't be deducted
if (remainingFee > 0) {
throw Exception("Not enough funds to cover the fee.");
}
// Identify all change outputs
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
final List<BitcoinOutput> changeOutputs = outputs
.where((output) => changeAddresses
.any((element) => element.address == output.address.toAddress(network)))
.toList();
// look for a change address in the outputs int totalChangeAmount =
final changeOutput = outputs.firstWhereOrNull((output) => changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
// deduct the change amount from the output amount // The final amount that the receiver will receive
if (changeOutput != null) { int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
totalOutAmount -= changeOutput.value.toInt();
}
final txb = BitcoinTransactionBuilder( final txb = BitcoinTransactionBuilder(
utxos: utxos, utxos: utxos,
@ -1527,10 +1529,10 @@ abstract class ElectrumWalletBase
transaction, transaction,
type, type,
electrumClient: electrumClient, electrumClient: electrumClient,
amount: totalOutAmount, amount: sendingAmount,
fee: newFee, fee: newFee,
network: network, network: network,
hasChange: changeOutput != null, hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(), feeRate: newFee.toString(),
)..addListener((transaction) async { )..addListener((transaction) async {
transactionHistory.addOne(transaction); transactionHistory.addOne(transaction);
@ -2026,6 +2028,39 @@ abstract class ElectrumWalletBase
}); });
} }
} }
void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) {
tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList();
if (tx.inputAddresses == null ||
tx.inputAddresses!.isEmpty ||
tx.outputAddresses == null ||
tx.outputAddresses!.isEmpty) {
List<String> inputAddresses = [];
List<String> outputAddresses = [];
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address = addressFromOutputScript(outTransaction.scriptPubKey, network);
if (address.isNotEmpty) inputAddresses.add(address);
}
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
final out = bundle.originalTransaction.outputs[i];
final address = addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);
}
tx.inputAddresses = inputAddresses;
tx.outputAddresses = outputAddresses;
transactionHistory.addOne(tx);
}
}
} }
class ScanNode { class ScanNode {

View file

@ -398,9 +398,10 @@ class CWBitcoin extends Bitcoin {
} }
@override @override
Future<bool> canReplaceByFee(Object wallet, String transactionHash) async { Future<bool> canReplaceByFee(Object wallet, Object transactionInfo) async {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.canReplaceByFee(transactionHash); final tx = transactionInfo as ElectrumTransactionInfo;
return bitcoinWallet.canReplaceByFee(tx);
} }
@override @override

View file

@ -18,6 +18,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/exceptions.dart'; import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
@ -392,25 +393,38 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
} }
@action @action
Future<void> replaceByFee(String txId, String newFee) async { Future<void> replaceByFee(TransactionInfo tx, String newFee) async {
state = IsExecutingState(); state = IsExecutingState();
final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee); try {
final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee);
if (!isSufficient) { if (!isSufficient) {
state = AwaitingConfirmationState( state = AwaitingConfirmationState(
title: S.current.confirm_fee_deduction, title: S.current.confirm_fee_deduction,
message: S.current.confirm_fee_deduction_content, message: S.current.confirm_fee_deduction_content,
onConfirm: () async { onConfirm: () async => await _executeReplaceByFee(tx, newFee),
pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee); onCancel: () => state = FailureState('Insufficient change for fee'));
state = ExecutedSuccessfullyState(); } else {
}, await _executeReplaceByFee(tx, newFee);
onCancel: () { }
state = FailureState('Insufficient change for fee'); } catch (e) {
}); state = FailureState(e.toString());
} else { }
pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee); }
Future<void> _executeReplaceByFee(TransactionInfo tx, String newFee) async {
clearOutputs();
final output = outputs.first;
output.address = tx.outputAddresses?.first ?? '';
try {
pendingTransaction = await bitcoin!.replaceByFee(wallet, tx.id, newFee);
state = ExecutedSuccessfullyState(); state = ExecutedSuccessfullyState();
} catch (e) {
state = FailureState(e.toString());
} }
} }

View file

@ -52,7 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store {
case WalletType.bitcoin: case WalletType.bitcoin:
_addElectrumListItems(tx, dateFormat); _addElectrumListItems(tx, dateFormat);
_addBumpFeesListItems(tx); _addBumpFeesListItems(tx);
_checkForRBF(); _checkForRBF(tx);
break; break;
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
@ -349,12 +349,15 @@ abstract class TransactionDetailsViewModelBase with Store {
void _addBumpFeesListItems(TransactionInfo tx) { void _addBumpFeesListItems(TransactionInfo tx) {
transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium(); transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true)
? 1
: transactionInfo.inputAddresses!.length;
final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true)
? 1
: transactionInfo.outputAddresses!.length;
newFee = bitcoin!.getFeeAmountForPriority( newFee = bitcoin!.getFeeAmountForPriority(
wallet, wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount);
bitcoin!.getBitcoinTransactionPriorityMedium(),
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1);
RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0'));
@ -383,12 +386,12 @@ abstract class TransactionDetailsViewModelBase with Store {
return setNewFee(value: sliderValue, priority: transactionPriority!); return setNewFee(value: sliderValue, priority: transactionPriority!);
})); }));
if (transactionInfo.inputAddresses != null) { if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) {
RBFListItems.add(StandardExpandableListItem( RBFListItems.add(StandardExpandableListItem(
title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!)); title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!));
} }
if (transactionInfo.outputAddresses != null) { if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) {
RBFListItems.add(StandardExpandableListItem( RBFListItems.add(StandardExpandableListItem(
title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!)); title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!));
} }
@ -416,10 +419,10 @@ abstract class TransactionDetailsViewModelBase with Store {
} }
@action @action
Future<void> _checkForRBF() async { Future<void> _checkForRBF(TransactionInfo tx) async {
if (wallet.type == WalletType.bitcoin && if (wallet.type == WalletType.bitcoin &&
transactionInfo.direction == TransactionDirection.outgoing) { transactionInfo.direction == TransactionDirection.outgoing) {
if (await bitcoin!.canReplaceByFee(wallet, transactionInfo.id)) { if (await bitcoin!.canReplaceByFee(wallet, tx)) {
_canReplaceByFee = true; _canReplaceByFee = true;
} }
} }
@ -441,7 +444,7 @@ abstract class TransactionDetailsViewModelBase with Store {
return bitcoin!.formatterBitcoinAmountToString(amount: newFee); return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
} }
void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo.id, newFee); void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee,);
@computed @computed
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled

View file

@ -79,6 +79,7 @@ import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
import 'package:cw_core/output_info.dart'; import 'package:cw_core/output_info.dart';
@ -204,7 +205,7 @@ abstract class Bitcoin {
bool isTestnet(Object wallet); bool isTestnet(Object wallet);
Future<PendingTransaction> replaceByFee(Object wallet, String transactionHash, String fee); Future<PendingTransaction> replaceByFee(Object wallet, String transactionHash, String fee);
Future<bool> canReplaceByFee(Object wallet, String transactionHash); Future<bool> canReplaceByFee(Object wallet, Object tx);
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee); Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee);
int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size});
int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount,