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.
- package/.env.example +38 -0
- package/LICENSE +21 -0
- package/README.md +1915 -2
- package/dist/alert-queue.d.ts +72 -0
- package/dist/alert-queue.js +155 -0
- package/dist/alert-store.d.ts +29 -0
- package/dist/alert-store.js +75 -0
- package/dist/balances.d.ts +221 -0
- package/dist/balances.js +1232 -0
- package/dist/cache.d.ts +37 -0
- package/dist/cache.js +128 -0
- package/dist/call.d.ts +46 -0
- package/dist/call.js +377 -0
- package/dist/chain.d.ts +97 -0
- package/dist/chain.js +419 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1011 -0
- package/dist/compare-history.d.ts +82 -0
- package/dist/compare-history.js +340 -0
- package/dist/compare.d.ts +98 -0
- package/dist/compare.js +492 -0
- package/dist/contract.d.ts +107 -0
- package/dist/contract.js +162 -0
- package/dist/default-abi.d.ts +53 -0
- package/dist/default-abi.js +85 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +422 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +27 -0
- package/dist/market-page.d.ts +8 -0
- package/dist/market-page.js +554 -0
- package/dist/market-server.d.ts +46 -0
- package/dist/market-server.js +473 -0
- package/dist/market.d.ts +323 -0
- package/dist/market.js +835 -0
- package/dist/metadata-cache.d.ts +72 -0
- package/dist/metadata-cache.js +197 -0
- package/dist/metrics.d.ts +64 -0
- package/dist/metrics.js +138 -0
- package/dist/plot.d.ts +60 -0
- package/dist/plot.js +317 -0
- package/dist/presets-history.d.ts +372 -0
- package/dist/presets-history.js +1243 -0
- package/dist/presets-summary.d.ts +17 -0
- package/dist/presets-summary.js +55 -0
- package/dist/presets.d.ts +59 -0
- package/dist/presets.js +181 -0
- package/dist/prices.d.ts +63 -0
- package/dist/prices.js +154 -0
- package/dist/rotate.d.ts +19 -0
- package/dist/rotate.js +34 -0
- package/dist/scan.d.ts +81 -0
- package/dist/scan.js +501 -0
- package/dist/sessions.d.ts +104 -0
- package/dist/sessions.js +515 -0
- package/dist/signatures.d.ts +13 -0
- package/dist/signatures.js +43 -0
- package/dist/tokenfile.d.ts +50 -0
- package/dist/tokenfile.js +218 -0
- package/dist/views.d.ts +63 -0
- package/dist/views.js +415 -0
- package/dist/watch.d.ts +306 -0
- package/dist/watch.js +1121 -0
- package/dist/webhook.d.ts +17 -0
- package/dist/webhook.js +35 -0
- package/examples/erc20.abi.json +57 -0
- package/examples/xlayer.tokens.json +4 -0
- package/package.json +77 -4
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { StoredAlert } from "./alert-store.ts";
|
|
2
|
+
import type { WebhookResult } from "./webhook.ts";
|
|
3
|
+
export interface AlertTarget {
|
|
4
|
+
url: string;
|
|
5
|
+
payload: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** Durable backing for the queue; keeps undelivered alerts across restarts. */
|
|
8
|
+
export interface AlertQueuePersistence {
|
|
9
|
+
load(): StoredAlert[];
|
|
10
|
+
save(alerts: StoredAlert[]): void;
|
|
11
|
+
}
|
|
12
|
+
export interface AlertQueueOptions {
|
|
13
|
+
deliver: (target: AlertTarget) => Promise<WebhookResult>;
|
|
14
|
+
/** Injected for tests; defaults to a cancellable timer. */
|
|
15
|
+
sleep?: (ms: number) => Promise<void>;
|
|
16
|
+
/** First retry delay, doubled per attempt. Default 500 ms. */
|
|
17
|
+
baseDelayMs?: number;
|
|
18
|
+
/** Upper bound for a single backoff wait. Default 30 s. */
|
|
19
|
+
maxDelayMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Attempts per alert before giving up. 0 (the default) retries until the
|
|
22
|
+
* receiver accepts, so a transient outage never drops an alert. A positive
|
|
23
|
+
* value is mainly useful in tests.
|
|
24
|
+
*/
|
|
25
|
+
maxAttempts?: number;
|
|
26
|
+
/** When set, every queue change is written here and `restore()` replays it. */
|
|
27
|
+
store?: AlertQueuePersistence;
|
|
28
|
+
onRetry?: (target: AlertTarget, attempt: number, delayMs: number, message: string) => void;
|
|
29
|
+
onGiveUp?: (target: AlertTarget, attempts: number, message: string) => void;
|
|
30
|
+
/** A failed durable write is reported here; in-memory delivery continues. */
|
|
31
|
+
onPersistError?: (error: unknown) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Serial alert delivery: one POST at a time, in enqueue order. A failed delivery
|
|
35
|
+
* stays at the head of the queue and is retried with exponential backoff, so a
|
|
36
|
+
* burst during an outage waits instead of firing concurrently and no alert is
|
|
37
|
+
* skipped. With a `store`, the pending queue is rewritten after every change:
|
|
38
|
+
* `stop()` clears memory but leaves the file intact, and `restore()` on the next
|
|
39
|
+
* run replays those alerts. Delivery is at-least-once — a crash between a
|
|
40
|
+
* successful POST and the store rewrite can resend one alert.
|
|
41
|
+
*/
|
|
42
|
+
export declare class AlertQueue {
|
|
43
|
+
private readonly items;
|
|
44
|
+
private readonly deliver;
|
|
45
|
+
private readonly customSleep;
|
|
46
|
+
private readonly baseDelayMs;
|
|
47
|
+
private readonly maxDelayMs;
|
|
48
|
+
private readonly maxAttempts;
|
|
49
|
+
private readonly store;
|
|
50
|
+
private readonly onRetry;
|
|
51
|
+
private readonly onGiveUp;
|
|
52
|
+
private readonly onPersistError;
|
|
53
|
+
private loop;
|
|
54
|
+
private stopped;
|
|
55
|
+
private wake;
|
|
56
|
+
constructor(options: AlertQueueOptions);
|
|
57
|
+
get pending(): number;
|
|
58
|
+
/**
|
|
59
|
+
* Load alerts left by an earlier run and start delivering them. Attempts
|
|
60
|
+
* restart at zero so a restart tries immediately instead of waiting out the
|
|
61
|
+
* previous run's backoff.
|
|
62
|
+
*/
|
|
63
|
+
restore(): number;
|
|
64
|
+
enqueue(target: AlertTarget): void;
|
|
65
|
+
/** Resolves once the current delivery loop has ended. */
|
|
66
|
+
flush(): Promise<void>;
|
|
67
|
+
stop(): void;
|
|
68
|
+
private start;
|
|
69
|
+
private run;
|
|
70
|
+
private persist;
|
|
71
|
+
private wait;
|
|
72
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Serial alert delivery: one POST at a time, in enqueue order. A failed delivery
|
|
4
|
+
* stays at the head of the queue and is retried with exponential backoff, so a
|
|
5
|
+
* burst during an outage waits instead of firing concurrently and no alert is
|
|
6
|
+
* skipped. With a `store`, the pending queue is rewritten after every change:
|
|
7
|
+
* `stop()` clears memory but leaves the file intact, and `restore()` on the next
|
|
8
|
+
* run replays those alerts. Delivery is at-least-once — a crash between a
|
|
9
|
+
* successful POST and the store rewrite can resend one alert.
|
|
10
|
+
*/
|
|
11
|
+
export class AlertQueue {
|
|
12
|
+
items = [];
|
|
13
|
+
deliver;
|
|
14
|
+
customSleep;
|
|
15
|
+
baseDelayMs;
|
|
16
|
+
maxDelayMs;
|
|
17
|
+
maxAttempts;
|
|
18
|
+
store;
|
|
19
|
+
onRetry;
|
|
20
|
+
onGiveUp;
|
|
21
|
+
onPersistError;
|
|
22
|
+
loop = null;
|
|
23
|
+
stopped = false;
|
|
24
|
+
wake = null;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.deliver = options.deliver;
|
|
27
|
+
this.customSleep = options.sleep;
|
|
28
|
+
this.baseDelayMs = options.baseDelayMs ?? 500;
|
|
29
|
+
this.maxDelayMs = options.maxDelayMs ?? 30_000;
|
|
30
|
+
this.maxAttempts = options.maxAttempts ?? 0;
|
|
31
|
+
this.store = options.store;
|
|
32
|
+
this.onRetry = options.onRetry;
|
|
33
|
+
this.onGiveUp = options.onGiveUp;
|
|
34
|
+
this.onPersistError = options.onPersistError;
|
|
35
|
+
}
|
|
36
|
+
get pending() {
|
|
37
|
+
return this.items.length;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load alerts left by an earlier run and start delivering them. Attempts
|
|
41
|
+
* restart at zero so a restart tries immediately instead of waiting out the
|
|
42
|
+
* previous run's backoff.
|
|
43
|
+
*/
|
|
44
|
+
restore() {
|
|
45
|
+
if (!this.store)
|
|
46
|
+
return 0;
|
|
47
|
+
for (const alert of this.store.load()) {
|
|
48
|
+
this.items.push({
|
|
49
|
+
id: alert.id,
|
|
50
|
+
target: { url: alert.url, payload: alert.payload },
|
|
51
|
+
attempts: 0,
|
|
52
|
+
queuedAt: alert.queuedAt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (this.items.length > 0)
|
|
56
|
+
this.start();
|
|
57
|
+
return this.items.length;
|
|
58
|
+
}
|
|
59
|
+
enqueue(target) {
|
|
60
|
+
if (this.stopped)
|
|
61
|
+
return;
|
|
62
|
+
this.items.push({
|
|
63
|
+
id: randomUUID(),
|
|
64
|
+
target,
|
|
65
|
+
attempts: 0,
|
|
66
|
+
queuedAt: new Date().toISOString(),
|
|
67
|
+
});
|
|
68
|
+
this.persist();
|
|
69
|
+
this.start();
|
|
70
|
+
}
|
|
71
|
+
/** Resolves once the current delivery loop has ended. */
|
|
72
|
+
async flush() {
|
|
73
|
+
while (this.loop !== null)
|
|
74
|
+
await this.loop;
|
|
75
|
+
}
|
|
76
|
+
stop() {
|
|
77
|
+
this.stopped = true;
|
|
78
|
+
this.items.length = 0;
|
|
79
|
+
this.wake?.();
|
|
80
|
+
}
|
|
81
|
+
start() {
|
|
82
|
+
if (this.stopped || this.loop !== null)
|
|
83
|
+
return;
|
|
84
|
+
this.loop = this.run().finally(() => {
|
|
85
|
+
this.loop = null;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async run() {
|
|
89
|
+
while (!this.stopped && this.items.length > 0) {
|
|
90
|
+
const item = this.items[0];
|
|
91
|
+
if (item === undefined)
|
|
92
|
+
break;
|
|
93
|
+
item.attempts += 1;
|
|
94
|
+
this.persist();
|
|
95
|
+
let result;
|
|
96
|
+
try {
|
|
97
|
+
result = await this.deliver(item.target);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
result = {
|
|
101
|
+
ok: false,
|
|
102
|
+
message: error instanceof Error ? error.message : String(error),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (this.stopped)
|
|
106
|
+
break;
|
|
107
|
+
if (result.ok) {
|
|
108
|
+
this.items.shift();
|
|
109
|
+
this.persist();
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const message = result.message ?? "unknown error";
|
|
113
|
+
if (this.maxAttempts > 0 && item.attempts >= this.maxAttempts) {
|
|
114
|
+
this.items.shift();
|
|
115
|
+
this.persist();
|
|
116
|
+
this.onGiveUp?.(item.target, item.attempts, message);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const delayMs = Math.min(this.maxDelayMs, this.baseDelayMs * 2 ** Math.max(0, item.attempts - 1));
|
|
120
|
+
this.onRetry?.(item.target, item.attempts, delayMs, message);
|
|
121
|
+
await this.wait(delayMs);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
persist() {
|
|
125
|
+
if (!this.store)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
this.store.save(this.items.map((item) => ({
|
|
129
|
+
id: item.id,
|
|
130
|
+
url: item.target.url,
|
|
131
|
+
payload: item.target.payload,
|
|
132
|
+
attempts: item.attempts,
|
|
133
|
+
queuedAt: item.queuedAt,
|
|
134
|
+
})));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.onPersistError?.(error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
wait(ms) {
|
|
141
|
+
if (this.customSleep)
|
|
142
|
+
return this.customSleep(ms);
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
this.wake = null;
|
|
146
|
+
resolve();
|
|
147
|
+
}, ms);
|
|
148
|
+
this.wake = () => {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
this.wake = null;
|
|
151
|
+
resolve();
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** One queued webhook alert as it appears in the durable queue file. */
|
|
2
|
+
export interface StoredAlert {
|
|
3
|
+
id: string;
|
|
4
|
+
url: string;
|
|
5
|
+
payload: Record<string, unknown>;
|
|
6
|
+
/** Delivery attempts so far; informational, replay starts a fresh backoff. */
|
|
7
|
+
attempts: number;
|
|
8
|
+
/** ISO 8601 time the alert was first queued. */
|
|
9
|
+
queuedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_ALERT_QUEUE_PATH = ".lwc-cache/webhook-queue.json";
|
|
12
|
+
/**
|
|
13
|
+
* Flag path wins, then a path derived from the watch log, then the
|
|
14
|
+
* project-local default. A log-adjacent file keeps separate services apart
|
|
15
|
+
* without needing another flag.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveAlertQueuePath(flagPath?: string | null, logPath?: string | null): string;
|
|
18
|
+
/**
|
|
19
|
+
* JSON-file store for undelivered alerts. Writes go to a temporary file that is
|
|
20
|
+
* renamed over the real one, so a crash never leaves a half-written queue; an
|
|
21
|
+
* empty queue removes the file. Loading tolerates a missing, malformed or
|
|
22
|
+
* partially invalid file and keeps only usable entries.
|
|
23
|
+
*/
|
|
24
|
+
export declare class AlertStore {
|
|
25
|
+
readonly path: string;
|
|
26
|
+
constructor(path: string);
|
|
27
|
+
load(): StoredAlert[];
|
|
28
|
+
save(alerts: StoredAlert[]): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
export const DEFAULT_ALERT_QUEUE_PATH = ".lwc-cache/webhook-queue.json";
|
|
4
|
+
/**
|
|
5
|
+
* Flag path wins, then a path derived from the watch log, then the
|
|
6
|
+
* project-local default. A log-adjacent file keeps separate services apart
|
|
7
|
+
* without needing another flag.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveAlertQueuePath(flagPath = null, logPath = null) {
|
|
10
|
+
const flag = flagPath?.trim();
|
|
11
|
+
if (flag)
|
|
12
|
+
return flag;
|
|
13
|
+
return logPath ? `${logPath}.alerts.json` : DEFAULT_ALERT_QUEUE_PATH;
|
|
14
|
+
}
|
|
15
|
+
function usableAlert(value) {
|
|
16
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
17
|
+
return null;
|
|
18
|
+
const alert = value;
|
|
19
|
+
if (typeof alert.id !== "string" || alert.id.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
if (typeof alert.url !== "string" || !/^https?:\/\/\S+$/.test(alert.url))
|
|
22
|
+
return null;
|
|
23
|
+
const payload = alert.payload;
|
|
24
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
|
25
|
+
return null;
|
|
26
|
+
const attempts = typeof alert.attempts === "number" && Number.isInteger(alert.attempts) && alert.attempts >= 0
|
|
27
|
+
? alert.attempts
|
|
28
|
+
: 0;
|
|
29
|
+
return {
|
|
30
|
+
id: alert.id,
|
|
31
|
+
url: alert.url,
|
|
32
|
+
payload: payload,
|
|
33
|
+
attempts,
|
|
34
|
+
queuedAt: typeof alert.queuedAt === "string" ? alert.queuedAt : new Date(0).toISOString(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* JSON-file store for undelivered alerts. Writes go to a temporary file that is
|
|
39
|
+
* renamed over the real one, so a crash never leaves a half-written queue; an
|
|
40
|
+
* empty queue removes the file. Loading tolerates a missing, malformed or
|
|
41
|
+
* partially invalid file and keeps only usable entries.
|
|
42
|
+
*/
|
|
43
|
+
export class AlertStore {
|
|
44
|
+
path;
|
|
45
|
+
constructor(path) {
|
|
46
|
+
this.path = path;
|
|
47
|
+
}
|
|
48
|
+
load() {
|
|
49
|
+
if (!existsSync(this.path))
|
|
50
|
+
return [];
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(readFileSync(this.path, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const alerts = parsed?.alerts;
|
|
59
|
+
if (!Array.isArray(alerts))
|
|
60
|
+
return [];
|
|
61
|
+
return alerts
|
|
62
|
+
.map(usableAlert)
|
|
63
|
+
.filter((alert) => alert !== null);
|
|
64
|
+
}
|
|
65
|
+
save(alerts) {
|
|
66
|
+
if (alerts.length === 0) {
|
|
67
|
+
rmSync(this.path, { force: true });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
71
|
+
const temporary = `${this.path}.tmp`;
|
|
72
|
+
writeFileSync(temporary, `${JSON.stringify({ version: 1, alerts }, null, 2)}\n`);
|
|
73
|
+
renameSync(temporary, this.path);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { type Address, type PublicClient } from "viem";
|
|
2
|
+
import type { ChainConfig } from "./chain.ts";
|
|
3
|
+
import { type MetadataCache, type TokenMetadata } from "./metadata-cache.ts";
|
|
4
|
+
export interface TokenBalance {
|
|
5
|
+
address: Address;
|
|
6
|
+
symbol: string;
|
|
7
|
+
/** Optional name from the token list or an inline `--token` label. */
|
|
8
|
+
label: string | null;
|
|
9
|
+
/** Null when the contract did not return a usable decimals value. */
|
|
10
|
+
decimals: number | null;
|
|
11
|
+
/** Null when balanceOf could not be read (not an ERC-20, or the call reverted). */
|
|
12
|
+
balance: bigint | null;
|
|
13
|
+
/** Age of the reused metadata entry; null when read fresh or unrecorded. */
|
|
14
|
+
cacheAgeMs: number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Why this address does not look like a usable ERC-20, when it does not.
|
|
17
|
+
* Null for a token whose metadata all decoded cleanly.
|
|
18
|
+
*/
|
|
19
|
+
issue: string | null;
|
|
20
|
+
}
|
|
21
|
+
export type ChunkStatus = "batched" | "recovered" | "partial" | "failed";
|
|
22
|
+
export interface RecoverySummary {
|
|
23
|
+
/** Original chunks whose first aggregate3 request failed. */
|
|
24
|
+
failedChunks: number;
|
|
25
|
+
/** Retry requests sent for those chunks, two per multi-token chunk. */
|
|
26
|
+
retryRequests: number;
|
|
27
|
+
/** Chunks whose retries recovered every token. */
|
|
28
|
+
recoveredChunks: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ChunkReport {
|
|
31
|
+
/** 1-based position among this owner's chunks. */
|
|
32
|
+
index: number;
|
|
33
|
+
total: number;
|
|
34
|
+
/** Tokens assigned to this chunk. */
|
|
35
|
+
tokens: number;
|
|
36
|
+
status: ChunkStatus;
|
|
37
|
+
/** Why the first batch attempt failed; null when it succeeded. */
|
|
38
|
+
error: string | null;
|
|
39
|
+
/** Tokens from this chunk that were re-read individually. */
|
|
40
|
+
individual: Address[];
|
|
41
|
+
}
|
|
42
|
+
export interface ChunkProgress extends ChunkReport {
|
|
43
|
+
owner: Address;
|
|
44
|
+
}
|
|
45
|
+
export interface CacheStats {
|
|
46
|
+
/** Unique tokens whose metadata was served from the cache. */
|
|
47
|
+
hits: number;
|
|
48
|
+
/** Unique tokens whose metadata had to be read. */
|
|
49
|
+
misses: number;
|
|
50
|
+
}
|
|
51
|
+
export interface BalancesReport {
|
|
52
|
+
owner: Address;
|
|
53
|
+
chainId: number;
|
|
54
|
+
chainName: string;
|
|
55
|
+
native: {
|
|
56
|
+
symbol: string;
|
|
57
|
+
decimals: number;
|
|
58
|
+
balance: bigint;
|
|
59
|
+
};
|
|
60
|
+
tokens: TokenBalance[];
|
|
61
|
+
blockNumber: bigint;
|
|
62
|
+
/** Why the Multicall3 batch could not be used, if it could not. */
|
|
63
|
+
batchWarning: string | null;
|
|
64
|
+
/** Tokens per Multicall3 request selected for this read. */
|
|
65
|
+
batchSize: number;
|
|
66
|
+
/** Number of Multicall3 chunks attempted. */
|
|
67
|
+
chunkCount: number;
|
|
68
|
+
/** Per-chunk outcomes, including retries and individual fallbacks. */
|
|
69
|
+
chunks: ChunkReport[];
|
|
70
|
+
/** First-attempt failures and their retry outcome. */
|
|
71
|
+
recovery: RecoverySummary;
|
|
72
|
+
/** Metadata cache hits and misses for this read. */
|
|
73
|
+
cache: CacheStats;
|
|
74
|
+
}
|
|
75
|
+
export declare class BalancesArgError extends Error {
|
|
76
|
+
constructor(message: string);
|
|
77
|
+
}
|
|
78
|
+
export interface BalanceArgs {
|
|
79
|
+
owner: Address;
|
|
80
|
+
tokens: Address[];
|
|
81
|
+
/** Lowercased token address → label from the token list file. */
|
|
82
|
+
labels: Map<string, string>;
|
|
83
|
+
/** Tokens per Multicall3 request. */
|
|
84
|
+
batchSize: number;
|
|
85
|
+
/** Fixed sub-call cap per Multicall3 request, independent of cache state. */
|
|
86
|
+
maxSubCalls: number | null;
|
|
87
|
+
/** Exit non-zero when any chunk needed a retry or individual fallback. */
|
|
88
|
+
failOnRetry: boolean;
|
|
89
|
+
/** Print one line per Multicall3 chunk, with its retry outcome, to stderr. */
|
|
90
|
+
verbose: boolean;
|
|
91
|
+
/** Refresh cached metadata older than this many milliseconds. */
|
|
92
|
+
maxCacheAgeMs: number | null;
|
|
93
|
+
/** Warn when cached metadata is older than this many milliseconds; never refreshes. */
|
|
94
|
+
staleAfterMs: number | null;
|
|
95
|
+
/** Non-fatal label conflicts found while merging token sources. */
|
|
96
|
+
warnings: string[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read a saved address list: a JSON array of addresses, or an object holding one
|
|
100
|
+
* of `keys` as that array (matching the tolerance of the ABI loader).
|
|
101
|
+
*/
|
|
102
|
+
export declare function loadAddressList(path: string, label: string, keys: string[]): Address[];
|
|
103
|
+
export interface TokenListEntry {
|
|
104
|
+
address: Address;
|
|
105
|
+
label: string | null;
|
|
106
|
+
}
|
|
107
|
+
/** Parse an inline token spec: `0xAddress:Label` or `0xAddress=Label`. */
|
|
108
|
+
export declare function parseTokenSpec(raw: string): TokenListEntry;
|
|
109
|
+
/** Record the last label for a token, warning when a different one was present. */
|
|
110
|
+
export declare function rememberTokenLabel(entry: TokenListEntry, labels: Map<string, string>, warnings: string[]): void;
|
|
111
|
+
/**
|
|
112
|
+
* Merge ad-hoc and file labels. Inline `--token` labels win over the token
|
|
113
|
+
* file, and a differing file label is reported once rather than silently lost.
|
|
114
|
+
*/
|
|
115
|
+
export declare function mergeTokenLabels(inlineLabels: ReadonlyMap<string, string>, fileLabels: ReadonlyMap<string, string>, warnings: string[]): Map<string, string>;
|
|
116
|
+
/**
|
|
117
|
+
* Saved token list: a JSON array of addresses, or `{ address, label }` objects,
|
|
118
|
+
* or an object with a `tokens` array of either.
|
|
119
|
+
*/
|
|
120
|
+
export declare function loadTokenList(path: string): TokenListEntry[];
|
|
121
|
+
export declare function parseBalanceArgs(argv: string[]): BalanceArgs;
|
|
122
|
+
/** Render a base-unit value without trailing zeros. */
|
|
123
|
+
export declare function formatTokenAmount(value: bigint, decimals: number): string;
|
|
124
|
+
/** Compact age for cached metadata: `45s`, `12m`, `5h`, `3d`. */
|
|
125
|
+
/**
|
|
126
|
+
* Warn-only check for cached metadata older than `staleAfterMs`. Freshly read
|
|
127
|
+
* tokens and undated legacy entries carry no age, so they never warn.
|
|
128
|
+
*/
|
|
129
|
+
export declare function staleMetadataWarnings(tokens: readonly TokenBalance[], staleAfterMs: number | null): string[];
|
|
130
|
+
export declare function formatCacheAge(ms: number): string;
|
|
131
|
+
export declare function shortAddress(address: string): string;
|
|
132
|
+
/** One stderr line per chunk for `--verbose`, matching `scan`'s progress text. */
|
|
133
|
+
export declare function formatChunkProgress(chunk: ChunkProgress): string;
|
|
134
|
+
/**
|
|
135
|
+
* Precompiles live at 0x…01–0x…0a. They have no code, so they can never be
|
|
136
|
+
* ERC-20s, and some (notably 0x05 modexp) read a 4-byte selector as an enormous
|
|
137
|
+
* operand length and burn every unit of gas available — which reverts the whole
|
|
138
|
+
* aggregate3 batch with "out of gas". They are answered locally instead.
|
|
139
|
+
*/
|
|
140
|
+
export declare function isPrecompileAddress(address: Address): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* One eth_call: native balance plus decimals/symbol/balanceOf/totalSupply for
|
|
143
|
+
* every token, through Multicall3 aggregate3. Returns null when the batch cannot
|
|
144
|
+
* be used at all, so the caller can fall back to individual reads.
|
|
145
|
+
*/
|
|
146
|
+
/** Default tokens per Multicall3 request: 12 uncached tokens is 49 sub-calls,
|
|
147
|
+
* comfortably within the 50M eth_call gas cap that a gas-burning address can
|
|
148
|
+
* otherwise exhaust. `--batch-size` lowers it for stricter RPCs. */
|
|
149
|
+
export declare const TOKENS_PER_BATCH = 12;
|
|
150
|
+
/** Cached tokens need only balanceOf and totalSupply, so 24 still fits 49. */
|
|
151
|
+
export declare const CACHED_TOKENS_PER_BATCH = 24;
|
|
152
|
+
export declare const MAX_BATCH_SIZE = 100;
|
|
153
|
+
/** Upper bound for `--max-subcalls`: a fixed request budget, cache independent. */
|
|
154
|
+
export declare const MAX_SUB_CALLS = 1000;
|
|
155
|
+
export declare function readOwnerBatch(client: PublicClient, owner: Address, tokens: Address[], includeNative?: boolean, cache?: MetadataCache): Promise<{
|
|
156
|
+
ok: true;
|
|
157
|
+
native: bigint | null;
|
|
158
|
+
tokens: TokenBalance[];
|
|
159
|
+
metadata: Map<string, TokenMetadata>;
|
|
160
|
+
} | {
|
|
161
|
+
ok: false;
|
|
162
|
+
error: string;
|
|
163
|
+
}>;
|
|
164
|
+
/**
|
|
165
|
+
* Read `decimals`/`symbol` once for every token missing from the cache. `scan`
|
|
166
|
+
* calls this before fanning out to owners so concurrent owners do not each
|
|
167
|
+
* re-read the same metadata.
|
|
168
|
+
*/
|
|
169
|
+
export declare function prefetchMetadata(client: PublicClient, tokens: Address[], cache: MetadataCache, batchSize?: number, maxSubCalls?: number): Promise<number>;
|
|
170
|
+
/** Count unique ERC-20 metadata lookups the cache can answer. */
|
|
171
|
+
export declare function countCacheStats(tokens: Address[], cache?: MetadataCache): CacheStats;
|
|
172
|
+
export interface TokenCheck {
|
|
173
|
+
address: Address;
|
|
174
|
+
symbol: string | null;
|
|
175
|
+
decimals: number | null;
|
|
176
|
+
/** Why this address does not look like a usable ERC-20; null when it does. */
|
|
177
|
+
issue: string | null;
|
|
178
|
+
}
|
|
179
|
+
export interface TokenCheckOptions {
|
|
180
|
+
/** Tokens per Multicall3 request. */
|
|
181
|
+
batchSize?: number;
|
|
182
|
+
/** Fixed sub-call budget per request (3 per token), ignoring `batchSize`. */
|
|
183
|
+
maxSubCalls?: number;
|
|
184
|
+
}
|
|
185
|
+
/** Verification checks plus the request budget that produced them. */
|
|
186
|
+
export interface TokenCheckReport {
|
|
187
|
+
checks: TokenCheck[];
|
|
188
|
+
/** Multicall3 verification requests sent; 0 when every address was local. */
|
|
189
|
+
chunkCount: number;
|
|
190
|
+
/** The sub-call cap used, or null when the default token batch size applied. */
|
|
191
|
+
maxSubCalls: number | null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Read decimals, symbol and totalSupply for each address and classify it, so
|
|
195
|
+
* `tokens` can warn about entries that are not readable ERC-20s before saving
|
|
196
|
+
* them. Nothing here throws for a bad address: reverts, empty answers and batch
|
|
197
|
+
* failures all become an `issue`. The report also carries the number of
|
|
198
|
+
* Multicall3 verification requests and the cap that shaped them.
|
|
199
|
+
*/
|
|
200
|
+
export declare function checkTokenAddressesReport(client: PublicClient, tokens: Address[], options?: TokenCheckOptions): Promise<TokenCheckReport>;
|
|
201
|
+
/** The classified checks only, for callers that do not need the request count. */
|
|
202
|
+
export declare function checkTokenAddresses(client: PublicClient, tokens: Address[], options?: TokenCheckOptions): Promise<TokenCheck[]>;
|
|
203
|
+
export interface BalancesOptions {
|
|
204
|
+
/** Reuse `decimals`/`symbol` seen on earlier runs instead of re-reading them. */
|
|
205
|
+
cache?: MetadataCache;
|
|
206
|
+
/** Ignore cached metadata for the requested tokens and re-read it. */
|
|
207
|
+
refreshCache?: boolean;
|
|
208
|
+
/** Refresh cached metadata older than this many milliseconds. */
|
|
209
|
+
maxCacheAgeMs?: number;
|
|
210
|
+
/** Lowercased address → label, shown next to the symbol in output. */
|
|
211
|
+
labels?: ReadonlyMap<string, string>;
|
|
212
|
+
/** Tokens per Multicall3 request; smaller values fit stricter gas caps. */
|
|
213
|
+
batchSize?: number;
|
|
214
|
+
/** Fixed sub-call cap per request; takes precedence over `batchSize`. */
|
|
215
|
+
maxSubCalls?: number;
|
|
216
|
+
/** Called once per chunk with its final status and fallback tokens. */
|
|
217
|
+
onChunkProgress?: (progress: ChunkProgress) => void;
|
|
218
|
+
}
|
|
219
|
+
export declare function readBalances(client: PublicClient, config: ChainConfig, owner: Address, tokens: Address[], options?: BalancesOptions): Promise<BalancesReport>;
|
|
220
|
+
export declare function formatBalances(report: BalancesReport): string;
|
|
221
|
+
export declare function toBalancesJson(report: BalancesReport): Record<string, unknown>;
|