@x1id/resolve 0.11.0 → 0.12.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.
- package/README.md +74 -5
- package/dist/adminConfig.d.ts +63 -0
- package/dist/adminConfig.js +113 -0
- package/dist/adminMarket.d.ts +67 -0
- package/dist/adminMarket.js +134 -0
- package/dist/attestation.d.ts +103 -1
- package/dist/attestation.js +124 -0
- package/dist/gateClient.d.ts +98 -0
- package/dist/gateClient.js +118 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +137 -2
- package/dist/namespaceOverride.d.ts +123 -0
- package/dist/namespaceOverride.js +204 -0
- package/dist/register.d.ts +30 -0
- package/dist/register.js +37 -0
- package/dist/scoped.d.ts +84 -0
- package/dist/scoped.js +126 -0
- package/dist/types.d.ts +23 -4
- package/dist/types.js +2 -1
- package/dist/universal.d.ts +58 -0
- package/dist/universal.js +67 -0
- package/dist/universalDetect.d.ts +44 -0
- package/dist/universalDetect.js +105 -0
- package/dist/universalEns.d.ts +44 -0
- package/dist/universalEns.js +138 -0
- package/dist/universalNative.d.ts +13 -0
- package/dist/universalNative.js +43 -0
- package/dist/universalSns.d.ts +95 -0
- package/dist/universalSns.js +223 -0
- package/dist/universalTypes.d.ts +117 -0
- package/dist/universalTypes.js +24 -0
- package/dist/wasm.d.ts +17 -0
- package/dist/wasm.js +38 -0
- package/package.json +1 -1
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/dist/types.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Display label for a namespace. **Must be shown** next to any resolved
|
|
3
|
-
* address — see the module docs on why this is not optional.
|
|
3
|
+
* address — see the module docs on why this is not optional. A scoped TLD label
|
|
4
|
+
* renders as `.<label>` (e.g. `.testtld`), the same as an X1NS suffix.
|
|
4
5
|
*/
|
|
5
6
|
export function namespaceLabel(ns) {
|
|
6
7
|
return ns === "handle" ? "@handle" : `.${ns}`;
|
|
@@ -0,0 +1,58 @@
|
|
|
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 type { Resolver } from "./index.js";
|
|
28
|
+
import { type SnsAdapterConfig } from "./universalSns.js";
|
|
29
|
+
import { type EnsAdapterConfig } from "./universalEns.js";
|
|
30
|
+
import type { AdapterStatus, UniversalNamespace, UniversalResolveOptions, UniversalResolved } from "./universalTypes.js";
|
|
31
|
+
export * from "./universalTypes.js";
|
|
32
|
+
export { detectNamespace, isUniversalName, sourceForNamespace, externalLabel, type DetectedNamespace, } from "./universalDetect.js";
|
|
33
|
+
export { createNativeAdapter } from "./universalNative.js";
|
|
34
|
+
export { createSnsAdapter, makeSnsDeriver, snsHashedName, SPL_NAME_PROGRAM_ID, SOL_TLD_AUTHORITY, SNS_HASH_PREFIX, NAME_TOKENIZER_ID, SNS_MINT_PREFIX, type SnsAdapterConfig, type SnsDeriver, } from "./universalSns.js";
|
|
35
|
+
export { createEnsAdapter, ensNamehash, ENS_REGISTRY_ADDRESS, type EnsAdapterConfig, type Keccak256, } from "./universalEns.js";
|
|
36
|
+
export interface UniversalResolverConfig {
|
|
37
|
+
/** The native `@handle` / X1NS resolver (from `createResolver`). Required. */
|
|
38
|
+
readonly native: Resolver;
|
|
39
|
+
/** SNS (`.sol`) adapter config. Omit (or leave RPC/deriver unset) to gate it off. */
|
|
40
|
+
readonly sns?: SnsAdapterConfig;
|
|
41
|
+
/** ENS (`.eth`) adapter config. Omit (or leave ETH_RPC_URL unset) to gate it off. */
|
|
42
|
+
readonly ens?: EnsAdapterConfig;
|
|
43
|
+
}
|
|
44
|
+
export interface NamespaceResolver {
|
|
45
|
+
/**
|
|
46
|
+
* Resolve any supported name to a normalized result.
|
|
47
|
+
* @throws {ResolveError} with a `code` a UI can branch on — `unrecognized`,
|
|
48
|
+
* `ambiguous`, `invalid-domain`, `not-found`, `rpc-error`, or
|
|
49
|
+
* `not-configured` for a known-but-disabled namespace.
|
|
50
|
+
*/
|
|
51
|
+
resolve(input: string, opts?: UniversalResolveOptions): Promise<UniversalResolved>;
|
|
52
|
+
/** Classify `input` into a namespace by shape, or null if unrecognized. */
|
|
53
|
+
detect(input: string): UniversalNamespace | null;
|
|
54
|
+
/** Per-source adapter status (which namespaces are live vs config-gated). */
|
|
55
|
+
status(): readonly AdapterStatus[];
|
|
56
|
+
}
|
|
57
|
+
/** Build the universal resolver over a native resolver plus optional external adapters. */
|
|
58
|
+
export declare function createUniversalResolver(config: UniversalResolverConfig): NamespaceResolver;
|
|
@@ -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;
|