1
0

Merge pull request #9704 from f321x/future_tx_fee

tx / gui: Disable output value rounding for 0 fee tx and don't show relay fee warning in gui
This commit is contained in:
ghost43
2025-04-07 15:40:11 +00:00
committed by GitHub
6 changed files with 56 additions and 14 deletions

View File

@@ -201,7 +201,10 @@ class CoinChooserBase(Logger):
# Last change output. Round down to maximum precision but lose
# no more than 10**max_dp_to_round_for_privacy
# e.g. a max of 2 decimal places means losing 100 satoshis to fees
max_dp_to_round_for_privacy = 2 if self.enable_output_value_rounding else 0
# don't round if the fee estimator is set to 0 fixed fee, so a 0 fee tx remains a 0 fee tx
is_zero_fee_tx = True if fee_estimator_numchange(1) == 0 else False
output_value_rounding = self.enable_output_value_rounding and not is_zero_fee_tx
max_dp_to_round_for_privacy = 2 if output_value_rounding else 0
N = int(pow(10, min(max_dp_to_round_for_privacy, zeroes[0])))
amount = (remaining // N) * N
amounts.append(amount)

View File

@@ -299,7 +299,7 @@ class TxFeeSlider(FeeSlider):
if invoice_amt == 0:
invoice_amt = tx.output_value()
fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning(
invoice_amt=invoice_amt, tx_size=tx.estimated_size(), fee=tx.get_fee())
invoice_amt=invoice_amt, tx_size=tx.estimated_size(), fee=tx.get_fee(), txid=tx.txid())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
self.warning = _('Warning') + ': ' + long_warning

View File

@@ -517,7 +517,7 @@ class TxEditor(WindowModalDialog):
amount = self.tx.output_value() if self.output_value == '!' else self.output_value
tx_size = self.tx.estimated_size()
fee_warning_tuple = self.wallet.get_tx_fee_warning(
invoice_amt=amount, tx_size=tx_size, fee=fee)
invoice_amt=amount, tx_size=tx_size, fee=fee, txid=self.tx.txid())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
if not allow_send:

View File

