byreal-test 1.3.7 → 2.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/cli.js +15 -0
- package/package.json +1 -1
- package/src/api-runner.js +422 -0
package/cli.js
CHANGED
|
@@ -101,4 +101,19 @@ program
|
|
|
101
101
|
process.exit(result.hasAlert ? 1 : 0);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
program
|
|
105
|
+
.command('run_api')
|
|
106
|
+
.description('Call byreal-cli overview and verify key metrics (TVL / Fees)')
|
|
107
|
+
.option('--lark-webhook <url>', 'Lark bot webhook URL', 'https://open.larksuite.com/open-apis/bot/v2/hook/c465fa0d-c3c5-42b8-9018-39ae079c4f2a')
|
|
108
|
+
.option('--state-file <path>', 'Path to persist state between runs (default: ~/.byreal-test/api-state.json)')
|
|
109
|
+
.option('--no-lark', 'Disable Lark notification')
|
|
110
|
+
.action(async (opts) => {
|
|
111
|
+
const { runApi } = require('./src/api-runner');
|
|
112
|
+
const result = await runApi({
|
|
113
|
+
larkWebhook: opts.lark ? opts.larkWebhook : undefined,
|
|
114
|
+
stateFile: opts.stateFile,
|
|
115
|
+
});
|
|
116
|
+
process.exit(result.hasAlert ? 1 : 0);
|
|
117
|
+
});
|
|
118
|
+
|
|
104
119
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { parseJsonOutput } = require('./runner');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_STATE_FILE = path.join(os.homedir(), '.byreal-test', 'api-state.json');
|
|
11
|
+
|
|
12
|
+
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function utc8Label() {
|
|
15
|
+
const now = new Date(Date.now() + 8 * 3600_000);
|
|
16
|
+
const mm = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
17
|
+
const dd = String(now.getUTCDate()).padStart(2, '0');
|
|
18
|
+
const hh = String(now.getUTCHours()).padStart(2, '0');
|
|
19
|
+
const min = String(now.getUTCMinutes()).padStart(2, '0');
|
|
20
|
+
return `${mm}-${dd} ${hh}:${min}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fmtUsd(val) {
|
|
24
|
+
if (val >= 1_000_000) return `$${(val / 1_000_000).toFixed(2)}M`;
|
|
25
|
+
if (val >= 1_000) return `$${(val / 1_000).toFixed(2)}K`;
|
|
26
|
+
return `$${val.toFixed(2)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fmtPct(val) {
|
|
30
|
+
return `${val >= 0 ? '+' : ''}${val.toFixed(2)}%`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadState(filePath) {
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(filePath)) {
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveState(filePath, payload) {
|
|
43
|
+
try {
|
|
44
|
+
const dir = path.dirname(filePath);
|
|
45
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn(chalk.yellow(` Warning: state save failed: ${err.message}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function sendLark(webhookUrl, body) {
|
|
53
|
+
const res = await fetch(webhookUrl, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── overview checks ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function checkOverview(data, prev) {
|
|
64
|
+
const checks = [];
|
|
65
|
+
|
|
66
|
+
// TVL 24h change within ±30% (tvl_change_24h is already in %)
|
|
67
|
+
const tvlChg = data.tvl_change_24h;
|
|
68
|
+
const tvlOk = Math.abs(tvlChg) <= 30;
|
|
69
|
+
checks.push({
|
|
70
|
+
name: 'TVL 24h',
|
|
71
|
+
value: `${fmtUsd(data.tvl)} (${fmtPct(tvlChg)})`,
|
|
72
|
+
ok: tvlOk,
|
|
73
|
+
reason: tvlOk ? null : `24h 变动 ${fmtPct(tvlChg)} 超过 ±30% 阈值`,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Fees (24h) > $1 000
|
|
77
|
+
const fee24hOk = data.fee_24h_usd >= 1_000;
|
|
78
|
+
checks.push({
|
|
79
|
+
name: 'Fees (24h)',
|
|
80
|
+
value: fmtUsd(data.fee_24h_usd),
|
|
81
|
+
ok: fee24hOk,
|
|
82
|
+
reason: fee24hOk ? null : `低于 $1K 最低阈值`,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Fees (All Time): cumulative — flag if drops ≥ 20% vs last run
|
|
86
|
+
if (prev?.fee_all) {
|
|
87
|
+
const chgPct = (data.fee_all - prev.fee_all) / prev.fee_all * 100;
|
|
88
|
+
const feeAllOk = chgPct >= -20;
|
|
89
|
+
checks.push({
|
|
90
|
+
name: 'Fees (All Time)',
|
|
91
|
+
value: `${fmtUsd(data.fee_all)} (vs prev ${fmtUsd(prev.fee_all)}: ${fmtPct(chgPct)})`,
|
|
92
|
+
ok: feeAllOk,
|
|
93
|
+
reason: feeAllOk ? null : `相比上次下降 ${Math.abs(chgPct).toFixed(2)}%,超过 20% 警戒线`,
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
checks.push({
|
|
97
|
+
name: 'Fees (All Time)',
|
|
98
|
+
value: `${fmtUsd(data.fee_all)} (首次运行,记录基准)`,
|
|
99
|
+
ok: true,
|
|
100
|
+
reason: null,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return checks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── pools checks ─────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function checkPools(pools, prevPoolMap) {
|
|
110
|
+
const checks = [];
|
|
111
|
+
|
|
112
|
+
// APR: any pool ≥ 30 000% is suspicious
|
|
113
|
+
const highAprPools = pools.filter(p => p.apr >= 30_000);
|
|
114
|
+
const aprOk = highAprPools.length === 0;
|
|
115
|
+
checks.push({
|
|
116
|
+
name: 'APR 上限',
|
|
117
|
+
value: aprOk
|
|
118
|
+
? `最高 ${Math.max(...pools.map(p => p.apr)).toFixed(0)}%,共 ${pools.length} 个池`
|
|
119
|
+
: `${highAprPools.length} 个池 APR ≥ 30000%`,
|
|
120
|
+
ok: aprOk,
|
|
121
|
+
reason: aprOk ? null :
|
|
122
|
+
highAprPools.slice(0, 5).map(p => `${p.pair} ${p.apr.toFixed(0)}%`).join(' | '),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Low-APR pools: flag if ≥ 50% of all pools have apr < 1%
|
|
126
|
+
const lowAprCount = pools.filter(p => p.apr < 1).length;
|
|
127
|
+
const lowAprThreshold = Math.floor(pools.length * 0.5);
|
|
128
|
+
const lowAprOk = lowAprCount < lowAprThreshold;
|
|
129
|
+
checks.push({
|
|
130
|
+
name: 'APR < 1% 数量',
|
|
131
|
+
value: `${lowAprCount} / ${pools.length} 个池`,
|
|
132
|
+
ok: lowAprOk,
|
|
133
|
+
reason: lowAprOk ? null : `${lowAprCount} 个池 APR < 1%,超过总数 50%(${lowAprThreshold} 个)`,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// TVL / Volume non-negative
|
|
137
|
+
const negTvl = pools.filter(p => p.tvl_usd < 0);
|
|
138
|
+
const negVol = pools.filter(p => p.volume_24h_usd < 0);
|
|
139
|
+
const nonNegOk = negTvl.length === 0 && negVol.length === 0;
|
|
140
|
+
checks.push({
|
|
141
|
+
name: 'TVL/Volume ≥ 0',
|
|
142
|
+
value: nonNegOk ? '全部正常' : `TVL<0: ${negTvl.length} | Vol<0: ${negVol.length}`,
|
|
143
|
+
ok: nonNegOk,
|
|
144
|
+
reason: nonNegOk ? null :
|
|
145
|
+
[...negTvl.map(p => `${p.pair} TVL=${fmtUsd(p.tvl_usd)}`),
|
|
146
|
+
...negVol.map(p => `${p.pair} Vol=${fmtUsd(p.volume_24h_usd)}`)].slice(0, 5).join(' | '),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Per-pool TVL drop vs previous run: flag if ≥ 5 pools dropped > 30%
|
|
150
|
+
if (prevPoolMap && Object.keys(prevPoolMap).length > 0) {
|
|
151
|
+
const droppedPools = pools.filter(p => {
|
|
152
|
+
const prevTvl = prevPoolMap[p.id];
|
|
153
|
+
if (prevTvl == null || prevTvl === 0) return false;
|
|
154
|
+
return (p.tvl_usd - prevTvl) / prevTvl < -0.30;
|
|
155
|
+
});
|
|
156
|
+
const poolTvlOk = droppedPools.length < 5;
|
|
157
|
+
checks.push({
|
|
158
|
+
name: 'Pool TVL 跌幅',
|
|
159
|
+
value: poolTvlOk
|
|
160
|
+
? `${droppedPools.length} 个池跌幅 > 30%`
|
|
161
|
+
: `${droppedPools.length} 个池跌幅 > 30%`,
|
|
162
|
+
ok: poolTvlOk,
|
|
163
|
+
reason: poolTvlOk ? null :
|
|
164
|
+
`${droppedPools.length} 个池 TVL 相比上次下降 > 30%,超过 5 个警戒线\n ` +
|
|
165
|
+
droppedPools.slice(0, 5).map(p => {
|
|
166
|
+
const chg = ((p.tvl_usd - prevPoolMap[p.id]) / prevPoolMap[p.id] * 100).toFixed(1);
|
|
167
|
+
return `${p.pair} ${chg}%`;
|
|
168
|
+
}).join(' | '),
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
checks.push({
|
|
172
|
+
name: 'Pool TVL 跌幅',
|
|
173
|
+
value: '首次运行,记录基准',
|
|
174
|
+
ok: true,
|
|
175
|
+
reason: null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return checks;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── tokens checks ────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
const STABLE_SYMBOLS = new Set(['USDC', 'USDT', 'USD1', 'USDS', 'DAI', 'FDUSD']);
|
|
185
|
+
|
|
186
|
+
function checkTokens(tokens, prevTokenMap) {
|
|
187
|
+
const checks = [];
|
|
188
|
+
|
|
189
|
+
// Stablecoins: price_usd within ±5% of 1.0
|
|
190
|
+
const stables = tokens.filter(t => STABLE_SYMBOLS.has(t.symbol));
|
|
191
|
+
const badStables = stables.filter(t => Math.abs(t.price_usd - 1) > 0.05);
|
|
192
|
+
const stableOk = badStables.length === 0;
|
|
193
|
+
checks.push({
|
|
194
|
+
name: 'Stablecoin peg',
|
|
195
|
+
value: stableOk
|
|
196
|
+
? stables.map(t => `${t.symbol} $${t.price_usd.toFixed(4)}`).join(' | ')
|
|
197
|
+
: badStables.map(t => `${t.symbol} $${t.price_usd.toFixed(4)}`).join(' | '),
|
|
198
|
+
ok: stableOk,
|
|
199
|
+
reason: stableOk ? null :
|
|
200
|
+
badStables.map(t => `${t.symbol} 偏离 1.0 超过 5%`).join(', '),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Non-stable: compare price / change_24h / volume_24h vs previous run.
|
|
204
|
+
// Key by mint (unique) to avoid false positives from duplicate symbols.
|
|
205
|
+
if (prevTokenMap && Object.keys(prevTokenMap).length > 0) {
|
|
206
|
+
const anomalies = [];
|
|
207
|
+
|
|
208
|
+
for (const t of tokens) {
|
|
209
|
+
if (STABLE_SYMBOLS.has(t.symbol)) continue;
|
|
210
|
+
const prev = prevTokenMap[t.mint];
|
|
211
|
+
if (!prev) continue;
|
|
212
|
+
|
|
213
|
+
const issues = [];
|
|
214
|
+
|
|
215
|
+
// price: relative change > 50%
|
|
216
|
+
if (prev.price_usd > 0) {
|
|
217
|
+
const chg = Math.abs(t.price_usd - prev.price_usd) / prev.price_usd;
|
|
218
|
+
if (chg > 0.50) issues.push(`price ${(chg * 100).toFixed(0)}%↑↓`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// price_change_24h (decimal, e.g. 0.06 = 6%):
|
|
222
|
+
// flag only when BOTH conditions met:
|
|
223
|
+
// 1. relative shift > 50% (or absolute shift > 0.50 when baseline is tiny)
|
|
224
|
+
// 2. absolute diff > 0.30 (i.e. 30 percentage-points) — filters noise
|
|
225
|
+
const prevChg = prev.price_change_24h;
|
|
226
|
+
const curChg = t.price_change_24h;
|
|
227
|
+
const absDiff = Math.abs(curChg - prevChg);
|
|
228
|
+
if (absDiff > 0.30) {
|
|
229
|
+
if (Math.abs(prevChg) >= 0.02) {
|
|
230
|
+
const relChg = absDiff / Math.abs(prevChg);
|
|
231
|
+
if (relChg > 0.50) issues.push(`chg24h rel ${(relChg * 100).toFixed(0)}% abs ${(absDiff * 100).toFixed(0)}pp`);
|
|
232
|
+
} else {
|
|
233
|
+
if (absDiff > 0.50) {
|
|
234
|
+
issues.push(`chg24h shift ${(absDiff * 100).toFixed(0)}pp`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// volume_24h: relative change > 50%, only when absolute value > $10 000
|
|
240
|
+
if (prev.volume_24h_usd > 0 && t.volume_24h_usd > 10_000) {
|
|
241
|
+
const chg = Math.abs(t.volume_24h_usd - prev.volume_24h_usd) / prev.volume_24h_usd;
|
|
242
|
+
if (chg > 0.50) issues.push(`vol24h ${(chg * 100).toFixed(0)}%↑↓`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (issues.length > 0) {
|
|
246
|
+
anomalies.push(`${t.symbol}(${issues.join(', ')})`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const tokenDriftOk = anomalies.length === 0;
|
|
251
|
+
checks.push({
|
|
252
|
+
name: 'Token 数据波动',
|
|
253
|
+
value: tokenDriftOk
|
|
254
|
+
? `${tokens.filter(t => !STABLE_SYMBOLS.has(t.symbol)).length} 个代币均正常`
|
|
255
|
+
: `${anomalies.length} 个代币异常`,
|
|
256
|
+
ok: tokenDriftOk,
|
|
257
|
+
reason: tokenDriftOk ? null :
|
|
258
|
+
`相比上次变化 > 50%:${anomalies.slice(0, 8).join(' | ')}`,
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
checks.push({
|
|
262
|
+
name: 'Token 数据波动',
|
|
263
|
+
value: `首次运行,记录基准(${tokens.length} 个代币)`,
|
|
264
|
+
ok: true,
|
|
265
|
+
reason: null,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return checks;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
async function runApi({ larkWebhook, stateFile } = {}) {
|
|
275
|
+
const statePath = stateFile || DEFAULT_STATE_FILE;
|
|
276
|
+
const prev = loadState(statePath);
|
|
277
|
+
|
|
278
|
+
// 1. fetch overview
|
|
279
|
+
let overviewData;
|
|
280
|
+
try {
|
|
281
|
+
const stdout = execSync('byreal-cli overview -o json', {
|
|
282
|
+
timeout: 30_000,
|
|
283
|
+
encoding: 'utf8',
|
|
284
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
285
|
+
});
|
|
286
|
+
const parsed = parseJsonOutput(stdout);
|
|
287
|
+
if (!parsed?.success || !parsed?.data) throw new Error('invalid overview response');
|
|
288
|
+
overviewData = parsed.data;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error(chalk.red(`\n ✗ byreal-cli overview failed: ${err.message}\n`));
|
|
291
|
+
return { success: false, error: err.message, hasAlert: true };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 2. fetch pools
|
|
295
|
+
let pools = [];
|
|
296
|
+
try {
|
|
297
|
+
const stdout = execSync('byreal-cli pools list --page-size 100 -o json', {
|
|
298
|
+
timeout: 40_000,
|
|
299
|
+
encoding: 'utf8',
|
|
300
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
301
|
+
});
|
|
302
|
+
const parsed = parseJsonOutput(stdout);
|
|
303
|
+
if (!Array.isArray(parsed?.data?.pools)) throw new Error('invalid pools response');
|
|
304
|
+
pools = parsed.data.pools;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error(chalk.red(`\n ✗ byreal-cli pools failed: ${err.message}\n`));
|
|
307
|
+
return { success: false, error: err.message, hasAlert: true };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 3. fetch tokens
|
|
311
|
+
let tokens = [];
|
|
312
|
+
try {
|
|
313
|
+
const stdout = execSync('byreal-cli tokens list --page-size 100 -o json', {
|
|
314
|
+
timeout: 30_000,
|
|
315
|
+
encoding: 'utf8',
|
|
316
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
317
|
+
});
|
|
318
|
+
const parsed = parseJsonOutput(stdout);
|
|
319
|
+
if (!Array.isArray(parsed?.data?.tokens)) throw new Error('invalid tokens response');
|
|
320
|
+
tokens = parsed.data.tokens;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error(chalk.red(`\n ✗ byreal-cli tokens failed: ${err.message}\n`));
|
|
323
|
+
return { success: false, error: err.message, hasAlert: true };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 4. run checks
|
|
327
|
+
const overviewChecks = checkOverview(overviewData, prev);
|
|
328
|
+
const poolChecks = checkPools(pools, prev?.poolTvlMap);
|
|
329
|
+
const tokenChecks = checkTokens(tokens, prev?.tokenMap);
|
|
330
|
+
const allChecks = [...overviewChecks, ...poolChecks, ...tokenChecks];
|
|
331
|
+
const hasAlert = allChecks.some(c => !c.ok);
|
|
332
|
+
|
|
333
|
+
// 5. print
|
|
334
|
+
const time = utc8Label();
|
|
335
|
+
console.log('\n ' + chalk.bold('Byreal API Sanity Check') + ` ${chalk.dim(time)}\n`);
|
|
336
|
+
|
|
337
|
+
const sections = [
|
|
338
|
+
{ label: '── Overview ──', checks: overviewChecks },
|
|
339
|
+
{ label: '── Pools ──', checks: poolChecks },
|
|
340
|
+
{ label: '── Tokens ──', checks: tokenChecks },
|
|
341
|
+
];
|
|
342
|
+
for (const sec of sections) {
|
|
343
|
+
console.log(' ' + chalk.cyan(sec.label));
|
|
344
|
+
for (const c of sec.checks) {
|
|
345
|
+
const icon = c.ok ? chalk.green('✓') : chalk.red('✗');
|
|
346
|
+
console.log(` ${icon} ${c.name.padEnd(22)} ${c.value}`);
|
|
347
|
+
if (!c.ok) console.log(` ${chalk.red('→ ' + c.reason)}`);
|
|
348
|
+
}
|
|
349
|
+
console.log('');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (hasAlert) {
|
|
353
|
+
console.log(chalk.red(' ⚠️ 检测到异常,请关注\n'));
|
|
354
|
+
} else {
|
|
355
|
+
console.log(chalk.green(' ✅ 所有指标正常\n'));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 6. persist state
|
|
359
|
+
const poolTvlMap = {};
|
|
360
|
+
for (const p of pools) poolTvlMap[p.id] = p.tvl_usd;
|
|
361
|
+
|
|
362
|
+
// keyed by mint (unique) to handle duplicate symbols (e.g. counterfeit tokens)
|
|
363
|
+
const tokenMap = {};
|
|
364
|
+
for (const t of tokens) {
|
|
365
|
+
tokenMap[t.mint] = {
|
|
366
|
+
symbol: t.symbol,
|
|
367
|
+
price_usd: t.price_usd,
|
|
368
|
+
price_change_24h: t.price_change_24h,
|
|
369
|
+
volume_24h_usd: t.volume_24h_usd,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
saveState(statePath, {
|
|
374
|
+
timestamp: new Date().toISOString(),
|
|
375
|
+
tvl: overviewData.tvl,
|
|
376
|
+
tvl_change_24h: overviewData.tvl_change_24h,
|
|
377
|
+
fee_24h_usd: overviewData.fee_24h_usd,
|
|
378
|
+
fee_all: overviewData.fee_all,
|
|
379
|
+
pools_count: overviewData.pools_count,
|
|
380
|
+
poolTvlMap,
|
|
381
|
+
tokenMap,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// 7. lark — only notify when there is an alert
|
|
385
|
+
if (larkWebhook && hasAlert) {
|
|
386
|
+
const title = `🚨 Byreal API Monitor · ${time}`;
|
|
387
|
+
|
|
388
|
+
const rows = [];
|
|
389
|
+
|
|
390
|
+
const sectionDefs = [
|
|
391
|
+
{ label: '📊 Overview', checks: overviewChecks },
|
|
392
|
+
{ label: '🏊 Pools', checks: poolChecks },
|
|
393
|
+
{ label: '🪙 Tokens', checks: tokenChecks },
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
for (const sec of sectionDefs) {
|
|
397
|
+
const hasIssue = sec.checks.some(c => !c.ok);
|
|
398
|
+
rows.push([{ tag: 'text', text: `${hasIssue ? '⚠️' : '✅'} ${sec.label}` }]);
|
|
399
|
+
for (const c of sec.checks) {
|
|
400
|
+
rows.push([{ tag: 'text', text: ` ${c.ok ? '✅' : '❌'} ${c.name}: ${c.value}` }]);
|
|
401
|
+
if (!c.ok) {
|
|
402
|
+
rows.push([{ tag: 'text', text: ` → ${c.reason}` }]);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
rows.push([{ tag: 'text', text: ' ' }]);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await sendLark(larkWebhook, {
|
|
410
|
+
msg_type: 'post',
|
|
411
|
+
content: { post: { zh_cn: { title, content: rows } } },
|
|
412
|
+
});
|
|
413
|
+
console.log(chalk.dim(' Lark alert sent.\n'));
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.warn(chalk.yellow(` Warning: Lark send failed: ${err.message}\n`));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { success: true, hasAlert, checks: allChecks, overviewData, pools, tokens };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
module.exports = { runApi };
|