brainblast 0.7.0 → 0.7.2

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.
@@ -7,7 +7,7 @@ import {
7
7
  loadPack,
8
8
  resolveRules,
9
9
  walk
10
- } from "./chunk-CRYFCQYM.js";
10
+ } from "./chunk-IY52XKWL.js";
11
11
 
12
12
  // src/costAnalysis.ts
13
13
  import { Project, SyntaxKind } from "ts-morph";
@@ -913,6 +913,89 @@ function anchorIdlAccount(c, params) {
913
913
  };
914
914
  }
915
915
 
916
+ // src/checkers/anchorAccountMissingConstraint.ts
917
+ var DEFAULT_AUTHORITY_RE = /^(authority|admin|owner|payer|caller|signer|user|operator|manager|creator|deployer)$/i;
918
+ function anchorAccountMissingConstraint(c, p) {
919
+ const namePattern = p?.nameRegex ? new RegExp(p.nameRegex) : DEFAULT_AUTHORITY_RE;
920
+ const authorityFields = c.accountFields.filter((f) => namePattern.test(f.name));
921
+ if (authorityFields.length === 0) {
922
+ return {
923
+ result: "cant_tell",
924
+ detail: p?.absentDetail ?? `Handler '${c.fnName}' has no authority-named account fields matching the pattern; rule does not apply.`
925
+ };
926
+ }
927
+ const missing = authorityFields.filter(
928
+ (f) => f.typeName.includes("AccountInfo") && !f.attrText.includes("signer") && !f.typeName.includes("Signer<")
929
+ );
930
+ if (missing.length > 0) {
931
+ const names = missing.map((f) => `'${f.name}'`).join(", ");
932
+ return {
933
+ result: "fail",
934
+ detail: p?.failDetail ?? `Handler '${c.fnName}': authority-named field(s) ${names} use AccountInfo<'info> without a \`signer\` constraint. Anchor performs no signing check on AccountInfo \u2014 any key can be passed as the authority and privileged instructions will execute without signature validation. Fix: change the type to Signer<'info>, or add #[account(signer)] to the field's constraint.`
935
+ };
936
+ }
937
+ return {
938
+ result: "pass",
939
+ detail: p?.passDetail ?? `Handler '${c.fnName}': all authority-named fields use Signer<'info> or have a signer constraint.`
940
+ };
941
+ }
942
+
943
+ // src/checkers/anchorForbiddenAccountType.ts
944
+ var DEFAULT_FORBIDDEN = "UncheckedAccount";
945
+ function anchorForbiddenAccountType(c, p) {
946
+ const forbidden = p?.forbiddenType ?? DEFAULT_FORBIDDEN;
947
+ const flagged = c.accountFields.filter((f) => f.typeName.includes(forbidden));
948
+ if (flagged.length === 0) {
949
+ if (c.accountFields.length > 0) {
950
+ return {
951
+ result: "pass",
952
+ detail: p?.passDetail ?? `Handler '${c.fnName}' has no '${forbidden}' account fields.`
953
+ };
954
+ }
955
+ return {
956
+ result: "cant_tell",
957
+ detail: p?.absentDetail ?? `Handler '${c.fnName}' has no account fields to inspect; rule does not apply.`
958
+ };
959
+ }
960
+ const names = flagged.map((f) => `'${f.name}'`).join(", ");
961
+ return {
962
+ result: "fail",
963
+ detail: p?.failDetail ?? `Handler '${c.fnName}' uses ${forbidden}<'info> on account(s) ${names}. ${forbidden} performs no runtime validation \u2014 ownership, signer status, and data layout are entirely unchecked. Replace with a typed account: Account<'info, T> (program-owned data), Signer<'info> (must sign), SystemAccount<'info> (system-owned), or InterfaceAccount<'info, T> (Token-2022 compatible).`
964
+ };
965
+ }
966
+
967
+ // src/checkers/anchorBodyCallPattern.ts
968
+ function anchorBodyCallPattern(c, p) {
969
+ if (!p?.forbiddenPattern) {
970
+ return { result: "cant_tell", detail: "anchorBodyCallPattern: no forbiddenPattern param provided." };
971
+ }
972
+ const forbidden = new RegExp(p.forbiddenPattern);
973
+ const exempt = p?.exemptPattern ? new RegExp(p.exemptPattern) : null;
974
+ const hasForbidden = forbidden.test(c.fnBodyText);
975
+ if (!hasForbidden) {
976
+ if (c.fnBodyText.trim().length > 0) {
977
+ return {
978
+ result: "pass",
979
+ detail: p?.passDetail ?? `Handler '${c.fnName}' does not contain the forbidden pattern '${p.forbiddenPattern}'.`
980
+ };
981
+ }
982
+ return {
983
+ result: "cant_tell",
984
+ detail: p?.absentDetail ?? `Handler '${c.fnName}' has an empty body; rule does not apply.`
985
+ };
986
+ }
987
+ if (exempt && exempt.test(c.fnBodyText)) {
988
+ return {
989
+ result: "pass",
990
+ detail: p?.passDetail ?? `Handler '${c.fnName}' contains '${p.forbiddenPattern}' but also matches the exemption pattern \u2014 considered safe.`
991
+ };
992
+ }
993
+ return {
994
+ result: "fail",
995
+ detail: p?.failDetail ?? `Handler '${c.fnName}' body contains '${p.forbiddenPattern}', which is a known footgun pattern. Use the Anchor seeds + bump constraint on the Accounts struct instead of deriving PDAs at runtime.`
996
+ };
997
+ }
998
+
916
999
  // src/checkers/index.ts
