dexterm-cli 0.2.0 → 0.2.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/packages/agents/src/spectator.js +28 -2
- package/dist/packages/agents/src/tiers.js +3 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/autotrade/src/backtest.js +6 -2
- package/dist/packages/autotrade/src/fleet.js +2 -2
- package/dist/packages/autotrade/src/index.js +2 -0
- package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
- package/dist/packages/autotrade/src/rugproof.js +231 -0
- package/dist/packages/autotrade/src/runner.js +132 -17
- package/dist/packages/autotrade/src/service-install.js +6 -4
- package/dist/packages/autotrade/src/sessions.js +16 -7
- package/dist/packages/autotrade/src/strategy-stats.js +344 -0
- package/dist/packages/autotrade/src/strategy.js +12 -1
- package/dist/packages/classifier/src/calibrate.js +138 -0
- package/dist/packages/classifier/src/classify.js +107 -0
- package/dist/packages/classifier/src/config.js +220 -0
- package/dist/packages/classifier/src/dossier.js +95 -0
- package/dist/packages/classifier/src/hardware.js +63 -0
- package/dist/packages/classifier/src/index.js +18 -0
- package/dist/packages/classifier/src/models.js +111 -0
- package/dist/packages/classifier/src/providers/heuristic.js +77 -0
- package/dist/packages/classifier/src/providers/http.js +63 -0
- package/dist/packages/classifier/src/providers/jev-open.js +86 -0
- package/dist/packages/classifier/src/providers/llm.js +89 -0
- package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
- package/dist/packages/classifier/src/questions.js +97 -0
- package/dist/packages/classifier/src/registry.js +42 -0
- package/dist/packages/classifier/src/snapshot.js +31 -0
- package/dist/packages/classifier/src/systemone.js +88 -0
- package/dist/packages/classifier/src/types.js +18 -0
- package/dist/packages/cli/assets/dash/dash.css +139 -0
- package/dist/packages/cli/assets/dash/dash.js +200 -0
- package/dist/packages/cli/assets/dash/icon.svg +1 -0
- package/dist/packages/cli/assets/dash/index.html +36 -0
- package/dist/packages/cli/src/app-help-gen.js +12 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +9 -2
- package/dist/packages/cli/src/autotrade-cmd/run.js +11 -1
- package/dist/packages/cli/src/autotrade-cmd/signal.js +2 -2
- package/dist/packages/cli/src/classifier-cmd.js +353 -0
- package/dist/packages/cli/src/classifier-live.js +81 -0
- package/dist/packages/cli/src/dash/data.js +217 -0
- package/dist/packages/cli/src/dash/server.js +92 -0
- package/dist/packages/cli/src/dash-cmd.js +113 -0
- package/dist/packages/cli/src/doctor.js +34 -0
- package/dist/packages/cli/src/live/screens/token-live.js +4 -0
- package/dist/packages/cli/src/live/signals.js +8 -0
- package/dist/packages/cli/src/main.js +98 -6
- package/dist/packages/cli/src/mcp-follow.js +21 -1
- package/dist/packages/cli/src/narratives-cmd.js +147 -0
- package/dist/packages/cli/src/narratives.js +114 -0
- package/dist/packages/cli/src/overlay/model.js +147 -0
- package/dist/packages/cli/src/overlay/page.js +146 -0
- package/dist/packages/cli/src/overlay/pnl.js +75 -0
- package/dist/packages/cli/src/overlay/server.js +170 -0
- package/dist/packages/cli/src/overlay-cmd.js +367 -0
- package/dist/packages/cli/src/record-cmd.js +82 -11
- package/dist/packages/cli/src/rules-cmd.js +2 -1
- package/dist/packages/cli/src/rules.js +24 -4
- package/dist/packages/cli/src/screens/narrative.js +154 -2
- package/dist/packages/cli/src/state.js +1 -0
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/custom/src/rules.js +5 -1
- package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
- package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
- package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
- package/dist/packages/mcp/src/server.js +104 -0
- package/dist/packages/mcp/src/spectator-sink.js +27 -0
- package/dist/packages/narratives/src/cluster.js +193 -0
- package/dist/packages/narratives/src/embed.js +184 -0
- package/dist/packages/narratives/src/export.js +60 -0
- package/dist/packages/narratives/src/index.js +10 -0
- package/dist/packages/narratives/src/label.js +48 -0
- package/dist/packages/narratives/src/normalize.js +179 -0
- package/dist/packages/narratives/src/recorder.js +93 -0
- package/dist/packages/narratives/src/tracker.js +328 -0
- package/dist/packages/recorder/src/exit-probes.js +215 -0
- package/dist/packages/recorder/src/index.js +1 -0
- package/dist/packages/recorder/src/recorder.js +11 -1
- package/dist/packages/recorder/src/retention.js +4 -0
- package/dist/packages/recorder/src/schema.js +20 -1
- package/dist/packages/tui/src/keymap.js +5 -0
- package/package.json +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
|
@@ -95,13 +95,18 @@ const finiteOr = (x, d = null) => (typeof x === 'number' && Number.isFinite(x) ?
|
|
|
95
95
|
// eslint-disable-next-line no-control-regex
|
|
96
96
|
const UNSAFE_RE = /[\u0000-\u001f\u007f-\u009f---]/g;
|
|
97
97
|
const shortStr = (x, max) => (typeof x === 'string' ? x.replace(UNSAFE_RE, '').slice(0, max) : '');
|
|
98
|
-
const
|
|
98
|
+
const isFullMint = (x) => typeof x === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(x);
|
|
99
|
+
/** A mint as the redactor leaves it (maskAddress: head4…tail4). */
|
|
100
|
+
const isMaskedMint = (x) => typeof x === 'string' && /^[1-9A-HJ-NP-Za-km-z]{4}…[1-9A-HJ-NP-Za-km-z]{4}$/.test(x);
|
|
99
101
|
/**
|
|
100
102
|
* task-1466: re-validate a detail read off the local channel (shape, types, lengths). The channel is our own
|
|
101
103
|
* server's 0600 file and token-checked, but the viewer still treats it as data: anything malformed is dropped,
|
|
102
104
|
* strings are stripped of control/bidi characters and capped, numbers must be finite. Returns null when empty.
|
|
103
105
|
*/
|
|
104
|
-
export function sanitizeDetail(raw) {
|
|
106
|
+
export function sanitizeDetail(raw, opts = {}) {
|
|
107
|
+
// task-1427: a detail read off the SHARED stream (or a saved file) is already redacted, so its mints are head…tail;
|
|
108
|
+
// `masked` accepts that shape (and only that shape) instead of a full mint. The local channel keeps the strict check.
|
|
109
|
+
const isMintish = (x) => (opts.masked ? isMaskedMint(x) : isFullMint(x));
|
|
105
110
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
106
111
|
return null;
|
|
107
112
|
const r = raw;
|
|
@@ -140,6 +145,27 @@ export function sanitizeDetail(raw) {
|
|
|
140
145
|
out.caps = { perTradeUsd: cp.perTradeUsd, totalUsd: cp.totalUsd, maxPriceImpactPct: finiteOr(cp.maxPriceImpactPct) };
|
|
141
146
|
if (typeof r.refusal === 'string' && r.refusal)
|
|
142
147
|
out.refusal = shortStr(r.refusal, 40);
|
|
148
|
+
const h = r.holders;
|
|
149
|
+
const top10 = h ? finiteOr(h.top10Pct) : null;
|
|
150
|
+
if (h && top10 !== null && top10 >= 0 && top10 <= 100 && (h.basis === 'ex-pools' || h.basis === 'raw')) {
|
|
151
|
+
const count = finiteOr(h.count), ageSec = finiteOr(h.ageSec);
|
|
152
|
+
out.holders = { top10Pct: top10, count: count !== null && count >= 0 ? Math.round(count) : null, basis: h.basis, ageSec: ageSec !== null && ageSec >= 0 ? ageSec : null };
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(r.liveFlags)) {
|
|
155
|
+
const LEVELS = ['up', 'down', 'info', 'warn'];
|
|
156
|
+
const sigs = r.liveFlags.slice(0, 8).flatMap((x) => {
|
|
157
|
+
const o = x;
|
|
158
|
+
if (!o || typeof o !== 'object')
|
|
159
|
+
return [];
|
|
160
|
+
const kind = shortStr(o.kind, 16), title = shortStr(o.title, 24), text = shortStr(o.text, 120);
|
|
161
|
+
const at = finiteOr(o.at), ageSec = finiteOr(o.ageSec);
|
|
162
|
+
if (!kind || !title || at === null || ageSec === null || ageSec < 0 || !LEVELS.includes(o.level))
|
|
163
|
+
return [];
|
|
164
|
+
return [{ kind, level: o.level, title, text, source: shortStr(o.source, 24) || '?', at, ageSec, stale: o.stale === true }];
|
|
165
|
+
});
|
|
166
|
+
if (sigs.length)
|
|
167
|
+
out.liveFlags = sigs;
|
|
168
|
+
}
|
|
143
169
|
return Object.keys(out).length ? out : null;
|
|
144
170
|
}
|
|
145
171
|
// ---------------------------------------------------------------------------------------------------
|
|
@@ -46,6 +46,9 @@ export const TOOL_TIERS = Object.freeze({
|
|
|
46
46
|
// agents in task-1405 so a bring-your-own-agent gets the same code-stage analysis the built-in analyst does.
|
|
47
47
|
analyze_token: 'observe',
|
|
48
48
|
narrative_board: 'observe',
|
|
49
|
+
narratives_top: 'observe',
|
|
50
|
+
narrative_detail: 'observe',
|
|
51
|
+
narrative_of: 'observe',
|
|
49
52
|
rug_backtest: 'observe',
|
|
50
53
|
kb_lookup: 'observe',
|
|
51
54
|
kb_search: 'observe',
|
|
@@ -35,6 +35,9 @@ const TOOLS_DEF = [
|
|
|
35
35
|
{ name: 'get_dev_history', tier: 'observe', description: 'Creator address and prior-launch forensics. Tier-1 fields need a key and are null on the keyless tier.', inputSchema: { type: 'object', properties: { mint: MINT }, required: ['mint'], additionalProperties: false } },
|
|
36
36
|
{ name: 'analyze_token', tier: 'observe', description: 'Deterministic token-dossier analysis pack: numeric features, named rule flags, a WATCH/SPECULATIVE/AVOID verdict, a p(rug) heuristic and data ages. No model, no token cost; a reasoning stage may only tighten the verdict, never loosen it.', inputSchema: { type: 'object', properties: { mint: MINT, symbol: SYMBOL, interval: { type: 'string', enum: ['1m', '5m', '15m', '1h', '4h', '1d'] } }, additionalProperties: false } },
|
|
37
37
|
{ name: 'narrative_board', tier: 'observe', description: 'The narrative board over recorded + trending tokens: per-narrative volume, new launches, runners and a "what is working" score. No model; returns a note when there is no recorded history yet.', inputSchema: { type: 'object', properties: { window: { type: 'string', enum: ['1h', '24h'] }, limit: { type: 'integer', minimum: 1, maximum: 50 }, summary: { type: 'boolean' } }, additionalProperties: false } },
|
|
38
|
+
{ name: 'narratives_top', tier: 'observe', description: 'MEMES + NARRATIVES tracker: the hottest name/ticker-theme clusters of the last 26 h with lifecycle (spark/ignition/peak/fading/dead), copy-cat saturation, volume share and a traction score. Measured heat, not advice.', inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 50 }, lifecycle: { type: 'string', enum: ['spark', 'ignition', 'peak', 'fading'] } }, additionalProperties: false } },
|
|
39
|
+
{ name: 'narrative_detail', tier: 'observe', description: 'One narrative by id: per-window metrics, lifecycle rule and member coins in spawn order. Measured heat, not advice.', inputSchema: { type: 'object', properties: { id: { type: 'string' }, limit: { type: 'integer', minimum: 1, maximum: 100 } }, required: ['id'], additionalProperties: false } },
|
|
40
|
+
{ name: 'narrative_of', tier: 'observe', description: 'Which narrative a coin belongs to: label, lifecycle, rank, spawn index, saturation. Measured heat, not advice.', inputSchema: { type: 'object', properties: { mint: { type: 'string' } }, required: ['mint'], additionalProperties: false } },
|
|
38
41
|
{ name: 'rug_backtest', tier: 'observe', description: 'Backtest the rug model or recorded pack verdicts against RECORDED outcomes: honest precision/recall, Brier, AUC and calibration. Leakage-free by construction; returns a note when there is not enough history. Read-only.', inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['heuristic', 'pack_verdicts'] }, horizon_sec: { type: 'integer', enum: [3600, 7200, 21600, 86400, 259200] }, positive_labels: { type: 'array', items: { type: 'string', enum: ['rug', 'dead', 'fade', 'flat', 'runner'] }, maxItems: 5 } }, additionalProperties: false } },
|
|
39
42
|
{ name: 'kb_lookup', tier: 'observe', description: 'One knowledge-base entry by id (mechanics / patterns / glossary). KB never holds facts about a live token.', inputSchema: { type: 'object', properties: { id: { type: 'string', maxLength: 64 } }, required: ['id'], additionalProperties: false } },
|
|
40
43
|
{ name: 'kb_search', tier: 'observe', description: 'Search the knowledge base; returns ids, titles and an excerpt.', inputSchema: { type: 'object', properties: { query: { type: 'string', maxLength: 200 }, limit: { type: 'integer', minimum: 1, maximum: 10 } }, required: ['query'], additionalProperties: false } },
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { SKILL_MIN_N, wilson } from '../../packs/src/index.js';
|
|
20
20
|
import { EntryRules } from "./engine.js";
|
|
21
21
|
import { runAutotrade } from "./runner.js";
|
|
22
|
+
import { rugproofTapeVet } from "./rugproof.js";
|
|
22
23
|
import { validateStrategy, round } from "./strategy.js";
|
|
23
24
|
export const DATA_LIMITS = [
|
|
24
25
|
'Price path = recorder snapshots (~2-min cadence while listed, written on >= 5% moves or every 30 min), not candles or trades: intrabar spikes and dips are invisible, so stops and take-profits fill at the next recorded price.',
|
|
@@ -31,9 +32,12 @@ export const DATA_LIMITS = [
|
|
|
31
32
|
];
|
|
32
33
|
/** A closed trade at or below this is counted as a rug (the challenge's rugAtPct). */
|
|
33
34
|
export const RUG_PCT = -80;
|
|
34
|
-
export async function backtest(strategy, tape, sessions = null) {
|
|
35
|
+
export async function backtest(strategy, tape, sessions = null, opts = {}) {
|
|
36
|
+
// task-1748: a rug-proof strategy replays its tick gates + the pump.fun-mint proxy (the chain gates need live data).
|
|
37
|
+
const rp = strategy.entry.safety.rugproof;
|
|
38
|
+
const vet = opts.vet === null ? undefined : opts.vet ?? (rp ? rugproofTapeVet(rp) : undefined);
|
|
35
39
|
// task-1645: a strategy in mode real backtests as its paper twin (a backtest never emits a trade-fire signal).
|
|
36
|
-
const run = await runAutotrade({ strategy: strategy.mode === 'real' ? { ...strategy, mode: 'paper' } : strategy, tape, sessions, pacing: { mode: 'lockstep' } });
|
|
40
|
+
const run = await runAutotrade({ strategy: strategy.mode === 'real' ? { ...strategy, mode: 'paper' } : strategy, tape, sessions, pacing: { mode: 'lockstep' }, ...(vet ? { vet } : {}) });
|
|
37
41
|
return { report: report(strategy, tape, run), run };
|
|
38
42
|
}
|
|
39
43
|
export function report(s, tape, run) {
|
|
@@ -268,7 +268,7 @@ export class MemberGate extends SessionGate {
|
|
|
268
268
|
overDay() { const o = super.overDay(); if (o)
|
|
269
269
|
return o; const g = this.fleet.gate.overDay(); return g ? `fleet-${g}` : null; }
|
|
270
270
|
rateLimit(r) { super.rateLimit(r); this.fleet.rateLimit(r, this); }
|
|
271
|
-
block(reason) { super.block(reason); this.fleet.block(reason, this); }
|
|
271
|
+
block(reason, sessionId) { super.block(reason, sessionId); this.fleet.block(reason, this); }
|
|
272
272
|
paused() { return super.paused() || this.fleet.paused(); }
|
|
273
273
|
/** A reading another strategy's session made (the same Claude account). */
|
|
274
274
|
shareRateLimit(r) { super.rateLimit(r); }
|
|
@@ -543,7 +543,7 @@ export async function runFleet(o) {
|
|
|
543
543
|
l.done = runAutotrade({
|
|
544
544
|
...(adopt?.length ? { adopt } : {}),
|
|
545
545
|
strategy: spec.strategy, source: { name: o.tape?.source ?? o.source.name, ticks, startT: now }, sessions, gate, ledger, runId,
|
|
546
|
-
dossier: o.dossier?.(spec), vet: o.vet?.(spec), executor: execs.executors.get(id), verbose: o.verbose ?? true, onLine: (s) => say(`[${id}] ${s}`),
|
|
546
|
+
dossier: o.dossier?.(spec), vet: o.vet?.(spec), drain: o.drain?.(spec), executor: execs.executors.get(id), verbose: o.verbose ?? true, onLine: (s) => say(`[${id}] ${s}`),
|
|
547
547
|
pacing, ...(account ? { account, member: id } : {}),
|
|
548
548
|
safety: () => {
|
|
549
549
|
const g = o.globalSafety();
|
|
@@ -55,6 +55,8 @@ export * from "./trade-ledger.js";
|
|
|
55
55
|
export * from "./execution-port.js";
|
|
56
56
|
export * from "./chat/index.js";
|
|
57
57
|
export * from "./yolo.js";
|
|
58
|
+
export * from "./rugproof.js";
|
|
59
|
+
export * from "./rugproof-probe.js";
|
|
58
60
|
export * from "./signal.js";
|
|
59
61
|
export * from "./outbox.js";
|
|
60
62
|
export * from "./signal-channels.js";
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// dexterm autotrade — the rug-proof probes (task-1748): the network half of rugproof.ts. READ-ONLY, PAPER ONLY.
|
|
2
|
+
//
|
|
3
|
+
// DexScreener tokens/v1 the coin's deepest pool: dex, base / quote mint, creation time, liquidity keyless
|
|
4
|
+
// Solana RPC the PumpSwap pool account (creator, lp mint, vaults), the LP mint's supply and
|
|
5
|
+
// largest holders, the pool's newest transactions (Withdraw scan), the SOL vault public or the user's own key
|
|
6
|
+
// Jupiter lite quote token -> SOL for a given raw amount keyless
|
|
7
|
+
//
|
|
8
|
+
// No wallet, no key of any wallet, no signing, no send: only reads. An RPC URL may carry an api key: it is never
|
|
9
|
+
// printed (redactSecrets) and never written to the ledger. Every read has a timeout; a read that fails is `error`
|
|
10
|
+
// and the gate that needed it refuses (fail closed). The vet runs the gates in rugproof.ts order and stops at the first
|
|
11
|
+
// refusal, so a coin refused by a free gate costs no network call.
|
|
12
|
+
import { redactSecrets, base58 } from '../../data/src/index.js';
|
|
13
|
+
import { PUMPSWAP_PROGRAM, WSOL_MINT, drainVerdict, gateHolders, gateLiquidityRecheck, gateLpLocked, gatePoolAge, gatePoolStructure, gateSellRoute, gateWithdrawHistory, verdictOf, } from "./rugproof.js";
|
|
14
|
+
export const defaultFetchJson = (timeoutMs = 12_000) => async (url, init) => {
|
|
15
|
+
const r = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
16
|
+
let json = null;
|
|
17
|
+
try {
|
|
18
|
+
json = await r.json();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
json = null;
|
|
22
|
+
}
|
|
23
|
+
return { status: r.status, json };
|
|
24
|
+
};
|
|
25
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
|
+
const isObj = (x) => !!x && typeof x === 'object' && !Array.isArray(x);
|
|
27
|
+
const str = (x) => (typeof x === 'string' && x ? x : null);
|
|
28
|
+
const fin = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : null);
|
|
29
|
+
export class RugProbe {
|
|
30
|
+
id = 0;
|
|
31
|
+
d;
|
|
32
|
+
/** Per-run cache: a pool's structure does not change, its LP and history are re-read at most every 10 min. */
|
|
33
|
+
lpCache = new Map();
|
|
34
|
+
decimals = new Map();
|
|
35
|
+
rpcCalls = 0;
|
|
36
|
+
/** Backoff unit after a rate limit (ms); tests set 0. */
|
|
37
|
+
backoffMs;
|
|
38
|
+
constructor(d) { this.d = d; this.backoffMs = d.backoffMs ?? 1500; }
|
|
39
|
+
async rpc(method, params) {
|
|
40
|
+
if (!this.d.rpcUrl)
|
|
41
|
+
throw new Error('no RPC endpoint');
|
|
42
|
+
let r = { status: 0, json: null };
|
|
43
|
+
// A rate limit (HTTP 429 or JSON-RPC -32429) is retried twice with a backoff before the read counts as failed.
|
|
44
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
45
|
+
this.rpcCalls++;
|
|
46
|
+
try {
|
|
47
|
+
r = await this.d.fetchJson(this.d.rpcUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++this.id, method, params }) });
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new Error(redactSecrets(`rpc ${method}: ${e.message}`).slice(0, 200));
|
|
51
|
+
}
|
|
52
|
+
const limited = r.status === 429 || (isObj(r.json) && isObj(r.json.error) && r.json.error.code === -32429);
|
|
53
|
+
if (!limited || attempt === 2)
|
|
54
|
+
break;
|
|
55
|
+
await sleep(this.backoffMs * (attempt + 1));
|
|
56
|
+
}
|
|
57
|
+
if (!isObj(r.json))
|
|
58
|
+
throw new Error(`rpc ${method}: HTTP ${r.status}`);
|
|
59
|
+
if (r.json.error)
|
|
60
|
+
throw new Error(redactSecrets(`rpc ${method}: ${JSON.stringify(r.json.error)}`).slice(0, 200));
|
|
61
|
+
return r.json.result;
|
|
62
|
+
}
|
|
63
|
+
pairCache = new Map();
|
|
64
|
+
scanCache = new Map();
|
|
65
|
+
/** The coin's deepest DexScreener pool (shared for 30 s: every fleet member asks about the same coin). */
|
|
66
|
+
pair(mint, now = Date.now()) {
|
|
67
|
+
const hit = this.pairCache.get(mint);
|
|
68
|
+
if (hit && now - hit.t < 30_000)
|
|
69
|
+
return hit.v;
|
|
70
|
+
const v = this.readPair(mint);
|
|
71
|
+
this.pairCache.set(mint, { t: now, v });
|
|
72
|
+
v.catch(() => this.pairCache.delete(mint));
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
async readPair(mint) {
|
|
76
|
+
const r = await this.d.fetchJson(`${this.d.dexscreenerBase ?? 'https://api.dexscreener.com'}/tokens/v1/solana/${mint}`);
|
|
77
|
+
if (!Array.isArray(r.json))
|
|
78
|
+
throw new Error(`dexscreener HTTP ${r.status}`);
|
|
79
|
+
const pairs = r.json.filter((p) => isObj(p) && str(p.pairAddress)).sort((a, b) => (fin(b.liquidity?.usd) ?? 0) - (fin(a.liquidity?.usd) ?? 0));
|
|
80
|
+
const p = pairs[0];
|
|
81
|
+
if (!p)
|
|
82
|
+
return null;
|
|
83
|
+
return { address: p.pairAddress, dex: str(p.dexId) ?? 'unknown', baseMint: str(p.baseToken?.address) ?? '', quoteMint: str(p.quoteToken?.address) ?? '', createdAt: fin(p.pairCreatedAt), liqUsd: fin(p.liquidity?.usd) };
|
|
84
|
+
}
|
|
85
|
+
/** The PumpSwap pool account + its LP mint (supply, largest holders' owners). null = not a PumpSwap account. */
|
|
86
|
+
async poolLp(pool, now = Date.now()) {
|
|
87
|
+
const hit = this.lpCache.get(pool);
|
|
88
|
+
if (hit && now - hit.t < 10 * 60e3 && hit.v !== 'error')
|
|
89
|
+
return hit.v;
|
|
90
|
+
let v;
|
|
91
|
+
try {
|
|
92
|
+
v = await this.readPoolLp(pool);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
v = 'error';
|
|
96
|
+
}
|
|
97
|
+
this.lpCache.set(pool, { t: now, v });
|
|
98
|
+
return v;
|
|
99
|
+
}
|
|
100
|
+
async readPoolLp(pool) {
|
|
101
|
+
const acc = await this.rpc('getAccountInfo', [pool, { encoding: 'base64' }]);
|
|
102
|
+
const value = isObj(acc) ? acc.value : null;
|
|
103
|
+
if (!value)
|
|
104
|
+
return null;
|
|
105
|
+
if (value.owner !== PUMPSWAP_PROGRAM)
|
|
106
|
+
return null;
|
|
107
|
+
const data = Buffer.from(String(value.data[0] ?? ''), 'base64');
|
|
108
|
+
const lp = decodePumpSwapPool(data);
|
|
109
|
+
const sup = await this.rpc('getTokenSupply', [lp.lpMint]);
|
|
110
|
+
const lpSupply = BigInt(String((sup?.value?.amount) ?? '0'));
|
|
111
|
+
let holders = [];
|
|
112
|
+
if (lpSupply > 0n) {
|
|
113
|
+
const big = await this.rpc('getTokenLargestAccounts', [lp.lpMint]);
|
|
114
|
+
const rows = ((big?.value ?? []).filter(isObj)).slice(0, 5);
|
|
115
|
+
const accs = rows.length ? await this.rpc('getMultipleAccounts', [rows.map((x) => x.address), { encoding: 'jsonParsed' }]) : null;
|
|
116
|
+
const vals = accs?.value ?? [];
|
|
117
|
+
holders = rows.map((x, i) => {
|
|
118
|
+
const owner = str(vals[i]?.data?.parsed?.info?.owner) ?? 'unknown';
|
|
119
|
+
return { owner, amount: BigInt(String(x.amount ?? '0')) };
|
|
120
|
+
}).sort((a, b) => (b.amount > a.amount ? 1 : b.amount < a.amount ? -1 : 0));
|
|
121
|
+
}
|
|
122
|
+
return { ...lp, lpSupply, holders };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `Withdraw` instructions among the pool's newest `n` successful transactions (and how many the creator signed).
|
|
126
|
+
* Fails closed: one transaction that cannot be read (after the rpc's rate-limit retries) could be the creator's
|
|
127
|
+
* Withdraw, so any unread one makes the whole scan `error` (the gate refuses, the vet looks again in 5 min).
|
|
128
|
+
*/
|
|
129
|
+
async withdraws(pool, creator, n, now = Date.now()) {
|
|
130
|
+
const hit = this.scanCache.get(pool);
|
|
131
|
+
if (hit && now - hit.t < 10 * 60e3)
|
|
132
|
+
return hit.v;
|
|
133
|
+
const v = await this.scanWithdraws(pool, creator, n);
|
|
134
|
+
if (v !== 'error')
|
|
135
|
+
this.scanCache.set(pool, { t: now, v });
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
async scanWithdraws(pool, creator, n) {
|
|
139
|
+
try {
|
|
140
|
+
const sigs = await this.rpc('getSignaturesForAddress', [pool, { limit: Math.min(100, n * 2) }]);
|
|
141
|
+
const ok = (Array.isArray(sigs) ? sigs : []).filter((x) => isObj(x) && !x.err).slice(0, n);
|
|
142
|
+
let withdraws = 0;
|
|
143
|
+
let byCreator = 0;
|
|
144
|
+
let scanned = 0;
|
|
145
|
+
for (const s of ok) {
|
|
146
|
+
// maxSupportedTransactionVersion 1: Helius answers -32015 for a version-1 transaction under 0 (task-1394).
|
|
147
|
+
let tx = null;
|
|
148
|
+
try {
|
|
149
|
+
tx = await this.rpc('getTransaction', [s.signature, { encoding: 'jsonParsed', maxSupportedTransactionVersion: 1 }]);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return 'error';
|
|
153
|
+
}
|
|
154
|
+
if (!isObj(tx))
|
|
155
|
+
return 'error';
|
|
156
|
+
scanned++;
|
|
157
|
+
const logs = tx.meta?.logMessages ?? [];
|
|
158
|
+
if (!logs.some((l) => l === 'Program log: Instruction: Withdraw'))
|
|
159
|
+
continue;
|
|
160
|
+
withdraws++;
|
|
161
|
+
const keys = tx.transaction?.message?.accountKeys ?? [];
|
|
162
|
+
const signer = keys.map((k) => (isObj(k) ? k.pubkey : k))[0];
|
|
163
|
+
if (signer === creator)
|
|
164
|
+
byCreator++;
|
|
165
|
+
}
|
|
166
|
+
return { scanned, withdraws, byCreator };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return 'error';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async tokenDecimals(mint) {
|
|
173
|
+
const hit = this.decimals.get(mint);
|
|
174
|
+
if (hit !== undefined)
|
|
175
|
+
return hit;
|
|
176
|
+
try {
|
|
177
|
+
const sup = await this.rpc('getTokenSupply', [mint]);
|
|
178
|
+
const d = fin(sup?.value?.decimals);
|
|
179
|
+
if (d !== null) {
|
|
180
|
+
this.decimals.set(mint, d);
|
|
181
|
+
return d;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* below */ }
|
|
185
|
+
// pump.fun mints are 6 decimals by construction.
|
|
186
|
+
return mint.endsWith('pump') ? 6 : null;
|
|
187
|
+
}
|
|
188
|
+
/** Jupiter token -> SOL quote for `raw` units. HTTP 429 / a network failure is not a quote error. */
|
|
189
|
+
async route(mint, raw) {
|
|
190
|
+
if (raw <= 0n)
|
|
191
|
+
return { ok: false, kind: 'no-route', error: 'zero amount' };
|
|
192
|
+
let r = { status: 0, json: null };
|
|
193
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
194
|
+
try {
|
|
195
|
+
r = await this.d.fetchJson(`${this.d.jupiterBase ?? 'https://lite-api.jup.ag'}/swap/v1/quote?inputMint=${mint}&outputMint=${WSOL_MINT}&amount=${raw}&slippageBps=1000`);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
return { ok: false, kind: 'network', error: e.message.slice(0, 120) };
|
|
199
|
+
}
|
|
200
|
+
if (r.status !== 429 || attempt === 2)
|
|
201
|
+
break;
|
|
202
|
+
await sleep(this.backoffMs * (attempt + 1));
|
|
203
|
+
}
|
|
204
|
+
if (r.status === 429)
|
|
205
|
+
return { ok: false, kind: 'rate-limited', error: 'HTTP 429' };
|
|
206
|
+
const j = isObj(r.json) ? r.json : {};
|
|
207
|
+
if (str(j.outAmount) && !j.errorCode && !j.error)
|
|
208
|
+
return { ok: true, outLamports: BigInt(j.outAmount), impactPct: fin(Number(j.priceImpactPct)) === null ? null : Math.round(Number(j.priceImpactPct) * 10000) / 100 };
|
|
209
|
+
if (r.status >= 500 && !j.errorCode)
|
|
210
|
+
return { ok: false, kind: 'network', error: `HTTP ${r.status}` };
|
|
211
|
+
return { ok: false, kind: 'no-route', error: String(j.errorCode ?? j.error ?? `HTTP ${r.status}`).slice(0, 120) };
|
|
212
|
+
}
|
|
213
|
+
/** The pool's SOL vault balance (SOL). */
|
|
214
|
+
async vaultSol(vault) {
|
|
215
|
+
try {
|
|
216
|
+
const b = await this.rpc('getTokenAccountBalance', [vault]);
|
|
217
|
+
const v = b?.value ?? {};
|
|
218
|
+
return fin(v.uiAmount) ?? (str(v.amount) ? Number(v.amount) / 1e9 : null);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** PumpSwap `Pool`: disc 8, bump u8, index u16, creator, base_mint, quote_mint, lp_mint, base vault, quote vault, lp_supply u64, coin_creator. */
|
|
226
|
+
export function decodePumpSwapPool(d) {
|
|
227
|
+
if (d.length < 211)
|
|
228
|
+
throw new Error(`PumpSwap pool account too short (${d.length} bytes)`);
|
|
229
|
+
const key = (o) => base58(d.subarray(o, o + 32));
|
|
230
|
+
return { creator: key(11), baseMint: key(43), quoteMint: key(75), lpMint: key(107), baseVault: key(139), quoteVault: key(171) };
|
|
231
|
+
}
|
|
232
|
+
// ───────────────────────────── the vet ─────────────────────────────
|
|
233
|
+
export const UNKNOWN_RETRY_MS = 5 * 60e3;
|
|
234
|
+
/**
|
|
235
|
+
* The strategy's rug-proof gates as a runner vet: free gates first (tick + history), then the pair, the pool account,
|
|
236
|
+
* the withdraw scan and the Jupiter route. The first refusal ends it; a time gate asks to be retried.
|
|
237
|
+
*/
|
|
238
|
+
export function rugproofVet(r, o) {
|
|
239
|
+
return async (k, ctx) => {
|
|
240
|
+
const results = [];
|
|
241
|
+
const now = k.t;
|
|
242
|
+
const done = (x) => {
|
|
243
|
+
results.push(x);
|
|
244
|
+
if (x.pass)
|
|
245
|
+
return null;
|
|
246
|
+
o.log?.({ mint: k.mint, symbol: k.symbol, gate: x.gate, rule: x.rule, detail: x.detail, results: results.map((y) => `${y.gate}:${y.pass ? 'ok' : 'no'}`) });
|
|
247
|
+
// A read that failed (`-unknown`: a rate limit, an RPC outage) refuses now (fail closed) and looks again in 5 min,
|
|
248
|
+
// instead of skipping the coin for the rest of the run; a structural refusal is for good.
|
|
249
|
+
const retry = x.retryAfterMs ?? (x.rule.endsWith('-unknown') ? UNKNOWN_RETRY_MS : undefined);
|
|
250
|
+
return { ok: false, rule: x.rule, detail: x.detail, ...(retry !== undefined ? { retryAfterMs: retry } : {}) };
|
|
251
|
+
};
|
|
252
|
+
const history = ctx?.history ?? [];
|
|
253
|
+
const sizeUsd = ctx?.sizeUsd ?? 0;
|
|
254
|
+
const floor = ctx?.exitFloorUsd ?? 2000;
|
|
255
|
+
// 1-3: free.
|
|
256
|
+
let f = done(gatePoolAge(r, k, undefined, now));
|
|
257
|
+
if (f)
|
|
258
|
+
return f;
|
|
259
|
+
f = done(gateHolders(k, ctx?.maxTop10Pct ?? null, ctx?.maxDevPct ?? null, ctx?.unknownHolders));
|
|
260
|
+
if (f)
|
|
261
|
+
return f;
|
|
262
|
+
f = done(gateLiquidityRecheck(r, history, k, floor));
|
|
263
|
+
if (f)
|
|
264
|
+
return f;
|
|
265
|
+
// 4: the pool as listed.
|
|
266
|
+
let pair = null;
|
|
267
|
+
let pairErr = null;
|
|
268
|
+
try {
|
|
269
|
+
pair = await o.probe.pair(k.mint);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
pairErr = e.message.slice(0, 160);
|
|
273
|
+
}
|
|
274
|
+
results.shift(); // the token-age pass above is replaced by the pool's own age
|
|
275
|
+
if (pairErr !== null)
|
|
276
|
+
return done({ gate: 'pool-structure', pass: false, rule: 'rugproof:pool-structure-unknown', detail: `the pool could not be read: ${pairErr}` });
|
|
277
|
+
f = done(gatePoolAge(r, k, pair ? pair.createdAt : null, now));
|
|
278
|
+
if (f)
|
|
279
|
+
return f;
|
|
280
|
+
f = done(gatePoolStructure(r, k.mint, k.launchpad, pair));
|
|
281
|
+
if (f)
|
|
282
|
+
return f;
|
|
283
|
+
// 5-6: the chain.
|
|
284
|
+
let lp = null;
|
|
285
|
+
if (r.requireSafePool && pair) {
|
|
286
|
+
lp = o.probe.d.rpcUrl ? await o.probe.poolLp(pair.address, now) : 'error';
|
|
287
|
+
f = done(gateLpLocked(r, pair, lp, k.mint));
|
|
288
|
+
if (f)
|
|
289
|
+
return f;
|
|
290
|
+
const poolLp = lp;
|
|
291
|
+
const scan = poolLp.lpSupply === 0n && r.maxWithdraws === 0 ? null : await o.probe.withdraws(pair.address, poolLp.creator, r.withdrawScanTx);
|
|
292
|
+
f = done(gateWithdrawHistory(r, scan, poolLp));
|
|
293
|
+
if (f)
|
|
294
|
+
return f;
|
|
295
|
+
}
|
|
296
|
+
// 7: a sell route at routeX x the size.
|
|
297
|
+
if (r.routeX !== null) {
|
|
298
|
+
const dec = await o.probe.tokenDecimals(k.mint);
|
|
299
|
+
let route = null;
|
|
300
|
+
if (dec !== null && k.price) {
|
|
301
|
+
const tokens = (sizeUsd * r.routeX) / k.price;
|
|
302
|
+
route = await o.probe.route(k.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** dec))));
|
|
303
|
+
}
|
|
304
|
+
f = done(gateSellRoute(r, route, sizeUsd));
|
|
305
|
+
if (f)
|
|
306
|
+
return f;
|
|
307
|
+
}
|
|
308
|
+
const v = verdictOf(r, results, pair?.createdAt ? now - pair.createdAt : null);
|
|
309
|
+
const note = `rugproof ok: ${results.map((x) => `${x.gate}${x.partial ? '~' : ''}`).join(', ')}${v.sizeCapPct !== null ? `; size cap ${v.sizeCapPct}% (young or partly verified pool)` : ''}`;
|
|
310
|
+
o.log?.({ mint: k.mint, symbol: k.symbol, pass: true, sizeCapPct: v.sizeCapPct, results: results.map((x) => `${x.gate}:${x.partial ? 'partial' : 'ok'}`) });
|
|
311
|
+
return { ok: true, note, recheck: true, ...(v.sizeCapPct !== null ? { sizeCapPct: v.sizeCapPct } : {}), ...(pair ? { pool: { address: pair.address, quoteVault: lp && lp !== 'error' ? lp.quoteVault : null } } : {}) };
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Polls every open position's pool (SOL vault when the pool is known, plus a Jupiter quote for the held amount)
|
|
316
|
+
* every pollSec, outside the candle / tick cadence. The runner reads `verdict()` on every step and sells the whole
|
|
317
|
+
* position on an exit verdict. Timers are unref'd: they never keep a process alive.
|
|
318
|
+
*/
|
|
319
|
+
export class DrainPoller {
|
|
320
|
+
w = new Map();
|
|
321
|
+
g;
|
|
322
|
+
probe;
|
|
323
|
+
log;
|
|
324
|
+
constructor(g, probe, log) { this.g = g; this.probe = probe; this.log = log; }
|
|
325
|
+
watch(p) {
|
|
326
|
+
if (this.w.has(p.positionId))
|
|
327
|
+
return;
|
|
328
|
+
const x = { pos: p, base: { reserveSol: null, liqUsd: p.entryLiqUsd }, timer: null, verdict: null, reads: 0, quoteErrors: 0, noRoute: 0, busy: false, decimals: null };
|
|
329
|
+
this.w.set(p.positionId, x);
|
|
330
|
+
void this.poll(x, true);
|
|
331
|
+
x.timer = setInterval(() => { void this.poll(x, false); }, Math.max(2, Math.min(10, this.g.pollSec)) * 1000);
|
|
332
|
+
x.timer.unref?.();
|
|
333
|
+
}
|
|
334
|
+
unwatch(positionId) {
|
|
335
|
+
const x = this.w.get(positionId);
|
|
336
|
+
if (x?.timer)
|
|
337
|
+
clearInterval(x.timer);
|
|
338
|
+
this.w.delete(positionId);
|
|
339
|
+
}
|
|
340
|
+
verdict(positionId) {
|
|
341
|
+
const v = this.w.get(positionId)?.verdict ?? null;
|
|
342
|
+
return v && v.exit ? v : null;
|
|
343
|
+
}
|
|
344
|
+
stop() { for (const id of [...this.w.keys()])
|
|
345
|
+
this.unwatch(id); }
|
|
346
|
+
/** One probe of one position (also called by tests). */
|
|
347
|
+
async poll(x, first) {
|
|
348
|
+
if (x.busy)
|
|
349
|
+
return;
|
|
350
|
+
x.busy = true;
|
|
351
|
+
try {
|
|
352
|
+
let vault = x.pos.quoteVault ?? null;
|
|
353
|
+
if (!vault && x.pos.poolAddress && this.probe.d.rpcUrl) {
|
|
354
|
+
const lp = await this.probe.poolLp(x.pos.poolAddress);
|
|
355
|
+
if (lp && lp !== 'error')
|
|
356
|
+
vault = x.pos.quoteVault = lp.quoteVault;
|
|
357
|
+
}
|
|
358
|
+
const reserveSol = vault && this.probe.d.rpcUrl ? await this.probe.vaultSol(vault) : null;
|
|
359
|
+
if (first || x.base.reserveSol === null) {
|
|
360
|
+
if (reserveSol !== null)
|
|
361
|
+
x.base.reserveSol = reserveSol;
|
|
362
|
+
}
|
|
363
|
+
x.decimals ??= await this.probe.tokenDecimals(x.pos.mint);
|
|
364
|
+
const tokens = x.pos.tokens();
|
|
365
|
+
const route = this.g.exitOnNoRoute && x.decimals !== null && tokens > 0 ? await this.probe.route(x.pos.mint, BigInt(Math.max(1, Math.round(tokens * 10 ** x.decimals)))) : null;
|
|
366
|
+
x.reads++;
|
|
367
|
+
if (route && !route.ok && route.kind !== 'no-route')
|
|
368
|
+
x.quoteErrors++;
|
|
369
|
+
let v = drainVerdict(this.g, x.base, { t: Date.now(), reserveSol, liqUsd: null, route });
|
|
370
|
+
// A quote error alone needs two reads in a row (one pollSec apart): a single odd answer is not a pull. A reserve
|
|
371
|
+
// read decides at once. The poller keeps polling after an exit: a route that comes back lifts the verdict.
|
|
372
|
+
x.noRoute = v.exit && v.reason === 'drain-no-route' ? x.noRoute + 1 : 0;
|
|
373
|
+
if (v.exit && v.reason === 'drain-no-route' && x.noRoute < 2)
|
|
374
|
+
v = { exit: false, note: 'no route once: re-checking' };
|
|
375
|
+
const was = x.verdict?.exit ?? false;
|
|
376
|
+
x.verdict = v;
|
|
377
|
+
if (v.exit && !was)
|
|
378
|
+
this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, drain: v.reason, detail: v.detail, reads: x.reads });
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
this.log?.({ positionId: x.pos.positionId, mint: x.pos.mint, probeError: e.message.slice(0, 160) });
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
x.busy = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
stats() {
|
|
388
|
+
let reads = 0;
|
|
389
|
+
let quoteErrors = 0;
|
|
390
|
+
for (const x of this.w.values()) {
|
|
391
|
+
reads += x.reads;
|
|
392
|
+
quoteErrors += x.quoteErrors;
|
|
393
|
+
}
|
|
394
|
+
return { watched: this.w.size, reads, quoteErrors };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** Run vets in order (the rug-proof gates first: they are the cheapest); the first refusal wins; notes and caps merge. */
|
|
398
|
+
export function chainVets(...vets) {
|
|
399
|
+
const vs = vets.filter((v) => !!v);
|
|
400
|
+
if (vs.length <= 1)
|
|
401
|
+
return vs[0];
|
|
402
|
+
return async (k, ctx) => {
|
|
403
|
+
const notes = [];
|
|
404
|
+
let out = { ok: true };
|
|
405
|
+
for (const v of vs) {
|
|
406
|
+
const r = await v(k, ctx);
|
|
407
|
+
if (!r.ok)
|
|
408
|
+
return r;
|
|
409
|
+
if (r.note)
|
|
410
|
+
notes.push(r.note);
|
|
411
|
+
out = { ...out, ...r, recheck: !!(out.ok && out.recheck) || !!r.recheck, ...(out.ok && out.sizeCapPct !== undefined && (r.sizeCapPct === undefined || out.sizeCapPct < r.sizeCapPct) ? { sizeCapPct: out.sizeCapPct } : {}) };
|
|
412
|
+
}
|
|
413
|
+
return notes.length ? { ...out, note: notes.join('; ') } : out;
|
|
414
|
+
};
|
|
415
|
+
}
|