wallet.bump_fee: add new strategy: decrease payment amounts
- Rename bump_fee "methods" to "strategies". - Refactor strategies so that bump_fee can use any subset of them in any permutation. - Adds a new strategy which decreases the payment outputs (instead of change).
This commit is contained in:
@@ -238,7 +238,7 @@ class CoinChooserBase(Logger):
|
||||
assert is_address(change_addrs[0])
|
||||
|
||||
# This takes a count of change outputs and returns a tx fee
|
||||
output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
|
||||
output_weight = 4 * Transaction.estimated_output_size_for_address(change_addrs[0])
|
||||
fee_estimator_numchange = lambda count: fee_estimator_w(tx_weight + count * output_weight)
|
||||
change = self._change_outputs(tx, change_addrs, fee_estimator_numchange, dust_threshold)
|
||||
tx.add_outputs(change)
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestTransaction(ElectrumTestCase):
|
||||
self.assertEqual(tx.estimated_size(), 193)
|
||||
|
||||
def test_estimated_output_size(self):
|
||||
estimated_output_size = transaction.Transaction.estimated_output_size
|
||||
estimated_output_size = transaction.Transaction.estimated_output_size_for_address
|
||||
self.assertEqual(estimated_output_size('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), 34)
|
||||
self.assertEqual(estimated_output_size('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), 32)
|
||||
self.assertEqual(estimated_output_size('bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af'), 31)
|
||||
|
||||
@@ -10,7 +10,8 @@ from electrum import storage, bitcoin, keystore, bip32, wallet
|
||||
from electrum import Transaction
|
||||
from electrum import SimpleConfig
|
||||
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT
|
||||
from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text, Abstract_Wallet
|
||||
from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet,
|
||||
restore_wallet_from_text, Abstract_Wallet, BumpFeeStrategy)
|
||||
from electrum.util import bfh, bh2u, create_and_start_event_loop
|
||||
from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxOutput, PartialTxInput, tx_from_any
|
||||
from electrum.mnemonic import seed_type
|
||||
@@ -937,6 +938,14 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment", simulate_moving_txs=simulate_moving_txs):
|
||||
self._bump_fee_p2wpkh_decrease_payment(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment_batch", simulate_moving_txs=simulate_moving_txs):
|
||||
self._bump_fee_p2wpkh_decrease_payment_batch(
|
||||
simulate_moving_txs=simulate_moving_txs,
|
||||
config=config)
|
||||
|
||||
def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config):
|
||||
wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean',
|
||||
@@ -1044,6 +1053,89 @@ class TestWalletSending(TestCaseForTestnet):
|
||||
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
|
||||
self.assertEqual((0, 461600, 0), wallet.get_balance())
|
||||
|
||||
def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config):
|
||||
wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',
|
||||
config=config)
|
||||
|
||||
# bootstrap wallet
|
||||
funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00')
|
||||
funding_txid = funding_tx.txid()
|
||||
self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid)
|
||||
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
orig_rbf_tx = Transaction('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a45850c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a02473044022076d298537b524a926a8fadad0e9ded5868c8f4cf29246048f76f00eb4afa56310220739ad9e0417e97ce03fad98a454b4977972c2805cef37bfa822c6d6c56737c870121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d48a1d00')
|
||||
orig_rbf_txid = orig_rbf_tx.txid()
|
||||
self.assertEqual('db2f77709a4a04417b3a45838c21470877fe7c182a4f81005a21ce1315c6a5e6', orig_rbf_txid)
|
||||
wallet.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
# bump tx
|
||||
tx = wallet.bump_fee(
|
||||
tx=tx_from_any(orig_rbf_tx.serialize()),
|
||||
new_fee_rate=60,
|
||||
strategies=[BumpFeeStrategy.DECREASE_PAYMENT],
|
||||
)
|
||||
tx.locktime = 1936085
|
||||
tx.version = 2
|
||||
if simulate_moving_txs:
|
||||
partial_tx = tx.serialize_as_bytes().hex()
|
||||
self.assertEqual("70736274ff010071020000000146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a458ccb5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5ad58a1d00000100fd7201020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d002206024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af410ce2dd7cb00000080000000000000000000220203ecb63cc22d200c96225671b88a51a71deb053c6445dbd4694f61166e3e5bd05910ce2dd7cb0000008001000000000000000000",
|
||||
partial_tx)
|
||||
tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
|
||||
self.assertFalse(tx.is_complete())
|
||||
|
||||
wallet.sign_transaction(tx, password=None)
|
||||
self.assertTrue(tx.is_complete())
|
||||
self.assertTrue(tx.is_segwit())
|
||||
tx_copy = tx_from_any(tx.serialize())
|
||||
self.assertEqual('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a458ccb5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a024730440220063a2d330f0d659b3f686cc291722a87cc37371d3520c946e74da8dbbd4c57e00220604b0f387754988f71af47db78263698a513173e8ce3b27a696b9e3954ba757b0121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d58a1d00',
|
||||
str(tx_copy))
|
||||
self.assertEqual('6b03c00f47cb145ffb632c3ce54dece29b9a980949ef5c574321f7fc83fa2238', tx_copy.txid())
|
||||
self.assertEqual('cb1f123231a3de5b02babddb43208f0273cb0df8addd4275583234eb50c7a87d', tx_copy.wtxid())
|
||||
|
||||
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
|
||||
self.assertEqual((0, 45000, 0), wallet.get_balance())
|
||||
|
||||
def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config):
|
||||
wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label',
|
||||
config=config)
|
||||
|
||||
# bootstrap wallet
|
||||
funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00')
|
||||
funding_txid = funding_tx.txid()
|
||||
self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid)
|
||||
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
orig_rbf_tx = Transaction('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff05e803000000000000160014a01f6b2a4bdaf3fb61f2a45e5eac92fcc58daee3881300000000000016001470fcde1ed0159ba5af97baec085ceb857098cedb0c49000000000000160014999a95482213a896c72a251b6cc9f3d137b0a458a86100000000000016001440c234c451fbd9ddf7824d6b8f0dc968a220946450c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a024730440220782fb75f2398997ac77cd1b5c0d78f30a66b83df1d2d21c7a06cb03eb592d91702200540cf329c4b21e26aaba79a0c0ebdf465c4befb76a61e4eec924bc482cbf2930121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4a58a1d00')
|
||||
orig_rbf_txid = orig_rbf_tx.txid()
|
||||
self.assertEqual('9e0c7d890053c47c7cd653be984bc4b9a5dab8acf9a6ae075a00113d3077ad74', orig_rbf_txid)
|
||||
wallet.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED)
|
||||
|
||||
# bump tx
|
||||
tx = wallet.bump_fee(
|
||||
tx=tx_from_any(orig_rbf_tx.serialize()),
|
||||
new_fee_rate=60,
|
||||
strategies=[BumpFeeStrategy.DECREASE_PAYMENT],
|
||||
)
|
||||
tx.locktime = 1936095
|
||||
tx.version = 2
|
||||
if simulate_moving_txs:
|
||||
partial_tx = tx.serialize_as_bytes().hex()
|
||||
self.assertEqual("70736274ff0100af020000000146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff045d0500000000000016001470fcde1ed0159ba5af97baec085ceb857098cedb0c49000000000000160014999a95482213a896c72a251b6cc9f3d137b0a4587d5300000000000016001440c234c451fbd9ddf7824d6b8f0dc968a220946425b5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5adf8a1d00000100fd7201020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d002206024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af410ce2dd7cb0000008000000000000000000000220203ecb63cc22d200c96225671b88a51a71deb053c6445dbd4694f61166e3e5bd05910ce2dd7cb000000800100000000000000000000",
|
||||
partial_tx)
|
||||
tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners
|
||||
self.assertFalse(tx.is_complete())
|
||||
|
||||
wallet.sign_transaction(tx, password=None)
|
||||
self.assertTrue(tx.is_complete())
|
||||
self.assertTrue(tx.is_segwit())
|
||||
tx_copy = tx_from_any(tx.serialize())
|
||||
self.assertEqual('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff045d0500000000000016001470fcde1ed0159ba5af97baec085ceb857098cedb0c49000000000000160014999a95482213a896c72a251b6cc9f3d137b0a4587d5300000000000016001440c234c451fbd9ddf7824d6b8f0dc968a220946425b5000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a024730440220477ff315d3ac58de3bc1ec0b44b90a90da9bc09c440982fd9a1563eae98df0dc0220574033b0e306d388edcc77e4c2b39338fc8f182c747014aef3ce2c99cf9e5e960121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4df8a1d00',
|
||||
str(tx_copy))
|
||||
self.assertEqual('bc86f4f14fea5305b197c02ae7b0d6b04c5f49144d9ad37c9f64ec0ec6d34594', tx_copy.txid())
|
||||
self.assertEqual('368e4c0429b38e66ac64ac9dbb66145c9f28dfaf2fad60f6424db32c379a12da', tx_copy.wtxid())
|
||||
|
||||
wallet.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED)
|
||||
self.assertEqual((0, 18700, 0), wallet.get_balance())
|
||||
|
||||
@mock.patch.object(wallet.Abstract_Wallet, 'save_db')
|
||||
def test_cpfp_p2pkh(self, mock_save_db):
|
||||
|
||||
@@ -892,11 +892,18 @@ class Transaction:
|
||||
return 4 * input_size + witness_size
|
||||
|
||||
@classmethod
|
||||
def estimated_output_size(cls, address):
|
||||
def estimated_output_size_for_address(cls, address: str) -> int:
|
||||
"""Return an estimate of serialized output size in bytes."""
|
||||
script = bitcoin.address_to_script(address)
|
||||
# 8 byte value + 1 byte script len + script
|
||||
return 9 + len(script) // 2
|
||||
return cls.estimated_output_size_for_script(script)
|
||||
|
||||
@classmethod
|
||||
def estimated_output_size_for_script(cls, script: str) -> int:
|
||||
"""Return an estimate of serialized output size in bytes."""
|
||||
# 8 byte value + varint script len + script
|
||||
script_len = len(script) // 2
|
||||
var_int_len = len(var_int(script_len)) // 2
|
||||
return 8 + var_int_len + script_len
|
||||
|
||||
@classmethod
|
||||
def virtual_size_from_weight(cls, weight):
|
||||
|
||||
@@ -44,6 +44,7 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequ
|
||||
from abc import ABC, abstractmethod
|
||||
import itertools
|
||||
import threading
|
||||
import enum
|
||||
|
||||
from aiorpcx import TaskGroup
|
||||
|
||||
@@ -97,6 +98,12 @@ TX_STATUS = [
|
||||
]
|
||||
|
||||
|
||||
class BumpFeeStrategy(enum.Enum):
|
||||
COINCHOOSER = enum.auto()
|
||||
DECREASE_CHANGE = enum.auto()
|
||||
DECREASE_PAYMENT = enum.auto()
|
||||
|
||||
|
||||
async def _append_utxos_to_inputs(*, inputs: List[PartialTxInput], network: 'Network',
|
||||
pubkey: str, txin_type: str, imax: int) -> None:
|
||||
if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
|
||||
@@ -1439,6 +1446,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
txid: str = None,
|
||||
new_fee_rate: Union[int, float, Decimal],
|
||||
coins: Sequence[PartialTxInput] = None,
|
||||
strategies: Sequence[BumpFeeStrategy] = None,
|
||||
) -> PartialTransaction:
|
||||
"""Increase the miner fee of 'tx'.
|
||||
'new_fee_rate' is the target min rate in sat/vbyte
|
||||
@@ -1465,29 +1473,42 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
old_fee_rate = old_fee / old_tx_size # sat/vbyte
|
||||
if new_fee_rate <= old_fee_rate:
|
||||
raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
|
||||
try:
|
||||
# method 1: keep all inputs, keep all not is_mine outputs,
|
||||
# allow adding new inputs
|
||||
tx_new = self._bump_fee_through_coinchooser(
|
||||
tx=tx,
|
||||
txid=txid,
|
||||
new_fee_rate=new_fee_rate,
|
||||
coins=coins,
|
||||
)
|
||||
method_used = 1
|
||||
except CannotBumpFee:
|
||||
# method 2: keep all inputs, no new inputs are added,
|
||||
# allow decreasing and removing outputs (change is decreased first)
|
||||
# This is less "safe" as it might end up decreasing e.g. a payment to a merchant;
|
||||
# but e.g. if the user has sent "Max" previously, this is the only way to RBF.
|
||||
tx_new = self._bump_fee_through_decreasing_outputs(
|
||||
tx=tx, new_fee_rate=new_fee_rate)
|
||||
method_used = 2
|
||||
|
||||
if not strategies:
|
||||
strategies = [BumpFeeStrategy.COINCHOOSER, BumpFeeStrategy.DECREASE_CHANGE]
|
||||
tx_new = None
|
||||
exc = None
|
||||
for strat in strategies:
|
||||
try:
|
||||
if strat == BumpFeeStrategy.COINCHOOSER:
|
||||
tx_new = self._bump_fee_through_coinchooser(
|
||||
tx=tx,
|
||||
txid=txid,
|
||||
new_fee_rate=new_fee_rate,
|
||||
coins=coins,
|
||||
)
|
||||
elif strat == BumpFeeStrategy.DECREASE_CHANGE:
|
||||
tx_new = self._bump_fee_through_decreasing_change(
|
||||
tx=tx, new_fee_rate=new_fee_rate)
|
||||
elif strat == BumpFeeStrategy.DECREASE_PAYMENT:
|
||||
tx_new = self._bump_fee_through_decreasing_payment(
|
||||
tx=tx, new_fee_rate=new_fee_rate)
|
||||
else:
|
||||
raise NotImplementedError(f"unexpected strategy: {strat}")
|
||||
except CannotBumpFee as e:
|
||||
exc = e
|
||||
else:
|
||||
strat_used = strat
|
||||
break
|
||||
if tx_new is None:
|
||||
assert exc
|
||||
raise exc # all strategies failed, re-raise last exception
|
||||
|
||||
target_min_fee = new_fee_rate * tx_new.estimated_size()
|
||||
actual_fee = tx_new.get_fee()
|
||||
if actual_fee + 1 < target_min_fee:
|
||||
raise CannotBumpFee(
|
||||
f"bump_fee fee target was not met (method: {method_used}). "
|
||||
f"bump_fee fee target was not met (strategy: {strat_used}). "
|
||||
f"got {actual_fee}, expected >={target_min_fee}. "
|
||||
f"target rate was {new_fee_rate}")
|
||||
tx_new.locktime = get_locktime_for_new_transaction(self.network)
|
||||
@@ -1503,6 +1524,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
new_fee_rate: Union[int, Decimal],
|
||||
coins: Sequence[PartialTxInput] = None,
|
||||
) -> PartialTransaction:
|
||||
"""Increase the miner fee of 'tx'.
|
||||
|
||||
- keeps all inputs
|
||||
- keeps all not is_mine outputs,
|
||||
- allows adding new inputs
|
||||
"""
|
||||
assert txid
|
||||
tx = copy.deepcopy(tx)
|
||||
tx.add_info_from_wallet(self)
|
||||
@@ -1549,12 +1576,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
except NotEnoughFunds as e:
|
||||
raise CannotBumpFee(e)
|
||||
|
||||
def _bump_fee_through_decreasing_outputs(
|
||||
def _bump_fee_through_decreasing_change(
|
||||
self,
|
||||
*,
|
||||
tx: PartialTransaction,
|
||||
new_fee_rate: Union[int, Decimal],
|
||||
) -> PartialTransaction:
|
||||
"""Increase the miner fee of 'tx'.
|
||||
|
||||
- keeps all inputs
|
||||
- no new inputs are added
|
||||
- allows decreasing and removing outputs (change is decreased first)
|
||||
This is less "safe" than "coinchooser" method as it might end up decreasing
|
||||
e.g. a payment to a merchant; but e.g. if the user has sent "Max" previously,
|
||||
this is the only way to RBF.
|
||||
"""
|
||||
tx = copy.deepcopy(tx)
|
||||
tx.add_info_from_wallet(self)
|
||||
assert tx.get_fee() is not None
|
||||
@@ -1594,6 +1630,67 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
||||
|
||||
return PartialTransaction.from_io(inputs, outputs)
|
||||
|
||||
def _bump_fee_through_decreasing_payment(
|
||||
self,
|
||||
*,
|
||||
tx: PartialTransaction,
|
||||
new_fee_rate: Union[int, Decimal],
|
||||
) -> PartialTransaction:
|
||||
"""Increase the miner fee of 'tx'.
|
||||
|
||||
- keeps all inputs
|
||||
- no new inputs are added
|
||||
- decreases payment outputs (not change!). Each non-ismine output is decreased
|
||||
proportionally to their byte-size.
|
||||
"""
|
||||
tx = copy.deepcopy(tx)
|
||||
tx.add_info_from_wallet(self)
|
||||
assert tx.get_fee() is not None
|
||||
inputs = tx.inputs()
|
||||
outputs = tx.outputs()
|
||||
|
||||
# select non-ismine outputs
|
||||
s = [(idx, out) for (idx, out) in enumerate(outputs)
|
||||
if not self.is_mine(out.address)]
|
||||
# exempt 2fa fee output if present
|
||||
x_fee = run_hook('get_tx_extra_fee', self, tx)
|
||||
if x_fee:
|
||||
x_fee_address, x_fee_amount = x_fee
|
||||
s = [(idx, out) for (idx, out) in s if out.address != x_fee_address]
|
||||
if not s:
|
||||
raise CannotBumpFee("Cannot find payment output")
|
||||
|
||||
del_out_idxs = set()
|
||||
tx_size = tx.estimated_size()
|
||||
cur_fee = tx.get_fee()
|
||||
# Main loop. Each iteration decreases value of all selected outputs.
|
||||
# The number of iterations is bounded by len(s) as only the final iteration
|
||||
# can *not remove* any output.
|
||||
for __ in range(len(s) + 1):
|
||||
target_fee = int(math.ceil(tx_size * new_fee_rate))
|
||||
delta_total = target_fee - cur_fee
|
||||
if delta_total <= 0:
|
||||
break
|
||||
out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey.hex())
|
||||
for (idx, out) in s if idx not in del_out_idxs)
|
||||
for idx, out in s:
|
||||
out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey.hex())
|
||||
delta = int(math.ceil(delta_total * out_size / out_size_total))
|
||||
if out.value - delta >= self.dust_threshold():
|
||||
new_output_value = out.value - delta
|
||||
assert isinstance(new_output_value, int)
|
||||
outputs[idx].value = new_output_value
|
||||
cur_fee += delta
|
||||
else: # remove output
|
||||
tx_size -= out_size
|
||||
cur_fee += out.value
|
||||
del_out_idxs.add(idx)
|
||||
if delta_total > 0:
|
||||
raise CannotBumpFee(_('Could not find suitable outputs'))
|
||||
|
||||
outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]
|
||||
return PartialTransaction.from_io(inputs, outputs)
|
||||
|
||||
def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
|
||||
txid = tx.txid()
|
||||
for i, o in enumerate(tx.outputs()):
|
||||
|
||||
Reference in New Issue
Block a user