917
1000
  var registry = {
918
1001
  "positional-arg-identity": positionalArgIdentity,
@@ -927,7 +1010,10 @@ var registry = {
927
1010
  "literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
928
1011
  "forbidden-call-replacement": forbiddenCallReplacement,
929
1012
  "solana-mint-identity-mismatch": solanaMintIdentity,
930
- "anchor-account-matches-idl": anchorIdlAccount
1013
+ "anchor-account-matches-idl": anchorIdlAccount,
1014
+ "anchor-account-missing-constraint": anchorAccountMissingConstraint,
1015
+ "anchor-forbidden-account-type": anchorForbiddenAccountType,
1016
+ "anchor-body-call-pattern": anchorBodyCallPattern
931
1017
  };
932
1018
  function runChecker(kind, c, params) {
933
1019
  const fn = registry[kind];
package/dist/cli.js CHANGED
@@ -4,14 +4,16 @@ import {
4
4
  applyDiffToFile,
5
5
  initPack,
6
6
  isTelemetryEnabled,
7
+ lamportsToSol,
7
8
  parseDiff,
8
9
  recordGraduationEvents,
9
10
  renderCostReportMd,
11
+ rentExemptMinimum,
10
12
  startWatch,
11
13
  submitTelemetry,
12
14
  telemetryFilePath,
13
15
  validatePack
14
- } from "./chunk-34VXOLJF.js";
16
+ } from "./chunk-B2M3TZSA.js";
15
17
  import {
16
18
  renderTrustGraphMd
17
19
  } from "./chunk-2UZGWXIX.js";
@@ -25,7 +27,7 @@ import {
25
27
  audit,
26
28
  getChangedRanges,
27
29
  resolveRules
28
- } from "./chunk-CRYFCQYM.js";
30
+ } from "./chunk-IY52XKWL.js";
29
31
  import "./chunk-2XJORJPQ.js";
30
32
  import "./chunk-O5Z4ZJHC.js";
31
33
  import "./chunk-XSVQSK53.js";
@@ -36,7 +38,7 @@ import "./chunk-3RG5ZIWI.js";
36
38
 
37
39
  // src/cli.ts
38
40
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
39
- import { join as join2 } from "path";
41
+ import { join as join3 } from "path";
40
42
 
41
43
  // src/memory.ts
42
44
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
@@ -109,6 +111,351 @@ function updateMemory(memory, checks2, now = /* @__PURE__ */ new Date()) {
109
111
  return { memory: { schemaVersion: "1.0", lastRun, fixHistory }, precedents };
110
112
  }
111
113
 
