brainblast 0.6.4 → 0.7.1
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-HXQNNGSC.js → chunk-B2M3TZSA.js} +32 -433
- package/dist/chunk-DQ4KAYKQ.js +111 -0
- package/dist/chunk-HL7NVANZ.js +331 -0
- package/dist/{chunk-2Y6UILTZ.js → chunk-IY52XKWL.js} +149 -1
- 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-WX3IR7LK.js +148 -0
- package/dist/chunk-XSVQSK53.js +100 -0
- package/dist/cli.js +238 -9
- package/dist/firewall-HN5XJLGC.js +18 -0
- package/dist/idlRules-3KZML4NL.js +17 -0
- package/dist/index.d.ts +238 -1
- package/dist/index.js +105 -34
- package/dist/{mcp-FCKMS2MQ.js → mcp-AM7MTCSZ.js} +3 -2
- package/dist/pumpCheck-K2ESOBNU.js +16 -0
- package/dist/rpc-W5F4KXS2.js +18 -0
- package/dist/rules/anchor-pda-find-program-address.yaml +28 -0
- package/dist/rules/anchor-signer-constraint-missing.yaml +27 -0
- package/dist/rules/anchor-unchecked-account-type.yaml +27 -0
- package/dist/score-VLKER37D.js +18 -0
- package/dist/trustGraph-4SSJOQKT.js +49 -0
- package/dist/watchChain-F6INXAPA.js +13 -0
- package/package.json +2 -1
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildTrustGraph
|
|
3
|
+
} from "./chunk-SVSVVW6U.js";
|
|
4
|
+
|
|
5
|
+
// src/score.ts
|
|
6
|
+
var W_AUTHORITY = 35;
|
|
7
|
+
var W_VERIFIED = 25;
|
|
8
|
+
var W_AUDITS = 20;
|
|
9
|
+
var W_CURATION = 10;
|
|
10
|
+
var W_PARITY = 10;
|
|
11
|
+
function scoreAuthority(ua) {
|
|
12
|
+
let points;
|
|
13
|
+
let detail;
|
|
14
|
+
switch (ua.kind) {
|
|
15
|
+
case "renounced":
|
|
16
|
+
points = W_AUTHORITY;
|
|
17
|
+
detail = "Upgrade authority renounced \u2014 program code is frozen forever.";
|
|
18
|
+
break;
|
|
19
|
+
case "dao":
|
|
20
|
+
points = 30;
|
|
21
|
+
detail = "Upgrade authority held by a governance program (DAO).";
|
|
22
|
+
break;
|
|
23
|
+
case "multisig":
|
|
24
|
+
points = 26;
|
|
25
|
+
detail = "Upgrade authority held by a multisig.";
|
|
26
|
+
break;
|
|
27
|
+
case "unknown":
|
|
28
|
+
if (ua.address) {
|
|
29
|
+
points = 12;
|
|
30
|
+
detail = `Upgrade authority present but unclassified (${ua.address}). Could be a single hot key.`;
|
|
31
|
+
} else {
|
|
32
|
+
points = 10;
|
|
33
|
+
detail = "Upgrade authority could not be determined.";
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
case "single-key":
|
|
37
|
+
default:
|
|
38
|
+
points = 8;
|
|
39
|
+
detail = "Upgrade authority is a single key \u2014 it can replace the program's code at any time.";
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return { name: "Upgrade authority", weight: W_AUTHORITY, points, detail };
|
|
43
|
+
}
|
|
44
|
+
function scoreVerified(vb) {
|
|
45
|
+
let points;
|
|
46
|
+
let detail;
|
|
47
|
+
switch (vb.state) {
|
|
48
|
+
case "verified":
|
|
49
|
+
points = W_VERIFIED;
|
|
50
|
+
detail = `On-chain bytecode matches a published source build (${vb.registryUrl}).`;
|
|
51
|
+
break;
|
|
52
|
+
case "unverified":
|
|
53
|
+
points = 2;
|
|
54
|
+
detail = "Not present in any verified-build registry we trust.";
|
|
55
|
+
break;
|
|
56
|
+
case "unknown":
|
|
57
|
+
default:
|
|
58
|
+
points = 8;
|
|
59
|
+
detail = "Verified-build status not checked.";
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
return { name: "Verified build", weight: W_VERIFIED, points, detail };
|
|
63
|
+
}
|
|
64
|
+
function scoreAudits(audits) {
|
|
65
|
+
let points = 0;
|
|
66
|
+
let detail = "No audits on record.";
|
|
67
|
+
if (audits.length >= 2) {
|
|
68
|
+
points = W_AUDITS;
|
|
69
|
+
detail = `${audits.length} audits on record (${audits.map((a) => a.firm).join(", ")}).`;
|
|
70
|
+
} else if (audits.length === 1) {
|
|
71
|
+
points = 16;
|
|
72
|
+
detail = `One audit on record (${audits[0].firm}, ${audits[0].date}).`;
|
|
73
|
+
}
|
|
74
|
+
return { name: "Audits", weight: W_AUDITS, points, detail };
|
|
75
|
+
}
|
|
76
|
+
function scoreCuration(program) {
|
|
77
|
+
const curated = program.upgradeAuthority.source === "directory" || !!program.provenance?.directoryFile;
|
|
78
|
+
return {
|
|
79
|
+
name: "Curation",
|
|
80
|
+
weight: W_CURATION,
|
|
81
|
+
points: curated ? W_CURATION : 0,
|
|
82
|
+
detail: curated ? "Listed in the curated program directory (known entity)." : "Not in the curated directory \u2014 identity is not independently corroborated."
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function scoreParity(parity) {
|
|
86
|
+
let points;
|
|
87
|
+
let detail;
|
|
88
|
+
switch (parity.mainnet) {
|
|
89
|
+
case "present":
|
|
90
|
+
points = W_PARITY;
|
|
91
|
+
detail = "Deployed on mainnet as expected.";
|
|
92
|
+
break;
|
|
93
|
+
case "unknown":
|
|
94
|
+
points = 5;
|
|
95
|
+
detail = "Cross-cluster parity not checked.";
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
points = 0;
|
|
99
|
+
detail = `Mainnet deployment is '${parity.mainnet}' \u2014 possible devnet-only or address mismatch.`;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return { name: "Cluster parity", weight: W_PARITY, points, detail };
|
|
103
|
+
}
|
|
104
|
+
function gradeForScore(score) {
|
|
105
|
+
if (score >= 90) return "A";
|
|
106
|
+
if (score >= 75) return "B";
|
|
107
|
+
if (score >= 60) return "C";
|
|
108
|
+
if (score >= 40) return "D";
|
|
109
|
+
return "F";
|
|
110
|
+
}
|
|
111
|
+
function scoreFromProgram(program) {
|
|
112
|
+
const factors = [
|
|
113
|
+
scoreAuthority(program.upgradeAuthority),
|
|
114
|
+
scoreVerified(program.verifiedBuild),
|
|
115
|
+
scoreAudits(program.audits ?? []),
|
|
116
|
+
scoreCuration(program),
|
|
117
|
+
scoreParity(program.parity ?? { mainnet: "unknown", devnet: "unknown" })
|
|
118
|
+
];
|
|
119
|
+
const score = factors.reduce((s, f) => s + f.points, 0);
|
|
120
|
+
const grade = gradeForScore(score);
|
|
121
|
+
return {
|
|
122
|
+
programId: program.programId,
|
|
123
|
+
resolved: true,
|
|
124
|
+
score,
|
|
125
|
+
grade,
|
|
126
|
+
factors,
|
|
127
|
+
program,
|
|
128
|
+
summary: `${program.name} \u2014 grade ${grade} (${score}/100). ` + factors.find((f) => f.name === "Upgrade authority").detail
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function scoreProgram(programId, opts = {}) {
|
|
132
|
+
const graph = await buildTrustGraph([programId], opts);
|
|
133
|
+
const program = graph.programs.find((p) => p.programId === programId);
|
|
134
|
+
if (!program) {
|
|
135
|
+
const reason = graph.unresolved.find((u) => u.programId === programId)?.reason ?? "not resolved";
|
|
136
|
+
return {
|
|
137
|
+
programId,
|
|
138
|
+
resolved: false,
|
|
139
|
+
score: null,
|
|
140
|
+
grade: "unrated",
|
|
141
|
+
factors: [],
|
|
142
|
+
summary: `Could not resolve program ${programId}: ${reason}`,
|
|
143
|
+
unresolvedReason: reason
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return scoreFromProgram(program);
|
|
147
|
+
}
|
|
148
|
+
function renderScoreText(s) {
|
|
149
|
+
if (!s.resolved) {
|
|
150
|
+
return `Trust score [unrated]
|
|
151
|
+
${s.summary}`;
|
|
152
|
+
}
|
|
153
|
+
const lines = [];
|
|
154
|
+
lines.push(`Trust score [${s.grade}] ${s.score}/100`);
|
|
155
|
+
lines.push(` Program: ${s.program?.name ?? s.programId}`);
|
|
156
|
+
lines.push(` Address: ${s.programId}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(" Factors:");
|
|
159
|
+
for (const f of s.factors) {
|
|
160
|
+
lines.push(` ${f.points}/${f.weight} ${f.name} \u2014 ${f.detail}`);
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
function gradeAtLeast(grade, min) {
|
|
165
|
+
const order = { F: 0, D: 1, C: 2, B: 3, A: 4 };
|
|
166
|
+
if (grade === "unrated") return false;
|
|
167
|
+
return order[grade] >= order[min];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export {
|
|
171
|
+
gradeForScore,
|
|
172
|
+
scoreFromProgram,
|
|
173
|
+
scoreProgram,
|
|
174
|
+
renderScoreText,
|
|
175
|
+
gradeAtLeast
|
|
176
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/trustGraph/base58.ts
|
|
2
|
+
var ALPHA = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
3
|
+
var MAP = {};
|
|
4
|
+
for (let i = 0; i < ALPHA.length; i++) MAP[ALPHA[i]] = i;
|
|
5
|
+
function base58Encode(bytes) {
|
|
6
|
+
let zeros = 0;
|
|
7
|
+
while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
|
|
8
|
+
const buf = Array.from(bytes);
|
|
9
|
+
const out = [];
|
|
10
|
+
let start = zeros;
|
|
11
|
+
while (start < buf.length) {
|
|
12
|
+
let rem = 0;
|
|
13
|
+
for (let i = start; i < buf.length; i++) {
|
|
14
|
+
const acc = rem * 256 + buf[i];
|
|
15
|
+
buf[i] = Math.floor(acc / 58);
|
|
16
|
+
rem = acc % 58;
|
|
17
|
+
}
|
|
18
|
+
out.push(rem);
|
|
19
|
+
if (buf[start] === 0) start++;
|
|
20
|
+
}
|
|
21
|
+
let s = "";
|
|
22
|
+
for (let i = 0; i < zeros; i++) s += "1";
|
|
23
|
+
for (let i = out.length - 1; i >= 0; i--) s += ALPHA[out[i]];
|
|
24
|
+
return s;
|
|
25
|
+
}
|
|
26
|
+
function base58Decode(s) {
|
|
27
|
+
let zeros = 0;
|
|
28
|
+
while (zeros < s.length && s[zeros] === "1") zeros++;
|
|
29
|
+
const buf = [];
|
|
30
|
+
for (let i = zeros; i < s.length; i++) {
|
|
31
|
+
const v = MAP[s[i]];
|
|
32
|
+
if (v === void 0) throw new Error(`base58: invalid char '${s[i]}' at ${i}`);
|
|
33
|
+
let carry = v;
|
|
34
|
+
for (let j = 0; j < buf.length; j++) {
|
|
35
|
+
const acc = buf[j] * 58 + carry;
|
|
36
|
+
buf[j] = acc & 255;
|
|
37
|
+
carry = acc >>> 8;
|
|
38
|
+
}
|
|
39
|
+
while (carry > 0) {
|
|
40
|
+
buf.push(carry & 255);
|
|
41
|
+
carry >>>= 8;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const out = new Uint8Array(zeros + buf.length);
|
|
45
|
+
for (let i = 0; i < buf.length; i++) out[zeros + buf.length - 1 - i] = buf[i];
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function isValidSolanaAddress(s) {
|
|
49
|
+
if (typeof s !== "string" || s.length < 32 || s.length > 44) return false;
|
|
50
|
+
try {
|
|
51
|
+
return base58Decode(s).length === 32;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
base58Encode,
|
|
59
|
+
base58Decode,
|
|
60
|
+
isValidSolanaAddress
|
|
61
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeToken
|
|
3
|
+
} from "./chunk-FQA5BYWW.js";
|
|
4
|
+
import {
|
|
5
|
+
verifyTokenIdentity
|
|
6
|
+
} from "./chunk-VI2JBH2T.js";
|
|
7
|
+
import {
|
|
8
|
+
getAccountInfo
|
|
9
|
+
} from "./chunk-XSVQSK53.js";
|
|
10
|
+
import {
|
|
11
|
+
base58Encode
|
|
12
|
+
} from "./chunk-VG5FMOLW.js";
|
|
13
|
+
|
|
14
|
+
// src/pumpCheck.ts
|
|
15
|
+
function readU32LE(d, o) {
|
|
16
|
+
return d[o] | d[o + 1] << 8 | d[o + 2] << 16 | d[o + 3] << 24;
|
|
17
|
+
}
|
|
18
|
+
function readU64LE(d, o) {
|
|
19
|
+
let v = 0n;
|
|
20
|
+
for (let i = 7; i >= 0; i--) v = v << 8n | BigInt(d[o + i]);
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
function parseMintAccount(data) {
|
|
24
|
+
if (data.length < 82) throw new Error(`not an SPL mint account (got ${data.length} bytes, need \u226582)`);
|
|
25
|
+
const mintAuthOption = readU32LE(data, 0);
|
|
26
|
+
const freezeAuthOption = readU32LE(data, 46);
|
|
27
|
+
return {
|
|
28
|
+
mintAuthorityRevoked: mintAuthOption === 0,
|
|
29
|
+
freezeAuthorityRevoked: freezeAuthOption === 0,
|
|
30
|
+
mintAuthority: mintAuthOption === 1 ? base58Encode(data.subarray(4, 36)) : null,
|
|
31
|
+
freezeAuthority: freezeAuthOption === 1 ? base58Encode(data.subarray(50, 82)) : null,
|
|
32
|
+
supply: readU64LE(data, 36).toString(),
|
|
33
|
+
decimals: data[44],
|
|
34
|
+
isInitialized: data[45] === 1
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function verdictFrom(checks) {
|
|
38
|
+
if (checks.some((c) => c.status === "fail")) return "NO-GO";
|
|
39
|
+
if (checks.some((c) => c.status === "warn")) return "CAUTION";
|
|
40
|
+
return "GO";
|
|
41
|
+
}
|
|
42
|
+
async function pumpPreflight(mint, opts = {}) {
|
|
43
|
+
const failOnRisk = opts.failOnRisk ?? 70;
|
|
44
|
+
const checks = [];
|
|
45
|
+
const report = { mint, verdict: "GO", checks };
|
|
46
|
+
try {
|
|
47
|
+
const acct = await getAccountInfo(mint, opts);
|
|
48
|
+
if (!acct) {
|
|
49
|
+
checks.push({ id: "mint-exists", label: "Mint account exists", status: "fail", detail: "No account found at this address on the selected cluster." });
|
|
50
|
+
} else {
|
|
51
|
+
const info = parseMintAccount(acct.data);
|
|
52
|
+
report.mintInfo = info;
|
|
53
|
+
checks.push({
|
|
54
|
+
id: "mint-authority-revoked",
|
|
55
|
+
label: "Mint authority revoked",
|
|
56
|
+
status: info.mintAuthorityRevoked ? "pass" : "fail",
|
|
57
|
+
detail: info.mintAuthorityRevoked ? "Mint authority is revoked \u2014 total supply is fixed." : `Mint authority is LIVE (${info.mintAuthority}). The holder can mint unlimited new supply and dilute every holder.`
|
|
58
|
+
});
|
|
59
|
+
checks.push({
|
|
60
|
+
id: "freeze-authority-revoked",
|
|
61
|
+
label: "Freeze authority revoked",
|
|
62
|
+
status: info.freezeAuthorityRevoked ? "pass" : "warn",
|
|
63
|
+
detail: info.freezeAuthorityRevoked ? "Freeze authority is revoked \u2014 token accounts cannot be frozen." : `Freeze authority is LIVE (${info.freezeAuthority}). The holder can freeze any user's token account.`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
checks.push({ id: "mint-read", label: "Read mint account", status: "skip", detail: `Could not read mint account: ${e?.message ?? String(e)}` });
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const identity = await verifyTokenIdentity(mint, { baseUrl: opts.jupBaseUrl, offline: opts.offline });
|
|
71
|
+
report.identity = identity;
|
|
72
|
+
checks.push({
|
|
73
|
+
id: "identity",
|
|
74
|
+
label: "Token identity",
|
|
75
|
+
status: identity.impersonation ? "fail" : "pass",
|
|
76
|
+
detail: identity.impersonation ? `Impersonation: claims symbol '${identity.symbol}' but the canonical mint is ${identity.canonicalMint}.` : `Identity: ${identity.status}${identity.symbol ? ` (${identity.symbol})` : ""}.`
|
|
77
|
+
});
|
|
78
|
+
} catch (e) {
|
|
79
|
+
checks.push({ id: "identity", label: "Token identity", status: "skip", detail: `Identity check failed: ${e?.message ?? String(e)}` });
|
|
80
|
+
}
|
|
81
|
+
if (!opts.offline) {
|
|
82
|
+
const outcome = await analyzeToken(mint, { apiKey: opts.apiKey, baseUrl: opts.ricoBaseUrl });
|
|
83
|
+
if (outcome.ok) {
|
|
84
|
+
const q = outcome.result;
|
|
85
|
+
report.quality = q;
|
|
86
|
+
checks.push({
|
|
87
|
+
id: "risk-score",
|
|
88
|
+
label: "Rico risk score",
|
|
89
|
+
status: q.riskScore >= failOnRisk ? "fail" : q.riskScore >= 40 ? "warn" : "pass",
|
|
90
|
+
detail: `Rico risk score ${q.riskScore}/100 (threshold ${failOnRisk}).`
|
|
91
|
+
});
|
|
92
|
+
checks.push({
|
|
93
|
+
id: "snipers",
|
|
94
|
+
label: "Sniper activity",
|
|
95
|
+
status: q.snipersDetected ? "warn" : "pass",
|
|
96
|
+
detail: q.snipersDetected ? `Snipers detected (${(q.sniperPct * 100).toFixed(1)}% of holders).` : "No snipers detected."
|
|
97
|
+
});
|
|
98
|
+
checks.push({
|
|
99
|
+
id: "bundle-clusters",
|
|
100
|
+
label: "Bundle clusters",
|
|
101
|
+
status: q.bundleClustersDetected ? "warn" : "pass",
|
|
102
|
+
detail: q.bundleClustersDetected ? "Bundle launch clusters detected." : "No bundle clusters detected."
|
|
103
|
+
});
|
|
104
|
+
checks.push({
|
|
105
|
+
id: "holders",
|
|
106
|
+
label: "Holder distribution",
|
|
107
|
+
status: q.totalHolders < 50 ? "warn" : "pass",
|
|
108
|
+
detail: `${q.totalHolders} holders${q.cabalCount ? `, ${q.cabalCount} cabal wallet(s)` : ""}.`
|
|
109
|
+
});
|
|
110
|
+
if (q.deployerFlags.length) {
|
|
111
|
+
checks.push({
|
|
112
|
+
id: "deployer-flags",
|
|
113
|
+
label: "Deployer flags",
|
|
114
|
+
status: "warn",
|
|
115
|
+
detail: `Deployer flags: ${q.deployerFlags.join(", ")}.`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
checks.push({
|
|
120
|
+
id: "rico-scan",
|
|
121
|
+
label: "Rico Maps scan",
|
|
122
|
+
status: "skip",
|
|
123
|
+
detail: `Quality scan skipped (${outcome.kind}): ${outcome.error}.`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
checks.push({ id: "rico-scan", label: "Rico Maps scan", status: "skip", detail: "Offline mode \u2014 quality scan skipped." });
|
|
128
|
+
}
|
|
129
|
+
report.verdict = verdictFrom(checks);
|
|
130
|
+
return report;
|
|
131
|
+
}
|
|
132
|
+
var STATUS_ICON = { pass: "\u2713", warn: "\u26A0", fail: "\u2717", skip: "\xB7" };
|
|
133
|
+
function renderPreflightText(r) {
|
|
134
|
+
const lines = [];
|
|
135
|
+
lines.push(`Launch pre-flight [${r.verdict}] ${r.mint}`);
|
|
136
|
+
if (r.mintInfo) lines.push(` Supply: ${r.mintInfo.supply} Decimals: ${r.mintInfo.decimals}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
for (const c of r.checks) {
|
|
139
|
+
lines.push(` ${STATUS_ICON[c.status]} ${c.label} \u2014 ${c.detail}`);
|
|
140
|
+
}
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
parseMintAccount,
|
|
146
|
+
pumpPreflight,
|
|
147
|
+
renderPreflightText
|
|
148
|
+
};
|