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
| Capability | How |
|---|---|
| Recognise input | looksLikeName(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 safely | Nothing 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 address | arcns.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 safely | The 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
code | User-facing message | Blocks send? |
|---|---|---|
unrecognized | none: pass the input through as a raw address | no (it is not a name) |
invalid-handle, invalid-domain | none 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 sunsetAt | n/a for reads |
tld-paused (write builders only) | "registrations for .circle are paused"; the name still resolves for sends | n/a for reads |
offchain-lookup, rpc-error | "Couldn't read the chain — try again" | yes; never shown as "not registered" |
not-configured | none for users; a configuration error: no deployment for this chain | yes |
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
| Approach | Use when | Trade-offs |
|---|---|---|
Direct RPC through @arcns/resolve | You 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.io | You 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 instance | Bulk consumers, or a compliance boundary | Same 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
verifiedis per coin type:alice.arccan 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 provides | The wallet builds |
|---|---|
@arcns/resolve (MIT, ESM and CJS, viem peer) — built from the repository's sdk/ directory today; not yet on the npm registry | the recipient field, the picker for unverified, in-wallet warnings |
typed ResolveError.code for every failure | the messages in the table above, in the wallet's own voice |
namespaceLabel() and every result field needed for the display rule | the badge, the registry short-address, the confirmation sheet |
reverse() with forward confirmation built in | the "sending to alice.arc" display and any short cache around it |
records() with per-coin-type verification | multi-chain address selection |
api.arcns.io with the same fields, CORS, a public cache and free first-party/X-API-Key reads | fallback or thin-client integration; paying over x402 for non-browser, keyless traffic |
transaction builders (buildSetAddr, buildVerifyAddrSelf, buildSetPrimary, …) returning a PreparedCall | signing and sending; the SDK never holds keys |
| the contract addresses, already deployed on Arc testnet and bundled in the SDK | pinning 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].