@finchagentic/mcp 4.0.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/LICENSE +21 -0
- package/README.md +345 -0
- package/dist/_http-cache.js +96 -0
- package/dist/agent-loop.js +231 -0
- package/dist/annotations.js +113 -0
- package/dist/cli.js +1195 -0
- package/dist/clink-input.js +15 -0
- package/dist/config.js +132 -0
- package/dist/convex.js +151 -0
- package/dist/dex-pair.js +54 -0
- package/dist/enrichment-router.js +315 -0
- package/dist/index.js +256 -0
- package/dist/llm.js +323 -0
- package/dist/local-memory.js +102 -0
- package/dist/local-vault.js +454 -0
- package/dist/output-schemas.js +551 -0
- package/dist/prompts.js +111 -0
- package/dist/public-url.js +107 -0
- package/dist/resources.js +116 -0
- package/dist/server.js +300 -0
- package/dist/signal-gate.js +57 -0
- package/dist/token-decimals.js +26 -0
- package/dist/token-gate.js +88 -0
- package/dist/tool-filter.js +44 -0
- package/dist/tools/_solidity-scan.js +313 -0
- package/dist/tools/agents.js +729 -0
- package/dist/tools/automation.js +314 -0
- package/dist/tools/base-mcp.js +478 -0
- package/dist/tools/base.js +269 -0
- package/dist/tools/chronicle.js +268 -0
- package/dist/tools/coder.js +94 -0
- package/dist/tools/deep-research.js +1416 -0
- package/dist/tools/defi.js +291 -0
- package/dist/tools/equity.js +364 -0
- package/dist/tools/events.js +182 -0
- package/dist/tools/framework.js +150 -0
- package/dist/tools/github.js +514 -0
- package/dist/tools/insider.js +264 -0
- package/dist/tools/insight.js +634 -0
- package/dist/tools/market.js +555 -0
- package/dist/tools/memory.js +1046 -0
- package/dist/tools/miroshark.js +343 -0
- package/dist/tools/monitor.js +319 -0
- package/dist/tools/os.js +226 -0
- package/dist/tools/packets.js +296 -0
- package/dist/tools/research-chain.js +226 -0
- package/dist/tools/research-compare.js +280 -0
- package/dist/tools/research.js +188 -0
- package/dist/tools/rh-bridge.js +148 -0
- package/dist/tools/rh-mcp.js +1411 -0
- package/dist/tools/rh-orders.js +471 -0
- package/dist/tools/scanner.js +534 -0
- package/dist/tools/vault.js +764 -0
- package/dist/tools/wallet.js +200 -0
- package/dist/types.js +2 -0
- package/dist/wallet.js +184 -0
- package/package.json +87 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Robinhood Chain automated orders — DCA / TP / SL (Phase 2).
|
|
3
|
+
//
|
|
4
|
+
// Self-contained order engine layered on the tested `rh_mcp_swap` path.
|
|
5
|
+
// Orders persist locally at ~/.finch/rh-orders.json. Execution is opt-in:
|
|
6
|
+
// `rh_orders_tick` previews by default and only trades when execute:true, and
|
|
7
|
+
// never when the kill-switch is set (env RH_ORDERS_DISABLED=1 or the file
|
|
8
|
+
// ~/.finch/rh-orders.OFF exists). DCA is bounded by a hard maxSpendEth;
|
|
9
|
+
// TP/SL only ever SELL tokens the wallet already holds.
|
|
10
|
+
//
|
|
11
|
+
// An always-on scheduler must call `rh_orders_tick {execute:true}` periodically
|
|
12
|
+
// (Convex cron via the CLI, Windows Task Scheduler, or the scheduled-tasks MCP).
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.RH_ORDER_TOOLS = void 0;
|
|
18
|
+
exports.buildOrdersList = buildOrdersList;
|
|
19
|
+
exports.handleRhOrderTool = handleRhOrderTool;
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const os_1 = __importDefault(require("os"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const ethers_1 = require("ethers");
|
|
24
|
+
const wallet_js_1 = require("../wallet.js");
|
|
25
|
+
const rh_mcp_js_1 = require("./rh-mcp.js");
|
|
26
|
+
const ORDERS_DIR = path_1.default.join(os_1.default.homedir(), ".finch");
|
|
27
|
+
const ORDERS_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.json");
|
|
28
|
+
const KILL_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.OFF");
|
|
29
|
+
function textResult(text, isError = false) {
|
|
30
|
+
return { content: [{ type: "text", text }], isError };
|
|
31
|
+
}
|
|
32
|
+
function loadStore() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs_1.default.readFileSync(ORDERS_FILE, "utf8");
|
|
35
|
+
const s = JSON.parse(raw);
|
|
36
|
+
if (s && Array.isArray(s.orders))
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* fresh store */
|
|
41
|
+
}
|
|
42
|
+
return { version: 1, orders: [] };
|
|
43
|
+
}
|
|
44
|
+
function saveStore(s) {
|
|
45
|
+
try {
|
|
46
|
+
fs_1.default.mkdirSync(ORDERS_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* ignore */
|
|
50
|
+
}
|
|
51
|
+
// Atomic write: a crash mid-write must not corrupt the order store.
|
|
52
|
+
const tmp = `${ORDERS_FILE}.tmp`;
|
|
53
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(s, null, 2));
|
|
54
|
+
fs_1.default.renameSync(tmp, ORDERS_FILE);
|
|
55
|
+
}
|
|
56
|
+
function genId(prefix) {
|
|
57
|
+
return `${prefix}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
58
|
+
}
|
|
59
|
+
function killSwitchOn() {
|
|
60
|
+
return process.env.RH_ORDERS_DISABLED === "1" || fs_1.default.existsSync(KILL_FILE);
|
|
61
|
+
}
|
|
62
|
+
function posNum(v) {
|
|
63
|
+
const n = Number(v);
|
|
64
|
+
return isFinite(n) ? n : NaN;
|
|
65
|
+
}
|
|
66
|
+
function fmtUsd(n) {
|
|
67
|
+
if (n == null || !isFinite(n))
|
|
68
|
+
return "?";
|
|
69
|
+
if (n >= 1)
|
|
70
|
+
return "$" + n.toLocaleString(undefined, { maximumFractionDigits: 4 });
|
|
71
|
+
return "$" + n.toPrecision(4);
|
|
72
|
+
}
|
|
73
|
+
function extractTxHash(text) {
|
|
74
|
+
// Target the swap tx specifically — sells print "Permit2 approvals: 0x…" BEFORE
|
|
75
|
+
// "Tx: 0x…", so a naive first-match would record the approval hash instead.
|
|
76
|
+
const tx = text.match(/Tx:\s*`?(0x[a-fA-F0-9]{64})/i);
|
|
77
|
+
if (tx)
|
|
78
|
+
return tx[1];
|
|
79
|
+
const any = text.match(/0x[a-fA-F0-9]{64}/);
|
|
80
|
+
return any ? any[0] : null;
|
|
81
|
+
}
|
|
82
|
+
exports.RH_ORDER_TOOLS = [
|
|
83
|
+
{
|
|
84
|
+
name: "rh_dca_create",
|
|
85
|
+
description: "Robinhood Chain — create a DCA plan: buy a fixed ETH amount of a token every N hours, " +
|
|
86
|
+
"up to a total number of buys, capped by maxSpendEth. Token by catalog symbol, crypto " +
|
|
87
|
+
"ticker, or 0x contract address (resolved via DexScreener). Does NOT execute now — the " +
|
|
88
|
+
"scheduler runs it via rh_orders_tick. Always confirm the resolved contract address first.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
token: { type: "string", description: "Ticker or 0x contract address to accumulate" },
|
|
93
|
+
amountEthPerBuy: { type: "string", description: "ETH spent per buy, e.g. '0.01'" },
|
|
94
|
+
intervalHours: { type: "number", description: "Hours between buys (e.g. 24 for daily)" },
|
|
95
|
+
totalBuys: { type: "number", description: "How many buys total" },
|
|
96
|
+
maxSpendEth: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Hard cap on total ETH spent (default amountEthPerBuy × totalBuys)",
|
|
99
|
+
},
|
|
100
|
+
slippagePct: { type: "number", description: "Slippage % per buy (default 3.0)" },
|
|
101
|
+
note: { type: "string", description: "Optional label" },
|
|
102
|
+
},
|
|
103
|
+
required: ["token", "amountEthPerBuy", "intervalHours", "totalBuys"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "rh_bracket_create",
|
|
108
|
+
description: "Robinhood Chain — set take-profit and/or stop-loss on a token you HOLD. When DexScreener " +
|
|
109
|
+
"price crosses tpPriceUsd (≥) or slPriceUsd (≤), the scheduler sells sellPct% of your " +
|
|
110
|
+
"current balance for ETH via rh_orders_tick. Provide at least one of tpPriceUsd / slPriceUsd. " +
|
|
111
|
+
"Only ever sells tokens already in the wallet — never borrows or shorts.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
token: { type: "string", description: "Ticker or 0x contract address you hold" },
|
|
116
|
+
tpPriceUsd: { type: "number", description: "Take-profit price in USD (sell when ≥)" },
|
|
117
|
+
slPriceUsd: { type: "number", description: "Stop-loss price in USD (sell when ≤)" },
|
|
118
|
+
sellPct: { type: "number", description: "Percent of balance to sell on trigger (default 100)" },
|
|
119
|
+
slippagePct: { type: "number", description: "Slippage % on the sell (default 5.0)" },
|
|
120
|
+
note: { type: "string", description: "Optional label" },
|
|
121
|
+
},
|
|
122
|
+
required: ["token"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "rh_orders_list",
|
|
127
|
+
description: "Robinhood Chain — list saved DCA / TP / SL orders and their progress. Optional status filter.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
status: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Filter: active | completed | cancelled | error (default all)",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: [],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "rh_order_cancel",
|
|
141
|
+
description: "Robinhood Chain — cancel a saved order by id (stops future DCA buys / TP-SL fills).",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: { id: { type: "string", description: "Order id (rho_… / rhb_…)" } },
|
|
145
|
+
required: ["id"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "rh_orders_tick",
|
|
150
|
+
description: "Robinhood Chain — evaluate all active orders and act on any that are due (DCA interval " +
|
|
151
|
+
"reached) or triggered (TP/SL price crossed). PREVIEW by default; pass execute:true to " +
|
|
152
|
+
"broadcast real swaps. Refuses to execute when the kill-switch is set (RH_ORDERS_DISABLED=1 " +
|
|
153
|
+
"or ~/.finch/rh-orders.OFF). This is the entry point an always-on scheduler calls.",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
execute: {
|
|
158
|
+
type: "boolean",
|
|
159
|
+
description: "true = broadcast real swaps; false/omitted = dry-run preview only",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: [],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
// Structured output builder for rh_orders_list (schema in output-schemas.ts).
|
|
167
|
+
function buildOrdersList(orders) {
|
|
168
|
+
return {
|
|
169
|
+
count: orders.length,
|
|
170
|
+
orders: orders.map((o) => ({
|
|
171
|
+
id: o.id,
|
|
172
|
+
type: o.type ?? null,
|
|
173
|
+
status: o.status ?? null,
|
|
174
|
+
symbol: o.token?.symbol ?? null,
|
|
175
|
+
address: o.token?.address ?? null,
|
|
176
|
+
dca: o.dca ?? null,
|
|
177
|
+
bracket: o.bracket ?? null,
|
|
178
|
+
lastError: o.lastError ?? null,
|
|
179
|
+
})),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function handleRhOrderTool(name, args) {
|
|
183
|
+
const a = (args ?? {});
|
|
184
|
+
switch (name) {
|
|
185
|
+
case "rh_dca_create": {
|
|
186
|
+
const amountEth = posNum(a.amountEthPerBuy);
|
|
187
|
+
const intervalHours = posNum(a.intervalHours);
|
|
188
|
+
const totalBuys = Math.floor(posNum(a.totalBuys));
|
|
189
|
+
if (!(amountEth > 0))
|
|
190
|
+
return textResult("amountEthPerBuy must be > 0", true);
|
|
191
|
+
if (!(intervalHours > 0))
|
|
192
|
+
return textResult("intervalHours must be > 0", true);
|
|
193
|
+
if (!(totalBuys > 0))
|
|
194
|
+
return textResult("totalBuys must be ≥ 1", true);
|
|
195
|
+
let resolved;
|
|
196
|
+
try {
|
|
197
|
+
resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
return textResult(`Token resolve failed: ${e?.message ?? e}`, true);
|
|
201
|
+
}
|
|
202
|
+
if (resolved.kind !== "token")
|
|
203
|
+
return textResult("token must be a stock/crypto, not ETH", true);
|
|
204
|
+
const defaultCap = amountEth * totalBuys;
|
|
205
|
+
const maxSpendEth = a.maxSpendEth != null ? posNum(a.maxSpendEth) : defaultCap;
|
|
206
|
+
if (!(maxSpendEth > 0))
|
|
207
|
+
return textResult("maxSpendEth must be > 0", true);
|
|
208
|
+
if (maxSpendEth < amountEth) {
|
|
209
|
+
return textResult(`maxSpendEth (${maxSpendEth}) is below amountEthPerBuy (${amountEth}) — no buy could ever fire.`, true);
|
|
210
|
+
}
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
const order = {
|
|
213
|
+
id: genId("rho"),
|
|
214
|
+
type: "dca",
|
|
215
|
+
status: "active",
|
|
216
|
+
token: { address: resolved.address, symbol: resolved.symbol, decimals: resolved.decimals },
|
|
217
|
+
slippagePct: posNum(a.slippagePct) > 0 ? posNum(a.slippagePct) : 3.0,
|
|
218
|
+
note: a.note ? String(a.note) : undefined,
|
|
219
|
+
createdAt: now,
|
|
220
|
+
updatedAt: now,
|
|
221
|
+
dca: {
|
|
222
|
+
amountEthPerBuy: String(amountEth),
|
|
223
|
+
intervalSec: Math.round(intervalHours * 3600),
|
|
224
|
+
totalBuys,
|
|
225
|
+
completedBuys: 0,
|
|
226
|
+
nextRunAt: now, // first buy on the next tick
|
|
227
|
+
maxSpendEth: String(maxSpendEth),
|
|
228
|
+
spentEth: "0",
|
|
229
|
+
fills: [],
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
const store = loadStore();
|
|
233
|
+
store.orders.push(order);
|
|
234
|
+
saveStore(store);
|
|
235
|
+
return textResult([
|
|
236
|
+
`## ✅ DCA created — ${order.id}`,
|
|
237
|
+
`Buy **${amountEth} ETH → ${resolved.symbol}** every **${intervalHours}h**, **${totalBuys}×** (cap **${maxSpendEth} ETH**).`,
|
|
238
|
+
`\`${resolved.address}\` · slippage ${order.slippagePct}%`,
|
|
239
|
+
``,
|
|
240
|
+
`First buy fires on the next \`rh_orders_tick {execute:true}\`. Preview anytime with \`rh_orders_tick\`.`,
|
|
241
|
+
`⚠️ Autonomous execution needs a scheduler calling the tick. Kill-switch: create \`~/.finch/rh-orders.OFF\`.`,
|
|
242
|
+
].join("\n"));
|
|
243
|
+
}
|
|
244
|
+
case "rh_bracket_create": {
|
|
245
|
+
const hasTp = a.tpPriceUsd != null;
|
|
246
|
+
const hasSl = a.slPriceUsd != null;
|
|
247
|
+
if (!hasTp && !hasSl)
|
|
248
|
+
return textResult("Provide at least one of tpPriceUsd / slPriceUsd", true);
|
|
249
|
+
const tp = hasTp ? posNum(a.tpPriceUsd) : undefined;
|
|
250
|
+
const sl = hasSl ? posNum(a.slPriceUsd) : undefined;
|
|
251
|
+
if (hasTp && !(tp > 0))
|
|
252
|
+
return textResult("tpPriceUsd must be > 0", true);
|
|
253
|
+
if (hasSl && !(sl > 0))
|
|
254
|
+
return textResult("slPriceUsd must be > 0", true);
|
|
255
|
+
if (hasTp && hasSl && sl >= tp)
|
|
256
|
+
return textResult("slPriceUsd must be below tpPriceUsd", true);
|
|
257
|
+
const sellPct = a.sellPct != null ? posNum(a.sellPct) : 100;
|
|
258
|
+
if (!(sellPct > 0 && sellPct <= 100))
|
|
259
|
+
return textResult("sellPct must be 1..100", true);
|
|
260
|
+
let resolved;
|
|
261
|
+
try {
|
|
262
|
+
resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
return textResult(`Token resolve failed: ${e?.message ?? e}`, true);
|
|
266
|
+
}
|
|
267
|
+
if (resolved.kind !== "token")
|
|
268
|
+
return textResult("token must be a stock/crypto, not ETH", true);
|
|
269
|
+
const entry = await (0, rh_mcp_js_1.rhPriceUsd)(resolved.address);
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const order = {
|
|
272
|
+
id: genId("rhb"),
|
|
273
|
+
type: "bracket",
|
|
274
|
+
status: "active",
|
|
275
|
+
token: { address: resolved.address, symbol: resolved.symbol, decimals: resolved.decimals },
|
|
276
|
+
slippagePct: posNum(a.slippagePct) > 0 ? posNum(a.slippagePct) : 5.0,
|
|
277
|
+
note: a.note ? String(a.note) : undefined,
|
|
278
|
+
createdAt: now,
|
|
279
|
+
updatedAt: now,
|
|
280
|
+
bracket: { entryPriceUsd: entry, tpPriceUsd: tp, slPriceUsd: sl, sellPct, fills: [] },
|
|
281
|
+
};
|
|
282
|
+
const store = loadStore();
|
|
283
|
+
store.orders.push(order);
|
|
284
|
+
saveStore(store);
|
|
285
|
+
return textResult([
|
|
286
|
+
`## ✅ TP/SL created — ${order.id}`,
|
|
287
|
+
`Token **${resolved.symbol}** \`${resolved.address}\``,
|
|
288
|
+
`Now: ${fmtUsd(entry)}${tp != null ? ` · 🎯 TP ${fmtUsd(tp)}` : ""}${sl != null ? ` · 🛑 SL ${fmtUsd(sl)}` : ""}`,
|
|
289
|
+
`Sells **${sellPct}%** of balance on trigger · slippage ${order.slippagePct}%`,
|
|
290
|
+
``,
|
|
291
|
+
`Fires via \`rh_orders_tick {execute:true}\` when price crosses. Only sells tokens you already hold.`,
|
|
292
|
+
].join("\n"));
|
|
293
|
+
}
|
|
294
|
+
case "rh_orders_list": {
|
|
295
|
+
const filter = a.status ? String(a.status) : null;
|
|
296
|
+
const store = loadStore();
|
|
297
|
+
const orders = filter ? store.orders.filter((o) => o.status === filter) : store.orders;
|
|
298
|
+
if (!orders.length)
|
|
299
|
+
return { content: [{ type: "text", text: `No orders${filter ? ` with status ${filter}` : ""}.` }], structuredContent: buildOrdersList(orders) };
|
|
300
|
+
const lines = [`## 📋 RH orders (${orders.length})`, ""];
|
|
301
|
+
for (const o of orders) {
|
|
302
|
+
if (o.type === "dca" && o.dca) {
|
|
303
|
+
lines.push(`**${o.id}** · DCA · ${o.status}`, ` ${o.dca.amountEthPerBuy} ETH → ${o.token.symbol} every ${(o.dca.intervalSec / 3600).toFixed(1)}h · ${o.dca.completedBuys}/${o.dca.totalBuys} done · spent ${o.dca.spentEth}/${o.dca.maxSpendEth} ETH`, ` next: ${o.status === "active" ? new Date(o.dca.nextRunAt).toISOString() : "—"}${o.lastError ? ` · ⚠️ ${o.lastError}` : ""}`);
|
|
304
|
+
}
|
|
305
|
+
else if (o.type === "bracket" && o.bracket) {
|
|
306
|
+
const b = o.bracket;
|
|
307
|
+
lines.push(`**${o.id}** · TP/SL · ${o.status}`, ` ${o.token.symbol}${b.tpPriceUsd != null ? ` · 🎯 ${fmtUsd(b.tpPriceUsd)}` : ""}${b.slPriceUsd != null ? ` · 🛑 ${fmtUsd(b.slPriceUsd)}` : ""} · sell ${b.sellPct}% · entry ${fmtUsd(b.entryPriceUsd)}`, ` fills: ${b.fills.length}${o.lastError ? ` · ⚠️ ${o.lastError}` : ""}`);
|
|
308
|
+
}
|
|
309
|
+
lines.push(` \`${o.token.address}\``);
|
|
310
|
+
}
|
|
311
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildOrdersList(orders) };
|
|
312
|
+
}
|
|
313
|
+
case "rh_order_cancel": {
|
|
314
|
+
const id = String(a.id ?? "").trim();
|
|
315
|
+
if (!id)
|
|
316
|
+
return textResult("Required: id", true);
|
|
317
|
+
const store = loadStore();
|
|
318
|
+
const o = store.orders.find((x) => x.id === id);
|
|
319
|
+
if (!o)
|
|
320
|
+
return textResult(`Order ${id} not found.`, true);
|
|
321
|
+
if (o.status === "active")
|
|
322
|
+
o.status = "cancelled";
|
|
323
|
+
o.updatedAt = Date.now();
|
|
324
|
+
saveStore(store);
|
|
325
|
+
return textResult(`🚫 Order ${id} cancelled.`);
|
|
326
|
+
}
|
|
327
|
+
case "rh_orders_tick": {
|
|
328
|
+
const execute = a.execute === true;
|
|
329
|
+
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
|
+
const doExecute = execute && !blocked;
|
|
339
|
+
let acted = 0;
|
|
340
|
+
let wallet = null;
|
|
341
|
+
for (const o of active) {
|
|
342
|
+
try {
|
|
343
|
+
// ---- DCA ----
|
|
344
|
+
if (o.type === "dca" && o.dca) {
|
|
345
|
+
const d = o.dca;
|
|
346
|
+
if (d.completedBuys >= d.totalBuys) {
|
|
347
|
+
o.status = "completed";
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (now < d.nextRunAt) {
|
|
351
|
+
report.push(`⏳ ${o.id} DCA ${o.token.symbol}: next buy at ${new Date(d.nextRunAt).toISOString()}`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const wouldSpend = Number(d.spentEth) + Number(d.amountEthPerBuy);
|
|
355
|
+
if (wouldSpend > Number(d.maxSpendEth) + 1e-12) {
|
|
356
|
+
o.status = "completed";
|
|
357
|
+
report.push(`✅ ${o.id} DCA ${o.token.symbol}: maxSpend reached — completed`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
acted++;
|
|
361
|
+
if (!doExecute) {
|
|
362
|
+
report.push(`🟡 ${o.id} DCA: would buy ${d.amountEthPerBuy} ETH → ${o.token.symbol} (${d.completedBuys + 1}/${d.totalBuys})`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
|
|
366
|
+
fromToken: "ETH",
|
|
367
|
+
toToken: o.token.address,
|
|
368
|
+
amount: d.amountEthPerBuy,
|
|
369
|
+
maxSlippagePct: o.slippagePct,
|
|
370
|
+
confirm: true,
|
|
371
|
+
});
|
|
372
|
+
const txt = res?.content?.[0]?.text ?? "";
|
|
373
|
+
if (res?.isError) {
|
|
374
|
+
o.lastError = txt.slice(0, 140);
|
|
375
|
+
o.updatedAt = now;
|
|
376
|
+
report.push(`🔴 ${o.id} DCA buy failed: ${o.lastError}`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const txHash = extractTxHash(txt) ?? "unknown";
|
|
380
|
+
d.fills.push({ ts: now, txHash, amountEth: d.amountEthPerBuy });
|
|
381
|
+
d.completedBuys += 1;
|
|
382
|
+
d.spentEth = String(Number(d.spentEth) + Number(d.amountEthPerBuy));
|
|
383
|
+
d.nextRunAt = now + d.intervalSec * 1000; // no burst catch-up
|
|
384
|
+
o.lastError = undefined;
|
|
385
|
+
o.updatedAt = now;
|
|
386
|
+
if (d.completedBuys >= d.totalBuys)
|
|
387
|
+
o.status = "completed";
|
|
388
|
+
report.push(`🟢 ${o.id} DCA bought ${d.amountEthPerBuy} ETH → ${o.token.symbol} · ${d.completedBuys}/${d.totalBuys} · tx \`${txHash}\``);
|
|
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);
|
|
433
|
+
o.updatedAt = now;
|
|
434
|
+
report.push(`🔴 ${o.id} ${trigger.toUpperCase()} sell failed: ${o.lastError}`);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const txHash = extractTxHash(txt) ?? "unknown";
|
|
438
|
+
b.fills.push({ ts: now, txHash, kind: trigger, priceUsd: price });
|
|
439
|
+
o.status = "completed";
|
|
440
|
+
o.lastError = undefined;
|
|
441
|
+
o.updatedAt = now;
|
|
442
|
+
report.push(`🟢 ${o.id} ${trigger.toUpperCase()} sold ${sellHuman} ${o.token.symbol} → ETH at ${fmtUsd(price)} · tx \`${txHash}\``);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
// One bad order (e.g. wallet decrypt failure) must not abort the whole tick.
|
|
448
|
+
o.lastError = String(e?.message ?? e).slice(0, 140);
|
|
449
|
+
o.updatedAt = now;
|
|
450
|
+
report.push(`🔴 ${o.id}: tick error — ${o.lastError}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Preview must be read-only: only persist state when actually executing.
|
|
454
|
+
if (doExecute)
|
|
455
|
+
saveStore(store);
|
|
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.`);
|
|
464
|
+
}
|
|
465
|
+
report.push("", `_Explorer: ${rh_mcp_js_1.RH_EXPLORER}_`);
|
|
466
|
+
return textResult(report.join("\n"));
|
|
467
|
+
}
|
|
468
|
+
default:
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|