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/drift.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Which differences between two decoded argument trees are LAYOUT differences?
|
|
2
|
+
//
|
|
3
|
+
// Why this file exists (measured 2026-11-09): two verified copies of the same
|
|
4
|
+
// program can spell the same field differently — the on-chain copy of
|
|
5
|
+
// meteora-damm-v1 says `lpMint`, its published copy says `lp_mint`. The union
|
|
6
|
+
// reported that as a LAYOUT DRIFT and listed the fields one by one, so a
|
|
7
|
+
// reader was told "the two copies disagree about the data" about a field that
|
|
8
|
+
// both copies decoded to the SAME value.
|
|
9
|
+
//
|
|
10
|
+
// The measurement (tools/measure-drift-naming.mjs, research/drift-naming-2026-11-09.json)
|
|
11
|
+
// over every program in the repo that holds two verified copies:
|
|
12
|
+
//
|
|
13
|
+
// pump.fun 88 instruction drift rows, 6 account drift rows: all real differences
|
|
14
|
+
// meteora-damm-v1 643 instruction drift rows on the pre-fix tree, ALL of them
|
|
15
|
+
// NOTHING BUT spelling differences (`inAmount` vs `in_amount`,
|
|
16
|
+
// `claimFee` vs `claim_fee`) that raw JSON equality turned
|
|
17
|
+
// into `layout_drift`
|
|
18
|
+
// meteora-damm-v1 11 account drift rows, every row hitting the 20-path cap (220 paths);
|
|
19
|
+
// 33 REAL differences were crowded out by spelling paths
|
|
20
|
+
//
|
|
21
|
+
// The rule this file applies is the one the declared-layout report already
|
|
22
|
+
// states in the README: NAMING STYLE IS NOT A LAYOUT DIFFERENCE. Fields are
|
|
23
|
+
// paired by normalised spelling (`_padding0` and `padding0` are the same
|
|
24
|
+
// field; `totalAmount` and `total_amount` are the same field), and only a
|
|
25
|
+
// difference in the VALUE or in the SHAPE of a paired field is a difference
|
|
26
|
+
// in the data.
|
|
27
|
+
//
|
|
28
|
+
// Two things this file does NOT do:
|
|
29
|
+
//
|
|
30
|
+
// * it does not change WHICH source won — arbitration is by bytes, and this
|
|
31
|
+
// file only describes what the disagreement was;
|
|
32
|
+
// * it does not hide the spelling difference — it is reported, in its own
|
|
33
|
+
// list, under `spellingOnly`. The reader learns that the two copies call
|
|
34
|
+
// the same field different things; they just do not get told the two
|
|
35
|
+
// copies disagree about the data when they do not.
|
|
36
|
+
//
|
|
37
|
+
// A field only one side declares is still a real difference and is still
|
|
38
|
+
// reported: pairing by name does not make a missing field present.
|
|
39
|
+
|
|
40
|
+
/** The bound on how many differences one report lists. */
|
|
41
|
+
export const MAX_DRIFT_PATHS = 20;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalise a field name so that spelling styles pair up:
|
|
45
|
+
* leading underscores are dropped and `snake_case` becomes `camelCase`.
|
|
46
|
+
* `totalAmount` / `total_amount` / `_total_amount` all normalise to
|
|
47
|
+
* `totalAmount`.
|
|
48
|
+
*/
|
|
49
|
+
export function normaliseFieldName(name) {
|
|
50
|
+
return String(name)
|
|
51
|
+
.replace(/^_+/, '')
|
|
52
|
+
.replace(/_+([a-z0-9])/g, (m, c) => c.toUpperCase());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Pair two sides' keys by normalised spelling, in declaration order. */
|
|
56
|
+
function pairKeys(a, b) {
|
|
57
|
+
const group = (obj) => {
|
|
58
|
+
const map = new Map();
|
|
59
|
+
for (const key of Object.keys(obj)) {
|
|
60
|
+
const nk = normaliseFieldName(key);
|
|
61
|
+
if (!map.has(nk)) map.set(nk, []);
|
|
62
|
+
map.get(nk).push(key);
|
|
63
|
+
}
|
|
64
|
+
return map;
|
|
65
|
+
};
|
|
66
|
+
const aMap = group(a);
|
|
67
|
+
const bMap = group(b);
|
|
68
|
+
const pairs = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const nk of [...aMap.keys(), ...bMap.keys()]) {
|
|
71
|
+
if (seen.has(nk)) continue;
|
|
72
|
+
seen.add(nk);
|
|
73
|
+
const aKeys = aMap.get(nk) ?? [];
|
|
74
|
+
const bKeys = bMap.get(nk) ?? [];
|
|
75
|
+
const n = Math.min(aKeys.length, bKeys.length);
|
|
76
|
+
for (let i = 0; i < n; i += 1) pairs.push({ a: aKeys[i], b: bKeys[i], paired: true });
|
|
77
|
+
for (let i = n; i < aKeys.length; i += 1) pairs.push({ a: aKeys[i], b: null, paired: false });
|
|
78
|
+
for (let i = n; i < bKeys.length; i += 1) pairs.push({ a: null, b: bKeys[i], paired: false });
|
|
79
|
+
}
|
|
80
|
+
return pairs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
84
|
+
const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compare two decoded argument trees.
|
|
88
|
+
*
|
|
89
|
+
* @returns {{
|
|
90
|
+
* paths: string[],
|
|
91
|
+
* truncated: boolean,
|
|
92
|
+
* differs: boolean,
|
|
93
|
+
* spellingOnly: Array<{ path: string, primary: string, secondary: string }>,
|
|
94
|
+
* details: Array<Record<string, unknown>>,
|
|
95
|
+
* }}
|
|
96
|
+
* `paths` are the REAL differences (value or shape), named in the primary's
|
|
97
|
+
* spelling where the field exists on both sides; `spellingOnly` are the
|
|
98
|
+
* fields both sides declare under different names. `differs` is the verdict
|
|
99
|
+
* input: true only when `paths` is non-empty, so a spelling difference can
|
|
100
|
+
* never make the union say the two copies disagree about the data.
|
|
101
|
+
*/
|
|
102
|
+
export function diffArgsByNormalisedName(primaryArgs, secondaryArgs, { max = MAX_DRIFT_PATHS } = {}) {
|
|
103
|
+
const paths = [];
|
|
104
|
+
const details = [];
|
|
105
|
+
const spellingOnly = [];
|
|
106
|
+
let truncated = false;
|
|
107
|
+
|
|
108
|
+
const push = (path, detail) => {
|
|
109
|
+
if (paths.length >= max) { truncated = true; return; }
|
|
110
|
+
paths.push(path);
|
|
111
|
+
details.push(detail);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const compareArrays = (a, b, path) => {
|
|
115
|
+
const n = Math.min(a.length, b.length);
|
|
116
|
+
let differing = 0;
|
|
117
|
+
for (let i = 0; i < n; i += 1) {
|
|
118
|
+
if (!sameValue(a[i], b[i])) differing += 1;
|
|
119
|
+
}
|
|
120
|
+
const extra = Math.abs(a.length - b.length);
|
|
121
|
+
if (differing === 0 && extra === 0) return;
|
|
122
|
+
push(path || '(root)', {
|
|
123
|
+
path: path || '(root)',
|
|
124
|
+
kind: 'array',
|
|
125
|
+
primaryLength: a.length,
|
|
126
|
+
secondaryLength: b.length,
|
|
127
|
+
differingElements: differing + extra,
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const walk = (a, b, path) => {
|
|
132
|
+
if (truncated) return;
|
|
133
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
134
|
+
if (Array.isArray(a) && Array.isArray(b)) { compareArrays(a, b, path); return; }
|
|
135
|
+
push(path || '(root)', { path: path || '(root)', kind: 'shape' });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!isPlainObject(a) || !isPlainObject(b)) {
|
|
139
|
+
if (!sameValue(a, b)) {
|
|
140
|
+
push(path || '(root)', { path: path || '(root)', kind: 'value', primary: a, secondary: b });
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const { a: aKey, b: bKey, paired } of pairKeys(a, b)) {
|
|
145
|
+
if (truncated) return;
|
|
146
|
+
const display = aKey ?? bKey;
|
|
147
|
+
const childPath = path ? `${path}.${display}` : display;
|
|
148
|
+
if (!paired) {
|
|
149
|
+
push(childPath, { path: childPath, kind: 'missing', side: aKey === null ? 'secondary' : 'primary' });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (aKey !== bKey) {
|
|
153
|
+
// Same field, different spelling. It is still reported as a naming
|
|
154
|
+
// difference; it only counts as a difference in the DATA when the
|
|
155
|
+
// values behind the two names are not the same.
|
|
156
|
+
spellingOnly.push({ path: childPath, primary: aKey, secondary: bKey });
|
|
157
|
+
if (sameValue(a[aKey], b[bKey])) continue;
|
|
158
|
+
}
|
|
159
|
+
walk(a[aKey], b[bKey], childPath);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
walk(primaryArgs, secondaryArgs, '');
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
paths,
|
|
167
|
+
truncated,
|
|
168
|
+
differs: paths.length > 0,
|
|
169
|
+
spellingOnly,
|
|
170
|
+
details,
|
|
171
|
+
};
|
|
172
|
+
}
|
package/src/gap.mjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// `sememe gap <program>` — why is there no interface here? Asked BEFORE pull.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS
|
|
4
|
+
//
|
|
5
|
+
// The full foreign sweep (research/step-ingestooor-full-sweep-2026-11-04.json)
|
|
6
|
+
// ended with 306 no-interface payloads across 70 programs, and the reflex is to
|
|
7
|
+
// call them all "acquisition gaps ... next derivation candidates". That label
|
|
8
|
+
// conflates two problems that need OPPOSITE fixes:
|
|
9
|
+
//
|
|
10
|
+
// - a program that PUBLISHES an on-chain IDL at one of the two addresses
|
|
11
|
+
// sememe can read is a NEVER-PULLED gap: `sememe pull` closes it, no
|
|
12
|
+
// derivation exists to do;
|
|
13
|
+
// - a program with neither address answering publishes nothing: pulling it
|
|
14
|
+
// forever cannot help (it needs `sememe adopt` of a first-party published
|
|
15
|
+
// copy, or a pinned upstream derivation, or a vendored wire table).
|
|
16
|
+
//
|
|
17
|
+
// Measured 2026-11-04 against live mainnet over all 70 gap programs:
|
|
18
|
+
// 34 publish an IDL (177 payloads) — including Jupiter DCA, which the card had
|
|
19
|
+
// listed as a derivation candidate — and 36 publish nothing (129 payloads),
|
|
20
|
+
// among them the largest single gap (L2TExM, 42 payloads, Lighthouse).
|
|
21
|
+
//
|
|
22
|
+
// WHY IT IS A TOOL AND NOT A PARAGRAPH
|
|
23
|
+
//
|
|
24
|
+
// 1. An RPC failure is NEVER recorded as "publishes nothing". The crates.io
|
|
25
|
+
// 403 incident (signerd, 2026-11-03) is the standing example: a client
|
|
26
|
+
// failure that reads like an absence. Unknown rows are counted separately
|
|
27
|
+
// and the verdict is withheld (`no-verdict`) so a partial probe cannot be
|
|
28
|
+
// quoted as a complete one.
|
|
29
|
+
// 2. A program sememe answers WITHOUT an IDL (System, ComputeBudget, SPL Token,
|
|
30
|
+
// Token-2022, Raydium AMM v4) is `vendored`, not a gap: telling a reader to
|
|
31
|
+
// pull it would send them after a receipt that can never exist.
|
|
32
|
+
// 3. An account that EXISTS but cannot be decoded is `present-undecodable`, its
|
|
33
|
+
// own verdict, never folded into "publishes nothing": an unusable interface
|
|
34
|
+
// is a different finding from an absent one.
|
|
35
|
+
// 4. Payload counts come from the committed sweep report, and its
|
|
36
|
+
// `noInterface` headline must equal the sum of the per-program counts —
|
|
37
|
+
// a hand-edited artifact is refused before it can be probed.
|
|
38
|
+
//
|
|
39
|
+
// This file lives in src/ so the shipped CLI can offer it: tools/ is not part
|
|
40
|
+
// of the published package (see "files" in package.json), and a consumer who
|
|
41
|
+
// hits `no stored IDL for program X` must be able to ask WHY from the same
|
|
42
|
+
// install they run `sememe decode` from. The acquisition dependency
|
|
43
|
+
// (@solana/web3.js, via ./acquire.mjs) is loaded lazily by the caller exactly
|
|
44
|
+
// like `pull`, so the decode-only install path stays dependency-free.
|
|
45
|
+
|
|
46
|
+
import { addresses, inspect, rpcAccounts } from './acquire.mjs';
|
|
47
|
+
import { LIGHTHOUSE_PROGRAM } from './lighthouse.mjs';
|
|
48
|
+
import { isNativeProgram } from './native.mjs';
|
|
49
|
+
import { isTokenProgram } from './token.mjs';
|
|
50
|
+
import { isRaydiumProgram } from './raydium.mjs';
|
|
51
|
+
import { isAtaProgram } from './ata.mjs';
|
|
52
|
+
|
|
53
|
+
const VENDORED = 'vendored';
|
|
54
|
+
|
|
55
|
+
/** Programs sememe answers with a pinned wire table, so there is no IDL to fetch. */
|
|
56
|
+
export function vendoredProgram(program) {
|
|
57
|
+
if (isNativeProgram(program)) return 'native';
|
|
58
|
+
if (isTokenProgram(program)) return 'token';
|
|
59
|
+
if (isRaydiumProgram(program)) return 'raydium';
|
|
60
|
+
if (isAtaProgram(program)) return 'ata';
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Classify ONE gap program from its two probe accounts (metadata, anchor).
|
|
66
|
+
*
|
|
67
|
+
* Every path returns a verdict; nothing throws on bad data, so a caller can
|
|
68
|
+
* aggregate instead of losing a whole run to one malformed account.
|
|
69
|
+
*/
|
|
70
|
+
export function classify(program, accounts, hints = {}) {
|
|
71
|
+
const vendored = vendoredProgram(program);
|
|
72
|
+
if (vendored) return { program, verdict: VENDORED, kind: vendored };
|
|
73
|
+
const c = hints.canonical ?? inspect(accounts?.[0], 'metadata', program);
|
|
74
|
+
const l = hints.legacy ?? inspect(accounts?.[1], 'anchor', program);
|
|
75
|
+
const usable = c.state === 'usable' ? 'metadata' : l.state === 'usable' ? 'anchor' : null;
|
|
76
|
+
if (usable) {
|
|
77
|
+
const chosen = usable === 'metadata' ? c : l;
|
|
78
|
+
return {
|
|
79
|
+
program, verdict: 'pullable', source: usable,
|
|
80
|
+
instructions: chosen.instructions,
|
|
81
|
+
decodableStoredOnly: chosen.decodableStoredOnly,
|
|
82
|
+
canonical: c.state, legacy: l.state,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (c.state === 'missing' && l.state === 'missing') {
|
|
86
|
+
return { program, verdict: 'no-interface-published', canonical: c.state, legacy: l.state };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
program, verdict: 'present-undecodable',
|
|
90
|
+
canonical: c.state, legacy: l.state,
|
|
91
|
+
error: c.state === 'undecodable' ? c.error : l.state === 'undecodable' ? l.error : undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const chunk = (items, size) => {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Probe a list of `{ program, payloads }` entries against the chain.
|
|
103
|
+
*
|
|
104
|
+
* 25 programs per request = 50 keys; the public endpoint rejects larger
|
|
105
|
+
* getMultipleAccounts batches (-32602), measured while building this.
|
|
106
|
+
*/
|
|
107
|
+
export async function probeGap(programs, options = {}) {
|
|
108
|
+
const {
|
|
109
|
+
url = 'https://api.mainnet-beta.solana.com',
|
|
110
|
+
fetcher = fetch,
|
|
111
|
+
batchSize = 25,
|
|
112
|
+
tries = 3,
|
|
113
|
+
} = options;
|
|
114
|
+
if (!Array.isArray(programs) || !programs.length) throw Error('programs required');
|
|
115
|
+
const rows = [];
|
|
116
|
+
let slot = null;
|
|
117
|
+
for (const group of chunk(programs, batchSize)) {
|
|
118
|
+
const locations = await Promise.all(group.map(async ({ program }) => addresses(program)));
|
|
119
|
+
const keys = locations.flatMap(({ metadata, anchor }) => [metadata, anchor]);
|
|
120
|
+
const response = await rpcAccounts(url, keys, fetcher, tries);
|
|
121
|
+
if (response.error) {
|
|
122
|
+
// The whole batch is UNKNOWN — and unknown must never become absence.
|
|
123
|
+
for (const { program, payloads } of group) {
|
|
124
|
+
rows.push({ program, payloads, verdict: 'unknown', error: response.error });
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
slot = response.slot ?? slot;
|
|
129
|
+
group.forEach(({ program, payloads }, index) => {
|
|
130
|
+
const accounts = [response.accounts[2 * index], response.accounts[2 * index + 1]];
|
|
131
|
+
rows.push({ program, payloads, slot, ...classify(program, accounts) });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return { rows, summary: summarize(rows), slot };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function summarize(rows) {
|
|
138
|
+
const byVerdict = {};
|
|
139
|
+
const payloadsByVerdict = {};
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
byVerdict[row.verdict] = (byVerdict[row.verdict] ?? 0) + 1;
|
|
142
|
+
payloadsByVerdict[row.verdict] = (payloadsByVerdict[row.verdict] ?? 0) + (row.payloads ?? 0);
|
|
143
|
+
}
|
|
144
|
+
const unknown = byVerdict.unknown ?? 0;
|
|
145
|
+
return {
|
|
146
|
+
programs: rows.length,
|
|
147
|
+
payloads: rows.reduce((sum, row) => sum + (row.payloads ?? 0), 0),
|
|
148
|
+
byVerdict, payloadsByVerdict,
|
|
149
|
+
unknown,
|
|
150
|
+
// Withhold the verdict when any probe failed: a partial probe is not a
|
|
151
|
+
// statement about the rows it failed on, and must not be quoted as one.
|
|
152
|
+
verdict: unknown > 0 ? 'no-verdict' : 'measured',
|
|
153
|
+
pullablePrograms: byVerdict.pullable ?? 0,
|
|
154
|
+
pullablePayloads: payloadsByVerdict.pullable ?? 0,
|
|
155
|
+
publishesNothingPrograms: byVerdict['no-interface-published'] ?? 0,
|
|
156
|
+
publishesNothingPayloads: payloadsByVerdict['no-interface-published'] ?? 0,
|
|
157
|
+
undecodablePrograms: byVerdict['present-undecodable'] ?? 0,
|
|
158
|
+
vendoredPrograms: byVerdict[VENDORED] ?? 0,
|
|
159
|
+
knownPrograms: rows.length - unknown,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Turn a committed sweep report's `noInterfaceByProgram` map into the entries
|
|
165
|
+
* probeGap takes. The report is the denominator: its headline must equal the
|
|
166
|
+
* sum of the per-program counts, or the artifact has been hand-edited.
|
|
167
|
+
*/
|
|
168
|
+
export function programsFromReport(report) {
|
|
169
|
+
const perProgram = report?.noInterfaceByProgram;
|
|
170
|
+
if (!perProgram || typeof perProgram !== 'object' || Array.isArray(perProgram)) {
|
|
171
|
+
throw Error('report has no noInterfaceByProgram map');
|
|
172
|
+
}
|
|
173
|
+
const programs = Object.entries(perProgram).map(([program, payloads]) => ({ program, payloads }));
|
|
174
|
+
if (!programs.length) throw Error('report has no gap programs');
|
|
175
|
+
const sum = programs.reduce((total, { payloads }) => total + payloads, 0);
|
|
176
|
+
if (sum !== report.noInterface) {
|
|
177
|
+
throw Error(`gap counts sum to ${sum} but report.noInterface says ${report.noInterface} — hand-edited artifact`);
|
|
178
|
+
}
|
|
179
|
+
return programs;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The `next` line for one verdict — the fix that actually applies. */
|
|
183
|
+
export function nextStep(row, program) {
|
|
184
|
+
switch (row.verdict) {
|
|
185
|
+
case 'pullable':
|
|
186
|
+
return `sememe pull ${program}`;
|
|
187
|
+
case 'no-interface-published':
|
|
188
|
+
// Lighthouse is the one measured case where sememe SHIPS the verified
|
|
189
|
+
// first-party copy (vendor/lighthouse, hash-pinned): the general advice
|
|
190
|
+
// would send the user off to find a file the package already carries.
|
|
191
|
+
if (program === LIGHTHOUSE_PROGRAM) {
|
|
192
|
+
return 'this program publishes no on-chain interface at either address — pulling it can never succeed. '
|
|
193
|
+
+ 'sememe ships its first-party published copy (Jac0xb/lighthouse, MIT, hash-pinned): '
|
|
194
|
+
+ 'use sememe adopt-lighthouse --store <dir>, then sememe decode --union.';
|
|
195
|
+
}
|
|
196
|
+
return 'this program publishes no on-chain interface at either address — pulling it can never succeed. '
|
|
197
|
+
+ 'Use sememe adopt <program> <file> with a first-party published copy, or a pinned upstream derivation.';
|
|
198
|
+
case 'present-undecodable':
|
|
199
|
+
return 'an interface account exists but its schema is unusable — pull it to inspect; decode will refuse by name';
|
|
200
|
+
case VENDORED:
|
|
201
|
+
return 'answered by the pinned wire table shipped with sememe; there is no IDL to store';
|
|
202
|
+
default:
|
|
203
|
+
return 'the RPC did not answer — absence is NOT established; retry before believing anything about the chain';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The Lighthouse acquisition: a pinned, hash-verified first-party published
|
|
2
|
+
// IDL for the program that was the LARGEST remaining acquisition gap after
|
|
3
|
+
// ATA (measured 2026-11-11: L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95,
|
|
4
|
+
// 1,214 payloads in 6 finalized blocks, no IDL account at either pullable
|
|
5
|
+
// address — research/ecosystem-coverage-2026-11-11.json, acquisitionQueue[0]).
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS FILE EXISTS
|
|
8
|
+
//
|
|
9
|
+
// `sememe adopt` stores any file the caller names, and its origin is DECLARED,
|
|
10
|
+
// never verified (src/adopt.mjs says so on every surface). For ATA the answer
|
|
11
|
+
// was a derived wire table pinned to the vendored upstream Rust. For Lighthouse
|
|
12
|
+
// the answer already exists as a FIRST-PARTY PUBLISHED COPY: the program's own
|
|
13
|
+
// repository ships programs/lighthouse/lighthouse.json (shank dialect, which
|
|
14
|
+
// src/shank.mjs decodes — measured 2026-11-04 on 42 payloads of the foreign
|
|
15
|
+
// sweep). The gap was never "no interface exists"; it was "no one can adopt it
|
|
16
|
+
// reproducibly". This module makes the adoption checkable:
|
|
17
|
+
//
|
|
18
|
+
// - the copy is VENDORED under vendor/lighthouse/<commit>/ with the commit
|
|
19
|
+
// it was read at and the sha256 of every file (license MIT, LICENSE
|
|
20
|
+
// vendored beside it);
|
|
21
|
+
// - loadVendoredLighthouse() re-hashes the vendored bytes against the pin
|
|
22
|
+
// and REFUSES by name on any mismatch, missing file, unparseable JSON, a
|
|
23
|
+
// declared address that disagrees, or a non-shank instruction table —
|
|
24
|
+
// the same two-check contract src/native.mjs / src/token.mjs /
|
|
25
|
+
// src/raydium.mjs / src/ata.mjs prove;
|
|
26
|
+
// - the shank discriminants themselves are checked for self-consistency
|
|
27
|
+
// (every instruction declares {type:'u8',value:N}; N must be the
|
|
28
|
+
// declaration index) so a doctored IDL cannot claim 0x00 for two
|
|
29
|
+
// instructions.
|
|
30
|
+
//
|
|
31
|
+
// What is deliberately NOT claimed: the vendored bytes are not proven to come
|
|
32
|
+
// from the commit by this module (GitHub blobs are content-addressed, git
|
|
33
|
+
// commits are not signed here); the receipt will say originStatus 'declared'
|
|
34
|
+
// and name repo + commit + file. Chain proof stays with the decode: every
|
|
35
|
+
// payload must still re-encode byte-exactly or the union refuses.
|
|
36
|
+
|
|
37
|
+
import { readFileSync } from 'node:fs';
|
|
38
|
+
import { createHash } from 'node:crypto';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
|
|
42
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
export const REPO_ROOT = path.resolve(HERE, '..');
|
|
44
|
+
|
|
45
|
+
export const LIGHTHOUSE_PROGRAM = 'L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95';
|
|
46
|
+
|
|
47
|
+
export const LIGHTHOUSE_PIN = {
|
|
48
|
+
repo: 'Jac0xb/lighthouse',
|
|
49
|
+
commit: '4c579479c98635e419b1b167f08be02a71604a71',
|
|
50
|
+
license: 'MIT',
|
|
51
|
+
// sha256 of the bytes AS PUBLISHED at the commit, read via the GitHub
|
|
52
|
+
// contents API at that ref (not from a mutable working tree).
|
|
53
|
+
files: [
|
|
54
|
+
{
|
|
55
|
+
file: 'programs/lighthouse/lighthouse.json',
|
|
56
|
+
role: 'first-party published IDL (shank dialect: per-instruction u8 discriminant)',
|
|
57
|
+
sha256: '9c54749abd27c09cf8d61e50016b625b5ae4aeb5cb1b8baf295199a1d7ecfd48',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
file: 'LICENSE',
|
|
61
|
+
role: 'the license the vendored copy ships under',
|
|
62
|
+
sha256: '64d73100cafc1f5cb148cbbd5b3c7fc74bafc46f156646b0861c698570d02327',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
origin: `github.com/${'Jac0xb/lighthouse'}@4c579479c98635e419b1b167f08be02a71604a71:programs/lighthouse/lighthouse.json`,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function vendoredLighthousePath(file, root = REPO_ROOT) {
|
|
69
|
+
return path.join(root, 'vendor', 'lighthouse', LIGHTHOUSE_PIN.commit, path.basename(file));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function sha256File(filePath) {
|
|
73
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class LighthousePinError extends Error {
|
|
77
|
+
constructor(reason, detail) {
|
|
78
|
+
super(detail ? `${reason}: ${detail}` : reason);
|
|
79
|
+
this.name = 'LighthousePinError';
|
|
80
|
+
this.reason = reason;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the vendored IDL and verify it against the pin. Every refusal is
|
|
86
|
+
* NAMED — a caller can tally failure faces without parsing prose.
|
|
87
|
+
* Returns { idl, path, sha256, bytes } on success.
|
|
88
|
+
*/
|
|
89
|
+
export function loadVendoredLighthouse(root = REPO_ROOT, pin = LIGHTHOUSE_PIN) {
|
|
90
|
+
const pinned = pin.files.find((f) => f.file.endsWith('lighthouse.json'));
|
|
91
|
+
const vendoredPath = path.join(root, 'vendor', 'lighthouse', pin.commit, path.basename(pinned.file));
|
|
92
|
+
let raw;
|
|
93
|
+
try {
|
|
94
|
+
raw = readFileSync(vendoredPath);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new LighthousePinError(
|
|
97
|
+
'lighthouse-vendor-missing',
|
|
98
|
+
`${vendoredPath} is not present (${error.code ?? error.message})`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const now = createHash('sha256').update(raw).digest('hex');
|
|
102
|
+
if (now !== pinned.sha256) {
|
|
103
|
+
throw new LighthousePinError(
|
|
104
|
+
'lighthouse-vendor-hash-mismatch',
|
|
105
|
+
`pinned ${pinned.sha256}, hashed now ${now} — refusing to adopt bytes that are not the reviewed copy`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
let idl;
|
|
109
|
+
try {
|
|
110
|
+
idl = JSON.parse(raw.toString('utf8'));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new LighthousePinError('lighthouse-vendor-unparseable', String(error.message ?? error));
|
|
113
|
+
}
|
|
114
|
+
if (!idl || typeof idl !== 'object' || Array.isArray(idl)) {
|
|
115
|
+
throw new LighthousePinError('lighthouse-vendor-unparseable', 'not an IDL object');
|
|
116
|
+
}
|
|
117
|
+
// Address binding: this file claims Lighthouse by its own metadata, and the
|
|
118
|
+
// program id this module pins is the one the measurement saw. A file that
|
|
119
|
+
// declares a different program refuses, it is never re-labelled.
|
|
120
|
+
const declared = idl.metadata?.address ?? idl.address ?? null;
|
|
121
|
+
if (declared !== LIGHTHOUSE_PROGRAM) {
|
|
122
|
+
throw new LighthousePinError(
|
|
123
|
+
'lighthouse-address-disagrees',
|
|
124
|
+
`vendored file declares ${declared}, pin binds ${LIGHTHOUSE_PROGRAM}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(idl.instructions) || idl.instructions.length === 0) {
|
|
128
|
+
throw new LighthousePinError('lighthouse-no-instructions', 'not an interface copy');
|
|
129
|
+
}
|
|
130
|
+
// Shank table integrity: every instruction must declare a u8 discriminant
|
|
131
|
+
// equal to its declaration index (measured truth at the pin: 0..17 in
|
|
132
|
+
// order). This is the IDL checking ITSELF, not a transcription.
|
|
133
|
+
idl.instructions.forEach((instruction, index) => {
|
|
134
|
+
const disc = instruction?.discriminant;
|
|
135
|
+
if (!disc || disc.type !== 'u8' || disc.value !== index) {
|
|
136
|
+
throw new LighthousePinError(
|
|
137
|
+
'lighthouse-discriminant-table',
|
|
138
|
+
`instruction ${index} (${instruction?.name}) discriminant ` +
|
|
139
|
+
JSON.stringify(disc ?? null) + ` is not the shank u8 value ${index}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return { idl, path: vendoredPath, sha256: now, bytes: raw.length };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The origin label adopt records for a load that passed the pin checks. */
|
|
147
|
+
export function lighthouseOriginLabel() {
|
|
148
|
+
return `github.com/${LIGHTHOUSE_PIN.repo}@${LIGHTHOUSE_PIN.commit}:programs/lighthouse/lighthouse.json`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adopt the verified first-party copy into a store, through the PRODUCT's one
|
|
153
|
+
* write path (src/adopt.mjs — the same path `sememe adopt` uses).
|
|
154
|
+
*
|
|
155
|
+
* Order matters and is the point of this function: the pin is checked FIRST,
|
|
156
|
+
* so a tampered or missing vendored file means NOTHING was written. The
|
|
157
|
+
* receipt's origin stays `declared` (adopt's own rule): the pin binds the
|
|
158
|
+
* BYTES to a commit, not the bytes to the deployed program.
|
|
159
|
+
*/
|
|
160
|
+
export async function adoptLighthouse(storeDir, { adopt } = {}) {
|
|
161
|
+
if (typeof adopt !== 'function') throw Error('adoptLighthouse needs the product write path (src/adopt.mjs)');
|
|
162
|
+
const loaded = loadVendoredLighthouse();
|
|
163
|
+
const receipt = await adopt(LIGHTHOUSE_PROGRAM, loaded.path, storeDir, {
|
|
164
|
+
source: 'published',
|
|
165
|
+
origin: lighthouseOriginLabel(),
|
|
166
|
+
});
|
|
167
|
+
return { receipt, loaded };
|
|
168
|
+
}
|