lovewebconnection 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1915 -2
  4. package/dist/alert-queue.d.ts +72 -0
  5. package/dist/alert-queue.js +155 -0
  6. package/dist/alert-store.d.ts +29 -0
  7. package/dist/alert-store.js +75 -0
  8. package/dist/balances.d.ts +221 -0
  9. package/dist/balances.js +1232 -0
  10. package/dist/cache.d.ts +37 -0
  11. package/dist/cache.js +128 -0
  12. package/dist/call.d.ts +46 -0
  13. package/dist/call.js +377 -0
  14. package/dist/chain.d.ts +97 -0
  15. package/dist/chain.js +419 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +1011 -0
  18. package/dist/compare-history.d.ts +82 -0
  19. package/dist/compare-history.js +340 -0
  20. package/dist/compare.d.ts +98 -0
  21. package/dist/compare.js +492 -0
  22. package/dist/contract.d.ts +107 -0
  23. package/dist/contract.js +162 -0
  24. package/dist/default-abi.d.ts +53 -0
  25. package/dist/default-abi.js +85 -0
  26. package/dist/history.d.ts +83 -0
  27. package/dist/history.js +422 -0
  28. package/dist/index.d.ts +54 -0
  29. package/dist/index.js +27 -0
  30. package/dist/market-page.d.ts +8 -0
  31. package/dist/market-page.js +554 -0
  32. package/dist/market-server.d.ts +46 -0
  33. package/dist/market-server.js +473 -0
  34. package/dist/market.d.ts +323 -0
  35. package/dist/market.js +835 -0
  36. package/dist/metadata-cache.d.ts +72 -0
  37. package/dist/metadata-cache.js +197 -0
  38. package/dist/metrics.d.ts +64 -0
  39. package/dist/metrics.js +138 -0
  40. package/dist/plot.d.ts +60 -0
  41. package/dist/plot.js +317 -0
  42. package/dist/presets-history.d.ts +372 -0
  43. package/dist/presets-history.js +1243 -0
  44. package/dist/presets-summary.d.ts +17 -0
  45. package/dist/presets-summary.js +55 -0
  46. package/dist/presets.d.ts +59 -0
  47. package/dist/presets.js +181 -0
  48. package/dist/prices.d.ts +63 -0
  49. package/dist/prices.js +154 -0
  50. package/dist/rotate.d.ts +19 -0
  51. package/dist/rotate.js +34 -0
  52. package/dist/scan.d.ts +81 -0
  53. package/dist/scan.js +501 -0
  54. package/dist/sessions.d.ts +104 -0
  55. package/dist/sessions.js +515 -0
  56. package/dist/signatures.d.ts +13 -0
  57. package/dist/signatures.js +43 -0
  58. package/dist/tokenfile.d.ts +50 -0
  59. package/dist/tokenfile.js +218 -0
  60. package/dist/views.d.ts +63 -0
  61. package/dist/views.js +415 -0
  62. package/dist/watch.d.ts +306 -0
  63. package/dist/watch.js +1121 -0
  64. package/dist/webhook.d.ts +17 -0
  65. package/dist/webhook.js +35 -0
  66. package/examples/erc20.abi.json +57 -0
  67. package/examples/xlayer.tokens.json +4 -0
  68. package/package.json +77 -4
