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.
src/multirpc/utils.py ADDED
@@ -0,0 +1,265 @@
1
+ import asyncio
2
+ import enum
3
+ import json
4
+ import time
5
+ import traceback
6
+ from dataclasses import dataclass
7
+ from functools import reduce, wraps
8
+ from threading import Thread
9
+ from typing import Any, Dict, List, Tuple, Union
10
+
11
+ import aiohttp.client_exceptions
12
+ from aiohttp import ClientTimeout
13
+ from eth_typing import URI
14
+ from web3 import AsyncHTTPProvider, AsyncWeb3, Web3, WebSocketProvider
15
+ from web3._utils.http import DEFAULT_HTTP_TIMEOUT
16
+ from web3._utils.http_session_manager import HTTPSessionManager
17
+ from web3.middleware import ExtraDataToPOAMiddleware
18
+
19
+ from .constants import MaxRPCInEachBracket, MultiRPCLogger
20
+ from .exceptions import AtLastProvideOneValidRPCInEachBracket, MaximumRPCInEachBracketReached
21
+
22
+
23
+ def get_span_proper_label_from_provider(endpoint_uri):
24
+ return endpoint_uri.split("//")[-1].replace(".", "__").replace("/", "__")
25
+
26
+
27
+ class ReturnableThread(Thread):
28
+ def __init__(self, target, args=(), kwargs=None):
29
+ super().__init__(target=target, args=args, kwargs=kwargs)
30
+ self.target = target
31
+ self.args = args
32
+ self.kwargs = kwargs if kwargs is not None else {}
33
+ self.result = None
34
+ self._exception = None
35
+
36
+ def run(self) -> None:
37
+ try:
38
+ self.result = self.target(*self.args, **self.kwargs)
39
+ except Exception as e:
40
+ self._exception = e
41
+ traceback.print_exc()
42
+
43
+ def join(self, *args, **kwargs):
44
+ super().join(*args, **kwargs)
45
+ if self._exception:
46
+ raise self._exception
47
+ return self.result
48
+
49
+
50
+ def thread_safe(func):
51
+ @wraps(func)
52
+ def wrapper(*args, **kwargs):
53
+ event_loop = asyncio._get_running_loop()
54
+ if event_loop is None:
55
+ return func(*args, **kwargs)
56
+ t = ReturnableThread(target=func, args=args, kwargs=kwargs)
57
+ t.start()
58
+ return t.join()
59
+
60
+ return wrapper
61
+
62
+
63
+ class ResultEvent(asyncio.Event):
64
+ def __init__(self):
65
+ super().__init__()
66
+ self.result_ = None
67
+
68
+ def set_result(self, result):
69
+ self.result_ = result
70
+
71
+ def get_result(self):
72
+ return self.result_
73
+
74
+
75
+ def get_unix_time():
76
+ return int(time.time() * 1000)
77
+
78
+
79
+ class TxPriority(enum.Enum):
80
+ Low = "low"
81
+ Medium = "medium"
82
+ High = "high"
83
+
84
+
85
+ class ContractFunctionType:
86
+ View = "view"
87
+ Transaction = "transaction"
88
+
89
+
90
+ class NestedDict:
91
+ def __init__(self, data: Dict = None):
92
+ if data is None:
93
+ data = dict()
94
+ self.data = data
95
+
96
+ def __getitem__(self, keys: Union[Tuple[any], any]):
97
+ if not isinstance(keys, tuple):
98
+ keys = (keys,)
99
+ result = self.data
100
+ for key in keys:
101
+ result = result[key]
102
+ return result
103
+
104
+ def __setitem__(self, keys: Union[Tuple[any], any], value) -> None:
105
+ if not isinstance(keys, tuple):
106
+ keys = (keys,)
107
+ current_dict = self.data
108
+ for key in keys[:-1]:
109
+ if not isinstance(current_dict.get(key), dict):
110
+ current_dict[key] = {}
111
+ current_dict = current_dict[key]
112
+ current_dict[keys[-1]] = value
113
+
114
+ def get(self, keys, default=None):
115
+ if not isinstance(keys, tuple):
116
+ keys = (keys,)
117
+ current_dict = self.data
118
+ for key in keys:
119
+ try:
120
+ current_dict = current_dict[key]
121
+ except KeyError:
122
+ return default
123
+ return current_dict
124
+
125
+ def items(self):
126
+ def get_items_recursive(data, current_keys=()):
127
+ for key, value in data.items():
128
+ if isinstance(value, dict):
129
+ yield from get_items_recursive(value, current_keys + (key,))
130
+ else:
131
+ yield current_keys + (key,), value
132
+
133
+ return get_items_recursive(self.data)
134
+
135
+ def __str__(self):
136
+ return str(self.data)
137
+
138
+ def __repr__(self):
139
+ return json.dumps(self.data, indent=1)
140
+
141
+
142
+ class MultiRpcHTTPSessionManager(HTTPSessionManager):
143
+ """
144
+ This class extends the default HTTPSessionManager used by Web3 to ensure that
145
+ the aiohttp ClientSession is always closed—even in the case of a failure or
146
+ cancellation. By placing session closure inside a 'finally' block in both
147
+ 'async_make_post_request' and 'async_json_make_get_request', we guarantee
148
+ proper cleanup of network connections and resources, preventing potential
149
+ resource leaks or connection pooling issues if an exception is raised during
150
+ the request or the task is cancelled.
151
+
152
+ NOTE: It's based on web3==7.7.0 . If you update web3 check if it's compatible.
153
+ """
154
+
155
+ async def async_make_post_request(
156
+ self, endpoint_uri: URI, data: Union[bytes, Dict[str, Any]], **kwargs: Any
157
+ ) -> bytes:
158
+ kwargs.setdefault("timeout", ClientTimeout(DEFAULT_HTTP_TIMEOUT))
159
+ session = await self.async_cache_and_return_session(
160
+ endpoint_uri, request_timeout=kwargs["timeout"]
161
+ )
162
+
163
+ try:
164
+ self.logger.debug(f'making post request, {endpoint_uri=}, {kwargs=}')
165
+ response = await session.post(endpoint_uri, **dict(**kwargs, data=data))
166
+ response.raise_for_status()
167
+ return await response.read()
168
+ finally:
169
+ self.logger.debug(f'task is done/canceled, session will close, {session=}')
170
+ if not session.closed:
171
+ await session.close()
172
+
173
+ async def async_json_make_get_request(
174
+ self, endpoint_uri: URI, *args: Any, **kwargs: Any
175
+ ) -> Dict[str, Any]:
176
+ kwargs.setdefault("timeout", ClientTimeout(DEFAULT_HTTP_TIMEOUT))
177
+ session = await self.async_cache_and_return_session(
178
+ endpoint_uri, request_timeout=kwargs["timeout"]
179
+ )
180
+ try:
181
+ response = await session.get(endpoint_uri, *args, **kwargs)
182
+ response.raise_for_status()
183
+ return await response.json()
184
+ finally:
185
+ self.logger.debug(f'task is done/canceled, {session=}')
186
+ if not session.closed:
187
+ await session.close()
188
+
189
+
190
+ class MultiRpcAsyncHTTPProvider(AsyncHTTPProvider):
191
+ """
192
+ NOTE: It's based on web3==7.7.0 . If you update web3 check if it's compatible.
193
+ """
194
+
195
+ def __init__(self, *args, **kwargs):
196
+ super().__init__(*args, **kwargs)
197
+ self._request_session_manager = MultiRpcHTTPSessionManager()
198
+
199
+
200
+ async def create_web3_from_rpc(rpc_urls: NestedDict, is_proof_of_authority: bool) -> NestedDict:
201
+ async def create_web3(rpc_: str):
202
+ async_w3: AsyncWeb3
203
+ if rpc_.startswith("http"):
204
+ async_w3 = AsyncWeb3(MultiRpcAsyncHTTPProvider(rpc_))
205
+ else:
206
+ async_w3 = AsyncWeb3(WebSocketProvider(rpc_))
207
+ if is_proof_of_authority:
208
+ async_w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
209
+ try:
210
+ status = await async_w3.is_connected()
211
+ except (asyncio.exceptions.TimeoutError, aiohttp.client_exceptions.ClientResponseError):
212
+ status = False
213
+ return async_w3, status
214
+
215
+ providers = NestedDict()
216
+ for key, rpcs in rpc_urls.items():
217
+ valid_rpcs = []
218
+
219
+ if len(rpcs) > MaxRPCInEachBracket:
220
+ raise MaximumRPCInEachBracketReached
221
+
222
+ for i, rpc in enumerate(rpcs):
223
+ w3, w3_connected = await create_web3(rpc)
224
+ if not w3_connected:
225
+ MultiRPCLogger.warning(f"This rpc({rpc}) doesn't work")
226
+ continue
227
+ valid_rpcs.append(w3)
228
+
229
+ if len(valid_rpcs) == 0:
230
+ raise AtLastProvideOneValidRPCInEachBracket
231
+
232
+ providers[key] = valid_rpcs
233
+
234
+ return providers
235
+
236
+
237
+ async def calculate_chain_id(providers: NestedDict) -> int:
238
+ last_error = None
239
+ for key, providers in providers.items():
240
+ for provider in providers:
241
+ try:
242
+ return await asyncio.wait_for(provider.eth.chain_id, timeout=2)
243
+ except asyncio.TimeoutError as e:
244
+ last_error = e
245
+ MultiRPCLogger.warning(f"Can't acquire chain id from this RPC {provider.provider.endpoint_uri}")
246
+ raise last_error
247
+
248
+
249
+ def reduce_list_of_list(ls: List[List]) -> List[any]:
250
+ return reduce(lambda ps, p: ps + p, ls)
251
+
252
+
253
+ @dataclass
254
+ class ChainConfigTest:
255
+ name: str
256
+ contract_address: str
257
+ rpc: NestedDict
258
+ tx_hash: str
259
+ is_proof_authority: bool = False
260
+ multicall_address: str = None
261
+
262
+ def __post_init__(self):
263
+ self.contract_address = Web3.to_checksum_address(self.contract_address)
264
+ if self.multicall_address:
265
+ self.multicall_address = Web3.to_checksum_address(self.multicall_address)
src/tests/__init__.py ADDED
File without changes
src/tests/abi.json ADDED
@@ -0,0 +1,34 @@
1
+ [
2
+ {
3
+ "inputs": [
4
+ {
5
+ "internalType": "uint256",
6
+ "name": "value",
7
+ "type": "uint256"
8
+ }
9
+ ],
10
+ "name": "set",
11
+ "outputs": [],
12
+ "stateMutability": "nonpayable",
13
+ "type": "function"
14
+ },
15
+ {
16
+ "inputs": [
17
+ {
18
+ "internalType": "address",
19
+ "name": "",
20
+ "type": "address"
21
+ }
22
+ ],
23
+ "name": "map",
24
+ "outputs": [
25
+ {
26
+ "internalType": "uint256",
27
+ "name": "",
28
+ "type": "uint256"
29
+ }
30
+ ],
31
+ "stateMutability": "view",
32
+ "type": "function"
33
+ }
34
+ ]
src/tests/constants.py ADDED
@@ -0,0 +1,78 @@
1
+ import json
2
+
3
+ from src.multirpc.utils import ChainConfigTest, NestedDict
4
+
5
+ # Arbitrum Configuration
6
+ ArbConfig = ChainConfigTest(
7
+ 'Arbitrum',
8
+ '0xCFE3c06Fe982A7D16ce3826C64c5f0730054Dc95',
9
+ NestedDict({
10
+ "view": {
11
+ 1: ['https://1rpc.io/arb', 'https://rpc.ankr.com/arbitrum', 'https://arbitrum.drpc.org'],
12
+ },
13
+ "transaction": {
14
+ 1: ['https://1rpc.io/arb', 'https://rpc.ankr.com/arbitrum', 'https://arbitrum.drpc.org'],
15
+ }
16
+ }),
17
+ '0xbc0f34536fdf5d2593081b112d49d714993d879032e0e9c6998afc3110b7f0ed'
18
+ )
19
+
20
+ # Polygon Configuration
21
+ PolyConfig = ChainConfigTest(
22
+ 'Polygon',
23
+ '0xCa7DFDc4dB0F27484Cf5EEa1CdF380301Ef07Ce2',
24
+ NestedDict({
25
+ "view": {
26
+ 1: ['https://1rpc.io/matic', 'https://polygon-rpc.com'],
27
+ },
28
+ "transaction": {
29
+ 1: ['https://1rpc.io/matic', 'https://polygon-rpc.com'],
30
+ }
31
+ }),
32
+ '0x4b8756bd1d32f62b2b9e3b46b80917bd3de4fd95695bad33e483293284f28678',
33
+ is_proof_authority=True
34
+ )
35
+
36
+ # Base Configuration
37
+ BaseConfig = ChainConfigTest(
38
+ 'Base',
39
+ '0x1d58e7F58d085c87E34b18DAe5A6D08d187cbcbe',
40
+ NestedDict({
41
+ "view": {
42
+ 1: ['https://base-rpc.publicnode.com', 'https://base.drpc.org'],
43
+ },
44
+ "transaction": {
45
+ 1: ['https://base-rpc.publicnode.com', 'https://base.drpc.org'],
46
+ }
47
+ }),
48
+ '0xbd342d36d503af057cd79fd4f252b4629d6013d0748a2742dc99c9fcbe522072',
49
+ is_proof_authority=True
50
+ )
51
+
52
+ # Mantle Configuration
53
+ MantleConfig = ChainConfigTest(
54
+ 'Mantle',
55
+ '0x535D41D93cDc0818Ad8Eeb452B74e502A5742874',
56
+ NestedDict({
57
+ "view": {
58
+ 1: ['https://1rpc.io/mantle', 'https://mantle.drpc.org'],
59
+ },
60
+ "transaction": {
61
+ 1: ['https://1rpc.io/mantle', 'https://mantle.drpc.org'],
62
+ }
63
+ }),
64
+ '0x9f33a56be9983753abebbe8fb048601a141097289d96b9844afb36e68f72ef82',
65
+ is_proof_authority=False,
66
+ )
67
+
68
+ RPCsSupportingTxTrace = [
69
+ 'https://arbitrum.drpc.org', # Arbitrum
70
+ 'https://polygon-rpc.com', # Polygon
71
+ 'https://base.drpc.org', # Base
72
+ 'https://mantle.drpc.org' # Mantle
73
+ ]
74
+
75
+ with open("tests/abi.json", "r") as f:
76
+ abi = json.load(f)
77
+
78
+ PreviousBlock = 3
src/tests/contract.sol ADDED
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: UNLICENSED
2
+ pragma solidity ^0.8.9;
3
+
4
+ contract Mapping {
5
+ mapping(address => uint256) public map;
6
+
7
+ function set(uint256 value) public {
8
+ // Revert if the input is exactly one byte of 0x00
9
+ require(
10
+ value >= 10,
11
+ "Error: 10 < value is not allowed."
12
+ );
13
+
14
+ map[msg.sender] = value;
15
+ }
16
+ }
src/tests/test.py ADDED
@@ -0,0 +1,138 @@
1
+ import asyncio
2
+ import logging
3
+ import random
4
+
5
+ from eth_account import Account
6
+ from web3.exceptions import MismatchedABI
7
+
8
+ from src.multirpc.async_multi_rpc_interface import AsyncMultiRpc
9
+ from src.multirpc.constants import GasEstimationMethod, ViewPolicy
10
+ from src.multirpc.sync_multi_rpc_interface import MultiRpc
11
+ from src.multirpc.utils import ChainConfigTest
12
+ from src.tests.constants import ArbConfig, BaseConfig, PolyConfig, RPCsSupportingTxTrace, abi
13
+ from src.tests.test_settings import LogLevel, PrivateKey1, PrivateKey2
14
+
15
+ PreviousBlock = 3
16
+
17
+
18
+ async def async_test_map(mr: AsyncMultiRpc, addr: str = None, pk: str = None):
19
+ random_int = random.randint(10, 100)
20
+ print(f"Random int: {random_int}")
21
+ # await mr.functions.set(random_int).call(address=addr, private_key=pk,
22
+ # gas_estimation_method=GasEstimationMethod.GAS_API_PROVIDER)
23
+ # await mr.functions.set(random_int).call(address=addr, private_key=pk,
24
+ # gas_estimation_method=GasEstimationMethod.FIXED)
25
+
26
+ # for failure purpose
27
+ try:
28
+ await mr.functions.set(random_int, random_int).call(address=addr, private_key=pk,
29
+ gas_estimation_method=GasEstimationMethod.RPC)
30
+ except MismatchedABI:
31
+ pass
32
+
33
+ type(random_int)
34
+
35
+ try:
36
+ await mr.functions.set(random.randint(1, 9)
37
+ ).call(address=addr, private_key=pk,
38
+ gas_estimation_method=GasEstimationMethod.RPC,
39
+ enable_estimate_gas_limit=False
40
+ )
41
+ except Exception as e:
42
+ print(e)
43
+
44
+ print(f'encoded function: {mr.functions.set(random_int).get_encoded_data()}')
45
+ tx_receipt = await mr.functions.set(random_int).call(address=addr, private_key=pk,
46
+ gas_estimation_method=GasEstimationMethod.RPC)
47
+
48
+ print(f"{tx_receipt=}")
49
+ result = await mr.functions.map(addr).call()
50
+ print(f"map(addr: {addr}): {result}")
51
+ assert random_int == result, "test was not successful"
52
+
53
+
54
+ async def async_main(chain_config: ChainConfigTest):
55
+ multi_rpc = AsyncMultiRpc(chain_config.rpc, chain_config.contract_address,
56
+ rpcs_supporting_tx_trace=RPCsSupportingTxTrace,
57
+ view_policy=ViewPolicy.MostUpdated,
58
+ contract_abi=abi, gas_estimation=None, log_level=LogLevel,
59
+ is_proof_authority=config_.is_proof_authority,
60
+ multicall_custom_address=chain_config.multicall_address, enable_estimate_gas_limit=True)
61
+ multi_rpc.set_account(address1, private_key=PrivateKey1)
62
+
63
+ p_block = await multi_rpc.get_block_number() - PreviousBlock
64
+ print(f"tx_receipt: {await multi_rpc.get_tx_receipt(chain_config.tx_hash)}")
65
+ print(f"block: {await multi_rpc.get_block(p_block - 1000)}")
66
+ print(f"Nonce: {await multi_rpc.get_nonce(address1)}")
67
+ print(f"map({address1}): {await multi_rpc.functions.map(address1).call()}")
68
+
69
+ results = await multi_rpc.functions.map([(address1,), (address2,)] * 100).multicall()
70
+ print(f"map({address1, address2}): {[res for res in results]}")
71
+ print(f"map({address1}) in {p_block=}: "
72
+ f"{await multi_rpc.functions.map(address1).call(block_identifier=p_block)}")
73
+
74
+ await async_test_map(multi_rpc, address1)
75
+ # await async_test_map(multi_rpc, address2, PrivateKey2)
76
+
77
+
78
+ def sync_test_map(mr: MultiRpc, addr: str = None, pk: str = None):
79
+ random_int = random.randint(10, 100)
80
+ print(f"Random int: {random_int}")
81
+ print(f'encoded function: {mr.functions.set(random_int).get_encoded_data()}')
82
+ mr.functions.set(random_int).call(address=addr, private_key=pk)
83
+
84
+ result = mr.functions.map(addr).call()
85
+ print(f"map(addr: {addr}): {result}")
86
+ assert random_int == result, "test was not successful"
87
+
88
+
89
+ def sync_main(chain_config: ChainConfigTest):
90
+ multi_rpc = MultiRpc(chain_config.rpc, chain_config.contract_address, contract_abi=abi,
91
+ rpcs_supporting_tx_trace=RPCsSupportingTxTrace,
92
+ gas_estimation=None,
93
+ enable_estimate_gas_limit=True, log_level=LogLevel,
94
+ is_proof_authority=config_.is_proof_authority,
95
+ multicall_custom_address=chain_config.multicall_address)
96
+ multi_rpc.set_account(address1, private_key=PrivateKey1)
97
+
98
+ p_block = multi_rpc.get_block_number() - PreviousBlock
99
+ print(f"tx_receipt: {multi_rpc.get_tx_receipt(chain_config.tx_hash)}")
100
+ print(f"block: {multi_rpc.get_block(p_block - 1000)}")
101
+ print(f"Nonce: {multi_rpc.get_nonce(address1)}")
102
+ print(f"map({address1}): {multi_rpc.functions.map(address1).call()}")
103
+
104
+ results = multi_rpc.functions.map([(address1,), (address2,)]).multicall()
105
+ print(f"map({address1, address2}): {[res for res in results]}")
106
+ print(f"map({address1}) in {p_block=}: "
107
+ f"{multi_rpc.functions.map(address1).call(block_identifier=p_block)}")
108
+
109
+ sync_test_map(multi_rpc, address1)
110
+ sync_test_map(multi_rpc, address2, PrivateKey2)
111
+
112
+
113
+ async def test(chain_config: ChainConfigTest):
114
+ # try:
115
+ # sync_main(chain_config)
116
+ # print("###sync test was successful###")
117
+ # except Exception as e:
118
+ # logging.error(e)
119
+
120
+ try:
121
+ await async_main(chain_config)
122
+ print('###async test was successful###')
123
+ except Exception as e:
124
+ logging.error(e)
125
+
126
+
127
+ if __name__ == '__main__':
128
+ address1 = Account.from_key(PrivateKey1).address
129
+ address2 = Account.from_key(PrivateKey2).address
130
+ for config_ in [
131
+ ArbConfig,
132
+ PolyConfig,
133
+ BaseConfig,
134
+ # MantleConfig
135
+ ]:
136
+ print(f"=============================== Start Testing on {config_.name} ===============================")
137
+ asyncio.run(test(config_))
138
+ print(f"=============================== Test on {config_.name} Completed ===============================\n\n")
@@ -0,0 +1,7 @@
1
+ import logging
2
+
3
+ # private key that have gas for in fantom network
4
+ PrivateKey1 = ""
5
+ PrivateKey2 = ""
6
+
7
+ LogLevel = logging.INFO