@moly-mcp/lido 1.0.6 → 1.0.8

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.
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ L2_CHAINS,
4
+ getBalance,
5
+ getConversionRate,
6
+ getRuntime
7
+ } from "./chunk-EKZFGIVK.js";
8
+
9
+ // src/tools/bridge.ts
10
+ import { formatEther, parseEther, parseAbi } from "viem";
11
+ var LIFI_BASE = "https://li.quest/v1";
12
+ var ERC20_ABI = parseAbi([
13
+ "function balanceOf(address) view returns (uint256)",
14
+ "function allowance(address owner, address spender) view returns (uint256)",
15
+ "function approve(address spender, uint256 amount) returns (bool)"
16
+ ]);
17
+ var NATIVE_TOKEN = "0x0000000000000000000000000000000000000000";
18
+ var L1_WSTETH = "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0";
19
+ function tokenAddress(chain, token) {
20
+ if (token === "ETH") return NATIVE_TOKEN;
21
+ return L2_CHAINS[chain].wstETH;
22
+ }
23
+ function toTokenAddress(toToken) {
24
+ if (!toToken || toToken === "ETH") return NATIVE_TOKEN;
25
+ return L1_WSTETH;
26
+ }
27
+ function ensureMainnet() {
28
+ const rt = getRuntime();
29
+ if (rt.config.network !== "mainnet") {
30
+ throw new Error("Bridge tools only work on mainnet. LI.FI does not support testnets.");
31
+ }
32
+ }
33
+ async function getL2Balance(sourceChain, address) {
34
+ ensureMainnet();
35
+ const rt = getRuntime();
36
+ const addr = address ?? rt.getAddress();
37
+ const client = rt.getL2PublicClient(sourceChain);
38
+ const cfg = L2_CHAINS[sourceChain];
39
+ const [eth, wsteth] = await Promise.all([
40
+ client.getBalance({ address: addr }),
41
+ client.readContract({ address: cfg.wstETH, abi: ERC20_ABI, functionName: "balanceOf", args: [addr] })
42
+ ]);
43
+ return {
44
+ address: addr,
45
+ chain: cfg.name,
46
+ chainId: cfg.chainId,
47
+ balances: { ETH: formatEther(eth), wstETH: formatEther(wsteth) }
48
+ };
49
+ }
50
+ async function getBridgeQuote(sourceChain, token, amount, toToken) {
51
+ ensureMainnet();
52
+ const rt = getRuntime();
53
+ const addr = rt.getAddress();
54
+ const cfg = L2_CHAINS[sourceChain];
55
+ const fromToken = tokenAddress(sourceChain, token);
56
+ const toAddr = toTokenAddress(toToken);
57
+ const fromAmount = parseEther(amount).toString();
58
+ const url = `${LIFI_BASE}/quote?fromChain=${cfg.chainId}&toChain=1&fromToken=${fromToken}&toToken=${toAddr}&fromAmount=${fromAmount}&fromAddress=${addr}`;
59
+ const res = await fetch(url);
60
+ if (!res.ok) {
61
+ const body = await res.text();
62
+ throw new Error(`LI.FI quote failed (${res.status}): ${body}`);
63
+ }
64
+ const data = await res.json();
65
+ return {
66
+ sourceChain: cfg.name,
67
+ token,
68
+ amount,
69
+ toToken: toToken ?? "ETH",
70
+ toAmount: formatEther(BigInt(data.estimate?.toAmount ?? "0")),
71
+ estimatedDuration: `${Math.ceil((data.estimate?.executionDuration ?? 0) / 60)} min`,
72
+ gasCosts: data.estimate?.gasCosts ?? [],
73
+ feeCosts: data.estimate?.feeCosts ?? [],
74
+ tool: data.tool
75
+ };
76
+ }
77
+ async function bridgeToEthereum(sourceChain, token, amount, toToken, dryRun) {
78
+ ensureMainnet();
79
+ const rt = getRuntime();
80
+ const addr = rt.getAddress();
81
+ const cfg = L2_CHAINS[sourceChain];
82
+ const fromToken = tokenAddress(sourceChain, token);
83
+ const toAddr = toTokenAddress(toToken);
84
+ const fromAmount = parseEther(amount).toString();
85
+ const shouldDryRun = rt.config.mode === "simulation" ? dryRun !== false : !!dryRun;
86
+ const url = `${LIFI_BASE}/quote?fromChain=${cfg.chainId}&toChain=1&fromToken=${fromToken}&toToken=${toAddr}&fromAmount=${fromAmount}&fromAddress=${addr}`;
87
+ const res = await fetch(url);
88
+ if (!res.ok) {
89
+ const body = await res.text();
90
+ throw new Error(`LI.FI quote failed (${res.status}): ${body}`);
91
+ }
92
+ const data = await res.json();
93
+ const txReq = data.transactionRequest;
94
+ const quote = {
95
+ sourceChain: cfg.name,
96
+ token,
97
+ amount,
98
+ toToken: toToken ?? "ETH",
99
+ toAmount: formatEther(BigInt(data.estimate?.toAmount ?? "0")),
100
+ estimatedDuration: `${Math.ceil((data.estimate?.executionDuration ?? 0) / 60)} min`,
101
+ tool: data.tool
102
+ };
103
+ if (shouldDryRun) {
104
+ return { simulated: true, mode: rt.config.mode, ...quote };
105
+ }
106
+ const wallet = rt.getL2Wallet(sourceChain);
107
+ const client = rt.getL2PublicClient(sourceChain);
108
+ if (token === "wstETH" && txReq.to) {
109
+ const allowance = await client.readContract({
110
+ address: cfg.wstETH,
111
+ abi: ERC20_ABI,
112
+ functionName: "allowance",
113
+ args: [addr, txReq.to]
114
+ });
115
+ if (allowance < BigInt(fromAmount)) {
116
+ const approveTx = await wallet.writeContract({
117
+ address: cfg.wstETH,
118
+ abi: ERC20_ABI,
119
+ functionName: "approve",
120
+ args: [txReq.to, BigInt(fromAmount)]
121
+ });
122
+ await client.waitForTransactionReceipt({ hash: approveTx });
123
+ }
124
+ }
125
+ const hash = await wallet.sendTransaction({
126
+ to: txReq.to,
127
+ data: txReq.data,
128
+ value: txReq.value ? BigInt(txReq.value) : 0n,
129
+ gas: txReq.gasLimit ? BigInt(txReq.gasLimit) : void 0,
130
+ chain: void 0
131
+ });
132
+ return {
133
+ simulated: false,
134
+ mode: rt.config.mode,
135
+ ...quote,
136
+ txHash: hash,
137
+ note: "Bridge submitted. Use get_bridge_status to track progress (may take 1-20 min)."
138
+ };
139
+ }
140
+ async function getBridgeStatus(txHash, sourceChain) {
141
+ ensureMainnet();
142
+ const cfg = L2_CHAINS[sourceChain];
143
+ const url = `${LIFI_BASE}/status?txHash=${txHash}&fromChain=${cfg.chainId}&toChain=1`;
144
+ const res = await fetch(url);
145
+ if (!res.ok) {
146
+ const body = await res.text();
147
+ throw new Error(`LI.FI status failed (${res.status}): ${body}`);
148
+ }
149
+ const data = await res.json();
150
+ return {
151
+ txHash,
152
+ sourceChain: cfg.name,
153
+ status: data.status,
154
+ substatus: data.substatus ?? null,
155
+ sending: data.sending ? { txHash: data.sending.txHash, amount: data.sending.amount } : null,
156
+ receiving: data.receiving ? { txHash: data.receiving.txHash, amount: data.receiving.amount } : null
157
+ };
158
+ }
159
+
160
+ // src/tools/position.ts
161
+ async function getTotalPosition(address) {
162
+ const rt = getRuntime();
163
+ const addr = address ?? rt.getAddress();
164
+ const isMainnet = rt.config.network === "mainnet";
165
+ const l1 = await getBalance(addr);
166
+ const rate = await getConversionRate();
167
+ const wstethToEth = parseFloat(rate["1_wstETH_in_stETH"]);
168
+ const l1Eth = parseFloat(l1.balances.eth);
169
+ const l1Steth = parseFloat(l1.balances.stETH);
170
+ const l1Wsteth = parseFloat(l1.balances.wstETH);
171
+ const l1WstethInEth = l1Wsteth * wstethToEth;
172
+ const positions = {
173
+ ethereum: {
174
+ eth: l1.balances.eth,
175
+ stETH: l1.balances.stETH,
176
+ wstETH: l1.balances.wstETH,
177
+ ethEquivalent: (l1Eth + l1Steth + l1WstethInEth).toFixed(6)
178
+ }
179
+ };
180
+ let totalEthEquiv = l1Eth + l1Steth + l1WstethInEth;
181
+ if (isMainnet) {
182
+ for (const chain of ["base", "arbitrum"]) {
183
+ try {
184
+ const l2 = await getL2Balance(chain, addr);
185
+ const l2Eth = parseFloat(l2.balances.ETH);
186
+ const l2Wsteth = parseFloat(l2.balances.wstETH);
187
+ const l2WstethInEth = l2Wsteth * wstethToEth;
188
+ positions[chain] = {
189
+ eth: l2.balances.ETH,
190
+ wstETH: l2.balances.wstETH,
191
+ ethEquivalent: (l2Eth + l2WstethInEth).toFixed(6)
192
+ };
193
+ totalEthEquiv += l2Eth + l2WstethInEth;
194
+ } catch {
195
+ positions[chain] = { error: "failed to fetch" };
196
+ }
197
+ }
198
+ }
199
+ return {
200
+ address: addr,
201
+ network: rt.chainAddresses.name,
202
+ conversionRate: rate["1_wstETH_in_stETH"],
203
+ positions,
204
+ totalEthEquivalent: totalEthEquiv.toFixed(6),
205
+ note: isMainnet ? "Includes Ethereum + Base + Arbitrum" : "Testnet: Ethereum only"
206
+ };
207
+ }
208
+
209
+ export {
210
+ getL2Balance,
211
+ getBridgeQuote,
212
+ bridgeToEthereum,
213
+ getBridgeStatus,
214
+ getTotalPosition
215
+ };
@@ -11,10 +11,14 @@ function configExists() {
11
11
  }
