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:
Serhii 2024-11-28 02:42:46 +02:00 committed by GitHub
parent 5ca1c0c56e
commit 4ca50b5e63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1503,14 +1503,21 @@ abstract class ElectrumWalletBase
final bundle = await getTransactionExpanded(hash: txId); final bundle = await getTransactionExpanded(hash: txId);
final outputs = bundle.originalTransaction.outputs; 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 receiverAmount = outputs
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any( .where((output) => !ownAddresses.contains(addressFromOutputScript(output.scriptPubKey, network)))
(element) => element.address == 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++) { for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i]; final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i]; final inputTransaction = bundle.ins[i];
@ -1521,12 +1528,10 @@ abstract class ElectrumWalletBase
int totalOutAmount = bundle.originalTransaction.outputs int totalOutAmount = bundle.originalTransaction.outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt()); .fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
var currentFee = allInputsAmount - totalOutAmount; var currentFee = allInputsAmount - totalOutAmount;
int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee; int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;
return totalBalance - receiverAmount - remainingFee >= _dustAmount;
return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0;
} }
Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async { Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
@ -1534,12 +1539,13 @@ abstract class ElectrumWalletBase
final bundle = await getTransactionExpanded(hash: hash); final bundle = await getTransactionExpanded(hash: hash);
final utxos = <UtxoWithAddress>[]; final utxos = <UtxoWithAddress>[];
final outputs = <BitcoinOutput>[];
List<ECPrivate> privateKeys = []; List<ECPrivate> privateKeys = [];
var allInputsAmount = 0; var allInputsAmount = 0;
String? memo; String? memo;
// Add inputs // Add original inputs
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i]; final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i]; final inputTransaction = bundle.ins[i];
@ -1549,8 +1555,7 @@ abstract class ElectrumWalletBase
allInputsAmount += outTransaction.amount.toInt(); allInputsAmount += outTransaction.amount.toInt();
final addressRecord = final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address); walletAddresses.allAddresses.firstWhere((element) => element.address == address);
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
final privkey = generateECPrivate( final privkey = generateECPrivate(
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
@ -1568,15 +1573,13 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(btcAddress), scriptType: _getScriptType(btcAddress),
), ),
ownerDetails: ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress), UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
), ),
); );
} }
// Create a list of available outputs // Add original outputs
final outputs = <BitcoinOutput>[];
for (final out in bundle.originalTransaction.outputs) { for (final out in bundle.originalTransaction.outputs) {
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script; final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN') && memo == null) { if (script.contains('OP_RETURN') && memo == null) {
final index = script.indexOf('OP_RETURN'); final index = script.indexOf('OP_RETURN');
@ -1598,7 +1601,7 @@ abstract class ElectrumWalletBase
// Calculate the total amount and fees // Calculate the total amount and fees
int totalOutAmount = 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 currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee; int remainingFee = newFee - currentFee;
@ -1606,17 +1609,95 @@ abstract class ElectrumWalletBase
throw Exception("New fee must be higher than the current fee."); 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) { if (remainingFee > 0) {
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
for (int i = outputs.length - 1; i >= 0; i--) { 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) { if (outputAmount > _dustAmount) {
int deduction = (outputAmount - _dustAmount >= remainingFee) int deduction = (outputAmount - _dustAmount >= remainingFee)
? remainingFee ? remainingFee
: outputAmount - _dustAmount; : outputAmount - _dustAmount;
outputs[i] = BitcoinOutput( outputs[i] = BitcoinOutput(
address: outputs[i].address, value: BigInt.from(outputAmount - deduction)); address: output.address, value: BigInt.from(outputAmount - deduction));
remainingFee -= deduction; remainingFee -= deduction;
if (remainingFee <= 0) break; if (remainingFee <= 0) break;
@ -1633,11 +1714,11 @@ abstract class ElectrumWalletBase
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
final List<BitcoinOutput> changeOutputs = outputs final List<BitcoinOutput> changeOutputs = outputs
.where((output) => changeAddresses .where((output) => changeAddresses
.any((element) => element.address == output.address.toAddress(network))) .any((element) => element.address == output.address.toAddress(network)))
.toList(); .toList();
int totalChangeAmount = 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 // The final amount that the receiver will receive
int sendingAmount = allInputsAmount - newFee - totalChangeAmount; int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
@ -1654,8 +1735,7 @@ abstract class ElectrumWalletBase
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = final key =
privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey); privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
if (key == null) { if (key == null) {
throw Exception("Cannot find private key"); throw Exception("Cannot find private key");
} }
@ -1665,6 +1745,7 @@ abstract class ElectrumWalletBase
} else { } else {
return key.signInput(txDigest, sigHash: sighash); return key.signInput(txDigest, sigHash: sighash);
} }
}); });
return PendingBitcoinTransaction( return PendingBitcoinTransaction(
@ -1677,16 +1758,16 @@ abstract class ElectrumWalletBase
hasChange: changeOutputs.isNotEmpty, hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(), feeRate: newFee.toString(),
)..addListener((transaction) async { )..addListener((transaction) async {
transactionHistory.transactions.values.forEach((tx) { transactionHistory.transactions.values.forEach((tx) {
if (tx.id == hash) { if (tx.id == hash) {
tx.isReplaced = true; tx.isReplaced = true;
tx.isPending = false; tx.isPending = false;
transactionHistory.addOne(tx); transactionHistory.addOne(tx);
} }
});
transactionHistory.addOne(transaction);
await updateBalance();
}); });
transactionHistory.addOne(transaction);
await updateBalance();
});
} catch (e) { } catch (e) {
throw e; throw e;
} }