This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
React app for the Ar.io Name System (ArNS) Registry — search for, purchase, and manage ArNS names. Vite + React 18 + TypeScript + TailwindCSS.
The backend is Solana-only. The codebase went through a "de-AO refactor": ArNS records and ANTs live in Solana programs (ANTs are Metaplex Core NFTs), not in AO processes. Comments referencing "the de-AO refactor" mark code touched by that migration. Arweave is still used for data storage/retrieval (Turbo uploads, logo images, GraphQL), but not for contract state.
Node 24 (.nvmrc), Yarn 1.
yarn # Install
yarn dev # Dev server (NODE_ENV=prod, VITE_GITHUB_HASH=local)
yarn build # Production build (32GB max-old-space-size)
yarn build:production # VITE_ENVIRONMENT=production
yarn build:develop # VITE_ENVIRONMENT=develop
yarn preview # Preview built output
yarn test # Jest unit tests
yarn test:coverage # With coverage (80% threshold: branches/functions/lines)
yarn test:updateSnapshot # Update snapshots
yarn test:playwright # Playwright e2e
yarn lint:check / lint:fix # Biome (lint:fix uses --unsafe)
yarn format:check / format:fix # Biome formatter
yarn storybook # Storybook on :6006
yarn publish:arweave # Build + `ario-deploy deploy --arns-name $VITE_ARNS_NAME`Run a single test file:
npx cross-env NODE_ENV=test jest src/utils/searchUtils/searchUtils.test.tsNODE_ENV=test is required — Jest uses tsconfig.test.json and a custom
import.meta AST transformer (tests/common/import-meta-transformer.js) to make
Vite's import.meta.env work under CommonJS. Jest collects *.test.ts(x) from
anywhere except tests/common/ and tests/playwright/ (Playwright's testDir).
Playwright's globalSetup (tests/playwright/setup.ts) spawns yarn preview
itself, so yarn build first. Specs hit process.env.URL, defaulting to
http://localhost:4173/. After sign-in, navigate with navigate() from
tests/playwright/helpers.ts, never page.goto — a hard reload wipes the
in-memory devtools keypair, which is deliberately not persisted. Call
skipConsoleNotice(page) before the first navigation, or the Console
migration popup covers the page.
Biome is the source of truth for lint/format. .eslintrc and .prettierrc are
vestigial — don't wire new tooling to them.
src/utils/solana.ts owns the active Solana config (network, RPC URL, program
IDs, ARIO mint). Key points:
- Config is runtime-switchable via
setSolanaConfig()— the Settings → Network page exposesdevnet/mainnet-betapresets plus per-program-ID overrides. Switching invalidates the memoized RPC clients, so always read throughgetActiveSolanaConfig()/getSolanaRpc()/getSolanaRpcSubscriptions()rather than caching module-level constants. - The exported
SOLANA_NETWORK/SOLANA_RPC_URL/SOLANA_PROGRAM_IDSconstants are the initial env-derived values only. Use them for bootstrap, not for live reads.
src/utils/sdk-init.ts is the chokepoint for building @ar.io/sdk clients:
buildArio, buildArioRead, buildAnt, buildAntRead, plus the
isSolanaWallet() type guard. Read paths work without a wallet (read-only
client against the configured RPC), so unauthenticated browsing keeps working.
Add new SDK instantiation here rather than calling ARIO.init / ANT.init
inline.
isSolanaWallet() checks both tokenType === 'solana' and a present
solanaSigner — Phantom attaches signTransaction a tick after connected
flips, so write helpers must fall back to read-only during that window.
The on-chain ANT ACL (paginated AclConfig / AclPage PDAs) is an
eventually-consistent index of which ANTs a wallet owns. Raw Metaplex Core
transfers move the asset immediately but do not update the ACL, so it lags in
both directions — a received name is invisible until synced, a sent name lingers.
computeAclDrift() establishes ground truth via a getProgramAccounts owner
scan against MPL_CORE_PROGRAM_ID, filtered by the ANT Program Metaplex
attribute. If names appear missing or stale in Manage, this is the first place to
look.
Fresh purchases drift too. With no processId, the SDK's buyRecord spawns the
ANT and buys the name in one transaction, then records the owner's ACL entries
in a separate, best-effort transaction whose failure is only logged. A rejected
or failed follow-up leaves a just-bought name flagged needsOwnerSync, which
surfaces as the "Sync Ownership" button in Manage.
The ACL is not an authorization source: the ANT program checks writes against the live Metaplex Core owner and the controller list.
WALLET_TYPES has a single member: SOLANA. Wallet discovery goes through
@solana/wallet-adapter-react using the Wallet Standard registry —
main.tsx passes wallets={[]} on purpose; Phantom, Solflare, Backpack, Glow
etc. self-register. Do not add legacy per-wallet adapter packages.
autoConnect must stay true: the wallet-adapter UI picker only calls
select(name) on click, never adapter.connect(). With autoConnect=false
clicking a wallet silently does nothing.
SolanaWalletConnector (src/services/wallets/) is a thin shim implementing the
app's ArNSWalletConnector interface over the adapter. Its important output is
solanaSigner — a @solana/kit TransactionSigner built by
walletAdapterToKitSigner.ts — which is what gets handed to the SDK.
PrivateKeySolanaWalletConnector backs the devtools-only private-key login at
/settings/devtools.
solanaSigner is a kit TransactionModifyingSigner, not a partial signer:
Phantom rewrites transactions on real origins (priority fee, Lighthouse guard
instructions), so the bridge returns the wallet's rewritten message together with
its signature. Transaction assembly, compute-budget and priority-fee pinning, and
multi-signer ordering all live in the SDK's sendAndConfirm
(@ar.io/sdk, solana/send), not in this repo.
ArNSWalletConnector still carries contractSigner / turboSigner fields from
the multi-chain era; the Solana connector leaves them undefined.
wagmi is still a dependency and src/utils/baseNetwork.ts,
BaseTokenPurchaseService, and Base-token branches in Checkout still reference
it — but WagmiProvider was removed from the app shell. Calling a wagmi hook
crashes with WagmiProviderNotFoundError. Affected files stub the hooks out with
a NOTE (de-AO refactor) comment; the resulting undefined values flow into
EVM-funded branches that are unreachable from the Solana-only UI. Do not
"restore" these imports without re-adding the provider.
Three shims run before anything else and must not be reordered or removed:
BigInt.prototype.toJSON—@ar.io/sdk's Solana backend callsJSON.stringifyon simulation errors containing BigInts. Without this the diagnostic stringify throws and the real on-chain failure is swallowed.Buffer.read/writeBigUInt64LE— the ESMbuffer@6.0.3path strips these; without themgetBalancethrows and checkout silently shows "0 ARIO".- Ed25519 WebCrypto polyfill — feature-detected, then
@solana/webcrypto-ed25519-polyfillis installed for Chrome <137 / Firefox. Required bygenerateKeyPairSignerduring ANT spawn. React only mounts insideed25519PolyfillReady.finally().
React Context + reducer per domain, all nested in main.tsx (order matters —
WalletState reads from GlobalState):
QueryClientProvider → SolanaWalletShell → GlobalState → WalletState →
ArNSState → TransactionState → RegistrationState → antd ConfigProvider →
ModalState → App
- GlobalState: gateways, Turbo network,
solanaConfig, ARIO contract instance. Persists tolocalStorageunderarns-app-settings(seeuseSyncSettings). - WalletState: bridges
useWallet()from wallet-adapter intoSolanaWalletConnector, rebuilds the ARIO contract when the signer orsolanaConfigchanges, persistswalletType.
Write flows go through src/state/actions/ — dispatchANTInteraction,
dispatchArIOInteraction, dispatchArIOContract. These require a connected
Solana wallet with a signer and throw otherwise. dispatchArNSUpdate is the read
side: it reloads the wallet's names and ANT states (merging in ACL drift) and
resets their query caches. Interaction names are the ANT_INTERACTION_TYPES /
ARNS_INTERACTION_TYPES enums in src/types.ts.
React Query for all server state. queryClient in src/utils/network.ts
(gcTime 1 day, staleTime 5 min, refetchOnWindowFocus: false — deliberately
tuned to stop refetch storms). An IndexedDB persister is implemented
(createIDBPersister) but not currently wired up in main.tsx.
Per-name queries override that default: useDomainInfo and the ANT state
queries (['ant', processId, …]) use staleTime: Infinity, so a write that
doesn't bust them leaves the UI showing old values. Post-write invalidation
happens when TransactionState.interactionResult changes (TransactionState
and DomainSettings both react to it) and in dispatchArNSUpdate. A new write
flow must set interactionResult the way the dispatch actions do, or invalidate
explicitly.
Domain logic lives in src/hooks/use<Feature>.tsx. Arweave data retrieval goes
through ArweaveCompositeDataProvider / SimpleArweaveDataProvider in
src/services/arweave/.
Hash router (createHashRouter) with lazy-loaded pages, defined in App.tsx.
Two top-level layouts: Layout for the app, and a separate SettingsLayout for
/settings/network and /settings/devtools. Breadcrumbs come from per-route
handle.crumbs functions; ANT_FLAG is a sentinel the Breadcrumbs component
resolves to the ANT's display name.
- Turbo SDK (
@ardrive/turbo-sdk/web) — credits, uploads. Logo uploads viauseUploadArNSLogowith progress tracking. - Stripe — fiat, initialized in
App.tsxkeyed onturboNetwork.STRIPE_PUBLISHABLE_KEYso a network switch remountsElements. - Checkout payment methods:
crypto(SOL/ARIO),credits(Turbo),card(Stripe). Base-token branches exist but are unreachable (see Dead EVM code).
Turbo's web build has different type signatures than Node. Import types from
@ardrive/turbo-sdk/web (re-exported via src/types/turbo.ts) and use
as unknown as TurboWebAuthenticatedClient when creating authenticated clients
for web upload — the web uploadFile takes File directly.
useUploadArNSLogo.tsx is the reference usage.
TailwindCSS (tailwind.config.mjs) + Ant Design with heavy token overrides in
main.tsx's ConfigProvider (antd is themed via CSS custom properties like
var(--primary), var(--card-bg)). Radix UI for headless primitives, Framer
Motion for animation. Per-component styles.css.
- Components: one folder each, containing
<ComponentName>.tsxandstyles.css. The README asks for tests in__tests__/named<component-name>.test.ts(x), but most existing tests sit beside the source file instead — either is picked up. - Utils:
src/utils/, with colocated or sibling tests. - Types for external libs that don't export what we need:
src/types/. - Images:
assets/images/{dark,light,common}/. - Translations:
assets/translations/<native-language-name>.json.
Import eventEmitter from src/utils/events.ts to raise notifications.
NotificationOnlyError (src/utils/errors.ts) for expected, user-facing
problems — shows a notification, no console noise. Subclasses include
ValidationError, InsufficientFundsError, WalletNotInstalledError,
ANTStateError, BaseTokenError, TopUpError, and several legacy wallet errors
(WanderError, MetamaskError, BeaconError, …) kept from the multi-chain era.
Use a plain Error for unexpected failures that should be logged.
Different domains for different jobs — using the wrong one is a real bug:
arweave.net— Arweave L1 GraphQL only (ARWEAVE_HOST,ARWEAVE_GRAPHQL_URL).turbo-gateway.com— Arweave data retrieval (DEFAULT_ARWEAVE,NETWORK_DEFAULTS.DATA.HOST,TURBO.GATEWAY_URL, static HTML assets).ar.io— ArNS name links (NETWORK_DEFAULTS.ARNS.HOST).
In src/utils/constants.ts, read at runtime — no other changes needed to toggle.
When true, flows that mint new ANTs are blocked: BUY_RECORD and
UPGRADE_NAME. Affects Register, Checkout, HomeSearch, ReturnedNamesTable, and
the "Permanently Buy" tab on ExtendLease. Disabled controls show
ARNS_PURCHASES_DISABLED_TOOLTIP.
Flows on existing ANTs are unaffected: EXTEND_LEASE, INCREASE_UNDERNAMES.
Every VITE_-prefixed variable set at build time is inlined into the public
bundle through import.meta.env, so never give a secret that prefix. Separately,
the define block in vite.config.ts shims process.env with URL only —
never widen it to the whole process.env, or CI secrets leak into the bundle.
- Solana:
VITE_SOLANA_NETWORK,VITE_SOLANA_RPC_URL,VITE_ARIO_CORE_PROGRAM_ID,VITE_ARIO_GAR_PROGRAM_ID,VITE_ARIO_ARNS_PROGRAM_ID,VITE_ARIO_ANT_PROGRAM_ID,VITE_ARIO_MINT_ADDRESS - Arweave:
VITE_ARWEAVE_HOST,VITE_ARWEAVE_GRAPHQL_URL,VITE_HYPERBEAM_URL - Build:
VITE_ENVIRONMENT(production/develop),VITE_NODE_ENV,VITE_GITHUB_HASH - Legacy AO, still read in
constants.tsand still passed by CI:VITE_ARIO_PROCESS_ID,VITE_ARIO_AO_CU_URL,VITE_ANT_AO_CU_URL VITE_ARNS_NAMEis a shell variable forpublish:arweave; app code doesn't read it.
VITE_SOLANA_RPC_URL is read with ||, not ??, on purpose — CI injects ""
when the secret is unset.
@src/* → ./src/*, @tests/* → ./tests/* (declared in both tsconfig.json
and vite.config.ts; Jest mirrors them in moduleNameMapper).
esbuild: falseandoptimizeDeps.esbuildOptions.target: 'esnext'— but build output must avoid top-level await (Safari 14 / es2020), which is why the Ed25519 polyfill uses an IIFE wrapper.vite-plugin-node-stdlib-browsersupplies Node polyfills for Arweave libs.server.allowedHostsincludes ngrok wildcards for tunnelled dev sessions.
transformIgnorePatterns explicitly un-ignores @ar.io, @permaweb,
arbundles, @dha-team/arbundles, arweave-wallet-connector, @wagmi, and
wagmi — these ship ESM that must be transformed. Add new ESM-only deps here
when they break tests. @ar.io/solana-contracts subpaths are remapped to their
built lib/*/index.js.
- pre-commit:
lint-staged→biome check --write --unsafe+biome format --write - commit-msg: commitlint, conventional commits (
feat,fix,docs,style,refactor,test,chore), no length limits
.github/workflows/: build_and_test.yml, pr-preview.yaml,
staging_deploy.yml, production.yml.
- PRs gate on
lint:checkandbuildonly. The Playwright job hasif: github.ref_name == 'main', so it's skipped on every PR, andyarn testis commented out inbuild_and_test.ymlandproduction.yml. Neither suite blocks a merge — run both locally. pr-preview.yamlpasses noVITE_SOLANA_*or program-ID variables, so preview builds fall back to the defaults insrc/utils/solana.ts.