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/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.24",
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
- parts.push(encodeAt(argument.type, args[argument.name], ctx, `${instruction.name}.${argument.name}`, 0));
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
+ }