feat(frontend): rank economic tokens above test tokens in the Transfer picker #136

Closed
opened 2026-08-17 14:30:09 +00:00 by PlasticDigits · 5 comments
PlasticDigits commented 2026-08-17 14:30:09 +00:00 (Migrated from gitlab.com)

Summary

On the Transfer page token picker, economic (real-value) tokens must always appear first and noneconomic test tokens must always appear last. Bundle classification, stable sort, default selection, unit/e2e coverage, and a frontend invariant into this single issue. Display order only — do not change which tokens are bridgeable, mappings, fees, or on-chain behavior.


Current codebase

The Transfer form builds the token dropdown from Terra’s on-chain token registry, then filters to tokens that have a dest mapping on the selected source/dest chain. There is no economic vs test ranking.

Area Behavior today
Options builder buildTransferTokens() in packages/frontend/src/services/transfer/buildTransferTokens.ts
Terra / Solana source Enabled registry rows, optionally filtered by destChainMappings; order = Terra tokens query order
EVM source Object.entries(sourceChainMappings) — insertion order of a map filled as parallel token_dest_mapping queries complete (non-deterministic)
Fallback Registry rows with evm_token_address, or a single env fallback token
Dropdown render TokenSelect maps tokens in array order (packages/frontend/src/components/transfer/TokenSelect.tsx)
Default selection TransferForm auto-selects transferTokens[0] when the current id is empty or no longer valid (TransferForm.tsx ~377–384)
Token identity TokenOption is { id, symbol, tokenId, evmTokenAddress? } — no isTest / economic field (packages/frontend/src/types/tokenOption.ts)
Tokenlist packages/frontend/public/tokens/tokenlist.json lists economic assets (LUNC, USTC, CL8Y, ALPHA, USTR, USTRIX, SpaceUSD, CL8Y-cb). Test tokens are not in this list.
Test / faucet catalog FaucetPanel.tsx MAINNET_FAUCET_TOKENS: testa / testb / tdec (hard-coded CW20 + EVM + SPL). Local QA: TKNA / TKNB / TKNC / KDEC (+ tLUNC / synthetic SOL for faucet, not the same as mainnet economic LUNC)
Registry source useTokenRegistry() paginates Terra tokens { start_after, limit } — CosmWasm map iteration order, not product order
Existing tests buildTransferTokens.test.ts covers mapping-load gating only; e2e/token-selection.spec.ts only checks that a selector/symbol appears; TokenSelect has no dedicated sort tests

Known mainnet noneconomic test tokens (must sort to the bottom when present):

Symbol Terra CW20
testa terra16ahm9hn5teayt2as384zf3uudgqvmmwahqfh0v9e3kaslhu30l8q38ftvh
testb terra1vqfe2ake427depchntwwl6dvyfgxpu5qdlqzfjuznxvw6pqza0hqalc9g3
tdec terra1pa7jxtjcu3clmv0v8n2tfrtlfepneyv8pxa7zmhz50kj8unuv0zq37apvv

Peer EVM/SPL addresses live in FaucetPanel MAINNET_FAUCET_TOKENS and Solana register scripts. Local QA test tokens: TKNA / TKNB / TKNC / KDEC (env-driven addresses).

Economic tokens (must sort to the top when present and routed): LUNC (uluna), USTC (uusd), CL8Y (CW20 terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3 and MegaETH CL8Y-cb 0xfBAa45A537cF07dC768c469FfaC4e88208B0098D), plus other real-value registry tokens (ALPHA, USTR, USTRIX, SpaceUSD, future listed assets).

The picker is the Amount field combobox (data-testid="token-select"), not Settings → Tokens or Settings → Faucet.


Why this is needed

  1. Default selection follows list order. TransferForm selects transferTokens[0]. If testa/testb/tdec sort first (registry pagination or racey Object.entries on mappings), a user who opens Bridge can start a transfer on a noneconomic faucet token without noticing.
  2. Mainnet mixes real value and QA tokens. testa / testb / tdec are intentionally registered on production so QA can exercise routes. They must remain selectable, but they must not compete with LUNC / CL8Y / USTC for first-screen attention.
  3. Unstable EVM-source order. Mapping queries complete independently; Object.entries order can change across reloads. Users see a jumpy dropdown and an inconsistent default token.
  4. Product rule. Economic tokens always at the top; test tokens always at the bottom. This is a display/default-selection rule only.

