solver-multirpc 3.1.4__py3-none-any.whl

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.
@@ -0,0 +1,640 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+ from abc import ABC
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from time import sleep
7
+ from typing import Callable, Coroutine, Dict, List, Optional, Tuple, TypeVar, Union
8
+
9
+ import web3
10
+ from eth_account import Account
11
+ from eth_account.datastructures import SignedTransaction
12
+ from eth_account.signers.local import LocalAccount
13
+ from eth_typing import Address, ChecksumAddress, HexStr
14
+ from multicallable.async_multicallable import AsyncCall, AsyncMulticall
15
+ from requests import ConnectionError, HTTPError, ReadTimeout
16
+ from web3 import AsyncWeb3, Web3
17
+ from web3._utils.contracts import encode_transaction_data # noqa
18
+ from web3.contract import Contract
19
+ from web3.exceptions import BadResponseFormat, BlockNotFound, TimeExhausted, TransactionNotFound
20
+ from web3.types import BlockData, BlockIdentifier, TxReceipt
21
+
22
+ from .constants import EstimateGasLimitBuffer, GasLimit, GasUpperBound, MultiRPCLogger, ViewPolicy
23
+ from .exceptions import (DontHaveThisRpcType, FailedOnAllRPCs, GetBlockFailed, NotValidViewPolicy,
24
+ TransactionFailedStatus, TransactionValueError, Web3InterfaceException)
25
+ from .gas_estimation import GasEstimation, GasEstimationMethod
26
+ from .tx_trace import TxTrace
27
+ from .utils import NestedDict, ResultEvent, TxPriority, calculate_chain_id, create_web3_from_rpc, \
28
+ get_span_proper_label_from_provider, get_unix_time, reduce_list_of_list
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ class BaseMultiRpc(ABC):
34
+ """
35
+ This class is used to be more sure when running web3 view calls and sending transactions by using of multiple RPCs.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ rpc_urls: NestedDict,
41
+ contract_address: Union[Address, ChecksumAddress, str],
42
+ contract_abi: Dict,
43
+ rpcs_supporting_tx_trace: Optional[List[str]] = None,
44
+ view_policy: ViewPolicy = ViewPolicy.MostUpdated,
45
+ gas_estimation: Optional[GasEstimation] = None,
46
+ gas_limit: int = GasLimit,
47
+ gas_upper_bound: int = GasUpperBound,
48
+ apm=None,
49
+ enable_estimate_gas_limit: bool = False,
50
+ is_proof_authority: bool = False,
51
+ multicall_custom_address: str = None,
52
+ log_level: logging = logging.WARN
53
+ ):
54
+ """
55
+ Args:
56
+ gas_estimation: gas_estimation is module we use to estimate gas price for current chain
57
+ gas_limit: The gas limit is the maximum amount of gas units that a user is willing to spend on a transaction
58
+ - It limits the total computational work or "effort" that the network will
59
+ put into processing a transaction
60
+ - for same contracts on different chains gas limit can be different
61
+ gas_upper_bound: is upper bound for gasPrice.
62
+ - gasPrice: determines the total cost a user will pay for each unit of gas
63
+ - max_tx_fee = gas_limit * gas_price
64
+ enable_estimate_gas_limit: use web3.estimate_gas() before real tx
65
+ - for checking if tx can be executed successfully without paying tx fee
66
+ - also this(estimate_gas) function return gas limit that it will be used for tx.
67
+ """
68
+ self.rpc_urls = rpc_urls
69
+
70
+ self.gas_estimation = gas_estimation
71
+
72
+ self.contract_address = Web3.to_checksum_address(contract_address)
73
+ self.contract_abi = contract_abi
74
+ self.rpcs_supporting_tx_trace = [] if rpcs_supporting_tx_trace is None else rpcs_supporting_tx_trace
75
+ self.apm = apm
76
+
77
+ self.contracts: NestedDict = NestedDict({'transaction': None, 'view': None})
78
+ self.multi_calls: NestedDict = NestedDict({'transaction': None, 'view': None})
79
+
80
+ self.functions = type("functions", (object,), {})()
81
+
82
+ self.view_policy = view_policy
83
+ self.gas_limit = gas_limit
84
+ self.gas_upper_bound = gas_upper_bound
85
+ self.enable_estimate_gas_limit = enable_estimate_gas_limit
86
+ self.is_proof_authority = is_proof_authority
87
+ self.multicall_custom_address = multicall_custom_address
88
+ self.max_gas_limit = None
89
+ self.providers = None
90
+ self.address = None
91
+ self.private_key = None
92
+ self.chain_id = None
93
+
94
+ MultiRPCLogger.setLevel(log_level)
95
+
96
+ def _logger_params(self, **kwargs) -> None:
97
+ if self.apm:
98
+ self.apm.span_label(**kwargs)
99
+ else:
100
+ MultiRPCLogger.info(f'params={kwargs}')
101
+
102
+ def set_account(self, address: Union[ChecksumAddress, str], private_key: str) -> None:
103
+ """
104
+ Set public key and private key for sending transactions. If these values set, there is no need to pass address,
105
+ private_key in "call" function.
106
+ Args:
107
+ address: sender public_key
108
+ private_key: sender private key
109
+ """
110
+ self.address = Web3.to_checksum_address(address)
111
+ self.private_key = private_key
112
+
113
+ async def setup(self) -> None:
114
+ self.providers = await create_web3_from_rpc(self.rpc_urls, self.is_proof_authority)
115
+ self.chain_id = await calculate_chain_id(self.providers)
116
+
117
+ if self.gas_estimation is None and self.providers.get('transaction'):
118
+ self.gas_estimation = GasEstimation(
119
+ self.chain_id,
120
+ reduce_list_of_list(self.providers['transaction'].values()),
121
+ # GasEstimationMethod.RPC,
122
+ )
123
+
124
+ is_rpc_provided = False
125
+ for wb3_k, wb3_v in self.providers.items(): # type: Tuple, List[web3.AsyncWeb3]
126
+ multi_calls = []
127
+ contracts = []
128
+ for wb3 in wb3_v:
129
+ rpc_url = wb3.provider.endpoint_uri
130
+ try:
131
+ mc = AsyncMulticall()
132
+ await mc.setup(w3=wb3, custom_address=self.multicall_custom_address)
133
+ multi_calls.append(mc)
134
+ contracts.append(
135
+ wb3.eth.contract(self.contract_address, abi=self.contract_abi)
136
+ )
137
+ except (ConnectionError, ReadTimeout, asyncio.TimeoutError) as e:
138
+ # fixme: at least we should retry not ignoring rpc
139
+ MultiRPCLogger.warning(f"Ignore rpc {rpc_url} because of {e}")
140
+ if len(multi_calls) != 0 and len(contracts) != 0:
141
+ is_rpc_provided = True
142
+
143
+ self.multi_calls[wb3_k] = multi_calls
144
+ self.contracts[wb3_k] = contracts
145
+
146
+ if not is_rpc_provided:
147
+ raise ValueError("No available rpc provided")
148
+
149
+ @staticmethod
150
+ async def __gather_tasks(execution_list: List[Coroutine], result_selector: Callable[[List], any],
151
+ view_policy: ViewPolicy = ViewPolicy.MostUpdated):
152
+ """
153
+ Get an execution list and wait for all to end. If all executable raise an exception, it will raise a
154
+ 'Web3InterfaceException' exception, otherwise returns all results which has no exception
155
+ Args:
156
+ execution_list:
157
+
158
+ Returns:
159
+
160
+ """
161
+
162
+ def wrap_coroutine(coro: Coroutine):
163
+ def sync_wrapper():
164
+ try:
165
+ res = asyncio.run(coro)
166
+ return res, None
167
+ except Exception as e:
168
+ return None, e
169
+
170
+ return sync_wrapper
171
+
172
+ if view_policy == view_policy.MostUpdated: # wait for all task to be completed
173
+ results = []
174
+ exceptions = []
175
+ with ThreadPoolExecutor() as executor:
176
+ wrapped_coroutines = [wrap_coroutine(coro) for coro in execution_list]
177
+ for result, exception in executor.map(lambda f: f(), wrapped_coroutines):
178
+ if exception:
179
+ exceptions.append(exception)
180
+ else:
181
+ results.append(result)
182
+
183
+ if len(results) == 0:
184
+ for exc in exceptions:
185
+ MultiRPCLogger.exception(f"RAISED EXCEPTION: {exc}")
186
+ raise FailedOnAllRPCs(f"All of RPCs raise exception. first exception: {exceptions[0]}")
187
+ return result_selector(results)
188
+ elif view_policy == view_policy.FirstSuccess: # wait to at least 1 task completed
189
+ return result_selector([await BaseMultiRpc.__execute_batch_tasks(
190
+ execution_list,
191
+ [HTTPError, ConnectionError, ValueError],
192
+ FailedOnAllRPCs
193
+ )])
194
+
195
+ raise NotValidViewPolicy()
196
+
197
+ async def _call_view_function(self,
198
+ func_name: str,
199
+ block_identifier: Union[str, int] = 'latest',
200
+ use_multicall=False,
201
+ *args, **kwargs):
202
+ """
203
+ Calling view function 'func_name' by using of multicall
204
+
205
+ Args:
206
+ func_name: view function name
207
+ *args:
208
+ **kwargs:
209
+
210
+ Returns:
211
+ the results of multicallable object for each rpc
212
+ """
213
+
214
+ def max_block_finder(results: List):
215
+ max_block_number = results[0][0]
216
+ max_index = 0
217
+ for i, result in enumerate(results):
218
+ if result[0] > max_block_number:
219
+ max_block_number = result[0]
220
+ max_index = i
221
+ if use_multicall:
222
+ return results[max_index][2]
223
+ return results[max_index][2][0]
224
+
225
+ last_error = None
226
+ for contracts, multi_calls in zip(self.contracts['view'].values(),
227
+ self.multi_calls['view'].values()): # type: any, List[AsyncMulticall]
228
+ rpc_bracket = list(map(lambda c: c.w3.provider.endpoint_uri, contracts))
229
+
230
+ if use_multicall:
231
+ calls = [[AsyncCall(cont, func_name, arg) for arg in args[0]] for cont in contracts]
232
+ else:
233
+ calls = [[AsyncCall(cont, func_name, args, kwargs)] for cont in contracts]
234
+ execution_list = [mc.call(call, block_identifier=block_identifier) for mc, call in zip(multi_calls, calls)]
235
+ try:
236
+ return await self.__gather_tasks(execution_list, max_block_finder, view_policy=self.view_policy)
237
+ except (Web3InterfaceException, asyncio.TimeoutError) as e:
238
+ last_error = e
239
+ MultiRPCLogger.warning(f"Can't call view function from this list of rpc({rpc_bracket}), error: {e}")
240
+ raise Web3InterfaceException(f"All of RPCs raise exception. {last_error=}")
241
+
242
+ async def _get_nonce(self, address: Union[Address, ChecksumAddress, str],
243
+ block_identifier: Optional[BlockIdentifier] = None) -> int:
244
+ address = Web3.to_checksum_address(address)
245
+ providers_4_nonce = self.providers.get('view') or self.providers['transaction']
246
+ last_error = None
247
+ for providers in providers_4_nonce.values():
248
+ execution_list = [
249
+ prov.eth.get_transaction_count(address, block_identifier=block_identifier) for prov in providers
250
+ ]
251
+ try:
252
+ return await self.__gather_tasks(execution_list, max)
253
+ except (Web3InterfaceException, asyncio.TimeoutError) as e:
254
+ last_error = e
255
+ MultiRPCLogger.warning(f"get_nounce: {e}")
256
+ pass
257
+ raise Web3InterfaceException(f"All of RPCs raise exception. {last_error=}")
258
+
259
+ async def _get_tx_params(
260
+ self, nonce: int, address: str, gas_limit: int, gas_upper_bound: int, priority: TxPriority,
261
+ gas_estimation_method: GasEstimationMethod) -> Dict:
262
+ gas_params = await self.gas_estimation.get_gas_price(gas_upper_bound, priority, gas_estimation_method)
263
+
264
+ # max transaction fee = gas_limit * gas_price
265
+ tx_params = {
266
+ "from": address,
267
+ "nonce": nonce,
268
+ "gas": gas_limit or self.gas_limit, # gas is gas_limit
269
+ "chainId": self.chain_id,
270
+ }
271
+ tx_params.update(gas_params)
272
+ return tx_params
273
+
274
+ @staticmethod
275
+ async def _build_transaction(contract: Contract, func_name: str, func_args: Tuple,
276
+ func_kwargs: Dict, tx_params: Dict):
277
+ func_args = func_args or []
278
+ func_kwargs = func_kwargs or {}
279
+ return await contract.functions.__getattribute__(func_name)(*func_args, **func_kwargs
280
+ ).build_transaction(tx_params)
281
+
282
+ async def _build_and_sign_transaction(
283
+ self,
284
+ contract: Contract,
285
+ provider: AsyncWeb3,
286
+ func_name: str,
287
+ func_args: Tuple,
288
+ func_kwargs: Dict,
289
+ signer_private_key: str,
290
+ tx_params: Dict,
291
+ enable_estimate_gas_limit: bool
292
+ ) -> SignedTransaction:
293
+ try:
294
+ tx = await self._build_transaction(contract, func_name, func_args, func_kwargs, tx_params)
295
+ account: LocalAccount = Account.from_key(signer_private_key)
296
+ if enable_estimate_gas_limit:
297
+ del tx['gas']
298
+ estimate_gas = await provider.eth.estimate_gas(tx)
299
+ MultiRPCLogger.info(f"gas_estimation({estimate_gas} gas needed) is successful")
300
+ return account.sign_transaction({**tx, 'gas': int(estimate_gas * EstimateGasLimitBuffer)})
301
+ return account.sign_transaction(tx)
302
+ except Exception as e:
303
+ MultiRPCLogger.error("exception in build and sign transaction: %s, %s", e.__class__.__name__, str(e))
304
+ raise
305
+
306
+ async def _send_transaction(self, provider: web3.AsyncWeb3, raw_transaction: any) -> Tuple[AsyncWeb3, any]:
307
+ rpc_url = provider.provider.endpoint_uri
308
+ try:
309
+ rpc_label_prefix = get_span_proper_label_from_provider(rpc_url)
310
+ transaction = await provider.eth.send_raw_transaction(raw_transaction)
311
+ self._logger_params(**{f"{rpc_label_prefix}_post_send_time": get_unix_time()})
312
+ self._logger_params(tx_send_time=int(time.time() * 1000))
313
+ return provider, transaction
314
+ except ValueError as e:
315
+ MultiRPCLogger.error(f"RPC({rpc_url}) value error: {str(e)}")
316
+ t_bnb_flag = "transaction would cause overdraft" in str(e).lower() and (await provider.eth.chain_id) == 97
317
+ if not (
318
+ t_bnb_flag or
319
+ 'nonce too low' in str(e).lower() or
320
+ 'already known' in str(e).lower() or
321
+ 'transaction underpriced' in str(e).lower() or
322
+ 'account suspended' in str(e).lower() or
323
+ 'exceeds the configured cap' in str(e).lower() or
324
+ 'no backends available for method' in str(e).lower() or
325
+ 'future transaction tries to replace pending' in str(e).lower() or
326
+ 'over rate limit' in str(e).lower()
327
+ ):
328
+ MultiRPCLogger.exception("_send_transaction_exception")
329
+ raise TransactionValueError
330
+ raise
331
+ except (ConnectionError, ReadTimeout, HTTPError) as e:
332
+ MultiRPCLogger.debug(f"network exception in send transaction: {e.__class__.__name__}, {str(e)}")
333
+ raise
334
+ except Exception as e:
335
+ # FIXME needs better exception handling
336
+ MultiRPCLogger.error(f"exception in send transaction: {e.__class__.__name__}, {str(e)}")
337
+ if self.apm:
338
+ self.apm.capture_exception()
339
+ raise
340
+
341
+ @staticmethod
342
+ def _handle_tx_trace(trace: TxTrace, func_name: str, func_args: Tuple, func_kwargs: Dict):
343
+ """
344
+ You can override this method to customize handling failed transaction.
345
+
346
+ example:
347
+ if "out of gas" in trace.text():
348
+ return InsufficientGasBalance(f'out of gas in {func_name}')
349
+ if "PartyBFacet: Will be liquidatable" in trace.text():
350
+ return PartyBWillBeLiquidatable(f'partyB will be liquidatable in {func_name}')
351
+ if "LibMuon: TSS not verified" in trace.text():
352
+ return TssNotVerified(trace.tx_hash, func_name, func_args, func_kwargs, trace)
353
+ if trace.ok():
354
+ MultiRPCLogger.error(f'TraceTransaction({func_name}): {trace.result().long_error()}')
355
+ apm.capture_message(param_message={
356
+ 'message': f'tr failed ({func_name}, {trace.result().first_usable_error()}): %s',
357
+ 'params': (trace.text(),),
358
+ })
359
+ """
360
+
361
+ pass
362
+
363
+ async def _wait_and_get_tx_receipt(self, provider: AsyncWeb3, tx, timeout: float) -> Tuple[AsyncWeb3, TxReceipt]:
364
+ con_err_count = tx_err_count = 0
365
+ rpc_url = provider.provider.endpoint_uri
366
+ while True:
367
+ try:
368
+ self._logger_params(received_provider=rpc_url)
369
+ tx_receipt = await provider.eth.wait_for_transaction_receipt(tx, timeout=timeout)
370
+ return provider, tx_receipt
371
+ except ConnectionError:
372
+ if con_err_count >= 5:
373
+ raise
374
+ con_err_count += 1
375
+ sleep(5)
376
+ except (TimeExhausted, TransactionNotFound):
377
+ if tx_err_count >= 1: # double-check the endpoint_uri
378
+ raise
379
+ tx_err_count += 1
380
+ timeout *= 2
381
+
382
+ @staticmethod
383
+ async def __get_tx_trace(tx, provider_url, func_name=None, func_args=None, func_kwargs=None):
384
+ tx_hash = Web3.to_hex(tx)
385
+ trace = TxTrace(tx_hash, provider_url)
386
+ BaseMultiRpc._handle_tx_trace(trace, func_name, func_args, func_kwargs)
387
+ return TransactionFailedStatus(tx_hash, func_name, func_args, func_kwargs, trace)
388
+
389
+ @staticmethod
390
+ async def __execute_batch_tasks(
391
+ execution_list: List[Coroutine],
392
+ ignored_exceptions: Optional[List[type[BaseException]]] = None,
393
+ final_exception: Optional[type[BaseException]] = None
394
+ ) -> T:
395
+ """
396
+ Executes a batch of asynchronous tasks concurrently and returns the result of the first completed task.
397
+
398
+ This function runs multiple coroutines concurrently and waits for the first one to complete successfully.
399
+ If any task raises an exception, it checks whether the exception is in the `exception_handler` list.
400
+ If so, it stores the exception but continues execution. If a terminal exception occurs (not in the
401
+ `exception_handler` list), it cancels all remaining tasks and raises that exception.
402
+
403
+ Parameters:
404
+ execution_list (List[Coroutine[None, None, T]]): A list of coroutine objects to be executed concurrently.
405
+ ignored_exceptions (Optional[List[type[BaseException]]], optional): A list of exception types to be handled
406
+ without terminating all tasks immediately. Exceptions of these types are stored and raised after
407
+ all tasks have been processed. Defaults to None.
408
+ final_exception (Optional[type[BaseException]], optional): An exception type to raise if no tasks complete
409
+ successfully and no terminal exceptions occur. Defaults to None.
410
+
411
+ Returns:
412
+ T: The result returned by the first task that completes successfully.
413
+
414
+ Raises:
415
+ BaseException: If a terminal exception occurs in any of the tasks, it is raised immediately.
416
+ BaseException: If all tasks fail with exceptions specified in `exception_handler`, the last exception is raised.
417
+ final_exception: If provided and no tasks complete successfully or raise terminal exceptions, this exception
418
+ is raised.
419
+ RuntimeError: If no tasks complete successfully and no exceptions are raised, a RuntimeError is raised.
420
+
421
+ """
422
+
423
+ async def exec_task(task: Coroutine, cancel_event_: ResultEvent, lock_: asyncio.Lock):
424
+ res = await task
425
+ async with lock_:
426
+ cancel_event_.set_result(res)
427
+ cancel_event_.set()
428
+
429
+ cancel_event = ResultEvent()
430
+ lock = asyncio.Lock()
431
+
432
+ tasks = [
433
+ asyncio.create_task(exec_task(task, cancel_event, lock))
434
+ for task in execution_list
435
+ ]
436
+ not_completed_tasks = tasks.copy()
437
+ exception = None
438
+ terminal_exception = None
439
+
440
+ while len(not_completed_tasks) > 0:
441
+ dones, not_completed_tasks = await asyncio.wait(
442
+ not_completed_tasks, return_when=asyncio.FIRST_COMPLETED
443
+ )
444
+
445
+ for task in list(dones):
446
+ e = task.exception()
447
+ if e:
448
+ if ignored_exceptions and isinstance(e, tuple(ignored_exceptions)):
449
+ exception = e
450
+ else:
451
+ terminal_exception = e
452
+ continue
453
+ if cancel_event.is_set():
454
+ break
455
+
456
+ if cancel_event.is_set() or terminal_exception:
457
+ break
458
+
459
+ # Cancel the remaining tasks
460
+ for task in not_completed_tasks:
461
+ if not task.done():
462
+ task.cancel()
463
+ await asyncio.gather(*tasks, return_exceptions=True)
464
+
465
+ if cancel_event.is_set():
466
+ return cancel_event.get_result()
467
+ if terminal_exception or exception:
468
+ raise terminal_exception or exception
469
+ raise final_exception or RuntimeError("Execution completed without setting a result or exception.")
470
+
471
+ async def __call_tx(
472
+ self,
473
+ func_name: str,
474
+ func_args: Tuple,
475
+ func_kwargs: Dict,
476
+ private_key: str,
477
+ wait_for_receipt: int,
478
+ providers: List[AsyncWeb3],
479
+ contracts: List[Contract],
480
+ tx_params: Dict,
481
+ enable_estimate_gas_limit: bool,
482
+ ) -> Union[str, TxReceipt]:
483
+ signed_transaction = await self._build_and_sign_transaction(
484
+ contracts[0], providers[0], func_name, func_args, func_kwargs, private_key, tx_params,
485
+ enable_estimate_gas_limit
486
+ )
487
+ tx_hash = Web3.to_hex(signed_transaction.hash)
488
+ self._logger_params(tx_hash=tx_hash)
489
+
490
+ execution_tx_list = [
491
+ self._send_transaction(p, signed_transaction.raw_transaction) for p in providers
492
+ ]
493
+ result = await self.__execute_batch_tasks(
494
+ execution_tx_list,
495
+ [ValueError, ConnectionError, ReadTimeout, HTTPError],
496
+ FailedOnAllRPCs
497
+ )
498
+ provider, tx = result
499
+
500
+ MultiRPCLogger.info(f"success tx: {provider= }, {tx= }")
501
+ rpc_url = provider.provider.endpoint_uri
502
+ self._logger_params(sent_provider=rpc_url, tx_send_time=int(time.time()) * 1000)
503
+
504
+ if not wait_for_receipt:
505
+ return tx_hash
506
+ execution_receipt_list = [
507
+ self._wait_and_get_tx_receipt(p, tx_hash, wait_for_receipt) for p in providers
508
+ ]
509
+ provider, tx_receipt = await self.__execute_batch_tasks(
510
+ execution_receipt_list,
511
+ [TimeExhausted, TransactionNotFound, ConnectionError, ReadTimeout,
512
+ ValueError, BadResponseFormat, HTTPError],
513
+ )
514
+ if tx_receipt.status == 1:
515
+ return tx_receipt
516
+
517
+ # get tx_trace In case transaction failed
518
+ execution_trace_list = [
519
+ self.__get_tx_trace(tx, p.provider.endpoint_uri, func_name, func_args, func_kwargs) for p in providers
520
+ if p.provider.endpoint_uri in self.rpcs_supporting_tx_trace
521
+ ]
522
+ if not execution_trace_list:
523
+ raise TransactionFailedStatus(tx_hash, func_name, func_args, func_kwargs)
524
+
525
+ raise await self.__execute_batch_tasks(
526
+ execution_trace_list,
527
+ [HTTPError, ConnectionError, ReadTimeout, BadResponseFormat],
528
+ )
529
+
530
+ async def _call_tx_function(self, address: str, gas_limit: int, gas_upper_bound: int, priority: TxPriority,
531
+ gas_estimation_method: GasEstimationMethod,
532
+ enable_estimate_gas_limit: Optional[bool] = None, **kwargs):
533
+ nonce = await self._get_nonce(address)
534
+ tx_params = await self._get_tx_params(
535
+ nonce, address, gas_limit, gas_upper_bound, priority, gas_estimation_method
536
+ )
537
+ last_error = None
538
+ enable_estimate_gas_limit = self.enable_estimate_gas_limit if enable_estimate_gas_limit is None \
539
+ else enable_estimate_gas_limit
540
+
541
+ for p, c in zip(
542
+ self.providers['transaction'].values(), self.contracts['transaction'].values()
543
+ ): # type: List[AsyncWeb3], List[Contract]
544
+ try:
545
+ return await self.__call_tx(**kwargs, providers=p, contracts=c, tx_params=tx_params,
546
+ enable_estimate_gas_limit=enable_estimate_gas_limit)
547
+ except (TransactionFailedStatus, TransactionValueError):
548
+ raise
549
+ except (ConnectionError, ReadTimeout, TimeExhausted, TransactionNotFound, FailedOnAllRPCs) as e:
550
+ last_error = e
551
+ except Exception:
552
+ raise
553
+ raise Web3InterfaceException(f"All of RPCs raise exception. {last_error=}")
554
+
555
+ def check_for_view(self):
556
+ if self.providers.get('view') is None:
557
+ raise DontHaveThisRpcType(f"Doesn't have view RPCs")
558
+
559
+ async def get_tx_receipt(self, tx_hash) -> TxReceipt:
560
+ self.check_for_view()
561
+
562
+ exceptions = (HTTPError, ConnectionError, ReadTimeout, ValueError, TimeExhausted, TransactionNotFound)
563
+
564
+ last_exception = None
565
+ for provider in self.providers['view'].values(): # type: List[AsyncWeb3]
566
+ execution_tx_list = [p.eth.wait_for_transaction_receipt(tx_hash) for p in provider]
567
+ try:
568
+ return await self.__execute_batch_tasks(
569
+ execution_tx_list,
570
+ list(exceptions),
571
+ TransactionFailedStatus
572
+ )
573
+ except exceptions as e:
574
+ last_exception = e
575
+ pass
576
+ except TransactionFailedStatus:
577
+ raise
578
+ raise last_exception
579
+
580
+ async def get_block(self, block_identifier: BlockIdentifier = 'latest',
581
+ full_transactions: bool = False) -> BlockData:
582
+ self.check_for_view()
583
+
584
+ exceptions = (HTTPError, ConnectionError, ReadTimeout, ValueError, TimeExhausted, BlockNotFound)
585
+ last_exception = None
586
+ for provider in self.providers['view'].values(): # type: List[AsyncWeb3]
587
+ execution_tx_params_list = [p.eth.get_block(block_identifier, full_transactions) for p in provider]
588
+ try:
589
+ return await self.__execute_batch_tasks(
590
+ execution_tx_params_list,
591
+ list(exceptions),
592
+ GetBlockFailed
593
+ )
594
+ except exceptions as e:
595
+ last_exception = e
596
+ pass
597
+ except GetBlockFailed:
598
+ raise
599
+ raise last_exception
600
+
601
+ async def get_block_number(self) -> int:
602
+ self.check_for_view()
603
+
604
+ exceptions = (HTTPError, ConnectionError, ReadTimeout, ValueError, TimeExhausted)
605
+ last_exception = None
606
+ for provider in self.providers['view'].values(): # type: List[AsyncWeb3]
607
+ execution_tx_params_list = [p.eth.get_block_number() for p in provider]
608
+ try:
609
+ result = await self.__execute_batch_tasks(
610
+ execution_tx_params_list,
611
+ list(exceptions),
612
+ GetBlockFailed
613
+ )
614
+ return result
615
+ except exceptions as e:
616
+ last_exception = e
617
+ pass
618
+ except GetBlockFailed:
619
+ raise
620
+ raise last_exception
621
+
622
+
623
+ class BaseContractFunction:
624
+ def __init__(self, name: str, abi: Dict, multi_rpc_web3: BaseMultiRpc, typ: str):
625
+ self.name = name
626
+ self.mr = multi_rpc_web3
627
+ self.typ = typ
628
+ self.abi = abi
629
+ self.args = None
630
+ self.kwargs = None
631
+
632
+ def get_encoded_data(self) -> HexStr:
633
+ return encode_transaction_data(
634
+ reduce_list_of_list(self.mr.providers['transaction'].values())[0],
635
+ self.name,
636
+ self.mr.contract_abi,
637
+ self.abi,
638
+ self.args,
639
+ self.kwargs,
640
+ )
@@ -0,0 +1,52 @@
1
+ import enum
2
+ import logging
3
+
4
+ DEFAULT_API_PROVIDER = 'https://gas-api.metaswap.codefi.network/networks/{chain_id}/suggestedGasFees'
5
+
6
+
7
+ class ViewPolicy(enum.Enum):
8
+ FirstSuccess = 0
9
+ MostUpdated = 1
10
+
11
+
12
+ class GasEstimationMethod(enum.Enum):
13
+ GAS_API_PROVIDER = 0
14
+ RPC = 1
15
+ FIXED = 2
16
+ CUSTOM = 3
17
+
18
+
19
+ GasLimit = 1_000_000
20
+ GasUpperBound = 50_000
21
+
22
+ GasMultiplierLow = 1.1
23
+ GasMultiplierMedium = 1.3
24
+ GasMultiplierHigh = 1.5
25
+
26
+ MaxRPCInEachBracket = 3
27
+
28
+ # config
29
+ # It must be greater than 1, so we have a safe margin to ensure the transaction can be successful.
30
+ EstimateGasLimitBuffer = 1.1
31
+ ChainIdToGas = {
32
+ 97: 10.1, # Test BNB Network
33
+ 250: 20, # Ftm
34
+ 5000: 0.02 # Mantle
35
+ }
36
+ GasFromRpcChainIds = [] # for this chain ids use rpc to estimate gas
37
+ FixedValueGas = 30
38
+
39
+ MultiRPCLoggerName = 'Multi-RPC'
40
+ GasEstimationLoggerName = MultiRPCLoggerName + '.Gas-Estimation'
41
+
42
+ console_handler = logging.StreamHandler()
43
+ console_handler.setLevel(logging.INFO)
44
+
45
+ MultiRPCLogger = logging.getLogger(MultiRPCLoggerName)
46
+ GasEstimationLogger = logging.getLogger(GasEstimationLoggerName)
47
+
48
+ MultiRPCLogger.addHandler(console_handler)
49
+ GasEstimationLogger.addHandler(console_handler)
50
+
51
+ RequestTimeout = 30
52
+ DevEnv = True