sememe 0.0.0-stage → 0.4.22
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 +2777 -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 +206 -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 +1118 -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 +180 -0
- package/src/discriminators.mjs +192 -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 +454 -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 +253 -0
- package/src/store-write.mjs +46 -0
- package/src/token-interface.mjs +394 -0
- package/src/token-metadata-interface.mjs +371 -0
- package/src/token-metadata.mjs +317 -0
- package/src/token.mjs +443 -0
- package/src/union.mjs +428 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
package/src/decode.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Decode a Solana instruction payload with a locally stored, verified IDL.
|
|
2
|
+
//
|
|
3
|
+
// The store layout is the one produced by `sememe pull`:
|
|
4
|
+
// <store>/receipts/<program>-<slot>-<source>-<fingerprint>.json
|
|
5
|
+
// <store>/objects/<fingerprint>.json
|
|
6
|
+
// The receipt binds the program id to the object, so decoding is offline and
|
|
7
|
+
// reproducible: same store + same payload => same result, no network call.
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { canonicalJson } from './canonical.mjs';
|
|
13
|
+
import {
|
|
14
|
+
decodeData,
|
|
15
|
+
decodeInstructionData,
|
|
16
|
+
encodeInstructionData,
|
|
17
|
+
encodeEventData,
|
|
18
|
+
toJsonSafe,
|
|
19
|
+
} from './codec.mjs';
|
|
20
|
+
import { decodeBase58 } from './base58.mjs';
|
|
21
|
+
import { findStoredIdl, findStoredIdls } from './store-read.mjs';
|
|
22
|
+
import { unionDecode, UnionRefusal } from './union.mjs';
|
|
23
|
+
import { unionAccountDecode } from './account-union.mjs';
|
|
24
|
+
import { withDerivedDiscriminators } from './discriminators.mjs';
|
|
25
|
+
import { resolveAbsentPolicy, describeShortForm, assertShortFormEvidence } from './short-forms.mjs';
|
|
26
|
+
import {
|
|
27
|
+
withDerivedAccountDiscriminators, identifyAccount, decodeAccountData,
|
|
28
|
+
} from './account.mjs';
|
|
29
|
+
|
|
30
|
+
// Kept as a re-export: callers of `src/decode.mjs` import `decodeBase58`
|
|
31
|
+
// from here, and the implementation now lives in `src/base58.mjs` so the
|
|
32
|
+
// decode core has no third-party import at all.
|
|
33
|
+
export { decodeBase58 } from './base58.mjs';
|
|
34
|
+
|
|
35
|
+
export function decodeHex(input) {
|
|
36
|
+
const cleaned = input.replace(/^0x/, '');
|
|
37
|
+
if (!/^[0-9a-fA-F]*$/.test(cleaned) || cleaned.length % 2 !== 0) {
|
|
38
|
+
throw Error('hex payload must have an even number of hex digits');
|
|
39
|
+
}
|
|
40
|
+
return Buffer.from(cleaned, 'hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parsePayload(input) {
|
|
44
|
+
if (typeof input !== 'string') throw Error('payload must be a string');
|
|
45
|
+
const trimmed = input.trim();
|
|
46
|
+
if (/^0x[0-9a-fA-F]+$/.test(trimmed)) return decodeHex(trimmed);
|
|
47
|
+
if (/^[0-9a-fA-F]{16,}$/.test(trimmed) && trimmed.length % 2 === 0) return decodeHex(trimmed);
|
|
48
|
+
return decodeBase58(trimmed);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Find the newest stored IDL for a program id, verified against its receipt.
|
|
52
|
+
*
|
|
53
|
+
* The verification contract lives in `src/store-read.mjs`, shared by the
|
|
54
|
+
* single-source and the union reader: receipt program binding, object
|
|
55
|
+
* presence, re-hash against the receipt's fingerprint, address agreement.
|
|
56
|
+
* Any violation is refused with a named reason, never tolerated.
|
|
57
|
+
*/
|
|
58
|
+
export { findStoredIdl, findStoredIdls } from './store-read.mjs';
|
|
59
|
+
export { adopt } from './adopt.mjs';
|
|
60
|
+
export { unionDecode, UnionRefusal } from './union.mjs';
|
|
61
|
+
export { unionAccountDecode } from './account-union.mjs';
|
|
62
|
+
export { verifiedShortForm, VERIFIED_SHORT_FORMS, SHORT_FORM_SOURCE, describeShortForm, resolveAbsentPolicy } from './short-forms.mjs';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The provenance of the interface that answered — INCLUDING the declared
|
|
66
|
+
* origin and its status.
|
|
67
|
+
*
|
|
68
|
+
* The plain decode quoted only fingerprint/slot/source/receipt, so an ADOPTED
|
|
69
|
+
* copy's origin (a label the caller typed, never checked) never appeared on
|
|
70
|
+
* this surface at all: the union path showed it as a bare provenance claim
|
|
71
|
+
* and the plain path hid it. Both now name the same thing, and the status
|
|
72
|
+
* says the origin was DECLARED, not verified. An on-chain receipt names no
|
|
73
|
+
* file, so it carries no origin at all.
|
|
74
|
+
*/
|
|
75
|
+
function provenanceOf(receipt, receiptName) {
|
|
76
|
+
return {
|
|
77
|
+
fingerprint: receipt.fingerprint,
|
|
78
|
+
slot: receipt.slot,
|
|
79
|
+
source: receipt.source,
|
|
80
|
+
receipt: receiptName,
|
|
81
|
+
...(receipt.origin === undefined || receipt.origin === null ? {} : {
|
|
82
|
+
origin: receipt.origin,
|
|
83
|
+
originStatus: receipt.originStatus ?? 'declared',
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decode one payload with BOTH verified store sources for the program.
|
|
90
|
+
*
|
|
91
|
+
* Primary = newest on-chain receipt (real slot), secondary = newest adopted
|
|
92
|
+
* published copy. Without an adopted copy this is exactly the plain
|
|
93
|
+
* single-source decode, labelled `view.verdict: 'single_source'` so callers
|
|
94
|
+
* can tell the difference instead of assuming a union happened.
|
|
95
|
+
*
|
|
96
|
+
* Refusals from `unionDecode` are re-thrown with `view` attached: the caller
|
|
97
|
+
* gets BOTH sources' verdicts in the failure, not just an error string.
|
|
98
|
+
*/
|
|
99
|
+
export async function decodeUnionPayload(storeDir, program, payloadInput, options = {}) {
|
|
100
|
+
const { primary, secondary, additional } = await findStoredIdls(storeDir, program);
|
|
101
|
+
const bytes = parsePayload(payloadInput);
|
|
102
|
+
// 'verified' is evidence-gated: only payload shapes with a chain witness in
|
|
103
|
+
// src/short-forms.mjs may decode with an absent field; every other payload
|
|
104
|
+
// stays strict, including one that merely ends on a field boundary. The gate
|
|
105
|
+
// runs before either source is consulted so the policy cannot depend on which
|
|
106
|
+
// source happens to win.
|
|
107
|
+
const { policy, form } = resolveAbsentPolicy(program, bytes, options.absent);
|
|
108
|
+
try {
|
|
109
|
+
const result = unionDecode({ program, primary, secondary, additional, bytes, absent: policy });
|
|
110
|
+
if (form) {
|
|
111
|
+
// 'verified' is a promise about EVIDENCE, not a hint. If the decode did
|
|
112
|
+
// not reproduce the measured absence the witness supports, there is no
|
|
113
|
+
// evidence for this payload any more — so it is refused rather than
|
|
114
|
+
// decoded and quietly labelled unverified.
|
|
115
|
+
if (options.absent === 'verified') {
|
|
116
|
+
result.shortForm = assertShortFormEvidence(form, result, bytes);
|
|
117
|
+
} else {
|
|
118
|
+
result.shortForm = describeShortForm(form, result, bytes);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (options.trailing === 'error' && result.trailing.count > 0) {
|
|
122
|
+
throw new Error(`payload carries ${result.trailing.count} unexplained byte(s) after ${result.name}`);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof UnionRefusal) error.view = error.details?.view;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Decode one account's data with the locally stored, verified IDL.
|
|
133
|
+
*
|
|
134
|
+
* Same provenance guarantee as decodePayload: the receipt binds the program id
|
|
135
|
+
* to a fingerprinted object, so decoding is offline and reproducible.
|
|
136
|
+
* `name` skips identification (useful when the caller already knows); without
|
|
137
|
+
* it the account's discriminator picks the type.
|
|
138
|
+
*/
|
|
139
|
+
export async function decodeStoredAccount(storeDir, program, dataInput, { name, trailing } = {}) {
|
|
140
|
+
const { idl: storedIdl, receipt, receiptName, objectPath } = await findStoredIdl(storeDir, program);
|
|
141
|
+
const { idl, filled, conflicts } = withDerivedAccountDiscriminators(storedIdl);
|
|
142
|
+
const bytes = parseAccountBytes(dataInput);
|
|
143
|
+
const identified = name ? { name } : identifyAccount(idl, bytes);
|
|
144
|
+
if (!identified) {
|
|
145
|
+
throw Error(`no account type in the stored IDL matches the first 8 bytes of these ${bytes.length} byte(s)`);
|
|
146
|
+
}
|
|
147
|
+
const decoded = decodeAccountData(idl, bytes, identified.name, trailing ? { trailing } : {});
|
|
148
|
+
return {
|
|
149
|
+
program,
|
|
150
|
+
account: decoded.name,
|
|
151
|
+
args: decoded.args,
|
|
152
|
+
bytes: decoded.total,
|
|
153
|
+
consumed: decoded.consumed,
|
|
154
|
+
roundTripByteExact: decoded.roundTripByteExact,
|
|
155
|
+
discriminator: decoded.discriminator,
|
|
156
|
+
// True when the account bytes actually carry the discriminator the IDL
|
|
157
|
+
// names. False means the caller forced a type name onto bytes that
|
|
158
|
+
// belong to a different (or corrupted) account.
|
|
159
|
+
discriminatorMatches: decoded.hasDiscriminator,
|
|
160
|
+
trailing: decoded.trailing,
|
|
161
|
+
discriminator: decoded.discriminator,
|
|
162
|
+
discriminatorSource: filled > 0 ? (conflicts.length ? 'stored+derived' : 'derived') : 'stored',
|
|
163
|
+
discriminatorDerivation: { filledAccounts: filled, conflicts: conflicts.length },
|
|
164
|
+
idl: provenanceOf(receipt, receiptName),
|
|
165
|
+
object: objectPath,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Parse account bytes the way the CLI documents: base64 (as
|
|
170
|
+
* `getMultipleAccounts` returns them) or hex. Kept in ONE place so the
|
|
171
|
+
* single-source and union paths cannot disagree about what "account bytes"
|
|
172
|
+
* means — two parsers for one input shape is how a caller ends up decoding
|
|
173
|
+
* something other than the bytes they pasted. */
|
|
174
|
+
function parseAccountBytes(dataInput) {
|
|
175
|
+
if (dataInput instanceof Uint8Array) return Buffer.from(dataInput);
|
|
176
|
+
if (typeof dataInput !== 'string') throw Error('account data must be a string or byte array');
|
|
177
|
+
return dataInput.startsWith('data:') || /[^0-9a-fA-F]/.test(dataInput.trim())
|
|
178
|
+
? Buffer.from(dataInput.trim().replace(/^base64,/, ''), 'base64')
|
|
179
|
+
: decodeHex(dataInput);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Decode one account's data with BOTH verified store sources for the program.
|
|
184
|
+
*
|
|
185
|
+
* Primary = newest on-chain receipt (real slot), secondary = newest adopted
|
|
186
|
+
* published copy — the same pairing `decodeUnionPayload` reads, so the two
|
|
187
|
+
* union surfaces can never consult different copies of one store (measured
|
|
188
|
+
* hazard class: src/store-read.mjs, rules 1 and 2). Without a second source
|
|
189
|
+
* this is exactly the plain single-source decode, labelled
|
|
190
|
+
* `view.verdict: 'single_source'` so a caller can tell the difference instead
|
|
191
|
+
* of assuming arbitration happened.
|
|
192
|
+
*
|
|
193
|
+
* Refusals from `unionAccountDecode` are re-thrown with `view` attached: the
|
|
194
|
+
* caller gets BOTH sources' verdicts in the failure, not just an error string.
|
|
195
|
+
*/
|
|
196
|
+
export async function decodeUnionStoredAccount(storeDir, program, dataInput, { name, trailing } = {}) {
|
|
197
|
+
const { primary, secondary, additional } = await findStoredIdls(storeDir, program);
|
|
198
|
+
const bytes = parseAccountBytes(dataInput);
|
|
199
|
+
try {
|
|
200
|
+
return unionAccountDecode({ program, primary, secondary, additional, bytes, name, trailing });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof UnionRefusal) error.view = error.details?.view;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Decode one program payload (instruction or Anchor event self-CPI).
|
|
209
|
+
* Returns the decoded instruction/event plus provenance and a byte-exactness check:
|
|
210
|
+
* re-encoding the decoded value must reproduce the input bytes.
|
|
211
|
+
*
|
|
212
|
+
* `trailing` selects what happens to bytes the stored IDL does not explain:
|
|
213
|
+
* 'report' (default) decodes the described prefix and reports the rest under
|
|
214
|
+
* `trailing`; 'error' refuses the payload. See src/codec.mjs for why 'report'
|
|
215
|
+
* is the default and what real mainnet bytes motivated it.
|
|
216
|
+
*/
|
|
217
|
+
export async function decodePayload(storeDir, program, payloadInput, { allowTrailing, trailing, absent } = {}) {
|
|
218
|
+
const { idl: storedIdl, receipt, receiptName, objectPath } = await findStoredIdl(storeDir, program);
|
|
219
|
+
const bytes = parsePayload(payloadInput);
|
|
220
|
+
// 'verified' is evidence-gated (src/short-forms.mjs): only shapes with a
|
|
221
|
+
// chain witness may decode with an absent field; every other payload stays
|
|
222
|
+
// strict. The gate runs before the store is consulted, so the policy cannot
|
|
223
|
+
// depend on which source happens to win.
|
|
224
|
+
const requestedAbsent = absent;
|
|
225
|
+
const { policy, form } = resolveAbsentPolicy(program, bytes, absent);
|
|
226
|
+
absent = policy;
|
|
227
|
+
// Legacy on-chain IDLs (measured: Kamino Lend, 66 instructions) carry no
|
|
228
|
+
// discriminators at all — they name instructions but cannot decode a byte.
|
|
229
|
+
// Fill the gaps with Anchor's own rule; stored discriminators are never
|
|
230
|
+
// touched, and any conflict with a stored value is reported back to the
|
|
231
|
+
// caller instead of being silently overridden.
|
|
232
|
+
const { idl, filled, conflicts } = withDerivedDiscriminators(storedIdl);
|
|
233
|
+
const decoded = decodeData(idl, bytes, { allowTrailing, trailing, absent });
|
|
234
|
+
let reencoded;
|
|
235
|
+
if (decoded.kind === 'instruction') {
|
|
236
|
+
// Re-encode omitting exactly the fields the decode reported absent, so a
|
|
237
|
+
// short-form payload still has to reproduce byte-for-byte to be trusted.
|
|
238
|
+
reencoded = encodeInstructionData(idl, decoded.instruction, decoded.args, { absent: decoded.absent });
|
|
239
|
+
} else {
|
|
240
|
+
reencoded = encodeEventData(idl, decoded.event, decoded.args, { tagged: decoded.tagged });
|
|
241
|
+
}
|
|
242
|
+
const described = bytes.subarray(0, decoded.consumed);
|
|
243
|
+
if (form) {
|
|
244
|
+
// 'verified' is a promise about EVIDENCE, not a hint: if the decode did
|
|
245
|
+
// not reproduce the measured absence the witness supports, there is no
|
|
246
|
+
// evidence for this payload any more — refuse instead of decoding and
|
|
247
|
+
// quietly labelling it unverified.
|
|
248
|
+
if (requestedAbsent === 'verified') {
|
|
249
|
+
assertShortFormEvidence(form, decoded, bytes);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
program,
|
|
254
|
+
kind: decoded.kind,
|
|
255
|
+
instruction: decoded.kind === 'instruction' ? decoded.instruction : undefined,
|
|
256
|
+
event: decoded.kind === 'event' ? decoded.event : undefined,
|
|
257
|
+
name: decoded.kind === 'instruction' ? decoded.instruction : decoded.event,
|
|
258
|
+
discriminator: decoded.discriminator,
|
|
259
|
+
tagged: decoded.tagged,
|
|
260
|
+
args: toJsonSafe(decoded.args),
|
|
261
|
+
// Declared fields the payload did not carry (see src/codec.mjs `absent`).
|
|
262
|
+
absent: decoded.absent ?? [],
|
|
263
|
+
bytes: bytes.length,
|
|
264
|
+
consumed: decoded.consumed,
|
|
265
|
+
// True when the re-encoded bytes equal the whole input. With unexplained
|
|
266
|
+
// trailing bytes this is false by construction, so the described prefix is
|
|
267
|
+
// checked separately: `describedRoundTripByteExact` says the IDL-described
|
|
268
|
+
// part reproduces exactly, `trailing.count` says how much was unexplained.
|
|
269
|
+
roundTripByteExact: reencoded.equals(bytes),
|
|
270
|
+
describedRoundTripByteExact: reencoded.equals(described),
|
|
271
|
+
trailing: decoded.trailing,
|
|
272
|
+
// Provenance of the discriminator that matched: stored = the bytes the
|
|
273
|
+
// program wrote on-chain; derived = Anchor's sha256 rule filled a gap.
|
|
274
|
+
discriminatorSource: conflicts.length === 0 && (filled.instructions + filled.events) > 0
|
|
275
|
+
? 'derived'
|
|
276
|
+
: (filled.instructions + filled.events) > 0 ? 'stored+derived' : 'stored',
|
|
277
|
+
discriminatorDerivation: {
|
|
278
|
+
filledInstructions: filled.instructions,
|
|
279
|
+
filledEvents: filled.events,
|
|
280
|
+
conflicts: conflicts.length,
|
|
281
|
+
},
|
|
282
|
+
...(form ? { shortForm: describeShortForm(form, decoded, bytes) } : {}),
|
|
283
|
+
idl: provenanceOf(receipt, receiptName),
|
|
284
|
+
object: objectPath,
|
|
285
|
+
};
|
|
286
|
+
}
|
package/src/dialect.mjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// IDL dialect detection — the gate that turns a codec CRASH into a NAMED
|
|
2
|
+
// REFUSAL for interfaces sememe does not speak.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: not every program publishes an Anchor or shank IDL.
|
|
5
|
+
// Measured 2026-11-12 on DiabLokxisGR8P4Qqp2QL6PmBhhVsdzPstCE4WoLxc5z
|
|
6
|
+
// (first-party interface: cryptodiablo/diabloarb, sdk/diabloarb-executor.
|
|
7
|
+
// idl.json @ f5005ac4 — raw little-endian, "NOT Anchor/Borsh" by its own
|
|
8
|
+
// description):
|
|
9
|
+
// - `types` is a DICT keyed by name ({Hop:…, Base:…, Route:…}), not the
|
|
10
|
+
// Anchor array — and context() iterated `idl.types ?? []`, so every one
|
|
11
|
+
// of the program's 102 sampled payloads died with the unnamed
|
|
12
|
+
// `TypeError: object is not iterable (cannot read property
|
|
13
|
+
// Symbol(Symbol.iterator))` instead of a refusal. Measured pre-fix:
|
|
14
|
+
// crash-voice 102/102, named-voice 0.
|
|
15
|
+
// - instructions dispatch on a 1-byte `tag` with fields under `data`
|
|
16
|
+
// (no `args`, no `discriminator`, no `discriminant`). withDerived-
|
|
17
|
+
// Discriminators filled all 4 with Anchor sighashes that match no wire
|
|
18
|
+
// (measured: filled {instructions: 4, events: 0}).
|
|
19
|
+
// - the program declares itself under `program_id` (not `address` /
|
|
20
|
+
// `metadata.address`), so adopt() and readVerified() — which only check
|
|
21
|
+
// `idl.address` — adopted the diabloarb file happily under
|
|
22
|
+
// So11111111111111111111111111111111111111112, a DIFFERENT program
|
|
23
|
+
// (measured: adopt succeeded; this is the mislabelling face).
|
|
24
|
+
//
|
|
25
|
+
// The rules, applied at every store/codec boundary so the reason is identical
|
|
26
|
+
// everywhere:
|
|
27
|
+
// - the members the codec ITERATES must be arrays. A mapping dialect
|
|
28
|
+
// (instructions/types/events/accounts as a dict) is refused by name.
|
|
29
|
+
// - a name-only legacy IDL (no discriminator, no discriminant, no stored
|
|
30
|
+
// bytes) stays SUPPORTED: `sememe conform` uses those account lists for
|
|
31
|
+
// layout checking, and the decode path already refuses such a payload by
|
|
32
|
+
// name ("unknown instruction discriminator") at the exact 0-decoded
|
|
33
|
+
// boundary the Kamino measurement pins. Naming that shape "unsupported"
|
|
34
|
+
// would change attested behavior without a measured wire failure.
|
|
35
|
+
// - a tag-dispatch instruction (fields under `data`, no `args`) is refused:
|
|
36
|
+
// the dialect names no wire layout the codec can read, and Anchor sighash
|
|
37
|
+
// derivation must not be applied to it (a derived table matches no wire).
|
|
38
|
+
// - oracle fixtures (marinade/meteora/openbook anchor-ts-discriminators.json)
|
|
39
|
+
// are captured artifacts, never passed to the codec as IDLs — the guard
|
|
40
|
+
// cannot false-positive on them because nothing feeds them here.
|
|
41
|
+
//
|
|
42
|
+
// Nothing here changes decoding bytes for a supported interface: anchor and
|
|
43
|
+
// shank take the same paths as before, byte for byte.
|
|
44
|
+
|
|
45
|
+
import { isShankDiscriminant } from './shank.mjs';
|
|
46
|
+
import { rawLeDialectReason } from './rawle.mjs';
|
|
47
|
+
|
|
48
|
+
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
49
|
+
|
|
50
|
+
const isByteArray = (value) => Array.isArray(value) && value.length > 0
|
|
51
|
+
&& value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);
|
|
52
|
+
|
|
53
|
+
/** The one machine-readable name of this refusal, used by every boundary. */
|
|
54
|
+
export const UNSUPPORTED_DIALECT_CODE = 'unsupported-idl-dialect';
|
|
55
|
+
|
|
56
|
+
const MAPPING_HINT = ' (mapping dialects like diabloarb tag-dispatch are not decodable)';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* True when `instruction` names a tag-dispatch dialect entry: fields under
|
|
60
|
+
* `data` and/or an explicit `tag`, with no anchor `args` array. A plain
|
|
61
|
+
* name-only legacy entry ({ name }) is NOT tagged — it stays a supported
|
|
62
|
+
* (if selector-less) anchor shape whose account lists drive conform.
|
|
63
|
+
*/
|
|
64
|
+
export function isTagDispatchInstruction(instruction) {
|
|
65
|
+
return isPlainObject(instruction) && !Array.isArray(instruction.args)
|
|
66
|
+
&& (instruction.data !== undefined || instruction.tag !== undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The named refusal reason for an IDL the codec cannot read, or null when
|
|
71
|
+
* the shape is fine. `unsupportedIdlReason` is the ONE definition of the
|
|
72
|
+
* reason text so every entry point (codec context, discriminator fill,
|
|
73
|
+
* union, decode, adopt, store read) states the same refusal.
|
|
74
|
+
*/
|
|
75
|
+
export function unsupportedIdlReason(idl) {
|
|
76
|
+
if (!isPlainObject(idl)) return 'unsupported IDL dialect: not an IDL object';
|
|
77
|
+
// Mapping dialects put the members the codec iterates into a dict
|
|
78
|
+
// (measured: diabloarb `types` as {Hop, Base, Route}).
|
|
79
|
+
for (const key of ['instructions', 'types', 'events', 'accounts']) {
|
|
80
|
+
if (idl[key] !== undefined && !Array.isArray(idl[key])) {
|
|
81
|
+
return `unsupported IDL dialect: ${key} must be an array, got ${typeof idl[key]}${MAPPING_HINT}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(idl.instructions)) return 'unsupported IDL dialect: no instructions array';
|
|
85
|
+
for (const instruction of idl.instructions) {
|
|
86
|
+
if (!isPlainObject(instruction)) {
|
|
87
|
+
return 'unsupported IDL dialect: instruction entries must be objects';
|
|
88
|
+
}
|
|
89
|
+
// A tag-dispatch instruction carries its fields under `data` and has no
|
|
90
|
+
// `args`: the dialect names no field list the codec could read, and the
|
|
91
|
+
// Anchor sighash derivation must not be applied to it.
|
|
92
|
+
if (isTagDispatchInstruction(instruction)) {
|
|
93
|
+
return `unsupported IDL dialect: instruction ${JSON.stringify(instruction.name ?? null)}`
|
|
94
|
+
+ ` carries no anchor args (tag/data dispatch)${MAPPING_HINT}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const event of idl.events ?? []) {
|
|
98
|
+
if (!isPlainObject(event)) return 'unsupported IDL dialect: event entries must be objects';
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Which dispatch dialect an IDL claims — the pure-shape question the
|
|
105
|
+
* measurement pins. `name-only` is a supported dialect (a legacy Anchor IDL
|
|
106
|
+
* that publishes names and layouts but no selectors): the decode path
|
|
107
|
+
* refuses those payloads by name, and the conform path uses their account
|
|
108
|
+
* lists.
|
|
109
|
+
*/
|
|
110
|
+
export function idlDialect(idl) {
|
|
111
|
+
// A raw-LE file is SUPPORTED now (PULS Z wiring) but is not one of the
|
|
112
|
+
// three Anchor-family dialects this classifier enumerates; it reports
|
|
113
|
+
// its own name so callers can tell a wired dialect from a refused one.
|
|
114
|
+
if (unsupportedIdlReason(idl)) {
|
|
115
|
+
return rawLeDialectReason(idl) === null ? 'raw-le' : 'unsupported';
|
|
116
|
+
}
|
|
117
|
+
const instructions = idl.instructions;
|
|
118
|
+
if (instructions.length === 0) return 'anchor';
|
|
119
|
+
for (const instruction of instructions) {
|
|
120
|
+
if (isShankDiscriminant(instruction)) return 'shank';
|
|
121
|
+
if (isByteArray(instruction?.discriminator)) return 'anchor';
|
|
122
|
+
}
|
|
123
|
+
return 'name-only';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The program an IDL declares for itself, under any of the field names
|
|
128
|
+
* measured in the wild: `address` (Anchor on-chain), `metadata.address`
|
|
129
|
+
* (shank), `program_id` (diabloarb and other raw dialects).
|
|
130
|
+
*/
|
|
131
|
+
export function declaredProgram(idl) {
|
|
132
|
+
const candidates = [idl?.address, idl?.metadata?.address, idl?.program_id];
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Throw the caller's error type when the IDL is not structurally decodable.
|
|
141
|
+
* One line at each of the boundaries, so the guard cannot drift between them.
|
|
142
|
+
*/
|
|
143
|
+
export function assertIdlShape(idl, makeError) {
|
|
144
|
+
const reason = unsupportedIdlReason(idl);
|
|
145
|
+
if (!reason) return;
|
|
146
|
+
const error = makeError(reason);
|
|
147
|
+
if (error && typeof error === 'object') {
|
|
148
|
+
error.code = UNSUPPORTED_DIALECT_CODE;
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The refusal reason ONLY when no engine in the product can read the file.
|
|
155
|
+
* Since 12.11. PULS Z the raw little-endian engine (src/rawle.mjs) is wired
|
|
156
|
+
* into the codec: an IDL the Anchor codec refuses by SHAPE but the engine
|
|
157
|
+
* claims grammar for is decodable, so the store write gate, the store read
|
|
158
|
+
* gate and the discriminator derivation must not refuse it. Every other
|
|
159
|
+
* unsupported shape keeps the exact named refusal of `unsupportedIdlReason`.
|
|
160
|
+
* The codec's own dispatch (src/codec.mjs) asks the same two questions in
|
|
161
|
+
* the same order: is it outside the Anchor shape, and is it inside the
|
|
162
|
+
* engine grammar?
|
|
163
|
+
*/
|
|
164
|
+
export function unreadableDialectReason(idl) {
|
|
165
|
+
const reason = unsupportedIdlReason(idl);
|
|
166
|
+
if (!reason) return null;
|
|
167
|
+
return rawLeDialectReason(idl) === null ? null : reason;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Like assertIdlShape, but raw-LE-readable files pass — the engine speaks
|
|
171
|
+
* them now (PULS Z). Same named refusal and same error code otherwise. */
|
|
172
|
+
export function assertReadableDialect(idl, makeError) {
|
|
173
|
+
const reason = unreadableDialectReason(idl);
|
|
174
|
+
if (!reason) return;
|
|
175
|
+
const error = makeError(reason);
|
|
176
|
+
if (error && typeof error === 'object') {
|
|
177
|
+
error.code = UNSUPPORTED_DIALECT_CODE;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Derive Anchor discriminators for IDLs that do not carry them.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: the on-chain IDL of a program is not always the IDL the
|
|
4
|
+
// program was built from. Legacy Anchor IDLs (the `anchor:idl` account form)
|
|
5
|
+
// and several deployed programs publish instruction NAMES and ARGUMENT LAYOUTS
|
|
6
|
+
// but no `discriminator` arrays. Measured on Kamino Lend
|
|
7
|
+
// (KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD), pulled on-chain 2026-10-29:
|
|
8
|
+
// 66 of 66 instructions and 5 of 5 events carry NO discriminator.
|
|
9
|
+
//
|
|
10
|
+
// Such an IDL is not a decoder input: without a discriminator there is no way
|
|
11
|
+
// to tell which instruction a payload is. It is also what the coverage
|
|
12
|
+
// measurement called "usable" — a schema that names instructions but cannot
|
|
13
|
+
// decode a single byte.
|
|
14
|
+
//
|
|
15
|
+
// The rule is Anchor's own, not a guess:
|
|
16
|
+
// instruction: sha256("global:" + snake_case(name))[0..8]
|
|
17
|
+
// event: sha256("event:" + name)[0..8]
|
|
18
|
+
// (anchor-lang `discriminator` helper; the IDL generator uses the same one).
|
|
19
|
+
//
|
|
20
|
+
// The rule is verified, not assumed. `verifyDerivedDiscriminators` compares
|
|
21
|
+
// derived values against discriminators that ARE present in the IDL, so a
|
|
22
|
+
// program that stores them acts as the oracle. Measured on two independent
|
|
23
|
+
// real programs: Whirlpool 66/66 instructions, Jupiter AMM v6 19/19
|
|
24
|
+
// instructions and 6/6 events agree with the derivation.
|
|
25
|
+
//
|
|
26
|
+
// Nothing here overwrites a stored discriminator. A present value wins; the
|
|
27
|
+
// derivation only fills gaps. A conflict is an error, not a silent override.
|
|
28
|
+
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { isShankDiscriminant } from './shank.mjs';
|
|
31
|
+
import { unreadableDialectReason, idlDialect } from './dialect.mjs';
|
|
32
|
+
|
|
33
|
+
const sha256 = (text) => createHash('sha256').update(text).digest();
|
|
34
|
+
|
|
35
|
+
/** Anchor's snake_case of an instruction name (Rust fn name -> wire name). */
|
|
36
|
+
export function snakeCase(name) {
|
|
37
|
+
if (typeof name !== 'string' || !name) throw new Error('instruction name must be a non-empty string');
|
|
38
|
+
return name
|
|
39
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
40
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
41
|
+
.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Anchor's global instruction discriminator for a name. */
|
|
45
|
+
export function globalDiscriminator(name) {
|
|
46
|
+
return Array.from(sha256(`global:${snakeCase(name)}`).subarray(0, 8));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Anchor's event discriminator for an event name. */
|
|
50
|
+
export function eventDiscriminator(name) {
|
|
51
|
+
if (typeof name !== 'string' || !name) throw new Error('event name must be a non-empty string');
|
|
52
|
+
return Array.from(sha256(`event:${name}`).subarray(0, 8));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isDiscriminator = (value) => Array.isArray(value)
|
|
56
|
+
&& value.length === 8
|
|
57
|
+
&& value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Any stored discriminator bytes, whatever length the source published. A
|
|
61
|
+
* non-8-byte array is NOT a gap: it is a stored value under a different
|
|
62
|
+
* dispatch scheme (measured: HADRo… publishes 38 one-byte instruction
|
|
63
|
+
* discriminators — a real, chain-accepted wire table that Anchor's sighash
|
|
64
|
+
* derivation cannot describe). The codec matches and re-encodes on the stored
|
|
65
|
+
* prefix length, so preservation here is what makes those bytes decodable.
|
|
66
|
+
*/
|
|
67
|
+
const isStoredDiscriminator = (value) => Array.isArray(value)
|
|
68
|
+
&& value.length > 0
|
|
69
|
+
&& value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compare derived discriminators against the ones an IDL already stores.
|
|
73
|
+
*
|
|
74
|
+
* @returns {{ checked: number, agree: number, conflicts: object[] }}
|
|
75
|
+
*/
|
|
76
|
+
export function verifyDerivedDiscriminators(idl) {
|
|
77
|
+
const conflicts = [];
|
|
78
|
+
let checked = 0;
|
|
79
|
+
let agree = 0;
|
|
80
|
+
for (const instruction of idl?.instructions ?? []) {
|
|
81
|
+
if (!isDiscriminator(instruction?.discriminator)) continue;
|
|
82
|
+
checked++;
|
|
83
|
+
const derived = globalDiscriminator(instruction.name);
|
|
84
|
+
const same = derived.every((byte, index) => byte === instruction.discriminator[index]);
|
|
85
|
+
if (same) agree++;
|
|
86
|
+
else conflicts.push({
|
|
87
|
+
kind: 'instruction',
|
|
88
|
+
name: instruction.name,
|
|
89
|
+
stored: instruction.discriminator.slice(),
|
|
90
|
+
derived,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
for (const event of idl?.events ?? []) {
|
|
94
|
+
if (!isDiscriminator(event?.discriminator)) continue;
|
|
95
|
+
checked++;
|
|
96
|
+
const derived = eventDiscriminator(event.name);
|
|
97
|
+
const same = derived.every((byte, index) => byte === event.discriminator[index]);
|
|
98
|
+
if (same) agree++;
|
|
99
|
+
else conflicts.push({
|
|
100
|
+
kind: 'event',
|
|
101
|
+
name: event.name,
|
|
102
|
+
stored: event.discriminator.slice(),
|
|
103
|
+
derived,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return { checked, agree, conflicts };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Return a copy of the IDL with discriminators filled in where they are
|
|
111
|
+
* missing. Stored discriminators are never changed — at ANY length, not just
|
|
112
|
+
* Anchor's 8 (pre-fix, a stored 1-byte array fell through to the sighash fill
|
|
113
|
+
* and HADRo's whole wire table was silently replaced by bytes no payload
|
|
114
|
+
* carries; measured 0/195 decoded pre-fix, 123/195 post-fix). Only an 8-byte
|
|
115
|
+
* stored value makes a claim under Anchor's rule, so only those are checked
|
|
116
|
+
* for a conflict; a stored value that disagrees with the derivation is
|
|
117
|
+
* reported as a conflict.
|
|
118
|
+
*
|
|
119
|
+
* @returns {{ idl: object, filled: { instructions: number, events: number }, conflicts: object[] }}
|
|
120
|
+
*/
|
|
121
|
+
export function withDerivedDiscriminators(idl) {
|
|
122
|
+
// A dialect NO engine in the product can read gets NO derived table at
|
|
123
|
+
// all. Measured 2026-11-12 on the diabloarb interface: the derivation
|
|
124
|
+
// filled all four tag-dispatch instructions with Anchor sighashes that
|
|
125
|
+
// match no wire — a table that looks authoritative and decodes nothing.
|
|
126
|
+
// The refusal is named (see src/dialect.mjs) and identical to the
|
|
127
|
+
// codec's own. SINCE THE PULS-Z WIRING the raw little-endian engine is
|
|
128
|
+
// a product engine: a raw-LE interface carries its own tag dispatch (a
|
|
129
|
+
// u8 `tag`, not a sighash) and passes this gate to the codec, which
|
|
130
|
+
// serves it from src/rawle.mjs — no Anchor table is derived for it and
|
|
131
|
+
// none is needed.
|
|
132
|
+
const refusal = unreadableDialectReason(idl);
|
|
133
|
+
if (refusal) throw new Error(refusal);
|
|
134
|
+
const copy = structuredClone(idl);
|
|
135
|
+
// A raw-LE file (PULS Z wiring) dispatches on the u8 tag its own file
|
|
136
|
+
// declares — Anchor sighash derivation for it is the measured PULS X
|
|
137
|
+
// defect one gate change later: filled {instructions: 4, events: 0}, a
|
|
138
|
+
// table that looks authoritative and matches no wire byte. The honest
|
|
139
|
+
// table for this dialect is NO derived table, and the codec serves the
|
|
140
|
+
// file from src/rawle.mjs. The file still passes the gate (an engine
|
|
141
|
+
// reads it); only the Anchor filling is withheld.
|
|
142
|
+
if (idlDialect(idl) === 'raw-le') {
|
|
143
|
+
return { idl: copy, filled: { instructions: 0, events: 0 }, conflicts: [] };
|
|
144
|
+
}
|
|
145
|
+
let instructions = 0;
|
|
146
|
+
let events = 0;
|
|
147
|
+
const conflicts = [];
|
|
148
|
+
for (const instruction of copy.instructions ?? []) {
|
|
149
|
+
// A shank discriminant is a 1-byte variant selector, not a sighash. The
|
|
150
|
+
// two schemes are independent: deriving Anchor's sighash for a shank
|
|
151
|
+
// instruction yields a table that matches no real wire (measured: all 18
|
|
152
|
+
// lighthouse instructions filled with bogus sighashes before this rule
|
|
153
|
+
// existed). The stored discriminant is copied verbatim — it is a STORED
|
|
154
|
+
// value under a different field name, so it is not counted as an Anchor
|
|
155
|
+
// derivation and never checked against the sighash rule.
|
|
156
|
+
if (isShankDiscriminant(instruction)) {
|
|
157
|
+
instruction.discriminator = [instruction.discriminant.value];
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (isStoredDiscriminator(instruction.discriminator)) {
|
|
161
|
+
// STORED bytes under any scheme: copied verbatim, never filled. Only
|
|
162
|
+
// Anchor's own 8-byte form is checked against his sighash rule — a
|
|
163
|
+
// 1-byte stored value is a different dispatch scheme and makes no
|
|
164
|
+
// sighash claim, so there is nothing to conflict.
|
|
165
|
+
if (isDiscriminator(instruction.discriminator)) {
|
|
166
|
+
const derived = globalDiscriminator(instruction.name);
|
|
167
|
+
if (!derived.every((byte, index) => byte === instruction.discriminator[index])) {
|
|
168
|
+
conflicts.push({ kind: 'instruction', name: instruction.name, stored: instruction.discriminator.slice(), derived });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
instruction.discriminator = globalDiscriminator(instruction.name);
|
|
174
|
+
instructions++;
|
|
175
|
+
}
|
|
176
|
+
for (const event of copy.events ?? []) {
|
|
177
|
+
if (isStoredDiscriminator(event.discriminator)) {
|
|
178
|
+
// Same rule as instructions: stored bytes at any length are kept
|
|
179
|
+
// verbatim; only the 8-byte Anchor form is conflict-checked.
|
|
180
|
+
if (isDiscriminator(event.discriminator)) {
|
|
181
|
+
const derived = eventDiscriminator(event.name);
|
|
182
|
+
if (!derived.every((byte, index) => byte === event.discriminator[index])) {
|
|
183
|
+
conflicts.push({ kind: 'event', name: event.name, stored: event.discriminator.slice(), derived });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
event.discriminator = eventDiscriminator(event.name);
|
|
189
|
+
events++;
|
|
190
|
+
}
|
|
191
|
+
return { idl: copy, filled: { instructions, events }, conflicts };
|
|
192
|
+
}
|