Skip to content
Arc Testnet — names have no monetary value yet
ArcNS / Wallet integration

Wallet integration

The end-to-end recipe for resolving an ArcNS name in a wallet's recipient field, from "user types alice.arc" to "send enabled", with every failure mode, its user-facing message, and whether it blocks the send.

Purpose

This is the package for a wallet, payment app or agent that wants to accept @alice, alice.arc or alice.circle where it accepts a 0x… address today. It walks the recipient field end to end: what to recognise, what to resolve, what to show, and exactly when "Send" may be enabled.

Wallet-native support means a user pastes a name instead of an address and the wallet does the resolution itself, on chain, with nothing to trust but the RPC. What exists today: the @arcns/resolve SDK (built from source in the repository — not yet on the npm registry, see Integration approaches), the read-only API at api.arcns.io, and the contracts, already deployed on Arc testnet (see Contracts). What the wallet consumes is one function, resolve(), and the fields it returns.

Rule

A name is never just an address. Show the namespace, the registry and the verification status before enabling "Send". Refuse a mixed shape; never fall back from the controller to a payment address. Everything below is that rule, applied.

Required capabilities

CapabilityHow
Recognise inputlooksLikeName(input): a @ prefix or a known TLD suffix. Anything else is passed through as a raw address, so a pasted 0x… never shows a name error.
Normalise safelyNothing to do beyond what the SDK does: labels are ASCII a-z0-9-, 1 to 32 bytes, no edge or double hyphen, not all digits, lower-cased. No Unicode, no emoji, so no homograph can be typed.
Resolve name to addressarcns.resolve(input, { chain }), which reads the chain over eth_call against the single authoritative ArcNS registry for the namespace.
Verify address to primary (forward-confirmed)arcns.reverse(address) for the in-wallet "you are sending to alice.arc" display; the SDK returns a primary only when it forward-confirms.
Handle no-result safelyThe typed ResolveError.code switch below: not-found and no-record-for-chain are explicit; every other failure gets its own message, never a silent guess.

Required contracts, endpoints and ABI

Direct RPC through the SDK needs no ABI work on your side: @arcns/resolve carries the ABIs and reads HandleRegistry, the verbatim ENS registry, the ArcNSResolver (addr(node, coinType), verified(node, coinType), recordVersions), the UniversalResolver and the ReverseRegistrar. The Arc testnet addresses are already deployed and bundled in the package as ARCNS_DEPLOYMENTS[5042002] (the full list is on Contracts); pass an explicit deployment only to point at a different chain or environment. The REST route is GET https://api.arcns.io/resolve/:name with the same fields under slightly different names (owner for the controller, addr.verified for verification); it is free for first-party browser origins and X-API-Key holders and priced at $0.001/call over x402 for everyone else (see Integration approaches below); its full contract is on the API page.

The flow, end to end

What resolve() does with one input, in the order the SDK does it (sdk/src/parse.ts, sdk/src/resolve.ts): the shape decision first with no chain read, then the TLD gate, then the registry, then the record and its verified flag. Every RPC failure is rpc-error (or offchain-lookup), never not-found.

1. The user types alice.arc in the recipient field. looksLikeName("alice.arc") is true, so the wallet attempts resolution. While the label is still invalid (invalid-domain, reason: "HyphenAtEdge" for alice-), the field shows nothing: the user is still typing.

2. Resolve, with the chain the send is on.

import { ResolveError, namespaceLabel } from "@arcns/resolve";

export async function resolveRecipient(input: string, chain: "ARC" | "ETH" | "BTC" | "SOL" = "ARC") {
  try {
    const r = await arcns.resolve(input, { chain });
    // r = { name, namespace, registry, product, chainId, epoch, controller,
    //       coinType, address, verification, tld? }
    // SHOW ALL OF THESE before enabling "Send":
    //   name + namespaceLabel(r.namespace)      -> "alice.arc  ·  .arc"
    //   short(r.registry) + r.product            -> "registry 0xc3…3a01 (arcns)"
    //   r.verification                           -> "verified" | "unverified" (warn)
    //   r.tld?.status === "sunset"               -> banner: this TLD sunsets on r.tld.sunsetAt
    return r;
  } catch (e) {
    if (!(e instanceof ResolveError)) throw e;
    switch (e.code) {
      case "unrecognized":        return null;                                   // probably a raw address — pass it through
      case "invalid-handle":
      case "invalid-domain":      return null;                                   // still typing; e.reason names the rule
      case "ambiguous":           throw new Error("@name and name.arc are different names — type one of them");
      case "not-found":           throw new Error(`${input} is not registered`);
      case "no-record-for-chain": throw new Error(`${input} has no ${chain} address`);
      case "tld-retired":         throw new Error(`${input}: this TLD has been retired`);
      case "offchain-lookup":
      case "rpc-error":           throw new Error("Couldn't read the chain — try again");
      default:                    throw e;
    }
  }
}

