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,77 @@
1
+ from src.multirpc.tx_trace import TxTrace
2
+
3
+ BaseException_ = Exception
4
+
5
+
6
+ class Web3InterfaceException(BaseException_):
7
+ def __str__(self):
8
+ return f"{self.__class__.__name__}({self.args[0]})"
9
+
10
+
11
+ class OutOfRangeTransactionFee(Web3InterfaceException):
12
+ pass
13
+
14
+
15
+ class FailedOnAllRPCs(Web3InterfaceException):
16
+ pass
17
+
18
+
19
+ class ViewCallFailed(Web3InterfaceException):
20
+ pass
21
+
22
+
23
+ class TransactionFailedStatus(Web3InterfaceException):
24
+ def __init__(self, hex_tx_hash, func_name=None, func_args=None, func_kwargs=None, trace=None):
25
+ self.hex_tx_hash = hex_tx_hash
26
+ self.func_name = func_name
27
+ self.func_args = func_args
28
+ self.func_kwargs = func_kwargs
29
+ self.trace: TxTrace = trace
30
+
31
+ def __str__(self):
32
+ return (f'{self.__class__.__name__}({self.hex_tx_hash} func={self.func_name}, '
33
+ f'{self.func_name=}, {self.func_kwargs=}, {self.trace=})')
34
+
35
+ def __repr__(self):
36
+ return (f'{self.__class__.__name__}({self.hex_tx_hash} func={self.func_name}, '
37
+ f'{self.func_name=}, {self.func_kwargs=}, {self.trace=})')
38
+
39
+
40
+ class FailedToGetGasPrice(Web3InterfaceException):
41
+ pass
42
+
43
+
44
+ class MaximumRPCInEachBracketReached(Web3InterfaceException):
45
+ pass
46
+
47
+
48
+ class AtLastProvideOneValidRPCInEachBracket(Web3InterfaceException):
49
+ pass
50
+
51
+
52
+ class TransactionValueError(Web3InterfaceException):
53
+ pass
54
+
55
+
56
+ class GetBlockFailed(Web3InterfaceException):
57
+ pass
58
+
59
+
60
+ class DontHaveThisRpcType(Web3InterfaceException):
61
+ pass
62
+
63
+
64
+ class NotValidViewPolicy(Web3InterfaceException):
65
+ pass
66
+
67
+
68
+ class TransactionTypeNotSupportedInMultiCall(Web3InterfaceException):
69
+ pass
70
+
71
+
72
+ class KwargsNotSupportedInMultiCall(Web3InterfaceException):
73
+ pass
74
+
75
+
76
+ class FailedToGetGasFromApi(Web3InterfaceException):
77
+ pass
@@ -0,0 +1,151 @@
1
+ import logging
2
+ from _decimal import Decimal
3
+ from decimal import Decimal
4
+ from typing import Callable, Dict, List, Optional, Union
5
+
6
+ import requests
7
+ from aiohttp import ClientResponseError
8
+ from requests import ConnectionError, JSONDecodeError, ReadTimeout, RequestException
9
+ from web3 import AsyncWeb3, Web3
10
+ from web3.types import Wei
11
+
12
+ from .constants import ChainIdToGas, DEFAULT_API_PROVIDER, DevEnv, FixedValueGas, GasEstimationLogger, \
13
+ GasEstimationMethod, \
14
+ GasFromRpcChainIds, GasMultiplierHigh, GasMultiplierLow, GasMultiplierMedium, RequestTimeout
15
+ from .exceptions import FailedToGetGasFromApi, FailedToGetGasPrice, OutOfRangeTransactionFee
16
+ from .utils import TxPriority
17
+
18
+
19
+ class GasEstimation:
20
+
21
+ def __init__(
22
+ self,
23
+ chain_id: int,
24
+ providers: List[AsyncWeb3],
25
+ default_method: Optional[GasEstimationMethod] = None,
26
+ apm_client=None,
27
+ gas_multiplier_low: Union[float, Decimal] = GasMultiplierLow,
28
+ gas_multiplier_medium: Union[float, Decimal] = GasMultiplierMedium,
29
+ gas_multiplier_high: Union[float, Decimal] = GasMultiplierHigh,
30
+ gas_api_provider: str = DEFAULT_API_PROVIDER,
31
+ log_level: logging = logging.WARN
32
+ ):
33
+ self.gas_api_provider = gas_api_provider
34
+ self.chain_id = chain_id
35
+ self.providers = providers
36
+ self.default_method: GasEstimationMethod = default_method
37
+ self.apm = apm_client
38
+ self.multipliers = {
39
+ TxPriority.Low: gas_multiplier_low,
40
+ TxPriority.Medium: gas_multiplier_medium,
41
+ TxPriority.High: gas_multiplier_high,
42
+ }
43
+ self.gas_estimation_method: Dict[GasEstimationMethod, Callable] = {
44
+ GasEstimationMethod.GAS_API_PROVIDER: self._get_gas_from_api,
45
+ GasEstimationMethod.RPC: self._get_gas_from_rpc,
46
+ GasEstimationMethod.FIXED: self._get_fixed_value,
47
+ GasEstimationMethod.CUSTOM: self._custom_gas_estimation,
48
+ }
49
+ self.method_sorted_priority = [
50
+ GasEstimationMethod.GAS_API_PROVIDER,
51
+ GasEstimationMethod.RPC,
52
+ GasEstimationMethod.FIXED,
53
+ GasEstimationMethod.CUSTOM
54
+ ]
55
+ GasEstimationLogger.setLevel(log_level)
56
+
57
+ def __logger_params(self, **kwargs):
58
+ if self.apm:
59
+ self.apm.span_label(**kwargs)
60
+ else:
61
+ GasEstimationLogger.info(f'params={kwargs}')
62
+
63
+ async def _get_gas_from_api(self, priority: TxPriority, gas_upper_bound: Union[float, Decimal]) -> Dict[str, Wei]:
64
+ gas_provider = self.gas_api_provider.format(chain_id=self.chain_id)
65
+ resp = None
66
+ try:
67
+ resp = requests.get(gas_provider, timeout=RequestTimeout)
68
+ if resp.status_code != 200:
69
+ raise FailedToGetGasFromApi(f'failed to get gas with {resp.status_code=} on {gas_provider=}')
70
+ resp_json = resp.json()
71
+ max_fee_per_gas = Decimal(resp_json[priority.value]["suggestedMaxFeePerGas"])
72
+ max_priority_fee_per_gas = Decimal(resp_json[priority.value]["suggestedMaxPriorityFeePerGas"])
73
+ base_fee = Decimal(resp_json[priority.value]["estimatedBaseFee"])
74
+ self.__logger_params(
75
+ max_fee_per_gas=max_fee_per_gas,
76
+ max_priority_fee_per_gas=max_priority_fee_per_gas,
77
+ gas_price_provider=gas_provider,
78
+ )
79
+ if max_fee_per_gas > gas_upper_bound:
80
+ raise OutOfRangeTransactionFee(
81
+ f"gas price exceeded. {gas_upper_bound=} but it is {max_fee_per_gas}"
82
+ )
83
+ gas_params = {
84
+ 'baseFee': Web3.to_wei(base_fee, "GWei"),
85
+ "maxFeePerGas": Web3.to_wei(max_fee_per_gas, "GWei"),
86
+ "maxPriorityFeePerGas": Web3.to_wei(max_priority_fee_per_gas, "GWei"),
87
+ }
88
+ return gas_params
89
+ except (RequestException, JSONDecodeError, KeyError) as e:
90
+ if not DevEnv:
91
+ GasEstimationLogger.exception(f'Failed to get gas info from api({self.chain_id=}) {resp.status_code=}')
92
+ raise FailedToGetGasPrice(f"Failed to get gas info from api({self.chain_id=}): {e}")
93
+
94
+ async def _get_gas_from_rpc(self, priority: TxPriority, gas_upper_bound: Union[float, Decimal]) -> Dict[str, Wei]:
95
+ gas_price = None
96
+ found_gas_below_upper_bound = False
97
+
98
+ for provider in self.providers: # type: AsyncWeb3
99
+ rpc_url = provider.provider.endpoint_uri
100
+ try:
101
+ gas_price = await provider.eth.gas_price
102
+ self.__logger_params(gas_price=str(gas_price / 1e9), gas_price_provider=rpc_url)
103
+ if gas_price / 1e9 <= gas_upper_bound:
104
+ found_gas_below_upper_bound = True
105
+ break
106
+ except (ConnectionError, ReadTimeout, ValueError, ConnectionResetError) as e:
107
+ GasEstimationLogger.error(f"Failed to get gas price from {rpc_url}, {e=}")
108
+ except ClientResponseError as e:
109
+ if e.message.startswith("Too Many Requests"):
110
+ GasEstimationLogger.error(f"Failed to get gas price from {rpc_url}, {e=}")
111
+ raise
112
+
113
+ if gas_price is None:
114
+ raise FailedToGetGasPrice("Non of RCP could provide gas price!")
115
+ if not found_gas_below_upper_bound:
116
+ raise OutOfRangeTransactionFee(
117
+ f"gas price exceeded. {gas_upper_bound=} but it is {gas_price / 1e9}"
118
+ )
119
+ return {'gasPrice': Wei(int(gas_price * self.multipliers.get(priority, 1)))}
120
+
121
+ async def _get_fixed_value(self, priority: TxPriority, gas_upper_bound: Union[float, Decimal]) -> Dict[str, Wei]:
122
+ gas = ChainIdToGas.get(self.chain_id) or FixedValueGas
123
+ if gas > gas_upper_bound:
124
+ raise OutOfRangeTransactionFee(f"gas price exceeded. {gas_upper_bound=} but it is {gas}")
125
+ return {"gasPrice": Web3.to_wei(gas * self.multipliers.get(priority, 1), "GWei")}
126
+
127
+ async def _custom_gas_estimation(self, priority: TxPriority, gas_upper_bound: Union[float, Decimal]):
128
+ raise NotImplemented()
129
+
130
+ async def get_gas_price(
131
+ self, gas_upper_bound: int, priority: TxPriority, method: GasEstimationMethod = None
132
+ ) -> Dict[str, Wei]:
133
+ if method := self.gas_estimation_method.get(method) or self.gas_estimation_method.get(self.default_method):
134
+ try:
135
+ return await method(priority, gas_upper_bound)
136
+ except FailedToGetGasPrice as e:
137
+ raise e
138
+ gas_params = {}
139
+
140
+ if DevEnv or self.chain_id in GasFromRpcChainIds:
141
+ return await self._get_gas_from_rpc(priority, gas_upper_bound)
142
+ for method_key in self.method_sorted_priority:
143
+ try:
144
+ gas_params = await self.gas_estimation_method[method_key](priority, gas_upper_bound)
145
+ break
146
+ except (FailedToGetGasPrice, OutOfRangeTransactionFee) as e:
147
+ GasEstimationLogger.warning(f"This method({method_key}) failed to provide gas with this error: {e}")
148
+ continue
149
+ if not gas_params:
150
+ raise FailedToGetGasPrice("All of methods failed to estimate gas")
151
+ return gas_params
@@ -0,0 +1,135 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Dict, List, Optional, Union
4
+
5
+ from eth_typing import Address, ChecksumAddress
6
+ from web3._utils.contracts import encode_transaction_data # noqa
7
+ from web3.types import BlockData, BlockIdentifier, TxReceipt
8
+
9
+ from . import BaseMultiRpc
10
+ from .base_multi_rpc_interface import BaseContractFunction
11
+ from .constants import GasLimit, GasUpperBound, ViewPolicy
12
+ from .exceptions import DontHaveThisRpcType, KwargsNotSupportedInMultiCall, TransactionTypeNotSupportedInMultiCall
13
+ from .gas_estimation import GasEstimation, GasEstimationMethod
14
+ from .utils import ContractFunctionType, NestedDict, TxPriority, thread_safe
15
+
16
+
17
+ class MultiRpc(BaseMultiRpc):
18
+ """
19
+ This class is used to be more sure when running web3 view calls and sending transactions by using of multiple RPCs.
20
+ """
21
+
22
+ @thread_safe
23
+ def __init__(
24
+ self,
25
+ rpc_urls: NestedDict,
26
+ contract_address: Union[Address, ChecksumAddress, str],
27
+ contract_abi: Dict,
28
+ rpcs_supporting_tx_trace: Optional[List[str]] = None,
29
+ view_policy: ViewPolicy = ViewPolicy.MostUpdated,
30
+ gas_estimation: Optional[GasEstimation] = None,
31
+ gas_limit: int = GasLimit,
32
+ gas_upper_bound: int = GasUpperBound,
33
+ apm=None,
34
+ enable_estimate_gas_limit: bool = False,
35
+ is_proof_authority: bool = False,
36
+ multicall_custom_address: str = None,
37
+ log_level: logging = logging.WARN,
38
+ setup_on_init: bool = True
39
+ ):
40
+ super().__init__(rpc_urls, contract_address, contract_abi, rpcs_supporting_tx_trace,
41
+ view_policy, gas_estimation, gas_limit,
42
+ gas_upper_bound, apm, enable_estimate_gas_limit,
43
+ is_proof_authority, multicall_custom_address, log_level)
44
+
45
+ for func_abi in self.contract_abi:
46
+ if func_abi.get("stateMutability") in ("view", "pure"):
47
+ function_type = ContractFunctionType.View
48
+ elif func_abi.get("type") == "function":
49
+ function_type = ContractFunctionType.Transaction
50
+ else:
51
+ continue
52
+ self.functions.__setattr__(
53
+ func_abi["name"],
54
+ self.ContractFunction(func_abi["name"], func_abi, self, function_type),
55
+ )
56
+ if setup_on_init:
57
+ self.setup()
58
+
59
+ @thread_safe
60
+ def setup(self) -> None:
61
+ return asyncio.run(super().setup())
62
+
63
+ @thread_safe
64
+ def get_nonce(self, address: Union[Address, ChecksumAddress, str]) -> int:
65
+ return asyncio.run(super()._get_nonce(address))
66
+
67
+ @thread_safe
68
+ def get_tx_receipt(self, tx_hash) -> TxReceipt:
69
+ return asyncio.run(super().get_tx_receipt(tx_hash))
70
+
71
+ @thread_safe
72
+ def get_block(self, block_identifier: BlockIdentifier = 'latest', full_transactions: bool = False) -> BlockData:
73
+ return asyncio.run(super().get_block(block_identifier, full_transactions))
74
+
75
+ @thread_safe
76
+ def get_block_number(self) -> int:
77
+ return asyncio.run((super().get_block_number()))
78
+
79
+ class ContractFunction(BaseContractFunction):
80
+ def __call__(self, *args, **kwargs):
81
+ cf = MultiRpc.ContractFunction(self.name, self.abi, self.mr, self.typ)
82
+ cf.args = args
83
+ cf.kwargs = kwargs
84
+ return cf
85
+
86
+ @thread_safe
87
+ def call(
88
+ self,
89
+ address: str = None,
90
+ private_key: str = None,
91
+ gas_limit: int = None,
92
+ gas_upper_bound: int = None,
93
+ wait_for_receipt: int = 90,
94
+ priority: TxPriority = TxPriority.Low,
95
+ gas_estimation_method: GasEstimationMethod = None,
96
+ block_identifier: Union[str, int] = 'latest',
97
+ enable_estimate_gas_limit: Optional[bool] = None,
98
+ use_multicall=False,
99
+ ):
100
+ if self.mr.providers.get(self.typ) is None:
101
+ raise DontHaveThisRpcType(f"Doesn't have {self.typ} RPCs")
102
+ if self.typ == ContractFunctionType.View:
103
+ return asyncio.run(self.mr._call_view_function(
104
+ self.name, block_identifier, use_multicall, *self.args, **self.kwargs,
105
+ ))
106
+ elif self.typ == ContractFunctionType.Transaction:
107
+ return asyncio.run(self.mr._call_tx_function(
108
+ func_name=self.name,
109
+ func_args=self.args,
110
+ func_kwargs=self.kwargs,
111
+ address=address or self.mr.address,
112
+ private_key=private_key or self.mr.private_key,
113
+ gas_limit=gas_limit or self.mr.gas_limit,
114
+ gas_upper_bound=gas_upper_bound or self.mr.gas_upper_bound,
115
+ wait_for_receipt=wait_for_receipt,
116
+ priority=priority,
117
+ gas_estimation_method=gas_estimation_method,
118
+ enable_estimate_gas_limit=enable_estimate_gas_limit
119
+ ))
120
+
121
+ @thread_safe
122
+ def multicall(
123
+ self,
124
+ block_identifier: Union[str, int] = 'latest',
125
+ ):
126
+ if self.mr.providers.get(self.typ) is None:
127
+ raise DontHaveThisRpcType(f"Doesn't have {self.typ} RPCs")
128
+ if self.kwargs != {}:
129
+ raise KwargsNotSupportedInMultiCall
130
+ if self.typ == ContractFunctionType.View:
131
+ return asyncio.run(self.mr._call_view_function(
132
+ self.name, block_identifier, True, *self.args, **self.kwargs,
133
+ ))
134
+ elif self.typ == ContractFunctionType.Transaction:
135
+ raise TransactionTypeNotSupportedInMultiCall
@@ -0,0 +1,153 @@
1
+ import requests
2
+
3
+ from src.multirpc.constants import MultiRPCLogger
4
+
5
+
6
+ class TxTrace:
7
+ """
8
+ geth trace transaction sample output :
9
+
10
+ {
11
+ "jsonrpc": "2.0",
12
+ "id": 1,
13
+ "result": {
14
+ "from": "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65",
15
+ "gas": "0x989680",
16
+ "gasUsed": "0xca4b",
17
+ "to": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0",
18
+ "input": "0x3f65c7f4000000000000000000000000ba55e57bb198a641135e9dc9e96ebff834cab11000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006590012200000000000000000000000000000000000000000000000000000000000001a0ffffffffffffffffffffffffffffffffffffffffffffffffe440925e7ed57800ffffffffffffffffffffffffffffffffffffffffffffffffe440925e7ed5780000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000220891d24df965ef1367f4c8d974f394b0a43f7be3a2767ba887cef8dd2adfd89c000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906000000000000000000000000392118fc9f5acf2b2b9e509804bb7e68253635b80000000000000000000000000000000000000000000000000000000000000020ad0a18a3d1d4340dcc24a08636a2782e2edf6d8e5434939763ebf402b166d7e40000000000000000000000000000000000000000000000000000000000000020b01bc3cb7585f01e38c0af92705ce6799b89ab1cd49955757b4094468941f8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000417de46c7e53aa9365ab17de1e6f3efedd87d67fd77b341d3f4bd5919cd19805ae2a28c3b17771caf6e661db72e6f684cc6caba3e40e1436d591e98e0f1e73ec2b1b00000000000000000000000000000000000000000000000000000000000000",
19
+ "output": "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000234c69717569646174696f6e46616365743a2050617274794120697320736f6c76656e740000000000000000000000000000000000000000000000000000000000",
20
+ "error": "execution reverted",
21
+ "revertReason": "LiquidationFacet: PartyA is solvent",
22
+ "calls": [
23
+ {
24
+ "from": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0",
25
+ "gas": "0x95b6b0",
26
+ "gasUsed": "0x4aa7",
27
+ "to": "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853",
28
+ "input": "0x3f65c7f4000000000000000000000000ba55e57bb198a641135e9dc9e96ebff834cab11000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000006590012200000000000000000000000000000000000000000000000000000000000001a0ffffffffffffffffffffffffffffffffffffffffffffffffe440925e7ed57800ffffffffffffffffffffffffffffffffffffffffffffffffe440925e7ed5780000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000220891d24df965ef1367f4c8d974f394b0a43f7be3a2767ba887cef8dd2adfd89c000000000000000000000000090f79bf6eb2c4f870365e785982e1f101e93b906000000000000000000000000392118fc9f5acf2b2b9e509804bb7e68253635b80000000000000000000000000000000000000000000000000000000000000020ad0a18a3d1d4340dcc24a08636a2782e2edf6d8e5434939763ebf402b166d7e40000000000000000000000000000000000000000000000000000000000000020b01bc3cb7585f01e38c0af92705ce6799b89ab1cd49955757b4094468941f8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000417de46c7e53aa9365ab17de1e6f3efedd87d67fd77b341d3f4bd5919cd19805ae2a28c3b17771caf6e661db72e6f684cc6caba3e40e1436d591e98e0f1e73ec2b1b00000000000000000000000000000000000000000000000000000000000000",
29
+ "output": "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000234c69717569646174696f6e46616365743a2050617274794120697320736f6c76656e740000000000000000000000000000000000000000000000000000000000",
30
+ "error": "execution reverted",
31
+ "revertReason": "LiquidationFacet: PartyA is solvent",
32
+ "value": "0x0",
33
+ "type": "DELEGATECALL"
34
+ }
35
+ ],
36
+ "value": "0x0",
37
+ "type": "CALL"
38
+ }
39
+ }
40
+ """
41
+
42
+ def __init__(self, tx_hash, rpc: str):
43
+ self.tx_hash = tx_hash
44
+ self.rpc = rpc
45
+ self.response = self.tx_trace()
46
+ self._json = None
47
+
48
+ def __repr__(self):
49
+ return f'{self.tx_hash}-{self.response and self.response.text}'
50
+
51
+ def __str__(self):
52
+ return f'{self.tx_hash}-{self.response and self.response.text}'
53
+
54
+ def tx_trace(self):
55
+ try:
56
+ data = {
57
+ "id": 1,
58
+ "jsonrpc": "2.0",
59
+ "method": "debug_traceTransaction",
60
+ "params": [
61
+ self.tx_hash,
62
+ {"tracer": 'callTracer', "disableStack": False, "disableStorage": True}
63
+ ]
64
+ }
65
+
66
+ response = requests.post(self.rpc, json=data)
67
+ if response.status_code == 200:
68
+ if error := response.json().get('error'):
69
+ MultiRPCLogger.error(f'failed to get tx({self.tx_hash}) trace with error: {error}')
70
+ return None
71
+ return response
72
+ MultiRPCLogger.error(f'tx_trace({self.tx_hash}) status = {response.status_code}, \n {response.json()}')
73
+
74
+ except requests.HTTPError:
75
+ MultiRPCLogger.exception('Exception in debug_traceTransaction')
76
+
77
+ def ok(self):
78
+ return bool(self.response)
79
+
80
+ def text(self):
81
+ if self.ok():
82
+ return self.response.text
83
+ return ''
84
+
85
+ def json(self) -> dict:
86
+ if self._json:
87
+ return self._json
88
+ if self.ok():
89
+ self._json = self.response.json()
90
+ else:
91
+ self._json = {}
92
+ return self._json
93
+
94
+ def result(self):
95
+ return TxTraceResult(result=self.json().get('result') or {})
96
+
97
+
98
+ class TxTraceResult:
99
+
100
+ def __init__(self, result):
101
+ self._json = result
102
+
103
+ def __repr__(self):
104
+ return f'{self._json}'
105
+
106
+ def get(self, key, default=None):
107
+ return self._json.get(key, default)
108
+
109
+ def error(self):
110
+ return self.get('error')
111
+
112
+ def revert_reason(self):
113
+ return self.get('revertReason', '')
114
+
115
+ def from_(self):
116
+ return self.get('from')
117
+
118
+ def to(self):
119
+ return self.get('to')
120
+
121
+ def gas_used(self):
122
+ return self.get('gasUsed')
123
+
124
+ def calls(self) -> list['TxTraceResult']:
125
+ return [TxTraceResult(result=call) for call in self.get('calls', [])]
126
+
127
+ def all_revert_reasons(self) -> list:
128
+ revert_reasons = []
129
+
130
+ if current_reason := self.get('revertReason'):
131
+ revert_reasons.append(current_reason)
132
+
133
+ if calls := self.calls():
134
+ for child_call in calls:
135
+ revert_reasons += child_call.all_revert_reasons()
136
+
137
+ return revert_reasons
138
+
139
+ def short_error(self):
140
+ return f'error={self.error()} revertReason={self.revert_reason()}'
141
+
142
+ def long_error(self):
143
+ revert_reasons = self.all_revert_reasons()
144
+ return f'error={self.error()} revert-reasons={revert_reasons}'
145
+
146
+ def first_usable_error(self):
147
+ for error in [self.error()] + self.all_revert_reasons():
148
+ if error and \
149
+ 'execution reverted' not in error and \
150
+ 'MultiAccount: Error occurred' not in error and \
151
+ 'Execution reverted' not in error:
152
+ return error
153
+ return ''