mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 03:29:36 +00:00
Cw 649 rbf improvements opt in (#1772)
* initial commit * revert changes * allow adding inputs to RBF transactions * address review comments[skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
parent
5ca1c0c56e
commit
4ca50b5e63
1 changed files with 113 additions and 32 deletions
|
@ -1503,14 +1503,21 @@ abstract class ElectrumWalletBase
|
|||
final bundle = await getTransactionExpanded(hash: txId);
|
||||
final outputs = bundle.originalTransaction.outputs;
|
||||
|
||||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
||||
final ownAddresses = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
||||
|
||||
// look for a change address in the outputs
|
||||
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
|
||||
(element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
|
||||
final receiverAmount = outputs
|
||||
.where((output) => !ownAddresses.contains(addressFromOutputScript(output.scriptPubKey, network)))
|
||||
.fold<int>(0, (sum, output) => sum + output.amount.toInt());
|
||||
|
||||
var allInputsAmount = 0;
|
||||
if (receiverAmount == 0) {
|
||||
throw Exception("Receiver output not found.");
|
||||
}
|
||||
|
||||
final availableInputs = unspentCoins.where((utxo) => utxo.isSending && !utxo.isFrozen).toList();
|
||||
int totalBalance = availableInputs.fold<int>(
|
||||
0, (previousValue, element) => previousValue + element.value.toInt());
|
||||
|
||||
int allInputsAmount = 0;
|
||||
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
final input = bundle.originalTransaction.inputs[i];
|
||||
final inputTransaction = bundle.ins[i];
|
||||
|
@ -1521,12 +1528,10 @@ 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 > 0) ? newFee - currentFee : newFee;
|
||||
|
||||
return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0;
|
||||
return totalBalance - receiverAmount - remainingFee >= _dustAmount;
|
||||
}
|
||||
|
||||
Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
|
||||
|
@ -1534,12 +1539,13 @@ abstract class ElectrumWalletBase
|
|||
final bundle = await getTransactionExpanded(hash: hash);
|
||||
|
||||
final utxos = <UtxoWithAddress>[];
|
||||
final outputs = <BitcoinOutput>[];
|
||||
List<ECPrivate> privateKeys = [];
|
||||
|
||||
var allInputsAmount = 0;
|
||||
String? memo;
|
||||
|
||||
// Add inputs
|
||||
// Add original inputs
|
||||
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
|
||||
final input = bundle.originalTransaction.inputs[i];
|
||||
final inputTransaction = bundle.ins[i];
|
||||
|
@ -1549,8 +1555,7 @@ abstract class ElectrumWalletBase
|
|||
allInputsAmount += outTransaction.amount.toInt();
|
||||
|
||||
final addressRecord =
|
||||
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
|
||||
|
||||
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
|
||||
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
|
@ -1568,15 +1573,13 @@ abstract class ElectrumWalletBase
|
|||
scriptType: _getScriptType(btcAddress),
|
||||
),
|
||||
ownerDetails:
|
||||
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
|
||||
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Create a list of available outputs
|
||||
final outputs = <BitcoinOutput>[];
|
||||
// Add original outputs
|
||||
for (final out in bundle.originalTransaction.outputs) {
|
||||
// Check if the script contains OP_RETURN
|
||||
final script = out.scriptPubKey.script;
|
||||
if (script.contains('OP_RETURN') && memo == null) {
|
||||
final index = script.indexOf('OP_RETURN');
|
||||
|
@ -1598,7 +1601,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
// Calculate the total amount and fees
|
||||
int totalOutAmount =
|
||||
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
|
||||
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
|
||||
int currentFee = allInputsAmount - totalOutAmount;
|
||||
int remainingFee = newFee - currentFee;
|
||||
|
||||
|
@ -1606,17 +1609,95 @@ abstract class ElectrumWalletBase
|
|||
throw Exception("New fee must be higher than the current fee.");
|
||||
}
|
||||
|
||||
// Deduct Remaining Fee from Main Outputs
|
||||
// Deduct fee from change outputs first, if possible
|
||||
if (remainingFee > 0) {
|
||||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
||||
for (int i = outputs.length - 1; i >= 0; i--) {
|
||||
int outputAmount = outputs[i].value.toInt();
|
||||
final output = outputs[i];
|
||||
final isChange = changeAddresses
|
||||
.any((element) => element.address == output.address.toAddress(network));
|
||||
|
||||
if (isChange) {
|
||||
int outputAmount = output.value.toInt();
|
||||
if (outputAmount > _dustAmount) {
|
||||
int deduction = (outputAmount - _dustAmount >= remainingFee)
|
||||
? remainingFee
|
||||
: outputAmount - _dustAmount;
|
||||
outputs[i] = BitcoinOutput(
|
||||
address: output.address, value: BigInt.from(outputAmount - deduction));
|
||||
remainingFee -= deduction;
|
||||
|
||||
if (remainingFee <= 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still not enough, add UTXOs until the fee is covered
|
||||
if (remainingFee > 0) {
|
||||
final unusedUtxos = unspentCoins
|
||||
.where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0)
|
||||
.toList();
|
||||
|
||||
for (final utxo in unusedUtxos) {
|
||||
final address = RegexUtils.addressTypeFromStr(utxo.address, network);
|
||||
final privkey = generateECPrivate(
|
||||
hd: utxo.bitcoinAddressRecord.isHidden
|
||||
? walletAddresses.sideHd
|
||||
: walletAddresses.mainHd,
|
||||
index: utxo.bitcoinAddressRecord.index,
|
||||
network: network,
|
||||
);
|
||||
privateKeys.add(privkey);
|
||||
|
||||
utxos.add(UtxoWithAddress(
|
||||
utxo: BitcoinUtxo(
|
||||
txHash: utxo.hash,
|
||||
value: BigInt.from(utxo.value),
|
||||
vout: utxo.vout,
|
||||
scriptType: _getScriptType(address)),
|
||||
ownerDetails:
|
||||
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
|
||||
));
|
||||
|
||||
allInputsAmount += utxo.value;
|
||||
remainingFee -= utxo.value;
|
||||
|
||||
if (remainingFee < 0) {
|
||||
final changeOutput = outputs.firstWhereOrNull((output) => walletAddresses.allAddresses
|
||||
.any((addr) => addr.address == output.address.toAddress(network)));
|
||||
if (changeOutput != null) {
|
||||
final newValue = changeOutput.value.toInt() + (-remainingFee);
|
||||
outputs[outputs.indexOf(changeOutput)] =
|
||||
BitcoinOutput(address: changeOutput.address, value: BigInt.from(newValue));
|
||||
} else {
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
outputs.add(BitcoinOutput(
|
||||
address: RegexUtils.addressTypeFromStr(changeAddress.address, network),
|
||||
value: BigInt.from(-remainingFee)));
|
||||
}
|
||||
|
||||
remainingFee = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (remainingFee <= 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Deduct from the receiver's output if remaining fee is still greater than 0
|
||||
if (remainingFee > 0) {
|
||||
for (int i = 0; i < outputs.length; i++) {
|
||||
final output = outputs[i];
|
||||
int outputAmount = output.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));
|
||||
address: output.address, value: BigInt.from(outputAmount - deduction));
|
||||
remainingFee -= deduction;
|
||||
|
||||
if (remainingFee <= 0) break;
|
||||
|
@ -1633,11 +1714,11 @@ abstract class ElectrumWalletBase
|
|||
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
|
||||
final List<BitcoinOutput> changeOutputs = outputs
|
||||
.where((output) => changeAddresses
|
||||
.any((element) => element.address == output.address.toAddress(network)))
|
||||
.any((element) => element.address == output.address.toAddress(network)))
|
||||
.toList();
|
||||
|
||||
int totalChangeAmount =
|
||||
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
|
||||
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
|
||||
|
||||
// The final amount that the receiver will receive
|
||||
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
|
||||
|
@ -1654,8 +1735,7 @@ abstract class ElectrumWalletBase
|
|||
|
||||
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
|
||||
final key =
|
||||
privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
|
||||
|
||||
privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
|
||||
if (key == null) {
|
||||
throw Exception("Cannot find private key");
|
||||
}
|
||||
|
@ -1665,6 +1745,7 @@ abstract class ElectrumWalletBase
|
|||
} else {
|
||||
return key.signInput(txDigest, sigHash: sighash);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return PendingBitcoinTransaction(
|
||||
|
@ -1677,16 +1758,16 @@ abstract class ElectrumWalletBase
|
|||
hasChange: changeOutputs.isNotEmpty,
|
||||
feeRate: newFee.toString(),
|
||||
)..addListener((transaction) async {
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.id == hash) {
|
||||
tx.isReplaced = true;
|
||||
tx.isPending = false;
|
||||
transactionHistory.addOne(tx);
|
||||
}
|
||||
});
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
transactionHistory.transactions.values.forEach((tx) {
|
||||
if (tx.id == hash) {
|
||||
tx.isReplaced = true;
|
||||
tx.isPending = false;
|
||||
transactionHistory.addOne(tx);
|
||||
}
|
||||
});
|
||||
transactionHistory.addOne(transaction);
|
||||
await updateBalance();
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue