@onrail-xyz/evm 1.0.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/src/query.ts ADDED
@@ -0,0 +1,257 @@
1
+ import type { Abi, AbiStateMutability, Address, Hex, PublicClient, ParseAbi,
2
+ ContractFunctionName, ContractFunctionArgs, ContractFunctionReturnType } from "viem";
3
+ import { multicall3Abi, encodeFunctionData, decodeFunctionResult, parseAbi } from "viem";
4
+ import type { RoUint8Array, RoArray, RoPair } from "@onrail-xyz/utils";
5
+ import { type MaybeArray, isArray,
6
+ isUint8Array, throwOnNullish, hex, unixTime } from "@onrail-xyz/utils";
7
+ import type { Layout, DeriveType } from "@onrail-xyz/binary-layout";
8
+ import { serialize, deserialize } from "@onrail-xyz/binary-layout";
9
+ import type { SigNameOf } from "./layouting.js";
10
+ import { sigNameOf } from "./layouting.js";
11
+
12
+ // ---- ABI string type utilities ----
13
+ //signatures omit the `function` keyword: "balanceOf(address) view returns (uint256)"
14
+
15
+ //viem couldn't be bothered to think about the order of their type parameters and what their users
16
+ // actually need, so we have to drag in AbiStateMutability when we don't care about it
17
+ type ViemResolve<A extends Abi, N extends ContractFunctionName<A>, T extends "params" | "return"> =
18
+ T extends "params"
19
+ ? ContractFunctionArgs<A, AbiStateMutability, N>
20
+ : ContractFunctionReturnType<A, AbiStateMutability, N>;
21
+
22
+ type AbiResolve<S extends string, T extends "params" | "return"> =
23
+ string extends S
24
+ ? T extends "params"
25
+ ? RoArray
26
+ : unknown
27
+ : ParseAbi<[`function ${S}`]> extends infer A extends Abi
28
+ ? SigNameOf<S> extends infer N extends ContractFunctionName<A>
29
+ ? ViemResolve<A, N, T>
30
+ : never
31
+ : never;
32
+
33
+ type AbiParameters<S extends string> = AbiResolve<S, "params">;
34
+
35
+ export type QueryAbiReturn<S extends string> = AbiResolve<S, "return">;
36
+
37
+ export type QueryAbiPair<S extends string = string> = RoPair<S, AbiParameters<S>>;
38
+
39
+ export type QueryLayoutTriple<CL extends Layout = Layout, RL extends Layout = Layout> =
40
+ readonly [CL, DeriveType<CL>, RL];
41
+
42
+ export type QueryCallData = RoUint8Array | QueryLayoutTriple | QueryAbiPair;
43
+
44
+ // ---- QueryCall ----
45
+
46
+ export type QueryCall<D extends QueryCallData = QueryCallData> =
47
+ { readonly to: Address; readonly data: D; readonly allowFailure?: boolean };
48
+
49
+ type _CheckCall<C> =
50
+ C extends { data: infer D }
51
+ ? D extends RoUint8Array
52
+ ? RoUint8Array
53
+ : D extends QueryLayoutTriple<infer CL extends Layout, infer RL extends Layout>
54
+ ? QueryLayoutTriple<CL, RL>
55
+ : D extends QueryAbiPair<infer S extends string>
56
+ ? QueryAbiPair<S>
57
+ : never
58
+ : never;
59
+
60
+ type CheckCall<C> = Omit<C, "data"> & { data: _CheckCall<C> };
61
+
62
+ type CheckCalls<A> =
63
+ A extends RoArray<QueryCall>
64
+ ? { [K in keyof A]: CheckCall<A[K]> }
65
+ : CheckCall<A>;
66
+
67
+ // ---- Result type mapping ----
68
+
69
+ type ResultOf<D> =
70
+ D extends RoUint8Array ? RoUint8Array :
71
+ D extends QueryLayoutTriple<Layout, infer RL extends Layout> ? DeriveType<RL> :
72
+ D extends QueryAbiPair<infer S extends string> ? QueryAbiReturn<S> :
73
+ never;
74
+
75
+ type WrappedResult<C extends QueryCall> =
76
+ { success: true; data: ResultOf<C["data"]> } | { success: false; data: RoUint8Array };
77
+
78
+ type ByAllowFailure<F, C extends QueryCall> =
79
+ F extends true ? WrappedResult<C> : ResultOf<C["data"]>; //intentional distribution
80
+
81
+ type CallResult<C> = C extends QueryCall ? ByAllowFailure<C["allowFailure"], C> : never;
82
+
83
+ export type QueryResult<A extends MaybeArray<QueryCall>> =
84
+ A extends RoArray<QueryCall>
85
+ ? { [K in keyof A]: CallResult<A[K]> }
86
+ : CallResult<A>;
87
+
88
+ export type QueryResultWithMeta<A extends MaybeArray<QueryCall>> =
89
+ [QueryResult<A>, bigint, RoUint8Array, Date];
90
+
91
+ // ---- Runtime encoding/decoding ----
92
+
93
+ const viemAbi = (sig: string) => ({
94
+ abi: parseAbi([`function ${sig}` as string]) as Abi,
95
+ functionName: sigNameOf(sig),
96
+ } as const);
97
+
98
+ const encodeCallData = (data: QueryCallData): Hex =>
99
+ !Array.isArray(data)
100
+ ? hex.encode(data as RoUint8Array, true)
101
+ : typeof data[0] === "string"
102
+ ? encodeFunctionData({ ...viemAbi(data[0]), args: data[1] })
103
+ : hex.encode(serialize(data[0], data[1]), true)
104
+
105
+ const decodeResult = (data: QueryCallData, returnData: Hex) =>
106
+ !Array.isArray(data)
107
+ ? hex.decode(returnData)
108
+ : typeof data[0] === "string"
109
+ ? decodeFunctionResult({ ...viemAbi(data[0]), data: returnData })
110
+ : deserialize(data[2], hex.decode(returnData));
111
+
112
+ const processResult = (
113
+ call: QueryCall,
114
+ raw: ViemResolve<typeof multicall3Abi, "aggregate3", "return">[number],
115
+ ) => {
116
+ const { success, returnData } = raw;
117
+ const data = success ? decodeResult(call.data, returnData) : hex.decode(returnData);
118
+
119
+ if (!raw.success && !call.allowFailure)
120
+ throw new Error(`Call to ${call.to} reverted: ${hex.encode(data as RoUint8Array, true)}`);
121
+
122
+ return call.allowFailure ? { success, data } : data;
123
+ };
124
+
125
+ // ---- createQuery ----
126
+
127
+ //Multicall3's deterministic deployment puts it at the same address on nearly every chain, but a
128
+ // few (zkSync and friends) deviate, hence the chain config takes precedence when it has an entry
129
+ const CANONICAL_MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11" as const;
130
+ const multicall3Of = (viemClient: PublicClient) =>
131
+ viemClient.chain?.contracts?.multicall3?.address ?? CANONICAL_MULTICALL3;
132
+
133
+ const encodeAggregate3 = (calls: RoArray<QueryCall>): Hex =>
134
+ encodeFunctionData({
135
+ abi: multicall3Abi,
136
+ functionName: "aggregate3",
137
+ args: [calls.map(c => ({
138
+ target: c.to,
139
+ allowFailure: true,
140
+ callData: encodeCallData(c.data),
141
+ }))],
142
+ });
143
+
144
+ async function aggregate3(viemClient: PublicClient, callData: Hex, blockHash: Hex) {
145
+ const returnData = await viemClient.request({
146
+ method: "eth_call",
147
+ params: [
148
+ { to: multicall3Of(viemClient), data: callData },
149
+ { blockHash, requireCanonical: true },
150
+ ],
151
+ });
152
+
153
+ return decodeFunctionResult({
154
+ abi: multicall3Abi,
155
+ functionName: "aggregate3",
156
+ data: returnData,
157
+ });
158
+ }
159
+
160
+ const MAX_RETRIES = 3;
161
+
162
+ export type BlockSpec<S extends "hash" | "ref" = "hash" | "ref"> =
163
+ S extends "hash"
164
+ ? RoUint8Array
165
+ : bigint | "latest" | "finalized";
166
+
167
+ //resolves a block tag or number to a canonical block hash, retrying whenever the batch does not
168
+ // come back, which is what a reorged-out block looks like from here
169
+ async function resolveBlock(
170
+ viemClient: PublicClient,
171
+ callData: Hex,
172
+ block: BlockSpec<"ref">,
173
+ ) {
174
+ const blockIdentifier = typeof block === "bigint" ? { blockNumber: block } : { blockTag: block };
175
+ let lastError: unknown;
176
+ for (let attempt = 0; attempt < MAX_RETRIES; ++attempt) {
177
+ const blockInfo = await viemClient.getBlock(blockIdentifier);
178
+
179
+ const blockHash = throwOnNullish(blockInfo.hash, "Block hash is null (pending block)");
180
+
181
+ const results = await aggregate3(viemClient, callData, blockHash)
182
+ .catch(error => { lastError = error; return undefined; });
183
+ if (results === undefined)
184
+ continue;
185
+
186
+ return {
187
+ results,
188
+ blockInfo: {
189
+ number: blockInfo.number,
190
+ hash: hex.decode(blockHash),
191
+ timestamp: unixTime.toDate(blockInfo.timestamp),
192
+ },
193
+ };
194
+ }
195
+ throw new Error(
196
+ `Querying canonical block failed ${MAX_RETRIES} times in a row`,
197
+ { cause: lastError },
198
+ );
199
+ }
200
+
201
+ const _query = async (
202
+ viemClient: PublicClient,
203
+ callS: MaybeArray<QueryCall>,
204
+ block: BlockSpec,
205
+ ) => {
206
+ const multi = isArray(callS);
207
+ const calls = multi ? callS : [callS];
208
+ //encoded before any block is resolved: a malformed argument is the caller's error, which the
209
+ // reorg retry must neither repeat nor report as a failed block
210
+ const callData = encodeAggregate3(calls);
211
+
212
+ return (isUint8Array(block)
213
+ ? aggregate3(viemClient, callData, hex.encode(block, true))
214
+ : resolveBlock(viemClient, callData, block)
215
+ ).then(raw => {
216
+ const [results, blockInfo] = "results" in raw
217
+ ? [raw.results, raw.blockInfo]
218
+ : [raw, undefined];
219
+
220
+ const result = multi
221
+ ? calls.map((c, i) => processResult(c, results[i]!))
222
+ : processResult(calls[0]!, results[0]!);
223
+
224
+ return blockInfo
225
+ ? [result, blockInfo.number, blockInfo.hash, blockInfo.timestamp]
226
+ : result;
227
+ });
228
+ };
229
+
230
+ export type Query = ReturnType<typeof createQuery>;
231
+
232
+ export const createQuery = (viemClient: PublicClient) => {
233
+ //query by blockhash -> only get actual result
234
+ function query<const A extends MaybeArray<QueryCall>>(
235
+ callS: A & CheckCalls<A>,
236
+ block: BlockSpec<"hash">,
237
+ ): Promise<QueryResult<A>>;
238
+ //query by block ref -> get result + meta
239
+ function query<const A extends MaybeArray<QueryCall>>(
240
+ callS: A & CheckCalls<A>,
241
+ block?: BlockSpec<"ref">, //default = "latest"
242
+ ): Promise<QueryResultWithMeta<A>>;
243
+ //convenience overload to allow both
244
+ function query<const A extends MaybeArray<QueryCall>>(
245
+ callS: A & CheckCalls<A>,
246
+ block: BlockSpec,
247
+ ): Promise<QueryResult<A> | QueryResultWithMeta<A>>;
248
+ //impl
249
+ function query<const A extends MaybeArray<QueryCall>>(
250
+ callS: A & CheckCalls<A>,
251
+ block: BlockSpec = "latest",
252
+ ) {
253
+ return _query(viemClient, callS, block);
254
+ }
255
+
256
+ return query;
257
+ };