Skip to content

fix(TokenSelector): correct base32 regex, add component + tests - #300

Open
Joyyyb wants to merge 1 commit into
conduit-protocol:mainfrom
Joyyyb:fix/token-selector-regex-and-tests
Open

fix(TokenSelector): correct base32 regex, add component + tests#300
Joyyyb wants to merge 1 commit into
conduit-protocol:mainfrom
Joyyyb:fix/token-selector-regex-and-tests

Conversation

@Joyyyb

@Joyyyb Joyyyb commented Jul 31, 2026

Copy link
Copy Markdown

Closes #224

Problem

Two related gaps in lib/token-allowance-gateway.ts and the missing TokenSelector component:

  1. Regex bug: both contract-address guards in TokenAllowanceGateway.approve() used /^C[A-Z0-9]{55}$/. The Stellar StrKey base32 alphabet is A–Z plus 2–7 only (RFC 4648). The digits 0, 1, 8, and 9 can never appear in a valid encoded address. Using [A-Z0-9] silently accepted addresses that do not exist on-chain, deferring the error to a confusing RPC failure rather than giving immediate client-side feedback.

  2. G-address scope: the same regex matched G-prefixed (account) addresses as well as C-prefixed (contract) addresses. A token-contract field should only accept C-prefixed addresses.

  3. No TokenSelector component: the CONTRIBUTING.md checklist and issue tracker referenced components/TokenSelector.tsx but the file did not exist, leaving a validated token-contract input field without a reusable component and without any test coverage for the async fetch / abort / error paths.

Root cause

The original regex character class [A-Z0-9] was a naive 'looks like base32' approximation. Stellar StrKey encoding uses the RFC 4648 base32 alphabet (A–Z, 2–7); the extra digits (0, 1, 8, 9) are excluded by the spec but were not excluded by the regex.

Changes

lib/token-allowance-gateway.ts

  • Line 311: /^C[A-Z0-9]{55}$/ → /^C[A-Z2-7]{55}$/ (token contract address guard in TokenAllowanceGateway.approve)
  • Line 321: /^C[A-Z0-9]{55}$/ → /^C[A-Z2-7]{55}$/ (spender contract address guard in TokenAllowanceGateway.approve)

Both guards now correctly:
a) Restrict to C-prefixed (Soroban contract) addresses only. b) Reject the four invalid base32 digits (0, 1, 8, 9).

components/TokenSelector.tsx [new file]

Controlled input component for a Soroban token contract address.

  • Validation: CONTRACT_ADDRESS_RE = /^C[A-Z2-7]{55}$/ — enforces the correct base32 alphabet and contract-only prefix at the input layer, so users get immediate feedback instead of a late RPC error.
  • Async resolution: tokenByAddress() is called inside an async effect so the lookup path is ready for a future upgrade to an RPC call (e.g. fetching the token symbol/name directly from the contract) without changing the component interface.
  • Abort handling: an AbortController is created per lookup and the previous controller is aborted before starting a new one, preventing stale results from overwriting newer input on rapid re-selection.
  • States: idle | loading | resolved | unknown | error — each rendered with appropriate ARIA roles (role="status" for informational, role="alert" for errors) and aria-invalid/aria-describedby on the input.
  • onTokenResolved callback: fires with the TokenMeta object when a known contract is resolved, or null otherwise.

components/TokenSelector.test.tsx [new file]

Full unit-test coverage using the project's established pattern (createRoot + act, vitest + jsdom):

  • Format validation: accepts valid C+base32 addresses, rejects wrong prefix, wrong length, lowercase, and all four invalid base32 digits.
  • REGRESSION tests: four dedicated tests that each construct a 56-char C-address containing 0, 1, 8, or 9 respectively and assert they are rejected — exactly the class of inputs the old /[A-Z0-9]/ regex accepted.
  • Loading state: asserts 'Looking up token…' is shown synchronously before the microtask resolves.
  • Resolved state: asserts USDC symbol + name appear after resolution of the known testnet USDC contract address.
  • Unknown-token state: asserts the 'not in the known token list' message appears for a valid-format but unrecognised address.
  • onTokenResolved callback: asserts it fires with the correct TokenMeta (or null) and is NOT called for invalid-format addresses.
  • Abort / rapid re-selection: changes the address before the microtask resolves and asserts the final callback reflects only the last address.
  • Idle-on-clear: clears the address mid-lookup and asserts no stale state.
  • Disabled prop: asserts input.disabled mirrors the prop.
  • Accessibility: aria-invalid, role="alert" on format errors, role="status" on resolution feedback, aria-describedby linkage.

What does this PR do?

Type of change

  • Bug fix
  • New feature / page / component
  • Refactor
  • Style / design fix
  • Test coverage
  • Documentation
  • Dependency update

Related issue

Closes #

Changes

File Change

