commands: use format_satoshis consistently. don't use sci-notation
What changed, and why it matters
This commit fixes a formatting bug in Electrum's command-line output where very small bitcoin amounts were displayed in scientific notation (like '1E-8' instead of '0.00000001'). This is a usability and correctness fix that makes wallet balances and transaction values display consistently. It is not a direct security vulnerability, but scientific notation in financial outputs could theoretically confuse users or downstream scripts that parse amounts.
No urgent action required. This is a low-risk correctness/usability fix. Users and integrators relying on Electrum's JSON-RPC or command-line output should verify that downstream parsers handle the new fixed-point format correctly, especially for very small or zero amounts.
Security signals we found
Output formatting bug could cause user/script misinterpretation of amounts
Scientific notation in financial amounts is non-standard and potentially misleading
Fix centralizes formatting logic to reduce future inconsistencies
No cryptographic, network, or privilege-boundary changes observed
Evidence from the diff
The patch replaces an ad-hoc format_satoshis() implementation in electrum/commands.py with a call to util.format_satoshis_plain(). The old code converted satoshis to BTC by dividing by COIN (10^8) and calling str() on a Decimal, which for 1 satoshi produced ‘1E-8’. The new code uses a dedicated formatter that always emits a fixed-point decimal string. The change also adds an is_max_allowed parameter to util.format_satoshis_plain() and passes False in the command formatter so that the literal ‘max’ sentinel is not interpreted as a maximum-spend amount. Several command outputs (getbalance, getaddressbalance, listunspent, listaddresses) are updated to use the consistent formatter.
Changed components
electrum/commands.pyelectrum/util.pytests/test_commands.pyInspect captured patch +34 / −12
diff --git a/electrum/commands.py b/electrum/commands.py
index 6bc172e..b54a41b 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -36,7 +36,7 @@ import inspect
from collections import defaultdict
from functools import wraps
from decimal import Decimal, InvalidOperation
-from typing import Optional, TYPE_CHECKING, Dict, List, Any
+from typing import Optional, TYPE_CHECKING, Dict, List, Any, Union
import os
import re
@@ -101,8 +101,14 @@ def satoshis(amount):
return int(COIN*to_decimal(amount)) if amount is not None else None
-def format_satoshis(x):
- return str(to_decimal(x)/COIN) if x is not None else None
+def format_satoshis(x: Union[str, float, int, Decimal, None]) -> Optional[str]:
+ """
+ input: satoshis as a Number
+ output: str formatted as bitcoin amount
+ """
+ if x is None:
+ return None
+ return util.format_satoshis_plain(x, is_max_allowed=False)
class Command:
@@ -476,7 +482,7 @@ class Commands(Logger):
for txin in wallet.get_utxos():
d = txin.to_json()
v = d.pop("value_sats")
- d["value"] = str(to_decimal(v)/COIN) if v is not None else None
+ d["value"] = format_satoshis(v)
coins.append(d)
return coins
@@ -719,13 +725,13 @@ class Commands(Logger):
"""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(to_decimal(c)/COIN)}
+ out = {"confirmed": format_satoshis(c)}
if u:
- out["unconfirmed"] = str(to_decimal(u)/COIN)
+ out["unconfirmed"] = format_satoshis(u)
if x:
- out["unmatured"] = str(to_decimal(x)/COIN)
+ out["unmatured"] = format_satoshis(x)
if l:
- out["lightning"] = str(to_decimal(l)/COIN)
+ out["lightning"] = format_satoshis(l)
return out
@command('n')
@@ -738,8 +744,8 @@ class Commands(Logger):
"""
sh = bitcoin.address_to_scripthash(address)
out = await self.network.get_balance_for_scripthash(sh)
- out["confirmed"] = str(to_decimal(out["confirmed"])/COIN)
- out["unconfirmed"] = str(to_decimal(out["unconfirmed"])/COIN)
+ out["confirmed"] = format_satoshis(out["confirmed"])
+ out["unconfirmed"] = format_satoshis(out["unconfirmed"])
return out
@command('n')
@@ -1183,7 +1189,7 @@ class Commands(Logger):
if labels or balance:
item = (item,)
if balance:
- item += (util.format_satoshis(sum(wallet.get_addr_balance(addr))),)
+ item += (format_satoshis(sum(wallet.get_addr_balance(addr))),)
if labels:
item += (repr(wallet.get_label_for_address(addr)),)
out.append(item)
diff --git a/electrum/util.py b/electrum/util.py
index 033283e..915f7ce 100644
--- a/electrum/util.py
+++ b/electrum/util.py
@@ -793,10 +793,11 @@ def format_satoshis_plain(
x: Union[int, float, Decimal, str], # amount in satoshis,
*,
decimal_point: int = 8, # how much to shift decimal point to left (default: sat->BTC)
+ is_max_allowed: bool = True,
) -> str:
"""Display a satoshi amount scaled. Always uses a '.' as a decimal
point and has no thousands separator"""
- if parse_max_spend(x):
+ 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"
scale_factor = pow(10, decimal_point)
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 2a93d09..607a691 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -171,6 +171,21 @@ class TestCommands(ElectrumTestCase):
with self.assertRaises(binascii.Error): # perhaps it should raise some nice UserFacingException instead
await cmds.decrypt(pubkey, ciphertext+"trailinggarbage", wallet=wallet)
+ def test_format_satoshis(self):
+ format_satoshis = electrum.commands.format_satoshis
+ # input type is highly polymorphic:
+ self.assertEqual(format_satoshis(None), None)
+ self.assertEqual(format_satoshis(1), "0.00000001")
+ self.assertEqual(format_satoshis(1.0), "0.00000001")
+ self.assertEqual(format_satoshis(Decimal(1)), "0.00000001")
+ # trailing zeroes are cut
+ self.assertEqual(format_satoshis(51000), "0.00051")
+ self.assertEqual(format_satoshis(123456_12345670), "123456.1234567")
+ # sub-satoshi precision is rounded
+ 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")
+
class TestCommandsTestnet(ElectrumTestCase):
TESTNET = True
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.