@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.
- package/README.md +130 -7
- package/dist/accounts.d.ts +86 -0
- package/dist/accounts.js +183 -0
- package/dist/attestation.d.ts +214 -0
- package/dist/attestation.js +278 -0
- package/dist/control.d.ts +201 -0
- package/dist/control.js +316 -0
- package/dist/delegate.d.ts +153 -0
- package/dist/delegate.js +166 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +53 -179
- package/dist/integrator.d.ts +117 -0
- package/dist/integrator.js +166 -0
- package/dist/lock.d.ts +180 -0
- package/dist/lock.js +211 -0
- package/dist/recordCount.d.ts +98 -0
- package/dist/recordCount.js +114 -0
- package/dist/records.d.ts +113 -0
- package/dist/records.js +178 -0
- package/dist/subname.d.ts +277 -0
- package/dist/subname.js +366 -0
- package/dist/textRecords.d.ts +248 -0
- package/dist/textRecords.js +357 -0
- package/dist/voucher.d.ts +130 -0
- package/dist/voucher.js +185 -0
- package/package.json +9 -37
- package/wasm/x1_resolve_wasm.wasm +0 -0
- package/LICENSE +0 -21
|
@@ -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;
|
package/dist/voucher.js
ADDED
|
@@ -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
|
+
}
|