sememe 0.0.0-stage → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2667 -2
  3. package/bin/sememe.mjs +556 -0
  4. package/index.mjs +48 -0
  5. package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
  6. package/interface/native-11111111111111111111111111111111.json +226 -0
  7. package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
  8. package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
  9. package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
  10. package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
  11. package/interface/token-metadata.json +132 -0
  12. package/package.json +71 -4
  13. package/src/account-list.mjs +308 -0
  14. package/src/account-union.mjs +523 -0
  15. package/src/account.mjs +199 -0
  16. package/src/acquire.mjs +145 -0
  17. package/src/adopt.mjs +205 -0
  18. package/src/ata-interface.mjs +276 -0
  19. package/src/ata.mjs +223 -0
  20. package/src/base58.mjs +96 -0
  21. package/src/canonical.mjs +24 -0
  22. package/src/cli-surface.mjs +300 -0
  23. package/src/codec.mjs +1069 -0
  24. package/src/conformance.mjs +431 -0
  25. package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
  26. package/src/corpus/harvest-system-2026-10-31.json +26090 -0
  27. package/src/decode.mjs +286 -0
  28. package/src/dialect.mjs +145 -0
  29. package/src/discriminators.mjs +177 -0
  30. package/src/drift.mjs +172 -0
  31. package/src/gap.mjs +205 -0
  32. package/src/lighthouse.mjs +168 -0
  33. package/src/lock.mjs +440 -0
  34. package/src/native-audit.mjs +56 -0
  35. package/src/native-corpus.mjs +57 -0
  36. package/src/native-interface.mjs +252 -0
  37. package/src/native.mjs +324 -0
  38. package/src/peer.mjs +95 -0
  39. package/src/program-id.mjs +74 -0
  40. package/src/pull.mjs +117 -0
  41. package/src/rawle.mjs +450 -0
  42. package/src/raydium-interface.mjs +747 -0
  43. package/src/raydium.mjs +391 -0
  44. package/src/receipt-digest.mjs +84 -0
  45. package/src/receipt-name.mjs +128 -0
  46. package/src/receipt-order.mjs +56 -0
  47. package/src/shank.mjs +73 -0
  48. package/src/short-forms.mjs +209 -0
  49. package/src/store-read.mjs +250 -0
  50. package/src/store-write.mjs +46 -0
  51. package/src/token-interface.mjs +394 -0
  52. package/src/token-metadata-interface.mjs +371 -0
  53. package/src/token-metadata.mjs +317 -0
  54. package/src/token.mjs +443 -0
  55. package/src/union.mjs +428 -0
  56. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
  57. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
  58. package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
  59. package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
  60. package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
  61. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
  62. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
  63. package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
  64. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
  65. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
  66. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
  67. package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
  68. package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
  69. package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
  70. package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
  71. package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
  72. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
  73. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
  74. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
  75. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
  76. package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
