@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/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
- const parsed = parseName(input); // throws with a specific code
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
- if (!owner)
277
- throw new ResolveError("unrecognized", `"${address}" is not an address`, address);
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 {};