@x1id/resolve 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The X1ID Universal Resolver — one entry that resolves native `@handle` / X1NS
3
+ * names AND external namespaces (`.sol` via SNS, `.eth` via ENS) by reading each
4
+ * name's own chain, returning a single normalized {@link UniversalResolved}.
5
+ *
6
+ * ```ts
7
+ * import { createResolver, WasmResolver } from "@x1id/resolve";
8
+ * import { createUniversalResolver, makeSnsDeriver } from "@x1id/resolve";
9
+ *
10
+ * const wasm = await WasmResolver.fromBytes(wasmBytes);
11
+ * const native = createResolver({ rpcUrl: X1_RPC, wasm });
12
+ * const universal = createUniversalResolver({
13
+ * native,
14
+ * sns: { rpcUrl: SOLANA_RPC, deriver: await makeSnsDeriver() }, // live
15
+ * ens: {}, // gated off (no ETH_RPC_URL)
16
+ * });
17
+ *
18
+ * await universal.resolve("@jack"); // → source: "x1id"
19
+ * await universal.resolve("bob.sol"); // → source: "sns"
20
+ * await universal.resolve("bob.eth"); // → ResolveError "not-configured" (gated)
21
+ * ```
22
+ *
23
+ * Dispatch is by SHAPE (see universalDetect.ts) to exactly one adapter — never a
24
+ * try-one-then-fall-back chain. An unconfigured external namespace throws a typed
25
+ * `not-configured`; it does not poison resolution of the namespaces that ARE live.
26
+ */
27
+ import { ResolveError } from "./types.js";
28
+ import { detectNamespace, isUniversalName } from "./universalDetect.js";
29
+ import { createNativeAdapter } from "./universalNative.js";
30
+ import { createSnsAdapter } from "./universalSns.js";
31
+ import { createEnsAdapter } from "./universalEns.js";
32
+ export * from "./universalTypes.js";
33
+ export { detectNamespace, isUniversalName, sourceForNamespace, externalLabel, } from "./universalDetect.js";
34
+ export { createNativeAdapter } from "./universalNative.js";
35
+ export { createSnsAdapter, makeSnsDeriver, snsHashedName, SPL_NAME_PROGRAM_ID, SOL_TLD_AUTHORITY, SNS_HASH_PREFIX, NAME_TOKENIZER_ID, SNS_MINT_PREFIX, } from "./universalSns.js";
36
+ export { createEnsAdapter, ensNamehash, ENS_REGISTRY_ADDRESS, } from "./universalEns.js";
37
+ /** Build the universal resolver over a native resolver plus optional external adapters. */
38
+ export function createUniversalResolver(config) {
39
+ const nativeAdapter = createNativeAdapter(config.native);
40
+ const snsAdapter = createSnsAdapter(config.sns ?? {});
41
+ const ensAdapter = createEnsAdapter(config.ens ?? {});
42
+ const bySource = Object.freeze({
43
+ x1id: nativeAdapter,
44
+ sns: snsAdapter,
45
+ ens: ensAdapter,
46
+ });
47
+ return {
48
+ async resolve(input, opts) {
49
+ const { source } = detectNamespace(input); // throws unrecognized / ambiguous by shape
50
+ const adapter = bySource[source];
51
+ // Defensive: detection and dispatch agree by construction, but never let a
52
+ // mismatch silently resolve through the wrong chain.
53
+ if (!adapter) {
54
+ throw new ResolveError("unrecognized", `no adapter for "${input}"`, input);
55
+ }
56
+ return adapter.resolve(input, opts);
57
+ },
58
+ detect(input) {
59
+ if (!isUniversalName(input))
60
+ return null;
61
+ return detectNamespace(input).namespace;
62
+ },
63
+ status() {
64
+ return [nativeAdapter.status(), snsAdapter.status(), ensAdapter.status()];
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shape-based namespace detection for the universal resolver.
3
+ *
4
+ * Classification is by SHAPE ONLY — the same discipline as `parse.ts`'s
5
+ * `parseName`: we never try one namespace and fall back to another, because that
6
+ * is exactly how `@jack`, `jack.x1` and `jack.sol` get conflated and a transfer
7
+ * succeeds to the wrong person on the wrong chain.
8
+ */
9
+ import type { NamespaceSource, UniversalNamespace } from "./universalTypes.js";
10
+ export interface DetectedNamespace {
11
+ readonly namespace: UniversalNamespace;
12
+ readonly source: NamespaceSource;
13
+ }
14
+ /** The system that owns a given namespace. A scoped customer-TLD label (not one
15
+ * of the fixed namespaces) is always the native X1ID system. */
16
+ export declare function sourceForNamespace(ns: UniversalNamespace): NamespaceSource;
17
+ /**
18
+ * Classify `input` into exactly one namespace by shape, or throw.
19
+ *
20
+ * - `@name` → `handle` (never a dotted TLD; `@x.sol` is ambiguous → throws).
21
+ * - `name.<tld>` for a known tld → that namespace.
22
+ * - `name.<tld>` for an unknown tld shaped like a customer-TLD label → the
23
+ * label itself, `source: "x1id"` (a SCOPED customer-TLD candidate, #8484/
24
+ * #8485). Whether `.tld` is a LAUNCHED namespace is a chain fact the native
25
+ * resolver establishes — this only classifies the shape.
26
+ * - anything else → `unrecognized`.
27
+ *
28
+ * @throws {ResolveError} `ambiguous` for `@…​.tld`; `unrecognized` otherwise.
29
+ */
30
+ export declare function detectNamespace(input: string): DetectedNamespace;
31
+ /** Non-throwing probe: is `input` shaped like any namespace the resolver knows? */
32
+ export declare function isUniversalName(input: string): boolean;
33
+ /**
34
+ * The single label of an external domain (`"bob"` from `"bob.sol"`), lowercased
35
+ * and validated. Shared by the SNS and ENS adapters so their label rules stay
36
+ * identical and homograph-safe.
37
+ *
38
+ * ASCII only — a payment identifier that renders a Cyrillic homograph as a
39
+ * successful result is a vector we refuse, the same stance as `parse.ts`.
40
+ * Interior dots (subdomains like `pay.bob.sol` / `pay.bob.eth`) use a different
41
+ * derivation per namespace and are NOT supported in this increment: rejected
42
+ * explicitly rather than mis-derived to a wrong account.
43
+ */
44
+ export declare function externalLabel(input: string, suffix: UniversalNamespace): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shape-based namespace detection for the universal resolver.
3
+ *
4
+ * Classification is by SHAPE ONLY — the same discipline as `parse.ts`'s
5
+ * `parseName`: we never try one namespace and fall back to another, because that
6
+ * is exactly how `@jack`, `jack.x1` and `jack.sol` get conflated and a transfer
7
+ * succeeds to the wrong person on the wrong chain.
8
+ */
9
+ import { ResolveError } from "./types.js";
10
+ import { scopedTldCandidate } from "./scoped.js";
11
+ /** Suffix → namespace for the dotted (domain-shaped) FIXED namespaces. */
12
+ const SUFFIX_NAMESPACE = Object.freeze({
13
+ x1: "x1",
14
+ xnt: "xnt",
15
+ xen: "xen",
16
+ sol: "sol",
17
+ eth: "eth",
18
+ });
19
+ const SOURCE_FOR = Object.freeze({
20
+ handle: "x1id",
21
+ x1: "x1id",
22
+ xnt: "x1id",
23
+ xen: "x1id",
24
+ sol: "sns",
25
+ eth: "ens",
26
+ });
27
+ /** The system that owns a given namespace. A scoped customer-TLD label (not one
28
+ * of the fixed namespaces) is always the native X1ID system. */
29
+ export function sourceForNamespace(ns) {
30
+ return SOURCE_FOR[ns] ?? "x1id";
31
+ }
32
+ /**
33
+ * Classify `input` into exactly one namespace by shape, or throw.
34
+ *
35
+ * - `@name` → `handle` (never a dotted TLD; `@x.sol` is ambiguous → throws).
36
+ * - `name.<tld>` for a known tld → that namespace.
37
+ * - `name.<tld>` for an unknown tld shaped like a customer-TLD label → the
38
+ * label itself, `source: "x1id"` (a SCOPED customer-TLD candidate, #8484/
39
+ * #8485). Whether `.tld` is a LAUNCHED namespace is a chain fact the native
40
+ * resolver establishes — this only classifies the shape.
41
+ * - anything else → `unrecognized`.
42
+ *
43
+ * @throws {ResolveError} `ambiguous` for `@…​.tld`; `unrecognized` otherwise.
44
+ */
45
+ export function detectNamespace(input) {
46
+ const t = input.trim();
47
+ const looksHandle = t.startsWith("@");
48
+ const dot = t.lastIndexOf(".");
49
+ const suffix = dot === -1 ? "" : t.slice(dot + 1).toLowerCase();
50
+ const dottedNs = SUFFIX_NAMESPACE[suffix];
51
+ if (looksHandle && dottedNs) {
52
+ throw new ResolveError("ambiguous", `"${t}" is both a handle and a domain shape`, t);
53
+ }
54
+ if (looksHandle) {
55
+ return { namespace: "handle", source: "x1id" };
56
+ }
57
+ if (dottedNs) {
58
+ return { namespace: dottedNs, source: SOURCE_FOR[dottedNs] };
59
+ }
60
+ // A dotted, non-`@`, non-fixed-TLD name shaped like `name.<label>` is a scoped
61
+ // customer-TLD candidate — route it to the native (x1id) path, which reads
62
+ // the chain to confirm the TLD is launched (and otherwise re-throws
63
+ // `unrecognized`, so `.sol`/`.eth`/unknown TLDs are unaffected here).
64
+ const scoped = scopedTldCandidate(t);
65
+ if (scoped) {
66
+ return { namespace: scoped.tld, source: "x1id" };
67
+ }
68
+ throw new ResolveError("unrecognized", `"${t}" is not a handle or a known namespace (.x1 .xnt .xen .sol .eth)`, t);
69
+ }
70
+ /** Non-throwing probe: is `input` shaped like any namespace the resolver knows? */
71
+ export function isUniversalName(input) {
72
+ try {
73
+ detectNamespace(input);
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * The single label of an external domain (`"bob"` from `"bob.sol"`), lowercased
82
+ * and validated. Shared by the SNS and ENS adapters so their label rules stay
83
+ * identical and homograph-safe.
84
+ *
85
+ * ASCII only — a payment identifier that renders a Cyrillic homograph as a
86
+ * successful result is a vector we refuse, the same stance as `parse.ts`.
87
+ * Interior dots (subdomains like `pay.bob.sol` / `pay.bob.eth`) use a different
88
+ * derivation per namespace and are NOT supported in this increment: rejected
89
+ * explicitly rather than mis-derived to a wrong account.
90
+ */
91
+ export function externalLabel(input, suffix) {
92
+ const t = input.trim();
93
+ const dot = t.lastIndexOf(".");
94
+ if (dot <= 0 || t.slice(dot + 1).toLowerCase() !== suffix) {
95
+ throw new ResolveError("invalid-domain", `"${t}" is not a .${suffix} domain`, input);
96
+ }
97
+ const label = t.slice(0, dot).toLowerCase();
98
+ if (label.includes(".")) {
99
+ throw new ResolveError("invalid-domain", `subdomains are not supported yet (.${suffix}): "${t}"`, input);
100
+ }
101
+ if (!/^[a-z0-9-]+$/.test(label)) {
102
+ throw new ResolveError("invalid-domain", `.${suffix} labels must be ASCII letters, digits or hyphen`, input);
103
+ }
104
+ return label;
105
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ENS adapter — `.eth` names on Ethereum. CONFIG-GATED.
3
+ *
4
+ * Full ENS resolution reads Ethereum mainnet: namehash(name) → the ENS registry's
5
+ * `resolver(node)` → that resolver's `addr(node)`. We do NOT have an Ethereum RPC
6
+ * endpoint configured on this deployment, so this adapter is GATED:
7
+ *
8
+ * - Without `ethRpcUrl` (and a `keccak256` for namehash) it is NOT configured,
9
+ * and `resolve` throws a typed `not-configured` ResolveError. It NEVER
10
+ * fabricates an address — a gated namespace returns "not enabled", never a
11
+ * guess.
12
+ * - With both injected, it performs the real read over JSON-RPC `eth_call`.
13
+ *
14
+ * `keccak256` is injected for the same reason the native path injects curve math
15
+ * via WASM: this SDK does not hand-roll cryptographic primitives. A consumer with
16
+ * an Ethereum stack (ethers/viem/js-sha3) passes their vetted keccak256.
17
+ *
18
+ * The constants below are the well-known ENS MAINNET deployment values. This path
19
+ * has NOT been exercised against a live endpoint here (we have none) — enabling
20
+ * ENS is an owner decision (provision `ETH_RPC_URL`), at which point it must be
21
+ * verified end-to-end before being trusted.
22
+ */
23
+ import type { NamespaceAdapter } from "./universalTypes.js";
24
+ /** ENS registry (mainnet). `resolver(bytes32)` lives here. */
25
+ export declare const ENS_REGISTRY_ADDRESS = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e";
26
+ /** A keccak-256 implementation over bytes → 32 bytes. Injected (not hand-rolled). */
27
+ export type Keccak256 = (data: Uint8Array) => Uint8Array;
28
+ export interface EnsAdapterConfig {
29
+ /** Ethereum mainnet JSON-RPC endpoint. Absent → adapter not configured. */
30
+ readonly ethRpcUrl?: string;
31
+ /** keccak-256 for namehash. Absent → adapter not configured. */
32
+ readonly keccak256?: Keccak256;
33
+ /** Optional fetch override (testing / custom transport). */
34
+ readonly fetchImpl?: typeof fetch;
35
+ /** ENS registry override (defaults to the mainnet registry). */
36
+ readonly registryAddress?: string;
37
+ }
38
+ /**
39
+ * EIP-137 namehash. `node("")` = 32 zero bytes; for each label (right-to-left)
40
+ * `node = keccak256(node ++ keccak256(label))`. Returns 32 bytes.
41
+ */
42
+ export declare function ensNamehash(name: string, keccak256: Keccak256): Uint8Array;
43
+ /** Build the `.eth` (ENS) adapter — gated on `ethRpcUrl` + `keccak256`. */
44
+ export declare function createEnsAdapter(config?: EnsAdapterConfig): NamespaceAdapter;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * ENS adapter — `.eth` names on Ethereum. CONFIG-GATED.
3
+ *
4
+ * Full ENS resolution reads Ethereum mainnet: namehash(name) → the ENS registry's
5
+ * `resolver(node)` → that resolver's `addr(node)`. We do NOT have an Ethereum RPC
6
+ * endpoint configured on this deployment, so this adapter is GATED:
7
+ *
8
+ * - Without `ethRpcUrl` (and a `keccak256` for namehash) it is NOT configured,
9
+ * and `resolve` throws a typed `not-configured` ResolveError. It NEVER
10
+ * fabricates an address — a gated namespace returns "not enabled", never a
11
+ * guess.
12
+ * - With both injected, it performs the real read over JSON-RPC `eth_call`.
13
+ *
14
+ * `keccak256` is injected for the same reason the native path injects curve math
15
+ * via WASM: this SDK does not hand-roll cryptographic primitives. A consumer with
16
+ * an Ethereum stack (ethers/viem/js-sha3) passes their vetted keccak256.
17
+ *
18
+ * The constants below are the well-known ENS MAINNET deployment values. This path
19
+ * has NOT been exercised against a live endpoint here (we have none) — enabling
20
+ * ENS is an owner decision (provision `ETH_RPC_URL`), at which point it must be
21
+ * verified end-to-end before being trusted.
22
+ */
23
+ import { ResolveError } from "./types.js";
24
+ import { externalLabel } from "./universalDetect.js";
25
+ /** ENS registry (mainnet). `resolver(bytes32)` lives here. */
26
+ export const ENS_REGISTRY_ADDRESS = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e";
27
+ /** `resolver(bytes32)` selector = keccak256("resolver(bytes32)")[:4]. */
28
+ const SELECTOR_RESOLVER = "0178b8bf";
29
+ /** `addr(bytes32)` selector = keccak256("addr(bytes32)")[:4]. */
30
+ const SELECTOR_ADDR = "3b3b57de";
31
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
32
+ function toHex(bytes) {
33
+ let s = "";
34
+ for (const b of bytes)
35
+ s += b.toString(16).padStart(2, "0");
36
+ return s;
37
+ }
38
+ /**
39
+ * EIP-137 namehash. `node("")` = 32 zero bytes; for each label (right-to-left)
40
+ * `node = keccak256(node ++ keccak256(label))`. Returns 32 bytes.
41
+ */
42
+ export function ensNamehash(name, keccak256) {
43
+ let node = new Uint8Array(32);
44
+ if (name.length > 0) {
45
+ const labels = name.split(".");
46
+ const enc = new TextEncoder();
47
+ for (let i = labels.length - 1; i >= 0; i--) {
48
+ const labelHash = keccak256(enc.encode(labels[i]));
49
+ const joined = new Uint8Array(64);
50
+ joined.set(node, 0);
51
+ joined.set(labelHash, 32);
52
+ node = keccak256(joined);
53
+ }
54
+ }
55
+ return node;
56
+ }
57
+ /** The 20-byte address in the last 20 bytes of a 32-byte ABI word, as 0x-hex. */
58
+ function addressFromWord(hexWord) {
59
+ const clean = hexWord.startsWith("0x") ? hexWord.slice(2) : hexWord;
60
+ if (clean.length < 64)
61
+ return null;
62
+ const addr = "0x" + clean.slice(24, 64).toLowerCase();
63
+ return addr === ZERO_ADDRESS ? null : addr;
64
+ }
65
+ /** Build the `.eth` (ENS) adapter — gated on `ethRpcUrl` + `keccak256`. */
66
+ export function createEnsAdapter(config = {}) {
67
+ const registry = config.registryAddress ?? ENS_REGISTRY_ADDRESS;
68
+ const configured = Boolean(config.ethRpcUrl && config.keccak256);
69
+ function status() {
70
+ if (configured)
71
+ return { namespace: "eth", source: "ens", configured: true };
72
+ const missing = [];
73
+ if (!config.ethRpcUrl)
74
+ missing.push("ETH_RPC_URL");
75
+ if (!config.keccak256)
76
+ missing.push("a keccak256 implementation");
77
+ return { namespace: "eth", source: "ens", configured: false, reason: `ENS resolution requires ${missing.join(" and ")}` };
78
+ }
79
+ async function ethCall(to, data) {
80
+ const doFetch = config.fetchImpl ?? globalThis.fetch;
81
+ let res;
82
+ try {
83
+ res = await doFetch(config.ethRpcUrl, {
84
+ method: "POST",
85
+ headers: { "content-type": "application/json" },
86
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to, data }, "latest"] }),
87
+ });
88
+ }
89
+ catch (e) {
90
+ throw new ResolveError("rpc-error", `ETH RPC request failed: ${String(e)}`);
91
+ }
92
+ if (!res.ok)
93
+ throw new ResolveError("rpc-error", `ETH RPC returned HTTP ${res.status}`);
94
+ const body = (await res.json());
95
+ if (body.error)
96
+ throw new ResolveError("rpc-error", body.error.message ?? "ETH RPC error");
97
+ return body.result ?? "0x";
98
+ }
99
+ return {
100
+ namespace: "eth",
101
+ source: "ens",
102
+ detect(input) {
103
+ const t = input.trim();
104
+ const dot = t.lastIndexOf(".");
105
+ return dot > 0 && t.slice(dot + 1).toLowerCase() === "eth" && !t.startsWith("@");
106
+ },
107
+ status,
108
+ async resolve(input) {
109
+ const label = externalLabel(input, "eth"); // throws invalid-domain on bad shape
110
+ if (!configured) {
111
+ // Gated: a known namespace we cannot reach — typed, never a fabricated address.
112
+ throw new ResolveError("not-configured", `.eth (ENS) resolution is not enabled on this deployment: ${status().reason}`, input);
113
+ }
114
+ const keccak256 = config.keccak256;
115
+ const name = `${label}.eth`;
116
+ const node = toHex(ensNamehash(name, keccak256));
117
+ // 1) registry.resolver(node)
118
+ const resolverWord = await ethCall(registry, "0x" + SELECTOR_RESOLVER + node);
119
+ const resolverAddr = addressFromWord(resolverWord);
120
+ if (!resolverAddr)
121
+ throw new ResolveError("not-found", `${name} has no resolver set`, input);
122
+ // 2) resolver.addr(node)
123
+ const addrWord = await ethCall(resolverAddr, "0x" + SELECTOR_ADDR + node);
124
+ const address = addressFromWord(addrWord);
125
+ if (!address)
126
+ throw new ResolveError("no-record-for-chain", `${name} has no ETH address record`, input);
127
+ return {
128
+ input,
129
+ name,
130
+ namespace: "eth",
131
+ address,
132
+ chain: "ETH",
133
+ source: "ens",
134
+ verification: "unverified",
135
+ };
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Native adapter: `@handle` and X1NS (`.x1` / `.xnt` / `.xen`).
3
+ *
4
+ * This is a thin wrapper over the EXISTING on-chain `Resolver` (`createResolver`
5
+ * in index.ts) — it does not reimplement any resolution. The native path is the
6
+ * primary and stays entirely unchanged; this adapter only maps the native
7
+ * `Resolved` onto the universal `{..., source}` shape so it sits alongside the
8
+ * external adapters uniformly.
9
+ */
10
+ import type { Resolver } from "./index.js";
11
+ import type { NamespaceAdapter } from "./universalTypes.js";
12
+ /** Build the native adapter from an already-constructed `Resolver`. */
13
+ export declare function createNativeAdapter(resolver: Resolver): NamespaceAdapter;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Native adapter: `@handle` and X1NS (`.x1` / `.xnt` / `.xen`).
3
+ *
4
+ * This is a thin wrapper over the EXISTING on-chain `Resolver` (`createResolver`
5
+ * in index.ts) — it does not reimplement any resolution. The native path is the
6
+ * primary and stays entirely unchanged; this adapter only maps the native
7
+ * `Resolved` onto the universal `{..., source}` shape so it sits alongside the
8
+ * external adapters uniformly.
9
+ */
10
+ import { detectNamespace } from "./universalDetect.js";
11
+ import { NATIVE_NAMESPACES } from "./universalTypes.js";
12
+ /** Build the native adapter from an already-constructed `Resolver`. */
13
+ export function createNativeAdapter(resolver) {
14
+ return {
15
+ namespace: NATIVE_NAMESPACES,
16
+ source: "x1id",
17
+ detect(input) {
18
+ try {
19
+ return NATIVE_NAMESPACES.includes(detectNamespace(input).namespace);
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ },
25
+ status() {
26
+ // The native resolver is always configured — it reads X1 over the RPC the
27
+ // resolver was built with (required at construction).
28
+ return { namespace: NATIVE_NAMESPACES, source: "x1id", configured: true };
29
+ },
30
+ async resolve(input, opts) {
31
+ const res = await resolver.resolve(input, { chain: opts?.chain ?? "X1" });
32
+ return {
33
+ input: res.input,
34
+ name: res.name,
35
+ namespace: res.namespace,
36
+ address: res.address,
37
+ chain: res.chain,
38
+ source: "x1id",
39
+ verification: res.verification,
40
+ };
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * SNS adapter — `.sol` domains, read directly from Solana mainnet.
3
+ *
4
+ * `.sol` is built on the SPL Name Service program, the same primitive X1NS uses,
5
+ * so the registry header is identical (parentName(32) | owner(32) | class(32),
6
+ * then data at offset 96). The differences from the native X1NS read are the
7
+ * derivation constants and that the name-account PDA needs `find_program_address`
8
+ * — which this SDK deliberately does not hand-roll (it needs an ed25519 on-curve
9
+ * check; see wasm.ts). Derivation is therefore an INJECTED dependency: pass your
10
+ * own {@link SnsDeriver}, or build the default via {@link makeSnsDeriver} (which
11
+ * uses `@solana/web3.js`).
12
+ *
13
+ * Every constant and the header layout were verified BOTH against live Solana
14
+ * mainnet accounts (bonfida.sol, toly.sol — 2026-10-02) AND against the SNS-SDK
15
+ * source (github.com/SolanaNameService/sns-sdk `js/src/constants.ts`,
16
+ * `state.ts`, `nft/const.ts`, `nft/getDomainMint.ts`). Not copied from an
17
+ * adjacent API surface.
18
+ *
19
+ * bonfida.sol → Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb
20
+ * owning program = namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX
21
+ * parent = 58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx (the .sol TLD)
22
+ * owner = Fw1ETanDZafof7xEULsnq9UY6o71Tpds89tNwPkWLb1v
23
+ *
24
+ * # Scope of this increment (honest limits)
25
+ *
26
+ * We resolve the domain's CURRENT OWNER: the registry `owner` field, OR — when
27
+ * the domain is wrapped as an NFT — the NFT holder (the registry `owner` of a
28
+ * wrapped domain is a tokenizer ESCROW PDA, never the human, so returning it
29
+ * would be a wrong address). We do NOT yet honor SOL payment records (SNS-IP-1
30
+ * V1 / SNS-IP-3 V2), which can redirect a domain's receive address and carry
31
+ * their own staleness/signature rules — a documented follow-up. We therefore
32
+ * return ownership, marked `unverified`.
33
+ *
34
+ * # SRS migration watch
35
+ *
36
+ * The SNS-SDK `master` ships a new "SRS" (Solana Record Service) resolver under
37
+ * program `srsWjm76StJucL7atFyPSdXFaVLNPFqEt1uFEDPrZsn`, currently gated OFF
38
+ * (`SOL_SRS_RESOLUTION_ENABLED = false`, cutoff ~slot 452_825_395, est.
39
+ * 2026-10-15). Until that flips, the classic NameRegistry path below is the live
40
+ * resolution path. Revisit after the cutover.
41
+ */
42
+ import type { NamespaceAdapter } from "./universalTypes.js";
43
+ /** SPL Name Service program on Solana mainnet (owns every `.sol` registry account). */
44
+ export declare const SPL_NAME_PROGRAM_ID = "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX";
45
+ /** The `.sol` TLD authority — the `nameParent` every root `.sol` domain derives under. */
46
+ export declare const SOL_TLD_AUTHORITY = "58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx";
47
+ /** Prefix hashed with the label to form the SPL Name Service seed. */
48
+ export declare const SNS_HASH_PREFIX = "SPL Name Service";
49
+ /** SNS Name Tokenizer program — wraps a domain into a Metaplex NFT. */
50
+ export declare const NAME_TOKENIZER_ID = "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk";
51
+ /** Tokenizer mint-PDA seed prefix (`["tokenized_name", nameAccount]`). */
52
+ export declare const SNS_MINT_PREFIX = "tokenized_name";
53
+ /**
54
+ * The two `find_program_address` derivations SNS resolution needs. Injected so
55
+ * the SDK core stays free of curve math (and of a hard `@solana/web3.js`
56
+ * dependency), and so tests can supply deterministic fakes.
57
+ */
58
+ export interface SnsDeriver {
59
+ /** Name-account address for a bare `.sol` label (no suffix), base58. */
60
+ domainKey(label: string): Promise<string> | string;
61
+ /** Tokenizer mint address for a name-account address, base58. */
62
+ mintKey(nameAccount: string): Promise<string> | string;
63
+ }
64
+ /**
65
+ * `hashed_name = sha256(HASH_PREFIX + label)` — the SPL Name Service name hash.
66
+ * Pure (Web Crypto SHA-256, available in Node 18+ and browsers); no curve math,
67
+ * so it lives in the SDK and is reused by a deriver as the first PDA seed.
68
+ */
69
+ export declare function snsHashedName(label: string): Promise<Uint8Array>;
70
+ /**
71
+ * Build the default SNS deriver using `@solana/web3.js`.
72
+ *
73
+ * Seeds (verified live + against the SNS-SDK source):
74
+ * domainKey: [ sha256("SPL Name Service"+label), 32 zero bytes (no class),
75
+ * SOL_TLD(32) ] under the SPL Name Service program.
76
+ * mintKey: [ "tokenized_name", nameAccount(32) ] under the Name Tokenizer.
77
+ */
78
+ export declare function makeSnsDeriver(web3Module?: unknown): Promise<SnsDeriver>;
79
+ export interface SnsAdapterConfig {
80
+ /**
81
+ * Solana mainnet JSON-RPC endpoint. When absent the adapter is NOT configured
82
+ * and `resolve` throws `not-configured` — it never falls back to a fabricated
83
+ * address or silently to a public endpoint.
84
+ */
85
+ readonly rpcUrl?: string;
86
+ /** Optional fetch override (testing / custom transport). */
87
+ readonly fetchImpl?: typeof fetch;
88
+ /**
89
+ * The two PDA derivations. Required to resolve; absent → not configured. Build
90
+ * the default with {@link makeSnsDeriver}, or inject a mock in tests.
91
+ */
92
+ readonly deriver?: SnsDeriver;
93
+ }
94
+ /** Build the `.sol` (SNS) adapter. */
95
+ export declare function createSnsAdapter(config?: SnsAdapterConfig): NamespaceAdapter;