sememe 0.4.24 → 0.4.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2742
- package/index.mjs +6 -0
- package/package.json +1 -1
- package/src/codec.mjs +95 -1
- package/src/wire-layout.mjs +88 -0
package/index.mjs
CHANGED
|
@@ -38,6 +38,12 @@ export {
|
|
|
38
38
|
|
|
39
39
|
export { encodeBase58 } from './src/base58.mjs';
|
|
40
40
|
export { canonicalJson } from './src/canonical.mjs';
|
|
41
|
+
// The count-from-header table: a vector whose element count the wire states
|
|
42
|
+
// in a DECLARED header argument carries NO Borsh length prefix. The table is
|
|
43
|
+
// the evidence — it names the measured program address, the measured
|
|
44
|
+
// instruction names and where the measurement lives. Exported so a consumer
|
|
45
|
+
// can ask WHICH layouts are evidence-backed instead of guessing.
|
|
46
|
+
export { COUNT_FROM_HEADER, countFromHeader } from './src/wire-layout.mjs';
|
|
41
47
|
// The native programs (System, ComputeBudget) are NOT re-exported here on
|
|
42
48
|
// purpose: src/native.mjs derives its wire table from the pinned upstream
|
|
43
49
|
// sources AT IMPORT TIME and refuses to load when the committed artifact
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sememe",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.25",
|
|
4
4
|
"description": "Local, fingerprint-verified Solana program interfaces: pull an IDL once, then decode instructions, accounts and events offline. Zero dependencies on the decode path. Ships derived wire tables for System, ComputeBudget, SPL Token, Token-2022 (incl. its token-metadata fallback), Raydium AMM v4 and the SPL Associated Token Account, pinned to upstream Rust.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/codec.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { encodeBase58, decodeBase58 } from './base58.mjs';
|
|
|
22
22
|
import { isShankIdl, isShankDiscriminant, shankDiscriminatorBytes, SHANK_LEB128_TYPES } from './shank.mjs';
|
|
23
23
|
import { assertIdlShape, unsupportedIdlReason } from './dialect.mjs';
|
|
24
24
|
import { rawLeDialectReason, decodeRawLe, encodeRawLe, RawLeError } from './rawle.mjs';
|
|
25
|
+
import { countFromHeader } from './wire-layout.mjs';
|
|
25
26
|
|
|
26
27
|
const MAX_DEPTH = 32;
|
|
27
28
|
// Instruction data is bounded by transaction size; a hostile length prefix
|
|
@@ -356,6 +357,31 @@ function decodeAt(type, cur, ctx, path, depth) {
|
|
|
356
357
|
return { __option: 'Some', value: decodeAt(type.option, cur, ctx, `${path}.value`, depth + 1) };
|
|
357
358
|
}
|
|
358
359
|
if ('vec' in type) {
|
|
360
|
+
// Count-from-header (src/wire-layout.mjs): a measured program+instruction
|
|
361
|
+
// whose wire carries the element count in a declared header argument and
|
|
362
|
+
// NO u32 Borsh prefix. The hook fires ONLY when ctx.__countFromHeader
|
|
363
|
+
// names THIS path — set by the instruction loop after the count argument
|
|
364
|
+
// decoded, cleared before every other argument — so every other interface
|
|
365
|
+
// keeps the strict prefix codec byte for byte.
|
|
366
|
+
if (ctx.__countFromHeader && ctx.__countFromHeader.vectorPath === path) {
|
|
367
|
+
const length = ctx.__countFromHeader.count;
|
|
368
|
+
const elementSize = ctx.__countFromHeader.pointBytes;
|
|
369
|
+
const needed = length * elementSize;
|
|
370
|
+
if (needed > cur.remaining) {
|
|
371
|
+
// The measured count does not fit the remaining bytes: this is NOT a
|
|
372
|
+
// Borsh prefix, so the refusal names the layout rule — never the
|
|
373
|
+
// garbage number a prefix read would produce.
|
|
374
|
+
throw new CodecError(
|
|
375
|
+
`${length} ${elementSize}-byte element(s) need ${needed} byte(s) but ${cur.remaining} remain — ` +
|
|
376
|
+
`${ctx.__countFromHeader.instruction}.${ctx.__countFromHeader.vectorArg} has no length prefix on the wire; ` +
|
|
377
|
+
`the count comes from ${ctx.__countFromHeader.countArg}`, path);
|
|
378
|
+
}
|
|
379
|
+
const out = [];
|
|
380
|
+
for (let index = 0; index < length; index++) {
|
|
381
|
+
out.push(decodeAt(type.vec, cur, ctx, `${path}[${index}]`, depth + 1));
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
359
385
|
const length = readLittle(cur, 4, false, path);
|
|
360
386
|
const minimum = minSize(type.vec, ctx);
|
|
361
387
|
if (minimum === 0 && length > HARD_VEC_CAP) {
|
|
@@ -571,6 +597,23 @@ function encodeAt(type, value, ctx, path, depth) {
|
|
|
571
597
|
return Buffer.concat([Buffer.from([1]), encodeAt(type.option, value.value, ctx, `${path}.value`, depth + 1)]);
|
|
572
598
|
}
|
|
573
599
|
if ('vec' in type) {
|
|
600
|
+
// Encoder half of count-from-header (src/wire-layout.mjs): the measured
|
|
601
|
+
// program+instruction writes the elements with NO u32 prefix. When the
|
|
602
|
+
// elements disagree with the declared count, the encode is refused by
|
|
603
|
+
// name — the count IS the layout, so silently fixing it would invent a
|
|
604
|
+
// wire the program never speaks.
|
|
605
|
+
if (ctx.__countFromHeader && ctx.__countFromHeader.vectorPath === path) {
|
|
606
|
+
const { count, countArg, instruction, vectorArg } = ctx.__countFromHeader;
|
|
607
|
+
if (value.length !== count) {
|
|
608
|
+
throw new CodecError(
|
|
609
|
+
`${instruction}.${vectorArg} carries ${value.length} element(s) but ${countArg} says ${count} — ` +
|
|
610
|
+
`this vector has no length prefix on the wire, the count is the header`, path);
|
|
611
|
+
}
|
|
612
|
+
const parts = [];
|
|
613
|
+
value.forEach((item, index) =>
|
|
614
|
+
parts.push(encodeAt(type.vec, item, ctx, `${path}[${index}]`, depth + 1)));
|
|
615
|
+
return Buffer.concat(parts);
|
|
616
|
+
}
|
|
574
617
|
if (!Array.isArray(value)) throw new CodecError(`expected an array, got ${describe(value)}`, path);
|
|
575
618
|
if (value.length > 0xffffffff) throw new CodecError(`vector length ${value.length} exceeds u32`, path);
|
|
576
619
|
const parts = [writeLittle(4, false, value.length, path)];
|
|
@@ -1083,6 +1126,12 @@ export function decodeInstructionData(idl, data, { allowTrailing, trailing, abse
|
|
|
1083
1126
|
const args = {};
|
|
1084
1127
|
const absentArgs = [];
|
|
1085
1128
|
const declaredArgs = instruction.args ?? [];
|
|
1129
|
+
// Count-from-header (src/wire-layout.mjs): the measured program+instruction
|
|
1130
|
+
// pair whose vector argument carries NO u32 prefix because the wire states
|
|
1131
|
+
// the element count in a declared header argument. The rule is looked up by
|
|
1132
|
+
// THIS idl's address and THIS instruction name, so an identical shape under
|
|
1133
|
+
// another address or name keeps the strict codec byte for byte.
|
|
1134
|
+
const headerRule = countFromHeader(idl, instruction.name);
|
|
1086
1135
|
// Struct-internal absences (the payload ends INSIDE a declared struct
|
|
1087
1136
|
// argument, on that struct's own field boundary) surface as a marker the
|
|
1088
1137
|
// struct decoder leaves on the decoded value. The struct decoder only
|
|
@@ -1106,11 +1155,28 @@ export function decodeInstructionData(idl, data, { allowTrailing, trailing, abse
|
|
|
1106
1155
|
// With absent:'error' (default) this falls through to decodeAt, which
|
|
1107
1156
|
// reports `truncated input` — the behaviour every existing caller pins.
|
|
1108
1157
|
ctx.__absentTrail.push(argument.name);
|
|
1158
|
+
// Arm the vec hook ONLY for the argument the table names, and ONLY when
|
|
1159
|
+
// the count it depends on was actually declared and decoded BEFORE this
|
|
1160
|
+
// argument (G3: an interface without a declared count stays strict).
|
|
1161
|
+
const headerCount = headerRule && argument.name === headerRule.vectorArg && 'vec' in (argument.type ?? {})
|
|
1162
|
+
&& typeof args[headerRule.countArg] === 'number' && Number.isSafeInteger(args[headerRule.countArg])
|
|
1163
|
+
? args[headerRule.countArg] : null;
|
|
1164
|
+
if (headerCount !== null) {
|
|
1165
|
+
ctx.__countFromHeader = {
|
|
1166
|
+
count: headerCount,
|
|
1167
|
+
countArg: headerRule.countArg,
|
|
1168
|
+
vectorArg: headerRule.vectorArg,
|
|
1169
|
+
vectorPath: `${instruction.name}.${headerRule.vectorArg}`,
|
|
1170
|
+
pointBytes: headerRule.pointBytes,
|
|
1171
|
+
instruction: instruction.name,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1109
1174
|
let value;
|
|
1110
1175
|
try {
|
|
1111
1176
|
value = decodeAt(argument.type, cursor, ctx, `${instruction.name}.${argument.name}`, 0);
|
|
1112
1177
|
} finally {
|
|
1113
1178
|
ctx.__absentTrail.pop();
|
|
1179
|
+
if (headerCount !== null) delete ctx.__countFromHeader;
|
|
1114
1180
|
}
|
|
1115
1181
|
// A struct the wire stopped inside carries the `__absentFields` marker;
|
|
1116
1182
|
// peel it off so the decoded args never show codec-internal keys. The
|
|
@@ -1276,10 +1342,38 @@ export function encodeInstructionData(idl, name, args, { absent } = {}) {
|
|
|
1276
1342
|
}
|
|
1277
1343
|
try {
|
|
1278
1344
|
const parts = [bytes];
|
|
1345
|
+
// Encoder half of count-from-header (src/wire-layout.mjs): same gates as
|
|
1346
|
+
// the decode — the table must name THIS program+instruction, and the
|
|
1347
|
+
// args must carry an integer count for the vector being encoded. A
|
|
1348
|
+
// count that disagrees with the elements is refused by name, never
|
|
1349
|
+
// silently corrected: the count IS the wire layout.
|
|
1350
|
+
const headerRule = countFromHeader(idl, instruction.name);
|
|
1351
|
+
const countIndex = headerRule ? declared.findIndex((argument) => argument.name === headerRule.countArg) : -1;
|
|
1352
|
+
const vectorIndex = headerRule ? declared.findIndex((argument) => argument.name === headerRule.vectorArg) : -1;
|
|
1279
1353
|
for (const argument of declared) {
|
|
1280
1354
|
if (absentSet.has(argument.name)) continue;
|
|
1281
1355
|
if (!(argument.name in args)) throw new CodecError(`missing argument ${JSON.stringify(argument.name)}`, instruction.name);
|
|
1282
|
-
|
|
1356
|
+
const headerCount = headerRule
|
|
1357
|
+
&& argument.name === headerRule.vectorArg
|
|
1358
|
+
&& 'vec' in (argument.type ?? {})
|
|
1359
|
+
&& countIndex !== -1 && vectorIndex !== -1 && countIndex < vectorIndex
|
|
1360
|
+
&& typeof args[headerRule.countArg] === 'number' && Number.isSafeInteger(args[headerRule.countArg])
|
|
1361
|
+
? args[headerRule.countArg] : null;
|
|
1362
|
+
if (headerCount !== null) {
|
|
1363
|
+
ctx.__countFromHeader = {
|
|
1364
|
+
count: headerCount,
|
|
1365
|
+
countArg: headerRule.countArg,
|
|
1366
|
+
vectorArg: headerRule.vectorArg,
|
|
1367
|
+
vectorPath: `${instruction.name}.${headerRule.vectorArg}`,
|
|
1368
|
+
pointBytes: headerRule.pointBytes,
|
|
1369
|
+
instruction: instruction.name,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
try {
|
|
1373
|
+
parts.push(encodeAt(argument.type, args[argument.name], ctx, `${instruction.name}.${argument.name}`, 0));
|
|
1374
|
+
} finally {
|
|
1375
|
+
if (headerCount !== null) delete ctx.__countFromHeader;
|
|
1376
|
+
}
|
|
1283
1377
|
}
|
|
1284
1378
|
return Buffer.concat(parts);
|
|
1285
1379
|
} finally {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Count-from-header: an evidence-gated rule for a vector whose element count
|
|
2
|
+
// the wire carries in an instruction HEADER field, not as a Borsh length
|
|
3
|
+
// prefix.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS RULE EXISTS (measured 2026-11-13, 13.11. PULS AD -> AE)
|
|
6
|
+
//
|
|
7
|
+
// HADRoNbLovyqhCsocfYQYB7QdfCAAinN9HTePvBCVDQ8 publishes an on-chain IDL
|
|
8
|
+
// that declares `points` as `vec<CurvePointInput>`, which means "a u32 Borsh
|
|
9
|
+
// length prefix, then the elements". The deployed program writes NO prefix:
|
|
10
|
+
// the element count is the `numPoints` u8 the instruction already declares
|
|
11
|
+
// in its own header, and the points follow the header directly. Measured on
|
|
12
|
+
// 144 chain-accepted payloads (research/hadro-vec-wire-2026-11-13.json):
|
|
13
|
+
//
|
|
14
|
+
// wire = 1-byte discriminator + u8 header + numPoints x 21-byte
|
|
15
|
+
// CurvePointInput + an 8-byte tail
|
|
16
|
+
//
|
|
17
|
+
// What the strict u32-prefix codec did with that, measured on the same
|
|
18
|
+
// payloads:
|
|
19
|
+
//
|
|
20
|
+
// * 72 setRiskCurve payloads REFUSED with a garbage number — the u32 the
|
|
21
|
+
// codec read as the vector length was the first four bytes of the first
|
|
22
|
+
// curve point;
|
|
23
|
+
// * 72 setCurve payloads "decoded" with points: [] and 46 trailing bytes —
|
|
24
|
+
// an EMPTY curve, described, not decoded. 0 byte-exact.
|
|
25
|
+
//
|
|
26
|
+
// THE RULE: when a listed program+instruction declares an integer count
|
|
27
|
+
// argument BEFORE the vector argument, the vector carries no length prefix on
|
|
28
|
+
// the wire; its element count is the count argument's decoded value. The
|
|
29
|
+
// encoder half writes the elements without a prefix, and a count that does
|
|
30
|
+
// not fit the remaining bytes is refused BY NAME ("no length prefix on the
|
|
31
|
+
// wire"), never by a garbage number.
|
|
32
|
+
//
|
|
33
|
+
// GATES — every one is load-bearing, and each has a guard in
|
|
34
|
+
// test/hadro-count-from-header.test.mjs:
|
|
35
|
+
//
|
|
36
|
+
// G1 the program address must be the measured program
|
|
37
|
+
// (G4: the same interface under another address keeps the strict codec)
|
|
38
|
+
// G2 the instruction name must be one of the measured names
|
|
39
|
+
// (G4: a shape the table does not name keeps the strict codec)
|
|
40
|
+
// G3 the instruction must DECLARE an integer count argument before the
|
|
41
|
+
// vector argument (G3: removing `numPoints` keeps the strict codec)
|
|
42
|
+
//
|
|
43
|
+
// A rule that fired on evidence the table was not measured on would decode
|
|
44
|
+
// bytes by an argument that happens to be named "numPoints"; the gates are
|
|
45
|
+
// what make this a measurement instead of a name match.
|
|
46
|
+
//
|
|
47
|
+
// Measured on the committed 144 payloads: setCurve (discriminator 0x04) is
|
|
48
|
+
// always numPoints=2; setRiskCurve (0x08) carries numPoints in 2..19. The
|
|
49
|
+
// tail after the points is 8 bytes for all 144 (an undeclared field the IDL
|
|
50
|
+
// does not name — reported as trailing, never invented).
|
|
51
|
+
|
|
52
|
+
const POINT_BYTES = 21;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The measured count-from-header table, keyed by program address.
|
|
56
|
+
*
|
|
57
|
+
* Every entry is a program+instruction pair whose wire layout was measured
|
|
58
|
+
* against chain-accepted payloads with a committed artifact; nothing here is
|
|
59
|
+
* inferred from an IDL claim or from a shape match.
|
|
60
|
+
*/
|
|
61
|
+
export const COUNT_FROM_HEADER = {
|
|
62
|
+
HADRoNbLovyqhCsocfYQYB7QdfCAAinN9HTePvBCVDQ8: {
|
|
63
|
+
source: 'research/hadro-vec-wire-2026-11-13.json',
|
|
64
|
+
instructions: {
|
|
65
|
+
setCurve: { countArg: 'numPoints', vectorArg: 'points' },
|
|
66
|
+
setRiskCurve: { countArg: 'numPoints', vectorArg: 'points' },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the count-from-header rule for an IDL+instruction, or null.
|
|
73
|
+
*
|
|
74
|
+
* The returned rule is a promise about EVIDENCE, not a hint: it carries the
|
|
75
|
+
* count argument's name, the vector argument's name, and the measured point
|
|
76
|
+
* size. The caller must re-check that the count argument was actually
|
|
77
|
+
* declared (and decoded) before the vector, or the rule has no evidence to
|
|
78
|
+
* speak and must stay silent.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} idl the IDL being decoded (must carry `address`)
|
|
81
|
+
* @param {string} name the instruction name being decoded/encoded
|
|
82
|
+
* @returns {{countArg: string, vectorArg: string, pointBytes: number}|null}
|
|
83
|
+
*/
|
|
84
|
+
export function countFromHeader(idl, name) {
|
|
85
|
+
const entry = COUNT_FROM_HEADER[idl?.address]?.instructions?.[name];
|
|
86
|
+
if (!entry) return null;
|
|
87
|
+
return { ...entry, pointBytes: POINT_BYTES };
|
|
88
|
+
}
|