@x1id/resolve 0.2.1 → 0.6.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,282 @@
1
+ /**
2
+ * On-chain subnames (#7375): a label under a parent `@handle`, e.g. `pay`
3
+ * under `alice` → `pay.alice`. Instruction builders, account decoding, and —
4
+ * critically — resolution with the TWO-LAYER staleness rule of
5
+ * docs/record-trust.md structurally enforced.
6
+ *
7
+ * Hand-rolled like the rest of this package — no Anchor client, no
8
+ * `@solana/web3.js` import. Builders return the transport-neutral
9
+ * {@link BuiltInstruction} (see `delegate.ts`'s module docs for the two-line
10
+ * web3.js adapter).
11
+ *
12
+ * # What a subname is (mirror of the on-chain [`Subname`] doc, state.rs)
13
+ *
14
+ * - A single label under a parent handle, same character rules as a handle
15
+ * label ({@link normalizeHandle}); ONE level deep (a subname cannot have
16
+ * subnames — the program takes a `Handle` as parent, never a `Subname`).
17
+ * - It can hold cross-chain address `Record`s (the SAME account type handle
18
+ * records use, seeded from the subname's pubkey), but can NEVER be a primary
19
+ * name or be listed/offered/auctioned.
20
+ * - It has no owner of its own: the parent handle's CURRENT authority controls
21
+ * it and may revoke it unilaterally (subject only to the parent's own #7377
22
+ * lock — while locked, its whole subtree is frozen). Rent-only to create.
23
+ *
24
+ * # Deriving the PDAs — do it with your runtime, not here
25
+ *
26
+ * Like `delegate.ts`, this module does NOT derive PDAs (find_program_address
27
+ * needs an on-curve check this zero-dependency package refuses to hand-roll —
28
+ * see `wasm.ts`). Derive with your runtime, e.g. web3.js:
29
+ *
30
+ * ```ts
31
+ * // subname account
32
+ * PublicKey.findProgramAddressSync(
33
+ * [Buffer.from(SUBNAME_SEED), parentHandlePda.toBytes(), Buffer.from(label, "utf8")],
34
+ * programId,
35
+ * );
36
+ * // a record under a subname (the SAME "record" seed handle records use,
37
+ * // only the subname pubkey stands in for the handle)
38
+ * PublicKey.findProgramAddressSync(
39
+ * [Buffer.from("record"), subnamePda.toBytes(), new Uint8Array(new Uint32Array([coinType]).buffer)],
40
+ * programId,
41
+ * );
42
+ * ```
43
+ *
44
+ * The label is used as a PDA seed DIRECTLY (no hash): a canonical label is
45
+ * ≤ 32 bytes, so it fits one seed — unlike a text-record key (up to 64,
46
+ * hashed).
47
+ *
48
+ * # The two-layer staleness rule — BOTH are mandatory
49
+ *
50
+ * A subname carries no owner, so the universal rule (docs/record-trust.md) is
51
+ * applied through the subname's `createdAt` in two places. A subname record is
52
+ * the current name's address ONLY when BOTH hold:
53
+ *
54
+ * 1. `subname.createdAt >= parent.registeredAt` — the subname belongs to the
55
+ * parent's CURRENT owner. Any parent transfer/sale/recovery/re-registration
56
+ * bumps `parent.registeredAt`, stranding every subname the previous owner
57
+ * made (design point #4: never resolves to a stale owner after transfer).
58
+ * 2. `record.updatedAt >= subname.createdAt` — the record belongs to the
59
+ * CURRENT incarnation of the subname (its `["subname", parent, label]` PDA
60
+ * is reused across revoke + re-create, so a record left by a previous
61
+ * incarnation must go stale under the new one).
62
+ *
63
+ * {@link resolveSubnameRecords} enforces BOTH. A reader that applies only one
64
+ * reopens the exact money-misdirection hazard the rule exists to close.
65
+ */
66
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
67
+ import { type HandleRecord } from "./records.js";
68
+ import type { RpcFn } from "./accounts.js";
69
+ /** Seed prefix of a subname PDA: `["subname", parentHandlePda, label]`. */
70
+ export declare const SUBNAME_SEED = "subname";
71
+ /** Anchor account discriminator: `sha256("account:Subname")[0..8]`. */
72
+ export declare const SUBNAME_DISCRIMINATOR: Uint8Array;
73
+ /** Anchor instruction discriminator: `sha256("global:create_subname")[0..8]`. */
74
+ export declare const CREATE_SUBNAME_DISCRIMINATOR: Uint8Array;
75
+ /** Anchor instruction discriminator: `sha256("global:revoke_subname")[0..8]`. */
76
+ export declare const REVOKE_SUBNAME_DISCRIMINATOR: Uint8Array;
77
+ /** Anchor instruction discriminator: `sha256("global:create_subname_record")[0..8]`. */
78
+ export declare const CREATE_SUBNAME_RECORD_DISCRIMINATOR: Uint8Array;
79
+ /** Anchor instruction discriminator: `sha256("global:update_subname_record")[0..8]`. */
80
+ export declare const UPDATE_SUBNAME_RECORD_DISCRIMINATOR: Uint8Array;
81
+ /** Anchor instruction discriminator: `sha256("global:close_subname_record")[0..8]`. */
82
+ export declare const CLOSE_SUBNAME_RECORD_DISCRIMINATOR: Uint8Array;
83
+ /** `Subname` account size: disc(8) parent(32) label[32] label_len(1)
84
+ * created_at(i64,8) bump(1). */
85
+ export declare const SUBNAME_LEN = 82;
86
+ /** Max label bytes — same as a handle label (`handle_normalize::MAX_LEN`). */
87
+ export declare const SUBNAME_LABEL_MAX_LEN = 32;
88
+ /** A decoded `Subname` account. */
89
+ export interface Subname {
90
+ /** The parent `Handle` PDA this subname hangs under (base58). */
91
+ readonly parent: string;
92
+ /** The subname's label, plaintext (e.g. `"pay"`). */
93
+ readonly label: string;
94
+ /**
95
+ * Unix seconds this subname was (last) created. THE staleness anchor:
96
+ * trust the subname only while `createdAt >= parent.registeredAt`, and trust
97
+ * its records only while `record.updatedAt >= createdAt`. See the module
98
+ * docs.
99
+ */
100
+ readonly createdAt: bigint;
101
+ readonly bump: number;
102
+ }
103
+ /**
104
+ * Decode a `Subname` account's raw data (exactly what `getAccountInfo`
105
+ * returns for the `["subname", parent, label]` PDA), or null if the bytes are
106
+ * not a `Subname`.
107
+ */
108
+ export declare function decodeSubname(data: Uint8Array): Subname | null;
109
+ /**
110
+ * Whether a subname belongs to its parent's CURRENT owner — staleness layer 1
111
+ * (see the module docs). `parentRegisteredAt` is `Handle.registered_at` from
112
+ * the parent account the caller already has. A subname that fails this belongs
113
+ * to a PREVIOUS owner of the parent name and must never be resolved.
114
+ */
115
+ export declare function subnameIsLive(subname: Subname, parentRegisteredAt: bigint): boolean;
116
+ /** Fields shared by every subname builder that gates on parent authority. */
117
+ interface ParentAuthorityParams {
118
+ /** TOKENIZED parent only: the holder's ATA for the parent's NFT mint,
119
+ * appended as the strict-authority remaining-account proof. Omit for an
120
+ * untokenized parent. */
121
+ readonly holderTokenAccount?: AddressLike;
122
+ }
123
+ export interface CreateSubnameParams extends ParentAuthorityParams {
124
+ /** The registry program id. */
125
+ readonly programId: AddressLike;
126
+ /** Pays the subname account's rent. Signer. */
127
+ readonly payer: AddressLike;
128
+ /** The parent handle's CURRENT authority (owner, or NFT holder). Signer. */
129
+ readonly owner: AddressLike;
130
+ /** The parent `["handle", name]` PDA. */
131
+ readonly parent: AddressLike;
132
+ /** The `["subname", parent, label]` PDA — derive per the module docs. */
133
+ readonly subname: AddressLike;
134
+ /** The subname's label (canonical, 1..=32 bytes — same rules as a handle
135
+ * label). Feeds both the PDA seed and the stored field. */
136
+ readonly label: string;
137
+ }
138
+ /**
139
+ * Build `create_subname` — create `label` under the parent. Rent-only,
140
+ * parent-owner-gated, refused while the parent is #7377-locked.
141
+ */
142
+ export declare function buildCreateSubnameIx(p: CreateSubnameParams): BuiltInstruction;
143
+ export interface RevokeSubnameParams extends ParentAuthorityParams {
144
+ /** The registry program id. */
145
+ readonly programId: AddressLike;
146
+ /** The parent handle's CURRENT authority. Signer. */
147
+ readonly owner: AddressLike;
148
+ /** The parent `["handle", name]` PDA. */
149
+ readonly parent: AddressLike;
150
+ /** The `["subname", parent, label]` PDA being closed. */
151
+ readonly subname: AddressLike;
152
+ /** Receives the closed subname's rent — any account the caller chooses. */
153
+ readonly recipient: AddressLike;
154
+ }
155
+ /**
156
+ * Build `revoke_subname` — delete the subname, rent to `recipient`. No
157
+ * independent-owner consent or records-must-be-empty condition; also how a NEW
158
+ * parent owner sweeps a previous owner's stale subname. REVERTS `HandleLocked`
159
+ * while the parent is #7377-locked (STRICT FREEZE, owner decision F1): a locked
160
+ * handle's whole subtree is frozen — unlock first, then revoke. A post-transfer
161
+ * new owner is never locked, so cleanup after a transfer is unaffected.
162
+ *
163
+ * NOTE: any `Record`s under the subname are NOT closed by this — close them
164
+ * first with {@link buildCloseSubnameRecordIx} for rent hygiene (they are
165
+ * resolution-safe if left, but their rent stays locked). Enumerate them with
166
+ * {@link fetchSubnameRecords}.
167
+ */
168
+ export declare function buildRevokeSubnameIx(p: RevokeSubnameParams): BuiltInstruction;
169
+ export interface CreateSubnameRecordParams extends ParentAuthorityParams {
170
+ /** The registry program id. */
171
+ readonly programId: AddressLike;
172
+ /** Pays the record's rent. Signer. */
173
+ readonly payer: AddressLike;
174
+ /** The parent handle's CURRENT authority. Signer. */
175
+ readonly owner: AddressLike;
176
+ /** The parent `["handle", name]` PDA. */
177
+ readonly parent: AddressLike;
178
+ /** The `["subname", parent, label]` PDA. */
179
+ readonly subname: AddressLike;
180
+ /** The `["record", subname, coinType]` PDA — derive per the module docs. */
181
+ readonly record: AddressLike;
182
+ /** SLIP-44 / ENSIP-11 coin type. */
183
+ readonly coinType: number;
184
+ /** The address bytes (1..=64) — same encoding as a handle `Record`. */
185
+ readonly value: Uint8Array;
186
+ }
187
+ /**
188
+ * Build `create_subname_record` — a cross-chain address `Record` under the
189
+ * subname. Refused if the subname is stale
190
+ * (`subname.createdAt < parent.registeredAt`).
191
+ */
192
+ export declare function buildCreateSubnameRecordIx(p: CreateSubnameRecordParams): BuiltInstruction;
193
+ export interface UpdateSubnameRecordParams extends ParentAuthorityParams {
194
+ /** The registry program id. */
195
+ readonly programId: AddressLike;
196
+ /** The parent handle's CURRENT authority. Signer. */
197
+ readonly owner: AddressLike;
198
+ /** The parent `["handle", name]` PDA. */
199
+ readonly parent: AddressLike;
200
+ /** The `["subname", parent, label]` PDA. */
201
+ readonly subname: AddressLike;
202
+ /** The `["record", subname, coinType]` PDA being updated. */
203
+ readonly record: AddressLike;
204
+ /** The new address bytes (1..=64). */
205
+ readonly value: Uint8Array;
206
+ }
207
+ /**
208
+ * Build `update_subname_record` — replace the address and re-stamp
209
+ * `updated_at` (also how a record is re-adopted after a fresh
210
+ * `create_subname`). Refused if the subname is stale.
211
+ */
212
+ export declare function buildUpdateSubnameRecordIx(p: UpdateSubnameRecordParams): BuiltInstruction;
213
+ export interface CloseSubnameRecordParams extends ParentAuthorityParams {
214
+ /** The registry program id. */
215
+ readonly programId: AddressLike;
216
+ /** The parent handle's CURRENT authority. Signer. */
217
+ readonly owner: AddressLike;
218
+ /** The parent `["handle", name]` PDA. */
219
+ readonly parent: AddressLike;
220
+ /** The `["subname", parent, label]` PDA. */
221
+ readonly subname: AddressLike;
222
+ /** The `["record", subname, coinType]` PDA being closed. */
223
+ readonly record: AddressLike;
224
+ /** Receives the closed record's rent. */
225
+ readonly recipient: AddressLike;
226
+ }
227
+ /**
228
+ * Build `close_subname_record` — close the record, rent to `recipient`. No
229
+ * stale-subname gate (a stale subname's records must always be cleanable), but
230
+ * REVERTS `HandleLocked` while the parent is #7377-locked (STRICT FREEZE, owner
231
+ * decision F1) — unlock the parent first. A post-transfer new owner is not
232
+ * locked, so cleanup after a transfer is unaffected.
233
+ */
234
+ export declare function buildCloseSubnameRecordIx(p: CloseSubnameRecordParams): BuiltInstruction;
235
+ /**
236
+ * Fetch every `Subname` account of a parent handle — one `getProgramAccounts`
237
+ * call, filtered by size, the Subname discriminator at offset 0, and the
238
+ * parent pubkey at offset 8, then every byte re-checked locally. Scoping the
239
+ * scan to the registry program id IS the ownership check.
240
+ *
241
+ * `parentRegisteredAt` is `Handle.registered_at` from the parent account the
242
+ * caller already has. Every subname is returned with a `live` flag
243
+ * (`createdAt >= parentRegisteredAt`, staleness layer 1); a stale one belongs
244
+ * to a previous owner of the parent name and must never be resolved — surface
245
+ * it only to the current owner as "left behind; you can revoke this".
246
+ */
247
+ export declare function fetchSubnames(rpc: RpcFn, programId: string, parentHandleAccount: string, parentRegisteredAt: bigint): Promise<(Subname & {
248
+ account: string;
249
+ live: boolean;
250
+ })[]>;
251
+ /**
252
+ * Fetch every `Record` account hanging off a SUBNAME — reuses the handle
253
+ * record scanner verbatim, because subname records ARE `Record` accounts whose
254
+ * `handle` field is the subname pubkey. Passing `subname.createdAt` as the
255
+ * epoch applies staleness layer 2 (`record.updatedAt >= subname.createdAt`):
256
+ * a record left by a previous incarnation of the same subname label is
257
+ * flagged `stale` (and its `verified` forced false).
258
+ *
259
+ * This does NOT apply layer 1 (subname-vs-parent). Use
260
+ * {@link resolveSubnameRecords} for the complete, safe resolution — or gate
261
+ * this call on {@link subnameIsLive} yourself.
262
+ *
263
+ * `recordsClearedAt` is fixed at `0n`, not the parent handle's: `clear_records`
264
+ * (#8139) only ever writes the PARENT `Handle` account's own extension — a
265
+ * subname is a separate account with no `records_cleared_at` of its own, so
266
+ * the parent's clear has no bearing on records keyed by the subname's pubkey.
267
+ */
268
+ export declare function fetchSubnameRecords(rpc: RpcFn, programId: string, subnameAccount: string, subnameCreatedAt: bigint): Promise<HandleRecord[]>;
269
+ /**
270
+ * Resolve a subname's LIVE address records — the ONLY safe entry point, both
271
+ * staleness layers enforced:
272
+ *
273
+ * 1. if `subname.createdAt < parentRegisteredAt` the subname is stale →
274
+ * returns `[]` (it belongs to a previous parent owner);
275
+ * 2. otherwise its records are fetched with `subname.createdAt` as the epoch,
276
+ * so a record from a previous incarnation of the subname is dropped.
277
+ *
278
+ * Returns only records the current name actually resolves to (stale ones
279
+ * excluded, `verified` already forced false for any that slipped the filter).
280
+ */
281
+ export declare function resolveSubnameRecords(rpc: RpcFn, programId: string, subnameAccount: string, subname: Subname, parentRegisteredAt: bigint): Promise<HandleRecord[]>;
282
+ export {};
@@ -0,0 +1,371 @@
1
+ /**
2
+ * On-chain subnames (#7375): a label under a parent `@handle`, e.g. `pay`
3
+ * under `alice` → `pay.alice`. Instruction builders, account decoding, and —
4
+ * critically — resolution with the TWO-LAYER staleness rule of
5
+ * docs/record-trust.md structurally enforced.
6
+ *
7
+ * Hand-rolled like the rest of this package — no Anchor client, no
8
+ * `@solana/web3.js` import. Builders return the transport-neutral
9
+ * {@link BuiltInstruction} (see `delegate.ts`'s module docs for the two-line
10
+ * web3.js adapter).
11
+ *
12
+ * # What a subname is (mirror of the on-chain [`Subname`] doc, state.rs)
13
+ *
14
+ * - A single label under a parent handle, same character rules as a handle
15
+ * label ({@link normalizeHandle}); ONE level deep (a subname cannot have
16
+ * subnames — the program takes a `Handle` as parent, never a `Subname`).
17
+ * - It can hold cross-chain address `Record`s (the SAME account type handle
18
+ * records use, seeded from the subname's pubkey), but can NEVER be a primary
19
+ * name or be listed/offered/auctioned.
20
+ * - It has no owner of its own: the parent handle's CURRENT authority controls
21
+ * it and may revoke it unilaterally (subject only to the parent's own #7377
22
+ * lock — while locked, its whole subtree is frozen). Rent-only to create.
23
+ *
24
+ * # Deriving the PDAs — do it with your runtime, not here
25
+ *
26
+ * Like `delegate.ts`, this module does NOT derive PDAs (find_program_address
27
+ * needs an on-curve check this zero-dependency package refuses to hand-roll —
28
+ * see `wasm.ts`). Derive with your runtime, e.g. web3.js:
29
+ *
30
+ * ```ts
31
+ * // subname account
32
+ * PublicKey.findProgramAddressSync(
33
+ * [Buffer.from(SUBNAME_SEED), parentHandlePda.toBytes(), Buffer.from(label, "utf8")],
34
+ * programId,
35
+ * );
36
+ * // a record under a subname (the SAME "record" seed handle records use,
37
+ * // only the subname pubkey stands in for the handle)
38
+ * PublicKey.findProgramAddressSync(
39
+ * [Buffer.from("record"), subnamePda.toBytes(), new Uint8Array(new Uint32Array([coinType]).buffer)],
40
+ * programId,
41
+ * );
42
+ * ```
43
+ *
44
+ * The label is used as a PDA seed DIRECTLY (no hash): a canonical label is
45
+ * ≤ 32 bytes, so it fits one seed — unlike a text-record key (up to 64,
46
+ * hashed).
47
+ *
48
+ * # The two-layer staleness rule — BOTH are mandatory
49
+ *
50
+ * A subname carries no owner, so the universal rule (docs/record-trust.md) is
51
+ * applied through the subname's `createdAt` in two places. A subname record is
52
+ * the current name's address ONLY when BOTH hold:
53
+ *
54
+ * 1. `subname.createdAt >= parent.registeredAt` — the subname belongs to the
55
+ * parent's CURRENT owner. Any parent transfer/sale/recovery/re-registration
56
+ * bumps `parent.registeredAt`, stranding every subname the previous owner
57
+ * made (design point #4: never resolves to a stale owner after transfer).
58
+ * 2. `record.updatedAt >= subname.createdAt` — the record belongs to the
59
+ * CURRENT incarnation of the subname (its `["subname", parent, label]` PDA
60
+ * is reused across revoke + re-create, so a record left by a previous
61
+ * incarnation must go stale under the new one).
62
+ *
63
+ * {@link resolveSubnameRecords} enforces BOTH. A reader that applies only one
64
+ * reopens the exact money-misdirection hazard the rule exists to close.
65
+ */
66
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
67
+ import { fetchRecords } from "./records.js";
68
+ /** Seed prefix of a subname PDA: `["subname", parentHandlePda, label]`. */
69
+ export const SUBNAME_SEED = "subname";
70
+ /** Anchor account discriminator: `sha256("account:Subname")[0..8]`. */
71
+ export const SUBNAME_DISCRIMINATOR = Uint8Array.from([
72
+ 139, 112, 87, 17, 41, 196, 150, 109,
73
+ ]);
74
+ /** Anchor instruction discriminator: `sha256("global:create_subname")[0..8]`. */
75
+ export const CREATE_SUBNAME_DISCRIMINATOR = Uint8Array.from([
76
+ 171, 24, 16, 42, 114, 52, 73, 43,
77
+ ]);
78
+ /** Anchor instruction discriminator: `sha256("global:revoke_subname")[0..8]`. */
79
+ export const REVOKE_SUBNAME_DISCRIMINATOR = Uint8Array.from([
80
+ 87, 101, 106, 225, 23, 207, 143, 114,
81
+ ]);
82
+ /** Anchor instruction discriminator: `sha256("global:create_subname_record")[0..8]`. */
83
+ export const CREATE_SUBNAME_RECORD_DISCRIMINATOR = Uint8Array.from([
84
+ 105, 119, 250, 240, 198, 138, 84, 86,
85
+ ]);
86
+ /** Anchor instruction discriminator: `sha256("global:update_subname_record")[0..8]`. */
87
+ export const UPDATE_SUBNAME_RECORD_DISCRIMINATOR = Uint8Array.from([
88
+ 41, 233, 2, 89, 35, 65, 253, 241,
89
+ ]);
90
+ /** Anchor instruction discriminator: `sha256("global:close_subname_record")[0..8]`. */
91
+ export const CLOSE_SUBNAME_RECORD_DISCRIMINATOR = Uint8Array.from([
92
+ 124, 229, 58, 219, 15, 20, 159, 214,
93
+ ]);
94
+ /** `Subname` account size: disc(8) parent(32) label[32] label_len(1)
95
+ * created_at(i64,8) bump(1). */
96
+ export const SUBNAME_LEN = 82;
97
+ /** Max label bytes — same as a handle label (`handle_normalize::MAX_LEN`). */
98
+ export const SUBNAME_LABEL_MAX_LEN = 32;
99
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
100
+ function toBytes32(v, what) {
101
+ if (typeof v === "string") {
102
+ const b = decodeBase58_32(v);
103
+ if (!b)
104
+ throw new Error(`${what} is not a valid base58 address`);
105
+ return b;
106
+ }
107
+ if (v.length !== 32)
108
+ throw new Error(`${what} must be exactly 32 bytes`);
109
+ return v;
110
+ }
111
+ function toBase58(v, what) {
112
+ return encodeBase58(toBytes32(v, what));
113
+ }
114
+ /** Borsh `String`/`Vec<u8>`: u32 LE length + bytes. */
115
+ function encVec(bytes) {
116
+ const out = new Uint8Array(4 + bytes.length);
117
+ new DataView(out.buffer).setUint32(0, bytes.length, true);
118
+ out.set(bytes, 4);
119
+ return out;
120
+ }
121
+ /** u32 LE — the wire form of `coin_type`. */
122
+ function encU32(n) {
123
+ const out = new Uint8Array(4);
124
+ new DataView(out.buffer).setUint32(0, n >>> 0, true);
125
+ return out;
126
+ }
127
+ /**
128
+ * Decode a `Subname` account's raw data (exactly what `getAccountInfo`
129
+ * returns for the `["subname", parent, label]` PDA), or null if the bytes are
130
+ * not a `Subname`.
131
+ */
132
+ export function decodeSubname(data) {
133
+ if (data.length !== SUBNAME_LEN)
134
+ return null;
135
+ for (let i = 0; i < 8; i++) {
136
+ if (data[i] !== SUBNAME_DISCRIMINATOR[i])
137
+ return null;
138
+ }
139
+ const labelLen = Math.min(data[72], SUBNAME_LABEL_MAX_LEN);
140
+ const label = new TextDecoder().decode(data.slice(40, 40 + labelLen));
141
+ const createdAt = new DataView(data.buffer, data.byteOffset, data.byteLength).getBigInt64(73, true);
142
+ return {
143
+ parent: encodeBase58(data.slice(8, 40)),
144
+ label,
145
+ createdAt,
146
+ bump: data[81],
147
+ };
148
+ }
149
+ /**
150
+ * Whether a subname belongs to its parent's CURRENT owner — staleness layer 1
151
+ * (see the module docs). `parentRegisteredAt` is `Handle.registered_at` from
152
+ * the parent account the caller already has. A subname that fails this belongs
153
+ * to a PREVIOUS owner of the parent name and must never be resolved.
154
+ */
155
+ export function subnameIsLive(subname, parentRegisteredAt) {
156
+ return subname.createdAt >= parentRegisteredAt;
157
+ }
158
+ function pushHolderAta(keys, p) {
159
+ if (p.holderTokenAccount !== undefined) {
160
+ keys.push({
161
+ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"),
162
+ isSigner: false,
163
+ isWritable: false,
164
+ });
165
+ }
166
+ }
167
+ /**
168
+ * Build `create_subname` — create `label` under the parent. Rent-only,
169
+ * parent-owner-gated, refused while the parent is #7377-locked.
170
+ */
171
+ export function buildCreateSubnameIx(p) {
172
+ const labelBytes = new TextEncoder().encode(p.label);
173
+ if (labelBytes.length === 0 || labelBytes.length > SUBNAME_LABEL_MAX_LEN) {
174
+ throw new Error(`label must be 1..=${SUBNAME_LABEL_MAX_LEN} bytes`);
175
+ }
176
+ const encLabel = encVec(labelBytes);
177
+ const data = new Uint8Array(8 + encLabel.length);
178
+ data.set(CREATE_SUBNAME_DISCRIMINATOR, 0);
179
+ data.set(encLabel, 8);
180
+ const keys = [
181
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
182
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
183
+ { pubkey: toBase58(p.parent, "parent"), isSigner: false, isWritable: false },
184
+ { pubkey: toBase58(p.subname, "subname"), isSigner: false, isWritable: true },
185
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
186
+ ];
187
+ pushHolderAta(keys, p);
188
+ return { programId: toBase58(p.programId, "programId"), keys, data };
189
+ }
190
+ /**
191
+ * Build `revoke_subname` — delete the subname, rent to `recipient`. No
192
+ * independent-owner consent or records-must-be-empty condition; also how a NEW
193
+ * parent owner sweeps a previous owner's stale subname. REVERTS `HandleLocked`
194
+ * while the parent is #7377-locked (STRICT FREEZE, owner decision F1): a locked
195
+ * handle's whole subtree is frozen — unlock first, then revoke. A post-transfer
196
+ * new owner is never locked, so cleanup after a transfer is unaffected.
197
+ *
198
+ * NOTE: any `Record`s under the subname are NOT closed by this — close them
199
+ * first with {@link buildCloseSubnameRecordIx} for rent hygiene (they are
200
+ * resolution-safe if left, but their rent stays locked). Enumerate them with
201
+ * {@link fetchSubnameRecords}.
202
+ */
203
+ export function buildRevokeSubnameIx(p) {
204
+ const keys = [
205
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
206
+ { pubkey: toBase58(p.parent, "parent"), isSigner: false, isWritable: false },
207
+ { pubkey: toBase58(p.subname, "subname"), isSigner: false, isWritable: true },
208
+ { pubkey: toBase58(p.recipient, "recipient"), isSigner: false, isWritable: true },
209
+ ];
210
+ pushHolderAta(keys, p);
211
+ return {
212
+ programId: toBase58(p.programId, "programId"),
213
+ keys,
214
+ data: REVOKE_SUBNAME_DISCRIMINATOR.slice(),
215
+ };
216
+ }
217
+ /**
218
+ * Build `create_subname_record` — a cross-chain address `Record` under the
219
+ * subname. Refused if the subname is stale
220
+ * (`subname.createdAt < parent.registeredAt`).
221
+ */
222
+ export function buildCreateSubnameRecordIx(p) {
223
+ if (p.value.length === 0 || p.value.length > 64) {
224
+ throw new Error("value must be 1..=64 bytes");
225
+ }
226
+ const encValue = encVec(p.value);
227
+ const data = new Uint8Array(8 + 4 + encValue.length);
228
+ data.set(CREATE_SUBNAME_RECORD_DISCRIMINATOR, 0);
229
+ data.set(encU32(p.coinType), 8);
230
+ data.set(encValue, 12);
231
+ const keys = [
232
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
233
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
234
+ { pubkey: toBase58(p.parent, "parent"), isSigner: false, isWritable: false },
235
+ { pubkey: toBase58(p.subname, "subname"), isSigner: false, isWritable: false },
236
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
237
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
238
+ ];
239
+ pushHolderAta(keys, p);
240
+ return { programId: toBase58(p.programId, "programId"), keys, data };
241
+ }
242
+ /**
243
+ * Build `update_subname_record` — replace the address and re-stamp
244
+ * `updated_at` (also how a record is re-adopted after a fresh
245
+ * `create_subname`). Refused if the subname is stale.
246
+ */
247
+ export function buildUpdateSubnameRecordIx(p) {
248
+ if (p.value.length === 0 || p.value.length > 64) {
249
+ throw new Error("value must be 1..=64 bytes");
250
+ }
251
+ const encValue = encVec(p.value);
252
+ const data = new Uint8Array(8 + encValue.length);
253
+ data.set(UPDATE_SUBNAME_RECORD_DISCRIMINATOR, 0);
254
+ data.set(encValue, 8);
255
+ const keys = [
256
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
257
+ { pubkey: toBase58(p.parent, "parent"), isSigner: false, isWritable: false },
258
+ { pubkey: toBase58(p.subname, "subname"), isSigner: false, isWritable: false },
259
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
260
+ ];
261
+ pushHolderAta(keys, p);
262
+ return { programId: toBase58(p.programId, "programId"), keys, data };
263
+ }
264
+ /**
265
+ * Build `close_subname_record` — close the record, rent to `recipient`. No
266
+ * stale-subname gate (a stale subname's records must always be cleanable), but
267
+ * REVERTS `HandleLocked` while the parent is #7377-locked (STRICT FREEZE, owner
268
+ * decision F1) — unlock the parent first. A post-transfer new owner is not
269
+ * locked, so cleanup after a transfer is unaffected.
270
+ */
271
+ export function buildCloseSubnameRecordIx(p) {
272
+ const keys = [
273
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
274
+ { pubkey: toBase58(p.parent, "parent"), isSigner: false, isWritable: false },
275
+ { pubkey: toBase58(p.subname, "subname"), isSigner: false, isWritable: false },
276
+ { pubkey: toBase58(p.record, "record"), isSigner: false, isWritable: true },
277
+ { pubkey: toBase58(p.recipient, "recipient"), isSigner: false, isWritable: true },
278
+ ];
279
+ pushHolderAta(keys, p);
280
+ return {
281
+ programId: toBase58(p.programId, "programId"),
282
+ keys,
283
+ data: CLOSE_SUBNAME_RECORD_DISCRIMINATOR.slice(),
284
+ };
285
+ }
286
+ // ---------------------------------------------------------------------------
287
+ // Resolution
288
+ // ---------------------------------------------------------------------------
289
+ const SUBNAME_PARENT_OFFSET = 8; // `parent` field, right after the discriminator
290
+ /**
291
+ * Fetch every `Subname` account of a parent handle — one `getProgramAccounts`
292
+ * call, filtered by size, the Subname discriminator at offset 0, and the
293
+ * parent pubkey at offset 8, then every byte re-checked locally. Scoping the
294
+ * scan to the registry program id IS the ownership check.
295
+ *
296
+ * `parentRegisteredAt` is `Handle.registered_at` from the parent account the
297
+ * caller already has. Every subname is returned with a `live` flag
298
+ * (`createdAt >= parentRegisteredAt`, staleness layer 1); a stale one belongs
299
+ * to a previous owner of the parent name and must never be resolved — surface
300
+ * it only to the current owner as "left behind; you can revoke this".
301
+ */
302
+ export async function fetchSubnames(rpc, programId, parentHandleAccount, parentRegisteredAt) {
303
+ const res = (await rpc("getProgramAccounts", [
304
+ programId,
305
+ {
306
+ encoding: "base64",
307
+ commitment: "confirmed",
308
+ filters: [
309
+ { dataSize: SUBNAME_LEN },
310
+ { memcmp: { offset: 0, bytes: encodeBase58(SUBNAME_DISCRIMINATOR) } },
311
+ { memcmp: { offset: SUBNAME_PARENT_OFFSET, bytes: parentHandleAccount } },
312
+ ],
313
+ },
314
+ ]));
315
+ const out = [];
316
+ for (const a of res ?? []) {
317
+ const bin = atob(a.account.data[0]);
318
+ const raw = new Uint8Array(bin.length);
319
+ for (let i = 0; i < bin.length; i++)
320
+ raw[i] = bin.charCodeAt(i);
321
+ const s = decodeSubname(raw);
322
+ // Defence in depth: the memcmp should guarantee the parent match, but a
323
+ // wrong offset would silently attribute someone else's subname here.
324
+ if (s && s.parent === parentHandleAccount) {
325
+ out.push({ ...s, account: a.pubkey, live: subnameIsLive(s, parentRegisteredAt) });
326
+ }
327
+ }
328
+ out.sort((x, y) => x.label.localeCompare(y.label));
329
+ return out;
330
+ }
331
+ /**
332
+ * Fetch every `Record` account hanging off a SUBNAME — reuses the handle
333
+ * record scanner verbatim, because subname records ARE `Record` accounts whose
334
+ * `handle` field is the subname pubkey. Passing `subname.createdAt` as the
335
+ * epoch applies staleness layer 2 (`record.updatedAt >= subname.createdAt`):
336
+ * a record left by a previous incarnation of the same subname label is
337
+ * flagged `stale` (and its `verified` forced false).
338
+ *
339
+ * This does NOT apply layer 1 (subname-vs-parent). Use
340
+ * {@link resolveSubnameRecords} for the complete, safe resolution — or gate
341
+ * this call on {@link subnameIsLive} yourself.
342
+ *
343
+ * `recordsClearedAt` is fixed at `0n`, not the parent handle's: `clear_records`
344
+ * (#8139) only ever writes the PARENT `Handle` account's own extension — a
345
+ * subname is a separate account with no `records_cleared_at` of its own, so
346
+ * the parent's clear has no bearing on records keyed by the subname's pubkey.
347
+ */
348
+ export async function fetchSubnameRecords(rpc, programId, subnameAccount, subnameCreatedAt) {
349
+ // The record filter already includes size + Record discriminator; the
350
+ // handle-field memcmp is the subname pubkey here. Reuse the exact scanner so
351
+ // subname records and handle records can never decode differently.
352
+ return fetchRecords(rpc, programId, subnameAccount, subnameCreatedAt, 0n);
353
+ }
354
+ /**
355
+ * Resolve a subname's LIVE address records — the ONLY safe entry point, both
356
+ * staleness layers enforced:
357
+ *
358
+ * 1. if `subname.createdAt < parentRegisteredAt` the subname is stale →
359
+ * returns `[]` (it belongs to a previous parent owner);
360
+ * 2. otherwise its records are fetched with `subname.createdAt` as the epoch,
361
+ * so a record from a previous incarnation of the subname is dropped.
362
+ *
363
+ * Returns only records the current name actually resolves to (stale ones
364
+ * excluded, `verified` already forced false for any that slipped the filter).
365
+ */
366
+ export async function resolveSubnameRecords(rpc, programId, subnameAccount, subname, parentRegisteredAt) {
367
+ if (!subnameIsLive(subname, parentRegisteredAt))
368
+ return [];
369
+ const records = await fetchSubnameRecords(rpc, programId, subnameAccount, subname.createdAt);
370
+ return records.filter((r) => !r.stale);
371
+ }