@x1id/resolve 0.21.0 → 0.23.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,378 @@
1
+ /**
2
+ * TLD creator pricing (docs/specs/tld-creator-pricing.md): the SDK side of a
3
+ * TLD creator's own price table for names under their TLD.
4
+ *
5
+ * - `["ns_pricing", label]` holds a `NamespacePricing` account: a flat lamport
6
+ * price per length band (index = `tier_index(len)`: 0 = 1 char … 8 = 9 chars,
7
+ * 9 = 10+), written ONLY by the namespace creator via `set_namespace_pricing`.
8
+ * - `register_under_namespace_v2` (v1's accounts + the pricing PDA as the
9
+ * first remaining account) charges that band FLAT (no launch ramp) when
10
+ * the account exists, else the legacy root price (`Config.tier_lamports` ×
11
+ * the time/volume ramp), and refuses to charge more than the buyer's
12
+ * `max_price_lamports`.
13
+ * - The legacy `register_under_namespace` (v1) fails closed
14
+ * (`NamespacePricingRequiresV2`, 6117) on any TLD whose creator opted in.
15
+ *
16
+ * {@link quoteScopedName} reproduces the program's price selection exactly, so
17
+ * pass its `lamports` as `maxPriceLamports` to {@link buildRegisterUnderNamespaceV2Ix}.
18
+ *
19
+ * Hand-rolled like the rest of this package: no Anchor client, no
20
+ * `@solana/web3.js` import, no `@types/node`. PDAs are derived through an
21
+ * INJECTED `find_program_address` (see {@link PdaFinder}) because this package
22
+ * never hand-rolls the ed25519 on-curve check.
23
+ */
24
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
25
+ // ---------------------------------------------------------------------------
26
+ // constants (mirror programs/x1-handles/src/{lib,state}.rs)
27
+ // ---------------------------------------------------------------------------
28
+ /** Seed prefix of the `["ns_pricing", label]` PDA. */
29
+ export const NAMESPACE_PRICING_SEED = "ns_pricing";
30
+ /** `NAMESPACE_TIER_FLOOR_LAMPORTS`: every creator tier must be >= 1 XNT. */
31
+ export const NAMESPACE_TIER_FLOOR_LAMPORTS = 1000000000n;
32
+ /** `NAMESPACE_TIER_MAX_LAMPORTS`: every creator tier must be <= 100,000,000 XNT. */
33
+ export const NAMESPACE_TIER_MAX_LAMPORTS = 100000000n * 1000000000n;
34
+ /** Number of length bands (same banding as the root `Config.tier_lamports`). */
35
+ export const NAMESPACE_TIER_COUNT = 10;
36
+ /** `sha256("global:set_namespace_pricing")[0..8]`. */
37
+ export const SET_NAMESPACE_PRICING_DISCRIMINATOR = Uint8Array.from([
38
+ 245, 190, 192, 77, 94, 146, 50, 246,
39
+ ]);
40
+ /** `sha256("global:register_under_namespace_v2")[0..8]`. */
41
+ export const REGISTER_UNDER_NAMESPACE_V2_DISCRIMINATOR = Uint8Array.from([
42
+ 233, 132, 213, 179, 60, 113, 18, 247,
43
+ ]);
44
+ /** `sha256("account:NamespacePricing")[0..8]`. */
45
+ export const NAMESPACE_PRICING_ACCOUNT_DISCRIMINATOR = Uint8Array.from([
46
+ 197, 197, 26, 252, 150, 176, 211, 188,
47
+ ]);
48
+ /** `Namespace::PRICING_FLAG_EXT_OFFSET` — the one-byte "creator pricing is on"
49
+ * marker past the Anchor-allocated `Namespace` (8 + INIT_SPACE = 107). */
50
+ export const NAMESPACE_PRICING_MARKER_OFFSET = 107;
51
+ /** Program error codes added by this feature (6000 + variant index). */
52
+ export const NAMESPACE_PRICING_ERRORS = {
53
+ NamespacePricingMismatch: 6116,
54
+ NamespacePricingRequiresV2: 6117,
55
+ PriceExceedsMax: 6118,
56
+ NamespaceTierBelowFloor: 6119,
57
+ NamespaceTierAboveMax: 6120,
58
+ };
59
+ /** Anchor's `InstructionFallbackNotFound`: the deployed program does not know
60
+ * the instruction (i.e. it predates this feature). */
61
+ const ANCHOR_INSTRUCTION_FALLBACK_NOT_FOUND = 101;
62
+ // Root pricing curve — mirrors lib.rs `ramp_bps` / `quarters_elapsed` /
63
+ // `compute_price` / `price_now` exactly (same constants as app/src/lib/x1/pricing.ts).
64
+ const INTRO_START_BPS = 5000n;
65
+ const INTRO_STEP_BPS = 1250n;
66
+ const QUARTER_SECS = 91n * 24n * 60n * 60n;
67
+ const VOLUME_START_BPS = 5000n;
68
+ const VOLUME_STEP_BPS = 1250n;
69
+ const VOLUME_STEP_COUNT = 500n;
70
+ // Config byte offsets (state.rs): disc(8) admin(32) treasury(32) launch_ts(8)
71
+ // tier_lamports(80) nft_tier_lamports(80) total_handles_sold(8) ...
72
+ const CONFIG_LAUNCH_TS_OFFSET = 72;
73
+ const CONFIG_TIERS_OFFSET = 80;
74
+ const CONFIG_TOTAL_SOLD_OFFSET = 240;
75
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
76
+ const TE = new TextEncoder();
77
+ // ---------------------------------------------------------------------------
78
+ // pure helpers
79
+ // ---------------------------------------------------------------------------
80
+ function toBytes32(v, what) {
81
+ if (typeof v === "string") {
82
+ const b = decodeBase58_32(v);
83
+ if (!b)
84
+ throw new Error(`${what} is not a valid base58 address`);
85
+ return b;
86
+ }
87
+ if (v.length !== 32)
88
+ throw new Error(`${what} must be exactly 32 bytes`);
89
+ return v;
90
+ }
91
+ function toBase58(v, what) {
92
+ return encodeBase58(toBytes32(v, what));
93
+ }
94
+ function key(pubkey, what, isSigner, isWritable) {
95
+ return { pubkey: toBase58(pubkey, what), isSigner, isWritable };
96
+ }
97
+ function borshString(s) {
98
+ const b = TE.encode(s);
99
+ const out = new Uint8Array(4 + b.length);
100
+ new DataView(out.buffer).setUint32(0, b.length, true);
101
+ out.set(b, 4);
102
+ return out;
103
+ }
104
+ function u64le(n) {
105
+ const out = new Uint8Array(8);
106
+ new DataView(out.buffer).setBigUint64(0, n, true);
107
+ return out;
108
+ }
109
+ function concat(...parts) {
110
+ const out = new Uint8Array(parts.reduce((a, p) => a + p.length, 0));
111
+ let o = 0;
112
+ for (const p of parts) {
113
+ out.set(p, o);
114
+ o += p.length;
115
+ }
116
+ return out;
117
+ }
118
+ /** lib.rs `tier_index`: 1 char -> 0 … 10+ -> 9 (0 clamps to 0). */
119
+ export function scopedTierIndex(len) {
120
+ return Math.min(Math.max(len, 1), 10) - 1;
121
+ }
122
+ /** lib.rs `price_now` (registration price), in bigint like the program's u128. */
123
+ export function rootPriceNow(tierLamports, launchTs, nowSecs, totalHandlesSold) {
124
+ const elapsed = nowSecs > launchTs ? nowSecs - launchTs : 0n;
125
+ const quarters = elapsed / QUARTER_SECS;
126
+ const ramp = (start, step, n) => {
127
+ const v = start + step * n;
128
+ return v > 10000n ? 10000n : v;
129
+ };
130
+ const timeBps = ramp(INTRO_START_BPS, INTRO_STEP_BPS, quarters);
131
+ const volumeBps = ramp(VOLUME_START_BPS, VOLUME_STEP_BPS, totalHandlesSold / VOLUME_STEP_COUNT);
132
+ return (tierLamports * timeBps * volumeBps) / 10000n / 10000n;
133
+ }
134
+ /** Problems with a creator tier table, or null when the program would accept
135
+ * it (exactly `validate_namespace_tiers`). */
136
+ export function namespaceTiersProblem(tiers) {
137
+ if (tiers.length !== NAMESPACE_TIER_COUNT)
138
+ return `expected ${NAMESPACE_TIER_COUNT} tiers, got ${tiers.length}`;
139
+ for (let i = 0; i < tiers.length; i++) {
140
+ const t = tiers[i];
141
+ if (t < NAMESPACE_TIER_FLOOR_LAMPORTS)
142
+ return `tier ${i} is below the 1 XNT protocol floor`;
143
+ if (t > NAMESPACE_TIER_MAX_LAMPORTS)
144
+ return `tier ${i} is above the 100,000,000 XNT protocol maximum`;
145
+ }
146
+ return null;
147
+ }
148
+ /** The seeds of `["ns_pricing", label]`. */
149
+ export function namespacePricingSeeds(label) {
150
+ return [TE.encode(NAMESPACE_PRICING_SEED), TE.encode(label)];
151
+ }
152
+ /** Derive the canonical `["ns_pricing", label]` PDA. */
153
+ export async function deriveNamespacePricingPda(label, programId, find) {
154
+ return find(namespacePricingSeeds(label), toBase58(programId, "programId"));
155
+ }
156
+ /**
157
+ * Decode a `NamespacePricing` account's raw data (discriminator included):
158
+ * disc(8) | version(u8) | bump(u8) | tier_lamports([u64;10]) | updated_at(i64)
159
+ * | label(String). Returns null when the bytes are not a `NamespacePricing`.
160
+ */
161
+ export function decodeNamespacePricing(data) {
162
+ if (data.length < 8 + 1 + 1 + 80 + 8 + 4)
163
+ return null;
164
+ for (let i = 0; i < 8; i++) {
165
+ if (data[i] !== NAMESPACE_PRICING_ACCOUNT_DISCRIMINATOR[i])
166
+ return null;
167
+ }
168
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
169
+ const tierLamports = [];
170
+ for (let i = 0; i < NAMESPACE_TIER_COUNT; i++)
171
+ tierLamports.push(dv.getBigUint64(10 + 8 * i, true));
172
+ const updatedAt = dv.getBigInt64(90, true);
173
+ const labelLen = dv.getUint32(98, true);
174
+ if (data.length < 102 + labelLen)
175
+ return null;
176
+ let label;
177
+ try {
178
+ label = new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(102, 102 + labelLen));
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ return { version: data[8], bump: data[9], tierLamports, updatedAt, label };
184
+ }
185
+ /** Whether a raw `Namespace` account carries the "creator pricing is on"
186
+ * marker (any nonzero byte at offset 107; a shorter account = off). */
187
+ export function namespaceHasPricingMarker(namespaceData) {
188
+ const b = namespaceData[NAMESPACE_PRICING_MARKER_OFFSET];
189
+ return b !== undefined && b !== 0;
190
+ }
191
+ /**
192
+ * Build `set_namespace_pricing(label, tier_lamports)`. Accounts IN ORDER
193
+ * (`SetNamespacePricing`): creator(s,w), namespace(w), namespace_pricing(w),
194
+ * system_program. Validates the table client-side with the program's exact
195
+ * bounds so a bad table never reaches a wallet prompt.
196
+ */
197
+ export function buildSetNamespacePricingIx(p) {
198
+ const problem = namespaceTiersProblem(p.tierLamports);
199
+ if (problem)
200
+ throw new Error(problem);
201
+ return {
202
+ programId: toBase58(p.programId, "programId"),
203
+ keys: [
204
+ key(p.creator, "creator", true, true),
205
+ key(p.namespace, "namespace", false, true),
206
+ key(p.namespacePricing, "namespacePricing", false, true),
207
+ key(SYSTEM_PROGRAM, "systemProgram", false, false),
208
+ ],
209
+ data: concat(SET_NAMESPACE_PRICING_DISCRIMINATOR, borshString(p.label), ...p.tierLamports.map(u64le)),
210
+ };
211
+ }
212
+ const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
213
+ const ASSOCIATED_TOKEN_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
214
+ const TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
215
+ const SYSVAR_INSTRUCTIONS = "Sysvar1nstructions1111111111111111111111111";
216
+ /**
217
+ * Build `register_under_namespace_v2(name, handle_type, max_price_lamports)`.
218
+ * Accounts IN ORDER: the exact v1 `RegisterUnderNamespace` list (payer(s,w),
219
+ * owner, config(w), treasury(w), namespace, creator_payout(w), handle(w),
220
+ * mint(w), owner_token_account(w), metadata(w), master_edition(w),
221
+ * token_record(w), collection_mint, collection_metadata(w),
222
+ * collection_master_edition, collection_authority, token_metadata_program,
223
+ * token_program, associated_token_program, system_program,
224
+ * sysvar_instructions), then the three optional slots ALWAYS present
225
+ * (integrator, integrator_allowlist, minter_authorization — each the program
226
+ * id when absent, Anchor's "explicitly None"), then `namespace_pricing` as
227
+ * `remaining_accounts[0]`. The program fails closed if it is missing or
228
+ * shifted.
229
+ */
230
+ export function buildRegisterUnderNamespaceV2Ix(p) {
231
+ if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
232
+ throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
233
+ }
234
+ if (typeof p.maxPriceLamports !== "bigint" || p.maxPriceLamports < 0n || p.maxPriceLamports > 0xffffffffffffffffn) {
235
+ throw new Error("maxPriceLamports must be a u64 bigint");
236
+ }
237
+ if ((p.integrator === undefined) !== (p.integratorAllowlist === undefined)) {
238
+ throw new Error("integrator and integratorAllowlist must be supplied together");
239
+ }
240
+ const programId = toBase58(p.programId, "programId");
241
+ const none = () => ({ pubkey: programId, isSigner: false, isWritable: false });
242
+ const keys = [
243
+ key(p.payer, "payer", true, true),
244
+ key(p.owner, "owner", false, false),
245
+ key(p.config, "config", false, true),
246
+ key(p.treasury, "treasury", false, true),
247
+ key(p.namespace, "namespace", false, false),
248
+ key(p.creatorPayout, "creatorPayout", false, true),
249
+ key(p.handle, "handle", false, true),
250
+ key(p.mint, "mint", false, true),
251
+ key(p.ownerTokenAccount, "ownerTokenAccount", false, true),
252
+ key(p.metadata, "metadata", false, true),
253
+ key(p.masterEdition, "masterEdition", false, true),
254
+ key(p.tokenRecord, "tokenRecord", false, true),
255
+ key(p.collectionMint, "collectionMint", false, false),
256
+ key(p.collectionMetadata, "collectionMetadata", false, true),
257
+ key(p.collectionMasterEdition, "collectionMasterEdition", false, false),
258
+ key(p.collectionAuthority, "collectionAuthority", false, false),
259
+ key(TOKEN_METADATA_PROGRAM, "tokenMetadataProgram", false, false),
260
+ key(TOKEN_PROGRAM, "tokenProgram", false, false),
261
+ key(ASSOCIATED_TOKEN_PROGRAM, "associatedTokenProgram", false, false),
262
+ key(SYSTEM_PROGRAM, "systemProgram", false, false),
263
+ key(SYSVAR_INSTRUCTIONS, "sysvarInstructions", false, false),
264
+ p.integrator !== undefined ? key(p.integrator, "integrator", false, true) : none(),
265
+ p.integratorAllowlist !== undefined ? key(p.integratorAllowlist, "integratorAllowlist", false, false) : none(),
266
+ p.minterAuthorization !== undefined ? key(p.minterAuthorization, "minterAuthorization", false, false) : none(),
267
+ key(p.namespacePricing, "namespacePricing", false, false),
268
+ ];
269
+ return {
270
+ programId,
271
+ keys,
272
+ data: concat(REGISTER_UNDER_NAMESPACE_V2_DISCRIMINATOR, borshString(p.name), Uint8Array.of(p.handleType), u64le(p.maxPriceLamports)),
273
+ };
274
+ }
275
+ /** Read `launch_ts`, `tier_lamports` and `total_handles_sold` from raw Config bytes. */
276
+ export function decodeRootPricingInputs(config) {
277
+ if (config.length < CONFIG_TOTAL_SOLD_OFFSET + 8)
278
+ return null;
279
+ const dv = new DataView(config.buffer, config.byteOffset, config.byteLength);
280
+ const tierLamports = [];
281
+ for (let i = 0; i < NAMESPACE_TIER_COUNT; i++)
282
+ tierLamports.push(dv.getBigUint64(CONFIG_TIERS_OFFSET + 8 * i, true));
283
+ return {
284
+ launchTs: dv.getBigInt64(CONFIG_LAUNCH_TS_OFFSET, true),
285
+ tierLamports,
286
+ totalHandlesSold: dv.getBigUint64(CONFIG_TOTAL_SOLD_OFFSET, true),
287
+ };
288
+ }
289
+ /**
290
+ * The price `register_under_namespace_v2` charges, from already-read state —
291
+ * `scoped_registration_price` in lib.rs: the creator's band flat when a table
292
+ * exists, else the root band × the ramp.
293
+ */
294
+ export function scopedRegistrationPrice(pricing, root, nameLen, nowSecs) {
295
+ const i = scopedTierIndex(nameLen);
296
+ if (pricing)
297
+ return pricing.tierLamports[i];
298
+ return rootPriceNow(root.tierLamports[i], root.launchTs, nowSecs, root.totalHandlesSold);
299
+ }
300
+ async function readAccount(rpc, address) {
301
+ const res = (await rpc("getAccountInfo", [address, { encoding: "base64", commitment: "confirmed" }]));
302
+ const v = res?.value;
303
+ if (!v)
304
+ return null;
305
+ const bin = atob(v.data[0]);
306
+ const data = new Uint8Array(bin.length);
307
+ for (let i = 0; i < bin.length; i++)
308
+ data[i] = bin.charCodeAt(i);
309
+ return { owner: v.owner, data };
310
+ }
311
+ /**
312
+ * Quote `name.label` exactly like the program prices it (pricing-or-fallback).
313
+ * Throws when Config is missing, or when the pricing account exists but is not
314
+ * a valid `NamespacePricing` owned by the program (the program would refuse
315
+ * too), so a caller never shows a price the chain will not honour.
316
+ */
317
+ export async function quoteScopedName(label, name, opts) {
318
+ const programId = toBase58(opts.programId, "programId");
319
+ const [config, pricingAccount] = await Promise.all([
320
+ opts.find([TE.encode("config")], programId),
321
+ deriveNamespacePricingPda(label, programId, opts.find),
322
+ ]);
323
+ const [configAcct, pricingAcct] = await Promise.all([readAccount(opts.rpc, config), readAccount(opts.rpc, pricingAccount)]);
324
+ if (!configAcct)
325
+ throw new Error("registry Config not found");
326
+ const root = decodeRootPricingInputs(configAcct.data);
327
+ if (!root)
328
+ throw new Error("registry Config is too short to read pricing");
329
+ let pricing = null;
330
+ if (pricingAcct && pricingAcct.owner === programId) {
331
+ pricing = decodeNamespacePricing(pricingAcct.data);
332
+ if (!pricing)
333
+ throw new Error(`.${label} pricing account is not a valid NamespacePricing`);
334
+ }
335
+ const nowSecs = opts.nowSecs ?? BigInt(Math.floor(Date.now() / 1000));
336
+ const nameLen = TE.encode(name).length;
337
+ return {
338
+ lamports: scopedRegistrationPrice(pricing, root, nameLen, nowSecs),
339
+ source: pricing ? "custom" : "fallback",
340
+ tierIndex: scopedTierIndex(nameLen),
341
+ pricingAccount,
342
+ pricing,
343
+ };
344
+ }
345
+ // ---------------------------------------------------------------------------
346
+ // upgrade probe
347
+ // ---------------------------------------------------------------------------
348
+ /**
349
+ * The instruction to SIMULATE (never send) to learn whether the deployed
350
+ * program has this feature: `set_namespace_pricing` with no accounts. An
351
+ * upgraded program recognizes the discriminator and fails account validation
352
+ * (e.g. 3005 `AccountNotEnoughKeys`); a pre-upgrade program fails with
353
+ * Anchor's `InstructionFallbackNotFound` (101). Interpret the simulation's
354
+ * `err` with {@link interpretNamespacePricingProbe}.
355
+ */
356
+ export function buildNamespacePricingProbeIx(programId) {
357
+ return { programId: toBase58(programId, "programId"), keys: [], data: SET_NAMESPACE_PRICING_DISCRIMINATOR.slice() };
358
+ }
359
+ /**
360
+ * Read a `simulateTransaction` result's `value.err` for the probe tx (whose
361
+ * probe instruction is at `probeIndex`). `true` = the program knows the
362
+ * instruction; `false` = it does not, or the result is not the shape we
363
+ * expect (fail closed: hide the feature rather than show a broken button).
364
+ */
365
+ export function interpretNamespacePricingProbe(err, probeIndex) {
366
+ if (!err || typeof err !== "object")
367
+ return false;
368
+ const ie = err.InstructionError;
369
+ if (!Array.isArray(ie) || ie.length !== 2 || ie[0] !== probeIndex)
370
+ return false;
371
+ const detail = ie[1];
372
+ // Anchor reports both outcomes as `Custom` codes; anything else is an
373
+ // unexpected shape and reads as "not supported".
374
+ if (typeof detail === "object" && detail !== null && typeof detail.Custom === "number") {
375
+ return detail.Custom !== ANCHOR_INSTRUCTION_FALLBACK_NOT_FOUND;
376
+ }
377
+ return false;
378
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Minimal synchronous SHA-256 (FIPS 180-4). This SDK is zero-dependency and
3
+ * cannot assume WebCrypto is present AND synchronous (`crypto.subtle.digest`
4
+ * is async), so the x402 request-reference derivation (x402.ts) uses this
5
+ * pure implementation. Cross-checked against node:crypto in sdk/test/x402.test.js.
6
+ */
7
+ /** SHA-256 of `data` (bytes, or a string hashed as its UTF-8 encoding). */
8
+ export declare function sha256(data: Uint8Array | string): Uint8Array;
9
+ /** Lowercase hex of `bytes`. */
10
+ export declare function toHex(bytes: Uint8Array): string;
package/dist/sha256.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Minimal synchronous SHA-256 (FIPS 180-4). This SDK is zero-dependency and
3
+ * cannot assume WebCrypto is present AND synchronous (`crypto.subtle.digest`
4
+ * is async), so the x402 request-reference derivation (x402.ts) uses this
5
+ * pure implementation. Cross-checked against node:crypto in sdk/test/x402.test.js.
6
+ */
7
+ const K = new Uint32Array([
8
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
9
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
10
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
11
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
12
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
13
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
14
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
15
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
16
+ ]);
17
+ /** SHA-256 of `data` (bytes, or a string hashed as its UTF-8 encoding). */
18
+ export function sha256(data) {
19
+ const msg = typeof data === "string" ? new TextEncoder().encode(data) : data;
20
+ const bitLen = BigInt(msg.length) * 8n;
21
+ const padded = new Uint8Array(Math.ceil((msg.length + 9) / 64) * 64);
22
+ padded.set(msg);
23
+ padded[msg.length] = 0x80;
24
+ const dv = new DataView(padded.buffer);
25
+ dv.setBigUint64(padded.length - 8, bitLen, false);
26
+ const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
27
+ const W = new Uint32Array(64);
28
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
29
+ for (let off = 0; off < padded.length; off += 64) {
30
+ for (let t = 0; t < 16; t++)
31
+ W[t] = dv.getUint32(off + t * 4, false);
32
+ for (let t = 16; t < 64; t++) {
33
+ const w15 = W[t - 15];
34
+ const w2 = W[t - 2];
35
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
36
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
37
+ W[t] = (W[t - 16] + s0 + W[t - 7] + s1) >>> 0;
38
+ }
39
+ let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
40
+ for (let t = 0; t < 64; t++) {
41
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
42
+ const ch = (e & f) ^ (~e & g);
43
+ const t1 = (h + S1 + ch + K[t] + W[t]) >>> 0;
44
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
45
+ const maj = (a & b) ^ (a & c) ^ (b & c);
46
+ const t2 = (S0 + maj) >>> 0;
47
+ h = g;
48
+ g = f;
49
+ f = e;
50
+ e = (d + t1) >>> 0;
51
+ d = c;
52
+ c = b;
53
+ b = a;
54
+ a = (t1 + t2) >>> 0;
55
+ }
56
+ H[0] = (H[0] + a) >>> 0;
57
+ H[1] = (H[1] + b) >>> 0;
58
+ H[2] = (H[2] + c) >>> 0;
59
+ H[3] = (H[3] + d) >>> 0;
60
+ H[4] = (H[4] + e) >>> 0;
61
+ H[5] = (H[5] + f) >>> 0;
62
+ H[6] = (H[6] + g) >>> 0;
63
+ H[7] = (H[7] + h) >>> 0;
64
+ }
65
+ const out = new Uint8Array(32);
66
+ const odv = new DataView(out.buffer);
67
+ for (let i = 0; i < 8; i++)
68
+ odv.setUint32(i * 4, H[i], false);
69
+ return out;
70
+ }
71
+ /** Lowercase hex of `bytes`. */
72
+ export function toHex(bytes) {
73
+ let s = "";
74
+ for (const b of bytes)
75
+ s += b.toString(16).padStart(2, "0");
76
+ return s;
77
+ }
package/dist/voucher.d.ts CHANGED
@@ -25,18 +25,26 @@
25
25
  * a bound recipient (audit S6 — open/recipient-less vouchers were removed as
26
26
  * front-runnable; `claimSecretHash` is vestigial and rejected). An optional integrator referrer (the same trailing accounts as
27
27
  * `register`, from `integrator.ts`) is snapshotted so the claim can split
28
- * the escrow. `expiresAt` must be strictly in the future.
28
+ * the escrow. `expiresAt` must be strictly in the future and at most
29
+ * `MAX_VOUCHER_DURATION_SECS` (366 days) away (audit R4-A-2).
29
30
  * - `claim_voucher(name)` registers the handle, drawing the fee from the
30
31
  * escrow (split integrator/treasury per the snapshot), and closes the
31
32
  * voucher — rent back to the original payer. The `claimer` pays only the tx
32
- * fee and the handle's rent, never the mint fee; a relayer may claim for a
33
- * bound recipient. Supply the integrator wallet iff the voucher carries one.
33
+ * fee and the handle's rent, never the mint fee. This untokenized claim must
34
+ * be signed by the bound recipient (audit R4-A-3); a relayer uses
35
+ * `claim_voucher_tokenized`. Supply the integrator wallet iff the voucher
36
+ * carries one.
34
37
  * - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
35
- * payer-signed, only at/after `expiresAt`.
38
+ * payer-signed, at/after `expiresAt`, or earlier once the name has been
39
+ * registered by someone else (pass `handle`, audit R4-A-2).
36
40
  */
37
41
  import type { AddressLike, BuiltInstruction, InstructionKey } from "./delegate.js";
38
42
  /** Seed prefix of the voucher PDA: `["voucher", name]`. */
39
43
  export declare const VOUCHER_SEED = "voucher";
44
+ /** Longest a voucher may stay open: `create_voucher` refuses an `expiresAt`
45
+ * more than this many seconds ahead (`VoucherExpiryTooFar`, 6112; audit
46
+ * R4-A-2). Mirrors lib.rs `MAX_VOUCHER_DURATION_SECS`. */
47
+ export declare const MAX_VOUCHER_DURATION_SECS: number;
40
48
  /** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
41
49
  export declare const CREATE_VOUCHER_DISCRIMINATOR: Uint8Array;
42
50
  /** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
@@ -130,8 +138,11 @@ export interface CreateVoucherParams {
130
138
  export declare function buildCreateVoucherIx(p: CreateVoucherParams): BuiltInstruction;
131
139
  export interface ClaimVoucherParams {
132
140
  readonly programId: AddressLike;
133
- /** Pays the tx fee + the handle's rent (NOT the mint fee). Signer. May be a
134
- * relayer when the voucher binds a recipient. */
141
+ /** Pays the tx fee + the handle's rent (NOT the mint fee). Signer. For a
142
+ * bound voucher this MUST be the recipient (audit R4-A-3,
143
+ * `VoucherClaimNotRecipient` 6109): this untokenized claim skips the
144
+ * bundled pNFT, so only the recipient may opt out of it. Relayers use
145
+ * `buildClaimVoucherTokenizedIx`, which stays permissionless. */
135
146
  readonly claimer: AddressLike;
136
147
  readonly config: AddressLike;
137
148
  /** `Config.treasury`. */
@@ -160,8 +171,15 @@ export interface RefundVoucherParams {
160
171
  readonly payer: AddressLike;
161
172
  readonly voucher: AddressLike;
162
173
  readonly name: string;
174
+ /** The `["handle", name]` PDA (audit R4-A-2), appended as the trailing
175
+ * optional account. When that handle is already registered (someone else
176
+ * took the name, so the claim can never succeed) the payer may refund
177
+ * BEFORE `expiresAt`. Omit to send the original two-account form. */
178
+ readonly handle?: AddressLike;
163
179
  }
164
- /** Build `refund_voucher` — payer-signed, only valid at/after `expiresAt`. */
180
+ /** Build `refund_voucher` — payer-signed. Valid at/after `expiresAt`, or
181
+ * earlier when `handle` is passed and that name is already registered
182
+ * (audit R4-A-2). */
165
183
  export declare function buildRefundVoucherIx(p: RefundVoucherParams): BuiltInstruction;
166
184
  /** `sha256("global:claim_voucher_tokenized")[0..8]`. */
167
185
  export declare const CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR: Uint8Array;
package/dist/voucher.js CHANGED
@@ -25,18 +25,26 @@
25
25
  * a bound recipient (audit S6 — open/recipient-less vouchers were removed as
26
26
  * front-runnable; `claimSecretHash` is vestigial and rejected). An optional integrator referrer (the same trailing accounts as
27
27
  * `register`, from `integrator.ts`) is snapshotted so the claim can split
28
- * the escrow. `expiresAt` must be strictly in the future.
28
+ * the escrow. `expiresAt` must be strictly in the future and at most
29
+ * `MAX_VOUCHER_DURATION_SECS` (366 days) away (audit R4-A-2).
29
30
  * - `claim_voucher(name)` registers the handle, drawing the fee from the
30
31
  * escrow (split integrator/treasury per the snapshot), and closes the
31
32
  * voucher — rent back to the original payer. The `claimer` pays only the tx
32
- * fee and the handle's rent, never the mint fee; a relayer may claim for a
33
- * bound recipient. Supply the integrator wallet iff the voucher carries one.
33
+ * fee and the handle's rent, never the mint fee. This untokenized claim must
34
+ * be signed by the bound recipient (audit R4-A-3); a relayer uses
35
+ * `claim_voucher_tokenized`. Supply the integrator wallet iff the voucher
36
+ * carries one.
34
37
  * - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
35
- * payer-signed, only at/after `expiresAt`.
38
+ * payer-signed, at/after `expiresAt`, or earlier once the name has been
39
+ * registered by someone else (pass `handle`, audit R4-A-2).
36
40
  */
37
41
  import { encodeBase58, decodeBase58_32 } from "./base58.js";
38
42
  /** Seed prefix of the voucher PDA: `["voucher", name]`. */
39
43
  export const VOUCHER_SEED = "voucher";
44
+ /** Longest a voucher may stay open: `create_voucher` refuses an `expiresAt`
45
+ * more than this many seconds ahead (`VoucherExpiryTooFar`, 6112; audit
46
+ * R4-A-2). Mirrors lib.rs `MAX_VOUCHER_DURATION_SECS`. */
47
+ export const MAX_VOUCHER_DURATION_SECS = 366 * 24 * 60 * 60;
40
48
  /** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
41
49
  export const CREATE_VOUCHER_DISCRIMINATOR = Uint8Array.from([22, 97, 32, 21, 104, 137, 188, 143]);
42
50
  /** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
@@ -231,20 +239,22 @@ export function buildClaimVoucherIx(p) {
231
239
  }
232
240
  return { programId: toBase58(p.programId, "programId"), keys, data };
233
241
  }
234
- /** Build `refund_voucher` — payer-signed, only valid at/after `expiresAt`. */
242
+ /** Build `refund_voucher` — payer-signed. Valid at/after `expiresAt`, or
243
+ * earlier when `handle` is passed and that name is already registered
244
+ * (audit R4-A-2). */
235
245
  export function buildRefundVoucherIx(p) {
236
246
  const name = encString(p.name);
237
247
  const data = new Uint8Array(8 + name.length);
238
248
  data.set(REFUND_VOUCHER_DISCRIMINATOR, 0);
239
249
  data.set(name, 8);
240
- return {
241
- programId: toBase58(p.programId, "programId"),
242
- keys: [
243
- { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
244
- { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
245
- ],
246
- data,
247
- };
250
+ const keys = [
251
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
252
+ { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
253
+ ];
254
+ if (p.handle !== undefined) {
255
+ keys.push({ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false });
256
+ }
257
+ return { programId: toBase58(p.programId, "programId"), keys, data };
248
258
  }
249
259
  // ===================== claim_voucher_tokenized (gift NFTs, #8335) =====================
250
260
  /** `sha256("global:claim_voucher_tokenized")[0..8]`. */