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,82 @@
1
+ export declare class CompareHistoryArgError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface CompareHistoryArgs {
5
+ /** Compare NDJSON log to read. */
6
+ path: string;
7
+ /** Where to write the per-run CSV, "-" for stdout, or null for the table. */
8
+ csvPath: string | null;
9
+ }
10
+ export declare function parseCompareHistoryArgs(argv: string[]): CompareHistoryArgs;
11
+ export declare function readCompareLog(path: string): string;
12
+ export interface CompareRunProvider {
13
+ url: string;
14
+ label: string | null;
15
+ wins: number;
16
+ observed: number;
17
+ misses: number;
18
+ /** This run's median head delay in ms; null when it observed no new heads. */
19
+ delayMs: number | null;
20
+ errors: number;
21
+ ok: boolean;
22
+ }
23
+ export interface CompareRun {
24
+ session: string;
25
+ /** Epoch milliseconds of the record. */
26
+ at: number;
27
+ chainId: number | null;
28
+ chainName: string | null;
29
+ samples: number;
30
+ newBlocks: number;
31
+ providers: CompareRunProvider[];
32
+ }
33
+ export interface ProviderHistory {
34
+ url: string;
35
+ label: string | null;
36
+ /** Label when the log carried one, otherwise the sanitized URL. */
37
+ name: string;
38
+ /** Runs that included this provider. */
39
+ runs: number;
40
+ wins: number;
41
+ /** Wins as a share of the new blocks in those runs; null when none. */
42
+ winRate: number | null;
43
+ /** Median across runs of each run's own median head delay. */
44
+ delayMs: number | null;
45
+ /** Delay from the newest run that observed heads; null when none did. */
46
+ lastDelayMs: number | null;
47
+ /** Epoch milliseconds of the newest run that included this provider. */
48
+ lastSeen: number;
49
+ }
50
+ export interface CompareHistoryReport {
51
+ path: string;
52
+ runs: CompareRun[];
53
+ providers: ProviderHistory[];
54
+ firstAt: number | null;
55
+ lastAt: number | null;
56
+ /** Sum of distinct new heads across runs. */
57
+ newBlocks: number;
58
+ /** Chains seen in the log, in first-appearance order. */
59
+ chains: Array<{
60
+ id: number | null;
61
+ name: string | null;
62
+ }>;
63
+ /** Malformed lines, or compare records without a session or timestamp. */
64
+ skippedLines: number;
65
+ }
66
+ /**
67
+ * Read the `compare` records of an NDJSON log and pool them by endpoint. Other
68
+ * event types are ignored so a mixed log can be passed without noise.
69
+ */
70
+ export declare function summarizeCompareLog(text: string): Omit<CompareHistoryReport, "path">;
71
+ export declare function formatCompareHistory(report: CompareHistoryReport): string;
72
+ export declare const COMPARE_HISTORY_CSV_HEADER: readonly ["session", "at", "chain", "chain_id", "samples", "new_blocks", "provider", "provider_url", "wins", "observed", "misses", "delay_ms", "errors", "ok"];
73
+ /**
74
+ * One row per run and provider: the run's identity and chain are repeated for
75
+ * each of its endpoints, so a spreadsheet can pivot by provider, session, or
76
+ * time without parsing nested JSON. `delay_ms` is empty when that provider saw
77
+ * no new heads in the run.
78
+ */
79
+ export declare function compareHistoryCsv(report: Pick<CompareHistoryReport, "runs">): string;
80
+ /** Write the CSV, creating parent directories; an existing file is replaced. */
81
+ export declare function saveCompareHistoryCsv(path: string, csv: string): void;
82
+ export declare function toCompareHistoryJson(report: CompareHistoryReport): Record<string, unknown>;
@@ -0,0 +1,340 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { csvCell } from "./scan.js";
4
+ export class CompareHistoryArgError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "CompareHistoryArgError";
8
+ }
9
+ }
10
+ export function parseCompareHistoryArgs(argv) {
11
+ const positional = [];
12
+ let csvPath = null;
13
+ for (let index = 0; index < argv.length; index += 1) {
14
+ const arg = argv[index] ?? "";
15
+ if (arg === "--csv") {
16
+ const value = argv[index + 1];
17
+ if (!value || (value.startsWith("-") && value !== "-")) {
18
+ throw new CompareHistoryArgError("--csv requires a path, or - for stdout.");
19
+ }
20
+ csvPath = value;
21
+ index += 1;
22
+ }
23
+ else if (arg.startsWith("--csv=")) {
24
+ const value = arg.slice("--csv=".length);
25
+ if (!value)
26
+ throw new CompareHistoryArgError("--csv requires a path, or - for stdout.");
27
+ csvPath = value;
28
+ }
29
+ else if (arg.startsWith("-")) {
30
+ continue; // --json and other global flags.
31
+ }
32
+ else {
33
+ positional.push(arg);
34
+ }
35
+ }
36
+ const [path] = positional;
37
+ if (!path) {
38
+ throw new CompareHistoryArgError("A compare log path is required: lwc compare-history <compare.ndjson>");
39
+ }
40
+ if (positional.length > 1) {
41
+ throw new CompareHistoryArgError(`Unexpected extra argument: ${positional[1]}.`);
42
+ }
43
+ return { path, csvPath };
44
+ }
45
+ export function readCompareLog(path) {
46
+ if (!existsSync(path)) {
47
+ throw new CompareHistoryArgError(`compare log not found: ${path}`);
48
+ }
49
+ return readFileSync(path, "utf8");
50
+ }
51
+ function objectOf(value) {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value)
53
+ ? value
54
+ : null;
55
+ }
56
+ function numberField(value) {
57
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
58
+ }
59
+ function timeField(value) {
60
+ if (typeof value !== "string")
61
+ return null;
62
+ const ms = Date.parse(value);
63
+ return Number.isFinite(ms) ? ms : null;
64
+ }
65
+ function median(values) {
66
+ if (values.length === 0)
67
+ return null;
68
+ const sorted = [...values].sort((a, b) => a - b);
69
+ const middle = Math.floor(sorted.length / 2);
70
+ const value = sorted.length % 2 === 0
71
+ ? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
72
+ : (sorted[middle] ?? 0);
73
+ return Math.round(value);
74
+ }
75
+ /**
76
+ * Read the `compare` records of an NDJSON log and pool them by endpoint. Other
77
+ * event types are ignored so a mixed log can be passed without noise.
78
+ */
79
+ export function summarizeCompareLog(text) {
80
+ const runs = [];
81
+ const chains = [];
82
+ let skippedLines = 0;
83
+ for (const raw of text.split(/\r?\n/)) {
84
+ if (raw.trim().length === 0)
85
+ continue;
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(raw);
89
+ }
90
+ catch {
91
+ skippedLines += 1;
92
+ continue;
93
+ }
94
+ const record = objectOf(parsed);
95
+ if (record === null || typeof record.event !== "string") {
96
+ skippedLines += 1;
97
+ continue;
98
+ }
99
+ if (record.event !== "compare")
100
+ continue;
101
+ const session = typeof record.session === "string" ? record.session : null;
102
+ const at = timeField(record.at);
103
+ if (session === null || at === null) {
104
+ skippedLines += 1;
105
+ continue;
106
+ }
107
+ const chain = objectOf(record.chain);
108
+ const chainId = numberField(chain?.id);
109
+ const chainName = chain !== null && typeof chain.name === "string" ? chain.name : null;
110
+ if (!chains.some((entry) => entry.id === chainId && entry.name === chainName)) {
111
+ chains.push({ id: chainId, name: chainName });
112
+ }
113
+ const providers = [];
114
+ for (const value of Array.isArray(record.endpoints) ? record.endpoints : []) {
115
+ const endpoint = objectOf(value);
116
+ if (endpoint === null || typeof endpoint.url !== "string")
117
+ continue;
118
+ const delivery = objectOf(endpoint.delivery);
119
+ providers.push({
120
+ url: endpoint.url,
121
+ label: typeof endpoint.label === "string" ? endpoint.label : null,
122
+ wins: numberField(delivery?.wins) ?? 0,
123
+ observed: numberField(delivery?.observed) ?? 0,
124
+ misses: numberField(delivery?.misses) ?? 0,
125
+ delayMs: numberField(delivery?.p50Ms),
126
+ errors: numberField(endpoint.errors) ?? 0,
127
+ ok: endpoint.ok === true,
128
+ });
129
+ }
130
+ runs.push({
131
+ session,
132
+ at,
133
+ chainId,
134
+ chainName,
135
+ samples: numberField(record.samples) ?? 0,
136
+ newBlocks: numberField(record.newBlocks) ?? 0,
137
+ providers,
138
+ });
139
+ }
140
+ runs.sort((a, b) => a.at - b.at);
141
+ const grouped = new Map();
142
+ for (const run of runs) {
143
+ for (const provider of run.providers) {
144
+ let entry = grouped.get(provider.url);
145
+ if (!entry) {
146
+ entry = {
147
+ url: provider.url,
148
+ label: null,
149
+ runs: 0,
150
+ wins: 0,
151
+ blocks: 0,
152
+ delays: [],
153
+ lastDelayMs: null,
154
+ lastDelayAt: Number.NEGATIVE_INFINITY,
155
+ lastSeen: run.at,
156
+ };
157
+ grouped.set(provider.url, entry);
158
+ }
159
+ entry.runs += 1;
160
+ entry.wins += provider.wins;
161
+ entry.blocks += run.newBlocks;
162
+ if (provider.label !== null)
163
+ entry.label = provider.label;
164
+ if (provider.delayMs !== null) {
165
+ entry.delays.push(provider.delayMs);
166
+ if (run.at >= entry.lastDelayAt) {
167
+ entry.lastDelayAt = run.at;
168
+ entry.lastDelayMs = provider.delayMs;
169
+ }
170
+ }
171
+ if (run.at >= entry.lastSeen)
172
+ entry.lastSeen = run.at;
173
+ }
174
+ }
175
+ const providers = [...grouped.values()].map((entry) => ({
176
+ url: entry.url,
177
+ label: entry.label,
178
+ name: entry.label ?? entry.url,
179
+ runs: entry.runs,
180
+ wins: entry.wins,
181
+ winRate: entry.blocks === 0 ? null : entry.wins / entry.blocks,
182
+ delayMs: median(entry.delays),
183
+ lastDelayMs: entry.lastDelayMs,
184
+ lastSeen: entry.lastSeen,
185
+ }));
186
+ providers.sort((a, b) => b.wins - a.wins ||
187
+ (a.delayMs ?? Number.POSITIVE_INFINITY) - (b.delayMs ?? Number.POSITIVE_INFINITY) ||
188
+ a.name.localeCompare(b.name));
189
+ return {
190
+ runs,
191
+ providers,
192
+ firstAt: runs[0]?.at ?? null,
193
+ lastAt: runs[runs.length - 1]?.at ?? null,
194
+ newBlocks: runs.reduce((sum, run) => sum + run.newBlocks, 0),
195
+ chains,
196
+ skippedLines,
197
+ };
198
+ }
199
+ function iso(at) {
200
+ return new Date(at).toISOString();
201
+ }
202
+ function stamp(at) {
203
+ return iso(at).slice(5, 16).replace("T", " ");
204
+ }
205
+ export function formatCompareHistory(report) {
206
+ const lines = ["lwc — compare history", ""];
207
+ lines.push(` ${"log".padEnd(12)}${report.path}`);
208
+ const first = report.firstAt;
209
+ const last = report.lastAt;
210
+ const range = first === null
211
+ ? "—"
212
+ : last === null || last === first
213
+ ? iso(first)
214
+ : `${iso(first)} → ${iso(last)}`;
215
+ lines.push(` ${"runs".padEnd(12)}${report.runs.length} (${range})`);
216
+ lines.push(` ${"new blocks".padEnd(12)}${report.newBlocks}`);
217
+ if (report.chains.length === 1) {
218
+ const chain = report.chains[0];
219
+ lines.push(` ${"chain".padEnd(12)}${chain?.name ?? "?"}${chain?.id == null ? "" : ` (${chain.id})`}`);
220
+ }
221
+ else if (report.chains.length > 1) {
222
+ const names = report.chains.map((chain) => `${chain.name ?? "?"}${chain.id == null ? "" : ` (${chain.id})`}`);
223
+ lines.push(` ${"chains".padEnd(12)}${names.join(", ")}`);
224
+ }
225
+ if (report.skippedLines > 0) {
226
+ lines.push(` ${"skipped".padEnd(12)}${report.skippedLines} malformed line(s)`);
227
+ }
228
+ lines.push("");
229
+ if (report.providers.length === 0) {
230
+ lines.push(report.runs.length === 0 ? " (no compare records)" : " (no endpoint records)");
231
+ return lines.join("\n");
232
+ }
233
+ const width = Math.max(8, ...report.providers.map((provider) => provider.name.length));
234
+ lines.push(` ${"provider".padEnd(width)} ${"runs".padStart(4)} ${"wins".padStart(4)} ${"win%".padStart(5)} ${"delay".padStart(6)} ${"last".padStart(6)} ${"last seen".padStart(11)}`);
235
+ for (const provider of report.providers) {
236
+ const winRate = provider.winRate === null ? "n/a" : `${Math.round(provider.winRate * 100)}%`;
237
+ const delay = provider.delayMs === null ? "n/a" : `${provider.delayMs}ms`;
238
+ const last = provider.lastDelayMs === null ? "n/a" : `${provider.lastDelayMs}ms`;
239
+ lines.push(` ${provider.name.padEnd(width)} ${String(provider.runs).padStart(4)} ${String(provider.wins).padStart(4)} ${winRate.padStart(5)} ${delay.padStart(6)} ${last.padStart(6)} ${stamp(provider.lastSeen).padStart(11)}`);
240
+ }
241
+ return lines.join("\n");
242
+ }
243
+ export const COMPARE_HISTORY_CSV_HEADER = [
244
+ "session",
245
+ "at",
246
+ "chain",
247
+ "chain_id",
248
+ "samples",
249
+ "new_blocks",
250
+ "provider",
251
+ "provider_url",
252
+ "wins",
253
+ "observed",
254
+ "misses",
255
+ "delay_ms",
256
+ "errors",
257
+ "ok",
258
+ ];
259
+ /**
260
+ * One row per run and provider: the run's identity and chain are repeated for
261
+ * each of its endpoints, so a spreadsheet can pivot by provider, session, or
262
+ * time without parsing nested JSON. `delay_ms` is empty when that provider saw
263
+ * no new heads in the run.
264
+ */
265
+ export function compareHistoryCsv(report) {
266
+ const lines = [COMPARE_HISTORY_CSV_HEADER.join(",")];
267
+ for (const run of report.runs) {
268
+ for (const provider of run.providers) {
269
+ lines.push([
270
+ run.session,
271
+ iso(run.at),
272
+ run.chainName ?? "",
273
+ run.chainId === null ? "" : String(run.chainId),
274
+ String(run.samples),
275
+ String(run.newBlocks),
276
+ provider.label ?? provider.url,
277
+ provider.url,
278
+ String(provider.wins),
279
+ String(provider.observed),
280
+ String(provider.misses),
281
+ provider.delayMs === null ? "" : String(provider.delayMs),
282
+ String(provider.errors),
283
+ provider.ok ? "true" : "false",
284
+ ]
285
+ .map(csvCell)
286
+ .join(","));
287
+ }
288
+ }
289
+ return `${lines.join("\n")}\n`;
290
+ }
291
+ /** Write the CSV, creating parent directories; an existing file is replaced. */
292
+ export function saveCompareHistoryCsv(path, csv) {
293
+ try {
294
+ mkdirSync(dirname(path), { recursive: true });
295
+ writeFileSync(path, csv);
296
+ }
297
+ catch (error) {
298
+ const reason = error instanceof Error ? error.message : String(error);
299
+ throw new CompareHistoryArgError(`cannot write ${path}: ${reason}`);
300
+ }
301
+ }
302
+ export function toCompareHistoryJson(report) {
303
+ return {
304
+ path: report.path,
305
+ runs: report.runs.length,
306
+ firstAt: report.firstAt === null ? null : iso(report.firstAt),
307
+ lastAt: report.lastAt === null ? null : iso(report.lastAt),
308
+ newBlocks: report.newBlocks,
309
+ chains: report.chains,
310
+ skippedLines: report.skippedLines,
311
+ providers: report.providers.map((provider) => ({
312
+ url: provider.url,
313
+ label: provider.label,
314
+ name: provider.name,
315
+ runs: provider.runs,
316
+ wins: provider.wins,
317
+ winRate: provider.winRate,
318
+ delayMs: provider.delayMs,
319
+ lastDelayMs: provider.lastDelayMs,
320
+ lastSeen: iso(provider.lastSeen),
321
+ })),
322
+ history: report.runs.map((run) => ({
323
+ session: run.session,
324
+ at: iso(run.at),
325
+ chain: { id: run.chainId, name: run.chainName },
326
+ samples: run.samples,
327
+ newBlocks: run.newBlocks,
328
+ endpoints: run.providers.map((provider) => ({
329
+ url: provider.url,
330
+ label: provider.label,
331
+ ok: provider.ok,
332
+ wins: provider.wins,
333
+ observed: provider.observed,
334
+ misses: provider.misses,
335
+ delayMs: provider.delayMs,
336
+ errors: provider.errors,
337
+ })),
338
+ })),
339
+ };
340
+ }
@@ -0,0 +1,98 @@
1
+ import type { PublicClient } from "viem";
2
+ import { type ChainConfig } from "./chain.ts";
3
+ export declare class CompareArgError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export interface CompareEndpoint {
7
+ url: string;
8
+ /** Display name for this provider; null falls back to the URL. */
9
+ label: string | null;
10
+ /** Authorization header for this endpoint; null inherits LWC_RPC_AUTH. */
11
+ authHeader: string | null;
12
+ }
13
+ export interface CompareArgs {
14
+ endpoints: CompareEndpoint[];
15
+ samples: number;
16
+ intervalMs: number;
17
+ /** Append one JSON record per run here; null disables logging. */
18
+ logPath: string | null;
19
+ /** Rotate the log to <path>.1 past this many bytes; 0 disables it. */
20
+ logMaxBytes: number;
21
+ }
22
+ export interface LatencySummary {
23
+ p50: number;
24
+ p95: number;
25
+ min: number;
26
+ max: number;
27
+ mean: number;
28
+ }
29
+ export interface HeadDeliveryStats {
30
+ /** New heads this endpoint reported before any other endpoint. */
31
+ wins: number;
32
+ /** New heads it reported at all. */
33
+ observed: number;
34
+ /** New heads it never reported during the run. */
35
+ misses: number;
36
+ /** Median ms after the first report of the same head; null if none observed. */
37
+ p50Ms: number | null;
38
+ /** p95 ms after the first report of the same head; null if none observed. */
39
+ p95Ms: number | null;
40
+ }
41
+ export interface EndpointStats {
42
+ url: string;
43
+ /** Provider label from the endpoint spec, or null when unnamed. */
44
+ label: string | null;
45
+ ok: boolean;
46
+ /** Rounds attempted. */
47
+ samples: number;
48
+ errors: number;
49
+ latency: LatencySummary;
50
+ head: bigint | null;
51
+ /** Blocks behind the freshest endpoint at the end of the run. */
52
+ headLag: number;
53
+ /** Rounds in which this endpoint was behind the round's freshest head. */
54
+ roundsBehind: number;
55
+ headAgeSeconds: number | null;
56
+ /** How quickly this endpoint delivered each new head relative to the first. */
57
+ delivery: HeadDeliveryStats;
58
+ chainId: number | null;
59
+ chainMismatch: boolean;
60
+ error: string | null;
61
+ }
62
+ export interface CompareReport {
63
+ chainId: number;
64
+ chainName: string;
65
+ samples: number;
66
+ intervalMs: number;
67
+ /** Distinct heads that first appeared after the baseline round. */
68
+ newBlocks: number;
69
+ endpoints: EndpointStats[];
70
+ fastest: string | null;
71
+ freshest: string | null;
72
+ }
73
+ export interface CompareDeps {
74
+ clientFor?: (config: ChainConfig) => PublicClient;
75
+ now?: () => number;
76
+ sleep?: (ms: number) => Promise<void>;
77
+ }
78
+ /**
79
+ * Environment variable that supplies a labeled endpoint's header: the label is
80
+ * uppercased and any dot, dash, or underscore becomes an underscore, so `drpc`
81
+ * reads LWC_RPC_AUTH_DRPC and `my-node` reads LWC_RPC_AUTH_MY_NODE.
82
+ */
83
+ export declare function endpointAuthEnvName(label: string): string;
84
+ export declare function parseCompareArgs(argv: string[], env?: Record<string, string | undefined>): CompareArgs;
85
+ export declare function summarizeLatency(values: number[]): LatencySummary;
86
+ /**
87
+ * Probe every endpoint in the same round, in parallel, so the latency and head
88
+ * numbers describe the same wall-clock window on each RPC.
89
+ */
90
+ export declare function compareEndpoints(config: ChainConfig, args: CompareArgs, deps?: CompareDeps): Promise<CompareReport>;
91
+ export declare function formatCompare(report: CompareReport): string;
92
+ /**
93
+ * One NDJSON record per comparison run. URLs are reduced to origin and path so
94
+ * a provider URL carrying an API key never reaches the log, and error text is
95
+ * scrubbed the same way.
96
+ */
97
+ export declare function toCompareLogRecord(report: CompareReport, session: string, at?: number): Record<string, unknown>;
98
+ export declare function toCompareJson(report: CompareReport): Record<string, unknown>;