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.
- solver_multirpc-3.1.4.dist-info/METADATA +241 -0
- solver_multirpc-3.1.4.dist-info/RECORD +19 -0
- solver_multirpc-3.1.4.dist-info/WHEEL +4 -0
- src/__init__.py +0 -0
- src/multirpc/__init__.py +1 -0
- src/multirpc/async_multi_rpc_interface.py +125 -0
- src/multirpc/base_multi_rpc_interface.py +640 -0
- src/multirpc/constants.py +52 -0
- src/multirpc/exceptions.py +77 -0
- src/multirpc/gas_estimation.py +151 -0
- src/multirpc/sync_multi_rpc_interface.py +135 -0
- src/multirpc/tx_trace.py +153 -0
- src/multirpc/utils.py +265 -0
- src/tests/__init__.py +0 -0
- src/tests/abi.json +34 -0
- src/tests/constants.py +78 -0
- src/tests/contract.sol +16 -0
- src/tests/test.py +138 -0
- src/tests/test_settings.py.example +7 -0
|
@@ -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
|
src/multirpc/tx_trace.py
ADDED
|
@@ -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 ''
|