sememe 0.0.0-stage → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2667 -2
- package/bin/sememe.mjs +556 -0
- package/index.mjs +48 -0
- package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
- package/interface/native-11111111111111111111111111111111.json +226 -0
- package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
- package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
- package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
- package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
- package/interface/token-metadata.json +132 -0
- package/package.json +71 -4
- package/src/account-list.mjs +308 -0
- package/src/account-union.mjs +523 -0
- package/src/account.mjs +199 -0
- package/src/acquire.mjs +145 -0
- package/src/adopt.mjs +205 -0
- package/src/ata-interface.mjs +276 -0
- package/src/ata.mjs +223 -0
- package/src/base58.mjs +96 -0
- package/src/canonical.mjs +24 -0
- package/src/cli-surface.mjs +300 -0
- package/src/codec.mjs +1069 -0
- package/src/conformance.mjs +431 -0
- package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
- package/src/corpus/harvest-system-2026-10-31.json +26090 -0
- package/src/decode.mjs +286 -0
- package/src/dialect.mjs +145 -0
- package/src/discriminators.mjs +177 -0
- package/src/drift.mjs +172 -0
- package/src/gap.mjs +205 -0
- package/src/lighthouse.mjs +168 -0
- package/src/lock.mjs +440 -0
- package/src/native-audit.mjs +56 -0
- package/src/native-corpus.mjs +57 -0
- package/src/native-interface.mjs +252 -0
- package/src/native.mjs +324 -0
- package/src/peer.mjs +95 -0
- package/src/program-id.mjs +74 -0
- package/src/pull.mjs +117 -0
- package/src/rawle.mjs +450 -0
- package/src/raydium-interface.mjs +747 -0
- package/src/raydium.mjs +391 -0
- package/src/receipt-digest.mjs +84 -0
- package/src/receipt-name.mjs +128 -0
- package/src/receipt-order.mjs +56 -0
- package/src/shank.mjs +73 -0
- package/src/short-forms.mjs +209 -0
- package/src/store-read.mjs +250 -0
- package/src/store-write.mjs +46 -0
- package/src/token-interface.mjs +394 -0
- package/src/token-metadata-interface.mjs +371 -0
- package/src/token-metadata.mjs +317 -0
- package/src/token.mjs +443 -0
- package/src/union.mjs +428 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Conformance of a WHOLE TRANSACTION against the stored interfaces.
|
|
2
|
+
//
|
|
3
|
+
// `src/account-list.mjs` answers "does this account list match this
|
|
4
|
+
// instruction's declaration". This file answers "what does the stored
|
|
5
|
+
// interface say about every instruction of this transaction", using the
|
|
6
|
+
// same verified store the decoder uses.
|
|
7
|
+
//
|
|
8
|
+
// The transaction shape is the RPC `encoding: "json"` shape (getTransaction
|
|
9
|
+
// or getBlock), because that is what a consumer has. Two facts of that shape
|
|
10
|
+
// matter and are easy to get wrong:
|
|
11
|
+
//
|
|
12
|
+
// - `message.accountKeys` holds only addresses. The SIGNER and WRITABLE
|
|
13
|
+
// flags live in `message.header` as COUNTS, in the canonical order
|
|
14
|
+
// [signers-writable, signers-readonly, non-signers-writable,
|
|
15
|
+
// non-signers-readonly]
|
|
16
|
+
// and the loaded (address-lookup-table) addresses follow, writable
|
|
17
|
+
// first. Reading the flags off the key objects — as many tools do when
|
|
18
|
+
// they use the `jsonParsed` encoding — is a DIFFERENT shape; this file
|
|
19
|
+
// handles the `json` shape and says so when the shape is not one it
|
|
20
|
+
// understands.
|
|
21
|
+
// - an instruction's `accounts` are INDICES into that key list, not
|
|
22
|
+
// addresses. Resolving them wrongly turns a signer question into a
|
|
23
|
+
// no-op. An index that does NOT resolve (past the end, negative, a
|
|
24
|
+
// non-integer, or an object from the `jsonParsed` shape this file does
|
|
25
|
+
// not model) is a shape this gate cannot check — it must REFUSE, never
|
|
26
|
+
// fold the position into a null row (measured 2026-11-11, v0.4.16:
|
|
27
|
+
// null rows downgraded the fixture's real `undeclared-signing-account`
|
|
28
|
+
// disagreement to `remaining-accounts`, exit 0).
|
|
29
|
+
//
|
|
30
|
+
// When the shape cannot be understood (no header, no account keys, an
|
|
31
|
+
// encoding this file does not model), the transaction is REFUSED with a
|
|
32
|
+
// named reason instead of being reported as "conforms". An unknown shape
|
|
33
|
+
// must never be reported as a clean bill of health.
|
|
34
|
+
|
|
35
|
+
import { checkAccountList, instructionByDiscriminator, declaredAccounts, summarizeConformance } from './account-list.mjs';
|
|
36
|
+
import { withDerivedDiscriminators } from './discriminators.mjs';
|
|
37
|
+
import { findStoredIdls } from './store-read.mjs';
|
|
38
|
+
import { isProgramId } from './program-id.mjs';
|
|
39
|
+
|
|
40
|
+
export class ConformanceRefusal extends Error {
|
|
41
|
+
constructor(reason, detail = '') {
|
|
42
|
+
super(`conformance refused: ${reason}${detail ? ` — ${detail}` : ''}`);
|
|
43
|
+
this.name = 'ConformanceRefusal';
|
|
44
|
+
this.reason = reason;
|
|
45
|
+
this.detail = detail;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The canonical key list of a transaction, with each key's signer and
|
|
51
|
+
* writable flags derived from the message header.
|
|
52
|
+
*
|
|
53
|
+
* @returns {{ keys: Array<{address: string, signer: boolean, writable: boolean}>, byIndex: Array }}
|
|
54
|
+
*/
|
|
55
|
+
export function transactionKeys(transaction) {
|
|
56
|
+
const message = transaction?.transaction?.message;
|
|
57
|
+
const meta = transaction?.meta ?? {};
|
|
58
|
+
if (!message || !Array.isArray(message.accountKeys)) {
|
|
59
|
+
throw new ConformanceRefusal('unreadable-transaction', 'no transaction.message.accountKeys');
|
|
60
|
+
}
|
|
61
|
+
const header = message.header;
|
|
62
|
+
if (!header || !Number.isSafeInteger(header.numRequiredSignatures)) {
|
|
63
|
+
throw new ConformanceRefusal('unreadable-transaction', 'no message.header (jsonParsed encoding or a truncated response)');
|
|
64
|
+
}
|
|
65
|
+
const readonlySigned = Number.isSafeInteger(header.numReadonlySignedAccounts) ? header.numReadonlySignedAccounts : 0;
|
|
66
|
+
const readonlyUnsigned = Number.isSafeInteger(header.numReadonlyUnsignedAccounts) ? header.numReadonlyUnsignedAccounts : 0;
|
|
67
|
+
|
|
68
|
+
const addresses = message.accountKeys.map((key) => (typeof key === 'string' ? key : key?.pubkey));
|
|
69
|
+
if (addresses.some((address) => typeof address !== 'string' || !address)) {
|
|
70
|
+
throw new ConformanceRefusal('unreadable-transaction', 'account key without an address');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const staticCount = addresses.length;
|
|
74
|
+
const loaded = meta.loadedAddresses ?? {};
|
|
75
|
+
const loadedWritable = Array.isArray(loaded.writable) ? loaded.writable : [];
|
|
76
|
+
const loadedReadonly = Array.isArray(loaded.readonly) ? loaded.readonly : [];
|
|
77
|
+
const all = [...addresses, ...loadedWritable, ...loadedReadonly];
|
|
78
|
+
|
|
79
|
+
const keys = all.map((address, index) => {
|
|
80
|
+
let signer = false;
|
|
81
|
+
let writable = false;
|
|
82
|
+
if (index < staticCount) {
|
|
83
|
+
signer = index < header.numRequiredSignatures;
|
|
84
|
+
const signerWritableCount = header.numRequiredSignatures - readonlySigned;
|
|
85
|
+
if (signer) writable = index < signerWritableCount;
|
|
86
|
+
else writable = index < header.numRequiredSignatures + readonlyUnsigned;
|
|
87
|
+
} else {
|
|
88
|
+
writable = index < staticCount + loadedWritable.length;
|
|
89
|
+
}
|
|
90
|
+
return { address, signer, writable };
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { keys, byIndex: keys };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The program id an instruction invokes, resolved against the key list. */
|
|
97
|
+
export function programIdOf(instruction, keys) {
|
|
98
|
+
if (!instruction) return null;
|
|
99
|
+
if (typeof instruction.programId === 'string') return instruction.programId;
|
|
100
|
+
const index = instruction.programIdIndex;
|
|
101
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= keys.length) return null;
|
|
102
|
+
return keys[index].address;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Every instruction of the transaction (top level and CPI), with the
|
|
106
|
+
* account list resolved to addresses + flags. */
|
|
107
|
+
export function transactionInstructions(transaction) {
|
|
108
|
+
const message = transaction?.transaction?.message;
|
|
109
|
+
const meta = transaction?.meta ?? {};
|
|
110
|
+
const { keys } = transactionKeys(transaction);
|
|
111
|
+
const out = [];
|
|
112
|
+
|
|
113
|
+
const push = (instruction, depth, position, parentIndex) => {
|
|
114
|
+
const accounts = Array.isArray(instruction?.accounts) ? instruction.accounts : [];
|
|
115
|
+
// Account references this key list CANNOT resolve: out of range,
|
|
116
|
+
// negative, non-integer (a jsonParsed object entry lands here too), or
|
|
117
|
+
// absent from the loaded range. They are collected per instruction so
|
|
118
|
+
// `checkTransaction` can refuse the shape by name — a null row would
|
|
119
|
+
// let `checkAccountList` file them under "signer unknown" (a note) and
|
|
120
|
+
// the gate would exit 0 over an account list it never checked.
|
|
121
|
+
const unresolvedAccounts = [];
|
|
122
|
+
const resolved = accounts.map((index, positionInInstruction) => {
|
|
123
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= keys.length) {
|
|
124
|
+
unresolvedAccounts.push({ position: positionInInstruction, value: index });
|
|
125
|
+
return { address: null, signer: null, writable: null };
|
|
126
|
+
}
|
|
127
|
+
const key = keys[index];
|
|
128
|
+
return { address: key.address, signer: key.signer, writable: key.writable };
|
|
129
|
+
});
|
|
130
|
+
out.push({
|
|
131
|
+
depth,
|
|
132
|
+
position,
|
|
133
|
+
parentIndex,
|
|
134
|
+
programId: programIdOf(instruction, keys),
|
|
135
|
+
// The RAW program id reference the message carried, kept so a refusal
|
|
136
|
+
// can name the offending value instead of only its resolved shape
|
|
137
|
+
// (src/conformance.mjs `instruction-program-id-unresolvable`).
|
|
138
|
+
programIdRef: instruction?.programId ?? instruction?.programIdIndex ?? null,
|
|
139
|
+
accounts: resolved,
|
|
140
|
+
accountIndices: accounts.slice(),
|
|
141
|
+
unresolvedAccounts,
|
|
142
|
+
dataBase58: instruction?.data ?? '',
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
(message?.instructions ?? []).forEach((instruction, index) => push(instruction, 0, index, null));
|
|
147
|
+
const groups = meta.innerInstructions ?? message?.innerInstructions ?? [];
|
|
148
|
+
for (const group of groups) {
|
|
149
|
+
const parentIndex = group?.index ?? null;
|
|
150
|
+
(group?.instructions ?? []).forEach((instruction, position) => push(instruction, group?.depth ?? 1, position, parentIndex));
|
|
151
|
+
}
|
|
152
|
+
return { instructions: out, keys };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The 8 discriminator bytes of an instruction's data, or null. */
|
|
156
|
+
export function discriminatorOf(dataBase58) {
|
|
157
|
+
if (typeof dataBase58 !== 'string' || !dataBase58.length) return null;
|
|
158
|
+
// base58 decode without a third-party import: the alphabet is fixed.
|
|
159
|
+
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
160
|
+
let value = 0n;
|
|
161
|
+
for (const character of dataBase58) {
|
|
162
|
+
const digit = ALPHABET.indexOf(character);
|
|
163
|
+
if (digit < 0) return null;
|
|
164
|
+
value = value * 58n + BigInt(digit);
|
|
165
|
+
}
|
|
166
|
+
const bytes = [];
|
|
167
|
+
while (value > 0n) { bytes.unshift(Number(value & 0xffn)); value >>= 8n; }
|
|
168
|
+
for (const character of dataBase58) { if (character === '1') bytes.unshift(0); else break; }
|
|
169
|
+
return bytes.length >= 8 ? bytes.slice(0, 8) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check every instruction of a transaction against the interfaces stored for
|
|
174
|
+
* the programs it invokes.
|
|
175
|
+
*
|
|
176
|
+
* WHAT THIS REFUSES (measured 2026-11-09 on the published v0.4.12 install):
|
|
177
|
+
* a report over ZERO instructions used to exit 0 — a green CI gate over a
|
|
178
|
+
* check that never ran. Two ways to get there, both silent:
|
|
179
|
+
*
|
|
180
|
+
* * `--only <program>` where the program is never invoked by this
|
|
181
|
+
* transaction -> `rows: []`, `summary.instructions: 0`, exit 0, no
|
|
182
|
+
* disagreement reported, because there was nothing to disagree about;
|
|
183
|
+
* * `--only <not-a-program-id>` -> the same empty green, and the value
|
|
184
|
+
* could not even be a program id.
|
|
185
|
+
*
|
|
186
|
+
* The rule is the one this file already states for an unknown SHAPE: an
|
|
187
|
+
* empty check is not a clean bill of health. When nothing can be checked,
|
|
188
|
+
* the transaction (or the filter) is REFUSED with a named reason that
|
|
189
|
+
* carries the repair — for a filter miss, the program ids the transaction
|
|
190
|
+
* actually invokes, so a typo is visible instead of green.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} transaction an RPC `encoding: "json"` transaction
|
|
193
|
+
* @param {string} storeDir a sememe store directory
|
|
194
|
+
* @param {object} [options]
|
|
195
|
+
* @param {string[]} [options.only] restrict to these program ids
|
|
196
|
+
* @returns {Promise<{ rows: object[], summary: object }>}
|
|
197
|
+
* @throws {ConformanceRefusal} when the check would cover no instruction
|
|
198
|
+
*/
|
|
199
|
+
export async function checkTransaction(transaction, storeDir, options = {}) {
|
|
200
|
+
const { only = null } = options;
|
|
201
|
+
const { instructions } = transactionInstructions(transaction);
|
|
202
|
+
const executed = transaction?.meta?.err === null || transaction?.meta?.err === undefined
|
|
203
|
+
? true
|
|
204
|
+
: false;
|
|
205
|
+
|
|
206
|
+
// Decide what CAN be checked before reading the store: an empty check is a
|
|
207
|
+
// refusal, and the refusal must not depend on which interfaces happen to be
|
|
208
|
+
// stored (that would make the verdict about the store, not about the filter).
|
|
209
|
+
if (only !== null) {
|
|
210
|
+
for (const entry of only) {
|
|
211
|
+
if (!isProgramId(entry)) {
|
|
212
|
+
throw new ConformanceRefusal(
|
|
213
|
+
'filter-is-not-a-program-id',
|
|
214
|
+
`${JSON.stringify(String(entry))} — a program id is 32 bytes in base58; ` + 'this filter can never match an instruction',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const invoked = [...new Set(instructions.map((i) => i.programId).filter(Boolean))];
|
|
220
|
+
const unresolvable = instructions
|
|
221
|
+
.map((instruction, index) => ({ instruction, index }))
|
|
222
|
+
.filter(({ instruction }) => !instruction.programId);
|
|
223
|
+
|
|
224
|
+
// An instruction whose program id CANNOT be resolved is not a hole in the
|
|
225
|
+
// report — it is a shape this gate cannot check, and a partial check must
|
|
226
|
+
// never be reported as a check of the transaction it was handed.
|
|
227
|
+
//
|
|
228
|
+
// MEASURED 2026-11-11 (this host, pre-fix tree 94aa00b = the published
|
|
229
|
+
// v0.4.15, order-engine fixture): the loop below used to `continue` over an
|
|
230
|
+
// instruction with an unresolvable programId — no row, no count, no
|
|
231
|
+
// refusal. With the fill instruction's programIdIndex pushed past the end
|
|
232
|
+
// of accountKeys, the gate reported 11 of 12 instructions,
|
|
233
|
+
// `byVerdict {no-stored-interface: 11}`, exit 0 — while the SAME
|
|
234
|
+
// transaction with an intact index exits 1 with
|
|
235
|
+
// `undeclared-signing-account`. The transaction's REAL disagreement was not
|
|
236
|
+
// just unchecked, it was HIDDEN behind a report that looked complete.
|
|
237
|
+
//
|
|
238
|
+
// The order of the refusals matters:
|
|
239
|
+
// * ALL instructions unresolvable keeps its EXISTING reason
|
|
240
|
+
// (`transaction-has-no-resolvable-program-id`) — that refusal already
|
|
241
|
+
// existed and must not be rewritten;
|
|
242
|
+
// * SOME unresolvable is the middle case that was silent, and it is
|
|
243
|
+
// refused here — BEFORE the filter selection, because a filter that
|
|
244
|
+
// selects nothing (`filter-matched-no-instruction`) must not mask a
|
|
245
|
+
// transaction whose shape is broken, and a broken shape must not be
|
|
246
|
+
// reported as "this filter matched nothing";
|
|
247
|
+
// * a filter that is not a program id is still refused first (above) —
|
|
248
|
+
// that is about the CALLER's argument, not about the transaction.
|
|
249
|
+
if (unresolvable.length && unresolvable.length === instructions.length) {
|
|
250
|
+
throw new ConformanceRefusal(
|
|
251
|
+
'transaction-has-no-resolvable-program-id',
|
|
252
|
+
`all ${instructions.length} instruction(s) lack a program id that resolves inside message.accountKeys — `
|
|
253
|
+
+ 'this shape cannot be checked',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (unresolvable.length) {
|
|
257
|
+
const described = unresolvable
|
|
258
|
+
.map(({ instruction, index }) => {
|
|
259
|
+
const raw = instruction.programIdRef;
|
|
260
|
+
const shown = raw === undefined || raw === null ? '(none)' : JSON.stringify(raw);
|
|
261
|
+
const where = instruction.depth
|
|
262
|
+
? `CPI of top-level #${instruction.parentIndex} at position ${instruction.position}`
|
|
263
|
+
: `#${instruction.position ?? index}`;
|
|
264
|
+
return `${where} (programIdIndex ${shown})`;
|
|
265
|
+
})
|
|
266
|
+
.join(', ');
|
|
267
|
+
throw new ConformanceRefusal(
|
|
268
|
+
'instruction-program-id-unresolvable',
|
|
269
|
+
`${unresolvable.length} of ${instructions.length} instruction(s) have no program id that resolves inside `
|
|
270
|
+
+ `message.accountKeys (${described}) — the gate cannot report a transaction it can only check in part; `
|
|
271
|
+
+ 'repair: fix programIdIndex (or set programId) on the named instruction(s)',
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// An ACCOUNT reference the key list cannot resolve is the same defect one
|
|
276
|
+
// level over (the program-id sibling was refused 2026-11-11, v0.4.16).
|
|
277
|
+
//
|
|
278
|
+
// MEASURED 2026-11-11 (this host, pre-fix tree 7b30c68 = the published
|
|
279
|
+
// v0.4.16, order-engine fixture): resolving `keys[index]` past the end
|
|
280
|
+
// returned `{address: null, signer: null, writable: null}`, and
|
|
281
|
+
// `checkAccountList` files a null signer under `unknownSigner` — a NOTE,
|
|
282
|
+
// never a violation. Breaking the ONE signing extra account's index
|
|
283
|
+
// downgraded the fill from `undeclared-signing-account` (exit 1) to
|
|
284
|
+
// `remaining-accounts` (EXIT 0): the transaction's only real disagreement
|
|
285
|
+
// hidden behind a report that looked complete. An object entry (the
|
|
286
|
+
// `jsonParsed` shape this file's header says it does not model) did the
|
|
287
|
+
// same for the WHOLE transaction.
|
|
288
|
+
//
|
|
289
|
+
// The refusal runs BEFORE the store is read and BEFORE the filter
|
|
290
|
+
// selection: it is about the transaction's shape, not about which
|
|
291
|
+
// interface the store happens to hold, and a broken shape must never be
|
|
292
|
+
// reported as "this filter matched nothing".
|
|
293
|
+
const unresolvableAccounts = instructions
|
|
294
|
+
.map((instruction, index) => ({ instruction, index }))
|
|
295
|
+
.filter(({ instruction }) => instruction.unresolvedAccounts.length > 0);
|
|
296
|
+
if (unresolvableAccounts.length) {
|
|
297
|
+
const described = unresolvableAccounts.map(({ instruction, index }) => {
|
|
298
|
+
const where = instruction.depth
|
|
299
|
+
? `CPI of top-level #${instruction.parentIndex} at position ${instruction.position}`
|
|
300
|
+
: `#${instruction.position ?? index}`;
|
|
301
|
+
const values = instruction.unresolvedAccounts
|
|
302
|
+
.map((entry) => `account position ${entry.position} (value ${JSON.stringify(entry.value)})`)
|
|
303
|
+
.join(', ');
|
|
304
|
+
return `${where} — ${values}`;
|
|
305
|
+
}).join('; ');
|
|
306
|
+
throw new ConformanceRefusal(
|
|
307
|
+
'instruction-account-index-unresolvable',
|
|
308
|
+
`${unresolvableAccounts.length} of ${instructions.length} instruction(s) carry account indices that do not `
|
|
309
|
+
+ `resolve inside message.accountKeys + meta.loadedAddresses (${described}) — the gate cannot report a `
|
|
310
|
+
+ 'transaction whose account list it can only check in part; repair: fix the named account index '
|
|
311
|
+
+ '(indices are integers into the canonical key list, loaded addresses included)',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const eligible = instructions.filter((i) => i.programId && (!only || only.includes(i.programId)));
|
|
316
|
+
if (eligible.length === 0) {
|
|
317
|
+
if (!instructions.length) {
|
|
318
|
+
throw new ConformanceRefusal(
|
|
319
|
+
'transaction-has-no-instruction',
|
|
320
|
+
'the transaction exposes no instruction to check — nothing here can be reported as conforming',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (!invoked.length) {
|
|
324
|
+
throw new ConformanceRefusal(
|
|
325
|
+
'transaction-has-no-resolvable-program-id',
|
|
326
|
+
`all ${instructions.length} instruction(s) lack a program id that resolves inside message.accountKeys — `
|
|
327
|
+
+ 'this shape cannot be checked',
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (only) {
|
|
331
|
+
throw new ConformanceRefusal(
|
|
332
|
+
'filter-matched-no-instruction',
|
|
333
|
+
`--only ${JSON.stringify(only)} selects none of this transaction's instructions; it invokes: ${invoked.join(', ')}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
throw new ConformanceRefusal('no-instruction-is-checkable', `the transaction invokes: ${invoked.join(', ')}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const rows = [];
|
|
340
|
+
const idlCache = new Map();
|
|
341
|
+
for (const instruction of instructions) {
|
|
342
|
+
if (only && !only.includes(instruction.programId)) continue;
|
|
343
|
+
let idl = idlCache.get(instruction.programId);
|
|
344
|
+
if (idl === undefined) {
|
|
345
|
+
try {
|
|
346
|
+
const sources = await findStoredIdls(storeDir, instruction.programId);
|
|
347
|
+
idl = sources.primary?.idl ?? sources.secondary?.idl ?? null;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// THE READER'S REFUSAL IS NOT A VERDICT ABOUT THE TRANSACTION.
|
|
350
|
+
//
|
|
351
|
+
// findStoredIdls is the ONE store reader that refuses damage by name
|
|
352
|
+
// (src/store-read.mjs): a receipt whose object no longer re-hashes to
|
|
353
|
+
// its fingerprint, a receipt bound to another program, a missing
|
|
354
|
+
// object, a malformed receipt, a rewritten origin. Swallowing that
|
|
355
|
+
// here turned every refusal into `no-stored-interface` and the gate
|
|
356
|
+
// exited 0 — measured 2026-11-10 on the order-engine fixture: a
|
|
357
|
+
// tampered receipt made the transaction's REAL disagreement
|
|
358
|
+
// (undeclared-signing-account) disappear behind 12
|
|
359
|
+
// `no-stored-interface` rows and a green exit.
|
|
360
|
+
//
|
|
361
|
+
// Exactly ONE failure is not damage: the store holds no receipt for
|
|
362
|
+
// this program (code `no-stored-idl`). That is an honest acquisition
|
|
363
|
+
// gap and keeps its own verdict below. A missing store
|
|
364
|
+
// (`no-sememe-store`) and every reader refusal are REFUSED by name,
|
|
365
|
+
// carrying the reader's own message and the repair.
|
|
366
|
+
const code = error?.code ?? null;
|
|
367
|
+
if (code !== 'no-stored-idl') {
|
|
368
|
+
const reason = code === 'no-sememe-store' ? 'no-sememe-store' : 'store-reader-refused';
|
|
369
|
+
throw new ConformanceRefusal(
|
|
370
|
+
reason,
|
|
371
|
+
`${instruction.programId}: ${String(error?.message ?? error)} — ` +
|
|
372
|
+
'the conformance gate cannot read this program from the store, so it cannot say the transaction conforms; ' +
|
|
373
|
+
'repair: restore the receipt (re-run "sememe pull") or point --store at a verified store',
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
idl = null;
|
|
377
|
+
}
|
|
378
|
+
idlCache.set(instruction.programId, idl);
|
|
379
|
+
}
|
|
380
|
+
const discriminator = discriminatorOf(instruction.dataBase58);
|
|
381
|
+
if (!idl) {
|
|
382
|
+
rows.push({
|
|
383
|
+
program: instruction.programId,
|
|
384
|
+
instruction: null,
|
|
385
|
+
executed,
|
|
386
|
+
depth: instruction.depth,
|
|
387
|
+
verdict: 'no-stored-interface',
|
|
388
|
+
violations: [],
|
|
389
|
+
extras: [],
|
|
390
|
+
notes: [],
|
|
391
|
+
unknownSigner: [],
|
|
392
|
+
positions: { declared: null, observed: instruction.accounts.length },
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (!discriminator) {
|
|
397
|
+
rows.push({
|
|
398
|
+
program: instruction.programId,
|
|
399
|
+
instruction: null,
|
|
400
|
+
executed,
|
|
401
|
+
depth: instruction.depth,
|
|
402
|
+
verdict: 'unknown-discriminator',
|
|
403
|
+
violations: [],
|
|
404
|
+
extras: [],
|
|
405
|
+
notes: [],
|
|
406
|
+
unknownSigner: [],
|
|
407
|
+
positions: { declared: null, observed: instruction.accounts.length },
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const { idl: filled } = withDerivedDiscriminators(idl);
|
|
412
|
+
const match = instructionByDiscriminator(filled, discriminator);
|
|
413
|
+
const result = checkAccountList(match, instruction.accounts, {
|
|
414
|
+
program: instruction.programId,
|
|
415
|
+
programId: instruction.programId,
|
|
416
|
+
executed,
|
|
417
|
+
depth: instruction.depth,
|
|
418
|
+
});
|
|
419
|
+
rows.push({
|
|
420
|
+
...result,
|
|
421
|
+
depth: instruction.depth,
|
|
422
|
+
accountCount: instruction.accounts.length,
|
|
423
|
+
declaredAccountCount: match ? declaredAccounts(match).length : null,
|
|
424
|
+
instructionNames: match ? match.name : null,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const summary = summarizeConformance(rows);
|
|
429
|
+
|
|
430
|
+
return { rows, summary };
|
|
431
|
+
}
|