114
+ // src/deployPlan.ts
115
+ import { readFileSync as readFileSync2, readdirSync, statSync, existsSync as existsSync2 } from "fs";
116
+ import { join as join2 } from "path";
117
+ import { createRequire } from "module";
118
+ var PROGRAM_ACCOUNT_SIZE = 36;
119
+ var PROGRAMDATA_METADATA = 45;
120
+ var BUFFER_METADATA = 37;
121
+ var DEFAULT_MAX_LEN_MULTIPLIER = 2;
122
+ var WRITE_CHUNK_BYTES = 1012;
123
+ var BASE_TX_FEE_LAMPORTS = 5e3;
124
+ var _require = createRequire(import.meta.url);
125
+ var _parser = null;
126
+ function getParser() {
127
+ if (_parser) return _parser;
128
+ const Parser = _require("tree-sitter");
129
+ const Rust = _require("tree-sitter-rust");
130
+ _parser = new Parser();
131
+ _parser.setLanguage(Rust);
132
+ return _parser;
133
+ }
134
+ function walkRust(dir, out = []) {
135
+ for (const entry of readdirSync(dir)) {
136
+ if (entry === "node_modules" || entry === ".git" || entry === "target") continue;
137
+ const p = join2(dir, entry);
138
+ const st = statSync(p);
139
+ if (st.isDirectory()) walkRust(p, out);
140
+ else if (p.endsWith(".rs")) out.push(p);
141
+ }
142
+ return out;
143
+ }
144
+ function named(node) {
145
+ const out = [];
146
+ for (let i = 0; i < node.childCount; i++) {
147
+ const c = node.child(i);
148
+ if (c.isNamed) out.push(c);
149
+ }
150
+ return out;
151
+ }
152
+ function itemsWithAttrs(containerNode) {
153
+ const result = [];
154
+ let pending = [];
155
+ for (const kid of named(containerNode)) {
156
+ if (kid.type === "attribute_item") pending.push(kid.text);
157
+ else {
158
+ result.push({ attrs: pending, node: kid });
159
+ pending = [];
160
+ }
161
+ }
162
+ return result;
163
+ }
164
+ function evalSpaceExpr(expr) {
165
+ const tokens = expr.split("+").map((t) => t.trim());
166
+ let value = 0;
167
+ let literal = true;
168
+ for (const t of tokens) {
169
+ if (/^\d+$/.test(t)) value += parseInt(t, 10);
170
+ else if (t.length > 0) literal = false;
171
+ }
172
+ return { value, literal };
173
+ }
174
+ function attrValue(attrText, key) {
175
+ const re = new RegExp(`\\b${key}\\s*=\\s*`);
176
+ const m = re.exec(attrText);
177
+ if (!m) return null;
178
+ let i = m.index + m[0].length;
179
+ let depth = 0;
180
+ let out = "";
181
+ for (; i < attrText.length; i++) {
182
+ const ch = attrText[i];
183
+ if (ch === "[" || ch === "(") depth++;
184
+ else if (ch === "]" || ch === ")") {
185
+ if (depth === 0) break;
186
+ depth--;
187
+ } else if (ch === "," && depth === 0) break;
188
+ out += ch;
189
+ }
190
+ return out.trim() || null;
191
+ }
192
+ function parseInitAccounts(targetDir2) {
193
+ const parser = getParser();
194
+ const accounts = [];
195
+ for (const file of walkRust(targetDir2)) {
196
+ let src;
197
+ try {
198
+ src = readFileSync2(file, "utf8");
199
+ } catch {
200
+ continue;
201
+ }
202
+ if (!src.includes("#[derive(Accounts)]") && !src.includes("Accounts)]")) continue;
203
+ const tree = parser.parse(src);
204
+ const root = tree.rootNode;
205
+ const topPairs = itemsWithAttrs(root);
206
+ for (const { attrs, node } of topPairs) {
207
+ if (node.type !== "struct_item") continue;
208
+ const isAccounts = attrs.some((a) => a.includes("Accounts"));
209
+ if (!isAccounts) continue;
210
+ const nameNode = node.childForFieldName("name");
211
+ const structName = nameNode?.text ?? "<anonymous>";
212
+ const body = node.childForFieldName("body");
213
+ if (!body) continue;
214
+ for (const { attrs: fAttrs, node: fNode } of itemsWithAttrs(body)) {
215
+ if (fNode.type !== "field_declaration") continue;
216
+ const attrText = fAttrs.join("\n");
217
+ const hasInit = /\binit\b/.test(attrText) || /\binit_if_needed\b/.test(attrText);
218
+ if (!hasInit) continue;
219
+ const fieldName = fNode.childForFieldName("name")?.text ?? "?";
220
+ const typeName = fNode.childForFieldName("type")?.text ?? "?";
221
+ const spaceRaw = attrValue(attrText, "space");
222
+ let space = null;
223
+ let spaceExpr;
224
+ if (spaceRaw) {
225
+ const { value, literal } = evalSpaceExpr(spaceRaw);
226
+ if (literal) space = value;
227
+ else {
228
+ space = null;
229
+ spaceExpr = spaceRaw;
230
+ }
231
+ }
232
+ accounts.push({
233
+ name: fieldName,
234
+ struct: structName,
235
+ file,
236
+ line: fNode.startPosition.row + 1,
237
+ typeName,
238
+ space,
239
+ spaceExpr,
240
+ rentLamports: space === null ? null : rentExemptMinimum(space),
241
+ seeds: attrValue(attrText, "seeds"),
242
+ payer: attrValue(attrText, "payer"),
243
+ conditional: /\binit_if_needed\b/.test(attrText)
244
+ });
245
+ }
246
+ }
247
+ }
248
+ return accounts;
249
+ }
250
+ function findProgramBinary(targetDir2) {
251
+ const candidates = [
252
+ join2(targetDir2, "target", "deploy"),
253
+ join2(targetDir2, "target", "sbf-solana-solana", "release"),
254
+ join2(targetDir2, "target", "bpfel-unknown-unknown", "release")
255
+ ];
256
+ let best = null;
257
+ for (const dir of candidates) {
258
+ if (!existsSync2(dir)) continue;
259
+ for (const entry of readdirSync(dir)) {
260
+ if (!entry.endsWith(".so")) continue;
261
+ const p = join2(dir, entry);
262
+ const bytes = statSync(p).size;
263
+ if (!best || bytes > best.bytes) best = { path: p, bytes };
264
+ }
265
+ }
266
+ return best;
267
+ }
268
+ function buildDeployPlan(targetDir2, opts = {}) {
269
+ const maxLenMultiplier = opts.maxLenMultiplier ?? DEFAULT_MAX_LEN_MULTIPLIER;
270
+ const priorityMicroLamports = opts.priorityMicroLamports ?? 0;
271
+ const binary = opts.programLen != null ? null : findProgramBinary(targetDir2);
272
+ const programLen = opts.programLen ?? binary?.bytes ?? null;
273
+ const initAccounts = parseInitAccounts(targetDir2);
274
+ const unresolvedInit = initAccounts.filter((a) => a.rentLamports === null);
275
+ const initRentLamports = initAccounts.reduce((s, a) => s + (a.rentLamports ?? 0), 0);
276
+ const programAccountRent = programLen == null ? 0 : rentExemptMinimum(PROGRAM_ACCOUNT_SIZE);
277
+ const programDataRent = programLen == null ? 0 : rentExemptMinimum(PROGRAMDATA_METADATA + maxLenMultiplier * programLen);
278
+ const bufferRent = programLen == null ? 0 : rentExemptMinimum(BUFFER_METADATA + programLen);
279
+ const writeTxCount = programLen == null ? 0 : Math.ceil(programLen / WRITE_CHUNK_BYTES);
280
+ const initStructs = [...new Set(initAccounts.map((a) => a.struct))];
281
+ const baseTxCount = (programLen == null ? 0 : 2 + writeTxCount) + initStructs.length;
282
+ const txFeeLamports = baseTxCount * BASE_TX_FEE_LAMPORTS;
283
+ const steps = [];
284
+ let idx = 1;
285
+ if (programLen != null) {
286
+ steps.push({
287
+ index: idx++,
288
+ kind: "create-buffer",
289
+ label: "Create buffer account",
290
+ rentLamports: 0,
291
+ transientLamports: bufferRent,
292
+ feeLamports: BASE_TX_FEE_LAMPORTS,
293
+ detail: `Allocate a ${BUFFER_METADATA + programLen}-byte buffer (held by the upgradeable loader) and fund it with ${bufferRent.toLocaleString()} lamports of rent. Refunded to you when the buffer is drained at deploy time.`
294
+ });
295
+ steps.push({
296
+ index: idx++,
297
+ kind: "write",
298
+ label: `Write program bytes (${writeTxCount} transaction${writeTxCount === 1 ? "" : "s"})`,
299
+ rentLamports: 0,
300
+ transientLamports: 0,
301
+ feeLamports: writeTxCount * BASE_TX_FEE_LAMPORTS,
302
+ detail: `Stream the ${programLen.toLocaleString()}-byte program into the buffer in ~${WRITE_CHUNK_BYTES}-byte chunks. ${writeTxCount} write transaction${writeTxCount === 1 ? "" : "s"} at ${BASE_TX_FEE_LAMPORTS} lamports each.`
303
+ });
304
+ steps.push({
305
+ index: idx++,
306
+ kind: "deploy",
307
+ label: "Deploy program from buffer",
308
+ rentLamports: programAccountRent + programDataRent,
309
+ transientLamports: -bufferRent,
310
+ feeLamports: BASE_TX_FEE_LAMPORTS,
311
+ detail: `Create the program account (${PROGRAM_ACCOUNT_SIZE} B, rent ${programAccountRent.toLocaleString()}) and the programdata account (${(PROGRAMDATA_METADATA + maxLenMultiplier * programLen).toLocaleString()} B at ${maxLenMultiplier}\xD7 upgrade headroom, rent ${programDataRent.toLocaleString()}). The buffer's lamports roll into the programdata account.`
312
+ });
313
+ }
314
+ for (const struct of initStructs) {
315
+ const accts = initAccounts.filter((a) => a.struct === struct);
316
+ const rent = accts.reduce((s, a) => s + (a.rentLamports ?? 0), 0);
317
+ const names = accts.map((a) => a.name).join(", ");
318
+ const anyUnresolved = accts.some((a) => a.rentLamports === null);
319
+ steps.push({
320
+ index: idx++,
321
+ kind: "initialize",
322
+ label: `Initialize: ${struct}`,
323
+ rentLamports: rent,
324
+ transientLamports: 0,
325
+ feeLamports: BASE_TX_FEE_LAMPORTS,
326
+ detail: `Invoke the handler using \`Context<${struct}>\` to create ${accts.length} account(s): ${names}. Payer funds ${rent.toLocaleString()} lamports of rent` + (anyUnresolved ? " (plus unresolved-space accounts \u2014 see notes)." : ".")
327
+ });
328
+ }
329
+ const lockedLamports = programAccountRent + programDataRent + initRentLamports;
330
+ const walletRequiredLamports = lockedLamports + bufferRent + txFeeLamports;
331
+ return {
332
+ binary,
333
+ programLen,
334
+ maxLenMultiplier,
335
+ priorityMicroLamports,
336
+ programAccountRent,
337
+ programDataRent,
338
+ bufferRent,
339
+ writeTxCount,
340
+ txFeeLamports,
341
+ initAccounts,
342
+ initRentLamports,
343
+ unresolvedInit,
344
+ steps,
345
+ lockedLamports,
346
+ walletRequiredLamports,
347
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
348
+ };
349
+ }
350
+ function sol(lamports) {
351
+ return `${lamportsToSol(lamports)} SOL`;
352
+ }
353
+ function renderDeployPlanMd(p) {
354
+ const L = ["## Deployment Plan\n"];
355
+ if (p.programLen == null) {
356
+ L.push(
357
+ "\u26A0\uFE0F **No compiled `.so` found** under `target/deploy/`. Run `anchor build` (or `cargo build-sbf`) first for exact deploy cost. The transaction sequence below is structural; rent figures for the program binary are omitted.\n"
358
+ );
359
+ } else {
360
+ const srcNote = p.binary ? `\`${p.binary.path.split("/").slice(-1)[0]}\` (${p.programLen.toLocaleString()} bytes)` : `${p.programLen.toLocaleString()} bytes (provided)`;
361
+ L.push(`**Program binary:** ${srcNote}
362
+ `);
363
+ L.push("### How much SOL do I need?\n");
364
+ L.push("| Item | Size | Rent (lamports) | SOL | Recoverable? |");
365
+ L.push("|------|------|-----------------|-----|--------------|");
366
+ L.push(
367
+ `| Program account | ${PROGRAM_ACCOUNT_SIZE} B | ${p.programAccountRent.toLocaleString()} | ${lamportsToSol(p.programAccountRent)} | \u274C until program closed |`
368
+ );
369
+ L.push(
370
+ `| Program data (${p.maxLenMultiplier}\xD7 headroom) | ${(PROGRAMDATA_METADATA + p.maxLenMultiplier * p.programLen).toLocaleString()} B | ${p.programDataRent.toLocaleString()} | ${lamportsToSol(p.programDataRent)} | \u274C until program closed |`
371
+ );
372
+ L.push(
373
+ `| Buffer (transient) | ${(BUFFER_METADATA + p.programLen).toLocaleString()} B | ${p.bufferRent.toLocaleString()} | ${lamportsToSol(p.bufferRent)} | \u2705 refunded at deploy |`
374
+ );
375
+ if (p.initAccounts.length > 0) {
376
+ L.push(
377
+ `| Init accounts (${p.initAccounts.length}) | \u2014 | ${p.initRentLamports.toLocaleString()} | ${lamportsToSol(p.initRentLamports)} | depends on close logic |`
378
+ );
379
+ }
380
+ L.push(
381
+ `| Transaction fees (~${p.steps.reduce((s, x) => s + (x.feeLamports > 0 ? 1 : 0), 0)} steps) | \u2014 | ${p.txFeeLamports.toLocaleString()} | ${lamportsToSol(p.txFeeLamports)} | \u274C spent |`
382
+ );
383
+ L.push("");
384
+ L.push(
385
+ `**\u2192 Fund the deploying wallet with at least ${sol(p.walletRequiredLamports)}** (${p.walletRequiredLamports.toLocaleString()} lamports).`
386
+ );
387
+ L.push(
388
+ `Steady-state locked after deploy: **${sol(p.lockedLamports)}** (program + programdata + init rent). The buffer rent and fees are not part of the steady-state lockup.
389
+ `
390
+ );
391
+ if (p.priorityMicroLamports > 0) {
392
+ L.push(
393
+ `_Priority fee of ${p.priorityMicroLamports} \xB5lamports/CU requested \u2014 add it on top of the base fees above for congested-network safety._
394
+ `
395
+ );
396
+ }
397
+ }
398
+ L.push("### Exact transaction sequence\n");
399
+ for (const s of p.steps) {
400
+ const tags = [];
401
+ if (s.rentLamports > 0) tags.push(`locks ${sol(s.rentLamports)}`);
402
+ if (s.transientLamports > 0) tags.push(`transient ${sol(s.transientLamports)}`);
403
+ if (s.transientLamports < 0) tags.push(`refunds ${sol(-s.transientLamports)}`);
404
+ if (s.feeLamports > 0) tags.push(`fee ${sol(s.feeLamports)}`);
405
+ const tagStr = tags.length ? ` _(${tags.join(", ")})_` : "";
406
+ L.push(`${s.index}. **${s.label}**${tagStr}`);
407
+ L.push(` ${s.detail}`);
408
+ }
409
+ L.push("");
410
+ if (p.initAccounts.length > 0) {
411
+ L.push("### Init accounts (rent at setup)\n");
412
+ L.push("| Account | Struct | Type | Space | Rent | PDA seeds | Payer |");
413
+ L.push("|---------|--------|------|-------|------|-----------|-------|");
414
+ for (const a of p.initAccounts) {
415
+ const file = a.file.split("/").slice(-2).join("/");
416
+ const space = a.space != null ? `${a.space} B` : `\u26A0\uFE0F \`${a.spaceExpr ?? "?"}\``;
417
+ const rent = a.rentLamports != null ? lamportsToSol(a.rentLamports) + " SOL" : "\u2014";
418
+ const seeds = a.seeds ? `\`${a.seeds.replace(/\|/g, "\\|")}\`` : "(keypair)";
419
+ L.push(
420
+ `| \`${a.name}\`${a.conditional ? " (cond.)" : ""} (${file}:${a.line}) | ${a.struct} | \`${a.typeName.replace(/\|/g, "\\|")}\` | ${space} | ${rent} | ${seeds} | ${a.payer ?? "\u2014"} |`
421
+ );
422
+ }
423
+ L.push("");
424
+ if (p.unresolvedInit.length > 0) {
425
+ L.push(
426
+ `> \u26A0\uFE0F ${p.unresolvedInit.length} account(s) declare \`space\` via a non-literal expression (e.g. \`8 + State::INIT_SPACE\`). Their rent is excluded from the totals above \u2014 resolve the constant to get an exact figure.
427
+ `
428
+ );
429
+ }
430
+ }
431
+ return L.join("\n");
432
+ }
433
+ function renderDeployPlanText(p) {
434
+ const L = [];
435
+ L.push("\u2500\u2500 Deployment Plan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
436
+ if (p.programLen == null) {
437
+ L.push(" no compiled .so found \u2014 run `anchor build` for exact cost.");
438
+ L.push(" (showing structural transaction sequence only)");
439
+ } else {
440
+ L.push(` program binary: ${p.programLen.toLocaleString()} bytes`);
441
+ L.push(` program account: ${p.programAccountRent.toLocaleString()} lamports (${lamportsToSol(p.programAccountRent)} SOL)`);
442
+ L.push(` program data (${p.maxLenMultiplier}x): ${p.programDataRent.toLocaleString()} lamports (${lamportsToSol(p.programDataRent)} SOL)`);
443
+ L.push(` buffer (transient): ${p.bufferRent.toLocaleString()} lamports (${lamportsToSol(p.bufferRent)} SOL, refunded)`);
444
+ if (p.initAccounts.length > 0)
445
+ L.push(` init accounts: ${p.initRentLamports.toLocaleString()} lamports (${lamportsToSol(p.initRentLamports)} SOL)`);
446
+ L.push(` tx fees (est): ${p.txFeeLamports.toLocaleString()} lamports (${lamportsToSol(p.txFeeLamports)} SOL)`);
447
+ L.push(` \u2500\u2500\u2500 fund wallet with \u2265 ${lamportsToSol(p.walletRequiredLamports)} SOL (steady-state locked: ${lamportsToSol(p.lockedLamports)} SOL)`);
448
+ }
449
+ L.push(" sequence:");
450
+ for (const s of p.steps) {
451
+ const locks = s.rentLamports > 0 ? ` +${lamportsToSol(s.rentLamports)} SOL` : "";
452
+ L.push(` ${s.index}. ${s.label}${locks}`);
453
+ }
454
+ if (p.unresolvedInit.length > 0)
455
+ L.push(` note: ${p.unresolvedInit.length} init account(s) have non-literal space \u2014 excluded from totals.`);
456
+ return L.join("\n");
457
+ }
458
+
112
459
  // src/cli.ts
113
460
  import { execFileSync } from "child_process";
114
461
  var args = process.argv.slice(2);
@@ -128,7 +475,7 @@ if (args[0] === "drift") {
128
475
  process.exit(0);
129
476
  }
130
477
  if (args[0] === "mcp") {
131
- const { startMcpServer } = await import("./mcp-ML2X44WE.js");
478
+ const { startMcpServer } = await import("./mcp-AM7MTCSZ.js");
132
479
  await startMcpServer();
133
480
  process.exit(0);
134
481
  }
@@ -203,6 +550,10 @@ if (args[0] === "batch") {
203
550
  await runBatch(args.slice(1));
204
551
  process.exit(0);
205
552
  }
553
+ if (args[0] === "deploy-plan") {
554
+ runDeployPlan(args.slice(1));
555
+ process.exit(0);
556
+ }
206
557
  if (args[0] === "fix") {
207
558
  await runFix(args.slice(1));
208
559
  process.exit(0);
@@ -253,11 +604,11 @@ if (!changedRanges) {
253
604
  }
254
605
  var costReport = analyzeCosts(targetDir);
255
606
  report.costAnalysis = costReport;
256
- var outDir = join2(targetDir, ".agent-research");
607
+ var outDir = join3(targetDir, ".agent-research");
257
608
  mkdirSync2(outDir, { recursive: true });
258
- var reportPath = join2(outDir, "report.json");
609
+ var reportPath = join3(outDir, "report.json");
259
610
  writeFileSync2(reportPath, JSON.stringify(report, null, 2));
260
- var costMdPath = join2(outDir, "cost-analysis.md");
611
+ var costMdPath = join3(outDir, "cost-analysis.md");
261
612
  writeFileSync2(costMdPath, renderCostReportMd(costReport));
262
613
  console.log(`brainblast: scanned ${targetDir} with ${rules.length} rule(s)`);
263
614
  if (checks.length === 0) console.log(" (no catastrophic components detected)");
@@ -308,6 +659,38 @@ if (ci) {
308
659
  const gateFail = fails > 0 || strict && cantTell > 0;
309
660
  process.exit(gateFail ? 1 : 0);
310
661
  }
662
+ function runDeployPlan(argv) {
663
+ if (argv.includes("--help") || argv.includes("-h")) {
664
+ console.log("usage: brainblast deploy-plan [targetDir] [--json] [--max-len-mult N] [--program-len BYTES] [--priority-fee MICROLAMPORTS]");
665
+ console.log(" Estimate SOL needed to deploy an Anchor program and print the exact ordered");
666
+ console.log(" transaction sequence (create buffer \u2192 write \u2192 deploy \u2192 initialize PDAs).");
667
+ console.log(" Reads the compiled .so under target/deploy/; pass --program-len to model a");
668
+ console.log(" build you haven't compiled yet.");
669
+ process.exit(0);
670
+ }
671
+ const num = (name) => {
672
+ const idx = argv.indexOf(`--${name}`);
673
+ if (idx < 0) return void 0;
674
+ const v = parseInt(argv[idx + 1], 10);
675
+ return Number.isFinite(v) ? v : void 0;
676
+ };
677
+ const targetDir2 = argv.find((a, i) => !a.startsWith("--") && !/^\d+$/.test(a) && argv[i - 1] !== "--max-len-mult" && argv[i - 1] !== "--program-len" && argv[i - 1] !== "--priority-fee") ?? process.cwd();
678
+ const plan = buildDeployPlan(targetDir2, {
679
+ maxLenMultiplier: num("max-len-mult"),
680
+ programLen: num("program-len"),
681
+ priorityMicroLamports: num("priority-fee")
682
+ });
683
+ if (argv.includes("--json")) {
684
+ console.log(JSON.stringify(plan, null, 2));
685
+ return;
686
+ }
687
+ console.log(renderDeployPlanText(plan));
688
+ const outDir2 = join3(targetDir2, ".agent-research");
689
+ mkdirSync2(outDir2, { recursive: true });
690
+ const mdPath = join3(outDir2, "deploy-plan.md");
691
+ writeFileSync2(mdPath, renderDeployPlanMd(plan));
692
+ console.log(` deploy plan: ${mdPath}`);
693
+ }
311
694
  function runPack(argv) {
312
695
  const sub = argv[0];
313
696
  if (sub === "init") {
@@ -329,8 +712,8 @@ function runPack(argv) {
329
712
  description: flag("description")
330
713
  });
331
714
  console.log(`brainblast pack init: wrote ${manifestFile}`);
332
- console.log(` rules: ${join2(dir, "rules")}/`);
333
- console.log(` fixtures: ${join2(dir, "fixtures")}/`);
715
+ console.log(` rules: ${join3(dir, "rules")}/`);
716
+ console.log(` fixtures: ${join3(dir, "fixtures")}/`);
334
717
  return;
335
718
  }
336
719
  if (sub === "validate") {
@@ -707,8 +1090,8 @@ async function runFirewall(argv) {
707
1090
  }
708
1091
  }
709
1092
  async function runIdlRules(argv) {
710
- const { readFileSync: readFileSync2, writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("fs");
711
- const { join: join3 } = await import("path");
1093
+ const { readFileSync: readFileSync3, writeFileSync: writeFileSync3, mkdirSync: mkdirSync3 } = await import("fs");
1094
+ const { join: join4 } = await import("path");
712
1095
  const { parseIdl, generateRulesFromIdl, renderRulesYaml } = await import("./idlRules-3KZML4NL.js");
713
1096
  const idlPath = argv.find((a) => !a.startsWith("--"));
714
1097
  if (!idlPath) {
@@ -721,7 +1104,7 @@ async function runIdlRules(argv) {
721
1104
  const jsonOut = argv.includes("--json");
722
1105
  let idl;
723
1106
  try {
724
- idl = parseIdl(JSON.parse(readFileSync2(idlPath, "utf8")));
1107
+ idl = parseIdl(JSON.parse(readFileSync3(idlPath, "utf8")));
725
1108
  } catch (e) {
726
1109
  console.error(`brainblast idl-rules: ${e?.message ?? String(e)}`);
727
1110
  process.exit(2);
@@ -738,7 +1121,7 @@ async function runIdlRules(argv) {
738
1121
  const yaml = renderRulesYaml(rules2);
739
1122
  if (outDir2) {
740
1123
  mkdirSync3(outDir2, { recursive: true });
741
- const file = join3(outDir2, `${rules2[0].id}.yaml`);
1124
+ const file = join4(outDir2, `${rules2[0].id}.yaml`);
742
1125
  writeFileSync3(file, yaml);
743
1126
  console.log(`Generated ${rules2.length} rule(s) \u2192 ${file}`);
744
1127
  console.log(` Run against your program: npx brainblast <program-dir> --packs <pack-with-this-rule>`);
@@ -812,7 +1195,7 @@ async function runPumpCheck(argv) {
812
1195
  if (report2.verdict === "NO-GO") process.exit(1);
813
1196
  }
814
1197
  async function runBatch(argv) {
815
- const { readFileSync: readFileSync2 } = await import("fs");
1198
+ const { readFileSync: readFileSync3 } = await import("fs");
816
1199
  const { batchScan, parseMintList, renderBatchText } = await import("./batchScan-JR2G5JCF.js");
817
1200
  const file = argv.find((a) => !a.startsWith("--"));
818
1201
  if (!file) {
@@ -822,7 +1205,7 @@ async function runBatch(argv) {
822
1205
  }
823
1206
  let mints;
824
1207
  try {
825
- mints = parseMintList(readFileSync2(file, "utf8"));
1208
+ mints = parseMintList(readFileSync3(file, "utf8"));
826
1209
  } catch (e) {
827
1210
  console.error(`brainblast batch: ${e?.message ?? String(e)}`);
828
1211
  process.exit(2);
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  submitTelemetry,
55
55
  telemetryFilePath,
56
56
  validatePack
57
- } from "./chunk-34VXOLJF.js";
57
+ } from "./chunk-B2M3TZSA.js";
58
58
  import {
59
59
  renderTrustGraphMd
60
60
  } from "./chunk-2UZGWXIX.js";
@@ -91,7 +91,7 @@ import {
91
91
  runChecker,
92
92
  testKinds,
93
93
  validatePackManifest
94
- } from "./chunk-CRYFCQYM.js";
94
+ } from "./chunk-IY52XKWL.js";
95
95
  import {
96
96
  CANONICAL_BY_MINT,
97
97
  CANONICAL_MINTS,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  audit,
3
3
  resolveRules
4
- } from "./chunk-CRYFCQYM.js";
4
+ } from "./chunk-IY52XKWL.js";
5
5
  import "./chunk-2XJORJPQ.js";
6
6
  import "./chunk-O5Z4ZJHC.js";
7
7
  import {
@@ -0,0 +1,28 @@
1
+ id: anchor-pda-find-program-address
2
+ severity: high
3
+ title: Anchor handler calls find_program_address at runtime instead of using seeds+bump constraint
4
+ component:
5
+ name: Anchor Framework
6
+ type: Blockchain
7
+ version: ">=0.26.0"
8
+ sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#seeds-and-bump
9
+ detect:
10
+ lang: rust
11
+ modules:
12
+ - "@coral-xyz/anchor"
13
+ - "@project-serum/anchor"
14
+ nameRegex: ".*"
15
+ triggerCalls: []
16
+ check:
17
+ kind: anchor-body-call-pattern
18
+ params:
19
+ forbiddenPattern: "Pubkey::find_program_address|find_program_address\\s*\\("
20
+ test:
21
+ kind: anchor-program-test
22
+ params:
23
+ scenario: pda-canonical-bump-mismatch
24
+ description: >-
25
+ Initializes the PDA storing the canonical bump, then calls the handler with
26
+ a non-canonical bump seed. With find_program_address the handler may accept
27
+ this (re-deriving a different nonce), whereas the seeds+bump constraint
28
+ enforces the stored canonical bump and will reject it.
@@ -0,0 +1,27 @@
1
+ id: anchor-signer-constraint-missing
2
+ severity: critical
3
+ title: Anchor authority account missing signer constraint
4
+ component:
5
+ name: Anchor Framework
6
+ type: Blockchain
7
+ version: ">=0.26.0"
8
+ sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#signer
9
+ detect:
10
+ lang: rust
11
+ modules:
12
+ - "@coral-xyz/anchor"
13
+ - "@project-serum/anchor"
14
+ nameRegex: ".*"
15
+ triggerCalls: []
16
+ check:
17
+ kind: anchor-account-missing-constraint
18
+ params:
19
+ nameRegex: "^(authority|admin|owner|payer|caller|signer|user|operator|manager|creator|deployer)$"
20
+ test:
21
+ kind: anchor-program-test
22
+ params:
23
+ scenario: unauthorized-caller
24
+ description: >-
25
+ Calls the instruction with a key that does not match the expected authority
26
+ and has not signed the transaction. Must be rejected with a signing error.
27
+ If it succeeds, the authority check is absent and privilege escalation is possible.
@@ -0,0 +1,27 @@
1
+ id: anchor-unchecked-account-type
2
+ severity: high
3
+ title: Anchor instruction uses UncheckedAccount — no runtime validation
4
+ component:
5
+ name: Anchor Framework
6
+ type: Blockchain
7
+ version: ">=0.26.0"
8
+ sourceUrl: https://www.anchor-lang.com/docs/the-accounts-struct#uncheckedaccount
9
+ detect:
10
+ lang: rust
11
+ modules:
12
+ - "@coral-xyz/anchor"
13
+ - "@project-serum/anchor"
14
+ nameRegex: ".*"
15
+ triggerCalls: []
16
+ check:
17
+ kind: anchor-forbidden-account-type
18
+ params:
19
+ forbiddenType: "UncheckedAccount"
20
+ test:
21
+ kind: anchor-program-test
22
+ params:
23
+ scenario: arbitrary-account-substitution
24
+ description: >-
25
+ Passes an account owned by an arbitrary program (not the expected program ID)
26
+ as the UncheckedAccount field. Must be rejected. If it succeeds, an attacker
27
+ can substitute any account and the instruction will operate on attacker-controlled data.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
6
6
  "keywords": [