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
package/src/codec.mjs
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
// Instruction-data codec driven purely by a stored on-chain IDL.
|
|
2
|
+
//
|
|
3
|
+
// Design rules:
|
|
4
|
+
// - The byte layout is read from the IDL itself; no generated code and no
|
|
5
|
+
// third-party codec library is involved (sememe's independence claim).
|
|
6
|
+
// The only non-`node:` import used to be `@solana/web3.js`, and it was
|
|
7
|
+
// used solely to base58-encode 32 raw pubkey bytes; `src/base58.mjs`
|
|
8
|
+
// does that now, so the decode core runs in a repository with no npm
|
|
9
|
+
// dependencies at all.
|
|
10
|
+
// - Decode is strict: unknown discriminators, unknown variants, invalid option
|
|
11
|
+
// tags, invalid bool bytes, out-of-range numbers and trailing bytes are
|
|
12
|
+
// errors instead of silent coercion.
|
|
13
|
+
// - Values are JSON-friendly after toJsonSafe(): 64/128-bit integers become
|
|
14
|
+
// decimal strings, pubkeys become base58 strings, options use
|
|
15
|
+
// {"__option":"None"|"Some", ...} and enums use {"__kind": "<Variant>", ...}.
|
|
16
|
+
// Field names are the exact IDL names (snake_case).
|
|
17
|
+
//
|
|
18
|
+
// Supported IDL shapes: spec 0.1.0 (Anchor on-chain) plus the legacy forms
|
|
19
|
+
// {"defined": "Name"}, ["u8", 32] and {"kind": "type"} aliases.
|
|
20
|
+
|
|
21
|
+
import { encodeBase58, decodeBase58 } from './base58.mjs';
|
|
22
|
+
import { isShankIdl, isShankDiscriminant, shankDiscriminatorBytes, SHANK_LEB128_TYPES } from './shank.mjs';
|
|
23
|
+
import { assertIdlShape } from './dialect.mjs';
|
|
24
|
+
|
|
25
|
+
const MAX_DEPTH = 32;
|
|
26
|
+
// Instruction data is bounded by transaction size; a hostile length prefix
|
|
27
|
+
// larger than this cannot occur in honest data and guards empty-element loops.
|
|
28
|
+
const HARD_VEC_CAP = 1 << 20;
|
|
29
|
+
|
|
30
|
+
export class CodecError extends Error {
|
|
31
|
+
constructor(message, path) {
|
|
32
|
+
super(path ? `${path}: ${message}` : message);
|
|
33
|
+
this.name = 'CodecError';
|
|
34
|
+
this.path = path ?? null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class Cursor {
|
|
39
|
+
constructor(bytes) {
|
|
40
|
+
// The bytes are normalized to a Buffer ONCE, here. Everything below
|
|
41
|
+
// reads them with Buffer methods: `readFloatLE`/`readDoubleLE` for
|
|
42
|
+
// f32/f64, `toString('base64')` for a `bytes` argument, and
|
|
43
|
+
// `Buffer.equals` for the round-trip check. A plain Uint8Array has
|
|
44
|
+
// none of those, so a caller that passed one got "slice.readDoubleLE is
|
|
45
|
+
// not a function" or a comma-separated byte list instead of base64.
|
|
46
|
+
// Normalizing at the boundary means the shape of the input cannot
|
|
47
|
+
// decide whether it decodes.
|
|
48
|
+
this.bytes = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer ?? bytes, bytes.byteOffset ?? 0, bytes.byteLength ?? bytes.length);
|
|
49
|
+
this.offset = 0;
|
|
50
|
+
}
|
|
51
|
+
take(count, what) {
|
|
52
|
+
if (!Number.isSafeInteger(count) || count < 0 || this.offset + count > this.bytes.length) {
|
|
53
|
+
throw new CodecError(
|
|
54
|
+
`truncated input: need ${count} byte(s) for ${what} at offset ${this.offset}, ` +
|
|
55
|
+
`only ${this.bytes.length - this.offset} left`);
|
|
56
|
+
}
|
|
57
|
+
const slice = this.bytes.subarray(this.offset, this.offset + count);
|
|
58
|
+
this.offset += count;
|
|
59
|
+
return slice;
|
|
60
|
+
}
|
|
61
|
+
get remaining() { return this.bytes.length - this.offset; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- primitives -------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const PRIMITIVES = {
|
|
67
|
+
u8: { size: 1, signed: false }, u16: { size: 2, signed: false },
|
|
68
|
+
u32: { size: 4, signed: false }, u64: { size: 8, signed: false },
|
|
69
|
+
u128: { size: 16, signed: false },
|
|
70
|
+
i8: { size: 1, signed: true }, i16: { size: 2, signed: true },
|
|
71
|
+
i32: { size: 4, signed: true }, i64: { size: 8, signed: true },
|
|
72
|
+
i128: { size: 16, signed: true },
|
|
73
|
+
};
|
|
74
|
+
// Alias accepted for readers of newer IDL drafts.
|
|
75
|
+
const ALIASES = { publicKey: 'pubkey', address: 'pubkey' };
|
|
76
|
+
|
|
77
|
+
function readLittle(cur, size, signed, path) {
|
|
78
|
+
const bytes = cur.take(size, `${size}-byte integer`);
|
|
79
|
+
let value = 0n;
|
|
80
|
+
for (let index = size - 1; index >= 0; index--) value = (value << 8n) | BigInt(bytes[index]);
|
|
81
|
+
if (signed && value >= (1n << BigInt(size * 8 - 1))) value -= 1n << BigInt(size * 8);
|
|
82
|
+
return size <= 4 ? Number(value) : value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeLittle(size, signed, input, path) {
|
|
86
|
+
let value = typeof input === 'bigint' ? input
|
|
87
|
+
: typeof input === 'number' && Number.isSafeInteger(input) ? BigInt(input)
|
|
88
|
+
: typeof input === 'string' && /^-?\d+$/.test(input) ? BigInt(input)
|
|
89
|
+
: (() => { throw new CodecError(`expected an integer, got ${describe(input)}`, path); })();
|
|
90
|
+
const bits = BigInt(size * 8);
|
|
91
|
+
if (signed) {
|
|
92
|
+
if (value < -(1n << (bits - 1n)) || value >= 1n << (bits - 1n)) {
|
|
93
|
+
throw new CodecError(`${value} out of range for ${size}-byte signed integer`, path);
|
|
94
|
+
}
|
|
95
|
+
if (value < 0n) value += 1n << bits;
|
|
96
|
+
} else {
|
|
97
|
+
if (value < 0n || value >= 1n << bits) {
|
|
98
|
+
throw new CodecError(`${value} out of range for ${size}-byte unsigned integer`, path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const out = Buffer.alloc(size);
|
|
102
|
+
for (let index = 0; index < size; index++) {
|
|
103
|
+
out[index] = Number(value & 0xffn);
|
|
104
|
+
value >>= 8n;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function describe(value) {
|
|
110
|
+
if (value === null) return 'null';
|
|
111
|
+
if (value === undefined) return 'undefined';
|
|
112
|
+
if (typeof value === 'string') return `string ${JSON.stringify(value.slice(0, 40))}`;
|
|
113
|
+
if (typeof value === 'object') return Array.isArray(value) ? 'array' : `object with keys [${Object.keys(value).slice(0, 8)}]`;
|
|
114
|
+
return `${typeof value} ${String(value).slice(0, 40)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
118
|
+
|
|
119
|
+
function decodePrimitive(name, cur, path) {
|
|
120
|
+
switch (name) {
|
|
121
|
+
case 'bool': {
|
|
122
|
+
const byte = cur.take(1, 'bool')[0];
|
|
123
|
+
if (byte === 0) return false;
|
|
124
|
+
if (byte === 1) return true;
|
|
125
|
+
throw new CodecError(`invalid bool byte ${byte} (must be 0 or 1)`, path);
|
|
126
|
+
}
|
|
127
|
+
case 'pubkey': return encodeBase58(cur.take(32, 'pubkey'));
|
|
128
|
+
case 'string': {
|
|
129
|
+
const length = readLittle(cur, 4, false, path);
|
|
130
|
+
try { return utf8Decoder.decode(cur.take(length, 'utf-8 string')); }
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof CodecError) throw error;
|
|
133
|
+
throw new CodecError(`invalid utf-8: ${error.message}`, path);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
case 'bytes': {
|
|
137
|
+
const length = readLittle(cur, 4, false, path);
|
|
138
|
+
return cur.take(length, 'bytes').toString('base64');
|
|
139
|
+
}
|
|
140
|
+
case 'f32': { const slice = cur.take(4, 'f32'); return slice.readFloatLE(0); }
|
|
141
|
+
case 'f64': { const slice = cur.take(8, 'f64'); return slice.readDoubleLE(0); }
|
|
142
|
+
default: {
|
|
143
|
+
const primitive = PRIMITIVES[name];
|
|
144
|
+
if (!primitive) throw new CodecError(`unknown primitive type ${JSON.stringify(name)}`, path);
|
|
145
|
+
return readLittle(cur, primitive.size, primitive.signed, path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function encodePrimitive(name, value, path) {
|
|
151
|
+
switch (name) {
|
|
152
|
+
case 'bool': {
|
|
153
|
+
if (value !== true && value !== false) throw new CodecError(`expected true/false, got ${describe(value)}`, path);
|
|
154
|
+
return Buffer.from([value ? 1 : 0]);
|
|
155
|
+
}
|
|
156
|
+
case 'pubkey': {
|
|
157
|
+
if (value instanceof Uint8Array || Array.isArray(value)) {
|
|
158
|
+
if (value.length !== 32) throw new CodecError(`pubkey must be 32 bytes, got ${value.length}`, path);
|
|
159
|
+
return Buffer.from(value);
|
|
160
|
+
}
|
|
161
|
+
if (typeof value !== 'string') throw new CodecError(`expected a base58 pubkey, got ${describe(value)}`, path);
|
|
162
|
+
try {
|
|
163
|
+
const decoded = decodeBase58(value);
|
|
164
|
+
// `PublicKey` enforced exactly this: a base58 string that decodes to 32 bytes.
|
|
165
|
+
if (decoded.length !== 32) {
|
|
166
|
+
throw new CodecError(`invalid pubkey: expected 32 bytes, got ${decoded.length}`, path);
|
|
167
|
+
}
|
|
168
|
+
return Buffer.from(decoded);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof CodecError) throw error;
|
|
171
|
+
throw new CodecError(`invalid pubkey: ${error.message}`, path);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
case 'string': {
|
|
175
|
+
if (typeof value !== 'string') throw new CodecError(`expected a string, got ${describe(value)}`, path);
|
|
176
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
177
|
+
if (Buffer.compare(Buffer.from(bytes, 'utf8'), Buffer.from(value, 'utf8')) !== 0) {
|
|
178
|
+
/* round-trip check is redundant; utf8 encoding is lossless for valid strings */
|
|
179
|
+
}
|
|
180
|
+
return Buffer.concat([writeLittle(4, false, bytes.length, path), bytes]);
|
|
181
|
+
}
|
|
182
|
+
case 'bytes': {
|
|
183
|
+
if (typeof value !== 'string') throw new CodecError(`expected base64 for bytes, got ${describe(value)}`, path);
|
|
184
|
+
const bytes = Buffer.from(value, 'base64');
|
|
185
|
+
if (bytes.toString('base64').replace(/=+$/, '') !== value.replace(/=+$/, '')) {
|
|
186
|
+
throw new CodecError('invalid base64 payload', path);
|
|
187
|
+
}
|
|
188
|
+
return Buffer.concat([writeLittle(4, false, bytes.length, path), bytes]);
|
|
189
|
+
}
|
|
190
|
+
case 'f32': {
|
|
191
|
+
if (typeof value !== 'number') throw new CodecError(`expected a number, got ${describe(value)}`, path);
|
|
192
|
+
const out = Buffer.alloc(4); out.writeFloatLE(value, 0); return out;
|
|
193
|
+
}
|
|
194
|
+
case 'f64': {
|
|
195
|
+
if (typeof value !== 'number') throw new CodecError(`expected a number, got ${describe(value)}`, path);
|
|
196
|
+
const out = Buffer.alloc(8); out.writeDoubleLE(value, 0); return out;
|
|
197
|
+
}
|
|
198
|
+
default: {
|
|
199
|
+
const primitive = PRIMITIVES[name];
|
|
200
|
+
if (!primitive) throw new CodecError(`unknown primitive type ${JSON.stringify(name)}`, path);
|
|
201
|
+
return writeLittle(primitive.size, primitive.signed, value, path);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- type dispatch ----------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
function assign(target, key, value, path) {
|
|
209
|
+
if (key === '__kind' || key === '__option') {
|
|
210
|
+
// defineProperty would still shadow the representation keys on decode.
|
|
211
|
+
throw new CodecError(`field name ${JSON.stringify(key)} collides with the decoded representation`, path);
|
|
212
|
+
}
|
|
213
|
+
Object.defineProperty(target, key, {
|
|
214
|
+
value, enumerable: true, writable: true, configurable: true,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function resolveDefined(typeName, ctx, path) {
|
|
219
|
+
const typedef = ctx.types.get(typeName);
|
|
220
|
+
if (!typedef) throw new CodecError(`unknown defined type ${JSON.stringify(typeName)}`, path);
|
|
221
|
+
return typedef.type;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- shank LEB128 length dialect --------------------------------------------
|
|
225
|
+
//
|
|
226
|
+
// The lighthouse family (shank) encodes array/string lengths as unsigned
|
|
227
|
+
// LEB128, not Borsh's fixed u32 — and ships the types (`CompactU64`,
|
|
228
|
+
// `CompactBytes`, `*Assertions`) WITHOUT definitions. They are hooked at the
|
|
229
|
+
// `defined` branch below. Two rules keep this from becoming a guess:
|
|
230
|
+
// - only an IDL in the shank dialect engages them (Anchor IDLs keep strict
|
|
231
|
+
// u32 lengths even for a same-named type);
|
|
232
|
+
// - a name the IDL itself defines always wins (ctx.types is checked first).
|
|
233
|
+
|
|
234
|
+
function readLeb128(cur, path) {
|
|
235
|
+
let value = 0n;
|
|
236
|
+
let shift = 0n;
|
|
237
|
+
for (;;) {
|
|
238
|
+
if (shift >= 70n) throw new CodecError('LEB128 longer than 10 bytes', path);
|
|
239
|
+
const byte = cur.take(1, 'LEB128 byte')[0];
|
|
240
|
+
value |= BigInt(byte & 0x7f) << shift;
|
|
241
|
+
if ((byte & 0x80) === 0) break;
|
|
242
|
+
shift += 7n;
|
|
243
|
+
}
|
|
244
|
+
if (value > 0xffffffffffffffffn) throw new CodecError(`LEB128 value ${value} exceeds u64`, path);
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function writeLeb128(value, path) {
|
|
249
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
250
|
+
throw new CodecError(`LEB128 needs a non-negative safe integer, got ${describe(value)}`, path);
|
|
251
|
+
}
|
|
252
|
+
const out = [];
|
|
253
|
+
let rest = value;
|
|
254
|
+
for (;;) {
|
|
255
|
+
let byte = rest & 0x7f;
|
|
256
|
+
rest >>>= 7;
|
|
257
|
+
if (rest !== 0) byte |= 0x80;
|
|
258
|
+
out.push(byte);
|
|
259
|
+
if (rest === 0) return Buffer.from(out);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The hooked LEB128 type for `name`, or null: engaged ONLY for an IDL in the
|
|
265
|
+
* shank dialect. Anchor IDLs keep refusing an undefined `CompactU64` even if
|
|
266
|
+
* same-named — the length dialect belongs to the IDL, not to the string.
|
|
267
|
+
*/
|
|
268
|
+
function shankLeb128Type(name, ctx) {
|
|
269
|
+
if (!ctx.shank) return null;
|
|
270
|
+
return SHANK_LEB128_TYPES.get(name) ?? null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function arrayParts(type) {
|
|
274
|
+
const array = type.array;
|
|
275
|
+
if (Array.isArray(array)) {
|
|
276
|
+
if (array.length !== 2) throw null;
|
|
277
|
+
return { inner: array[0], length: array[1] };
|
|
278
|
+
}
|
|
279
|
+
if (array && typeof array === 'object' && 'inner' in array && 'len' in array) {
|
|
280
|
+
return { inner: array.inner, length: array.len };
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function variantFields(variant) {
|
|
286
|
+
const fields = variant.fields;
|
|
287
|
+
if (fields === undefined || fields === null) return { kind: 'unit', fields: [] };
|
|
288
|
+
if (!Array.isArray(fields)) return null;
|
|
289
|
+
if (fields.length === 0) return { kind: 'tuple', fields: [] };
|
|
290
|
+
const named = fields.every((field) => field && typeof field === 'object'
|
|
291
|
+
&& typeof field.name === 'string' && 'type' in field);
|
|
292
|
+
if (named) return { kind: 'named', fields };
|
|
293
|
+
const plain = fields.every((field) => typeof field === 'string' || (field && typeof field === 'object' && !('name' in field)));
|
|
294
|
+
if (plain) return { kind: 'tuple', fields };
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function structShape(type) {
|
|
299
|
+
if (type && typeof type === 'object' && Array.isArray(type.fields)
|
|
300
|
+
&& (type.kind === 'struct' || type.kind === undefined)) return type.fields;
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// A Rust tuple struct (`pub struct OptionBool(pub bool)`) is described by bare
|
|
305
|
+
// TYPES in the fields array — no {name,type} pairs. Decoded shape follows Anchor's
|
|
306
|
+
// own borsh coder: positional string keys {"0": value}, which is NOT an array and
|
|
307
|
+
// NOT an unwrapped value, so a reader that guesses one of those disagrees with the
|
|
308
|
+
// reference implementation on bytes that are identical either way.
|
|
309
|
+
function isTupleStructFields(fields) {
|
|
310
|
+
if (!Array.isArray(fields) || fields.length === 0) return false;
|
|
311
|
+
return fields.every((field) =>
|
|
312
|
+
typeof field === 'string'
|
|
313
|
+
|| (field && typeof field === 'object' && !('name' in field)));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isNamedStructFields(fields) {
|
|
317
|
+
return Array.isArray(fields) && fields.length > 0 && fields.every((field) =>
|
|
318
|
+
field && typeof field === 'object' && typeof field.name === 'string' && 'type' in field);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function enumShape(type) {
|
|
322
|
+
if (type && typeof type === 'object' && Array.isArray(type.variants)
|
|
323
|
+
&& (type.kind === 'enum' || type.kind === undefined)) return type.variants;
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function decodeType(type, bytes, ctx, path = 'value', depth = 0, cursor = null) {
|
|
328
|
+
const cur = cursor ?? new Cursor(bytes);
|
|
329
|
+
const value = decodeAt(type, cur, ctx, path, depth);
|
|
330
|
+
if (cursor === null) {
|
|
331
|
+
if (cur.remaining !== 0) throw new CodecError(`trailing bytes: consumed ${cur.offset} of ${cur.bytes.length}`, path);
|
|
332
|
+
return { value, consumed: cur.offset };
|
|
333
|
+
}
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function decodeAt(type, cur, ctx, path, depth) {
|
|
338
|
+
if (depth > MAX_DEPTH) throw new CodecError('type nesting too deep (recursive type?)', path);
|
|
339
|
+
|
|
340
|
+
if (typeof type === 'string') return decodePrimitive(ALIASES[type] ?? type, cur, path);
|
|
341
|
+
|
|
342
|
+
if (Array.isArray(type)) { // legacy ["u8", 32]
|
|
343
|
+
if (type.length !== 2) throw new CodecError(`malformed array type ${JSON.stringify(type).slice(0, 60)}`, path);
|
|
344
|
+
return decodeArray(type[0], type[1], cur, ctx, path, depth);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!type || typeof type !== 'object') {
|
|
348
|
+
throw new CodecError(`unsupported type ${JSON.stringify(type)?.slice(0, 80)}`, path);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if ('option' in type) {
|
|
352
|
+
const tag = cur.take(1, 'option tag')[0];
|
|
353
|
+
if (tag === 0) return { __option: 'None' };
|
|
354
|
+
if (tag !== 1) throw new CodecError(`invalid option tag ${tag} (must be 0 or 1)`, path);
|
|
355
|
+
return { __option: 'Some', value: decodeAt(type.option, cur, ctx, `${path}.value`, depth + 1) };
|
|
356
|
+
}
|
|
357
|
+
if ('vec' in type) {
|
|
358
|
+
const length = readLittle(cur, 4, false, path);
|
|
359
|
+
const minimum = minSize(type.vec, ctx);
|
|
360
|
+
if (minimum === 0 && length > HARD_VEC_CAP) {
|
|
361
|
+
throw new CodecError(`vector length ${length} exceeds limit for ${path}`, path);
|
|
362
|
+
}
|
|
363
|
+
if (minimum !== null && BigInt(length) * BigInt(minimum) > BigInt(cur.remaining)) {
|
|
364
|
+
throw new CodecError(`vector length ${length} needs ${length * minimum} byte(s) but ${cur.remaining} remain`, path);
|
|
365
|
+
}
|
|
366
|
+
const out = [];
|
|
367
|
+
for (let index = 0; index < length; index++) {
|
|
368
|
+
out.push(decodeAt(type.vec, cur, ctx, `${path}[${index}]`, depth + 1));
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
if ('array' in type) {
|
|
373
|
+
const parts = arrayParts(type);
|
|
374
|
+
if (!parts) throw new CodecError(`malformed array type ${JSON.stringify(type.array).slice(0, 60)}`, path);
|
|
375
|
+
if (!Number.isSafeInteger(parts.length) || parts.length < 0) {
|
|
376
|
+
throw new CodecError(`array length must be a non-negative integer, got ${JSON.stringify(parts.length)}`, path);
|
|
377
|
+
}
|
|
378
|
+
return decodeArray(parts.inner, parts.length, cur, ctx, path, depth);
|
|
379
|
+
}
|
|
380
|
+
if ('defined' in type) {
|
|
381
|
+
const name = typeof type.defined === 'string' ? type.defined : type.defined?.name;
|
|
382
|
+
if (typeof name !== 'string') throw new CodecError('malformed defined type', path);
|
|
383
|
+
// A name the IDL itself defines always wins; only an UNDEFINED name may
|
|
384
|
+
// fall back to the shank LEB128 hook.
|
|
385
|
+
if (!ctx.types.has(name)) {
|
|
386
|
+
const hooked = shankLeb128Type(name, ctx);
|
|
387
|
+
if (hooked) {
|
|
388
|
+
if (hooked.kind === 'scalar') {
|
|
389
|
+
const value = readLeb128(cur, path);
|
|
390
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
391
|
+
throw new CodecError(`LEB128 ${value} exceeds the JS safe integer range`, path);
|
|
392
|
+
}
|
|
393
|
+
return Number(value);
|
|
394
|
+
}
|
|
395
|
+
const length = Number(readLeb128(cur, path));
|
|
396
|
+
if (hooked.kind === 'bytes') {
|
|
397
|
+
return Array.from(cur.take(length, 'CompactBytes'));
|
|
398
|
+
}
|
|
399
|
+
const element = { defined: hooked.element };
|
|
400
|
+
const out = [];
|
|
401
|
+
for (let index = 0; index < length; index++) {
|
|
402
|
+
out.push(decodeAt(element, cur, ctx, `${path}[${index}]`, depth + 1));
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
ctx.missing?.add(name);
|
|
408
|
+
return decodeAt(resolveDefined(name, ctx, path), cur, ctx, name, depth + 1);
|
|
409
|
+
}
|
|
410
|
+
if ('generic' in type) {
|
|
411
|
+
throw new CodecError(`generic parameter ${JSON.stringify(type.generic)} cannot be decoded`, path);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (type.kind === 'type') return decodeAt(type.type, cur, ctx, path, depth + 1);
|
|
415
|
+
|
|
416
|
+
const fields = structShape(type);
|
|
417
|
+
if (fields) {
|
|
418
|
+
if (isTupleStructFields(fields)) {
|
|
419
|
+
const out = {};
|
|
420
|
+
fields.forEach((fieldType, position) => {
|
|
421
|
+
const key = String(position);
|
|
422
|
+
assign(out, key, decodeAt(fieldType, cur, ctx, `${path}.${key}`, depth + 1), path);
|
|
423
|
+
});
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
if (!isNamedStructFields(fields)) {
|
|
427
|
+
throw new CodecError('malformed struct field', path);
|
|
428
|
+
}
|
|
429
|
+
const out = {};
|
|
430
|
+
for (const field of fields) {
|
|
431
|
+
assign(out, field.name, decodeAt(field.type, cur, ctx, `${path}.${field.name}`, depth + 1), path);
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const variants = enumShape(type);
|
|
437
|
+
if (variants) {
|
|
438
|
+
const index = cur.take(1, 'enum variant index')[0];
|
|
439
|
+
if (index >= variants.length) {
|
|
440
|
+
throw new CodecError(`unknown variant index ${index} (type has ${variants.length} variant(s))`, path);
|
|
441
|
+
}
|
|
442
|
+
const variant = variants[index];
|
|
443
|
+
const shape = variantFields(variant);
|
|
444
|
+
if (!shape) throw new CodecError(`malformed variant ${JSON.stringify(variant.name)}`, path);
|
|
445
|
+
const out = { __kind: variant.name };
|
|
446
|
+
if (shape.kind === 'unit') return out;
|
|
447
|
+
if (shape.kind === 'tuple') {
|
|
448
|
+
out.fields = shape.fields.map((fieldType, position) =>
|
|
449
|
+
decodeAt(fieldType, cur, ctx, `${path}.fields[${position}]`, depth + 1));
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
for (const field of shape.fields) {
|
|
453
|
+
assign(out, field.name, decodeAt(field.type, cur, ctx, `${path}.${field.name}`, depth + 1), path);
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
throw new CodecError(`unsupported type ${JSON.stringify(type).slice(0, 80)}`, path);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function decodeArray(inner, length, cur, ctx, path, depth) {
|
|
462
|
+
const out = [];
|
|
463
|
+
for (let index = 0; index < length; index++) {
|
|
464
|
+
out.push(decodeAt(inner, cur, ctx, `${path}[${index}]`, depth + 1));
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// --- encoder ----------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
function encodeAt(type, value, ctx, path, depth) {
|
|
472
|
+
if (depth > MAX_DEPTH) throw new CodecError('type nesting too deep (recursive type?)', path);
|
|
473
|
+
|
|
474
|
+
if (typeof type === 'string') return encodePrimitive(ALIASES[type] ?? type, value, path);
|
|
475
|
+
|
|
476
|
+
if (Array.isArray(type)) {
|
|
477
|
+
if (type.length !== 2) throw new CodecError(`malformed array type ${JSON.stringify(type).slice(0, 60)}`, path);
|
|
478
|
+
return encodeArray(type[0], type[1], value, ctx, path, depth);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!type || typeof type !== 'object') {
|
|
482
|
+
throw new CodecError(`unsupported type ${JSON.stringify(type)?.slice(0, 80)}`, path);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if ('option' in type) {
|
|
486
|
+
if (!value || typeof value !== 'object' || !('__option' in value)) {
|
|
487
|
+
throw new CodecError(`expected {"__option":"None"|"Some"} object, got ${describe(value)}`, path);
|
|
488
|
+
}
|
|
489
|
+
const extra = Object.keys(value).filter((key) => key !== '__option' && key !== 'value');
|
|
490
|
+
if (extra.length) throw new CodecError(`unexpected keys in option: ${extra.join(', ')}`, path);
|
|
491
|
+
if (value.__option === 'None') return Buffer.from([0]);
|
|
492
|
+
if (value.__option !== 'Some') {
|
|
493
|
+
throw new CodecError(`__option must be "None" or "Some", got ${JSON.stringify(value.__option)}`, path);
|
|
494
|
+
}
|
|
495
|
+
if (!('value' in value)) throw new CodecError('option "Some" requires a "value" key', path);
|
|
496
|
+
return Buffer.concat([Buffer.from([1]), encodeAt(type.option, value.value, ctx, `${path}.value`, depth + 1)]);
|
|
497
|
+
}
|
|
498
|
+
if ('vec' in type) {
|
|
499
|
+
if (!Array.isArray(value)) throw new CodecError(`expected an array, got ${describe(value)}`, path);
|
|
500
|
+
if (value.length > 0xffffffff) throw new CodecError(`vector length ${value.length} exceeds u32`, path);
|
|
501
|
+
const parts = [writeLittle(4, false, value.length, path)];
|
|
502
|
+
value.forEach((item, index) =>
|
|
503
|
+
parts.push(encodeAt(type.vec, item, ctx, `${path}[${index}]`, depth + 1)));
|
|
504
|
+
return Buffer.concat(parts);
|
|
505
|
+
}
|
|
506
|
+
if ('array' in type) {
|
|
507
|
+
const parts = arrayParts(type);
|
|
508
|
+
if (!parts) throw new CodecError(`malformed array type ${JSON.stringify(type.array).slice(0, 60)}`, path);
|
|
509
|
+
return encodeArray(parts.inner, parts.length, value, ctx, path, depth);
|
|
510
|
+
}
|
|
511
|
+
if ('defined' in type) {
|
|
512
|
+
const name = typeof type.defined === 'string' ? type.defined : type.defined?.name;
|
|
513
|
+
if (typeof name !== 'string') throw new CodecError('malformed defined type', path);
|
|
514
|
+
if (!ctx.types.has(name)) {
|
|
515
|
+
const hooked = shankLeb128Type(name, ctx);
|
|
516
|
+
if (hooked) {
|
|
517
|
+
if (hooked.kind === 'scalar') return writeLeb128(value, path);
|
|
518
|
+
if (!Array.isArray(value)) throw new CodecError(`expected an array for ${name}, got ${describe(value)}`, path);
|
|
519
|
+
const length = writeLeb128(value.length, path);
|
|
520
|
+
if (hooked.kind === 'bytes') {
|
|
521
|
+
if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
|
522
|
+
throw new CodecError(`${name} must be bytes 0-255, got ${describe(value)}`, path);
|
|
523
|
+
}
|
|
524
|
+
return Buffer.concat([length, Buffer.from(value)]);
|
|
525
|
+
}
|
|
526
|
+
const element = { defined: hooked.element };
|
|
527
|
+
return Buffer.concat(
|
|
528
|
+
[length, ...value.map((entry) => encodeAt(element, entry, ctx, `${path}[]`, depth + 1))]);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return encodeAt(resolveDefined(name, ctx, path), value, ctx, name, depth + 1);
|
|
532
|
+
}
|
|
533
|
+
if ('generic' in type) {
|
|
534
|
+
throw new CodecError(`generic parameter ${JSON.stringify(type.generic)} cannot be encoded`, path);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (type.kind === 'type') return encodeAt(type.type, value, ctx, path, depth + 1);
|
|
538
|
+
|
|
539
|
+
const fields = structShape(type);
|
|
540
|
+
if (fields) {
|
|
541
|
+
if (isTupleStructFields(fields)) {
|
|
542
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
543
|
+
throw new CodecError(`expected a tuple-struct object like {"0": …}, got ${describe(value)}`, path);
|
|
544
|
+
}
|
|
545
|
+
const expected = fields.map((_, position) => String(position));
|
|
546
|
+
const unknown = Object.keys(value).filter((key) => !expected.includes(key));
|
|
547
|
+
if (unknown.length) throw new CodecError(`unexpected field(s): ${unknown.join(', ')}`, path);
|
|
548
|
+
const parts = [];
|
|
549
|
+
for (const key of expected) {
|
|
550
|
+
if (!(key in value)) throw new CodecError(`missing field ${JSON.stringify(key)}`, path);
|
|
551
|
+
parts.push(encodeAt(fields[Number(key)], value[key], ctx, `${path}.${key}`, depth + 1));
|
|
552
|
+
}
|
|
553
|
+
return Buffer.concat(parts);
|
|
554
|
+
}
|
|
555
|
+
if (!isNamedStructFields(fields)) {
|
|
556
|
+
throw new CodecError('malformed struct field', path);
|
|
557
|
+
}
|
|
558
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
559
|
+
throw new CodecError(`expected a struct object, got ${describe(value)}`, path);
|
|
560
|
+
}
|
|
561
|
+
const expected = new Set(fields.map((field) => field.name));
|
|
562
|
+
const unknown = Object.keys(value).filter((key) => !expected.has(key));
|
|
563
|
+
if (unknown.length) throw new CodecError(`unexpected field(s): ${unknown.join(', ')}`, path);
|
|
564
|
+
const parts = [];
|
|
565
|
+
for (const field of fields) {
|
|
566
|
+
if (!(field.name in value)) throw new CodecError(`missing field ${JSON.stringify(field.name)}`, path);
|
|
567
|
+
parts.push(encodeAt(field.type, value[field.name], ctx, `${path}.${field.name}`, depth + 1));
|
|
568
|
+
}
|
|
569
|
+
return Buffer.concat(parts);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const variants = enumShape(type);
|
|
573
|
+
if (variants) {
|
|
574
|
+
if (!value || typeof value !== 'object' || typeof value.__kind !== 'string') {
|
|
575
|
+
throw new CodecError(`expected {"__kind": "<variant>"}, got ${describe(value)}`, path);
|
|
576
|
+
}
|
|
577
|
+
const index = variants.findIndex((variant) => variant.name === value.__kind);
|
|
578
|
+
if (index < 0) {
|
|
579
|
+
throw new CodecError(`unknown variant ${JSON.stringify(value.__kind)}; expected one of [${variants.map((v) => v.name).join(', ')}]`, path);
|
|
580
|
+
}
|
|
581
|
+
if (index > 0xff) throw new CodecError(`variant index ${index} does not fit in one byte`, path);
|
|
582
|
+
const variant = variants[index];
|
|
583
|
+
const shape = variantFields(variant);
|
|
584
|
+
if (!shape) throw new CodecError(`malformed variant ${JSON.stringify(variant.name)}`, path);
|
|
585
|
+
const extra = Object.keys(value).filter((key) =>
|
|
586
|
+
key !== '__kind' && !(shape.kind === 'tuple' && key === 'fields') && !shape.fields.some((f) => f.name === key));
|
|
587
|
+
if (extra.length) throw new CodecError(`unexpected key(s) for variant ${variant.name}: ${extra.join(', ')}`, path);
|
|
588
|
+
const parts = [Buffer.from([index])];
|
|
589
|
+
if (shape.kind === 'unit') return Buffer.concat(parts);
|
|
590
|
+
if (shape.kind === 'tuple') {
|
|
591
|
+
if (!Array.isArray(value.fields)) throw new CodecError(`variant ${variant.name} requires a "fields" array`, path);
|
|
592
|
+
if (value.fields.length !== shape.fields.length) {
|
|
593
|
+
throw new CodecError(`variant ${variant.name} expects ${shape.fields.length} field(s), got ${value.fields.length}`, path);
|
|
594
|
+
}
|
|
595
|
+
shape.fields.forEach((fieldType, position) =>
|
|
596
|
+
parts.push(encodeAt(fieldType, value.fields[position], ctx, `${path}.fields[${position}]`, depth + 1)));
|
|
597
|
+
return Buffer.concat(parts);
|
|
598
|
+
}
|
|
599
|
+
for (const field of shape.fields) {
|
|
600
|
+
if (!(field.name in value)) throw new CodecError(`missing field ${JSON.stringify(field.name)}`, path);
|
|
601
|
+
parts.push(encodeAt(field.type, value[field.name], ctx, `${path}.${field.name}`, depth + 1));
|
|
602
|
+
}
|
|
603
|
+
return Buffer.concat(parts);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
throw new CodecError(`unsupported type ${JSON.stringify(type).slice(0, 80)}`, path);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function encodeArray(inner, length, value, ctx, path, depth) {
|
|
610
|
+
if (!Array.isArray(value)) throw new CodecError(`expected an array of length ${length}, got ${describe(value)}`, path);
|
|
611
|
+
if (value.length !== length) throw new CodecError(`expected an array of length ${length}, got ${value.length} element(s)`, path);
|
|
612
|
+
return Buffer.concat(value.map((item, index) =>
|
|
613
|
+
encodeAt(inner, item, ctx, `${path}[${index}]`, depth + 1)));
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// --- sizing (for hostile-length pre-checks only) -----------------------------
|
|
617
|
+
|
|
618
|
+
function minSize(type, ctx) {
|
|
619
|
+
if (typeof type === 'string') {
|
|
620
|
+
const name = ALIASES[type] ?? type;
|
|
621
|
+
if (name === 'bool' || name === 'pubkey') return name === 'bool' ? 1 : 32;
|
|
622
|
+
if (name === 'string' || name === 'bytes') return 4;
|
|
623
|
+
if (name === 'f32') return 4;
|
|
624
|
+
if (name === 'f64') return 8;
|
|
625
|
+
return PRIMITIVES[name]?.size ?? null;
|
|
626
|
+
}
|
|
627
|
+
if (Array.isArray(type)) {
|
|
628
|
+
const inner = minSize(type[0], ctx);
|
|
629
|
+
return inner === null || !Number.isSafeInteger(type[1]) ? null : inner * type[1];
|
|
630
|
+
}
|
|
631
|
+
if (!type || typeof type !== 'object') return null;
|
|
632
|
+
// An option may encode as a single `None` tag: `1 + inner` is the cost of
|
|
633
|
+
// `Some`, not a lower bound, so using it here would refuse real payloads
|
|
634
|
+
// whose optionals are None (found on mainnet: Jupiter route_plan).
|
|
635
|
+
if ('option' in type) return 1;
|
|
636
|
+
if ('vec' in type) return 4;
|
|
637
|
+
if ('array' in type) {
|
|
638
|
+
const parts = arrayParts(type);
|
|
639
|
+
if (!parts) return null;
|
|
640
|
+
const inner = minSize(parts.inner, ctx);
|
|
641
|
+
return inner === null ? null : inner * parts.length;
|
|
642
|
+
}
|
|
643
|
+
if ('defined' in type) {
|
|
644
|
+
const name = typeof type.defined === 'string' ? type.defined : type.defined?.name;
|
|
645
|
+
const typedef = name ? ctx.types.get(name) : null;
|
|
646
|
+
return typedef ? minSize(typedef.type, ctx) : null;
|
|
647
|
+
}
|
|
648
|
+
if (type.kind === 'type') return minSize(type.type, ctx);
|
|
649
|
+
const fields = structShape(type);
|
|
650
|
+
if (fields) {
|
|
651
|
+
let total = 0;
|
|
652
|
+
for (const field of fields) {
|
|
653
|
+
const size = minSize(field.type, ctx);
|
|
654
|
+
if (size === null) return null;
|
|
655
|
+
total += size;
|
|
656
|
+
}
|
|
657
|
+
return total;
|
|
658
|
+
}
|
|
659
|
+
const variants = enumShape(type);
|
|
660
|
+
if (variants) {
|
|
661
|
+
// Lower bound, not upper: an enum element costs 1 tag byte plus whatever
|
|
662
|
+
// ITS OWN variant carries. Taking the largest variant here made the vec
|
|
663
|
+
// pre-check demand the widest variant's bytes for every element, which
|
|
664
|
+
// refused decodable mainnet payloads (Jupiter route: "vector length 1
|
|
665
|
+
// needs 85 byte(s) but 25 remain" while the payload decodes byte-exact).
|
|
666
|
+
let smallest = null;
|
|
667
|
+
for (const variant of variants) {
|
|
668
|
+
const shape = variantFields(variant);
|
|
669
|
+
if (!shape) return null;
|
|
670
|
+
let total = 1; // variant tag
|
|
671
|
+
if (shape.kind === 'tuple') {
|
|
672
|
+
for (const fieldType of shape.fields) {
|
|
673
|
+
const size = minSize(fieldType, ctx);
|
|
674
|
+
if (size === null) return null;
|
|
675
|
+
total += size;
|
|
676
|
+
}
|
|
677
|
+
} else if (shape.kind === 'named') {
|
|
678
|
+
for (const field of shape.fields) {
|
|
679
|
+
const size = minSize(field.type, ctx);
|
|
680
|
+
if (size === null) return null;
|
|
681
|
+
total += size;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (smallest === null || total < smallest) smallest = total;
|
|
685
|
+
}
|
|
686
|
+
return smallest ?? 1;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// --- instruction level ------------------------------------------------------
|
|
692
|
+
|
|
693
|
+
function context(idl) {
|
|
694
|
+
// The dialect guard runs FIRST, before any iteration: a mapping-dialect
|
|
695
|
+
// IDL (measured: diabloarb's dict `types`) used to crash every payload of
|
|
696
|
+
// the program with the unnamed `TypeError: object is not iterable` — a
|
|
697
|
+
// crash, not a refusal (found 2026-11-12, 102/102 payloads).
|
|
698
|
+
assertIdlShape(idl, (reason) => new CodecError(reason, null));
|
|
699
|
+
if (!Array.isArray(idl.instructions)) throw new CodecError('IDL has no instructions array', null);
|
|
700
|
+
const types = new Map();
|
|
701
|
+
for (const typedef of idl.types ?? []) {
|
|
702
|
+
if (typedef && typeof typedef.name === 'string') types.set(typedef.name, typedef);
|
|
703
|
+
}
|
|
704
|
+
// The shank LEB128 hook engages only for shank-dialect IDLs; `types` is
|
|
705
|
+
// consulted first everywhere, so a name the IDL defines wins regardless.
|
|
706
|
+
return { types, shank: isShankIdl(idl) };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function discriminatorBytes(instruction, kind = 'instruction') {
|
|
710
|
+
// Shank IDLs store the selector under `discriminant` (1 byte), not
|
|
711
|
+
// `discriminator` (8 bytes); shankDiscriminatorBytes refuses anything else.
|
|
712
|
+
const shank = shankDiscriminatorBytes(instruction);
|
|
713
|
+
if (shank) return shank;
|
|
714
|
+
const raw = instruction?.discriminator;
|
|
715
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
716
|
+
if (!raw.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) {
|
|
717
|
+
throw new CodecError(`${kind} ${JSON.stringify(instruction?.name)} has a malformed discriminator`, null);
|
|
718
|
+
}
|
|
719
|
+
return Buffer.from(raw);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function instructionIndex(idl) {
|
|
723
|
+
const index = new Map();
|
|
724
|
+
for (const instruction of idl.instructions) {
|
|
725
|
+
const bytes = discriminatorBytes(instruction);
|
|
726
|
+
if (!bytes) continue;
|
|
727
|
+
const key = bytes.toString('hex');
|
|
728
|
+
if (index.has(key)) {
|
|
729
|
+
throw new CodecError(`duplicate instruction discriminator ${key} (${index.get(key).name} / ${instruction.name})`, null);
|
|
730
|
+
}
|
|
731
|
+
index.set(key, instruction);
|
|
732
|
+
}
|
|
733
|
+
return index;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// --- Anchor event self-CPI ---------------------------------------------------
|
|
737
|
+
//
|
|
738
|
+
// `emit_cpi!` builds a real instruction to the program itself whose data is
|
|
739
|
+
// EVENT_IX_TAG_LE (8) ++ event_discriminator (8) ++ borsh(event fields)
|
|
740
|
+
// and the program dispatches on the tag (anchor/lang/src/event.rs:
|
|
741
|
+
// EVENT_IX_TAG = 0x1d9acb512ea545e4). These payloads arrive in a transaction
|
|
742
|
+
// exactly like instructions — same account lists, same CPI stacks — so a
|
|
743
|
+
// decoder that only knows instruction discriminators reports them as
|
|
744
|
+
// "unknown instruction discriminator". Measured on mainnet Jupiter AMM v6:
|
|
745
|
+
// 396 of 773 harvested payloads (51%) were event self-CPIs, all of them
|
|
746
|
+
// carrying a discriminator the stored IDL does list under `events`.
|
|
747
|
+
const EVENT_IX_TAG = Buffer.alloc(8);
|
|
748
|
+
EVENT_IX_TAG.writeBigUInt64LE(0x1d9acb512ea545e4n);
|
|
749
|
+
|
|
750
|
+
export const EVENT_IX_TAG_HEX = EVENT_IX_TAG.toString('hex');
|
|
751
|
+
|
|
752
|
+
export function eventIndex(idl) {
|
|
753
|
+
const index = new Map();
|
|
754
|
+
for (const event of idl?.events ?? []) {
|
|
755
|
+
const bytes = discriminatorBytes(event, 'event');
|
|
756
|
+
if (!bytes) continue;
|
|
757
|
+
const key = bytes.toString('hex');
|
|
758
|
+
if (index.has(key)) {
|
|
759
|
+
throw new CodecError(`duplicate event discriminator ${key} (${index.get(key).name} / ${event.name})`, null);
|
|
760
|
+
}
|
|
761
|
+
index.set(key, event);
|
|
762
|
+
}
|
|
763
|
+
return index;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** True when `data` is an Anchor event payload (tagged or bare-disc form). */
|
|
767
|
+
export function isEventData(idl, data) {
|
|
768
|
+
if (!(data instanceof Uint8Array) || data.length < 8) return false;
|
|
769
|
+
const key = Buffer.from(data.subarray(0, 8)).toString('hex');
|
|
770
|
+
if (key === EVENT_IX_TAG_HEX) return data.length >= 16;
|
|
771
|
+
return eventIndex(idl).has(key);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function eventShape(idl, event, ctx, path) {
|
|
775
|
+
// spec 0.1.0 on-chain IDLs carry the body in `types` under the event name;
|
|
776
|
+
// older IDLs carry `fields` inline on the event itself.
|
|
777
|
+
if (Array.isArray(event.fields)) return { kind: 'struct', fields: event.fields };
|
|
778
|
+
const typedef = ctx.types.get(event.name);
|
|
779
|
+
if (!typedef) throw new CodecError(`event ${JSON.stringify(event.name)} has no type definition`, path);
|
|
780
|
+
return typedef.type;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Decode an Anchor event payload: the `emit_cpi!` form (tag + disc + body)
|
|
785
|
+
* and the bare log-disc form (disc + body).
|
|
786
|
+
*
|
|
787
|
+
* Same trailing policy as decodeInstructionData: 'report' returns the bytes
|
|
788
|
+
* the IDL does not explain instead of dropping them, 'error' refuses.
|
|
789
|
+
*/
|
|
790
|
+
export function decodeEventData(idl, data, { allowTrailing, trailing } = {}) {
|
|
791
|
+
const policy = trailing ?? (allowTrailing === false ? 'error' : 'report');
|
|
792
|
+
if (policy !== 'report' && policy !== 'error') {
|
|
793
|
+
throw new CodecError(`unknown trailing policy ${JSON.stringify(trailing)}`, null);
|
|
794
|
+
}
|
|
795
|
+
if (!(data instanceof Uint8Array)) throw new CodecError('event data must be a Uint8Array/Buffer', null);
|
|
796
|
+
const ctx = context(idl);
|
|
797
|
+
const index = eventIndex(idl);
|
|
798
|
+
let offset = 0;
|
|
799
|
+
const head = Buffer.from(data.subarray(0, 8)).toString('hex');
|
|
800
|
+
if (head === EVENT_IX_TAG_HEX) {
|
|
801
|
+
if (data.length < 16) throw new CodecError('truncated event CPI: no event discriminator after the tag', null);
|
|
802
|
+
offset = 8;
|
|
803
|
+
} else if (!index.has(head)) {
|
|
804
|
+
throw new CodecError(
|
|
805
|
+
`unknown event discriminator ${head} (${index.size} known in this IDL)`, null);
|
|
806
|
+
}
|
|
807
|
+
const discHex = Buffer.from(data.subarray(offset, offset + 8)).toString('hex');
|
|
808
|
+
const event = index.get(discHex);
|
|
809
|
+
if (!event) {
|
|
810
|
+
const hint = [...index.keys()].slice(0, 3).join(', ');
|
|
811
|
+
throw new CodecError(
|
|
812
|
+
`unknown event discriminator ${discHex} (${index.size} known` +
|
|
813
|
+
(hint ? `, e.g. ${hint}` : '') + ')', null);
|
|
814
|
+
}
|
|
815
|
+
const cursor = new Cursor(data);
|
|
816
|
+
cursor.offset = offset + 8;
|
|
817
|
+
const shape = eventShape(idl, event, ctx, event.name);
|
|
818
|
+
const body = decodeAt(shape, cursor, ctx, event.name, 0);
|
|
819
|
+
const trailingCount = cursor.remaining;
|
|
820
|
+
const trailingHex = trailingCount > 0
|
|
821
|
+
? Buffer.from(data.subarray(cursor.offset)).toString('hex')
|
|
822
|
+
: '';
|
|
823
|
+
if (trailingCount > 0 && policy === 'error') {
|
|
824
|
+
throw new CodecError(
|
|
825
|
+
`trailing bytes after event ${event.name}: consumed ${cursor.offset} of ${data.length}`, event.name);
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
kind: 'event',
|
|
829
|
+
event: event.name,
|
|
830
|
+
discriminator: Buffer.from(data.subarray(offset, offset + 8)).toString('hex'),
|
|
831
|
+
tagged: offset === 8,
|
|
832
|
+
args: toJsonSafe(body),
|
|
833
|
+
consumed: cursor.offset,
|
|
834
|
+
total: data.length,
|
|
835
|
+
trailing: { count: trailingCount, hex: trailingHex },
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** Encode an Anchor event payload back to bytes: tag ++ disc ++ body. */
|
|
840
|
+
export function encodeEventData(idl, name, args, { tagged = true } = {}) {
|
|
841
|
+
const ctx = context(idl);
|
|
842
|
+
const event = (idl.events ?? []).find((candidate) => candidate.name === name);
|
|
843
|
+
if (!event) throw new CodecError(`unknown event ${JSON.stringify(name)}`, null);
|
|
844
|
+
const bytes = discriminatorBytes(event, 'event');
|
|
845
|
+
if (!bytes) throw new CodecError(`event ${JSON.stringify(name)} has no discriminator in this IDL`, null);
|
|
846
|
+
if (args === undefined || args === null) args = {};
|
|
847
|
+
const body = encodeAt(eventShape(idl, event, ctx, event.name), args, ctx, event.name, 0);
|
|
848
|
+
return Buffer.concat(tagged ? [EVENT_IX_TAG, bytes, body] : [bytes, body]);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Decode any program payload: instruction first, then Anchor event self-CPI.
|
|
853
|
+
* Instruction wins on an ambiguous discriminator, mirroring Anchor's own
|
|
854
|
+
* dispatch order (global instructions are matched before the event tag).
|
|
855
|
+
* Returns { kind: 'instruction' | 'event', ... }.
|
|
856
|
+
*/
|
|
857
|
+
export function decodeData(idl, data, options = {}) {
|
|
858
|
+
try {
|
|
859
|
+
return { kind: 'instruction', ...decodeInstructionData(idl, data, options) };
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (!(error instanceof CodecError) || !/unknown instruction discriminator/.test(error.message)) throw error;
|
|
862
|
+
if (!isEventData(idl, data)) throw error;
|
|
863
|
+
return decodeEventData(idl, data, options);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function instructionByName(idl, name) {
|
|
868
|
+
const instruction = idl.instructions.find((candidate) => candidate.name === name);
|
|
869
|
+
if (!instruction) {
|
|
870
|
+
const known = idl.instructions.map((candidate) => candidate.name);
|
|
871
|
+
const near = known.filter((candidate) => candidate.startsWith(String(name).slice(0, 6))).slice(0, 3);
|
|
872
|
+
throw new CodecError(
|
|
873
|
+
`unknown instruction ${JSON.stringify(name)}` +
|
|
874
|
+
(near.length ? ` (similar: ${near.join(', ')})` : `; ${known.length} instruction(s) in this IDL`), null);
|
|
875
|
+
}
|
|
876
|
+
return instruction;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Decode one instruction payload (discriminator + args) using the IDL.
|
|
881
|
+
*
|
|
882
|
+
* The stored IDL describes the bytes it knows; it does not have to describe
|
|
883
|
+
* every byte in the buffer. `trailing` chooses what happens to bytes the IDL
|
|
884
|
+
* does not explain:
|
|
885
|
+
* 'report' (default) decode the described prefix and return the unexplained
|
|
886
|
+
* bytes under `trailing` — surfaced, never silently dropped.
|
|
887
|
+
* 'error' refuse the whole payload.
|
|
888
|
+
* `allowTrailing: false` is accepted as an alias for `{ trailing: 'error' }`.
|
|
889
|
+
*
|
|
890
|
+
* 'report' is the default because it matches the program being decoded: Anchor
|
|
891
|
+
* deserialises instruction args with Borsh `deserialize` over a slice, which
|
|
892
|
+
* reads what the struct needs and does not require the slice to be exhausted.
|
|
893
|
+
* Two real mainnet Whirlpool `swap_v2` payloads carry 7 bytes beyond the stored
|
|
894
|
+
* IDL, both in transactions that succeeded on-chain (slot 462101502, and the
|
|
895
|
+
* 43-byte form of the same instruction at slot 462101506) — refusing them
|
|
896
|
+
* rejected 2 of 50 harvested payloads that the deployed program accepted.
|
|
897
|
+
*
|
|
898
|
+
* `absent` is the mirror image of `trailing`, and it is opt-in because offline
|
|
899
|
+
* it cannot be told apart from garbage: what if the IDL declares a field and
|
|
900
|
+
* the payload simply ENDS where that field would start?
|
|
901
|
+
* 'error' (default) refuse with `truncated input`, exactly as before.
|
|
902
|
+
* 'report' decode every field the bytes actually carry and name the declared
|
|
903
|
+
* fields that are absent under `absent`. Nothing is invented: an
|
|
904
|
+
* absent field gets NO value, so the decoded args never claim a
|
|
905
|
+
* default the program may not have used.
|
|
906
|
+
*
|
|
907
|
+
* The distinction that matters: a payload that ends ON a field boundary can
|
|
908
|
+
* name the fields it carries; a payload that ends INSIDE a field still cannot
|
|
909
|
+
* — a partial u64 is not a value — and that case refuses under both policies.
|
|
910
|
+
*
|
|
911
|
+
* `absent: 'report'` is justified by measurement, not by optimism: 171 of the
|
|
912
|
+
* 172 payloads this repo's unbiased mainnet sample refuses are chain-ACCEPTED
|
|
913
|
+
* wire forms whose interface declares one field more than the wire carries
|
|
914
|
+
* (research/short-form-wire-forms-2026-11-03.json — err:null, byte-identical
|
|
915
|
+
* payload, and the program's own "Instruction:" log line). Offline the decoder
|
|
916
|
+
* cannot reach that verdict, so the default stays strict and the caller opts in.
|
|
917
|
+
*/
|
|
918
|
+
export function decodeInstructionData(idl, data, { allowTrailing, trailing, absent } = {}) {
|
|
919
|
+
const policy = trailing ?? (allowTrailing === false ? 'error' : 'report');
|
|
920
|
+
if (policy !== 'report' && policy !== 'error') {
|
|
921
|
+
throw new CodecError(`unknown trailing policy ${JSON.stringify(trailing)}`, null);
|
|
922
|
+
}
|
|
923
|
+
const absentPolicy = absent ?? 'error';
|
|
924
|
+
if (absentPolicy !== 'report' && absentPolicy !== 'error') {
|
|
925
|
+
throw new CodecError(`unknown absent policy ${JSON.stringify(absent)}`, null);
|
|
926
|
+
}
|
|
927
|
+
if (!(data instanceof Uint8Array)) throw new CodecError('instruction data must be a Uint8Array/Buffer', null);
|
|
928
|
+
const ctx = context(idl);
|
|
929
|
+
const index = instructionIndex(idl);
|
|
930
|
+
const prefixLength = Math.min(8, data.length);
|
|
931
|
+
const key = Buffer.from(data.subarray(0, prefixLength)).toString('hex');
|
|
932
|
+
let instruction = null;
|
|
933
|
+
let consumed = 0;
|
|
934
|
+
// Discriminators may be shorter than 8 bytes in generated IDLs; match on the
|
|
935
|
+
// stored prefix length instead of assuming 8.
|
|
936
|
+
for (const [candidate, entry] of index) {
|
|
937
|
+
if (key.startsWith(candidate)) { instruction = entry; consumed = candidate.length / 2; break; }
|
|
938
|
+
}
|
|
939
|
+
if (!instruction) {
|
|
940
|
+
const hint = [...index.keys()].slice(0, 3).join(', ');
|
|
941
|
+
throw new CodecError(
|
|
942
|
+
`unknown instruction discriminator ${key.slice(0, 16)} ` +
|
|
943
|
+
`(${index.size} known, e.g. ${hint})`, null);
|
|
944
|
+
}
|
|
945
|
+
if (data.length < consumed) throw new CodecError('truncated discriminator', null);
|
|
946
|
+
const cursor = new Cursor(data);
|
|
947
|
+
cursor.offset = consumed;
|
|
948
|
+
const args = {};
|
|
949
|
+
const absentArgs = [];
|
|
950
|
+
const declaredArgs = instruction.args ?? [];
|
|
951
|
+
for (const [position, argument] of declaredArgs.entries()) {
|
|
952
|
+
if (!argument || typeof argument.name !== 'string' || !('type' in argument)) {
|
|
953
|
+
throw new CodecError(`instruction ${instruction.name} has a malformed argument`, null);
|
|
954
|
+
}
|
|
955
|
+
// The payload ended exactly where the next field would begin: every field
|
|
956
|
+
// from here on is ABSENT, not truncated. A field boundary is the only
|
|
957
|
+
// place a payload can end and still carry a complete value for what came
|
|
958
|
+
// before — which is why this, and only this, is nameable.
|
|
959
|
+
if (cursor.remaining === 0 && absentPolicy === 'report') {
|
|
960
|
+
for (const rest of declaredArgs.slice(position)) absentArgs.push(rest.name);
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
// With absent:'error' (default) this falls through to decodeAt, which
|
|
964
|
+
// reports `truncated input` — the behaviour every existing caller pins.
|
|
965
|
+
assign(args, argument.name, decodeAt(argument.type, cursor, ctx, `${instruction.name}.${argument.name}`, 0), instruction.name);
|
|
966
|
+
}
|
|
967
|
+
const trailingCount = cursor.remaining;
|
|
968
|
+
const trailingHex = trailingCount > 0
|
|
969
|
+
? Buffer.from(data.subarray(cursor.offset)).toString('hex')
|
|
970
|
+
: '';
|
|
971
|
+
if (trailingCount > 0 && policy === 'error') {
|
|
972
|
+
throw new CodecError(
|
|
973
|
+
`trailing bytes after ${instruction.name}: consumed ${cursor.offset} of ${data.length}`, instruction.name);
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
instruction: instruction.name,
|
|
977
|
+
discriminator: Buffer.from(data.subarray(0, consumed)).toString('hex'),
|
|
978
|
+
args,
|
|
979
|
+
// Declared fields the payload did not carry, in declaration order.
|
|
980
|
+
// Empty for a fully-described payload, so `absent.length === 0` is the
|
|
981
|
+
// completeness assertion that `trailing.count === 0` is for extra bytes.
|
|
982
|
+
absent: absentArgs,
|
|
983
|
+
consumed: cursor.offset,
|
|
984
|
+
total: data.length,
|
|
985
|
+
// Bytes the IDL does not explain, as count + hex. Present (0, '') when the
|
|
986
|
+
// payload is fully described, so `trailing.count === 0` is a usable
|
|
987
|
+
// completeness assertion and `count > 0` is an IDL-drift signal.
|
|
988
|
+
trailing: { count: trailingCount, hex: trailingHex },
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Encode one instruction payload (discriminator + args) using the IDL.
|
|
994
|
+
*
|
|
995
|
+
* `absent` names declared arguments to skip — the inverse of a decode that
|
|
996
|
+
* reported them absent. Skipping is allowed only for a SUFFIX of the
|
|
997
|
+
* declaration: a hole in the middle would be re-encoded as a different byte
|
|
998
|
+
* layout, and byte-exactness is the only evidence this function offers.
|
|
999
|
+
*/
|
|
1000
|
+
export function encodeInstructionData(idl, name, args, { absent } = {}) {
|
|
1001
|
+
const ctx = context(idl);
|
|
1002
|
+
const instruction = instructionByName(idl, name);
|
|
1003
|
+
const bytes = discriminatorBytes(instruction) ?? (() => {
|
|
1004
|
+
throw new CodecError(`instruction ${JSON.stringify(name)} has no discriminator in this IDL`, null);
|
|
1005
|
+
})();
|
|
1006
|
+
if (args === undefined || args === null) args = {};
|
|
1007
|
+
if (typeof args !== 'object' || Array.isArray(args)) throw new CodecError('args must be an object', null);
|
|
1008
|
+
const declared = instruction.args ?? [];
|
|
1009
|
+
const known = new Set(declared.map((argument) => argument.name));
|
|
1010
|
+
const absentSet = new Set(Array.isArray(absent) ? absent : []);
|
|
1011
|
+
for (const fieldName of absentSet) {
|
|
1012
|
+
if (!known.has(fieldName)) {
|
|
1013
|
+
throw new CodecError(`unexpected argument(s) for ${instruction.name}: ${fieldName}`, instruction.name);
|
|
1014
|
+
}
|
|
1015
|
+
if (fieldName in args) {
|
|
1016
|
+
throw new CodecError(`argument ${JSON.stringify(fieldName)} is both absent and present`, instruction.name);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// Only a trailing run may be absent: a gap would silently reorder bytes.
|
|
1020
|
+
let seenAbsent = false;
|
|
1021
|
+
for (const argument of declared) {
|
|
1022
|
+
if (absentSet.has(argument.name)) { seenAbsent = true; continue; }
|
|
1023
|
+
if (seenAbsent) {
|
|
1024
|
+
throw new CodecError(
|
|
1025
|
+
`argument ${JSON.stringify(argument.name)} follows an absent argument — only a trailing run may be omitted`,
|
|
1026
|
+
instruction.name);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
1030
|
+
if (unknown.length) {
|
|
1031
|
+
throw new CodecError(`unexpected argument(s) for ${instruction.name}: ${unknown.join(', ')}`, null);
|
|
1032
|
+
}
|
|
1033
|
+
const parts = [bytes];
|
|
1034
|
+
for (const argument of declared) {
|
|
1035
|
+
if (absentSet.has(argument.name)) continue;
|
|
1036
|
+
if (!(argument.name in args)) throw new CodecError(`missing argument ${JSON.stringify(argument.name)}`, instruction.name);
|
|
1037
|
+
parts.push(encodeAt(argument.type, args[argument.name], ctx, `${instruction.name}.${argument.name}`, 0));
|
|
1038
|
+
}
|
|
1039
|
+
return Buffer.concat(parts);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Decode a type from the front of `bytes` and report how much was consumed,
|
|
1044
|
+
* without requiring the buffer to be exhausted. Used where a struct describes
|
|
1045
|
+
* a prefix of a larger buffer (account data with trailing bytes, instruction
|
|
1046
|
+
* payloads the IDL does not fully explain).
|
|
1047
|
+
*/
|
|
1048
|
+
export function decodeTypePartial(type, bytes, ctx, path = 'value') {
|
|
1049
|
+
const cursor = new Cursor(bytes);
|
|
1050
|
+
const value = decodeAt(type, cursor, ctx, path, 0);
|
|
1051
|
+
return { value, consumed: cursor.offset };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** Encode a value back into the bytes of `type` (inverse of decodeType). */
|
|
1055
|
+
export function encodeType(type, value, ctx, path = 'value') {
|
|
1056
|
+
return encodeAt(type, value, ctx, path, 0);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Deep-copy a decoded value into JSON-safe form: bigint -> decimal string. */
|
|
1060
|
+
export function toJsonSafe(value) {
|
|
1061
|
+
if (typeof value === 'bigint') return value.toString();
|
|
1062
|
+
if (Array.isArray(value)) return value.map(toJsonSafe);
|
|
1063
|
+
if (value && typeof value === 'object') {
|
|
1064
|
+
const out = {};
|
|
1065
|
+
for (const key of Object.keys(value)) out[key] = toJsonSafe(value[key]);
|
|
1066
|
+
return out;
|
|
1067
|
+
}
|
|
1068
|
+
return value;
|
|
1069
|
+
}
|