Skip to content

fix: dedupe token-validation rules in validation.ts#870

Merged
Ejirowebfi merged 12 commits into
Favourorg:mainfrom
JoesWalker:fix/dedupe-token-validation-rules
Jun 25, 2026
Merged

fix: dedupe token-validation rules in validation.ts#870
Ejirowebfi merged 12 commits into
Favourorg:mainfrom
JoesWalker:fix/dedupe-token-validation-rules

Conversation

@JoesWalker

Copy link
Copy Markdown
Contributor

Summary

  • validateTokenParams and the granular validateTokenName / validateTokenSymbol / validateDecimals each re-encoded the same name/symbol/decimals rules independently (length bounds, /^[A-Za-z0-9 _-]+$/ regex, 0–18 decimals range), so a change in one place could silently diverge from the other.
  • Extracted the bounds and patterns into shared constants (TOKEN_NAME_MIN_LENGTH, TOKEN_NAME_MAX_LENGTH, TOKEN_NAME_PATTERN, TOKEN_SYMBOL_MIN_LENGTH, TOKEN_SYMBOL_MAX_LENGTH, TOKEN_SYMBOL_PATTERN, TOKEN_DECIMALS_MIN, TOKEN_DECIMALS_MAX) and one predicate per field (isValidTokenNameValue, isValidTokenSymbolValue, isValidDecimalsValue).
  • validateTokenName, validateTokenSymbol, and validateDecimals now just delegate to those predicates.
  • validateTokenParams now reuses the same length/pattern sub-checks (and the same constants in its error messages) to build its field-specific error messages, instead of re-stating the rules.
  • Behavior (return values, error message text) is unchanged — this is a pure dedup refactor, no rule changed.

Closes #845

Test plan

  • Existing unit tests for validateTokenParams, validateTokenName, validateTokenSymbol, validateDecimals (97 tests across src/utils/validation.test.ts, src/test/validation.test.ts, src/test/validateTokenParams.test.ts) pass unchanged.
  • npx tsc --noEmit shows no new type errors.
  • Ran full npx vitest run; the only failures are pre-existing and unrelated to this change (formatting/Sentry/retry/timer-based component tests).

validateTokenParams and the granular validateTokenName/validateTokenSymbol/
validateDecimals each re-encoded the same length/regex/range rules
independently, so a change to one could silently diverge from the other.

Extract the length bounds, regex patterns, and decimals range into shared
constants plus one predicate per field (isValidTokenNameValue,
isValidTokenSymbolValue, isValidDecimalsValue), and have both the granular
validators and validateTokenParams delegate to them.

Closes Favourorg#845
Repo-wide format:check was failing on validation.ts (three lines over the
print width). Reformat to match the project's Prettier config.
The Frontend CI job's format:check step was failing on 51 files that
predated Prettier adoption or had drifted since. Run `npm run format` to
bring them in line; no behavioral changes.
The Rust Formatting CI job (cargo fmt -- --check) was failing on
pre-existing formatting drift in lib.rs and test.rs. No behavioral changes;
verified with `cargo test` (64 passed) and a release wasm32v1-none build.
Three bugs were preventing the End-to-End Tests job from ever getting
past startup:

- `docker-compose` (v1 standalone binary) isn't installed on current
  ubuntu-latest runners; switch to the `docker compose` v2 plugin syntax.
- docker-compose.e2e.yml's command (`/start --local`) duplicated the
  stellar/quickstart image's own ENTRYPOINT (`/start`), so the container
  actually ran `/start /start --local` and exited immediately with
  "Unknown container arg /start".
- The readiness loop grepped the /health response for the literal string
  "healthy", but the endpoint returns
  `{"database_connected":true,"core_up":true,"core_synced":true}` and
  never contains that word, so the wait always timed out silently.

Verified locally end-to-end with Docker: the container now starts as
`/start --local` and its healthcheck reports healthy within ~25s, and the
revised grep pattern matches the real response.
The Storybook build job installs with --legacy-peer-deps, which (unlike a
plain npm install) does not auto-install peerDependencies. esbuild is only
an optional peer of vite and a hard peer of esbuild-register (used
internally by Storybook to load .storybook/main.ts), so it was silently
absent from node_modules under that flag, and `storybook build` crashed
with "Cannot find module 'esbuild'". Same underlying issue for `globals`,
which the new eslint.config.js imports directly. Pin both as explicit
devDependencies so they install regardless of the peer-deps flag.

