@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,357 @@
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 { encodeBase58, decodeBase58_32 } from "./base58.js";
58
+ import { recordDelegateNonePlaceholder, recordDelegateSomeSlot } from "./delegate.js";
59
+ /** Seed prefix of a text-record PDA: `["text", handlePda, sha256(key)]`. */
60
+ export const TEXT_RECORD_SEED = "text";
61
+ /** Max key length, BYTES of UTF-8 (program error `BadTextKey`, 6047). */
62
+ export const TEXT_KEY_MAX_LEN = 64;
63
+ /** Max value length, bytes (program error `BadTextValue`, 6048). */
64
+ export const TEXT_VALUE_MAX_LEN = 256;
65
+ // Well-known keys (ENS-style: bare names for generic fields, reverse-DNS for
66
+ // service-specific ones). Any key up to 64 bytes is valid on-chain; these
67
+ // constants exist so every surface spells the common ones identically.
68
+ /** The handle's website URL. */
69
+ export const TEXT_KEY_WEBSITE = "website";
70
+ /** The handle's avatar image URL. */
71
+ export const TEXT_KEY_AVATAR = "avatar";
72
+ /** Discord username. */
73
+ export const TEXT_KEY_DISCORD = "com.discord";
74
+ /** GitHub username. */
75
+ export const TEXT_KEY_GITHUB = "com.github";
76
+ /** X (Twitter) username. */
77
+ export const TEXT_KEY_X = "com.x";
78
+ /** Decentralized-content hash (#7382) — value is BINARY (multicodec), not
79
+ * text; {@link textValueToString} correctly returns null for it. */
80
+ export const TEXT_KEY_CONTENTHASH = "contenthash";
81
+ /** Every well-known key this SDK names, frozen, for pickers/iteration. */
82
+ export const WELL_KNOWN_TEXT_KEYS = Object.freeze([
83
+ TEXT_KEY_WEBSITE,
84
+ TEXT_KEY_AVATAR,
85
+ TEXT_KEY_DISCORD,
86
+ TEXT_KEY_GITHUB,
87
+ TEXT_KEY_X,
88
+ TEXT_KEY_CONTENTHASH,
89
+ ]);
90
+ /** Anchor account discriminator: `sha256("account:TextRecord")[0..8]`.
91
+ * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
92
+ * everywhere it runs); asserted against a re-derivation in the test suite
93
+ * so a typo can never silently pass. */
94
+ export const TEXT_RECORD_DISCRIMINATOR = Uint8Array.from([
95
+ 177, 94, 37, 181, 209, 75, 179, 30,
96
+ ]);
97
+ /** Anchor instruction discriminator: `sha256("global:create_text_record")[0..8]`. */
98
+ export const CREATE_TEXT_RECORD_DISCRIMINATOR = Uint8Array.from([
99
+ 6, 129, 8, 35, 121, 55, 67, 149,
100
+ ]);
101
+ /** Anchor instruction discriminator: `sha256("global:update_text_record")[0..8]`. */
102
+ export const UPDATE_TEXT_RECORD_DISCRIMINATOR = Uint8Array.from([
103
+ 233, 174, 2, 216, 24, 80, 99, 192,
104
+ ]);
105
+ /** Anchor instruction discriminator: `sha256("global:close_text_record")[0..8]`. */
106
+ export const CLOSE_TEXT_RECORD_DISCRIMINATOR = Uint8Array.from([
107
+ 185, 179, 63, 132, 23, 60, 195, 219,
108
+ ]);
109
+ /** `8 + TextRecord::INIT_SPACE` — the program allocates the full max
110
+ * capacity, so every TextRecord account is exactly this long, live fields
111
+ * packed at the front and the rest zero padding:
112
+ * 8 + 32 + (4 + 64) + (4 + 256) + 8 + 1. */
113
+ export const TEXT_RECORD_LEN = 377;
114
+ /** Byte offset of `handle` within a TextRecord account: the 8-byte
115
+ * discriminator. */
116
+ const TEXT_RECORD_HANDLE_OFFSET = 8;
117
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
118
+ function toBytes32(v, what) {
119
+ if (typeof v === "string") {
120
+ const b = decodeBase58_32(v);
121
+ if (!b)
122
+ throw new Error(`${what} is not a valid base58 address`);
123
+ return b;
124
+ }
125
+ if (v.length !== 32)
126
+ throw new Error(`${what} must be exactly 32 bytes`);
127
+ return v;
128
+ }
129
+ function toBase58(v, what) {
130
+ // Round-trip through bytes so a non-canonical base58 spelling and a byte
131
+ // input both come out identically.
132
+ return encodeBase58(toBytes32(v, what));
133
+ }
134
+ /**
135
+ * `sha256(utf8(key))` — the 32-byte third PDA seed for
136
+ * `["text", handlePda, hash]`. WebCrypto (`crypto.subtle`), so it is async
137
+ * and available in every modern browser, Node ≥ 18, and workers. The full
138
+ * digest is the seed, untruncated — mirrors the program's `text_key_hash`.
139
+ */
140
+ export async function hashTextKey(key) {
141
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key));
142
+ return new Uint8Array(digest);
143
+ }
144
+ /**
145
+ * Render stored value bytes as text: a STRICT UTF-8 decode, or null when the
146
+ * bytes are not valid UTF-8 (binary values like `contenthash`). Callers
147
+ * needing to show binary values render hex themselves — never a replacement-
148
+ * character decode, which would corrupt silently.
149
+ */
150
+ export function textValueToString(value) {
151
+ try {
152
+ return new TextDecoder("utf-8", { fatal: true }).decode(value);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ /**
159
+ * Decode one TextRecord account.
160
+ *
161
+ * TextRecord account layout, after the 8-byte Anchor discriminator:
162
+ *
163
+ * handle: Pubkey(32) key: String (u32 len + bytes, max 64)
164
+ * value: Vec<u8> (u32 len + bytes, max 256) updated_at: i64(8) bump: u8(1)
165
+ *
166
+ * `registeredAt` is the owning Handle's `registered_at`, decoded by the
167
+ * caller from the Handle account — it decides `stale`. There is
168
+ * intentionally no overload without it (see the module docs).
169
+ *
170
+ * Returns null for anything that is not a TextRecord: wrong length, wrong
171
+ * discriminator, lengths that do not fit, or a key that is not valid UTF-8
172
+ * (the program's Borsh `String` guarantees it is, so that is not a
173
+ * TextRecord).
174
+ */
175
+ export function decodeTextRecord(raw, account, registeredAt) {
176
+ if (raw.length !== TEXT_RECORD_LEN)
177
+ return null;
178
+ for (let i = 0; i < 8; i++) {
179
+ if (raw[i] !== TEXT_RECORD_DISCRIMINATOR[i])
180
+ return null;
181
+ }
182
+ const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
183
+ let o = 8;
184
+ const handle = encodeBase58(raw.slice(o, o + 32));
185
+ o += 32;
186
+ const keyLen = dv.getUint32(o, true);
187
+ o += 4;
188
+ // `#[max_len(64)]` — and every fixed field after must still fit.
189
+ if (keyLen > TEXT_KEY_MAX_LEN || o + keyLen + 4 > raw.length)
190
+ return null;
191
+ const keyBytes = raw.slice(o, o + keyLen);
192
+ o += keyLen;
193
+ const valueLen = dv.getUint32(o, true);
194
+ o += 4;
195
+ if (valueLen > TEXT_VALUE_MAX_LEN || o + valueLen + 8 + 1 > raw.length)
196
+ return null;
197
+ const value = raw.slice(o, o + valueLen);
198
+ o += valueLen;
199
+ const updatedAt = dv.getBigInt64(o, true);
200
+ const key = textValueToString(keyBytes);
201
+ if (key === null)
202
+ return null; // Borsh Strings are always valid UTF-8
203
+ return {
204
+ account,
205
+ handle,
206
+ key,
207
+ value,
208
+ text: textValueToString(value),
209
+ updatedAt,
210
+ stale: updatedAt < registeredAt,
211
+ };
212
+ }
213
+ /** The text records the CURRENT owner actually has — `stale` ones excluded.
214
+ * This is the list to render on a profile and count. */
215
+ export function liveTextRecords(records) {
216
+ return records.filter((r) => !r.stale);
217
+ }
218
+ /**
219
+ * Fetch every TextRecord account of a handle — one `getProgramAccounts`
220
+ * call, filtered by the RPC on size (377), the TextRecord discriminator at
221
+ * offset 0 and the handle pubkey at offset 8, then every byte re-checked
222
+ * locally (the node's filters are an optimisation, never the guarantee) —
223
+ * the exact shape of `fetchRecords`. Scoping the scan to the registry
224
+ * program id also IS the ownership check.
225
+ *
226
+ * `registeredAt` is `Handle.registered_at` as decoded from the Handle
227
+ * account the caller already has — the staleness rule needs it, and there
228
+ * is no variant of this function without it. Every record is returned,
229
+ * stale ones flagged, so an owner surface can show what a previous owner
230
+ * left behind; anything that renders a profile takes
231
+ * {@link liveTextRecords}.
232
+ */
233
+ export async function fetchTextRecords(rpc, programId, handleAccount, registeredAt) {
234
+ const res = (await rpc("getProgramAccounts", [
235
+ programId,
236
+ {
237
+ encoding: "base64",
238
+ commitment: "confirmed",
239
+ filters: [
240
+ { dataSize: TEXT_RECORD_LEN },
241
+ { memcmp: { offset: 0, bytes: encodeBase58(TEXT_RECORD_DISCRIMINATOR) } },
242
+ { memcmp: { offset: TEXT_RECORD_HANDLE_OFFSET, bytes: handleAccount } },
243
+ ],
244
+ },
245
+ ]));
246
+ const out = [];
247
+ for (const a of res ?? []) {
248
+ const bin = atob(a.account.data[0]);
249
+ const raw = new Uint8Array(bin.length);
250
+ for (let i = 0; i < bin.length; i++)
251
+ raw[i] = bin.charCodeAt(i);
252
+ const decoded = decodeTextRecord(raw, a.pubkey, registeredAt);
253
+ // Defence in depth: the memcmp filter should guarantee the handle
254
+ // match, but a wrong offset would silently attribute someone else's
255
+ // record to this handle. A malformed account is skipped, not fatal.
256
+ if (decoded && decoded.handle === handleAccount)
257
+ out.push(decoded);
258
+ }
259
+ out.sort((x, y) => x.key.localeCompare(y.key));
260
+ return out;
261
+ }
262
+ function pushAuthorityTail(keys, programId, p) {
263
+ if (p.recordDelegate !== undefined) {
264
+ keys.push(recordDelegateSomeSlot(p.recordDelegate));
265
+ }
266
+ else if (p.holderTokenAccount !== undefined) {
267
+ keys.push(recordDelegateNonePlaceholder(programId));
268
+ }
269
+ if (p.holderTokenAccount !== undefined) {
270
+ keys.push({
271
+ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"),
272
+ isSigner: false,
273
+ isWritable: false,
274
+ });
275
+ }
276
+ }
277
+ /** Borsh `String`/`Vec<u8>`: u32 LE length + bytes. */
278
+ function encVec(bytes) {
279
+ const out = new Uint8Array(4 + bytes.length);
280
+ new DataView(out.buffer).setUint32(0, bytes.length, true);
281
+ out.set(bytes, 4);
282
+ return out;
283
+ }
284
+ function checkKey(key) {
285
+ const bytes = new TextEncoder().encode(key);
286
+ if (bytes.length === 0 || bytes.length > TEXT_KEY_MAX_LEN) {
287
+ throw new Error(`key must be 1..=${TEXT_KEY_MAX_LEN} bytes of UTF-8`);
288
+ }
289
+ return bytes;
290
+ }
291
+ function checkValue(value) {
292
+ if (value.length === 0 || value.length > TEXT_VALUE_MAX_LEN) {
293
+ throw new Error(`value must be 1..=${TEXT_VALUE_MAX_LEN} bytes`);
294
+ }
295
+ return value;
296
+ }
297
+ /**
298
+ * Build `create_text_record` — create the (handle, key) record, stamping
299
+ * `updated_at` from the Clock and incrementing the handle's record_count.
300
+ */
301
+ export function buildCreateTextRecordIx(p) {
302
+ const keyBytes = checkKey(p.key);
303
+ const value = checkValue(p.value);
304
+ const encKey = encVec(keyBytes);
305
+ const encValue = encVec(value);
306
+ const data = new Uint8Array(8 + encKey.length + encValue.length);
307
+ data.set(CREATE_TEXT_RECORD_DISCRIMINATOR, 0);
308
+ data.set(encKey, 8);
309
+ data.set(encValue, 8 + encKey.length);
310
+ const keys = [
311
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
312
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
313
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
314
+ { pubkey: toBase58(p.textRecord, "textRecord"), isSigner: false, isWritable: true },
315
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
316
+ ];
317
+ pushAuthorityTail(keys, p.programId, p);
318
+ return { programId: toBase58(p.programId, "programId"), keys, data };
319
+ }
320
+ /**
321
+ * Build `update_text_record` — replace the value and re-stamp `updated_at`
322
+ * (also how a record is re-adopted into a new ownership epoch).
323
+ */
324
+ export function buildUpdateTextRecordIx(p) {
325
+ const value = checkValue(p.value);
326
+ const encValue = encVec(value);
327
+ const data = new Uint8Array(8 + encValue.length);
328
+ data.set(UPDATE_TEXT_RECORD_DISCRIMINATOR, 0);
329
+ data.set(encValue, 8);
330
+ const keys = [
331
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
332
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
333
+ { pubkey: toBase58(p.textRecord, "textRecord"), isSigner: false, isWritable: true },
334
+ ];
335
+ pushAuthorityTail(keys, p.programId, p);
336
+ return { programId: toBase58(p.programId, "programId"), keys, data };
337
+ }
338
+ /**
339
+ * Build `close_text_record` — close the record, rent to `recipient`,
340
+ * decrementing the handle's record_count. Close every text record before
341
+ * `release_handle` — for a counted handle that is enforced
342
+ * (`HandleHasRecords`), not advised.
343
+ */
344
+ export function buildCloseTextRecordIx(p) {
345
+ const keys = [
346
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
347
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
348
+ { pubkey: toBase58(p.textRecord, "textRecord"), isSigner: false, isWritable: true },
349
+ { pubkey: toBase58(p.recipient, "recipient"), isSigner: false, isWritable: true },
350
+ ];
351
+ pushAuthorityTail(keys, p.programId, p);
352
+ return {
353
+ programId: toBase58(p.programId, "programId"),
354
+ keys,
355
+ data: CLOSE_TEXT_RECORD_DISCRIMINATOR.slice(),
356
+ };
357
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Prepaid gift-registration vouchers (`PrepaidVoucher`, #7399): instruction
3
+ * builders and account decoding for `create_voucher` / `claim_voucher` /
4
+ * `refund_voucher`.
5
+ *
6
+ * Hand-rolled like the rest of this package — see `delegate.ts` for the
7
+ * web3.js adapter and `wasm.ts` for why PDAs are derived by the caller, not
8
+ * here.
9
+ *
10
+ * # Deriving the PDAs
11
+ *
12
+ * ```ts
13
+ * // the voucher itself
14
+ * PublicKey.findProgramAddressSync([Buffer.from(VOUCHER_SEED), Buffer.from(name)], programId);
15
+ * // the handle the claim will register (canonical name bytes)
16
+ * PublicKey.findProgramAddressSync([Buffer.from("handle"), Buffer.from(name)], programId);
17
+ * ```
18
+ *
19
+ * # Lifecycle (mirror of lib.rs)
20
+ *
21
+ * - `create_voucher(name, handleType, recipient, expiresAt)` escrows the mint
22
+ * price (a snapshot of the live price) into `["voucher", name]` and reserves
23
+ * the voucher slot. `recipient` bound ⇒ the claim registers the handle to
24
+ * that wallet no matter who submits; `null` ⇒ open-code (the claimer owns
25
+ * it). An optional integrator referrer (the same trailing accounts as
26
+ * `register`, from `integrator.ts`) is snapshotted so the claim can split
27
+ * the escrow. `expiresAt` must be strictly in the future.
28
+ * - `claim_voucher(name)` registers the handle, drawing the fee from the
29
+ * escrow (split integrator/treasury per the snapshot), and closes the
30
+ * voucher — rent back to the original payer. The `claimer` pays only the tx
31
+ * fee and the handle's rent, never the mint fee; a relayer may claim for a
32
+ * bound recipient. Supply the integrator wallet iff the voucher carries one.
33
+ * - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
34
+ * payer-signed, only at/after `expiresAt`.
35
+ */
36
+ import type { AddressLike, BuiltInstruction, InstructionKey } from "./delegate.js";
37
+ /** Seed prefix of the voucher PDA: `["voucher", name]`. */
38
+ export declare const VOUCHER_SEED = "voucher";
39
+ /** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
40
+ export declare const CREATE_VOUCHER_DISCRIMINATOR: Uint8Array;
41
+ /** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
42
+ export declare const CLAIM_VOUCHER_DISCRIMINATOR: Uint8Array;
43
+ /** Anchor instruction discriminator `sha256("global:refund_voucher")[0..8]`. */
44
+ export declare const REFUND_VOUCHER_DISCRIMINATOR: Uint8Array;
45
+ /** Anchor account discriminator `sha256("account:PrepaidVoucher")[0..8]`. */
46
+ export declare const PREPAID_VOUCHER_DISCRIMINATOR: Uint8Array;
47
+ /** `HandleType` (state.rs): descriptive only, does not gate anything. */
48
+ export declare const HandleType: {
49
+ readonly Human: 0;
50
+ readonly Merchant: 1;
51
+ readonly Org: 2;
52
+ readonly Agent: 3;
53
+ };
54
+ export type HandleTypeValue = (typeof HandleType)[keyof typeof HandleType];
55
+ /** Decoded `PrepaidVoucher` account. */
56
+ export interface PrepaidVoucher {
57
+ /** Who funded it and gets the refund (base58). */
58
+ readonly payer: string;
59
+ /** Canonical name (no `@`). */
60
+ readonly name: string;
61
+ readonly handleType: number;
62
+ /** Escrowed mint fee, in lamports. */
63
+ readonly amount: bigint;
64
+ /** Bound recipient (base58) or null for an open-code voucher. */
65
+ readonly recipient: string | null;
66
+ /** Integrator referrer (base58) or null. */
67
+ readonly integrator: string | null;
68
+ /** Integrator share in basis points (meaningful only if `integrator`). */
69
+ readonly integratorRateBps: number;
70
+ readonly expiresAt: bigint;
71
+ readonly createdAt: bigint;
72
+ readonly bump: number;
73
+ }
74
+ /**
75
+ * Decode a `PrepaidVoucher` account's raw data, or null if not one. Parsed
76
+ * sequentially (Borsh): the two `Option<Pubkey>` fields are 1 byte when
77
+ * absent, 33 when present, so fixed offsets after them would be wrong.
78
+ */
79
+ export declare function decodePrepaidVoucher(data: Uint8Array): PrepaidVoucher | null;
80
+ export interface CreateVoucherParams {
81
+ readonly programId: AddressLike;
82
+ /** Funds the escrow + the voucher's rent. Signer. */
83
+ readonly payer: AddressLike;
84
+ /** The `["config"]` PDA. */
85
+ readonly config: AddressLike;
86
+ /** The `["voucher", name]` PDA. */
87
+ readonly voucher: AddressLike;
88
+ /** The `["handle", name]` PDA — must be unregistered. */
89
+ readonly handle: AddressLike;
90
+ /** Canonical name (no `@`). */
91
+ readonly name: string;
92
+ readonly handleType: HandleTypeValue;
93
+ /** Bind the gift to this wallet, or null for open-code. */
94
+ readonly recipient?: AddressLike | null;
95
+ /** Unix seconds; must be strictly in the future. */
96
+ readonly expiresAt: bigint | number;
97
+ /** Optional integrator pair from `integratorAccountsForCreateVoucher`. */
98
+ readonly integratorAccounts?: readonly InstructionKey[];
99
+ }
100
+ /** Build `create_voucher`. */
101
+ export declare function buildCreateVoucherIx(p: CreateVoucherParams): BuiltInstruction;
102
+ export interface ClaimVoucherParams {
103
+ readonly programId: AddressLike;
104
+ /** Pays the tx fee + the handle's rent (NOT the mint fee). Signer. May be a
105
+ * relayer when the voucher binds a recipient. */
106
+ readonly claimer: AddressLike;
107
+ readonly config: AddressLike;
108
+ /** `Config.treasury`. */
109
+ readonly treasury: AddressLike;
110
+ readonly voucher: AddressLike;
111
+ /** The voucher's recorded payer — receives the voucher's rent on close. */
112
+ readonly voucherPayer: AddressLike;
113
+ /** The `["handle", name]` PDA being registered. */
114
+ readonly handle: AddressLike;
115
+ readonly name: string;
116
+ /** The integrator wallet — REQUIRED iff the voucher carries one; must equal
117
+ * the voucher's `integrator`. Omit otherwise. */
118
+ readonly integrator?: AddressLike;
119
+ }
120
+ /** Build `claim_voucher`. */
121
+ export declare function buildClaimVoucherIx(p: ClaimVoucherParams): BuiltInstruction;
122
+ export interface RefundVoucherParams {
123
+ readonly programId: AddressLike;
124
+ /** The original payer. Signer; receives escrow + rent. */
125
+ readonly payer: AddressLike;
126
+ readonly voucher: AddressLike;
127
+ readonly name: string;
128
+ }
129
+ /** Build `refund_voucher` — payer-signed, only valid at/after `expiresAt`. */
130
+ export declare function buildRefundVoucherIx(p: RefundVoucherParams): BuiltInstruction;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Prepaid gift-registration vouchers (`PrepaidVoucher`, #7399): instruction
3
+ * builders and account decoding for `create_voucher` / `claim_voucher` /
4
+ * `refund_voucher`.
5
+ *
6
+ * Hand-rolled like the rest of this package — see `delegate.ts` for the
7
+ * web3.js adapter and `wasm.ts` for why PDAs are derived by the caller, not
8
+ * here.
9
+ *
10
+ * # Deriving the PDAs
11
+ *
12
+ * ```ts
13
+ * // the voucher itself
14
+ * PublicKey.findProgramAddressSync([Buffer.from(VOUCHER_SEED), Buffer.from(name)], programId);
15
+ * // the handle the claim will register (canonical name bytes)
16
+ * PublicKey.findProgramAddressSync([Buffer.from("handle"), Buffer.from(name)], programId);
17
+ * ```
18
+ *
19
+ * # Lifecycle (mirror of lib.rs)
20
+ *
21
+ * - `create_voucher(name, handleType, recipient, expiresAt)` escrows the mint
22
+ * price (a snapshot of the live price) into `["voucher", name]` and reserves
23
+ * the voucher slot. `recipient` bound ⇒ the claim registers the handle to
24
+ * that wallet no matter who submits; `null` ⇒ open-code (the claimer owns
25
+ * it). An optional integrator referrer (the same trailing accounts as
26
+ * `register`, from `integrator.ts`) is snapshotted so the claim can split
27
+ * the escrow. `expiresAt` must be strictly in the future.
28
+ * - `claim_voucher(name)` registers the handle, drawing the fee from the
29
+ * escrow (split integrator/treasury per the snapshot), and closes the
30
+ * voucher — rent back to the original payer. The `claimer` pays only the tx
31
+ * fee and the handle's rent, never the mint fee; a relayer may claim for a
32
+ * bound recipient. Supply the integrator wallet iff the voucher carries one.
33
+ * - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
34
+ * payer-signed, only at/after `expiresAt`.
35
+ */
36
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
37
+ /** Seed prefix of the voucher PDA: `["voucher", name]`. */
38
+ export const VOUCHER_SEED = "voucher";
39
+ /** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
40
+ export const CREATE_VOUCHER_DISCRIMINATOR = Uint8Array.from([22, 97, 32, 21, 104, 137, 188, 143]);
41
+ /** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
42
+ export const CLAIM_VOUCHER_DISCRIMINATOR = Uint8Array.from([229, 30, 138, 35, 188, 87, 230, 7]);
43
+ /** Anchor instruction discriminator `sha256("global:refund_voucher")[0..8]`. */
44
+ export const REFUND_VOUCHER_DISCRIMINATOR = Uint8Array.from([27, 159, 115, 120, 212, 202, 186, 248]);
45
+ /** Anchor account discriminator `sha256("account:PrepaidVoucher")[0..8]`. */
46
+ export const PREPAID_VOUCHER_DISCRIMINATOR = Uint8Array.from([159, 185, 203, 226, 62, 6, 57, 194]);
47
+ /** `HandleType` (state.rs): descriptive only, does not gate anything. */
48
+ export const HandleType = { Human: 0, Merchant: 1, Org: 2, Agent: 3 };
49
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
50
+ function toBytes32(v, what) {
51
+ if (typeof v === "string") {
52
+ const b = decodeBase58_32(v);
53
+ if (!b)
54
+ throw new Error(`${what} is not a valid base58 address`);
55
+ return b;
56
+ }
57
+ if (v.length !== 32)
58
+ throw new Error(`${what} must be exactly 32 bytes`);
59
+ return v;
60
+ }
61
+ function toBase58(v, what) {
62
+ return encodeBase58(toBytes32(v, what));
63
+ }
64
+ /** Borsh `String`: u32 LE length + UTF-8 bytes. */
65
+ function encString(s) {
66
+ const bytes = new TextEncoder().encode(s);
67
+ const out = new Uint8Array(4 + bytes.length);
68
+ new DataView(out.buffer).setUint32(0, bytes.length, true);
69
+ out.set(bytes, 4);
70
+ return out;
71
+ }
72
+ function encU64(v) {
73
+ const out = new Uint8Array(8);
74
+ new DataView(out.buffer).setBigInt64(0, v, true);
75
+ return out;
76
+ }
77
+ /**
78
+ * Decode a `PrepaidVoucher` account's raw data, or null if not one. Parsed
79
+ * sequentially (Borsh): the two `Option<Pubkey>` fields are 1 byte when
80
+ * absent, 33 when present, so fixed offsets after them would be wrong.
81
+ */
82
+ export function decodePrepaidVoucher(data) {
83
+ if (data.length < 8 + 32 + 32 + 1 + 1 + 8 + 1 + 1 + 2 + 8 + 8 + 1)
84
+ return null;
85
+ for (let i = 0; i < 8; i++)
86
+ if (data[i] !== PREPAID_VOUCHER_DISCRIMINATOR[i])
87
+ return null;
88
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
89
+ let o = 8;
90
+ const payer = encodeBase58(data.slice(o, o + 32));
91
+ o += 32;
92
+ const nameBytes = data.slice(o, o + 32);
93
+ o += 32;
94
+ const nameLen = data[o];
95
+ o += 1;
96
+ const name = new TextDecoder().decode(nameBytes.slice(0, nameLen));
97
+ const handleType = data[o];
98
+ o += 1;
99
+ const amount = view.getBigUint64(o, true);
100
+ o += 8;
101
+ const recipientTag = data[o];
102
+ o += 1;
103
+ let recipient = null;
104
+ if (recipientTag === 1) {
105
+ recipient = encodeBase58(data.slice(o, o + 32));
106
+ o += 32;
107
+ }
108
+ const integratorTag = data[o];
109
+ o += 1;
110
+ let integrator = null;
111
+ if (integratorTag === 1) {
112
+ integrator = encodeBase58(data.slice(o, o + 32));
113
+ o += 32;
114
+ }
115
+ const integratorRateBps = view.getUint16(o, true);
116
+ o += 2;
117
+ const expiresAt = view.getBigInt64(o, true);
118
+ o += 8;
119
+ const createdAt = view.getBigInt64(o, true);
120
+ o += 8;
121
+ const bump = data[o];
122
+ return { payer, name, handleType, amount, recipient, integrator, integratorRateBps, expiresAt, createdAt, bump };
123
+ }
124
+ /** Build `create_voucher`. */
125
+ export function buildCreateVoucherIx(p) {
126
+ const name = encString(p.name);
127
+ const recipient = p.recipient ?? null;
128
+ const recBytes = recipient === null ? Uint8Array.from([0]) : Uint8Array.from([1, ...toBytes32(recipient, "recipient")]);
129
+ const data = new Uint8Array(8 + name.length + 1 + recBytes.length + 8);
130
+ let o = 0;
131
+ data.set(CREATE_VOUCHER_DISCRIMINATOR, o);
132
+ o += 8;
133
+ data.set(name, o);
134
+ o += name.length;
135
+ data[o] = p.handleType;
136
+ o += 1;
137
+ data.set(recBytes, o);
138
+ o += recBytes.length;
139
+ data.set(encU64(BigInt(p.expiresAt)), o);
140
+ const keys = [
141
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
142
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
143
+ { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
144
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
145
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
146
+ ];
147
+ if (p.integratorAccounts)
148
+ keys.push(...p.integratorAccounts);
149
+ return { programId: toBase58(p.programId, "programId"), keys, data };
150
+ }
151
+ /** Build `claim_voucher`. */
152
+ export function buildClaimVoucherIx(p) {
153
+ const name = encString(p.name);
154
+ const data = new Uint8Array(8 + name.length);
155
+ data.set(CLAIM_VOUCHER_DISCRIMINATOR, 0);
156
+ data.set(name, 8);
157
+ const keys = [
158
+ { pubkey: toBase58(p.claimer, "claimer"), isSigner: true, isWritable: true },
159
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
160
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
161
+ { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
162
+ { pubkey: toBase58(p.voucherPayer, "voucherPayer"), isSigner: false, isWritable: true },
163
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
164
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
165
+ ];
166
+ if (p.integrator !== undefined) {
167
+ keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true });
168
+ }
169
+ return { programId: toBase58(p.programId, "programId"), keys, data };
170
+ }
171
+ /** Build `refund_voucher` — payer-signed, only valid at/after `expiresAt`. */
172
+ export function buildRefundVoucherIx(p) {
173
+ const name = encString(p.name);
174
+ const data = new Uint8Array(8 + name.length);
175
+ data.set(REFUND_VOUCHER_DISCRIMINATOR, 0);
176
+ data.set(name, 8);
177
+ return {
178
+ programId: toBase58(p.programId, "programId"),
179
+ keys: [
180
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
181
+ { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
182
+ ],
183
+ data,
184
+ };
185
+ }