sememe 0.0.0-stage → 0.4.21

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2667 -2
  3. package/bin/sememe.mjs +556 -0
  4. package/index.mjs +48 -0
  5. package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
  6. package/interface/native-11111111111111111111111111111111.json +226 -0
  7. package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
  8. package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
  9. package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
  10. package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
  11. package/interface/token-metadata.json +132 -0
  12. package/package.json +71 -4
  13. package/src/account-list.mjs +308 -0
  14. package/src/account-union.mjs +523 -0
  15. package/src/account.mjs +199 -0
  16. package/src/acquire.mjs +145 -0
  17. package/src/adopt.mjs +205 -0
  18. package/src/ata-interface.mjs +276 -0
  19. package/src/ata.mjs +223 -0
  20. package/src/base58.mjs +96 -0
  21. package/src/canonical.mjs +24 -0
  22. package/src/cli-surface.mjs +300 -0
  23. package/src/codec.mjs +1069 -0
  24. package/src/conformance.mjs +431 -0
  25. package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
  26. package/src/corpus/harvest-system-2026-10-31.json +26090 -0
  27. package/src/decode.mjs +286 -0
  28. package/src/dialect.mjs +145 -0
  29. package/src/discriminators.mjs +177 -0
  30. package/src/drift.mjs +172 -0
  31. package/src/gap.mjs +205 -0
  32. package/src/lighthouse.mjs +168 -0
  33. package/src/lock.mjs +440 -0
  34. package/src/native-audit.mjs +56 -0
  35. package/src/native-corpus.mjs +57 -0
  36. package/src/native-interface.mjs +252 -0
  37. package/src/native.mjs +324 -0
  38. package/src/peer.mjs +95 -0
  39. package/src/program-id.mjs +74 -0
  40. package/src/pull.mjs +117 -0
  41. package/src/rawle.mjs +450 -0
  42. package/src/raydium-interface.mjs +747 -0
  43. package/src/raydium.mjs +391 -0
  44. package/src/receipt-digest.mjs +84 -0
  45. package/src/receipt-name.mjs +128 -0
  46. package/src/receipt-order.mjs +56 -0
  47. package/src/shank.mjs +73 -0
  48. package/src/short-forms.mjs +209 -0
  49. package/src/store-read.mjs +250 -0
  50. package/src/store-write.mjs +46 -0
  51. package/src/token-interface.mjs +394 -0
  52. package/src/token-metadata-interface.mjs +371 -0
  53. package/src/token-metadata.mjs +317 -0
  54. package/src/token.mjs +443 -0
  55. package/src/union.mjs +428 -0
  56. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
  57. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
  58. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
  59. package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
  60. package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
  61. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
  62. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
  63. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
  64. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
  65. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
  66. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
  67. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
  68. package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
  69. package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
  70. package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
  71. package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
  72. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
  73. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
  74. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
  75. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
  76. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
