Problem
tokens(registry, filter) drops every mirror entry whose first asset is absent, so a coinless
token — one whose genesis data carries no value envelope — never appears in the wallet's token list.
dist/index.js of 0.16.0, :12985-12991:
tokens(registry, filter) {
const out = [];
for (const [tokenId, entry] of this.mirror) {
if (entry.status !== "active") continue;
const asset = entry.assets[0];
if (!asset) continue; // <- a coinless token exits here
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
…
The guard is not itself wrong — toToken(tokenId, entry, asset, registry, …) takes asset as a
required argument, so skipping was the only safe thing to do when every indexed token was guaranteed to
have one. That guarantee no longer holds.
Why it changed
wallet-api ships coinless tokens as of unicity-sphere/wallet-api#140 (cb4c275, deployed). Such a
token is indexed as an ordinary active row with zero asset rows, and GET /v1/inventory returns it
with assets omitted and a new tokenType field. Verified end to end on staging: mint, deposit,
owner-to-owner transfer, claim, spend and restore all work.
The sync path already handles the new shape safely — applyOne (:13070-13082) normalises with
assets: item.assets ?? prev?.assets ?? [], so the mirror entry is always an array and nothing throws.
Confirmed by driving the shipped 0.16.0 client against deployed staging holding a coinless token: it
authenticates, syncs, parses the response including the unknown tokenType field (parseJsonBody is a
bare JSON.parse, no strict rejection), and reports correct balances. The token is simply absent
from tokens().
So this is a display gap rather than a correctness bug: the token is held, tracked in the mirror,
spendable and transferable through the lower-level paths. It just cannot be seen.
Fix
Make the asset optional through tokens() and toToken rather than filtering the entry out. Two
questions that belong to whoever owns the token model:
filter.coinId. A coinless token matches no coin id. It should presumably be excluded when a
coin filter is supplied and included when none is, which falls out naturally if the filter check
moves inside an asset !== undefined branch.
- What the token carries instead of
coinId/amount. wallet-api now indexes and returns
tokenType — the 1–64 byte genesis type — precisely so a client can name the KIND of token it
holds. Note it is a class label, not an identity: token_id is the instance key, and every
token of one kind shares a type. Canonical per-network ids live in the upstream registry
(unicity-ids.<network>.json, the assetKind: "non-fungible" entry — testnet2 971a26ee…,
mainnet 9f190eea…), but a minter may use its own and the token is still valid, so a recognition
table must be keyed by network and must not reject an unrecognised type.
Caveat for grouping: SphereTokenEngine.mint() derives tokenType per operation and split outputs
derive it per output ordinal, so for ordinary value tokens it is per-mint noise, not a class. Only
a caller-supplied type (as mintDataToken takes) is a stable class identifier.
Acceptance
- A coinless token appears in
tokens() when no coin filter is supplied.
- A coin-filtered call still returns only tokens carrying that coin.
- Valued tokens are unchanged.
- Nothing throws for an entry with an empty asset list.
Related, not duplicate
Problem
tokens(registry, filter)drops every mirror entry whose first asset is absent, so a coinlesstoken — one whose genesis data carries no value envelope — never appears in the wallet's token list.
dist/index.jsof0.16.0,:12985-12991:The guard is not itself wrong —
toToken(tokenId, entry, asset, registry, …)takesassetas arequired argument, so skipping was the only safe thing to do when every indexed token was guaranteed to
have one. That guarantee no longer holds.
Why it changed
wallet-api ships coinless tokens as of unicity-sphere/wallet-api#140 (
cb4c275, deployed). Such atoken is indexed as an ordinary active row with zero asset rows, and
GET /v1/inventoryreturns itwith
assetsomitted and a newtokenTypefield. Verified end to end on staging: mint, deposit,owner-to-owner transfer, claim, spend and restore all work.
The sync path already handles the new shape safely —
applyOne(:13070-13082) normalises withassets: item.assets ?? prev?.assets ?? [], so the mirror entry is always an array and nothing throws.Confirmed by driving the shipped 0.16.0 client against deployed staging holding a coinless token: it
authenticates, syncs, parses the response including the unknown
tokenTypefield (parseJsonBodyis abare
JSON.parse, no strict rejection), and reports correct balances. The token is simply absentfrom
tokens().So this is a display gap rather than a correctness bug: the token is held, tracked in the mirror,
spendable and transferable through the lower-level paths. It just cannot be seen.
Fix
Make the asset optional through
tokens()andtoTokenrather than filtering the entry out. Twoquestions that belong to whoever owns the token model:
filter.coinId. A coinless token matches no coin id. It should presumably be excluded when acoin filter is supplied and included when none is, which falls out naturally if the filter check
moves inside an
asset !== undefinedbranch.coinId/amount. wallet-api now indexes and returnstokenType— the 1–64 byte genesis type — precisely so a client can name the KIND of token itholds. Note it is a class label, not an identity:
token_idis the instance key, and everytoken of one kind shares a type. Canonical per-network ids live in the upstream registry
(
unicity-ids.<network>.json, theassetKind: "non-fungible"entry — testnet2971a26ee…,mainnet
9f190eea…), but a minter may use its own and the token is still valid, so a recognitiontable must be keyed by network and must not reject an unrecognised type.
Caveat for grouping:
SphereTokenEngine.mint()derivestokenTypeper operation and split outputsderive it per output ordinal, so for ordinary value tokens it is per-mint noise, not a class. Only
a caller-supplied type (as
mintDataTokentakes) is a stable class identifier.Acceptance
tokens()when no coin filter is supplied.Related, not duplicate
symptom of it; filing separately because the guard above is a specific, self-contained fix).
[{coinId:"",amount:"0"}]and silently dropped. Different path, same root cause: the client assumes a token has ≥ 1 asset.
isSpherePaymentDatamisreads a valid envelope as valueless.