From 7834f6c427594283095274d76ca3203b33df3618 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 22 Mar 2023 12:22:36 +0000 Subject: [PATCH] commands: fix satoshis decimal conversion in payto cmd and others When called via jsonrpc (but not via cli) with non-string amounts, there could be a rounding error resulting in sending 1 sat less. example: ``` $ ./run_electrum --testnet -w ~/.electrum/testnet/wallets/test_segwit_2 paytomany '[["tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg", 0.00033389]]' --fee 0 02000000000101b9e6018acb16952e3c9618b069df404dc85544eda8120e5f6e7cd7e94ce5ae8d0100000000fdffffff02fd8100000000000016001410c5b97085ec1637a9f702852f5a81f650fae1566d82000000000000160014d5a97ae05a1f4f315d0579b6577daceb5177a42b024730440220251d2ce83f6e69273de8e9be8602fbcf72b9157e1c0116161fa52f7e04db6e4302202d84045cc6b7056a215d1db3f59884e28dadd5257e1a3960068f90df90b452d1012102b0eff3bf364a2ab5effe952cba33521ebede81dac88c71951a5ed598cb48347b3a022500 $ curl --data-binary '{"id":"curltext","method":"paytomany","params":{"outputs":[["tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg", 0.00033389]], "fee": 0, "wallet": "/home/user/.electrum/testnet/wallets/test_segwit_2"}}' http://user:pass@127.0.0.1:7777 {"id": "curltext", "jsonrpc": "2.0", "result": "02000000000101b9e6018acb16952e3c9618b069df404dc85544eda8120e5f6e7cd7e94ce5ae8d0100000000fdffffff02fe8100000000000016001410c5b97085ec1637a9f702852f5a81f650fae1566c82000000000000160014d5a97ae05a1f4f315d0579b6577daceb5177a42b0247304402206ef66b845ca298c14dc6e8049cba9ed19db1671132194518ce5d521de6f5df8802205ca4b1aee703e3b98331fb9b88210917b385560020c8b2a8a88da38996b101c4012102b0eff3bf364a2ab5effe952cba33521ebede81dac88c71951a5ed598cb48347b39022500"} ``` ^ note that first tx has output for 0.00033389, second tx has output for 0.00033388 fixes https://github.com/spesmilo/electrum/issues/8274 --- electrum/commands.py | 30 +++++++++++++++--------------- electrum/exchange_rate.py | 14 +------------- electrum/util.py | 11 +++++++++++ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index d7e1af79e..fbae8858e 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -42,7 +42,7 @@ import os from .import util, ecc from .util import (bfh, format_satoshis, json_decode, json_normalize, - is_hash256_str, is_hex_str, to_bytes, parse_max_spend) + is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal) from . import bitcoin from .bitcoin import is_address, hash_160, COIN from .bip32 import BIP32Node @@ -85,10 +85,10 @@ def satoshis_or_max(amount): def satoshis(amount): # satoshi conversion must not be performed by the parser - return int(COIN*Decimal(amount)) if amount is not None else None + return int(COIN*to_decimal(amount)) if amount is not None else None def format_satoshis(x): - return str(Decimal(x)/COIN) if x is not None else None + return str(to_decimal(x)/COIN) if x is not None else None class Command: @@ -357,7 +357,7 @@ class Commands: for txin in wallet.get_utxos(): d = txin.to_json() v = d.pop("value_sats") - d["value"] = str(Decimal(v)/COIN) if v is not None else None + d["value"] = str(to_decimal(v)/COIN) if v is not None else None coins.append(d) return coins @@ -543,13 +543,13 @@ class Commands: """Return the balance of your wallet. """ c, u, x = wallet.get_balance() l = wallet.lnworker.get_balance() if wallet.lnworker else None - out = {"confirmed": str(Decimal(c)/COIN)} + out = {"confirmed": str(to_decimal(c)/COIN)} if u: - out["unconfirmed"] = str(Decimal(u)/COIN) + out["unconfirmed"] = str(to_decimal(u)/COIN) if x: - out["unmatured"] = str(Decimal(x)/COIN) + out["unmatured"] = str(to_decimal(x)/COIN) if l: - out["lightning"] = str(Decimal(l)/COIN) + out["lightning"] = str(to_decimal(l)/COIN) return out @command('n') @@ -559,8 +559,8 @@ class Commands: """ sh = bitcoin.address_to_scripthash(address) out = await self.network.get_balance_for_scripthash(sh) - out["confirmed"] = str(Decimal(out["confirmed"])/COIN) - out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) + out["confirmed"] = str(to_decimal(out["confirmed"])/COIN) + out["unconfirmed"] = str(to_decimal(out["unconfirmed"])/COIN) return out @command('n') @@ -1056,7 +1056,7 @@ class Commands: else: raise Exception('Invalid fee estimation method: {}'.format(fee_method)) if fee_level is not None: - fee_level = Decimal(fee_level) + fee_level = to_decimal(fee_level) return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level) @command('w') @@ -1358,7 +1358,7 @@ class Commands: raise Exception(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable') # Conversion try: - from_amount = Decimal(from_amount) + from_amount = to_decimal(from_amount) to_amount = from_amount / rate_from * rate_to except InvalidOperation: raise Exception("from_amount is not a number") @@ -1456,7 +1456,7 @@ command_options = { # don't use floats because of rounding errors from .transaction import convert_raw_tx_to_hex -json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x))) +json_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x))) arg_types = { 'num': int, 'nbits': int, @@ -1469,8 +1469,8 @@ arg_types = { 'jsontx': json_loads, 'inputs': json_loads, 'outputs': json_loads, - 'fee': lambda x: str(Decimal(x)) if x is not None else None, - 'amount': lambda x: str(Decimal(x)) if not parse_max_spend(x) else x, + 'fee': lambda x: str(to_decimal(x)) if x is not None else None, + 'amount': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x, 'locktime': int, 'addtransaction': eval_bool, 'fee_method': str, diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 55c9edee0..5bb8bc428 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -17,7 +17,7 @@ from . import util from .bitcoin import COIN from .i18n import _ from .util import (ThreadJob, make_dir, log_exceptions, OldTaskGroup, - make_aiohttp_session, resource_path, EventListener, event_listener) + make_aiohttp_session, resource_path, EventListener, event_listener, to_decimal) from .network import Network from .simple_config import SimpleConfig from .logging import Logger @@ -39,18 +39,6 @@ CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, 'BTC': 8, 'LTC': 8, 'XRP': 6, 'ETH': 18, } - -def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal: - # helper function mainly for float->Decimal conversion, i.e.: - # >>> Decimal(41754.681) - # Decimal('41754.680999999996856786310672760009765625') - # >>> Decimal("41754.681") - # Decimal('41754.681') - if isinstance(x, Decimal): - return x - return Decimal(str(x)) - - POLL_PERIOD_SPOT_RATE = 150 # approx. every 2.5 minutes, try to refresh spot price EXPIRY_SPOT_RATE = 600 # spot price becomes stale after 10 minutes diff --git a/electrum/util.py b/electrum/util.py index f15e0df2c..edd0ff6c8 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -205,6 +205,17 @@ class UserCancelled(Exception): pass +def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal: + # helper function mainly for float->Decimal conversion, i.e.: + # >>> Decimal(41754.681) + # Decimal('41754.680999999996856786310672760009765625') + # >>> Decimal("41754.681") + # Decimal('41754.681') + if isinstance(x, Decimal): + return x + return Decimal(str(x)) + + # note: this is not a NamedTuple as then its json encoding cannot be customized class Satoshis(object): __slots__ = ('value',)