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>
Serhii committed
Nov 28, 2024 at 02:42 UTC
4ca50b5e6329dd5be066396ccb2e5a4c0483df53
1 file changed
+113
-32
cw_bitcoin/lib/electrum_wallet.dart
+113
-32
@@ -1503,14 +1503,21 @@ abstract class ElectrumWalletBase
1503
final bundle = await getTransactionExpanded(hash: txId);
1504
final outputs = bundle.originalTransaction.outputs;
1505
1506
- final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
1506
+ final ownAddresses = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
1507
1508
- // look for a change address in the outputs
1509
- final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
1510
- (element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
1508
+ final receiverAmount = outputs
1509
+ .where((output) => !ownAddresses.contains(addressFromOutputScript(output.scriptPubKey, network)))
1510
+ .fold<int>(0, (sum, output) => sum + output.amount.toInt());
1511
1512
- var allInputsAmount = 0;
1512
+ if (receiverAmount == 0) {
1513
+ throw Exception("Receiver output not found.");
1514
+ }
1515
+
1516
+ final availableInputs = unspentCoins.where((utxo) => utxo.isSending && !utxo.isFrozen).toList();
1517
+ int totalBalance = availableInputs.fold<int>(
1518
+ 0, (previousValue, element) => previousValue + element.value.toInt());
1519
1520
+ int allInputsAmount = 0;
1521
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
1522
final input = bundle.originalTransaction.inputs[i];
1523
final inputTransaction = bundle.ins[i];
@@ -1521,12 +1528,10 @@ abstract class ElectrumWalletBase
1528
1529
int totalOutAmount = bundle.originalTransaction.outputs
1530
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());
1524
-
1531
var currentFee = allInputsAmount - totalOutAmount;
1532
1533
int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;
1528
-
1529
- return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0;
1534
+ return totalBalance - receiverAmount - remainingFee >= _dustAmount;
1535
}
1536
1537
Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
@@ -1534,12 +1539,13 @@ abstract class ElectrumWalletBase
1539
final bundle = await getTransactionExpanded(hash: hash);
1540
1541
final utxos = <UtxoWithAddress>[];
1542
+ final outputs = <BitcoinOutput>[];
1543
List<ECPrivate> privateKeys = [];
1544
1545
var allInputsAmount = 0;
1546
String? memo;
1547
1542
- // Add inputs
1548
+ // Add original inputs
1549
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
1550
final input = bundle.originalTransaction.inputs[i];
1551
final inputTransaction = bundle.ins[i];
@@ -1549,8 +1555,7 @@ abstract class ElectrumWalletBase
1555
allInputsAmount += outTransaction.amount.toInt();
1556
1557
final addressRecord =
1552
- walletAddresses.allAddresses.firstWhere((element) => element.address == address);
1553
-
1558
+ walletAddresses.allAddresses.firstWhere((element) => element.address == address);
1559
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
1560
final privkey = generateECPrivate(
1561
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
@@ -1568,15 +1573,13 @@ abstract class ElectrumWalletBase
1573
scriptType: _getScriptType(btcAddress),
1574
),
1575
ownerDetails:
1571
- UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
1576
+ UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
1577
),
1578
);
1579
}
1580
1576
- // Create a list of available outputs
1577
- final outputs = <BitcoinOutput>[];
1581
+ // Add original outputs
1582
for (final out in bundle.originalTransaction.outputs) {
1579
- // Check if the script contains OP_RETURN
1583
final script = out.scriptPubKey.script;
1584
if (script.contains('OP_RETURN') && memo == null) {
1585
final index = script.indexOf('OP_RETURN');
@@ -1598,7 +1601,7 @@ abstract class ElectrumWalletBase
1601
1602
// Calculate the total amount and fees
1603
int totalOutAmount =
1601
- outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
1604
+ outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
1605
int currentFee = allInputsAmount - totalOutAmount;
1606
int remainingFee = newFee - currentFee;
1607
@@ -1606,17 +1609,95 @@ abstract class ElectrumWalletBase
1609
throw Exception("New fee must be higher than the current fee.");
1610
}
1611
1609
- // Deduct Remaining Fee from Main Outputs
1612
+ // Deduct fee from change outputs first, if possible
1613
if (remainingFee > 0) {
1614
+ final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
1615
for (int i = outputs.length - 1; i >= 0; i--) {
1612
- int outputAmount = outputs[i].value.toInt();
1616
+ final output = outputs[i];
1617
+ final isChange = changeAddresses
1618
+ .any((element) => element.address == output.address.toAddress(network));
1619
+
1620
+ if (isChange) {
1621
+ int outputAmount = output.value.toInt();
1622
+ if (outputAmount > _dustAmount) {
1623
+ int deduction = (outputAmount - _dustAmount >= remainingFee)
1624
+ ? remainingFee
1625
+ : outputAmount - _dustAmount;
1626
+ outputs[i] = BitcoinOutput(
1627
+ address: output.address, value: BigInt.from(outputAmount - deduction));
1628
+ remainingFee -= deduction;
1629
+
1630
+ if (remainingFee <= 0) break;
1631
+ }
1632
+ }
1633
+ }
1634
+ }
1635
+
1636
+ // If still not enough, add UTXOs until the fee is covered
1637
+ if (remainingFee > 0) {
1638
+ final unusedUtxos = unspentCoins
1639
+ .where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0)
1640
+ .toList();
1641
+
1642
+ for (final utxo in unusedUtxos) {
1643
+ final address = RegexUtils.addressTypeFromStr(utxo.address, network);
1644
+ final privkey = generateECPrivate(
1645
+ hd: utxo.bitcoinAddressRecord.isHidden
1646
+ ? walletAddresses.sideHd
1647
+ : walletAddresses.mainHd,
1648
+ index: utxo.bitcoinAddressRecord.index,
1649
+ network: network,
1650
+ );
1651
+ privateKeys.add(privkey);
1652
+
1653
+ utxos.add(UtxoWithAddress(
1654
+ utxo: BitcoinUtxo(
1655
+ txHash: utxo.hash,
1656
+ value: BigInt.from(utxo.value),
1657
+ vout: utxo.vout,
1658
+ scriptType: _getScriptType(address)),
1659
+ ownerDetails:
1660
+ UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
1661
+ ));
1662
+
1663
+ allInputsAmount += utxo.value;
1664
+ remainingFee -= utxo.value;
1665
+
1666
+ if (remainingFee < 0) {
1667
+ final changeOutput = outputs.firstWhereOrNull((output) => walletAddresses.allAddresses
1668
+ .any((addr) => addr.address == output.address.toAddress(network)));
1669
+ if (changeOutput != null) {
1670
+ final newValue = changeOutput.value.toInt() + (-remainingFee);
1671
+ outputs[outputs.indexOf(changeOutput)] =
1672
+ BitcoinOutput(address: changeOutput.address, value: BigInt.from(newValue));
1673
+ } else {
1674
+ final changeAddress = await walletAddresses.getChangeAddress();
1675
+ outputs.add(BitcoinOutput(
1676
+ address: RegexUtils.addressTypeFromStr(changeAddress.address, network),
1677
+ value: BigInt.from(-remainingFee)));
1678
+ }
1679
+
1680
+ remainingFee = 0;
1681
+ break;
1682
+ }
1683
+
1684
+ if (remainingFee <= 0) break;
1685
+ }
1686
+ }
1687
+
1688
+ // Deduct from the receiver's output if remaining fee is still greater than 0
1689
+ if (remainingFee > 0) {
1690
+ for (int i = 0; i < outputs.length; i++) {
1691
+ final output = outputs[i];
1692
+ int outputAmount = output.value.toInt();
1693
1694
if (outputAmount > _dustAmount) {
1695
int deduction = (outputAmount - _dustAmount >= remainingFee)
1696
? remainingFee
1697
: outputAmount - _dustAmount;
1698
+
1699
outputs[i] = BitcoinOutput(
1619
- address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
1700
+ address: output.address, value: BigInt.from(outputAmount - deduction));
1701
remainingFee -= deduction;
1702
1703
if (remainingFee <= 0) break;
@@ -1633,11 +1714,11 @@ abstract class ElectrumWalletBase
1714
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
1715
final List<BitcoinOutput> changeOutputs = outputs
1716
.where((output) => changeAddresses
1636
- .any((element) => element.address == output.address.toAddress(network)))
1717
+ .any((element) => element.address == output.address.toAddress(network)))
1718
.toList();
1719
1720
int totalChangeAmount =
1640
- changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
1721
+ changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
1722
1723
// The final amount that the receiver will receive
1724
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
@@ -1654,8 +1735,7 @@ abstract class ElectrumWalletBase
1735
1736
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
1737
final key =
1657
- privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
1658
-
1738
+ privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
1739
if (key == null) {
1740
throw Exception("Cannot find private key");
1741
}
@@ -1665,6 +1745,7 @@ abstract class ElectrumWalletBase
1745
} else {
1746
return key.signInput(txDigest, sigHash: sighash);
1747
}
1748
+
1749
});
1750
1751
return PendingBitcoinTransaction(
@@ -1677,16 +1758,16 @@ abstract class ElectrumWalletBase
1758
hasChange: changeOutputs.isNotEmpty,
1759
feeRate: newFee.toString(),
1760
)..addListener((transaction) async {
1680
- transactionHistory.transactions.values.forEach((tx) {
1681
- if (tx.id == hash) {
1682
- tx.isReplaced = true;
1683
- tx.isPending = false;
1684
- transactionHistory.addOne(tx);
1685
- }
1686
- });
1687
- transactionHistory.addOne(transaction);
1688
- await updateBalance();
1761
+ transactionHistory.transactions.values.forEach((tx) {
1762
+ if (tx.id == hash) {
1763
+ tx.isReplaced = true;
1764
+ tx.isPending = false;
1765
+ transactionHistory.addOne(tx);
1766
+ }
1767
});
1768
+ transactionHistory.addOne(transaction);
1769
+ await updateBalance();
1770
+ });
1771
} catch (e) {
1772
throw e;
1773
}