From 8d8d1dba0f204a42451741c7e7d937f76bb455ca Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 22 Aug 2025 13:30:26 +0000 Subject: [PATCH] util.format_satoshis: floating-point paranoia --- electrum/commands.py | 2 +- electrum/util.py | 10 ++++++++-- electrum/wallet.py | 2 +- tests/test_commands.py | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 5b1688c43..a886197a4 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -101,7 +101,7 @@ def satoshis(amount): return int(COIN*to_decimal(amount)) if amount is not None else None -def format_satoshis(x: Union[str, float, int, Decimal, None]) -> Optional[str]: +def format_satoshis(x: Union[float, int, Decimal, None]) -> Optional[str]: """ input: satoshis as a Number output: str formatted as bitcoin amount diff --git a/electrum/util.py b/electrum/util.py index 915f7ce21..0a57b68a2 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -234,6 +234,8 @@ def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal: # Decimal('41754.681') if isinstance(x, Decimal): return x + if isinstance(x, int): + return Decimal(x) return Decimal(str(x)) @@ -800,8 +802,10 @@ def format_satoshis_plain( if is_max_allowed and parse_max_spend(x): return f'max({x})' assert isinstance(x, (int, float, Decimal)), f"{x!r} should be a number" + # TODO(ghost43) just hard-fail if x is a float. do we even use floats for money anywhere? + x = to_decimal(x) scale_factor = pow(10, decimal_point) - return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.') + return "{:.8f}".format(x / scale_factor).rstrip('0').rstrip('.') # Check that Decimal precision is sufficient. @@ -833,8 +837,10 @@ def format_satoshis( if parse_max_spend(x): return f'max({x})' assert isinstance(x, (int, float, Decimal)), f"{x!r} should be a number" + # TODO(ghost43) just hard-fail if x is a float. do we even use floats for money anywhere? + x = to_decimal(x) # lose redundant precision - x = Decimal(x).quantize(Decimal(10) ** (-precision)) + x = x.quantize(Decimal(10) ** (-precision)) # format string overall_precision = decimal_point + precision # max digits after final decimal point decimal_format = "." + str(overall_precision) if overall_precision > 0 else "" diff --git a/electrum/wallet.py b/electrum/wallet.py index 894c3a963..e28778abd 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1759,7 +1759,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): fee = self.adb.get_tx_fee(tx_hash) if fee is not None: size = tx.estimated_size() - fee_per_byte = fee / size + fee_per_byte = Decimal(fee) / size extra.append(format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VB}") if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \ and self.network and self.network.has_fee_mempool(): diff --git a/tests/test_commands.py b/tests/test_commands.py index 2d88431be..f1169ec40 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -185,6 +185,7 @@ class TestCommands(ElectrumTestCase): self.assertEqual(format_satoshis(Decimal(123.456)), "0.00000123") self.assertEqual(format_satoshis(Decimal(123.5)), "0.00000124") self.assertEqual(format_satoshis(Decimal(123.789)), "0.00000124") + self.assertEqual(format_satoshis(41754.681), "0.00041755") class TestCommandsTestnet(ElectrumTestCase):