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,218 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { getAddress } from "viem";
|
|
4
|
+
import { BalancesArgError, MAX_SUB_CALLS, loadTokenList, parseTokenSpec, rememberTokenLabel, } from "./balances.js";
|
|
5
|
+
export class TokenFileArgError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "TokenFileArgError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Parse one address spec (`0xAddress`, `0xAddress=Label`, `0xAddress:Label`). */
|
|
12
|
+
function parseMaxSubCalls(raw) {
|
|
13
|
+
if (!/^\d+$/.test(raw)) {
|
|
14
|
+
throw new TokenFileArgError(`--max-subcalls must be a whole number, got "${raw}".`);
|
|
15
|
+
}
|
|
16
|
+
const value = Number(raw);
|
|
17
|
+
if (value < 1 || value > MAX_SUB_CALLS) {
|
|
18
|
+
throw new TokenFileArgError(`--max-subcalls must be between 1 and ${MAX_SUB_CALLS}.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function parseEntry(raw) {
|
|
23
|
+
try {
|
|
24
|
+
return parseTokenSpec(raw);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof BalancesArgError)
|
|
28
|
+
throw new TokenFileArgError(error.message);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Normalize addresses and `{address, label}` entries to labeled entries. */
|
|
33
|
+
function normalizeEntries(entries) {
|
|
34
|
+
return entries.map((entry) => {
|
|
35
|
+
if (typeof entry === "string")
|
|
36
|
+
return { address: getAddress(entry), label: null };
|
|
37
|
+
const label = entry.label?.trim() ?? "";
|
|
38
|
+
return { address: getAddress(entry.address), label: label.length > 0 ? label : null };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Keep the entries already in a list — same order, same labels — then append the
|
|
43
|
+
* addresses that are not there yet. A new label for an existing address replaces
|
|
44
|
+
* the old one, and a differing pair is reported in `warnings`.
|
|
45
|
+
*/
|
|
46
|
+
export function mergeTokenEntries(existing, added, warnings = []) {
|
|
47
|
+
const order = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const labels = new Map();
|
|
50
|
+
for (const entry of [...normalizeEntries(existing), ...normalizeEntries(added)]) {
|
|
51
|
+
const key = entry.address.toLowerCase();
|
|
52
|
+
if (!seen.has(key)) {
|
|
53
|
+
seen.add(key);
|
|
54
|
+
order.push(entry.address);
|
|
55
|
+
}
|
|
56
|
+
// Present labels win in order, so an existing label survives an unlabeled
|
|
57
|
+
// repeat and a later label replaces an earlier one.
|
|
58
|
+
rememberTokenLabel(entry, labels, warnings);
|
|
59
|
+
}
|
|
60
|
+
return order.map((address) => ({
|
|
61
|
+
address,
|
|
62
|
+
label: labels.get(address.toLowerCase()) ?? null,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a `--tokens-file` list from command-line addresses. Pure local work: no
|
|
67
|
+
* RPC call is needed, and labels are carried through to the written file. With
|
|
68
|
+
* `--merge` the existing file is read so only new addresses are appended.
|
|
69
|
+
*/
|
|
70
|
+
export function parseTokenFileArgs(argv) {
|
|
71
|
+
const raw = [];
|
|
72
|
+
let outPath = null;
|
|
73
|
+
let force = false;
|
|
74
|
+
let merge = false;
|
|
75
|
+
let maxSubCalls = null;
|
|
76
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
77
|
+
const arg = argv[index] ?? "";
|
|
78
|
+
if (arg === "--out") {
|
|
79
|
+
const value = argv[index + 1];
|
|
80
|
+
if (!value || (value.startsWith("-") && value !== "-")) {
|
|
81
|
+
throw new TokenFileArgError("--out requires a path, or - for stdout.");
|
|
82
|
+
}
|
|
83
|
+
outPath = value;
|
|
84
|
+
index += 1;
|
|
85
|
+
}
|
|
86
|
+
else if (arg.startsWith("--out=")) {
|
|
87
|
+
const value = arg.slice("--out=".length);
|
|
88
|
+
if (!value)
|
|
89
|
+
throw new TokenFileArgError("--out requires a path, or - for stdout.");
|
|
90
|
+
outPath = value;
|
|
91
|
+
}
|
|
92
|
+
else if (arg === "--force") {
|
|
93
|
+
force = true;
|
|
94
|
+
}
|
|
95
|
+
else if (arg === "--merge") {
|
|
96
|
+
merge = true;
|
|
97
|
+
}
|
|
98
|
+
else if (arg === "--max-subcalls") {
|
|
99
|
+
const value = argv[index + 1];
|
|
100
|
+
if (!value)
|
|
101
|
+
throw new TokenFileArgError("--max-subcalls requires a value.");
|
|
102
|
+
maxSubCalls = parseMaxSubCalls(value);
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
else if (arg.startsWith("--max-subcalls=")) {
|
|
106
|
+
maxSubCalls = parseMaxSubCalls(arg.slice("--max-subcalls=".length));
|
|
107
|
+
}
|
|
108
|
+
else if (arg === "--token") {
|
|
109
|
+
const value = argv[index + 1];
|
|
110
|
+
if (!value || value.startsWith("-"))
|
|
111
|
+
throw new TokenFileArgError("--token requires an address.");
|
|
112
|
+
raw.push(value);
|
|
113
|
+
index += 1;
|
|
114
|
+
}
|
|
115
|
+
else if (arg.startsWith("--token=")) {
|
|
116
|
+
raw.push(arg.slice("--token=".length));
|
|
117
|
+
}
|
|
118
|
+
else if (arg === "--tokens") {
|
|
119
|
+
const value = argv[index + 1];
|
|
120
|
+
if (!value || value.startsWith("-")) {
|
|
121
|
+
throw new TokenFileArgError("--tokens requires a comma-separated list.");
|
|
122
|
+
}
|
|
123
|
+
raw.push(...value.split(","));
|
|
124
|
+
index += 1;
|
|
125
|
+
}
|
|
126
|
+
else if (arg.startsWith("--tokens=")) {
|
|
127
|
+
raw.push(...arg.slice("--tokens=".length).split(","));
|
|
128
|
+
}
|
|
129
|
+
else if (arg.startsWith("-")) {
|
|
130
|
+
continue; // --json and other global flags are handled by the caller.
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
raw.push(arg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (outPath === null) {
|
|
137
|
+
throw new TokenFileArgError("--out <path> is required (use --out - for stdout).");
|
|
138
|
+
}
|
|
139
|
+
if (merge && outPath === "-") {
|
|
140
|
+
throw new TokenFileArgError("--merge needs a file path; it cannot append to --out -.");
|
|
141
|
+
}
|
|
142
|
+
const addresses = [];
|
|
143
|
+
const labels = new Map();
|
|
144
|
+
const warnings = [];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
for (const value of raw) {
|
|
147
|
+
const candidate = value.trim();
|
|
148
|
+
if (!candidate)
|
|
149
|
+
continue;
|
|
150
|
+
const entry = parseEntry(candidate);
|
|
151
|
+
// Duplicates keep the first position but the last label, and a differing
|
|
152
|
+
// label is reported rather than silently dropped.
|
|
153
|
+
rememberTokenLabel(entry, labels, warnings);
|
|
154
|
+
const key = entry.address.toLowerCase();
|
|
155
|
+
if (seen.has(key))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(key);
|
|
158
|
+
addresses.push(entry.address);
|
|
159
|
+
}
|
|
160
|
+
if (addresses.length === 0) {
|
|
161
|
+
throw new TokenFileArgError("at least one token address is required.");
|
|
162
|
+
}
|
|
163
|
+
const given = addresses.map((address) => ({
|
|
164
|
+
address,
|
|
165
|
+
label: labels.get(address.toLowerCase()) ?? null,
|
|
166
|
+
}));
|
|
167
|
+
let entries = given;
|
|
168
|
+
if (merge && existsSync(outPath)) {
|
|
169
|
+
let existing;
|
|
170
|
+
try {
|
|
171
|
+
existing = loadTokenList(outPath);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new TokenFileArgError(error instanceof Error ? error.message : String(error));
|
|
175
|
+
}
|
|
176
|
+
entries = mergeTokenEntries(existing, given, warnings);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
addresses: entries.map((entry) => entry.address),
|
|
180
|
+
entries,
|
|
181
|
+
outPath,
|
|
182
|
+
force,
|
|
183
|
+
merge,
|
|
184
|
+
maxSubCalls,
|
|
185
|
+
warnings,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The written shape: a checksummed address string when there is no label, and an
|
|
190
|
+
* `{ address, label }` object when there is — both forms `--tokens-file` reads.
|
|
191
|
+
*/
|
|
192
|
+
export function tokenListItems(entries) {
|
|
193
|
+
return normalizeEntries(entries).map((entry) => entry.label === null ? entry.address : { address: entry.address, label: entry.label });
|
|
194
|
+
}
|
|
195
|
+
/** JSON array in the format `--tokens-file` reads, labels included. */
|
|
196
|
+
export function buildTokenListJson(entries) {
|
|
197
|
+
return `${JSON.stringify(tokenListItems(entries), null, 2)}\n`;
|
|
198
|
+
}
|
|
199
|
+
/** Write the list (or return it for "-"), never replacing a file without force. */
|
|
200
|
+
export function saveTokenList(outPath, entries, options = {}) {
|
|
201
|
+
const json = buildTokenListJson(entries);
|
|
202
|
+
if (outPath === "-")
|
|
203
|
+
return json;
|
|
204
|
+
if (!options.force && existsSync(outPath)) {
|
|
205
|
+
throw new TokenFileArgError(`${outPath} already exists; pass --force to replace it.`);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
209
|
+
const temporary = `${outPath}.tmp`;
|
|
210
|
+
writeFileSync(temporary, json);
|
|
211
|
+
renameSync(temporary, outPath);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
215
|
+
throw new TokenFileArgError(`cannot write ${outPath}: ${reason}`);
|
|
216
|
+
}
|
|
217
|
+
return json;
|
|
218
|
+
}
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Abi, type AbiFunction, type Address, type PublicClient } from "viem";
|
|
2
|
+
import { type CallArgError } from "./call.ts";
|
|
3
|
+
export declare class ViewsArgError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export type ViewsArgsSpec = {
|
|
7
|
+
kind: "array";
|
|
8
|
+
args: unknown[];
|
|
9
|
+
} | {
|
|
10
|
+
kind: "map";
|
|
11
|
+
args: Record<string, unknown[]>;
|
|
12
|
+
} | null;
|
|
13
|
+
export interface ViewsArgs {
|
|
14
|
+
address: Address;
|
|
15
|
+
/** null means no `--abi` was given, so the built-in ERC-20 ABI applies. */
|
|
16
|
+
abiPath: string | null;
|
|
17
|
+
spec: ViewsArgsSpec;
|
|
18
|
+
/** Block to read at, or "latest" to use the head at run time. */
|
|
19
|
+
block: "latest" | bigint;
|
|
20
|
+
/** Fixed sub-call budget per Multicall3 request, or null for one request. */
|
|
21
|
+
maxSubCalls: number | null;
|
|
22
|
+
}
|
|
23
|
+
export declare function parseViewsArgs(argv: string[]): ViewsArgs;
|
|
24
|
+
/** Every function in the ABI that can be called read-only, in ABI order. */
|
|
25
|
+
export declare function listViewFunctions(abi: Abi): AbiFunction[];
|
|
26
|
+
export type ViewCallStatus = "ok" | "error" | "skipped";
|
|
27
|
+
export interface ViewCallResult {
|
|
28
|
+
fn: AbiFunction;
|
|
29
|
+
signature: string;
|
|
30
|
+
status: ViewCallStatus;
|
|
31
|
+
/** Decoded result when status is "ok". */
|
|
32
|
+
value: unknown;
|
|
33
|
+
/** Failure message when status is "error". */
|
|
34
|
+
error: string | null;
|
|
35
|
+
/** Why the call was not attempted, when status is "skipped". */
|
|
36
|
+
reason: string | null;
|
|
37
|
+
args: unknown[];
|
|
38
|
+
}
|
|
39
|
+
export interface ViewsReport {
|
|
40
|
+
contract: Address;
|
|
41
|
+
/** Display source: the `--abi` path, or the built-in ABI label. */
|
|
42
|
+
abiPath: string;
|
|
43
|
+
blockNumber: bigint;
|
|
44
|
+
results: ViewCallResult[];
|
|
45
|
+
called: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
skipped: number;
|
|
48
|
+
/** Multicall3 requests sent; 1 without a cap, more when chunked. */
|
|
49
|
+
chunks: number;
|
|
50
|
+
maxSubCalls: number | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Call every view function once, packed into a single Multicall3 aggregate3
|
|
54
|
+
* request at one block. Functions that need arguments are skipped unless `spec`
|
|
55
|
+
* supplies them, a reverting getter is reported in its own row, and if a
|
|
56
|
+
* batch itself fails that group's calls are retried individually. A
|
|
57
|
+
* `maxSubCalls` cap splits the work across several Multicall3 requests.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runViewFunctions(client: PublicClient, contract: Address, abi: Abi, abiPath: string, spec?: ViewsArgsSpec, block?: "latest" | bigint, maxSubCalls?: number | null): Promise<ViewsReport>;
|
|
60
|
+
export declare function formatViews(report: ViewsReport): string;
|
|
61
|
+
export declare function toViewsJson(report: ViewsReport): Record<string, unknown>;
|
|
62
|
+
/** Exported only so callers can share the error type in their own checks. */
|
|
63
|
+
export type { CallArgError };
|
package/dist/views.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { decodeErrorResult, decodeFunctionResult, encodeFunctionData, getAddress, isAddress, } from "viem";
|
|
2
|
+
import { callViewFunction, coerceArgs, formatValue, serializeValue, signatureOf, } from "./call.js";
|
|
3
|
+
import { MAX_SUB_CALLS } from "./balances.js";
|
|
4
|
+
import { MULTICALL3_ADDRESS, multicall3BatchAbi } from "./contract.js";
|
|
5
|
+
/** Standard Solidity revert payloads, decoded so `revert("...")` keeps its reason. */
|
|
6
|
+
const revertErrorAbi = [
|
|
7
|
+
{ type: "error", name: "Error", inputs: [{ name: "message", type: "string" }] },
|
|
8
|
+
{ type: "error", name: "Panic", inputs: [{ name: "code", type: "uint256" }] },
|
|
9
|
+
];
|
|
10
|
+
export class ViewsArgError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ViewsArgError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function normalizeSignatureKey(value) {
|
|
17
|
+
return value.replace(/\s+/g, "");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A map entry may name the bare function or a full signature, so overloads are
|
|
21
|
+
* addressable. Signatures are matched first so `get(uint256)` beats a `get`
|
|
22
|
+
* entry regardless of key order, and whitespace in a key is ignored.
|
|
23
|
+
*/
|
|
24
|
+
function lookupMapArgs(args, fn) {
|
|
25
|
+
const signature = normalizeSignatureKey(signatureOf(fn));
|
|
26
|
+
const name = normalizeSignatureKey(fn.name);
|
|
27
|
+
for (const [key, value] of Object.entries(args)) {
|
|
28
|
+
if (normalizeSignatureKey(key) === signature)
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
for (const [key, value] of Object.entries(args)) {
|
|
32
|
+
if (normalizeSignatureKey(key) === name)
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function parseBlock(raw) {
|
|
38
|
+
if (raw === "latest")
|
|
39
|
+
return "latest";
|
|
40
|
+
if (!/^\d+$/.test(raw)) {
|
|
41
|
+
throw new ViewsArgError(`--block must be "latest" or a block number, got "${raw}".`);
|
|
42
|
+
}
|
|
43
|
+
return BigInt(raw);
|
|
44
|
+
}
|
|
45
|
+
function parseMaxSubCalls(raw) {
|
|
46
|
+
if (!/^\d+$/.test(raw)) {
|
|
47
|
+
throw new ViewsArgError(`--max-subcalls must be a whole number, got "${raw}".`);
|
|
48
|
+
}
|
|
49
|
+
const value = Number(raw);
|
|
50
|
+
if (value < 1 || value > MAX_SUB_CALLS) {
|
|
51
|
+
throw new ViewsArgError(`--max-subcalls must be between 1 and ${MAX_SUB_CALLS}.`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function parseArgSpec(raw) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new ViewsArgError(`--args is not valid JSON: ${raw}`);
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(parsed))
|
|
64
|
+
return { kind: "array", args: parsed };
|
|
65
|
+
if (parsed && typeof parsed === "object") {
|
|
66
|
+
const entries = Object.entries(parsed);
|
|
67
|
+
if (entries.length === 0)
|
|
68
|
+
return null;
|
|
69
|
+
for (const [name, value] of entries) {
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
throw new ViewsArgError(`--args.${name} must be an array of arguments.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { kind: "map", args: Object.fromEntries(entries) };
|
|
75
|
+
}
|
|
76
|
+
throw new ViewsArgError("--args must be a JSON array or an object of name → argument array.");
|
|
77
|
+
}
|
|
78
|
+
export function parseViewsArgs(argv) {
|
|
79
|
+
const positional = [];
|
|
80
|
+
let abiPath = null;
|
|
81
|
+
let spec = null;
|
|
82
|
+
let block = "latest";
|
|
83
|
+
let maxSubCalls = null;
|
|
84
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
85
|
+
const arg = argv[index] ?? "";
|
|
86
|
+
const readValue = (flag) => {
|
|
87
|
+
const value = argv[index + 1];
|
|
88
|
+
if (!value)
|
|
89
|
+
throw new ViewsArgError(`${flag} requires a value.`);
|
|
90
|
+
index += 1;
|
|
91
|
+
return value;
|
|
92
|
+
};
|
|
93
|
+
if (arg === "--abi" || arg.startsWith("--abi=")) {
|
|
94
|
+
abiPath = arg.startsWith("--abi=") ? arg.slice("--abi=".length) : readValue("--abi");
|
|
95
|
+
if (!abiPath)
|
|
96
|
+
throw new ViewsArgError("--abi requires a file path.");
|
|
97
|
+
}
|
|
98
|
+
else if (arg === "--args" || arg.startsWith("--args=")) {
|
|
99
|
+
const value = arg.startsWith("--args=") ? arg.slice("--args=".length) : readValue("--args");
|
|
100
|
+
if (!value)
|
|
101
|
+
throw new ViewsArgError("--args requires JSON.");
|
|
102
|
+
spec = parseArgSpec(value);
|
|
103
|
+
}
|
|
104
|
+
else if (arg === "--block" || arg.startsWith("--block=")) {
|
|
105
|
+
const value = arg.startsWith("--block=") ? arg.slice("--block=".length) : readValue("--block");
|
|
106
|
+
block = parseBlock(value);
|
|
107
|
+
}
|
|
108
|
+
else if (arg === "--max-subcalls") {
|
|
109
|
+
maxSubCalls = parseMaxSubCalls(readValue("--max-subcalls"));
|
|
110
|
+
}
|
|
111
|
+
else if (arg.startsWith("--max-subcalls=")) {
|
|
112
|
+
maxSubCalls = parseMaxSubCalls(arg.slice("--max-subcalls=".length));
|
|
113
|
+
}
|
|
114
|
+
else if (arg.startsWith("-")) {
|
|
115
|
+
// Other flags (e.g. --json) are handled by the caller.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
positional.push(arg);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const [addressRaw] = positional;
|
|
123
|
+
if (!addressRaw) {
|
|
124
|
+
throw new ViewsArgError("A contract address is required: lwc views <address> [--abi <file>]");
|
|
125
|
+
}
|
|
126
|
+
if (!isAddress(addressRaw)) {
|
|
127
|
+
throw new ViewsArgError(`"${addressRaw}" is not a valid EVM address.`);
|
|
128
|
+
}
|
|
129
|
+
if (positional.length > 1) {
|
|
130
|
+
throw new ViewsArgError(`Unexpected extra argument: ${positional[1]}.`);
|
|
131
|
+
}
|
|
132
|
+
return { address: getAddress(addressRaw), abiPath, spec, block, maxSubCalls };
|
|
133
|
+
}
|
|
134
|
+
/** Every function in the ABI that can be called read-only, in ABI order. */
|
|
135
|
+
export function listViewFunctions(abi) {
|
|
136
|
+
return abi.filter((item) => {
|
|
137
|
+
if (item.type !== "function")
|
|
138
|
+
return false;
|
|
139
|
+
const fn = item;
|
|
140
|
+
return (fn.stateMutability === "view" ||
|
|
141
|
+
fn.stateMutability === "pure" ||
|
|
142
|
+
(fn.stateMutability === undefined && fn.constant === true));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function describeError(error) {
|
|
146
|
+
if (error instanceof Error) {
|
|
147
|
+
const short = error.shortMessage;
|
|
148
|
+
if (typeof short === "string" && short.length > 0)
|
|
149
|
+
return short;
|
|
150
|
+
return error.message;
|
|
151
|
+
}
|
|
152
|
+
return String(error);
|
|
153
|
+
}
|
|
154
|
+
function resultOf(plan, status, value, error, reason) {
|
|
155
|
+
return {
|
|
156
|
+
fn: plan.fn,
|
|
157
|
+
signature: plan.signature,
|
|
158
|
+
status,
|
|
159
|
+
value,
|
|
160
|
+
error,
|
|
161
|
+
reason,
|
|
162
|
+
args: plan.args ?? plan.rawArgs,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Work out arguments and calldata before any network read, so a wide ABI is one batch. */
|
|
166
|
+
function planViewCall(fn, spec) {
|
|
167
|
+
const signature = signatureOf(fn);
|
|
168
|
+
const inputs = fn.inputs ?? [];
|
|
169
|
+
let rawArgs;
|
|
170
|
+
if (inputs.length === 0) {
|
|
171
|
+
rawArgs = [];
|
|
172
|
+
}
|
|
173
|
+
else if (spec === null) {
|
|
174
|
+
return {
|
|
175
|
+
fn,
|
|
176
|
+
signature,
|
|
177
|
+
args: null,
|
|
178
|
+
rawArgs: [],
|
|
179
|
+
callData: null,
|
|
180
|
+
skipReason: `needs ${inputs.length} argument(s)`,
|
|
181
|
+
prepError: null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
else if (spec.kind === "array") {
|
|
185
|
+
if (spec.args.length !== inputs.length) {
|
|
186
|
+
return {
|
|
187
|
+
fn,
|
|
188
|
+
signature,
|
|
189
|
+
args: null,
|
|
190
|
+
rawArgs: [],
|
|
191
|
+
callData: null,
|
|
192
|
+
skipReason: `needs ${inputs.length} argument(s), --args has ${spec.args.length}`,
|
|
193
|
+
prepError: null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
rawArgs = spec.args;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const entry = lookupMapArgs(spec.args, fn);
|
|
200
|
+
if (entry === undefined) {
|
|
201
|
+
return {
|
|
202
|
+
fn,
|
|
203
|
+
signature,
|
|
204
|
+
args: null,
|
|
205
|
+
rawArgs: [],
|
|
206
|
+
callData: null,
|
|
207
|
+
skipReason: `needs ${inputs.length} argument(s)`,
|
|
208
|
+
prepError: null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
rawArgs = entry;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const args = coerceArgs(fn, rawArgs);
|
|
215
|
+
const callData = encodeFunctionData({
|
|
216
|
+
abi: [fn],
|
|
217
|
+
functionName: fn.name,
|
|
218
|
+
args,
|
|
219
|
+
});
|
|
220
|
+
return { fn, signature, args, rawArgs, callData, skipReason: null, prepError: null };
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return {
|
|
224
|
+
fn,
|
|
225
|
+
signature,
|
|
226
|
+
args: null,
|
|
227
|
+
rawArgs,
|
|
228
|
+
callData: null,
|
|
229
|
+
skipReason: null,
|
|
230
|
+
prepError: describeError(error),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function revertMessage(data) {
|
|
235
|
+
if (!data || data === "0x")
|
|
236
|
+
return "execution reverted (no data)";
|
|
237
|
+
try {
|
|
238
|
+
const decoded = decodeErrorResult({ abi: revertErrorAbi, data });
|
|
239
|
+
const args = decoded.args;
|
|
240
|
+
if (decoded.errorName === "Error" && typeof args?.[0] === "string") {
|
|
241
|
+
return `execution reverted: ${args[0]}`;
|
|
242
|
+
}
|
|
243
|
+
if (decoded.errorName === "Panic" && typeof args?.[0] === "bigint") {
|
|
244
|
+
return `execution reverted: panic 0x${args[0].toString(16)}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Not a standard Error(string) or Panic(uint256) payload.
|
|
249
|
+
}
|
|
250
|
+
return `execution reverted (${data.length > 10 ? `${data.slice(0, 10)}…` : data})`;
|
|
251
|
+
}
|
|
252
|
+
function decodeReturn(plan, data) {
|
|
253
|
+
try {
|
|
254
|
+
const value = decodeFunctionResult({
|
|
255
|
+
abi: [plan.fn],
|
|
256
|
+
functionName: plan.fn.name,
|
|
257
|
+
data,
|
|
258
|
+
});
|
|
259
|
+
return resultOf(plan, "ok", value, null, null);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return resultOf(plan, "error", null, describeError(error), null);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* One eth_call: every callable function as an aggregate3 sub-call, pinned to
|
|
267
|
+
* the same block. Returns null when the batch cannot be used at all, so the
|
|
268
|
+
* caller can fall back to individual reads.
|
|
269
|
+
*/
|
|
270
|
+
async function callBatch(client, contract, plans, blockNumber) {
|
|
271
|
+
const calls = plans.map((plan) => ({
|
|
272
|
+
target: contract,
|
|
273
|
+
allowFailure: true,
|
|
274
|
+
callData: plan.callData,
|
|
275
|
+
}));
|
|
276
|
+
let results;
|
|
277
|
+
try {
|
|
278
|
+
results = (await client.readContract({
|
|
279
|
+
address: MULTICALL3_ADDRESS,
|
|
280
|
+
abi: multicall3BatchAbi,
|
|
281
|
+
functionName: "aggregate3",
|
|
282
|
+
args: [calls],
|
|
283
|
+
blockNumber,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (!Array.isArray(results) || results.length !== plans.length)
|
|
290
|
+
return null;
|
|
291
|
+
return plans.map((plan, index) => {
|
|
292
|
+
const result = results[index];
|
|
293
|
+
if (!result || result.success !== true) {
|
|
294
|
+
return resultOf(plan, "error", null, revertMessage(result?.returnData), null);
|
|
295
|
+
}
|
|
296
|
+
return decodeReturn(plan, result.returnData);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** Split calls into request-sized groups; one group when no cap is set. */
|
|
300
|
+
function chunkPlans(plans, maxSubCalls) {
|
|
301
|
+
if (plans.length === 0)
|
|
302
|
+
return [];
|
|
303
|
+
if (maxSubCalls === null)
|
|
304
|
+
return [plans];
|
|
305
|
+
const chunks = [];
|
|
306
|
+
for (let index = 0; index < plans.length; index += maxSubCalls) {
|
|
307
|
+
chunks.push(plans.slice(index, index + maxSubCalls));
|
|
308
|
+
}
|
|
309
|
+
return chunks;
|
|
310
|
+
}
|
|
311
|
+
async function callOneByOne(client, contract, abi, plans, blockNumber) {
|
|
312
|
+
return Promise.all(plans.map(async (plan) => {
|
|
313
|
+
try {
|
|
314
|
+
const outcome = await callViewFunction(client, contract, abi, plan.fn, plan.args ?? [], blockNumber);
|
|
315
|
+
return resultOf(plan, "ok", outcome.result, null, null);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return resultOf(plan, "error", null, describeError(error), null);
|
|
319
|
+
}
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Call every view function once, packed into a single Multicall3 aggregate3
|
|
324
|
+
* request at one block. Functions that need arguments are skipped unless `spec`
|
|
325
|
+
* supplies them, a reverting getter is reported in its own row, and if a
|
|
326
|
+
* batch itself fails that group's calls are retried individually. A
|
|
327
|
+
* `maxSubCalls` cap splits the work across several Multicall3 requests.
|
|
328
|
+
*/
|
|
329
|
+
export async function runViewFunctions(client, contract, abi, abiPath, spec = null, block = "latest", maxSubCalls = null) {
|
|
330
|
+
const planned = listViewFunctions(abi).map((fn) => planViewCall(fn, spec));
|
|
331
|
+
const blockNumber = block === "latest" ? await client.getBlockNumber() : block;
|
|
332
|
+
const callable = planned.filter((plan) => plan.callData !== null);
|
|
333
|
+
const batches = chunkPlans(callable, maxSubCalls);
|
|
334
|
+
const outcomes = [];
|
|
335
|
+
for (const batch of batches) {
|
|
336
|
+
outcomes.push(...((await callBatch(client, contract, batch, blockNumber)) ??
|
|
337
|
+
(await callOneByOne(client, contract, abi, batch, blockNumber))));
|
|
338
|
+
}
|
|
339
|
+
let next = 0;
|
|
340
|
+
const results = planned.map((plan) => {
|
|
341
|
+
if (plan.skipReason !== null)
|
|
342
|
+
return resultOf(plan, "skipped", null, null, plan.skipReason);
|
|
343
|
+
if (plan.prepError !== null)
|
|
344
|
+
return resultOf(plan, "error", null, plan.prepError, null);
|
|
345
|
+
const outcome = outcomes[next];
|
|
346
|
+
next += 1;
|
|
347
|
+
return outcome ?? resultOf(plan, "error", null, "no result returned for this call", null);
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
contract,
|
|
351
|
+
abiPath,
|
|
352
|
+
blockNumber,
|
|
353
|
+
results,
|
|
354
|
+
called: results.filter((result) => result.status === "ok").length,
|
|
355
|
+
failed: results.filter((result) => result.status === "error").length,
|
|
356
|
+
skipped: results.filter((result) => result.status === "skipped").length,
|
|
357
|
+
chunks: batches.length,
|
|
358
|
+
maxSubCalls,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export function formatViews(report) {
|
|
362
|
+
const width = Math.max(8, ...report.results.map((result) => result.signature.length));
|
|
363
|
+
const lines = [
|
|
364
|
+
"lwc — view calls",
|
|
365
|
+
"",
|
|
366
|
+
` ${"contract".padEnd(14)}${report.contract}`,
|
|
367
|
+
` ${"abi".padEnd(14)}${report.abiPath}`,
|
|
368
|
+
` ${"functions".padEnd(14)}${report.results.length} view/pure: ${report.called} called, ${report.failed} failed, ${report.skipped} skipped`,
|
|
369
|
+
` ${"block".padEnd(14)}${report.blockNumber}`,
|
|
370
|
+
];
|
|
371
|
+
if (report.maxSubCalls !== null) {
|
|
372
|
+
lines.push(` ${"requests".padEnd(14)}${report.chunks} multicall3 request${report.chunks === 1 ? "" : "s"}, max ${report.maxSubCalls} sub-calls`);
|
|
373
|
+
}
|
|
374
|
+
lines.push("");
|
|
375
|
+
for (const result of report.results) {
|
|
376
|
+
const detail = result.status === "ok"
|
|
377
|
+
? formatValue(result.value)
|
|
378
|
+
: result.status === "error"
|
|
379
|
+
? `error: ${result.error}`
|
|
380
|
+
: `skipped: ${result.reason}`;
|
|
381
|
+
lines.push(` ${result.signature.padEnd(width)} ${detail}`);
|
|
382
|
+
}
|
|
383
|
+
return lines.join("\n");
|
|
384
|
+
}
|
|
385
|
+
export function toViewsJson(report) {
|
|
386
|
+
return {
|
|
387
|
+
ok: report.failed === 0,
|
|
388
|
+
contract: report.contract,
|
|
389
|
+
abi: report.abiPath,
|
|
390
|
+
blockNumber: report.blockNumber.toString(),
|
|
391
|
+
called: report.called,
|
|
392
|
+
failed: report.failed,
|
|
393
|
+
skipped: report.skipped,
|
|
394
|
+
chunkCount: report.chunks,
|
|
395
|
+
maxSubCalls: report.maxSubCalls,
|
|
396
|
+
functions: report.results.map((result) => ({
|
|
397
|
+
name: result.fn.name,
|
|
398
|
+
signature: result.signature,
|
|
399
|
+
stateMutability: result.fn.stateMutability ?? null,
|
|
400
|
+
inputs: (result.fn.inputs ?? []).map((input) => ({
|
|
401
|
+
name: input.name && input.name.length > 0 ? input.name : null,
|
|
402
|
+
type: input.type,
|
|
403
|
+
})),
|
|
404
|
+
outputs: (result.fn.outputs ?? []).map((output) => ({
|
|
405
|
+
name: output.name && output.name.length > 0 ? output.name : null,
|
|
406
|
+
type: output.type,
|
|
407
|
+
})),
|
|
408
|
+
status: result.status,
|
|
409
|
+
args: serializeValue(result.args),
|
|
410
|
+
value: result.status === "ok" ? serializeValue(result.value) : null,
|
|
411
|
+
error: result.error,
|
|
412
|
+
reason: result.reason,
|
|
413
|
+
})),
|
|
414
|
+
};
|
|
415
|
+
}
|