@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.
- package/README.md +112 -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 +108 -0
- package/dist/gateClient.js +128 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +326 -4
- 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/signin.d.ts +15 -3
- package/dist/signin.js +0 -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/index.js
CHANGED
|
@@ -35,12 +35,14 @@ export * from "./subname.js";
|
|
|
35
35
|
export * from "./recordCount.js";
|
|
36
36
|
export * from "./attestation.js";
|
|
37
37
|
export * from "./gate.js";
|
|
38
|
+
export * from "./gateClient.js";
|
|
38
39
|
export * from "./textRecords.js";
|
|
39
40
|
export * from "./lock.js";
|
|
40
41
|
export * from "./integrator.js";
|
|
41
42
|
export * from "./voucher.js";
|
|
42
43
|
export * from "./agent.js";
|
|
43
44
|
export * from "./register.js";
|
|
45
|
+
export * from "./adminConfig.js";
|
|
44
46
|
export * from "./lease.js";
|
|
45
47
|
// Was previously imported here for internal use only, never re-exported —
|
|
46
48
|
// promoted to public API 2026-09-22 because a second real package (mcp/)
|
|
@@ -57,11 +59,18 @@ export * from "./commitReveal.js";
|
|
|
57
59
|
export * from "./pnftTransfer.js";
|
|
58
60
|
export * from "./signin.js";
|
|
59
61
|
export * from "./domainProof.js";
|
|
62
|
+
// Named (not `export *`): `NAMESPACE_MAX_LEN` is already exported by
|
|
63
|
+
// adminMarket.js, and two `export *` sharing a name would make it ambiguous
|
|
64
|
+
// (dropped from the package's public surface). Re-export the scoped API, minus
|
|
65
|
+
// that one equal-valued constant.
|
|
66
|
+
export { scopedTldCandidate, namespaceIsActive, makeScopedHandleDeriver, } from "./scoped.js";
|
|
60
67
|
import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
|
|
61
|
-
import { parseName } from "./parse.js";
|
|
68
|
+
import { parseName, normalizeHandle } from "./parse.js";
|
|
69
|
+
import { scopedTldCandidate, namespaceIsActive, NAMESPACE_MAX_LEN, } from "./scoped.js";
|
|
62
70
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
63
71
|
import { DEFAULT_HANDLE_PROGRAM, TOKEN_ACCOUNT_MIN_LEN, bytesEqual, makeAccountReader, nftHolder, parseHandleAccount, readI64, readU64, } from "./accounts.js";
|
|
64
72
|
import { fetchRecords, liveRecords } from "./records.js";
|
|
73
|
+
import { fetchSubnames, resolveSubnameRecords } from "./subname.js";
|
|
65
74
|
/** Root authority for each X1NS TLD — used to verify a fetched account really
|
|
66
75
|
* belongs to the TLD it claims. Without this check a caller handed an
|
|
67
76
|
* arbitrary account would read an owner straight out of it. */
|
|
@@ -71,6 +80,47 @@ const TLD_ROOT = Object.freeze({
|
|
|
71
80
|
xen: "3SUwpSz33AsyJwf6B48cKZuDTswuUEdUhcXszZrFWPqo",
|
|
72
81
|
});
|
|
73
82
|
const SPL_NAME_HEADER_LEN = 96;
|
|
83
|
+
/**
|
|
84
|
+
* Suffixes that are NEVER a subname parent: the native X1NS namespaces (handled
|
|
85
|
+
* by `parseName` before we ever reach the subname path) and the external TLDs
|
|
86
|
+
* the universal resolver owns. `foo.sol`/`foo.eth` must stay `unrecognized` to
|
|
87
|
+
* the native resolver — never resolved as a subname under `@sol`/`@eth` — so
|
|
88
|
+
* the whole never-conflate-a-namespace rule holds on the money path too.
|
|
89
|
+
*/
|
|
90
|
+
const SUBNAME_RESERVED_SUFFIXES = new Set([
|
|
91
|
+
"x1",
|
|
92
|
+
"xnt",
|
|
93
|
+
"xen",
|
|
94
|
+
"sol",
|
|
95
|
+
"eth",
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Classify `input` as a `label.parent` SUBNAME candidate by SHAPE ONLY (no
|
|
99
|
+
* chain read) — the FIRST interpretation tried for a dotted, non-`@`, non-X1NS
|
|
100
|
+
* name (owner-approved disambiguation order: subname before scoped-TLD). The
|
|
101
|
+
* shape is a single interior dot with non-empty halves, a `parent` that is not
|
|
102
|
+
* a reserved suffix, and a `label` with no further dot (a deeper `a.b.c` is
|
|
103
|
+
* sub-subname territory the one-level program does not support → stays
|
|
104
|
+
* `unrecognized`). Whether `@parent` is actually a registered handle is a chain
|
|
105
|
+
* fact the resolver establishes separately. Both halves are normalized for real
|
|
106
|
+
* at resolve; a parent that cannot be a handle (e.g. > 32 bytes) simply fails
|
|
107
|
+
* the registration check and falls through to the scoped path.
|
|
108
|
+
*/
|
|
109
|
+
function subnameCandidate(input) {
|
|
110
|
+
const t = input.trim().toLowerCase();
|
|
111
|
+
if (!t || t.startsWith("@"))
|
|
112
|
+
return null;
|
|
113
|
+
const dot = t.lastIndexOf(".");
|
|
114
|
+
if (dot <= 0 || dot === t.length - 1)
|
|
115
|
+
return null;
|
|
116
|
+
const label = t.slice(0, dot);
|
|
117
|
+
const parent = t.slice(dot + 1);
|
|
118
|
+
if (label.includes("."))
|
|
119
|
+
return null; // a.b.c — a true sub-subname, unsupported
|
|
120
|
+
if (SUBNAME_RESERVED_SUFFIXES.has(parent))
|
|
121
|
+
return null; // never conflate with X1NS / external
|
|
122
|
+
return { label, parent };
|
|
123
|
+
}
|
|
74
124
|
// `Primary` pointer account (`["primary", owner]` under the registry):
|
|
75
125
|
// disc(8) | owner(32) | handle(32) | set_at(i64 LE, 8) | bump(1) = 81 bytes
|
|
76
126
|
const PRIMARY_LEN = 81;
|
|
@@ -91,6 +141,7 @@ export function createResolver(config) {
|
|
|
91
141
|
// Re-encoded (not the caller's string) so a non-canonical base58 spelling of
|
|
92
142
|
// the same key still compares equal to the RPC's `owner` field.
|
|
93
143
|
const programBase58 = encodeBase58(handleProgram);
|
|
144
|
+
const scopedDeriver = config.scopedHandleDeriver ?? null;
|
|
94
145
|
async function resolveX1ns(canonical, label, tld, chain, input) {
|
|
95
146
|
const account = config.wasm.deriveX1nsAccount(label, tld);
|
|
96
147
|
if (!account) {
|
|
@@ -178,6 +229,237 @@ export function createResolver(config) {
|
|
|
178
229
|
: await nftHolder(reader, config.wasm, canonical, handle.nftMint, input);
|
|
179
230
|
return { input, name: canonical, namespace: "handle", address, chain, verification };
|
|
180
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Resolve a SCOPED customer-TLD name `name.tld` (#8484/#8485), or throw
|
|
234
|
+
* `unrecognized` when `.tld` is not one of OUR launched namespaces — so an
|
|
235
|
+
* unlaunched `.tld` behaves exactly as before (never a scoped hit). The
|
|
236
|
+
* contract mirrors `tools/api`'s `resolve_scoped`: canonicalize both halves
|
|
237
|
+
* as the program does, confirm `["namespace", tld]` is a program-owned Active
|
|
238
|
+
* `Namespace`, then derive `["handle", tld, name]` and read its CURRENT
|
|
239
|
+
* authority through the SAME owner/tokenization/record path a bare `@handle`
|
|
240
|
+
* uses — only the `name`/`namespace` fields differ (the TLD label).
|
|
241
|
+
*/
|
|
242
|
+
async function resolveScoped(subRaw, tldRaw, chain, input) {
|
|
243
|
+
// The exact fall-through the native parser would have produced: a
|
|
244
|
+
// non-canonical half, or a `.tld` that is not a launched namespace, is
|
|
245
|
+
// simply "not a handle or a known domain" — never a misleading
|
|
246
|
+
// invalid-handle, and never a scoped hit on a name we do not own.
|
|
247
|
+
const notScoped = () => new ResolveError("unrecognized", `"${input.trim()}" is not a handle or a known domain`, input);
|
|
248
|
+
// Canonicalize both halves exactly as the program's `handle_normalize`
|
|
249
|
+
// does. A half that does not normalize can never name one of our scoped
|
|
250
|
+
// names — fall through rather than erroring (matches resolve_scoped).
|
|
251
|
+
let name;
|
|
252
|
+
let label;
|
|
253
|
+
try {
|
|
254
|
+
name = normalizeHandle(subRaw);
|
|
255
|
+
label = normalizeHandle(tldRaw);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
throw notScoped();
|
|
259
|
+
}
|
|
260
|
+
if (label.length > NAMESPACE_MAX_LEN)
|
|
261
|
+
throw notScoped();
|
|
262
|
+
if (ttl > 0) {
|
|
263
|
+
const hit = cache.get(`scoped:${label}:${name}:${chain}`);
|
|
264
|
+
if (hit && hit.expires > Date.now())
|
|
265
|
+
return hit.value;
|
|
266
|
+
}
|
|
267
|
+
// Is `.label` a launched, Active X1ID namespace? Derive `["namespace",
|
|
268
|
+
// label]` via the WASM (canonical, always available) and read it. It must
|
|
269
|
+
// exist, be owned by the registry program, and carry `status == Active`.
|
|
270
|
+
const nsAccountKey = config.wasm.deriveNamespaceAccount(label, handleProgram);
|
|
271
|
+
if (!nsAccountKey)
|
|
272
|
+
throw notScoped();
|
|
273
|
+
const ns = await accountInfo(encodeBase58(nsAccountKey));
|
|
274
|
+
if (!ns || ns.owner !== programBase58 || !namespaceIsActive(ns.data)) {
|
|
275
|
+
// Not one of our launched namespaces — preserve today's behavior so an
|
|
276
|
+
// unlaunched `.tld` (and `.sol`/`.eth`, handled elsewhere) stays
|
|
277
|
+
// "unrecognized", never resolved through the wrong path.
|
|
278
|
+
throw notScoped();
|
|
279
|
+
}
|
|
280
|
+
// Confirmed a scoped X1ID name. From here any failure is a REAL error,
|
|
281
|
+
// never a silent fall-through (mirrors resolve_scoped's comment).
|
|
282
|
+
if (!scopedDeriver) {
|
|
283
|
+
throw new ResolveError("not-configured", `.${label} is a launched X1ID TLD, but scoped resolution needs a PDA deriver — install @solana/web3.js and pass scopedHandleDeriver (see makeScopedHandleDeriver)`, input);
|
|
284
|
+
}
|
|
285
|
+
// `makeScopedHandleDeriver(web3)` is async; a common footgun is passing the
|
|
286
|
+
// un-awaited Promise as `scopedHandleDeriver`, which would otherwise surface
|
|
287
|
+
// a baffling `scopedDeriver.scopedHandleKey is not a function` wrapped as an
|
|
288
|
+
// rpc-error. Detect the Promise/thenable (or any object missing the method)
|
|
289
|
+
// and fail with an actionable message instead.
|
|
290
|
+
if (typeof scopedDeriver.then === "function" ||
|
|
291
|
+
typeof scopedDeriver.scopedHandleKey !== "function") {
|
|
292
|
+
throw new ResolveError("not-configured", "scopedHandleDeriver must be the awaited result of makeScopedHandleDeriver(web3), not the Promise it returns — did you forget `await`?", input);
|
|
293
|
+
}
|
|
294
|
+
let pda;
|
|
295
|
+
try {
|
|
296
|
+
pda = await scopedDeriver.scopedHandleKey(label, name, programBase58);
|
|
297
|
+
}
|
|
298
|
+
catch (e) {
|
|
299
|
+
throw new ResolveError("rpc-error", `scoped handle derivation failed: ${String(e)}`, input);
|
|
300
|
+
}
|
|
301
|
+
const h = await accountInfo(pda);
|
|
302
|
+
// An account at the PDA the registry does not own is not a handle — the
|
|
303
|
+
// scoped name is unregistered under this (live) TLD.
|
|
304
|
+
if (!h || h.owner !== programBase58) {
|
|
305
|
+
throw new ResolveError("not-found", `${name}.${label} is not registered`, input);
|
|
306
|
+
}
|
|
307
|
+
const handle = parseHandleAccount(h.data);
|
|
308
|
+
if (!handle) {
|
|
309
|
+
throw new ResolveError("rpc-error", `${name}.${label} returned a malformed account`, input);
|
|
310
|
+
}
|
|
311
|
+
const display = `${name}.${label}`;
|
|
312
|
+
let value;
|
|
313
|
+
if (chain !== "X1" && chain !== "SOL") {
|
|
314
|
+
// ETH/BTC addresses live in per-chain `Record` accounts — same staleness
|
|
315
|
+
// rule as a bare handle (fetchRecords → liveRecords).
|
|
316
|
+
const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt));
|
|
317
|
+
const record = live.find((r) => r.coinType === CHAIN_COIN_TYPE[chain]);
|
|
318
|
+
if (!record) {
|
|
319
|
+
throw new ResolveError("no-record-for-chain", `${display} has no ${chain} record`, input);
|
|
320
|
+
}
|
|
321
|
+
value = {
|
|
322
|
+
input,
|
|
323
|
+
name: display,
|
|
324
|
+
namespace: label,
|
|
325
|
+
address: record.address,
|
|
326
|
+
chain,
|
|
327
|
+
verification: record.verified ? "verified" : "unverified",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Same authority rule as a bare handle: untokenized → `Handle.owner`
|
|
332
|
+
// (verified); tokenized → whoever holds the NFT now (verified only in its
|
|
333
|
+
// ATA). A scoped name is always tokenized in practice, but both branches
|
|
334
|
+
// are kept so the shape is byte-identical to resolveHandle.
|
|
335
|
+
const { address, verification } = handle.nftMint === null
|
|
336
|
+
? { address: encodeBase58(handle.owner), verification: "verified" }
|
|
337
|
+
: await nftHolder(reader, config.wasm, display, handle.nftMint, input);
|
|
338
|
+
value = { input, name: display, namespace: label, address, chain, verification };
|
|
339
|
+
}
|
|
340
|
+
if (ttl > 0)
|
|
341
|
+
cache.set(`scoped:${label}:${name}:${chain}`, { value, expires: Date.now() + ttl });
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Resolve a `label.parent` SUBNAME (#7375) — a label under a parent
|
|
346
|
+
* `@handle`, e.g. `pay.alice`. Called ONLY for a dotted, non-`@`, non-X1NS
|
|
347
|
+
* input (the native parser's `unrecognized` fall-through), and BEFORE the
|
|
348
|
+
* scoped customer-TLD path, per the owner-approved disambiguation order.
|
|
349
|
+
*
|
|
350
|
+
* Returns `null` — the signal to try the scoped path next — when `@parent`
|
|
351
|
+
* is NOT a registered handle (so `x.y` was never a subname). Once `@parent`
|
|
352
|
+
* IS a registered handle we are COMMITTED to the subname interpretation:
|
|
353
|
+
* every outcome is a `Resolved` or a throw (`not-found` / `no-record-for-chain`),
|
|
354
|
+
* never a fall-through to scoped.
|
|
355
|
+
*
|
|
356
|
+
* A subname has NO owner; it resolves PURELY through its per-chain `Record`s
|
|
357
|
+
* (like an ETH/BTC record on a handle), so EVERY chain — X1/SOL included —
|
|
358
|
+
* comes from a record, never an owner key. Both staleness layers of
|
|
359
|
+
* docs/record-trust.md are enforced via `subname.ts`:
|
|
360
|
+
* 1. `subname.createdAt >= parent.registeredAt` — else the subname was left
|
|
361
|
+
* behind by a PREVIOUS owner of the parent (any transfer/sale/recovery/
|
|
362
|
+
* re-registration bumps `registered_at`): `not-found`, never resolved.
|
|
363
|
+
* 2. `record.updatedAt >= subname.createdAt` — a record from a previous
|
|
364
|
+
* incarnation of the same `["subname", parent, label]` PDA is dropped.
|
|
365
|
+
* The subname account itself is located by the program-scoped
|
|
366
|
+
* `getProgramAccounts` scan (`fetchSubnames`) — scoping the scan to the
|
|
367
|
+
* registry program id IS the ownership check — then matched by canonical
|
|
368
|
+
* label; nothing is hand-derived here (the WASM module has no subname PDA
|
|
369
|
+
* derivation and this package never hand-rolls the on-curve check).
|
|
370
|
+
*/
|
|
371
|
+
async function resolveSubname(labelRaw, parentRaw, chain, input) {
|
|
372
|
+
// The parent half must canonicalize as a handle; if it cannot, `@parent`
|
|
373
|
+
// can never be a registered handle → not a subname, try scoped instead.
|
|
374
|
+
let parentCanonical;
|
|
375
|
+
try {
|
|
376
|
+
parentCanonical = normalizeHandle(parentRaw);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
// The subname label must also canonicalize as a handle label (same rules).
|
|
382
|
+
// If it cannot we cannot build a cache key yet — defer the verdict until
|
|
383
|
+
// after the parent-registration check decides the branch.
|
|
384
|
+
let subLabel = null;
|
|
385
|
+
try {
|
|
386
|
+
subLabel = normalizeHandle(labelRaw);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
subLabel = null;
|
|
390
|
+
}
|
|
391
|
+
if (subLabel !== null && ttl > 0) {
|
|
392
|
+
const hit = cache.get(`subname:${parentCanonical}:${subLabel}:${chain}`);
|
|
393
|
+
if (hit && hit.expires > Date.now())
|
|
394
|
+
return hit.value;
|
|
395
|
+
}
|
|
396
|
+
// Is `@parent` a REGISTERED handle? That — and only that — decides whether
|
|
397
|
+
// `x.y` is a subname. `not-found`/`invalid-handle` → not a subname, fall
|
|
398
|
+
// through to scoped; a real rpc-error/malformed account propagates.
|
|
399
|
+
let parent;
|
|
400
|
+
try {
|
|
401
|
+
parent = await fetchHandle(parentCanonical, input);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
if (e instanceof ResolveError && (e.code === "not-found" || e.code === "invalid-handle")) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
throw e;
|
|
408
|
+
}
|
|
409
|
+
// COMMITTED to the subname interpretation: `@parent` is a live handle.
|
|
410
|
+
if (subLabel === null) {
|
|
411
|
+
throw new ResolveError("not-found", `"${input.trim()}" is not a subname of @${parentCanonical}`, input);
|
|
412
|
+
}
|
|
413
|
+
const display = `${subLabel}.${parentCanonical}`;
|
|
414
|
+
// Locate the subname account via the program scan (ownership check), then
|
|
415
|
+
// match the canonical label. `fetchSubnames` already flags each with
|
|
416
|
+
// `live` (staleness layer 1: createdAt >= parent.registeredAt).
|
|
417
|
+
const subs = await fetchSubnames(rpc, programBase58, parent.pda, parent.handle.registeredAt);
|
|
418
|
+
const match = subs.find((s) => s.label === subLabel);
|
|
419
|
+
if (!match) {
|
|
420
|
+
throw new ResolveError("not-found", `${display} is not a subname of @${parentCanonical}`, input);
|
|
421
|
+
}
|
|
422
|
+
// Staleness layer 1 — a subname stranded by a parent transfer/re-registration
|
|
423
|
+
// belongs to a PREVIOUS owner and must NEVER resolve to a payment address.
|
|
424
|
+
if (!match.live) {
|
|
425
|
+
throw new ResolveError("not-found", `${display} was created before the current @${parentCanonical} registration — it belongs to a previous owner`, input);
|
|
426
|
+
}
|
|
427
|
+
// Read the subname's LIVE records. `resolveSubnameRecords` re-applies layer
|
|
428
|
+
// 1 (returns [] when stale) AND layer 2 (record.updatedAt >= createdAt),
|
|
429
|
+
// dropping any stale record — the only safe entry point.
|
|
430
|
+
const subname = {
|
|
431
|
+
parent: match.parent,
|
|
432
|
+
label: match.label,
|
|
433
|
+
createdAt: match.createdAt,
|
|
434
|
+
bump: match.bump,
|
|
435
|
+
};
|
|
436
|
+
const records = await resolveSubnameRecords(rpc, programBase58, match.account, subname, parent.handle.registeredAt);
|
|
437
|
+
const record = records.find((r) => !r.stale && r.coinType === CHAIN_COIN_TYPE[chain]);
|
|
438
|
+
if (!record) {
|
|
439
|
+
throw new ResolveError("no-record-for-chain", `${display} has no ${chain} record`, input);
|
|
440
|
+
}
|
|
441
|
+
const value = {
|
|
442
|
+
input,
|
|
443
|
+
name: display,
|
|
444
|
+
// A subname is its own namespace — never "handle" (so a consumer that
|
|
445
|
+
// branches on `=== "handle"`, e.g. dev-api's attestation enrichment, does
|
|
446
|
+
// not treat it as a top-level handle) and never the parent's.
|
|
447
|
+
namespace: "subname",
|
|
448
|
+
address: record.address,
|
|
449
|
+
chain,
|
|
450
|
+
// Like a handle's ETH/BTC record: `unverified` unless the record itself
|
|
451
|
+
// is co-signed/verified (and `verified` is already forced false for any
|
|
452
|
+
// stale record by records.ts).
|
|
453
|
+
verification: record.verified ? "verified" : "unverified",
|
|
454
|
+
};
|
|
455
|
+
if (ttl > 0) {
|
|
456
|
+
cache.set(`subname:${parentCanonical}:${subLabel}:${chain}`, {
|
|
457
|
+
value,
|
|
458
|
+
expires: Date.now() + ttl,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
181
463
|
/** See `Resolver.records`. */
|
|
182
464
|
async function records(input) {
|
|
183
465
|
const parsed = parseName(input); // throws with a specific code
|
|
@@ -189,7 +471,34 @@ export function createResolver(config) {
|
|
|
189
471
|
}
|
|
190
472
|
async function resolve(input, opts) {
|
|
191
473
|
const chain = opts?.chain ?? "X1";
|
|
192
|
-
|
|
474
|
+
let parsed;
|
|
475
|
+
try {
|
|
476
|
+
parsed = parseName(input); // throws with a specific code
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
// A dotted, non-`@`, non-X1NS name is `unrecognized` to the strict native
|
|
480
|
+
// parser — but it MAY be a SUBNAME `label.parent` (#7375) or a scoped
|
|
481
|
+
// customer-TLD name `name.tld` (#8484/#8485). Owner-approved
|
|
482
|
+
// disambiguation order: try the subname interpretation FIRST (when
|
|
483
|
+
// `@parent` is a registered handle), then the scoped path. Each re-throws
|
|
484
|
+
// / falls through so `.sol`/`.eth`/unknown TLDs behave exactly as before.
|
|
485
|
+
// Any other code (ambiguous / invalid-*) is preserved verbatim.
|
|
486
|
+
if (e instanceof ResolveError && e.code === "unrecognized") {
|
|
487
|
+
const sub = subnameCandidate(input);
|
|
488
|
+
if (sub) {
|
|
489
|
+
// `null` ⇒ `@parent` is not a registered handle ⇒ not a subname;
|
|
490
|
+
// fall through to the scoped path. A committed subname path either
|
|
491
|
+
// returns a `Resolved` or throws (never reaches scoped).
|
|
492
|
+
const resolved = await resolveSubname(sub.label, sub.parent, chain, input);
|
|
493
|
+
if (resolved !== null)
|
|
494
|
+
return resolved;
|
|
495
|
+
}
|
|
496
|
+
const cand = scopedTldCandidate(input);
|
|
497
|
+
if (cand)
|
|
498
|
+
return resolveScoped(cand.sub, cand.tld, chain, input);
|
|
499
|
+
}
|
|
500
|
+
throw e;
|
|
501
|
+
}
|
|
193
502
|
const key = `${parsed.namespace}:${parsed.canonical}:${chain}`;
|
|
194
503
|
if (ttl > 0) {
|
|
195
504
|
const hit = cache.get(key);
|
|
@@ -273,8 +582,15 @@ export function createResolver(config) {
|
|
|
273
582
|
*/
|
|
274
583
|
async function reverse(address) {
|
|
275
584
|
const owner = decodeBase58_32(address);
|
|
276
|
-
|
|
277
|
-
|
|
585
|
+
// `decodeBase58_32` left-pads a short/tiny base58 input into 32 bytes, so a
|
|
586
|
+
// malformed address like "abc" would otherwise slip through as a valid tiny
|
|
587
|
+
// key. Require the canonical round-trip: a malformed address must THROW
|
|
588
|
+
// (matching the dev-api's 400 INVALID_ADDRESS and the docs/drill checklist),
|
|
589
|
+
// reserving the `null` return exclusively for a WELL-FORMED address that
|
|
590
|
+
// genuinely has no primary.
|
|
591
|
+
if (!owner || encodeBase58(owner) !== address) {
|
|
592
|
+
throw new ResolveError("unrecognized", `"${address}" is not a valid X1 address`, address);
|
|
593
|
+
}
|
|
278
594
|
const handleName = await reverseHandle(owner);
|
|
279
595
|
if (handleName !== null)
|
|
280
596
|
return handleName;
|
|
@@ -304,3 +620,9 @@ export function createResolver(config) {
|
|
|
304
620
|
clearCache: () => cache.clear(),
|
|
305
621
|
};
|
|
306
622
|
}
|
|
623
|
+
export * from "./adminMarket.js";
|
|
624
|
+
export * from "./namespaceOverride.js";
|
|
625
|
+
// The X1ID Universal Resolver (#8475): cross-namespace resolution (.sol via SNS,
|
|
626
|
+
// .eth via ENS) alongside the native path. Additive — the native resolver above
|
|
627
|
+
// is untouched; `createUniversalResolver` composes it with external adapters.
|
|
628
|
+
export * from "./universal.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-handle namespace overrides — `create_namespace_override` /
|
|
3
|
+
* `update_namespace_override` / `close_namespace_override` (#8138 family).
|
|
4
|
+
*
|
|
5
|
+
* A namespace override lets a handle's owner redirect `<handle>.<namespace>`
|
|
6
|
+
* (e.g. `jack.xnt`) to a DIFFERENT address than the handle's default, per
|
|
7
|
+
* namespace label. It is a record-edit-authority action (the handle owner, or a
|
|
8
|
+
* record delegate, signs — NOT an admin action), in the same family as the
|
|
9
|
+
* address `Record` writers: an optional `record_delegate` named slot plus the
|
|
10
|
+
* tokenized-handle holder-ATA proof in `remaining_accounts[0]`.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: this capability is built but NOT yet wired into public resolution —
|
|
13
|
+
* `resolve()` does not read overrides yet (that is the cutover switch). These
|
|
14
|
+
* builders + the decoder are the on-chain-write + read-decode half, ready for
|
|
15
|
+
* when namespaced resolution goes live.
|
|
16
|
+
*
|
|
17
|
+
* Hand-rolled like the rest of the package — no Anchor client, no web3.js. PDAs
|
|
18
|
+
* are not derived here: derive `["handle", name]`, `["namespace", label]`,
|
|
19
|
+
* `["namespace_override", handlePubkey, label]`, `["delegate", handlePubkey]`
|
|
20
|
+
* with your runtime and pass them in, same convention as records.ts / subname.ts.
|
|
21
|
+
*/
|
|
22
|
+
import type { AddressLike, BuiltInstruction } from "./delegate.js";
|
|
23
|
+
import type { RpcFn } from "./accounts.js";
|
|
24
|
+
/** sha256("global:create_namespace_override")[..8]. */
|
|
25
|
+
export declare const CREATE_NAMESPACE_OVERRIDE_DISCRIMINATOR: Uint8Array;
|
|
26
|
+
/** sha256("global:update_namespace_override")[..8]. */
|
|
27
|
+
export declare const UPDATE_NAMESPACE_OVERRIDE_DISCRIMINATOR: Uint8Array;
|
|
28
|
+
/** sha256("global:close_namespace_override")[..8]. */
|
|
29
|
+
export declare const CLOSE_NAMESPACE_OVERRIDE_DISCRIMINATOR: Uint8Array;
|
|
30
|
+
/** The Anchor ACCOUNT discriminator for a `NamespaceOverride` — sha256("account:NamespaceOverride")[..8]. */
|
|
31
|
+
export declare const NAMESPACE_OVERRIDE_ACCOUNT_DISCRIMINATOR: Uint8Array;
|
|
32
|
+
/** The record-edit-authority tail every writer here shares: an optional named
|
|
33
|
+
* `record_delegate` slot (None = the program-id placeholder), then the
|
|
34
|
+
* tokenized-handle holder-ATA proof in remaining_accounts[0]. Own copy per
|
|
35
|
+
* module, matching recordWrite.ts's convention. */
|
|
36
|
+
interface AuthorityTail {
|
|
37
|
+
/** `["delegate", handle]` PDA — pass when a record delegate (not the owner) signs. */
|
|
38
|
+
readonly recordDelegate?: AddressLike;
|
|
39
|
+
/** The owner's pNFT token account — REQUIRED for a tokenized handle (proves
|
|
40
|
+
* the signer holds the capability NFT); omit for an untokenized handle. */
|
|
41
|
+
readonly holderTokenAccount?: AddressLike;
|
|
42
|
+
}
|
|
43
|
+
export interface CreateNamespaceOverrideParams extends AuthorityTail {
|
|
44
|
+
readonly programId: AddressLike;
|
|
45
|
+
/** Rent payer for the new override account; signs. */
|
|
46
|
+
readonly payer: AddressLike;
|
|
47
|
+
/** The handle owner (or delegate signer); signs. */
|
|
48
|
+
readonly owner: AddressLike;
|
|
49
|
+
/** `["handle", name]` PDA. */
|
|
50
|
+
readonly handle: AddressLike;
|
|
51
|
+
/** `["namespace", label]` PDA — must be Active for a create. */
|
|
52
|
+
readonly namespace: AddressLike;
|
|
53
|
+
/** `["namespace_override", handle, label]` PDA (init). */
|
|
54
|
+
readonly namespaceOverride: AddressLike;
|
|
55
|
+
/** The namespace label (e.g. "xnt"), an instruction ARG (== the seed). */
|
|
56
|
+
readonly namespaceLabel: string;
|
|
57
|
+
/** The address `<handle>.<label>` should resolve to. */
|
|
58
|
+
readonly value: AddressLike;
|
|
59
|
+
}
|
|
60
|
+
/** Build `create_namespace_override`. Args: namespace_label (String) + value (Pubkey). */
|
|
61
|
+
export declare function buildCreateNamespaceOverrideIx(p: CreateNamespaceOverrideParams): BuiltInstruction;
|
|
62
|
+
export interface UpdateNamespaceOverrideParams extends AuthorityTail {
|
|
63
|
+
readonly programId: AddressLike;
|
|
64
|
+
readonly owner: AddressLike;
|
|
65
|
+
readonly handle: AddressLike;
|
|
66
|
+
/** `["namespace_override", handle, label]` PDA (self-seeds from its stored label). */
|
|
67
|
+
readonly namespaceOverride: AddressLike;
|
|
68
|
+
/** The new target address. */
|
|
69
|
+
readonly value: AddressLike;
|
|
70
|
+
}
|
|
71
|
+
/** Build `update_namespace_override` — change the target address. Arg: value (Pubkey). */
|
|
72
|
+
export declare function buildUpdateNamespaceOverrideIx(p: UpdateNamespaceOverrideParams): BuiltInstruction;
|
|
73
|
+
export interface CloseNamespaceOverrideParams extends AuthorityTail {
|
|
74
|
+
readonly programId: AddressLike;
|
|
75
|
+
readonly owner: AddressLike;
|
|
76
|
+
readonly handle: AddressLike;
|
|
77
|
+
/** `["namespace_override", handle, label]` PDA (closed to recipient). */
|
|
78
|
+
readonly namespaceOverride: AddressLike;
|
|
79
|
+
/** Lamports recipient for the closed override's rent; need not sign. */
|
|
80
|
+
readonly recipient: AddressLike;
|
|
81
|
+
}
|
|
82
|
+
/** Build `close_namespace_override` — close the override, rent to recipient. No args. */
|
|
83
|
+
export declare function buildCloseNamespaceOverrideIx(p: CloseNamespaceOverrideParams): BuiltInstruction;
|
|
84
|
+
export interface NamespaceOverride {
|
|
85
|
+
/** The handle PDA this override belongs to. */
|
|
86
|
+
readonly handle: string;
|
|
87
|
+
/** The namespace label it applies to (e.g. "xnt"). */
|
|
88
|
+
readonly namespace: string;
|
|
89
|
+
/** The address `<handle>.<namespace>` resolves to instead of the default. */
|
|
90
|
+
readonly value: string;
|
|
91
|
+
/** Unix seconds last stamped. Trust ONLY while `updatedAt >= handle.registeredAt`
|
|
92
|
+
* (the universal epoch-staleness rule — a release+re-register reuses the PDA). */
|
|
93
|
+
readonly updatedAt: bigint;
|
|
94
|
+
readonly bump: number;
|
|
95
|
+
}
|
|
96
|
+
/** Decode a `NamespaceOverride` account. Layout: disc(8) | handle(32) |
|
|
97
|
+
* namespace(String: u32 len + bytes) | value(32) | updated_at(i64) | bump(1).
|
|
98
|
+
* Returns null if the data is too short / malformed. */
|
|
99
|
+
export declare function decodeNamespaceOverride(data: Uint8Array): NamespaceOverride | null;
|
|
100
|
+
/**
|
|
101
|
+
* Fetch + decode a handle's namespace override for one label, applying the
|
|
102
|
+
* universal epoch-staleness rule — a `NamespaceOverride` PDA is reused across a
|
|
103
|
+
* release + re-register of the same handle, so an override from a PREVIOUS owner
|
|
104
|
+
* (stamped before `handleRegisteredAt`) must NOT count. Returns the live
|
|
105
|
+
* override, or null when there is none / it is stale / the account is malformed.
|
|
106
|
+
*
|
|
107
|
+
* GATED read path: this is NOT called by `resolve()` — namespaced resolution does
|
|
108
|
+
* not honor overrides yet (that is the cutover switch). Derive `overrideAccount`
|
|
109
|
+
* with `WasmResolver.deriveNamespaceOverrideAccount(handle, label, programId)`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function fetchNamespaceOverride(rpc: RpcFn, overrideAccount: string, handleRegisteredAt: bigint): Promise<NamespaceOverride | null>;
|
|
112
|
+
/**
|
|
113
|
+
* List all LIVE namespace overrides for one handle (a getProgramAccounts scan
|
|
114
|
+
* filtered by the account discriminator + the `handle` field at offset 8), with
|
|
115
|
+
* the epoch-staleness rule applied. Each entry includes its account address.
|
|
116
|
+
*
|
|
117
|
+
* GATED read path — not used by `resolve()` yet. Pass the handle's
|
|
118
|
+
* `registeredAt` so previous-owner overrides are dropped.
|
|
119
|
+
*/
|
|
120
|
+
export declare function fetchNamespaceOverridesForHandle(rpc: RpcFn, programId: string, handleAccount: string, handleRegisteredAt: bigint): Promise<Array<NamespaceOverride & {
|
|
121
|
+
account: string;
|
|
122
|
+
}>>;
|
|
123
|
+
export {};
|