Skip to content

Token UX fixes: enforce declared supply, honest supply display, faucet source, atomic control - #291

Merged
srikanth-bitdynamics merged 3 commits into
mainfrom
token-declared-supply
Jul 29, 2026
Merged

Token UX fixes: enforce declared supply, honest supply display, faucet source, atomic control#291
srikanth-bitdynamics merged 3 commits into
mainfrom
token-declared-supply

Conversation

@srikanth-bitdynamics

Copy link
Copy Markdown
Collaborator

Summary

A batch of token UX bugs surfaced while testing the mint/transfer flow on a live LocalNet. Each was a case where the tool quietly did something other than what the operator was told. All fixes land on both the CLI and the Web UI through the shared orchestration, per the CLI ↔ Web UI parity rule.

What was wrong, and the fix

1. "Total supply 0" on a token created with a supply. token create --initial-supply 2001 only records the figure as registry metadata — it mints nothing — so a created-but-unminted instrument correctly read 0, with nothing explaining why. The supply tile now shows 0 · declared 2,001 with a Mint initial supply action, and the annotation disappears once minted, so a settled instrument is just its number. Also in this commit: requireFields now names the missing input (mint: recipient party is required) instead of field at position 2, and the party-alias error says the pattern is lowercase (it always was, but the message didn't say so, making Zheholder look legal).

2. Declared supply was never enforced. You could mint 200,004 against a declared 2,001 — the number the operator typed meant nothing. Mint now rejects anything that would push circulating supply past the declared amount, naming the headroom left. Instruments with no declared supply (Amulet, anything not created here) stay uncapped. The supply tile also surfaces an overshoot (declared 2,001 · over by 197,993) for instruments minted past their declared supply before the cap existed.

3. Faucet always failed for created tokens, and the UI offered a control that couldn't work. The faucet defaulted its source to the role's own party — right for Amulet (bootstrap-funded) but wrong for a created token whose supply sits with whoever it was minted to, so it failed with "sender holds no units of this instrument". It now dispenses from the instrument's largest holder (--source still overrides). Separately, the transfer modal shipped an enabled "Atomic" checkbox whose own warning said the submit would fail (batching can't rebind the accept leg on this Splice version); it's now shown-but-unavailable so it can't be armed.

A caveat worth stating

The supply cap is devkit-side bookkeeping, not a ledger invariant — the test token's TokenRules has no on-ledger cap, so a client minting against it directly still can. It makes the number meaningful in this tool and surfaces overshoots honestly; it does not make it enforceable on-chain.

Testing

  • New regression tests for the cap enforcement and decimal trimming; comparisons use big.Rat (float drifts past ~15 digits and supply carries 10 decimals).
  • Verified live on a running instance: the 200,004-against-2,001 mint is now refused (CLI and API/422), an under-cap mint succeeds, the faucet dispenses from the real holder and the totals reconcile, and the atomic checkbox is unclickable.
  • go test ./internal/..., golangci-lint, tsc, and the frontend test suite pass. dist/index.html kept as the placeholder.

Parity

All three fixes go through the shared RunCreate/RunMint/RunFaucet and shared internal/api/types, so the CLI --json and the Web UI emit the same shapes and enforce the same guards.

…elds

Three issues an operator hits on a freshly created instrument:

Creating a token records an initial supply but mints nothing, so the
instrument reads "Total supply 0" with no explanation and looks broken.
Surface the recorded figure as declared_supply on the instrument summary
and annotate the tile ("0 · declared 2,001") only while it differs from
what is actually minted, alongside a "Mint N remaining" action that
prefills the outstanding amount. Once fully minted the note and the
action disappear, so a settled instrument is just its number.

requireFields could only report an index — a missing recipient surfaced
in the Web UI as "mint: field at position 2 is required". Take
name/value pairs so the error names the input ("mint: recipient party is
required"); this text goes straight into the form.

The party-alias error said an alias "must be a letter followed by
letters, digits or hyphens" while the pattern requires lowercase, so
"Zheholder" looked legal. Say lowercase.
The initial supply recorded at create was never checked, so an instrument
declaring 2001 could be minted to 200,004 — the figure the operator typed
meant nothing. Reject a mint that would push circulating supply past the
declared amount, naming the numbers and the headroom left:

  mint exceeds the instrument's declared supply: ZHE declares 2001,
  199994 already minted — at most 0 more can be minted

The cap lives in RunMint, so the CLI and the Web UI enforce it
identically; the handler maps it to 422 SUPPLY_CAP_EXCEEDED so the form
can show the numbers. Instruments with no declared supply (Amulet,
anything not created here) stay uncapped, and a scan failure lets the
mint through rather than blocking on an unrelated read error.

This is devkit bookkeeping, not a ledger invariant — the test token's
TokenRules has no cap, so a client minting against it directly still can.
The supply tile therefore also reports an overshoot ('declared 2,001 ·
over by 197,993') instead of hiding it, which is the state instruments
minted before this change are already in.

Comparisons use big.Rat: float drifts past ~15 digits, and supply
figures carry ten decimal places.
The faucet defaulted its source to the role's own party. That holds for
Amulet, which the LocalNet bootstrap funds, but a token created here
starts with its supply wherever it was minted — so funding from a created
instrument always failed with "sender holds no units of this instrument"
even though supply existed. Default to the instrument's largest current
holder instead, which is what a faucet should dispense from; --source
still overrides, and an instrument nobody holds yields the same (now
accurate) error. CLI help updated to match.

The transfer modal offered an enabled "Atomic" checkbox whose own warning
said the submit would fail: ExecuteBatch on this Splice version cannot
rebind the accept leg to the instruction the transfer leg creates, so
every attempt errors with CONTRACT_DOES_NOT_IMPLEMENT_INTERFACE. A
control whose only outcome is an error should not be armable — it stays
visible, and labelled unavailable, so the gap against the CLI's --atomic
is still documented.
@srikanth-bitdynamics
srikanth-bitdynamics merged commit 9f545ef into main Jul 29, 2026
4 checks passed
@srikanth-bitdynamics
srikanth-bitdynamics deleted the token-declared-supply branch July 29, 2026 14:04
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