7
7
import eth_utils
8
8
import rlp
9
9
from eth_utils import to_checksum_address , to_bytes
10
- from web3 ._utils .abi import get_abi_output_types
10
+ from web3 ._utils .abi import get_abi_output_types , get_abi_input_types
11
11
from web3 .contract .contract import ContractFunction , ContractConstructor
12
12
from web3 .exceptions import ContractLogicError
13
13
@@ -31,7 +31,7 @@ class MultiCall:
31
31
CALLER_ADDRESS = "0x0000000000000000000000000000000000000123"
32
32
33
33
MULTICALL_DEPLOYMENTS : dict [int , str ] = {
34
- 1116 : "0x2C310a21E21a3eaDF4e53E1118aeD4614c51B576"
34
+ 1116 : "0x2cd05AcF9aBe54D57eb1E6B12f2129880fA4cF65" ,
35
35
}
36
36
37
37
@classmethod
@@ -75,46 +75,59 @@ def call(self, use_revert: Optional[bool] = None, batch_size: int = 1_000):
75
75
if use_revert is None :
76
76
use_revert = self .w3 .revert_reason_available
77
77
78
- return self ._inner_call (use_revert = use_revert , calls = self .calls , batch_size = batch_size )
78
+ calls = self .calls
79
+ calls_with_calldata = self .add_calls_calldata (calls )
79
80
80
- def _inner_call (self , use_revert : bool , calls : list [ContractFunction ], batch_size : int ):
81
+ return self ._inner_call (use_revert = use_revert , calls_with_calldata = calls_with_calldata , batch_size = batch_size )
82
+
83
+ def _inner_call (
84
+ self ,
85
+ use_revert : bool ,
86
+ calls_with_calldata : list [tuple [ContractFunction , bytes ]],
87
+ batch_size : int
88
+ ):
81
89
kwargs = dict (
82
90
use_revert = use_revert ,
83
91
batch_size = batch_size ,
84
92
)
85
93
# make sure calls are not bigger than batch_size
86
- if len (calls ) > batch_size :
94
+ if len (calls_with_calldata ) > batch_size :
87
95
results = []
88
- for start in range (0 , len (calls ), batch_size ):
96
+ for start in range (0 , len (calls_with_calldata ), batch_size ):
89
97
results += self ._inner_call (
90
98
** kwargs ,
91
- calls = calls [start : min (start + batch_size , len (calls ))],
99
+ calls_with_calldata = calls_with_calldata [start : min (start + batch_size , len (calls_with_calldata ))],
92
100
)
93
101
return results
94
102
95
103
if self .multicall .address is None :
96
- multicall_call = self ._build_constructor_calldata (calls = calls , use_revert = use_revert )
104
+ multicall_call = self ._build_constructor_calldata (
105
+ calls_with_calldata = calls_with_calldata ,
106
+ use_revert = use_revert
107
+ )
97
108
else :
98
- multicall_call = self ._build_calldata (calls = calls )
109
+ multicall_call = self ._build_calldata (
110
+ calls_with_calldata = calls_with_calldata
111
+ )
99
112
try :
100
113
raw_returns = self ._call_multicall (
101
114
multicall_call = multicall_call ,
102
- retry = len (calls ) == 1
115
+ retry = len (calls_with_calldata ) == 1
103
116
)
104
117
except Exception as e :
105
- if len (calls ) == 1 :
118
+ if len (calls_with_calldata ) == 1 :
106
119
print (f"Multicall with single call got Exception '{ repr (e )} ', retrying in 1 sec" )
107
120
sleep (1 )
108
- return self ._inner_call (** kwargs , calls = calls )
121
+ return self ._inner_call (** kwargs , calls_with_calldata = calls_with_calldata )
109
122
print (f"Multicall got Exception '{ repr (e )} ', splitting and retrying" )
110
- left_results = self ._inner_call (** kwargs , calls = calls [:len (calls ) // 2 ])
111
- right_results = self ._inner_call (** kwargs , calls = calls [len (calls ) // 2 :])
123
+ left_results = self ._inner_call (** kwargs , calls_with_calldata = calls_with_calldata [:len (calls_with_calldata ) // 2 ])
124
+ right_results = self ._inner_call (** kwargs , calls_with_calldata = calls_with_calldata [len (calls_with_calldata ) // 2 :])
112
125
return left_results + right_results
113
- results = self .decode_contract_function_results (raw_returns = raw_returns , contract_functions = calls )
114
- if len (results ) == len (calls ):
126
+ results = self .decode_contract_function_results (raw_returns = raw_returns , contract_functions = [ call for call , _ in calls_with_calldata ] )
127
+ if len (results ) == len (calls_with_calldata ):
115
128
return results
116
129
# if not all calls were executed, recursively execute remaining calls and concatenate results
117
- return results + self ._inner_call (** kwargs , calls = calls [len (results ):])
130
+ return results + self ._inner_call (** kwargs , calls_with_calldata = calls_with_calldata [len (results ):])
118
131
119
132
@staticmethod
120
133
def calculate_expected_contract_address (sender : str , nonce : int ):
@@ -125,33 +138,47 @@ def calculate_expected_contract_address(sender: str, nonce: int):
125
138
@staticmethod
126
139
def calculate_create_address (sender : str , nonce : int ) -> str :
127
140
assert len (sender ) == 42
128
- sender_bytes = eth_utils . to_bytes (hexstr = sender )
141
+ sender_bytes = to_bytes (hexstr = sender )
129
142
raw = rlp .encode ([sender_bytes , nonce ])
130
143
h = eth_utils .keccak (raw )
131
144
address_bytes = h [12 :]
132
145
return eth_utils .to_checksum_address (address_bytes )
133
146
134
- def _build_calldata (self , calls : list [ContractFunction ]) -> ContractFunction :
147
+ @staticmethod
148
+ def add_calls_calldata (calls : list [ContractFunction ]) -> list [tuple [ContractFunction , bytes ]]:
149
+ calls_with_calldata = []
150
+ for call in calls :
151
+ function_abi = get_abi_input_types (call .abi )
152
+ assert len (function_abi ) == len (call .arguments )
153
+ function_args = []
154
+ for aby_type , arg in zip (function_abi , call .arguments ):
155
+ if aby_type == "bytes" :
156
+ arg = to_bytes (hexstr = arg )
157
+ function_args .append (arg )
158
+ call_data = to_bytes (hexstr = call .selector ) + eth_abi .encode (function_abi , function_args )
159
+ calls_with_calldata .append ((call , call_data ))
160
+ assert len (calls_with_calldata ) == len (calls )
161
+ return calls_with_calldata
162
+
163
+ def _build_calldata (self , calls_with_calldata : list [tuple [ContractFunction , bytes ]]) -> ContractFunction :
135
164
assert self .multicall .address is not None
136
165
137
166
if self .undeployed_contract_constructor is not None :
138
167
# deploy undeployed contract first and then call the other functions
139
168
contract_deployment_call = self .multicall .functions .deployContract (
140
- contractBytecode = self .undeployed_contract_constructor .data_in_transaction
169
+ contractBytecode = to_bytes ( hexstr = self .undeployed_contract_constructor .data_in_transaction )
141
170
)
142
- calls = [contract_deployment_call ] + calls
171
+ contract_deployment_calldata = to_bytes (hexstr = contract_deployment_call .selector ) + \
172
+ eth_abi .encode (
173
+ get_abi_input_types (contract_deployment_call .abi ),
174
+ contract_deployment_call .arguments
175
+ )
176
+ # contract_deployment_calldata = to_bytes(hexstr=contract_deployment_call._encode_transaction_data())
177
+ calls_with_calldata = [(contract_deployment_call , contract_deployment_calldata )] + calls_with_calldata
143
178
144
179
encoded_calls = []
145
- for call in calls :
146
- target = call .address
147
- call_data_hex = call ._encode_transaction_data ()
148
- call_data = to_bytes (hexstr = call_data_hex )
149
-
150
- encoded_calls .append ({
151
- "target" : target ,
152
- "gasLimit" : 100_000_000 ,
153
- "callData" : call_data ,
154
- })
180
+ for call , call_data in calls_with_calldata :
181
+ encoded_calls .append ((call .address , 100_000_000 , call_data )) # target, gasLimit, callData
155
182
156
183
# build multicall transaction
157
184
multicall_call = self .multicall .functions .multicallWithGasLimitation (
@@ -162,20 +189,22 @@ def _build_calldata(self, calls: list[ContractFunction]) -> ContractFunction:
162
189
# return multicall address and calldata
163
190
return multicall_call
164
191
165
- def _build_constructor_calldata (self , calls : list [ContractFunction ], use_revert : bool ) -> ContractConstructor :
192
+ def _build_constructor_calldata (
193
+ self ,
194
+ calls_with_calldata : list [tuple [ContractFunction , bytes ]],
195
+ use_revert : bool
196
+ ) -> ContractConstructor :
166
197
assert self .multicall .address is None
167
198
168
199
# Encode the number of calls as the first 32 bytes
169
- number_of_calls = len (calls )
200
+ number_of_calls = len (calls_with_calldata )
170
201
encoded_calls = eth_abi .encode (['uint256' ], [number_of_calls ]).hex ()
171
202
172
203
previous_target = None
173
204
previous_call_data = None
174
205
175
- for call in calls :
206
+ for call , call_data in calls_with_calldata :
176
207
target = call .address
177
- call_data_hex = call ._encode_transaction_data ()
178
- call_data = to_bytes (hexstr = call_data_hex )
179
208
180
209
# Determine the flags
181
210
flags = 0
@@ -197,7 +226,7 @@ def _build_constructor_calldata(self, calls: list[ContractFunction], use_revert:
197
226
# Encode call data length (16 bits / 2 bytes)
198
227
call_data_length_encoded = eth_abi .encode (['uint16' ], [len (call_data )]).hex ().zfill (4 )[- 4 :]
199
228
# Encode call data (variable length)
200
- call_data_encoded = call_data_hex [ 2 :]
229
+ call_data_encoded = call_data . hex ()
201
230
else :
202
231
call_data_length_encoded = ""
203
232
call_data_encoded = ""
@@ -215,7 +244,7 @@ def _build_constructor_calldata(self, calls: list[ContractFunction], use_revert:
215
244
multicall_call = self .multicall .constructor (
216
245
useRevert = use_revert ,
217
246
contractBytecode = contract_constructor_data ,
218
- encodedCalls = bytes . fromhex ( encoded_calls )
247
+ encodedCalls = to_bytes ( hexstr = encoded_calls )
219
248
)
220
249
221
250
return multicall_call
@@ -224,7 +253,7 @@ def _build_constructor_calldata(self, calls: list[ContractFunction], use_revert:
224
253
def _decode_muilticall (multicall_result : bytes | list [tuple [bool , int , bytes ]]) -> list [str | Exception ]:
225
254
raw_returns : list [str or Exception ] = []
226
255
227
- if isinstance (multicall_result , list ):
256
+ if isinstance (multicall_result , list ) or isinstance ( multicall_result , tuple ) :
228
257
# deployed multicall
229
258
for sucess , _ , raw_return in multicall_result :
230
259
if not sucess :
@@ -281,24 +310,39 @@ def _call_multicall(self, multicall_call: ContractConstructor | ContractFunction
281
310
})
282
311
else :
283
312
assert isinstance (multicall_call , ContractFunction )
284
- _ , multicall_result , _ = multicall_call .call ({
313
+ # manually encoding and decoding call because web3.py is sooooo slow...
314
+ # The simple but slow version is as below:
315
+ # _, multicall_result, completed_calls = multicall_call.call({
316
+ # "from": self.CALLER_ADDRESS,
317
+ # "nonce": 0,
318
+ # "no_retry": not retry,
319
+ # })
320
+
321
+ calldata = to_bytes (hexstr = multicall_call .selector ) + \
322
+ eth_abi .encode (get_abi_input_types (multicall_call .abi ), multicall_call .arguments )
323
+ raw_response = self .w3 .eth .call ({
285
324
"from" : self .CALLER_ADDRESS ,
325
+ "to" : multicall_call .address ,
286
326
"nonce" : 0 ,
327
+ "data" : calldata ,
287
328
"no_retry" : not retry ,
288
329
})
330
+ _ , multicall_result , completed_calls = eth_abi .decode (get_abi_output_types (multicall_call .abi ), raw_response )
331
+
289
332
if self .undeployed_contract_constructor is not None :
290
333
# remove first call result as that's the deployment of the undeployed contract
291
334
success , _ , address_encoded = multicall_result [0 ]
292
335
assert success , "Undeployed contract constructor reverted"
293
336
assert "0x" + address_encoded [- 20 :].hex () == self .undeployed_contract_address .lower (), "unexpected undeployed contract address"
294
337
multicall_result = multicall_result [1 :]
338
+ multicall_result = multicall_result [:completed_calls ]
295
339
except ContractLogicError as e :
296
340
if not e .message .startswith ("execution reverted: " ):
297
341
raise
298
342
result_str = e .message .removeprefix ("execution reverted: " )
299
343
if any ((char not in HEX_CHARS for char in result_str )):
300
344
raise
301
- multicall_result = bytes . fromhex ( result_str )
345
+ multicall_result = to_bytes ( hexstr = result_str )
302
346
303
347
if len (multicall_result ) == 0 :
304
348
raise ValueError ("No data returned from multicall" )
@@ -310,8 +354,7 @@ def decode_contract_function_result(raw_return: str | Exception, contract_functi
310
354
if isinstance (raw_return , Exception ):
311
355
return raw_return
312
356
try :
313
- output_types = get_abi_output_types (contract_function .abi )
314
- result = contract_function .w3 .codec .decode (output_types , raw_return )
357
+ result = eth_abi .decode (get_abi_output_types (contract_function .abi ), raw_return )
315
358
if hasattr (result , "__len__" ) and len (result ) == 1 :
316
359
result = result [0 ]
317
360
return result
0 commit comments