12
12
  function loadConfig() {
13
13
  if (!existsSync(CONFIG_PATH)) {
14
- throw new Error("No config found. Run: npx @moly/lido (or: moly setup)");
14
+ throw new Error("No config found. Run: npx @moly-mcp/lido (or: moly setup)");
15
15
  }
16
16
  try {
17
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
17
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
18
+ if (!cfg.chainScope) cfg.chainScope = "ethereum";
19
+ if (!cfg.ows) cfg.ows = null;
20
+ if (!cfg.alertChannels) cfg.alertChannels = null;
21
+ return cfg;
18
22
  } catch {
19
23
  throw new Error(`Failed to parse config at ${CONFIG_PATH}. Run: moly reset`);
20
24
  }
@@ -29,6 +33,12 @@ function saveConfig(cfg) {
29
33
  } catch {
30
34
  }
31
35
  }
36
+ function updateConfig(partial) {
37
+ const current = loadConfig();
38
+ const next = { ...current, ...partial };
39
+ saveConfig(next);
40
+ return next;
41
+ }
32
42
  function deleteConfig() {
33
43
  if (existsSync(CONFIG_PATH)) unlinkSync(CONFIG_PATH);
34
44
  }
@@ -39,8 +49,14 @@ function redactedConfig(cfg) {
39
49
  return {
40
50
  network: cfg.network,
41
51
  mode: cfg.mode,
52
+ chainScope: cfg.chainScope ?? "ethereum",
42
53
  rpc: cfg.rpc ?? "(default public RPC)",
43
54
  privateKey: cfg.privateKey ? "*** configured" : "(not set)",
55
+ ows: cfg.ows ? { walletName: cfg.ows.walletName, passphrase: "*** configured" } : "(not configured)",
56
+ alertChannels: cfg.alertChannels ? {
57
+ telegram: cfg.alertChannels.telegram ? { chatId: cfg.alertChannels.telegram.chatId, token: "*** configured" } : void 0,
58
+ webhook: cfg.alertChannels.webhook ? { url: cfg.alertChannels.webhook.url } : void 0
59
+ } : "(not configured)",
44
60
  ai: cfg.ai ? {
45
61
  provider: cfg.ai.provider,
46
62
  model: cfg.ai.model,
@@ -53,6 +69,7 @@ export {
53
69
  configExists,
54
70
  loadConfig,
55
71
  saveConfig,
72
+ updateConfig,
56
73
  deleteConfig,
57
74
  getConfigPath,
58
75
  redactedConfig
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ export {
10
+ __require
11
+ };
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ __require
4
+ } from "./chunk-PDX44BCA.js";
5
+
6
+ // src/ledger/store.ts
7
+ import Database from "better-sqlite3";
8
+ import { existsSync, readdirSync, readFileSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+ var DB_PATH = join(homedir(), ".moly", "ledger.db");
12
+ var MARKER = join(homedir(), ".moly", ".ledger_imported");
13
+ var _db = null;
14
+ function getDb() {
15
+ if (_db) return _db;
16
+ _db = new Database(DB_PATH);
17
+ _db.pragma("journal_mode = WAL");
18
+ _db.exec(`
19
+ CREATE TABLE IF NOT EXISTS ledger (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ timestamp TEXT NOT NULL,
22
+ tool TEXT NOT NULL,
23
+ args TEXT,
24
+ result TEXT,
25
+ wallet TEXT,
26
+ chain TEXT,
27
+ status TEXT DEFAULT 'ok',
28
+ tx_hash TEXT,
29
+ gas_used TEXT,
30
+ amount TEXT
31
+ );
32
+ CREATE INDEX IF NOT EXISTS idx_ledger_ts ON ledger(timestamp);
33
+ CREATE INDEX IF NOT EXISTS idx_ledger_tool ON ledger(tool);
34
+ CREATE INDEX IF NOT EXISTS idx_ledger_wallet ON ledger(wallet);
35
+ CREATE INDEX IF NOT EXISTS idx_ledger_tx ON ledger(tx_hash);
36
+ `);
37
+ return _db;
38
+ }
39
+ function initLedger() {
40
+ getDb();
41
+ importJsonlIfNeeded();
42
+ }
43
+ function logEntry(entry) {
44
+ const db = getDb();
45
+ db.prepare(`
46
+ INSERT INTO ledger (timestamp, tool, args, result, wallet, chain, status, tx_hash, gas_used, amount)
47
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
48
+ `).run(
49
+ (/* @__PURE__ */ new Date()).toISOString(),
50
+ entry.tool,
51
+ JSON.stringify(entry.args),
52
+ entry.result.slice(0, 4e3),
53
+ entry.wallet ?? null,
54
+ entry.chain ?? null,
55
+ entry.status ?? "ok",
56
+ entry.tx_hash ?? null,
57
+ entry.gas_used ?? null,
58
+ entry.amount ?? null
59
+ );
60
+ }
61
+ function queryLedger(opts = {}) {
62
+ const db = getDb();
63
+ const conditions = [];
64
+ const params = [];
65
+ if (opts.tool) {
66
+ conditions.push("tool = ?");
67
+ params.push(opts.tool);
68
+ }
69
+ if (opts.wallet) {
70
+ conditions.push("wallet = ?");
71
+ params.push(opts.wallet);
72
+ }
73
+ if (opts.chain) {
74
+ conditions.push("chain = ?");
75
+ params.push(opts.chain);
76
+ }
77
+ if (opts.since) {
78
+ conditions.push("timestamp >= ?");
79
+ params.push(opts.since);
80
+ }
81
+ if (opts.tx_hash) {
82
+ conditions.push("tx_hash = ?");
83
+ params.push(opts.tx_hash);
84
+ }
85
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
86
+ const limit = opts.limit ?? 50;
87
+ return db.prepare(`SELECT * FROM ledger ${where} ORDER BY id DESC LIMIT ?`).all(...params, limit);
88
+ }
89
+ function ledgerStats(since) {
90
+ const db = getDb();
91
+ const where = since ? "WHERE timestamp >= ?" : "";
92
+ const params = since ? [since] : [];
93
+ const total = db.prepare(`SELECT COUNT(*) as count FROM ledger ${where}`).get(...params)?.count ?? 0;
94
+ const errors = db.prepare(`SELECT COUNT(*) as count FROM ledger ${where ? where + " AND" : "WHERE"} status = 'error'`).get(...params)?.count ?? 0;
95
+ const byTool = db.prepare(`SELECT tool, COUNT(*) as count FROM ledger ${where} GROUP BY tool ORDER BY count DESC`).all(...params);
96
+ const stakeWhere = since ? `WHERE tool = 'stake_eth' AND status = 'ok' AND timestamp >= ?` : `WHERE tool = 'stake_eth' AND status = 'ok'`;
97
+ const staked = db.prepare(`SELECT SUM(CAST(amount AS REAL)) as total FROM ledger ${stakeWhere}`).get(...params);
98
+ return {
99
+ total,
100
+ errors,
101
+ byTool,
102
+ totalStakedEth: (staked?.total ?? 0).toFixed(6)
103
+ };
104
+ }
105
+ function exportLedger(opts) {
106
+ const rows = queryLedger({ since: opts.since, limit: opts.limit ?? 1e4 });
107
+ if (opts.format === "csv") {
108
+ if (rows.length === 0) return "";
109
+ const headers = Object.keys(rows[0]);
110
+ const lines = [headers.join(",")];
111
+ for (const row of rows) {
112
+ lines.push(headers.map((h) => {
113
+ const v = String(row[h] ?? "");
114
+ return v.includes(",") || v.includes('"') ? `"${v.replace(/"/g, '""')}"` : v;
115
+ }).join(","));
116
+ }
117
+ return lines.join("\n");
118
+ }
119
+ return JSON.stringify(rows, null, 2);
120
+ }
121
+ function importJsonlIfNeeded() {
122
+ if (existsSync(MARKER)) return;
123
+ const tradesDir = join(process.cwd(), "trades");
124
+ if (!existsSync(tradesDir)) {
125
+ writeMarker();
126
+ return;
127
+ }
128
+ const files = readdirSync(tradesDir).filter((f) => f.endsWith(".jsonl"));
129
+ const db = getDb();
130
+ const insert = db.prepare(`
131
+ INSERT INTO ledger (timestamp, tool, args, result, status)
132
+ VALUES (?, ?, ?, ?, 'ok')
133
+ `);
134
+ const importAll = db.transaction(() => {
135
+ for (const file of files) {
136
+ const lines = readFileSync(join(tradesDir, file), "utf-8").split("\n").filter(Boolean);
137
+ for (const line of lines) {
138
+ try {
139
+ const rec = JSON.parse(line);
140
+ insert.run(rec.ts, rec.tool, JSON.stringify(rec.args), JSON.stringify(rec.result));
141
+ } catch {
142
+ }
143
+ }
144
+ }
145
+ });
146
+ importAll();
147
+ writeMarker();
148
+ }
149
+ function writeMarker() {
150
+ try {
151
+ const { writeFileSync: wf } = __require("fs");
152
+ wf(MARKER, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
153
+ } catch {
154
+ }
155
+ }
156
+
157
+ export {
158
+ initLedger,
159
+ logEntry,
160
+ queryLedger,
161
+ ledgerStats,
162
+ exportLedger
163
+ };
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getProposals,
4
+ getWithdrawalRequests,
5
+ getWithdrawalStatus
6
+ } from "./chunk-EQYEWCQO.js";
7
+ import {
8
+ loadAlerts,
9
+ loadChannelConfig,
10
+ saveAlerts
11
+ } from "./chunk-6F64RPQQ.js";
12
+ import {
13
+ getBalance,
14
+ getConversionRate,
15
+ getRewards,
16
+ getRuntime
17
+ } from "./chunk-EKZFGIVK.js";
18
+ import "./chunk-P6VFMSPM.js";
19
+ import "./chunk-PDX44BCA.js";
20
+
21
+ // src/alerts/notify.ts
22
+ async function sendTelegram(token, chatId, message) {
23
+ try {
24
+ const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({ chat_id: chatId, text: message, parse_mode: "Markdown" })
28
+ });
29
+ if (!res.ok) {
30
+ process.stderr.write(`Telegram send failed (${res.status}): ${await res.text()}
31
+ `);
32
+ return false;
33
+ }
34
+ return true;
35
+ } catch (err) {
36
+ process.stderr.write(`Telegram error: ${err.message}
37
+ `);
38
+ return false;
39
+ }
40
+ }
41
+ async function sendWebhook(url, payload) {
42
+ try {
43
+ const res = await fetch(url, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify(payload)
47
+ });
48
+ if (!res.ok) {
49
+ process.stderr.write(`Webhook send failed (${res.status})
50
+ `);
51
+ return false;
52
+ }
53
+ return true;
54
+ } catch (err) {
55
+ process.stderr.write(`Webhook error: ${err.message}
56
+ `);
57
+ return false;
58
+ }
59
+ }
60
+ function formatTelegramMessage(alert, data2) {
61
+ const lines = [
62
+ "*Moly Alert*",
63
+ "",
64
+ `Condition: \`${alert.condition}\``
65
+ ];
66
+ if (alert.threshold !== void 0) lines.push(`Threshold: ${alert.threshold}`);
67
+ if (data2.current !== void 0) lines.push(`Current: ${data2.current}`);
68
+ if (data2.detail) lines.push(`Detail: ${data2.detail}`);
69
+ lines.push(`Triggered: ${(/* @__PURE__ */ new Date()).toISOString()}`);
70
+ return lines.join("\n");
71
+ }
72
+ async function dispatch(channelConfig, channel, alert, data2) {
73
+ if (channel === "telegram") {
74
+ const tg = channelConfig.telegram;
75
+ if (!tg) {
76
+ process.stderr.write("Telegram not configured\n");
77
+ return false;
78
+ }
79
+ return sendTelegram(tg.token, tg.chatId, formatTelegramMessage(alert, data2));
80
+ }
81
+ if (channel === "webhook") {
82
+ const wh = channelConfig.webhook;
83
+ if (!wh) {
84
+ process.stderr.write("Webhook not configured\n");
85
+ return false;
86
+ }
87
+ return sendWebhook(wh.url, {
88
+ alert: { id: alert.id, condition: alert.condition, threshold: alert.threshold },
89
+ data: data2,
90
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
91
+ source: "moly-alerts"
92
+ });
93
+ }
94
+ return false;
95
+ }
96
+
97
+ // src/alerts/daemon.ts
98
+ var COOLDOWN_MS = 5 * 60 * 1e3;
99
+ async function evaluateAlert(alert, lastProposalCount) {
100
+ try {
101
+ switch (alert.condition) {
102
+ case "balance_below":
103
+ case "balance_above": {
104
+ const res = await getBalance();
105
+ const total = parseFloat(res.balances.eth) + parseFloat(res.balances.stETH) + parseFloat(res.balances.wstETH);
106
+ const hit = alert.condition === "balance_below" ? total < (alert.threshold ?? 0) : total > (alert.threshold ?? 0);
107
+ return { triggered: hit, data: { current: total.toFixed(6), balances: res.balances } };
108
+ }
109
+ case "reward_rate_below":
110
+ case "reward_rate_above": {
111
+ const res = await getRewards(void 0, 30);
112
+ const rate = parseFloat(res.totalRewards);
113
+ const hit = alert.condition === "reward_rate_below" ? rate < (alert.threshold ?? 0) : rate > (alert.threshold ?? 0);
114
+ return { triggered: hit, data: { current: rate.toFixed(6), detail: `${res.totalRewards} ETH over 30d` } };
115
+ }
116
+ case "conversion_rate_above":
117
+ case "conversion_rate_below": {
118
+ const res = await getConversionRate();
119
+ const rate = parseFloat(res["1_wstETH_in_stETH"]);
120
+ const hit = alert.condition === "conversion_rate_above" ? rate > (alert.threshold ?? 0) : rate < (alert.threshold ?? 0);
121
+ return { triggered: hit, data: { current: rate.toFixed(6) } };
122
+ }
123
+ case "withdrawal_ready": {
124
+ const reqs = await getWithdrawalRequests();
125
+ if (!reqs.requestIds?.length) return { triggered: false, data: { detail: "no pending withdrawals" } };
126
+ const status = await getWithdrawalStatus(reqs.requestIds);
127
+ const ready = (status.statuses ?? []).filter((s) => s.isFinalized);
128
+ return {
129
+ triggered: ready.length > 0,
130
+ data: { current: ready.length, detail: `${ready.length} of ${reqs.requestIds.length} finalized` }
131
+ };
132
+ }
133
+ case "proposal_new": {
134
+ const res = await getProposals(1);
135
+ const current = res.totalProposals ?? 0;
136
+ if (lastProposalCount === void 0) {
137
+ return { triggered: false, data: { detail: "tracking started" }, newProposalCount: current };
138
+ }
139
+ return {
140
+ triggered: current > lastProposalCount,
141
+ data: { current, detail: `${current - lastProposalCount} new proposal(s)` },
142
+ newProposalCount: current
143
+ };
144
+ }
145
+ case "reward_delta": {
146
+ const res = await getBalance();
147
+ const current = parseFloat(res.balances.stETH);
148
+ const prev = data.lastRewardBalance ? parseFloat(data.lastRewardBalance) : current;
149
+ const delta = current - prev;
150
+ data.lastRewardBalance = current.toString();
151
+ return {
152
+ triggered: delta > (alert.threshold ?? 0),
153
+ data: { delta: delta.toFixed(8), current: current.toFixed(6), previous: prev.toFixed(6) }
154
+ };
155
+ }
156
+ case "governance_expiring": {
157
+ const res = await getProposals(10);
158
+ const now = Date.now();
159
+ const soon = 24 * 60 * 60 * 1e3;
160
+ const votingWindow = 72 * 60 * 60 * 1e3;
161
+ const expiring = (res.proposals ?? []).filter(
162
+ (p) => p.open && p.startDate && new Date(p.startDate).getTime() + votingWindow - now < soon
163
+ );
164
+ return {
165
+ triggered: expiring.length > 0,
166
+ data: { count: expiring.length, ids: expiring.map((p) => p.id) }
167
+ };
168
+ }
169
+ default:
170
+ return null;
171
+ }
172
+ } catch (err) {
173
+ process.stderr.write(`Alert eval error [${alert.condition}]: ${err.message}
174
+ `);
175
+ return null;
176
+ }
177
+ }
178
+ async function runDaemon(intervalMs = 3e4) {
179
+ getRuntime();
180
+ const channelConfig = loadChannelConfig();
181
+ process.stderr.write(`Alert daemon running, polling every ${intervalMs / 1e3}s
182
+ `);
183
+ const initData = loadAlerts();
184
+ initData.daemonPid = process.pid;
185
+ initData.daemonStartedAt = (/* @__PURE__ */ new Date()).toISOString();
186
+ saveAlerts(initData);
187
+ while (true) {
188
+ const data2 = loadAlerts();
189
+ const active = data2.alerts.filter((a) => a.enabled);
190
+ for (const alert of active) {
191
+ if (alert.lastTriggered) {
192
+ const elapsed = Date.now() - new Date(alert.lastTriggered).getTime();
193
+ if (elapsed < COOLDOWN_MS) continue;
194
+ }
195
+ const result = await evaluateAlert(alert, data2.lastProposalCount);
196
+ if (!result) continue;
197
+ if (result.newProposalCount !== void 0) {
198
+ data2.lastProposalCount = result.newProposalCount;
199
+ }
200
+ if (result.triggered) {
201
+ process.stderr.write(`Alert triggered: ${alert.condition} (${alert.id.slice(0, 8)})
202
+ `);
203
+ await dispatch(channelConfig, alert.channel, alert, result.data);
204
+ alert.lastTriggered = (/* @__PURE__ */ new Date()).toISOString();
205
+ }
206
+ }
207
+ data2.lastCheckAt = (/* @__PURE__ */ new Date()).toISOString();
208
+ saveAlerts(data2);
209
+ await new Promise((r) => setTimeout(r, intervalMs));
210
+ }
211
+ }
212
+ export {
213
+ runDaemon
214
+ };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getTotalPosition
4
+ } from "./chunk-GL6TLHSF.js";
5
+ import "./chunk-EKZFGIVK.js";
6
+ import "./chunk-P6VFMSPM.js";
7
+ import "./chunk-PDX44BCA.js";
8
+ export {
9
+ getTotalPosition
10
+ };