brainblast 0.7.1 → 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.
- package/dist/cli.js +395 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,11 @@ 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,
|
|
@@ -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
|
|
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);
|
|
@@ -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 =
|
|
607
|
+
var outDir = join3(targetDir, ".agent-research");
|
|
257
608
|
mkdirSync2(outDir, { recursive: true });
|
|
258
|
-
var reportPath =
|
|
609
|
+
var reportPath = join3(outDir, "report.json");
|
|
259
610
|
writeFileSync2(reportPath, JSON.stringify(report, null, 2));
|
|
260
|
-
var costMdPath =
|
|
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: ${
|
|
333
|
-
console.log(` 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:
|
|
711
|
-
const { join:
|
|
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(
|
|
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 =
|
|
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:
|
|
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(
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.7.
|
|
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": [
|