mm-sol 0.5.8__py3-none-any.whl → 0.6.0__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.
mm_sol/rpc.py CHANGED
@@ -1,232 +1,81 @@
1
+ import json
2
+ from collections.abc import Sequence
1
3
  from typing import Any
2
4
 
3
- from mm_std import Err, Ok, Result, hr
4
- from pydantic import BaseModel, ConfigDict, Field
5
+ import websockets
6
+ from mm_crypto_utils import Nodes, Proxies, retry_with_node_and_proxy
7
+ from mm_std import Result, http_request
5
8
 
6
- DEFAULT_MAINNET_RPC = "https://api.mainnet-beta.solana.com"
7
- DEFAULT_TESTNET_RPC = "https://api.testnet.solana.com"
8
9
 
9
-
10
- class EpochInfo(BaseModel):
11
- model_config = ConfigDict(populate_by_name=True)
12
-
13
- epoch: int
14
- absolute_slot: int = Field(..., alias="absoluteSlot")
15
- block_height: int = Field(..., alias="blockHeight")
16
- slot_index: int = Field(..., alias="slotIndex")
17
- slots_in_epoch: int = Field(..., alias="slotsInEpoch")
18
- transaction_count: int = Field(..., alias="transactionCount")
19
-
20
- @property
21
- def progress(self) -> float:
22
- return round(self.slot_index / self.slots_in_epoch * 100, 2)
23
-
24
-
25
- class ClusterNode(BaseModel):
26
- pubkey: str
27
- version: str | None
28
- gossip: str | None
29
- rpc: str | None
30
-
31
-
32
- class VoteAccount(BaseModel):
33
- class EpochCredits(BaseModel):
34
- epoch: int
35
- credits: int
36
- previous_credits: int
37
-
38
- validator: str
39
- vote: str
40
- commission: int
41
- stake: int
42
- credits: list[EpochCredits]
43
- epoch_vote_account: bool
44
- root_slot: int
45
- last_vote: int
46
- delinquent: bool
47
-
48
-
49
- class BlockProduction(BaseModel):
50
- class Leader(BaseModel):
51
- address: str
52
- produced: int
53
- skipped: int
54
-
55
- slot: int
56
- first_slot: int
57
- last_slot: int
58
- leaders: list[Leader]
59
-
60
- @property
61
- def total_produced(self) -> int:
62
- return sum(leader.produced for leader in self.leaders)
63
-
64
- @property
65
- def total_skipped(self) -> int:
66
- return sum(leader.skipped for leader in self.leaders)
67
-
68
-
69
- class StakeActivation(BaseModel):
70
- state: str
71
- active: int
72
- inactive: int
73
-
74
-
75
- def rpc_call(
76
- *,
10
+ async def rpc_call(
77
11
  node: str,
78
12
  method: str,
79
- params: list[Any],
13
+ params: Sequence[object],
14
+ timeout: float,
15
+ proxy: str | None,
80
16
  id_: int = 1,
81
- timeout: float = 10,
82
- proxy: str | None = None,
83
17
  ) -> Result[Any]:
84
18
  data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
85
19
  if node.startswith("http"):
86
- return _http_call(node, data, timeout, proxy)
87
- raise NotImplementedError("ws is not implemented")
20
+ return await _http_call(node, data, timeout, proxy)
21
+ return await _ws_call(node, data, timeout)
88
22
 
89
23
 
90
- def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
91
- res = hr(node, method="POST", proxy=proxy, timeout=timeout, params=data, json_params=True)
24
+ async def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
25
+ res = await http_request(node, method="POST", proxy=proxy, timeout=timeout, json=data)
26
+ if res.is_error():
27
+ return res.to_result_failure()
92
28
  try:
93
- if res.is_error():
94
- return res.to_err_result()
95
-
96
- err = res.json.get("error", {}).get("message", "")
29
+ parsed_body = res.parse_json_body()
30
+ err = parsed_body.get("error", {}).get("message", "")
97
31
  if err:
98
- return res.to_err_result(f"service_error: {err}")
99
- if "result" in res.json:
100
- return res.to_ok_result(res.json["result"])
101
-
102
- return res.to_err_result("unknown_response")
32
+ return res.to_result_failure(f"service_error: {err}")
33
+ if "result" in parsed_body:
34
+ return res.to_result_success(parsed_body["result"])
35
+ return res.to_result_failure("unknown_response")
103
36
  except Exception as e:
104
- return res.to_err_result(f"exception: {e!s}")
105
-
106
-
107
- def get_balance(node: str, address: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
108
- """Returns balance in lamports"""
109
- return rpc_call(node=node, method="getBalance", params=[address], timeout=timeout, proxy=proxy).and_then(lambda r: r["value"])
110
-
111
-
112
- def get_block_height(node: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
113
- """Returns balance in lamports"""
114
- return rpc_call(node=node, method="getBlockHeight", params=[], timeout=timeout, proxy=proxy)
115
-
116
-
117
- def get_slot(node: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
118
- return rpc_call(node=node, method="getSlot", params=[], timeout=timeout, proxy=proxy)
119
-
120
-
121
- def get_epoch_info(node: str, epoch: int | None = None, timeout: float = 10, proxy: str | None = None) -> Result[EpochInfo]:
122
- params = [epoch] if epoch else []
123
- return rpc_call(node=node, method="getEpochInfo", params=params, timeout=timeout, proxy=proxy).and_then(
124
- lambda r: EpochInfo(**r),
125
- )
126
-
127
-
128
- def get_health(node: str, timeout: float = 10, proxy: str | None = None) -> Result[bool]:
129
- return rpc_call(node=node, method="getHealth", params=[], timeout=timeout, proxy=proxy).and_then(lambda r: r == "ok")
130
-
131
-
132
- def get_cluster_nodes(node: str, timeout: float = 30, proxy: str | None = None) -> Result[list[ClusterNode]]:
133
- return rpc_call(node=node, method="getClusterNodes", timeout=timeout, proxy=proxy, params=[]).and_then(
134
- lambda r: [ClusterNode(**n) for n in r],
135
- )
37
+ return res.to_result_failure(e)
136
38
 
137
39
 
138
- def get_vote_accounts(node: str, timeout: float = 30, proxy: str | None = None) -> Result[list[VoteAccount]]:
139
- res = rpc_call(node=node, method="getVoteAccounts", timeout=timeout, proxy=proxy, params=[])
140
- if res.is_err():
141
- return res
40
+ async def _ws_call(node: str, data: dict[str, object], timeout: float) -> Result[Any]:
41
+ response = None
142
42
  try:
143
- data = res.unwrap()
144
- result: list[VoteAccount] = []
145
- for a in data["current"]:
146
- result.append( # noqa: PERF401
147
- VoteAccount(
148
- validator=a["nodePubkey"],
149
- vote=a["votePubkey"],
150
- commission=a["commission"],
151
- stake=a["activatedStake"],
152
- credits=[
153
- VoteAccount.EpochCredits(epoch=c[0], credits=c[1], previous_credits=c[2]) for c in a["epochCredits"]
154
- ],
155
- delinquent=False,
156
- epoch_vote_account=a["epochVoteAccount"],
157
- root_slot=a["rootSlot"],
158
- last_vote=a["lastVote"],
159
- ),
160
- )
161
- for a in data["delinquent"]:
162
- result.append( # noqa: PERF401
163
- VoteAccount(
164
- validator=a["nodePubkey"],
165
- vote=a["votePubkey"],
166
- commission=a["commission"],
167
- stake=a["activatedStake"],
168
- credits=[
169
- VoteAccount.EpochCredits(epoch=c[0], credits=c[1], previous_credits=c[2]) for c in a["epochCredits"]
170
- ],
171
- delinquent=True,
172
- epoch_vote_account=a["epochVoteAccount"],
173
- root_slot=a["rootSlot"],
174
- last_vote=a["lastVote"],
175
- ),
176
- )
177
- return Ok(result, res.data)
43
+ async with websockets.connect(node, open_timeout=timeout) as ws:
44
+ await ws.send(json.dumps(data))
45
+ response = json.loads(await ws.recv())
46
+
47
+ err = response.get("error", {}).get("message", "")
48
+ if err:
49
+ return Result.failure(f"service_error: {err}", {"response": response})
50
+ if "result" in response:
51
+ return Result.success(response["result"], {"response": response})
52
+ return Result.failure("unknown_response", {"response": response})
53
+ except TimeoutError:
54
+ return Result.failure("timeout", {"response": response})
178
55
  except Exception as e:
179
- return Err(e, res.data)
56
+ return Result.failure(e, {"response": response})
180
57
 
181
58
 
182
- def get_leader_scheduler(
183
- node: str,
184
- slot: int | None = None,
185
- timeout: float = 10,
186
- proxy: str | None = None,
187
- ) -> Result[dict[str, list[int]]]:
188
- return rpc_call(
189
- node=node,
190
- method="getLeaderSchedule",
191
- timeout=timeout,
192
- proxy=proxy,
193
- params=[slot],
194
- )
59
+ async def get_block_height(node: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
60
+ return await rpc_call(node=node, method="getBlockHeight", params=[], timeout=timeout, proxy=proxy)
195
61
 
196
62
 
197
- def get_block_production(node: str, timeout: float = 60, proxy: str | None = None) -> Result[BlockProduction]:
198
- res = rpc_call(node=node, method="getBlockProduction", timeout=timeout, proxy=proxy, params=[])
199
- if res.is_err():
200
- return res
201
- try:
202
- res_ok = res.unwrap()
203
- slot = res_ok["context"]["slot"]
204
- first_slot = res_ok["value"]["range"]["firstSlot"]
205
- last_slot = res_ok["value"]["range"]["lastSlot"]
206
- leaders = []
207
- for address, (leader, produced) in res.ok["value"]["byIdentity"].items(): # type: ignore[index]
208
- leaders.append(BlockProduction.Leader(address=address, produced=produced, skipped=leader - produced))
209
- return Ok(BlockProduction(slot=slot, first_slot=first_slot, last_slot=last_slot, leaders=leaders), res.data)
210
- except Exception as e:
211
- return Err(e, data=res.data)
212
-
213
-
214
- def get_stake_activation(node: str, address: str, timeout: float = 60, proxy: str | None = None) -> Result[StakeActivation]:
215
- return rpc_call(node=node, method="getStakeActivation", timeout=timeout, proxy=proxy, params=[address]).and_then(
216
- lambda ok: StakeActivation(**ok),
63
+ async def get_balance(node: str, address: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
64
+ """Returns balance in lamports"""
65
+ return (await rpc_call(node=node, method="getBalance", params=[address], timeout=timeout, proxy=proxy)).map(
66
+ lambda r: r["value"]
217
67
  )
218
68
 
219
69
 
220
- def get_transaction(
221
- node: str,
222
- signature: str,
223
- max_supported_transaction_version: int | None = None,
224
- encoding: str = "json",
225
- timeout: float = 60,
226
- proxy: str | None = None,
227
- ) -> Result[dict[str, object] | None]:
228
- if max_supported_transaction_version is not None:
229
- params = [signature, {"maxSupportedTransactionVersion": max_supported_transaction_version, "encoding": encoding}]
230
- else:
231
- params = [signature, encoding]
232
- return rpc_call(node=node, method="getTransaction", timeout=timeout, proxy=proxy, params=params)
70
+ async def get_balance_with_retries(
71
+ retries: int, nodes: Nodes, proxies: Proxies, *, address: str, timeout: float = 5
72
+ ) -> Result[int]:
73
+ """
74
+ Retry get_balance with different nodes and proxies
75
+ """
76
+ return await retry_with_node_and_proxy(
77
+ retries,
78
+ nodes,
79
+ proxies,
80
+ lambda node, proxy: get_balance(node=node, address=address, timeout=timeout, proxy=proxy),
81
+ )
mm_sol/rpc_sync.py ADDED
@@ -0,0 +1,232 @@
1
+ from typing import Any
2
+
3
+ import pydash
4
+ from mm_std import Result, http_request_sync
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ DEFAULT_MAINNET_RPC = "https://api.mainnet-beta.solana.com"
8
+ DEFAULT_TESTNET_RPC = "https://api.testnet.solana.com"
9
+
10
+
11
+ class EpochInfo(BaseModel):
12
+ model_config = ConfigDict(populate_by_name=True)
13
+
14
+ epoch: int
15
+ absolute_slot: int = Field(..., alias="absoluteSlot")
16
+ block_height: int = Field(..., alias="blockHeight")
17
+ slot_index: int = Field(..., alias="slotIndex")
18
+ slots_in_epoch: int = Field(..., alias="slotsInEpoch")
19
+ transaction_count: int = Field(..., alias="transactionCount")
20
+
21
+ @property
22
+ def progress(self) -> float:
23
+ return round(self.slot_index / self.slots_in_epoch * 100, 2)
24
+
25
+
26
+ class ClusterNode(BaseModel):
27
+ pubkey: str
28
+ version: str | None
29
+ gossip: str | None
30
+ rpc: str | None
31
+
32
+
33
+ class VoteAccount(BaseModel):
34
+ class EpochCredits(BaseModel):
35
+ epoch: int
36
+ credits: int
37
+ previous_credits: int
38
+
39
+ validator: str
40
+ vote: str
41
+ commission: int
42
+ stake: int
43
+ credits: list[EpochCredits]
44
+ epoch_vote_account: bool
45
+ root_slot: int
46
+ last_vote: int
47
+ delinquent: bool
48
+
49
+
50
+ class BlockProduction(BaseModel):
51
+ class Leader(BaseModel):
52
+ address: str
53
+ produced: int
54
+ skipped: int
55
+
56
+ slot: int
57
+ first_slot: int
58
+ last_slot: int
59
+ leaders: list[Leader]
60
+
61
+ @property
62
+ def total_produced(self) -> int:
63
+ return sum(leader.produced for leader in self.leaders)
64
+
65
+ @property
66
+ def total_skipped(self) -> int:
67
+ return sum(leader.skipped for leader in self.leaders)
68
+
69
+
70
+ class StakeActivation(BaseModel):
71
+ state: str
72
+ active: int
73
+ inactive: int
74
+
75
+
76
+ def rpc_call(
77
+ *,
78
+ node: str,
79
+ method: str,
80
+ params: list[Any],
81
+ id_: int = 1,
82
+ timeout: float = 10,
83
+ proxy: str | None = None,
84
+ ) -> Result[Any]:
85
+ data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
86
+ if node.startswith("http"):
87
+ return _http_call(node, data, timeout, proxy)
88
+ raise NotImplementedError("ws is not implemented")
89
+
90
+
91
+ def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
92
+ res = http_request_sync(node, method="POST", proxy=proxy, timeout=timeout, json=data)
93
+ try:
94
+ if res.is_error():
95
+ return res.to_result_failure()
96
+
97
+ json_body = res.parse_json_body()
98
+ err = pydash.get(json_body, "error.message")
99
+ if err:
100
+ return res.to_result_failure(f"service_error: {err}")
101
+ if "result" in json_body:
102
+ return res.to_result_success(json_body["result"])
103
+
104
+ return res.to_result_failure("unknown_response")
105
+ except Exception as e:
106
+ return res.to_result_failure(e)
107
+
108
+
109
+ def get_balance(node: str, address: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
110
+ """Returns balance in lamports"""
111
+ return rpc_call(node=node, method="getBalance", params=[address], timeout=timeout, proxy=proxy).map(lambda r: r["value"])
112
+
113
+
114
+ def get_block_height(node: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
115
+ """Returns balance in lamports"""
116
+ return rpc_call(node=node, method="getBlockHeight", params=[], timeout=timeout, proxy=proxy)
117
+
118
+
119
+ def get_slot(node: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
120
+ return rpc_call(node=node, method="getSlot", params=[], timeout=timeout, proxy=proxy)
121
+
122
+
123
+ def get_epoch_info(node: str, epoch: int | None = None, timeout: float = 10, proxy: str | None = None) -> Result[EpochInfo]:
124
+ params = [epoch] if epoch else []
125
+ return rpc_call(node=node, method="getEpochInfo", params=params, timeout=timeout, proxy=proxy).map(lambda r: EpochInfo(**r))
126
+
127
+
128
+ def get_health(node: str, timeout: float = 10, proxy: str | None = None) -> Result[bool]:
129
+ return rpc_call(node=node, method="getHealth", params=[], timeout=timeout, proxy=proxy).map(lambda r: r == "ok")
130
+
131
+
132
+ def get_cluster_nodes(node: str, timeout: float = 30, proxy: str | None = None) -> Result[list[ClusterNode]]:
133
+ return rpc_call(node=node, method="getClusterNodes", timeout=timeout, proxy=proxy, params=[]).map(
134
+ lambda r: [ClusterNode(**n) for n in r],
135
+ )
136
+
137
+
138
+ def get_vote_accounts(node: str, timeout: float = 30, proxy: str | None = None) -> Result[list[VoteAccount]]:
139
+ res = rpc_call(node=node, method="getVoteAccounts", timeout=timeout, proxy=proxy, params=[])
140
+ if res.is_error():
141
+ return res
142
+ try:
143
+ data = res.unwrap()
144
+ result: list[VoteAccount] = []
145
+ for a in data["current"]:
146
+ result.append( # noqa: PERF401
147
+ VoteAccount(
148
+ validator=a["nodePubkey"],
149
+ vote=a["votePubkey"],
150
+ commission=a["commission"],
151
+ stake=a["activatedStake"],
152
+ credits=[
153
+ VoteAccount.EpochCredits(epoch=c[0], credits=c[1], previous_credits=c[2]) for c in a["epochCredits"]
154
+ ],
155
+ delinquent=False,
156
+ epoch_vote_account=a["epochVoteAccount"],
157
+ root_slot=a["rootSlot"],
158
+ last_vote=a["lastVote"],
159
+ ),
160
+ )
161
+ for a in data["delinquent"]:
162
+ result.append( # noqa: PERF401
163
+ VoteAccount(
164
+ validator=a["nodePubkey"],
165
+ vote=a["votePubkey"],
166
+ commission=a["commission"],
167
+ stake=a["activatedStake"],
168
+ credits=[
169
+ VoteAccount.EpochCredits(epoch=c[0], credits=c[1], previous_credits=c[2]) for c in a["epochCredits"]
170
+ ],
171
+ delinquent=True,
172
+ epoch_vote_account=a["epochVoteAccount"],
173
+ root_slot=a["rootSlot"],
174
+ last_vote=a["lastVote"],
175
+ ),
176
+ )
177
+ return Result.success(result, res.extra)
178
+ except Exception as e:
179
+ return Result.failure(e, res.extra)
180
+
181
+
182
+ def get_leader_scheduler(
183
+ node: str,
184
+ slot: int | None = None,
185
+ timeout: float = 10,
186
+ proxy: str | None = None,
187
+ ) -> Result[dict[str, list[int]]]:
188
+ return rpc_call(
189
+ node=node,
190
+ method="getLeaderSchedule",
191
+ timeout=timeout,
192
+ proxy=proxy,
193
+ params=[slot],
194
+ )
195
+
196
+
197
+ def get_block_production(node: str, timeout: float = 60, proxy: str | None = None) -> Result[BlockProduction]:
198
+ res = rpc_call(node=node, method="getBlockProduction", timeout=timeout, proxy=proxy, params=[])
199
+ if res.is_error():
200
+ return res
201
+ try:
202
+ res_ok = res.unwrap()
203
+ slot = res_ok["context"]["slot"]
204
+ first_slot = res_ok["value"]["range"]["firstSlot"]
205
+ last_slot = res_ok["value"]["range"]["lastSlot"]
206
+ leaders = []
207
+ for address, (leader, produced) in res.ok["value"]["byIdentity"].items(): # type: ignore[index]
208
+ leaders.append(BlockProduction.Leader(address=address, produced=produced, skipped=leader - produced))
209
+ return Result.success(BlockProduction(slot=slot, first_slot=first_slot, last_slot=last_slot, leaders=leaders), res.extra)
210
+ except Exception as e:
211
+ return Result.failure(e, res.extra)
212
+
213
+
214
+ def get_stake_activation(node: str, address: str, timeout: float = 60, proxy: str | None = None) -> Result[StakeActivation]:
215
+ return rpc_call(node=node, method="getStakeActivation", timeout=timeout, proxy=proxy, params=[address]).map(
216
+ lambda ok: StakeActivation(**ok),
217
+ )
218
+
219
+
220
+ def get_transaction(
221
+ node: str,
222
+ signature: str,
223
+ max_supported_transaction_version: int | None = None,
224
+ encoding: str = "json",
225
+ timeout: float = 60,
226
+ proxy: str | None = None,
227
+ ) -> Result[dict[str, object] | None]:
228
+ if max_supported_transaction_version is not None:
229
+ params = [signature, {"maxSupportedTransactionVersion": max_supported_transaction_version, "encoding": encoding}]
230
+ else:
231
+ params = [signature, encoding]
232
+ return rpc_call(node=node, method="getTransaction", timeout=timeout, proxy=proxy, params=params)
mm_sol/spl_token.py ADDED
@@ -0,0 +1,85 @@
1
+ from mm_crypto_utils import Nodes, Proxies, retry_with_node_and_proxy
2
+ from mm_std import Result
3
+ from solana.exceptions import SolanaRpcException
4
+ from solana.rpc.core import RPCException
5
+ from solders.solders import InvalidParamsMessage, Pubkey, get_associated_token_address
6
+
7
+ from mm_sol.utils import get_async_client
8
+
9
+
10
+ async def get_balance(
11
+ node: str,
12
+ owner: str,
13
+ token: str,
14
+ token_account: str | None = None,
15
+ timeout: float = 5,
16
+ proxy: str | None = None,
17
+ ) -> Result[int]:
18
+ response = None
19
+ try:
20
+ client = get_async_client(node, proxy=proxy, timeout=timeout)
21
+ if not token_account:
22
+ token_account = str(get_associated_token_address(Pubkey.from_string(owner), Pubkey.from_string(token)))
23
+
24
+ res = await client.get_token_account_balance(Pubkey.from_string(token_account))
25
+ response = res.to_json()
26
+
27
+ # Sometimes it not raise an error, but it returns this :(
28
+ if isinstance(res, InvalidParamsMessage) and "could not find account" in res.message:
29
+ return Result.success(0, {"response": response})
30
+ return Result.success(int(res.value.amount), {"response": response})
31
+ except RPCException as e:
32
+ if "could not find account" in str(e):
33
+ return Result.success(0, {"response": response, "rpc_exception": str(e)})
34
+ return Result.failure(e, {"response": response})
35
+ except SolanaRpcException as e:
36
+ return Result.failure((e.error_msg, e), {"response": response})
37
+ except Exception as e:
38
+ return Result.failure(e, {"response": response})
39
+
40
+
41
+ async def get_balance_with_retries(
42
+ retries: int,
43
+ nodes: Nodes,
44
+ proxies: Proxies,
45
+ *,
46
+ owner: str,
47
+ token: str,
48
+ token_account: str | None = None,
49
+ timeout: float = 5,
50
+ ) -> Result[int]:
51
+ return await retry_with_node_and_proxy(
52
+ retries,
53
+ nodes,
54
+ proxies,
55
+ lambda node, proxy: get_balance(
56
+ node,
57
+ owner=owner,
58
+ token=token,
59
+ token_account=token_account,
60
+ timeout=timeout,
61
+ proxy=proxy,
62
+ ),
63
+ )
64
+
65
+
66
+ async def get_decimals(node: str, token: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
67
+ response = None
68
+ try:
69
+ client = get_async_client(node, proxy=proxy, timeout=timeout)
70
+ res = await client.get_token_supply(Pubkey.from_string(token))
71
+ response = res.to_json()
72
+ return Result.success(res.value.decimals, {"response": response})
73
+ except Exception as e:
74
+ return Result.failure(e, {"response": response})
75
+
76
+
77
+ async def get_decimals_with_retries(
78
+ retries: int, nodes: Nodes, proxies: Proxies, *, token: str, timeout: float = 5
79
+ ) -> Result[int]:
80
+ return await retry_with_node_and_proxy(
81
+ retries,
82
+ nodes,
83
+ proxies,
84
+ lambda node, proxy: get_decimals(node, token=token, proxy=proxy, timeout=timeout),
85
+ )