Skip to content

feat(ui): add custom token import with unverified asset disclaimer (#789) - #849

Merged
Jaydbrown merged 1 commit into
conduit-protocol:mainfrom
oyinda-11:feat/789-custom-token-import
Oct 22, 2026
Merged

Jaydbrown merged 1 commit into
conduit-protocol:mainfrom
oyinda-11:feat/789-custom-token-import

Conversation

@oyinda-11

Copy link
Copy Markdown
Contributor

Summary

Users can now paste an arbitrary Soroban contract address into the TokenSelector search and import a newly deployed token, so protocols can get streaming support on day one. Before anything is added, they see an Unverified Asset disclaimer that they must explicitly acknowledge. Imported tokens are saved locally and labeled Unverified in the list.

Closes #789

Changes

  • lib/tokens.ts
    • TokenMeta gets an optional verified?: boolean. Curated entries leave it unset and imported tokens are false. TokenSelector already read t.verified, which was a typecheck error on main (TS2339), so that error is fixed too.
    • New fetchSep41Metadata(contractId, source, { signal, timeoutMs }) reads SEP-41 name, symbol and decimals through the existing simulateReadOnly RPC client, so it gets the same timeouts, abort handling and circuit breaker as other calls. It validates the address with StrKey, needs a valid funded source account, rejects empty or oversized strings and decimals outside 0–38, and wraps simulation failures with a "may not be a SEP-41 token" hint.
  • lib/imported-tokens-storage.ts (new): imported tokens are stored per network (mainnet, testnet, local) under conduit:imported-tokens. It uses the same safe localStorage / in-memory fallback pattern as onboarding-storage.ts and stream-notes-storage.ts. Entries are always forced to verified: false, re-importing an address replaces the old entry, and malformed or tampered entries are dropped when loading.
  • components/TokenSelector.tsx
    • When the search query is a valid contract address (checked with StrKey, including the checksum) that isn't in the curated or imported list, the selector offers Import token.
    • Metadata lookup has a loading state (Reading token metadata…, announced via role="status"). A failure shows an inline role="alert" error and the user can retry. Changing the query or unmounting cancels the in-flight lookup.
    • The Unverified Asset modal reuses components/ui/Modal, which provides the focus trap, Escape to close, closing on overlay click and returning focus afterwards. It shows the name, symbol, decimals and full contract address. Import token stays disabled until the labeled checkbox "I understand this token is unverified…" is ticked; the checkbox is linked to the warning text with aria-describedby.
    • Confirming saves the token, selects it through onChange and clears the search. Imported tokens show up in search results with an Unverified badge, and the resolved state reads "(unverified, imported by you)".
    • New optional prop sourceAddress (normally the connected wallet's public key) is used as the simulation source. Without it, the import button is disabled and the reason is shown: "Connect a wallet to look up token metadata."

How it was tested

New and updated Vitest suites:

  • components/TokenSelector.import.test.tsx (13 tests) covers:
    • When import is offered and when it isn't (verified address, bad checksum, free text)
    • The metadata call uses the wallet as the source
    • The modal content and aria-modal
    • The acknowledgement gate
    • Saving, selecting and the Unverified badge
    • Loading saved tokens on mount
    • Cancel, Escape and focus moving into the modal
    • The error-and-retry path, the loading state and the no-wallet state
  • lib/imported-tokens-storage.test.ts (8 tests) covers saving, per-network scoping, replacement, removal, rejecting invalid addresses, and tampered or unparseable storage.
  • lib/tokens.test.ts has 6 new fetchSep41Metadata tests: success, invalid address, missing source, simulation failure, empty symbol and out-of-range decimals.

Local results:

  • Touched suites: npx vitest run components/TokenSelector lib/tokens.test.ts lib/imported-tokens-storage.test.ts components/ui/Modal.test.tsx gives 5 files, 81 tests, all passing.
  • next lint --file … on every touched file reports no ESLint warnings or errors.
  • tsc --noEmit reports 0 errors in touched files (the one on main in TokenSelector.tsx is gone). For this check I excluded app/dashboard/page.tsx, because its parse error stops tsc before it reports any type errors.
  • Full npx vitest run: 26 failing files / 85 failing tests on both main and this branch, the same counts, and none of the failures are in files this PR touches. The 2 new test files pass.

Pre-existing failures on main (not caused by this PR)

CI fails on main before any of this code runs. For example, run 39464762854 on cf5b2b0 fails every job at the Run npm ci step. The four runs on main before it fail the same way.

  1. npm ci fails because package-lock.json is out of sync with package.json (Missing: playwright-core@1.64.1 from lock file, Missing: playwright@1.62.1, sentry@0.44.1, …). All CI jobs stop there, so they will be red on this PR too.
  2. app/dashboard/page.tsx has a JSX syntax error around lines 438–445 (TS17002 Expected corresponding JSX closing tag for 'div'). It breaks npm run typecheck, npm run build and the 4 app/dashboard/__tests__/* suites.
  3. npm run lint already has errors (import/no-duplicates in components/stream/__tests__/StreamFlowPipe.test.tsx and components/ui/__tests__/ui-upgrades.test.tsx).
  4. Unit tests already fail on main: 26 files / 85 tests (for example app/create/__tests__/page.test.tsx, app/streams/__tests__/page.test.tsx, components/ThemeToggle.test.tsx, lib/stream-notes-storage.test.ts).

I left these out of this PR to keep it focused and avoid conflicts with other open PRs. Each one would be a small separate fix, starting with regenerating the lockfile.

Notes

  • Nothing passes sourceAddress to TokenSelector yet; the component isn't mounted on any page today. Callers can pass useWallet().publicKey when they wire it in.
  • Imported tokens live only in the user's browser and are never added to the curated list.

🤖 Generated with Claude Code

…onduit-protocol#789)

Let users paste a Soroban contract address into the TokenSelector search
and import tokens that are not in the curated list, so newly deployed
tokens can be streamed on day one.

- lib/tokens: add `verified` flag to TokenMeta and `fetchSep41Metadata`,
  which reads name/symbol/decimals via `simulateReadOnly`, validates the
  address with StrKey and rejects malformed metadata
- lib/imported-tokens-storage: per-network local persistence for imported
  tokens, always flagged unverified, tolerant of tampered storage
- TokenSelector: "Import token" action for unknown C-addresses, loading
  and error/retry states, an "Unverified Asset" modal that requires an
  explicit acknowledgement checkbox, and an "Unverified" badge in the list
- new optional `sourceAddress` prop supplies the simulation source

Closes conduit-protocol#789

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jaydbrown
Jaydbrown merged commit e458388 into conduit-protocol:main Oct 22, 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.

components/TokenSelector.tsx: add custom token import dialog with warning modal for unverified assets

2 participants