mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-05-03 16:22:14 +00:00
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
This commit is contained in:
parent
d01199bd04
commit
4c2d061363
6 changed files with 132 additions and 78 deletions
cw_bitcoin/lib
lib
tool
|
@ -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)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue