npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arcns/resolve

v0.2.0-testnet.1

Published

Resolve @handles, .arc and .circle names on Circle's Arc chain. Every result names its namespace and the registry it was read from; a mixed shape is refused, never guessed.

Readme

@arcns/resolve

status: not yet on npm license MIT node ≥ 24.20

Resolve @handles, .arc and .circle names to addresses on Circle's Arc chain — the SDK behind arcns.io.

Built for wallets, explorers and payment flows. One rule drives the whole design: a name is never just an address. @jack, jack.arc and jack.circle are different namespaces that can belong to different people — so every result tells you which namespace answered and which registry it was read from. Resolution is single-registry and authoritative: this SDK reads only the FortiBlox ArcNS registry, never a competing one. A mixed shape (@jack.arc) is refused rather than guessed; a name absent from the registry is a plain not-found.

import { createPublicClient, http } from "viem";
import { arcTestnet, createResolver } from "@arcns/resolve";

const client = createPublicClient({ chain: arcTestnet, transport: http(), ccipRead: false });
const arcns = createResolver({ client });

const r = await arcns.resolve("@jack");
// { name: "@jack", namespace: "handle", registry: "0x…", product: "arcns",
//   address: "0x…", coinType: 2152525650, verification: "verified", epoch: 3n, … }

await arcns.resolve("jack.arc");      // our registry only, one authoritative answer
await arcns.resolve("@jack.arc");     // throws ResolveError { code: "ambiguous" }
await arcns.reverse("0xce42…54AC");   // the address's one primary across all namespaces, forward-confirmed, or null

