lovewebconnection 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1937 -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
@@ -0,0 +1,515 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { percentile } from "./metrics.js";
4
+ import { csvCell } from "./scan.js";
5
+ export class SessionsArgError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "SessionsArgError";
9
+ }
10
+ }
11
+ const SINCE_UNITS_MS = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
12
+ /** `--since` takes an ISO timestamp or a span relative to now, e.g. `24h`. */
13
+ function parseSince(raw) {
14
+ const relative = /^(\d+)([smhd])$/.exec(raw);
15
+ if (relative) {
16
+ const amount = Number(relative[1]);
17
+ const unit = SINCE_UNITS_MS[relative[2]];
18
+ return Date.now() - amount * unit;
19
+ }
20
+ const parsed = Date.parse(raw);
21
+ if (!Number.isFinite(parsed)) {
22
+ throw new SessionsArgError(`--since must be an ISO timestamp or a span like 24h, got "${raw}".`);
23
+ }
24
+ return parsed;
25
+ }
26
+ export function parseSessionsArgs(argv) {
27
+ const positional = [];
28
+ let since = null;
29
+ const sessionPrefixes = [];
30
+ let csvPath = null;
31
+ for (let index = 0; index < argv.length; index += 1) {
32
+ const arg = argv[index] ?? "";
33
+ const readValue = (flag) => {
34
+ const value = argv[index + 1];
35
+ if (!value)
36
+ throw new SessionsArgError(`${flag} requires a value.`);
37
+ index += 1;
38
+ return value;
39
+ };
40
+ if (arg === "--since" || arg.startsWith("--since=")) {
41
+ const value = arg.startsWith("--since=") ? arg.slice("--since=".length) : readValue("--since");
42
+ since = parseSince(value);
43
+ }
44
+ else if (arg === "--session" || arg.startsWith("--session=")) {
45
+ const value = arg.startsWith("--session=") ? arg.slice("--session=".length) : readValue("--session");
46
+ if (!value)
47
+ throw new SessionsArgError("--session requires a prefix.");
48
+ sessionPrefixes.push(value);
49
+ }
50
+ else if (arg === "--csv") {
51
+ const value = argv[index + 1];
52
+ if (!value || (value.startsWith("-") && value !== "-")) {
53
+ throw new SessionsArgError("--csv requires a path, or - for stdout.");
54
+ }
55
+ csvPath = value;
56
+ index += 1;
57
+ }
58
+ else if (arg.startsWith("--csv=")) {
59
+ const value = arg.slice("--csv=".length);
60
+ if (!value)
61
+ throw new SessionsArgError("--csv requires a path, or - for stdout.");
62
+ csvPath = value;
63
+ }
64
+ else if (arg.startsWith("-")) {
65
+ // --json and any other global flags are handled by the CLI.
66
+ continue;
67
+ }
68
+ else {
69
+ positional.push(arg);
70
+ }
71
+ }
72
+ const [path] = positional;
73
+ if (!path) {
74
+ throw new SessionsArgError("A watch log path is required: lwc sessions <log.ndjson>");
75
+ }
76
+ if (positional.length > 1) {
77
+ throw new SessionsArgError(`Unexpected extra argument: ${positional[1]}.`);
78
+ }
79
+ return { path, since, sessionPrefixes, csvPath };
80
+ }
81
+ export function readWatchLog(path) {
82
+ if (!existsSync(path)) {
83
+ throw new SessionsArgError(`watch log not found: ${path}`);
84
+ }
85
+ return readFileSync(path, "utf8");
86
+ }
87
+ function objectOf(value) {
88
+ return value !== null && typeof value === "object" && !Array.isArray(value)
89
+ ? value
90
+ : null;
91
+ }
92
+ function numberField(value) {
93
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
94
+ }
95
+ function bigintField(value) {
96
+ if (typeof value === "string" && /^\d+$/.test(value))
97
+ return BigInt(value);
98
+ if (typeof value === "number" && Number.isSafeInteger(value))
99
+ return BigInt(value);
100
+ return null;
101
+ }
102
+ function timeField(value) {
103
+ if (typeof value !== "string")
104
+ return null;
105
+ const ms = Date.parse(value);
106
+ return Number.isFinite(ms) ? ms : null;
107
+ }
108
+ function latencyStats(values) {
109
+ if (values.length === 0)
110
+ return { count: 0, p50: 0, p95: 0, max: 0 };
111
+ const sorted = [...values].sort((a, b) => a - b);
112
+ return {
113
+ count: sorted.length,
114
+ p50: percentile(sorted, 50),
115
+ p95: percentile(sorted, 95),
116
+ max: sorted[sorted.length - 1] ?? 0,
117
+ };
118
+ }
119
+ function blockTimeStats(values) {
120
+ if (values.length === 0)
121
+ return { count: 0, medianSeconds: 0, minSeconds: 0, maxSeconds: 0 };
122
+ const sorted = [...values].sort((a, b) => a - b);
123
+ return {
124
+ count: sorted.length,
125
+ medianSeconds: percentile(sorted, 50),
126
+ minSeconds: sorted[0] ?? 0,
127
+ maxSeconds: sorted[sorted.length - 1] ?? 0,
128
+ };
129
+ }
130
+ function summaryOf(lines) {
131
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
132
+ if (lines[index]?.event === "summary")
133
+ return lines[index] ?? null;
134
+ }
135
+ return null;
136
+ }
137
+ function buildSession(id, lines) {
138
+ const start = lines.find((line) => line.event === "start") ?? null;
139
+ const summary = summaryOf(lines);
140
+ const advances = lines.filter((line) => line.event === "advance");
141
+ const holds = lines.filter((line) => line.event === "hold");
142
+ const latencyValues = [...advances, ...holds]
143
+ .map((line) => numberField(line.latencyMs))
144
+ .filter((value) => value !== null);
145
+ // Block time is the gap between consecutive advances that moved the head,
146
+ // which is what the watch run itself samples.
147
+ const blockTimes = [];
148
+ for (let index = 1; index < advances.length; index += 1) {
149
+ const delta = bigintField(advances[index]?.delta);
150
+ const at = timeField(advances[index]?.at);
151
+ const previousAt = timeField(advances[index - 1]?.at);
152
+ if (delta !== null && delta > 0n && at !== null && previousAt !== null) {
153
+ blockTimes.push(Math.max(0, (at - previousAt) / 1000));
154
+ }
155
+ }
156
+ const startTime = timeField(start?.at);
157
+ const eventTimes = lines
158
+ .map((line) => timeField(line.at))
159
+ .filter((value) => value !== null);
160
+ const firstEventTime = eventTimes.length > 0 ? Math.min(...eventTimes) : null;
161
+ const lastEventTime = eventTimes.length > 0 ? Math.max(...eventTimes) : null;
162
+ const startedAt = startTime ?? firstEventTime;
163
+ const summaryDuration = summary === null ? null : numberField(summary.durationMs);
164
+ const durationMs = summaryDuration ??
165
+ (startedAt !== null && lastEventTime !== null ? Math.max(0, lastEventTime - startedAt) : 0);
166
+ const blockNumbers = [...advances, ...holds]
167
+ .map((line) => bigintField(line.blockNumber))
168
+ .filter((value) => value !== null);
169
+ const firstBlock = summary === null ? null : bigintField(summary.firstBlock);
170
+ const lastBlock = summary === null ? null : bigintField(summary.lastBlock);
171
+ const chainRaw = start === null ? null : objectOf(start.chain);
172
+ const chainId = chainRaw === null ? null : numberField(chainRaw.id);
173
+ const chainName = chainRaw !== null && typeof chainRaw.name === "string" ? chainRaw.name : null;
174
+ const firstBlockNumber = blockNumbers[0] ?? null;
175
+ const lastBlockNumber = blockNumbers[blockNumbers.length - 1] ?? null;
176
+ const rawFirst = firstBlock ?? firstBlockNumber;
177
+ const rawLast = lastBlock ?? lastBlockNumber;
178
+ const advancedBy = summary === null
179
+ ? rawFirst !== null && rawLast !== null
180
+ ? rawLast - rawFirst
181
+ : 0n
182
+ : (bigintField(summary.advancedBy) ?? 0n);
183
+ return {
184
+ id,
185
+ chain: chainId === null ? null : { id: chainId, name: chainName ?? "?" },
186
+ endpoint: start !== null && typeof start.endpoint === "string" ? start.endpoint : null,
187
+ state: summary === null ? "partial" : summary.aborted === true ? "aborted" : "complete",
188
+ startedAt,
189
+ endedAt: startedAt === null ? lastEventTime : startedAt + durationMs,
190
+ durationMs,
191
+ samples: (summary === null ? null : numberField(summary.samples)) ?? advances.length + holds.length,
192
+ latency: latencyStats(latencyValues),
193
+ blockTime: blockTimeStats(blockTimes),
194
+ stalls: (summary === null ? null : numberField(summary.stalls)) ??
195
+ lines.filter((line) => line.event === "stall").length,
196
+ gasSurges: (summary === null ? null : numberField(summary.gasSurges)) ??
197
+ lines.filter((line) => line.event === "gas-surge").length,
198
+ alerts: (summary === null ? null : numberField(summary.alerts)) ??
199
+ lines.filter((line) => line.event === "alert" || line.event === "balance-alert").length,
200
+ errors: (summary === null ? null : numberField(summary.errors)) ??
201
+ lines.filter((line) => line.event === "error").length,
202
+ chainDrift: summary === null
203
+ ? lines.some((line) => line.event === "chain-drift")
204
+ : summary.chainDrift === true,
205
+ firstBlock: rawFirst,
206
+ lastBlock: rawLast,
207
+ advancedBy,
208
+ };
209
+ }
210
+ function buildEndpoint(endpoint, sessions, latencyValues) {
211
+ const total = (pick) => sessions.reduce((sum, session) => sum + pick(session), 0);
212
+ const samples = total((session) => session.samples);
213
+ const stalls = total((session) => session.stalls);
214
+ return {
215
+ endpoint,
216
+ sessions: sessions.length,
217
+ samples,
218
+ latency: latencyStats(latencyValues),
219
+ stalls,
220
+ gasSurges: total((session) => session.gasSurges),
221
+ errors: total((session) => session.errors),
222
+ stallRate: samples > 0 ? stalls / samples : 0,
223
+ };
224
+ }
225
+ /**
226
+ * Group a watch NDJSON log by session id and summarise each run. Latency
227
+ * percentiles are computed from every poll in the session rather than the
228
+ * watch run's rolling window, so two sessions with different `--window`
229
+ * values are still comparable. A session without a `summary` line (killed, or
230
+ * still running) is kept and marked partial. `filter` narrows a long log before
231
+ * the comparison: prefixes drop non-matching lines, and `since` drops completed
232
+ * sessions that started too early.
233
+ */
234
+ export function summarizeWatchLog(text, filter = {}) {
235
+ const groups = new Map();
236
+ const prefixes = filter.sessionPrefixes ?? [];
237
+ let skippedLines = 0;
238
+ for (const rawLine of text.split(/\r?\n/)) {
239
+ const line = rawLine.trim();
240
+ if (line.length === 0)
241
+ continue;
242
+ let parsed;
243
+ try {
244
+ parsed = JSON.parse(line);
245
+ }
246
+ catch {
247
+ skippedLines += 1;
248
+ continue;
249
+ }
250
+ const record = objectOf(parsed);
251
+ const session = record !== null && typeof record.session === "string" ? record.session : null;
252
+ if (record === null || session === null) {
253
+ skippedLines += 1;
254
+ continue;
255
+ }
256
+ // A session narrowed away by a prefix is not malformed, so it is not counted.
257
+ if (prefixes.length > 0 && !prefixes.some((prefix) => session.startsWith(prefix))) {
258
+ continue;
259
+ }
260
+ const existing = groups.get(session);
261
+ if (existing)
262
+ existing.push(record);
263
+ else
264
+ groups.set(session, [record]);
265
+ }
266
+ const sessions = [...groups.entries()]
267
+ .map(([id, lines]) => buildSession(id, lines))
268
+ .filter((session) => filter.since == null ||
269
+ (session.startedAt !== null && session.startedAt >= filter.since));
270
+ sessions.sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0));
271
+ // Pool by RPC host: percentiles over all polls are fairer than averaging the
272
+ // per-session percentiles, and the host keeps API keys out of the summary.
273
+ const byEndpoint = new Map();
274
+ for (const session of sessions) {
275
+ const lines = groups.get(session.id) ?? [];
276
+ const start = lines.find((line) => line.event === "start") ?? null;
277
+ const rawEndpoint = start !== null && typeof start.endpoint === "string" ? start.endpoint : null;
278
+ const endpoint = rawEndpoint === null ? null : endpointHost(rawEndpoint);
279
+ const key = endpoint ?? "";
280
+ let group = byEndpoint.get(key);
281
+ if (!group) {
282
+ group = { endpoint, sessions: [], latencies: [] };
283
+ byEndpoint.set(key, group);
284
+ }
285
+ group.sessions.push(session);
286
+ for (const line of lines) {
287
+ if (line.event !== "advance" && line.event !== "hold")
288
+ continue;
289
+ const latency = numberField(line.latencyMs);
290
+ if (latency !== null)
291
+ group.latencies.push(latency);
292
+ }
293
+ }
294
+ const endpoints = [...byEndpoint.values()]
295
+ .map((group) => buildEndpoint(group.endpoint, group.sessions, group.latencies))
296
+ .sort((a, b) => b.samples - a.samples ||
297
+ b.sessions - a.sessions ||
298
+ (a.endpoint ?? "").localeCompare(b.endpoint ?? ""));
299
+ return { sessions, endpoints, skippedLines };
300
+ }
301
+ function formatDuration(ms) {
302
+ const seconds = Math.round(ms / 1000);
303
+ if (seconds < 60)
304
+ return `${seconds}s`;
305
+ const minutes = Math.floor(seconds / 60);
306
+ const rest = seconds % 60;
307
+ return `${minutes}m${rest.toString().padStart(2, "0")}s`;
308
+ }
309
+ function endpointHost(endpoint) {
310
+ if (endpoint === null)
311
+ return "?";
312
+ try {
313
+ return new URL(endpoint).host;
314
+ }
315
+ catch {
316
+ return endpoint;
317
+ }
318
+ }
319
+ function formatStart(at) {
320
+ if (at === null)
321
+ return "—";
322
+ // UTC keeps the table stable regardless of the machine's timezone.
323
+ return new Date(at).toISOString().slice(5, 19).replace("T", " ");
324
+ }
325
+ function iso(at) {
326
+ return new Date(at).toISOString();
327
+ }
328
+ const NA = "n/a";
329
+ export function formatSessions(report) {
330
+ const idWidth = Math.max(7, ...report.sessions.map((session) => session.id.length));
331
+ const chainWidth = Math.max(5, ...report.sessions.map((session) => session.chain === null ? 1 : `${session.chain.name} (${session.chain.id})`.length));
332
+ const rpcWidth = Math.max(3, ...report.sessions.map((session) => endpointHost(session.endpoint).length));
333
+ const partial = report.sessions.filter((session) => session.state === "partial").length;
334
+ const header = [
335
+ "session".padEnd(idWidth),
336
+ "chain".padEnd(chainWidth),
337
+ "rpc".padEnd(rpcWidth),
338
+ "started (UTC)".padEnd(15),
339
+ "duration".padStart(8),
340
+ "polls".padStart(5),
341
+ "p50".padStart(5),
342
+ "p95".padStart(5),
343
+ "stalls".padStart(6),
344
+ "surges".padStart(6),
345
+ "errors".padStart(6),
346
+ "state".padEnd(8),
347
+ ].join(" ");
348
+ const rows = report.sessions.map((session) => {
349
+ const chain = session.chain === null ? "?" : `${session.chain.name} (${session.chain.id})`;
350
+ const latency = session.latency;
351
+ return [
352
+ session.id.padEnd(idWidth),
353
+ chain.padEnd(chainWidth),
354
+ endpointHost(session.endpoint).padEnd(rpcWidth),
355
+ formatStart(session.startedAt).padEnd(15),
356
+ formatDuration(session.durationMs).padStart(8),
357
+ String(session.samples).padStart(5),
358
+ (latency.count > 0 ? String(latency.p50) : NA).padStart(5),
359
+ (latency.count > 0 ? String(latency.p95) : NA).padStart(5),
360
+ String(session.stalls).padStart(6),
361
+ String(session.gasSurges).padStart(6),
362
+ String(session.errors).padStart(6),
363
+ session.state.padEnd(8),
364
+ ].join(" ");
365
+ });
366
+ const lines = [
367
+ "lwc — watch sessions",
368
+ "",
369
+ ` ${"log".padEnd(14)}${report.path}`,
370
+ ` ${"sessions".padEnd(14)}${report.sessions.length}${partial > 0 ? ` (${partial} partial)` : ""}`,
371
+ ];
372
+ if (report.skippedLines > 0) {
373
+ lines.push(` ${"skipped".padEnd(14)}${report.skippedLines} malformed line(s)`);
374
+ }
375
+ lines.push("", header, ...rows);
376
+ if (report.endpoints.length > 0) {
377
+ const hostWidth = Math.max(8, ...report.endpoints.map((item) => (item.endpoint ?? "?").length));
378
+ const endpointHeader = [
379
+ "endpoint".padEnd(hostWidth),
380
+ "sessions".padStart(8),
381
+ "polls".padStart(6),
382
+ "p50".padStart(5),
383
+ "p95".padStart(5),
384
+ "stalls".padStart(6),
385
+ "stall rate".padStart(10),
386
+ ].join(" ");
387
+ lines.push("", "by endpoint", "", endpointHeader);
388
+ for (const item of report.endpoints) {
389
+ const latency = item.latency;
390
+ lines.push([
391
+ (item.endpoint ?? "?").padEnd(hostWidth),
392
+ String(item.sessions).padStart(8),
393
+ String(item.samples).padStart(6),
394
+ (latency.count > 0 ? String(latency.p50) : NA).padStart(5),
395
+ (latency.count > 0 ? String(latency.p95) : NA).padStart(5),
396
+ String(item.stalls).padStart(6),
397
+ (item.samples > 0 ? `${(item.stallRate * 100).toFixed(1)}%` : NA).padStart(10),
398
+ ].join(" "));
399
+ }
400
+ }
401
+ return lines.join("\n");
402
+ }
403
+ export const SESSIONS_CSV_HEADER = [
404
+ "session",
405
+ "started_at",
406
+ "ended_at",
407
+ "state",
408
+ "chain",
409
+ "chain_id",
410
+ "endpoint",
411
+ "duration_ms",
412
+ "samples",
413
+ "latency_p50_ms",
414
+ "latency_p95_ms",
415
+ "latency_max_ms",
416
+ "block_time_median_s",
417
+ "block_time_min_s",
418
+ "block_time_max_s",
419
+ "stalls",
420
+ "gas_surges",
421
+ "alerts",
422
+ "errors",
423
+ "chain_drift",
424
+ "first_block",
425
+ "last_block",
426
+ "advanced_by",
427
+ ];
428
+ /**
429
+ * One row per session: identity, chain, RPC endpoint, and the same latency,
430
+ * block-time, and counter fields as the table. Missing measurements are empty
431
+ * cells rather than zeroes, so a spreadsheet can tell `no polls` from `0 ms`.
432
+ */
433
+ export function sessionsCsv(report) {
434
+ const lines = [SESSIONS_CSV_HEADER.join(",")];
435
+ for (const session of report.sessions) {
436
+ const latency = session.latency.count > 0 ? session.latency : null;
437
+ const blockTime = session.blockTime.count > 0 ? session.blockTime : null;
438
+ lines.push([
439
+ session.id,
440
+ session.startedAt === null ? "" : iso(session.startedAt),
441
+ session.endedAt === null ? "" : iso(session.endedAt),
442
+ session.state,
443
+ session.chain?.name ?? "",
444
+ session.chain === null ? "" : String(session.chain.id),
445
+ session.endpoint ?? "",
446
+ String(session.durationMs),
447
+ String(session.samples),
448
+ latency === null ? "" : String(latency.p50),
449
+ latency === null ? "" : String(latency.p95),
450
+ latency === null ? "" : String(latency.max),
451
+ blockTime === null ? "" : String(blockTime.medianSeconds),
452
+ blockTime === null ? "" : String(blockTime.minSeconds),
453
+ blockTime === null ? "" : String(blockTime.maxSeconds),
454
+ String(session.stalls),
455
+ String(session.gasSurges),
456
+ String(session.alerts),
457
+ String(session.errors),
458
+ session.chainDrift ? "true" : "false",
459
+ session.firstBlock === null ? "" : session.firstBlock.toString(),
460
+ session.lastBlock === null ? "" : session.lastBlock.toString(),
461
+ session.advancedBy.toString(),
462
+ ]
463
+ .map(csvCell)
464
+ .join(","));
465
+ }
466
+ return `${lines.join("\n")}\n`;
467
+ }
468
+ /** Write the CSV, creating parent directories; an existing file is replaced. */
469
+ export function saveSessionsCsv(path, csv) {
470
+ try {
471
+ mkdirSync(dirname(path), { recursive: true });
472
+ writeFileSync(path, csv);
473
+ }
474
+ catch (error) {
475
+ const reason = error instanceof Error ? error.message : String(error);
476
+ throw new SessionsArgError(`cannot write ${path}: ${reason}`);
477
+ }
478
+ }
479
+ export function toSessionsJson(report) {
480
+ return {
481
+ ok: report.sessions.every((session) => session.state !== "partial"),
482
+ log: report.path,
483
+ skippedLines: report.skippedLines,
484
+ sessions: report.sessions.map((session) => ({
485
+ id: session.id,
486
+ chain: session.chain,
487
+ endpoint: session.endpoint,
488
+ state: session.state,
489
+ startedAt: session.startedAt === null ? null : new Date(session.startedAt).toISOString(),
490
+ endedAt: session.endedAt === null ? null : new Date(session.endedAt).toISOString(),
491
+ durationMs: session.durationMs,
492
+ samples: session.samples,
493
+ latency: session.latency,
494
+ blockTime: session.blockTime,
495
+ stalls: session.stalls,
496
+ gasSurges: session.gasSurges,
497
+ alerts: session.alerts,
498
+ errors: session.errors,
499
+ chainDrift: session.chainDrift,
500
+ firstBlock: session.firstBlock === null ? null : session.firstBlock.toString(),
501
+ lastBlock: session.lastBlock === null ? null : session.lastBlock.toString(),
502
+ advancedBy: session.advancedBy.toString(),
503
+ })),
504
+ endpoints: report.endpoints.map((item) => ({
505
+ endpoint: item.endpoint,
506
+ sessions: item.sessions,
507
+ samples: item.samples,
508
+ latency: item.latency,
509
+ stalls: item.stalls,
510
+ gasSurges: item.gasSurges,
511
+ errors: item.errors,
512
+ stallRate: item.stallRate,
513
+ })),
514
+ };
515
+ }
@@ -0,0 +1,13 @@
1
+ import type { Abi } from "viem";
2
+ export declare class SignaturesArgError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export interface SignaturesArgs {
6
+ /** ABI file to read, or null for the built-in ERC-20 ABI. */
7
+ abiPath: string | null;
8
+ }
9
+ export declare function parseSignaturesArgs(argv: string[]): SignaturesArgs;
10
+ /** Every callable signature, sorted so overloads are easy to scan and copy. */
11
+ export declare function callableSignatures(abi: Abi): string[];
12
+ export declare function formatSignatures(signatures: string[], source: string): string;
13
+ export declare function toSignaturesJson(signatures: string[], source: string): Record<string, unknown>;
@@ -0,0 +1,43 @@
1
+ import { signatureOf } from "./call.js";
2
+ import { listViewFunctions } from "./views.js";
3
+ export class SignaturesArgError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "SignaturesArgError";
7
+ }
8
+ }
9
+ export function parseSignaturesArgs(argv) {
10
+ const positional = [];
11
+ for (const arg of argv) {
12
+ if (arg.startsWith("-"))
13
+ continue; // --json is handled by the caller.
14
+ positional.push(arg);
15
+ }
16
+ if (positional.length > 1) {
17
+ throw new SignaturesArgError(`Unexpected extra argument: ${positional[1]}.`);
18
+ }
19
+ return { abiPath: positional[0] ?? null };
20
+ }
21
+ /** Every callable signature, sorted so overloads are easy to scan and copy. */
22
+ export function callableSignatures(abi) {
23
+ return listViewFunctions(abi).map(signatureOf).sort();
24
+ }
25
+ export function formatSignatures(signatures, source) {
26
+ const lines = [
27
+ "lwc — callable signatures",
28
+ "",
29
+ ` ${"abi".padEnd(14)}${source}`,
30
+ ` ${"signatures".padEnd(14)}${signatures.length} view/pure`,
31
+ "",
32
+ ];
33
+ if (signatures.length === 0) {
34
+ lines.push(" (no view or pure functions)");
35
+ }
36
+ else {
37
+ lines.push(...signatures.map((signature) => ` ${signature}`));
38
+ }
39
+ return lines.join("\n");
40
+ }
41
+ export function toSignaturesJson(signatures, source) {
42
+ return { ok: true, abi: source, count: signatures.length, signatures };
43
+ }
@@ -0,0 +1,50 @@
1
+ import { type Address } from "viem";
2
+ export declare class TokenFileArgError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** One entry as written to the file: a label when the input carried one. */
6
+ export interface TokenFileEntry {
7
+ address: Address;
8
+ label: string | null;
9
+ }
10
+ export interface TokenFileArgs {
11
+ /** Unique addresses, checksummed, in the order given (or the merged order). */
12
+ addresses: Address[];
13
+ /** The same addresses with their labels, ready to write. */
14
+ entries: TokenFileEntry[];
15
+ /** Where to write the list, or "-" for stdout. */
16
+ outPath: string;
17
+ force: boolean;
18
+ /** Keep the entries already in the file and append only new addresses. */
19
+ merge: boolean;
20
+ /** Fixed sub-call budget for the verification reads, or null for the default. */
21
+ maxSubCalls: number | null;
22
+ /** Non-fatal label conflicts found while merging duplicate addresses. */
23
+ warnings: string[];
24
+ }
25
+ /**
26
+ * Keep the entries already in a list — same order, same labels — then append the
27
+ * addresses that are not there yet. A new label for an existing address replaces
28
+ * the old one, and a differing pair is reported in `warnings`.
29
+ */
30
+ export declare function mergeTokenEntries(existing: readonly (Address | TokenFileEntry)[], added: readonly (Address | TokenFileEntry)[], warnings?: string[]): TokenFileEntry[];
31
+ /**
32
+ * Build a `--tokens-file` list from command-line addresses. Pure local work: no
33
+ * RPC call is needed, and labels are carried through to the written file. With
34
+ * `--merge` the existing file is read so only new addresses are appended.
35
+ */
36
+ export declare function parseTokenFileArgs(argv: string[]): TokenFileArgs;
37
+ /**
38
+ * The written shape: a checksummed address string when there is no label, and an
39
+ * `{ address, label }` object when there is — both forms `--tokens-file` reads.
40
+ */
41
+ export declare function tokenListItems(entries: readonly (Address | TokenFileEntry)[]): Array<Address | {
42
+ address: Address;
43
+ label: string;
44
+ }>;
45
+ /** JSON array in the format `--tokens-file` reads, labels included. */
46
+ export declare function buildTokenListJson(entries: readonly (Address | TokenFileEntry)[]): string;
47
+ /** Write the list (or return it for "-"), never replacing a file without force. */
48
+ export declare function saveTokenList(outPath: string, entries: readonly (Address | TokenFileEntry)[], options?: {
49
+ force?: boolean;
50
+ }): string;