brainblast 0.6.4 → 0.7.0

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.
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  CANONICAL_MINTS
3
3
  } from "./chunk-2XJORJPQ.js";
4
+ import {
5
+ toSnakeCase
6
+ } from "./chunk-O5Z4ZJHC.js";
4
7
 
5
8
  // src/finder.ts
6
9
  import { Project, SyntaxKind } from "ts-morph";
@@ -852,6 +855,64 @@ var solanaMintIdentity = (c, _params) => {
852
855
  return { result: "cant_tell", detail: "No symbol-named mint constants found" };
853
856
  };
854
857
 
858
+ // src/checkers/anchorIdlAccount.ts
859
+ function fieldIsSigner(f) {
860
+ if (/\bSigner\s*</.test(f.typeName)) return true;
861
+ if (/\bsigner\b/.test(f.attrText)) return true;
862
+ return false;
863
+ }
864
+ function fieldIsMut(f) {
865
+ if (/\bmut\b/.test(f.attrText)) return true;
866
+ if (/\binit\b/.test(f.attrText)) return true;
867
+ if (f.hasInitIfNeeded) return true;
868
+ return false;
869
+ }
870
+ function anchorIdlAccount(c, params) {
871
+ const handlerName = toSnakeCase(c.fnName);
872
+ const spec = params?.instructions?.find((i) => i.name === handlerName);
873
+ if (!spec) {
874
+ return {
875
+ result: "cant_tell",
876
+ detail: `Handler '${c.fnName}' is not declared in the ${params?.idlName ?? "IDL"}; constraint rule does not apply.`
877
+ };
878
+ }
879
+ if (spec.signers.length === 0 && spec.mutable.length === 0) {
880
+ return {
881
+ result: "cant_tell",
882
+ detail: `Instruction '${spec.name}' declares no signer or mutable accounts in the IDL; nothing to verify.`
883
+ };
884
+ }
885
+ const byName = /* @__PURE__ */ new Map();
886
+ for (const f of c.accountFields) byName.set(toSnakeCase(f.name), f);
887
+ const violations = [];
888
+ for (const acct of spec.signers) {
889
+ const f = byName.get(acct);
890
+ if (!f) {
891
+ violations.push(`'${acct}' (IDL signer) is not present in the Accounts struct`);
892
+ } else if (!fieldIsSigner(f)) {
893
+ violations.push(`'${acct}' must be a Signer (IDL marks it isSigner) but the Rust field has no signer constraint`);
894
+ }
895
+ }
896
+ for (const acct of spec.mutable) {
897
+ const f = byName.get(acct);
898
+ if (!f) {
899
+ violations.push(`'${acct}' (IDL mutable) is not present in the Accounts struct`);
900
+ } else if (!fieldIsMut(f)) {
901
+ violations.push(`'${acct}' must be mutable (IDL marks it isMut) but the Rust field has no mut/init constraint`);
902
+ }
903
+ }
904
+ if (violations.length > 0) {
905
+ return {
906
+ result: "fail",
907
+ detail: `Handler '${c.fnName}' diverges from the ${params.idlName} IDL: ` + violations.join("; ") + ". A missing signer/mut constraint is a silent authorization hole."
908
+ };
909
+ }
910
+ return {
911
+ result: "pass",
912
+ detail: `Handler '${c.fnName}' declares all ${spec.signers.length} signer and ${spec.mutable.length} mutable account(s) the ${params.idlName} IDL requires.`
913
+ };
914
+ }
915
+
855
916
  // src/checkers/index.ts
