@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
|
@@ -0,0 +1,223 @@
|
|
|
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 { ResolveError } from "./types.js";
|
|
43
|
+
import { encodeBase58, decodeBase58 } from "./base58.js";
|
|
44
|
+
import { makeAccountReader, readU64, bytesEqual, SPL_TOKEN_PROGRAM, TOKEN_ACCOUNT_MIN_LEN, } from "./accounts.js";
|
|
45
|
+
import { externalLabel } from "./universalDetect.js";
|
|
46
|
+
/** SPL Name Service program on Solana mainnet (owns every `.sol` registry account). */
|
|
47
|
+
export const SPL_NAME_PROGRAM_ID = "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX";
|
|
48
|
+
/** The `.sol` TLD authority — the `nameParent` every root `.sol` domain derives under. */
|
|
49
|
+
export const SOL_TLD_AUTHORITY = "58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx";
|
|
50
|
+
/** Prefix hashed with the label to form the SPL Name Service seed. */
|
|
51
|
+
export const SNS_HASH_PREFIX = "SPL Name Service";
|
|
52
|
+
/** SNS Name Tokenizer program — wraps a domain into a Metaplex NFT. */
|
|
53
|
+
export const NAME_TOKENIZER_ID = "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk";
|
|
54
|
+
/** Tokenizer mint-PDA seed prefix (`["tokenized_name", nameAccount]`). */
|
|
55
|
+
export const SNS_MINT_PREFIX = "tokenized_name";
|
|
56
|
+
/** SPL Name Service header: parentName(32) | owner(32) | class(32) = 96 bytes, then data. */
|
|
57
|
+
const SPL_NAME_HEADER_LEN = 96;
|
|
58
|
+
/**
|
|
59
|
+
* `hashed_name = sha256(HASH_PREFIX + label)` — the SPL Name Service name hash.
|
|
60
|
+
* Pure (Web Crypto SHA-256, available in Node 18+ and browsers); no curve math,
|
|
61
|
+
* so it lives in the SDK and is reused by a deriver as the first PDA seed.
|
|
62
|
+
*/
|
|
63
|
+
export async function snsHashedName(label) {
|
|
64
|
+
const subtle = globalThis.crypto?.subtle;
|
|
65
|
+
if (!subtle) {
|
|
66
|
+
throw new ResolveError("not-configured", "Web Crypto (crypto.subtle) is unavailable for SNS name hashing");
|
|
67
|
+
}
|
|
68
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(SNS_HASH_PREFIX + label));
|
|
69
|
+
return new Uint8Array(digest);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the default SNS deriver using `@solana/web3.js`.
|
|
73
|
+
*
|
|
74
|
+
* Seeds (verified live + against the SNS-SDK source):
|
|
75
|
+
* domainKey: [ sha256("SPL Name Service"+label), 32 zero bytes (no class),
|
|
76
|
+
* SOL_TLD(32) ] under the SPL Name Service program.
|
|
77
|
+
* mintKey: [ "tokenized_name", nameAccount(32) ] under the Name Tokenizer.
|
|
78
|
+
*/
|
|
79
|
+
export async function makeSnsDeriver(web3Module) {
|
|
80
|
+
// Prefer an INJECTED `@solana/web3.js` module — the robust path when this
|
|
81
|
+
// package is linked (`file:`/`npm link`), where Node resolves a bare specifier
|
|
82
|
+
// from THIS package's realpath, not the consumer's node_modules, so the
|
|
83
|
+
// consumer that owns web3.js (e.g. the dev-api) passes it in. Fall back to a
|
|
84
|
+
// dynamic import (variable specifier, so tsc never tries to resolve it) for a
|
|
85
|
+
// normal `npm i @x1id/resolve @solana/web3.js` install.
|
|
86
|
+
let web3;
|
|
87
|
+
if (web3Module) {
|
|
88
|
+
web3 = web3Module;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const spec = "@solana/web3.js";
|
|
92
|
+
try {
|
|
93
|
+
web3 = (await import(spec));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new ResolveError("not-configured", "SNS resolution needs a deriver: install @solana/web3.js, pass the web3 module to makeSnsDeriver, or inject `deriver` in the SNS adapter config");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const program = new web3.PublicKey(SPL_NAME_PROGRAM_ID);
|
|
100
|
+
const tld = new web3.PublicKey(SOL_TLD_AUTHORITY).toBytes();
|
|
101
|
+
const tokenizer = new web3.PublicKey(NAME_TOKENIZER_ID);
|
|
102
|
+
const prefix = new TextEncoder().encode(SNS_MINT_PREFIX);
|
|
103
|
+
const zeroClass = new Uint8Array(32);
|
|
104
|
+
return {
|
|
105
|
+
async domainKey(label) {
|
|
106
|
+
const hashed = await snsHashedName(label);
|
|
107
|
+
const [key] = web3.PublicKey.findProgramAddressSync([hashed, zeroClass, tld], program);
|
|
108
|
+
return key.toBase58();
|
|
109
|
+
},
|
|
110
|
+
mintKey(nameAccount) {
|
|
111
|
+
const [key] = web3.PublicKey.findProgramAddressSync([prefix, new web3.PublicKey(nameAccount).toBytes()], tokenizer);
|
|
112
|
+
return key.toBase58();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Build the `.sol` (SNS) adapter. */
|
|
117
|
+
export function createSnsAdapter(config = {}) {
|
|
118
|
+
const reader = config.rpcUrl
|
|
119
|
+
? makeAccountReader(config.rpcUrl, config.fetchImpl)
|
|
120
|
+
: null;
|
|
121
|
+
const deriver = config.deriver ?? null;
|
|
122
|
+
function status() {
|
|
123
|
+
if (!reader) {
|
|
124
|
+
return { namespace: "sol", source: "sns", configured: false, reason: "no Solana mainnet RPC configured (SOLANA_RPC_URL)" };
|
|
125
|
+
}
|
|
126
|
+
if (!deriver) {
|
|
127
|
+
return { namespace: "sol", source: "sns", configured: false, reason: "no SNS deriver configured (install @solana/web3.js or inject `deriver`)" };
|
|
128
|
+
}
|
|
129
|
+
return { namespace: "sol", source: "sns", configured: true };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The NFT holder of a wrapped domain, or null when the domain is not
|
|
133
|
+
* tokenized. Mirrors the SNS-SDK `retrieveNftOwnerV2`: derive the tokenizer
|
|
134
|
+
* mint for the name account, take its unique (`amount === 1`) token account,
|
|
135
|
+
* return that account's SPL-Token owner.
|
|
136
|
+
*/
|
|
137
|
+
async function nftHolder(rdr, drv, nameAccount) {
|
|
138
|
+
const mint = await drv.mintKey(nameAccount);
|
|
139
|
+
// A `.sol` domain is wrapped only if its tokenizer mint account EXISTS. The
|
|
140
|
+
// common case is an un-wrapped domain, whose derived mint PDA has never been
|
|
141
|
+
// created — and `getTokenLargestAccounts` on a non-existent account is an RPC
|
|
142
|
+
// *error* ("could not find account"), NOT an empty result. Letting that error
|
|
143
|
+
// propagate surfaced as a 502 for ordinary `.sol` names. Probe the mint first:
|
|
144
|
+
// absent (or not an SPL-Token mint) → the domain is not tokenized, so fall
|
|
145
|
+
// back to the registry owner; present → it is wrapped, read the NFT holder and
|
|
146
|
+
// let a genuine RPC failure there remain an rpc-error.
|
|
147
|
+
const mintInfo = await rdr.accountInfo(mint);
|
|
148
|
+
if (!mintInfo || mintInfo.owner !== SPL_TOKEN_PROGRAM)
|
|
149
|
+
return null;
|
|
150
|
+
const largest = (await rdr.rpc("getTokenLargestAccounts", [mint, { commitment: "confirmed" }]));
|
|
151
|
+
const holders = (largest?.value ?? []).filter((a) => a.amount === "1");
|
|
152
|
+
if (holders.length !== 1)
|
|
153
|
+
return null; // not tokenized / no unique holder
|
|
154
|
+
const t = await rdr.accountInfo(holders[0].address);
|
|
155
|
+
if (!t ||
|
|
156
|
+
t.owner !== SPL_TOKEN_PROGRAM ||
|
|
157
|
+
t.data.length < TOKEN_ACCOUNT_MIN_LEN ||
|
|
158
|
+
readU64(t.data, 64) !== 1n) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const mintBytes = decodeBase58(mint);
|
|
162
|
+
if (!mintBytes || !bytesEqual(t.data.slice(0, 32), mintBytes))
|
|
163
|
+
return null;
|
|
164
|
+
return encodeBase58(t.data.slice(32, 64));
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
namespace: "sol",
|
|
168
|
+
source: "sns",
|
|
169
|
+
detect(input) {
|
|
170
|
+
const t = input.trim();
|
|
171
|
+
const dot = t.lastIndexOf(".");
|
|
172
|
+
return dot > 0 && t.slice(dot + 1).toLowerCase() === "sol" && !t.startsWith("@");
|
|
173
|
+
},
|
|
174
|
+
status,
|
|
175
|
+
async resolve(input) {
|
|
176
|
+
const label = externalLabel(input, "sol"); // throws invalid-domain on bad shape
|
|
177
|
+
if (!reader || !deriver) {
|
|
178
|
+
throw new ResolveError("not-configured", `.sol (SNS) resolution is not enabled on this deployment: ${status().reason}`, input);
|
|
179
|
+
}
|
|
180
|
+
let accountKey;
|
|
181
|
+
try {
|
|
182
|
+
accountKey = await deriver.domainKey(label);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
if (e instanceof ResolveError)
|
|
186
|
+
throw e;
|
|
187
|
+
throw new ResolveError("rpc-error", `SNS derivation failed: ${String(e)}`, input);
|
|
188
|
+
}
|
|
189
|
+
const acct = await reader.accountInfo(accountKey);
|
|
190
|
+
if (!acct) {
|
|
191
|
+
throw new ResolveError("not-found", `${label}.sol is not registered`, input);
|
|
192
|
+
}
|
|
193
|
+
// Must be owned by the SPL Name Service program — anyone can fund an
|
|
194
|
+
// address into existence; its bytes are not a registry record.
|
|
195
|
+
if (acct.owner !== SPL_NAME_PROGRAM_ID) {
|
|
196
|
+
throw new ResolveError("not-found", `${label}.sol is not an SNS registry account`, input);
|
|
197
|
+
}
|
|
198
|
+
if (acct.data.length < SPL_NAME_HEADER_LEN) {
|
|
199
|
+
throw new ResolveError("rpc-error", `${label}.sol returned a malformed account`, input);
|
|
200
|
+
}
|
|
201
|
+
// Verify the record sits under the .sol TLD — without this an arbitrary SPL
|
|
202
|
+
// name account could be presented and its bytes read as an owner.
|
|
203
|
+
if (encodeBase58(acct.data.slice(0, 32)) !== SOL_TLD_AUTHORITY) {
|
|
204
|
+
throw new ResolveError("not-found", `${label}.sol is not a .sol domain`, input);
|
|
205
|
+
}
|
|
206
|
+
// Wrapped domains: the registry `owner` is a tokenizer escrow — resolve the
|
|
207
|
+
// NFT holder instead, and NEVER return the escrow.
|
|
208
|
+
const holder = await nftHolder(reader, deriver, accountKey);
|
|
209
|
+
const address = holder ?? encodeBase58(acct.data.slice(32, 64));
|
|
210
|
+
return {
|
|
211
|
+
input,
|
|
212
|
+
name: `${label}.sol`,
|
|
213
|
+
namespace: "sol",
|
|
214
|
+
address,
|
|
215
|
+
chain: "SOL",
|
|
216
|
+
source: "sns",
|
|
217
|
+
// Ownership (registry owner or NFT holder), not a per-chain payment
|
|
218
|
+
// proof and not yet SOL-record-aware — surfaced conservatively.
|
|
219
|
+
verification: "unverified",
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The X1ID Universal Resolver — shared types.
|
|
3
|
+
*
|
|
4
|
+
* X1ID becomes the aggregator: one call resolves a NATIVE `@handle` / X1NS name
|
|
5
|
+
* AND an external namespace (`.sol` via SNS on Solana, `.eth` via ENS on
|
|
6
|
+
* Ethereum) by reading each name's OWN chain. Every external namespace is read
|
|
7
|
+
* directly from its chain over RPC — never through a third party's resolver API,
|
|
8
|
+
* exactly like the native path's "reads chain, not an API" rule.
|
|
9
|
+
*
|
|
10
|
+
* # The one rule still holds
|
|
11
|
+
*
|
|
12
|
+
* `@jack`, `jack.x1`, `jack.sol` and `jack.eth` are FOUR different names with
|
|
13
|
+
* four potentially different owners on three different chains. The universal
|
|
14
|
+
* resolver classifies by SHAPE and dispatches to exactly one adapter — it never
|
|
15
|
+
* tries one namespace and falls back to another, and every result carries both
|
|
16
|
+
* its `namespace` and the `source` system that answered.
|
|
17
|
+
*/
|
|
18
|
+
import type { Chain, Verification } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* The FIXED namespaces the universal resolver classifies by a known suffix. The
|
|
21
|
+
* first four are native (resolved by `@x1id/resolve`'s on-chain resolver);
|
|
22
|
+
* `sol` and `eth` are external adapters. Scoped customer-TLD labels are NOT
|
|
23
|
+
* here — see {@link UniversalNamespace}.
|
|
24
|
+
*/
|
|
25
|
+
export type FixedNamespace = "handle" | "x1" | "xnt" | "xen" | "sol" | "eth";
|
|
26
|
+
/**
|
|
27
|
+
* Every namespace a universal result can carry. The fixed six above, PLUS — via
|
|
28
|
+
* `(string & {})` — an arbitrary SCOPED customer-TLD label (`"testtld"`,
|
|
29
|
+
* #8484/#8485), which resolves through the native (`source: "x1id"`) path with
|
|
30
|
+
* `namespace` set to the `.tld`. Literal autocomplete for the fixed six is
|
|
31
|
+
* preserved; a value that is none of them is a scoped TLD label.
|
|
32
|
+
*/
|
|
33
|
+
export type UniversalNamespace = FixedNamespace | (string & {});
|
|
34
|
+
/** Native X1 namespaces — resolved by the bundled `createResolver`. */
|
|
35
|
+
export declare const NATIVE_NAMESPACES: readonly UniversalNamespace[];
|
|
36
|
+
/**
|
|
37
|
+
* Which naming SYSTEM produced a result. Orthogonal to `namespace`: `x1`, `xnt`
|
|
38
|
+
* and `xen` all come from `source: "x1id"`. Carried so an integrator can show
|
|
39
|
+
* "resolved via SNS" / "resolved via ENS" and reason about trust per system.
|
|
40
|
+
*/
|
|
41
|
+
export type NamespaceSource = "x1id" | "sns" | "ens";
|
|
42
|
+
/**
|
|
43
|
+
* A normalized, cross-namespace resolution result. Deliberately the same shape
|
|
44
|
+
* regardless of which chain answered, so a recipient field treats every
|
|
45
|
+
* namespace uniformly — plus `source` and `verification` so it can label and
|
|
46
|
+
* warn.
|
|
47
|
+
*/
|
|
48
|
+
export interface UniversalResolved {
|
|
49
|
+
/** Exactly what the user typed, for display. */
|
|
50
|
+
readonly input: string;
|
|
51
|
+
/** Canonical form of the name (e.g. `"jack"`, `"jack.x1"`, `"jack.sol"`). */
|
|
52
|
+
readonly name: string;
|
|
53
|
+
/** Which naming system matched. Render this before allowing a send. */
|
|
54
|
+
readonly namespace: UniversalNamespace;
|
|
55
|
+
/** The address to send to — base58 for X1/SOL, 0x-hex for ETH. */
|
|
56
|
+
readonly address: string;
|
|
57
|
+
/** Chain `address` belongs to. */
|
|
58
|
+
readonly chain: Chain;
|
|
59
|
+
/** The system that answered (`x1id` | `sns` | `ens`). */
|
|
60
|
+
readonly source: NamespaceSource;
|
|
61
|
+
/**
|
|
62
|
+
* Whether ownership of `address` was PROVED. External namespaces that only
|
|
63
|
+
* expose a registry-owner (SNS's registry `owner`, ENS's `addr` record) come
|
|
64
|
+
* back `unverified` — the address controls/claims the name, which is not the
|
|
65
|
+
* same as a per-chain ownership proof.
|
|
66
|
+
*/
|
|
67
|
+
readonly verification: Verification;
|
|
68
|
+
/**
|
|
69
|
+
* Optional per-chain / text records, when an adapter supplies them. Omitted in
|
|
70
|
+
* the first increment for external namespaces (SNS Records V2 / ENS text
|
|
71
|
+
* records are a follow-up); the field exists so adding them later is additive.
|
|
72
|
+
*/
|
|
73
|
+
readonly records?: readonly UniversalRecord[];
|
|
74
|
+
}
|
|
75
|
+
/** A single auxiliary record an adapter chose to surface alongside the address. */
|
|
76
|
+
export interface UniversalRecord {
|
|
77
|
+
/** Record key — a chain symbol (`"ETH"`), coin type, or text key. */
|
|
78
|
+
readonly key: string;
|
|
79
|
+
/** Record value as stored (address or free text). */
|
|
80
|
+
readonly value: string;
|
|
81
|
+
}
|
|
82
|
+
/** Whether an adapter can actually reach its chain on this deployment. */
|
|
83
|
+
export interface AdapterStatus {
|
|
84
|
+
readonly namespace: UniversalNamespace | readonly UniversalNamespace[];
|
|
85
|
+
readonly source: NamespaceSource;
|
|
86
|
+
readonly configured: boolean;
|
|
87
|
+
/** Human-readable reason when `configured` is false (what env/dep is missing). */
|
|
88
|
+
readonly reason?: string;
|
|
89
|
+
}
|
|
90
|
+
/** Optional per-call options. `chain` applies only to the native namespaces —
|
|
91
|
+
* SNS always answers on SOL and ENS on ETH. */
|
|
92
|
+
export interface UniversalResolveOptions {
|
|
93
|
+
readonly chain?: Chain;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A pluggable resolver for one namespace family. Adapters are modular and
|
|
97
|
+
* independently testable: each owns its own detection, chain read and
|
|
98
|
+
* normalization, and reports whether it is configured.
|
|
99
|
+
*/
|
|
100
|
+
export interface NamespaceAdapter {
|
|
101
|
+
/** The namespace(s) this adapter answers for. */
|
|
102
|
+
readonly namespace: UniversalNamespace | readonly UniversalNamespace[];
|
|
103
|
+
/** The system this adapter represents. */
|
|
104
|
+
readonly source: NamespaceSource;
|
|
105
|
+
/** True iff `input` is shaped for THIS adapter (suffix/prefix classification,
|
|
106
|
+
* never a speculative resolve). */
|
|
107
|
+
detect(input: string): boolean;
|
|
108
|
+
/** Whether this adapter can reach its chain (RPC + any crypto dep present). */
|
|
109
|
+
status(): AdapterStatus;
|
|
110
|
+
/**
|
|
111
|
+
* Resolve `input` to a normalized result.
|
|
112
|
+
* @throws {ResolveError} `not-configured` when `status().configured` is false;
|
|
113
|
+
* `not-found` / `rpc-error` / shape codes exactly like the native resolver.
|
|
114
|
+
* NEVER returns a fabricated address.
|
|
115
|
+
*/
|
|
116
|
+
resolve(input: string, opts?: UniversalResolveOptions): Promise<UniversalResolved>;
|
|
117
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The X1ID Universal Resolver — shared types.
|
|
3
|
+
*
|
|
4
|
+
* X1ID becomes the aggregator: one call resolves a NATIVE `@handle` / X1NS name
|
|
5
|
+
* AND an external namespace (`.sol` via SNS on Solana, `.eth` via ENS on
|
|
6
|
+
* Ethereum) by reading each name's OWN chain. Every external namespace is read
|
|
7
|
+
* directly from its chain over RPC — never through a third party's resolver API,
|
|
8
|
+
* exactly like the native path's "reads chain, not an API" rule.
|
|
9
|
+
*
|
|
10
|
+
* # The one rule still holds
|
|
11
|
+
*
|
|
12
|
+
* `@jack`, `jack.x1`, `jack.sol` and `jack.eth` are FOUR different names with
|
|
13
|
+
* four potentially different owners on three different chains. The universal
|
|
14
|
+
* resolver classifies by SHAPE and dispatches to exactly one adapter — it never
|
|
15
|
+
* tries one namespace and falls back to another, and every result carries both
|
|
16
|
+
* its `namespace` and the `source` system that answered.
|
|
17
|
+
*/
|
|
18
|
+
/** Native X1 namespaces — resolved by the bundled `createResolver`. */
|
|
19
|
+
export const NATIVE_NAMESPACES = Object.freeze([
|
|
20
|
+
"handle",
|
|
21
|
+
"x1",
|
|
22
|
+
"xnt",
|
|
23
|
+
"xen",
|
|
24
|
+
]);
|
package/dist/wasm.d.ts
CHANGED
|
@@ -78,5 +78,22 @@ export declare class WasmResolver {
|
|
|
78
78
|
* Needed on BOTH sides of a `ProgrammableNonFungible` transfer.
|
|
79
79
|
*/
|
|
80
80
|
deriveTokenRecordAccount(mint: Uint8Array, token: Uint8Array, programId: Uint8Array): Uint8Array | null;
|
|
81
|
+
/**
|
|
82
|
+
* Derive the per-handle `NamespaceOverride` PDA under the registry program id
|
|
83
|
+
* — seeds `["namespace_override", handle, label]`, matching the on-chain
|
|
84
|
+
* `CreateNamespaceOverride` account. `handle` is the 32-byte handle PDA
|
|
85
|
+
* (from {@link deriveHandleAccount}); `label` is the namespace label (e.g.
|
|
86
|
+
* "xnt"), 1..=16 bytes. Returns the 32-byte override account, or null.
|
|
87
|
+
*
|
|
88
|
+
* NOTE: overrides are built but not yet honored by `resolve()` — this derive
|
|
89
|
+
* is for the write builders (namespaceOverride.ts) and the gated read path.
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* Derive a `Namespace` directory PDA under the registry program id — seeds
|
|
93
|
+
* `["namespace", label]`. Needed to pass the namespace account into a
|
|
94
|
+
* create-override instruction. `label` is 1..=16 bytes. Returns 32 bytes or null.
|
|
95
|
+
*/
|
|
96
|
+
deriveNamespaceAccount(label: string, programId: Uint8Array): Uint8Array | null;
|
|
97
|
+
deriveNamespaceOverrideAccount(handle: Uint8Array, label: string, programId: Uint8Array): Uint8Array | null;
|
|
81
98
|
}
|
|
82
99
|
export {};
|
package/dist/wasm.js
CHANGED
|
@@ -146,4 +146,42 @@ export class WasmResolver {
|
|
|
146
146
|
const n = this.#write(joined);
|
|
147
147
|
return this.#x.derive_token_record_account(n) === 1 ? this.#read() : null;
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Derive the per-handle `NamespaceOverride` PDA under the registry program id
|
|
151
|
+
* — seeds `["namespace_override", handle, label]`, matching the on-chain
|
|
152
|
+
* `CreateNamespaceOverride` account. `handle` is the 32-byte handle PDA
|
|
153
|
+
* (from {@link deriveHandleAccount}); `label` is the namespace label (e.g.
|
|
154
|
+
* "xnt"), 1..=16 bytes. Returns the 32-byte override account, or null.
|
|
155
|
+
*
|
|
156
|
+
* NOTE: overrides are built but not yet honored by `resolve()` — this derive
|
|
157
|
+
* is for the write builders (namespaceOverride.ts) and the gated read path.
|
|
158
|
+
*/
|
|
159
|
+
/**
|
|
160
|
+
* Derive a `Namespace` directory PDA under the registry program id — seeds
|
|
161
|
+
* `["namespace", label]`. Needed to pass the namespace account into a
|
|
162
|
+
* create-override instruction. `label` is 1..=16 bytes. Returns 32 bytes or null.
|
|
163
|
+
*/
|
|
164
|
+
deriveNamespaceAccount(label, programId) {
|
|
165
|
+
if (programId.length !== 32)
|
|
166
|
+
return null;
|
|
167
|
+
const labelBytes = this.#enc.encode(label);
|
|
168
|
+
if (labelBytes.length < 1 || labelBytes.length > 16)
|
|
169
|
+
return null;
|
|
170
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
|
|
171
|
+
const n = this.#write(labelBytes);
|
|
172
|
+
return this.#x.derive_namespace_account(n) === 1 ? this.#read() : null;
|
|
173
|
+
}
|
|
174
|
+
deriveNamespaceOverrideAccount(handle, label, programId) {
|
|
175
|
+
if (handle.length !== 32 || programId.length !== 32)
|
|
176
|
+
return null;
|
|
177
|
+
const labelBytes = this.#enc.encode(label);
|
|
178
|
+
if (labelBytes.length < 1 || labelBytes.length > 16)
|
|
179
|
+
return null;
|
|
180
|
+
new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
|
|
181
|
+
const joined = new Uint8Array(32 + labelBytes.length);
|
|
182
|
+
joined.set(handle, 0);
|
|
183
|
+
joined.set(labelBytes, 32);
|
|
184
|
+
const n = this.#write(joined);
|
|
185
|
+
return this.#x.derive_namespace_override_account(n) === 1 ? this.#read() : null;
|
|
186
|
+
}
|
|
149
187
|
}
|
package/package.json
CHANGED
|
Binary file
|