Skip to content

Spike: read pending balance from the node instead of deriving it - #301

Closed
droplister wants to merge 1 commit into
mainfrom
spike/pending-balance
Closed

Spike: read pending balance from the node instead of deriving it#301
droplister wants to merge 1 commit into
mainfrom
spike/pending-balance

Conversation

@droplister

Copy link
Copy Markdown
Member

Research spike into @davesta's STARMONEY report. No UI yet — the finding is the deliverable, and the display decisions are the part worth arguing with before anything gets drawn.

The good news: we don't have to derive this

My worry was that computing "what's committed but not yet debited" means re-implementing Counterparty's debit rules in the wallet — which message types debit what, in what order, under which activations. That's consensus logic, and a second copy of it would be wrong eventually and wrong silently.

It isn't necessary. Core parses mempool transactions and emits the same DEBIT/CREDIT events it emits for confirmed ones, with action/calling_function naming the reason. The wallet asks instead of deriving.

Verified live — a pending issuance returns its CREDIT of the new asset and its DEBIT of the XCP fee, both labelled. And a fairmint debits XCP with an action string (fairmint.py:287), so davesta's exact case arrives pre-explained.

Three things I checked because they'd have sunk it

Double counting. I expected a race between "confirmed in a block" and "still in mempool". Core closes it: mempool rows for a block's transactions are deleted in the same database transaction that marks the block parsed, with a comment saying that's exactly so an API reader can't observe both. Nothing to defend against.

Address matching. /v2/addresses/mempool matches with SQL LIKE '%address%' against a joined column, so results are a superset. Every event is filtered on its own params.address here — without that, a neighbour's debit could be subtracted from your balance.

64-bit exactness. Quantities stay bigint end to end. Rounding one through a double is the precise failure this exists to prevent.

What it can't do

It only sees what the queried node has in its mempool. Point the wallet at a node that never saw your broadcast and it shows nothing pending. That's a floor on any client-side answer, and it's why the UI wording should describe what's known rather than assert what's spendable.

Two display decisions baked in

The confirmed balance is never adjusted. spendable is a separate figure. A headline number that quietly changes meaning is its own defect — read "4 XCP", then later "0 XCP", same label, two different claims.

Pending debits exceeding the confirmed balance is reported as a disagreement, not rendered. The ledger says it can't happen, so seeing it means the two reads disagree (mid-reorg, stale balance). A confident negative would be a lie.

One correction to the original feedback

davesta asked for escrow display on DEX orders too. That's already correct behaviour — an order debits give_quantity at parse time with action "open order" (order.py:439), so a confirmed open order's funds are already out of your balance. The gap there isn't accounting, it's that nothing tells you why your XCP dropped or that cancelling returns it. Different feature.

Verification

19 tests on the pure module; 4406 unit tests pass overall; tsc and biome clean. Live-checked against api.counterparty.io.

https://claude.ai/code/session_01QJS9Bj6uAMoYPATvfr6GZ1

A spike into what @davesta hit: mint 400,000 STARMONEY for 4 XCP, and the 4 XCP
is still sitting in your balance looking spendable until the mint confirms.
Compose a DEX order against the same coins in the meantime and whichever
confirms second fails.

The thing not to do here is re-derive Counterparty's debit rules in the wallet.
Which message types debit what, in what order, under which activations, is
consensus logic; a second implementation of it would be wrong eventually and
wrong silently. It turns out not to be necessary: core parses mempool
transactions and emits the same DEBIT and CREDIT events it emits for confirmed
ones, with action/calling_function naming the reason. So the wallet asks rather
than derives, and a fairmint's XCP debit arrives already labelled.

Verified against a live node and against core's source:

  - /v2/addresses/mempool returns those events per address. Confirmed live: a
    pending issuance shows its CREDIT of the new asset and its DEBIT of the XCP
    fee, both with readable reasons.
  - Double counting is core's problem and core handles it. When a block is
    parsed, mempool rows for the transactions it contained are deleted in the
    same database transaction that marks the block parsed, specifically so an API
    reader cannot see a transaction as both confirmed and pending. There is no
    window to defend against.
  - That endpoint matches addresses with SQL LIKE against a joined column, so its
    results are a superset. Every event is filtered on its own params.address
    here; without that a neighbour's debit could be subtracted from your balance.
  - Quantities are unsigned 64-bit and stay bigint throughout. Rounding one
    through a double is the precise failure this exists to prevent.

Two display decisions are baked in and are the part worth arguing with. The
confirmed balance is never adjusted -- spendable is a separate figure -- because
a headline number that quietly changes meaning is its own defect. And pending
debits exceeding the confirmed balance is reported as a disagreement rather than
rendered as a negative: the ledger says it cannot happen, so seeing it means the
two reads disagree, and a confident negative would be a lie.

Pure and tested without a browser; no UI yet, deliberately.

Claude-Session: https://claude.ai/code/session_01QJS9Bj6uAMoYPATvfr6GZ1
@droplister

Copy link
Copy Markdown
Member Author

Superseded by #302, which contains this branch's commit (1e551ae) plus the display and Max layers built on top of it. Closing rather than merging so the same change doesn't land twice.

@droplister droplister closed this Aug 12, 2026
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.

1 participant