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 # Last change output. Round down to maximum precision but lose
# no more than 10**max_dp_to_round_for_privacy # no more than 10**max_dp_to_round_for_privacy
# e.g. a max of 2 decimal places means losing 100 satoshis to fees # 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]))) N = int(pow(10, min(max_dp_to_round_for_privacy, zeroes[0])))
amount = (remaining // N) * N amount = (remaining // N) * N
amounts.append(amount) amounts.append(amount)

View File

@@ -299,7 +299,7 @@ class TxFeeSlider(FeeSlider):
if invoice_amt == 0: if invoice_amt == 0:
invoice_amt = tx.output_value() invoice_amt = tx.output_value()
fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning( 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: if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple allow_send, long_warning, short_warning = fee_warning_tuple
self.warning = _('Warning') + ': ' + long_warning 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 amount = self.tx.output_value() if self.output_value == '!' else self.output_value
tx_size = self.tx.estimated_size() tx_size = self.tx.estimated_size()
fee_warning_tuple = self.wallet.get_tx_fee_warning( 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: if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple allow_send, long_warning, short_warning = fee_warning_tuple
if not allow_send: 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 # '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() invoice_amt = abs(amount) if amount else self.tx.output_value()
fee_warning_tuple = self.wallet.get_tx_fee_warning( 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: if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple allow_send, long_warning, short_warning = fee_warning_tuple
fee_str += " - <font color={color}>{header}: {body}</font>".format( fee_str += " - <font color={color}>{header}: {body}</font>".format(

View File

@@ -3276,16 +3276,18 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self, *, self, *,
invoice_amt: int, invoice_amt: int,
tx_size: 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 invoice_amt >= 0, f"{invoice_amt=!r} must be non-negative satoshis"
assert fee >= 0, f"{fee=!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 feerate = Decimal(fee) / tx_size # sat/byte
fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0 fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
long_warning = None long_warning = None
short_warning = None short_warning = None
allow_send = True allow_send = True
if feerate < self.relayfee() / 1000: if feerate < self.relayfee() / 1000 and not is_future_tx:
long_warning = ' '.join([ long_warning = ' '.join([
_("This transaction requires a higher fee, or it will not be propagated by your current server."), _("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.") _("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.coinchooser import CoinChooserPrivacy
from electrum.util import NotEnoughFunds from electrum.util import NotEnoughFunds
from electrum.transaction import PartialTxInput, TxOutpoint, Transaction from electrum.transaction import PartialTxInput, TxOutpoint, Transaction, PartialTxOutput
from electrum.fee_policy import FeePolicy from electrum.fee_policy import FeePolicy, FixedFeePolicy
from functools import partial from functools import partial
from typing import Optional
from . import ElectrumTestCase from . import ElectrumTestCase
class TestCoinChooser(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 test_bucket_candidates_with_empty_buckets(self):
def sufficient_funds(buckets, *, bucket_value_sum): def sufficient_funds(buckets, *, bucket_value_sum):
return True return True
@@ -34,12 +58,7 @@ class TestCoinChooser(ElectrumTestCase):
single_txin.utxo = Transaction("02000000000101956449bdc8059b680a20483e64e139ce63fe64333b92cd7811a1b116d6b967ad0000000000fdffffff024a01000000000000160014a21d1fbcf571153f57b40855e059c134405a89ecd682010000000000160014fd7debf75d6c410bf6ba1c8ba05f90f23ce4646a0247304402207f07ec0c2415b31743527dea2f7bff3868f494dc0a5d45adec5e05031725a0af02202aa0ac7d06dbcad8ac0b9808a829b6bdaa98bc831aef31a5ab4e5d1890f7552101210278a5d9b2796f2743ccf1b36b2bf47695d766d0841c17b00ce83943c8b37dde0ceea60300") single_txin.utxo = Transaction("02000000000101956449bdc8059b680a20483e64e139ce63fe64333b92cd7811a1b116d6b967ad0000000000fdffffff024a01000000000000160014a21d1fbcf571153f57b40855e059c134405a89ecd682010000000000160014fd7debf75d6c410bf6ba1c8ba05f90f23ce4646a0247304402207f07ec0c2415b31743527dea2f7bff3868f494dc0a5d45adec5e05031725a0af02202aa0ac7d06dbcad8ac0b9808a829b6bdaa98bc831aef31a5ab4e5d1890f7552101210278a5d9b2796f2743ccf1b36b2bf47695d766d0841c17b00ce83943c8b37dde0ceea60300")
# dummy input to be used as potential additional input of higher value # dummy input to be used as potential additional input of higher value
prevout_txid = bytes.fromhex("b3d9174cb5d3234764a089bb91fdbd1117b7958be4870d1a544136ab017a67dd") coin = self.get_dummy_txin_1_284_474_sat()
coin = PartialTxInput(
prevout=TxOutpoint(txid=prevout_txid, out_idx=0),
)
coin.utxo = Transaction("02000000000105a5a00ad10e754a17154446bbe1c557b44b86a7cd53308ad9ab813388a9d6d1520000000000fdffffff2c48659d10a752b0c0a3efa4092ebee5210943a2f41ff0607ba0e03a4cdf7bbd0000000000fdffffff93f4feb485581654caffa523c500dc417a98097fb731045040bb162acb3e14e90000000000fdffffffbf4b69292acaabf8a415db412eedd8a202d4dd2ca12e532628a756276fec00f50000000000fdffffffa96e18c45ecc56608d0be3c1b2cd93e52d569a9c3a68ed51aead570beeef29ff0000000000fdffffff017a991300000000001600142a55fbef3e419e1c862632a826ae89ade0b07e3a0247304402204de416135d26711df2cbd5209e3f79f95a1de5ddea5980215606ebfe639747bd0220286f9de38a96a078c818e97fe6fbbbe3637287461cd7407adf9e85ba6d899005012102c329033555adccaadb6c83fb486f540ba00aad3edba7a4ec3347b5cc0935c4050247304402200132c1c4c41b840f05efeacf96cccedab38d68c9021c79f940738572b049c1a302200d59ba4719d4d4e55ced651cde35cbf79838bf7122cbafd6584dd934a67db0ed01210380194ab3704b5524a0c97f78b4458b80efd365faaedaebe90fa7807eeab041700247304402202c966fbea5db4bb3794e843b59240d678a3c8e97be1d10c18475a467c101b97c02203edb0ca11e7605af2437ddffbbe416317bca8788c23c4816c13698042222d30f0121035edcdcad9affcff41302ad49a19ccbd47ae50b153ba7c2abaaf3bb2d22c859120247304402206890a622513bb9c8b8ca83e5e82532f9753ecd3188c27d8da9452e264f5500cc022012dad8fb872478b7f9d0d9d310859fca6534b8e9f44511eeef5450beaf13c690012103f683aa56e036b1307c1ae75e81553b6730aea312560e36afad487ba2bc6cf98f02473044022006807df115d6bce73e384651fbb9e932ee313133218be7769e52809b758529b6022078b2859f98a62211f130e69982f96d2968e1820c58bb817f9ab31645b6b4ceae0121026402c3c0a2b4dd5703686f8c5b5b3dcecbbf4d772ef83e440bfad22b742753e730a60300")
coin.block_height = 100
tx = coin_chooser.make_tx( tx = coin_chooser.make_tx(
coins=[coin], 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.outputs()) == 1, f"expected 1 output got {len(tx.outputs())}"
assert len(tx.inputs()) == 1, f"expected 1 input got {len(tx.inputs())}" 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())}"