rbf fixes issues sum utxo and fee calculation (#1625)
Some checks are pending
Cache Dependencies / test (push) Waiting to run

* 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() {
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;
Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0));
Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1));
final EncryptionFileUtils encryptionFileUtils;
@ -1363,26 +1364,15 @@ abstract class ElectrumWalletBase
}
}
Future<bool> canReplaceByFee(String hash) async {
final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash);
final String? transactionHex;
int confirmations = 0;
if (verboseTransaction.isEmpty) {
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) {
Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
try {
final bundle = await getTransactionExpanded(hash: tx.txHash);
_updateInputsAndOutputs(tx, bundle);
if (bundle.confirmations > 0) return false;
return bundle.originalTransaction.canReplaceByFee;
} catch (e) {
return false;
}
return BtcTransaction.fromRaw(transactionHex).canReplaceByFee;
}
Future<bool> isChangeSufficientForFee(String txId, int newFee) async {
@ -1458,47 +1448,59 @@ abstract class ElectrumWalletBase
);
}
int totalOutAmount = bundle.originalTransaction.outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
var currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;
// Create a list of available outputs
final outputs = <BitcoinOutput>[];
// 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];
for (final out in bundle.originalTransaction.outputs) {
final address = addressFromOutputScript(out.scriptPubKey, network);
final btcAddress = addressTypeFromStr(address, network);
outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt())));
}
int newAmount;
if (out.amount.toInt() >= remainingFee) {
newAmount = out.amount.toInt() - remainingFee;
remainingFee = 0;
// Calculate the total amount and fees
int totalOutAmount =
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
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 (newAmount <= _dustAmount) {
continue;
if (remainingFee <= 0) {
throw Exception("New fee must be higher than the current fee.");
}
// 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 List<BitcoinOutput> changeOutputs = outputs
.where((output) => changeAddresses
.any((element) => element.address == output.address.toAddress(network)))
.toList();
// look for a change address in the outputs
final changeOutput = outputs.firstWhereOrNull((output) =>
changeAddresses.any((element) => element.address == output.address.toAddress(network)));
int totalChangeAmount =
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
// deduct the change amount from the output amount
if (changeOutput != null) {
totalOutAmount -= changeOutput.value.toInt();
}
// The final amount that the receiver will receive
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
final txb = BitcoinTransactionBuilder(
utxos: utxos,
@ -1527,10 +1529,10 @@ abstract class ElectrumWalletBase
transaction,
type,
electrumClient: electrumClient,
amount: totalOutAmount,
amount: sendingAmount,
fee: newFee,
network: network,
hasChange: changeOutput != null,
hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(),
)..addListener((transaction) async {
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 {

View file

@ -398,9 +398,10 @@ class CWBitcoin extends Bitcoin {
}
@override
Future<bool> canReplaceByFee(Object wallet, String transactionHash) async {
Future<bool> canReplaceByFee(Object wallet, Object transactionInfo) async {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.canReplaceByFee(transactionHash);
final tx = transactionInfo as ElectrumTransactionInfo;
return bitcoinWallet.canReplaceByFee(tx);
}
@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/wownero/wownero.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/view_model/send/output.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
Future<void> replaceByFee(String txId, String newFee) async {
Future<void> replaceByFee(TransactionInfo tx, String newFee) async {
state = IsExecutingState();
final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee);
try {
final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee);
if (!isSufficient) {
state = AwaitingConfirmationState(
title: S.current.confirm_fee_deduction,
message: S.current.confirm_fee_deduction_content,
onConfirm: () async {
pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
state = ExecutedSuccessfullyState();
},
onCancel: () {
state = FailureState('Insufficient change for fee');
});
} else {
pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
if (!isSufficient) {
state = AwaitingConfirmationState(
title: S.current.confirm_fee_deduction,
message: S.current.confirm_fee_deduction_content,
onConfirm: () async => await _executeReplaceByFee(tx, newFee),
onCancel: () => state = FailureState('Insufficient change for fee'));
} else {
await _executeReplaceByFee(tx, newFee);
}
} catch (e) {
state = FailureState(e.toString());
}
}
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();
} catch (e) {
state = FailureState(e.toString());
}
}

View file

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

View file

@ -79,6 +79,7 @@ import 'dart:typed_data';
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/send/output.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/output_info.dart';
@ -204,7 +205,7 @@ abstract class Bitcoin {
bool isTestnet(Object wallet);
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);
int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size});
int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount,