@crewhaus/tool-chaincall 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 +264 -0
- package/dist/fixtures.d.ts +120 -0
- package/dist/fixtures.js +205 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1231 -0
- package/dist/lib/batch.d.ts +13 -0
- package/dist/lib/batch.js +55 -0
- package/dist/lib/codec.d.ts +49 -0
- package/dist/lib/codec.js +113 -0
- package/dist/lib/fees.d.ts +61 -0
- package/dist/lib/fees.js +113 -0
- package/dist/lib/hex.d.ts +56 -0
- package/dist/lib/hex.js +118 -0
- package/dist/lib/proxy.d.ts +99 -0
- package/dist/lib/proxy.js +152 -0
- package/dist/lib/rpc.d.ts +110 -0
- package/dist/lib/rpc.js +190 -0
- package/dist/lib/simulate.d.ts +68 -0
- package/dist/lib/simulate.js +154 -0
- package/package.json +47 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Aggregate3Result, Call3 } from "@crewhaus/tool-onchain";
|
|
2
|
+
import { type ChainRpc } from "./rpc";
|
|
3
|
+
export type { Aggregate3Result, Call3 };
|
|
4
|
+
/**
|
|
5
|
+
* Send one `aggregate3` and return the per-call rows.
|
|
6
|
+
*
|
|
7
|
+
* `block` is a tag or a quantity and is passed through verbatim; pinning it
|
|
8
|
+
* to a number is the caller's job, because only the caller knows whether it
|
|
9
|
+
* is sending one batch or five.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runAggregate3(rpc: ChainRpc, chainId: string, calls: ReadonlyArray<Call3>, multicall3Address: string, block: string, signal: AbortSignal, what: string): Promise<ReadonlyArray<Aggregate3Result>>;
|
|
12
|
+
/** Split into runs of at most `size`, preserving order. */
|
|
13
|
+
export declare function chunk<T>(items: ReadonlyArray<T>, size: number): T[][];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driving a Multicall3 batch: pack, one `eth_call`, unpack.
|
|
3
|
+
*
|
|
4
|
+
* The packing and unpacking are `@crewhaus/tool-onchain`'s
|
|
5
|
+
* `encodeAggregate3` / `decodeAggregate3` — this file adds only the round
|
|
6
|
+
* trip and the distinction the whole batch idea rests on:
|
|
7
|
+
*
|
|
8
|
+
* - a SUB-CALL that reverted is a row, with `success: false` and its
|
|
9
|
+
* revert bytes;
|
|
10
|
+
* - the BATCH failing is an exception, because there are then no rows at
|
|
11
|
+
* all and the caller's next move is different.
|
|
12
|
+
*
|
|
13
|
+
* `decodeAggregate3` already refuses a blob whose result count does not
|
|
14
|
+
* match the batch that was sent. That refusal is load-bearing: results are
|
|
15
|
+
* matched by position and nothing in them names the call they answer, so a
|
|
16
|
+
* short answer zipped against the calls sent attributes every result after
|
|
17
|
+
* the gap to the wrong contract.
|
|
18
|
+
*/
|
|
19
|
+
import { decodeAggregate3, encodeAggregate3 } from "@crewhaus/tool-onchain";
|
|
20
|
+
import { data as asData } from "./hex";
|
|
21
|
+
import { ChainCallError, isAbort, rpcErrorText, rpcRead } from "./rpc";
|
|
22
|
+
/**
|
|
23
|
+
* Send one `aggregate3` and return the per-call rows.
|
|
24
|
+
*
|
|
25
|
+
* `block` is a tag or a quantity and is passed through verbatim; pinning it
|
|
26
|
+
* to a number is the caller's job, because only the caller knows whether it
|
|
27
|
+
* is sending one batch or five.
|
|
28
|
+
*/
|
|
29
|
+
export async function runAggregate3(rpc, chainId, calls, multicall3Address, block, signal, what) {
|
|
30
|
+
const request = encodeAggregate3(calls, multicall3Address);
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = await rpcRead(rpc, chainId, "eth_call", [{ to: request.to, data: request.data }, block], signal);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// A cancellation passes through unchanged. Dressing it as "the batch
|
|
37
|
+
// failed" would send a caller looking for a missing Multicall3 deploy
|
|
38
|
+
// when what actually happened is that their deadline ran out.
|
|
39
|
+
if (isAbort(err, signal))
|
|
40
|
+
throw err;
|
|
41
|
+
// The aggregate call itself failing is not a sub-call reverting. It means
|
|
42
|
+
// the batch never ran: no Multicall3 at that address on this chain, a
|
|
43
|
+
// block the node has pruned, a gas cap the batch went past, or a sub-call
|
|
44
|
+
// with allowFailure:false taking the whole batch down with it.
|
|
45
|
+
throw new ChainCallError(`${what}: the Multicall3 batch itself failed at ${request.to} — ${rpcErrorText(err)}`, err);
|
|
46
|
+
}
|
|
47
|
+
return decodeAggregate3(asData(raw, `${what}: the batch's return data`), request.callCount);
|
|
48
|
+
}
|
|
49
|
+
/** Split into runs of at most `size`, preserving order. */
|
|
50
|
+
export function chunk(items, size) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (let i = 0; i < items.length; i += size)
|
|
53
|
+
out.push(items.slice(i, i + size));
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Encode a call from a signature and its arguments. Throws on a bad type. */
|
|
2
|
+
export declare function encodeCallData(signature: string, args: ReadonlyArray<unknown>, what: string): Promise<string>;
|
|
3
|
+
/** Decode return data against a list of ABI types. */
|
|
4
|
+
export declare function decodeValues(types: ReadonlyArray<string>, hex: string, what: string): Promise<unknown[]>;
|
|
5
|
+
export type CheckedAddress = {
|
|
6
|
+
/** EIP-55 mixed case, which is what goes into any output. */
|
|
7
|
+
readonly address: string;
|
|
8
|
+
readonly lowercase: string;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the input CARRIED a checksum this verified. An all-lowercase
|
|
11
|
+
* address is well-formed and unverifiable, and the difference matters
|
|
12
|
+
* enough that it travels with the answer rather than collapsing into
|
|
13
|
+
* "valid".
|
|
14
|
+
*/
|
|
15
|
+
readonly checksumVerified: boolean;
|
|
16
|
+
readonly isZero: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Validate an address through `tool-onchain`'s EIP-55 implementation. */
|
|
19
|
+
export declare function checkAddress(raw: string, what: string): Promise<CheckedAddress>;
|
|
20
|
+
/**
|
|
21
|
+
* Selectors this package builds calldata from, with the signature each one
|
|
22
|
+
* is the hash of. `lib.test.ts` recomputes all of them.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SELECTORS: Readonly<{
|
|
25
|
+
/** Multicall3, self-called inside a batch to learn the block it ran at. */
|
|
26
|
+
readonly getBlockNumber: "0x42cbb15c";
|
|
27
|
+
/** Multicall3, so a native balance can be read inside a batch. */
|
|
28
|
+
readonly getEthBalance: "0x4d2301cc";
|
|
29
|
+
/** ERC-20. */
|
|
30
|
+
readonly balanceOf: "0x70a08231";
|
|
31
|
+
/** ERC-165. */
|
|
32
|
+
readonly supportsInterface: "0x01ffc9a7";
|
|
33
|
+
/** What a transparent or UUPS proxy tends to expose as a view. */
|
|
34
|
+
readonly implementation: "0x5c60da1b";
|
|
35
|
+
/** EIP-1822's marker method; also what EIP-1967 UUPS proxies implement. */
|
|
36
|
+
readonly proxiableUUID: "0x52d1902d";
|
|
37
|
+
}>;
|
|
38
|
+
/** The signature each selector above is the Keccak prefix of. */
|
|
39
|
+
export declare const SELECTOR_SIGNATURES: Readonly<Record<keyof typeof SELECTORS, string>>;
|
|
40
|
+
/** Left-pad an address into a 32-byte argument word. */
|
|
41
|
+
export declare function addressArg(address: string): string;
|
|
42
|
+
/** Right-pad a `bytes4` into its 32-byte argument word. */
|
|
43
|
+
export declare function bytes4Arg(id: string): string;
|
|
44
|
+
/** `supportsInterface(interfaceId)` calldata. */
|
|
45
|
+
export declare function supportsInterfaceData(interfaceId: string): string;
|
|
46
|
+
/** `getEthBalance(account)` calldata, for Multicall3. */
|
|
47
|
+
export declare function getEthBalanceData(account: string): string;
|
|
48
|
+
/** `balanceOf(account)` calldata, for an ERC-20. */
|
|
49
|
+
export declare function balanceOfData(account: string): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ABI codec this package uses, which is somebody else's.
|
|
3
|
+
*
|
|
4
|
+
* `@crewhaus/tool-onchain` owns the encoder, the decoder, the EIP-55
|
|
5
|
+
* checksum and the Multicall3 packing. It publishes the first three as
|
|
6
|
+
* TOOLS rather than as functions — its module surface is `RegisteredTool`s
|
|
7
|
+
* plus the multicall helpers — so this file calls those tools' `execute`
|
|
8
|
+
* directly and parses the JSON they return.
|
|
9
|
+
*
|
|
10
|
+
* That indirection is deliberate and cheap. The alternative is a second ABI
|
|
11
|
+
* encoder living here, and a second encoder is a second set of tail offsets
|
|
12
|
+
* to get wrong: calldata with a misplaced offset is accepted by every node
|
|
13
|
+
* and misread by the contract, which is a wrong answer rather than an error.
|
|
14
|
+
* One encoder, one place it can be wrong, one place it gets fixed.
|
|
15
|
+
*
|
|
16
|
+
* The four-byte selectors below are constants because computing them needs
|
|
17
|
+
* Keccak, which this package does not depend on. They are not trusted on
|
|
18
|
+
* sight: `lib.test.ts` recomputes every one of them with `tool-onchain`'s
|
|
19
|
+
* own `FunctionSelector` and fails if a digit is off.
|
|
20
|
+
*/
|
|
21
|
+
import { abiDecode, abiEncodeCall, addressCheck } from "@crewhaus/tool-onchain";
|
|
22
|
+
import { ChainCallError } from "./rpc";
|
|
23
|
+
/** Read a tool result that is known to be a JSON string. */
|
|
24
|
+
async function toolJson(tool, input, what) {
|
|
25
|
+
const result = await tool.execute(input);
|
|
26
|
+
if (typeof result !== "string") {
|
|
27
|
+
throw new ChainCallError(`${what}: ${tool.name} returned non-text content`);
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(result);
|
|
30
|
+
}
|
|
31
|
+
/** Encode a call from a signature and its arguments. Throws on a bad type. */
|
|
32
|
+
export async function encodeCallData(signature, args, what) {
|
|
33
|
+
const out = await toolJson(abiEncodeCall, { signature, args }, what);
|
|
34
|
+
const encoded = out["data"];
|
|
35
|
+
if (typeof encoded !== "string")
|
|
36
|
+
throw new ChainCallError(`${what}: could not encode the call`);
|
|
37
|
+
return encoded;
|
|
38
|
+
}
|
|
39
|
+
/** Decode return data against a list of ABI types. */
|
|
40
|
+
export async function decodeValues(types, hex, what) {
|
|
41
|
+
const out = await toolJson(abiDecode, { data: hex, types }, what);
|
|
42
|
+
const values = out["values"];
|
|
43
|
+
if (!Array.isArray(values))
|
|
44
|
+
throw new ChainCallError(`${what}: could not decode the result`);
|
|
45
|
+
return values;
|
|
46
|
+
}
|
|
47
|
+
/** Validate an address through `tool-onchain`'s EIP-55 implementation. */
|
|
48
|
+
export async function checkAddress(raw, what) {
|
|
49
|
+
const out = await toolJson(addressCheck, { address: raw }, what);
|
|
50
|
+
if (out["valid"] !== true) {
|
|
51
|
+
throw new ChainCallError(`${what}: ${String(out["reason"] ?? "not a valid address")}`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
address: String(out["checksummed"]),
|
|
55
|
+
lowercase: String(out["lowercase"]),
|
|
56
|
+
checksumVerified: out["hadChecksum"] === true,
|
|
57
|
+
isZero: out["isZero"] === true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// selectors
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Selectors this package builds calldata from, with the signature each one
|
|
65
|
+
* is the hash of. `lib.test.ts` recomputes all of them.
|
|
66
|
+
*/
|
|
67
|
+
export const SELECTORS = Object.freeze({
|
|
68
|
+
/** Multicall3, self-called inside a batch to learn the block it ran at. */
|
|
69
|
+
getBlockNumber: "0x42cbb15c",
|
|
70
|
+
/** Multicall3, so a native balance can be read inside a batch. */
|
|
71
|
+
getEthBalance: "0x4d2301cc",
|
|
72
|
+
/** ERC-20. */
|
|
73
|
+
balanceOf: "0x70a08231",
|
|
74
|
+
/** ERC-165. */
|
|
75
|
+
supportsInterface: "0x01ffc9a7",
|
|
76
|
+
/** What a transparent or UUPS proxy tends to expose as a view. */
|
|
77
|
+
implementation: "0x5c60da1b",
|
|
78
|
+
/** EIP-1822's marker method; also what EIP-1967 UUPS proxies implement. */
|
|
79
|
+
proxiableUUID: "0x52d1902d",
|
|
80
|
+
});
|
|
81
|
+
/** The signature each selector above is the Keccak prefix of. */
|
|
82
|
+
export const SELECTOR_SIGNATURES = Object.freeze({
|
|
83
|
+
getBlockNumber: "getBlockNumber()",
|
|
84
|
+
getEthBalance: "getEthBalance(address)",
|
|
85
|
+
balanceOf: "balanceOf(address)",
|
|
86
|
+
supportsInterface: "supportsInterface(bytes4)",
|
|
87
|
+
implementation: "implementation()",
|
|
88
|
+
proxiableUUID: "proxiableUUID()",
|
|
89
|
+
});
|
|
90
|
+
/** Left-pad an address into a 32-byte argument word. */
|
|
91
|
+
export function addressArg(address) {
|
|
92
|
+
return address.replace(/^0x/, "").toLowerCase().padStart(64, "0");
|
|
93
|
+
}
|
|
94
|
+
/** Right-pad a `bytes4` into its 32-byte argument word. */
|
|
95
|
+
export function bytes4Arg(id) {
|
|
96
|
+
const body = id.replace(/^0x/, "").toLowerCase();
|
|
97
|
+
if (!/^[0-9a-f]{8}$/.test(body)) {
|
|
98
|
+
throw new ChainCallError(`an interface id is 0x followed by 8 hex digits, got "${id}"`);
|
|
99
|
+
}
|
|
100
|
+
return body.padEnd(64, "0");
|
|
101
|
+
}
|
|
102
|
+
/** `supportsInterface(interfaceId)` calldata. */
|
|
103
|
+
export function supportsInterfaceData(interfaceId) {
|
|
104
|
+
return `${SELECTORS.supportsInterface}${bytes4Arg(interfaceId)}`;
|
|
105
|
+
}
|
|
106
|
+
/** `getEthBalance(account)` calldata, for Multicall3. */
|
|
107
|
+
export function getEthBalanceData(account) {
|
|
108
|
+
return `${SELECTORS.getEthBalance}${addressArg(account)}`;
|
|
109
|
+
}
|
|
110
|
+
/** `balanceOf(account)` calldata, for an ERC-20. */
|
|
111
|
+
export function balanceOfData(account) {
|
|
112
|
+
return `${SELECTORS.balanceOf}${addressArg(account)}`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** `gasLimit / elasticity` is the gas target a block is measured against. */
|
|
2
|
+
export declare const DEFAULT_ELASTICITY = 2n;
|
|
3
|
+
/** The 1/8 the base fee may move by in one block. */
|
|
4
|
+
export declare const BASE_FEE_MAX_CHANGE_DENOMINATOR = 8n;
|
|
5
|
+
export type BaseFeeProjection = {
|
|
6
|
+
readonly next: bigint;
|
|
7
|
+
readonly target: bigint;
|
|
8
|
+
readonly direction: "up" | "down" | "flat";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The next block's base fee, by the EIP-1559 rule.
|
|
12
|
+
*
|
|
13
|
+
* Three cases and one floor. At exactly the target the fee is unchanged;
|
|
14
|
+
* above it the fee rises by `parent * excess / target / 8` with a minimum of
|
|
15
|
+
* one wei (the spec's `max(1, …)`, which is what stops a chain with a
|
|
16
|
+
* one-wei base fee from being stuck there through full blocks); below it the
|
|
17
|
+
* fee falls by the same expression with no such floor.
|
|
18
|
+
*
|
|
19
|
+
* A zero `gasLimit` — which only a malformed block has — returns the parent
|
|
20
|
+
* fee rather than dividing by zero.
|
|
21
|
+
*/
|
|
22
|
+
export declare function nextBaseFee(parentBaseFee: bigint, gasUsed: bigint, gasLimit: bigint, elasticity?: bigint, denominator?: bigint): BaseFeeProjection;
|
|
23
|
+
/**
|
|
24
|
+
* The median of a list of bigints, with the low middle chosen for an even
|
|
25
|
+
* count rather than an average.
|
|
26
|
+
*
|
|
27
|
+
* Averaging two adjacent samples invents a wei value that no block ever
|
|
28
|
+
* charged. For a fee percentile, "a tip this size was enough in half the
|
|
29
|
+
* sampled blocks" is a statement about an observed sample, and the lower of
|
|
30
|
+
* the two middles is still one of the observations.
|
|
31
|
+
*/
|
|
32
|
+
export declare function medianOf(values: ReadonlyArray<bigint>): bigint | undefined;
|
|
33
|
+
export type PercentileSummary = {
|
|
34
|
+
readonly percentile: number;
|
|
35
|
+
readonly medianWei: string;
|
|
36
|
+
readonly minWei: string;
|
|
37
|
+
readonly maxWei: string;
|
|
38
|
+
/** How many sampled blocks contributed a reward at this percentile. */
|
|
39
|
+
readonly samples: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Summarise one column of `feeHistory.reward`.
|
|
43
|
+
*
|
|
44
|
+
* The node computes each percentile per block; this reduces the column
|
|
45
|
+
* across the window. Blocks with no reward entry — an empty block has no
|
|
46
|
+
* transactions to take a percentile of — are counted out rather than read as
|
|
47
|
+
* a zero tip, because "nobody paid anything" and "nobody paid" are different
|
|
48
|
+
* claims and only one of them is true.
|
|
49
|
+
*/
|
|
50
|
+
export declare function summarisePercentile(percentile: number, column: ReadonlyArray<bigint>): PercentileSummary | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* How far the base fee moved across the window, in basis points of where it
|
|
53
|
+
* started. Reported instead of a "rising"/"falling" word because the word
|
|
54
|
+
* needs a threshold nobody agreed on, and one 1559 step is already 1250 bps.
|
|
55
|
+
*
|
|
56
|
+
* A decimal STRING, like every other figure derived from a wei value here. A
|
|
57
|
+
* chain whose base fee sits at a one-wei floor and then spikes produces a
|
|
58
|
+
* ratio past 2^53, and `Number()` turns that into `1e+34` — a value that is
|
|
59
|
+
* neither an integer nor the one that was computed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function changeBps(first: bigint, last: bigint): string | undefined;
|
package/dist/lib/fees.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The EIP-1559 base-fee rule, and the statistics over a fee-history window.
|
|
3
|
+
*
|
|
4
|
+
* All of it is integer arithmetic on `bigint`. That is not fastidiousness:
|
|
5
|
+
* `eth_feeHistory` returns `gasUsedRatio` as a JSON FLOAT, and computing the
|
|
6
|
+
* next base fee from that float is the obvious shortcut and the wrong answer
|
|
7
|
+
* — the ratio has already lost the low bits of `gasUsed`, and the rule
|
|
8
|
+
* multiplies a wei value by it. So the projection reads `gasUsed` and
|
|
9
|
+
* `gasLimit` off the block as integers and never touches the ratio.
|
|
10
|
+
*
|
|
11
|
+
* The other thing this file refuses to do is assume. Not every chain runs a
|
|
12
|
+
* 1559 market, and several that look like one do not follow the vanilla rule
|
|
13
|
+
* — a different elasticity, a different denominator, or a constant floor
|
|
14
|
+
* that never moves. So the projection is COMPARED against the value the node
|
|
15
|
+
* itself put at the end of `feeHistory.baseFeePerGas`, and a disagreement is
|
|
16
|
+
* reported rather than hidden behind whichever number came first.
|
|
17
|
+
*/
|
|
18
|
+
import { ChainCallError } from "./rpc";
|
|
19
|
+
/** `gasLimit / elasticity` is the gas target a block is measured against. */
|
|
20
|
+
export const DEFAULT_ELASTICITY = 2n;
|
|
21
|
+
/** The 1/8 the base fee may move by in one block. */
|
|
22
|
+
export const BASE_FEE_MAX_CHANGE_DENOMINATOR = 8n;
|
|
23
|
+
/**
|
|
24
|
+
* The next block's base fee, by the EIP-1559 rule.
|
|
25
|
+
*
|
|
26
|
+
* Three cases and one floor. At exactly the target the fee is unchanged;
|
|
27
|
+
* above it the fee rises by `parent * excess / target / 8` with a minimum of
|
|
28
|
+
* one wei (the spec's `max(1, …)`, which is what stops a chain with a
|
|
29
|
+
* one-wei base fee from being stuck there through full blocks); below it the
|
|
30
|
+
* fee falls by the same expression with no such floor.
|
|
31
|
+
*
|
|
32
|
+
* A zero `gasLimit` — which only a malformed block has — returns the parent
|
|
33
|
+
* fee rather than dividing by zero.
|
|
34
|
+
*/
|
|
35
|
+
export function nextBaseFee(parentBaseFee, gasUsed, gasLimit, elasticity = DEFAULT_ELASTICITY, denominator = BASE_FEE_MAX_CHANGE_DENOMINATOR) {
|
|
36
|
+
if (elasticity <= 0n || denominator <= 0n) {
|
|
37
|
+
throw new ChainCallError("the 1559 elasticity and denominator must both be positive");
|
|
38
|
+
}
|
|
39
|
+
const target = gasLimit / elasticity;
|
|
40
|
+
if (target === 0n)
|
|
41
|
+
return { next: parentBaseFee, target, direction: "flat" };
|
|
42
|
+
if (gasUsed === target)
|
|
43
|
+
return { next: parentBaseFee, target, direction: "flat" };
|
|
44
|
+
if (gasUsed > target) {
|
|
45
|
+
const raw = (parentBaseFee * (gasUsed - target)) / target / denominator;
|
|
46
|
+
const delta = raw > 0n ? raw : 1n;
|
|
47
|
+
return { next: parentBaseFee + delta, target, direction: "up" };
|
|
48
|
+
}
|
|
49
|
+
const delta = (parentBaseFee * (target - gasUsed)) / target / denominator;
|
|
50
|
+
const next = parentBaseFee - delta;
|
|
51
|
+
return { next: next > 0n ? next : 0n, target, direction: "down" };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The median of a list of bigints, with the low middle chosen for an even
|
|
55
|
+
* count rather than an average.
|
|
56
|
+
*
|
|
57
|
+
* Averaging two adjacent samples invents a wei value that no block ever
|
|
58
|
+
* charged. For a fee percentile, "a tip this size was enough in half the
|
|
59
|
+
* sampled blocks" is a statement about an observed sample, and the lower of
|
|
60
|
+
* the two middles is still one of the observations.
|
|
61
|
+
*/
|
|
62
|
+
export function medianOf(values) {
|
|
63
|
+
if (values.length === 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
66
|
+
return sorted[(sorted.length - 1) >> 1];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Summarise one column of `feeHistory.reward`.
|
|
70
|
+
*
|
|
71
|
+
* The node computes each percentile per block; this reduces the column
|
|
72
|
+
* across the window. Blocks with no reward entry — an empty block has no
|
|
73
|
+
* transactions to take a percentile of — are counted out rather than read as
|
|
74
|
+
* a zero tip, because "nobody paid anything" and "nobody paid" are different
|
|
75
|
+
* claims and only one of them is true.
|
|
76
|
+
*/
|
|
77
|
+
export function summarisePercentile(percentile, column) {
|
|
78
|
+
if (column.length === 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
const median = medianOf(column);
|
|
81
|
+
if (median === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
let min = column[0];
|
|
84
|
+
let max = column[0];
|
|
85
|
+
for (const value of column) {
|
|
86
|
+
if (value < min)
|
|
87
|
+
min = value;
|
|
88
|
+
if (value > max)
|
|
89
|
+
max = value;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
percentile,
|
|
93
|
+
medianWei: median.toString(),
|
|
94
|
+
minWei: min.toString(),
|
|
95
|
+
maxWei: max.toString(),
|
|
96
|
+
samples: column.length,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* How far the base fee moved across the window, in basis points of where it
|
|
101
|
+
* started. Reported instead of a "rising"/"falling" word because the word
|
|
102
|
+
* needs a threshold nobody agreed on, and one 1559 step is already 1250 bps.
|
|
103
|
+
*
|
|
104
|
+
* A decimal STRING, like every other figure derived from a wei value here. A
|
|
105
|
+
* chain whose base fee sits at a one-wei floor and then spikes produces a
|
|
106
|
+
* ratio past 2^53, and `Number()` turns that into `1e+34` — a value that is
|
|
107
|
+
* neither an integer nor the one that was computed.
|
|
108
|
+
*/
|
|
109
|
+
export function changeBps(first, last) {
|
|
110
|
+
if (first === 0n)
|
|
111
|
+
return undefined;
|
|
112
|
+
return (((last - first) * 10000n) / first).toString();
|
|
113
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a JSON-RPC QUANTITY as a bigint.
|
|
3
|
+
*
|
|
4
|
+
* Leading zeros are tolerated rather than refused: several providers emit
|
|
5
|
+
* `0x0000…` for a zero field, and rejecting a node's own formatting habit
|
|
6
|
+
* would fail a read that is perfectly well-defined. What is refused is
|
|
7
|
+
* anything that is not hex at all, a decimal string that a caller mistook
|
|
8
|
+
* for a quantity, and `0x` with no digits.
|
|
9
|
+
*/
|
|
10
|
+
export declare function quantity(value: unknown, what: string): bigint;
|
|
11
|
+
/** Same, but a missing or null field is an absent value rather than an error. */
|
|
12
|
+
export declare function optionalQuantity(value: unknown, what: string): bigint | undefined;
|
|
13
|
+
/** Render a bigint as a minimal QUANTITY, the form the wire wants. */
|
|
14
|
+
export declare function toQuantity(value: bigint): string;
|
|
15
|
+
/**
|
|
16
|
+
* Validate a 0x DATA string and lowercase it. `0x` passes: an empty return
|
|
17
|
+
* is the single most important value in this package, because it is what an
|
|
18
|
+
* address with no code answers, and treating it as an error loses the fact.
|
|
19
|
+
*/
|
|
20
|
+
export declare function data(value: unknown, what: string): string;
|
|
21
|
+
/** Byte count of a 0x DATA string. */
|
|
22
|
+
export declare function byteLength(hex: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* The 32-byte word at `index`, or undefined when the data is shorter.
|
|
25
|
+
*
|
|
26
|
+
* Returning undefined rather than a zero word is the point: a short answer
|
|
27
|
+
* that gets padded reads as a real zero, and "the balance is 0" is exactly
|
|
28
|
+
* the wrong thing to say about a call that did not return one.
|
|
29
|
+
*/
|
|
30
|
+
export declare function wordAt(hex: string, index: number): string | undefined;
|
|
31
|
+
/** A 32-byte word as an unsigned integer. */
|
|
32
|
+
export declare function wordToBigint(word: string): bigint;
|
|
33
|
+
/**
|
|
34
|
+
* The address in the low 20 bytes of a word, lowercased.
|
|
35
|
+
*
|
|
36
|
+
* The high 12 bytes must be zero. A storage slot whose top bytes are dirty
|
|
37
|
+
* is not an address that happens to have junk above it — it is a slot that
|
|
38
|
+
* holds something else, and silently masking it off would report a proxy's
|
|
39
|
+
* implementation as whatever the low bytes of a packed struct happen to be.
|
|
40
|
+
*/
|
|
41
|
+
export declare function wordToAddress(word: string, what: string): string;
|
|
42
|
+
/** True when a 32-byte word is all zeros: an unset slot. */
|
|
43
|
+
export declare function isZeroWord(word: string): boolean;
|
|
44
|
+
/** A bool return value: 32 bytes, zero for false, anything else for true. */
|
|
45
|
+
export declare function wordToBool(word: string): boolean;
|
|
46
|
+
/** The zero address, which is what an unset slot decodes to. */
|
|
47
|
+
export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
48
|
+
/**
|
|
49
|
+
* A block tag or number, as the wire wants it.
|
|
50
|
+
*
|
|
51
|
+
* A number arrives as a decimal STRING and leaves as a quantity: a block
|
|
52
|
+
* number past 2^53 is not a today problem, but the rule that no chain
|
|
53
|
+
* quantity passes through a JS number has no exceptions in this package,
|
|
54
|
+
* because the exceptions are what get copied.
|
|
55
|
+
*/
|
|
56
|
+
export declare function blockParam(blockNumber: string | undefined, blockTag: string | undefined): string;
|
package/dist/lib/hex.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex on the wire, integers in memory, decimal strings on the way out.
|
|
3
|
+
*
|
|
4
|
+
* JSON-RPC speaks two hex dialects and conflating them is a real bug class:
|
|
5
|
+
* a QUANTITY is minimal-length with no leading zeros (`0x1`), and DATA is a
|
|
6
|
+
* byte string with an even number of digits (`0x01`). `0x` is a legal empty
|
|
7
|
+
* DATA — it is what an `eth_call` to an address with no code returns — and
|
|
8
|
+
* is NOT a legal quantity.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here ever produces a JS `number` for a chain value. A uint256 is
|
|
11
|
+
* 78 decimal digits and a double carries about 15, so a balance read into a
|
|
12
|
+
* number is wrong in its low bits while still printing plausibly. Quantities
|
|
13
|
+
* come back as `bigint` and leave this package as decimal strings.
|
|
14
|
+
*/
|
|
15
|
+
import { ChainCallError } from "./rpc";
|
|
16
|
+
const LOOSE_QUANTITY = /^0x[0-9a-fA-F]+$/;
|
|
17
|
+
const DATA = /^0x([0-9a-fA-F]{2})*$/;
|
|
18
|
+
/**
|
|
19
|
+
* Read a JSON-RPC QUANTITY as a bigint.
|
|
20
|
+
*
|
|
21
|
+
* Leading zeros are tolerated rather than refused: several providers emit
|
|
22
|
+
* `0x0000…` for a zero field, and rejecting a node's own formatting habit
|
|
23
|
+
* would fail a read that is perfectly well-defined. What is refused is
|
|
24
|
+
* anything that is not hex at all, a decimal string that a caller mistook
|
|
25
|
+
* for a quantity, and `0x` with no digits.
|
|
26
|
+
*/
|
|
27
|
+
export function quantity(value, what) {
|
|
28
|
+
if (typeof value === "bigint")
|
|
29
|
+
return value;
|
|
30
|
+
if (typeof value !== "string" || !LOOSE_QUANTITY.test(value)) {
|
|
31
|
+
throw new ChainCallError(`${what}: expected a 0x-prefixed hex quantity, got ${JSON.stringify(value)}`);
|
|
32
|
+
}
|
|
33
|
+
return BigInt(value);
|
|
34
|
+
}
|
|
35
|
+
/** Same, but a missing or null field is an absent value rather than an error. */
|
|
36
|
+
export function optionalQuantity(value, what) {
|
|
37
|
+
return value === undefined || value === null ? undefined : quantity(value, what);
|
|
38
|
+
}
|
|
39
|
+
/** Render a bigint as a minimal QUANTITY, the form the wire wants. */
|
|
40
|
+
export function toQuantity(value) {
|
|
41
|
+
if (value < 0n)
|
|
42
|
+
throw new ChainCallError(`a JSON-RPC quantity cannot be negative, got ${value}`);
|
|
43
|
+
return `0x${value.toString(16)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate a 0x DATA string and lowercase it. `0x` passes: an empty return
|
|
47
|
+
* is the single most important value in this package, because it is what an
|
|
48
|
+
* address with no code answers, and treating it as an error loses the fact.
|
|
49
|
+
*/
|
|
50
|
+
export function data(value, what) {
|
|
51
|
+
if (typeof value !== "string" || !DATA.test(value.trim())) {
|
|
52
|
+
throw new ChainCallError(`${what}: expected 0x followed by an even number of hex digits, got ${JSON.stringify(value)}`);
|
|
53
|
+
}
|
|
54
|
+
return value.trim().toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
/** Byte count of a 0x DATA string. */
|
|
57
|
+
export function byteLength(hex) {
|
|
58
|
+
return (hex.length - 2) / 2;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The 32-byte word at `index`, or undefined when the data is shorter.
|
|
62
|
+
*
|
|
63
|
+
* Returning undefined rather than a zero word is the point: a short answer
|
|
64
|
+
* that gets padded reads as a real zero, and "the balance is 0" is exactly
|
|
65
|
+
* the wrong thing to say about a call that did not return one.
|
|
66
|
+
*/
|
|
67
|
+
export function wordAt(hex, index) {
|
|
68
|
+
const start = 2 + index * 64;
|
|
69
|
+
return hex.length >= start + 64 ? `0x${hex.slice(start, start + 64)}` : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** A 32-byte word as an unsigned integer. */
|
|
72
|
+
export function wordToBigint(word) {
|
|
73
|
+
return BigInt(word);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The address in the low 20 bytes of a word, lowercased.
|
|
77
|
+
*
|
|
78
|
+
* The high 12 bytes must be zero. A storage slot whose top bytes are dirty
|
|
79
|
+
* is not an address that happens to have junk above it — it is a slot that
|
|
80
|
+
* holds something else, and silently masking it off would report a proxy's
|
|
81
|
+
* implementation as whatever the low bytes of a packed struct happen to be.
|
|
82
|
+
*/
|
|
83
|
+
export function wordToAddress(word, what) {
|
|
84
|
+
const body = word.replace(/^0x/, "").toLowerCase();
|
|
85
|
+
if (body.length !== 64)
|
|
86
|
+
throw new ChainCallError(`${what}: expected a 32-byte word`);
|
|
87
|
+
if (!/^0{24}/.test(body)) {
|
|
88
|
+
throw new ChainCallError(`${what}: the top 12 bytes of ${word} are not zero, so this slot does not hold an address`);
|
|
89
|
+
}
|
|
90
|
+
return `0x${body.slice(24)}`;
|
|
91
|
+
}
|
|
92
|
+
/** True when a 32-byte word is all zeros: an unset slot. */
|
|
93
|
+
export function isZeroWord(word) {
|
|
94
|
+
return /^0x0{64}$/.test(word.toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
/** A bool return value: 32 bytes, zero for false, anything else for true. */
|
|
97
|
+
export function wordToBool(word) {
|
|
98
|
+
return !isZeroWord(word);
|
|
99
|
+
}
|
|
100
|
+
/** The zero address, which is what an unset slot decodes to. */
|
|
101
|
+
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
102
|
+
/**
|
|
103
|
+
* A block tag or number, as the wire wants it.
|
|
104
|
+
*
|
|
105
|
+
* A number arrives as a decimal STRING and leaves as a quantity: a block
|
|
106
|
+
* number past 2^53 is not a today problem, but the rule that no chain
|
|
107
|
+
* quantity passes through a JS number has no exceptions in this package,
|
|
108
|
+
* because the exceptions are what get copied.
|
|
109
|
+
*/
|
|
110
|
+
export function blockParam(blockNumber, blockTag) {
|
|
111
|
+
if (blockNumber !== undefined) {
|
|
112
|
+
if (!/^\d+$/.test(blockNumber.trim())) {
|
|
113
|
+
throw new ChainCallError(`blockNumber must be a decimal integer string, got ${JSON.stringify(blockNumber)}`);
|
|
114
|
+
}
|
|
115
|
+
return toQuantity(BigInt(blockNumber.trim()));
|
|
116
|
+
}
|
|
117
|
+
return blockTag ?? "latest";
|
|
118
|
+
}
|