856
917
  var registry = {
857
918
  "positional-arg-identity": positionalArgIdentity,
@@ -865,7 +926,8 @@ var registry = {
865
926
  "taint-to-sink": taintToSink,
866
927
  "literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
867
928
  "forbidden-call-replacement": forbiddenCallReplacement,
868
- "solana-mint-identity-mismatch": solanaMintIdentity
929
+ "solana-mint-identity-mismatch": solanaMintIdentity,
930
+ "anchor-account-matches-idl": anchorIdlAccount
869
931
  };
870
932
  function runChecker(kind, c, params) {
871
933
  const fn = registry[kind];
@@ -0,0 +1,111 @@
1
+ import {
2
+ DEFAULT_RPC,
3
+ probeUpgradeAuthority
4
+ } from "./chunk-XSVQSK53.js";
5
+
6
+ // src/watchChain.ts
7
+ async function getSignaturesForAddress(programId, until, opts) {
8
+ const url = opts.rpcUrl ?? DEFAULT_RPC;
9
+ const fetchImpl = opts.fetchImpl ?? fetch;
10
+ const params = [programId, { limit: opts.limit ?? 25 }];
11
+ if (until) params[1].until = until;
12
+ const ac = new AbortController();
13
+ const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
14
+ try {
15
+ const res = await fetchImpl(url, {
16
+ method: "POST",
17
+ headers: { "content-type": "application/json" },
18
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSignaturesForAddress", params }),
19
+ signal: ac.signal
20
+ });
21
+ if (!res.ok) throw new Error(`getSignaturesForAddress: HTTP ${res.status}`);
22
+ const body = await res.json();
23
+ if (body.error) throw new Error(`getSignaturesForAddress: ${body.error.message}`);
24
+ return body.result ?? [];
25
+ } finally {
26
+ clearTimeout(t);
27
+ }
28
+ }
29
+ async function pollChainOnce(programId, state, opts = {}) {
30
+ const events = [];
31
+ const ts = () => (/* @__PURE__ */ new Date()).toISOString();
32
+ let sigs;
33
+ try {
34
+ sigs = await getSignaturesForAddress(programId, state.lastSignature, opts);
35
+ } catch (e) {
36
+ events.push({ type: "poll_error", programId, message: e?.message ?? String(e), ts: ts() });
37
+ return { events, state };
38
+ }
39
+ const probe = opts.probeAuthority ?? ((id, o) => probeUpgradeAuthority(id, o).then((ua) => ({ address: ua.address })));
40
+ let currentAuthority = state.baselineAuthority;
41
+ try {
42
+ const ua = await probe(programId, opts);
43
+ currentAuthority = ua.address;
44
+ } catch (e) {
45
+ events.push({ type: "poll_error", programId, message: `authority probe: ${e?.message ?? String(e)}`, ts: ts() });
46
+ }
47
+ if (!state.initialized) {
48
+ const head = sigs[0]?.signature ?? null;
49
+ events.push({ type: "watch_started", programId, headSignature: head, baselineAuthority: currentAuthority, ts: ts() });
50
+ return {
51
+ events,
52
+ state: { lastSignature: head, baselineAuthority: currentAuthority, initialized: true }
53
+ };
54
+ }
55
+ if (sigs.length > 0) {
56
+ events.push({
57
+ type: "new_activity",
58
+ programId,
59
+ newCount: sigs.length,
60
+ signatures: sigs.slice(0, 10).map((s) => s.signature),
61
+ ts: ts()
62
+ });
63
+ }
64
+ if (currentAuthority !== state.baselineAuthority) {
65
+ events.push({
66
+ type: "authority_changed",
67
+ programId,
68
+ from: state.baselineAuthority,
69
+ to: currentAuthority,
70
+ ts: ts()
71
+ });
72
+ }
73
+ return {
74
+ events,
75
+ state: {
76
+ lastSignature: sigs[0]?.signature ?? state.lastSignature,
77
+ baselineAuthority: currentAuthority,
78
+ initialized: true
79
+ }
80
+ };
81
+ }
82
+ function initialChainWatchState() {
83
+ return { lastSignature: null, baselineAuthority: null, initialized: false };
84
+ }
85
+ function startChainWatch(programId, opts = {}) {
86
+ const emit = opts.emit ?? ((e) => process.stdout.write(JSON.stringify(e) + "\n"));
87
+ const intervalMs = opts.intervalMs ?? 3e4;
88
+ let state = initialChainWatchState();
89
+ let stopped = false;
90
+ let timer;
91
+ const tick = async () => {
92
+ if (stopped) return;
93
+ const { events, state: next } = await pollChainOnce(programId, state, opts);
94
+ state = next;
95
+ for (const e of events) emit(e);
96
+ if (!stopped) timer = setTimeout(tick, intervalMs);
97
+ };
98
+ void tick();
99
+ return {
100
+ stop: () => {
101
+ stopped = true;
102
+ if (timer) clearTimeout(timer);
103
+ }
104
+ };
105
+ }
106
+
107
+ export {
108
+ pollChainOnce,
109
+ initialChainWatchState,
110
+ startChainWatch
111
+ };
@@ -0,0 +1,331 @@
1
+ import {
2
+ base58Encode
3
+ } from "./chunk-VG5FMOLW.js";
4
+
5
+ // src/firewall.ts
6
+ var KNOWN_PROGRAMS = {
7
+ "11111111111111111111111111111111": "System Program",
8
+ TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "SPL Token",
9
+ TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: "SPL Token-2022",
10
+ ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL: "Associated Token Account",
11
+ metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s: "Metaplex Token Metadata",
12
+ MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr: "SPL Memo",
13
+ ComputeBudget111111111111111111111111111111: "Compute Budget",
14
+ AddressLookupTab1e1111111111111111111111111: "Address Lookup Table",
15
+ BPFLoaderUpgradeab1e11111111111111111111111: "BPF Upgradeable Loader",
16
+ // Blue-chip DeFi / launch programs
17
+ JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4: "Jupiter Aggregator v6",
18
+ "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium AMM v4",
19
+ whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc: "Orca Whirlpools",
20
+ "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "pump.fun",
21
+ pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA: "PumpSwap AMM"
22
+ };
23
+ var SPL_TOKEN_PROGRAMS = /* @__PURE__ */ new Set([
24
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
25
+ "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
26
+ ]);
27
+ var BPF_UPGRADEABLE_LOADER = "BPFLoaderUpgradeab1e11111111111111111111111";
28
+ function readCompactU16(buf, offset) {
29
+ let value = 0;
30
+ let shift = 0;
31
+ let o = offset;
32
+ for (; ; ) {
33
+ const byte = buf[o++];
34
+ value |= (byte & 127) << shift;
35
+ if ((byte & 128) === 0) break;
36
+ shift += 7;
37
+ if (shift > 21) throw new Error("compact-u16 too long");
38
+ }
39
+ return [value, o];
40
+ }
41
+ function decodeTransaction(base64, opts = {}) {
42
+ const bytes = new Uint8Array(Buffer.from(base64, "base64"));
43
+ if (bytes.length < 35) throw new Error("transaction too short to be valid");
44
+ let o = 0;
45
+ if (!opts.messageOnly) {
46
+ const [sigCount, afterCount] = readCompactU16(bytes, 0);
47
+ o = afterCount + sigCount * 64;
48
+ if (o >= bytes.length) throw new Error("signature array overruns buffer");
49
+ }
50
+ const msg = bytes.subarray(o);
51
+ let mo = 0;
52
+ let version = "legacy";
53
+ if ((msg[0] & 128) !== 0) {
54
+ version = msg[0] & 127;
55
+ mo = 1;
56
+ }
57
+ const numRequiredSignatures = msg[mo++];
58
+ const numReadonlySigned = msg[mo++];
59
+ const numReadonlyUnsigned = msg[mo++];
60
+ const [acctCount, afterAccts] = readCompactU16(msg, mo);
61
+ mo = afterAccts;
62
+ const staticAccountKeys = [];
63
+ for (let i = 0; i < acctCount; i++) {
64
+ staticAccountKeys.push(base58Encode(msg.subarray(mo, mo + 32)));
65
+ mo += 32;
66
+ }
67
+ const recentBlockhash = base58Encode(msg.subarray(mo, mo + 32));
68
+ mo += 32;
69
+ const [ixCount, afterIxCount] = readCompactU16(msg, mo);
70
+ mo = afterIxCount;
71
+ const instructions = [];
72
+ for (let i = 0; i < ixCount; i++) {
73
+ const programIdIndex = msg[mo++];
74
+ const [naccts, afterNaccts] = readCompactU16(msg, mo);
75
+ mo = afterNaccts;
76
+ const accountIndexes = [];
77
+ for (let j = 0; j < naccts; j++) accountIndexes.push(msg[mo++]);
78
+ const [datalen, afterDatalen] = readCompactU16(msg, mo);
79
+ mo = afterDatalen;
80
+ const data = msg.subarray(mo, mo + datalen);
81
+ mo += datalen;
82
+ instructions.push({
83
+ programIdIndex,
84
+ programId: staticAccountKeys[programIdIndex] ?? `#${programIdIndex}`,
85
+ accountIndexes,
86
+ data
87
+ });
88
+ }
89
+ const addressTableLookups = [];
90
+ if (version !== "legacy") {
91
+ const [altCount, afterAlt] = readCompactU16(msg, mo);
92
+ mo = afterAlt;
93
+ for (let i = 0; i < altCount; i++) {
94
+ const accountKey = base58Encode(msg.subarray(mo, mo + 32));
95
+ mo += 32;
96
+ const [wlen, afterW] = readCompactU16(msg, mo);
97
+ mo = afterW + wlen;
98
+ const [rlen, afterR] = readCompactU16(msg, mo);
99
+ mo = afterR + rlen;
100
+ addressTableLookups.push({ accountKey, writableCount: wlen, readonlyCount: rlen });
101
+ }
102
+ }
103
+ return {
104
+ version,
105
+ numRequiredSignatures,
106
+ numReadonlySigned,
107
+ numReadonlyUnsigned,
108
+ staticAccountKeys,
109
+ recentBlockhash,
110
+ instructions,
111
+ addressTableLookups
112
+ };
113
+ }
114
+ function splTokenFinding(disc) {
115
+ switch (disc) {
116
+ case 4:
117
+ // Approve
118
+ case 13:
119
+ return {
120
+ severity: "warn",
121
+ kind: "token-delegate-approval",
122
+ detail: "Approves a delegate over a token account \u2014 a common drain vector. Verify the delegate is trusted."
123
+ };
124
+ case 6:
125
+ return {
126
+ severity: "critical",
127
+ kind: "token-set-authority",
128
+ detail: "Changes a mint/account authority (SetAuthority). This can hand control of a token to another party."
129
+ };
130
+ case 9:
131
+ return {
132
+ severity: "info",
133
+ kind: "token-close-account",
134
+ detail: "Closes a token account and reclaims its rent to the destination."
135
+ };
136
+ default:
137
+ return null;
138
+ }
139
+ }
140
+ function bpfLoaderFinding(disc) {
141
+ if (disc === 3)
142
+ return {
143
+ severity: "critical",
144
+ kind: "program-upgrade",
145
+ detail: "Upgrades a deployed program (BPF Upgradeable Loader Upgrade). Replaces on-chain code."
146
+ };
147
+ if (disc === 4 || disc === 6)
148
+ return {
149
+ severity: "critical",
150
+ kind: "program-set-authority",
151
+ detail: "Changes a program's upgrade authority (BPF Loader SetAuthority)."
152
+ };
153
+ return null;
154
+ }
155
+ function analyzeInstructions(decoded, known) {
156
+ const findings = [];
157
+ for (const ix of decoded.instructions) {
158
+ const pid = ix.programId;
159
+ if (SPL_TOKEN_PROGRAMS.has(pid) && ix.data.length >= 1) {
160
+ const f = splTokenFinding(ix.data[0]);
161
+ if (f) findings.push(f);
162
+ }
163
+ if (pid === BPF_UPGRADEABLE_LOADER && ix.data.length >= 4) {
164
+ const disc = ix.data[0] | ix.data[1] << 8 | ix.data[2] << 16 | ix.data[3] << 24;
165
+ const f = bpfLoaderFinding(disc);
166
+ if (f) findings.push(f);
167
+ }
168
+ if (!(pid in known)) {
169
+ findings.push({
170
+ severity: "warn",
171
+ kind: "unknown-program",
172
+ detail: `Top-level instruction invokes an unrecognized program: ${pid}. Verify it is a program you intend to call.`
173
+ });
174
+ }
175
+ }
176
+ if (decoded.addressTableLookups.length > 0) {
177
+ findings.push({
178
+ severity: "info",
179
+ kind: "address-lookup-tables",
180
+ detail: `Transaction uses ${decoded.addressTableLookups.length} address lookup table(s); some accounts are resolved off-message and not statically visible.`
181
+ });
182
+ }
183
+ return findings;
184
+ }
185
+ function parseCpiPrograms(logs) {
186
+ const programs = /* @__PURE__ */ new Set();
187
+ for (const line of logs) {
188
+ const m = line.match(/^Program (\S+) invoke \[\d+\]$/);
189
+ if (m) programs.add(m[1]);
190
+ }
191
+ return [...programs];
192
+ }
193
+ async function simulate(base64, opts) {
194
+ const { DEFAULT_RPC } = await import("./rpc-W5F4KXS2.js");
195
+ const url = opts.rpcUrl ?? DEFAULT_RPC;
196
+ const fetchImpl = opts.fetchImpl ?? fetch;
197
+ const ac = new AbortController();
198
+ const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
199
+ try {
200
+ const res = await fetchImpl(url, {
201
+ method: "POST",
202
+ headers: { "content-type": "application/json" },
203
+ body: JSON.stringify({
204
+ jsonrpc: "2.0",
205
+ id: 1,
206
+ method: "simulateTransaction",
207
+ params: [base64, { encoding: "base64", sigVerify: false, replaceRecentBlockhash: true, commitment: "confirmed" }]
208
+ }),
209
+ signal: ac.signal
210
+ });
211
+ if (!res.ok) throw new Error(`simulateTransaction: HTTP ${res.status}`);
212
+ const body = await res.json();
213
+ if (body.error) throw new Error(`simulateTransaction: ${body.error.message}`);
214
+ const v = body.result?.value ?? {};
215
+ return { err: v.err ?? null, logs: v.logs ?? null, unitsConsumed: v.unitsConsumed };
216
+ } finally {
217
+ clearTimeout(t);
218
+ }
219
+ }
220
+ function worstSeverity(findings) {
221
+ if (findings.some((f) => f.severity === "critical")) return "block";
222
+ if (findings.some((f) => f.severity === "warn")) return "warn";
223
+ return "allow";
224
+ }
225
+ async function inspectTransaction(base64, opts = {}) {
226
+ const known = { ...KNOWN_PROGRAMS, ...opts.knownPrograms ?? {} };
227
+ const decoded = decodeTransaction(base64, { messageOnly: opts.messageOnly });
228
+ const findings = analyzeInstructions(decoded, known);
229
+ const topLevelIds = new Set(decoded.instructions.map((ix) => ix.programId));
230
+ const programs = [...topLevelIds].map((id) => ({
231
+ id,
232
+ label: known[id] ?? null,
233
+ known: id in known,
234
+ topLevel: true
235
+ }));
236
+ const report = {
237
+ version: decoded.version,
238
+ feePayer: decoded.staticAccountKeys[0] ?? "(none)",
239
+ numSigners: decoded.numRequiredSignatures,
240
+ staticAccounts: decoded.staticAccountKeys.length,
241
+ programs,
242
+ usesAddressLookupTables: decoded.addressTableLookups.length > 0,
243
+ simulation: { ran: false },
244
+ findings,
245
+ verdict: "allow"
246
+ };
247
+ if (opts.simulate !== false) {
248
+ try {
249
+ const sim = await simulate(base64, opts);
250
+ const cpiPrograms = sim.logs ? parseCpiPrograms(sim.logs) : [];
251
+ report.simulation = {
252
+ ran: true,
253
+ ok: sim.err === null,
254
+ err: sim.err ?? void 0,
255
+ unitsConsumed: sim.unitsConsumed,
256
+ logsCount: sim.logs?.length,
257
+ cpiPrograms
258
+ };
259
+ for (const pid of cpiPrograms) {
260
+ if (!(pid in known) && !topLevelIds.has(pid)) {
261
+ findings.push({
262
+ severity: "warn",
263
+ kind: "unknown-cpi-program",
264
+ detail: `Cross-program invocation to an unrecognized program: ${pid}.`
265
+ });
266
+ report.programs.push({ id: pid, label: null, known: false, topLevel: false });
267
+ }
268
+ }
269
+ if (sim.err !== null) {
270
+ findings.push({
271
+ severity: "warn",
272
+ kind: "simulation-failed",
273
+ detail: `Transaction simulation returned an error: ${JSON.stringify(sim.err)}. Signing it would likely fail on-chain.`
274
+ });
275
+ }
276
+ } catch (e) {
277
+ report.simulation = { ran: false };
278
+ findings.push({
279
+ severity: "info",
280
+ kind: "simulation-unavailable",
281
+ detail: `Could not simulate (static analysis only): ${e?.message ?? String(e)}`
282
+ });
283
+ }
284
+ }
285
+ report.verdict = worstSeverity(findings);
286
+ return report;
287
+ }
288
+ var SEV_ICON = { critical: "\u26D4", warn: "\u26A0 ", info: "\xB7 " };
289
+ var VERDICT_BANNER = {
290
+ allow: "ALLOW \u2014 no dangerous patterns detected",
291
+ warn: "WARN \u2014 review before signing",
292
+ block: "BLOCK \u2014 dangerous pattern detected, do not sign blind"
293
+ };
294
+ function renderFirewallText(r) {
295
+ const lines = [];
296
+ lines.push(`Transaction firewall [${VERDICT_BANNER[r.verdict]}]`);
297
+ lines.push("");
298
+ lines.push(` Version: ${r.version}`);
299
+ lines.push(` Fee payer: ${r.feePayer}`);
300
+ lines.push(` Signers: ${r.numSigners}`);
301
+ lines.push(` Accounts: ${r.staticAccounts}${r.usesAddressLookupTables ? " (+ lookup tables)" : ""}`);
302
+ lines.push(` Programs:`);
303
+ for (const p of r.programs) {
304
+ const tag = p.known ? p.label : "UNKNOWN";
305
+ lines.push(` ${p.known ? "\u2713" : "?"} ${p.id} ${tag}${p.topLevel ? "" : " (via CPI)"}`);
306
+ }
307
+ if (r.simulation.ran) {
308
+ lines.push(` Simulation: ${r.simulation.ok ? "ok" : "FAILED"}${r.simulation.unitsConsumed != null ? ` (${r.simulation.unitsConsumed} CU)` : ""}`);
309
+ } else {
310
+ lines.push(` Simulation: not run`);
311
+ }
312
+ lines.push("");
313
+ if (r.findings.length === 0) {
314
+ lines.push(" No findings.");
315
+ } else {
316
+ lines.push(" Findings:");
317
+ for (const f of r.findings) {
318
+ lines.push(` ${SEV_ICON[f.severity]} [${f.kind}] ${f.detail}`);
319
+ }
320
+ }
321
+ return lines.join("\n");
322
+ }
323
+
324
+ export {
325
+ KNOWN_PROGRAMS,
326
+ decodeTransaction,
327
+ analyzeInstructions,
328
+ parseCpiPrograms,
329
+ inspectTransaction,
330
+ renderFirewallText
331
+ };
@@ -0,0 +1,89 @@
1
+ // src/idlRules.ts
2
+ import { stringify as yamlStringify } from "yaml";
3
+ function toSnakeCase(s) {
4
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
5
+ }
6
+ function idlProgramName(idl) {
7
+ return idl.metadata?.name ?? idl.name ?? "anchor-program";
8
+ }
9
+ function flattenAccounts(accounts) {
10
+ const out = [];
11
+ for (const a of accounts) {
12
+ if (a.accounts && a.accounts.length > 0) {
13
+ out.push(...flattenAccounts(a.accounts));
14
+ } else {
15
+ out.push(a);
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+ function parseIdl(json) {
21
+ if (!json || typeof json !== "object") throw new Error("IDL is not an object");
22
+ const idl = json;
23
+ if (!Array.isArray(idl.instructions)) throw new Error("IDL has no instructions array");
24
+ for (const ix of idl.instructions) {
25
+ if (!ix.name || !Array.isArray(ix.accounts)) {
26
+ throw new Error(`IDL instruction missing name/accounts: ${JSON.stringify(ix).slice(0, 80)}`);
27
+ }
28
+ }
29
+ return idl;
30
+ }
31
+ function buildConstraintParams(idl) {
32
+ const instructions = idl.instructions.map((ix) => {
33
+ const leaves = flattenAccounts(ix.accounts);
34
+ return {
35
+ name: toSnakeCase(ix.name),
36
+ signers: leaves.filter((a) => a.isSigner).map((a) => toSnakeCase(a.name)),
37
+ mutable: leaves.filter((a) => a.isMut).map((a) => toSnakeCase(a.name))
38
+ };
39
+ });
40
+ return { idlName: idlProgramName(idl), instructions };
41
+ }
42
+ function generateRulesFromIdl(idl) {
43
+ const params = buildConstraintParams(idl);
44
+ const handlerNames = params.instructions.map((i) => i.name).filter(Boolean);
45
+ if (handlerNames.length === 0) return [];
46
+ const nameRegex = `^(${handlerNames.map(escapeRegex).join("|")})$`;
47
+ const progName = params.idlName;
48
+ const rule = {
49
+ id: `idl-${toKebab(progName)}-account-constraints`,
50
+ severity: "critical",
51
+ title: `Anchor accounts match the ${progName} IDL signer/mut constraints`,
52
+ component: {
53
+ name: progName,
54
+ type: "Anchor program",
55
+ version: idl.metadata?.version ?? idl.version ?? "unversioned",
56
+ sourceUrl: "https://www.anchor-lang.com/docs/the-accounts-struct"
57
+ },
58
+ detect: {
59
+ lang: "rust",
60
+ modules: ["@coral-xyz/anchor", "@project-serum/anchor"],
61
+ nameRegex,
62
+ triggerCalls: []
63
+ },
64
+ check: {
65
+ kind: "anchor-account-matches-idl",
66
+ params
67
+ },
68
+ test: { kind: "none" }
69
+ };
70
+ return [rule];
71
+ }
72
+ function escapeRegex(s) {
73
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
74
+ }
75
+ function toKebab(s) {
76
+ return toSnakeCase(s).replace(/_/g, "-");
77
+ }
78
+ function renderRulesYaml(rules) {
79
+ return rules.map((r) => yamlStringify(r)).join("\n---\n");
80
+ }
81
+
82
+ export {
83
+ toSnakeCase,
84
+ idlProgramName,
85
+ parseIdl,
86
+ buildConstraintParams,
87
+ generateRulesFromIdl,
88
+ renderRulesYaml
89
+ };