@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.
@@ -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 {};
@@ -0,0 +1,204 @@
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 { encodeBase58, decodeBase58_32 } from "./base58.js";
23
+ import { recordDelegateNonePlaceholder, recordDelegateSomeSlot } from "./delegate.js";
24
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
25
+ /** sha256("global:create_namespace_override")[..8]. */
26
+ export const CREATE_NAMESPACE_OVERRIDE_DISCRIMINATOR = Uint8Array.from([86, 74, 181, 245, 1, 190, 77, 128]);
27
+ /** sha256("global:update_namespace_override")[..8]. */
28
+ export const UPDATE_NAMESPACE_OVERRIDE_DISCRIMINATOR = Uint8Array.from([6, 194, 181, 7, 155, 17, 115, 224]);
29
+ /** sha256("global:close_namespace_override")[..8]. */
30
+ export const CLOSE_NAMESPACE_OVERRIDE_DISCRIMINATOR = Uint8Array.from([95, 135, 159, 93, 70, 49, 213, 188]);
31
+ /** The Anchor ACCOUNT discriminator for a `NamespaceOverride` — sha256("account:NamespaceOverride")[..8]. */
32
+ export const NAMESPACE_OVERRIDE_ACCOUNT_DISCRIMINATOR = Uint8Array.from([53, 237, 118, 139, 3, 149, 13, 148]);
33
+ function toBytes32(v, what) {
34
+ if (typeof v === "string") {
35
+ const b = decodeBase58_32(v);
36
+ if (!b)
37
+ throw new Error(`${what} is not a valid base58 address`);
38
+ return b;
39
+ }
40
+ if (v.length !== 32)
41
+ throw new Error(`${what} must be exactly 32 bytes`);
42
+ return v;
43
+ }
44
+ function toBase58(v, what) {
45
+ return encodeBase58(toBytes32(v, what));
46
+ }
47
+ function encString(s) {
48
+ const bytes = new TextEncoder().encode(s);
49
+ const out = new Uint8Array(4 + bytes.length);
50
+ new DataView(out.buffer).setUint32(0, bytes.length, true);
51
+ out.set(bytes, 4);
52
+ return out;
53
+ }
54
+ function concat(parts) {
55
+ const len = parts.reduce((n, p) => n + p.length, 0);
56
+ const out = new Uint8Array(len);
57
+ let o = 0;
58
+ for (const p of parts) {
59
+ out.set(p, o);
60
+ o += p.length;
61
+ }
62
+ return out;
63
+ }
64
+ function pushAuthorityTail(keys, programId, p) {
65
+ if (p.recordDelegate !== undefined) {
66
+ keys.push(recordDelegateSomeSlot(p.recordDelegate));
67
+ }
68
+ else if (p.holderTokenAccount !== undefined) {
69
+ keys.push(recordDelegateNonePlaceholder(programId));
70
+ }
71
+ if (p.holderTokenAccount !== undefined) {
72
+ keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
73
+ }
74
+ }
75
+ /** Build `create_namespace_override`. Args: namespace_label (String) + value (Pubkey). */
76
+ export function buildCreateNamespaceOverrideIx(p) {
77
+ const data = concat([CREATE_NAMESPACE_OVERRIDE_DISCRIMINATOR, encString(p.namespaceLabel), toBytes32(p.value, "value")]);
78
+ const keys = [
79
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
80
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
81
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
82
+ { pubkey: toBase58(p.namespace, "namespace"), isSigner: false, isWritable: false },
83
+ { pubkey: toBase58(p.namespaceOverride, "namespaceOverride"), isSigner: false, isWritable: true },
84
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
85
+ ];
86
+ pushAuthorityTail(keys, p.programId, p);
87
+ return { programId: toBase58(p.programId, "programId"), keys, data };
88
+ }
89
+ /** Build `update_namespace_override` — change the target address. Arg: value (Pubkey). */
90
+ export function buildUpdateNamespaceOverrideIx(p) {
91
+ const data = concat([UPDATE_NAMESPACE_OVERRIDE_DISCRIMINATOR, toBytes32(p.value, "value")]);
92
+ const keys = [
93
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
94
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
95
+ { pubkey: toBase58(p.namespaceOverride, "namespaceOverride"), isSigner: false, isWritable: true },
96
+ ];
97
+ pushAuthorityTail(keys, p.programId, p);
98
+ return { programId: toBase58(p.programId, "programId"), keys, data };
99
+ }
100
+ /** Build `close_namespace_override` — close the override, rent to recipient. No args. */
101
+ export function buildCloseNamespaceOverrideIx(p) {
102
+ const keys = [
103
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
104
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
105
+ { pubkey: toBase58(p.namespaceOverride, "namespaceOverride"), isSigner: false, isWritable: true },
106
+ { pubkey: toBase58(p.recipient, "recipient"), isSigner: false, isWritable: true },
107
+ ];
108
+ pushAuthorityTail(keys, p.programId, p);
109
+ return { programId: toBase58(p.programId, "programId"), keys, data: CLOSE_NAMESPACE_OVERRIDE_DISCRIMINATOR.slice() };
110
+ }
111
+ /** Decode a `NamespaceOverride` account. Layout: disc(8) | handle(32) |
112
+ * namespace(String: u32 len + bytes) | value(32) | updated_at(i64) | bump(1).
113
+ * Returns null if the data is too short / malformed. */
114
+ export function decodeNamespaceOverride(data) {
115
+ if (data.length < 8 + 32 + 4)
116
+ return null;
117
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
118
+ const handle = encodeBase58(data.subarray(8, 40));
119
+ const labelLen = dv.getUint32(40, true);
120
+ const labelStart = 44;
121
+ const labelEnd = labelStart + labelLen;
122
+ const valueEnd = labelEnd + 32;
123
+ const updatedEnd = valueEnd + 8;
124
+ if (data.length < updatedEnd + 1)
125
+ return null;
126
+ let namespace;
127
+ try {
128
+ namespace = new TextDecoder().decode(data.subarray(labelStart, labelEnd));
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ const value = encodeBase58(data.subarray(labelEnd, valueEnd));
134
+ const updatedAt = dv.getBigInt64(valueEnd, true);
135
+ const bump = data[updatedEnd];
136
+ return { handle, namespace, value, updatedAt, bump };
137
+ }
138
+ /**
139
+ * Fetch + decode a handle's namespace override for one label, applying the
140
+ * universal epoch-staleness rule — a `NamespaceOverride` PDA is reused across a
141
+ * release + re-register of the same handle, so an override from a PREVIOUS owner
142
+ * (stamped before `handleRegisteredAt`) must NOT count. Returns the live
143
+ * override, or null when there is none / it is stale / the account is malformed.
144
+ *
145
+ * GATED read path: this is NOT called by `resolve()` — namespaced resolution does
146
+ * not honor overrides yet (that is the cutover switch). Derive `overrideAccount`
147
+ * with `WasmResolver.deriveNamespaceOverrideAccount(handle, label, programId)`.
148
+ */
149
+ export async function fetchNamespaceOverride(rpc, overrideAccount, handleRegisteredAt) {
150
+ const res = (await rpc("getAccountInfo", [
151
+ overrideAccount,
152
+ { encoding: "base64", commitment: "confirmed" },
153
+ ]));
154
+ const data0 = res?.value?.data?.[0];
155
+ if (!data0)
156
+ return null;
157
+ const bin = atob(data0);
158
+ const raw = new Uint8Array(bin.length);
159
+ for (let i = 0; i < bin.length; i++)
160
+ raw[i] = bin.charCodeAt(i);
161
+ const decoded = decodeNamespaceOverride(raw);
162
+ if (!decoded)
163
+ return null;
164
+ // Epoch-staleness: ignore an override left by a previous owner.
165
+ if (decoded.updatedAt < handleRegisteredAt)
166
+ return null;
167
+ return decoded;
168
+ }
169
+ /**
170
+ * List all LIVE namespace overrides for one handle (a getProgramAccounts scan
171
+ * filtered by the account discriminator + the `handle` field at offset 8), with
172
+ * the epoch-staleness rule applied. Each entry includes its account address.
173
+ *
174
+ * GATED read path — not used by `resolve()` yet. Pass the handle's
175
+ * `registeredAt` so previous-owner overrides are dropped.
176
+ */
177
+ export async function fetchNamespaceOverridesForHandle(rpc, programId, handleAccount, handleRegisteredAt) {
178
+ const res = (await rpc("getProgramAccounts", [
179
+ programId,
180
+ {
181
+ encoding: "base64",
182
+ commitment: "confirmed",
183
+ filters: [
184
+ { memcmp: { offset: 0, bytes: encodeBase58(NAMESPACE_OVERRIDE_ACCOUNT_DISCRIMINATOR) } },
185
+ { memcmp: { offset: 8, bytes: handleAccount } },
186
+ ],
187
+ },
188
+ ]));
189
+ const out = [];
190
+ for (const a of res ?? []) {
191
+ const bin = atob(a.account.data[0]);
192
+ const raw = new Uint8Array(bin.length);
193
+ for (let i = 0; i < bin.length; i++)
194
+ raw[i] = bin.charCodeAt(i);
195
+ const decoded = decodeNamespaceOverride(raw);
196
+ // Defence in depth + staleness: the memcmp should guarantee the handle, but
197
+ // skip a mismatch or a previous-owner override.
198
+ if (decoded && decoded.handle === handleAccount && decoded.updatedAt >= handleRegisteredAt) {
199
+ out.push({ ...decoded, account: a.pubkey });
200
+ }
201
+ }
202
+ out.sort((x, y) => x.namespace.localeCompare(y.namespace));
203
+ return out;
204
+ }
@@ -58,6 +58,36 @@ export interface RegisterParams {
58
58
  * payer a price before they sign.
59
59
  */
60
60
  export declare function buildRegisterIx(p: RegisterParams): BuiltInstruction;
61
+ /** Anchor instruction discriminator: `sha256("global:register_reserved")[0..8]`. */
62
+ export declare const REGISTER_RESERVED_DISCRIMINATOR: Uint8Array;
63
+ export interface RegisterReservedParams {
64
+ /** The registry program id. */
65
+ readonly programId: AddressLike;
66
+ /** `Config.admin` — the ONLY key this instruction accepts (the program's
67
+ * `has_one = admin` constraint). Signs AND pays the new `Handle` account's
68
+ * rent. On x1id today admin == treasury == the deploy key. */
69
+ readonly admin: AddressLike;
70
+ /** The `["config"]` PDA. */
71
+ readonly config: AddressLike;
72
+ /** The `["handle", name]` PDA being claimed — must not already exist
73
+ * (`init`). On success the handle's owner is set to `Config.treasury`. */
74
+ readonly handle: AddressLike;
75
+ /** Canonical form (`parseName(...).canonical`), MIN_LEN..=32 bytes. */
76
+ readonly name: string;
77
+ readonly handleType: HandleTypeValue;
78
+ }
79
+ /**
80
+ * Build `register_reserved` — the admin-only, free registration of a reserved
81
+ * name INTO THE TREASURY (owner is set on-chain to `Config.treasury`, not to an
82
+ * arbitrary target). Admin-gated: the `admin` account must equal `Config.admin`
83
+ * and must sign. No price, no treasury-payment account — reserved registration
84
+ * is free; the admin just pays the new account's rent.
85
+ *
86
+ * Same hand-rolled shape as {@link buildRegisterIx}. Returns unsigned
87
+ * instruction data for the caller's runtime to assemble, sign (with the admin
88
+ * key) and submit.
89
+ */
90
+ export declare function buildRegisterReservedIx(p: RegisterReservedParams): BuiltInstruction;
61
91
  /** The `Config` fields `register_with_forti` needs beyond what
62
92
  * {@link fetchConfigTreasury} already reads. `fortiRatePerLamport === 0n`
63
93
  * means FORTI payment is not enabled on this deployment — the same
package/dist/register.js CHANGED
@@ -103,6 +103,43 @@ export function buildRegisterIx(p) {
103
103
  }
104
104
  return { programId: toBase58(p.programId, "programId"), keys, data };
105
105
  }
106
+ // ============================= register_reserved (#8404) =============================
107
+ /** Anchor instruction discriminator: `sha256("global:register_reserved")[0..8]`. */
108
+ export const REGISTER_RESERVED_DISCRIMINATOR = Uint8Array.from([
109
+ 37, 151, 239, 242, 251, 26, 205, 224,
110
+ ]);
111
+ /**
112
+ * Build `register_reserved` — the admin-only, free registration of a reserved
113
+ * name INTO THE TREASURY (owner is set on-chain to `Config.treasury`, not to an
114
+ * arbitrary target). Admin-gated: the `admin` account must equal `Config.admin`
115
+ * and must sign. No price, no treasury-payment account — reserved registration
116
+ * is free; the admin just pays the new account's rent.
117
+ *
118
+ * Same hand-rolled shape as {@link buildRegisterIx}. Returns unsigned
119
+ * instruction data for the caller's runtime to assemble, sign (with the admin
120
+ * key) and submit.
121
+ */
122
+ export function buildRegisterReservedIx(p) {
123
+ if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
124
+ throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
125
+ }
126
+ const nameBytes = new TextEncoder().encode(p.name);
127
+ if (nameBytes.length < 1 || nameBytes.length > 32) {
128
+ throw new Error("name must be 1..=32 bytes (UTF-8)");
129
+ }
130
+ const data = new Uint8Array(8 + 4 + nameBytes.length + 1);
131
+ data.set(REGISTER_RESERVED_DISCRIMINATOR, 0);
132
+ new DataView(data.buffer).setUint32(8, nameBytes.length, true);
133
+ data.set(nameBytes, 12);
134
+ data[12 + nameBytes.length] = p.handleType;
135
+ const keys = [
136
+ { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
137
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
138
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
139
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
140
+ ];
141
+ return { programId: toBase58(p.programId, "programId"), keys, data };
142
+ }
106
143
  // ============================= register_with_forti (#8136, WP #8200) =============================
107
144
  /** Byte offsets of the `Config` fields `register_with_forti` needs, verbatim
108
145
  * from state.rs's field order (confirmed against `migrate_config_v4`'s own
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Scoped customer-TLD names (`alices.testtld`) — the SDK last-mile of the
3
+ * namespace-registrar feature (#8484/#8485).
4
+ *
5
+ * A launched X1ID namespace `.tld` is a program-owned `Namespace` account at
6
+ * `["namespace", tld]` with `status == Active`. A name registered under it is a
7
+ * (always-tokenized) `Handle` at the TWO-SEED-plus-prefix PDA
8
+ * `["handle", tld, name]` — a DIFFERENT account from the bare `@name`'s
9
+ * `["handle", name]`, so `alices.testtld` and `@alices` can have different
10
+ * owners. This module is the shape classifier, the `Namespace` status reader and
11
+ * the injected PDA deriver the resolver uses to answer a scoped name; the
12
+ * owner/tokenization/verification read reuses the bare-handle authority path
13
+ * verbatim (see `createResolver` in index.ts).
14
+ *
15
+ * The resolution CONTRACT is `tools/api`'s `resolve_scoped` (Rust, the
16
+ * authority): canonicalize both halves exactly as the program does, confirm
17
+ * `.tld` is a launched Active namespace, then derive `["handle", tld, name]` and
18
+ * resolve its current authority — with `namespace` set to the TLD label.
19
+ *
20
+ * # Why the deriver is injected (not hand-rolled, not WASM)
21
+ *
22
+ * The WASM module exposes `["namespace", label]` (`deriveNamespaceAccount`) but
23
+ * NO two-seed scoped-handle derivation, and this package's one rule is to never
24
+ * hand-roll `find_program_address`'s ed25519 on-curve check in TypeScript (see
25
+ * wasm.ts). So `["handle", tld, name]` is derived through an INJECTED
26
+ * {@link ScopedHandleDeriver} — exactly the pattern the SNS adapter uses for its
27
+ * `.sol` name-account PDA. Build the default with {@link makeScopedHandleDeriver}
28
+ * (which uses `@solana/web3.js`), or inject your own (a test fake, or a runtime
29
+ * that already owns a `findProgramAddress`).
30
+ */
31
+ /** Max byte length of a TLD label (`NAMESPACE_MAX_LEN`, the program's state.rs). */
32
+ export declare const NAMESPACE_MAX_LEN = 16;
33
+ /** A `name.tld` split, when `input` is SHAPED like a scoped customer-TLD name. */
34
+ export interface ScopedCandidate {
35
+ /** The sub-name half (`"alices"`), lowercased; validated for real at resolve. */
36
+ readonly sub: string;
37
+ /** The TLD-label half (`"testtld"`), lowercased and label-charset-checked. */
38
+ readonly tld: string;
39
+ }
40
+ /**
41
+ * Classify `input` as a scoped customer-TLD candidate by SHAPE ONLY — no chain
42
+ * read. Returns the split, or null when the input is not that shape.
43
+ *
44
+ * The shape is a single interior dot with non-empty halves, no leading `@`, and
45
+ * a `tld` that is a plausible namespace label (the program's charset + the
46
+ * 1..=`NAMESPACE_MAX_LEN` length rule) and is NOT one of the reserved suffixes.
47
+ * Whether `.tld` is actually a launched namespace is a chain fact the resolver
48
+ * establishes separately — this only decides whether it is worth asking.
49
+ * Mirrors `app/src/lib/x1/namespaceRegister.ts`'s `namespaceSublabelShape` and
50
+ * the shape gate in `tools/api`'s `resolve`.
51
+ */
52
+ export declare function scopedTldCandidate(input: string): ScopedCandidate | null;
53
+ /**
54
+ * Whether a raw `Namespace` account's bytes say it is `Active`, byte-for-byte
55
+ * the authority's `namespace_active_from_bytes` (tools/api). Layout: disc(8) |
56
+ * label(Borsh `String`: u32 LE length + UTF-8 bytes) | status(u8), and
57
+ * `NamespaceStatus::Active` is the first enum variant (Borsh tag 0). Only the
58
+ * length prefix and the one status byte are parsed — nothing past it. `false`
59
+ * for any buffer too short to hold disc + length prefix + the status byte (a
60
+ * malformed/wrong account is never read as Active). The CALLER must also check
61
+ * the account is owned by the registry program — this reads bytes only.
62
+ */
63
+ export declare function namespaceIsActive(data: Uint8Array): boolean;
64
+ /**
65
+ * The one `find_program_address` derivation scoped resolution needs: the
66
+ * two-seed-plus-prefix scoped handle PDA `["handle", tld, name]`. Injected so
67
+ * the SDK core stays free of curve math (and of a hard `@solana/web3.js`
68
+ * dependency), and so tests can supply a deterministic fake.
69
+ */
70
+ export interface ScopedHandleDeriver {
71
+ /** The `["handle", label, name]` PDA under `programId`, base58. */
72
+ scopedHandleKey(label: string, name: string, programId: string): Promise<string> | string;
73
+ }
74
+ /**
75
+ * Build the default scoped-handle deriver using `@solana/web3.js`.
76
+ *
77
+ * Seeds are `["handle", label, name]` under the registry program — exactly the
78
+ * order `tools/api`'s `scoped_handle_pda` and the program's
79
+ * `register_under_namespace` use, so the derived PDA is the account the name
80
+ * actually lives at. Prefers an INJECTED web3 module (robust under `file:` /
81
+ * `npm link`, where a bare specifier resolves from THIS package's realpath, not
82
+ * the consumer's); falls back to a dynamic import for a normal install.
83
+ */
84
+ export declare function makeScopedHandleDeriver(web3Module?: unknown): Promise<ScopedHandleDeriver>;
package/dist/scoped.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Scoped customer-TLD names (`alices.testtld`) — the SDK last-mile of the
3
+ * namespace-registrar feature (#8484/#8485).
4
+ *
5
+ * A launched X1ID namespace `.tld` is a program-owned `Namespace` account at
6
+ * `["namespace", tld]` with `status == Active`. A name registered under it is a
7
+ * (always-tokenized) `Handle` at the TWO-SEED-plus-prefix PDA
8
+ * `["handle", tld, name]` — a DIFFERENT account from the bare `@name`'s
9
+ * `["handle", name]`, so `alices.testtld` and `@alices` can have different
10
+ * owners. This module is the shape classifier, the `Namespace` status reader and
11
+ * the injected PDA deriver the resolver uses to answer a scoped name; the
12
+ * owner/tokenization/verification read reuses the bare-handle authority path
13
+ * verbatim (see `createResolver` in index.ts).
14
+ *
15
+ * The resolution CONTRACT is `tools/api`'s `resolve_scoped` (Rust, the
16
+ * authority): canonicalize both halves exactly as the program does, confirm
17
+ * `.tld` is a launched Active namespace, then derive `["handle", tld, name]` and
18
+ * resolve its current authority — with `namespace` set to the TLD label.
19
+ *
20
+ * # Why the deriver is injected (not hand-rolled, not WASM)
21
+ *
22
+ * The WASM module exposes `["namespace", label]` (`deriveNamespaceAccount`) but
23
+ * NO two-seed scoped-handle derivation, and this package's one rule is to never
24
+ * hand-roll `find_program_address`'s ed25519 on-curve check in TypeScript (see
25
+ * wasm.ts). So `["handle", tld, name]` is derived through an INJECTED
26
+ * {@link ScopedHandleDeriver} — exactly the pattern the SNS adapter uses for its
27
+ * `.sol` name-account PDA. Build the default with {@link makeScopedHandleDeriver}
28
+ * (which uses `@solana/web3.js`), or inject your own (a test fake, or a runtime
29
+ * that already owns a `findProgramAddress`).
30
+ */
31
+ import { ResolveError } from "./types.js";
32
+ /** Max byte length of a TLD label (`NAMESPACE_MAX_LEN`, the program's state.rs). */
33
+ export const NAMESPACE_MAX_LEN = 16;
34
+ /** The suffixes that are NEVER a scoped X1ID TLD: the native X1NS namespaces
35
+ * and the external ones the universal resolver owns. A scoped candidate whose
36
+ * suffix is one of these is refused here so `.x1`/`.xnt`/`.xen` stay on the
37
+ * X1NS path and `.sol`/`.eth` stay with their own adapters — the whole
38
+ * never-conflate-a-namespace rule. */
39
+ const RESERVED_SUFFIXES = new Set(["x1", "xnt", "xen", "sol", "eth"]);
40
+ /**
41
+ * Classify `input` as a scoped customer-TLD candidate by SHAPE ONLY — no chain
42
+ * read. Returns the split, or null when the input is not that shape.
43
+ *
44
+ * The shape is a single interior dot with non-empty halves, no leading `@`, and
45
+ * a `tld` that is a plausible namespace label (the program's charset + the
46
+ * 1..=`NAMESPACE_MAX_LEN` length rule) and is NOT one of the reserved suffixes.
47
+ * Whether `.tld` is actually a launched namespace is a chain fact the resolver
48
+ * establishes separately — this only decides whether it is worth asking.
49
+ * Mirrors `app/src/lib/x1/namespaceRegister.ts`'s `namespaceSublabelShape` and
50
+ * the shape gate in `tools/api`'s `resolve`.
51
+ */
52
+ export function scopedTldCandidate(input) {
53
+ const t = input.trim().toLowerCase();
54
+ if (!t || t.startsWith("@"))
55
+ return null;
56
+ const dot = t.lastIndexOf(".");
57
+ if (dot <= 0 || dot === t.length - 1)
58
+ return null;
59
+ const sub = t.slice(0, dot);
60
+ const tld = t.slice(dot + 1);
61
+ if (sub.includes("."))
62
+ return null; // a deeper dotted name is a true sub-subname
63
+ if (RESERVED_SUFFIXES.has(tld))
64
+ return null; // never conflate with X1NS / external
65
+ if (tld.length > NAMESPACE_MAX_LEN)
66
+ return null;
67
+ if (!/^[a-z0-9-]+$/.test(tld))
68
+ return null;
69
+ if (tld.startsWith("-") || tld.endsWith("-") || tld.includes("--") || /^[0-9]+$/.test(tld)) {
70
+ return null;
71
+ }
72
+ return { sub, tld };
73
+ }
74
+ /**
75
+ * Whether a raw `Namespace` account's bytes say it is `Active`, byte-for-byte
76
+ * the authority's `namespace_active_from_bytes` (tools/api). Layout: disc(8) |
77
+ * label(Borsh `String`: u32 LE length + UTF-8 bytes) | status(u8), and
78
+ * `NamespaceStatus::Active` is the first enum variant (Borsh tag 0). Only the
79
+ * length prefix and the one status byte are parsed — nothing past it. `false`
80
+ * for any buffer too short to hold disc + length prefix + the status byte (a
81
+ * malformed/wrong account is never read as Active). The CALLER must also check
82
+ * the account is owned by the registry program — this reads bytes only.
83
+ */
84
+ export function namespaceIsActive(data) {
85
+ if (data.length < 12)
86
+ return false;
87
+ const labelLen = new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(8, true);
88
+ const statusOff = 12 + labelLen;
89
+ if (data.length <= statusOff)
90
+ return false;
91
+ return data[statusOff] === 0;
92
+ }
93
+ /**
94
+ * Build the default scoped-handle deriver using `@solana/web3.js`.
95
+ *
96
+ * Seeds are `["handle", label, name]` under the registry program — exactly the
97
+ * order `tools/api`'s `scoped_handle_pda` and the program's
98
+ * `register_under_namespace` use, so the derived PDA is the account the name
99
+ * actually lives at. Prefers an INJECTED web3 module (robust under `file:` /
100
+ * `npm link`, where a bare specifier resolves from THIS package's realpath, not
101
+ * the consumer's); falls back to a dynamic import for a normal install.
102
+ */
103
+ export async function makeScopedHandleDeriver(web3Module) {
104
+ let web3;
105
+ if (web3Module) {
106
+ web3 = web3Module;
107
+ }
108
+ else {
109
+ const spec = "@solana/web3.js";
110
+ try {
111
+ web3 = (await import(spec));
112
+ }
113
+ catch {
114
+ throw new ResolveError("not-configured", "scoped TLD resolution needs a deriver: install @solana/web3.js, pass the web3 module to makeScopedHandleDeriver, or inject `scopedHandleDeriver` in the resolver config");
115
+ }
116
+ }
117
+ const enc = new TextEncoder();
118
+ const handleSeed = enc.encode("handle");
119
+ return {
120
+ scopedHandleKey(label, name, programId) {
121
+ const program = new web3.PublicKey(programId);
122
+ const [key] = web3.PublicKey.findProgramAddressSync([handleSeed, enc.encode(label), enc.encode(name)], program);
123
+ return key.toBase58();
124
+ },
125
+ };
126
+ }
package/dist/types.d.ts CHANGED
@@ -1,8 +1,18 @@
1
- /** Which naming system produced a result. */
2
- export type Namespace = "handle" | "x1" | "xnt" | "xen";
1
+ /**
2
+ * Which naming system produced a result.
3
+ *
4
+ * The first four are the fixed native namespaces. A SCOPED customer-TLD name
5
+ * (`alices.testtld`, #8484/#8485) carries its TLD LABEL here — an arbitrary,
6
+ * launch-time string — so the union is widened with `(string & {})`: literal
7
+ * autocomplete for the fixed four is preserved, while a scoped result can set
8
+ * `namespace` to its `.tld`. Compare against the literals as before; a value
9
+ * that is none of them is a scoped TLD label.
10
+ */
11
+ export type Namespace = "handle" | "x1" | "xnt" | "xen" | (string & {});
3
12
  /**
4
13
  * Display label for a namespace. **Must be shown** next to any resolved
5
- * address — see the module docs on why this is not optional.
14
+ * address — see the module docs on why this is not optional. A scoped TLD label
15
+ * renders as `.<label>` (e.g. `.testtld`), the same as an X1NS suffix.
6
16
  */
7
17
  export declare function namespaceLabel(ns: Namespace): string;
8
18
  /** Confidence in the address a name resolves to. */
@@ -83,7 +93,16 @@ export interface Resolved {
83
93
  /** Chains a handle can carry a record for. SLIP-44 based. */
84
94
  export type Chain = "X1" | "SOL" | "ETH" | "BTC";
85
95
  export declare const CHAIN_COIN_TYPE: Readonly<Record<Chain, number>>;
86
- export type ResolveErrorCode = "unrecognized" | "ambiguous" | "invalid-handle" | "invalid-domain" | "not-found" | "no-record-for-chain" | "rpc-error";
96
+ export type ResolveErrorCode = "unrecognized" | "ambiguous" | "invalid-handle" | "invalid-domain" | "not-found" | "no-record-for-chain" | "rpc-error"
97
+ /**
98
+ * A reachable namespace exists for this name, but the adapter that would
99
+ * resolve it is not configured on this deployment (e.g. no `ETH_RPC_URL` for
100
+ * ENS, or no Solana mainnet RPC for SNS). Distinct from `unrecognized`: the
101
+ * name IS a known namespace — we just cannot reach its chain from here. A UI
102
+ * should surface "this namespace isn't enabled", not "unknown name", and the
103
+ * universal resolver NEVER fabricates an address to paper over it.
104
+ */
105
+ | "not-configured";
87
106
  /**
88
107
  * Finer-grained cause for a `ResolveError`, when the code alone is not
89
108
  * enough for a UI to explain what happened.