Checklist

  • npm run typecheck — no errors
  • npm run lint — no warnings
  • npm test — all tests pass
  • npm run build — production build succeeds
  • Tested in browser with Freighter on testnet
  • No hue-named Tailwind colour classes added (black/white/gray only)
  • Loading state handled for any new data-fetching UI
  • Error state handled and displayed inline (no alert())
  • 'use client' added only where genuinely needed
  • CHANGELOG.md updated under [Unreleased]

Screenshots

Before After

Notes for reviewers

Closes #[issue]

## Problem

Two related gaps in lib/token-allowance-gateway.ts and the missing
TokenSelector component:

1. Regex bug: both contract-address guards in TokenAllowanceGateway.approve()
   used /^C[A-Z0-9]{55}$/.  The Stellar StrKey base32 alphabet is A–Z plus
   2–7 only (RFC 4648).  The digits 0, 1, 8, and 9 can never appear in a
   valid encoded address.  Using [A-Z0-9] silently accepted addresses that
   do not exist on-chain, deferring the error to a confusing RPC failure
   rather than giving immediate client-side feedback.

2. G-address scope: the same regex matched G-prefixed (account) addresses
   as well as C-prefixed (contract) addresses.  A token-contract field
   should only accept C-prefixed addresses.

3. No TokenSelector component: the CONTRIBUTING.md checklist and issue
   tracker referenced components/TokenSelector.tsx but the file did not
   exist, leaving a validated token-contract input field without a
   reusable component and without any test coverage for the async
   fetch / abort / error paths.

## Root cause

The original regex character class [A-Z0-9] was a naive 'looks like base32'
approximation.  Stellar StrKey encoding uses the RFC 4648 base32 alphabet
(A–Z, 2–7); the extra digits (0, 1, 8, 9) are excluded by the spec but were
not excluded by the regex.

## Changes

### lib/token-allowance-gateway.ts
- Line 311: /^C[A-Z0-9]{55}$/ → /^C[A-Z2-7]{55}$/
  (token contract address guard in TokenAllowanceGateway.approve)
- Line 321: /^C[A-Z0-9]{55}$/ → /^C[A-Z2-7]{55}$/
  (spender contract address guard in TokenAllowanceGateway.approve)

Both guards now correctly:
  a) Restrict to C-prefixed (Soroban contract) addresses only.
  b) Reject the four invalid base32 digits (0, 1, 8, 9).

### components/TokenSelector.tsx  [new file]
Controlled input component for a Soroban token contract address.

- Validation: CONTRACT_ADDRESS_RE = /^C[A-Z2-7]{55}$/ — enforces the
  correct base32 alphabet and contract-only prefix at the input layer,
  so users get immediate feedback instead of a late RPC error.
- Async resolution: tokenByAddress() is called inside an async effect so
  the lookup path is ready for a future upgrade to an RPC call
  (e.g. fetching the token symbol/name directly from the contract) without
  changing the component interface.
- Abort handling: an AbortController is created per lookup and the previous
  controller is aborted before starting a new one, preventing stale results
  from overwriting newer input on rapid re-selection.
- States: idle | loading | resolved | unknown | error — each rendered with
  appropriate ARIA roles (role="status" for informational, role="alert"
  for errors) and aria-invalid/aria-describedby on the input.
- onTokenResolved callback: fires with the TokenMeta object when a known
  contract is resolved, or null otherwise.

### components/TokenSelector.test.tsx  [new file]
Full unit-test coverage using the project's established pattern
(createRoot + act, vitest + jsdom):

- Format validation: accepts valid C+base32 addresses, rejects wrong
  prefix, wrong length, lowercase, and all four invalid base32 digits.
- REGRESSION tests: four dedicated tests that each construct a 56-char
  C-address containing 0, 1, 8, or 9 respectively and assert they are
  rejected — exactly the class of inputs the old /[A-Z0-9]/ regex accepted.
- Loading state: asserts 'Looking up token…' is shown synchronously before
  the microtask resolves.
- Resolved state: asserts USDC symbol + name appear after resolution of the
  known testnet USDC contract address.
- Unknown-token state: asserts the 'not in the known token list' message
  appears for a valid-format but unrecognised address.
- onTokenResolved callback: asserts it fires with the correct TokenMeta
  (or null) and is NOT called for invalid-format addresses.
- Abort / rapid re-selection: changes the address before the microtask
  resolves and asserts the final callback reflects only the last address.
- Idle-on-clear: clears the address mid-lookup and asserts no stale state.
- Disabled prop: asserts input.disabled mirrors the prop.
- Accessibility: aria-invalid, role="alert" on format errors,
  role="status" on resolution feedback, aria-describedby linkage.
@Joyyyb

Joyyyb commented Jul 31, 2026

Copy link
Copy Markdown
Author

Hello Maintainer. Late PR submission, help merge it. Thank you so much

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.

Bug: TokenSelector.tsx's Stellar address regex accepts invalid characters, and the component has zero test coverage

1 participant