@finchagentic/mcp 4.1.0 → 4.2.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/README.md +14 -14
- package/dist/agent-loop.js +75 -72
- package/dist/annotations.js +14 -13
- package/dist/convex.js +15 -18
- package/dist/index.js +4 -5
- package/dist/llm.js +12 -1
- package/dist/local-memory-file.js +14 -1
- package/dist/local-memory.js +13 -0
- package/dist/output-schemas.js +71 -17
- package/dist/resources.js +6 -11
- package/dist/server.js +27 -13
- package/dist/token-gate.js +2 -2
- package/dist/tool-filter.js +13 -4
- package/dist/tools/agents.js +62 -390
- package/dist/tools/base-mcp.js +3 -15
- package/dist/tools/base.js +34 -20
- package/dist/tools/defi.js +10 -30
- package/dist/tools/equity.js +9 -1
- package/dist/tools/insight.js +2 -2
- package/dist/tools/memory.js +44 -80
- package/dist/tools/monitor.js +6 -6
- package/dist/tools/os.js +0 -1
- package/dist/tools/packets.js +2 -2
- package/dist/tools/rh-mcp.js +13 -2
- package/dist/tools/rh-orders.js +201 -123
- package/dist/tools/scanner.js +30 -0
- package/dist/tools/stake.js +329 -0
- package/dist/tools/vault.js +99 -21
- package/dist/wallet.js +130 -19
- package/package.json +2 -2
- package/dist/tools/framework.js +0 -150
package/dist/tools/rh-orders.js
CHANGED
|
@@ -56,6 +56,53 @@ function saveStore(s) {
|
|
|
56
56
|
function genId(prefix) {
|
|
57
57
|
return `${prefix}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
58
58
|
}
|
|
59
|
+
// ── Tick lock ──────────────────────────────────────────────────────────────
|
|
60
|
+
// rh_orders_tick reads the store, awaits a real on-chain swap per due order
|
|
61
|
+
// (a slow network round trip), and only writes the store back at the end.
|
|
62
|
+
// Without a lock, two overlapping executing ticks (a slow RPC making one call
|
|
63
|
+
// outlast the next scheduler firing, or a manual call racing the scheduler)
|
|
64
|
+
// both read the SAME pre-update state, both see the same order as due, and
|
|
65
|
+
// both broadcast a real swap - real funds spent twice for what the store
|
|
66
|
+
// ends up recording as one buy. Preview ticks (execute:false) never write and
|
|
67
|
+
// are harmless to run concurrently, so only executing ticks take the lock.
|
|
68
|
+
const TICK_LOCK_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.tick.lock");
|
|
69
|
+
const LOCK_STALE_MS = 5 * 60 * 1000; // a tick this slow is presumed crashed, not just slow
|
|
70
|
+
function acquireTickLock() {
|
|
71
|
+
try {
|
|
72
|
+
fs_1.default.mkdirSync(ORDERS_DIR, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* ignore */
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
// Exclusive create - atomically fails if another tick already holds it.
|
|
79
|
+
fs_1.default.writeFileSync(TICK_LOCK_FILE, String(process.pid), { flag: "wx" });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs_1.default.statSync(TICK_LOCK_FILE);
|
|
85
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
86
|
+
// Prior holder almost certainly crashed without releasing - take over
|
|
87
|
+
// rather than deadlocking every future tick forever.
|
|
88
|
+
fs_1.default.writeFileSync(TICK_LOCK_FILE, String(process.pid));
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* lock vanished between the failed create and this stat - try again next tick */
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function releaseTickLock() {
|
|
99
|
+
try {
|
|
100
|
+
fs_1.default.unlinkSync(TICK_LOCK_FILE);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* already gone */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
59
106
|
function killSwitchOn() {
|
|
60
107
|
return process.env.RH_ORDERS_DISABLED === "1" || fs_1.default.existsSync(KILL_FILE);
|
|
61
108
|
}
|
|
@@ -63,6 +110,18 @@ function posNum(v) {
|
|
|
63
110
|
const n = Number(v);
|
|
64
111
|
return isFinite(n) ? n : NaN;
|
|
65
112
|
}
|
|
113
|
+
// Same bound quoteRh() enforces in rh-mcp.ts (matching defi.ts's Base-swap
|
|
114
|
+
// bound of .positive().max(50)) - reject an out-of-range slippagePct at order
|
|
115
|
+
// creation time rather than letting it silently reach an unattended
|
|
116
|
+
// rh_orders_tick execution weeks later.
|
|
117
|
+
function badSlippagePct(v) {
|
|
118
|
+
if (v == null)
|
|
119
|
+
return null;
|
|
120
|
+
const n = posNum(v);
|
|
121
|
+
if (!(n > 0) || n > 50)
|
|
122
|
+
return `slippagePct must be greater than 0 and at most 50 (got ${v})`;
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
66
125
|
function fmtUsd(n) {
|
|
67
126
|
if (n == null || !isFinite(n))
|
|
68
127
|
return "?";
|
|
@@ -192,6 +251,9 @@ async function handleRhOrderTool(name, args) {
|
|
|
192
251
|
return textResult("intervalHours must be > 0", true);
|
|
193
252
|
if (!(totalBuys > 0))
|
|
194
253
|
return textResult("totalBuys must be ≥ 1", true);
|
|
254
|
+
const slippageErr = badSlippagePct(a.slippagePct);
|
|
255
|
+
if (slippageErr)
|
|
256
|
+
return textResult(slippageErr, true);
|
|
195
257
|
let resolved;
|
|
196
258
|
try {
|
|
197
259
|
resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
|
|
@@ -257,6 +319,9 @@ async function handleRhOrderTool(name, args) {
|
|
|
257
319
|
const sellPct = a.sellPct != null ? posNum(a.sellPct) : 100;
|
|
258
320
|
if (!(sellPct > 0 && sellPct <= 100))
|
|
259
321
|
return textResult("sellPct must be 1..100", true);
|
|
322
|
+
const bracketSlippageErr = badSlippagePct(a.slippagePct);
|
|
323
|
+
if (bracketSlippageErr)
|
|
324
|
+
return textResult(bracketSlippageErr, true);
|
|
260
325
|
let resolved;
|
|
261
326
|
try {
|
|
262
327
|
resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
|
|
@@ -327,143 +392,156 @@ async function handleRhOrderTool(name, args) {
|
|
|
327
392
|
case "rh_orders_tick": {
|
|
328
393
|
const execute = a.execute === true;
|
|
329
394
|
const blocked = execute && killSwitchOn();
|
|
330
|
-
const store = loadStore();
|
|
331
|
-
const active = store.orders.filter((o) => o.status === "active");
|
|
332
|
-
const now = Date.now();
|
|
333
|
-
const report = [
|
|
334
|
-
`## ⏱️ RH orders tick — ${execute ? (blocked ? "EXECUTE (BLOCKED by kill-switch → preview)" : "EXECUTE") : "PREVIEW"}`,
|
|
335
|
-
`${active.length} active order(s)`,
|
|
336
|
-
"",
|
|
337
|
-
];
|
|
338
395
|
const doExecute = execute && !blocked;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
396
|
+
// Only an executing tick can double-spend; a preview never writes and
|
|
397
|
+
// is harmless to overlap, so only take the lock when it matters.
|
|
398
|
+
if (doExecute && !acquireTickLock()) {
|
|
399
|
+
return textResult("⏳ Another rh_orders_tick is already executing - refusing to run a second one " +
|
|
400
|
+
"concurrently (this is what prevents a slow tick from double-firing the same order). " +
|
|
401
|
+
"Try again shortly.", true);
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const store = loadStore();
|
|
405
|
+
const active = store.orders.filter((o) => o.status === "active");
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
const report = [
|
|
408
|
+
`## ⏱️ RH orders tick — ${execute ? (blocked ? "EXECUTE (BLOCKED by kill-switch → preview)" : "EXECUTE") : "PREVIEW"}`,
|
|
409
|
+
`${active.length} active order(s)`,
|
|
410
|
+
"",
|
|
411
|
+
];
|
|
412
|
+
let acted = 0;
|
|
413
|
+
let wallet = null;
|
|
414
|
+
for (const o of active) {
|
|
415
|
+
try {
|
|
416
|
+
// ---- DCA ----
|
|
417
|
+
if (o.type === "dca" && o.dca) {
|
|
418
|
+
const d = o.dca;
|
|
419
|
+
if (d.completedBuys >= d.totalBuys) {
|
|
420
|
+
o.status = "completed";
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (now < d.nextRunAt) {
|
|
424
|
+
report.push(`⏳ ${o.id} DCA ${o.token.symbol}: next buy at ${new Date(d.nextRunAt).toISOString()}`);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const wouldSpend = Number(d.spentEth) + Number(d.amountEthPerBuy);
|
|
428
|
+
if (wouldSpend > Number(d.maxSpendEth) + 1e-12) {
|
|
429
|
+
o.status = "completed";
|
|
430
|
+
report.push(`✅ ${o.id} DCA ${o.token.symbol}: maxSpend reached — completed`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
acted++;
|
|
434
|
+
if (!doExecute) {
|
|
435
|
+
report.push(`🟡 ${o.id} DCA: would buy ${d.amountEthPerBuy} ETH → ${o.token.symbol} (${d.completedBuys + 1}/${d.totalBuys})`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
|
|
439
|
+
fromToken: "ETH",
|
|
440
|
+
toToken: o.token.address,
|
|
441
|
+
amount: d.amountEthPerBuy,
|
|
442
|
+
maxSlippagePct: o.slippagePct,
|
|
443
|
+
confirm: true,
|
|
444
|
+
});
|
|
445
|
+
const txt = res?.content?.[0]?.text ?? "";
|
|
446
|
+
if (res?.isError) {
|
|
447
|
+
o.lastError = txt.slice(0, 140);
|
|
448
|
+
o.updatedAt = now;
|
|
449
|
+
report.push(`🔴 ${o.id} DCA buy failed: ${o.lastError}`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const txHash = extractTxHash(txt) ?? "unknown";
|
|
453
|
+
d.fills.push({ ts: now, txHash, amountEth: d.amountEthPerBuy });
|
|
454
|
+
d.completedBuys += 1;
|
|
455
|
+
d.spentEth = String(Number(d.spentEth) + Number(d.amountEthPerBuy));
|
|
456
|
+
d.nextRunAt = now + d.intervalSec * 1000; // no burst catch-up
|
|
457
|
+
o.lastError = undefined;
|
|
375
458
|
o.updatedAt = now;
|
|
376
|
-
|
|
459
|
+
if (d.completedBuys >= d.totalBuys)
|
|
460
|
+
o.status = "completed";
|
|
461
|
+
report.push(`🟢 ${o.id} DCA bought ${d.amountEthPerBuy} ETH → ${o.token.symbol} · ${d.completedBuys}/${d.totalBuys} · tx \`${txHash}\``);
|
|
377
462
|
continue;
|
|
378
463
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
464
|
+
// ---- Bracket (TP/SL) ----
|
|
465
|
+
if (o.type === "bracket" && o.bracket) {
|
|
466
|
+
const b = o.bracket;
|
|
467
|
+
const price = await (0, rh_mcp_js_1.rhPriceUsd)(o.token.address);
|
|
468
|
+
if (price == null) {
|
|
469
|
+
report.push(`⚠️ ${o.id} TP/SL ${o.token.symbol}: no price available`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
let trigger = null;
|
|
473
|
+
if (b.tpPriceUsd != null && price >= b.tpPriceUsd)
|
|
474
|
+
trigger = "tp";
|
|
475
|
+
else if (b.slPriceUsd != null && price <= b.slPriceUsd)
|
|
476
|
+
trigger = "sl";
|
|
477
|
+
if (!trigger) {
|
|
478
|
+
report.push(`⏳ ${o.id} TP/SL ${o.token.symbol}: ${fmtUsd(price)} (🎯${b.tpPriceUsd != null ? fmtUsd(b.tpPriceUsd) : "—"} / 🛑${b.slPriceUsd != null ? fmtUsd(b.slPriceUsd) : "—"})`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (!wallet)
|
|
482
|
+
wallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
483
|
+
const balRaw = await (0, rh_mcp_js_1.rhErc20Balance)(o.token.address, wallet.address);
|
|
484
|
+
if (balRaw <= 0n) {
|
|
485
|
+
o.status = "completed";
|
|
486
|
+
report.push(`✅ ${o.id} TP/SL ${o.token.symbol}: ${trigger.toUpperCase()} hit but 0 balance — completed`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const sellRaw = (balRaw * BigInt(Math.round(b.sellPct * 100))) / 10000n;
|
|
490
|
+
const sellHuman = ethers_1.ethers.formatUnits(sellRaw, o.token.decimals);
|
|
491
|
+
acted++;
|
|
492
|
+
if (!doExecute) {
|
|
493
|
+
report.push(`🟡 ${o.id} ${trigger.toUpperCase()} at ${fmtUsd(price)}: would sell ${b.sellPct}% (${sellHuman} ${o.token.symbol}) → ETH`);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
|
|
497
|
+
fromToken: o.token.address,
|
|
498
|
+
toToken: "ETH",
|
|
499
|
+
amount: sellHuman,
|
|
500
|
+
maxSlippagePct: o.slippagePct,
|
|
501
|
+
confirm: true,
|
|
502
|
+
});
|
|
503
|
+
const txt = res?.content?.[0]?.text ?? "";
|
|
504
|
+
if (res?.isError) {
|
|
505
|
+
o.lastError = txt.slice(0, 140);
|
|
506
|
+
o.updatedAt = now;
|
|
507
|
+
report.push(`🔴 ${o.id} ${trigger.toUpperCase()} sell failed: ${o.lastError}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const txHash = extractTxHash(txt) ?? "unknown";
|
|
511
|
+
b.fills.push({ ts: now, txHash, kind: trigger, priceUsd: price });
|
|
387
512
|
o.status = "completed";
|
|
388
|
-
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
// ---- Bracket (TP/SL) ----
|
|
392
|
-
if (o.type === "bracket" && o.bracket) {
|
|
393
|
-
const b = o.bracket;
|
|
394
|
-
const price = await (0, rh_mcp_js_1.rhPriceUsd)(o.token.address);
|
|
395
|
-
if (price == null) {
|
|
396
|
-
report.push(`⚠️ ${o.id} TP/SL ${o.token.symbol}: no price available`);
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
let trigger = null;
|
|
400
|
-
if (b.tpPriceUsd != null && price >= b.tpPriceUsd)
|
|
401
|
-
trigger = "tp";
|
|
402
|
-
else if (b.slPriceUsd != null && price <= b.slPriceUsd)
|
|
403
|
-
trigger = "sl";
|
|
404
|
-
if (!trigger) {
|
|
405
|
-
report.push(`⏳ ${o.id} TP/SL ${o.token.symbol}: ${fmtUsd(price)} (🎯${b.tpPriceUsd != null ? fmtUsd(b.tpPriceUsd) : "—"} / 🛑${b.slPriceUsd != null ? fmtUsd(b.slPriceUsd) : "—"})`);
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (!wallet)
|
|
409
|
-
wallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
410
|
-
const balRaw = await (0, rh_mcp_js_1.rhErc20Balance)(o.token.address, wallet.address);
|
|
411
|
-
if (balRaw <= 0n) {
|
|
412
|
-
o.status = "completed";
|
|
413
|
-
report.push(`✅ ${o.id} TP/SL ${o.token.symbol}: ${trigger.toUpperCase()} hit but 0 balance — completed`);
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
const sellRaw = (balRaw * BigInt(Math.round(b.sellPct * 100))) / 10000n;
|
|
417
|
-
const sellHuman = ethers_1.ethers.formatUnits(sellRaw, o.token.decimals);
|
|
418
|
-
acted++;
|
|
419
|
-
if (!doExecute) {
|
|
420
|
-
report.push(`🟡 ${o.id} ${trigger.toUpperCase()} at ${fmtUsd(price)}: would sell ${b.sellPct}% (${sellHuman} ${o.token.symbol}) → ETH`);
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
|
|
424
|
-
fromToken: o.token.address,
|
|
425
|
-
toToken: "ETH",
|
|
426
|
-
amount: sellHuman,
|
|
427
|
-
maxSlippagePct: o.slippagePct,
|
|
428
|
-
confirm: true,
|
|
429
|
-
});
|
|
430
|
-
const txt = res?.content?.[0]?.text ?? "";
|
|
431
|
-
if (res?.isError) {
|
|
432
|
-
o.lastError = txt.slice(0, 140);
|
|
513
|
+
o.lastError = undefined;
|
|
433
514
|
o.updatedAt = now;
|
|
434
|
-
report.push(
|
|
515
|
+
report.push(`🟢 ${o.id} ${trigger.toUpperCase()} sold ${sellHuman} ${o.token.symbol} → ETH at ${fmtUsd(price)} · tx \`${txHash}\``);
|
|
435
516
|
continue;
|
|
436
517
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
o.lastError =
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
// One bad order (e.g. wallet decrypt failure) must not abort the whole tick.
|
|
521
|
+
o.lastError = String(e?.message ?? e).slice(0, 140);
|
|
441
522
|
o.updatedAt = now;
|
|
442
|
-
report.push(
|
|
443
|
-
continue;
|
|
523
|
+
report.push(`🔴 ${o.id}: tick error — ${o.lastError}`);
|
|
444
524
|
}
|
|
445
525
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
report.push(
|
|
526
|
+
// Preview must be read-only: only persist state when actually executing.
|
|
527
|
+
if (doExecute)
|
|
528
|
+
saveStore(store);
|
|
529
|
+
if (blocked) {
|
|
530
|
+
report.push("", "🛑 Kill-switch active — no swaps broadcast. Remove ~/.finch/rh-orders.OFF or unset RH_ORDERS_DISABLED to enable.");
|
|
451
531
|
}
|
|
532
|
+
else if (acted === 0) {
|
|
533
|
+
report.push("", "Nothing due or triggered this tick.");
|
|
534
|
+
}
|
|
535
|
+
else if (!doExecute) {
|
|
536
|
+
report.push("", `${acted} action(s) pending. Re-run with \`execute:true\` (via scheduler) to broadcast.`);
|
|
537
|
+
}
|
|
538
|
+
report.push("", `_Explorer: ${rh_mcp_js_1.RH_EXPLORER}_`);
|
|
539
|
+
return textResult(report.join("\n"));
|
|
452
540
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (blocked) {
|
|
457
|
-
report.push("", "🛑 Kill-switch active — no swaps broadcast. Remove ~/.finch/rh-orders.OFF or unset RH_ORDERS_DISABLED to enable.");
|
|
458
|
-
}
|
|
459
|
-
else if (acted === 0) {
|
|
460
|
-
report.push("", "Nothing due or triggered this tick.");
|
|
461
|
-
}
|
|
462
|
-
else if (!doExecute) {
|
|
463
|
-
report.push("", `${acted} action(s) pending. Re-run with \`execute:true\` (via scheduler) to broadcast.`);
|
|
541
|
+
finally {
|
|
542
|
+
if (doExecute)
|
|
543
|
+
releaseTickLock();
|
|
464
544
|
}
|
|
465
|
-
report.push("", `_Explorer: ${rh_mcp_js_1.RH_EXPLORER}_`);
|
|
466
|
-
return textResult(report.join("\n"));
|
|
467
545
|
}
|
|
468
546
|
default:
|
|
469
547
|
return null;
|
package/dist/tools/scanner.js
CHANGED
|
@@ -134,6 +134,18 @@ function buildScanResults(mode, scanned, scored) {
|
|
|
134
134
|
// from the check_token handler so the text report and structuredContent can't
|
|
135
135
|
// drift apart.
|
|
136
136
|
function assessTokenSecurity(info) {
|
|
137
|
+
// GoPlusLabs returns HTTP 200 with an empty {} for a contract it has no
|
|
138
|
+
// record of (too new to be indexed yet, wrong address, etc.) - every field
|
|
139
|
+
// below defaults to "not risky" in that case, which previously produced a
|
|
140
|
+
// rugScore of 20 and a "SAFE" verdict for a token that was NEVER actually
|
|
141
|
+
// scanned. That's the exact class of token this tool exists to protect
|
|
142
|
+
// against (score_token/scan_market point brand-new tokens here first).
|
|
143
|
+
if (!info || Object.keys(info).length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
verdict: "UNSCANNED", rugScore: -1, isHoneypot: false, isMintable: false,
|
|
146
|
+
isFreezeAuth: false, isOpenSource: false, lpLockedPct: 0, buyTax: 0, sellTax: 0, holderCount: null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
137
149
|
const isHoneypot = info.is_honeypot === "1";
|
|
138
150
|
const isMintable = info.is_mintable === "1";
|
|
139
151
|
const isFreezeAuth = info.transfer_pausable === "1";
|
|
@@ -409,6 +421,24 @@ async function handleScannerTool(name, args) {
|
|
|
409
421
|
const info = data.result?.[address.toLowerCase()] ?? data.result?.[address] ?? {};
|
|
410
422
|
const sec = assessTokenSecurity(info);
|
|
411
423
|
const { verdict, rugScore, isHoneypot, isMintable, isFreezeAuth, isOpenSource, lpLockedPct, buyTax, sellTax } = sec;
|
|
424
|
+
if (verdict === "UNSCANNED") {
|
|
425
|
+
return {
|
|
426
|
+
content: [{
|
|
427
|
+
type: "text",
|
|
428
|
+
text: [
|
|
429
|
+
`## Token Security Check`,
|
|
430
|
+
`\`${address}\``,
|
|
431
|
+
``,
|
|
432
|
+
`**Verdict: ⚪ UNSCANNED** - GoPlusLabs has no record for this contract (too new to be indexed yet, or the address is wrong).`,
|
|
433
|
+
``,
|
|
434
|
+
`This is NOT a clean bill of health - it means no security data exists to check at all. ` +
|
|
435
|
+
`Brand-new tokens (exactly what \`scan_market\` surfaces) are the most likely to hit this. ` +
|
|
436
|
+
`Treat as unverified: confirm the contract address is correct, wait and retry once the token is a few hours/days old, or skip it.`,
|
|
437
|
+
].join("\n"),
|
|
438
|
+
}],
|
|
439
|
+
structuredContent: { address, ...sec },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
412
442
|
const icon = { DANGER: "🔴", CAUTION: "🟡", SAFE: "🟢" }[verdict];
|
|
413
443
|
const lines = [
|
|
414
444
|
`## Token Security Check`,
|