@@ -905,7 +905,7 @@ class TxDialog(QDialog, MessageBoxMixin):
# 'amount' is zero for self-payments, so in that case we use sum-of-outputs
invoice_amt = abs(amount) if amount else self.tx.output_value()
fee_warning_tuple = self.wallet.get_tx_fee_warning(
invoice_amt=invoice_amt, tx_size=size, fee=fee)
invoice_amt=invoice_amt, tx_size=size, fee=fee, txid=self.tx.txid())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
fee_str += " - <font color={color}>{header}: {body}</font>".format(

View File

@@ -3276,16 +3276,18 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self, *,
invoice_amt: int,
tx_size: int,
fee: int) -> Optional[Tuple[bool, str, str]]:
fee: int,
txid: Optional[str]) -> Optional[Tuple[bool, str, str]]:
assert invoice_amt >= 0, f"{invoice_amt=!r} must be non-negative satoshis"
assert fee >= 0, f"{fee=!r} must be non-negative satoshis"
is_future_tx = txid is not None and txid in self.adb.future_tx
feerate = Decimal(fee) / tx_size # sat/byte
fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
long_warning = None
short_warning = None
allow_send = True
if feerate < self.relayfee() / 1000:
if feerate < self.relayfee() / 1000 and not is_future_tx:
long_warning = ' '.join([
_("This transaction requires a higher fee, or it will not be propagated by your current server."),
_("Try to raise your transaction fee, or use a server with a lower relay fee.")

View File

@@ -1,14 +1,38 @@
from electrum.coinchooser import CoinChooserPrivacy
from electrum.util import NotEnoughFunds
from electrum.transaction import PartialTxInput, TxOutpoint, Transaction
from electrum.fee_policy import FeePolicy
from electrum.transaction import PartialTxInput, TxOutpoint, Transaction, PartialTxOutput
from electrum.fee_policy import FeePolicy, FixedFeePolicy
from functools import partial
from typing import Optional
from . import ElectrumTestCase
class TestCoinChooser(ElectrumTestCase):
@staticmethod
def get_dummy_txin_1_284_474_sat() -> PartialTxInput:
# value of 1_284_474 sat
prevout_txid = bytes.fromhex(
"b3d9174cb5d3234764a089bb91fdbd1117b7958be4870d1a544136ab017a67dd"
)
coin = PartialTxInput(
prevout=TxOutpoint(txid=prevout_txid, out_idx=0),
)
coin.utxo = Transaction(
"02000000000105a5a00ad10e754a17154446bbe1c557b44b86a7cd53308ad9ab813388a9d6d1520000000000fdffffff2c48659d10a752b0c0a3efa4092ebee5210943a2f41ff0607ba0e03a4cdf7bbd0000000000fdffffff93f4feb485581654caffa523c500dc417a98097fb731045040bb162acb3e14e90000000000fdffffffbf4b69292acaabf8a415db412eedd8a202d4dd2ca12e532628a756276fec00f50000000000fdffffffa96e18c45ecc56608d0be3c1b2cd93e52d569a9c3a68ed51aead570beeef29ff0000000000fdffffff017a991300000000001600142a55fbef3e419e1c862632a826ae89ade0b07e3a0247304402204de416135d26711df2cbd5209e3f79f95a1de5ddea5980215606ebfe639747bd0220286f9de38a96a078c818e97fe6fbbbe3637287461cd7407adf9e85ba6d899005012102c329033555adccaadb6c83fb486f540ba00aad3edba7a4ec3347b5cc0935c4050247304402200132c1c4c41b840f05efeacf96cccedab38d68c9021c79f940738572b049c1a302200d59ba4719d4d4e55ced651cde35cbf79838bf7122cbafd6584dd934a67db0ed01210380194ab3704b5524a0c97f78b4458b80efd365faaedaebe90fa7807eeab041700247304402202c966fbea5db4bb3794e843b59240d678a3c8e97be1d10c18475a467c101b97c02203edb0ca11e7605af2437ddffbbe416317bca8788c23c4816c13698042222d30f0121035edcdcad9affcff41302ad49a19ccbd47ae50b153ba7c2abaaf3bb2d22c859120247304402206890a622513bb9c8b8ca83e5e82532f9753ecd3188c27d8da9452e264f5500cc022012dad8fb872478b7f9d0d9d310859fca6534b8e9f44511eeef5450beaf13c690012103f683aa56e036b1307c1ae75e81553b6730aea312560e36afad487ba2bc6cf98f02473044022006807df115d6bce73e384651fbb9e932ee313133218be7769e52809b758529b6022078b2859f98a62211f130e69982f96d2968e1820c58bb817f9ab31645b6b4ceae0121026402c3c0a2b4dd5703686f8c5b5b3dcecbbf4d772ef83e440bfad22b742753e730a60300"
)
coin.block_height = 100
return coin
@staticmethod
def get_dummy_txout_1(amount: Optional[int] = 1000000) -> PartialTxOutput:
output = PartialTxOutput.from_address_and_value(
address="bc1q2089yvkkyw7yq7m6a7lxt45n35c587hk4sgj7c",
value=amount,
)
return output
def test_bucket_candidates_with_empty_buckets(self):
def sufficient_funds(buckets, *, bucket_value_sum):
return True
@@ -34,12 +58,7 @@ class TestCoinChooser(ElectrumTestCase):
single_txin.utxo = Transaction("02000000000101956449bdc8059b680a20483e64e139ce63fe64333b92cd7811a1b116d6b967ad0000000000fdffffff024a01000000000000160014a21d1fbcf571153f57b40855e059c134405a89ecd682010000000000160014fd7debf75d6c410bf6ba1c8ba05f90f23ce4646a0247304402207f07ec0c2415b31743527dea2f7bff3868f494dc0a5d45adec5e05031725a0af02202aa0ac7d06dbcad8ac0b9808a829b6bdaa98bc831aef31a5ab4e5d1890f7552101210278a5d9b2796f2743ccf1b36b2bf47695d766d0841c17b00ce83943c8b37dde0ceea60300")
# dummy input to be used as potential additional input of higher value
prevout_txid = bytes.fromhex("b3d9174cb5d3234764a089bb91fdbd1117b7958be4870d1a544136ab017a67dd")
coin = PartialTxInput(
prevout=TxOutpoint(txid=prevout_txid, out_idx=0),
)
coin.utxo = Transaction("02000000000105a5a00ad10e754a17154446bbe1c557b44b86a7cd53308ad9ab813388a9d6d1520000000000fdffffff2c48659d10a752b0c0a3efa4092ebee5210943a2f41ff0607ba0e03a4cdf7bbd0000000000fdffffff93f4feb485581654caffa523c500dc417a98097fb731045040bb162acb3e14e90000000000fdffffffbf4b69292acaabf8a415db412eedd8a202d4dd2ca12e532628a756276fec00f50000000000fdffffffa96e18c45ecc56608d0be3c1b2cd93e52d569a9c3a68ed51aead570beeef29ff0000000000fdffffff017a991300000000001600142a55fbef3e419e1c862632a826ae89ade0b07e3a0247304402204de416135d26711df2cbd5209e3f79f95a1de5ddea5980215606ebfe639747bd0220286f9de38a96a078c818e97fe6fbbbe3637287461cd7407adf9e85ba6d899005012102c329033555adccaadb6c83fb486f540ba00aad3edba7a4ec3347b5cc0935c4050247304402200132c1c4c41b840f05efeacf96cccedab38d68c9021c79f940738572b049c1a302200d59ba4719d4d4e55ced651cde35cbf79838bf7122cbafd6584dd934a67db0ed01210380194ab3704b5524a0c97f78b4458b80efd365faaedaebe90fa7807eeab041700247304402202c966fbea5db4bb3794e843b59240d678a3c8e97be1d10c18475a467c101b97c02203edb0ca11e7605af2437ddffbbe416317bca8788c23c4816c13698042222d30f0121035edcdcad9affcff41302ad49a19ccbd47ae50b153ba7c2abaaf3bb2d22c859120247304402206890a622513bb9c8b8ca83e5e82532f9753ecd3188c27d8da9452e264f5500cc022012dad8fb872478b7f9d0d9d310859fca6534b8e9f44511eeef5450beaf13c690012103f683aa56e036b1307c1ae75e81553b6730aea312560e36afad487ba2bc6cf98f02473044022006807df115d6bce73e384651fbb9e932ee313133218be7769e52809b758529b6022078b2859f98a62211f130e69982f96d2968e1820c58bb817f9ab31645b6b4ceae0121026402c3c0a2b4dd5703686f8c5b5b3dcecbbf4d772ef83e440bfad22b742753e730a60300")
coin.block_height = 100
coin = self.get_dummy_txin_1_284_474_sat()
tx = coin_chooser.make_tx(
coins=[coin],
@@ -72,3 +91,21 @@ class TestCoinChooser(ElectrumTestCase):
assert len(tx.outputs()) == 1, f"expected 1 output got {len(tx.outputs())}"
assert len(tx.inputs()) == 1, f"expected 1 input got {len(tx.inputs())}"
def test_doesnt_round_output_value_with_zerofee_estimator(self):
# output value rounding is enabled (as by default)
coin_chooser = CoinChooserPrivacy(enable_output_value_rounding=True)
# fixed fee estimator always returns 0
fee_estimator = FixedFeePolicy(0).estimate_fee
tx = coin_chooser.make_tx(
coins=[],
inputs=[self.get_dummy_txin_1_284_474_sat()] ,
outputs=[self.get_dummy_txout_1(1_000_000)],
change_addrs=[],
fee_estimator_vb=fee_estimator,
dust_threshold=500,
)
assert tx.get_fee() == 0, f"fee should be 0, is {tx.get_fee()}"
assert len(tx.outputs()) == 2, f"expected 2 output got {len(tx.outputs())}"
assert len(tx.inputs()) == 1, f"expected 1 input got {len(tx.inputs())}"