@x1id/resolve 0.2.1 → 0.3.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,366 @@
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
+ export async function fetchSubnameRecords(rpc, programId, subnameAccount, subnameCreatedAt) {
344
+ // The record filter already includes size + Record discriminator; the
345
+ // handle-field memcmp is the subname pubkey here. Reuse the exact scanner so
346
+ // subname records and handle records can never decode differently.
347
+ return fetchRecords(rpc, programId, subnameAccount, subnameCreatedAt);
348
+ }
349
+ /**
350
+ * Resolve a subname's LIVE address records — the ONLY safe entry point, both
351
+ * staleness layers enforced:
352
+ *
353
+ * 1. if `subname.createdAt < parentRegisteredAt` the subname is stale →
354
+ * returns `[]` (it belongs to a previous parent owner);
355
+ * 2. otherwise its records are fetched with `subname.createdAt` as the epoch,
356
+ * so a record from a previous incarnation of the subname is dropped.
357
+ *
358
+ * Returns only records the current name actually resolves to (stale ones
359
+ * excluded, `verified` already forced false for any that slipped the filter).
360
+ */
361
+ export async function resolveSubnameRecords(rpc, programId, subnameAccount, subname, parentRegisteredAt) {
362
+ if (!subnameIsLive(subname, parentRegisteredAt))
363
+ return [];
364
+ const records = await fetchSubnameRecords(rpc, programId, subnameAccount, subname.createdAt);
365
+ return records.filter((r) => !r.stale);
366
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Generic string-keyed metadata records (`TextRecord` accounts, #7376) —
3
+ * profile fields (`website`, `avatar`), social usernames (`com.discord`,
4
+ * `com.github`, `com.x`), and the future `contenthash` (#7382) — read WITH
5
+ * the universal staleness rule from docs/record-trust.md structurally
6
+ * enforced, plus instruction builders for `create_text_record` /
7
+ * `update_text_record` / `close_text_record`.
8
+ *
9
+ * Hand-rolled like the rest of this package — no Anchor client, no
10
+ * `@solana/web3.js` import (it stays an optional peer). Builders return the
11
+ * transport-neutral {@link BuiltInstruction} `delegate.ts` defines; PDAs are
12
+ * NOT derived here (see delegate.ts's module docs for why — derive
13
+ * `["text", handlePda, sha256(key)]` with your runtime's canonical
14
+ * `findProgramAddress`, hashing the key via {@link hashTextKey}).
15
+ *
16
+ * # The seed is `sha256(key)`; the plaintext key is ALSO stored
17
+ *
18
+ * A Solana PDA seed maxes out at 32 bytes; a text-record key is up to 64.
19
+ * The program hashes the key (sha256 — NOT keccak, so this module's
20
+ * {@link hashTextKey} is plain WebCrypto with no dependency) and uses the
21
+ * FULL 32-byte digest as the third seed. The account stores the key in
22
+ * plaintext too, so {@link fetchTextRecords} enumerates a handle's keys
23
+ * without preimage guessing — the hash is wire plumbing, the field is the
24
+ * truth, and the program guarantees they agree (the same instruction
25
+ * argument feeds both).
26
+ *
27
+ * # Values are RAW BYTES
28
+ *
29
+ * The program enforces only a length cap (1..=256), deliberately not UTF-8 —
30
+ * load-bearing for `contenthash`, whose value is a binary multicodec.
31
+ * Rendering is a reader decision: {@link textValueToString} decodes strictly
32
+ * and returns null for binary values, which callers hex-render or handle by
33
+ * key (`records.ts`'s `valueToAddress` convention).
34
+ *
35
+ * # Why every read function here demands `registeredAt`
36
+ *
37
+ * The identical PDA-reuse hazard `records.ts` documents: a `Handle`'s
38
+ * address is a pure function of the name, release + re-register lands at
39
+ * the SAME pubkey, and a previous owner's website/avatar/socials would
40
+ * otherwise be rendered as the new owner's. The mandatory read-side rule
41
+ * (docs/record-trust.md, the universal rule):
42
+ *
43
+ * text_record.updated_at >= handle.registered_at
44
+ *
45
+ * There is deliberately no way to decode or fetch a text record through
46
+ * this module without the handle's `registered_at` in hand.
47
+ *
48
+ * # These records count (#7384)
49
+ *
50
+ * `create_text_record` increments — and `close_text_record` decrements —
51
+ * the SAME `record_count` Handle extension as the address-record family
52
+ * (`recordCount.ts`), so `release_handle` refuses (`HandleHasRecords`,
53
+ * 6044) while any counted record of EITHER kind remains. Close every text
54
+ * record before releasing a handle, and pass the HANDLE account writable to
55
+ * create/close (the builders here do).
56
+ */
57
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
58
+ import type { RpcFn } from "./accounts.js";
59
+ /** Seed prefix of a text-record PDA: `["text", handlePda, sha256(key)]`. */
60
+ export declare const TEXT_RECORD_SEED = "text";
61
+ /** Max key length, BYTES of UTF-8 (program error `BadTextKey`, 6047). */
62
+ export declare const TEXT_KEY_MAX_LEN = 64;
63
+ /** Max value length, bytes (program error `BadTextValue`, 6048). */
64
+ export declare const TEXT_VALUE_MAX_LEN = 256;
65
+ /** The handle's website URL. */
66
+ export declare const TEXT_KEY_WEBSITE = "website";
67
+ /** The handle's avatar image URL. */
68
+ export declare const TEXT_KEY_AVATAR = "avatar";
69
+ /** Discord username. */
70
+ export declare const TEXT_KEY_DISCORD = "com.discord";
71
+ /** GitHub username. */
72
+ export declare const TEXT_KEY_GITHUB = "com.github";
73
+ /** X (Twitter) username. */
74
+ export declare const TEXT_KEY_X = "com.x";
75
+ /** Decentralized-content hash (#7382) — value is BINARY (multicodec), not
76
+ * text; {@link textValueToString} correctly returns null for it. */
77
+ export declare const TEXT_KEY_CONTENTHASH = "contenthash";
78
+ /** Every well-known key this SDK names, frozen, for pickers/iteration. */
79
+ export declare const WELL_KNOWN_TEXT_KEYS: readonly string[];
80
+ /** Anchor account discriminator: `sha256("account:TextRecord")[0..8]`.
81
+ * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
82
+ * everywhere it runs); asserted against a re-derivation in the test suite
83
+ * so a typo can never silently pass. */
84
+ export declare const TEXT_RECORD_DISCRIMINATOR: Uint8Array;
85
+ /** Anchor instruction discriminator: `sha256("global:create_text_record")[0..8]`. */
86
+ export declare const CREATE_TEXT_RECORD_DISCRIMINATOR: Uint8Array;
87
+ /** Anchor instruction discriminator: `sha256("global:update_text_record")[0..8]`. */
88
+ export declare const UPDATE_TEXT_RECORD_DISCRIMINATOR: Uint8Array;
89
+ /** Anchor instruction discriminator: `sha256("global:close_text_record")[0..8]`. */
90
+ export declare const CLOSE_TEXT_RECORD_DISCRIMINATOR: Uint8Array;
91
+ /** `8 + TextRecord::INIT_SPACE` — the program allocates the full max
92
+ * capacity, so every TextRecord account is exactly this long, live fields
93
+ * packed at the front and the rest zero padding:
94
+ * 8 + 32 + (4 + 64) + (4 + 256) + 8 + 1. */
95
+ export declare const TEXT_RECORD_LEN = 377;
96
+ /**
97
+ * `sha256(utf8(key))` — the 32-byte third PDA seed for
98
+ * `["text", handlePda, hash]`. WebCrypto (`crypto.subtle`), so it is async
99
+ * and available in every modern browser, Node ≥ 18, and workers. The full
100
+ * digest is the seed, untruncated — mirrors the program's `text_key_hash`.
101
+ */
102
+ export declare function hashTextKey(key: string): Promise<Uint8Array>;
103
+ /** A decoded string-keyed metadata record, staleness already judged. */
104
+ export interface HandleTextRecord {
105
+ /** The TextRecord account's address, base58. */
106
+ readonly account: string;
107
+ /** The Handle account this record belongs to, base58. */
108
+ readonly handle: string;
109
+ /** The record's key, PLAINTEXT as stored on-chain (e.g. `"website"`). */
110
+ readonly key: string;
111
+ /** Raw value bytes as stored on-chain — see the module docs. */
112
+ readonly value: Uint8Array;
113
+ /** `value` as a UTF-8 string when it decodes strictly, else null (binary
114
+ * values — `contenthash` — and garbage alike). Never a lossy decode. */
115
+ readonly text: string | null;
116
+ /** `TextRecord.updated_at`, unix seconds. */
117
+ readonly updatedAt: bigint;
118
+ /**
119
+ * The rule docs/record-trust.md mandates: this record is only the current
120
+ * owner's if `updated_at >= handle.registered_at`. A stale record belongs
121
+ * to a previous, unrelated owner of the same name: it must never be
122
+ * rendered as this name's website/avatar/social, and only ever offered to
123
+ * the CURRENT owner as something to remove or overwrite.
124
+ */
125
+ readonly stale: boolean;
126
+ }
127
+ /**
128
+ * Render stored value bytes as text: a STRICT UTF-8 decode, or null when the
129
+ * bytes are not valid UTF-8 (binary values like `contenthash`). Callers
130
+ * needing to show binary values render hex themselves — never a replacement-
131
+ * character decode, which would corrupt silently.
132
+ */
133
+ export declare function textValueToString(value: Uint8Array): string | null;
134
+ /**
135
+ * Decode one TextRecord account.
136
+ *
137
+ * TextRecord account layout, after the 8-byte Anchor discriminator:
138
+ *
139
+ * handle: Pubkey(32) key: String (u32 len + bytes, max 64)
140
+ * value: Vec<u8> (u32 len + bytes, max 256) updated_at: i64(8) bump: u8(1)
141
+ *
142
+ * `registeredAt` is the owning Handle's `registered_at`, decoded by the
143
+ * caller from the Handle account — it decides `stale`. There is
144
+ * intentionally no overload without it (see the module docs).
145
+ *
146
+ * Returns null for anything that is not a TextRecord: wrong length, wrong
147
+ * discriminator, lengths that do not fit, or a key that is not valid UTF-8
148
+ * (the program's Borsh `String` guarantees it is, so that is not a
149
+ * TextRecord).
150
+ */
151
+ export declare function decodeTextRecord(raw: Uint8Array, account: string, registeredAt: bigint): HandleTextRecord | null;
152
+ /** The text records the CURRENT owner actually has — `stale` ones excluded.
153
+ * This is the list to render on a profile and count. */
154
+ export declare function liveTextRecords(records: readonly HandleTextRecord[]): HandleTextRecord[];
155
+ /**
156
+ * Fetch every TextRecord account of a handle — one `getProgramAccounts`
157
+ * call, filtered by the RPC on size (377), the TextRecord discriminator at
158
+ * offset 0 and the handle pubkey at offset 8, then every byte re-checked
159
+ * locally (the node's filters are an optimisation, never the guarantee) —
160
+ * the exact shape of `fetchRecords`. Scoping the scan to the registry
161
+ * program id also IS the ownership check.
162
+ *
163
+ * `registeredAt` is `Handle.registered_at` as decoded from the Handle
164
+ * account the caller already has — the staleness rule needs it, and there
165
+ * is no variant of this function without it. Every record is returned,
166
+ * stale ones flagged, so an owner surface can show what a previous owner
167
+ * left behind; anything that renders a profile takes
168
+ * {@link liveTextRecords}.
169
+ */
170
+ export declare function fetchTextRecords(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint): Promise<HandleTextRecord[]>;
171
+ /** The delegate-aware trailing accounts every text-record builder shares —
172
+ * the exact rules of `delegate.ts`'s module docs. */
173
+ interface RecordEditAuthorityParams {
174
+ /** A DELEGATE caller's `["delegate", handle]` PDA for the trailing
175
+ * named-optional slot. Omit as the owner/NFT holder. */
176
+ readonly recordDelegate?: AddressLike;
177
+ /** TOKENIZED handles, owner flow: the holder's ATA for the handle's NFT
178
+ * mint (the `remaining_accounts[0]` proof). When given WITHOUT
179
+ * `recordDelegate`, the "explicitly None" placeholder (the program id) is
180
+ * inserted ahead of it automatically. Omit for an untokenized owner. */
181
+ readonly holderTokenAccount?: AddressLike;
182
+ }
183
+ export interface CreateTextRecordParams extends RecordEditAuthorityParams {
184
+ /** The registry program id. */
185
+ readonly programId: AddressLike;
186
+ /** Pays the record's rent (and the handle's one-time record_count grow,
187
+ * when it is the first counted record). Signer. */
188
+ readonly payer: AddressLike;
189
+ /** The handle's current authority, or its record-editing delegate.
190
+ * Signer. */
191
+ readonly owner: AddressLike;
192
+ /** The `["handle", name]` PDA — passed WRITABLE (the record_count
193
+ * extension lives on it). */
194
+ readonly handle: AddressLike;
195
+ /** The `["text", handle, sha256(key)]` PDA — derive per the module docs,
196
+ * hashing the SAME key passed below with {@link hashTextKey}. */
197
+ readonly textRecord: AddressLike;
198
+ /** The record's key, plaintext (1..=64 bytes of UTF-8). */
199
+ readonly key: string;
200
+ /** The record's value, raw bytes (1..=256). */
201
+ readonly value: Uint8Array;
202
+ }
203
+ /**
204
+ * Build `create_text_record` — create the (handle, key) record, stamping
205
+ * `updated_at` from the Clock and incrementing the handle's record_count.
206
+ */
207
+ export declare function buildCreateTextRecordIx(p: CreateTextRecordParams): BuiltInstruction;
208
+ export interface UpdateTextRecordParams extends RecordEditAuthorityParams {
209
+ /** The registry program id. */
210
+ readonly programId: AddressLike;
211
+ /** The handle's current authority, or its record-editing delegate.
212
+ * Signer. */
213
+ readonly owner: AddressLike;
214
+ /** The `["handle", name]` PDA. */
215
+ readonly handle: AddressLike;
216
+ /** The `["text", handle, sha256(key)]` PDA of the record being updated. */
217
+ readonly textRecord: AddressLike;
218
+ /** The new value, raw bytes (1..=256). The key is immutable — changing a
219
+ * key is close + create. */
220
+ readonly value: Uint8Array;
221
+ }
222
+ /**
223
+ * Build `update_text_record` — replace the value and re-stamp `updated_at`
224
+ * (also how a record is re-adopted into a new ownership epoch).
225
+ */
226
+ export declare function buildUpdateTextRecordIx(p: UpdateTextRecordParams): BuiltInstruction;
227
+ export interface CloseTextRecordParams extends RecordEditAuthorityParams {
228
+ /** The registry program id. */
229
+ readonly programId: AddressLike;
230
+ /** The handle's current authority, or its record-editing delegate.
231
+ * Signer. */
232
+ readonly owner: AddressLike;
233
+ /** The `["handle", name]` PDA — passed WRITABLE (the record_count
234
+ * decrement lives on it). */
235
+ readonly handle: AddressLike;
236
+ /** The `["text", handle, sha256(key)]` PDA being closed. */
237
+ readonly textRecord: AddressLike;
238
+ /** Receives the closed account's rent — any account the caller chooses. */
239
+ readonly recipient: AddressLike;
240
+ }
241
+ /**
242
+ * Build `close_text_record` — close the record, rent to `recipient`,
243
+ * decrementing the handle's record_count. Close every text record before
244
+ * `release_handle` — for a counted handle that is enforced
245
+ * (`HandleHasRecords`), not advised.
246
+ */
247
+ export declare function buildCloseTextRecordIx(p: CloseTextRecordParams): BuiltInstruction;
248
+ export {};