mm-sol 0.5.9__py3-none-any.whl → 0.6.1__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 -23
- mm_sol/cli/cmd/balances_cmd.py +29 -29
- mm_sol/cli/cmd/node_cmd.py +2 -2
- mm_sol/cli/cmd/transfer_cmd.py +53 -58
- mm_sol/retry.py +108 -0
- mm_sol/rpc.py +40 -206
- mm_sol/rpc_sync.py +215 -0
- mm_sol/spl_token.py +48 -0
- mm_sol/transfer.py +27 -84
- {mm_sol-0.5.9.dist-info → mm_sol-0.6.1.dist-info}/METADATA +2 -2
- mm_sol-0.6.1.dist-info/RECORD +31 -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.1.dist-info}/WHEEL +0 -0
- {mm_sol-0.5.9.dist-info → mm_sol-0.6.1.dist-info}/entry_points.txt +0 -0
mm_sol/retry.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from mm_crypto_utils import Nodes, Proxies, retry_with_node_and_proxy
|
|
2
|
+
from mm_std import Result
|
|
3
|
+
from solders.solders import Pubkey, Signature
|
|
4
|
+
|
|
5
|
+
from mm_sol import rpc, spl_token, transfer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def get_sol_balance(retries: int, nodes: Nodes, proxies: Proxies, *, address: str, timeout: float = 5) -> Result[int]:
|
|
9
|
+
return await retry_with_node_and_proxy(
|
|
10
|
+
retries,
|
|
11
|
+
nodes,
|
|
12
|
+
proxies,
|
|
13
|
+
lambda node, proxy: rpc.get_balance(node=node, address=address, timeout=timeout, proxy=proxy),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def get_token_balance(
|
|
18
|
+
retries: int,
|
|
19
|
+
nodes: Nodes,
|
|
20
|
+
proxies: Proxies,
|
|
21
|
+
*,
|
|
22
|
+
owner: str,
|
|
23
|
+
token: str,
|
|
24
|
+
token_account: str | None = None,
|
|
25
|
+
timeout: float = 5,
|
|
26
|
+
) -> Result[int]:
|
|
27
|
+
return await retry_with_node_and_proxy(
|
|
28
|
+
retries,
|
|
29
|
+
nodes,
|
|
30
|
+
proxies,
|
|
31
|
+
lambda node, proxy: spl_token.get_balance(
|
|
32
|
+
node,
|
|
33
|
+
owner=owner,
|
|
34
|
+
token=token,
|
|
35
|
+
token_account=token_account,
|
|
36
|
+
timeout=timeout,
|
|
37
|
+
proxy=proxy,
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def transfer_token(
|
|
43
|
+
retries: int,
|
|
44
|
+
nodes: Nodes,
|
|
45
|
+
proxies: Proxies,
|
|
46
|
+
*,
|
|
47
|
+
token_mint_address: str | Pubkey,
|
|
48
|
+
from_address: str | Pubkey,
|
|
49
|
+
private_key: str,
|
|
50
|
+
to_address: str | Pubkey,
|
|
51
|
+
amount: int, # smallest unit
|
|
52
|
+
decimals: int,
|
|
53
|
+
timeout: float = 10,
|
|
54
|
+
create_token_account_if_not_exists: bool = True,
|
|
55
|
+
) -> Result[Signature]:
|
|
56
|
+
return await retry_with_node_and_proxy(
|
|
57
|
+
retries,
|
|
58
|
+
nodes,
|
|
59
|
+
proxies,
|
|
60
|
+
lambda node, proxy: transfer.transfer_token(
|
|
61
|
+
node=node,
|
|
62
|
+
token_mint_address=token_mint_address,
|
|
63
|
+
from_address=from_address,
|
|
64
|
+
private_key=private_key,
|
|
65
|
+
to_address=to_address,
|
|
66
|
+
amount=amount,
|
|
67
|
+
decimals=decimals,
|
|
68
|
+
proxy=proxy,
|
|
69
|
+
timeout=timeout,
|
|
70
|
+
create_token_account_if_not_exists=create_token_account_if_not_exists,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def transfer_sol(
|
|
76
|
+
retries: int,
|
|
77
|
+
nodes: Nodes,
|
|
78
|
+
proxies: Proxies,
|
|
79
|
+
*,
|
|
80
|
+
from_address: str,
|
|
81
|
+
private_key: str,
|
|
82
|
+
to_address: str,
|
|
83
|
+
lamports: int,
|
|
84
|
+
timeout: float = 10,
|
|
85
|
+
) -> Result[Signature]:
|
|
86
|
+
return await retry_with_node_and_proxy(
|
|
87
|
+
retries,
|
|
88
|
+
nodes,
|
|
89
|
+
proxies,
|
|
90
|
+
lambda node, proxy: transfer.transfer_sol(
|
|
91
|
+
node=node,
|
|
92
|
+
proxy=proxy,
|
|
93
|
+
from_address=from_address,
|
|
94
|
+
to_address=to_address,
|
|
95
|
+
lamports=lamports,
|
|
96
|
+
private_key=private_key,
|
|
97
|
+
timeout=timeout,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def get_token_decimals(retries: int, nodes: Nodes, proxies: Proxies, *, token: str, timeout: float = 5) -> Result[int]:
|
|
103
|
+
return await retry_with_node_and_proxy(
|
|
104
|
+
retries,
|
|
105
|
+
nodes,
|
|
106
|
+
proxies,
|
|
107
|
+
lambda node, proxy: spl_token.get_decimals(node, token=token, proxy=proxy, timeout=timeout),
|
|
108
|
+
)
|
mm_sol/rpc.py
CHANGED
|
@@ -1,232 +1,66 @@
|
|
|
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_std import Result, http_request
|
|
5
7
|
|
|
6
|
-
DEFAULT_MAINNET_RPC = "https://api.mainnet-beta.solana.com"
|
|
7
|
-
DEFAULT_TESTNET_RPC = "https://api.testnet.solana.com"
|
|
8
8
|
|
|
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
|
-
*,
|
|
9
|
+
async def rpc_call(
|
|
77
10
|
node: str,
|
|
78
11
|
method: str,
|
|
79
|
-
params:
|
|
12
|
+
params: Sequence[object],
|
|
13
|
+
timeout: float,
|
|
14
|
+
proxy: str | None,
|
|
80
15
|
id_: int = 1,
|
|
81
|
-
timeout: float = 10,
|
|
82
|
-
proxy: str | None = None,
|
|
83
16
|
) -> Result[Any]:
|
|
84
17
|
data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
|
|
85
18
|
if node.startswith("http"):
|
|
86
|
-
return _http_call(node, data, timeout, proxy)
|
|
87
|
-
|
|
19
|
+
return await _http_call(node, data, timeout, proxy)
|
|
20
|
+
return await _ws_call(node, data, timeout)
|
|
88
21
|
|
|
89
22
|
|
|
90
|
-
def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
|
|
91
|
-
res =
|
|
23
|
+
async def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
|
|
24
|
+
res = await http_request(node, method="POST", proxy=proxy, timeout=timeout, json=data)
|
|
25
|
+
if res.is_err():
|
|
26
|
+
return res.to_err_result()
|
|
92
27
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
err = res.json.get("error", {}).get("message", "")
|
|
28
|
+
parsed_body = res.parse_json_body()
|
|
29
|
+
err = parsed_body.get("error", {}).get("message", "")
|
|
97
30
|
if err:
|
|
98
31
|
return res.to_err_result(f"service_error: {err}")
|
|
99
|
-
if "result" in
|
|
100
|
-
return res.to_ok_result(
|
|
101
|
-
|
|
32
|
+
if "result" in parsed_body:
|
|
33
|
+
return res.to_ok_result(parsed_body["result"])
|
|
102
34
|
return res.to_err_result("unknown_response")
|
|
103
35
|
except Exception as e:
|
|
104
|
-
return res.to_err_result(
|
|
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
|
-
|
|
36
|
+
return res.to_err_result(e)
|
|
127
37
|
|
|
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
38
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
)
|
|
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_err():
|
|
141
|
-
return res
|
|
39
|
+
async def _ws_call(node: str, data: dict[str, object], timeout: float) -> Result[Any]:
|
|
40
|
+
response = None
|
|
142
41
|
try:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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)
|
|
178
|
-
except Exception as e:
|
|
179
|
-
return Err(e, res.data)
|
|
42
|
+
async with websockets.connect(node, open_timeout=timeout) as ws:
|
|
43
|
+
await ws.send(json.dumps(data))
|
|
44
|
+
response = json.loads(await ws.recv())
|
|
180
45
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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_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)
|
|
46
|
+
err = response.get("error", {}).get("message", "")
|
|
47
|
+
if err:
|
|
48
|
+
return Result.err(f"service_error: {err}", {"response": response})
|
|
49
|
+
if "result" in response:
|
|
50
|
+
return Result.ok(response["result"], {"response": response})
|
|
51
|
+
return Result.err("unknown_response", {"response": response})
|
|
52
|
+
except TimeoutError:
|
|
53
|
+
return Result.err("timeout", {"response": response})
|
|
210
54
|
except Exception as e:
|
|
211
|
-
return
|
|
55
|
+
return Result.err(e, {"response": response})
|
|
212
56
|
|
|
213
57
|
|
|
214
|
-
def
|
|
215
|
-
return rpc_call(node=node, method="
|
|
216
|
-
lambda ok: StakeActivation(**ok),
|
|
217
|
-
)
|
|
58
|
+
async def get_block_height(node: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
|
|
59
|
+
return await rpc_call(node=node, method="getBlockHeight", params=[], timeout=timeout, proxy=proxy)
|
|
218
60
|
|
|
219
61
|
|
|
220
|
-
def
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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)
|
|
62
|
+
async def get_balance(node: str, address: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
63
|
+
"""Returns balance in lamports"""
|
|
64
|
+
return (await rpc_call(node=node, method="getBalance", params=[address], timeout=timeout, proxy=proxy)).map(
|
|
65
|
+
lambda r: r["value"]
|
|
66
|
+
)
|
mm_sol/rpc_sync.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
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_err():
|
|
95
|
+
return res.to_err_result()
|
|
96
|
+
|
|
97
|
+
json_body = res.parse_json_body()
|
|
98
|
+
err = pydash.get(json_body, "error.message")
|
|
99
|
+
if err:
|
|
100
|
+
return res.to_err_result(f"service_error: {err}")
|
|
101
|
+
if "result" in json_body:
|
|
102
|
+
return res.to_ok_result(json_body["result"])
|
|
103
|
+
|
|
104
|
+
return res.to_err_result("unknown_response")
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return res.to_err_result(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_err():
|
|
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 res.with_value(result)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return res.with_error(e)
|
|
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_stake_activation(node: str, address: str, timeout: float = 60, proxy: str | None = None) -> Result[StakeActivation]:
|
|
198
|
+
return rpc_call(node=node, method="getStakeActivation", timeout=timeout, proxy=proxy, params=[address]).map(
|
|
199
|
+
lambda ok: StakeActivation(**ok),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_transaction(
|
|
204
|
+
node: str,
|
|
205
|
+
signature: str,
|
|
206
|
+
max_supported_transaction_version: int | None = None,
|
|
207
|
+
encoding: str = "json",
|
|
208
|
+
timeout: float = 60,
|
|
209
|
+
proxy: str | None = None,
|
|
210
|
+
) -> Result[dict[str, object] | None]:
|
|
211
|
+
if max_supported_transaction_version is not None:
|
|
212
|
+
params = [signature, {"maxSupportedTransactionVersion": max_supported_transaction_version, "encoding": encoding}]
|
|
213
|
+
else:
|
|
214
|
+
params = [signature, encoding]
|
|
215
|
+
return rpc_call(node=node, method="getTransaction", timeout=timeout, proxy=proxy, params=params)
|
mm_sol/spl_token.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from mm_std import Result
|
|
2
|
+
from solana.exceptions import SolanaRpcException
|
|
3
|
+
from solana.rpc.core import RPCException
|
|
4
|
+
from solders.solders import InvalidParamsMessage, Pubkey, get_associated_token_address
|
|
5
|
+
|
|
6
|
+
from mm_sol.utils import get_async_client
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def get_balance(
|
|
10
|
+
node: str,
|
|
11
|
+
owner: str,
|
|
12
|
+
token: str,
|
|
13
|
+
token_account: str | None = None,
|
|
14
|
+
timeout: float = 5,
|
|
15
|
+
proxy: str | None = None,
|
|
16
|
+
) -> Result[int]:
|
|
17
|
+
response = None
|
|
18
|
+
try:
|
|
19
|
+
client = get_async_client(node, proxy=proxy, timeout=timeout)
|
|
20
|
+
if not token_account:
|
|
21
|
+
token_account = str(get_associated_token_address(Pubkey.from_string(owner), Pubkey.from_string(token)))
|
|
22
|
+
|
|
23
|
+
res = await client.get_token_account_balance(Pubkey.from_string(token_account))
|
|
24
|
+
response = res.to_json()
|
|
25
|
+
|
|
26
|
+
# Sometimes it not raise an error, but it returns this :(
|
|
27
|
+
if isinstance(res, InvalidParamsMessage) and "could not find account" in res.message:
|
|
28
|
+
return Result.ok(0, {"response": response})
|
|
29
|
+
return Result.ok(int(res.value.amount), {"response": response})
|
|
30
|
+
except RPCException as e:
|
|
31
|
+
if "could not find account" in str(e):
|
|
32
|
+
return Result.ok(0, {"response": response, "rpc_exception": str(e)})
|
|
33
|
+
return Result.err(e, {"response": response})
|
|
34
|
+
except SolanaRpcException as e:
|
|
35
|
+
return Result.err((e.error_msg, e), {"response": response})
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return Result.err(e, {"response": response})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def get_decimals(node: str, token: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
41
|
+
response = None
|
|
42
|
+
try:
|
|
43
|
+
client = get_async_client(node, proxy=proxy, timeout=timeout)
|
|
44
|
+
res = await client.get_token_supply(Pubkey.from_string(token))
|
|
45
|
+
response = res.to_json()
|
|
46
|
+
return Result.ok(res.value.decimals, {"response": response})
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return Result.err(e, {"response": response})
|