Documentation
Overview
Arc Names is a minimal name service for Arc: one registrar (an ERC-721 that holds the USDC) and one resolver (records and primary names, no admin, no funds). Names are registered with a commit-reveal flow, priced per year in USDC, and expire with a 90-day grace period.
- Network for this build: Arc Testnet (chain id 5042002). Native currency: USDC with 18 decimals at the EVM level — 1e18 == 1.00 USDC.
- Registration and gas are both paid in USDC. There is no ERC-20 payment path and no oracle.
- Unaudited software — use at your own risk. Source, tests and the full specification live in the repository.
Contract addresses
Generated from the deployment records. A zero address means the contracts are not deployed on that chain yet.
| Chain | ArcRegistrar | ArcResolver | Deploy block |
|---|---|---|---|
| Arc Testnetid 5042002this build | 0xC18D4E6bdD42351F49b63e0df07e999161Da8653 | 0x46504B51b5ed1Ad6d3E3782344acB4463393eb3f | 62434396 |
| Arcid 5042 | not deployed | not deployed | — |
Both contracts are verified on the Arc explorer when deployed. The registrar’s resolver() getter returns the canonical resolver address on-chain, so you can discover it without trusting this page.
Names, nodes and token ids
Label rules
- 3–63 bytes, each in [a-z0-9-]. Uppercase, unicode, spaces and dots are invalid.
- No leading or trailing hyphen.
- No -- at positions 3–4 (blocks xn-- lookalikes).
- 1–2 character names are not registrable in v1.
^(?!-)(?!..--)[a-z0-9-]{3,63}(?<!-)$
Identifiers
- label
- the part before .arc, e.g. alice
- node (resolver key)
- labelhash(label) = keccak256(bytes(label))
- tokenId (ERC-721)
- BigInt(node) = uint256(keccak256(bytes(label)))
- display name
- label + ".arc"
In v1 the node is the ENS labelhash, not namehash("label.arc").
| State | Condition | What works |
|---|---|---|
| Active | now < expires | Resolves, transferable, renewable by anyone. |
| Grace (90 days) | expires ≤ now < expires + 90d | Records hidden, ownerOf reverts, not transferable, still renewable, not available to others. |
| Available again | now ≥ expires + 90d | Anyone may register. A premium of 1,000 USDC decays linearly to 0 over 21 days. Re-registration bumps the nonce, so old records never come back. |
Prices per 365 days: 3 characters 100 USDC, 4 characters 25 USDC, 5+ characters 5 USDC (owner-settable, read them live with rentPrice(label, duration)). Minimum duration 28 days, maximum 10 years total. Commitments must be at least 60 seconds and at most 24 hours old when revealed.
Read names with viem
Everything an integrator needs is on the resolver: getName(address) for the verified reverse lookup and resolve(name) for the forward lookup. Both return empty values instead of reverting.
import { createPublicClient, defineChain, http, labelhash, parseAbi } from "viem";
// Arc Testnet (chain id 5042002); native currency is USDC with 18 decimals.
const arc = defineChain({
id: 5042002,
name: "Arc Testnet",
nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"] } },
});
const client = createPublicClient({ chain: arc, transport: http() });
const resolver = "0x46504B51b5ed1Ad6d3E3782344acB4463393eb3f"; // ArcResolver
const abi = parseAbi([
"function getName(address account) view returns (string)",
"function resolve(string name) view returns (address)",
"function addr(bytes32 node) view returns (address)",
"function text(bytes32 node, string key) view returns (string)",
]);
// Reverse lookup: address -> verified primary name ("alice.arc", or "" when none).
const name = await client.readContract({ address: resolver, abi, functionName: "getName", args: [account] });
// Forward lookup: "alice" or "alice.arc" -> address (zero address when unset or expired).
const owner = await client.readContract({ address: resolver, abi, functionName: "resolve", args: ["alice.arc"] });
// Records: node = labelhash(label) — NOT namehash("alice.arc").
const node = labelhash("alice");
const twitter = await client.readContract({ address: resolver, abi, functionName: "text", args: [node, "com.twitter"] });ABI
Human-readable ABIs (drop straight into viem’s parseAbi or ethers’ Interface). The full contract ABIs are on the verified explorer pages; these subsets cover every call the dapp makes.
ArcRegistrar
function tokenIdOf(string label) pure returns (uint256)
function isValidLabel(string label) pure returns (bool)
function available(string label) view returns (bool)
function reserved(uint256 id) view returns (bool)
function nameState(uint256 id) view returns (uint64 nonce, uint64 expires)
function nameOf(uint256 id) view returns (string)
function nameInfo(uint256 id) view returns (address holder, string label, uint64 expires, uint64 nonce, bool isReserved)
function rentPrice(string label, uint256 duration) view returns (uint256 base, uint256 premium)
function premiumOf(uint256 id) view returns (uint256)
function makeCommitment(string label, address owner, uint256 duration, bytes32 secret) pure returns (bytes32)
function commitments(bytes32 commitment) view returns (uint256)
function minCommitmentAge() view returns (uint256)
function maxCommitmentAge() view returns (uint256)
function GRACE_PERIOD() view returns (uint256)
function MIN_REGISTRATION_DURATION() view returns (uint256)
function MAX_REGISTRATION_DURATION() view returns (uint256)
function paused() view returns (bool)
function resolver() view returns (address)
function commit(bytes32 commitment)
function register(string label, address owner, uint256 duration, bytes32 secret) payable
function renew(string label, uint256 duration) payable
function ownerOf(uint256 id) view returns (address)
function balanceOf(address owner) view returns (uint256)
function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)
function tokenURI(uint256 id) view returns (string)
function safeTransferFrom(address from, address to, uint256 id)
function setApprovalForAll(address operator, bool approved)
event Committed(bytes32 indexed commitment, uint256 timestamp)
event NameRegistered(uint256 indexed id, string label, address indexed owner, uint64 expires, uint256 baseCost, uint256 premium)
event NameRenewed(uint256 indexed id, uint64 expires, uint256 cost)
error InvalidLabel()
error NameUnavailable()
error NameExpired(uint256 id)
error NotRenewable()
error DurationTooShort()
error DurationTooLong()
error UnknownCommitment()
error CommitmentTooNew()
error CommitmentTooOld()
error CommitmentExists()
error InsufficientPayment(uint256 required, uint256 sent)
error RefundFailed()
error EnforcedPause()
error ERC721NonexistentToken(uint256 tokenId)ArcResolver
function addr(bytes32 node) view returns (address)
function text(bytes32 node, string key) view returns (string)
function getName(address account) view returns (string)
function primaryNode(address account) view returns (bytes32)
function resolve(string name) view returns (address)
function setAddr(bytes32 node, address a)
function setText(bytes32 node, string key, string value)
function setPrimaryName(bytes32 node)
function multicall(bytes[] data) returns (bytes[])
event AddrChanged(bytes32 indexed node, address a)
event TextChanged(bytes32 indexed node, string indexed indexedKey, string key, string value)
event PrimaryNameChanged(address indexed account, bytes32 indexed node)
error Unauthorized()
error NotNameOwner()For integrators
- Keys. node = labelhash(label) and tokenId = BigInt(node). Strip a trailing .arc before hashing (resolve does this for you).
- Reverse lookup. getName(account) is verified on every read: it returns "" unless the account still owns the name, the name is unexpired and its addr record points back at the account. Never trust primaryNode(account) alone — it is the raw, unverified pointer.
- Forward lookup. resolve("alice.arc") returns the zero address when no addr record is set or the name is expired. It never falls back to the token owner, so a cold wallet can own a name without exposing it as a payment target.
- Ownership display. Use nameInfo(id) (never reverts; returns the raw holder, expiry, nonce and reserved flag) rather than ownerOf, which reverts for expired names. Derive the state from expires, the 90-day grace period and chain time.
- Records. Text records use free-form keys; the dapp uses avatar, description, url, com.twitter, com.github, com.discord, email. Records are versioned by the registration nonce: they disappear the second a name expires and a fresh registration starts blank. They are self-declared and unverified.
- ENS compatibility. The resolver answers the ENS selectors addr(bytes32) (0x3b3b57de) and text(bytes32,string) (0x59d1d43c) and advertises them via supportsInterface. There is no ENS registry, namehash tree or subdomains in v1.
- Writes. setAddr / setText accept the owner or an approved operator; setPrimaryName(node) is owner-only and also writes the addr record so the reverse check passes. Batch several writes with multicall(bytes[]).
Ready to register? Search for a name.