brainblast 0.6.3 → 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.
- package/dist/batchScan-JR2G5JCF.js +14 -0
- package/dist/chunk-2UZGWXIX.js +77 -0
- package/dist/chunk-2XJORJPQ.js +31 -0
- package/dist/{chunk-ZZ6LBZV5.js → chunk-34VXOLJF.js} +32 -433
- package/dist/{chunk-A56IF3UX.js → chunk-CRYFCQYM.js} +145 -19
- package/dist/chunk-DQ4KAYKQ.js +111 -0
- package/dist/chunk-FQA5BYWW.js +89 -0
- package/dist/chunk-HL7NVANZ.js +331 -0
- package/dist/chunk-O5Z4ZJHC.js +89 -0
- package/dist/chunk-QC27GNQ7.js +101 -0
- package/dist/chunk-SVSVVW6U.js +187 -0
- package/dist/chunk-UWE6HAGS.js +176 -0
- package/dist/chunk-VG5FMOLW.js +61 -0
- package/dist/chunk-VI2JBH2T.js +79 -0
- package/dist/chunk-WX3IR7LK.js +148 -0
- package/dist/chunk-XSVQSK53.js +100 -0
- package/dist/cli.js +321 -10
- package/dist/firewall-HN5XJLGC.js +18 -0
- package/dist/idlRules-3KZML4NL.js +17 -0
- package/dist/index.d.ts +307 -1
- package/dist/index.js +115 -22
- package/dist/{mcp-AFYJQ7K6.js → mcp-ML2X44WE.js} +3 -1
- package/dist/pumpCheck-K2ESOBNU.js +16 -0
- package/dist/ricomaps-WTMWBBOY.js +11 -0
- package/dist/rpc-W5F4KXS2.js +18 -0
- package/dist/rules/solana-token-impersonation.yaml +17 -0
- package/dist/score-VLKER37D.js +18 -0
- package/dist/tokenRegistry-CYIUZHAZ.js +8 -0
- package/dist/trustGraph-4SSJOQKT.js +49 -0
- package/dist/watchChain-F6INXAPA.js +13 -0
- package/package.json +2 -1
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeToken
|
|
3
|
+
} from "./chunk-FQA5BYWW.js";
|
|
4
|
+
import {
|
|
5
|
+
verifyTokenIdentity
|
|
6
|
+
} from "./chunk-VI2JBH2T.js";
|
|
7
|
+
|
|
8
|
+
// src/batchScan.ts
|
|
9
|
+
async function mapPool(items, concurrency, fn) {
|
|
10
|
+
const results = new Array(items.length);
|
|
11
|
+
let next = 0;
|
|
12
|
+
const workers = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) }, async () => {
|
|
13
|
+
for (; ; ) {
|
|
14
|
+
const idx = next++;
|
|
15
|
+
if (idx >= items.length) break;
|
|
16
|
+
results[idx] = await fn(items[idx], idx);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
await Promise.all(workers);
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
async function scanOne(mint, opts) {
|
|
23
|
+
const failOn = opts.failOnRisk ?? 70;
|
|
24
|
+
try {
|
|
25
|
+
const identity = await verifyTokenIdentity(mint, { baseUrl: opts.jupBaseUrl, offline: opts.offline });
|
|
26
|
+
const row = {
|
|
27
|
+
mint,
|
|
28
|
+
identityStatus: identity.status,
|
|
29
|
+
impersonation: identity.impersonation,
|
|
30
|
+
symbol: identity.symbol,
|
|
31
|
+
rank: 0
|
|
32
|
+
};
|
|
33
|
+
if (!opts.offline) {
|
|
34
|
+
const outcome = await analyzeToken(mint, { apiKey: opts.apiKey, baseUrl: opts.ricoBaseUrl });
|
|
35
|
+
if (outcome.ok) {
|
|
36
|
+
const q = outcome.result;
|
|
37
|
+
row.riskScore = q.riskScore;
|
|
38
|
+
row.snipers = q.snipersDetected;
|
|
39
|
+
row.bundleClusters = q.bundleClustersDetected;
|
|
40
|
+
row.deployerFlags = q.deployerFlags;
|
|
41
|
+
if (!row.symbol) row.symbol = q.symbol;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
row.rank = (row.impersonation ? 1e3 : 0) + (row.riskScore ?? -1);
|
|
45
|
+
return row;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { mint, identityStatus: "error", impersonation: false, error: e?.message ?? String(e), rank: -2 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function batchScan(mints, opts = {}) {
|
|
51
|
+
const failOn = opts.failOnRisk ?? 70;
|
|
52
|
+
const unique = [...new Set(mints.map((m) => m.trim()).filter(Boolean))];
|
|
53
|
+
const rows = await mapPool(unique, opts.concurrency ?? 5, (m) => scanOne(m, opts));
|
|
54
|
+
rows.sort((a, b) => b.rank - a.rank);
|
|
55
|
+
const summary = {
|
|
56
|
+
total: rows.length,
|
|
57
|
+
impersonators: rows.filter((r) => r.impersonation).length,
|
|
58
|
+
highRisk: rows.filter((r) => (r.riskScore ?? -1) >= failOn).length,
|
|
59
|
+
errored: rows.filter((r) => r.identityStatus === "error").length
|
|
60
|
+
};
|
|
61
|
+
return { rows, summary };
|
|
62
|
+
}
|
|
63
|
+
function parseMintList(content) {
|
|
64
|
+
const trimmed = content.trim();
|
|
65
|
+
if (trimmed.startsWith("[")) {
|
|
66
|
+
const arr = JSON.parse(trimmed);
|
|
67
|
+
if (!Array.isArray(arr)) throw new Error("JSON mint list must be an array of strings");
|
|
68
|
+
return arr.map((x) => String(x));
|
|
69
|
+
}
|
|
70
|
+
return trimmed.split(/\r?\n/).map((l) => l.replace(/#.*$/, "").trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
function pad(s, n) {
|
|
73
|
+
return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
|
|
74
|
+
}
|
|
75
|
+
function renderBatchText(result) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
lines.push(`Batch token scan \u2014 ${result.summary.total} token(s)`);
|
|
78
|
+
lines.push(
|
|
79
|
+
` ${result.summary.impersonators} impersonator(s), ${result.summary.highRisk} high-risk, ${result.summary.errored} error(s)`
|
|
80
|
+
);
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(` ${pad("MINT", 14)} ${pad("SYMBOL", 8)} ${pad("IDENTITY", 18)} ${pad("RISK", 5)} FLAGS`);
|
|
83
|
+
for (const r of result.rows) {
|
|
84
|
+
const mintShort = r.mint.length > 12 ? r.mint.slice(0, 6) + ".." + r.mint.slice(-4) : r.mint;
|
|
85
|
+
const flags = [];
|
|
86
|
+
if (r.impersonation) flags.push("IMPERSONATION");
|
|
87
|
+
if (r.snipers) flags.push("snipers");
|
|
88
|
+
if (r.bundleClusters) flags.push("bundle");
|
|
89
|
+
if (r.error) flags.push(`error:${r.error}`);
|
|
90
|
+
lines.push(
|
|
91
|
+
` ${pad(mintShort, 14)} ${pad(r.symbol ?? "-", 8)} ${pad(r.identityStatus, 18)} ${pad(r.riskScore != null ? String(r.riskScore) : "-", 5)} ${flags.join(", ")}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
batchScan,
|
|
99
|
+
parseMintList,
|
|
100
|
+
renderBatchText
|
|
101
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
probeUpgradeAuthority
|
|
3
|
+
} from "./chunk-XSVQSK53.js";
|
|
4
|
+
|
|
5
|
+
// src/trustGraph/directory.ts
|
|
6
|
+
import { readFileSync, existsSync } from "fs";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { parse } from "yaml";
|
|
10
|
+
var cache = null;
|
|
11
|
+
function bundledPath() {
|
|
12
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
13
|
+
const candidates = [
|
|
14
|
+
join(here, "programs", "directory.yaml"),
|
|
15
|
+
// dist/programs/directory.yaml
|
|
16
|
+
join(here, "..", "..", "programs", "directory.yaml"),
|
|
17
|
+
// src/../../programs/
|
|
18
|
+
join(here, "..", "programs", "directory.yaml")
|
|
19
|
+
// fallback
|
|
20
|
+
];
|
|
21
|
+
for (const c of candidates) {
|
|
22
|
+
if (existsSync(c)) return c;
|
|
23
|
+
}
|
|
24
|
+
return candidates[0];
|
|
25
|
+
}
|
|
26
|
+
function loadDirectory(path = bundledPath()) {
|
|
27
|
+
if (cache && path === bundledPath()) return cache;
|
|
28
|
+
const raw = parse(readFileSync(path, "utf8"));
|
|
29
|
+
if (!raw || !Array.isArray(raw.programs)) {
|
|
30
|
+
throw new Error(`invalid program directory at ${path}: missing 'programs' array`);
|
|
31
|
+
}
|
|
32
|
+
const m = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const p of raw.programs) {
|
|
34
|
+
if (!p.programId || !p.name) throw new Error(`directory entry missing programId/name: ${JSON.stringify(p)}`);
|
|
35
|
+
if (m.has(p.programId)) throw new Error(`directory has duplicate programId ${p.programId}`);
|
|
36
|
+
m.set(p.programId, { ...p, provenance: { ...p.provenance ?? {}, directoryFile: path } });
|
|
37
|
+
}
|
|
38
|
+
if (path === bundledPath()) cache = m;
|
|
39
|
+
return m;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/trustGraph/programCache.ts
|
|
43
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
44
|
+
import { join as join2, dirname } from "path";
|
|
45
|
+
import { homedir } from "os";
|
|
46
|
+
var DEFAULT_TTL_HOURS = 168;
|
|
47
|
+
var SCHEMA_VERSION = "1.0";
|
|
48
|
+
function defaultCachePath() {
|
|
49
|
+
const envOverride = process.env["BRAINBLAST_CACHE_PATH"];
|
|
50
|
+
return envOverride ?? join2(homedir(), ".brainblast", "program-cache.json");
|
|
51
|
+
}
|
|
52
|
+
function emptyCache() {
|
|
53
|
+
return { schemaVersion: SCHEMA_VERSION, entries: {} };
|
|
54
|
+
}
|
|
55
|
+
function loadProgramCache(cachePath) {
|
|
56
|
+
const path = cachePath ?? defaultCachePath();
|
|
57
|
+
if (!existsSync2(path)) return emptyCache();
|
|
58
|
+
try {
|
|
59
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
60
|
+
if (raw?.schemaVersion !== SCHEMA_VERSION) {
|
|
61
|
+
return emptyCache();
|
|
62
|
+
}
|
|
63
|
+
if (!raw.entries || typeof raw.entries !== "object") return emptyCache();
|
|
64
|
+
return { schemaVersion: SCHEMA_VERSION, entries: raw.entries };
|
|
65
|
+
} catch {
|
|
66
|
+
return emptyCache();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function saveProgramCache(cache2, cachePath) {
|
|
70
|
+
const path = cachePath ?? defaultCachePath();
|
|
71
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
72
|
+
writeFileSync(path, JSON.stringify(cache2, null, 2), "utf8");
|
|
73
|
+
}
|
|
74
|
+
function getCacheEntry(cache2, programId, ttlHoursOverride) {
|
|
75
|
+
const entry = cache2.entries[programId];
|
|
76
|
+
if (!entry) return null;
|
|
77
|
+
if (isEntryExpired(entry, ttlHoursOverride)) return null;
|
|
78
|
+
return entry.program;
|
|
79
|
+
}
|
|
80
|
+
function putCacheEntry(cache2, programId, program, sourceRun, ttlHours = DEFAULT_TTL_HOURS) {
|
|
81
|
+
cache2.entries[programId] = {
|
|
82
|
+
program,
|
|
83
|
+
cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
84
|
+
sourceRun,
|
|
85
|
+
ttlHours
|
|
86
|
+
};
|
|
87
|
+
return cache2;
|
|
88
|
+
}
|
|
89
|
+
function getCacheEntryMeta(cache2, programId) {
|
|
90
|
+
return cache2.entries[programId] ?? null;
|
|
91
|
+
}
|
|
92
|
+
function isEntryExpired(entry, ttlHoursOverride) {
|
|
93
|
+
const ttl = ttlHoursOverride ?? entry.ttlHours ?? DEFAULT_TTL_HOURS;
|
|
94
|
+
if (ttl <= 0) return true;
|
|
95
|
+
const cachedMs = Date.parse(entry.cachedAt);
|
|
96
|
+
if (Number.isNaN(cachedMs)) return true;
|
|
97
|
+
const ageMs = Date.now() - cachedMs;
|
|
98
|
+
return ageMs >= ttl * 36e5;
|
|
99
|
+
}
|
|
100
|
+
function cacheSize(cache2, ttlHoursOverride) {
|
|
101
|
+
return Object.values(cache2.entries).filter((e) => !isEntryExpired(e, ttlHoursOverride)).length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/trustGraph/build.ts
|
|
105
|
+
async function buildTrustGraph(programIds, opts = {}) {
|
|
106
|
+
const dir = loadDirectory(opts.directoryPath);
|
|
107
|
+
const programs = [];
|
|
108
|
+
const unresolved = [];
|
|
109
|
+
const cacheEnabled = opts.cachePath !== null;
|
|
110
|
+
const cachePathArg = opts.cachePath === null ? void 0 : opts.cachePath;
|
|
111
|
+
const cache2 = cacheEnabled ? loadProgramCache(cachePathArg) : null;
|
|
112
|
+
const newFromRpc = [];
|
|
113
|
+
const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
115
|
+
const ordered = programIds.filter((id) => seen.has(id) ? false : (seen.add(id), true));
|
|
116
|
+
for (const id of ordered) {
|
|
117
|
+
const directoryHit = dir.get(id);
|
|
118
|
+
if (directoryHit) {
|
|
119
|
+
programs.push(directoryHit);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (cache2) {
|
|
123
|
+
const cached = getCacheEntry(cache2, id);
|
|
124
|
+
if (cached) {
|
|
125
|
+
const meta = getCacheEntryMeta(cache2, id);
|
|
126
|
+
programs.push({
|
|
127
|
+
...cached,
|
|
128
|
+
provenance: {
|
|
129
|
+
...cached.provenance ?? {},
|
|
130
|
+
notes: [
|
|
131
|
+
cached.provenance?.notes,
|
|
132
|
+
`cache-hit: cachedAt=${meta.cachedAt} sourceRun=${meta.sourceRun}`
|
|
133
|
+
].filter(Boolean).join("; ")
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (opts.probeRpc === false) {
|
|
140
|
+
unresolved.push({
|
|
141
|
+
programId: id,
|
|
142
|
+
reason: "not_in_directory_or_cache_and_rpc_disabled"
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
let authority;
|
|
147
|
+
try {
|
|
148
|
+
authority = await probeUpgradeAuthority(id, opts);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
unresolved.push({ programId: id, reason: `rpc_error: ${e?.message ?? String(e)}` });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const probed = {
|
|
154
|
+
programId: id,
|
|
155
|
+
name: `Unknown program (${id.slice(0, 8)}\u2026)`,
|
|
156
|
+
kind: "app",
|
|
157
|
+
upgradeAuthority: authority,
|
|
158
|
+
verifiedBuild: { state: "unknown" },
|
|
159
|
+
audits: [],
|
|
160
|
+
parity: { mainnet: "unknown", devnet: "unknown" },
|
|
161
|
+
provenance: { rpcUrl: opts.rpcUrl, notes: "live-probed; not in curated directory" }
|
|
162
|
+
};
|
|
163
|
+
programs.push(probed);
|
|
164
|
+
newFromRpc.push(id);
|
|
165
|
+
if (cache2) {
|
|
166
|
+
putCacheEntry(cache2, id, probed, runId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (cache2 && newFromRpc.length > 0) {
|
|
170
|
+
saveProgramCache(cache2, cachePathArg);
|
|
171
|
+
}
|
|
172
|
+
return { programs, unresolved, generatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
loadDirectory,
|
|
177
|
+
DEFAULT_TTL_HOURS,
|
|
178
|
+
defaultCachePath,
|
|
179
|
+
loadProgramCache,
|
|
180
|
+
saveProgramCache,
|
|
181
|
+
getCacheEntry,
|
|
182
|
+
putCacheEntry,
|
|
183
|
+
getCacheEntryMeta,
|
|
184
|
+
isEntryExpired,
|
|
185
|
+
cacheSize,
|
|
186
|
+
buildTrustGraph
|
|
187
|
+
};
|