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/account.mjs
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Decode a program's account state from the stored, verified on-chain IDL.
|
|
2
|
+
//
|
|
3
|
+
// Why this file exists: instructions are only half of a program's interface.
|
|
4
|
+
// An account carries state, and the meaning of that state lives in the same
|
|
5
|
+
// IDL — but the IDL describes it differently:
|
|
6
|
+
// - a newer IDL lists accounts as `{ name, discriminator, type }`
|
|
7
|
+
// - a legacy IDL lists accounts as `{ name, type }` with no discriminator
|
|
8
|
+
// Anchor's own rule for the account discriminator is
|
|
9
|
+
// sha256("account:" + PascalCase(name))[0..8]
|
|
10
|
+
// (anchor-ts `BorshAccountsCoder.accountDiscriminator`). The rule is filled in
|
|
11
|
+
// only where the IDL stores no discriminator; a stored value always wins and a
|
|
12
|
+
// conflict is reported, never silently overridden.
|
|
13
|
+
//
|
|
14
|
+
// An account's bytes are NOT a discriminator + a fixed struct in the general
|
|
15
|
+
// case. Two shapes occur on mainnet and both are handled here:
|
|
16
|
+
// 1. Anchor accounts: 8-byte discriminator ++ borsh(struct fields)
|
|
17
|
+
// 2. Accounts whose data starts with the struct itself (no discriminator):
|
|
18
|
+
// the first bytes are the first fields, so the struct may simply not
|
|
19
|
+
// fit in the account (a `vec` at the end is truncated) and the decode
|
|
20
|
+
// stops at the first field that needs bytes the account does not have.
|
|
21
|
+
//
|
|
22
|
+
// The decoder is strict about what it can prove: it reports which bytes it
|
|
23
|
+
// consumed, whether the re-encoded bytes reproduce the account exactly, and
|
|
24
|
+
// any bytes the IDL does not explain. A decode that consumed fewer bytes than
|
|
25
|
+
// the account holds is NOT a failure — it means the account is larger than the
|
|
26
|
+
// struct the IDL describes, which is normal for accounts with a trailing
|
|
27
|
+
// `vec` that grew.
|
|
28
|
+
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import {
|
|
31
|
+
decodeTypePartial, encodeType, toJsonSafe, CodecError,
|
|
32
|
+
} from './codec.mjs';
|
|
33
|
+
|
|
34
|
+
const ACCOUNT_DISCRIMINATOR_SIZE = 8;
|
|
35
|
+
|
|
36
|
+
const sha256 = (text) => createHash('sha256').update(text).digest();
|
|
37
|
+
|
|
38
|
+
/** Anchor's account discriminator for a name: sha256("account:" + PascalCase). */
|
|
39
|
+
export function accountDiscriminator(name) {
|
|
40
|
+
if (typeof name !== 'string' || !name) throw new CodecError('account name must be a non-empty string', null);
|
|
41
|
+
const preimage = `account:${name}`;
|
|
42
|
+
return Array.from(sha256(preimage).subarray(0, ACCOUNT_DISCRIMINATOR_SIZE));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fill in missing account discriminators with Anchor's rule.
|
|
47
|
+
* Stored discriminators are never changed; a conflict is returned.
|
|
48
|
+
*
|
|
49
|
+
* @returns {{ idl: object, filled: number, conflicts: object[] }}
|
|
50
|
+
*/
|
|
51
|
+
export function withDerivedAccountDiscriminators(idl) {
|
|
52
|
+
const copy = structuredClone(idl);
|
|
53
|
+
let filled = 0;
|
|
54
|
+
const conflicts = [];
|
|
55
|
+
for (const account of copy.accounts ?? []) {
|
|
56
|
+
const stored = account?.discriminator;
|
|
57
|
+
const isStored = Array.isArray(stored) && stored.length === ACCOUNT_DISCRIMINATOR_SIZE
|
|
58
|
+
&& stored.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);
|
|
59
|
+
const derived = accountDiscriminator(account.name);
|
|
60
|
+
if (isStored) {
|
|
61
|
+
const same = derived.every((byte, index) => byte === stored[index]);
|
|
62
|
+
if (!same) {
|
|
63
|
+
conflicts.push({ name: account.name, stored: stored.slice(), derived });
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
account.discriminator = derived;
|
|
68
|
+
filled++;
|
|
69
|
+
}
|
|
70
|
+
return { idl: copy, filled, conflicts };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The IDL's account type definition (a struct shape) for one account name.
|
|
75
|
+
* Two IDL shapes exist and both occur on mainnet:
|
|
76
|
+
* - newer IDL: `accounts: [{ name, discriminator }]` with the struct under
|
|
77
|
+
* `types: [{ name, type: { kind: 'struct', fields } }]`
|
|
78
|
+
* - legacy IDL: `accounts: [{ name, type: { fields } }]` inline
|
|
79
|
+
*/
|
|
80
|
+
function accountType(idl, name) {
|
|
81
|
+
const account = (idl.accounts ?? []).find((candidate) => candidate?.name === name);
|
|
82
|
+
if (!account) {
|
|
83
|
+
const known = (idl.accounts ?? []).map((candidate) => candidate?.name).filter(Boolean);
|
|
84
|
+
throw new CodecError(
|
|
85
|
+
`unknown account ${JSON.stringify(name)}; this IDL has ${known.length} account(s)`, null);
|
|
86
|
+
}
|
|
87
|
+
if (account.type && typeof account.type === 'object') return { account, type: account.type };
|
|
88
|
+
const typedef = (idl.types ?? []).find((candidate) => candidate?.name === name);
|
|
89
|
+
if (!typedef || !typedef.type) {
|
|
90
|
+
throw new CodecError(`account ${name} has no type in this IDL (neither inline nor in types)`, null);
|
|
91
|
+
}
|
|
92
|
+
return { account, type: typedef.type };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function typeContext(idl) {
|
|
96
|
+
const types = new Map();
|
|
97
|
+
for (const typedef of idl?.types ?? []) {
|
|
98
|
+
if (typedef && typeof typedef.name === 'string') types.set(typedef.name, typedef);
|
|
99
|
+
}
|
|
100
|
+
return { types };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decode one account's raw data with the stored IDL.
|
|
105
|
+
*
|
|
106
|
+
* `data` is the account's byte content (the value behind `getMultipleAccounts`,
|
|
107
|
+
* already base64-decoded).
|
|
108
|
+
*
|
|
109
|
+
* @returns {{ name, discriminator, args, consumed, total, trailing,
|
|
110
|
+
* roundTripByteExact, describedRoundTripByteExact, hasDiscriminator }}
|
|
111
|
+
*/
|
|
112
|
+
export function decodeAccountData(idl, data, name, options = {}) {
|
|
113
|
+
if (!(data instanceof Uint8Array)) throw new CodecError('account data must be a Uint8Array/Buffer', null);
|
|
114
|
+
const { trailing = 'report' } = options;
|
|
115
|
+
if (trailing !== 'report' && trailing !== 'error') {
|
|
116
|
+
throw new CodecError(`unknown trailing policy ${trailing}`, null);
|
|
117
|
+
}
|
|
118
|
+
const { account, type } = accountType(idl, name);
|
|
119
|
+
const ctx = typeContext(idl);
|
|
120
|
+
const bytes = Buffer.from(data);
|
|
121
|
+
|
|
122
|
+
// Does this account carry the Anchor account discriminator? Compare the
|
|
123
|
+
// first 8 bytes with the discriminator the IDL names (stored or derived).
|
|
124
|
+
const expected = Array.isArray(account.discriminator) && account.discriminator.length === ACCOUNT_DISCRIMINATOR_SIZE
|
|
125
|
+
? Buffer.from(account.discriminator)
|
|
126
|
+
: null;
|
|
127
|
+
const hasDiscriminator = expected ? bytes.subarray(0, ACCOUNT_DISCRIMINATOR_SIZE).equals(expected) : false;
|
|
128
|
+
|
|
129
|
+
// With a discriminator the struct starts after it; without one the struct
|
|
130
|
+
// starts at byte 0 (a non-Anchor or legacy account).
|
|
131
|
+
//
|
|
132
|
+
// The offset follows the IDL, not the bytes: if the IDL names a
|
|
133
|
+
// discriminator for this account type, the struct starts after 8 bytes even
|
|
134
|
+
// when the bytes in hand carry a different discriminator (a corrupted or
|
|
135
|
+
// foreign account). `hasDiscriminator` records whether the bytes actually
|
|
136
|
+
// carry the expected one.
|
|
137
|
+
const offset = expected ? ACCOUNT_DISCRIMINATOR_SIZE : 0;
|
|
138
|
+
const body = bytes.subarray(offset);
|
|
139
|
+
|
|
140
|
+
let decoded;
|
|
141
|
+
let consumed = 0;
|
|
142
|
+
try {
|
|
143
|
+
const result = decodeTypePartial(type, body, ctx, `${name}`);
|
|
144
|
+
decoded = result.value;
|
|
145
|
+
consumed = result.consumed;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// A truncated account is a normal mainnet condition: the struct's last
|
|
148
|
+
// field is often a `vec` that grew past the account size, or the account
|
|
149
|
+
// is smaller than the struct. Report it instead of pretending it decoded.
|
|
150
|
+
if (!(error instanceof CodecError)) throw error;
|
|
151
|
+
throw new CodecError(
|
|
152
|
+
`account ${name}: ${error.message} (account holds ${body.length} byte(s) after the discriminator)`, name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const trailingCount = body.length - consumed;
|
|
156
|
+
if (trailingCount > 0 && trailing === 'error') {
|
|
157
|
+
throw new CodecError(
|
|
158
|
+
`account ${name}: ${trailingCount} byte(s) after the described struct are not described by the IDL`, name);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Re-encoding the decoded struct must reproduce the bytes it came from.
|
|
162
|
+
let reencoded = null;
|
|
163
|
+
try {
|
|
164
|
+
reencoded = encodeType(type, decoded, ctx, `${name}`);
|
|
165
|
+
} catch {
|
|
166
|
+
reencoded = null;
|
|
167
|
+
}
|
|
168
|
+
const described = body.subarray(0, consumed);
|
|
169
|
+
return {
|
|
170
|
+
name,
|
|
171
|
+
discriminator: expected ? expected.toString('hex') : null,
|
|
172
|
+
hasDiscriminator,
|
|
173
|
+
args: toJsonSafe(decoded),
|
|
174
|
+
consumed: consumed + offset,
|
|
175
|
+
total: bytes.length,
|
|
176
|
+
trailing: { count: trailingCount, hex: trailingCount > 0 ? body.subarray(consumed).toString('hex') : '' },
|
|
177
|
+
roundTripByteExact: reencoded ? reencoded.equals(body) : false,
|
|
178
|
+
describedRoundTripByteExact: reencoded ? reencoded.equals(described) : false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Identify which account type a blob of account data belongs to, using the
|
|
184
|
+
* discriminator table the IDL provides (stored or derived).
|
|
185
|
+
*
|
|
186
|
+
* @returns {{ name, discriminator } | null}
|
|
187
|
+
*/
|
|
188
|
+
export function identifyAccount(idl, data) {
|
|
189
|
+
if (!(data instanceof Uint8Array) || data.length < ACCOUNT_DISCRIMINATOR_SIZE) return null;
|
|
190
|
+
const head = Buffer.from(data.subarray(0, ACCOUNT_DISCRIMINATOR_SIZE)).toString('hex');
|
|
191
|
+
for (const account of idl?.accounts ?? []) {
|
|
192
|
+
const discriminator = account?.discriminator;
|
|
193
|
+
if (!Array.isArray(discriminator) || discriminator.length !== ACCOUNT_DISCRIMINATOR_SIZE) continue;
|
|
194
|
+
if (Buffer.from(discriminator).toString('hex') === head) {
|
|
195
|
+
return { name: account.name, discriminator: head };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
package/src/acquire.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The acquisition path: find and decode a program's IDL account from chain.
|
|
2
|
+
//
|
|
3
|
+
// This module moved out of `tools/measure.mjs` on 2026-11-01 so that
|
|
4
|
+
// `src/pull.mjs` never reaches OUT of the shipped package (it used to import
|
|
5
|
+
// `../tools/measure.mjs`, which forced a consumer either to miss files or to
|
|
6
|
+
// download the whole qualification harness). `tools/measure.mjs` now imports
|
|
7
|
+
// these helpers back from here; the CLI/manifest logic stays in tools/.
|
|
8
|
+
//
|
|
9
|
+
// Everything here touches the network and the ed25519 curve, so src/peer.mjs
|
|
10
|
+
// is the ONLY place a third-party specifier may appear (allowlisted in
|
|
11
|
+
// test/deps.test.mjs). It is deliberately NOT reachable from the package
|
|
12
|
+
// entry (`index.mjs`): a decoder-only consumer installs zero packages, and
|
|
13
|
+
// the acquisition subpath (`sememe/pull`) declares web3.js as an optional
|
|
14
|
+
// peer — pull users install it themselves.
|
|
15
|
+
//
|
|
16
|
+
// 2026-11-06: the peer is loaded through src/peer.mjs, which refuses with a
|
|
17
|
+
// sentence when it is absent (measured: a bare tarball install without the
|
|
18
|
+
// peer used to surface a raw ERR_MODULE_NOT_FOUND from THIS file's import).
|
|
19
|
+
// Importing sememe/pull or sememe/gap still refuses — it now refuses in
|
|
20
|
+
// sememe's voice, at import time, naming the install command.
|
|
21
|
+
import { gunzipSync, inflateSync } from 'node:zlib';
|
|
22
|
+
import { createHash } from 'node:crypto';
|
|
23
|
+
import { PublicKey } from './peer.mjs';
|
|
24
|
+
|
|
25
|
+
export const METADATA_PROGRAM = new PublicKey('ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S');
|
|
26
|
+
const ANCHOR_DISCRIMINATOR = Buffer.from([24, 70, 98, 191, 58, 144, 123, 158]);
|
|
27
|
+
const MAX_BYTES = 16 << 20;
|
|
28
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
29
|
+
|
|
30
|
+
/** The two account addresses a program's interface can live at. */
|
|
31
|
+
export async function addresses(id) {
|
|
32
|
+
const program = new PublicKey(id);
|
|
33
|
+
const seed = Buffer.alloc(16); seed.write('idl');
|
|
34
|
+
const [metadata] = PublicKey.findProgramAddressSync([program.toBuffer(), seed], METADATA_PROGRAM);
|
|
35
|
+
const [base] = PublicKey.findProgramAddressSync([], program);
|
|
36
|
+
const anchor = await PublicKey.createWithSeed(base, 'anchor:idl', program);
|
|
37
|
+
return { metadata: metadata.toBase58(), anchor: anchor.toBase58() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bodyDecode(bytes, compression) {
|
|
41
|
+
if (bytes.length > MAX_BYTES) throw Error('body exceeds maximum');
|
|
42
|
+
let decoded;
|
|
43
|
+
if (compression === 0) decoded = bytes;
|
|
44
|
+
else if (compression === 1) decoded = gunzipSync(bytes, { maxOutputLength: MAX_BYTES });
|
|
45
|
+
else if (compression === 2) decoded = inflateSync(bytes, { maxOutputLength: MAX_BYTES });
|
|
46
|
+
else throw Error(`unsupported compression: ${compression}`);
|
|
47
|
+
if (decoded.length > MAX_BYTES) throw Error('decompressed body exceeds maximum');
|
|
48
|
+
return JSON.parse(decoded.toString('utf8'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Decode a raw IDL account (canonical Program Metadata or legacy anchor:idl). */
|
|
52
|
+
export function decodeAccount(account, source, programId) {
|
|
53
|
+
if (!account) throw Error('missing account');
|
|
54
|
+
const expectedOwner = source === 'metadata' ? METADATA_PROGRAM.toBase58() : programId;
|
|
55
|
+
if (account.owner !== expectedOwner) throw Error(`wrong owner: ${account.owner}`);
|
|
56
|
+
if (!Array.isArray(account.data) || account.data[1] !== 'base64') throw Error('invalid encoding');
|
|
57
|
+
const bytes = Buffer.from(account.data[0], 'base64');
|
|
58
|
+
let idl;
|
|
59
|
+
if (source === 'metadata') {
|
|
60
|
+
const seed = Buffer.alloc(16); seed.write('idl');
|
|
61
|
+
if (bytes.length < 96 || bytes[0] !== 2 || !bytes.subarray(1,33).equals(new PublicKey(programId).toBuffer()) ||
|
|
62
|
+
bytes[66] === 0 || !bytes.subarray(67,83).equals(seed) || bytes[83] !== 1 || bytes[85] > 1 || bytes[86] !== 0)
|
|
63
|
+
throw Error('invalid metadata header, binding, source or format');
|
|
64
|
+
const len = bytes.readUInt32LE(87);
|
|
65
|
+
if (len > MAX_BYTES || bytes.length < 96 + len) throw Error('invalid metadata length');
|
|
66
|
+
idl = bodyDecode(bytes.subarray(96, 96 + len), bytes[84]);
|
|
67
|
+
} else if (source === 'anchor') {
|
|
68
|
+
if (bytes.length < 44 || !bytes.subarray(0,8).equals(ANCHOR_DISCRIMINATOR)) throw Error('invalid Anchor header');
|
|
69
|
+
const len = bytes.readUInt32LE(40);
|
|
70
|
+
if (len > MAX_BYTES || bytes.length < 44 + len) throw Error('invalid Anchor length');
|
|
71
|
+
idl = bodyDecode(bytes.subarray(44, 44 + len), 2);
|
|
72
|
+
} else throw Error(`unknown source: ${source}`);
|
|
73
|
+
if (!idl || typeof idl !== 'object' || !Array.isArray(idl.instructions) || !idl.instructions.length ||
|
|
74
|
+
idl.instructions.some(x => !x || typeof x.name !== 'string' || !x.name))
|
|
75
|
+
throw Error('no usable instruction schema');
|
|
76
|
+
if (idl.address && idl.address !== programId) throw Error('IDL declares another program');
|
|
77
|
+
return idl;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Batched `getMultipleAccounts` with bounded retry; never throws — returns {error} instead. */
|
|
81
|
+
/**
|
|
82
|
+
* Classify ONE IDL account: is there a usable schema behind it?
|
|
83
|
+
*
|
|
84
|
+
* "Usable" must mean a decoder can USE the schema, not that the schema names
|
|
85
|
+
* things. A discriminator-less IDL names every instruction and still decodes
|
|
86
|
+
* nothing: measured on Kamino Lend (66 instructions, 0 discriminators), 0 of
|
|
87
|
+
* 1,432 real mainnet payloads decoded.
|
|
88
|
+
*
|
|
89
|
+
* This moved here from tools/measure.mjs on 2026-11-04: the gap classifier
|
|
90
|
+
* (src/gap.mjs) needs it, and the classifier has to work from an INSTALLED
|
|
91
|
+
* package — tools/ is not shipped.
|
|
92
|
+
*/
|
|
93
|
+
export function inspect(account, source, programId) {
|
|
94
|
+
if (!account) return { state: 'missing' };
|
|
95
|
+
const expectedOwner = source === 'metadata' ? METADATA_PROGRAM.toBase58() : programId;
|
|
96
|
+
if (account.owner !== expectedOwner) return { state: 'wrong_owner', owner: account.owner };
|
|
97
|
+
try {
|
|
98
|
+
const idl = decodeAccount(account, source, programId);
|
|
99
|
+
const digest = createHash('sha256').update(JSON.stringify(idl)).digest('hex');
|
|
100
|
+
const stored = idl.instructions.filter((ix) => Array.isArray(ix.discriminator)).length;
|
|
101
|
+
const derived = idl.instructions.length - stored;
|
|
102
|
+
const decodable = stored + derived > 0;
|
|
103
|
+
return {
|
|
104
|
+
state: 'usable',
|
|
105
|
+
instructions: idl.instructions.length,
|
|
106
|
+
fingerprint: digest,
|
|
107
|
+
// A strict, honest split for the coverage claim:
|
|
108
|
+
// storedDiscriminators - the program itself published the bytes
|
|
109
|
+
// derivedDiscriminators - Anchor's rule can supply them from the name
|
|
110
|
+
// decodableStoredOnly - usable with the published bytes alone
|
|
111
|
+
storedDiscriminators: stored,
|
|
112
|
+
derivedDiscriminators: derived,
|
|
113
|
+
decodableStoredOnly: stored > 0,
|
|
114
|
+
decodableWithDerivation: decodable,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return { state: 'undecodable', error: String(error?.message ?? error).slice(0, 150) };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function rpcAccounts(url, keys, fetcher = fetch, tries = 3) {
|
|
122
|
+
const payload = { jsonrpc: '2.0', id: 1, method: 'getMultipleAccounts',
|
|
123
|
+
params: [keys, { encoding: 'base64', commitment: 'finalized' }] };
|
|
124
|
+
for (let attempt = 0; attempt < tries; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetcher(url, { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
127
|
+
body: JSON.stringify(payload), signal: AbortSignal.timeout(15000) });
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
if (response.status === 429 || response.status >= 500) {
|
|
130
|
+
const retry = Number(response.headers.get('retry-after'));
|
|
131
|
+
if (attempt < tries - 1) { await sleep(Number.isFinite(retry) && retry > 0 ? Math.min(retry * 1000, 10000) : (attempt + 1) * 1500); continue; }
|
|
132
|
+
}
|
|
133
|
+
throw Error(`HTTP ${response.status}`);
|
|
134
|
+
}
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
if (data.error) throw Error(`RPC error ${data.error.code ?? 'unknown'}`);
|
|
137
|
+
if (!Array.isArray(data.result?.value) || data.result.value.length !== keys.length) throw Error('malformed RPC response');
|
|
138
|
+
return { slot: data.result.context?.slot ?? null, accounts: data.result.value };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (attempt === tries - 1) return { error: String(error?.message ?? error).slice(0, 150) };
|
|
141
|
+
await sleep((attempt + 1) * 1500);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { error: 'exhausted retries' };
|
|
145
|
+
}
|
package/src/adopt.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// `sememe adopt <program> <file>` — put a first-party PUBLISHED interface copy
|
|
2
|
+
// into the store so union decode has a second verified source to read.
|
|
3
|
+
//
|
|
4
|
+
// What adopt does and does not claim. `pull` gets its authority from the chain:
|
|
5
|
+
// the account is fetched at a finalized slot and bound to the program id.
|
|
6
|
+
// `adopt` gets its authority from a FILE the caller names: it is content-
|
|
7
|
+
// addressed and fingerprinted exactly like a pull, and every reader re-hashes
|
|
8
|
+
// it — but its receipt says `source: published` and carries the origin the
|
|
9
|
+
// caller declared. It is provenance of BYTES, not proof the bytes describe the
|
|
10
|
+
// deployed program. That distinction is why the two receipts stay separate and
|
|
11
|
+
// why union decode must keep naming which source answered.
|
|
12
|
+
//
|
|
13
|
+
// The ORIGIN is a DECLARED label and sememe says so on every surface
|
|
14
|
+
// (`originStatus: 'declared'`, the reader label, the CLI note): nothing checks
|
|
15
|
+
// that the named repository, commit or file exists. Measured 2026-11-07 (this
|
|
16
|
+
// host, published v0.4.10): a caller could name a repository that does not
|
|
17
|
+
// exist and every reader would repeat it as the origin, and `sememe verify`
|
|
18
|
+
// stayed green after the origin was rewritten in the receipt — because the
|
|
19
|
+
// fingerprint covers the OBJECT and the label next to it was covered by
|
|
20
|
+
// nothing. The receipt now carries `originDigest` (binds the declared label)
|
|
21
|
+
// and `receiptDigest` (binds the whole receipt body), see
|
|
22
|
+
// src/receipt-digest.mjs; readers refuse a receipt whose digest disagrees.
|
|
23
|
+
// The origin is still NOT verified — it is bound, so it cannot change
|
|
24
|
+
// silently. Checked provenance remains first-party adoption with a recorded
|
|
25
|
+
// commit and the on-chain pull, which needs no label at all.
|
|
26
|
+
//
|
|
27
|
+
// Why slot 0 in the receipt filename: receipts sort as
|
|
28
|
+
// `<program>-<slot>-<source>-<fingerprint>.json` and `findStoredIdl` picks the
|
|
29
|
+
// last name. Adopted copies must NEVER win the default single-source selection
|
|
30
|
+
// over an on-chain receipt — otherwise adopting a published copy would silently
|
|
31
|
+
// switch what plain `sememe decode` reads (measured hazard: pump's published
|
|
32
|
+
// copy disagrees with its on-chain account on 4 shared instructions). The
|
|
33
|
+
// receipt carries `slot: null` to say the truth about where it did NOT come
|
|
34
|
+
// from; the filename segment only orders it before every real slot.
|
|
35
|
+
|
|
36
|
+
import { createHash } from 'node:crypto';
|
|
37
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { canonicalJson } from './canonical.mjs';
|
|
40
|
+
import { writeOnce } from './store-write.mjs';
|
|
41
|
+
import { originDigestOf, receiptDigestOf } from './receipt-digest.mjs';
|
|
42
|
+
import { assertIdlShape, assertReadableDialect, declaredProgram } from './dialect.mjs';
|
|
43
|
+
|
|
44
|
+
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
45
|
+
|
|
46
|
+
/** What `sememe adopt` prints about the origin it recorded. It lives in the
|
|
47
|
+
* receipt body for new receipts and in the CLI output always. */
|
|
48
|
+
export const ORIGIN_NOTE = 'declared origin, not verified: sememe did not check that this repository, commit or file exists, and the bytes were not proven to come from it — provenance of bytes, not proof they describe the deployed program';
|
|
49
|
+
|
|
50
|
+
/** Fields that identify WHICH object a receipt points at. Two receipts of one
|
|
51
|
+
* filename that agree on these make the same claim; only a differing declared
|
|
52
|
+
* origin makes them two claims about one set of bytes. */
|
|
53
|
+
const IDENTITY_FIELDS = ['program', 'source', 'slot', 'fingerprint', 'instructionCount'];
|
|
54
|
+
|
|
55
|
+
const safeLabel = (value, what) => {
|
|
56
|
+
if (typeof value !== 'string' || !value.trim()) throw Error(`${what} must be a non-empty string`);
|
|
57
|
+
const cleaned = value.trim().replace(/[^A-Za-z0-9._:-]+/g, '-').slice(0, 64);
|
|
58
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(cleaned)) throw Error(`${what} is not usable in a receipt filename: ${JSON.stringify(value)}`);
|
|
59
|
+
return cleaned;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Store a published IDL file under a content-addressed object plus a receipt.
|
|
64
|
+
*
|
|
65
|
+
* Validation mirrors what `pull` enforces on fetched content: JSON, an
|
|
66
|
+
* instructions array, and — when the file declares an address — agreement with
|
|
67
|
+
* the program the receipt binds. A file that declares a DIFFERENT program is
|
|
68
|
+
* refused rather than re-labelled.
|
|
69
|
+
*
|
|
70
|
+
* Returns the receipt (same shape family as `pull`, plus `origin`).
|
|
71
|
+
*/
|
|
72
|
+
export async function adopt(programInput, filePath, storeDir, { source = 'published', origin } = {}) {
|
|
73
|
+
if (typeof programInput !== 'string' || !programInput.trim()) throw Error('program id required');
|
|
74
|
+
const program = programInput.trim();
|
|
75
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(program)) throw Error('program id must be base58');
|
|
76
|
+
if (typeof storeDir !== 'string' || !storeDir.trim()) throw Error('explicit --store path required');
|
|
77
|
+
const sourceLabel = safeLabel(source, '--source');
|
|
78
|
+
const originLabel = safeLabel(origin ?? path.basename(filePath), '--origin');
|
|
79
|
+
|
|
80
|
+
let idl;
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = await readFile(filePath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw Error(`cannot read ${filePath}: ${String(error?.message ?? error).slice(0, 140)}`);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
idl = JSON.parse(raw.toString('utf8'));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw Error(`${filePath} is not JSON: ${String(error?.message ?? error).slice(0, 140)}`);
|
|
91
|
+
}
|
|
92
|
+
if (!idl || typeof idl !== 'object' || Array.isArray(idl)) throw Error(`${filePath} is not an IDL object`);
|
|
93
|
+
if (!Array.isArray(idl.instructions)) throw Error(`${filePath} has no instructions array — not an interface copy`);
|
|
94
|
+
// Two INDEPENDENT gates, each with its own named voice, checked in the
|
|
95
|
+
// order the faces were measured — an integrity question first (does this
|
|
96
|
+
// file claim to be about THIS program?), a capability question second
|
|
97
|
+
// (can the codec read this dialect at all?). Collapsing them would hide
|
|
98
|
+
// one face behind the other.
|
|
99
|
+
//
|
|
100
|
+
// Gate 1 — declared-program agreement. A file may declare its program
|
|
101
|
+
// under `address` (Anchor), `metadata.address` (shank) or `program_id`
|
|
102
|
+
// (raw dialects). Whatever the field is called, a DECLARED program that
|
|
103
|
+
// differs from the receipt's program is a re-labelling: measured
|
|
104
|
+
// 2026-11-12, the diabloarb file declares `program_id` only, and the old
|
|
105
|
+
// address-only check adopted it under WSOL's program id unchanged.
|
|
106
|
+
if (idl.address !== undefined && typeof idl.address !== 'string') {
|
|
107
|
+
throw Error(`${filePath} declares a malformed address field`);
|
|
108
|
+
}
|
|
109
|
+
const declared = declaredProgram(idl);
|
|
110
|
+
if (declared && declared !== program) {
|
|
111
|
+
if (idl.address === undefined) {
|
|
112
|
+
// The declaration rides a field the old check never read — name the
|
|
113
|
+
// field so the repair is obvious (this is the measured diabloarb face).
|
|
114
|
+
const field = idl.program_id !== undefined ? 'program_id'
|
|
115
|
+
: idl?.metadata?.address !== undefined ? 'metadata.address' : 'declared program';
|
|
116
|
+
throw Error(`${filePath} declares program ${declared} via ${field}, not ${program} — refusing to re-label a foreign interface`);
|
|
117
|
+
}
|
|
118
|
+
throw Error(`${filePath} declares program ${declared}, not ${program} — refusing to re-label a foreign interface`);
|
|
119
|
+
}
|
|
120
|
+
// Gate 2 — the dialect gate at the store's WRITE path: a dialect NO
|
|
121
|
+
// engine of the product can read is refused HERE by name, before
|
|
122
|
+
// adoption can store an undecodable interface. SINCE THE PULS-Z WIRING
|
|
123
|
+
// a raw-LE file passes (the codec serves it from src/rawle.mjs),
|
|
124
|
+
// everything else keeps the named refusal (measured 2026-11-12:
|
|
125
|
+
// diabloarb's dict `types` was the face of that refusal).
|
|
126
|
+
assertReadableDialect(idl, (reason) => Error(`${filePath}: ${reason}`));
|
|
127
|
+
|
|
128
|
+
const json = Buffer.from(canonicalJson(idl) + '\n');
|
|
129
|
+
const fingerprint = sha256(json);
|
|
130
|
+
const directory = path.resolve(storeDir);
|
|
131
|
+
const objects = path.join(directory, 'objects');
|
|
132
|
+
const receipts = path.join(directory, 'receipts');
|
|
133
|
+
await mkdir(objects, { recursive: true, mode: 0o700 });
|
|
134
|
+
await mkdir(receipts, { recursive: true, mode: 0o700 });
|
|
135
|
+
await writeOnce(path.join(objects, `${fingerprint}.json`), json);
|
|
136
|
+
|
|
137
|
+
const receipt = {
|
|
138
|
+
program,
|
|
139
|
+
source: sourceLabel,
|
|
140
|
+
origin: originLabel,
|
|
141
|
+
originStatus: 'declared',
|
|
142
|
+
originNote: ORIGIN_NOTE,
|
|
143
|
+
originDigest: originDigestOf(originLabel),
|
|
144
|
+
// null, not 0: this receipt did not come from a slot. The filename uses 0
|
|
145
|
+
// purely so it sorts before every real slot in findStoredIdl's selection.
|
|
146
|
+
slot: null,
|
|
147
|
+
fingerprint: `sha256:${fingerprint}`,
|
|
148
|
+
instructionCount: idl.instructions.length,
|
|
149
|
+
};
|
|
150
|
+
// The digest covers the whole body INCLUDING the declared label — a receipt
|
|
151
|
+
// that is edited afterwards fails this check on every read, not only under
|
|
152
|
+
// a lock.
|
|
153
|
+
receipt.receiptDigest = receiptDigestOf(receipt);
|
|
154
|
+
const bytes = Buffer.from(canonicalJson(receipt) + '\n');
|
|
155
|
+
|
|
156
|
+
const plainPath = path.join(receipts, `${program}-0-${sourceLabel}-${fingerprint}.json`);
|
|
157
|
+
let receiptPath = plainPath;
|
|
158
|
+
const existing = await readReceiptIfPresent(plainPath);
|
|
159
|
+
if (existing) {
|
|
160
|
+
const sameClaim = IDENTITY_FIELDS
|
|
161
|
+
.every((field) => (existing[field] ?? null) === (receipt[field] ?? null));
|
|
162
|
+
if (sameClaim && existing.origin === receipt.origin) {
|
|
163
|
+
// Re-adopting the SAME bytes under the SAME declared origin changes
|
|
164
|
+
// nothing — including a legacy body written before the digest fields
|
|
165
|
+
// existed: an old receipt is never rewritten in place, its origin is
|
|
166
|
+
// then merely DECLARED without a binding.
|
|
167
|
+
return {
|
|
168
|
+
...existing,
|
|
169
|
+
originStatus: existing.originStatus ?? 'declared',
|
|
170
|
+
originNote: ORIGIN_NOTE,
|
|
171
|
+
path: path.join(objects, `${fingerprint}.json`),
|
|
172
|
+
receiptPath: plainPath,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (!sameClaim) {
|
|
176
|
+
// The filename already names program/source/fingerprint, so a receipt
|
|
177
|
+
// that disagrees on these is not adopt's doing — refuse instead of
|
|
178
|
+
// layering a second claim over a file that means something else.
|
|
179
|
+
throw Error(`receipt ${plainPath} exists with different content for the same program, source and fingerprint — refusing to store this interface copy`);
|
|
180
|
+
}
|
|
181
|
+
// The SAME bytes under a DIFFERENT declared origin: two claims about one
|
|
182
|
+
// object, so two receipts. The disambiguator is a digest of the declared
|
|
183
|
+
// origin (12 hex chars), placed BEFORE the fingerprint so the filename
|
|
184
|
+
// keeps its `<program>-<slot>-<source>-<fingerprint>` tail that readers
|
|
185
|
+
// and tools parse the object id out of. The caller's TEXT never enters
|
|
186
|
+
// the filename, and both receipts carry the same fingerprint, so which of
|
|
187
|
+
// them sorts last cannot change a decoded byte — only which declared
|
|
188
|
+
// label is shown first.
|
|
189
|
+
const originKey = originDigestOf(originLabel).slice(7, 19);
|
|
190
|
+
receiptPath = path.join(receipts, `${program}-0-${sourceLabel}-${originKey}-${fingerprint}.json`);
|
|
191
|
+
}
|
|
192
|
+
await writeOnce(receiptPath, bytes);
|
|
193
|
+
return { ...receipt, path: path.join(objects, `${fingerprint}.json`), receiptPath };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readReceiptIfPresent(receiptPath) {
|
|
197
|
+
try {
|
|
198
|
+
return JSON.parse(await readFile(receiptPath, 'utf8'));
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error?.code === 'ENOENT') return null;
|
|
201
|
+
if (error instanceof SyntaxError) {
|
|
202
|
+
throw Error(`receipt ${receiptPath} is not JSON — refusing to build a second claim over it`);
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|