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
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANONICAL_MINTS
|
|
3
|
+
} from "./chunk-2XJORJPQ.js";
|
|
4
|
+
import {
|
|
5
|
+
toSnakeCase
|
|
6
|
+
} from "./chunk-O5Z4ZJHC.js";
|
|
7
|
+
|
|
1
8
|
// src/finder.ts
|
|
2
9
|
import { Project, SyntaxKind } from "ts-morph";
|
|
3
10
|
|
|
@@ -789,6 +796,123 @@ var forbiddenCallReplacement = (c, p) => {
|
|
|
789
796
|
};
|
|
790
797
|
};
|
|
791
798
|
|
|
799
|
+
// src/checkers/solanaMintIdentity.ts
|
|
800
|
+
import { Node, SyntaxKind as SyntaxKind11 } from "ts-morph";
|
|
801
|
+
var BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
802
|
+
function tokenize(name) {
|
|
803
|
+
return name.replace(/([a-z])([A-Z])/g, "$1_$2").split(/[_\s]+/).map((t) => t.toUpperCase()).filter(Boolean);
|
|
804
|
+
}
|
|
805
|
+
function symbolFromName(name) {
|
|
806
|
+
return tokenize(name).find((t) => t in CANONICAL_MINTS);
|
|
807
|
+
}
|
|
808
|
+
function addressFromInitializer(init) {
|
|
809
|
+
if (Node.isStringLiteral(init)) {
|
|
810
|
+
return init.getLiteralValue();
|
|
811
|
+
}
|
|
812
|
+
if (Node.isNewExpression(init)) {
|
|
813
|
+
const args = init.getArguments();
|
|
814
|
+
if (args.length > 0 && Node.isStringLiteral(args[0])) {
|
|
815
|
+
return args[0].getLiteralValue?.() ?? args[0].getText().replace(/['"]/g, "");
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
var solanaMintIdentity = (c, _params) => {
|
|
821
|
+
const sf = c.fn.getSourceFile?.();
|
|
822
|
+
if (!sf) return { result: "cant_tell", detail: "Could not access source file" };
|
|
823
|
+
let foundWrong = false;
|
|
824
|
+
let foundRight = false;
|
|
825
|
+
for (const vd of sf.getDescendantsOfKind(SyntaxKind11.VariableDeclaration)) {
|
|
826
|
+
const sym = symbolFromName(vd.getName());
|
|
827
|
+
if (!sym) continue;
|
|
828
|
+
const init = vd.getInitializer();
|
|
829
|
+
if (!init) continue;
|
|
830
|
+
const addr = addressFromInitializer(init);
|
|
831
|
+
if (!addr || !BASE58_RE.test(addr)) continue;
|
|
832
|
+
if (addr === CANONICAL_MINTS[sym].mint) {
|
|
833
|
+
foundRight = true;
|
|
834
|
+
} else {
|
|
835
|
+
foundWrong = true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const pa of sf.getDescendantsOfKind(SyntaxKind11.PropertyAssignment)) {
|
|
839
|
+
const nameNode = pa.getNameNode();
|
|
840
|
+
const name = Node.isIdentifier(nameNode) ? nameNode.getText() : nameNode.getText().replace(/['"]/g, "");
|
|
841
|
+
const sym = symbolFromName(name);
|
|
842
|
+
if (!sym) continue;
|
|
843
|
+
const init = pa.getInitializer();
|
|
844
|
+
if (!init) continue;
|
|
845
|
+
const addr = addressFromInitializer(init);
|
|
846
|
+
if (!addr || !BASE58_RE.test(addr)) continue;
|
|
847
|
+
if (addr === CANONICAL_MINTS[sym].mint) {
|
|
848
|
+
foundRight = true;
|
|
849
|
+
} else {
|
|
850
|
+
foundWrong = true;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (foundWrong) return { result: "fail", detail: "Mint constant has wrong address for its symbol" };
|
|
854
|
+
if (foundRight) return { result: "pass", detail: "All symbol-named mint constants have canonical addresses" };
|
|
855
|
+
return { result: "cant_tell", detail: "No symbol-named mint constants found" };
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// src/checkers/anchorIdlAccount.ts
|
|
859
|
+
function fieldIsSigner(f) {
|
|
860
|
+
if (/\bSigner\s*</.test(f.typeName)) return true;
|
|
861
|
+
if (/\bsigner\b/.test(f.attrText)) return true;
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
function fieldIsMut(f) {
|
|
865
|
+
if (/\bmut\b/.test(f.attrText)) return true;
|
|
866
|
+
if (/\binit\b/.test(f.attrText)) return true;
|
|
867
|
+
if (f.hasInitIfNeeded) return true;
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
function anchorIdlAccount(c, params) {
|
|
871
|
+
const handlerName = toSnakeCase(c.fnName);
|
|
872
|
+
const spec = params?.instructions?.find((i) => i.name === handlerName);
|
|
873
|
+
if (!spec) {
|
|
874
|
+
return {
|
|
875
|
+
result: "cant_tell",
|
|
876
|
+
detail: `Handler '${c.fnName}' is not declared in the ${params?.idlName ?? "IDL"}; constraint rule does not apply.`
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (spec.signers.length === 0 && spec.mutable.length === 0) {
|
|
880
|
+
return {
|
|
881
|
+
result: "cant_tell",
|
|
882
|
+
detail: `Instruction '${spec.name}' declares no signer or mutable accounts in the IDL; nothing to verify.`
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const byName = /* @__PURE__ */ new Map();
|
|
886
|
+
for (const f of c.accountFields) byName.set(toSnakeCase(f.name), f);
|
|
887
|
+
const violations = [];
|
|
888
|
+
for (const acct of spec.signers) {
|
|
889
|
+
const f = byName.get(acct);
|
|
890
|
+
if (!f) {
|
|
891
|
+
violations.push(`'${acct}' (IDL signer) is not present in the Accounts struct`);
|
|
892
|
+
} else if (!fieldIsSigner(f)) {
|
|
893
|
+
violations.push(`'${acct}' must be a Signer (IDL marks it isSigner) but the Rust field has no signer constraint`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
for (const acct of spec.mutable) {
|
|
897
|
+
const f = byName.get(acct);
|
|
898
|
+
if (!f) {
|
|
899
|
+
violations.push(`'${acct}' (IDL mutable) is not present in the Accounts struct`);
|
|
900
|
+
} else if (!fieldIsMut(f)) {
|
|
901
|
+
violations.push(`'${acct}' must be mutable (IDL marks it isMut) but the Rust field has no mut/init constraint`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (violations.length > 0) {
|
|
905
|
+
return {
|
|
906
|
+
result: "fail",
|
|
907
|
+
detail: `Handler '${c.fnName}' diverges from the ${params.idlName} IDL: ` + violations.join("; ") + ". A missing signer/mut constraint is a silent authorization hole."
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
result: "pass",
|
|
912
|
+
detail: `Handler '${c.fnName}' declares all ${spec.signers.length} signer and ${spec.mutable.length} mutable account(s) the ${params.idlName} IDL requires.`
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
792
916
|
// src/checkers/index.ts
|
|
793
917
|
var registry = {
|
|
794
918
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -801,7 +925,9 @@ var registry = {
|
|
|
801
925
|
"env-secrets-committed": envSecretsCommitted,
|
|
802
926
|
"taint-to-sink": taintToSink,
|
|
803
927
|
"literal-multiplier-wrong-constant": literalMultiplierWrongConstant,
|
|
804
|
-
"forbidden-call-replacement": forbiddenCallReplacement
|
|
928
|
+
"forbidden-call-replacement": forbiddenCallReplacement,
|
|
929
|
+
"solana-mint-identity-mismatch": solanaMintIdentity,
|
|
930
|
+
"anchor-account-matches-idl": anchorIdlAccount
|
|
805
931
|
};
|
|
806
932
|
function runChecker(kind, c, params) {
|
|
807
933
|
const fn = registry[kind];
|
|
@@ -1017,7 +1143,7 @@ function findRustCandidates(targetDir, rule) {
|
|
|
1017
1143
|
}
|
|
1018
1144
|
|
|
1019
1145
|
// src/fixers/positionalArgIdentity.ts
|
|
1020
|
-
import { SyntaxKind as
|
|
1146
|
+
import { SyntaxKind as SyntaxKind12 } from "ts-morph";
|
|
1021
1147
|
|
|
1022
1148
|
// src/fixers/diffUtil.ts
|
|
1023
1149
|
function buildDiff(node, replacement) {
|
|
@@ -1042,9 +1168,9 @@ function buildDiff(node, replacement) {
|
|
|
1042
1168
|
// src/fixers/positionalArgIdentity.ts
|
|
1043
1169
|
var fixPositionalArgIdentity = (c, p, outcome) => {
|
|
1044
1170
|
if (outcome.result !== "fail") return void 0;
|
|
1045
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1171
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind12.CallExpression).filter((call) => {
|
|
1046
1172
|
const exp = call.getExpression();
|
|
1047
|
-
return exp.getKind() ===
|
|
1173
|
+
return exp.getKind() === SyntaxKind12.PropertyAccessExpression && exp.asKind(SyntaxKind12.PropertyAccessExpression).getName() === p.call;
|
|
1048
1174
|
});
|
|
1049
1175
|
if (calls.length === 0) {
|
|
1050
1176
|
const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
|
|
@@ -1059,7 +1185,7 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
1059
1185
|
}
|
|
1060
1186
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
1061
1187
|
const wantParam = c.params[p.paramIndex];
|
|
1062
|
-
if (arg && wantParam && arg.getKind() ===
|
|
1188
|
+
if (arg && wantParam && arg.getKind() === SyntaxKind12.CallExpression) {
|
|
1063
1189
|
return {
|
|
1064
1190
|
summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
|
|
1065
1191
|
diff: buildDiff(arg, wantParam)
|
|
@@ -1069,12 +1195,12 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
1069
1195
|
};
|
|
1070
1196
|
|
|
1071
1197
|
// src/fixers/requiredCallWithOptions.ts
|
|
1072
|
-
import { SyntaxKind as
|
|
1198
|
+
import { SyntaxKind as SyntaxKind13 } from "ts-morph";
|
|
1073
1199
|
function callName6(call) {
|
|
1074
1200
|
const exp = call.getExpression();
|
|
1075
|
-
if (exp.getKind() ===
|
|
1076
|
-
if (exp.getKind() ===
|
|
1077
|
-
return exp.asKind(
|
|
1201
|
+
if (exp.getKind() === SyntaxKind13.Identifier) return exp.getText();
|
|
1202
|
+
if (exp.getKind() === SyntaxKind13.PropertyAccessExpression) {
|
|
1203
|
+
return exp.asKind(SyntaxKind13.PropertyAccessExpression).getName();
|
|
1078
1204
|
}
|
|
1079
1205
|
return "";
|
|
1080
1206
|
}
|
|
@@ -1092,15 +1218,15 @@ function placeholderFor(propName) {
|
|
|
1092
1218
|
}
|
|
1093
1219
|
var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
1094
1220
|
if (outcome.result !== "fail") return void 0;
|
|
1095
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1221
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind13.CallExpression);
|
|
1096
1222
|
const verify = calls.filter((x) => p.verifyCalls.includes(callName6(x)));
|
|
1097
1223
|
if (verify.length > 0) {
|
|
1098
1224
|
const call = verify[0];
|
|
1099
1225
|
const args = call.getArguments();
|
|
1100
1226
|
const lastArg = args[args.length - 1];
|
|
1101
|
-
const obj = lastArg?.asKind(
|
|
1227
|
+
const obj = lastArg?.asKind(SyntaxKind13.ObjectLiteralExpression);
|
|
1102
1228
|
const presentNames = obj ? obj.getProperties().map((pr) => {
|
|
1103
|
-
const pa = pr.asKind(
|
|
1229
|
+
const pa = pr.asKind(SyntaxKind13.PropertyAssignment) ?? pr.asKind(SyntaxKind13.ShorthandPropertyAssignment);
|
|
1104
1230
|
return pa?.getName() ?? "";
|
|
1105
1231
|
}) : [];
|
|
1106
1232
|
const missingGroups = p.requiredProps.filter(
|
|
@@ -1134,22 +1260,22 @@ JWKS must come from Privy's published JWKS endpoint for your app.`
|
|
|
1134
1260
|
};
|
|
1135
1261
|
|
|
1136
1262
|
// src/fixers/literalMultiplierWrongConstant.ts
|
|
1137
|
-
import { SyntaxKind as
|
|
1263
|
+
import { SyntaxKind as SyntaxKind14 } from "ts-morph";
|
|
1138
1264
|
function callName7(call) {
|
|
1139
1265
|
const exp = call.getExpression();
|
|
1140
|
-
if (exp.getKind() ===
|
|
1141
|
-
if (exp.getKind() ===
|
|
1142
|
-
return exp.asKind(
|
|
1266
|
+
if (exp.getKind() === SyntaxKind14.Identifier) return exp.getText();
|
|
1267
|
+
if (exp.getKind() === SyntaxKind14.PropertyAccessExpression) {
|
|
1268
|
+
return exp.asKind(SyntaxKind14.PropertyAccessExpression).getName();
|
|
1143
1269
|
}
|
|
1144
1270
|
return "";
|
|
1145
1271
|
}
|
|
1146
1272
|
function findIdentifier(node, name) {
|
|
1147
|
-
if (node.getKind() ===
|
|
1148
|
-
return node.getDescendantsOfKind(
|
|
1273
|
+
if (node.getKind() === SyntaxKind14.Identifier && node.getText() === name) return node;
|
|
1274
|
+
return node.getDescendantsOfKind(SyntaxKind14.Identifier).find((id) => id.getText() === name);
|
|
1149
1275
|
}
|
|
1150
1276
|
var fixLiteralMultiplierWrongConstant = (c, p, outcome) => {
|
|
1151
1277
|
if (outcome.result !== "fail") return void 0;
|
|
1152
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
1278
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind14.CallExpression).filter((call) => callName7(call) === p.call);
|
|
1153
1279
|
if (calls.length === 0) return void 0;
|
|
1154
1280
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
1155
1281
|
if (!arg) return void 0;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RPC,
|
|
3
|
+
probeUpgradeAuthority
|
|
4
|
+
} from "./chunk-XSVQSK53.js";
|
|
5
|
+
|
|
6
|
+
// src/watchChain.ts
|
|
7
|
+
async function getSignaturesForAddress(programId, until, opts) {
|
|
8
|
+
const url = opts.rpcUrl ?? DEFAULT_RPC;
|
|
9
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
10
|
+
const params = [programId, { limit: opts.limit ?? 25 }];
|
|
11
|
+
if (until) params[1].until = until;
|
|
12
|
+
const ac = new AbortController();
|
|
13
|
+
const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetchImpl(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "content-type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getSignaturesForAddress", params }),
|
|
19
|
+
signal: ac.signal
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) throw new Error(`getSignaturesForAddress: HTTP ${res.status}`);
|
|
22
|
+
const body = await res.json();
|
|
23
|
+
if (body.error) throw new Error(`getSignaturesForAddress: ${body.error.message}`);
|
|
24
|
+
return body.result ?? [];
|
|
25
|
+
} finally {
|
|
26
|
+
clearTimeout(t);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function pollChainOnce(programId, state, opts = {}) {
|
|
30
|
+
const events = [];
|
|
31
|
+
const ts = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
32
|
+
let sigs;
|
|
33
|
+
try {
|
|
34
|
+
sigs = await getSignaturesForAddress(programId, state.lastSignature, opts);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
events.push({ type: "poll_error", programId, message: e?.message ?? String(e), ts: ts() });
|
|
37
|
+
return { events, state };
|
|
38
|
+
}
|
|
39
|
+
const probe = opts.probeAuthority ?? ((id, o) => probeUpgradeAuthority(id, o).then((ua) => ({ address: ua.address })));
|
|
40
|
+
let currentAuthority = state.baselineAuthority;
|
|
41
|
+
try {
|
|
42
|
+
const ua = await probe(programId, opts);
|
|
43
|
+
currentAuthority = ua.address;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
events.push({ type: "poll_error", programId, message: `authority probe: ${e?.message ?? String(e)}`, ts: ts() });
|
|
46
|
+
}
|
|
47
|
+
if (!state.initialized) {
|
|
48
|
+
const head = sigs[0]?.signature ?? null;
|
|
49
|
+
events.push({ type: "watch_started", programId, headSignature: head, baselineAuthority: currentAuthority, ts: ts() });
|
|
50
|
+
return {
|
|
51
|
+
events,
|
|
52
|
+
state: { lastSignature: head, baselineAuthority: currentAuthority, initialized: true }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (sigs.length > 0) {
|
|
56
|
+
events.push({
|
|
57
|
+
type: "new_activity",
|
|
58
|
+
programId,
|
|
59
|
+
newCount: sigs.length,
|
|
60
|
+
signatures: sigs.slice(0, 10).map((s) => s.signature),
|
|
61
|
+
ts: ts()
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (currentAuthority !== state.baselineAuthority) {
|
|
65
|
+
events.push({
|
|
66
|
+
type: "authority_changed",
|
|
67
|
+
programId,
|
|
68
|
+
from: state.baselineAuthority,
|
|
69
|
+
to: currentAuthority,
|
|
70
|
+
ts: ts()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
events,
|
|
75
|
+
state: {
|
|
76
|
+
lastSignature: sigs[0]?.signature ?? state.lastSignature,
|
|
77
|
+
baselineAuthority: currentAuthority,
|
|
78
|
+
initialized: true
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function initialChainWatchState() {
|
|
83
|
+
return { lastSignature: null, baselineAuthority: null, initialized: false };
|
|
84
|
+
}
|
|
85
|
+
function startChainWatch(programId, opts = {}) {
|
|
86
|
+
const emit = opts.emit ?? ((e) => process.stdout.write(JSON.stringify(e) + "\n"));
|
|
87
|
+
const intervalMs = opts.intervalMs ?? 3e4;
|
|
88
|
+
let state = initialChainWatchState();
|
|
89
|
+
let stopped = false;
|
|
90
|
+
let timer;
|
|
91
|
+
const tick = async () => {
|
|
92
|
+
if (stopped) return;
|
|
93
|
+
const { events, state: next } = await pollChainOnce(programId, state, opts);
|
|
94
|
+
state = next;
|
|
95
|
+
for (const e of events) emit(e);
|
|
96
|
+
if (!stopped) timer = setTimeout(tick, intervalMs);
|
|
97
|
+
};
|
|
98
|
+
void tick();
|
|
99
|
+
return {
|
|
100
|
+
stop: () => {
|
|
101
|
+
stopped = true;
|
|
102
|
+
if (timer) clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
pollChainOnce,
|
|
109
|
+
initialChainWatchState,
|
|
110
|
+
startChainWatch
|
|
111
|
+
};
|
|
@@ -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
|
+
};
|