brainblast 0.6.3 → 0.6.4
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/chunk-2XJORJPQ.js +31 -0
- package/dist/{chunk-A56IF3UX.js → chunk-2Y6UILTZ.js} +83 -19
- package/dist/chunk-FQA5BYWW.js +89 -0
- package/dist/{chunk-ZZ6LBZV5.js → chunk-HXQNNGSC.js} +1 -1
- package/dist/chunk-VI2JBH2T.js +79 -0
- package/dist/cli.js +86 -4
- package/dist/index.d.ts +70 -1
- package/dist/index.js +25 -3
- package/dist/{mcp-AFYJQ7K6.js → mcp-FCKMS2MQ.js} +2 -1
- package/dist/ricomaps-WTMWBBOY.js +11 -0
- package/dist/rules/solana-token-impersonation.yaml +17 -0
- package/dist/tokenRegistry-CYIUZHAZ.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/solanaCanonicalMints.ts
|
|
2
|
+
var CANONICAL_MINTS = {
|
|
3
|
+
USDC: { symbol: "USDC", name: "USD Coin", mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" },
|
|
4
|
+
USDT: { symbol: "USDT", name: "USD Tether", mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB" },
|
|
5
|
+
SOL: { symbol: "SOL", name: "Wrapped SOL", mint: "So11111111111111111111111111111111111111112" },
|
|
6
|
+
WSOL: { symbol: "WSOL", name: "Wrapped SOL", mint: "So11111111111111111111111111111111111111112" },
|
|
7
|
+
JUP: { symbol: "JUP", name: "Jupiter", mint: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" },
|
|
8
|
+
BONK: { symbol: "BONK", name: "Bonk", mint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263" },
|
|
9
|
+
WIF: { symbol: "WIF", name: "dogwifhat", mint: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm" },
|
|
10
|
+
PYTH: { symbol: "PYTH", name: "Pyth Network", mint: "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3" },
|
|
11
|
+
RAY: { symbol: "RAY", name: "Raydium", mint: "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R" },
|
|
12
|
+
ORCA: { symbol: "ORCA", name: "Orca", mint: "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE" },
|
|
13
|
+
MNGO: { symbol: "MNGO", name: "Mango", mint: "MangoCzJ36AjZyKwVPDeoDLiiwVqHVDRtNoCKzSPH7" },
|
|
14
|
+
mSOL: { symbol: "mSOL", name: "Marinade Staked SOL", mint: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So" }
|
|
15
|
+
};
|
|
16
|
+
var CANONICAL_BY_MINT = Object.fromEntries(
|
|
17
|
+
Object.values(CANONICAL_MINTS).map((c) => [c.mint, c])
|
|
18
|
+
);
|
|
19
|
+
function canonicalMintForSymbol(symbol) {
|
|
20
|
+
return CANONICAL_MINTS[symbol.toUpperCase()];
|
|
21
|
+
}
|
|
22
|
+
function isCanonicalMint(mint) {
|
|
23
|
+
return mint in CANONICAL_BY_MINT;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
CANONICAL_MINTS,
|
|
28
|
+
CANONICAL_BY_MINT,
|
|
29
|
+
canonicalMintForSymbol,
|
|
30
|
+
isCanonicalMint
|
|
31
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANONICAL_MINTS
|
|
3
|
+
} from "./chunk-2XJORJPQ.js";
|
|
4
|
+
|
|
1
5
|
// src/finder.ts
|
|
2
6
|
import { Project, SyntaxKind } from "ts-morph";
|
|
3
7
|
|
|
@@ -789,6 +793,65 @@ var forbiddenCallReplacement = (c, p) => {
|
|
|
789
793
|
};
|
|
790
794
|
};
|
|
791
795
|
|
|
796
|
+
// src/checkers/solanaMintIdentity.ts
|
|
797
|
+
import { Node, SyntaxKind as SyntaxKind11 } from "ts-morph";
|
|
798
|
+
var BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
799
|
+
function tokenize(name) {
|
|
800
|
+
return name.replace(/([a-z])([A-Z])/g, "$1_$2").split(/[_\s]+/).map((t) => t.toUpperCase()).filter(Boolean);
|
|
801
|
+
}
|
|
802
|
+
function symbolFromName(name) {
|
|
803
|
+
return tokenize(name).find((t) => t in CANONICAL_MINTS);
|
|
804
|
+
}
|
|
805
|
+
function addressFromInitializer(init) {
|
|
806
|
+
if (Node.isStringLiteral(init)) {
|
|
807
|
+
return init.getLiteralValue();
|
|
808
|
+
}
|
|
809
|
+
if (Node.isNewExpression(init)) {
|
|
810
|
+
const args = init.getArguments();
|
|
811
|
+
if (args.length > 0 && Node.isStringLiteral(args[0])) {
|
|
812
|
+
return args[0].getLiteralValue?.() ?? args[0].getText().replace(/['"]/g, "");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return void 0;
|
|
816
|
+
}
|
|
817
|
+
var solanaMintIdentity = (c, _params) => {
|
|
818
|
+
const sf = c.fn.getSourceFile?.();
|
|
819
|
+
if (!sf) return { result: "cant_tell", detail: "Could not access source file" };
|
|
820
|
+
let foundWrong = false;
|
|
821
|
+
let foundRight = false;
|
|
822
|
+
for (const vd of sf.getDescendantsOfKind(SyntaxKind11.VariableDeclaration)) {
|
|
823
|
+
const sym = symbolFromName(vd.getName());
|
|
824
|
+
if (!sym) continue;
|
|
825
|
+
const init = vd.getInitializer();
|
|
826
|
+
if (!init) continue;
|
|
827
|
+
const addr = addressFromInitializer(init);
|
|
828
|
+
if (!addr || !BASE58_RE.test(addr)) continue;
|
|
829
|
+
if (addr === CANONICAL_MINTS[sym].mint) {
|
|
830
|
+
foundRight = true;
|
|
831
|
+
} else {
|
|
832
|
+
foundWrong = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
for (const pa of sf.getDescendantsOfKind(SyntaxKind11.PropertyAssignment)) {
|
|
836
|
+
const nameNode = pa.getNameNode();
|
|
837
|
+
const name = Node.isIdentifier(nameNode) ? nameNode.getText() : nameNode.getText().replace(/['"]/g, "");
|
|
838
|
+
const sym = symbolFromName(name);
|
|
839
|
+
if (!sym) continue;
|
|
840
|
+
const init = pa.getInitializer();
|
|
841
|
+
if (!init) continue;
|
|
842
|
+
const addr = addressFromInitializer(init);
|
|
843
|
+
if (!addr || !BASE58_RE.test(addr)) continue;
|
|
844
|
+
if (addr === CANONICAL_MINTS[sym].mint) {
|
|
845
|
+
foundRight = true;
|
|
846
|
+
} else {
|
|
847
|
+
foundWrong = true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (foundWrong) return { result: "fail", detail: "Mint constant has wrong address for its symbol" };
|
|
851
|
+
if (foundRight) return { result: "pass", detail: "All symbol-named mint constants have canonical addresses" };
|
|
852
|
+
return { result: "cant_tell", detail: "No symbol-named mint constants found" };
|
|
853
|
+
};
|
|
854
|
+
|
|
792
855
|
// src/checkers/index.ts
|
|
793
856
|
var registry = {
|
|
794
857
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -801,7 +864,8 @@ var registry = {
|
|
|
801
864
|
"env-secrets-committed": envSecretsCommitted,
|
|
802
865
|
"taint-to-sink": taintToSink,
|
|
803
866
|
"literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
|
|
804
|
-
"forbidden-call-replacement": forbiddenCallReplacement
|
|
867
|
+
"forbidden-call-replacement": forbiddenCallReplacement,
|
|
868
|
+
"solana-mint-identity-mismatch": solanaMintIdentity
|
|
805
869
|
};
|
|
806
870
|
function runChecker(kind, c, params) {
|
|
807
871
|
const fn = registry[kind];
|
|
@@ -1017,7 +1081,7 @@ function findRustCandidates(targetDir, rule) {
|
|
|
1017
1081
|
}
|
|
1018
1082
|
|
|
1019
1083
|
// src/fixers/positionalArgIdentity.ts
|
|
1020
|
-
import { SyntaxKind as
|
|
1084
|
+
import { SyntaxKind as SyntaxKind12 } from "ts-morph";
|
|
1021
1085
|
|
|
1022
1086
|
// src/fixers/diffUtil.ts
|
|
1023
1087
|
function buildDiff(node, replacement) {
|
|
@@ -1042,9 +1106,9 @@ function buildDiff(node, replacement) {
|
|
|
1042
1106
|
// src/fixers/positionalArgIdentity.ts
|
|
1043
1107
|
var fixPositionalArgIdentity = (c, p, outcome) => {
|
|
1044
1108
|
if (outcome.result !== "fail") return void 0;
|
|
1045
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1109
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((call) => {
|
|
1046
1110
|
const exp = call.getExpression();
|
|
1047
|
-
return exp.getKind() ===
|
|
1111
|
+
return exp.getKind() === SyntaxKind12.PropertyAccessExpression && exp.asKind(SyntaxKind12.PropertyAccessExpression).getName() === p.call;
|
|
1048
1112
|
});
|
|
1049
1113
|
if (calls.length === 0) {
|
|
1050
1114
|
const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
|
|
@@ -1059,7 +1123,7 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
1059
1123
|
}
|
|
1060
1124
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
1061
1125
|
const wantParam = c.params[p.paramIndex];
|
|
1062
|
-
if (arg && wantParam && arg.getKind() ===
|
|
1126
|
+
if (arg && wantParam && arg.getKind() === SyntaxKind12.CallExpression) {
|
|
1063
1127
|
return {
|
|
1064
1128
|
summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
|
|
1065
1129
|
diff: buildDiff(arg, wantParam)
|
|
@@ -1069,12 +1133,12 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
1069
1133
|
};
|
|
1070
1134
|
|
|
1071
1135
|
// src/fixers/requiredCallWithOptions.ts
|
|
1072
|
-
import { SyntaxKind as
|
|
1136
|
+
import { SyntaxKind as SyntaxKind13 } from "ts-morph";
|
|
1073
1137
|
function callName6(call) {
|
|
1074
1138
|
const exp = call.getExpression();
|
|
1075
|
-
if (exp.getKind() ===
|
|
1076
|
-
if (exp.getKind() ===
|
|
1077
|
-
return exp.asKind(
|
|
1139
|
+
if (exp.getKind() === SyntaxKind13.Identifier) return exp.getText();
|
|
1140
|
+
if (exp.getKind() === SyntaxKind13.PropertyAccessExpression) {
|
|
1141
|
+
return exp.asKind(SyntaxKind13.PropertyAccessExpression).getName();
|
|
1078
1142
|
}
|
|
1079
1143
|
return "";
|
|
1080
1144
|
}
|
|
@@ -1092,15 +1156,15 @@ function placeholderFor(propName) {
|
|
|
1092
1156
|
}
|
|
1093
1157
|
var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
1094
1158
|
if (outcome.result !== "fail") return void 0;
|
|
1095
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1159
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression);
|
|
1096
1160
|
const verify = calls.filter((x) => p.verifyCalls.includes(callName6(x)));
|
|
1097
1161
|
if (verify.length > 0) {
|
|
1098
1162
|
const call = verify[0];
|
|
1099
1163
|
const args = call.getArguments();
|
|
1100
1164
|
const lastArg = args[args.length - 1];
|
|
1101
|
-
const obj = lastArg?.asKind(
|
|
1165
|
+
const obj = lastArg?.asKind(SyntaxKind13.ObjectLiteralExpression);
|
|
1102
1166
|
const presentNames = obj ? obj.getProperties().map((pr) => {
|
|
1103
|
-
const pa = pr.asKind(
|
|
1167
|
+
const pa = pr.asKind(SyntaxKind13.PropertyAssignment) ?? pr.asKind(SyntaxKind13.ShorthandPropertyAssignment);
|
|
1104
1168
|
return pa?.getName() ?? "";
|
|
1105
1169
|
}) : [];
|
|
1106
1170
|
const missingGroups = p.requiredProps.filter(
|
|
@@ -1134,22 +1198,22 @@ JWKS must come from Privy's published JWKS endpoint for your app.`
|
|
|
1134
1198
|
};
|
|
1135
1199
|
|
|
1136
1200
|
// src/fixers/literalMultiplierWrongConstant.ts
|
|
1137
|
-
import { SyntaxKind as
|
|
1201
|
+
import { SyntaxKind as SyntaxKind14 } from "ts-morph";
|
|
1138
1202
|
function callName7(call) {
|
|
1139
1203
|
const exp = call.getExpression();
|
|
1140
|
-
if (exp.getKind() ===
|
|
1141
|
-
if (exp.getKind() ===
|
|
1142
|
-
return exp.asKind(
|
|
1204
|
+
if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
|
|
1205
|
+
if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
|
|
1206
|
+
return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
|
|
1143
1207
|
}
|
|
1144
1208
|
return "";
|
|
1145
1209
|
}
|
|
1146
1210
|
function findIdentifier(node, name) {
|
|
1147
|
-
if (node.getKind() ===
|
|
1148
|
-
return node.getDescendantsOfKind(
|
|
1211
|
+
if (node.getKind() === SyntaxKind14.Identifier && node.getText() === name) return node;
|
|
1212
|
+
return node.getDescendantsOfKind(SyntaxKind14.Identifier).find((id) => id.getText() === name);
|
|
1149
1213
|
}
|
|
1150
1214
|
var fixLiteralMultiplierWrongConstant = (c, p, outcome) => {
|
|
1151
1215
|
if (outcome.result !== "fail") return void 0;
|
|
1152
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1216
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression).filter((call) => callName7(call) === p.call);
|
|
1153
1217
|
if (calls.length === 0) return void 0;
|
|
1154
1218
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
1155
1219
|
if (!arg) return void 0;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/ricomaps.ts
|
|
2
|
+
function deployerFlagsFrom(sec) {
|
|
3
|
+
if (!sec) return [];
|
|
4
|
+
const flags = [];
|
|
5
|
+
if (sec.hasMintAuthority) flags.push("mint-authority-live");
|
|
6
|
+
if (sec.hasFreezeAuthority) flags.push("freeze-authority-live");
|
|
7
|
+
if (sec.isMutable) flags.push("metadata-mutable");
|
|
8
|
+
if (sec.riskFactors) flags.push(...sec.riskFactors);
|
|
9
|
+
return flags;
|
|
10
|
+
}
|
|
11
|
+
function renderRicoText(r) {
|
|
12
|
+
const lines = [
|
|
13
|
+
`Rico Maps analysis \u2014 ${r.mint}`,
|
|
14
|
+
` Risk score: ${r.riskScore}/100`,
|
|
15
|
+
` Holders: ${r.totalHolders}`,
|
|
16
|
+
` Cabal: ${r.cabalCount}`,
|
|
17
|
+
` Snipers: ${r.snipersDetected ? `yes (${(r.sniperPct * 100).toFixed(1)}%)` : "none"}`,
|
|
18
|
+
` Bundle clusters: ${r.bundleClustersDetected ? "detected" : "none"}`
|
|
19
|
+
];
|
|
20
|
+
if (r.deployerFlags.length) {
|
|
21
|
+
lines.push(` Deployer flags: ${r.deployerFlags.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
if (r.tier) lines.push(` Tier: ${r.tier}`);
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
async function analyzeToken(mint, opts = {}) {
|
|
27
|
+
const url = `${opts.baseUrl ?? "https://ricomaps.fun"}/api/v1/analyze`;
|
|
28
|
+
let res;
|
|
29
|
+
try {
|
|
30
|
+
res = await fetch(url, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({ mint, apiKey: opts.apiKey ?? "" }),
|
|
34
|
+
signal: AbortSignal.timeout(15e3)
|
|
35
|
+
});
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return { ok: false, kind: "network", error: String(err) };
|
|
38
|
+
}
|
|
39
|
+
if (res.status === 401 || res.status === 403) {
|
|
40
|
+
return { ok: false, kind: "auth", status: res.status, error: "Invalid or missing API key" };
|
|
41
|
+
}
|
|
42
|
+
if (res.status === 429) {
|
|
43
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
kind: "rate-limit",
|
|
47
|
+
status: 429,
|
|
48
|
+
error: "Rate limit exceeded",
|
|
49
|
+
retryAfterMs: retryAfter ? parseInt(retryAfter, 10) * 1e3 : void 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (res.status === 400) {
|
|
53
|
+
const text = await res.text().catch(() => "bad request");
|
|
54
|
+
return { ok: false, kind: "bad-request", status: 400, error: text };
|
|
55
|
+
}
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
return { ok: false, kind: "server", status: res.status, error: `Server error ${res.status}` };
|
|
58
|
+
}
|
|
59
|
+
let body;
|
|
60
|
+
try {
|
|
61
|
+
body = await res.json();
|
|
62
|
+
} catch {
|
|
63
|
+
return { ok: false, kind: "server", error: "Invalid JSON response" };
|
|
64
|
+
}
|
|
65
|
+
const s = body.summary;
|
|
66
|
+
const sniperPct = s.totalHolders > 0 && s.sniperCount != null ? s.sniperCount / s.totalHolders : 0;
|
|
67
|
+
const result = {
|
|
68
|
+
mint,
|
|
69
|
+
riskScore: s.riskScore,
|
|
70
|
+
totalHolders: s.totalHolders,
|
|
71
|
+
cabalCount: s.cabalCount,
|
|
72
|
+
snipersDetected: s.snipersDetected,
|
|
73
|
+
sniperPct,
|
|
74
|
+
bundleClustersDetected: s.bundleClustersDetected,
|
|
75
|
+
symbol: body.tokenMetadata?.symbol,
|
|
76
|
+
name: body.tokenMetadata?.name,
|
|
77
|
+
tokenSecurity: body.tokenSecurity,
|
|
78
|
+
deployerFlags: deployerFlagsFrom(body.tokenSecurity),
|
|
79
|
+
tier: body.tier,
|
|
80
|
+
processingMs: body.processingMs
|
|
81
|
+
};
|
|
82
|
+
return { ok: true, result };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
deployerFlagsFrom,
|
|
87
|
+
renderRicoText,
|
|
88
|
+
analyzeToken
|
|
89
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANONICAL_BY_MINT,
|
|
3
|
+
CANONICAL_MINTS
|
|
4
|
+
} from "./chunk-2XJORJPQ.js";
|
|
5
|
+
|
|
6
|
+
// src/tokenRegistry.ts
|
|
7
|
+
async function jupiterLookup(mint, baseUrl) {
|
|
8
|
+
const url = `${baseUrl ?? "https://tokens.jup.ag"}/token/${mint}`;
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(1e4) });
|
|
11
|
+
if (res.status === 404) return null;
|
|
12
|
+
if (!res.ok) return null;
|
|
13
|
+
return await res.json();
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function detectImpersonation(resolvedSymbol) {
|
|
19
|
+
if (!resolvedSymbol) return { impersonation: false };
|
|
20
|
+
const upper = resolvedSymbol.toUpperCase();
|
|
21
|
+
const canonical = CANONICAL_MINTS[upper];
|
|
22
|
+
if (!canonical) return { impersonation: false };
|
|
23
|
+
return { impersonation: true, canonicalMint: canonical.mint };
|
|
24
|
+
}
|
|
25
|
+
async function verifyTokenIdentity(mint, opts = {}) {
|
|
26
|
+
const bundled = CANONICAL_BY_MINT[mint];
|
|
27
|
+
if (bundled) {
|
|
28
|
+
const expectMismatch2 = opts.expectSymbol ? opts.expectSymbol.toUpperCase() !== bundled.symbol.toUpperCase() : void 0;
|
|
29
|
+
return {
|
|
30
|
+
mint,
|
|
31
|
+
status: "verified-canonical",
|
|
32
|
+
symbol: bundled.symbol,
|
|
33
|
+
name: bundled.name,
|
|
34
|
+
source: "bundled",
|
|
35
|
+
impersonation: false,
|
|
36
|
+
expectMismatch: expectMismatch2 ?? void 0
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (opts.offline) {
|
|
40
|
+
const { impersonation: impersonation2, canonicalMint: canonicalMint2 } = detectImpersonation(opts.claimedSymbol);
|
|
41
|
+
return {
|
|
42
|
+
mint,
|
|
43
|
+
status: "unverified",
|
|
44
|
+
source: "none",
|
|
45
|
+
impersonation: impersonation2,
|
|
46
|
+
canonicalMint: canonicalMint2,
|
|
47
|
+
detail: "offline mode \u2014 Jupiter lookup skipped"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const jup = await jupiterLookup(mint, opts.baseUrl);
|
|
51
|
+
if (!jup) {
|
|
52
|
+
const { impersonation: impersonation2, canonicalMint: canonicalMint2 } = detectImpersonation(opts.claimedSymbol);
|
|
53
|
+
return {
|
|
54
|
+
mint,
|
|
55
|
+
status: "unknown",
|
|
56
|
+
source: "none",
|
|
57
|
+
impersonation: impersonation2,
|
|
58
|
+
canonicalMint: canonicalMint2
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const resolvedSymbol = jup.symbol ?? opts.claimedSymbol;
|
|
62
|
+
const { impersonation, canonicalMint } = detectImpersonation(resolvedSymbol);
|
|
63
|
+
const expectMismatch = opts.expectSymbol ? opts.expectSymbol.toUpperCase() !== (resolvedSymbol ?? "").toUpperCase() : void 0;
|
|
64
|
+
const status = impersonation ? "unverified" : "verified";
|
|
65
|
+
return {
|
|
66
|
+
mint,
|
|
67
|
+
status,
|
|
68
|
+
symbol: jup.symbol,
|
|
69
|
+
name: jup.name,
|
|
70
|
+
source: "jupiter",
|
|
71
|
+
impersonation,
|
|
72
|
+
canonicalMint,
|
|
73
|
+
expectMismatch: expectMismatch ?? void 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
verifyTokenIdentity
|
|
79
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -17,12 +17,13 @@ import {
|
|
|
17
17
|
submitTelemetry,
|
|
18
18
|
telemetryFilePath,
|
|
19
19
|
validatePack
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-HXQNNGSC.js";
|
|
21
21
|
import {
|
|
22
22
|
audit,
|
|
23
23
|
getChangedRanges,
|
|
24
24
|
resolveRules
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-2Y6UILTZ.js";
|
|
26
|
+
import "./chunk-2XJORJPQ.js";
|
|
26
27
|
import "./chunk-3RG5ZIWI.js";
|
|
27
28
|
|
|
28
29
|
// src/cli.ts
|
|
@@ -119,7 +120,7 @@ if (args[0] === "drift") {
|
|
|
119
120
|
process.exit(0);
|
|
120
121
|
}
|
|
121
122
|
if (args[0] === "mcp") {
|
|
122
|
-
const { startMcpServer } = await import("./mcp-
|
|
123
|
+
const { startMcpServer } = await import("./mcp-FCKMS2MQ.js");
|
|
123
124
|
await startMcpServer();
|
|
124
125
|
process.exit(0);
|
|
125
126
|
}
|
|
@@ -143,6 +144,10 @@ if (args[0] === "watch") {
|
|
|
143
144
|
await new Promise(() => {
|
|
144
145
|
});
|
|
145
146
|
}
|
|
147
|
+
if (args[0] === "rico") {
|
|
148
|
+
await runRico(args.slice(1));
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
146
151
|
if (args[0] === "fix") {
|
|
147
152
|
await runFix(args.slice(1));
|
|
148
153
|
process.exit(0);
|
|
@@ -510,7 +515,7 @@ async function runDiff(argv) {
|
|
|
510
515
|
console.error(" e.g.: brainblast diff lodash@4.17.20 lodash@4.17.21");
|
|
511
516
|
process.exit(2);
|
|
512
517
|
}
|
|
513
|
-
const { diffVersions, renderDiffText, renderDiffMd, riskScore } = await import("./diff-KIR4PCBC.js");
|
|
518
|
+
const { diffVersions, renderDiffText, renderDiffMd: _renderDiffMd, riskScore } = await import("./diff-KIR4PCBC.js");
|
|
514
519
|
let result;
|
|
515
520
|
try {
|
|
516
521
|
result = await diffVersions(ecosystem, pkgName, fromVersion, toVersion);
|
|
@@ -538,3 +543,80 @@ Risk profile unchanged (${result.unchanged.length} advisory${result.unchanged.le
|
|
|
538
543
|
console.log("\nNo known advisories for either version.");
|
|
539
544
|
}
|
|
540
545
|
}
|
|
546
|
+
async function runRico(argv) {
|
|
547
|
+
const { analyzeToken, renderRicoText } = await import("./ricomaps-WTMWBBOY.js");
|
|
548
|
+
const { verifyTokenIdentity } = await import("./tokenRegistry-CYIUZHAZ.js");
|
|
549
|
+
const mint = argv.find((a) => !a.startsWith("--"));
|
|
550
|
+
if (!mint) {
|
|
551
|
+
console.error("usage: brainblast rico <mint> [--expect SYMBOL] [--api-key KEY] [--fail-on SCORE] [--offline] [--json]");
|
|
552
|
+
process.exit(2);
|
|
553
|
+
}
|
|
554
|
+
const expectIdx = argv.indexOf("--expect");
|
|
555
|
+
const expectSymbol = expectIdx >= 0 ? argv[expectIdx + 1] : void 0;
|
|
556
|
+
const apiKeyIdx = argv.indexOf("--api-key");
|
|
557
|
+
let apiKey = apiKeyIdx >= 0 ? argv[apiKeyIdx + 1] : void 0;
|
|
558
|
+
const failOnIdx = argv.indexOf("--fail-on");
|
|
559
|
+
const failOn = failOnIdx >= 0 ? parseInt(argv[failOnIdx + 1], 10) : 70;
|
|
560
|
+
const offline = argv.includes("--offline");
|
|
561
|
+
const jsonOut = argv.includes("--json");
|
|
562
|
+
let ricoResult = null;
|
|
563
|
+
if (!offline) {
|
|
564
|
+
ricoResult = await analyzeToken(mint, { apiKey });
|
|
565
|
+
if (!ricoResult.ok && ricoResult.kind === "auth") {
|
|
566
|
+
const readline = await import("readline");
|
|
567
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
568
|
+
const answer = await new Promise((resolve) => {
|
|
569
|
+
rl.question(
|
|
570
|
+
"\nRico Maps API key missing or invalid.\n [s] Skip quality scan\n [k] Enter API key\nChoice: ",
|
|
571
|
+
resolve
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
rl.close();
|
|
575
|
+
if (answer.trim().toLowerCase().startsWith("k")) {
|
|
576
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
577
|
+
apiKey = await new Promise((resolve) => {
|
|
578
|
+
rl2.question("API key: ", resolve);
|
|
579
|
+
});
|
|
580
|
+
rl2.close();
|
|
581
|
+
ricoResult = await analyzeToken(mint, { apiKey });
|
|
582
|
+
} else {
|
|
583
|
+
ricoResult = null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const claimedSymbol = ricoResult?.ok ? ricoResult.result.symbol : void 0;
|
|
588
|
+
const identity = await verifyTokenIdentity(mint, { expectSymbol, claimedSymbol, offline });
|
|
589
|
+
if (jsonOut) {
|
|
590
|
+
console.log(JSON.stringify({ identity, quality: ricoResult?.ok ? ricoResult.result : null }, null, 2));
|
|
591
|
+
} else {
|
|
592
|
+
const impTag = identity.impersonation ? " \u26A0 IMPERSONATION" : "";
|
|
593
|
+
const expectTag = identity.expectMismatch ? " \u26A0 EXPECT MISMATCH" : "";
|
|
594
|
+
console.log(`
|
|
595
|
+
Identity [${identity.status}]${impTag}${expectTag}`);
|
|
596
|
+
if (identity.symbol) console.log(` Symbol: ${identity.symbol}`);
|
|
597
|
+
if (identity.name) console.log(` Name: ${identity.name}`);
|
|
598
|
+
console.log(` Source: ${identity.source}`);
|
|
599
|
+
if (identity.impersonation && identity.canonicalMint) {
|
|
600
|
+
console.log(` Canonical ${identity.symbol} mint: ${identity.canonicalMint}`);
|
|
601
|
+
console.log(` This token: ${mint}`);
|
|
602
|
+
}
|
|
603
|
+
if (identity.detail) console.log(` Note: ${identity.detail}`);
|
|
604
|
+
if (ricoResult === null) {
|
|
605
|
+
console.log("\nQuality [skipped]");
|
|
606
|
+
} else if (!ricoResult.ok) {
|
|
607
|
+
console.log(`
|
|
608
|
+
Quality [error: ${ricoResult.kind}] ${ricoResult.error}`);
|
|
609
|
+
if (ricoResult.kind === "rate-limit" && ricoResult.retryAfterMs) {
|
|
610
|
+
console.log(` Retry after: ${Math.ceil(ricoResult.retryAfterMs / 1e3)}s`);
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
console.log(`
|
|
614
|
+
${renderRicoText(ricoResult.result)}`);
|
|
615
|
+
}
|
|
616
|
+
console.log("");
|
|
617
|
+
}
|
|
618
|
+
const highRisk = ricoResult?.ok && ricoResult.result.riskScore >= failOn;
|
|
619
|
+
if (identity.impersonation || identity.expectMismatch || highRisk) {
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -555,4 +555,73 @@ declare function checkDrift(dir: string, opts?: {
|
|
|
555
555
|
}): Promise<DriftResult>;
|
|
556
556
|
declare function renderDriftText(result: DriftResult): string;
|
|
557
557
|
|
|
558
|
-
|
|
558
|
+
interface CanonicalMint {
|
|
559
|
+
symbol: string;
|
|
560
|
+
name: string;
|
|
561
|
+
mint: string;
|
|
562
|
+
}
|
|
563
|
+
declare const CANONICAL_MINTS: Record<string, CanonicalMint>;
|
|
564
|
+
declare const CANONICAL_BY_MINT: Record<string, CanonicalMint>;
|
|
565
|
+
declare function canonicalMintForSymbol(symbol: string): CanonicalMint | undefined;
|
|
566
|
+
declare function isCanonicalMint(mint: string): boolean;
|
|
567
|
+
|
|
568
|
+
type IdentityStatus = "verified-canonical" | "verified" | "unverified" | "unknown";
|
|
569
|
+
interface TokenIdentity {
|
|
570
|
+
mint: string;
|
|
571
|
+
status: IdentityStatus;
|
|
572
|
+
symbol?: string;
|
|
573
|
+
name?: string;
|
|
574
|
+
source: "bundled" | "jupiter" | "none";
|
|
575
|
+
impersonation: boolean;
|
|
576
|
+
canonicalMint?: string;
|
|
577
|
+
expectMismatch?: boolean;
|
|
578
|
+
detail?: string;
|
|
579
|
+
}
|
|
580
|
+
interface VerifyOpts {
|
|
581
|
+
expectSymbol?: string;
|
|
582
|
+
claimedSymbol?: string;
|
|
583
|
+
baseUrl?: string;
|
|
584
|
+
offline?: boolean;
|
|
585
|
+
}
|
|
586
|
+
declare function verifyTokenIdentity(mint: string, opts?: VerifyOpts): Promise<TokenIdentity>;
|
|
587
|
+
|
|
588
|
+
interface RicoTokenSecurity {
|
|
589
|
+
hasMintAuthority: boolean;
|
|
590
|
+
hasFreezeAuthority: boolean;
|
|
591
|
+
isMutable: boolean;
|
|
592
|
+
riskLevel?: string;
|
|
593
|
+
riskFactors?: string[];
|
|
594
|
+
}
|
|
595
|
+
interface RicoResult {
|
|
596
|
+
mint: string;
|
|
597
|
+
riskScore: number;
|
|
598
|
+
totalHolders: number;
|
|
599
|
+
cabalCount: number;
|
|
600
|
+
snipersDetected: boolean;
|
|
601
|
+
sniperPct: number;
|
|
602
|
+
bundleClustersDetected: boolean;
|
|
603
|
+
symbol?: string;
|
|
604
|
+
name?: string;
|
|
605
|
+
tokenSecurity?: RicoTokenSecurity;
|
|
606
|
+
deployerFlags: string[];
|
|
607
|
+
tier?: string;
|
|
608
|
+
processingMs?: number;
|
|
609
|
+
}
|
|
610
|
+
type RicoOutcome = {
|
|
611
|
+
ok: true;
|
|
612
|
+
result: RicoResult;
|
|
613
|
+
} | {
|
|
614
|
+
ok: false;
|
|
615
|
+
kind: "auth" | "rate-limit" | "bad-request" | "server" | "network";
|
|
616
|
+
status?: number;
|
|
617
|
+
error: string;
|
|
618
|
+
retryAfterMs?: number;
|
|
619
|
+
};
|
|
620
|
+
declare function deployerFlagsFrom(sec?: RicoTokenSecurity): string[];
|
|
621
|
+
declare function renderRicoText(r: RicoResult): string;
|
|
622
|
+
declare function analyzeToken(mint: string, opts?: {
|
|
623
|
+
apiKey?: string;
|
|
624
|
+
baseUrl?: string;
|
|
625
|
+
}): Promise<RicoOutcome>;
|
|
626
|
+
|
|
627
|
+
export { type AccountFlow, type AuditRef, type BuildOpts, CANONICAL_BY_MINT, CANONICAL_MINTS, type Candidate, type CanonicalMint, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_TTL_HOURS, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, type GraduationEvent, type IdentityStatus, type OnChainProgram, type OsvAdvisory, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type RicoOutcome, type RicoResult, type RicoTokenSecurity, type Rule, type RustAccountField, type RustCandidate, type RustChecker, type Severity, type TelemetrySubmitResult, type TokenIdentity, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type VerifyOpts, type WatchEvent, type WatchOptions, analyzeCosts, analyzeToken, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, canonicalMintForSymbol, checkDrift, checkerKinds, defaultCachePath, deployerFlagsFrom, diffVersions, fileChanged, findCandidates, findConfigCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getRepoHash, getUserHash, getWorkingTreeChanges, initPack, isCanonicalMint, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseDiff, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderRicoText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, seedPackages, startWatch, submitTelemetry, telemetryFilePath, testKinds, validatePack, validatePackManifest, verifyTokenIdentity };
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
submitTelemetry,
|
|
32
32
|
telemetryFilePath,
|
|
33
33
|
validatePack
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-HXQNNGSC.js";
|
|
35
35
|
import {
|
|
36
36
|
PACK_MANIFEST_FILE,
|
|
37
37
|
audit,
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
runChecker,
|
|
53
53
|
testKinds,
|
|
54
54
|
validatePackManifest
|
|
55
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-2Y6UILTZ.js";
|
|
56
56
|
import {
|
|
57
57
|
diffVersions,
|
|
58
58
|
queryOsv,
|
|
@@ -65,6 +65,20 @@ import {
|
|
|
65
65
|
renderDriftText,
|
|
66
66
|
seedPackages
|
|
67
67
|
} from "./chunk-EAKU3L7F.js";
|
|
68
|
+
import {
|
|
69
|
+
analyzeToken,
|
|
70
|
+
deployerFlagsFrom,
|
|
71
|
+
renderRicoText
|
|
72
|
+
} from "./chunk-FQA5BYWW.js";
|
|
73
|
+
import {
|
|
74
|
+
verifyTokenIdentity
|
|
75
|
+
} from "./chunk-VI2JBH2T.js";
|
|
76
|
+
import {
|
|
77
|
+
CANONICAL_BY_MINT,
|
|
78
|
+
CANONICAL_MINTS,
|
|
79
|
+
canonicalMintForSymbol,
|
|
80
|
+
isCanonicalMint
|
|
81
|
+
} from "./chunk-2XJORJPQ.js";
|
|
68
82
|
import "./chunk-3RG5ZIWI.js";
|
|
69
83
|
|
|
70
84
|
// src/generate.ts
|
|
@@ -81,10 +95,13 @@ function generateTestForResult(result, rule, outPath) {
|
|
|
81
95
|
return outPath;
|
|
82
96
|
}
|
|
83
97
|
export {
|
|
98
|
+
CANONICAL_BY_MINT,
|
|
99
|
+
CANONICAL_MINTS,
|
|
84
100
|
DEFAULT_REGISTRY_URL,
|
|
85
101
|
DEFAULT_TTL_HOURS,
|
|
86
102
|
PACK_MANIFEST_FILE,
|
|
87
103
|
analyzeCosts,
|
|
104
|
+
analyzeToken,
|
|
88
105
|
applyDiffToFile,
|
|
89
106
|
audit,
|
|
90
107
|
auditWithRule,
|
|
@@ -93,9 +110,11 @@ export {
|
|
|
93
110
|
buildTrustGraph,
|
|
94
111
|
rules as bundledRules,
|
|
95
112
|
cacheSize,
|
|
113
|
+
canonicalMintForSymbol,
|
|
96
114
|
checkDrift,
|
|
97
115
|
checkerKinds,
|
|
98
116
|
defaultCachePath,
|
|
117
|
+
deployerFlagsFrom,
|
|
99
118
|
diffVersions,
|
|
100
119
|
fileChanged,
|
|
101
120
|
findCandidates,
|
|
@@ -108,6 +127,7 @@ export {
|
|
|
108
127
|
getUserHash,
|
|
109
128
|
getWorkingTreeChanges,
|
|
110
129
|
initPack,
|
|
130
|
+
isCanonicalMint,
|
|
111
131
|
isEntryExpired,
|
|
112
132
|
isTelemetryEnabled,
|
|
113
133
|
isValidSolanaAddress,
|
|
@@ -126,6 +146,7 @@ export {
|
|
|
126
146
|
renderDiffMd,
|
|
127
147
|
renderDiffText,
|
|
128
148
|
renderDriftText,
|
|
149
|
+
renderRicoText,
|
|
129
150
|
renderTest,
|
|
130
151
|
renderTrustGraphMd,
|
|
131
152
|
rentExemptMinimum,
|
|
@@ -140,5 +161,6 @@ export {
|
|
|
140
161
|
telemetryFilePath,
|
|
141
162
|
testKinds,
|
|
142
163
|
validatePack,
|
|
143
|
-
validatePackManifest
|
|
164
|
+
validatePackManifest,
|
|
165
|
+
verifyTokenIdentity
|
|
144
166
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
id: solana-token-impersonation
|
|
2
|
+
severity: critical
|
|
3
|
+
title: Hardcoded Solana mint address doesn't match the named token's canonical address
|
|
4
|
+
component:
|
|
5
|
+
name: "@solana/web3.js / @solana/spl-token"
|
|
6
|
+
type: SDK
|
|
7
|
+
version: any
|
|
8
|
+
sourceUrl: https://github.com/solana-labs/solana-web3.js
|
|
9
|
+
detect:
|
|
10
|
+
modules: ["@solana/web3.js", "@solana/spl-token"]
|
|
11
|
+
nameRegex: "mint|payment|usdc|usdt|swap|pay|deposit|withdraw|treasury|stablecoin"
|
|
12
|
+
triggerCalls: []
|
|
13
|
+
requiresImport: true
|
|
14
|
+
check:
|
|
15
|
+
kind: solana-mint-identity-mismatch
|
|
16
|
+
test:
|
|
17
|
+
kind: none
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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": [
|