3. Render the answer. The display rule for every resolution and for the send confirmation: the namespace badge (@, .arc or .circle), the full canonical name, and the registry short-address, with verified shown only for a proof-backed record. For a search or a picker, show all three namespaces (@handle, .arc, .circle) side by side and never auto-select between them. The address itself stays visible (for example on hover or in the confirmation), because the name is a label for it, not a replacement.

4. Verification. verification === "verified" means the owner proved control of that address for that coin type on chain. unverified means a record exists but nobody proved it: show a warning the user has to pass through. Details on Verified records.

5. Sunset banner. A TLD can be put into sunset. Names in a sunset TLD keep resolving until sunsetAt; after that every read is tld-retired (410 on the API), never a stale address and never not-found. When r.tld?.status === "sunset", show a banner ("this TLD sunsets on …") on the resolution and on the confirmation; the send itself may proceed.

6. Send enabled. Only when: a single labelled answer exists, its chainId and coinType match the send, the namespace, registry and verification are on screen, and the user has passed any warning. The controller is never a fallback: a name whose owner has not set a record for this chain is no-record-for-chain, and "Send" stays disabled.

7. Confirmation. Repeat name, namespace badge and registry short-address on the confirmation sheet, next to the address the transaction will carry.

Failure modes

codeUser-facing messageBlocks send?
unrecognizednone: pass the input through as a raw addressno (it is not a name)
invalid-handle, invalid-domainnone while typing; on submit, the reason (for example "no hyphen at the edge of a name")yes
ambiguous"@name and name.arc are different names — type one of them"yes
not-found"alice.arc is not registered"yes
no-record-for-chain"alice.arc has no ETH address"yes
tld-retired"alice.circle: this TLD has been retired"yes
tld-sunset (write builders only)"registrations for .circle are closed"; the name still resolves for sends until sunsetAtn/a for reads
tld-paused (write builders only)"registrations for .circle are paused"; the name still resolves for sendsn/a for reads
offchain-lookup, rpc-error"Couldn't read the chain — try again"yes; never shown as "not registered"
not-configurednone for users; a configuration error: no deployment for this chainyes
verification === "unverified""This address has not been verified by the name's owner"no, after an explicit warning
tld.status === "sunset""The .circle namespace is being retired; names resolve until …"no

The two lines that matter most: an RPC failure is a retry, never "free"; and a controller is never a payment address.

Integration approaches

ApproachUse whenTrade-offs
Direct RPC through @arcns/resolveYou can run viem in-process (web, Node, React Native)No third party to trust: the wallet reads Arc itself. One shared 5-second cache per resolver. reverse() costs 2 to 4 eth_calls. Not yet on the npm registry — build it from the sdk/ directory of the repository and npm link it until the first testnet release ships.
REST through api.arcns.ioYou cannot run an EVM client (a thin client, a bot, a backend in another language)Same fields, cache-control: public, max-age=5, 20 requests per second per client with burst 60. CORS is open. Free for first-party browser origins on the ArcNS sites and for callers with an ArcNS-issued X-API-Key; every other caller (a bot, a backend, a script) gets 402 Payment Required and pays $0.001 per call over x402 v2. Treat 502 as "resolve later", never as "free". See Agents: pay per read with x402.
Your own API instanceBulk consumers, or a compliance boundarySame code, your RPC, your addresses from the deployment file. See API.

Whichever you choose: the chain is the source of truth, and an outage of api.arcns.io is not a resolution answer.

Verification semantics and the controller

  • verified is per coin type: alice.arc can have a verified Arc address and an unverified Bitcoin address.
  • Records are keyed by the name's epoch, which bumps on every ownership change, so a record set by a previous owner can never be returned.
  • controller (SDK) / owner (API) is who holds the name. It is exposed so a wallet can show "controlled by"; it is never the payment address and no resolver falls back to it.

What ArcNS provides, what the wallet builds

ArcNS providesThe wallet builds
@arcns/resolve (MIT, ESM and CJS, viem peer) — built from the repository's sdk/ directory today; not yet on the npm registrythe recipient field, the picker for unverified, in-wallet warnings
typed ResolveError.code for every failurethe messages in the table above, in the wallet's own voice
namespaceLabel() and every result field needed for the display rulethe badge, the registry short-address, the confirmation sheet
reverse() with forward confirmation built inthe "sending to alice.arc" display and any short cache around it
records() with per-coin-type verificationmulti-chain address selection
api.arcns.io with the same fields, CORS, a public cache and free first-party/X-API-Key readsfallback or thin-client integration; paying over x402 for non-browser, keyless traffic
transaction builders (buildSetAddr, buildVerifyAddrSelf, buildSetPrimary, …) returning a PreparedCallsigning and sending; the SDK never holds keys
the contract addresses, already deployed on Arc testnet and bundled in the SDKpinning them in the build

Handoff

  • We give: the SDK, the API, this page, the Quickstart and the Verified records page.
  • We need: the chains you send on (so we can confirm the coin types you read), and the URL of your integration once it ships so we can list it.
  • Contact: [email protected].

Questions

Questions: [email protected].

Browse the docs