Skip to main content
Everything the resolution endpoints serve is derived from public contract state you can read yourself. This page is the whole integration for explorers, indexers, and resolution services that want the answer without our API. There are two ways to read it, both trustless and both talking only to a Soroban RPC node:

Call the read functions

Invoke the contracts’ own view functions with simulateTransaction. The contracts apply resolver precedence, generation-gating, and expiry on-chain — you get the exact answer the SDK and API give. Start here.

Read ledger state directly

Pull the raw contract-data entries with getLedgerEntries. Full control for indexers, but you must replicate the resolution logic yourself — see the caveats.
Testnet only for now — mainnet is not deployed. At mainnet you swap all of the Registry + PrimaryName IDs, the RPC URL, and the passphrase (Public Global Stellar Network ; September 2015) — none are interchangeable with testnet. RPC https://soroban-testnet.stellar.org, passphrase Test SDF Network ; September 2015. Examples use @stellar/stellar-sdk v17.

Contracts

Only two contracts are platform-level, and they are the only addresses an integration should ever hard-code: Every namespace (nova, veil, …) has its own Registrar and Resolver instances, deployed by the Registry. You never hard-code them — you discover them from the Registry (registrar_of(nsNode) / resolver_of(nsNode)), so a single Registry address serves every namespace that exists now or launches later. A per-namespace address copied from a doc is a liability; the Registry lookup is the API.

The namehash

Every name maps to a 32-byte node. A namespace hashes from 32 zero bytes; a name hashes from its namespace’s node. label is the raw UTF-8 bytes of a single canonical label — lowercased, [a-z0-9-], 1–63 chars (the contracts reject anything else, so Nova and nova are different inputs and only nova resolves):
The node is 32 raw bytes. When you put it in a ledger key below, pass the bytes — not the 64-char hex string.

Resolve a name to an address

The resolved address is Resolver.addr(node) when a current forward record exists, otherwise the Registrar’s built-in resolve(label). Calling the contracts applies that precedence — and the generation and expiry gates — for you (nsNode / subNode and their import { hash } come from The namehash above):
Do not use the Registrar’s Name.address as the answer. The holder can point a name’s forward record (Resolver.addr) somewhere other than where the name is held; that resolver record is the pay-to target and takes precedence. The built-in Name.address is only the fallback for names whose holder never set a resolver record. Reading Name.address alone gives the wrong address for any name with a divergent forward record. addr, text, name_of, and primary_of apply both the generation gate and the on-chain expiry/liveness gate (an expired or unissued name resolves nothing); the built-in resolve fallback applies the expiry gate. A raw entry read applies neither.
The other view functions follow the same pattern: Resolver.text(node, key) (a text record), Resolver.name_of(addr) / PrimaryName.primary_of(addr) (reverse lookups — both re-verified on read), Registrar.holder_of_node(node) (the holder, which may differ from the pay-to address).

Reading raw state

If you’re indexing and want the raw entries, read them with getLedgerEntries. A Soran DataKey encodes as an ScVal inside a LedgerKey.contractData. Every record key in the tables below is PERSISTENT — the only instance-storage keys (each contract’s Config, the Resolver’s Provenance) are config, not resolution, and are read from the contract-instance entry, not by a DataKey lookup:
Raw reads bypass the on-chain gates, so to match the resolvers you must replicate them yourself:
  • Resolver pointer. Read the resolver from the Registry Node(nsNode) record’s resolver field (resolver_of) — not the Resolver(nsNode) key, which is the attested deployment. They’re equal for the reference stack today, but diverge the moment an owner repoints. resolver == None means a closed namespace — use Name.address (expiry-gated) directly.
  • Generation + expiry. Every Resolver record — Addr(node)AddrRec{ addr, generation }, Text(node, key)TextRec{ value, generation }, Reverse(addr)RevRec{ node, name, generation } — carries the generation it was written under, and only counts when that equals the Registrar Name(node).generation; a transfer or reissue bumps Name.generation and orphans the old records. A Name with nonzero expires_at in the past resolves nothing (0 = never expires).
  • Reverse needs a forward-match too. For a RevRec, generation-equality is necessary but not sufficient: also read the current-generation Addr(rev.node) and confirm it resolves back to the queried address. If the forward record is absent, stale-generation, or points elsewhere, the reverse is dead even when generations match (the contract’s name_of anti-spoof gate).
  • Archival. An empty result (entries: []) means the key was never written — “no such name,” not archived. A present persistent entry can still be archived (rent lapsed): liveUntilLedgerSeq < latestLedger. A read-only indexer cannot and need not restore it (restore is an owner/keeper write) — treat it as stale/unavailable and keep your last-known value; never read a missing-or-archived entry as “the name is free.”
  • RPC limits. getLedgerEntries accepts at most ~200 keys per call and the public node rate-limits — chunk your reads, and run your own Soroban RPC for indexing workloads.
Unless you’re building a full indexer, the view-function path above is simpler and correct by construction.

Storage layout

These record keys are all PERSISTENT contract data, keyed by the 32-byte node (or an address). A Soroban struct decodes to an object keyed by its field-name symbols; u64 fields come back as BigInt. A few encoding notes for raw reads: the Node record carries two extra provenance-status fields beyond owner/resolver (only those two matter for resolution). Registrar(nsNode)/Resolver(nsNode) are the Registry’s attested deployments — for resolution use the resolver_of pointer, which equals the attested resolver for reference-stack namespaces but can differ if an owner repoints it. Text’s second key element is a Symbol (scvSymbol), so its key is scvVec([scvSymbol("Text"), scvBytes(node), scvSymbol(key)]).

Discovering names

Both read paths resolve a name you already hold — but on-chain state is not enumerable, so you can’t list every name by scanning storage. To learn which nodes exist in the first place, index the contracts’ events: the Registrar emits issued / transfer / reclaimed, the Registry emits alloc / claim, and the Resolver emits its record updates. Stream them with the RPC’s getEvents (filtered to the contract IDs) to build and maintain your node set, then resolve each node with either method above. For a quick start you can also seed from GET /v1/namespaces and the directory endpoints.

A resolved address may be a contract

The value you get back is a Soroban Address — it can be an account (G…) or a contract (C…); muxed (M…) addresses are never stored. Branch on the type: a C… result is a contract — pay it through the asset’s Stellar Asset Contract transfer, not a classic Payment operation; only a G… address accepts a classic payment.
The authoritative storage format is the contract source — the DataKey enums and structs in contracts/{registry,registrar,resolver,primary}/src/lib.rs. The same data is served, already resolved, by the HTTP API and the Lookup SDK; the trust model explains why the Registry’s answers can’t be edited out from under you.