@@ -0,0 +1,391 @@
1
+ // The Raydium AMM v4 decoder: the vendored-upstream path for a LEGACY
2
+ // (pre-Anchor) program that publishes no interface at all.
3
+ //
4
+ // Why this file exists: measured 2026-11-02 on the unbiased four-block mainnet
5
+ // sample, Raydium AMM v4 was the LAST program with no interface sememe could
6
+ // acquire — 9 payloads (the whole remaining acquisition gap; every other
7
+ // program in the sample was already named). Neither of the two pullable IDL
8
+ // addresses answers for it (re-probed live at capture time), and its wire is
9
+ // not an Anchor 8-byte discriminator: it is a hand-written `pack()`/`unpack()`
10
+ // pair over a SPARSE u8 tag set. So, exactly like the System/ComputeBudget and
11
+ // SPL Token tables, the interface is DERIVED from the vendored upstream Rust
12
+ // program source by `src/raydium-interface.mjs` and committed as a reviewable
13
+ // artifact (`interface/raydium-<program>.json`).
14
+ //
15
+ // Ground truth chain, every row derived and then VERIFIED, not typed:
16
+ // 1. INTERFACE raydium-io/raydium-amm@91739d8d (Apache-2.0)
17
+ // program/src/instruction.rs AmmInstruction / pack() / unpack()
18
+ // program/src/state.rs AmmParams, Fees (LEN 64)
19
+ // DERIVED by tools/raydium-interface.mjs into
20
+ // interface/raydium-<program>.json. This module reads the
21
+ // artifact at load and re-hashes BOTH vendored files against
22
+ // the sha256 the artifact claims; a mismatch REFUSES the load
23
+ // instead of decoding from it — the two-check contract
24
+ // src/native.mjs and src/token.mjs already proved.
25
+ // 2. WIRE the pack() arm's literal tag push cross-checked against the
26
+ // unpack() arm that binds the same integer; the arm's byte-write
27
+ // sequence cross-checked against unpack()'s reads by count and
28
+ // width; optional groups carry upstream's own `rest.len() >= N`
29
+ // threshold (a trailing group, NOT a 1-byte option tag);
30
+ // branches carry upstream's own discriminator values
31
+ // (AmmParams enum text or numeric literal).
32
+ // 3. RUNTIME at this pin unpack() binds an explicit sparse u8 tag and an
33
+ // unknown tag is InvalidInstructionData; the seven variants
34
+ // upstream marks unsupported PANIC (unimplemented!()).
35
+ //
36
+ // What is deliberately NOT claimed: this table describes the program AT THE
37
+ // PIN. It carries no account list, no invariants and no state layout — only
38
+ // instruction data. Program ids 675kPX9… is the mainnet AMM v4 id; the table
39
+ // makes no claim about any other Raydium deployment.
40
+ //
41
+ // Strictness mirrors src/token.mjs: an unknown selector, a wire-rejected
42
+ // variant, an unmatched branch discriminator, a truncated payload or a
43
+ // truncated optional group is a refusal with a named reason — never a
44
+ // confident wrong label.
45
+
46
+ import { readFileSync } from 'node:fs';
47
+ import path from 'node:path';
48
+ import { fileURLToPath } from 'node:url';
49
+ import { decodeBase58, encodeBase58 } from './base58.mjs';
50
+ import {
51
+ deriveRaydiumInterface, RAYDIUM_SPECS, vendoredRaydiumHash,
52
+ } from './raydium-interface.mjs';
53
+
54
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
55
+ const ROOT = path.resolve(HERE, '..');
56
+
57
+ export { RAYDIUM_AMM_V4 } from './raydium-interface.mjs';
58
+
59
+ /**
60
+ * Load one Raydium interface: DERIVE it from the vendored upstream Rust, then
61
+ * require the committed artifact to agree with the derivation byte for byte.
62
+ *
63
+ * Two checks, deliberately (the contract src/native.mjs already proved):
64
+ * - the derivation runs against the vendored bytes themselves, so a swapped
65
+ * or edited source file cannot silently change the table it describes;
66
+ * - the committed artifact must equal that derivation, so a tampered or
67
+ * stale artifact refuses the load instead of decoding from it.
68
+ */
69
+ function loadInterface(spec) {
70
+ const artifactPath = path.join(ROOT, 'interface', `raydium-${spec.program}.json`);
71
+ const derived = deriveRaydiumInterface(spec);
72
+ let current;
73
+ try {
74
+ current = readFileSync(artifactPath, 'utf8');
75
+ } catch (error) {
76
+ throw new Error(`raydium interface artifact for ${spec.label} is missing: ${error.message}`);
77
+ }
78
+ const expected = `${JSON.stringify(derived, null, 2)}\n`;
79
+ if (current !== expected) {
80
+ const claimed = derived.source.files.map((entry) => entry.sha256).join(', ');
81
+ throw new Error(
82
+ `raydium interface for ${spec.label} refuses to load: interface/raydium-${spec.program}.json does not ` +
83
+ `equal a fresh derivation of ${derived.source.repo}@${derived.source.pin} (${claimed}, ` +
84
+ `hashed now: ${vendoredRaydiumHash(spec)})`,
85
+ );
86
+ }
87
+ if (!derived.instructions.length || !derived.selector?.width) {
88
+ throw new Error(`raydium interface artifact for ${spec.label} is malformed`);
89
+ }
90
+ return derived;
91
+ }
92
+
93
+ const ARTIFACTS = new Map(RAYDIUM_SPECS.map((spec) => [spec.program, loadInterface(spec)]));
94
+ const BY_TAG = new Map(RAYDIUM_SPECS.map((spec) => [
95
+ spec.program,
96
+ new Map(ARTIFACTS.get(spec.program).instructions.map((entry) => [entry.tag, entry])),
97
+ ]));
98
+ const BY_NAME = new Map(RAYDIUM_SPECS.map((spec) => [
99
+ spec.program,
100
+ new Map(ARTIFACTS.get(spec.program).instructions.map((entry) => [entry.name, entry])),
101
+ ]));
102
+
103
+ export class RaydiumCodecError extends Error {
104
+ constructor(reason, detail) {
105
+ super(detail ? `${reason}: ${detail}` : reason);
106
+ this.name = 'RaydiumCodecError';
107
+ this.reason = reason;
108
+ }
109
+ }
110
+
111
+ /** Whether `program` is a Raydium program this module owns. */
112
+ export function isRaydiumProgram(program) {
113
+ return ARTIFACTS.has(program);
114
+ }
115
+
116
+ /** Program ids this module can decode, for callers that enumerate. */
117
+ export function raydiumPrograms() {
118
+ return [...ARTIFACTS.keys()];
119
+ }
120
+
121
+ /** Provenance for one Raydium program, or null. */
122
+ export function raydiumProvenance(program) {
123
+ const artifact = ARTIFACTS.get(program);
124
+ if (!artifact) return null;
125
+ const reachable = artifact.instructions.filter((entry) => entry.wire !== 'rejected');
126
+ return {
127
+ label: artifact.label,
128
+ interface: `${artifact.source.repo}@${artifact.source.pin.slice(0, 8)} ` +
129
+ `${artifact.source.files.map((entry) => entry.path).join(' + ')} AmmInstruction (explicit sparse tags)`,
130
+ encoding: artifact.argEncoding,
131
+ runtime: artifact.runtime,
132
+ variants: artifact.instructions.length,
133
+ reachable: reachable.length,
134
+ refused: artifact.instructions.length - reachable.length,
135
+ scheme: artifact.scheme,
136
+ source: artifact.source,
137
+ };
138
+ }
139
+
140
+ function readLe(bytes, offset, size) {
141
+ let value = 0n;
142
+ for (let index = size - 1; index >= 0; index--) value = (value << 8n) | BigInt(bytes[offset + index]);
143
+ return value;
144
+ }
145
+
146
+ const INT_WIDTH = { u8: 1, u16: 2, u32: 4, u64: 8 };
147
+
148
+ /** Decode one arg. Returns the value plus the bytes it consumed. */
149
+ function decodeArg(spec, bytes, offset, artifact, what) {
150
+ const type = spec.type;
151
+ if (type in INT_WIDTH) {
152
+ const width = INT_WIDTH[type];
153
+ if (offset + width > bytes.length) {
154
+ throw new RaydiumCodecError('truncated', `${what}: truncated ${type} at ${offset} of ${bytes.length} byte(s)`);
155
+ }
156
+ const value = readLe(bytes, offset, width);
157
+ // u8/u16/u32 are safe as Numbers (native.mjs does the same); u64 is not.
158
+ return { value: width < 8 ? Number(value) : value, size: width };
159
+ }
160
+ if (type === 'pubkey') {
161
+ if (offset + 32 > bytes.length) {
162
+ throw new RaydiumCodecError('truncated', `${what}: truncated pubkey at ${offset} of ${bytes.length} byte(s)`);
163
+ }
164
+ return { value: encodeBase58(bytes.subarray(offset, offset + 32)), size: 32 };
165
+ }
166
+ if (type === 'amm_fees') {
167
+ const len = artifact.ammFees.len;
168
+ if (offset + len > bytes.length) {
169
+ throw new RaydiumCodecError('truncated', `${what}: truncated fees struct (${len} bytes) at ${offset} of ${bytes.length} byte(s)`);
170
+ }
171
+ const value = {};
172
+ let at = offset;
173
+ for (const field of artifact.ammFees.fields) {
174
+ value[field.name] = readLe(bytes, at, INT_WIDTH[field.type]);
175
+ at += INT_WIDTH[field.type];
176
+ }
177
+ if (at - offset !== len) {
178
+ throw new RaydiumCodecError('malformed-artifact',
179
+ `${what}: amm_fees fields sum to ${at - offset} byte(s), artifact declares ${len}`);
180
+ }
181
+ return { value, size: len };
182
+ }
183
+ throw new RaydiumCodecError('unknown-arg-type', `${what}: arg type ${type} is not in the pinned table`);
184
+ }
185
+
186
+ /** Encode one arg back to bytes. Throws on any value that cannot be written. */
187
+ function encodeArg(spec, value, artifact, what) {
188
+ const type = spec.type;
189
+ if (type in INT_WIDTH) {
190
+ const width = INT_WIDTH[type];
191
+ const v = typeof value === 'string' && /^-?\d+$/.test(value) ? BigInt(value)
192
+ : typeof value === 'number' ? BigInt(value)
193
+ : typeof value === 'bigint' ? value
194
+ : (() => { throw new RaydiumCodecError('bad-arg', `${what}: not an integer`); })();
195
+ if (v < 0n || v >= (1n << BigInt(width * 8))) {
196
+ throw new RaydiumCodecError('bad-arg', `${what}: out of ${type} range`);
197
+ }
198
+ const out = Buffer.alloc(width);
199
+ for (let index = 0; index < width; index++) out[index] = Number((v >> BigInt(index * 8)) & 0xffn);
200
+ return out;
201
+ }
202
+ if (type === 'pubkey') {
203
+ const raw = Buffer.from(decodeBase58(String(value)));
204
+ if (raw.length !== 32) throw new RaydiumCodecError('bad-arg', `${what}: not a 32-byte pubkey`);
205
+ return raw;
206
+ }
207
+ if (type === 'amm_fees') {
208
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
209
+ throw new RaydiumCodecError('bad-arg', `${what}: amm_fees must be an object of named fields`);
210
+ }
211
+ const chunks = [];
212
+ for (const field of artifact.ammFees.fields) {
213
+ if (value[field.name] === undefined) {
214
+ throw new RaydiumCodecError('bad-arg', `${what}: missing fee field ${field.name}`);
215
+ }
216
+ chunks.push(encodeArg({ name: field.name, type: field.type }, value[field.name], artifact, `${what}.${field.name}`));
217
+ }
218
+ return Buffer.concat(chunks);
219
+ }
220
+ throw new RaydiumCodecError('unknown-arg-type', `${what}: arg type ${type} is not in the pinned table`);
221
+ }
222
+
223
+ /** The branch of `entry` that its discriminator selects, or null. */
224
+ function branchFor(entry, discriminator, artifact, what) {
225
+ for (const branch of entry.branches) {
226
+ if (Array.isArray(branch.values) && branch.values.includes(discriminator)) return branch;
227
+ }
228
+ // A wildcard branch (`values: null`) is upstream's own `match ... { _ => Err }`
229
+ // arm: it is a refusal, not a catch-all decode.
230
+ const wildcard = entry.branches.find((branch) => branch.values === null);
231
+ if (wildcard) return wildcard;
232
+ throw new RaydiumCodecError('unknown-branch',
233
+ `${what}: discriminator ${JSON.stringify(discriminator)} selects no branch of the pinned ${entry.name}`);
234
+ }
235
+
236
+ /**
237
+ * Decode one Raydium AMM v4 instruction payload.
238
+ *
239
+ * Returns `{refused: 'not-raydium'}` when the caller was never supposed to ask,
240
+ * throws a `RaydiumCodecError` (named reason) when this program's own table
241
+ * refuses, and returns a result — named AND re-encodable — otherwise.
242
+ *
243
+ * `trailing: 'report'` (default) returns the decoded instruction plus the byte
244
+ * count the table does not explain; 'error' refuses instead.
245
+ */
246
+ export function decodeRaydiumInstruction(program, bytes, { trailing = 'report' } = {}) {
247
+ const artifact = ARTIFACTS.get(program);
248
+ if (!artifact) return { refused: 'not-raydium' };
249
+ if (!(bytes instanceof Uint8Array) || bytes.length === 0) {
250
+ throw new RaydiumCodecError('empty-payload', `${program}: no instruction data`);
251
+ }
252
+ const tag = bytes[0];
253
+ const entry = BY_TAG.get(program).get(tag);
254
+ if (!entry) {
255
+ throw new RaydiumCodecError('unknown-selector', `${program}: tag ${tag} is not in the pinned ${artifact.label} interface`);
256
+ }
257
+ if (entry.wire === 'rejected') {
258
+ throw new RaydiumCodecError('wire-rejected',
259
+ `${program}: ${entry.name} (tag ${tag}) is refused by upstream itself — ${entry.reason}`);
260
+ }
261
+
262
+ const args = {};
263
+ const absent = [];
264
+ let offset = 1;
265
+ for (const spec of entry.args) {
266
+ const decoded = decodeArg(spec, bytes, offset, artifact, `${entry.name}.${spec.name}`);
267
+ args[spec.name] = decoded.value;
268
+ offset += decoded.size;
269
+ }
270
+
271
+ if (entry.optional) {
272
+ // Upstream's rule is a LENGTH GUARD, not an option tag: the group is there
273
+ // only when at least `presentWhenRemainingAtLeast` bytes remain. Fewer
274
+ // bytes than that are NOT the group, and are reported as trailing.
275
+ const threshold = entry.optional.presentWhenRemainingAtLeast;
276
+ const remaining = bytes.length - offset;
277
+ if (remaining >= threshold) {
278
+ for (const spec of entry.optional.args) {
279
+ const decoded = decodeArg(spec, bytes, offset, artifact, `${entry.name}.${spec.name}`);
280
+ args[spec.name] = decoded.value;
281
+ offset += decoded.size;
282
+ }
283
+ } else {
284
+ for (const spec of entry.optional.args) absent.push(spec.name);
285
+ }
286
+ }
287
+
288
+ if (entry.branches) {
289
+ const branch = branchFor(entry, args[entry.branches[0].on], artifact, `${entry.name}.${entry.branches[0].on}`);
290
+ if (branch.wire === 'rejected') {
291
+ throw new RaydiumCodecError('wire-rejected',
292
+ `${program}: ${entry.name} with ${entry.branches[0].on}=${JSON.stringify(args[entry.branches[0].on])} ` +
293
+ `is refused by upstream itself — ${branch.reason}`);
294
+ }
295
+ for (const spec of branch.args) {
296
+ const decoded = decodeArg(spec, bytes, offset, artifact, `${entry.name}.${spec.name}`);
297
+ args[spec.name] = decoded.value;
298
+ offset += decoded.size;
299
+ }
300
+ }
301
+
302
+ const trailingBytes = bytes.length - offset;
303
+ if (trailingBytes > 0 && trailing === 'error') {
304
+ throw new RaydiumCodecError('trailing-bytes', `${program}: ${entry.name} explains ${offset} of ${bytes.length} byte(s)`);
305
+ }
306
+ return {
307
+ name: entry.name,
308
+ tag,
309
+ selectorWidth: 1,
310
+ args,
311
+ // Optional fields the payload did NOT carry: the group is absent because
312
+ // the payload was shorter than upstream's own length threshold.
313
+ absent,
314
+ consumed: offset,
315
+ bytes: bytes.length,
316
+ trailing: { count: trailingBytes },
317
+ roundTripByteExact: encodeRaydiumInstruction(program, entry.name, args).equals(Buffer.from(bytes)),
318
+ scheme: artifact.scheme,
319
+ source: 'pinned-vendored-upstream',
320
+ };
321
+ }
322
+
323
+ /** Why the last `decodeRaydiumInstruction` refused, or null if it did not. */
324
+ export function raydiumRefusal(program, bytes) {
325
+ try {
326
+ const result = decodeRaydiumInstruction(program, bytes);
327
+ return result.refused ? result.refused : null;
328
+ } catch (error) {
329
+ return error instanceof RaydiumCodecError ? error.reason : `thrown: ${error.message}`;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Re-encode one Raydium instruction from a name + args. Throws on mismatch.
335
+ *
336
+ * An optional group is written only when EVERY one of its args is supplied, so
337
+ * a decode that found the group absent re-encodes to exactly the shorter wire;
338
+ * supplying only part of the group is refused rather than guessed.
339
+ */
340
+ export function encodeRaydiumInstruction(program, name, values = {}) {
341
+ const artifact = ARTIFACTS.get(program);
342
+ if (!artifact) throw new RaydiumCodecError('not-raydium', program);
343
+ const entry = BY_NAME.get(program).get(name);
344
+ if (!entry) throw new RaydiumCodecError('unknown-variant', `${program}: ${name}`);
345
+ if (entry.wire === 'rejected') {
346
+ throw new RaydiumCodecError('wire-rejected', `${program}: ${name} is refused by upstream itself — ${entry.reason}`);
347
+ }
348
+ const chunks = [Buffer.from([entry.tag])];
349
+ const write = (spec) => {
350
+ const value = values[spec.name];
351
+ if (value === undefined || value === null) {
352
+ throw new RaydiumCodecError('missing-arg', `${name}.${spec.name}`);
353
+ }
354
+ chunks.push(encodeArg(spec, value, artifact, `${name}.${spec.name}`));
355
+ };
356
+ for (const spec of entry.args) write(spec);
357
+ if (entry.optional) {
358
+ const supplied = entry.optional.args.map((spec) => values[spec.name] ?? null);
359
+ const present = supplied.filter((value) => value !== null).length;
360
+ if (present > 0 && present < supplied.length) {
361
+ const missing = entry.optional.args.filter((spec) => values[spec.name] == null).map((spec) => spec.name);
362
+ throw new RaydiumCodecError('partial-optional',
363
+ `${name}: optional group ${entry.optional.args.map((spec) => spec.name).join('+')} is all-or-none; missing ${missing.join(', ')}`);
364
+ }
365
+ if (present === supplied.length) for (const spec of entry.optional.args) write(spec);
366
+ }
367
+ if (entry.branches) {
368
+ const discriminator = values[entry.branches[0].on];
369
+ if (discriminator === undefined || discriminator === null) {
370
+ throw new RaydiumCodecError('missing-arg', `${name}.${entry.branches[0].on}`);
371
+ }
372
+ const branch = branchFor(entry, discriminator, artifact, `${name}.${entry.branches[0].on}`);
373
+ if (branch.wire === 'rejected') {
374
+ throw new RaydiumCodecError('wire-rejected',
375
+ `${name}: ${entry.branches[0].on}=${JSON.stringify(discriminator)} is refused by upstream itself — ${branch.reason}`);
376
+ }
377
+ for (const spec of branch.args) write(spec);
378
+ }
379
+ return Buffer.concat(chunks);
380
+ }
381
+
382
+ /** Names the table can claim for `program` (coverage callers). */
383
+ export function raydiumNames(program) {
384
+ const artifact = ARTIFACTS.get(program);
385
+ return artifact ? artifact.instructions.filter((entry) => entry.wire !== 'rejected').map((entry) => entry.name) : [];
386
+ }
387
+
388
+ /** Whether `name` belongs to `program`'s Raydium table. */
389
+ export function isRaydiumName(program, name) {
390
+ return BY_NAME.get(program)?.has(name) ?? false;
391
+ }
@@ -0,0 +1,84 @@
1
+ // The receipt's own digest — so a DECLARED field cannot be rewritten silently.
2
+ //
3
+ // The store binds BYTES by content address: the object's fingerprint is
4
+ // sha256(canonicalJson(idl) + '\n'), and every reader re-hashes the object
5
+ // before it decodes. The RECEIPT was never covered by anything: it is a JSON
6
+ // file next to the object, and every field in it is free text after the fact.
7
+ //
8
+ // Measured 2026-11-07 (this host, the published v0.4.10 tree, main 939b320):
9
+ // rewriting `origin` in a receipt to `attacker/rewritten-origin` and running
10
+ // `sememe verify --mode warn` reported
11
+ // violations: 0 verified: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"]
12
+ // The fingerprint covers the object; the ORIGIN next to it was covered by
13
+ // nothing, so the CI gate stayed green over a rewritten provenance label.
14
+ //
15
+ // The fix mirrors the object pattern one level up: the receipt carries a
16
+ // digest over ITS OWN BODY, so a reader can tell "this receipt was written by
17
+ // sememe" from "this file was edited afterwards" — exactly the question the
18
+ // object fingerprint already answers for the bytes.
19
+ //
20
+ // receiptDigest sha256(canonicalJson(body without receiptDigest) + '\n')
21
+ // originDigest sha256(origin) — binds the DECLARED label on its own,
22
+ // so a rewrite of the label ALONE is
23
+ // named as a label rewrite and not
24
+ // as an anonymous body change.
25
+ //
26
+ // The digest is over the CANONICAL serialization, the same one the store
27
+ // fingerprints objects with, so a receipt is reproducible from its body alone.
28
+ //
29
+ // COMPATIBILITY: receipts written before this field existed carry NO digest.
30
+ // They are read as they always were (the origin is then treated as DECLARED
31
+ // without a binding). A legacy receipt is never refused for lacking a digest —
32
+ // only a receipt whose digest DISAGREES is refused.
33
+
34
+ import { createHash } from 'node:crypto';
35
+ import { canonicalJson } from './canonical.mjs';
36
+
37
+ const sha256Hex = (data) => createHash('sha256').update(data).digest('hex');
38
+
39
+ /** The receipt body WITHOUT its own digest field. */
40
+ export function receiptBody(receipt) {
41
+ const { receiptDigest, ...body } = receipt ?? {};
42
+ return body;
43
+ }
44
+
45
+ /** The digest a receipt body must carry. */
46
+ export function receiptDigestOf(receipt) {
47
+ return `sha256:${sha256Hex(`${canonicalJson(receiptBody(receipt))}\n`)}`;
48
+ }
49
+
50
+ /** The digest a declared origin must carry. */
51
+ export function originDigestOf(origin) {
52
+ return `sha256:${sha256Hex(String(origin))}`;
53
+ }
54
+
55
+ /** The two checks a reader runs on a receipt that carries the new fields.
56
+ * Returns null when the receipt is consistent, or a named reason when it is
57
+ * not. `relabelled` distinguishes "the declared origin was rewritten" from
58
+ * "some other field of the receipt was rewritten" — the lock reports the two
59
+ * differently, because they are different failures. */
60
+ export function receiptIntegrity(receipt) {
61
+ const origin = receipt?.origin;
62
+ if (typeof receipt?.originDigest === 'string' && origin !== undefined && origin !== null) {
63
+ if (originDigestOf(origin) !== receipt.originDigest) {
64
+ return {
65
+ code: 'receipt-relabelled',
66
+ reason: `the declared origin was rewritten after the receipt was written `
67
+ + `(the receipt still binds "${originDigestOf(origin)}" but the receipt claims `
68
+ + `${receipt.originDigest}) — refusing to read it as provenance`,
69
+ };
70
+ }
71
+ }
72
+ if (typeof receipt?.receiptDigest === 'string') {
73
+ const actual = receiptDigestOf(receipt);
74
+ if (actual !== receipt.receiptDigest) {
75
+ return {
76
+ code: 'receipt-tampered',
77
+ reason: `the receipt was modified after it was written `
78
+ + `(body digest ${actual}, receipt claims ${receipt.receiptDigest}) — `
79
+ + `refusing to read it as provenance`,
80
+ };
81
+ }
82
+ }
83
+ return null;
84
+ }
@@ -0,0 +1,128 @@
1
+ // ONE classifier for what a file in `receipts/` actually IS.
2
+ //
3
+ // The store's receipt directory holds two kinds of things, and until now the
4
+ // readers treated EVERY file in it as a receipt:
5
+ //
6
+ // 1. a RECEIPT — `<program>-<slot>-<source>-<fingerprint>.json`, written by
7
+ // `sememe pull` (src/pull.mjs) or `sememe adopt` (src/adopt.mjs). The
8
+ // fingerprint is the sha256 of the stored object; the slot is a decimal
9
+ // number (0 for an adopted copy); the source is a short label; an adopted
10
+ // copy may carry an extra `<originKey>` segment.
11
+ // 2. DEBRIS — a file nobody committed. The most common source is sememe
12
+ // ITSELF: src/store-write.mjs writes every receipt and object through a
13
+ // temporary file `<name>.tmp-<pid>-<random>` in the SAME directory and
14
+ // removes it in a `finally`. A crash between the write and the cleanup
15
+ // (SIGKILL, power loss, OOM) leaves that file behind FOREVER.
16
+ //
17
+ // MEASURED 2026-11-07 (this host, pre-fix tree a7848d4): one leftover
18
+ // `.tmp-4711-9f2c1a` file in `receipts/` broke EVERY surface of the product:
19
+ //
20
+ // sememe decode <program> <payload> -> EXIT 1 "store receipt <tmp file>
21
+ // is unreadable: Unexpected token"
22
+ // sememe decode --union <program> ... -> EXIT 1, same refusal
23
+ // sememe lock -> EXIT 1, "Cannot destructure
24
+ // property 'idl' of (intermediate
25
+ // value) as it is null"
26
+ // sememe verify --mode error -> EXIT 1, "unverified: <program>"
27
+ //
28
+ // The last one is the worst: the gate went RED and named a program that was
29
+ // perfectly intact, and the repair was never named — the user is told to
30
+ // re-pull a healthy program. One crashed write permanently bricked the store.
31
+ //
32
+ // WHY THE DEBRIS IS SAFE TO IGNORE (this is the reason, not a convenience):
33
+ // store-write.mjs installs a receipt by HARD-LINKING the temporary file to its
34
+ // final name (`link(temporary, destination)`) and only then removes the
35
+ // temporary. So the temporary file never holds anything the final name does
36
+ // not also hold:
37
+ // - died BEFORE the link -> only the temp exists; the receipt was never
38
+ // committed, so there is nothing to read;
39
+ // - died AFTER the link -> the final name holds the receipt AND the temp
40
+ // holds a byte-identical copy of it, so the temp carries no information
41
+ // the committed receipt does not.
42
+ // Either way the debris is an UNCOMMITTED write, and ignoring it cannot hide
43
+ // an answer.
44
+ //
45
+ // THE OTHER TWO KINDS, and why they are not debris:
46
+ // - MALFORMED — a name that LOOKS like a receipt (it has a numeric slot
47
+ // segment) but is not well-formed (the fingerprint is not 64 hex, the
48
+ // name was truncated, an editor left a suffix). This is DAMAGE to the
49
+ // store: it may be a receipt that was renamed or corrupted. It stays in
50
+ // the verified set, so the reader refuses it by name — exactly as it did
51
+ // before. Classifying it as ignorable junk would be a NEW green lie: a
52
+ // renamed receipt would vanish from the store's answers without a word.
53
+ // - FOREIGN — anything else in the directory (`.DS_Store`, `README`, a
54
+ // user's own note). It names no program and carries no slot, so it is
55
+ // not a receipt and never was one. Today it was read as one: `.DS_Store`
56
+ // became a PHANTOM program in `sememe verify` ("extra: [.DS_Store]"), and
57
+ // `my-file.json` became a phantom program "my" that FAILED the gate
58
+ // ("unverified: my") — a false red over a file that was never a receipt.
59
+ //
60
+ // So the rule is narrow on purpose: only what can be PROVEN to be an
61
+ // uncommitted write is ignored. Everything else keeps its previous verdict.
62
+ //
63
+ // A receipt whose name matches the grammar is a receipt WHATEVER its program
64
+ // id looks like — the grammar says nothing about base58, because a store may
65
+ // hold a synthetic program id and a reader that dropped such a receipt would
66
+ // be the green lie this repo keeps killing.
67
+
68
+ /** A receipt filename: `<program>-<slot>-<middle>-<64 hex>.json`.
69
+ * The middle segment may itself contain dashes (an adopted copy carries an
70
+ * extra `<originKey>`), so the fingerprint tail is anchored at the end. */
71
+ const RECEIPT_GRAMMAR = /^[^-]+-\d+-.+-[a-f0-9]{64}\.json$/;
72
+
73
+ /** sememe's own temporary file: `<something>.tmp-<pid>-<random hex>`. */
74
+ const DEBRIS_GRAMMAR = /\.tmp-\d+-[0-9a-f]+$/;
75
+
76
+ /** A name that looks like a receipt: it carries a numeric slot segment.
77
+ * `<program>-<slot>-...` — the shape every receipt has, whether or not the
78
+ * rest of the name is well-formed. */
79
+ const RECEIPT_SHAPED = /^[^-]+-\d+-/;
80
+
81
+ /**
82
+ * What one file in `receipts/` is.
83
+ *
84
+ * Returns one of:
85
+ * 'receipt' a well-formed receipt — read it, verify it, bind it.
86
+ * 'malformed' receipt-shaped but not well-formed — DAMAGE: read it so the
87
+ * refusal names it; never ignore it.
88
+ * 'debris' sememe's own uncommitted temporary file — ignore it, report
89
+ * it by name.
90
+ * 'foreign' a file that is not a receipt at all — ignore it, report it
91
+ * by name.
92
+ *
93
+ * The order matters: the debris rule is checked FIRST, because a debris file
94
+ * of a receipt-shaped name (`<receipt>.tmp-<pid>-<hex>`) is still debris —
95
+ * sememe's own leftovers are exactly the ones that carry a slot segment.
96
+ */
97
+ export function classifyReceiptFile(name) {
98
+ if (DEBRIS_GRAMMAR.test(name)) return 'debris';
99
+ if (RECEIPT_GRAMMAR.test(name)) return 'receipt';
100
+ if (RECEIPT_SHAPED.test(name)) return 'malformed';
101
+ return 'foreign';
102
+ }
103
+
104
+ /** True for the kinds that are receipts for the purposes of reading a store:
105
+ * well-formed receipts, and malformed ones (which must still be refused by
106
+ * name rather than skipped in silence). */
107
+ export function isReceiptFile(name) {
108
+ const kind = classifyReceiptFile(name);
109
+ return kind === 'receipt' || kind === 'malformed';
110
+ }
111
+
112
+ /**
113
+ * Partition a directory listing into the four kinds.
114
+ *
115
+ * `receipts` are the names to read and verify; `stray` are the names to
116
+ * report but not read (each with the kind that excluded it, so the report
117
+ * can say WHY a file was ignored).
118
+ */
119
+ export function classifyReceiptFiles(names) {
120
+ const receipts = [];
121
+ const stray = [];
122
+ for (const name of names) {
123
+ const kind = classifyReceiptFile(name);
124
+ if (kind === 'receipt' || kind === 'malformed') receipts.push(name);
125
+ else stray.push({ name, kind });
126
+ }
127
+ return { receipts, stray };
128
+ }
@@ -0,0 +1,56 @@
1
+ // ONE ordering for store receipts — because two orderings are two answers.
2
+ //
3
+ // The receipt filename is `<program>-<slot>-<source>-<fingerprint>.json`, and
4
+ // the SLOT inside it is a decimal number stored as a string. Sorting receipt
5
+ // names with a plain string comparison therefore orders by DIGITS, not by
6
+ // value: `…-99999999-…` sorts AFTER `…-123456789-…`, so the OLDER receipt wins
7
+ // "last position" — and `findStoredIdl` takes the last position as "newest".
8
+ //
9
+ // Measured 2026-11-07 (this host, pre-fix tree 00ae18c, the code the published
10
+ // v0.4.9 carries): with one program's receipts at slots 99999999 and 123456789
11
+ // in one store, `sememe decode` answered from slot 99999999 while
12
+ // `sememe decode --union` answered from 123456789 — the union path has always
13
+ // sorted on-chain receipts by NUMERIC slot (`onChain.sort((a, b) => a.slot -
14
+ // b.slot)`), while the plain reader and src/lock.mjs sorted by filename. Two
15
+ // sorts, two winners: the same defect shape 08eff55 fixed for labels, one
16
+ // dimension over, and the reason "the rule is now ONE ordering everywhere"
17
+ // was not yet true.
18
+ //
19
+ // The rule here:
20
+ // 1. by SLOT, numerically (adopted copies carry filename segment 0, so they
21
+ // sort before every real slot — adoption never switches the default
22
+ // source, the selection guard the union tests protect);
23
+ // 2. ties broken by the REST of the filename, byte-wise — so two receipts of
24
+ // the same slot (different sources) keep a deterministic, reproducible
25
+ // order instead of depending on readdir() order.
26
+ //
27
+ // Why parse the filename instead of reading each receipt: the selection rule
28
+ // must be computable before opening files (findStoredIdl filters and sorts
29
+ // first, and lock.mjs does the same), and it must not depend on which receipts
30
+ // are readable — an unreadable receipt is named by readVerified, not silently
31
+ // re-ordered around.
32
+
33
+ const RECEIPT_NAME = /^(?<program>[^-]+)-(?<slot>\d+)-(?<rest>.*)$/;
34
+
35
+ /** The slot a receipt filename declares, or null when the name is not a
36
+ * receipt name this rule understands (never happens for pull/adopt output). */
37
+ export function slotOfReceiptName(name) {
38
+ const match = RECEIPT_NAME.exec(name);
39
+ if (!match) return null;
40
+ const slot = Number(match.groups.slot);
41
+ return Number.isSafeInteger(slot) ? slot : null;
42
+ }
43
+
44
+ /**
45
+ * Compare two receipt filenames: numeric slot first, filename as tiebreak.
46
+ * Names this rule cannot parse sort before parsed ones (stable, deterministic)
47
+ * so an unexpected name can never displace a real receipt's position silently.
48
+ */
49
+ export function compareReceiptNames(a, b) {
50
+ const slotA = slotOfReceiptName(a);
51
+ const slotB = slotOfReceiptName(b);
52
+ if (slotA !== null && slotB !== null && slotA !== slotB) return slotA - slotB;
53
+ if (slotA === null && slotB !== null) return -1;
54
+ if (slotA !== null && slotB === null) return 1;
55
+ return a < b ? -1 : a > b ? 1 : 0;
56
+ }