feat(wallet): show and send coinless tokens in the Tokens tab - #517
Open
MastaP wants to merge 5 commits into
Open
feat(wallet): show and send coinless tokens in the Tokens tab#517MastaP wants to merge 5 commits into
MastaP wants to merge 5 commits into
Conversation
The Tokens tab rendered coin-shaped rows only, so a coinless token (an NFT) was invisible — and once sphere-sdk#777 made it transferable there was still no way to move one. Tokens tab now lists coinless holdings alongside coin tokens, with an "NFTs only" toggle. A coinless row shows the class name and icon the registry supplies for its token type; an UNRECOGNISED type renders from the type itself and stays fully usable, because the registry supplies a display name, never permission to show a token. No amount, no decimals — the token IS the thing. The Assets tab is unchanged: it still aggregates coins only. Every row gets a send action that moves THAT token whole — one named source, no split, so a valued token's coins travel with it. Coin rows route to sendWholeToken; coinless rows route to sendCoinless, which additionally refuses a valued source, so a row that is wrong about holding an NFT fails loudly instead of quietly moving coins. useTransfer is parameterized rather than forked: the subscription-key gate, the quota gate, the possibly-committed conversion that prevents double-pay, and the refetch fan-out all apply identically to a whole-token send, so the divergence is one branch at the SDK call. The coinless query key nests under tokens.all, so the refetch sites that already exist refresh it too. The empty state now counts both reads — a wallet holding only NFTs is not empty. tokens() and coinless() are disjoint, so nothing is counted twice. Also removes the `coinId !== 'NAMETAG'` filter (5 sites). The self-issued UnicityIdToken mint was removed with the 2.0.0 base-SDK bump; no such coinId is emitted anywhere, so the filter only obscured the real predicate. Pins @unicitylabs/sphere-sdk 0.17.1 (coinless(), tokenData(), sendWholeToken, sendCoinless, TokenRegistry.getTypeMeta). Closes #502
The modal is reused for every row, but nothing reset it when the target changed. Two consequences, both wrong in the same way — one token's outcome presented as another's: - a send that resolved after the parent swapped targets wrote its success onto the new token, so the screen claimed a token had been sent that never left; - a success screen from the previous token was still standing when the next one opened, so there was no form to send it with. State now resets when the target changes, and each send resolves against the generation it started in — a completion from a retired target is dropped rather than painted onto whatever is showing. Closing during a send is also refused now. The spend may already be on-chain, and the modal is the only surface saying so; dismissing it would leave the user believing nothing happened, which is exactly when someone sends again. Tests cover both races plus the keep-open copy and the per-kind verb routing.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f714cc118
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A coinless arrival is announced in `coinless`, never in `tokens` — it carries no coin. The incoming handler read only `tokens`, so an NFT summed an empty list to 0 and took '?' for the symbol no NFT has, and the receive toast said "+0 ?". It also published that zero as balance progress, promising a balance move that was never coming. Coinless arrivals now take their own path: the toast names what arrived (the class name, or a count when several land from one sender) with no amount and no symbol, and publishes no progress. An unrecognised token type still announces — it degrades to "an NFT" rather than going silent. Observed on staging receiving a real NFT.
Clicking any token row — coinless or coin — opens its genesis payload as hex, alongside the full token id and, for a coinless token, its class. One call serves both kinds because it returns whatever the minter wrote: for a coinless token that is its data; for a coin token it is the value envelope (CBOR tag 39050), so the hex decodes to the coin id and amount in any CBOR parser. Verified on staging — a 4200-unit mint reads back as d9988a...421068f6, where 0x1068 is the amount. Shown raw rather than decoded on purpose: the wallet has no schema for an arbitrary minter's payload, and rendering a guess would be worse than rendering the bytes. A read failure is surfaced as an error rather than as "no data" — readTokenData throws for a token not held or with no blob, and reporting that as empty would misdescribe what the token carries. Fetched only when a token is actually opened: the blob is a round trip each.
The toggle rendered only while the wallet held a coinless token, but the filter state outlived that condition: sending the last NFT with the filter on left nftOnly true and the toggle gone, so the remaining coin tokens were hidden with no control left to unhide them. The empty state then claimed "No individual tokens found" for a wallet that held several. The toggle now stays rendered whenever the filter is on, and the empty copy names the filter as the reason rather than describing the wallet wrongly. The decision moved into tokensTabView(), a pure function over (coinless, coins, nftOnly) — the stranding case is a state combination, and it is worth being able to assert it directly instead of through the view.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Coinless tokens (NFTs) were invisible in the wallet, and once sphere-sdk#777 made them transferable there was still no way to move one.
What changed
Tokens tab lists coinless holdings alongside coin tokens, with an "NFTs only" toggle. A coinless row shows the class name and icon the registry supplies for its token type; an unrecognised type renders from the type itself and stays fully usable — the registry supplies a display name, never permission to show a token. No amount, no decimals: the token is the thing.
Assets tab is unchanged — it still aggregates coins only.
Every row gets a send action that moves that token whole: one named source, no split, so a valued token's coins travel with it. Coin rows route to
sendWholeToken; coinless rows route tosendCoinless, which additionally refuses a valued source — so a row that is wrong about holding an NFT fails loudly instead of quietly moving coins.Empty state counts both reads. A wallet holding only NFTs is not empty.
tokens()andcoinless()are disjoint, so nothing is counted twice.Notes for review
useTransferis parameterized, not forked. The subscription-key gate, the quota gate, the possibly-committed conversion that prevents double-pay, and the refetch fan-out all apply identically to a whole-token send, so the divergence is one branch at the SDK call. Forking it would have meant re-implementing the money safety.The coinless query key nests under
tokens.all, so the three refetch sites that already exist (top-up, transfer, swap) refresh it with no new plumbing.Removes the
coinId !== 'NAMETAG'filter (5 sites). The self-issuedUnicityIdTokenmint was removed with the 2.0.0 base-SDK bump; the onlyNAMETAGleft in the SDK isAddressType = 'DIRECT' | 'NAMETAG'incore/address.ts, an address discriminator for parsing@alicethat is never assigned toToken.coinId. The filter guarded a value that cannot exist.Pins
@unicitylabs/sphere-sdk0.17.1.Verification
tsc -bclean, lint 0 errorscodex exec review; both findings addressed — the modal state races are fixed and covered, each new test verified to fail without its fixCloses #502