Constraints & guardrails

  1. Display order only. Sorting must not change id / tokenId / evmTokenAddress, dest mappings, decimals, fees, hash encoding, or which tokens are offered for a route.
  2. Do not hide test tokens. testa / testb / tdec (and local TKNA/B/C / KDEC) stay in the dropdown when the route is configured. QA and faucet users must still be able to select them.
  3. Explicit classification, not fuzzy heuristics. Do not classify as test solely because the symbol contains "test" (false positives). Prefer a denylist of known noneconomic token ids (Terra denom/CW20, plus EVM/SPL aliases from the faucet catalog) with case-insensitive address/denom match. Unknown registered tokens default to economic (top group) so a newly listed real asset is not buried.
  4. Do not trust display symbol for ranking. A registered test mint must stay in the bottom group even if on-chain symbol() is spoofed to CL8Y / LUNC. Rank by canonical registry id / addresses, not the label shown in the list.
  5. Stable within groups. After economic-then-test, sort each group stably (recommended: tokenlist.json order for listed assets, then alphabetical by display symbol, then id). Reloads and mapping-query races must not reshuffle.
  6. Default selection. When auto-selecting because the field is empty or the previous id is invalid for the new route, pick the first token after sort (an economic token if any exist). Do not yank a user off an explicitly chosen test token while that token remains valid for the current chain pair.
  7. Chain-pair filtering stays first. Only tokens with a valid dest mapping for the selected source/dest appear. Sort applies to that filtered set. If a route has only test tokens, they still appear (all at the “bottom” group; first of that list is the default).
  8. Scope = Transfer token picker. Do not reorder Settings → Tokens, Settings → Faucet, Verify, or History as part of this issue.
  9. No on-chain / operator changes. No new Terra query fields, no contract is_economic flag, no operator API. Frontend-only.
  10. Local vs mainnet. Same ranking function. Local QA tokens (TKNA/B/C, KDEC) are test (bottom). Local LUNC / uluna remains economic (top). Synthetic SOL used only in the faucet panel is out of scope unless it also appears in the Transfer picker.
  11. Do not break existing invariants in docs/FRONTEND_BRIDGE_INVARIANTS.md (INV-UX1 CTA/amount, INV-RCP1 recipient, token logos, mapping-load gating from glab #89).
  12. No secrets. Classification data is public token ids already in FaucetPanel / docs.

Relevant files

Path Why
packages/frontend/src/services/transfer/buildTransferTokens.ts Canonical place to sort after filter; all Transfer directions go through here
packages/frontend/src/services/transfer/buildTransferTokens.test.ts Unit tests for order, default-first, mixed/only-test/only-economic
packages/frontend/src/types/tokenOption.ts Optional isTest (or keep classification internal to the sorter)
New helper e.g. packages/frontend/src/utils/tokenEconomicRank.ts (name as implemented) Shared denylist + comparator; keep FaucetPanel addresses as the source of truth or extract a shared constant
packages/frontend/src/components/settings/FaucetPanel.tsx Existing testa/testb/tdec (+ local TKN*) address catalog — reuse, do not duplicate silently
packages/frontend/src/components/transfer/TokenSelect.tsx Renders given order; no extra sort unless tests prove a second caller bypasses buildTransferTokens
packages/frontend/src/components/transfer/AmountInput.tsx Passes tokens through to TokenSelect
packages/frontend/src/components/transfer/TransferForm.tsx Auto-selects transferTokens[0]; must remain valid after sort
packages/frontend/src/hooks/useSourceChainTokenMappings.ts Unordered Record today; sorting in the builder must neutralize this
packages/frontend/public/tokens/tokenlist.json Economic-symbol resolution; optional secondary sort key — not the test denylist
packages/frontend/src/components/transfer/SubComponents.test.tsx AmountInput / TokenSelect option order if covered here
packages/frontend/e2e/token-selection.spec.ts Extend: economic before test in the open listbox
docs/FRONTEND_BRIDGE_INVARIANTS.md New short INV for picker ranking
docs/solana-mainnet-test-tokens-checklist.md Documents noneconomic testa/testb/tdec (reference, not code)

  1. Extract a noneconomic id set (or isNoneconomicBridgeToken(id, option)) covering:
    • Mainnet Terra CW20s for testa / testb / tdec (table above)
    • Mainnet EVM addresses and Solana mints already listed in MAINNET_FAUCET_TOKENS
    • Local QA symbols/addresses for tkna / tknb / tknc / kdec (from LOCAL_FAUCET_TOKENS / env)
    • Match TokenOption.id, tokenId, and evmTokenAddress (lowercased)
  2. Prefer sharing the faucet catalog (move addresses to a small testTokens.ts imported by FaucetPanel and the sorter) so a newly deployed test mint is classified in one place. If extraction is too large for this MR, duplicate with a comment pointing at FaucetPanel and a test that both lists stay in sync.
  3. Sort once in buildTransferTokens after each return path that yields multiple options:
    • economic (!isNoneconomic) first
    • noneconomic last
    • within each group: tokenlist order if listed, else localeCompare on symbol, then id
  4. Keep TransferForm auto-select as transferTokens[0] — after sort that is the top economic token when any exist.
  5. Optional visual cue (not required): a muted “Test” suffix on bottom-group rows. If added, must not affect id or submit path. Skip if it expands UX scope; order is the acceptance bar.
  6. Docs: add INV-FE-TOKEN-ORDER-1 (or similar) to docs/FRONTEND_BRIDGE_INVARIANTS.md.
  7. Do not sort in TokenSelect only. Callers that use transferTokens[0] for default selection would still pick the unsorted first item.

Acceptance criteria

  • For any Transfer source/dest pair that includes both economic and test tokens, every economic option appears above every test option in the token listbox.
  • testa, testb, and tdec (mainnet) sort to the bottom whenever they are in the filtered set; same for local TKNA / TKNB / TKNC / KDEC.
  • LUNC, USTC, CL8Y (and other non-denylisted registered tokens) sort to the top whenever they are in the filtered set.
  • Test tokens remain selectable and still submit the correct token id / mapping (no identity swap).
  • With no prior valid selection, the default token is the first economic option if one exists; if the route is test-only, default is the first test option.
  • Changing source or dest chain re-filters then re-sorts; an explicit test-token selection is kept if still valid, otherwise selection jumps to the new list’s first (economic-preferred) item.
  • EVM-source mapping load races no longer produce a random dropdown order; two loads of the same filtered set produce the same order.
  • Settings → Faucet / Settings → Tokens order unchanged unless a shared constant is extracted (Faucet panel order may stay as today).
  • Unit tests cover mixed, economic-only, test-only, unknown-token-as-economic, spoofed symbol still ranked by id, and default-first.
  • Playwright or component test asserts listbox order (economic then test).
  • docs/FRONTEND_BRIDGE_INVARIANTS.md documents the ranking rule.
  • Existing frontend unit/e2e suites stay green (make test-frontend / package scripts).

Test plan — functional paths

Unit (buildTransferTokens + rank helper)

  • Mixed set: [testa, uluna, testb, CL8Y CW20] → [uluna, CL8Y, testa, testb] (economic first; within-group stable).
  • Economic only: order is stable (tokenlist / symbol / id); no empty holes.
  • Test only: all remain; first item is a test token (valid default).
  • Unknown registry id (not in denylist, not in tokenlist) sorts with economic (top), not bottom.
  • Classification by Terra CW20 / denom / EVM address / SPL mint aliases, not by display symbol.
  • Spoof: option with test CW20 id but symbol: 'CL8Y' still ranks as test (bottom).
  • Spoof: option with CL8Y CW20 id but symbol: 'testa' still ranks as economic (top).
  • EVM-source path (sourceChainMappings object) output order matches the same comparator (insertion order ignored).
  • Terra/Solana path (registry array + dest filter) same comparator.
  • Registry fallback path (evm_token_address rows) same comparator.
  • Empty / loading ([] while EVM mappings loading) unchanged.
  • Disabled registry tokens still excluded before sort (existing filter).
  • Case: mixed-case 0x EVM addresses still match the denylist.
  • Duplicate ids do not appear; sort is not a second source of duplicates.

Component (TokenSelect / AmountInput)

  • Open listbox: role="option" order matches the sorted tokens prop (no re-sort that undoes the builder).
  • Selecting a bottom-group test token updates the combobox value and does not jump back to the first economic token on re-render.
  • Single-token list: no dropdown (existing tokens.length > 1 behavior) still holds.

TransferForm default selection

  • Mount with mixed tokens and empty selectedTokenId → selects first economic id.
  • User selects testa; tokens array identity changes but still includes testa → selection stays testa.
  • Dest chain change drops testa from the filtered set → selection moves to first remaining (economic if any).

Local QA vs mainnet catalogs

  • Helper tests include both MAINNET_FAUCET_TOKENS ids and LOCAL_FAUCET_TOKENS symbols/addresses.
  • If catalogs are split files, a test or shared module prevents FaucetPanel and sorter from drifting.

Playwright (packages/frontend/e2e/token-selection.spec.ts or new spec)

  • After token-select is visible, open the listbox; collect option labels/data-tokenid; assert no test id appears before an economic id.
  • Existing token-selection / transfer specs still pass (default may now be LUNC/CL8Y instead of TKNA on local — update fixtures that assumed first token was TKNA/testa).
  • One smoke: select testa from the bottom, fill amount, confirm the form still holds testa (not silently swapped).

Manual QA (mainnet staging or production with faucet)

  • BSC (or opBNB / MegaETH) → Terra: dropdown shows LUNC/CL8Y/… first, testa/testb/tdec last.
  • Terra → EVM and Terra → Solana: same ranking.
  • Solana → EVM / Terra: same ranking when both groups are mapped.
  • Route with only test tokens (if any pair): list is non-empty and usable.
  • Reload twice: same order.
  • Settings → Faucet still lists testa/testb/tdec as today (this issue does not redesign faucet).

Test plan — attack, hack & abuse vectors

Vector Expected defense How to test
Spoof on-chain ERC20/CW20 symbol() to CL8Y / LUNC on a test mint Rank by canonical registry id / known test addresses, not display symbol Unit: test CW20 id + fake symbol still bottom
Spoof symbol to testa on an economic mint to bury it Unknown / non-denylisted ids stay economic (top) Unit: CL8Y id + symbol: 'testa' still top
Register a new noneconomic mint not on the denylist so it appears at the top next to LUNC Accepted residual risk: denylist is explicit. Mitigate by sharing FaucetPanel catalog + MR checklist to add new faucet tokens to the set. Do not auto-promote “anything not in tokenlist” to test (that would bury new real listings) Document; add regression when a new faucet token is deployed
Register a malicious economic-looking token (fake CL8Y CW20) Out of scope for sort (registry/enablement is admin). Sorting must not treat “looks like CL8Y” as identity. Do not add a symbol allowlist that could hide a legitimately registered asset Unit: unknown id stays top; enablement still from registry
XSS / HTML in token symbol to break list layout or clickjack another row Existing text rendering ({displayLabel}); no dangerouslySetInnerHTML. Sort must not inject markup Render a symbol containing <img> / <script>; expect escaped text
Click-jack / overlay to select a test token while the label shows CL8Y Options must keep data-tokenid equal to the row’s id; click handler uses that id, not visible text Component: click option whose label is spoofed; onChange receives test id
Race: mappings resolve test token first, UI defaults to testa, then economic tokens prepend and reset a user mid-input Auto-select only when current id is empty/invalid. Once the user (or an initial default) has a still-valid id, do not replace it when the list grows TransferForm test: select/default testa, then prepend uluna to the array; selection stays testa if that was already set. Initial empty state after full load should be economic — distinguish “first paint empty” vs “user chose test”. Recommended: do not auto-select until mappings have finished loading (already true for EVM via empty-while-loading). After load, set default once.
Reorder as an attack on hash / dest token Sort must not swap evmTokenAddress between rows Unit: after sort, each id still maps to the same address as input
Hide economic tokens by classifying everything as test Denylist is closed-set; default is economic Unit: empty denylist match → all top
LocalStorage / URL ?token=testa forcing a buried token without disclosure If deep-link token selection exists, it may select testa (explicit). Do not add a URL param that silently overrides to test. If no such param exists, do not introduce one Grep TransferForm for query-param token; none expected
Keyboard a11y: listbox order vs aria so a screen reader announces CL8Y but Enter submits testa DOM order = ranked order; aria-selected on the real selected option Keyboard: Arrow through options; selected id matches focused option
QA env fixture breakage: e2e assumed first token is TKNA and now defaults to LUNC, causing wrong-token transfers in CI Update e2e to select by data-tokenid / name, not “first option” Audit e2e/*.spec.ts for first-token assumptions; fix in the same MR
Operator/admin confuses Settings token list order with Transfer order and mis-operates Settings unsorted by this feature; docs say Transfer-only Manual: Settings → Tokens order unchanged

Verification criteria

Merge is verified when all of the following hold:

  1. Automated: New rank / buildTransferTokens tests pass, including spoofed-symbol and mixed-set order. make test-frontend (or package equivalent) is green.
  2. E2E: Token-selection spec asserts economic-before-test when both groups exist; transfer specs that depended on “first token” are updated and green.
  3. Manual: On a build with both CL8Y/LUNC and testa/testb/tdec routed, open Transfer, open the token combobox: economic group is contiguous at the top, test group contiguous at the bottom; default is economic; selecting testa still bridges testa.
  4. Stability: Reload and switch BSC ↔ opBNB ↔ MegaETH ↔ Terra ↔ Solana: ranking rule holds on every pair that has mixed tokens.
  5. Docs: INV added; MR links this issue.
  6. No regression: Mapping-load empty state (glab #89), INV-UX1 amount/CTA, recipient validation, and faucet claim UI still behave as before.

Out of scope

  • Hiding or disabling test tokens on mainnet
  • On-chain economic flag or Terra registry schema changes
  • Reordering Settings → Tokens / Faucet (except extracting a shared constant)
  • Token search/filter UI, favorites, or balance-based sort
  • Changing logos, names, or faucet claim amounts
  • Operator / contract / rate-limit work

References

  • Faucet catalog: packages/frontend/src/components/settings/FaucetPanel.tsx (MAINNET_FAUCET_TOKENS, LOCAL_FAUCET_TOKENS)
  • Noneconomic SPL checklist: docs/solana-mainnet-test-tokens-checklist.md
  • Tokenlist: packages/frontend/public/tokens/tokenlist.json
  • Default selection: packages/frontend/src/components/transfer/TransferForm.tsx (transferTokens[0])
  • Production bridge: https://bridge.cl8y.com/
## Summary On the Transfer page token picker, **economic (real-value) tokens must always appear first** and **noneconomic test tokens must always appear last**. Bundle classification, stable sort, default selection, unit/e2e coverage, and a frontend invariant into this single issue. Display order only — do not change which tokens are bridgeable, mappings, fees, or on-chain behavior. --- ## Current codebase The Transfer form builds the token dropdown from Terra’s on-chain token registry, then filters to tokens that have a dest mapping on the selected source/dest chain. There is **no economic vs test ranking**. | Area | Behavior today | |------|----------------| | Options builder | `buildTransferTokens()` in `packages/frontend/src/services/transfer/buildTransferTokens.ts` | | Terra / Solana source | Enabled registry rows, optionally filtered by `destChainMappings`; order = Terra `tokens` query order | | EVM source | `Object.entries(sourceChainMappings)` — insertion order of a map filled as parallel `token_dest_mapping` queries complete (**non-deterministic**) | | Fallback | Registry rows with `evm_token_address`, or a single env fallback token | | Dropdown render | `TokenSelect` maps `tokens` in array order (`packages/frontend/src/components/transfer/TokenSelect.tsx`) | | Default selection | `TransferForm` auto-selects `transferTokens[0]` when the current id is empty or no longer valid (`TransferForm.tsx` ~377–384) | | Token identity | `TokenOption` is `{ id, symbol, tokenId, evmTokenAddress? }` — **no** `isTest` / `economic` field (`packages/frontend/src/types/tokenOption.ts`) | | Tokenlist | `packages/frontend/public/tokens/tokenlist.json` lists **economic** assets (LUNC, USTC, CL8Y, ALPHA, USTR, USTRIX, SpaceUSD, CL8Y-cb). **Test tokens are not in this list.** | | Test / faucet catalog | `FaucetPanel.tsx` `MAINNET_FAUCET_TOKENS`: **testa / testb / tdec** (hard-coded CW20 + EVM + SPL). Local QA: TKNA / TKNB / TKNC / KDEC (+ tLUNC / synthetic SOL for faucet, not the same as mainnet economic LUNC) | | Registry source | `useTokenRegistry()` paginates Terra `tokens { start_after, limit }` — CosmWasm map iteration order, not product order | | Existing tests | `buildTransferTokens.test.ts` covers mapping-load gating only; `e2e/token-selection.spec.ts` only checks that a selector/symbol appears; `TokenSelect` has no dedicated sort tests | **Known mainnet noneconomic test tokens** (must sort to the bottom when present): | Symbol | Terra CW20 | |--------|------------| | testa | `terra16ahm9hn5teayt2as384zf3uudgqvmmwahqfh0v9e3kaslhu30l8q38ftvh` | | testb | `terra1vqfe2ake427depchntwwl6dvyfgxpu5qdlqzfjuznxvw6pqza0hqalc9g3` | | tdec | `terra1pa7jxtjcu3clmv0v8n2tfrtlfepneyv8pxa7zmhz50kj8unuv0zq37apvv` | Peer EVM/SPL addresses live in `FaucetPanel` `MAINNET_FAUCET_TOKENS` and Solana register scripts. Local QA test tokens: TKNA / TKNB / TKNC / KDEC (env-driven addresses). **Economic tokens** (must sort to the top when present and routed): LUNC (`uluna`), USTC (`uusd`), CL8Y (CW20 `terra16wtml2q66g82fdkx66tap0qjkahqwp4lwq3ngtygacg5q0kzycgqvhpax3` and MegaETH `CL8Y-cb` `0xfBAa45A537cF07dC768c469FfaC4e88208B0098D`), plus other real-value registry tokens (ALPHA, USTR, USTRIX, SpaceUSD, future listed assets). The picker is the Amount field combobox (`data-testid="token-select"`), not Settings → Tokens or Settings → Faucet. --- ## Why this is needed 1. **Default selection follows list order.** `TransferForm` selects `transferTokens[0]`. If testa/testb/tdec sort first (registry pagination or racey `Object.entries` on mappings), a user who opens Bridge can start a transfer on a **noneconomic faucet token** without noticing. 2. **Mainnet mixes real value and QA tokens.** testa / testb / tdec are intentionally registered on production so QA can exercise routes. They must remain selectable, but they must not compete with LUNC / CL8Y / USTC for first-screen attention. 3. **Unstable EVM-source order.** Mapping queries complete independently; `Object.entries` order can change across reloads. Users see a jumpy dropdown and an inconsistent default token. 4. **Product rule.** Economic tokens always at the top; test tokens always at the bottom. This is a display/default-selection rule only. --- ## Constraints & guardrails 1. **Display order only.** Sorting must not change `id` / `tokenId` / `evmTokenAddress`, dest mappings, decimals, fees, hash encoding, or which tokens are offered for a route. 2. **Do not hide test tokens.** testa / testb / tdec (and local TKNA/B/C / KDEC) stay in the dropdown when the route is configured. QA and faucet users must still be able to select them. 3. **Explicit classification, not fuzzy heuristics.** Do **not** classify as test solely because the symbol contains `"test"` (false positives). Prefer a **denylist of known noneconomic token ids** (Terra denom/CW20, plus EVM/SPL aliases from the faucet catalog) with case-insensitive address/denom match. Unknown registered tokens default to **economic** (top group) so a newly listed real asset is not buried. 4. **Do not trust display symbol for ranking.** A registered test mint must stay in the bottom group even if on-chain `symbol()` is spoofed to `CL8Y` / `LUNC`. Rank by canonical registry `id` / addresses, not the label shown in the list. 5. **Stable within groups.** After economic-then-test, sort each group stably (recommended: tokenlist.json order for listed assets, then alphabetical by display symbol, then `id`). Reloads and mapping-query races must not reshuffle. 6. **Default selection.** When auto-selecting because the field is empty or the previous id is invalid for the new route, pick the **first token after sort** (an economic token if any exist). Do **not** yank a user off an explicitly chosen test token while that token remains valid for the current chain pair. 7. **Chain-pair filtering stays first.** Only tokens with a valid dest mapping for the selected source/dest appear. Sort applies to that filtered set. If a route has only test tokens, they still appear (all at the “bottom” group; first of that list is the default). 8. **Scope = Transfer token picker.** Do not reorder Settings → Tokens, Settings → Faucet, Verify, or History as part of this issue. 9. **No on-chain / operator changes.** No new Terra query fields, no contract `is_economic` flag, no operator API. Frontend-only. 10. **Local vs mainnet.** Same ranking function. Local QA tokens (TKNA/B/C, KDEC) are test (bottom). Local LUNC / `uluna` remains economic (top). Synthetic SOL used only in the faucet panel is out of scope unless it also appears in the Transfer picker. 11. **Do not break existing invariants** in `docs/FRONTEND_BRIDGE_INVARIANTS.md` (INV-UX1 CTA/amount, INV-RCP1 recipient, token logos, mapping-load gating from glab #89). 12. **No secrets.** Classification data is public token ids already in `FaucetPanel` / docs. --- ## Relevant files | Path | Why | |------|-----| | `packages/frontend/src/services/transfer/buildTransferTokens.ts` | Canonical place to sort after filter; all Transfer directions go through here | | `packages/frontend/src/services/transfer/buildTransferTokens.test.ts` | Unit tests for order, default-first, mixed/only-test/only-economic | | `packages/frontend/src/types/tokenOption.ts` | Optional `isTest` (or keep classification internal to the sorter) | | New helper e.g. `packages/frontend/src/utils/tokenEconomicRank.ts` (name as implemented) | Shared denylist + comparator; keep `FaucetPanel` addresses as the source of truth or extract a shared constant | | `packages/frontend/src/components/settings/FaucetPanel.tsx` | Existing testa/testb/tdec (+ local TKN*) address catalog — reuse, do not duplicate silently | | `packages/frontend/src/components/transfer/TokenSelect.tsx` | Renders given order; no extra sort unless tests prove a second caller bypasses `buildTransferTokens` | | `packages/frontend/src/components/transfer/AmountInput.tsx` | Passes `tokens` through to `TokenSelect` | | `packages/frontend/src/components/transfer/TransferForm.tsx` | Auto-selects `transferTokens[0]`; must remain valid after sort | | `packages/frontend/src/hooks/useSourceChainTokenMappings.ts` | Unordered `Record` today; sorting in the builder must neutralize this | | `packages/frontend/public/tokens/tokenlist.json` | Economic-symbol resolution; optional secondary sort key — **not** the test denylist | | `packages/frontend/src/components/transfer/SubComponents.test.tsx` | AmountInput / TokenSelect option order if covered here | | `packages/frontend/e2e/token-selection.spec.ts` | Extend: economic before test in the open listbox | | `docs/FRONTEND_BRIDGE_INVARIANTS.md` | New short INV for picker ranking | | `docs/solana-mainnet-test-tokens-checklist.md` | Documents noneconomic testa/testb/tdec (reference, not code) | --- ## Recommended solution direction 1. **Extract a noneconomic id set** (or `isNoneconomicBridgeToken(id, option)`) covering: - Mainnet Terra CW20s for testa / testb / tdec (table above) - Mainnet EVM addresses and Solana mints already listed in `MAINNET_FAUCET_TOKENS` - Local QA symbols/addresses for tkna / tknb / tknc / kdec (from `LOCAL_FAUCET_TOKENS` / env) - Match `TokenOption.id`, `tokenId`, and `evmTokenAddress` (lowercased) 2. **Prefer sharing the faucet catalog** (move addresses to a small `testTokens.ts` imported by FaucetPanel and the sorter) so a newly deployed test mint is classified in one place. If extraction is too large for this MR, duplicate with a comment pointing at `FaucetPanel` and a test that both lists stay in sync. 3. **Sort once in `buildTransferTokens`** after each return path that yields multiple options: - `economic` (`!isNoneconomic`) first - `noneconomic` last - within each group: tokenlist order if listed, else localeCompare on symbol, then `id` 4. **Keep `TransferForm` auto-select as `transferTokens[0]`** — after sort that is the top economic token when any exist. 5. **Optional visual cue (not required):** a muted “Test” suffix on bottom-group rows. If added, must not affect `id` or submit path. Skip if it expands UX scope; order is the acceptance bar. 6. **Docs:** add **INV-FE-TOKEN-ORDER-1** (or similar) to `docs/FRONTEND_BRIDGE_INVARIANTS.md`. 7. **Do not sort in `TokenSelect` only.** Callers that use `transferTokens[0]` for default selection would still pick the unsorted first item. --- ## Acceptance criteria - [ ] For any Transfer source/dest pair that includes both economic and test tokens, **every economic option appears above every test option** in the token listbox. - [ ] testa, testb, and tdec (mainnet) sort to the bottom whenever they are in the filtered set; same for local TKNA / TKNB / TKNC / KDEC. - [ ] LUNC, USTC, CL8Y (and other non-denylisted registered tokens) sort to the top whenever they are in the filtered set. - [ ] Test tokens remain selectable and still submit the correct token id / mapping (no identity swap). - [ ] With no prior valid selection, the default token is the first **economic** option if one exists; if the route is test-only, default is the first test option. - [ ] Changing source or dest chain re-filters then re-sorts; an explicit test-token selection is kept if still valid, otherwise selection jumps to the new list’s first (economic-preferred) item. - [ ] EVM-source mapping load races no longer produce a random dropdown order; two loads of the same filtered set produce the same order. - [ ] Settings → Faucet / Settings → Tokens order unchanged unless a shared constant is extracted (Faucet panel order may stay as today). - [ ] Unit tests cover mixed, economic-only, test-only, unknown-token-as-economic, spoofed symbol still ranked by id, and default-first. - [ ] Playwright or component test asserts listbox order (economic then test). - [ ] `docs/FRONTEND_BRIDGE_INVARIANTS.md` documents the ranking rule. - [ ] Existing frontend unit/e2e suites stay green (`make test-frontend` / package scripts). --- ## Test plan — functional paths ### Unit (`buildTransferTokens` + rank helper) - [ ] Mixed set: `[testa, uluna, testb, CL8Y CW20]` → `[uluna, CL8Y, testa, testb]` (economic first; within-group stable). - [ ] Economic only: order is stable (tokenlist / symbol / id); no empty holes. - [ ] Test only: all remain; first item is a test token (valid default). - [ ] Unknown registry id (not in denylist, not in tokenlist) sorts with **economic** (top), not bottom. - [ ] Classification by Terra CW20 / denom / EVM address / SPL mint aliases, not by display symbol. - [ ] Spoof: option with test CW20 id but `symbol: 'CL8Y'` still ranks as test (bottom). - [ ] Spoof: option with CL8Y CW20 id but `symbol: 'testa'` still ranks as economic (top). - [ ] EVM-source path (`sourceChainMappings` object) output order matches the same comparator (insertion order ignored). - [ ] Terra/Solana path (registry array + dest filter) same comparator. - [ ] Registry fallback path (`evm_token_address` rows) same comparator. - [ ] Empty / loading (`[]` while EVM mappings loading) unchanged. - [ ] Disabled registry tokens still excluded **before** sort (existing filter). - [ ] Case: mixed-case `0x` EVM addresses still match the denylist. - [ ] Duplicate ids do not appear; sort is not a second source of duplicates. ### Component (TokenSelect / AmountInput) - [ ] Open listbox: `role="option"` order matches the sorted `tokens` prop (no re-sort that undoes the builder). - [ ] Selecting a bottom-group test token updates the combobox value and does not jump back to the first economic token on re-render. - [ ] Single-token list: no dropdown (existing `tokens.length > 1` behavior) still holds. ### TransferForm default selection - [ ] Mount with mixed tokens and empty `selectedTokenId` → selects first economic id. - [ ] User selects testa; tokens array identity changes but still includes testa → selection stays testa. - [ ] Dest chain change drops testa from the filtered set → selection moves to first remaining (economic if any). ### Local QA vs mainnet catalogs - [ ] Helper tests include both `MAINNET_FAUCET_TOKENS` ids and `LOCAL_FAUCET_TOKENS` symbols/addresses. - [ ] If catalogs are split files, a test or shared module prevents FaucetPanel and sorter from drifting. ### Playwright (`packages/frontend/e2e/token-selection.spec.ts` or new spec) - [ ] After token-select is visible, open the listbox; collect option labels/`data-tokenid`; assert no test id appears before an economic id. - [ ] Existing token-selection / transfer specs still pass (default may now be LUNC/CL8Y instead of TKNA on local — **update fixtures** that assumed first token was TKNA/testa). - [ ] One smoke: select testa from the bottom, fill amount, confirm the form still holds testa (not silently swapped). ### Manual QA (mainnet staging or production with faucet) - [ ] BSC (or opBNB / MegaETH) → Terra: dropdown shows LUNC/CL8Y/… first, testa/testb/tdec last. - [ ] Terra → EVM and Terra → Solana: same ranking. - [ ] Solana → EVM / Terra: same ranking when both groups are mapped. - [ ] Route with only test tokens (if any pair): list is non-empty and usable. - [ ] Reload twice: same order. - [ ] Settings → Faucet still lists testa/testb/tdec as today (this issue does not redesign faucet). --- ## Test plan — attack, hack & abuse vectors | Vector | Expected defense | How to test | |--------|------------------|-------------| | Spoof on-chain ERC20/CW20 `symbol()` to `CL8Y` / `LUNC` on a test mint | Rank by canonical registry id / known test addresses, not display symbol | Unit: test CW20 id + fake symbol still bottom | | Spoof symbol to `testa` on an economic mint to bury it | Unknown / non-denylisted ids stay **economic** (top) | Unit: CL8Y id + `symbol: 'testa'` still top | | Register a new noneconomic mint **not** on the denylist so it appears at the top next to LUNC | Accepted residual risk: denylist is explicit. Mitigate by sharing FaucetPanel catalog + MR checklist to add new faucet tokens to the set. Do **not** auto-promote “anything not in tokenlist” to test (that would bury new real listings) | Document; add regression when a new faucet token is deployed | | Register a malicious economic-looking token (fake CL8Y CW20) | Out of scope for **sort** (registry/enablement is admin). Sorting must not treat “looks like CL8Y” as identity. Do not add a symbol allowlist that could hide a legitimately registered asset | Unit: unknown id stays top; enablement still from registry | | XSS / HTML in token symbol to break list layout or clickjack another row | Existing text rendering (`{displayLabel}`); no `dangerouslySetInnerHTML`. Sort must not inject markup | Render a symbol containing `<img>` / `<script>`; expect escaped text | | Click-jack / overlay to select a test token while the label shows CL8Y | Options must keep `data-tokenid` equal to the row’s `id`; click handler uses that id, not visible text | Component: click option whose label is spoofed; `onChange` receives test id | | Race: mappings resolve test token first, UI defaults to testa, then economic tokens prepend and **reset** a user mid-input | Auto-select only when current id is empty/invalid. Once the user (or an initial default) has a still-valid id, do not replace it when the list grows | TransferForm test: select/default testa, then prepend uluna to the array; selection stays testa **if** that was already set. **Initial** empty state after full load should be economic — distinguish “first paint empty” vs “user chose test”. Recommended: do not auto-select until mappings have finished loading (already true for EVM via empty-while-loading). After load, set default once. | | Reorder as an attack on hash / dest token | Sort must not swap `evmTokenAddress` between rows | Unit: after sort, each id still maps to the same address as input | | Hide economic tokens by classifying everything as test | Denylist is closed-set; default is economic | Unit: empty denylist match → all top | | LocalStorage / URL `?token=testa` forcing a buried token without disclosure | If deep-link token selection exists, it may select testa (explicit). Do not add a URL param that silently overrides to test. If no such param exists, do not introduce one | Grep TransferForm for query-param token; none expected | | Keyboard a11y: listbox order vs aria so a screen reader announces CL8Y but Enter submits testa | DOM order = ranked order; `aria-selected` on the real selected option | Keyboard: Arrow through options; selected id matches focused option | | QA env fixture breakage: e2e assumed first token is TKNA and now defaults to LUNC, causing wrong-token transfers in CI | Update e2e to select by `data-tokenid` / name, not “first option” | Audit `e2e/*.spec.ts` for first-token assumptions; fix in the same MR | | Operator/admin confuses Settings token list order with Transfer order and mis-operates | Settings unsorted by this feature; docs say Transfer-only | Manual: Settings → Tokens order unchanged | --- ## Verification criteria Merge is verified when **all** of the following hold: 1. **Automated:** New rank / `buildTransferTokens` tests pass, including spoofed-symbol and mixed-set order. `make test-frontend` (or package equivalent) is green. 2. **E2E:** Token-selection spec asserts economic-before-test when both groups exist; transfer specs that depended on “first token” are updated and green. 3. **Manual:** On a build with both CL8Y/LUNC and testa/testb/tdec routed, open Transfer, open the token combobox: economic group is contiguous at the top, test group contiguous at the bottom; default is economic; selecting testa still bridges testa. 4. **Stability:** Reload and switch BSC ↔ opBNB ↔ MegaETH ↔ Terra ↔ Solana: ranking rule holds on every pair that has mixed tokens. 5. **Docs:** INV added; MR links this issue. 6. **No regression:** Mapping-load empty state (glab #89), INV-UX1 amount/CTA, recipient validation, and faucet claim UI still behave as before. --- ## Out of scope - Hiding or disabling test tokens on mainnet - On-chain `economic` flag or Terra registry schema changes - Reordering Settings → Tokens / Faucet (except extracting a shared constant) - Token search/filter UI, favorites, or balance-based sort - Changing logos, names, or faucet claim amounts - Operator / contract / rate-limit work ## References - Faucet catalog: `packages/frontend/src/components/settings/FaucetPanel.tsx` (`MAINNET_FAUCET_TOKENS`, `LOCAL_FAUCET_TOKENS`) - Noneconomic SPL checklist: `docs/solana-mainnet-test-tokens-checklist.md` - Tokenlist: `packages/frontend/public/tokens/tokenlist.json` - Default selection: `packages/frontend/src/components/transfer/TransferForm.tsx` (`transferTokens[0]`) - Production bridge: https://bridge.cl8y.com/
PlasticDigits commented 2026-08-31 05:18:28 +00:00 (Migrated from gitlab.com)

mentioned in commit 2a20cd3e4f

mentioned in commit 2a20cd3e4f40482a6692ac7bf7b3f36c034e5a30
PlasticDigits commented 2026-08-31 05:18:42 +00:00 (Migrated from gitlab.com)

mentioned in merge request !155

mentioned in merge request !155
PlasticDigits commented 2026-08-31 12:20:12 +00:00 (Migrated from gitlab.com)

mentioned in commit d86a844dfe

mentioned in commit d86a844dfe14075a538a7f5a41ce095feee77036
PlasticDigits commented 2026-08-31 12:21:17 +00:00 (Migrated from gitlab.com)

mentioned in commit 50b593d47c

mentioned in commit 50b593d47c4d47605671c47cc2fadccc392055d1
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-31 12:21:18 +00:00
PlasticDigits commented 2026-08-31 12:39:27 +00:00 (Migrated from gitlab.com)

Merge status (!155 landed on main; this issue auto-closed)

Code AC is met (economic-then-test ranking, closed-id denylist, default first economic, keep explicit test selection). Non-blocking leftovers from review — not reopen-worthy:

  • e2e/token-selection.spec.ts keep-selection locator uses descendant has: [data-tokenid] while the attribute is on the option itself; prefer [role=option][data-tokenid=…].
  • Local e2e ranking can pass vacuously if Playwright Node does not populate import.meta.env the way Vite does.
  • Manual mixed-route matrix (BSC/opBNB/MegaETH/Terra/Solana) still unchecked; ranking is deterministic.

No separate follow-up issue opened for these nits.

## Merge status (!155 landed on `main`; this issue auto-closed) Code AC is met (economic-then-test ranking, closed-id denylist, default first economic, keep explicit test selection). Non-blocking leftovers from review — not reopen-worthy: - `e2e/token-selection.spec.ts` keep-selection locator uses descendant `has: [data-tokenid]` while the attribute is on the option itself; prefer `[role=option][data-tokenid=…]`. - Local e2e ranking can pass vacuously if Playwright Node does not populate `import.meta.env` the way Vite does. - Manual mixed-route matrix (BSC/opBNB/MegaETH/Terra/Solana) still unchecked; ranking is deterministic. No separate follow-up issue opened for these nits.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#136
No description provided.