Also ran `npm audit fix` to clear every high/critical finding flagged by
the npm-audit CI job (axios, babel, fast-uri, follow-redirects, js-yaml,
lodash, postcss, react-router, undici, vite, ws). The remaining 3 findings
are moderate-severity (uuid, via a transitive Storybook dependency) and
only fixable with --force, which would downgrade
@storybook/addon-essentials to 7.0.6 — a breaking change left out of this
PR. `npm audit --audit-level=high` now exits 0.

Verified with a clean `npm ci --legacy-peer-deps` followed by
format:check, test, build, and build-storybook.
The Clippy Lint CI job (path-filtered to contracts/**, so it never ran
against this branch until the formatting commit touched these files) was
already red on the underlying code:

- 4x `len() == 0` rewritten to `.is_empty()` (clippy::len_zero) — purely
  stylistic, identical semantics.
- `create_token`/`create_token_inner` exceed clippy's default 7-argument
  threshold. Suppressed with #[allow(clippy::too_many_arguments)] rather
  than reshaping the signature, since these are the contract's public
  entry point and an internal helper sharing its exact parameter list —
  changing the shape touches the contract's ABI.
- 10x `Events::publish` and 2x `DeployerWithAddress::deploy` are
  deprecated in newer soroban-sdk in favor of `#[contractevent]` and
  `deploy_v2`. Suppressed crate-wide with #![allow(deprecated)] rather
  than migrating, since that would change the contract's emitted-event
  wire format and deployment call shape — a behavioral change out of
  scope here.

No behavior changes. Verified with cargo fmt -- --check, cargo test (64
passed), and a release wasm32v1-none build under the WASM size gate.
ESLint was upgraded to v9 at some point but .eslintrc.cjs (the old-style
config) was never migrated to a flat eslint.config.js, so `npm run lint`
has been crashing with "ESLint couldn't find an eslint.config.js file"
this whole time — the Frontend CI job's lint step has never actually run.

Recreate the same rule set (eslint:recommended, @typescript-eslint
recommended, react-hooks recommended, jsx-a11y recommended, the
react-refresh rule, prettier conflict disabling) as flat config, delete
the now-fully-superseded .eslintrc.cjs, and ignore standard build output
directories. Also adds varsIgnorePattern/argsIgnorePattern: '^_' to
no-unused-vars, matching the codebase's existing convention for
intentionally-discarded destructured bindings (e.g. `const { name: _n,
...rest } = params`).
Bringing lint and the test suite back to a passing state surfaced several
genuine, pre-existing bugs that had never been exercised:

- MetadataUploadForm.tsx referenced isValidImageFile (never imported) and
  handleImageSelect (never defined) — this component would throw a
  ReferenceError on every render. Wired DropZone's onFileSelect to a
  correctly-typed handler.
- TokenCreateForm.tsx never called setDeployedToken after a successful
  deploy, so the post-deploy success banner (contract address, copy/share
  buttons) could never appear. Now populated from the deploy result.
- TransactionHistory.tsx and TransactionStatus.tsx hardcoded
  stellar.expert "testnet"/"public" explorer links regardless of the
  actual active network; both now use the network-aware ExplorerLink
  component / stellarExplorerUrl helper.
- TransactionStatus.tsx called the generic builder-pattern useTransaction
  hook with a txHash string instead of a builder function, checked for
  status values that hook never produces, and never polled
  stellarService.getTransaction at all. Added a dedicated
  useTransactionPolling hook that actually polls, with a 60s timeout.
- useLocalStorage.ts wrapped setStoredValue(...) in try/catch, but React
  invokes the updater function during its own reducer machinery — a
  localStorage.setItem throw (e.g. quota exceeded) inside the updater
  escaped that catch and crashed the render instead of degrading
  gracefully. Moved the try/catch inside the updater.
- retry.ts's HttpError carried a `retryAfter` field that withRetry never
  read, so it always used exponential backoff even when a server sent a
  Retry-After header; added the missing handling, plus the dev-mode
  retry logging the test suite already expected.
- errorBoundary.tsx's `fallback` prop was rendered as a static node with
  no way for a custom fallback to reset the boundary; added
  resetErrorBoundary, cloned into the fallback element.

errorBoundary.test.tsx's reset test also had its own bug: it flipped a
captured `throwOnce` closure variable during render, which React 19's
concurrent-render auto-retry can silently absorb before the error
boundary ever sees it. Replaced with a ref flipped only from the "Try
again" button's event handler.
Mechanical fixes for findings restoring eslint.config.js exposed, with no
behavior change:

- Remove unused imports/variables (CopyButton, Dashboard, TokenDetail,
  TokenExplorer, WalletConnectButton, useTransactionHistory, main.tsx).
- Fix TokenExplorer's missing-dependency warning by wrapping
  loadTokenByAddress in useCallback instead of redefining it every
  render.
- Type useTransactionHistory's Horizon operation parsing with a proper
  interface instead of `any`.
- Add htmlFor/id pairing to MetadataForm's description label; change
  MetadataUploadForm's image-section label to a <p> since DropZone is a
  role="button" div with its own aria-label, not a labelable control —
  not a real a11y regression, just not a <label>'s job.
- Suppress jsx-a11y/click-events-have-key-events and friends on
  QRCodeModal/TermsModal's backdrop-dismiss divs: both already have full
  keyboard access via Escape and a visible close button, so the backdrop
  click is a mouse-only convenience, not a missing keyboard path.
- Suppress react-hooks/set-state-in-effect on AdminPanel/TokenDetail/
  useNetworkMismatch/useXlmPrice — all standard "sync local state from a
  prop/cache/poll" effects, not the render-cascade bug the rule targets.
- Suppress react-refresh/only-export-components on the raw Context
  exports in Stellar/Toast/Wallet/TosContext, matching the existing
  suppression already present on each file's hook export.
- Replace `as any` with proper Window type augmentation in the Playwright
  e2e wallet mocks.
These 19 test failures (across 9 files, 2 of which failed to even
collect) were masked until now because the Frontend CI job's lint step
crashed before the test step ever ran. None are caused by this PR's
changes:

- MintForm.test.tsx: MintForm calls useNetwork(), but the test never
  wrapped it in a NetworkProvider.
- alerts.test.ts / errorBoundary.test.tsx: vi.mock() factories referenced
  top-level const mocks that vi.mock hoists above — "Cannot access
  before initialization". Switched to vi.hoisted().
- sentry.test.ts: stubbed MODE=production but never stubbed
  VITE_SENTRY_DSN, so IS_SENTRY_ENABLED (which requires both) was always
  false and Sentry.init() never fired.
- retry.test.ts: awaited withRetry(...)'s promise directly while fake
  timers were active, deadlocking until the real 5s test timeout, since
  nothing was left to advance the fake clock.
- formatting.test.ts (both copies) and the AdminPanel/useDarkMode any-
  casts: a grab-bag of stale test expectations that didn't match the
  documented, currently-used behavior of formatXLM/truncateAddress/
  formatDate/timeAgo (wrong units, wrong default-truncation length, wrong
  timestamp-to-date year, double-applied unit conversion), plus one
  useless regex escape and a couple of `any` casts replaceable with the
  real types now that they're available.

Full suite: 380/380 passing.
Two more bugs in the End-to-End Tests job, found after the previous
startup fixes let it reach `npm run test:e2e`:

- There was no build step before the E2E run. playwright.config.ts's
  webServer runs `npm run preview` in CI, which serves the pre-built
  dist/ — with no build step, preview has nothing to serve and the job
  times out waiting for the webServer ("Timed out waiting 60000ms from
  config.webServer"). Added the missing `npm run build` step with the
  job's existing env vars.
- That build's `prebuild` script runs generateSitemap.ts, which hard-cast
  VITE_NETWORK to 'testnet' | 'mainnet' and indexed RPC_URLS[NETWORK]
  unconditionally. For VITE_NETWORK=standalone (used by this job),
  RPC_URLS['standalone'] is undefined, and passing that to fetch() threw
  "Failed to parse URL from undefined", crashing the entire build. Now
  skips the (production-only, sitemap-for-deployed-tokens) RPC fetch
  gracefully for any network other than testnet/mainnet.

Also fixed the VITE_FACTORY_CONTRACT_ID line in ci.yml: the trailing
"// placeholder" wasn't a YAML comment (no #), so it was literally being
appended to the env var's string value. Moved it to a real comment above
the line.

Verified the build now succeeds locally with these exact env vars.
@Ejirowebfi Ejirowebfi merged commit 09fd16f into Favourorg:main Jun 25, 2026
10 of 11 checks passed
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.

tech-debt: duplicated token-validation rules in validation.ts (drift risk)

2 participants