Status: pre-release, Arc testnet (chain id 5042002), not yet published to npm. Current version: 0.2.0-testnet.1 (see package.json); the @arcns npm org has not been created yet (CEO action), so nothing under this name is installable from the registry today — see Install and Releasing below. The arcns contract addresses are filled in when the contracts lane deploys (deployments/5042002.json); until then createResolver needs an explicit deployment — every read (@handle, our .arc/.circle registry, reverse()) throws not-configured without one. The contract ABIs mirror the exported contracts/abi/*.json (M1 registry + TLD stack + oracle): test/abi-drift.test.js pins every function, error and event fragment and test/enum-drift.test.js pins the Solidity enums, so a signature or enum change on chain fails the SDK build instead of shipping a silent mismatch.

Install

Not yet on the npm registry. The @arcns npm org has not been created (CEO action, tracked in RELEASING.md), so npm install @arcns/resolve will fail today — there is nothing to resolve. Once it is published (pre-release versions ship under the testnet dist-tag, never latest):

npm install @arcns/resolve@testnet viem

Until then, build it from a checkout of this repo and install the local tarball or path:

git clone https://forgejo.selfhsted.com/fortiblox/fortiblox-arcns.git
cd fortiblox-arcns/sdk
npm ci && npm run build
npm pack                       # arcns-resolve-<version>.tgz
# in your project:
npm install /path/to/fortiblox-arcns/sdk/arcns-resolve-<version>.tgz viem
# or, for local iteration: npm install /path/to/fortiblox-arcns/sdk viem

viem 2.x is a peer dependency. ESM and CommonJS builds are both shipped.

The one rule: show the namespace and the registry before you send

There is no API in this SDK that returns a bare address. Every Resolved carries namespace, registry, product, chainId and epoch (SR-04). Render the namespace badge and the registry short-address in your recipient field before the user commits a transfer (SR-72).

Dispatch is on shape (@ prefix vs a known TLD suffix), never "try one, fall back to the other". @jack.arc throws ambiguous. Use looksLikeName(input) to decide whether to attempt resolution at all, so a pasted 0x… address does not surface a validation error.

Namespaces

| Shape | Namespace | Where it lives | |---|---|---| | @alice, pay.@alice | handle | HandleRegistry (ERC-721, permanent, ASCII-only, reserved genesis) | | alice.arc, pay.alice.arc | arc | verbatim ENS registry, .arc registrar pair | | alice.circle | circle | verbatim ENS registry, .circle registrar pair |

TLDs are data: the SDK reads the live table from TldDirectory (loadTldTable — every row tldNodes() lists, or an explicit label set) and parseName / resolve / reverse take the loaded set. Adding a TLD is a table row, not a code change. A TLD in sunset keeps resolving until its sunsetAt, then every read is tld-retired — never a stale address.

Normalization

normalizeHandle / tryNormalizeHandle are byte-for-byte the Solidity HandleNormalize library (itself a port of the Rust handle-normalize crate — the single source of truth, SR-01). .arc / .circle labels use the same grammar: ASCII a-z0-9-, 1–32 bytes, no edge or double hyphen, not all-digit. No Unicode, no emoji — for a payment identifier a homograph is a live attack (@аlice with a Cyrillic а). The conformance suite (test/conformance.test.js) pins all 65 generated fixtures, reason names included; CI regenerates the fixtures from Solidity and fails on drift.

Rejections carry the same reason name the chain reports: ResolveError { code: "invalid-handle" | "invalid-domain", reason: "HyphenAtEdge" | … }.

Single-registry, authoritative

.arc / .circle resolve only through the FortiBlox ArcNS registry (deployment.ensRegistry). There is no cross-resolution against, comparison with, or warning about any other registry on Arc — resolve() / resolveAll() never read a second registry, and a name this registry does not hold is a plain ResolveError { code: "not-found" }, exactly like any other unregistered name (CEO decision 2026-09-09: single-registry, winner-take-all — see docs/plan/open-questions.md).

API

createResolver(config: ResolverConfig): Resolver

interface ResolverConfig {
  client: PublicClient;                 // viem; chain set; pass ccipRead: false (SR-24)
  deployment?: ArcnsDeployment;         // default ARCNS_DEPLOYMENTS[chainId]; absent → not-configured for our paths
  tldTable?: TldTable;                  // default: loadTldTable(client, deployment.tldDirectory), lazily
  cacheTtlMs?: number;                  // default 5_000 (SR-25); 0 disables; resolve() only
  now?: () => number;                   // unix seconds for TLD gates
}

interface Resolver {
  resolve(input, opts?: { chain?: Chain; fallbackChains?: Chain[] }): Promise<Resolved>;   // the one authoritative answer, or `not-found`
  resolveAll(input, opts?): Promise<readonly ResolvedCandidate[]>;                          // `[resolve(input, opts)]` — kept for API shape stability
  reverse(address): Promise<Primary | null>;                                                // one primary across all namespaces, forward-confirmed
  records(input): Promise<readonly AddressRecord[]>;                                        // every coin type with a record, each with `verification`
  text(input, key): Promise<string | null>;
  texts(input, keys): Promise<Readonly<Record<string, string | null>>>;                     // several text records, one gate (WP-211)
  state(input): Promise<NameState>;                                                          // controller / epoch / node, no record read
  record(input, chain): Promise<AddressRecord | null>;                                       // one coin type
  tlds(): Promise<TldTable>;
  challenge(input, chain, nonce?): Promise<RecordChallenge>;                                  // SR-41 challenge + EIP-712 typed data, bound to versionOf(node)
  clearCache(): void;
}

Chain is "ARC" | "ETH" | "BTC" | "SOL" | "X1"; COIN_TYPE.ARC is the ENSIP-11 coin type 0x80000000 | 5042002 = 2152525650. The default chain is ARC; fallbackChains: ["ETH"] tries the coin-type-60 record next and reports which one answered via coinType.

Every Resolved carries input, name, namespace, chainId, registry, product, resolver, node, controller, epoch, coinType, chain, address, addressBytes, verification, (for handles) handleType and (for TLDs) tld: { status, sunsetAt }. controller is who owns the name; it is never used as the payment address (SR-21).

Agent handles (WP-315)

import { isAgentHandle, handleTypeName, HANDLE_TYPE_NAMES } from "@arcns/resolve";

const r = await arcns.resolve("@payday-bot");
r.handleType;        // "Human" | "Merchant" | "Org" | "Agent"  (handles only; undefined for .arc / .circle)
isAgentHandle(r);    // true only for namespace "handle" + handleType "Agent"
isAgentHandle(await (await fetch("https://api.arcns.io/resolve/%40payday-bot")).json()); // same rule on an API body
handleTypeName(3);   // "Agent"; an unknown code is "Unknown", never a guess

handleType is the IHandleRegistry.HandleType the registrant declared at registration. It is descriptive — nobody verifies it — so it tells you how to address a name (an agent may publish an agent.manifest text record, see docs/architecture/agent-records.md), never whether to pay it; that is verification. The REST equivalent is handleType in the /resolve body (api/schema/resolve.json) and /search?handleType=Agent on the query API; docs/resolution-integration.md §6 has the agent checklist.

Agent records (WP-211)

An agent name carries its endpoints and identity as agent.* text records (docs/architecture/agent-records.md); the payment address is never a text record — it is the on-chain Arc record with its verified flag.

import { readAgentRecords, verifyErc8004Owner, agentVerificationLevel, validateManifest, x402AcceptsFromManifest } from "@arcns/resolve";

const r = await readAgentRecords(client, "@payday-bot");   // { manifest?, x402?, mcp?, a2a?, erc8004?, capabilities?, payTo?, verified, rejected, controller, … }
agentVerificationLevel(r);                                 // "none" | "L0"  (L0: declared by records or handleType Agent)
const l1 = await verifyErc8004Owner(client, r);            // IdentityRegistry.ownerOf(agentId) == controller?  { ok, agentOwner, nameOwner, reason? }
agentVerificationLevel(r, l1);                             // "L1" when l1.ok and l1.link is the name's own agent.erc8004

const json = await (await fetch(r.manifest)).json();       // your fetch, your policy (https:// or ipfs://<cid>)
const v = validateManifest(json, { records: r });          // schema (sdk/schema/agent-manifest.json) + cross-checks; { ok, manifest | issues }
if (v.ok) x402AcceptsFromManifest(v.manifest, { payTo: r.payTo }); // accepts[]-shaped hints; the live 402 is authoritative

| Export | What it does | |---|---| | AGENT_RECORD_KEYS | agent.manifest / agent.x402 / agent.mcp / agent.a2a / agent.erc8004 / agent.capabilities (D1) | | readAgentRecords(client, name, opts?) | every key plus payTo = addr(node, 0x804cef52) and verified, through createResolver (state / texts / record). Same gate as records(): not-found, nft-burned, TLD lifecycle, rpc-error. A value that breaks the reader rules is listed in rejected, never returned | | checkAgentUrl(value, key?) | D2: https:// only; ipfs://<cid> additionally for agent.manifest; credentials, empty host, anything else refused | | parseErc8004Link / tryParseErc8004Link / formatErc8004Link | eip155:<chainId>:<IdentityRegistry>:<agentId>; checksum-validated registry, decimal ids; AgentRecordError { code: "invalid-erc8004-link", reason } | | ERC8004_IDENTITY_REGISTRY[5042002] | 0x8004A818BFB912233c491871b3d84c89A494BD9e — a link to any other registry is unknown-registry, never read | | verifyErc8004Owner(client, name \| records, link?, opts?) | L1: ownerOf(agentId) vs the name's controller; wrong-chain / unknown-registry / agent-not-found / owner-mismatch; transport → rpc-error | | isAgentName(records) | D3: agent.manifest or agent.erc8004 set, or an Agent handle. isAgentHandle stays handle-only | | agentVerificationLevel(records, l1?) | none / L0 / L1. L2 (the agent.x402 URL answers 402 with payTo == addr record) is a live HTTP fact against an arbitrary host: the MCP server probes it, the SDK never does | | validateManifest(json, { records?, addressRecords? }) / assertManifest | JSON Schema draft 2020-12 (AGENT_MANIFEST_SCHEMA, strict, closed objects), then canonical name, link checksum, D2 URLs, and agreement with the chain: arcns.name/chainId/node, identity.erc8004, the x402 endpoint, and every payment.addresses[] mirror (never more verified than the chain). No content pinning (D6) | | x402AcceptsFromManifest(manifest, { payTo? }) | one x402 v2 accepts[]-shaped hint per x402 endpoint (scheme, network, resource, asset?, payTo?, facilitator?, extra?); pass the chain's payTo so it wins over the manifest's mirror |

The resolver gained the primitives these are built on: state(name) (controller / epoch / node, no record read), texts(name, keys) (one gated pass, keys in parallel) and record(name, chain) (one coin type).

Reverse

const p = await arcns.reverse("0xce42…54AC");
// { name: "alice.arc", namespace: "arc", registry, product, chainId, node, address, controller } | null

Returned only while the primary still forward-confirms: @alice must be owned by the address; alice.arc / bob.circle must resolve back to it (addr(node, 0x804cef52), then coin type 60) in our registry. A stale string is null, never the previous owner.

Writes — build, sign, relay

Every builder returns a PreparedCall { chainId, to, data, value, abi, functionName, args, description } you hand to walletClient.sendTransaction (or writeContract with abi/functionName/args). The SDK never holds keys. Registration is commit → wait minCommitmentAge → reveal. Every reveal field is inside the commitment (SR-10: handleType for a handle; resolver, data, reverseRecord for a TLD name), so pass the same fields to buildCommit and buildRegister:

import { generateSecret, buildCommit, buildRegister, buildSetAddr, buildSetPrimary, buildVerifyAddrSelf, tldRegistrarControllerAbi } from "@arcns/resolve";

const secret = generateSecret();
const reg = { deployment, name: "alice.arc", owner, secret, reverseRecord: true };
await wallet.sendTransaction(buildCommit(reg));
// … wait minCommitmentAge (read it from the controller) …
const price = await client.readContract({ address: deployment.tlds.arc.controller, abi: tldRegistrarControllerAbi, functionName: "quote", args: ["alice"] });
await wallet.sendTransaction(buildRegister({ ...reg, price }));                                        // price = msg.value (18-dec USDC wei); maxPrice defaults to price
await wallet.sendTransaction(buildSetAddr({ deployment, name: "alice.arc", chain: "BTC", address: "bc1q…" }));
await wallet.sendTransaction(buildVerifyAddrSelf({ deployment, name: "alice.arc" }));                  // proves the Arc address (msg.sender)
await wallet.sendTransaction(buildSetPrimary({ deployment, name: "alice.arc" }));                     // one primary per address, any namespace

register reverts PriceChanged when the live quote exceeds maxPrice and credits any overpayment to the payer — buildWithdraw({ deployment, namespace }) pulls it (SR-11/31). buildTokenize({ name, fee, maxPrice? }) pays exactly ArcNSPriceOracle.quoteTokenize(HANDLE_ROOT, label) (priceOracleAbi).

Also: buildTransfer / buildTransferFrom, buildClearAddr, buildSetText, buildVerifyAddr / buildVerifyAddrSelf1271 (with a proof), buildApproveDelegate, buildMulticall, buildRelease, buildLock / buildInitiateUnlock / buildCompleteUnlock / buildCancelUnlock, buildSetRecovery / buildInitiateRecovery / buildCancelRecovery / buildCompleteRecovery, buildCreateSubname / buildRevokeSubname, buildSetSubnodeRecord. TLD writes are refused with tld-paused / tld-sunset / tld-retired when the directory says so.

Ownership proofs (SR-41, onchain-design §5)

A proof is bound to ArcNSResolver.versionOf(node) — the record-version key that changes on every ownership change — so a captured proof from a previous owner never validates for the next. arcns.challenge() reads it and returns both signable forms:

import { buildVerifyAddr } from "@arcns/resolve";
const c = await arcns.challenge("alice.arc", "ETH");
// c.challenge = "arcns:v1:rec:0x<node>:60:0x<version>:5042002:0x<resolver>:<nonce>"   (Challenge.sol, byte for byte)
// c.typedData = EIP-712 { domain: { name: "arcns", version: "1", chainId, verifyingContract }, RecordProof(node, coinType, version, nonce) }
const signature = await ethWallet.signTypedData(c.typedData);          // EVM records sign the typed data
buildVerifyAddr({ deployment, name: "alice.arc", chain: "ETH", nonce: c.nonce, proof: { signature } });   // → verifyAddrSecp256k1

| record | wallet signs | resolver entry point (chosen by buildVerifyAddr) | |---|---|---| | ARC, EOA | nothing — msg.sender == record | buildVerifyAddrSelf → verifyAddrSelf | | ARC, contract wallet | EIP-712 RecordProof (ERC-1271) | buildVerifyAddrSelf1271 | | ETH / any EVM | EIP-712 RecordProof | verifyAddrSecp256k1 | | BTC | BIP-137 over c.challenge, plus the 64-byte uncompressed pubkey | verifyAddrBtc | | SOL / X1 | ed25519 over c.challenge bytes, plus the 32-byte pubkey | verifyAddrEd25519 |

recordProofDigest equals ArcNSResolver.recordProofDigest on chain; verifyEthTypedProofLocal / verifyControlProofLocal are client-side pre-checks so a wallet refuses a doomed transaction before it is sent. Control challenges (ctl, issueControlChallenge) share the layout and are verified off chain by the API, never by the resolver.

Errors

ResolveError carries a code a UI can branch on:

| code | meaning | |---|---| | unrecognized | not a handle or a known domain — probably a raw address | | ambiguous | both shapes at once, e.g. @jack.arc | | invalid-handle / invalid-domain | right shape, invalid content — reason names the rule | | not-found | valid name, not registered (or reason: "nft-burned"); only after a successful chain read | | no-record-for-chain | registered, but no address record for the requested chain — never the controller | | tld-retired / tld-sunset / tld-paused | TLD lifecycle gates (reads: tld-retired; writes: all three) | | offchain-lookup | the resolver asked for CCIP-read; v1 treats that as an error, never as data | | rpc-error | transport / decoding failure — never reported as not-found | | not-configured | no deployment for this chain — never a guessed address |

Configuration without a deployment

import { deploymentFromJson, deploymentFromEnv } from "@arcns/resolve";
const deployment = deploymentFromJson(JSON.parse(readFileSync("deployments/5042002.json", "utf8")));
// or, in a service: deploymentFromEnv(process.env)  — HANDLE_REGISTRY_ADDRESS, ENS_REGISTRY_ADDRESS, …

Missing contracts throw not-configured; there are no default addresses.

Developing

npm ci
npm run build       # tsc → dist/esm + dist/cjs
npm run lint        # eslint (type-aware on src); publint runs at the end of build
npm test            # node --test (fixture conformance is the first suite)

Releasing (current: 0.2.0-testnet.1, dist-tag testnet)

The npm org @arcns does not exist yet (CEO action); nothing is published from this repo until it does. The recipe, run by the release-director from a clean checkout of the tagged commit:

cd sdk
npm ci --no-audit --no-fund
npm run build && npm run lint && npm test          # publint runs at the end of build
npm pack --dry-run                                 # inspect the file list: dist/, README, CHANGELOG, LICENSE
NODE_AUTH_TOKEN="$(cat /path/to/npm-token)" npm publish --dry-run --access public --tag testnet
git tag -s "sdk-v$(node -p "require('./package.json').version")" -m "@arcns/resolve $(node -p "require('./package.json').version")"
git push origin --tags
NODE_AUTH_TOKEN="$(cat /path/to/npm-token)" npm publish --access public --tag testnet --provenance   # drop --provenance if CI OIDC is unavailable
npm view @arcns/resolve@testnet version             # must equal package.json

Rules: pre-releases go to the testnet tag, never latest; the token is read from a file at publish time, never exported into a shell profile or committed; rollback is npm deprecate @arcns/resolve@<version> "<reason>" (npm does not allow unpublishing after 72 h) plus moving the testnet tag back: npm dist-tag add @arcns/resolve@<previous> testnet.

License

MIT © Fortiblox