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.
- package/LICENSE +21 -0
- package/README.md +2667 -2
- package/bin/sememe.mjs +556 -0
- package/index.mjs +48 -0
- package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
- package/interface/native-11111111111111111111111111111111.json +226 -0
- package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
- package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
- package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
- package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
- package/interface/token-metadata.json +132 -0
- package/package.json +71 -4
- package/src/account-list.mjs +308 -0
- package/src/account-union.mjs +523 -0
- package/src/account.mjs +199 -0
- package/src/acquire.mjs +145 -0
- package/src/adopt.mjs +205 -0
- package/src/ata-interface.mjs +276 -0
- package/src/ata.mjs +223 -0
- package/src/base58.mjs +96 -0
- package/src/canonical.mjs +24 -0
- package/src/cli-surface.mjs +300 -0
- package/src/codec.mjs +1069 -0
- package/src/conformance.mjs +431 -0
- package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
- package/src/corpus/harvest-system-2026-10-31.json +26090 -0
- package/src/decode.mjs +286 -0
- package/src/dialect.mjs +145 -0
- package/src/discriminators.mjs +177 -0
- package/src/drift.mjs +172 -0
- package/src/gap.mjs +205 -0
- package/src/lighthouse.mjs +168 -0
- package/src/lock.mjs +440 -0
- package/src/native-audit.mjs +56 -0
- package/src/native-corpus.mjs +57 -0
- package/src/native-interface.mjs +252 -0
- package/src/native.mjs +324 -0
- package/src/peer.mjs +95 -0
- package/src/program-id.mjs +74 -0
- package/src/pull.mjs +117 -0
- package/src/rawle.mjs +450 -0
- package/src/raydium-interface.mjs +747 -0
- package/src/raydium.mjs +391 -0
- package/src/receipt-digest.mjs +84 -0
- package/src/receipt-name.mjs +128 -0
- package/src/receipt-order.mjs +56 -0
- package/src/shank.mjs +73 -0
- package/src/short-forms.mjs +209 -0
- package/src/store-read.mjs +250 -0
- package/src/store-write.mjs +46 -0
- package/src/token-interface.mjs +394 -0
- package/src/token-metadata-interface.mjs +371 -0
- package/src/token-metadata.mjs +317 -0
- package/src/token.mjs +443 -0
- package/src/union.mjs +428 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
- 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
|
+
}
|