@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.
Files changed (74) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +257 -0
  3. package/contracts/DeploylessMulticall3.sol +66 -0
  4. package/dist/aggregate.d.ts +27 -0
  5. package/dist/aggregate.d.ts.map +1 -0
  6. package/dist/aggregate.js +198 -0
  7. package/dist/aggregate.js.map +1 -0
  8. package/dist/batcher.d.ts +21 -0
  9. package/dist/batcher.d.ts.map +1 -0
  10. package/dist/batcher.js +100 -0
  11. package/dist/batcher.js.map +1 -0
  12. package/dist/call.d.ts +53 -0
  13. package/dist/call.d.ts.map +1 -0
  14. package/dist/call.js +82 -0
  15. package/dist/call.js.map +1 -0
  16. package/dist/contract.d.ts +45 -0
  17. package/dist/contract.d.ts.map +1 -0
  18. package/dist/contract.js +107 -0
  19. package/dist/contract.js.map +1 -0
  20. package/dist/deployless.d.ts +3 -0
  21. package/dist/deployless.d.ts.map +1 -0
  22. package/dist/deployless.js +4 -0
  23. package/dist/deployless.js.map +1 -0
  24. package/dist/erc20.d.ts +27 -0
  25. package/dist/erc20.d.ts.map +1 -0
  26. package/dist/erc20.js +17 -0
  27. package/dist/erc20.js.map +1 -0
  28. package/dist/errors.d.ts +20 -0
  29. package/dist/errors.d.ts.map +1 -0
  30. package/dist/errors.js +56 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/fallback.d.ts +23 -0
  33. package/dist/fallback.d.ts.map +1 -0
  34. package/dist/fallback.js +86 -0
  35. package/dist/fallback.js.map +1 -0
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +10 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/multicall.d.ts +13 -0
  41. package/dist/multicall.d.ts.map +1 -0
  42. package/dist/multicall.js +51 -0
  43. package/dist/multicall.js.map +1 -0
  44. package/dist/provider.d.ts +137 -0
  45. package/dist/provider.d.ts.map +1 -0
  46. package/dist/provider.js +269 -0
  47. package/dist/provider.js.map +1 -0
  48. package/dist/rpcNodes.d.ts +5 -0
  49. package/dist/rpcNodes.d.ts.map +1 -0
  50. package/dist/rpcNodes.js +193 -0
  51. package/dist/rpcNodes.js.map +1 -0
  52. package/dist/source.d.ts +23 -0
  53. package/dist/source.d.ts.map +1 -0
  54. package/dist/source.js +56 -0
  55. package/dist/source.js.map +1 -0
  56. package/dist/tron.d.ts +62 -0
  57. package/dist/tron.d.ts.map +1 -0
  58. package/dist/tron.js +223 -0
  59. package/dist/tron.js.map +1 -0
  60. package/package.json +63 -0
  61. package/src/aggregate.ts +254 -0
  62. package/src/batcher.ts +126 -0
  63. package/src/call.ts +144 -0
  64. package/src/contract.ts +145 -0
  65. package/src/deployless.ts +5 -0
  66. package/src/erc20.ts +43 -0
  67. package/src/errors.ts +66 -0
  68. package/src/fallback.ts +105 -0
  69. package/src/index.ts +31 -0
  70. package/src/multicall.ts +60 -0
  71. package/src/provider.ts +352 -0
  72. package/src/rpcNodes.ts +194 -0
  73. package/src/source.ts +84 -0
  74. package/src/tron.ts +281 -0
@@ -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
+ }
@@ -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'