@w3lib/evm-batch-call 0.1.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/LICENSE +24 -0
- package/README.md +257 -0
- package/contracts/DeploylessMulticall3.sol +66 -0
- package/dist/aggregate.d.ts +27 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +198 -0
- package/dist/aggregate.js.map +1 -0
- package/dist/batcher.d.ts +21 -0
- package/dist/batcher.d.ts.map +1 -0
- package/dist/batcher.js +100 -0
- package/dist/batcher.js.map +1 -0
- package/dist/call.d.ts +53 -0
- package/dist/call.d.ts.map +1 -0
- package/dist/call.js +82 -0
- package/dist/call.js.map +1 -0
- package/dist/contract.d.ts +45 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +107 -0
- package/dist/contract.js.map +1 -0
- package/dist/deployless.d.ts +3 -0
- package/dist/deployless.d.ts.map +1 -0
- package/dist/deployless.js +4 -0
- package/dist/deployless.js.map +1 -0
- package/dist/erc20.d.ts +27 -0
- package/dist/erc20.d.ts.map +1 -0
- package/dist/erc20.js +17 -0
- package/dist/erc20.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +56 -0
- package/dist/errors.js.map +1 -0
- package/dist/fallback.d.ts +23 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +86 -0
- package/dist/fallback.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/multicall.d.ts +13 -0
- package/dist/multicall.d.ts.map +1 -0
- package/dist/multicall.js +51 -0
- package/dist/multicall.js.map +1 -0
- package/dist/provider.d.ts +137 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +269 -0
- package/dist/provider.js.map +1 -0
- package/dist/rpcNodes.d.ts +5 -0
- package/dist/rpcNodes.d.ts.map +1 -0
- package/dist/rpcNodes.js +193 -0
- package/dist/rpcNodes.js.map +1 -0
- package/dist/source.d.ts +23 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +56 -0
- package/dist/source.js.map +1 -0
- package/dist/tron.d.ts +62 -0
- package/dist/tron.d.ts.map +1 -0
- package/dist/tron.js +223 -0
- package/dist/tron.js.map +1 -0
- package/package.json +63 -0
- package/src/aggregate.ts +254 -0
- package/src/batcher.ts +126 -0
- package/src/call.ts +144 -0
- package/src/contract.ts +145 -0
- package/src/deployless.ts +5 -0
- package/src/erc20.ts +43 -0
- package/src/errors.ts +66 -0
- package/src/fallback.ts +105 -0
- package/src/index.ts +31 -0
- package/src/multicall.ts +60 -0
- package/src/provider.ts +352 -0
- package/src/rpcNodes.ts +194 -0
- package/src/source.ts +84 -0
- package/src/tron.ts +281 -0
package/src/aggregate.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { AbiCoder, Interface, concat, dataSlice, id, isError, type Provider as EthersProvider } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import type { CallRequest, RawResult } from './call.js'
|
|
4
|
+
import { DEPLOYLESS_MULTICALL3_BYTECODE } from './deployless.js'
|
|
5
|
+
import { isExecutionError } from './errors.js'
|
|
6
|
+
import { MULTICALL3_ADDRESS, type Multicall } from './multicall.js'
|
|
7
|
+
|
|
8
|
+
/** 只用到 ethers Provider 的这两个方法,JsonRpcProvider / BrowserProvider 都满足 */
|
|
9
|
+
export type EthersLikeProvider = Pick<EthersProvider, 'call' | 'getBalance'>
|
|
10
|
+
|
|
11
|
+
export type BlockTag = number | bigint | string
|
|
12
|
+
|
|
13
|
+
export interface CallOverrides {
|
|
14
|
+
blockTag?: BlockTag
|
|
15
|
+
from?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const multicall3Interface = new Interface([
|
|
19
|
+
'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)',
|
|
20
|
+
'function getEthBalance(address addr) view returns (uint256 balance)',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const CALL3_TUPLE = 'tuple(address target, bool allowFailure, bytes callData)[]'
|
|
24
|
+
const RESULT_TUPLE = 'tuple(bool success, bytes returnData)[]'
|
|
25
|
+
/** error Aggregate3Result((bool,bytes)[]) */
|
|
26
|
+
const AGGREGATE3_RESULT_SELECTOR = id('Aggregate3Result((bool,bytes)[])').slice(0, 10)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 判定为无效的 multicall 地址(无代码 / 不是 Multicall3),按 chainId+address 记在模块级,
|
|
30
|
+
* 这样业务里每次 new Provider 也只会多试一次。
|
|
31
|
+
*/
|
|
32
|
+
const unusableMulticalls = new Set<string>()
|
|
33
|
+
|
|
34
|
+
export interface AggregateContext {
|
|
35
|
+
provider: EthersLikeProvider
|
|
36
|
+
chainId: number
|
|
37
|
+
/** null 表示直接走 deployless */
|
|
38
|
+
multicall: Multicall | null
|
|
39
|
+
chunkSize: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 执行一批请求,结果与 requests 一一对应。
|
|
44
|
+
* 有 multicall 合约就调合约的 aggregate3,否则(或合约不可用时)走 deployless。
|
|
45
|
+
* 超过 chunkSize 的请求会拆成多次 eth_call 并发执行(JsonRpcProvider 会把它们合到一个 HTTP batch 里)。
|
|
46
|
+
*/
|
|
47
|
+
export async function aggregate(
|
|
48
|
+
ctx: AggregateContext,
|
|
49
|
+
requests: CallRequest[],
|
|
50
|
+
overrides: CallOverrides = {},
|
|
51
|
+
): Promise<RawResult[]> {
|
|
52
|
+
if (requests.length === 0) {
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
const size = Math.max(1, ctx.chunkSize)
|
|
56
|
+
if (requests.length <= size) {
|
|
57
|
+
return aggregateChunk(ctx, requests, overrides)
|
|
58
|
+
}
|
|
59
|
+
const chunks: CallRequest[][] = []
|
|
60
|
+
for (let i = 0; i < requests.length; i += size) {
|
|
61
|
+
chunks.push(requests.slice(i, i + size))
|
|
62
|
+
}
|
|
63
|
+
const results = await Promise.all(chunks.map((chunk) => aggregateChunk(ctx, chunk, overrides)))
|
|
64
|
+
return results.flat()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function aggregateChunk(
|
|
68
|
+
ctx: AggregateContext,
|
|
69
|
+
requests: CallRequest[],
|
|
70
|
+
overrides: CallOverrides,
|
|
71
|
+
): Promise<RawResult[]> {
|
|
72
|
+
const multicall = ctx.multicall
|
|
73
|
+
if (multicall && isUsable(ctx.chainId, multicall, overrides.blockTag)) {
|
|
74
|
+
const results = await callContract(ctx, multicall, requests, overrides)
|
|
75
|
+
if (Array.isArray(results)) {
|
|
76
|
+
return results
|
|
77
|
+
}
|
|
78
|
+
// 只有“最新状态下地址上没有可用合约”才记缓存。
|
|
79
|
+
// - revert 无数据('reverted')有歧义:可能是 out of gas / 节点 gas 上限、子调用失败而钱包丢了 revert 数据,
|
|
80
|
+
// 这些情况地址本身是好的,只对这一次退回 deployless,不记缓存
|
|
81
|
+
// - 历史区块查到无代码可能只是那时还没部署,不能推及 latest
|
|
82
|
+
if (results === 'invalid' && isLatest(overrides.blockTag)) {
|
|
83
|
+
unusableMulticalls.add(cacheKey(ctx.chainId, multicall.address))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return callDeployless(ctx, requests, overrides)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 'invalid':地址上没有代码或返回无法解码;'reverted':执行 revert 且没有 revert 数据。
|
|
91
|
+
* 两种情况都退回 deployless。
|
|
92
|
+
*/
|
|
93
|
+
async function callContract(
|
|
94
|
+
ctx: AggregateContext,
|
|
95
|
+
multicall: Multicall,
|
|
96
|
+
requests: CallRequest[],
|
|
97
|
+
overrides: CallOverrides,
|
|
98
|
+
): Promise<RawResult[] | 'invalid' | 'reverted'> {
|
|
99
|
+
const calls = requests.map((req) => ({
|
|
100
|
+
target: req.ethBalanceOf ? multicall.address : req.target,
|
|
101
|
+
allowFailure: req.allowFailure,
|
|
102
|
+
callData: req.callData,
|
|
103
|
+
}))
|
|
104
|
+
let data: string
|
|
105
|
+
try {
|
|
106
|
+
data = await ctx.provider.call({
|
|
107
|
+
to: multicall.address,
|
|
108
|
+
data: multicall3Interface.encodeFunctionData('aggregate3', [calls]),
|
|
109
|
+
blockTag: overrides.blockTag,
|
|
110
|
+
from: overrides.from,
|
|
111
|
+
})
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// aggregate3 自己的失败(allowFailure=false 的子调用失败)会带 "Multicall3: call failed" 的 revert 数据;
|
|
114
|
+
// 执行层面 revert 且没有数据,基本就是这个地址上不是 Multicall3。
|
|
115
|
+
// 限流、节点缺状态等错误 ethers 也会包装成 CALL_EXCEPTION,那些要原样抛出,不能把地址判成无效
|
|
116
|
+
if (isExecutionError(err) && !hasRevertData(err)) {
|
|
117
|
+
return 'reverted'
|
|
118
|
+
}
|
|
119
|
+
throw err
|
|
120
|
+
}
|
|
121
|
+
// 地址上没有代码时 eth_call 返回 0x
|
|
122
|
+
if (!data || data === '0x') {
|
|
123
|
+
return 'invalid'
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
|
|
127
|
+
} catch {
|
|
128
|
+
return 'invalid'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* deployless:字节码 + 构造参数作为合约创建 eth_call 发出。
|
|
134
|
+
* 主币余额(ethBalanceOf)也在合约里用 BALANCE 读取,和其他调用在同一次请求里。
|
|
135
|
+
*
|
|
136
|
+
* 合约创建受 EIP-3860 限制,initcode(字节码 + 参数)不能超过 48KB,这里按编码后的大小切片;
|
|
137
|
+
* 返回超过 24KB 的情况由合约改用 revert 带回,见 contracts/DeploylessMulticall3.sol。
|
|
138
|
+
*/
|
|
139
|
+
async function callDeployless(
|
|
140
|
+
ctx: AggregateContext,
|
|
141
|
+
requests: CallRequest[],
|
|
142
|
+
overrides: CallOverrides,
|
|
143
|
+
): Promise<RawResult[]> {
|
|
144
|
+
const chunks = splitByInitcodeSize(requests)
|
|
145
|
+
const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides)))
|
|
146
|
+
return results.flat()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function callDeploylessChunk(
|
|
150
|
+
ctx: AggregateContext,
|
|
151
|
+
requests: CallRequest[],
|
|
152
|
+
overrides: CallOverrides,
|
|
153
|
+
): Promise<RawResult[]> {
|
|
154
|
+
const args = AbiCoder.defaultAbiCoder().encode(
|
|
155
|
+
[CALL3_TUPLE],
|
|
156
|
+
[
|
|
157
|
+
requests.map(({ target, allowFailure, callData, ethBalanceOf }) => ({
|
|
158
|
+
target: ethBalanceOf ? MULTICALL3_ADDRESS : target,
|
|
159
|
+
allowFailure,
|
|
160
|
+
callData,
|
|
161
|
+
})),
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
let data: string
|
|
165
|
+
try {
|
|
166
|
+
data = await ctx.provider.call({
|
|
167
|
+
data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]),
|
|
168
|
+
blockTag: overrides.blockTag,
|
|
169
|
+
from: overrides.from,
|
|
170
|
+
})
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const revertData = (err as { data?: string | null }).data
|
|
173
|
+
if (isError(err, 'CALL_EXCEPTION') && revertData?.startsWith(AGGREGATE3_RESULT_SELECTOR)) {
|
|
174
|
+
// 结果超过 24KB,合约通过 revert Aggregate3Result(...) 带回
|
|
175
|
+
return decodeAggregate3(AbiCoder.defaultAbiCoder().decode([RESULT_TUPLE], dataSlice(revertData, 4))[0])
|
|
176
|
+
}
|
|
177
|
+
// 有的钱包 / 中间层会丢掉 revert 数据,大结果拿不回来:对半拆开重试,小批量能正常 return
|
|
178
|
+
if (requests.length > 1 && isExecutionError(err) && !hasRevertData(err)) {
|
|
179
|
+
const mid = Math.ceil(requests.length / 2)
|
|
180
|
+
const [a, b] = await Promise.all([
|
|
181
|
+
callDeploylessChunk(ctx, requests.slice(0, mid), overrides),
|
|
182
|
+
callDeploylessChunk(ctx, requests.slice(mid), overrides),
|
|
183
|
+
])
|
|
184
|
+
return [...a, ...b]
|
|
185
|
+
}
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** EIP-3860 initcode 上限 49152 字节,留出余量 */
|
|
192
|
+
const MAX_INITCODE_SIZE = 48_000
|
|
193
|
+
const BYTECODE_SIZE = (DEPLOYLESS_MULTICALL3_BYTECODE.length - 2) / 2
|
|
194
|
+
|
|
195
|
+
function splitByInitcodeSize(requests: CallRequest[]): CallRequest[][] {
|
|
196
|
+
// 动态数组头 64 字节;每条 = 数组内偏移 32 + (target, allowFailure, bytes 偏移) 96 + bytes 长度 32 + 补齐后的数据
|
|
197
|
+
const budget = MAX_INITCODE_SIZE - BYTECODE_SIZE - 64
|
|
198
|
+
const chunks: CallRequest[][] = []
|
|
199
|
+
let current: CallRequest[] = []
|
|
200
|
+
let size = 0
|
|
201
|
+
for (const req of requests) {
|
|
202
|
+
const dataLength = (req.callData.length - 2) / 2
|
|
203
|
+
const itemSize = 160 + Math.ceil(dataLength / 32) * 32
|
|
204
|
+
if (current.length && size + itemSize > budget) {
|
|
205
|
+
chunks.push(current)
|
|
206
|
+
current = []
|
|
207
|
+
size = 0
|
|
208
|
+
}
|
|
209
|
+
current.push(req)
|
|
210
|
+
size += itemSize
|
|
211
|
+
}
|
|
212
|
+
if (current.length) {
|
|
213
|
+
chunks.push(current)
|
|
214
|
+
}
|
|
215
|
+
return chunks
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function hasRevertData(err: unknown): boolean {
|
|
219
|
+
const data = (err as { data?: string | null }).data
|
|
220
|
+
return Boolean(data && data !== '0x')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function decodeAggregate3(raw: ReadonlyArray<readonly [boolean, string]>): RawResult[] {
|
|
224
|
+
return raw.map(([success, returnData]) => ({ success, returnData }))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isUsable(chainId: number, multicall: Multicall, blockTag?: BlockTag): boolean {
|
|
228
|
+
if (unusableMulticalls.has(cacheKey(chainId, multicall.address))) {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
if (blockTag === 'earliest') {
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
if (typeof blockTag === 'number' || typeof blockTag === 'bigint') {
|
|
235
|
+
return BigInt(multicall.block) < BigInt(blockTag)
|
|
236
|
+
}
|
|
237
|
+
if (typeof blockTag === 'string' && /^0x[0-9a-f]+$/i.test(blockTag)) {
|
|
238
|
+
return BigInt(multicall.block) < BigInt(blockTag)
|
|
239
|
+
}
|
|
240
|
+
return true
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isLatest(blockTag?: BlockTag): boolean {
|
|
244
|
+
return blockTag === undefined || blockTag === null || blockTag === 'latest' || blockTag === 'pending'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function cacheKey(chainId: number, address: string): string {
|
|
248
|
+
return `${chainId}:${address.toLowerCase()}`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 测试用:清掉“地址不可用”的缓存 */
|
|
252
|
+
export function resetMulticallCache(): void {
|
|
253
|
+
unusableMulticalls.clear()
|
|
254
|
+
}
|
package/src/batcher.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { decodeCall, encodeCall, type Call, type CallRequest, type RawResult } from './call.js'
|
|
2
|
+
import type { CallOverrides } from './aggregate.js'
|
|
3
|
+
import { CallFailedError } from './errors.js'
|
|
4
|
+
|
|
5
|
+
export interface BatchOptions {
|
|
6
|
+
/** 收集窗口(毫秒),窗口内发起的 call 合并成一次 multicall。默认 0:同一事件循环 tick 内的调用合并 */
|
|
7
|
+
wait?: number
|
|
8
|
+
/** 队列达到这个数量立即发出,不再等窗口结束。默认 500 */
|
|
9
|
+
maxSize?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Runner = (requests: CallRequest[], overrides: CallOverrides) => Promise<RawResult[]>
|
|
13
|
+
|
|
14
|
+
interface Pending {
|
|
15
|
+
call: Call
|
|
16
|
+
resolve: (value: unknown) => void
|
|
17
|
+
reject: (reason: unknown) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Queue {
|
|
21
|
+
overrides: CallOverrides
|
|
22
|
+
entries: Pending[]
|
|
23
|
+
timer: ReturnType<typeof setTimeout> | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* DataLoader 式的自动合并:各处独立调用 `provider.call(x)`,
|
|
28
|
+
* 落在同一个收集窗口、且 blockTag/from 相同的调用会合成一次 aggregate3。
|
|
29
|
+
* 相同的 (target, callData) 只请求一次。
|
|
30
|
+
*/
|
|
31
|
+
export class Batcher {
|
|
32
|
+
readonly #run: Runner
|
|
33
|
+
readonly #wait: number
|
|
34
|
+
readonly #maxSize: number
|
|
35
|
+
readonly #queues = new Map<string, Queue>()
|
|
36
|
+
|
|
37
|
+
constructor(run: Runner, options: BatchOptions = {}) {
|
|
38
|
+
this.#run = run
|
|
39
|
+
this.#wait = options.wait ?? 0
|
|
40
|
+
this.#maxSize = Math.max(1, options.maxSize ?? 500)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
load<T>(call: Call, overrides: CallOverrides = {}): Promise<T> {
|
|
44
|
+
const key = `${String(overrides.blockTag ?? 'latest')}|${overrides.from ?? ''}`
|
|
45
|
+
let queue = this.#queues.get(key)
|
|
46
|
+
if (!queue) {
|
|
47
|
+
queue = { overrides, entries: [], timer: null }
|
|
48
|
+
this.#queues.set(key, queue)
|
|
49
|
+
}
|
|
50
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
51
|
+
queue.entries.push({ call, resolve: resolve as (value: unknown) => void, reject })
|
|
52
|
+
})
|
|
53
|
+
if (queue.entries.length >= this.#maxSize) {
|
|
54
|
+
this.#flush(key)
|
|
55
|
+
} else if (!queue.timer) {
|
|
56
|
+
queue.timer = setTimeout(() => this.#flush(key), this.#wait)
|
|
57
|
+
}
|
|
58
|
+
return promise
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#flush(key: string): void {
|
|
62
|
+
const queue = this.#queues.get(key)
|
|
63
|
+
if (!queue) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
this.#queues.delete(key)
|
|
67
|
+
if (queue.timer) {
|
|
68
|
+
clearTimeout(queue.timer)
|
|
69
|
+
}
|
|
70
|
+
void this.#dispatch(queue)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async #dispatch({ entries, overrides }: Queue): Promise<void> {
|
|
74
|
+
const requests: CallRequest[] = []
|
|
75
|
+
const indexByKey = new Map<string, number>()
|
|
76
|
+
const requestIndex: number[] = []
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
let request: CallRequest
|
|
80
|
+
try {
|
|
81
|
+
request = encodeCall(entry.call, true)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
entry.reject(err)
|
|
84
|
+
requestIndex.push(-1)
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const dedupeKey = `${request.ethBalanceOf ? 'eth' : request.target.toLowerCase()}|${request.callData}`
|
|
88
|
+
let index = indexByKey.get(dedupeKey)
|
|
89
|
+
if (index === undefined) {
|
|
90
|
+
index = requests.push(request) - 1
|
|
91
|
+
indexByKey.set(dedupeKey, index)
|
|
92
|
+
}
|
|
93
|
+
requestIndex.push(index)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (requests.length === 0) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let results: RawResult[]
|
|
101
|
+
try {
|
|
102
|
+
results = await this.#run(requests, overrides)
|
|
103
|
+
} catch (err) {
|
|
104
|
+
entries.forEach((entry, i) => requestIndex[i] !== -1 && entry.reject(err))
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
entries.forEach((entry, i) => {
|
|
109
|
+
const index = requestIndex[i] as number
|
|
110
|
+
if (index === -1) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
const result = results[index] as RawResult
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
entry.reject(new CallFailedError(entry.call, result.returnData))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
entry.resolve(decodeCall(entry.call, result.returnData))
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// 解码失败:多半是目标地址没有合约(返回空数据)
|
|
122
|
+
entry.reject(new CallFailedError(entry.call, result.returnData, err))
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/call.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { FunctionFragment, Interface, getAddress, toBeHex, zeroPadValue, type JsonFragmentType, type ParamType, type Result } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import { isTronAddress, toEvmAddress } from './tron.js'
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export type Params = any[]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 一次只读调用的描述。结构与 ethcall 的 Call 保持一致,手工构造的 Call 也能直接用。
|
|
10
|
+
* 类型参数 T 是解码后的结果类型(仅用于类型推断,运行时不存在)。
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
export interface Call<T = any> {
|
|
14
|
+
contract: {
|
|
15
|
+
address: string
|
|
16
|
+
}
|
|
17
|
+
name: string
|
|
18
|
+
inputs: readonly JsonFragmentType[]
|
|
19
|
+
outputs: readonly JsonFragmentType[]
|
|
20
|
+
params: Params
|
|
21
|
+
/**
|
|
22
|
+
* 调用自带的 overrides(`contract.method(...args, overrides)`)。
|
|
23
|
+
* blockTag / from 对 await 绑定 Call、provider.call、staticCall 生效;value / gasLimit 只对预执行生效。
|
|
24
|
+
* 传给 all / tryAll 时以批次的 overrides 为准(一批只能在同一个区块、同一个 from 下执行)。
|
|
25
|
+
*/
|
|
26
|
+
overrides?: {
|
|
27
|
+
blockTag?: number | bigint | string
|
|
28
|
+
from?: string
|
|
29
|
+
value?: bigint | string | number
|
|
30
|
+
gasLimit?: bigint | string | number
|
|
31
|
+
}
|
|
32
|
+
/** 内部标记:原生币余额查询(见 Provider.getEthBalance) */
|
|
33
|
+
kind?: 'ethBalance'
|
|
34
|
+
/** 仅用于类型推断 */
|
|
35
|
+
readonly __result?: T
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 绑定了 Provider 的 Call:可以直接 await(自动合并成 multicall),也可以照常传给 all / tryAll */
|
|
39
|
+
export type BoundCall<T = any> = Call<T> & PromiseLike<T>
|
|
40
|
+
|
|
41
|
+
export interface FailableCall extends Call {
|
|
42
|
+
canFail: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** multicall 返回的单条原始结果 */
|
|
46
|
+
export interface RawResult {
|
|
47
|
+
success: boolean
|
|
48
|
+
returnData: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 发给 aggregate3 前的单条请求 */
|
|
52
|
+
export interface CallRequest {
|
|
53
|
+
target: string
|
|
54
|
+
allowFailure: boolean
|
|
55
|
+
callData: string
|
|
56
|
+
/** 原生币余额查询:有 multicall 合约时走 getEthBalance,deployless 时改走 eth_getBalance */
|
|
57
|
+
ethBalanceOf?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// inputs → name → outputs → Interface。同一份 ABI 生成的 Call 共享 inputs/outputs 数组(见 contract.ts 的 ABI 缓存),
|
|
61
|
+
// 按引用缓存,避免每条 Call 都重新解析;三者都参与 key,手工构造的 Call 共用数组时也不会串
|
|
62
|
+
const interfaceCache = new WeakMap<readonly JsonFragmentType[], Map<string, WeakMap<readonly JsonFragmentType[], Interface>>>()
|
|
63
|
+
|
|
64
|
+
function getInterface(call: Call): Interface {
|
|
65
|
+
let byName = interfaceCache.get(call.inputs)
|
|
66
|
+
if (!byName) {
|
|
67
|
+
byName = new Map()
|
|
68
|
+
interfaceCache.set(call.inputs, byName)
|
|
69
|
+
}
|
|
70
|
+
let byOutputs = byName.get(call.name)
|
|
71
|
+
if (!byOutputs) {
|
|
72
|
+
byOutputs = new WeakMap()
|
|
73
|
+
byName.set(call.name, byOutputs)
|
|
74
|
+
}
|
|
75
|
+
let iface = byOutputs.get(call.outputs)
|
|
76
|
+
if (!iface) {
|
|
77
|
+
const fragment = FunctionFragment.from({
|
|
78
|
+
type: 'function',
|
|
79
|
+
name: call.name,
|
|
80
|
+
inputs: call.inputs,
|
|
81
|
+
outputs: call.outputs,
|
|
82
|
+
stateMutability: 'view',
|
|
83
|
+
})
|
|
84
|
+
iface = new Interface([fragment])
|
|
85
|
+
byOutputs.set(call.outputs, iface)
|
|
86
|
+
}
|
|
87
|
+
return iface
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function encodeCall(call: Call, allowFailure: boolean): CallRequest {
|
|
91
|
+
const iface = getInterface(call)
|
|
92
|
+
const fragment = iface.fragments[0] as FunctionFragment
|
|
93
|
+
const params = normalizeAddresses(fragment, call.params)
|
|
94
|
+
return {
|
|
95
|
+
target: getAddress(toEvmAddress(call.contract.address)),
|
|
96
|
+
allowFailure,
|
|
97
|
+
callData: iface.encodeFunctionData(fragment, params),
|
|
98
|
+
ethBalanceOf: call.kind === 'ethBalance' ? toEvmAddress(params[0] as string) : undefined,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Tron 的 T 开头地址在 ABI 里就是去掉 0x41 前缀的 20 字节,这里统一转成 0x 形式再编码
|
|
103
|
+
function normalizeAddresses(fragment: FunctionFragment, params: Params): Params {
|
|
104
|
+
if (params.length !== fragment.inputs.length) {
|
|
105
|
+
// 参数个数不对交给 ethers 报错(overrides 不在 params 里,见 Provider.staticCall)
|
|
106
|
+
return params
|
|
107
|
+
}
|
|
108
|
+
return fragment.inputs.map((input, i) => normalizeValue(input, params[i]))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ethers 的 ParamType.walk 不接受对象形式的 struct,这里自己递归
|
|
112
|
+
function normalizeValue(type: ParamType, value: unknown): unknown {
|
|
113
|
+
if (type.baseType === 'address') {
|
|
114
|
+
return isTronAddress(value) ? toEvmAddress(value as string) : value
|
|
115
|
+
}
|
|
116
|
+
if (type.isArray() && Array.isArray(value)) {
|
|
117
|
+
return value.map((v) => normalizeValue(type.arrayChildren, v))
|
|
118
|
+
}
|
|
119
|
+
if (type.isTuple() && value !== null && typeof value === 'object') {
|
|
120
|
+
if (Array.isArray(value)) {
|
|
121
|
+
return value.map((v, i) => (type.components[i] ? normalizeValue(type.components[i], v) : v))
|
|
122
|
+
}
|
|
123
|
+
const out: Record<string, unknown> = { ...value }
|
|
124
|
+
for (const component of type.components) {
|
|
125
|
+
if (component.name in out) {
|
|
126
|
+
out[component.name] = normalizeValue(component, out[component.name])
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out
|
|
130
|
+
}
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 单返回值直接返回该值,多返回值返回 ethers Result(可按下标或名字取) */
|
|
135
|
+
export function decodeCall<T>(call: Call, returnData: string): T {
|
|
136
|
+
const iface = getInterface(call)
|
|
137
|
+
const fragment = iface.fragments[0] as FunctionFragment
|
|
138
|
+
const result: Result = iface.decodeFunctionResult(fragment, returnData)
|
|
139
|
+
return (call.outputs.length === 1 ? result[0] : result) as T
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function encodeUint256(value: bigint): string {
|
|
143
|
+
return zeroPadValue(toBeHex(value), 32)
|
|
144
|
+
}
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Interface, type FunctionFragment, type InterfaceAbi, type JsonFragment, type JsonFragmentType } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import type { BoundCall, Call, Params } from './call.js'
|
|
4
|
+
|
|
5
|
+
export type ContractAbi = InterfaceAbi | Interface
|
|
6
|
+
|
|
7
|
+
/** 绑定后执行 Call 的对象(即 Provider),避免 contract ⇄ provider 循环依赖 */
|
|
8
|
+
export interface ContractRunner {
|
|
9
|
+
call(call: Call): Promise<any>
|
|
10
|
+
staticCall(call: Call): Promise<any>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 绑定合约上的方法:调用得到可 await 的 Call,`.staticCall(...)` 预执行 */
|
|
14
|
+
export interface BoundMethod<T = any> {
|
|
15
|
+
(...params: Params): BoundCall<T>
|
|
16
|
+
/** 预执行(直接 eth_call,可在最后一个参数传 { from, value, gasLimit, blockTag }) */
|
|
17
|
+
staticCall(...params: Params): Promise<T>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 生成 Call 的合约包装,用法与 ethcall 的 Contract 一致:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const erc20 = new Contract(token, ['function balanceOf(address) view returns (uint256)'])
|
|
25
|
+
* erc20.balanceOf(user) // => Call
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* 传入 runner(或用 `provider.contract(address, abi)`)得到绑定合约:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const usdt = multi.contract(token, ERC20_ABI)
|
|
32
|
+
* const balance = await usdt.balanceOf(user) // 同一 tick 内的调用自动合并成一次 multicall
|
|
33
|
+
* const out = await router.swap.staticCall(params, { value, from }) // 预执行,写法同 ethers
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* 与 ethcall 的差异:
|
|
37
|
+
* - ABI 直接支持人类可读字符串 / JSON / ethers Interface,不需要再 abiToJson 一遍
|
|
38
|
+
* - 暴露 ABI 里的所有函数(不只 view/pure),可以用来模拟 nonpayable 的报价函数(如 Uniswap V3 Quoter)
|
|
39
|
+
* - 重载函数可以用完整签名取:`contract['getAmount(uint256,address)'](...)`
|
|
40
|
+
*/
|
|
41
|
+
export class Contract {
|
|
42
|
+
readonly #address: string
|
|
43
|
+
readonly #runner: ContractRunner | undefined;
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
[method: string]: any
|
|
46
|
+
|
|
47
|
+
constructor(address: string, abi: ContractAbi, runner?: ContractRunner) {
|
|
48
|
+
this.#address = address
|
|
49
|
+
this.#runner = runner
|
|
50
|
+
for (const fn of parseAbi(abi)) {
|
|
51
|
+
const method = this.#makeMethod(fn)
|
|
52
|
+
for (const key of fn.keys) {
|
|
53
|
+
if (key in this) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
Object.defineProperty(this, key, { enumerable: true, value: method, writable: false })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get address(): string {
|
|
62
|
+
return this.#address
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#makeMethod({ name, inputs, outputs }: ParsedFunction) {
|
|
66
|
+
const address = this.#address
|
|
67
|
+
const runner = this.#runner
|
|
68
|
+
|
|
69
|
+
const build = (params: Params): Call => {
|
|
70
|
+
// 与 ethers 一致:多出来的最后一个参数是 overrides,如 router.swap(params, { value })
|
|
71
|
+
if (params.length === inputs.length + 1 && isPlainObject(params[params.length - 1])) {
|
|
72
|
+
const overrides = params[params.length - 1] as Call['overrides']
|
|
73
|
+
return { contract: { address }, name, inputs, outputs, params: params.slice(0, -1), overrides }
|
|
74
|
+
}
|
|
75
|
+
return { contract: { address }, name, inputs, outputs, params }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!runner) {
|
|
79
|
+
return (...params: Params): Call => build(params)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const method = (...params: Params): BoundCall => bindCall(build(params), runner)
|
|
83
|
+
Object.defineProperty(method, 'staticCall', {
|
|
84
|
+
value: (...params: Params) => runner.staticCall(build(params)),
|
|
85
|
+
})
|
|
86
|
+
return method
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ParsedFunction {
|
|
91
|
+
/** 挂到 Contract 上的属性名:完整签名,以及不重载时的函数名 */
|
|
92
|
+
keys: string[]
|
|
93
|
+
name: string
|
|
94
|
+
inputs: readonly JsonFragmentType[]
|
|
95
|
+
outputs: readonly JsonFragmentType[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 按 ABI 对象(数组 / Interface)缓存解析结果:循环里 new Contract(token, ERC20ABI) 只解析一次,
|
|
99
|
+
// 生成的 Call 共享 inputs/outputs 数组,call.ts 的 Interface 缓存才能命中
|
|
100
|
+
const abiCache = new WeakMap<object, ParsedFunction[]>()
|
|
101
|
+
|
|
102
|
+
function parseAbi(abi: ContractAbi): ParsedFunction[] {
|
|
103
|
+
const cacheable = typeof abi === 'object' && abi !== null
|
|
104
|
+
const cached = cacheable ? abiCache.get(abi) : undefined
|
|
105
|
+
if (cached) {
|
|
106
|
+
return cached
|
|
107
|
+
}
|
|
108
|
+
const iface = abi instanceof Interface ? abi : new Interface(abi)
|
|
109
|
+
const fragments: FunctionFragment[] = []
|
|
110
|
+
iface.forEachFunction((fn) => fragments.push(fn))
|
|
111
|
+
const counts = new Map<string, number>()
|
|
112
|
+
fragments.forEach((fn) => counts.set(fn.name, (counts.get(fn.name) ?? 0) + 1))
|
|
113
|
+
const parsed = fragments.map((fn) => {
|
|
114
|
+
const json = JSON.parse(fn.format('json')) as JsonFragment
|
|
115
|
+
return {
|
|
116
|
+
keys: counts.get(fn.name) === 1 ? [fn.format('sighash'), fn.name] : [fn.format('sighash')],
|
|
117
|
+
name: fn.name,
|
|
118
|
+
inputs: json.inputs ?? [],
|
|
119
|
+
outputs: json.outputs ?? [],
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
if (cacheable) {
|
|
123
|
+
abiCache.set(abi, parsed)
|
|
124
|
+
}
|
|
125
|
+
return parsed
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 给 Call 加上不可枚举的 then:第一次 await 时才发请求(交给 runner.call 自动合并),结果复用 */
|
|
129
|
+
export function bindCall<T>(call: Call<T>, runner: ContractRunner): BoundCall<T> {
|
|
130
|
+
let promise: Promise<T> | undefined
|
|
131
|
+
Object.defineProperty(call, 'then', {
|
|
132
|
+
enumerable: false,
|
|
133
|
+
value: (onFulfilled?: (value: T) => unknown, onRejected?: (reason: unknown) => unknown) => {
|
|
134
|
+
promise ??= runner.call(call)
|
|
135
|
+
return promise.then(onFulfilled, onRejected)
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
return call as BoundCall<T>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isPlainObject(value: unknown): boolean {
|
|
142
|
+
return value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default Contract
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// 由 scripts/build-contracts.mjs 生成,不要手改。源码:contracts/DeploylessMulticall3.sol(solc 0.8.30+commit.73712a01.Emscripten.clang)
|
|
2
|
+
|
|
3
|
+
/** constructor((address target, bool allowFailure, bytes callData)[] calls) — 以 aggregate3 的返回格式带回结果 */
|
|
4
|
+
export const DEPLOYLESS_MULTICALL3_BYTECODE =
|
|
5
|
+
'0x608060405260405161060f38038061060f8339810160408190526100229161036c565b80516000816001600160401b0381111561003e5761003e6102da565b60405190808252806020026020018201604052801561008457816020015b60408051808201909152600081526060602082015281526020019060019003908161005c5790505b50905060005b828110156102875760008482815181106100a6576100a6610507565b602090810291909101015160408101518151919250906001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156100e8575080516024145b801561010c5750631348c07360e21b6101008261051d565b6001600160e01b031916145b1561017e5760248101516040805180820182526001815290516001600160a01b039290921691602080830191610149918531910190815260200190565b60405160208183030381529060405281525085858151811061016d5761016d610507565b60200260200101819052505061027d565b60008083600001516001600160a01b03168360405161019d919061055b565b6000604051808303816000865af19150503d80600081146101da576040519150601f19603f3d011682016040523d82523d6000602084013e6101df565b606091505b5091509150811580156101f457508360200151155b156102465760405162461bcd60e51b815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064015b60405180910390fd5b604051806040016040528083151581526020018281525086868151811061026f5761026f610507565b602002602001018190525050505b505060010161008a565b5060008160405160200161029b9190610577565b6040516020818303038152906040529050616000815111156102d25781604051633649f05160e01b815260040161023d9190610577565b805160208201f35b634e487b7160e01b600052604160045260246000fd5b604051606081016001600160401b0381118282101715610312576103126102da565b60405290565b604051601f8201601f191681016001600160401b0381118282101715610340576103406102da565b604052919050565b60005b8381101561036357818101518382015260200161034b565b50506000910152565b60006020828403121561037e57600080fd5b81516001600160401b0381111561039457600080fd5b8201601f810184136103a557600080fd5b80516001600160401b038111156103be576103be6102da565b8060051b6103ce60208201610318565b918252602081840181019290810190878411156103ea57600080fd5b6020850192505b838310156104fc5782516001600160401b0381111561040f57600080fd5b85016060818a03601f1901121561042557600080fd5b61042d6102f0565b60208201516001600160a01b038116811461044757600080fd5b81526040820151801515811461045c57600080fd5b602082015260608201516001600160401b0381111561047a57600080fd5b60208184010192505089601f83011261049257600080fd5b81516001600160401b038111156104ab576104ab6102da565b6104be601f8201601f1916602001610318565b8181528b60208386010111156104d357600080fd5b6104e4826020830160208701610348565b604083015250835250602092830192909101906103f1565b979650505050505050565b634e487b7160e01b600052603260045260246000fd5b805160208201516001600160e01b0319811691906004821015610554576001600160e01b0319600483900360031b81901b82161692505b5050919050565b6000825161056d818460208701610348565b9190910192915050565b6000602082016020835280845180835260408501915060408160051b86010192506020860160005b8281101561060257603f198786030184528151805115158652602081015190506040602087015280518060408801526105df816060890160208501610348565b601f01601f1916959095016060019450602093840193919091019060010161059f565b5092969550505050505056fe'
|