mm-sol 0.5.9__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/account.py +0 -26
- mm_sol/cli/calcs.py +15 -19
- mm_sol/cli/cli.py +16 -13
- mm_sol/cli/cmd/balance_cmd.py +19 -22
- mm_sol/cli/cmd/balances_cmd.py +28 -29
- mm_sol/cli/cmd/node_cmd.py +2 -2
- mm_sol/cli/cmd/transfer_cmd.py +50 -55
- mm_sol/rpc.py +55 -206
- mm_sol/rpc_sync.py +232 -0
- mm_sol/spl_token.py +85 -0
- mm_sol/transfer.py +80 -76
- {mm_sol-0.5.9.dist-info → mm_sol-0.6.0.dist-info}/METADATA +3 -2
- mm_sol-0.6.0.dist-info/RECORD +30 -0
- mm_sol/async_rpc.py +0 -42
- mm_sol/balance.py +0 -158
- mm_sol/block.py +0 -58
- mm_sol/rpc_async.py +0 -65
- mm_sol/solana_cli.py +0 -252
- mm_sol/token.py +0 -33
- mm_sol/token_async.py +0 -40
- mm_sol-0.5.9.dist-info/RECORD +0 -35
- {mm_sol-0.5.9.dist-info → mm_sol-0.6.0.dist-info}/WHEEL +0 -0
- {mm_sol-0.5.9.dist-info → mm_sol-0.6.0.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
4
|
-
from
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
94
|
-
|
|
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.
|
|
99
|
-
if "result" in
|
|
100
|
-
return res.
|
|
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.
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
56
|
+
return Result.failure(e, {"response": response})
|
|
180
57
|
|
|
181
58
|
|
|
182
|
-
def
|
|
183
|
-
node
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
)
|