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
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// Union arbitration for ACCOUNT bytes: two verified interface sources, one
|
|
2
|
+
// account, and the disagreement made explicit instead of picked by preference.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists (2026-11-08): instructions and events have had union decode
|
|
5
|
+
// since v0.4.0 — `sememe decode --union` reads the on-chain pull and the
|
|
6
|
+
// adopted first-party published copy, reports which one answered, and lets
|
|
7
|
+
// byte evidence decide. Accounts never got that half. `sememe account` read
|
|
8
|
+
// exactly ONE receipt (findStoredIdl, the newest), so a store holding two
|
|
9
|
+
// verified copies of a program had two opinions about an account's layout and
|
|
10
|
+
// only one of them was ever consulted — the README named this as the open
|
|
11
|
+
// item ("the same arbitration for account decoding (accounts still read a
|
|
12
|
+
// single receipt)").
|
|
13
|
+
//
|
|
14
|
+
// The measured case that made it concrete (2026-11-08, 12 real mainnet
|
|
15
|
+
// pump.fun accounts, research/account-union-2026-11-08.json): the program's
|
|
16
|
+
// on-chain IDL copy describes `BondingCurve` with 13 fields, the published
|
|
17
|
+
// copy with 19. A live 166-byte BondingCurve account decodes under the
|
|
18
|
+
// on-chain copy as 125 consumed + 41 unexplained bytes (partial answer), and
|
|
19
|
+
// under the published copy as a full byte-exact round trip. The plain path
|
|
20
|
+
// reported the partial one; the copy that explains every byte sat in the same
|
|
21
|
+
// store unread.
|
|
22
|
+
//
|
|
23
|
+
// The rules are the instruction union's rules, translated to accounts:
|
|
24
|
+
//
|
|
25
|
+
// - Both sources refuse -> refusal naming BOTH reasons.
|
|
26
|
+
// - One refuses, the other decodes -> admitted ONLY when the survivor
|
|
27
|
+
// reproduces its own bytes (full round trip, or the described prefix with
|
|
28
|
+
// the rest reported as trailing). A source that names an account it cannot
|
|
29
|
+
// re-encode is refused exactly like a first source would be.
|
|
30
|
+
// - Both decode under DIFFERENT account names for the same 8 bytes
|
|
31
|
+
// -> hard refusal (`name_conflict`): two schemas for one discriminator is
|
|
32
|
+
// an interface conflict, and guessing which one to believe would launder it.
|
|
33
|
+
// - Both decode the same name to the same args -> `agree`.
|
|
34
|
+
// - Same name, different args -> `layout_drift`; the winner is chosen by
|
|
35
|
+
// byte evidence, strongest first: a source whose re-encode reproduces the
|
|
36
|
+
// WHOLE account body beats one that only reproduces a described prefix,
|
|
37
|
+
// and between two prefix-only answers the one with FEWER unexplained
|
|
38
|
+
// bytes explains strictly more of the account. When the evidence does not
|
|
39
|
+
// decide, the PRIMARY (the on-chain pull, whenever one exists) stands and
|
|
40
|
+
// the drift is reported — never resolved by preference.
|
|
41
|
+
//
|
|
42
|
+
// Nothing here mutates a source. Account discriminators are filled per source
|
|
43
|
+
// with Anchor's own rule before comparison (src/account.mjs); a stored value
|
|
44
|
+
// always wins and a conflict is recorded, never overwritten.
|
|
45
|
+
|
|
46
|
+
import { decodeAccountData, withDerivedAccountDiscriminators, identifyAccount } from './account.mjs';
|
|
47
|
+
import { toJsonSafe } from './codec.mjs';
|
|
48
|
+
import { UnionRefusal } from './union.mjs';
|
|
49
|
+
import { diffArgsByNormalisedName } from './drift.mjs';
|
|
50
|
+
|
|
51
|
+
const reasonOf = (error) => String(error?.message ?? error).slice(0, 200);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Field paths where two decoded account trees disagree, plus the fields the
|
|
55
|
+
* two copies name differently.
|
|
56
|
+
*
|
|
57
|
+
* The comparison is by NORMALISED spelling (src/drift.mjs): a field both
|
|
58
|
+
* copies declare — `lpMint` in one, `lp_mint` in the other — is the same
|
|
59
|
+
* field, and a difference in its NAME is not a difference in the account.
|
|
60
|
+
* What lands in `paths` is a difference in VALUE or SHAPE, or a field only one
|
|
61
|
+
* copy declares; what lands in `spellingOnly` is the naming difference itself.
|
|
62
|
+
*
|
|
63
|
+
* Measured 2026-11-09 (research/drift-naming-2026-11-09.json): before this
|
|
64
|
+
* rule, meteora-damm-v1's 11 drift rows filled the 20-path cap (11/11
|
|
65
|
+
* truncated, 220 paths), and 33 REAL differences were crowded out because the
|
|
66
|
+
* cap was filled with spelling differences.
|
|
67
|
+
*/
|
|
68
|
+
export function diffAccountPaths(primaryArgs, secondaryArgs) {
|
|
69
|
+
const diff = diffArgsByNormalisedName(primaryArgs, secondaryArgs);
|
|
70
|
+
return {
|
|
71
|
+
paths: diff.paths,
|
|
72
|
+
truncated: diff.truncated,
|
|
73
|
+
differs: diff.differs,
|
|
74
|
+
spellingOnly: diff.spellingOnly,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One source's account probe: identify the type in THIS source's table, then
|
|
79
|
+
* decode and re-encode under THIS source's layout. Every fact stays
|
|
80
|
+
* per-source — a trailing count or a round-trip flag from one copy must never
|
|
81
|
+
* be attached to the other copy's answer. */
|
|
82
|
+
function probeAccount(source, bytes, forcedName) {
|
|
83
|
+
try {
|
|
84
|
+
const identified = forcedName ? { name: forcedName } : identifyAccount(source.idl, bytes);
|
|
85
|
+
if (!identified) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: `no account type in this source matches the first 8 bytes `
|
|
89
|
+
+ `(${bytes.subarray(0, 8).toString('hex')})`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const decoded = decodeAccountData(source.idl, bytes, identified.name, { trailing: 'report' });
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
decoded,
|
|
96
|
+
fullExact: decoded.roundTripByteExact,
|
|
97
|
+
describedExact: decoded.describedRoundTripByteExact,
|
|
98
|
+
// Provenance of the discriminator that matched, kept PER SOURCE: a
|
|
99
|
+
// stored value is bytes the program wrote on chain, a derived one is
|
|
100
|
+
// Anchor's rule filling a gap. Reporting one source's derivation under
|
|
101
|
+
// the other source's label would be exactly the drift this module exists
|
|
102
|
+
// to make visible.
|
|
103
|
+
discriminatorSource: source.discriminatorDerivation.filledAccounts > 0
|
|
104
|
+
? (source.discriminatorDerivation.conflicts > 0 ? 'stored+derived' : 'derived')
|
|
105
|
+
: 'stored',
|
|
106
|
+
discriminatorDerivation: source.discriminatorDerivation,
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return { ok: false, reason: reasonOf(error) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Fill discriminator gaps for this source only, keeping the source's identity. */
|
|
114
|
+
function prepareAccountSource(source) {
|
|
115
|
+
const { idl, filled, conflicts } = withDerivedAccountDiscriminators(source.idl);
|
|
116
|
+
// Which accounts carried a STORED discriminator before filling — the line
|
|
117
|
+
// between "this copy derives its discriminator from the name" (so a renamed
|
|
118
|
+
// copy can never match) and "this copy pins a value that disagrees with the
|
|
119
|
+
// bytes" (a drifted copy). The rename diagnosis below must only claim the
|
|
120
|
+
// first mechanism when it is the first mechanism.
|
|
121
|
+
const storedAccountNames = new Set(
|
|
122
|
+
(source.idl?.accounts ?? [])
|
|
123
|
+
.filter((a) => Array.isArray(a?.discriminator) && a.discriminator.length === 8)
|
|
124
|
+
.map((a) => a.name),
|
|
125
|
+
);
|
|
126
|
+
return {
|
|
127
|
+
...source,
|
|
128
|
+
idl,
|
|
129
|
+
storedAccountNames,
|
|
130
|
+
discriminatorDerivation: { filledAccounts: filled, conflicts: conflicts.length },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The refusing copy may well KNOW this account — under a different spelling.
|
|
136
|
+
* Anchor derives an account discriminator from the EXACT name, so a copy that
|
|
137
|
+
* spells it `vault` instead of `Vault` derives a different 8 bytes and can
|
|
138
|
+
* never identify the deployed account, no matter how good its layout is.
|
|
139
|
+
* Reporting that as "no account type matches these bytes" is true of the
|
|
140
|
+
* discriminator but hides the mechanism from the user, who then cannot tell a
|
|
141
|
+
* missing account from a RENAMED one (measured on 12 real meteora-vault
|
|
142
|
+
* accounts, research/account-union-cross-2026-11-08.json).
|
|
143
|
+
*
|
|
144
|
+
* Called only when the OTHER source identified these bytes: a name is what
|
|
145
|
+
* makes the comparison possible. Two sources that both fail tell us nothing
|
|
146
|
+
* about each other (see the R3 control).
|
|
147
|
+
*/
|
|
148
|
+
function diagnoseRename(failedProbe, failedSource, bytes, otherName) {
|
|
149
|
+
if (!otherName) return null;
|
|
150
|
+
const accounts = failedSource.idl?.accounts ?? [];
|
|
151
|
+
const variant = accounts.find((a) => typeof a?.name === 'string'
|
|
152
|
+
&& a.name !== otherName
|
|
153
|
+
&& a.name.toLowerCase() === otherName.toLowerCase());
|
|
154
|
+
if (!variant) return null;
|
|
155
|
+
const head = bytes.subarray(0, 8).toString('hex');
|
|
156
|
+
const declared = Buffer.from(variant.discriminator ?? []).toString('hex');
|
|
157
|
+
if (failedSource.storedAccountNames?.has(variant.name)) {
|
|
158
|
+
// A stored value that disagrees is a different phenomenon (drift), not a
|
|
159
|
+
// rename: say that instead of blaming the derivation.
|
|
160
|
+
failedProbe.reason += `; this copy DOES declare the account, as "${variant.name}", with stored `
|
|
161
|
+
+ `discriminator ${declared} which does not match these bytes (${head}) — the other copy `
|
|
162
|
+
+ `identifies them as "${otherName}"`;
|
|
163
|
+
return variant.name;
|
|
164
|
+
}
|
|
165
|
+
failedProbe.reason += `; this copy DOES declare the account, as "${variant.name}" — but Anchor `
|
|
166
|
+
+ `derives an account discriminator from the exact name, so "${variant.name}" derives `
|
|
167
|
+
+ `${declared} and these bytes carry ${head} (the other copy identifies them as "${otherName}")`;
|
|
168
|
+
return variant.name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Format one field's declared type as the reader sees it in the IDL, so two
|
|
173
|
+
* copies' declarations can be compared line by line (`publicKey[30]`, `u64`,
|
|
174
|
+
* `VaultBumps`, `struct{4}`).
|
|
175
|
+
*/
|
|
176
|
+
function formatFieldType(type, depth = 0) {
|
|
177
|
+
if (typeof type === 'string') return type;
|
|
178
|
+
if (depth > 6) return '…';
|
|
179
|
+
if (Array.isArray(type)) return type.map((t) => formatFieldType(t, depth + 1)).join(', ');
|
|
180
|
+
if (type && typeof type === 'object') {
|
|
181
|
+
if (Array.isArray(type.array)) return `${formatFieldType(type.array[0], depth + 1)}[${type.array[1]}]`;
|
|
182
|
+
if (type.vec !== undefined) return `${formatFieldType(type.vec, depth + 1)}[]`;
|
|
183
|
+
if (type.option !== undefined) return `${formatFieldType(type.option, depth + 1)}?`;
|
|
184
|
+
if (type.defined !== undefined) {
|
|
185
|
+
return typeof type.defined === 'string' ? type.defined : (type.defined?.name ?? JSON.stringify(type.defined));
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(type.fields)) return `struct{${type.fields.length}}`;
|
|
188
|
+
if (type.kind) return `${type.kind}`;
|
|
189
|
+
}
|
|
190
|
+
return JSON.stringify(type);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The declared field list of one account type in one copy (inline or types). */
|
|
194
|
+
function declaredFields(idl, accountName) {
|
|
195
|
+
const account = (idl?.accounts ?? []).find((a) => a?.name === accountName);
|
|
196
|
+
const inline = account?.type && typeof account.type === 'object' ? account.type : null;
|
|
197
|
+
const typedef = (idl?.types ?? []).find((t) => t?.name === accountName);
|
|
198
|
+
const type = inline ?? (typedef?.type ?? null);
|
|
199
|
+
if (!type || !Array.isArray(type.fields)) return null;
|
|
200
|
+
return type.fields;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Compare two copies' DECLARED layout for the same account — nothing is
|
|
205
|
+
* decoded, no byte decides anything here.
|
|
206
|
+
*
|
|
207
|
+
* This is the fact the rename diagnosis could see but not state: a copy that
|
|
208
|
+
* can never identify the deployed bytes may still declare a different layout
|
|
209
|
+
* for them. Reporting it keeps the verdict `primary_only` (the report is not
|
|
210
|
+
* evidence) while telling the reader that the answer they got is contested
|
|
211
|
+
* field by field. Measured on 12 meteora-vault accounts: 30 keys vs 29 keys
|
|
212
|
+
* plus `version`, `totalLpSupply`, `padding0` — 960 bytes either way.
|
|
213
|
+
*
|
|
214
|
+
* Fields are paired by NORMALISED spelling so that a pure rename
|
|
215
|
+
* (`totalAmount` vs `total_amount`) is not reported as a layout difference;
|
|
216
|
+
* a field only one copy declares, or one both declare with different types,
|
|
217
|
+
* is.
|
|
218
|
+
*/
|
|
219
|
+
function diffDeclaredLayout(primaryIdl, primaryName, secondaryIdl, secondaryName) {
|
|
220
|
+
const primaryFields = declaredFields(primaryIdl, primaryName);
|
|
221
|
+
const secondaryFields = declaredFields(secondaryIdl, secondaryName);
|
|
222
|
+
if (!primaryFields || !secondaryFields) return null;
|
|
223
|
+
|
|
224
|
+
const normalise = (key) => String(key).replace(/^_+/, '')
|
|
225
|
+
.replace(/_+([a-z0-9])/g, (m, c) => c.toUpperCase());
|
|
226
|
+
const present = (field) => `${formatFieldType(field.type)}`;
|
|
227
|
+
|
|
228
|
+
const secondaryByNorm = new Map(secondaryFields.map((f) => [normalise(f.name), f]));
|
|
229
|
+
const matchedNorm = new Set();
|
|
230
|
+
const diffs = [];
|
|
231
|
+
|
|
232
|
+
for (const field of primaryFields) {
|
|
233
|
+
const key = normalise(field.name);
|
|
234
|
+
const other = secondaryByNorm.get(key);
|
|
235
|
+
if (!other) {
|
|
236
|
+
diffs.push({ path: field.name, primary: present(field), secondary: null });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
matchedNorm.add(key);
|
|
240
|
+
if (present(field) !== present(other)) {
|
|
241
|
+
diffs.push({ path: field.name, primary: present(field), secondary: present(other) });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const field of secondaryFields) {
|
|
245
|
+
if (matchedNorm.has(normalise(field.name))) continue; // paired above
|
|
246
|
+
diffs.push({ path: field.name, primary: null, secondary: present(field) });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
primary: { account: primaryName, fields: primaryFields.length },
|
|
251
|
+
secondary: { account: secondaryName, fields: secondaryFields.length },
|
|
252
|
+
differs: diffs.length > 0,
|
|
253
|
+
diffs,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function accountView(source, probeResult) {
|
|
258
|
+
if (probeResult === null) return { label: source?.label ?? null, present: false };
|
|
259
|
+
return {
|
|
260
|
+
label: source.label,
|
|
261
|
+
present: true,
|
|
262
|
+
fingerprint: source.idlMeta?.fingerprint ?? null,
|
|
263
|
+
receipt: source.idlMeta?.receipt ?? null,
|
|
264
|
+
decoded: probeResult.ok,
|
|
265
|
+
refusedReason: probeResult.ok ? undefined : probeResult.reason,
|
|
266
|
+
name: probeResult.ok ? probeResult.decoded.name : undefined,
|
|
267
|
+
// Which byte evidence THIS source produced: every byte, the described
|
|
268
|
+
// prefix only, or neither. The arbitration below reads these three.
|
|
269
|
+
evidence: probeResult.ok
|
|
270
|
+
? (probeResult.fullExact ? 'full_roundtrip'
|
|
271
|
+
: (probeResult.describedExact ? 'described_roundtrip' : 'none'))
|
|
272
|
+
: undefined,
|
|
273
|
+
roundTripByteExact: probeResult.ok ? probeResult.fullExact : undefined,
|
|
274
|
+
describedRoundTripByteExact: probeResult.ok ? probeResult.describedExact : undefined,
|
|
275
|
+
consumed: probeResult.ok ? probeResult.decoded.consumed : undefined,
|
|
276
|
+
trailingBytes: probeResult.ok ? probeResult.decoded.trailing.count : undefined,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function accountEnvelope(program, fields, winnerSource, view) {
|
|
281
|
+
return {
|
|
282
|
+
program,
|
|
283
|
+
...fields,
|
|
284
|
+
idl: winnerSource.idlMeta,
|
|
285
|
+
object: winnerSource.objectPath,
|
|
286
|
+
view,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function accountFields(probe, bytes) {
|
|
291
|
+
const decoded = probe.decoded;
|
|
292
|
+
return {
|
|
293
|
+
kind: 'account',
|
|
294
|
+
account: decoded.name,
|
|
295
|
+
name: decoded.name,
|
|
296
|
+
args: toJsonSafe(decoded.args),
|
|
297
|
+
discriminator: decoded.discriminator,
|
|
298
|
+
discriminatorMatches: decoded.hasDiscriminator,
|
|
299
|
+
bytes: decoded.total,
|
|
300
|
+
consumed: decoded.consumed,
|
|
301
|
+
trailing: decoded.trailing,
|
|
302
|
+
roundTripByteExact: decoded.roundTripByteExact,
|
|
303
|
+
describedRoundTripByteExact: decoded.describedRoundTripByteExact,
|
|
304
|
+
describedBytes: decoded.consumed,
|
|
305
|
+
discriminatorSource: probe.discriminatorSource,
|
|
306
|
+
discriminatorDerivation: probe.discriminatorDerivation,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Decode one account's bytes against a primary and an optional secondary
|
|
312
|
+
* verified interface source.
|
|
313
|
+
*
|
|
314
|
+
* Sources are `{ label, idl, idlMeta, objectPath }` and must already be
|
|
315
|
+
* store-verified (receipt re-hash etc.) — this module arbitrates, it does not
|
|
316
|
+
* re-verify. `forcedName` skips identification for callers that already know
|
|
317
|
+
* the account type; when given, both sources decode THAT name, so a name
|
|
318
|
+
* conflict is impossible by construction and the verdict can never be a
|
|
319
|
+
* coin flip over identification.
|
|
320
|
+
*
|
|
321
|
+
* @throws {UnionRefusal} for 'name_conflict', a fallback without byte
|
|
322
|
+
* evidence, and total refusal — carrying `details.view`, so the caller
|
|
323
|
+
* gets BOTH sources' verdicts in the failure.
|
|
324
|
+
*/
|
|
325
|
+
export function unionAccountDecode({ program, primary, secondary, bytes, name: forcedName, trailing, additional = [] }) {
|
|
326
|
+
const soleSource = !primary?.idl && secondary?.idl ? secondary : null;
|
|
327
|
+
if (!primary?.idl && !soleSource) {
|
|
328
|
+
throw new UnionRefusal('invalid_input', 'unionAccountDecode needs a primary source');
|
|
329
|
+
}
|
|
330
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
331
|
+
throw new UnionRefusal('invalid_input', 'unionAccountDecode needs the account bytes as a Buffer');
|
|
332
|
+
}
|
|
333
|
+
bytes = Buffer.isBuffer(bytes)
|
|
334
|
+
? bytes
|
|
335
|
+
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
336
|
+
if (trailing !== undefined && trailing !== 'report' && trailing !== 'error') {
|
|
337
|
+
throw new UnionRefusal('invalid_input', `unknown trailing policy ${JSON.stringify(trailing)}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const prim = prepareAccountSource(soleSource ?? primary);
|
|
341
|
+
const sec = secondary?.idl && secondary !== soleSource ? prepareAccountSource(secondary) : null;
|
|
342
|
+
const primAdopted = prim.idlMeta?.slot === null || prim.idlMeta?.slot === undefined;
|
|
343
|
+
const secAdopted = !sec || sec.idlMeta?.slot === null || sec.idlMeta?.slot === undefined;
|
|
344
|
+
const adoptedOnly = primAdopted && secAdopted;
|
|
345
|
+
|
|
346
|
+
const primProbe = probeAccount(prim, bytes, forcedName);
|
|
347
|
+
const secProbe = sec ? probeAccount(sec, bytes, forcedName) : null;
|
|
348
|
+
|
|
349
|
+
// A refusal that is really a RENAME must say so (see diagnoseRename): only
|
|
350
|
+
// when the other side identified the bytes do we have a name to compare with.
|
|
351
|
+
// The rename also tells us WHICH account the other copy declares — and that
|
|
352
|
+
// copy may declare a DIFFERENT LAYOUT for it (see diffDeclaredLayout). The
|
|
353
|
+
// report is NOT evidence and never enters the arbitration: it only tells the
|
|
354
|
+
// reader that the answer they got is contested, field by field.
|
|
355
|
+
let renamedTo = null;
|
|
356
|
+
if (primProbe.ok && secProbe && !secProbe.ok) {
|
|
357
|
+
renamedTo = diagnoseRename(secProbe, sec, bytes, primProbe.decoded.name);
|
|
358
|
+
} else if (secProbe?.ok && !primProbe.ok) {
|
|
359
|
+
renamedTo = diagnoseRename(primProbe, prim, bytes, secProbe.decoded.name);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const view = {
|
|
363
|
+
primary: accountView(prim, primProbe),
|
|
364
|
+
secondary: accountView(sec, secProbe),
|
|
365
|
+
sourcesAgree: null,
|
|
366
|
+
adoptedOnly,
|
|
367
|
+
additionalSources: additional.map((a) => ({
|
|
368
|
+
label: a.label,
|
|
369
|
+
fingerprint: a.idlMeta?.fingerprint,
|
|
370
|
+
receipt: a.idlMeta?.receipt,
|
|
371
|
+
})),
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// The layout the OTHER copy declares for the account it could not identify
|
|
375
|
+
// — a report, never evidence: `primary_only`/`fallback_secondary` still stand
|
|
376
|
+
// exactly as the byte arbitration decides them below.
|
|
377
|
+
if (renamedTo) {
|
|
378
|
+
const primaryName = primProbe.ok ? primProbe.decoded.name : renamedTo;
|
|
379
|
+
const secondaryName = secProbe?.ok ? secProbe.decoded.name : renamedTo;
|
|
380
|
+
const declared = diffDeclaredLayout(prim.idl, primaryName, sec.idl, secondaryName);
|
|
381
|
+
if (declared) view.declaredLayout = declared;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const finish = (winner, probe, verdict, basis, evidence) => {
|
|
385
|
+
const fields = accountFields(probe, bytes);
|
|
386
|
+
if (trailing === 'error' && fields.trailing.count > 0) {
|
|
387
|
+
throw new UnionRefusal('trailing',
|
|
388
|
+
`account carries ${fields.trailing.count} byte(s) after the structure `
|
|
389
|
+
+ `${winner.label} describes`,
|
|
390
|
+
{ view });
|
|
391
|
+
}
|
|
392
|
+
view.verdict = verdict;
|
|
393
|
+
view.arbitration = { winner: winner.label, basis, evidence };
|
|
394
|
+
return accountEnvelope(program, fields, winner, view);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Single source: behave like a plain decode, but say which verdict it is.
|
|
398
|
+
if (!sec) {
|
|
399
|
+
if (!primProbe.ok) {
|
|
400
|
+
throw new UnionRefusal('refused',
|
|
401
|
+
`decode refused by the only stored source (${prim.label}): ${primProbe.reason}`,
|
|
402
|
+
{ view });
|
|
403
|
+
}
|
|
404
|
+
view.adoptedOnly = adoptedOnly;
|
|
405
|
+
return finish(prim, primProbe, 'single_source', 'only_source',
|
|
406
|
+
primProbe.fullExact ? 'full_roundtrip' : 'described_roundtrip');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
view.discriminatorDerivation = {
|
|
410
|
+
primary: prim.discriminatorDerivation,
|
|
411
|
+
secondary: sec.discriminatorDerivation,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Both refuse: name both reasons, never one of them.
|
|
415
|
+
if (!primProbe.ok && !secProbe.ok) {
|
|
416
|
+
throw new UnionRefusal('refused',
|
|
417
|
+
`both stored sources refuse this account — ${prim.label}: ${primProbe.reason}; `
|
|
418
|
+
+ `${sec.label}: ${secProbe.reason}`,
|
|
419
|
+
{ view });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Primary refuses, secondary decodes: byte evidence admits the fallback.
|
|
423
|
+
if (!primProbe.ok && secProbe.ok) {
|
|
424
|
+
if (!secProbe.fullExact && !secProbe.describedExact) {
|
|
425
|
+
throw new UnionRefusal('union_fallback_refused',
|
|
426
|
+
`${prim.label} refuses (${primProbe.reason}); ${sec.label} names `
|
|
427
|
+
+ `"${secProbe.decoded.name}" but cannot re-encode these bytes, so the fallback is refused`,
|
|
428
|
+
{ view });
|
|
429
|
+
}
|
|
430
|
+
view.sourcesAgree = false;
|
|
431
|
+
return finish(sec, secProbe, 'fallback_secondary', 'primary_refused',
|
|
432
|
+
secProbe.fullExact ? 'full_roundtrip' : 'described_roundtrip');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Secondary refuses, primary decodes: the plain answer, with the refusal kept.
|
|
436
|
+
if (primProbe.ok && !secProbe.ok) {
|
|
437
|
+
view.sourcesAgree = false;
|
|
438
|
+
return finish(prim, primProbe, 'primary_only', 'secondary_refused',
|
|
439
|
+
primProbe.fullExact ? 'full_roundtrip' : 'described_roundtrip');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Both decode. Same 8 bytes matched by two sources -> the NAMES must agree.
|
|
443
|
+
if (primProbe.decoded.name !== secProbe.decoded.name) {
|
|
444
|
+
throw new UnionRefusal('name_conflict',
|
|
445
|
+
`interface conflict on the same account bytes: ${prim.label} says `
|
|
446
|
+
+ `"${primProbe.decoded.name}", ${sec.label} says "${secProbe.decoded.name}" — `
|
|
447
|
+
+ `refusing instead of picking a winner`,
|
|
448
|
+
{ view: { ...view, primary: { ...view.primary, name: primProbe.decoded.name }, secondary: { ...view.secondary, name: secProbe.decoded.name } } });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const primFields = accountFields(primProbe, bytes);
|
|
452
|
+
const secFields = accountFields(secProbe, bytes);
|
|
453
|
+
// Same rule as the instruction union: a field one copy spells `lpMint` and
|
|
454
|
+
// the other `lp_mint` is the same field. Raw JSON equality made a naming
|
|
455
|
+
// difference into the verdict `layout_drift` (measured 2026-11-09: 11/11
|
|
456
|
+
// meteora-damm-v1 account rows filled the 20-path cap, and 33 REAL
|
|
457
|
+
// differences were crowded out by spelling paths).
|
|
458
|
+
const diff = diffAccountPaths(primFields.args, secFields.args);
|
|
459
|
+
const argsAgree = !diff.differs;
|
|
460
|
+
|
|
461
|
+
// Arbitration by bytes, strongest evidence first (see the header).
|
|
462
|
+
let winner = prim;
|
|
463
|
+
let basis = 'primary_by_default';
|
|
464
|
+
let evidence = primProbe.fullExact ? 'full_roundtrip'
|
|
465
|
+
: (primProbe.describedExact ? 'described_roundtrip' : 'none');
|
|
466
|
+
const primTrailing = primProbe.decoded.trailing.count;
|
|
467
|
+
const secTrailing = secProbe.decoded.trailing.count;
|
|
468
|
+
if (secProbe.fullExact && !primProbe.fullExact) {
|
|
469
|
+
winner = sec;
|
|
470
|
+
basis = 'byte_evidence_full_roundtrip';
|
|
471
|
+
evidence = 'full_roundtrip';
|
|
472
|
+
} else if (!primProbe.fullExact && !secProbe.fullExact
|
|
473
|
+
&& primProbe.describedExact && secProbe.describedExact
|
|
474
|
+
&& secTrailing < primTrailing) {
|
|
475
|
+
winner = sec;
|
|
476
|
+
basis = 'byte_evidence_fewer_unexplained_bytes';
|
|
477
|
+
evidence = `trailing ${secTrailing} < ${primTrailing}`;
|
|
478
|
+
} else if (primProbe.fullExact && secProbe.fullExact && !argsAgree) {
|
|
479
|
+
// Both reproduce every byte yet describe different values: the evidence
|
|
480
|
+
// genuinely does not decide (two layouts can serialise to the same bytes).
|
|
481
|
+
basis = 'primary_by_default_evidence_tied';
|
|
482
|
+
evidence = 'both_full_roundtrip';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
view.sourcesAgree = argsAgree;
|
|
486
|
+
if (!argsAgree) {
|
|
487
|
+
view.verdict = 'layout_drift';
|
|
488
|
+
view.drift = {
|
|
489
|
+
...diff,
|
|
490
|
+
primaryExplainsFully: primProbe.fullExact,
|
|
491
|
+
secondaryExplainsFully: secProbe.fullExact,
|
|
492
|
+
primaryTrailingBytes: primTrailing,
|
|
493
|
+
secondaryTrailingBytes: secTrailing,
|
|
494
|
+
note: 'both sources decoded the same account bytes to different values; '
|
|
495
|
+
+ 'the winner was chosen by byte evidence where it decides, otherwise the primary stands '
|
|
496
|
+
+ 'and the drift is reported. `paths` names the fields whose VALUES differ or that only '
|
|
497
|
+
+ 'one copy declares; `spellingOnly` names the fields both copies declare under '
|
|
498
|
+
+ 'different spellings — a naming style, not a layout difference',
|
|
499
|
+
};
|
|
500
|
+
} else {
|
|
501
|
+
view.verdict = 'agree';
|
|
502
|
+
if (diff.spellingOnly.length > 0) {
|
|
503
|
+
view.naming = {
|
|
504
|
+
spellingOnly: diff.spellingOnly,
|
|
505
|
+
note: 'the two copies spell the same fields differently and decode them to the same values; the naming difference is reported, it is not a disagreement about the data',
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
view.arbitration = { winner: winner.label, basis, evidence };
|
|
510
|
+
if (trailing === 'error') {
|
|
511
|
+
const winnerProbe = winner === prim ? primProbe : secProbe;
|
|
512
|
+
const fields = accountFields(winnerProbe, bytes);
|
|
513
|
+
if (fields.trailing.count > 0) {
|
|
514
|
+
throw new UnionRefusal('trailing',
|
|
515
|
+
`account carries ${fields.trailing.count} byte(s) after the structure ${winner.label} describes`,
|
|
516
|
+
{ view });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const fields = winner === prim ? primFields : secFields;
|
|
520
|
+
return accountEnvelope(program, fields, winner, view);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export { UnionRefusal } from './union.mjs';
|