package/dist/cli.js ADDED
@@ -0,0 +1,1011 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { closeSync, existsSync, mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { BalancesArgError, checkTokenAddressesReport, formatBalances, formatChunkProgress, parseBalanceArgs, readBalances, shortAddress, staleMetadataWarnings, toBalancesJson, } from "./balances.js";
7
+ import { ChainConfigError, ChainMismatchError, buildClient, connectionWarnings, formatHealth, parseChainConfig, probeWithFallback, publicEndpoint, toJsonPayload, } from "./chain.js";
8
+ import { assertContractChain, formatExampleCall, readExampleCall, toExampleCallJson, } from "./contract.js";
9
+ import { CallArgError, callViewFunction, coerceArgs, findFunction, formatCall, parseCallArgs, resolveAbi, toCallJson, } from "./call.js";
10
+ import { SignaturesArgError, callableSignatures, formatSignatures, parseSignaturesArgs, toSignaturesJson, } from "./signatures.js";
11
+ import { CompareArgError, compareEndpoints, formatCompare, parseCompareArgs, toCompareJson, toCompareLogRecord, } from "./compare.js";
12
+ import { MetadataCache, resolveCachePath } from "./metadata-cache.js";
13
+ import { CacheArgError, formatMetadataCache, inspectMetadataCache, parseCacheArgs, toCacheJson, } from "./cache.js";
14
+ import { AlertQueue } from "./alert-queue.js";
15
+ import { AlertStore, resolveAlertQueuePath } from "./alert-store.js";
16
+ import { RotatingLog } from "./rotate.js";
17
+ import { postWebhook } from "./webhook.js";
18
+ import { SessionsArgError, formatSessions, parseSessionsArgs, readWatchLog, saveSessionsCsv, sessionsCsv, summarizeWatchLog, toSessionsJson, } from "./sessions.js";
19
+ import { HistoryArgError, extractBalanceHistory, formatHistory, historyCsv, historyWideCsv, parseHistoryArgs, readHistoryLog, saveHistoryCsv, toHistoryJson, } from "./history.js";
20
+ import { PlotArgError, buildPlotReport, formatPlot, parsePlotArgs, readHistoryCsv, toPlotJson, } from "./plot.js";
21
+ import { CompareHistoryArgError, compareHistoryCsv, formatCompareHistory, parseCompareHistoryArgs, readCompareLog, saveCompareHistoryCsv, summarizeCompareLog, toCompareHistoryJson, } from "./compare-history.js";
22
+ import { TokenFileArgError, parseTokenFileArgs, saveTokenList, tokenListItems, } from "./tokenfile.js";
23
+ import { ViewsArgError, formatViews, parseViewsArgs, runViewFunctions, toViewsJson, } from "./views.js";
24
+ import { ScanArgError, formatScan, parseScanArgs, scanBalances, scanCsv, toScanJson, } from "./scan.js";
25
+ import { WatchArgError, formatWatchEvent, formatWatchSummary, parseWatchArgs, toWatchEventJson, toWatchSummaryJson, watchChain, } from "./watch.js";
26
+ import { MarketArgError, MarketError, MarketPool, fixedPrice, formatLiquidity, formatMarketAmount, formatMarketState, formatSwapQuote, loadPoolFile, parseMarketAmount, parseMarketArgs, parseShares, savePoolFile, toMarketJson, toSwapQuoteJson, } from "./market.js";
27
+ import { MarketServer } from "./market-server.js";
28
+ import { PresetsArgError, formatPresets, parsePresetsArgs, probePresets, toPresetsJson, } from "./presets.js";
29
+ import { PresetsHistoryArgError, appendPresetHistory, comparePresetHistory, formatPresetHistory, formatPresetHistoryCheck, formatPresetHistoryComparison, mergePresetHistory, parsePresetHistoryArgs, presetHistoryCheck, presetHistoryCsv, presetHistoryTimelineCsv, presetsBelowUptime, narrowestRetentionMs, readPresetHistoryFile, retentionWindowWarnings, savePresetHistory, savePresetHistoryCsv, summarizePresetHistory, toPresetHistoryJson, } from "./presets-history.js";
30
+ const argv = process.argv.slice(2);
31
+ const command = argv[0] ?? "";
32
+ const isBalances = command === "balances";
33
+ const isExample = command === "example";
34
+ const isWatch = command === "watch";
35
+ const isCall = command === "call";
36
+ const isScan = command === "scan";
37
+ const isCompare = command === "compare";
38
+ const isTokens = command === "tokens";
39
+ const isViews = command === "views";
40
+ const isSessions = command === "sessions";
41
+ const isHistory = command === "history";
42
+ const isCompareHistory = command === "compare-history";
43
+ const isPlot = command === "plot";
44
+ const isSignatures = command === "signatures";
45
+ const isCache = command === "cache";
46
+ const isMarket = command === "market";
47
+ const isPresets = command === "presets";
48
+ const isPresetHistory = command === "presets-history";
49
+ const isHealth = command === "" || command === "chain" || command.startsWith("-");
50
+ if (!isHealth &&
51
+ !isBalances &&
52
+ !isTokens &&
53
+ !isViews &&
54
+ !isExample &&
55
+ !isWatch &&
56
+ !isCall &&
57
+ !isScan &&
58
+ !isCompare &&
59
+ !isSessions &&
60
+ !isHistory &&
61
+ !isCompareHistory &&
62
+ !isSignatures &&
63
+ !isCache &&
64
+ !isMarket &&
65
+ !isPresets &&
66
+ !isPresetHistory &&
67
+ !isPlot) {
68
+ console.error(`unknown command: ${command}`);
69
+ console.error("usage: lwc [chain|presets|presets-history|balances|tokens|views|signatures|example|scan|compare|compare-history|watch|call|sessions|history|plot|cache|market] [--chain <name|id>] [--json]");
70
+ process.exit(1);
71
+ }
72
+ /** The `--chain <name|id>` selector, or null when it was not passed. */
73
+ function chainFlagValue(args) {
74
+ const inline = args.find((arg) => arg.startsWith("--chain="));
75
+ if (inline)
76
+ return inline.slice("--chain=".length).trim() || null;
77
+ const index = args.indexOf("--chain");
78
+ if (index === -1)
79
+ return null;
80
+ const value = args[index + 1];
81
+ return value && !value.startsWith("-") ? value : null;
82
+ }
83
+ /** Remove the global --chain flag so per-command parsers never see it. */
84
+ function stripChainFlag(args) {
85
+ const kept = [];
86
+ for (let index = 0; index < args.length; index += 1) {
87
+ const arg = args[index] ?? "";
88
+ if (arg === "--chain") {
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (arg.startsWith("--chain="))
93
+ continue;
94
+ kept.push(arg);
95
+ }
96
+ return kept;
97
+ }
98
+ const chainOverride = chainFlagValue(argv);
99
+ const flags = stripChainFlag(isHealth ? argv : argv.slice(1));
100
+ const asJson = flags.includes("--json");
101
+ // Probes the built-in presets themselves, so it needs no configuration.
102
+ if (isPresets) {
103
+ try {
104
+ const { timeoutMs, historyPath } = parsePresetsArgs(flags);
105
+ const report = await probePresets({ timeoutMs });
106
+ console.log(asJson ? JSON.stringify(toPresetsJson(report), null, 2) : formatPresets(report));
107
+ // Recorded after the report is printed: a read-only history file must not
108
+ // hide the probe's result or change its exit code.
109
+ if (historyPath !== null) {
110
+ try {
111
+ appendPresetHistory(historyPath, report);
112
+ }
113
+ catch (error) {
114
+ console.error(`warning: could not record history at ${historyPath}: ${error instanceof Error ? error.message : String(error)}`);
115
+ }
116
+ }
117
+ // Non-zero when an endpoint is unhealthy, so scripts and CI can gate on it.
118
+ process.exit(report.failed === 0 ? 0 : 1);
119
+ }
120
+ catch (error) {
121
+ if (error instanceof PresetsArgError) {
122
+ console.error(`usage error: ${error.message}`);
123
+ console.error(" example: lwc presets --timeout 5000");
124
+ }
125
+ else {
126
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ process.exit(1);
129
+ }
130
+ }
131
+ // Local-only: reads the runs recorded by `lwc presets --history`.
132
+ if (isPresetHistory) {
133
+ let historyPath = "";
134
+ try {
135
+ const args = parsePresetHistoryArgs(flags);
136
+ historyPath = args.path;
137
+ const primary = readPresetHistoryFile(historyPath);
138
+ const records = primary.records;
139
+ // --merge folds other copies in before anything reads them, so a window,
140
+ // a gate and an export all judge the same combined record.
141
+ const extras = args.merge.map((path) => {
142
+ try {
143
+ return readPresetHistoryFile(path);
144
+ }
145
+ catch (error) {
146
+ if (error?.code === "ENOENT") {
147
+ throw new PresetsHistoryArgError(`no history at ${path} to merge.`);
148
+ }
149
+ throw error;
150
+ }
151
+ });
152
+ const merged = extras.length === 0
153
+ ? null
154
+ : mergePresetHistory([records, ...extras.map((file) => file.records)], { retainMs: args.retain });
155
+ const history = merged === null ? records : merged.records;
156
+ // What the files say they trimmed to, before this invocation adds its own.
157
+ const declaredRetain = narrowestRetentionMs([
158
+ primary.retainMs,
159
+ ...extras.map((file) => file.retainMs),
160
+ ]);
161
+ // Notes go to stderr so --json on stdout stays one parseable document.
162
+ if (merged !== null) {
163
+ const dropped = merged.duplicates === 0
164
+ ? ""
165
+ : `, ${merged.duplicates} duplicate${merged.duplicates === 1 ? "" : "s"} dropped`;
166
+ const pruned = merged.pruned === 0
167
+ ? ""
168
+ : `, ${merged.pruned} older run${merged.pruned === 1 ? "" : "s"} pruned`;
169
+ console.error(`merged ${history.length} run${history.length === 1 ? "" : "s"} from ${merged.counts.length} file${merged.counts.length === 1 ? "" : "s"}${dropped}${pruned}`);
170
+ }
171
+ // A file records the retention it was written with, so a later read can
172
+ // still explain a window it cannot cover; a --retain that just pruned is
173
+ // the tighter of the two. Without either, missing runs are not our doing.
174
+ const readRetain = narrowestRetentionMs([
175
+ declaredRetain,
176
+ merged !== null && merged.pruned > 0 ? args.retain : null,
177
+ ]);
178
+ for (const warning of retentionWindowWarnings(history.at(-1)?.at ?? null, readRetain, [
179
+ { label: "--since", since: args.since },
180
+ { label: "--compare", since: args.compare?.since ?? null },
181
+ ])) {
182
+ console.error(`warning: ${warning}`);
183
+ }
184
+ if (args.out !== null) {
185
+ savePresetHistory(args.out, history, { retainMs: args.retain ?? declaredRetain });
186
+ console.error(`wrote ${history.length} runs → ${args.out}`);
187
+ }
188
+ const summary = summarizePresetHistory(history, { since: args.since, until: args.until, limit: args.limit }, { retainMs: readRetain });
189
+ // --check is the local gate: coverage, retention and the verdict in one
190
+ // pass, so a machine without a CI job still gets the whole answer.
191
+ if (args.check) {
192
+ const threshold = args.failUnder;
193
+ if (threshold === null) {
194
+ throw new PresetsHistoryArgError("--check reports a gate, so pass --fail-under <pct> with it.");
195
+ }
196
+ const check = presetHistoryCheck(historyPath, summary, threshold, {
197
+ maxAgeMs: args.maxAge,
198
+ });
199
+ console.log(asJson
200
+ ? JSON.stringify(check, null, 2)
201
+ : formatPresetHistoryCheck(check, history, { rows: args.freshnessRows ?? 5 }));
202
+ process.exit(check.gate.passed ? 0 : 1);
203
+ }
204
+ const comparison = args.compare === null
205
+ ? null
206
+ : comparePresetHistory(history, { since: args.since, until: args.until }, args.compare, { retainMs: readRetain });
207
+ if (args.timelinePath !== null) {
208
+ const csv = presetHistoryTimelineCsv(history, {
209
+ since: args.since,
210
+ until: args.until,
211
+ limit: args.limit,
212
+ });
213
+ if (args.timelinePath === "-") {
214
+ console.log(csv);
215
+ }
216
+ else {
217
+ savePresetHistoryCsv(args.timelinePath, csv);
218
+ const rows = csv.split("\n").length - 1;
219
+ console.log(`wrote ${rows} row${rows === 1 ? "" : "s"} → ${args.timelinePath}`);
220
+ }
221
+ }
222
+ else if (args.csvPath !== null) {
223
+ const csv = presetHistoryCsv(summary);
224
+ if (args.csvPath === "-") {
225
+ console.log(csv);
226
+ }
227
+ else {
228
+ savePresetHistoryCsv(args.csvPath, csv);
229
+ const rows = summary.presets.length;
230
+ console.log(`wrote ${rows} row${rows === 1 ? "" : "s"} → ${args.csvPath}`);
231
+ }
232
+ }
233
+ else {
234
+ console.log(comparison === null
235
+ ? asJson
236
+ ? JSON.stringify(toPresetHistoryJson(summary), null, 2)
237
+ : formatPresetHistory(summary)
238
+ : asJson
239
+ ? JSON.stringify(comparison, null, 2)
240
+ : formatPresetHistoryComparison(comparison));
241
+ }
242
+ // --fail-under is a CI gate over the selected window, not a display option:
243
+ // an empty window cannot be shown to clear it.
244
+ if (args.failUnder !== null) {
245
+ if (summary.runs === 0) {
246
+ console.error(`error: no runs in the window to check against --fail-under ${args.failUnder}%.`);
247
+ process.exit(1);
248
+ }
249
+ const shortfalls = presetsBelowUptime(summary, args.failUnder);
250
+ for (const row of shortfalls) {
251
+ console.error(`error: ${row.preset} uptime ${row.uptimePct}% over ${row.runs} run${row.runs === 1 ? "" : "s"} is below --fail-under ${args.failUnder}%.`);
252
+ }
253
+ process.exit(shortfalls.length === 0 ? 0 : 1);
254
+ }
255
+ process.exit(0);
256
+ }
257
+ catch (error) {
258
+ if (error instanceof PresetsHistoryArgError) {
259
+ console.error(`usage error: ${error.message}`);
260
+ console.error(" example: lwc presets-history .lwc-history/presets.jsonl --json");
261
+ }
262
+ else if (error?.code === "ENOENT") {
263
+ console.error(`usage error: no history at ${historyPath || "(no path)"}.`);
264
+ console.error(" record runs first: lwc presets --history .lwc-history/presets.jsonl");
265
+ }
266
+ else {
267
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
268
+ }
269
+ process.exit(1);
270
+ }
271
+ }
272
+ // Local-only: listing an ABI's signatures needs no RPC configuration.
273
+ if (isSignatures) {
274
+ try {
275
+ const { abiPath } = parseSignaturesArgs(flags);
276
+ const { abi, source } = resolveAbi(abiPath);
277
+ const signatures = callableSignatures(abi);
278
+ console.log(asJson
279
+ ? JSON.stringify(toSignaturesJson(signatures, source), null, 2)
280
+ : formatSignatures(signatures, source));
281
+ }
282
+ catch (error) {
283
+ if (error instanceof SignaturesArgError || error instanceof CallArgError) {
284
+ console.error(`usage error: ${error.message}`);
285
+ console.error(" example: lwc signatures ./examples/erc20.abi.json");
286
+ }
287
+ else {
288
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
289
+ }
290
+ process.exit(1);
291
+ }
292
+ process.exit(0);
293
+ }
294
+ // Local-only: the market is a pool file, not chain state, so it needs no RPC.
295
+ if (isMarket) {
296
+ try {
297
+ const args = parseMarketArgs(flags);
298
+ if (args.serve !== null) {
299
+ const pool = loadPoolFile(args.poolPath);
300
+ const server = new MarketServer(pool, args.poolPath, args.serve);
301
+ const info = await server.listen();
302
+ const state = pool.state();
303
+ if (asJson) {
304
+ console.log(JSON.stringify({ listening: info.url, ...toMarketJson(state, args.poolPath) }, null, 2));
305
+ }
306
+ else {
307
+ console.log("lwc — market api");
308
+ console.log("");
309
+ console.log(` pool ${args.poolPath}`);
310
+ console.log(` listening ${info.url}`);
311
+ console.log(` price ${state.price} ${state.quoteSymbol} per ${state.baseSymbol} (fixed)`);
312
+ console.log(` liquidity ${formatMarketAmount(state.liquidityValueQuote, state.quoteDecimals)} ${state.quoteSymbol}`);
313
+ if (args.serve.corsOrigins.length > 0) {
314
+ console.log(` cors ${args.serve.corsOrigins.join(", ")}`);
315
+ }
316
+ const rateLimit = args.serve.rateLimitPerMinute ?? 120;
317
+ console.log(` rate limit ${rateLimit === 0 ? "off" : `${rateLimit} requests/min per client`}`);
318
+ console.log(` limits ${args.serve.limitsPath ?? "in memory"}`);
319
+ if (args.serve.accessKey !== null) {
320
+ console.log(" access key required (x-market-key header, ?key=, or bearer)");
321
+ }
322
+ else if (!["127.0.0.1", "localhost", "::1"].includes(args.serve.host)) {
323
+ console.log(` access OPEN — anyone who reaches ${args.serve.host} can swap`);
324
+ }
325
+ console.log("");
326
+ console.log(" GET / swap page (open in a browser)");
327
+ console.log(" GET /quote?side=buy&amount=1");
328
+ console.log(' POST /swap {"side":"buy","amount":"1"}');
329
+ console.log("");
330
+ console.log(" Ctrl+C stops the api.");
331
+ }
332
+ await new Promise((resolve) => {
333
+ process.once("SIGINT", () => resolve());
334
+ process.once("SIGTERM", () => resolve());
335
+ });
336
+ await server.close();
337
+ if (!asJson)
338
+ console.log("\nmarket api stopped.");
339
+ process.exit(0);
340
+ }
341
+ if (args.init !== null) {
342
+ const init = args.init;
343
+ const price = fixedPrice(init.price, init.baseDecimals, init.quoteDecimals);
344
+ const pool = MarketPool.create({
345
+ baseSymbol: init.baseSymbol,
346
+ quoteSymbol: init.quoteSymbol,
347
+ baseDecimals: init.baseDecimals,
348
+ quoteDecimals: init.quoteDecimals,
349
+ priceNum: price.num,
350
+ priceDen: price.den,
351
+ feeBps: init.feeBps,
352
+ }, parseMarketAmount(init.seed, init.quoteDecimals, "--seed"));
353
+ savePoolFile(args.poolPath, pool, { force: args.force });
354
+ const state = pool.state();
355
+ console.log(asJson
356
+ ? JSON.stringify({ created: true, ...toMarketJson(state, args.poolPath) }, null, 2)
357
+ : formatMarketState(state, args.poolPath));
358
+ process.exit(0);
359
+ }
360
+ const pool = loadPoolFile(args.poolPath);
361
+ const before = pool.state();
362
+ const operation = args.operation;
363
+ if (operation.kind === "state") {
364
+ console.log(asJson
365
+ ? JSON.stringify(toMarketJson(before, args.poolPath), null, 2)
366
+ : formatMarketState(before, args.poolPath));
367
+ process.exit(0);
368
+ }
369
+ if (operation.kind === "swap") {
370
+ const isBuy = operation.side === "buy";
371
+ const amount = parseMarketAmount(operation.amount, isBuy ? before.quoteDecimals : before.baseDecimals, isBuy ? "--buy" : "--sell");
372
+ if (args.dryRun) {
373
+ const quote = pool.quote(operation.side, amount);
374
+ console.log(asJson
375
+ ? JSON.stringify(toSwapQuoteJson(quote, before), null, 2)
376
+ : formatSwapQuote(quote, before));
377
+ process.exit(quote.executable ? 0 : 1);
378
+ }
379
+ const fill = pool.swap(operation.side, amount);
380
+ savePoolFile(args.poolPath, pool, { force: true });
381
+ const after = pool.state();
382
+ if (asJson) {
383
+ console.log(JSON.stringify({ ...toSwapQuoteJson(fill, after), state: toMarketJson(after, args.poolPath) }, null, 2));
384
+ }
385
+ else {
386
+ console.log(formatSwapQuote(fill, after, { applied: true }));
387
+ console.log("");
388
+ console.log(` volume ${formatMarketAmount(after.volumeQuote, after.quoteDecimals)} ${after.quoteSymbol} over ${after.swaps} swap${after.swaps === 1 ? "" : "s"}`);
389
+ }
390
+ process.exit(0);
391
+ }
392
+ let receipt;
393
+ if (operation.remove !== null) {
394
+ const shares = operation.remove === "all" ? before.shares : parseShares(operation.remove);
395
+ if (shares === 0n)
396
+ throw new MarketArgError("the pool has no shares to remove.");
397
+ receipt = pool.removeLiquidity(shares);
398
+ }
399
+ else {
400
+ const quoteIn = operation.quote === null
401
+ ? 0n
402
+ : parseMarketAmount(operation.quote, before.quoteDecimals, "--add");
403
+ const baseIn = operation.base === null
404
+ ? 0n
405
+ : parseMarketAmount(operation.base, before.baseDecimals, "--add-base");
406
+ receipt = pool.addLiquidity({ quote: quoteIn, base: baseIn });
407
+ }
408
+ savePoolFile(args.poolPath, pool, { force: true });
409
+ const after = pool.state();
410
+ console.log(asJson
411
+ ? JSON.stringify({
412
+ kind: receipt.kind,
413
+ base: receipt.base.toString(),
414
+ quote: receipt.quote.toString(),
415
+ shares: receipt.shares.toString(),
416
+ sharesAfter: receipt.sharesAfter.toString(),
417
+ state: toMarketJson(after, args.poolPath),
418
+ }, null, 2)
419
+ : `${formatLiquidity(receipt, after)}\n\n${formatMarketState(after, args.poolPath)}`);
420
+ process.exit(0);
421
+ }
422
+ catch (error) {
423
+ if (error instanceof MarketArgError) {
424
+ console.error(`usage error: ${error.message}`);
425
+ console.error(" example: lwc market --pool ./market.json --init --price 28.842 --base ZPC --quote AUD --seed 1");
426
+ console.error(" then: lwc market --pool ./market.json --buy 10");
427
+ }
428
+ else if (error instanceof MarketError) {
429
+ console.error(`market error: ${error.message}`);
430
+ }
431
+ else {
432
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
433
+ }
434
+ process.exit(1);
435
+ }
436
+ }
437
+ // Local-only: reading a watch log needs no RPC configuration, so this runs
438
+ // before .env is loaded and before the chain config is validated.
439
+ if (isSessions) {
440
+ try {
441
+ const { path, since, sessionPrefixes, csvPath } = parseSessionsArgs(flags);
442
+ const report = {
443
+ path,
444
+ ...summarizeWatchLog(readWatchLog(path), { since, sessionPrefixes }),
445
+ };
446
+ if (csvPath !== null && csvPath !== "-") {
447
+ saveSessionsCsv(csvPath, sessionsCsv(report));
448
+ const rows = report.sessions.length;
449
+ console.log(`wrote ${rows} row${rows === 1 ? "" : "s"} → ${csvPath}`);
450
+ }
451
+ else if (csvPath === "-") {
452
+ console.log(sessionsCsv(report).trimEnd());
453
+ }
454
+ else {
455
+ console.log(asJson ? JSON.stringify(toSessionsJson(report), null, 2) : formatSessions(report));
456
+ }
457
+ }
458
+ catch (error) {
459
+ if (error instanceof SessionsArgError) {
460
+ console.error(`usage error: ${error.message}`);
461
+ console.error(" example: lwc sessions ./watch.ndjson [--csv ./sessions.csv]");
462
+ }
463
+ else {
464
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
465
+ }
466
+ process.exit(1);
467
+ }
468
+ process.exit(0);
469
+ }
470
+ // Local-only: balance history comes from a watch log, not the chain.
471
+ if (isHistory) {
472
+ try {
473
+ const { path, csvPath, session, wide, usd, usdOnly } = parseHistoryArgs(flags);
474
+ const report = { path, ...extractBalanceHistory(readHistoryLog(path), { session }) };
475
+ const csv = wide ? historyWideCsv(report, { usd, usdOnly }) : historyCsv(report);
476
+ if (csvPath !== null && csvPath !== "-") {
477
+ saveHistoryCsv(csvPath, csv);
478
+ }
479
+ if (csvPath === "-") {
480
+ console.log(csv.trimEnd());
481
+ }
482
+ else if (asJson) {
483
+ console.log(JSON.stringify(toHistoryJson(report), null, 2));
484
+ }
485
+ else if (csvPath !== null) {
486
+ const written = Math.max(0, csv.trimEnd().split("\n").length - 1);
487
+ console.log(`wrote ${written} row${written === 1 ? "" : "s"} → ${csvPath}`);
488
+ }
489
+ else {
490
+ console.log(formatHistory(report));
491
+ }
492
+ }
493
+ catch (error) {
494
+ if (error instanceof HistoryArgError) {
495
+ console.error(`usage error: ${error.message}`);
496
+ console.error(" example: lwc history ./watch.ndjson --csv ./balances.csv [--wide [--usd | --usd-only]]");
497
+ }
498
+ else {
499
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
500
+ }
501
+ process.exit(1);
502
+ }
503
+ process.exit(0);
504
+ }
505
+ // Local-only: charting a history CSV needs no RPC configuration.
506
+ if (isPlot) {
507
+ try {
508
+ const { path, width } = parsePlotArgs(flags);
509
+ const report = buildPlotReport(readHistoryCsv(path), { path, width });
510
+ console.log(asJson ? JSON.stringify(toPlotJson(report), null, 2) : formatPlot(report));
511
+ }
512
+ catch (error) {
513
+ if (error instanceof PlotArgError) {
514
+ console.error(`usage error: ${error.message}`);
515
+ console.error(" example: lwc plot ./balances.csv --width 60");
516
+ }
517
+ else {
518
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
519
+ }
520
+ process.exit(1);
521
+ }
522
+ process.exit(0);
523
+ }
524
+ // Local-only: a compare log summary needs no RPC configuration.
525
+ if (isCompareHistory) {
526
+ try {
527
+ const { path, csvPath } = parseCompareHistoryArgs(flags);
528
+ const report = { path, ...summarizeCompareLog(readCompareLog(path)) };
529
+ if (csvPath !== null && csvPath !== "-") {
530
+ saveCompareHistoryCsv(csvPath, compareHistoryCsv(report));
531
+ const rows = report.runs.reduce((sum, run) => sum + run.providers.length, 0);
532
+ console.log(`wrote ${rows} row${rows === 1 ? "" : "s"} → ${csvPath}`);
533
+ }
534
+ else if (csvPath === "-") {
535
+ console.log(compareHistoryCsv(report).trimEnd());
536
+ }
537
+ else {
538
+ console.log(asJson
539
+ ? JSON.stringify(toCompareHistoryJson(report), null, 2)
540
+ : formatCompareHistory(report));
541
+ }
542
+ }
543
+ catch (error) {
544
+ if (error instanceof CompareHistoryArgError) {
545
+ console.error(`usage error: ${error.message}`);
546
+ console.error(" example: lwc compare-history ./compare.ndjson [--csv ./providers.csv]");
547
+ }
548
+ else {
549
+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
550
+ }
551
+ process.exit(1);
552
+ }
553
+ process.exit(0);
554
+ }
555
+ /** One id per run, so several sessions in one log stay separable. */
556
+ function newSessionId() {
557
+ return `${Date.now().toString(36)}-${randomUUID().slice(0, 6)}`;
558
+ }
559
+ /** Remove cache flags so the per-command parsers never see their values. */
560
+ function stripCacheFlags(args) {
561
+ const kept = [];
562
+ for (let index = 0; index < args.length; index += 1) {
563
+ const arg = args[index] ?? "";
564
+ if (arg === "--no-cache" || arg === "--refresh-cache")
565
+ continue;
566
+ if (arg === "--cache-file") {
567
+ index += 1;
568
+ continue;
569
+ }
570
+ if (arg.startsWith("--cache-file="))
571
+ continue;
572
+ kept.push(arg);
573
+ }
574
+ return kept;
575
+ }
576
+ function cacheFlagPath(args) {
577
+ const inline = args.find((arg) => arg.startsWith("--cache-file="));
578
+ if (inline)
579
+ return inline.slice("--cache-file=".length) || null;
580
+ const index = args.findIndex((arg) => arg === "--cache-file");
581
+ if (index === -1)
582
+ return null;
583
+ const value = args[index + 1];
584
+ return value && !value.startsWith("-") ? value : null;
585
+ }
586
+ function hasBatchSizeFlag(args) {
587
+ return args.some((arg) => arg === "--batch-size" || arg.startsWith("--batch-size="));
588
+ }
589
+ function openMetadataCache(args, chainId) {
590
+ if (args.includes("--no-cache"))
591
+ return null;
592
+ return new MetadataCache({ chainId, path: resolveCachePath(cacheFlagPath(args)) });
593
+ }
594
+ function saveMetadataCache(cache) {
595
+ const warning = cache?.save() ?? null;
596
+ if (warning)
597
+ console.error(`cache not saved: ${warning}`);
598
+ }
599
+ function printWarnings(warnings) {
600
+ for (const warning of warnings)
601
+ console.error(`warning: ${warning}`);
602
+ }
603
+ // Node's built-in .env loader; keep it optional so older runtimes still work.
604
+ const loadEnvFile = process.loadEnvFile;
605
+ if (typeof loadEnvFile === "function" && existsSync(".env")) {
606
+ loadEnvFile(".env");
607
+ }
608
+ function describeError(error) {
609
+ if (error instanceof Error) {
610
+ const short = error.shortMessage;
611
+ if (typeof short === "string" && short.length > 0)
612
+ return short;
613
+ return error.message;
614
+ }
615
+ return String(error);
616
+ }
617
+ try {
618
+ const config = parseChainConfig(process.env, chainOverride);
619
+ const client = buildClient(config);
620
+ if (isCache) {
621
+ if (flags.includes("--no-cache")) {
622
+ throw new CacheArgError("--no-cache leaves no cache to inspect.");
623
+ }
624
+ const cachePath = resolveCachePath(cacheFlagPath(flags));
625
+ const { clear } = parseCacheArgs(stripCacheFlags(flags));
626
+ const cache = new MetadataCache({ chainId: config.chainId, path: cachePath });
627
+ const report = inspectMetadataCache(cache, config, cachePath, { clear });
628
+ console.log(asJson ? JSON.stringify(toCacheJson(report), null, 2) : formatMetadataCache(report));
629
+ printWarnings(report.warnings);
630
+ saveMetadataCache(cache);
631
+ }
632
+ else if (isBalances) {
633
+ const cache = openMetadataCache(flags, config.chainId);
634
+ const { owner, tokens, labels, batchSize, maxSubCalls, failOnRetry, verbose, maxCacheAgeMs, staleAfterMs, warnings, } = parseBalanceArgs(stripCacheFlags(flags));
635
+ printWarnings(warnings);
636
+ const report = await readBalances(client, config, owner, tokens, {
637
+ cache: cache ?? undefined,
638
+ refreshCache: cache !== null && flags.includes("--refresh-cache"),
639
+ labels,
640
+ batchSize: hasBatchSizeFlag(flags) ? batchSize : undefined,
641
+ maxSubCalls: maxSubCalls ?? undefined,
642
+ maxCacheAgeMs: maxCacheAgeMs ?? undefined,
643
+ onChunkProgress: verbose
644
+ ? (chunk) => console.error(formatChunkProgress(chunk))
645
+ : undefined,
646
+ });
647
+ printWarnings(staleMetadataWarnings(report.tokens, staleAfterMs));
648
+ console.log(asJson ? JSON.stringify(toBalancesJson(report), null, 2) : formatBalances(report));
649
+ saveMetadataCache(cache);
650
+ if (failOnRetry && report.recovery.failedChunks > 0)
651
+ process.exitCode = 1;
652
+ }
653
+ else if (isTokens) {
654
+ const { entries, outPath, force, merge, maxSubCalls, warnings } = parseTokenFileArgs(flags);
655
+ const verification = await checkTokenAddressesReport(client, entries.map((entry) => entry.address), { maxSubCalls: maxSubCalls ?? undefined });
656
+ const checks = verification.checks;
657
+ const json = saveTokenList(outPath, entries, { force: force || merge });
658
+ printWarnings(warnings);
659
+ printWarnings(checks
660
+ .filter((check) => check.issue !== null)
661
+ .map((check) => `token ${shortAddress(check.address)} is not a readable ERC-20: ${check.issue}`));
662
+ if (asJson) {
663
+ console.log(JSON.stringify({
664
+ ok: true,
665
+ path: outPath,
666
+ tokens: tokenListItems(entries),
667
+ chunkCount: verification.chunkCount,
668
+ maxSubCalls: verification.maxSubCalls,
669
+ }, null, 2));
670
+ }
671
+ else if (outPath === "-") {
672
+ console.log(json.trimEnd());
673
+ }
674
+ else {
675
+ console.log(`wrote ${entries.length} token${entries.length === 1 ? "" : "s"} → ${outPath}`);
676
+ }
677
+ }
678
+ else if (isViews) {
679
+ const { address, abiPath, spec, block, maxSubCalls } = parseViewsArgs(flags);
680
+ const { abi, source } = resolveAbi(abiPath);
681
+ const report = await runViewFunctions(client, address, abi, source, spec, block, maxSubCalls);
682
+ console.log(asJson ? JSON.stringify(toViewsJson(report), null, 2) : formatViews(report));
683
+ if (report.failed > 0)
684
+ process.exitCode = 1;
685
+ }
686
+ else if (isExample) {
687
+ const startedAt = performance.now();
688
+ const result = await readExampleCall(client);
689
+ const latencyMs = Math.round(performance.now() - startedAt);
690
+ assertContractChain(result, config);
691
+ console.log(asJson
692
+ ? JSON.stringify(toExampleCallJson(result, config, latencyMs), null, 2)
693
+ : formatExampleCall(result, config, latencyMs));
694
+ }
695
+ else if (isCall) {
696
+ const { address, functionName, abiPath, args, block } = parseCallArgs(flags);
697
+ const { abi } = resolveAbi(abiPath);
698
+ const fn = findFunction(abi, functionName);
699
+ const coerced = coerceArgs(fn, args);
700
+ const outcome = await callViewFunction(client, address, abi, fn, coerced, block);
701
+ const display = { contract: address, fn, ...outcome };
702
+ console.log(asJson ? JSON.stringify(toCallJson(display), null, 2) : formatCall(display));
703
+ }
704
+ else if (isScan) {
705
+ const parsed = parseScanArgs(stripCacheFlags(flags));
706
+ const { owners, tokens, csvPath, labels, batchSize, maxSubCalls, failOnRetry, quiet, maxCacheAgeMs, staleAfterMs, warnings, } = parsed;
707
+ printWarnings(warnings);
708
+ if (asJson && csvPath) {
709
+ throw new ScanArgError("use either --json or --csv, not both.");
710
+ }
711
+ const cache = openMetadataCache(flags, config.chainId);
712
+ const report = await scanBalances(client, config, owners, tokens, {
713
+ cache: cache ?? undefined,
714
+ refreshCache: cache !== null && flags.includes("--refresh-cache"),
715
+ labels,
716
+ batchSize: hasBatchSizeFlag(flags) ? batchSize : undefined,
717
+ maxSubCalls: maxSubCalls ?? undefined,
718
+ maxCacheAgeMs: maxCacheAgeMs ?? undefined,
719
+ onChunkProgress: quiet ? undefined : (chunk) => {
720
+ const status = chunk.status === "batched"
721
+ ? "ok"
722
+ : chunk.status === "recovered"
723
+ ? "retried, recovered"
724
+ : chunk.status === "partial"
725
+ ? "retried, partial"
726
+ : "failed";
727
+ const individual = chunk.individual.length > 0 ? `, ${chunk.individual.length} individual` : "";
728
+ console.error(` chunk ${shortAddress(chunk.owner)} ${chunk.index}/${chunk.total} (${chunk.tokens} token${chunk.tokens === 1 ? "" : "s"}) ${status}${individual}`);
729
+ },
730
+ });
731
+ printWarnings(staleMetadataWarnings(report.tokens, staleAfterMs));
732
+ if (csvPath === null) {
733
+ console.log(asJson ? JSON.stringify(toScanJson(report), null, 2) : formatScan(report));
734
+ }
735
+ else if (csvPath === "-") {
736
+ console.log(scanCsv(report).trimEnd());
737
+ }
738
+ else {
739
+ mkdirSync(dirname(csvPath), { recursive: true });
740
+ writeFileSync(csvPath, scanCsv(report));
741
+ console.log(`wrote ${report.rows.length} owners × ${report.tokens.length} tokens → ${csvPath}`);
742
+ }
743
+ saveMetadataCache(cache);
744
+ if (failOnRetry && report.recovery.failedChunks > 0)
745
+ process.exitCode = 1;
746
+ }
747
+ else if (isCompare) {
748
+ const args = parseCompareArgs(flags);
749
+ const report = await compareEndpoints(config, args);
750
+ console.log(asJson ? JSON.stringify(toCompareJson(report), null, 2) : formatCompare(report));
751
+ if (args.logPath !== null) {
752
+ try {
753
+ mkdirSync(dirname(args.logPath), { recursive: true });
754
+ const log = new RotatingLog({ path: args.logPath, maxBytes: args.logMaxBytes });
755
+ log.append(JSON.stringify(toCompareLogRecord(report, newSessionId())));
756
+ }
757
+ catch (error) {
758
+ console.error(`log not written: ${describeError(error)}`);
759
+ process.exitCode = 1;
760
+ }
761
+ }
762
+ if (report.endpoints.some((endpoint) => !endpoint.ok)) {
763
+ process.exitCode = 1;
764
+ }
765
+ }
766
+ else if (isWatch) {
767
+ if (flags.includes("--stop")) {
768
+ const logIndex = flags.findIndex((arg) => arg === "--log" || arg.startsWith("--log="));
769
+ const logValue = logIndex === -1
770
+ ? null
771
+ : flags[logIndex] === "--log"
772
+ ? (flags[logIndex + 1] ?? null)
773
+ : (flags[logIndex] ?? "").slice("--log=".length);
774
+ if (!logValue) {
775
+ console.error("usage error: --stop requires --log <path> naming the running service.");
776
+ process.exit(1);
777
+ }
778
+ writeFileSync(`${logValue}.stop`, "");
779
+ console.log(`stop requested: ${logValue}.stop — the running watch exits with a summary.`);
780
+ process.exit(0);
781
+ }
782
+ const options = parseWatchArgs(flags);
783
+ if (options.detach) {
784
+ const logPath = options.logPath;
785
+ const outPath = `${logPath}.out`;
786
+ mkdirSync(dirname(logPath), { recursive: true });
787
+ const out = openSync(outPath, "a");
788
+ const childArgs = process.argv.slice(2).filter((arg) => arg !== "--detach");
789
+ const child = spawn(process.execPath, [process.argv[1] ?? "src/cli.ts", ...childArgs], {
790
+ cwd: process.cwd(),
791
+ detached: true,
792
+ stdio: ["ignore", out, out],
793
+ });
794
+ child.unref();
795
+ closeSync(out);
796
+ writeFileSync(`${logPath}.pid`, `${child.pid ?? ""}\n`);
797
+ console.log(`lwc watch detached (pid ${child.pid}); events → ${logPath}, console → ${outPath}`);
798
+ console.log(` stop with: lwc watch --log ${logPath} --stop`);
799
+ process.exit(0);
800
+ }
801
+ const logPath = options.logPath;
802
+ const alertQueuePath = resolveAlertQueuePath(options.alertQueuePath, logPath);
803
+ const alertStore = new AlertStore(alertQueuePath);
804
+ const session = newSessionId();
805
+ let log = null;
806
+ if (logPath) {
807
+ mkdirSync(dirname(logPath), { recursive: true });
808
+ log = new RotatingLog({ path: logPath, maxBytes: options.logMaxBytes });
809
+ log.append(JSON.stringify({
810
+ session,
811
+ event: "start",
812
+ at: new Date().toISOString(),
813
+ chain: { id: config.chainId, name: config.name },
814
+ endpoint: publicEndpoint(config.rpcUrl),
815
+ options: {
816
+ intervalMs: options.intervalMs,
817
+ stallAfterSeconds: options.stallAfterSeconds,
818
+ maxSamples: options.maxSamples,
819
+ gasSurgePercent: options.gasSurgePercent,
820
+ windowSize: options.windowSize,
821
+ reportEvery: options.reportEvery,
822
+ portfolio: options.owner === null
823
+ ? null
824
+ : {
825
+ owner: options.owner,
826
+ tokens: options.tokens.length,
827
+ every: options.portfolioEvery,
828
+ },
829
+ },
830
+ }));
831
+ }
832
+ const logLine = (record) => {
833
+ log?.append(JSON.stringify({ session, ...record }));
834
+ };
835
+ // Alerts are queued and retried in order, so a webhook outage delays
836
+ // delivery instead of dropping the alert.
837
+ const alertQueue = new AlertQueue({
838
+ deliver: (alert) => postWebhook(alert.url, alert.payload, { authHeader: options.alertAuthHeader }),
839
+ store: alertStore,
840
+ onPersistError: (error) => {
841
+ console.error(`alert queue not written (${alertQueuePath}): ${describeError(error)}`);
842
+ },
843
+ onRetry: (alert, attempt, delayMs, message) => {
844
+ console.error(`webhook retry ${attempt} in ${delayMs} ms (${alert.url}): ${message}`);
845
+ logLine({
846
+ event: "webhook-retry",
847
+ at: new Date().toISOString(),
848
+ url: alert.url,
849
+ attempt,
850
+ delayMs,
851
+ message,
852
+ });
853
+ },
854
+ });
855
+ let replayedAlerts = 0;
856
+ try {
857
+ replayedAlerts = alertQueue.restore();
858
+ }
859
+ catch (error) {
860
+ console.error(`alert queue not replayed (${alertQueuePath}): ${describeError(error)}`);
861
+ }
862
+ if (replayedAlerts > 0) {
863
+ console.error(`webhook queue: replaying ${replayedAlerts} undelivered alert(s) from ${alertQueuePath}`);
864
+ logLine({
865
+ event: "webhook-replay",
866
+ at: new Date().toISOString(),
867
+ count: replayedAlerts,
868
+ queue: alertQueuePath,
869
+ });
870
+ }
871
+ const deliverAlert = async (url, payload) => {
872
+ alertQueue.enqueue({ url, payload });
873
+ };
874
+ const controller = new AbortController();
875
+ const onStop = () => controller.abort();
876
+ process.on("SIGINT", onStop);
877
+ process.on("SIGTERM", onStop);
878
+ // Signals cannot be delivered to a detached process on Windows, so a stop
879
+ // file gives the service a portable way to shut down cleanly with a summary.
880
+ const stopPath = logPath ? `${logPath}.stop` : null;
881
+ let stopWatcher = null;
882
+ if (stopPath) {
883
+ if (existsSync(stopPath))
884
+ rmSync(stopPath, { force: true });
885
+ stopWatcher = setInterval(() => {
886
+ if (existsSync(stopPath)) {
887
+ rmSync(stopPath, { force: true });
888
+ controller.abort();
889
+ }
890
+ }, 500);
891
+ stopWatcher.unref();
892
+ }
893
+ if (!asJson) {
894
+ console.log(`lwc — watching ${config.name} (${config.chainId}) every ${options.intervalMs / 1000}s, stall threshold ${options.stallAfterSeconds}s, gas surge +${options.gasSurgePercent}%`);
895
+ if (logPath)
896
+ console.log(` logging to ${logPath}`);
897
+ if (options.webhookUrl) {
898
+ console.log(` alerts → ${options.webhookUrl}`);
899
+ console.log(` alert queue → ${alertQueuePath}${replayedAlerts > 0 ? ` (${replayedAlerts} pending)` : ""}`);
900
+ }
901
+ if (options.owner !== null && options.tokens.length > 0) {
902
+ console.log(` portfolio → ${options.owner}, ${options.tokens.length} tokens every ${options.portfolioEvery} poll(s)`);
903
+ }
904
+ if (options.alertBalancePercent !== null) {
905
+ console.log(` balance alerts → moves above ${options.alertBalancePercent}% between samples`);
906
+ }
907
+ console.log("");
908
+ }
909
+ const summary = await watchChain(client, config, options, async (event) => {
910
+ const json = toWatchEventJson(event);
911
+ logLine(json);
912
+ console.log(asJson ? JSON.stringify(json) : formatWatchEvent(event, config));
913
+ if ((event.type === "alert" || event.type === "balance-alert") &&
914
+ options.webhookUrl) {
915
+ await deliverAlert(options.webhookUrl, {
916
+ ...json,
917
+ session,
918
+ chain: { id: config.chainId, name: config.name },
919
+ });
920
+ }
921
+ }, controller.signal, options.verbose
922
+ ? { onChunkProgress: (chunk) => console.error(formatChunkProgress(chunk)) }
923
+ : undefined);
924
+ process.off("SIGINT", onStop);
925
+ process.off("SIGTERM", onStop);
926
+ if (stopWatcher)
927
+ clearInterval(stopWatcher);
928
+ const pendingAlerts = alertQueue.pending;
929
+ alertQueue.stop();
930
+ if (pendingAlerts > 0) {
931
+ console.error(`webhook queue stopped with ${pendingAlerts} undelivered alert(s); kept in ${alertQueuePath} for the next run.`);
932
+ logLine({
933
+ event: "webhook-pending",
934
+ at: new Date().toISOString(),
935
+ count: pendingAlerts,
936
+ queue: alertQueuePath,
937
+ });
938
+ }
939
+ const summaryJson = toWatchSummaryJson(summary);
940
+ logLine(summaryJson);
941
+ if (asJson) {
942
+ console.log(JSON.stringify(summaryJson));
943
+ }
944
+ else {
945
+ console.log("");
946
+ console.log(formatWatchSummary(summary));
947
+ }
948
+ if (summary.chainDrift || summary.stalls > 0 || summary.gasSurges > 0) {
949
+ process.exitCode = 1;
950
+ }
951
+ }
952
+ else {
953
+ // A preset's backup answers when the configured endpoint cannot, and the
954
+ // report names whichever endpoint actually answered.
955
+ const outcome = await probeWithFallback(buildClient, config);
956
+ const answered = outcome.config;
957
+ const warnings = connectionWarnings(answered);
958
+ if (outcome.usedFallback) {
959
+ warnings.push(`primary ${outcome.primaryEndpoint} failed (${outcome.primaryError}); answered by ${new URL(answered.rpcUrl).host}`);
960
+ }
961
+ console.log(asJson
962
+ ? JSON.stringify(toJsonPayload(outcome.health, answered, warnings, outcome), null, 2)
963
+ : formatHealth(outcome.health, answered, warnings));
964
+ }
965
+ }
966
+ catch (error) {
967
+ if (error instanceof CacheArgError) {
968
+ console.error(`usage error: ${error.message}`);
969
+ console.error(" example: lwc cache --clear 0xTokenAddress");
970
+ }
971
+ else if (error instanceof BalancesArgError) {
972
+ console.error(`usage error: ${error.message}`);
973
+ console.error(" example: lwc balances 0xYourAddress --token 0xTokenAddress");
974
+ }
975
+ else if (error instanceof ViewsArgError) {
976
+ console.error(`usage error: ${error.message}`);
977
+ console.error(" example: lwc views 0xToken (built-in ERC-20 ABI; --abi <file> overrides)");
978
+ }
979
+ else if (error instanceof TokenFileArgError) {
980
+ console.error(`usage error: ${error.message}`);
981
+ console.error(" example: lwc tokens 0xTokenA 0xTokenB --out ./examples/my.tokens.json");
982
+ }
983
+ else if (error instanceof CallArgError) {
984
+ console.error(`usage error: ${error.message}`);
985
+ console.error(" example: lwc call 0xToken balanceOf --args '[\"0xYourAddress\"]' (built-in ERC-20 ABI; --abi <file> overrides)");
986
+ }
987
+ else if (error instanceof WatchArgError) {
988
+ console.error(`usage error: ${error.message}`);
989
+ console.error(" example: lwc watch --interval 3 --stall-after 30");
990
+ }
991
+ else if (error instanceof CompareArgError) {
992
+ console.error(`usage error: ${error.message}`);
993
+ console.error(" example: lwc compare https://xlayer.drpc.org https://xlayerrpc.okx.com --samples 5");
994
+ }
995
+ else if (error instanceof ScanArgError) {
996
+ console.error(`usage error: ${error.message}`);
997
+ console.error(" example: lwc scan 0xAddrA 0xAddrB --tokens-file ./examples/xlayer.tokens.json --csv ./balances.csv");
998
+ }
999
+ else if (error instanceof ChainConfigError) {
1000
+ console.error(`config error: ${error.message}`);
1001
+ }
1002
+ else if (error instanceof ChainMismatchError) {
1003
+ console.error(`chain mismatch: ${error.message}`);
1004
+ }
1005
+ else {
1006
+ console.error(`connection failed: ${describeError(error)}`);
1007
+ console.error(` checked endpoint: ${process.env.LWC_RPC_URL ?? "(LWC_RPC_URL not set)"}`);
1008
+ console.error(" verify the RPC is reachable and accepts JSON-RPC over POST.");
1009
+ }
1010
+ process.exit(1);
1011
+ }