@@ -0,0 +1,371 @@
1
+ // Derive the SPL Token-Metadata wire table from the VENDORED UPSTREAM RUST.
2
+ //
3
+ // Same contract as src/token-interface.mjs, but for the OTHER crate that
4
+ // Token-2022's processor falls back to: `spl-token-metadata-interface`. That
5
+ // crate is NOT a token instruction enum — it is a set of Borsh-serialized
6
+ // structs behind EIGHT-BYTE discriminators, so the whole derivation rests on
7
+ // rules the token table does not have.
8
+ //
9
+ // Why this file exists (measured, not assumed):
10
+ // Token-2022's processor (program/src/processor.rs at the pinned revision)
11
+ // tries the 1-byte PodTokenInstruction selector first and, when that
12
+ // fails, falls through to `TokenMetadataInstruction::unpack(input)`. A
13
+ // metadata instruction therefore carries an 8-byte discriminator and
14
+ // NEVER matches the token table's 1-byte tags. sememe's token decoder
15
+ // refused those payloads as `unknown-selector` — a true statement about
16
+ // the token table, and a FALSE statement about the program.
17
+ //
18
+ // The rules, each one upstream's own text:
19
+ // - the discriminator is the first 8 bytes of sha256("<namespace>:<suffix>"),
20
+ // where the namespace and the suffix are the strings upstream itself
21
+ // writes in `#[discriminator_hash_input("...")]` (the derive macro hashes
22
+ // the literal; spl-discriminator 0.5.2's `new_with_hash_input` is
23
+ // sha256 -> first 8 bytes),
24
+ // - the ARG SEQUENCE is the field order of the Borsh-serialized struct, so
25
+ // it must be READ from the struct declaration — not from a hand-typed
26
+ // list, and not from the declaration index,
27
+ // - `borsh_string` is u32 LE length + the bytes (borsh 1.8.1's `String`
28
+ // serializes as its bytes, and `str`/`[T]` as a u32 LE length first),
29
+ // - `borsh_enum` is a 1-byte variant tag followed by that variant's
30
+ // payload (`Field` is a 4-variant Borsh enum; only `Key` carries a
31
+ // nested string),
32
+ // - `borsh_option` is 1 tag byte (0 none / 1 some) + the payload —
33
+ // borsh's OWN Option encoding, NOT the C-style option the token
34
+ // instructions use,
35
+ // - `maybe_null_address` is the TRANSPARENT inner value with NO option
36
+ // tag: 32 raw bytes, where the all-zero address means `None`
37
+ // (solana-nullable's `MaybeNull<Address>` is `#[repr(transparent)]`
38
+ // and `Nullable::NONE` for `Address` is the zero address),
39
+ // - `borsh_bool` is one byte, 0 or 1 (borsh writes `u8::from(bool)`).
40
+ //
41
+ // Strictness mirrors the token table: every struct field must be covered by
42
+ // exactly one pinned rule, every variant upstream declares must be named,
43
+ // and a variant whose wire shape this module cannot pin refuses to enter
44
+ // the table instead of being guessed.
45
+ //
46
+ // The upstream behaviour this table must MIRROR is exactness: upstream's
47
+ // `unpack` calls `try_from_slice`, which REFUSES trailing bytes. A table
48
+ // that decoded a trailing byte would name bytes the program itself rejects,
49
+ // so trailing bytes are a refusal here too (see `strict` in the artifact).
50
+
51
+ import { readFileSync } from 'node:fs';
52
+ import { createHash } from 'node:crypto';
53
+ import path from 'node:path';
54
+ import { fileURLToPath } from 'node:url';
55
+
56
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
57
+ export const REPO_ROOT = path.resolve(HERE, '..');
58
+
59
+ /**
60
+ * The vendored `spl-token-metadata-interface` crate.
61
+ *
62
+ * The directory name is the crate's UPSTREAM GIT SHA (the sha1 in the
63
+ * crates.io tarball's `.cargo_vcs_info.json`), and the four source files are
64
+ * byte-identical to crates.io `spl-token-metadata-interface` 1.0.1 — the
65
+ * version Token-2022's own Cargo.lock pins.
66
+ */
67
+ export const TOKEN_METADATA_PIN = {
68
+ repo: 'solana-program/token-2022',
69
+ crate: 'spl-token-metadata-interface',
70
+ version: '1.0.1',
71
+ dir: 'spl-token-metadata-interface',
72
+ pin: '7038969ec74a89f1afdb9bc97c112d4d26fead9a',
73
+ instructionFile: 'interface/src/instruction.rs',
74
+ stateFile: 'interface/src/state.rs',
75
+ };
76
+
77
+ /** The namespace upstream declares in its own lib.rs. */
78
+ export const TOKEN_METADATA_NAMESPACE = 'spl_token_metadata_interface';
79
+
80
+ /** sha256 of `text`, hex, first `bytes` bytes — spl-discriminator's rule. */
81
+ export function discriminatorOf(text, bytes = 8) {
82
+ return createHash('sha256').update(text).digest('hex').slice(0, bytes * 2);
83
+ }
84
+
85
+ const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
86
+
87
+ /** The body of `fn ...` from comment-stripped source. */
88
+ function fnBody(src, signature) {
89
+ const at = src.indexOf(signature);
90
+ if (at < 0) throw Error(`function not found: ${signature}`);
91
+ const open = src.indexOf('{', at);
92
+ let depth = 0;
93
+ for (let i = open; i < src.length; i++) {
94
+ if (src[i] === '{') depth++;
95
+ else if (src[i] === '}') {
96
+ depth--;
97
+ if (depth === 0) return src.slice(open + 1, i);
98
+ }
99
+ }
100
+ throw Error(`${signature}: body never closes`);
101
+ }
102
+
103
+ /**
104
+ * The field declarations of ONE `pub struct` in upstream, in declaration
105
+ * order — the Borsh field order, which is the wire order.
106
+ *
107
+ * Both field forms appear upstream and BOTH must be read: `pub name: String`
108
+ * and (for the enums) a tuple variant with a nested payload.
109
+ */
110
+ export function parseStructFields(src, structName) {
111
+ const at = src.indexOf(`pub struct ${structName} {`);
112
+ if (at < 0) throw Error(`struct ${structName} not found upstream`);
113
+ const open = src.indexOf('{', at);
114
+ let depth = 0, end = -1;
115
+ for (let i = open; i < src.length; i++) {
116
+ if (src[i] === '{') depth++;
117
+ else if (src[i] === '}') {
118
+ depth--;
119
+ if (depth === 0) { end = i; break; }
120
+ }
121
+ }
122
+ const body = src.slice(open + 1, end);
123
+ // Upstream annotates fields with multi-line `#[cfg_attr(...)]` blocks
124
+ // (serde-traits) — they are not field declarations and must go before the
125
+ // split, exactly as src/token-interface.mjs does for TokenInstruction.
126
+ const cleaned = body.replace(/#\[[\s\S]*?\]\s*/g, '');
127
+ const fields = [];
128
+ for (const part of cleaned.split(',')) {
129
+ const trimmed = part.trim();
130
+ if (!trimmed) continue;
131
+ const m = trimmed.match(/^pub\s+([a-z_][a-z0-9_]*)\s*:\s*(.+)$/);
132
+ if (!m) throw Error(`${structName}: unreadable field declaration "${trimmed}"`);
133
+ fields.push({ name: m[1], type: m[2].trim() });
134
+ }
135
+ if (!fields.length) throw Error(`${structName}: no fields parsed`);
136
+ return fields;
137
+ }
138
+
139
+ /**
140
+ * The `#[discriminator_hash_input("...")]` literal of one struct, and the
141
+ * struct's own declaration.
142
+ */
143
+ function parseHashInput(src, structName) {
144
+ const at = src.indexOf(`pub struct ${structName} {`);
145
+ if (at < 0) throw Error(`struct ${structName} not found upstream`);
146
+ // The attribute sits above the struct; look backwards for it.
147
+ const before = src.slice(Math.max(0, at - 600), at);
148
+ const m = [...before.matchAll(/#\[discriminator_hash_input\("([^"]+)"\)\]/g)].pop();
149
+ if (!m) throw Error(`${structName}: no discriminator_hash_input attribute upstream`);
150
+ return m[1];
151
+ }
152
+
153
+ /**
154
+ * The `Field` enum upstream declares, as variant name -> payload type.
155
+ *
156
+ * `Field` is a Borsh enum: the wire tag is the DECLARATION INDEX (a Borsh
157
+ * enum's variant index is its discriminant), and only `Key` carries a
158
+ * payload — a nested Borsh string.
159
+ */
160
+ export function parseFieldEnum(src) {
161
+ const at = src.indexOf('pub enum Field {');
162
+ if (at < 0) throw Error('Field enum not found upstream');
163
+ const open = src.indexOf('{', at);
164
+ let depth = 0, end = -1;
165
+ for (let i = open; i < src.length; i++) {
166
+ if (src[i] === '{') depth++;
167
+ else if (src[i] === '}') {
168
+ depth--;
169
+ if (depth === 0) { end = i; break; }
170
+ }
171
+ }
172
+ const body = src.slice(open + 1, end);
173
+ const variants = [];
174
+ let i = 0;
175
+ const IDENT = /[A-Za-z0-9_]/;
176
+ while (i < body.length) {
177
+ while (i < body.length && /[\s,]/.test(body[i])) i++;
178
+ if (i >= body.length) break;
179
+ const start = i;
180
+ while (i < body.length && IDENT.test(body[i])) i++;
181
+ const name = body.slice(start, i);
182
+ if (!/^[A-Z]/.test(name)) throw Error(`Field: unexpected token "${name || body[i]}"`);
183
+ while (i < body.length && /\s/.test(body[i])) i++;
184
+ let payload = null;
185
+ if (body[i] === '(') {
186
+ const open2 = i;
187
+ let d = 0;
188
+ for (; i < body.length; i++) {
189
+ if (body[i] === '(') d++;
190
+ else if (body[i] === ')') { d--; if (d === 0) { i++; break; } }
191
+ }
192
+ payload = body.slice(open2 + 1, i - 1).trim();
193
+ }
194
+ variants.push({ name, payload });
195
+ }
196
+ if (!variants.length) throw Error('Field yielded no variants');
197
+ return variants;
198
+ }
199
+
200
+ /**
201
+ * The wire type of one upstream field, by a pinned rule.
202
+ *
203
+ * Every upstream field must map to exactly one wire type, and every wire
204
+ * type must be justified by upstream's own declaration — a field this
205
+ * function cannot classify throws, so an upstream change fails loudly
206
+ * instead of decoding as garbage.
207
+ */
208
+ function wireTypeOf(field, variant) {
209
+ const { name, type } = field;
210
+ if (type === 'String') return { type: 'borsh_string' };
211
+ if (type === 'bool') return { type: 'borsh_bool' };
212
+ if (type === 'Field') return { type: 'borsh_enum', enum: 'Field' };
213
+ if (type === 'MaybeNull<Address>') return { type: 'maybe_null_address' };
214
+ if (type === 'Option<u64>') return { type: 'borsh_option_u64' };
215
+ throw Error(`${variant}.${name}: upstream field type ${type} has no pinned wire rule`);
216
+ }
217
+
218
+ /** The wire type of the `Field` enum's own payload, by a pinned rule. */
219
+ function fieldPayloadType(payload) {
220
+ if (payload === null) return null;
221
+ if (payload === 'String') return 'borsh_string';
222
+ throw Error(`Field: payload type ${payload} has no pinned wire rule`);
223
+ }
224
+
225
+ /**
226
+ * Derive the token-metadata interface document from the vendored source.
227
+ *
228
+ * Every variant is checked in BOTH directions against upstream:
229
+ * - every variant this table names must exist upstream, with the fields
230
+ * the table claims, in the order the table claims;
231
+ * - every struct upstream declares with a `discriminator_hash_input`
232
+ * attribute must be named by this table.
233
+ * An upstream addition nobody re-reviewed therefore fails here rather than
234
+ * decoding as "unknown".
235
+ */
236
+ export function deriveTokenMetadataInterface(spec = TOKEN_METADATA_PIN, { root = REPO_ROOT } = {}) {
237
+ const base = path.join(root, 'vendor', spec.dir, spec.pin);
238
+ const instructionPath = path.join(base, spec.instructionFile);
239
+ const statePath = path.join(base, spec.stateFile);
240
+ const rawInstruction = readFileSync(instructionPath);
241
+ const rawState = readFileSync(statePath);
242
+ const fileSha256 = `sha256:${createHash('sha256').update(rawInstruction).digest('hex')}`;
243
+ const stateSha256 = `sha256:${createHash('sha256').update(rawState).digest('hex')}`;
244
+
245
+ const instructionSrc = stripComments(rawInstruction.toString('utf8'));
246
+ const stateSrc = stripComments(rawState.toString('utf8'));
247
+
248
+ // The namespace upstream declares in its own lib.rs — not a literal here.
249
+ const libPath = path.join(base, 'interface', 'src', 'lib.rs');
250
+ const libSrc = stripComments(readFileSync(libPath, 'utf8'));
251
+ const nsMatch = libSrc.match(/pub\s+const\s+NAMESPACE\s*:\s*&str\s*=\s*"([^"]+)"/);
252
+ if (!nsMatch) throw Error('NAMESPACE not found in the vendored lib.rs');
253
+ if (nsMatch[1] !== TOKEN_METADATA_NAMESPACE) {
254
+ throw Error(`vendored NAMESPACE is ${nsMatch[1]}, expected ${TOKEN_METADATA_NAMESPACE}`);
255
+ }
256
+
257
+ // The discriminator rule is anchored by upstream's OWN literal: the
258
+ // TokenMetadata account's discriminator is hardcoded in state.rs and
259
+ // upstream's own test derives it from sha256("<NAMESPACE>:token_metadata").
260
+ // If our sha256 does not reproduce that literal, the rule is wrong and
261
+ // every discriminator would be wrong — so this check must fail loudly.
262
+ const anchorMatch = stateSrc.match(/ArrayDiscriminator::new\(\[([0-9,\s]+)\]\)/);
263
+ if (!anchorMatch) throw Error('the hardcoded TokenMetadata discriminator literal not found in state.rs');
264
+ const anchorBytes = anchorMatch[1].split(',').map((n) => Number(n.trim()));
265
+ const anchorHex = anchorBytes.map((b) => b.toString(16).padStart(2, '0')).join('');
266
+ const computedAnchor = discriminatorOf(`${TOKEN_METADATA_NAMESPACE}:token_metadata`);
267
+ if (computedAnchor !== anchorHex) {
268
+ throw Error(
269
+ `the sha256 discriminator rule does not reproduce upstream's own literal: ` +
270
+ `computed ${computedAnchor}, upstream ${anchorHex}`,
271
+ );
272
+ }
273
+
274
+ // The `Field` enum, from the vendored state.rs.
275
+ const fieldVariants = parseFieldEnum(stateSrc);
276
+ const fieldTable = fieldVariants.map((variant, index) => ({
277
+ tag: index,
278
+ name: variant.name,
279
+ payload: fieldPayloadType(variant.payload),
280
+ }));
281
+
282
+ // The struct variants upstream declares with a discriminator attribute.
283
+ const declaredStructs = [...instructionSrc.matchAll(/pub\s+struct\s+([A-Z][A-Za-z0-9_]*)\s*\{/g)]
284
+ .map((m) => m[1]);
285
+ const withDiscriminator = declaredStructs.filter((name) => {
286
+ const at = instructionSrc.indexOf(`pub struct ${name} {`);
287
+ const before = instructionSrc.slice(Math.max(0, at - 600), at);
288
+ return /#\[discriminator_hash_input\("([^"]+)"\)\]/.test(before);
289
+ });
290
+ if (!withDiscriminator.length) throw Error('no struct with a discriminator attribute found upstream');
291
+
292
+ // The enum variant names upstream names, so the table's names are
293
+ // upstream's names and not this module's invention.
294
+ const enumBody = fnBody(instructionSrc, 'pub enum TokenMetadataInstruction');
295
+ const enumVariants = [...enumBody.matchAll(/([A-Z][A-Za-z0-9_]*)\s*\(/g)].map((m) => m[1]);
296
+
297
+ const instructions = withDiscriminator.map((structName) => {
298
+ // The variant name in the enum is the struct name without the action
299
+ // suffix upstream uses (`Initialize` / `UpdateField` / ...). Upstream's
300
+ // enum arm binds the same struct, so read the mapping from upstream.
301
+ const armMatch = enumBody.match(new RegExp(`([A-Z][A-Za-z0-9_]*)\\(\\s*${structName}\\b`));
302
+ if (!armMatch) throw Error(`TokenMetadataInstruction has no arm for ${structName}`);
303
+ const variantName = armMatch[1];
304
+
305
+ const hashInput = parseHashInput(instructionSrc, structName);
306
+ if (!hashInput.startsWith(`${TOKEN_METADATA_NAMESPACE}:`)) {
307
+ throw Error(`${structName}: hash input "${hashInput}" is not in the ${TOKEN_METADATA_NAMESPACE} namespace`);
308
+ }
309
+ const discriminator = discriminatorOf(hashInput);
310
+
311
+ const fields = parseStructFields(instructionSrc, structName);
312
+ const args = fields.map((field) => ({ name: field.name, ...wireTypeOf(field, variantName) }));
313
+
314
+ return {
315
+ name: variantName,
316
+ struct: structName,
317
+ discriminator,
318
+ hashInput,
319
+ args,
320
+ };
321
+ });
322
+
323
+ // Every variant upstream's enum declares must be named by this table.
324
+ const named = new Set(instructions.map((ix) => ix.name));
325
+ for (const variant of enumVariants) {
326
+ if (!named.has(variant)) throw Error(`upstream declares ${variant}, this table does not name it`);
327
+ }
328
+ for (const ix of instructions) {
329
+ if (!enumVariants.includes(ix.name)) throw Error(`table names ${ix.name}, upstream does not declare it`);
330
+ }
331
+ // No two variants may share a discriminator.
332
+ const seen = new Map();
333
+ for (const ix of instructions) {
334
+ if (seen.has(ix.discriminator)) {
335
+ throw Error(`discriminator ${ix.discriminator} is claimed by both ${seen.get(ix.discriminator)} and ${ix.name}`);
336
+ }
337
+ seen.set(ix.discriminator, ix.name);
338
+ }
339
+
340
+ return {
341
+ program: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
342
+ label: 'SPL Token Metadata (Token-2022 fallback)',
343
+ scheme: 'native-token-metadata',
344
+ source: {
345
+ repo: spec.repo,
346
+ crate: spec.crate,
347
+ version: spec.version,
348
+ pin: spec.pin,
349
+ file: spec.instructionFile,
350
+ stateFile: spec.stateFile,
351
+ fileSha256,
352
+ stateFileSha256: stateSha256,
353
+ vendoredAt: `vendor/${spec.dir}/${spec.pin}`,
354
+ derivation: 'src/token-metadata-interface.mjs: discriminator = sha256(upstream\'s own #[discriminator_hash_input])[..8], anchored by upstream\'s hardcoded TokenMetadata literal; args = the Borsh field order of the struct declaration; string = u32 LE length + bytes; enum = 1-byte variant index; option = 1 tag byte + payload; MaybeNull<Address> = transparent 32 raw bytes, all-zero = None',
355
+ },
356
+ runtime: 'Token-2022 processor at this pin: the 1-byte PodTokenInstruction selector is tried first, then TokenMetadataInstruction::unpack(input) is tried; upstream\'s unpack uses try_from_slice, so TRAILING BYTES ARE REFUSED; an unknown 8-byte discriminator is InvalidInstruction',
357
+ selector: { width: 8, encoding: 'sha256(namespace:action)[..8]' },
358
+ argEncoding: 'borsh string = u32 LE length + bytes; borsh enum = 1-byte variant index; borsh option = 1 tag byte (0 none / 1 some) + payload; bool = 1 byte; MaybeNull<Address> = 32 raw bytes, all-zero = None',
359
+ strict: {
360
+ trailingBytes: 'refuse',
361
+ reason: 'upstream unpack() calls try_from_slice, which refuses a payload with unexplained trailing bytes (verified against the upstream crate: a trailing byte is a BorshIoError)',
362
+ },
363
+ fieldEnum: fieldTable,
364
+ instructions,
365
+ };
366
+ }
367
+
368
+ /** Derive the token-metadata interface (the single pinned crate). */
369
+ export function deriveTokenMetadataInterfaces(options) {
370
+ return [deriveTokenMetadataInterface(TOKEN_METADATA_PIN, options)];
371
+ }
@@ -0,0 +1,317 @@
1
+ // The SPL Token-Metadata decoder: the Token-2022 FALLBACK path.
2
+ //
3
+ // Why this file exists: Token-2022's processor (program/src/processor.rs at
4
+ // the pinned revision) tries the 1-byte PodTokenInstruction selector first
5
+ // and, when that fails, falls through to `TokenMetadataInstruction::unpack`.
6
+ // A metadata instruction therefore carries an EIGHT-BYTE discriminator and
7
+ // never matches the token table's 1-byte tags. Before this module, sememe
8
+ // refused those payloads as `unknown-selector` — a true statement about the
9
+ // token table and a false statement about the program.
10
+ //
11
+ // The wire table is derived from the vendored upstream crate by
12
+ // src/token-metadata-interface.mjs and committed as a reviewable artifact
13
+ // (interface/token-metadata.json), and it is re-derived AT LOAD: the module
14
+ // refuses to decode unless the committed artifact equals a fresh derivation
15
+ // of the vendored bytes. That is the same two-check contract as
16
+ // src/token.mjs and src/native.mjs:
17
+ //
18
+ // 1. the derivation runs against the VENDORED BYTES themselves, so a
19
+ // swapped or edited source file cannot silently change the table it
20
+ // describes;
21
+ // 2. the committed artifact must EQUAL that derivation, so a tampered or
22
+ // stale artifact refuses the load instead of decoding from it.
23
+ //
24
+ // Strictness mirrors upstream EXACTLY, and this was verified against the
25
+ // upstream crate itself rather than assumed: upstream's `unpack` calls
26
+ // `try_from_slice`, which REFUSES a payload with unexplained trailing
27
+ // bytes. A decoder that named such a payload would claim the chain accepts
28
+ // bytes the chain rejects, so trailing bytes are a refusal here too.
29
+ //
30
+ // What is deliberately NOT claimed: the account-side layout (the TokenMetadata
31
+ // TLV account) is a different structure and is not decoded here; only the
32
+ // instruction data is.
33
+
34
+ import { readFileSync } from 'node:fs';
35
+ import path from 'node:path';
36
+ import { fileURLToPath } from 'node:url';
37
+ import { decodeBase58, encodeBase58 } from './base58.mjs';
38
+ import {
39
+ deriveTokenMetadataInterface, TOKEN_METADATA_PIN,
40
+ } from './token-metadata-interface.mjs';
41
+
42
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
43
+ const ROOT = path.resolve(HERE, '..');
44
+
45
+ export const TOKEN_METADATA_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
46
+
47
+ /**
48
+ * Load the token-metadata interface: DERIVE it from the vendored upstream
49
+ * crate, then require the committed artifact to agree with the derivation
50
+ * byte for byte.
51
+ */
52
+ function loadInterface() {
53
+ const artifactPath = path.join(ROOT, 'interface', 'token-metadata.json');
54
+ const derived = deriveTokenMetadataInterface();
55
+ let current;
56
+ try {
57
+ current = readFileSync(artifactPath, 'utf8');
58
+ } catch (error) {
59
+ throw new Error(`token-metadata interface artifact is missing: ${error.message}`);
60
+ }
61
+ const expected = `${JSON.stringify(derived, null, 2)}\n`;
62
+ if (current !== expected) {
63
+ throw new Error(
64
+ 'token-metadata interface refuses to load: interface/token-metadata.json does not equal ' +
65
+ `a fresh derivation of ${derived.source.file} (${derived.source.fileSha256})`,
66
+ );
67
+ }
68
+ if (!derived.instructions.length || derived.selector?.width !== 8) {
69
+ throw new Error('token-metadata interface artifact is malformed');
70
+ }
71
+ return derived;
72
+ }
73
+
74
+ const ARTIFACT = loadInterface();
75
+ const BY_DISCRIMINATOR = new Map(ARTIFACT.instructions.map((ix) => [ix.discriminator, ix]));
76
+ const BY_NAME = new Map(ARTIFACT.instructions.map((ix) => [ix.name, ix]));
77
+ const FIELD_BY_TAG = new Map(ARTIFACT.fieldEnum.map((entry) => [entry.tag, entry]));
78
+ const FIELD_BY_NAME = new Map(ARTIFACT.fieldEnum.map((entry) => [entry.name, entry]));
79
+
80
+ export class TokenMetadataCodecError extends Error {
81
+ constructor(reason, detail) {
82
+ super(detail ? `${reason}: ${detail}` : reason);
83
+ this.name = 'TokenMetadataCodecError';
84
+ this.reason = reason;
85
+ }
86
+ }
87
+
88
+ /** The names this table can claim. */
89
+ export function tokenMetadataNames() {
90
+ return ARTIFACT.instructions.map((entry) => entry.name);
91
+ }
92
+
93
+ /** Provenance for the token-metadata table. */
94
+ export function tokenMetadataProvenance() {
95
+ return {
96
+ label: ARTIFACT.label,
97
+ interface: `${ARTIFACT.source.crate}@${ARTIFACT.source.version} (${ARTIFACT.source.pin.slice(0, 8)}) ${ARTIFACT.source.file}`,
98
+ encoding: ARTIFACT.argEncoding,
99
+ runtime: ARTIFACT.runtime,
100
+ variants: ARTIFACT.instructions.length,
101
+ selectorWidth: ARTIFACT.selector.width,
102
+ };
103
+ }
104
+
105
+ function readLe(bytes, offset, size) {
106
+ let value = 0n;
107
+ for (let index = size - 1; index >= 0; index--) value = (value << 8n) | BigInt(bytes[offset + index]);
108
+ return value;
109
+ }
110
+
111
+ function u64ToNumber(value, what) {
112
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw Error(`${what} exceeds Number.MAX_SAFE_INTEGER`);
113
+ return Number(value);
114
+ }
115
+
116
+ /**
117
+ * Decode one arg. Returns { value, size }.
118
+ *
119
+ * `borsh_enum` needs the table (to know whether a variant carries a nested
120
+ * payload), so the FIELD enum is passed in.
121
+ */
122
+ function decodeArg(spec, bytes, offset, what) {
123
+ if (spec.type === 'borsh_string') {
124
+ if (offset + 4 > bytes.length) throw new TokenMetadataCodecError('truncated-string-length', `${what}: truncated u32 length at ${offset}`);
125
+ const length = readLe(bytes, offset, 4);
126
+ if (length > BigInt(Number.MAX_SAFE_INTEGER)) {
127
+ throw new TokenMetadataCodecError('string-too-long', `${what}: length ${length} is not a usable length`);
128
+ }
129
+ const size = Number(length);
130
+ if (offset + 4 + size > bytes.length) {
131
+ throw new TokenMetadataCodecError('truncated-string', `${what}: string claims ${size} byte(s), ${bytes.length - offset - 4} left`);
132
+ }
133
+ const raw = bytes.subarray(offset + 4, offset + 4 + size);
134
+ const text = Buffer.from(raw).toString('utf8');
135
+ if (Buffer.from(text, 'utf8').compare(raw) !== 0) {
136
+ throw new TokenMetadataCodecError('invalid-utf8', `${what}: string bytes are not valid utf8`);
137
+ }
138
+ return { value: text, size: 4 + size };
139
+ }
140
+ if (spec.type === 'borsh_bool') {
141
+ if (offset + 1 > bytes.length) throw new TokenMetadataCodecError('truncated-bool', `${what}: truncated bool at ${offset}`);
142
+ const byte = bytes[offset];
143
+ if (byte !== 0 && byte !== 1) {
144
+ throw new TokenMetadataCodecError('invalid-bool', `${what}: bool byte ${byte} (borsh writes 0 or 1)`);
145
+ }
146
+ return { value: byte === 1, size: 1 };
147
+ }
148
+ if (spec.type === 'borsh_enum') {
149
+ if (offset + 1 > bytes.length) throw new TokenMetadataCodecError('truncated-enum-tag', `${what}: truncated enum tag at ${offset}`);
150
+ const tag = bytes[offset];
151
+ const entry = FIELD_BY_TAG.get(tag);
152
+ if (!entry) throw new TokenMetadataCodecError('unknown-enum-variant', `${what}: enum tag ${tag} is not a declared variant`);
153
+ if (!entry.payload) return { value: { variant: entry.name, tag }, size: 1 };
154
+ // The variant carries a nested payload: a Borsh string.
155
+ const nested = decodeArg({ type: entry.payload }, bytes, offset + 1, `${what}.${entry.name}`);
156
+ return { value: { variant: entry.name, tag, value: nested.value }, size: 1 + nested.size };
157
+ }
158
+ if (spec.type === 'maybe_null_address') {
159
+ if (offset + 32 > bytes.length) throw new TokenMetadataCodecError('truncated-address', `${what}: truncated address at ${offset}`);
160
+ const raw = bytes.subarray(offset, offset + 32);
161
+ const isNone = raw.every((byte) => byte === 0);
162
+ return {
163
+ value: isNone ? null : encodeBase58(raw),
164
+ size: 32,
165
+ };
166
+ }
167
+ if (spec.type === 'borsh_option_u64') {
168
+ if (offset >= bytes.length) throw new TokenMetadataCodecError('truncated-option-tag', `${what}: truncated option tag at ${offset}`);
169
+ const tag = bytes[offset];
170
+ if (tag === 0) return { value: null, size: 1 };
171
+ if (tag !== 1) throw new TokenMetadataCodecError('invalid-option-tag', `${what}: option tag ${tag} (borsh writes 0 or 1)`);
172
+ if (offset + 9 > bytes.length) throw new TokenMetadataCodecError('truncated-option-payload', `${what}: truncated option payload at ${offset}`);
173
+ return { value: readLe(bytes, offset + 1, 8), size: 9 };
174
+ }
175
+ throw new TokenMetadataCodecError('unknown-arg-type', `unknown arg type ${spec.type}`);
176
+ }
177
+
178
+ function encodeArg(spec, value, what) {
179
+ if (spec.type === 'borsh_string') {
180
+ const raw = Buffer.from(String(value), 'utf8');
181
+ const length = Buffer.alloc(4);
182
+ length.writeUInt32LE(raw.length, 0);
183
+ return Buffer.concat([length, raw]);
184
+ }
185
+ if (spec.type === 'borsh_bool') {
186
+ if (typeof value !== 'boolean') throw Error(`${what}: not a boolean`);
187
+ return Buffer.from([value ? 1 : 0]);
188
+ }
189
+ if (spec.type === 'borsh_enum') {
190
+ if (value === null || typeof value !== 'object') throw Error(`${what}: not a field value`);
191
+ // The variant may be given by NAME (the human label upstream uses) or by
192
+ // its byte tag; both must resolve to the same upstream variant, and a
193
+ // name this table does not know is an error rather than a guess.
194
+ let tag;
195
+ if (typeof value.tag === 'number') {
196
+ tag = value.tag;
197
+ } else if (typeof value.variant === 'string') {
198
+ tag = FIELD_BY_NAME.get(value.variant)?.tag;
199
+ if (tag === undefined) throw Error(`${what}: unknown Field variant ${value.variant}`);
200
+ } else {
201
+ throw Error(`${what}: a field needs a variant name or tag`);
202
+ }
203
+ const entry = FIELD_BY_TAG.get(tag);
204
+ if (!entry) throw Error(`${what}: unknown Field tag ${tag}`);
205
+ if (!entry.payload) return Buffer.from([tag]);
206
+ return Buffer.concat([Buffer.from([tag]), encodeArg({ type: entry.payload }, value.value, `${what}.${entry.name}`)]);
207
+ }
208
+ if (spec.type === 'maybe_null_address') {
209
+ if (value === null || value === undefined) return Buffer.alloc(32);
210
+ const raw = Buffer.from(decodeBase58(String(value)));
211
+ if (raw.length !== 32) throw Error(`${what}: not a 32-byte address`);
212
+ return raw;
213
+ }
214
+ if (spec.type === 'borsh_option_u64') {
215
+ if (value === null || value === undefined) return Buffer.from([0]);
216
+ const v = typeof value === 'string' && /^\d+$/.test(value) ? BigInt(value)
217
+ : typeof value === 'number' ? BigInt(value)
218
+ : typeof value === 'bigint' ? value
219
+ : (() => { throw Error(`${what}: not a u64`); })();
220
+ if (v < 0n || v >= 1n << 64n) throw Error(`${what}: out of u64 range`);
221
+ const out = Buffer.alloc(9);
222
+ out[0] = 1;
223
+ for (let index = 0; index < 8; index++) out[index + 1] = Number((v >> BigInt(index * 8)) & 0xffn);
224
+ return out;
225
+ }
226
+ throw Error(`unknown arg type ${spec.type}`);
227
+ }
228
+
229
+ /**
230
+ * Decode one token-metadata instruction payload.
231
+ *
232
+ * Returns null with a NAMED REASON attached via `tokenMetadataRefusal` when
233
+ * the bytes cannot honestly be named.
234
+ *
235
+ * Trailing bytes are ALWAYS a refusal (`trailing-bytes`), because upstream's
236
+ * own `unpack` refuses them: verified against the upstream crate, where a
237
+ * trailing byte is a BorshIoError. The caller's `trailing` option cannot
238
+ * relax this — a payload with trailing bytes is one the chain REJECTS.
239
+ */
240
+ export function decodeTokenMetadataInstruction(program, bytes) {
241
+ if (program !== TOKEN_METADATA_PROGRAM) return { refused: 'not-token-metadata' };
242
+ if (!(bytes instanceof Uint8Array) || bytes.length === 0) {
243
+ throw new TokenMetadataCodecError('empty-payload', `${program}: no instruction data`);
244
+ }
245
+ if (bytes.length < 8) {
246
+ throw new TokenMetadataCodecError('truncated-discriminator', `${program}: ${bytes.length} byte(s) cannot carry an 8-byte discriminator`);
247
+ }
248
+ const discriminator = Buffer.from(bytes.subarray(0, 8)).toString('hex');
249
+ const entry = BY_DISCRIMINATOR.get(discriminator);
250
+ if (!entry) {
251
+ throw new TokenMetadataCodecError(
252
+ 'unknown-selector',
253
+ `${program}: 8-byte discriminator ${discriminator} is not in the pinned ${ARTIFACT.label} table`,
254
+ );
255
+ }
256
+ const args = {};
257
+ let offset = 8;
258
+ for (const spec of entry.args) {
259
+ const decoded = decodeArg(spec, bytes, offset, `${entry.name}.${spec.name}`);
260
+ args[spec.name] = decoded.value;
261
+ offset += decoded.size;
262
+ }
263
+ const trailingBytes = bytes.length - offset;
264
+ if (trailingBytes > 0) {
265
+ // Upstream's `unpack` calls `try_from_slice`, which REFUSES a payload
266
+ // with unexplained trailing bytes — verified against the upstream crate,
267
+ // where a trailing byte is a BorshIoError. The `trailing` option is
268
+ // therefore NOT honoured here: reporting a trailing payload as decoded
269
+ // would claim the chain accepts bytes the chain rejects.
270
+ throw new TokenMetadataCodecError(
271
+ 'trailing-bytes',
272
+ `${program}: ${entry.name} explains ${offset} of ${bytes.length} byte(s); upstream's unpack refuses trailing bytes`,
273
+ );
274
+ }
275
+ return {
276
+ name: entry.name,
277
+ discriminator,
278
+ selectorWidth: 8,
279
+ args,
280
+ consumed: offset,
281
+ bytes: bytes.length,
282
+ trailing: { count: trailingBytes },
283
+ roundTripByteExact: encodeTokenMetadataInstruction(entry.name, args).equals(Buffer.from(bytes)),
284
+ scheme: ARTIFACT.scheme,
285
+ source: 'pinned-vendored-upstream',
286
+ };
287
+ }
288
+
289
+ /** Why the last `decodeTokenMetadataInstruction` refused, or null. */
290
+ export function tokenMetadataRefusal(program, bytes) {
291
+ try {
292
+ const result = decodeTokenMetadataInstruction(program, bytes);
293
+ return result.refused ? result.refused : null;
294
+ } catch (error) {
295
+ return error instanceof TokenMetadataCodecError ? error.reason : `thrown: ${error.message}`;
296
+ }
297
+ }
298
+
299
+ /** Re-encode one token-metadata instruction from a name + args. */
300
+ export function encodeTokenMetadataInstruction(name, values = {}) {
301
+ const entry = BY_NAME.get(name);
302
+ if (!entry) throw new TokenMetadataCodecError('unknown-variant', name);
303
+ const chunks = [Buffer.from(entry.discriminator, 'hex')];
304
+ for (const spec of entry.args) {
305
+ const value = values[spec.name];
306
+ if (value === undefined && spec.type !== 'borsh_option_u64' && spec.type !== 'maybe_null_address') {
307
+ throw new TokenMetadataCodecError('missing-arg', `${name}.${spec.name}`);
308
+ }
309
+ chunks.push(encodeArg(spec, value, `${name}.${spec.name}`));
310
+ }
311
+ return Buffer.concat(chunks);
312
+ }
313
+
314
+ /** The discriminators this table claims, for coverage callers. */
315
+ export function tokenMetadataDiscriminators() {
316
+ return ARTIFACT.instructions.map((entry) => entry.discriminator);
317
+ }