Skip to content
ArcNS docs
DocsWallet 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, the read-only API at api.arcns.io, and the contracts (addresses pending, 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; refuse to pick between registries; 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 and consults every pinned registry for a TLD.
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 and mismatch safelyThe typed ResolveError.code switch below: not-found and no-record-for-chain are explicit; conflict shows every candidate and never auto-selects.

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 addresses arrive in the package with the testnet deployment file; until then pass a deployment. 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); its full contract is on the API page.

The flow, end to end

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 "conflict":            return chooseRegistry(e.candidates!);          // never auto-pick
      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 (and both .arc / .circle registries) side by side and never auto-select. 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
conflicta picker listing every candidate with its product and registry; nothing selectedyes, until the user chooses
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
unknown-registrynone for users; a configuration error: an unpinned registry was passed without opt-inyes
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.
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. Treat 502 as "resolve later", never as "free". CORS is open.
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.
  • Answers from the pinned peer registry are always unverified and carry only a coin-type-60 record.

What ArcNS provides, what the wallet builds

ArcNS providesThe wallet builds
@arcns/resolve on npm (testnet tag), MIT, ESM and CJS, viem peerthe recipient field, the picker for conflict, the warning for unverified
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 and a public cachefallback or thin-client integration
transaction builders (buildSetAddr, buildVerifyAddrSelf, buildSetPrimary, …) returning a PreparedCallsigning and sending; the SDK never holds keys
the contract addresses in the deployment file, when publishedpinning 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].