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
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Address } from "viem";
|
|
2
|
+
import type { ChainConfig } from "./chain.ts";
|
|
3
|
+
import { MetadataCache } from "./metadata-cache.ts";
|
|
4
|
+
export declare class CacheArgError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface CacheArgs {
|
|
8
|
+
/** Token addresses whose metadata entries should be removed. */
|
|
9
|
+
clear: Address[];
|
|
10
|
+
}
|
|
11
|
+
export interface CacheEntryReport {
|
|
12
|
+
address: Address;
|
|
13
|
+
symbol: string;
|
|
14
|
+
decimals: number;
|
|
15
|
+
/** Epoch milliseconds when the entry was verified; null for legacy entries. */
|
|
16
|
+
cachedAt: number | null;
|
|
17
|
+
/** Age in milliseconds; null when the entry has no timestamp. */
|
|
18
|
+
ageMs: number | null;
|
|
19
|
+
}
|
|
20
|
+
export interface CacheReport {
|
|
21
|
+
chainId: number;
|
|
22
|
+
chainName: string;
|
|
23
|
+
path: string;
|
|
24
|
+
entries: CacheEntryReport[];
|
|
25
|
+
removed: CacheEntryReport[];
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
/** `--clear` takes one or more token addresses, repeated or comma-separated. */
|
|
29
|
+
export declare function parseCacheArgs(argv: string[]): CacheArgs;
|
|
30
|
+
/**
|
|
31
|
+
* List the current chain's cached metadata and remove the requested entries.
|
|
32
|
+
* Removal is in-memory; the caller saves the cache so a failed write never
|
|
33
|
+
* hides the report.
|
|
34
|
+
*/
|
|
35
|
+
export declare function inspectMetadataCache(cache: MetadataCache, config: ChainConfig, path: string, args: CacheArgs, now?: number): CacheReport;
|
|
36
|
+
export declare function formatMetadataCache(report: CacheReport): string;
|
|
37
|
+
export declare function toCacheJson(report: CacheReport): Record<string, unknown>;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getAddress, isAddress } from "viem";
|
|
2
|
+
import { formatCacheAge, shortAddress } from "./balances.js";
|
|
3
|
+
import { MetadataCache } from "./metadata-cache.js";
|
|
4
|
+
export class CacheArgError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CacheArgError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function addClearAddress(clear, raw) {
|
|
11
|
+
for (const part of raw.split(",")) {
|
|
12
|
+
const candidate = part.trim();
|
|
13
|
+
if (candidate.length === 0)
|
|
14
|
+
continue;
|
|
15
|
+
if (!isAddress(candidate)) {
|
|
16
|
+
throw new CacheArgError(`"${candidate}" is not a valid token address.`);
|
|
17
|
+
}
|
|
18
|
+
const address = getAddress(candidate);
|
|
19
|
+
if (!clear.includes(address))
|
|
20
|
+
clear.push(address);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** `--clear` takes one or more token addresses, repeated or comma-separated. */
|
|
24
|
+
export function parseCacheArgs(argv) {
|
|
25
|
+
const clear = [];
|
|
26
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
27
|
+
const arg = argv[index] ?? "";
|
|
28
|
+
if (arg === "--clear") {
|
|
29
|
+
const value = argv[index + 1];
|
|
30
|
+
if (!value)
|
|
31
|
+
throw new CacheArgError("--clear requires a token address.");
|
|
32
|
+
addClearAddress(clear, value);
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
else if (arg.startsWith("--clear=")) {
|
|
36
|
+
const value = arg.slice("--clear=".length);
|
|
37
|
+
if (!value)
|
|
38
|
+
throw new CacheArgError("--clear requires a token address.");
|
|
39
|
+
addClearAddress(clear, value);
|
|
40
|
+
}
|
|
41
|
+
else if (arg.startsWith("-")) {
|
|
42
|
+
// --json, --cache-file, and other global flags are handled by the CLI.
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new CacheArgError(`unexpected argument "${arg}"; the cache command takes no positional arguments.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { clear };
|
|
50
|
+
}
|
|
51
|
+
function entryReport(entry, now) {
|
|
52
|
+
return {
|
|
53
|
+
address: entry.address,
|
|
54
|
+
symbol: entry.symbol,
|
|
55
|
+
decimals: entry.decimals,
|
|
56
|
+
cachedAt: entry.cachedAt,
|
|
57
|
+
ageMs: entry.cachedAt === null ? null : Math.max(0, now - entry.cachedAt),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* List the current chain's cached metadata and remove the requested entries.
|
|
62
|
+
* Removal is in-memory; the caller saves the cache so a failed write never
|
|
63
|
+
* hides the report.
|
|
64
|
+
*/
|
|
65
|
+
export function inspectMetadataCache(cache, config, path, args, now = Date.now()) {
|
|
66
|
+
const warnings = [];
|
|
67
|
+
const removed = [];
|
|
68
|
+
const before = new Map(cache.list().map((entry) => [entry.address.toLowerCase(), entry]));
|
|
69
|
+
for (const address of args.clear) {
|
|
70
|
+
const entry = before.get(address.toLowerCase());
|
|
71
|
+
if (!entry || !cache.invalidate(address)) {
|
|
72
|
+
warnings.push(`token ${shortAddress(address)} is not in the ${config.name} metadata cache.`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
removed.push(entryReport(entry, now));
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
chainId: config.chainId,
|
|
79
|
+
chainName: config.name,
|
|
80
|
+
path,
|
|
81
|
+
entries: cache.list().map((entry) => entryReport(entry, now)),
|
|
82
|
+
removed,
|
|
83
|
+
warnings,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function formatMetadataCache(report) {
|
|
87
|
+
const lines = [
|
|
88
|
+
"lwc — cache",
|
|
89
|
+
"",
|
|
90
|
+
` ${"file".padEnd(14)}${report.path}`,
|
|
91
|
+
` ${"chain".padEnd(14)}${report.chainName} (${report.chainId})`,
|
|
92
|
+
` ${"entries".padEnd(14)}${report.entries.length}`,
|
|
93
|
+
];
|
|
94
|
+
if (report.removed.length > 0) {
|
|
95
|
+
lines.push(` ${"removed".padEnd(14)}${report.removed.length}`);
|
|
96
|
+
for (const entry of report.removed) {
|
|
97
|
+
lines.push(` ${shortAddress(entry.address)} ${entry.symbol} (${entry.decimals} decimals)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (report.entries.length === 0) {
|
|
101
|
+
lines.push("", " no cached token metadata for this chain");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lines.push("");
|
|
105
|
+
for (const entry of report.entries) {
|
|
106
|
+
const age = entry.ageMs === null ? "age unknown" : `cached ${formatCacheAge(entry.ageMs)}`;
|
|
107
|
+
lines.push(` ${"token".padEnd(14)}${shortAddress(entry.address)} ${entry.symbol} (${entry.decimals} decimals) ${age}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
export function toCacheJson(report) {
|
|
113
|
+
const entryJson = (entry) => ({
|
|
114
|
+
address: entry.address,
|
|
115
|
+
symbol: entry.symbol,
|
|
116
|
+
decimals: entry.decimals,
|
|
117
|
+
cachedAt: entry.cachedAt,
|
|
118
|
+
ageMs: entry.ageMs,
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
chain: { id: report.chainId, name: report.chainName },
|
|
123
|
+
cacheFile: report.path,
|
|
124
|
+
entries: report.entries.map(entryJson),
|
|
125
|
+
removed: report.removed.map(entryJson),
|
|
126
|
+
warnings: report.warnings,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/dist/call.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Abi, type AbiFunction, type Address, type PublicClient } from "viem";
|
|
2
|
+
export declare class CallArgError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export interface CallArgs {
|
|
6
|
+
address: Address;
|
|
7
|
+
functionName: string;
|
|
8
|
+
/** null means no `--abi` was given, so the built-in ERC-20 ABI applies. */
|
|
9
|
+
abiPath: string | null;
|
|
10
|
+
args: unknown[];
|
|
11
|
+
block: "latest" | bigint;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseCallArgs(argv: string[]): CallArgs;
|
|
14
|
+
export declare function extractAbi(raw: unknown): Abi;
|
|
15
|
+
export declare function loadAbi(path: string): Abi;
|
|
16
|
+
export interface ResolvedAbi {
|
|
17
|
+
abi: Abi;
|
|
18
|
+
/** The path given to `--abi`, or the built-in label when none was given. */
|
|
19
|
+
source: string;
|
|
20
|
+
}
|
|
21
|
+
/** `--abi` is optional: without it the built-in ERC-20 ABI is used. */
|
|
22
|
+
export declare function resolveAbi(abiPath: string | null): ResolvedAbi;
|
|
23
|
+
export declare function signatureOf(fn: AbiFunction): string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a function by bare name or by full signature. A bare name works only
|
|
26
|
+
* when the ABI has one candidate: overloads are never guessed from the first
|
|
27
|
+
* entry, since that silently calls the wrong function when argument types line
|
|
28
|
+
* up. `balanceOf(address)` and `balanceOf( address )` are equivalent.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findFunction(abi: Abi, nameOrSignature: string): AbiFunction;
|
|
31
|
+
export declare function coerceArgs(fn: AbiFunction, rawArgs: unknown[]): unknown[];
|
|
32
|
+
export interface CallOutcome {
|
|
33
|
+
blockNumber: bigint;
|
|
34
|
+
result: unknown;
|
|
35
|
+
}
|
|
36
|
+
export declare function callViewFunction(client: PublicClient, contract: Address, abi: Abi, fn: AbiFunction, args: unknown[], block: "latest" | bigint): Promise<CallOutcome>;
|
|
37
|
+
export declare function formatValue(value: unknown): string;
|
|
38
|
+
export interface CallDisplay {
|
|
39
|
+
contract: Address;
|
|
40
|
+
fn: AbiFunction;
|
|
41
|
+
blockNumber: bigint;
|
|
42
|
+
result: unknown;
|
|
43
|
+
}
|
|
44
|
+
export declare function formatCall(display: CallDisplay): string;
|
|
45
|
+
export declare function serializeValue(value: unknown): unknown;
|
|
46
|
+
export declare function toCallJson(display: CallDisplay): Record<string, unknown>;
|
package/dist/call.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { getAddress, isAddress, isHex, } from "viem";
|
|
3
|
+
import { DEFAULT_ABI_LABEL, DEFAULT_ERC20_ABI } from "./default-abi.js";
|
|
4
|
+
export class CallArgError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CallArgError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function parseBlock(raw) {
|
|
11
|
+
if (raw === "latest")
|
|
12
|
+
return "latest";
|
|
13
|
+
if (!/^\d+$/.test(raw)) {
|
|
14
|
+
throw new CallArgError(`--block must be "latest" or a block number, got "${raw}".`);
|
|
15
|
+
}
|
|
16
|
+
return BigInt(raw);
|
|
17
|
+
}
|
|
18
|
+
function parseArgsJson(raw) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new CallArgError(`--args is not valid JSON: ${raw}`);
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(parsed)) {
|
|
27
|
+
throw new CallArgError(`--args must be a JSON array, e.g. --args '["0xabc…"]'.`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
export function parseCallArgs(argv) {
|
|
32
|
+
const positional = [];
|
|
33
|
+
const singleArgs = [];
|
|
34
|
+
let abiPath = null;
|
|
35
|
+
let argsJson = null;
|
|
36
|
+
let block = "latest";
|
|
37
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
38
|
+
const arg = argv[index] ?? "";
|
|
39
|
+
const readValue = (flag) => {
|
|
40
|
+
const value = argv[index + 1];
|
|
41
|
+
if (!value)
|
|
42
|
+
throw new CallArgError(`${flag} requires a value.`);
|
|
43
|
+
index += 1;
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
if (arg === "--abi") {
|
|
47
|
+
abiPath = readValue("--abi");
|
|
48
|
+
}
|
|
49
|
+
else if (arg === "--args") {
|
|
50
|
+
argsJson = readValue("--args");
|
|
51
|
+
}
|
|
52
|
+
else if (arg === "--arg") {
|
|
53
|
+
singleArgs.push(readValue("--arg"));
|
|
54
|
+
}
|
|
55
|
+
else if (arg === "--block") {
|
|
56
|
+
block = parseBlock(readValue("--block"));
|
|
57
|
+
}
|
|
58
|
+
else if (arg.startsWith("-")) {
|
|
59
|
+
// Other flags (e.g. --json) are handled by the caller.
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
positional.push(arg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const [addressRaw, functionName] = positional;
|
|
67
|
+
if (!addressRaw) {
|
|
68
|
+
throw new CallArgError("A contract address is required: lwc call <address> <function> [--abi <file>]");
|
|
69
|
+
}
|
|
70
|
+
if (!isAddress(addressRaw)) {
|
|
71
|
+
throw new CallArgError(`"${addressRaw}" is not a valid EVM address.`);
|
|
72
|
+
}
|
|
73
|
+
if (!functionName) {
|
|
74
|
+
throw new CallArgError("A function name or signature is required: lwc call <address> <function|signature> [--abi <file>]");
|
|
75
|
+
}
|
|
76
|
+
if (positional.length > 2) {
|
|
77
|
+
throw new CallArgError(`Unexpected extra argument: ${positional[2]}.`);
|
|
78
|
+
}
|
|
79
|
+
if (argsJson !== null && singleArgs.length > 0) {
|
|
80
|
+
throw new CallArgError("Use either --args or --arg, not both.");
|
|
81
|
+
}
|
|
82
|
+
let args = [];
|
|
83
|
+
if (argsJson !== null) {
|
|
84
|
+
args = parseArgsJson(argsJson);
|
|
85
|
+
}
|
|
86
|
+
else if (singleArgs.length > 0) {
|
|
87
|
+
args = singleArgs.map((raw) => {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(raw);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Not JSON, so treat it as a literal string (e.g. 0xabc… or hello).
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { address: getAddress(addressRaw), functionName, abiPath, args, block };
|
|
98
|
+
}
|
|
99
|
+
export function extractAbi(raw) {
|
|
100
|
+
if (Array.isArray(raw))
|
|
101
|
+
return raw;
|
|
102
|
+
if (raw && typeof raw === "object" && Array.isArray(raw.abi)) {
|
|
103
|
+
return raw.abi;
|
|
104
|
+
}
|
|
105
|
+
throw new CallArgError('ABI file must be a JSON array of ABI items, or an object with an "abi" array.');
|
|
106
|
+
}
|
|
107
|
+
export function loadAbi(path) {
|
|
108
|
+
if (!existsSync(path)) {
|
|
109
|
+
throw new CallArgError(`ABI file not found: ${path}`);
|
|
110
|
+
}
|
|
111
|
+
let raw;
|
|
112
|
+
try {
|
|
113
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new CallArgError(`ABI file is not valid JSON: ${path}`);
|
|
117
|
+
}
|
|
118
|
+
return extractAbi(raw);
|
|
119
|
+
}
|
|
120
|
+
/** `--abi` is optional: without it the built-in ERC-20 ABI is used. */
|
|
121
|
+
export function resolveAbi(abiPath) {
|
|
122
|
+
if (abiPath === null)
|
|
123
|
+
return { abi: DEFAULT_ERC20_ABI, source: DEFAULT_ABI_LABEL };
|
|
124
|
+
return { abi: loadAbi(abiPath), source: abiPath };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Render an ABI parameter the way Solidity writes it: tuple components are
|
|
128
|
+
* expanded, and array suffixes are kept, so `f((uint256,address))` and
|
|
129
|
+
* `f((bool,bytes))` no longer both collapse to `f(tuple)`.
|
|
130
|
+
*/
|
|
131
|
+
function parameterType(param) {
|
|
132
|
+
const tuple = /^tuple(.*)$/.exec(param.type);
|
|
133
|
+
const components = param.components;
|
|
134
|
+
if (!tuple || components === undefined)
|
|
135
|
+
return param.type;
|
|
136
|
+
return `(${components.map(parameterType).join(",")})${tuple[1] ?? ""}`;
|
|
137
|
+
}
|
|
138
|
+
export function signatureOf(fn) {
|
|
139
|
+
return `${fn.name}(${(fn.inputs ?? []).map(parameterType).join(", ")})`;
|
|
140
|
+
}
|
|
141
|
+
function normalizeSignature(value) {
|
|
142
|
+
return value.replace(/\s+/g, "");
|
|
143
|
+
}
|
|
144
|
+
function availableFunctions(functions) {
|
|
145
|
+
const signatures = functions.map(signatureOf).sort();
|
|
146
|
+
return signatures.length > 0 ? signatures.join(", ") : "(none)";
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve a function by bare name or by full signature. A bare name works only
|
|
150
|
+
* when the ABI has one candidate: overloads are never guessed from the first
|
|
151
|
+
* entry, since that silently calls the wrong function when argument types line
|
|
152
|
+
* up. `balanceOf(address)` and `balanceOf( address )` are equivalent.
|
|
153
|
+
*/
|
|
154
|
+
export function findFunction(abi, nameOrSignature) {
|
|
155
|
+
const functions = abi.filter((item) => item.type === "function");
|
|
156
|
+
const selector = normalizeSignature(nameOrSignature);
|
|
157
|
+
let match;
|
|
158
|
+
if (selector.includes("(")) {
|
|
159
|
+
const exact = functions.find((fn) => normalizeSignature(signatureOf(fn)) === selector);
|
|
160
|
+
if (!exact) {
|
|
161
|
+
throw new CallArgError(`ABI has no function "${nameOrSignature}". Available: ${availableFunctions(functions)}.`);
|
|
162
|
+
}
|
|
163
|
+
match = exact;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const overloads = functions.filter((fn) => fn.name === selector);
|
|
167
|
+
if (overloads.length === 0) {
|
|
168
|
+
throw new CallArgError(`ABI has no function "${nameOrSignature}". Available: ${availableFunctions(functions)}.`);
|
|
169
|
+
}
|
|
170
|
+
if (overloads.length > 1) {
|
|
171
|
+
throw new CallArgError(`"${nameOrSignature}" is overloaded; pass a full signature: ${overloads
|
|
172
|
+
.map(signatureOf)
|
|
173
|
+
.sort()
|
|
174
|
+
.join(", ")}.`);
|
|
175
|
+
}
|
|
176
|
+
match = overloads[0];
|
|
177
|
+
}
|
|
178
|
+
const mutability = match.stateMutability;
|
|
179
|
+
if (mutability !== "view" && mutability !== "pure") {
|
|
180
|
+
throw new CallArgError(`${signatureOf(match)} is ${mutability}; only view and pure functions can be called read-only.`);
|
|
181
|
+
}
|
|
182
|
+
return match;
|
|
183
|
+
}
|
|
184
|
+
/** JSON.stringify throws on a bigint, which would hide the coercion error. */
|
|
185
|
+
function previewValue(value) {
|
|
186
|
+
if (typeof value === "bigint")
|
|
187
|
+
return `${value}n`;
|
|
188
|
+
try {
|
|
189
|
+
return previewValue(value) ?? String(value);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return String(value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function toBigInt(value, path, type) {
|
|
196
|
+
if (typeof value === "bigint")
|
|
197
|
+
return value;
|
|
198
|
+
if (typeof value === "number") {
|
|
199
|
+
if (!Number.isInteger(value)) {
|
|
200
|
+
throw new CallArgError(`${path} must be an integer for ${type}, got ${value}.`);
|
|
201
|
+
}
|
|
202
|
+
return BigInt(value);
|
|
203
|
+
}
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
if (/^-?\d+$/.test(trimmed))
|
|
207
|
+
return BigInt(trimmed);
|
|
208
|
+
if (/^0x[0-9a-fA-F]+$/.test(trimmed))
|
|
209
|
+
return BigInt(trimmed);
|
|
210
|
+
}
|
|
211
|
+
throw new CallArgError(`${path} must be an integer for ${type}, got ${previewValue(value)}.`);
|
|
212
|
+
}
|
|
213
|
+
function coerceValue(value, param, path) {
|
|
214
|
+
const type = param.type;
|
|
215
|
+
const arrayMatch = /^(.*)\[(\d*)\]$/.exec(type);
|
|
216
|
+
if (arrayMatch) {
|
|
217
|
+
const innerType = arrayMatch[1] ?? "";
|
|
218
|
+
const fixedLength = arrayMatch[2] ? Number(arrayMatch[2]) : null;
|
|
219
|
+
if (!Array.isArray(value)) {
|
|
220
|
+
throw new CallArgError(`${path} expects an array for type ${type}.`);
|
|
221
|
+
}
|
|
222
|
+
if (fixedLength !== null && value.length !== fixedLength) {
|
|
223
|
+
throw new CallArgError(`${path} expects ${fixedLength} items for type ${type}, got ${value.length}.`);
|
|
224
|
+
}
|
|
225
|
+
const innerParam = { ...param, type: innerType };
|
|
226
|
+
return value.map((item, index) => coerceValue(item, innerParam, `${path}[${index}]`));
|
|
227
|
+
}
|
|
228
|
+
if (type === "tuple") {
|
|
229
|
+
const components = param.components ?? [];
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
if (value.length !== components.length) {
|
|
232
|
+
throw new CallArgError(`${path} expects ${components.length} tuple fields, got ${value.length}.`);
|
|
233
|
+
}
|
|
234
|
+
return components.map((component, index) => coerceValue(value[index], component, `${path}.${component.name || index}`));
|
|
235
|
+
}
|
|
236
|
+
if (!value || typeof value !== "object") {
|
|
237
|
+
throw new CallArgError(`${path} expects an object or array for a tuple.`);
|
|
238
|
+
}
|
|
239
|
+
const record = value;
|
|
240
|
+
return Object.fromEntries(components.map((component, index) => {
|
|
241
|
+
const key = component.name && component.name.length > 0 ? component.name : String(index);
|
|
242
|
+
if (!(key in record)) {
|
|
243
|
+
throw new CallArgError(`${path} is missing tuple field "${key}".`);
|
|
244
|
+
}
|
|
245
|
+
return [key, coerceValue(record[key], component, `${path}.${key}`)];
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
if (/^u?int\d*$/.test(type)) {
|
|
249
|
+
const amount = toBigInt(value, path, type);
|
|
250
|
+
if (type.startsWith("u") && amount < 0n) {
|
|
251
|
+
throw new CallArgError(`${path} must not be negative for type ${type}.`);
|
|
252
|
+
}
|
|
253
|
+
return amount;
|
|
254
|
+
}
|
|
255
|
+
if (type === "bool") {
|
|
256
|
+
if (typeof value === "boolean")
|
|
257
|
+
return value;
|
|
258
|
+
if (value === "true" || value === 1 || value === "1")
|
|
259
|
+
return true;
|
|
260
|
+
if (value === "false" || value === 0 || value === "0")
|
|
261
|
+
return false;
|
|
262
|
+
throw new CallArgError(`${path} expects true or false, got ${previewValue(value)}.`);
|
|
263
|
+
}
|
|
264
|
+
if (type === "address") {
|
|
265
|
+
if (typeof value !== "string" || !isAddress(value)) {
|
|
266
|
+
throw new CallArgError(`${path} expects an EVM address, got ${previewValue(value)}.`);
|
|
267
|
+
}
|
|
268
|
+
return getAddress(value);
|
|
269
|
+
}
|
|
270
|
+
if (type.startsWith("bytes")) {
|
|
271
|
+
const size = type === "bytes" ? null : Number(type.slice(5));
|
|
272
|
+
if (typeof value !== "string" || !isHex(value)) {
|
|
273
|
+
throw new CallArgError(`${path} expects a 0x-prefixed hex string for ${type}.`);
|
|
274
|
+
}
|
|
275
|
+
if (size !== null && value.length !== size * 2 + 2) {
|
|
276
|
+
throw new CallArgError(`${path} expects ${size} bytes for ${type}.`);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
if (type === "string") {
|
|
281
|
+
if (typeof value === "string")
|
|
282
|
+
return value;
|
|
283
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
284
|
+
return String(value);
|
|
285
|
+
}
|
|
286
|
+
throw new CallArgError(`${path} expects a string, got ${previewValue(value)}.`);
|
|
287
|
+
}
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
export function coerceArgs(fn, rawArgs) {
|
|
291
|
+
const inputs = fn.inputs ?? [];
|
|
292
|
+
if (rawArgs.length !== inputs.length) {
|
|
293
|
+
throw new CallArgError(`${signatureOf(fn)} takes ${inputs.length} argument(s), got ${rawArgs.length}.`);
|
|
294
|
+
}
|
|
295
|
+
return inputs.map((input, index) => coerceValue(rawArgs[index], input, input.name && input.name.length > 0 ? input.name : `arg${index + 1}`));
|
|
296
|
+
}
|
|
297
|
+
export async function callViewFunction(client, contract, abi, fn, args, block) {
|
|
298
|
+
const result = await client.readContract({
|
|
299
|
+
address: contract,
|
|
300
|
+
abi,
|
|
301
|
+
functionName: fn.name,
|
|
302
|
+
args,
|
|
303
|
+
...(block === "latest" ? {} : { blockNumber: block }),
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
blockNumber: block === "latest" ? await client.getBlockNumber() : block,
|
|
307
|
+
result,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
export function formatValue(value) {
|
|
311
|
+
if (typeof value === "bigint")
|
|
312
|
+
return value.toString();
|
|
313
|
+
if (typeof value === "string")
|
|
314
|
+
return value;
|
|
315
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
316
|
+
return String(value);
|
|
317
|
+
if (value === null || value === undefined)
|
|
318
|
+
return "null";
|
|
319
|
+
if (Array.isArray(value))
|
|
320
|
+
return `[${value.map(formatValue).join(", ")}]`;
|
|
321
|
+
if (typeof value === "object") {
|
|
322
|
+
const entries = Object.entries(value).map(([key, item]) => `${key}: ${formatValue(item)}`);
|
|
323
|
+
return `{${entries.join(", ")}}`;
|
|
324
|
+
}
|
|
325
|
+
return String(value);
|
|
326
|
+
}
|
|
327
|
+
export function formatCall(display) {
|
|
328
|
+
const outputs = display.fn.outputs ?? [];
|
|
329
|
+
const values = Array.isArray(display.result) ? display.result : [display.result];
|
|
330
|
+
const lines = [
|
|
331
|
+
"lwc — contract call",
|
|
332
|
+
"",
|
|
333
|
+
` ${"contract".padEnd(14)}${display.contract}`,
|
|
334
|
+
` ${"function".padEnd(14)}${signatureOf(display.fn)}`,
|
|
335
|
+
` ${"block".padEnd(14)}${display.blockNumber}`,
|
|
336
|
+
"",
|
|
337
|
+
];
|
|
338
|
+
if (outputs.length <= 1) {
|
|
339
|
+
lines.push(` ${"result".padEnd(14)}${formatValue(values[0])}`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
outputs.forEach((output, index) => {
|
|
343
|
+
const label = output.name && output.name.length > 0 ? output.name : `[${index}]`;
|
|
344
|
+
lines.push(` ${label.padEnd(14)}${formatValue(values[index])}`);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
export function serializeValue(value) {
|
|
350
|
+
if (typeof value === "bigint")
|
|
351
|
+
return value.toString();
|
|
352
|
+
if (Array.isArray(value))
|
|
353
|
+
return value.map(serializeValue);
|
|
354
|
+
if (value && typeof value === "object") {
|
|
355
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
356
|
+
key,
|
|
357
|
+
serializeValue(item),
|
|
358
|
+
]));
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
export function toCallJson(display) {
|
|
363
|
+
const outputs = display.fn.outputs ?? [];
|
|
364
|
+
const values = Array.isArray(display.result) ? display.result : [display.result];
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
contract: display.contract,
|
|
368
|
+
function: { name: display.fn.name, signature: signatureOf(display.fn) },
|
|
369
|
+
blockNumber: display.blockNumber.toString(),
|
|
370
|
+
result: serializeValue(display.result),
|
|
371
|
+
outputs: outputs.map((output, index) => ({
|
|
372
|
+
name: output.name && output.name.length > 0 ? output.name : null,
|
|
373
|
+
type: output.type,
|
|
374
|
+
value: serializeValue(outputs.length === 1 ? display.result : values[index]),
|
|
375
|
+
})),
|
|
376
|
+
};
|
|
377
|
+
}
|
package/dist/chain.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type PublicClient } from "viem";
|
|
2
|
+
export interface ChainConfig {
|
|
3
|
+
chainId: number;
|
|
4
|
+
name: string;
|
|
5
|
+
rpcUrl: string;
|
|
6
|
+
nativeCurrency: {
|
|
7
|
+
name: string;
|
|
8
|
+
symbol: string;
|
|
9
|
+
decimals: number;
|
|
10
|
+
};
|
|
11
|
+
/** Authorization header value for the RPC, or null when unauthenticated. */
|
|
12
|
+
authHeader: string | null;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
/** Name of the built-in preset this config came from, or null for a custom chain. */
|
|
15
|
+
preset: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Endpoint to try when rpcUrl cannot answer. A preset supplies one, and
|
|
18
|
+
* LWC_FALLBACK_RPC_URL replaces it; null when there is none.
|
|
19
|
+
*/
|
|
20
|
+
fallbackRpcUrl?: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface ChainPreset {
|
|
23
|
+
chainId: number;
|
|
24
|
+
name: string;
|
|
25
|
+
rpcUrl: string;
|
|
26
|
+
/** Second public endpoint from a different operator, used when rpcUrl is down. */
|
|
27
|
+
fallbackRpcUrl: string;
|
|
28
|
+
nativeSymbol: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Built-in chains. A preset supplies the chain id, endpoint, and native
|
|
32
|
+
* currency; LWC_RPC_URL, LWC_CHAIN_NAME, and LWC_NATIVE_SYMBOL still override
|
|
33
|
+
* individual values, so a preset is a starting point rather than a lock-in.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CHAIN_PRESETS: Record<string, ChainPreset>;
|
|
36
|
+
export declare function chainPresetNames(): string[];
|
|
37
|
+
/** Find a preset by name (case-insensitive) or by its chain id. */
|
|
38
|
+
export declare function resolveChainPreset(selector: string): ChainPreset | null;
|
|
39
|
+
export interface ChainHealth {
|
|
40
|
+
chainId: number;
|
|
41
|
+
expectedChainId: number;
|
|
42
|
+
blockNumber: bigint;
|
|
43
|
+
blockTimestamp: bigint;
|
|
44
|
+
blockAgeSeconds: number;
|
|
45
|
+
gasPriceWei: bigint | null;
|
|
46
|
+
clientVersion: string | null;
|
|
47
|
+
latencyMs: number;
|
|
48
|
+
rpcHost: string;
|
|
49
|
+
}
|
|
50
|
+
export declare class ChainConfigError extends Error {
|
|
51
|
+
constructor(message: string);
|
|
52
|
+
}
|
|
53
|
+
export declare class ChainMismatchError extends Error {
|
|
54
|
+
readonly expected: number;
|
|
55
|
+
readonly actual: number;
|
|
56
|
+
readonly source: string;
|
|
57
|
+
constructor(expected: number, actual: number, source?: string);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the chain config from a preset name, a chain id, or explicit
|
|
61
|
+
* LWC_CHAIN_ID/LWC_RPC_URL values. An explicit selector (the --chain flag)
|
|
62
|
+
* wins over LWC_CHAIN.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseChainConfig(env: Record<string, string | undefined>, selector?: string | null): ChainConfig;
|
|
65
|
+
/**
|
|
66
|
+
* A log-safe endpoint label: origin plus path, with query, fragment, and any
|
|
67
|
+
* key-like path segment removed. Provider attribution needs the host, not the
|
|
68
|
+
* credential that happens to sit in the URL.
|
|
69
|
+
*/
|
|
70
|
+
export declare function publicEndpoint(rpcUrl: string): string;
|
|
71
|
+
/** Non-fatal advisories about the configured endpoint. */
|
|
72
|
+
export declare function connectionWarnings(config: ChainConfig): string[];
|
|
73
|
+
export declare function buildClient(config: ChainConfig): PublicClient;
|
|
74
|
+
/**
|
|
75
|
+
* Read live state from the endpoint and verify it is the chain we expect.
|
|
76
|
+
* Throws ChainMismatchError when the RPC reports a different chainId.
|
|
77
|
+
*/
|
|
78
|
+
export declare function probe(client: PublicClient, config: ChainConfig): Promise<ChainHealth>;
|
|
79
|
+
export interface ProbeOutcome {
|
|
80
|
+
health: ChainHealth;
|
|
81
|
+
/** The endpoint that answered: the backup when the primary was unhealthy. */
|
|
82
|
+
config: ChainConfig;
|
|
83
|
+
usedFallback: boolean;
|
|
84
|
+
/** Redacted primary endpoint, set only when the backup answered. */
|
|
85
|
+
primaryEndpoint: string | null;
|
|
86
|
+
/** Why the primary was skipped, set only when the backup answered. */
|
|
87
|
+
primaryError: string | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Probe the configured endpoint, falling back to the preset's second endpoint
|
|
91
|
+
* when the first cannot be used: unreachable, timing out, or answering with the
|
|
92
|
+
* wrong chain id. A configured auth header pins the probe to the primary, since
|
|
93
|
+
* sending someone's credentials to another operator would be a leak.
|
|
94
|
+
*/
|
|
95
|
+
export declare function probeWithFallback(clientFor: (config: ChainConfig) => PublicClient, config: ChainConfig): Promise<ProbeOutcome>;
|
|
96
|
+
export declare function formatHealth(health: ChainHealth, config: ChainConfig, warnings?: string[]): string;
|
|
97
|
+
export declare function toJsonPayload(health: ChainHealth, config: ChainConfig, warnings: string[], outcome?: Pick<ProbeOutcome, "usedFallback" | "primaryEndpoint" | "primaryError">): Record<string, unknown>;
|