@crewhaus/tool-chainread 0.7.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/README.md +396 -0
- package/dist/fixtures.d.ts +123 -0
- package/dist/fixtures.js +309 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +1302 -0
- package/dist/lib/blocks.d.ts +113 -0
- package/dist/lib/blocks.js +192 -0
- package/dist/lib/clock.d.ts +34 -0
- package/dist/lib/clock.js +55 -0
- package/dist/lib/endpoint.d.ts +91 -0
- package/dist/lib/endpoint.js +320 -0
- package/dist/lib/history.d.ts +196 -0
- package/dist/lib/history.js +663 -0
- package/dist/lib/logs.d.ts +79 -0
- package/dist/lib/logs.js +284 -0
- package/dist/lib/quantity.d.ts +69 -0
- package/dist/lib/quantity.js +120 -0
- package/dist/lib/rpc.d.ts +63 -0
- package/dist/lib/rpc.js +330 -0
- package/dist/lib/transfers.d.ts +134 -0
- package/dist/lib/transfers.js +231 -0
- package/package.json +48 -0
package/dist/lib/rpc.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One JSON-RPC request, done carefully, and the only way out of this package.
|
|
3
|
+
*
|
|
4
|
+
* Three properties the code below exists to hold:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Nothing here can send a transaction.** Every method name goes through
|
|
7
|
+
* `assertReadOnlyMethod` from `@crewhaus/chain-adapter-base` — the same
|
|
8
|
+
* allow-list the adapters use — before a socket is opened. That makes the
|
|
9
|
+
* "this package only reads" claim structural rather than a promise about
|
|
10
|
+
* the call sites: `eth_sendRawTransaction` throws here even if some
|
|
11
|
+
* future edit asks for it. `index.test.ts` asserts it.
|
|
12
|
+
* 2. **Failures are returned, not thrown.** A tool that polls, pages or
|
|
13
|
+
* probes several endpoints needs to branch on WHY a call failed — a
|
|
14
|
+
* halve-and-retry on a range cap is right, a halve-and-retry on a 401 is
|
|
15
|
+
* a denial-of-service against somebody's endpoint. `RpcOutcome` keeps
|
|
16
|
+
* those apart. The refusals a caller can fix (a bad URL, a write method)
|
|
17
|
+
* still throw, because they are not something to branch on.
|
|
18
|
+
* 3. **The URL never appears in an error.** Provider keys live in the path —
|
|
19
|
+
* `…/v2/<key>` on Alchemy, `…/v3/<key>` on Infura — so messages carry the
|
|
20
|
+
* origin only. An error that quotes the endpoint is how a key reaches a
|
|
21
|
+
* transcript.
|
|
22
|
+
*/
|
|
23
|
+
import { assertReadOnlyMethod } from "@crewhaus/chain-adapter-base";
|
|
24
|
+
import { vetEndpoint } from "./endpoint";
|
|
25
|
+
import { ChainReadError } from "./quantity";
|
|
26
|
+
export const DEFAULT_TIMEOUT_MS = 20_000;
|
|
27
|
+
export const MAX_TIMEOUT_MS = 120_000;
|
|
28
|
+
/**
|
|
29
|
+
* 16 MB. A single `eth_getLogs` page of ten thousand logs is a few megabytes;
|
|
30
|
+
* this is generous enough that no honest answer hits it and small enough that a
|
|
31
|
+
* broken endpoint streaming forever cannot pin the process.
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_MAX_BYTES = 16 * 1024 * 1024;
|
|
34
|
+
const USER_AGENT = "crewhaus-tool-chainread (+https://github.com/crewhaus/factory)";
|
|
35
|
+
/**
|
|
36
|
+
* Dial the vetted IP while keeping the real hostname for the Host header and
|
|
37
|
+
* TLS SNI, so virtual hosting and certificate validation still work. Mirrors
|
|
38
|
+
* `tool-containers`' dialer rather than re-deriving it: resolving in the guard
|
|
39
|
+
* and letting `fetch` re-resolve at connect time is the rebinding TOCTOU the
|
|
40
|
+
* guard exists to close.
|
|
41
|
+
*
|
|
42
|
+
* This function is the one place in the package a test never executes, because
|
|
43
|
+
* executing it means opening a socket.
|
|
44
|
+
*/
|
|
45
|
+
const pinnedFetch = async (req, pinnedIp) => {
|
|
46
|
+
const original = new URL(req.url);
|
|
47
|
+
const host = original.hostname;
|
|
48
|
+
const unbracketed = host.replace(/^\[/, "").replace(/\]$/, "");
|
|
49
|
+
if (pinnedIp === "" || unbracketed === pinnedIp)
|
|
50
|
+
return globalThis.fetch(req);
|
|
51
|
+
const pinnedUrl = new URL(original.toString());
|
|
52
|
+
pinnedUrl.hostname = pinnedIp.includes(":") ? `[${pinnedIp}]` : pinnedIp;
|
|
53
|
+
const headers = new Headers(req.headers);
|
|
54
|
+
headers.set("host", original.port === "" ? host : `${host}:${original.port}`);
|
|
55
|
+
// Re-read the body as a string rather than forwarding the Request's stream:
|
|
56
|
+
// a streaming body needs duplex support that runtimes disagree about, and a
|
|
57
|
+
// JSON-RPC envelope is a few hundred bytes.
|
|
58
|
+
const body = await req.text();
|
|
59
|
+
const init = {
|
|
60
|
+
method: req.method,
|
|
61
|
+
headers,
|
|
62
|
+
body,
|
|
63
|
+
signal: req.signal,
|
|
64
|
+
redirect: "manual",
|
|
65
|
+
tls: { serverName: host },
|
|
66
|
+
};
|
|
67
|
+
return globalThis.fetch(pinnedUrl.toString(), init);
|
|
68
|
+
};
|
|
69
|
+
let rpcFetch = pinnedFetch;
|
|
70
|
+
/**
|
|
71
|
+
* Test seam — `_setFetch(undefined)` restores the production dialer. A suite
|
|
72
|
+
* that sets it must restore it, or the next file in the same bun process
|
|
73
|
+
* inherits the stub.
|
|
74
|
+
*/
|
|
75
|
+
export function _setFetch(fn) {
|
|
76
|
+
rpcFetch = fn ?? pinnedFetch;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Vet the URL once, then reuse the verdict for every call on it.
|
|
80
|
+
*
|
|
81
|
+
* `EvmEventScan` issues dozens of requests against one endpoint and
|
|
82
|
+
* `EvmWaitForReceipt` issues one per poll; re-resolving the host each time
|
|
83
|
+
* would be a DNS query per call, and — worse — would let the answer change
|
|
84
|
+
* mid-scan, which is precisely the rebinding the pin defends against.
|
|
85
|
+
*/
|
|
86
|
+
export async function openRpc(rawUrl, options = {}) {
|
|
87
|
+
const endpoint = await vetEndpoint(rawUrl);
|
|
88
|
+
return clientFor(endpoint, options);
|
|
89
|
+
}
|
|
90
|
+
function clientFor(endpoint, options) {
|
|
91
|
+
const timeoutMs = Math.min(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
92
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
93
|
+
const origin = endpoint.url.origin;
|
|
94
|
+
let nextId = 1;
|
|
95
|
+
const client = {
|
|
96
|
+
origin,
|
|
97
|
+
calls: 0,
|
|
98
|
+
async call(method, params) {
|
|
99
|
+
assertReadOnlyMethod(origin, method);
|
|
100
|
+
client.calls += 1;
|
|
101
|
+
return request(endpoint, {
|
|
102
|
+
id: nextId++,
|
|
103
|
+
method,
|
|
104
|
+
params,
|
|
105
|
+
timeoutMs,
|
|
106
|
+
maxBytes,
|
|
107
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return client;
|
|
112
|
+
}
|
|
113
|
+
async function request(endpoint, spec) {
|
|
114
|
+
const origin = endpoint.url.origin;
|
|
115
|
+
const body = JSON.stringify({
|
|
116
|
+
jsonrpc: "2.0",
|
|
117
|
+
id: spec.id,
|
|
118
|
+
method: spec.method,
|
|
119
|
+
params: spec.params,
|
|
120
|
+
});
|
|
121
|
+
const timer = new AbortController();
|
|
122
|
+
const handle = setTimeout(() => timer.abort(new Error("deadline")), spec.timeoutMs);
|
|
123
|
+
const onOuterAbort = () => timer.abort();
|
|
124
|
+
spec.signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
125
|
+
try {
|
|
126
|
+
let res;
|
|
127
|
+
try {
|
|
128
|
+
res = await rpcFetch(new Request(endpoint.url.toString(), {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
accept: "application/json",
|
|
133
|
+
"user-agent": USER_AGENT,
|
|
134
|
+
},
|
|
135
|
+
body,
|
|
136
|
+
redirect: "manual",
|
|
137
|
+
signal: timer.signal,
|
|
138
|
+
}), endpoint.pinnedIp);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const error = err;
|
|
142
|
+
const aborted = error?.name === "AbortError" || timer.signal.aborted;
|
|
143
|
+
// The caller's own signal and our deadline both surface as an abort, and
|
|
144
|
+
// they are different situations: one means the run is shutting down, the
|
|
145
|
+
// other means this endpoint is slow. Only the second is worth retrying.
|
|
146
|
+
if (aborted && spec.signal?.aborted === true) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
kind: "timeout",
|
|
150
|
+
message: `the run was cancelled while ${origin} was answering ${spec.method}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (aborted) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
kind: "timeout",
|
|
157
|
+
message: `${origin} did not answer ${spec.method} within ${spec.timeoutMs}ms`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
kind: "transport",
|
|
163
|
+
message: `could not reach ${origin}: ${error?.message ?? String(err)}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// A redirect is not followed. An RPC endpoint that 302s is either
|
|
167
|
+
// misconfigured or pointing somewhere the guard never vetted, and the
|
|
168
|
+
// second is the whole reason `redirect: "manual"` is set.
|
|
169
|
+
if (res.status >= 300 && res.status < 400) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
kind: "status",
|
|
173
|
+
status: res.status,
|
|
174
|
+
message: `${origin} answered ${res.status} with a redirect — refusing to follow it to a host that was never vetted`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
let text;
|
|
178
|
+
try {
|
|
179
|
+
text = await readCapped(res, spec.maxBytes);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err instanceof BodyTooLarge) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
kind: "tooLarge",
|
|
186
|
+
message: `${origin} sent more than ${spec.maxBytes} bytes for ${spec.method} — refusing to parse a prefix of it`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
kind: "transport",
|
|
192
|
+
message: `${origin} closed the connection while sending ${spec.method}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (res.status < 200 || res.status >= 300) {
|
|
196
|
+
// Providers rate-limit with 429 and a JSON-RPC body, so the status is
|
|
197
|
+
// reported alongside whatever the body said rather than instead of it.
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
kind: "status",
|
|
201
|
+
status: res.status,
|
|
202
|
+
message: `${origin} answered HTTP ${res.status} for ${spec.method}: ${snippet(text)}`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
let parsed;
|
|
206
|
+
try {
|
|
207
|
+
parsed = JSON.parse(text);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
kind: "malformed",
|
|
213
|
+
message: `${origin} answered ${spec.method} with something that is not JSON: ${snippet(text)}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(parsed)) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
kind: "malformed",
|
|
220
|
+
message: `${origin} answered a single ${spec.method} request with a batch array`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
kind: "malformed",
|
|
227
|
+
message: `${origin} answered ${spec.method} with ${snippet(text)}, not a JSON-RPC envelope`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const envelope = parsed;
|
|
231
|
+
const rpcErr = envelope["error"];
|
|
232
|
+
if (rpcErr !== undefined && rpcErr !== null) {
|
|
233
|
+
const asObject = typeof rpcErr === "object" ? rpcErr : {};
|
|
234
|
+
const code = typeof asObject["code"] === "number" ? asObject["code"] : undefined;
|
|
235
|
+
// Capped here rather than where it is read. This string is the one field
|
|
236
|
+
// in the exchange the endpoint fills in freely, it arrives inside a body
|
|
237
|
+
// allowed up to `maxBytes`, and from here it goes into a refusal, a tool
|
|
238
|
+
// result and a model's context. A provider's longest real message is a
|
|
239
|
+
// sentence and a suggested range.
|
|
240
|
+
const message = typeof asObject["message"] === "string"
|
|
241
|
+
? capMessage(asObject["message"])
|
|
242
|
+
: snippet(text);
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
kind: "rpcError",
|
|
246
|
+
message,
|
|
247
|
+
...(code !== undefined ? { code } : {}),
|
|
248
|
+
...(asObject["data"] !== undefined ? { data: asObject["data"] } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (!("result" in envelope)) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
kind: "malformed",
|
|
255
|
+
message: `${origin} answered ${spec.method} with neither a result nor an error`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, result: envelope["result"] };
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
clearTimeout(handle);
|
|
262
|
+
spec.signal?.removeEventListener("abort", onOuterAbort);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
class BodyTooLarge extends Error {
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Read a body with a ceiling, WHILE reading.
|
|
269
|
+
*
|
|
270
|
+
* `await res.text()` would buffer whatever arrives first and check afterwards,
|
|
271
|
+
* which is no ceiling at all against an endpoint that streams. Stopping at the
|
|
272
|
+
* cap and refusing is also why the result is never parsed as a prefix: half a
|
|
273
|
+
* log array parses to a plausible SHORTER answer, which is the failure mode
|
|
274
|
+
* this whole package is organised against.
|
|
275
|
+
*/
|
|
276
|
+
async function readCapped(res, maxBytes) {
|
|
277
|
+
const body = res.body;
|
|
278
|
+
if (body === null)
|
|
279
|
+
return "";
|
|
280
|
+
const reader = body.getReader();
|
|
281
|
+
const chunks = [];
|
|
282
|
+
let total = 0;
|
|
283
|
+
try {
|
|
284
|
+
for (;;) {
|
|
285
|
+
const { done, value } = await reader.read();
|
|
286
|
+
if (done)
|
|
287
|
+
break;
|
|
288
|
+
if (value === undefined)
|
|
289
|
+
continue;
|
|
290
|
+
total += value.byteLength;
|
|
291
|
+
if (total > maxBytes)
|
|
292
|
+
throw new BodyTooLarge();
|
|
293
|
+
chunks.push(value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
reader.releaseLock();
|
|
298
|
+
}
|
|
299
|
+
const joined = new Uint8Array(total);
|
|
300
|
+
let offset = 0;
|
|
301
|
+
for (const chunk of chunks) {
|
|
302
|
+
joined.set(chunk, offset);
|
|
303
|
+
offset += chunk.byteLength;
|
|
304
|
+
}
|
|
305
|
+
return new TextDecoder().decode(joined);
|
|
306
|
+
}
|
|
307
|
+
/** The longest JSON-RPC error message this package will carry. */
|
|
308
|
+
export const MAX_ERROR_MESSAGE_CHARS = 2_000;
|
|
309
|
+
function capMessage(text) {
|
|
310
|
+
return text.length <= MAX_ERROR_MESSAGE_CHARS
|
|
311
|
+
? text
|
|
312
|
+
: `${text.slice(0, MAX_ERROR_MESSAGE_CHARS)}… (truncated from ${text.length} characters)`;
|
|
313
|
+
}
|
|
314
|
+
/** Enough of a body to recognise it, never enough to paste a secret into a transcript. */
|
|
315
|
+
function snippet(text) {
|
|
316
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
317
|
+
return flat.length > 200 ? `${flat.slice(0, 200)}…` : flat;
|
|
318
|
+
}
|
|
319
|
+
/** Turn a failed outcome into the refusal a tool throws, with the reason kept. */
|
|
320
|
+
export function failed(outcome, what) {
|
|
321
|
+
const code = outcome.code === undefined ? "" : ` (code ${outcome.code})`;
|
|
322
|
+
return new ChainReadError(`${what}: ${outcome.message}${code} [${outcome.kind}]`);
|
|
323
|
+
}
|
|
324
|
+
/** Call, or throw the refusal. For the steps where there is nothing to fall back to. */
|
|
325
|
+
export async function callOrThrow(client, method, params, what) {
|
|
326
|
+
const outcome = await client.call(method, params);
|
|
327
|
+
if (!outcome.ok)
|
|
328
|
+
throw failed(outcome, what);
|
|
329
|
+
return outcome.result;
|
|
330
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning one transaction into bookkeeping: what moved, who paid, and — the
|
|
3
|
+
* part most summaries get wrong — what is not visible from here at all.
|
|
4
|
+
*
|
|
5
|
+
* A receipt contains logs and nothing else. Native value that a CONTRACT sent
|
|
6
|
+
* during the call leaves no log; it is only visible in a trace, and
|
|
7
|
+
* `debug_traceTransaction` is not on any public endpoint's free tier and is not
|
|
8
|
+
* on this package's read-only allow-list. So the summary reports the top-level
|
|
9
|
+
* `value` and says, in the output and not only in the README, that internal
|
|
10
|
+
* transfers are excluded. A net delta that claims to be complete and is not is
|
|
11
|
+
* worse than no net delta: it reconciles.
|
|
12
|
+
*
|
|
13
|
+
* The event topics below are constants because computing them needs Keccak and
|
|
14
|
+
* this package has no hasher of its own. They are not TRUSTED as constants —
|
|
15
|
+
* `lib.test.ts` recomputes every one of them with `FunctionSelector` from
|
|
16
|
+
* `@crewhaus/tool-onchain` and fails if a digit is off. A mistyped topic is a
|
|
17
|
+
* filter that silently matches nothing, which is the same failure as a missing
|
|
18
|
+
* log.
|
|
19
|
+
*/
|
|
20
|
+
import type { RawLog } from "./logs";
|
|
21
|
+
/** `Transfer(address,address,uint256)` — ERC-20 and ERC-721 share it. */
|
|
22
|
+
export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
23
|
+
/** `Approval(address,address,uint256)`. */
|
|
24
|
+
export declare const APPROVAL_TOPIC = "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925";
|
|
25
|
+
/** `ApprovalForAll(address,address,bool)` — the blanket ERC-721/1155 approval. */
|
|
26
|
+
export declare const APPROVAL_FOR_ALL_TOPIC = "0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31";
|
|
27
|
+
/** `TransferSingle(address,address,address,uint256,uint256)` — ERC-1155. */
|
|
28
|
+
export declare const TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
|
|
29
|
+
/** The signatures the constants above are the Keccak of, so the test can recompute them. */
|
|
30
|
+
export declare const TOPIC_SIGNATURES: Readonly<Record<string, string>>;
|
|
31
|
+
/** The low 20 bytes of a 32-byte topic word, which is how an indexed address is stored. */
|
|
32
|
+
export declare function topicToAddress(topic: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* The inverse: an address as the 32-byte topic word a log filter matches on.
|
|
35
|
+
*
|
|
36
|
+
* Left-padded to 32 bytes and lowercased, because `eth_getLogs` compares topics
|
|
37
|
+
* as fixed-width words and an unpadded or mixed-case one matches nothing — a
|
|
38
|
+
* filter that silently returns an empty set, which reads exactly like an
|
|
39
|
+
* address that never moved anything.
|
|
40
|
+
*/
|
|
41
|
+
export declare function addressToTopic(address: string): string;
|
|
42
|
+
export type TokenMovement = {
|
|
43
|
+
readonly standard: "erc20" | "erc721" | "erc1155";
|
|
44
|
+
readonly token: string;
|
|
45
|
+
readonly from: string;
|
|
46
|
+
readonly to: string;
|
|
47
|
+
/** Base units for ERC-20, the token id for ERC-721. Always a decimal string. */
|
|
48
|
+
readonly amount: string;
|
|
49
|
+
readonly tokenId?: string;
|
|
50
|
+
readonly logIndex: number;
|
|
51
|
+
};
|
|
52
|
+
export type ApprovalEvent = {
|
|
53
|
+
readonly kind: "approval" | "approvalForAll";
|
|
54
|
+
readonly token: string;
|
|
55
|
+
readonly owner: string;
|
|
56
|
+
readonly spender: string;
|
|
57
|
+
/** For `approval`: the allowance in base units. `unlimited` marks the max-uint256 pattern. */
|
|
58
|
+
readonly amount?: string;
|
|
59
|
+
readonly unlimited?: boolean;
|
|
60
|
+
readonly approved?: boolean;
|
|
61
|
+
readonly logIndex: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* An ERC-20 transfer and an ERC-721 transfer are the same event with different
|
|
65
|
+
* indexing: the token id is indexed, the amount is not. So three topics means
|
|
66
|
+
* fungible and four means a specific NFT — the only signal there is, and
|
|
67
|
+
* reading a token id as an amount is how an NFT becomes "1e-18 of a token".
|
|
68
|
+
*/
|
|
69
|
+
export declare function decodeTransfer(log: RawLog): TokenMovement | null;
|
|
70
|
+
export declare function decodeApproval(log: RawLog): ApprovalEvent | null;
|
|
71
|
+
export type TransactionView = {
|
|
72
|
+
readonly hash: string;
|
|
73
|
+
readonly from: string;
|
|
74
|
+
readonly to: string | null;
|
|
75
|
+
readonly value: string;
|
|
76
|
+
readonly nonce: string;
|
|
77
|
+
readonly input: string;
|
|
78
|
+
readonly selector: string | null;
|
|
79
|
+
readonly gas: string | null;
|
|
80
|
+
readonly gasPrice: string | null;
|
|
81
|
+
readonly maxFeePerGas: string | null;
|
|
82
|
+
readonly maxPriorityFeePerGas: string | null;
|
|
83
|
+
readonly type: string | null;
|
|
84
|
+
readonly blockNumber: string | null;
|
|
85
|
+
readonly blockHash: string | null;
|
|
86
|
+
readonly contractCreation: boolean;
|
|
87
|
+
};
|
|
88
|
+
export declare function projectTransaction(raw: Record<string, unknown>): TransactionView;
|
|
89
|
+
/**
|
|
90
|
+
* What a receipt says happened.
|
|
91
|
+
*
|
|
92
|
+
* `status` has three values here, not two. `0x1` and `0x0` are the chain's
|
|
93
|
+
* answer; `unknown` is for a pre-Byzantium receipt, which carries a state
|
|
94
|
+
* `root` instead of a status and simply does not record whether the call
|
|
95
|
+
* reverted. Mapping that third case onto "failed" would invent a revert, and
|
|
96
|
+
* onto "succeeded" would hide one.
|
|
97
|
+
*/
|
|
98
|
+
export type ReceiptView = {
|
|
99
|
+
readonly status: "success" | "reverted" | "unknown";
|
|
100
|
+
readonly statusReason?: string;
|
|
101
|
+
readonly blockNumber: string;
|
|
102
|
+
readonly blockHash: string;
|
|
103
|
+
readonly transactionIndex: number | null;
|
|
104
|
+
readonly gasUsed: string;
|
|
105
|
+
readonly effectiveGasPrice: string | null;
|
|
106
|
+
readonly contractAddress: string | null;
|
|
107
|
+
readonly logs: ReadonlyArray<RawLog>;
|
|
108
|
+
};
|
|
109
|
+
export declare function projectReceipt(raw: Record<string, unknown>, projectLog: (value: unknown) => RawLog): ReceiptView;
|
|
110
|
+
export type FeeBreakdown = {
|
|
111
|
+
/** Which chain family's fee arithmetic was applied, inferred from the receipt's own fields. */
|
|
112
|
+
readonly model: "eip1559" | "op-stack" | "arbitrum" | "unknown";
|
|
113
|
+
readonly gasUsed: string;
|
|
114
|
+
readonly effectiveGasPrice: string | null;
|
|
115
|
+
readonly executionFeeWei: string | null;
|
|
116
|
+
/** The OP-stack L1 data fee, which is a separate charge and is NOT in gasUsed. */
|
|
117
|
+
readonly l1FeeWei: string | null;
|
|
118
|
+
readonly totalFeeWei: string | null;
|
|
119
|
+
readonly note: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* `gasUsed * effectiveGasPrice` is the whole fee on L1 and is WRONG on a
|
|
123
|
+
* rollup, in two different directions.
|
|
124
|
+
*
|
|
125
|
+
* OP-stack chains charge a separate L1 data fee that appears as `l1Fee` on the
|
|
126
|
+
* receipt and is not included in `gasUsed`; leaving it out understates the cost
|
|
127
|
+
* of a Base or Optimism transaction by most of the cost. Arbitrum folds its L1
|
|
128
|
+
* component INTO `gasUsed` and reports the share as `gasUsedForL1`, so the
|
|
129
|
+
* simple product is right there and adding anything would double-count.
|
|
130
|
+
*
|
|
131
|
+
* Both are detected from fields the receipt either has or does not, rather than
|
|
132
|
+
* from a chain id table that goes stale every time a new rollup ships.
|
|
133
|
+
*/
|
|
134
|
+
export declare function feeBreakdown(receipt: Record<string, unknown>, view: ReceiptView): FeeBreakdown;
|