Skip to content

millisat precision#10586

Open
accumulator wants to merge 14 commits into
spesmilo:masterfrom
accumulator:millisat_precision
Open

millisat precision#10586
accumulator wants to merge 14 commits into
spesmilo:masterfrom
accumulator:millisat_precision

Conversation

@accumulator

@accumulator accumulator commented Apr 17, 2026

Copy link
Copy Markdown
Member

support millisat precision throughout the codebase.

  • kwarged backend wallet funcs so it is clear whether sats or millisats as ints are passed
  • lnurl now native millisats
  • Decimal is used on the GUI side where it's easier to assume sats are the base unit
    • avoids having to check sat and msat fields/parameters
    • can check easily if amount has millisat precision where not allowed
    • extra millisat precision is carried transparently for eventual passing to backend
  • QML QEAmount is now synced between sats and millisats, simplifying assumptions and reducing if-else checks

should fix #6253, #10412

Comment thread electrum/gui/qml/components/LnurlWithdrawRequestDialog.qml Outdated
@accumulator accumulator marked this pull request as draft April 20, 2026 07:23
@accumulator accumulator force-pushed the millisat_precision branch 11 times, most recently from 62db370 to d8dfce5 Compare April 21, 2026 12:59
@accumulator accumulator marked this pull request as ready for review April 21, 2026 13:14
Comment thread electrum/wallet.py
Comment thread electrum/gui/qt/amountedit.py
"""Formats amount as string, converting to desired unit.
E.g. 500_000 -> '0.005'
"""
return self.config.format_amount(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should format_amount pass precision=3?

Image

vs with precision=3:

Image

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, 5891e03 introduced a config option whether to show msat precision or not. format_amount just takes this into account.

QML bypasses this by using Config.formatMilliSats where msat precision is used, which explicitly passes the requested msat precision.

I'm fine with reverting 5891e03 though, I think for bitcoin amounts you almost always want to see full precision..

Comment thread electrum/payment_identifier.py Outdated
Comment thread electrum/gui/qml/qefx.py Outdated
Comment thread electrum/gui/qml/qetypes.py Outdated
@f321x

f321x commented Jun 4, 2026

Copy link
Copy Markdown
Member

I looked through the PR, but the issues above were all found by Claude. For those i verified (point 1 - 6) i wrote the comments above. I didn't verify the remaining points but considering the first were all real it makes sense to look at the remaining ones as well (8. -12.).

This is the output: ## 🔴 High — confirmed crashes / broken core flows

1. create_request dropped its amountless-coercion — amountless requests break three ways

Root cause at electrum/wallet.py:3023: the old amount_sat = amount_sat or 0 +
amount_msat = None if not amount_sat else … is gone, so an empty amount no longer maps
to "amountless".

  • (a) crash — QML amountless Lightning request. electrum/gui/qml/qewallet.py:706
    passes amount_msat=amount.msatsInt (= 0 for an empty QEAmount). create_request
    keeps amount_msat=0, so lnworker.create_payment_info raises
    ValueError("amount_msat must not be 0…") at electrum/lnworker.py:2714. createRequest
    only catches InvoiceError (qewallet.py:709) → unhandled exception. This is the standard
    "donation / request-without-amount" flow and it's reachable — with inbound liquidity,
    lightningCanReceive.gt(empty) is true, so the Lightning button is enabled. Master
    produced an amountless invoice here.
  • (b) broken — Qt on-chain amountless request. receive_tab.py empty field →
    get_amount() returns Nonecreate_request(amount_sat=None, address=addr)
    wallet.py:3057 PartialTxOutput.from_address_and_value(addr, None)
    ValueError: bad txout value: None (reproduced). Caught by the generic except Exception
    and shown as "Error adding payment request"; the request is never created (worked on
    master). Same crash, uncaught, via the add_request RPC with no amount
    (satoshis(None)=None, commands.py:1383).
  • (c) wrong — QML on-chain amountless request. qewallet.py:708 passes amount_sat=0
    amount_msat=0Request.get_amount_sat() returns 0 (not None) → a fixed
    0-amount request instead of "any amount".

Fix: re-introduce the coercion in create_request (treat 0/None as amountless:
amount_msat=None, on-chain output value 0).

2. FeerateEdit.setAmount(None) crashes

electrum/gui/qt/amountedit.py:180FeerateEdit now subclasses AmountEdit instead of
BTCAmountEdit, losing BTCAmountEdit.setAmount's if amount is None: setText(" ") guard
(amountedit.py:165). AmountEdit.setAmount(None)to_decimal(None)Decimal('None')
InvalidOperation. confirm_tx_dialog.py:355 and :436 call
self.feerate_e.setAmount(None) whenever there's no fee estimate / not enough funds — a
common path. (fee_e is fine; it's still a BTCAmountEdit.)


🟠 Medium — confirmed wrong output / wire format

3. Qt desktop lists & notification mis-render sub-sat amounts

invoice_list.py:119, request_list.py:149, main_window.py:1375 (the "Payment received"
notification) and :1690 (invoice dialog) now read get_amount_sat_msat_precision() (which
returns a Decimal for sub-sat amounts) but call format_amount(...) without
precision=3
. Verified: format_amount(Decimal('1.5')) → rounds to 2 sat ('0.00002'),
and format_amount(Decimal('0.4')) → the malformed string '0.'. So the msat-precision
getter was wired up but the precision is dropped exactly where it should be shown (and
< 0.5 sat renders garbage). Pass precision=3 (as the QML side does).

4. LNURL-pay sends a non-integer amount on the wire

qeinvoice.py:609 computes amount = Decimal(msatsInt)/1000, then
payment_identifier.py:392 does amount_msat = amount_sat * 1000Decimal('2345.000'),
sent as params={'amount': …} (:403). aiohttp serializes that to ?amount=2345.000
(trailing zeros). Since BtcField here has msatPrecision: true, any sub-sat amount (or an
LNURL whose min_sendable_msat isn't a whole sat) triggers it, and strict servers doing
int(amount) reject it. Master sent integer msat. Fix: params={'amount': int(amount_msat)}.

5. qefx.satoshiValue stores a Decimal in _amount_msat

qefx.py:164 self._amount.satsInt = v.to_integral_value() (a Decimal); the satsInt
setter multiplies by 1000 and stores the Decimal, so _amount_msat is no longer an int
— violating the invariant the constructor asserts (assert isinstance(amount_msat, int)) and
the 'qint64' property type. Wrap in int(...).


🟡 Low / latent / robustness

  1. QEAmount.max/min/lt/gt is_max semantics are wrong (qetypes.py:177):
    gt()/lt() return False whenever either operand is is_max, so max(MAX, normal)
    returns normal (loses MAX) and the result is order-dependent. Latent — no current caller
    passes an is_max amount — but the helper is incorrect for future use.
  2. _msat_to_sat truncates toward zero, not floor (qetypes.py:54,
    int(Decimal(msat)/1000)): inconsistent with // 1000 used everywhere else in the
    codebase; off-by-1-sat for negative sub-sat LN amounts (reachable via
    qetransactionlistmodel.py where amount_msat < 0).
  3. __eq__ against an int silently changed from sat to msat and the str branch was
    removed (qetypes.py:192). No broken caller found (comparisons are all against 0), but
    amount == <nonzero int> now means msat — a footgun worth a comment or an explicit
    msat-named helper.
  4. assert used to validate untrusted input: lnurl.py:188 validates a remote server's
    min/max withdrawable with assert, and the QEAmount constructor asserts its args — both
    stripped under python -O, letting malformed LNURL ranges / bad amounts through.
    (Pre-existing pattern, but now on the canonical msat path.)
  5. formatMilliSatsForEditing lacks the None guard its siblings formatSats /
    formatMilliSats have (qeconfig.py:364).
  6. positive uses >= 0 (was > 0) (qetypes.py:110): a zero-value entry is labeled
    received/credit in LightningPaymentDetails.qml:55 / TxDetails.qml:110 (mostly hidden
    by visibility guards).
  7. min/max return shared referenceseffectiveMaxWithdrawable can alias the live
    wallet QEAmount in LnurlWithdrawRequestDialog.qml; self-corrected by updateLimits()
    but fragile. Minor: ChannelBar.qml switched to .satsInt, truncating msat in bar-width
    math (cosmetic).

@accumulator accumulator force-pushed the millisat_precision branch from 754820c to 19432c7 Compare June 5, 2026 09:05
@accumulator

Copy link
Copy Markdown
Member Author

I looked through the PR, but the issues above were all found by Claude. For those i verified (point 1 - 6) i wrote the comments above. I didn't verify the remaining points but considering the first were all real it makes sense to look at the remaining ones as well (8. -12.).

Thanks for reviewing. I missed these Claude findings due to the green checkmark, and cirrus-ci is by now unavailable..

@f321x

f321x commented Jun 5, 2026

Copy link
Copy Markdown
Member

I missed these Claude findings due to the green checkmark

The results i commented were from a local run, not sure what the CI said, but yeah it often finds some things even though the checkmark is green, so makes sense to look at the output anyway.
Btw, let me know if you want an API key as well.

@SomberNight SomberNight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked at the first commit.
Please don't think just because the genai finds no concerns, there are no bugs.

Comment thread electrum/gui/qt/amountedit.py Outdated
@accumulator

Copy link
Copy Markdown
Member Author

Btw, let me know if you want an API key as well.

Hmm, don't mind if I do :)

…imal point and

extra_precision up to AmountEdit

AmountEdit defaults to decimal_point = 0, declare FiatAmountEdit to encapsulate fx base unit and
precision, extra_precision now a callable and retrieved dynamically.
BTCAmountEdit extended with millisat_precision option kwarg to allow 3 digits extra precision.
…with parameter combinations to

constructor.

in QEConfig, add formatMilliSatsForEditing, update formatMilliSats passing Decimal to config.format_amount/_and_units
add amount_msat parameter and make mutually exclusive with amount_sat
qt: send_tab LN checks millisat precision
payment_identifier: mark pi invalid if multiline and any output uses msat precision
qt: update send_tab for lnurl msat precision
qml: update qeinvoice, qerequestdetails for lnurl msat precision
QEConfig.satsToUnits to QEConfig.amountToBaseunitStr with the latter now
also taking a QEAmount instance
Using javascript or QML `int` type is a long-standing issue, as there is not enough range to express millisats or even sats.
A short summary of the issues:
- integers in javascript context have a max range of +/- 2^54-1, which allows 21M BTC expressed in sats, but not in msats.
- QML `int` properties have a max range of +/- 2^31-1, which only allows 21 BTC expressed in sats, 21 mBTC expressed in msats
- `int` parameters declared in the `pyqtProperty` and `pyqtSlot` decorators have a max range of 2^31-1,
although this is somewhat alleviated in the QML->Python direction by using `q(u)int64`. Returning a `q(u)int64` does not
work around the `int` limitation

In most of the QML code, `QEAmount` is already used for storing and passing around BTC values.
The only exception is where amounts are compared (e.g. invoice amount < available balance etc),
so the `<`, `>`, `<=`, `>=` and `!=` operators, and where these operators are implied, like `Math.min()` and `Math.max()`

This commit delegates these operators to python scope.
…precision on

send_tab if recipient is onchain
@accumulator

Copy link
Copy Markdown
Member Author

rebased on master, commits reorganised for better reviewability

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

user interface only has satoshi precision (msat truncated to sat, rounding)

3 participants