mm-sol 0.2.5__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 ADDED
@@ -0,0 +1,232 @@
1
+ from typing import Any
2
+
3
+ from mm_std import Err, Ok, Result, hr
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+ DEFAULT_MAINNET_RPC = "https://api.mainnet-beta.solana.com"
7
+ DEFAULT_TESTNET_RPC = "https://api.testnet.solana.com"
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
+ *,
77
+ node: str,
78
+ method: str,
79
+ params: list[Any],
80
+ id_: int = 1,
81
+ timeout: int = 10,
82
+ proxy: str | None = None,
83
+ ) -> Result[Any]:
84
+ data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
85
+ if node.startswith("http"):
86
+ return _http_call(node, data, timeout, proxy)
87
+ raise NotImplementedError("ws is not implemented")
88
+
89
+
90
+ def _http_call(node: str, data: dict[str, object], timeout: int, proxy: str | None) -> Result[Any]:
91
+ res = hr(node, method="POST", proxy=proxy, timeout=timeout, params=data, json_params=True)
92
+ try:
93
+ if res.is_error():
94
+ return res.to_err_result()
95
+
96
+ err = res.json.get("error", {}).get("message", "")
97
+ 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")
103
+ 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: int = 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: int = 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: int = 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: int = 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: int = 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: int = 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
+ )
136
+
137
+
138
+ def get_vote_accounts(node: str, timeout: int = 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 Ok(result, res.data)
178
+ except Exception as e:
179
+ return Err(e, res.data)
180
+
181
+
182
+ def get_leader_scheduler(
183
+ node: str,
184
+ slot: int | None = None,
185
+ timeout: int = 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: int = 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: int = 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),
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: int = 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/solana_cli.py ADDED
@@ -0,0 +1,254 @@
1
+ import json
2
+ import random
3
+ from decimal import Decimal
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ import pydash
8
+ from mm_std import CommandResult, Err, Ok, Result, run_command, run_ssh_command
9
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
10
+
11
+
12
+ class ValidatorInfo(BaseModel):
13
+ identity_address: str
14
+ info_address: str
15
+ name: str | None
16
+ keybase: str | None
17
+ website: str | None
18
+ details: str | None
19
+
20
+
21
+ class StakeAccount(BaseModel):
22
+ type: str = Field(..., alias="stakeType")
23
+ balance: float | None = Field(..., alias="accountBalance")
24
+ withdrawer: str
25
+ staker: str
26
+ vote: str | None = Field(None, alias="delegatedVoteAccountAddress")
27
+
28
+ @field_validator("balance")
29
+ @classmethod
30
+ def from_lamports_to_sol(cls, v: int | None) -> float | None:
31
+ if v:
32
+ return v / 1_000_000_000
33
+
34
+
35
+ class Stake(BaseModel):
36
+ model_config = ConfigDict(populate_by_name=True)
37
+
38
+ stake_address: str = Field(..., alias="stakePubkey")
39
+ withdrawer_address: str = Field(..., alias="withdrawer")
40
+ vote_address: str | None = Field(None, alias="delegatedVoteAccountAddress")
41
+ balance: float | None = Field(..., alias="accountBalance")
42
+ delegated: float | None = Field(None, alias="delegatedStake")
43
+ active: float | None = Field(None, alias="activeStake")
44
+ lock_time: int | None = Field(None, alias="unixTimestamp")
45
+
46
+ @field_validator("balance", "delegated", "active")
47
+ @classmethod
48
+ def from_lamports_to_sol(cls, v: int | None) -> float | None:
49
+ if v:
50
+ return v / 1_000_000_000
51
+
52
+
53
+ def get_balance(
54
+ *,
55
+ address: str,
56
+ solana_dir: str = "",
57
+ url: str = "localhost",
58
+ ssh_host: str | None = None,
59
+ ssh_key_path: str | None = None,
60
+ timeout: int = 60,
61
+ ) -> Result[Decimal]:
62
+ solana_dir = _solana_dir(solana_dir)
63
+ cmd = f"{solana_dir}solana balance {address} -u {url}"
64
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
65
+ data = {"cmd": cmd, "stdout": res.stdout, "stderr": res.stderr}
66
+ try:
67
+ return Ok(Decimal(res.stdout.replace("SOL", "").strip()), data=data)
68
+ except Exception as e:
69
+ return Err(e, data=data)
70
+
71
+
72
+ def get_stake_account(
73
+ *,
74
+ address: str,
75
+ solana_dir: str = "",
76
+ url: str = "localhost",
77
+ ssh_host: str | None = None,
78
+ ssh_key_path: str | None = None,
79
+ timeout: int = 60,
80
+ ) -> Result[StakeAccount]:
81
+ solana_dir = _solana_dir(solana_dir)
82
+ cmd = f"{solana_dir}solana stake-account --output json -u {url} {address}"
83
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
84
+ data = {"cmd": cmd, "stdout": res.stdout, "stderr": res.stderr}
85
+ try:
86
+ json_res = json.loads(res.stdout)
87
+ return Ok(StakeAccount(**json_res), data=data)
88
+ except Exception as e:
89
+ return Err(e, data=data)
90
+
91
+
92
+ def transfer_with_private_key_file(
93
+ *,
94
+ recipient: str,
95
+ amount: Decimal,
96
+ private_key_path: str,
97
+ solana_dir: str = "",
98
+ url: str = "localhost",
99
+ ssh_host: str | None = None,
100
+ ssh_key_path: str | None = None,
101
+ allow_unfunded_recipient: bool = True,
102
+ timeout: int = 60,
103
+ ) -> Result[str]:
104
+ solana_dir = _solana_dir(solana_dir)
105
+ cmd = f"{solana_dir}solana transfer {recipient} {amount} --from {private_key_path} --fee-payer {private_key_path}"
106
+ if allow_unfunded_recipient:
107
+ cmd += " --allow-unfunded-recipient"
108
+ cmd += f" -u {url} --output json"
109
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
110
+ data = {"cmd": cmd, "stdout": res.stdout, "stderr": res.stderr}
111
+ try:
112
+ json_res = json.loads(res.stdout)
113
+ return Ok(json_res["signature"], data=data)
114
+ except Exception as e:
115
+ return Err(e, data=data)
116
+
117
+
118
+ def transfer_with_private_key_str(
119
+ *,
120
+ recipient: str,
121
+ amount: Decimal,
122
+ private_key: str,
123
+ tmp_dir_path: str,
124
+ solana_dir: str = "",
125
+ url: str = "localhost",
126
+ ssh_host: str | None = None,
127
+ ssh_key_path: str | None = None,
128
+ timeout: int = 60,
129
+ ) -> Result[str]:
130
+ # make private_key file
131
+ private_key_path = Path(f"{tmp_dir_path}/solana__{random.randint(1, 10_000_000_000)}.json")
132
+ private_key_path.write_text(private_key)
133
+
134
+ try:
135
+ return transfer_with_private_key_file(
136
+ recipient=recipient,
137
+ amount=amount,
138
+ private_key_path=private_key_path.as_posix(),
139
+ solana_dir=solana_dir,
140
+ url=url,
141
+ ssh_host=ssh_host,
142
+ ssh_key_path=ssh_key_path,
143
+ timeout=timeout,
144
+ )
145
+ finally:
146
+ private_key_path.unlink()
147
+
148
+
149
+ def withdraw_from_vote_account(
150
+ *,
151
+ recipient: str,
152
+ amount: Decimal | Literal["ALL"],
153
+ vote_key_path: str,
154
+ fee_payer_key_path: str,
155
+ solana_dir: str = "",
156
+ url: str = "localhost",
157
+ ssh_host: str | None = None,
158
+ ssh_key_path: str | None = None,
159
+ timeout: int = 60,
160
+ ) -> Result[str]:
161
+ solana_dir = _solana_dir(solana_dir)
162
+ cmd = f"{solana_dir}solana withdraw-from-vote-account --keypair {fee_payer_key_path} -u {url} --output json {vote_key_path} {recipient} {amount}" # noqa: E501
163
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
164
+ data = {"cmd": cmd, "stdout": res.stdout, "stderr": res.stderr}
165
+ try:
166
+ json_res = json.loads(res.stdout)
167
+ return Ok(json_res["signature"], data=data)
168
+ except Exception as e:
169
+ return Err(e, data=data)
170
+
171
+
172
+ def get_validators_info(
173
+ *,
174
+ solana_dir: str = "",
175
+ url: str = "localhost",
176
+ ssh_host: str | None = None,
177
+ ssh_key_path: str | None = None,
178
+ timeout: int = 60,
179
+ ) -> Result[list[ValidatorInfo]]:
180
+ solana_dir = _solana_dir(solana_dir)
181
+ cmd = f"{solana_dir}solana validator-info get --output json -u {url}"
182
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
183
+ data = {"cmd": cmd, "stdout": res.stdout, "stderr": res.stderr}
184
+ try:
185
+ validators = []
186
+ for v in json.loads(res.stdout):
187
+ validators.append( # noqa: PERF401
188
+ ValidatorInfo(
189
+ info_address=v["infoPubkey"],
190
+ identity_address=v["identityPubkey"],
191
+ name=pydash.get(v, "info.name"),
192
+ keybase=pydash.get(v, "info.keybaseUsername"),
193
+ details=pydash.get(v, "info.details"),
194
+ website=pydash.get(v, "info.website"),
195
+ ),
196
+ )
197
+ return Ok(validators, data=data)
198
+ except Exception as e:
199
+ return Err(e, data=data)
200
+
201
+
202
+ def get_vote_account_rewards(
203
+ *,
204
+ address: str,
205
+ solana_dir: str = "",
206
+ url: str = "localhost",
207
+ ssh_host: str | None = None,
208
+ ssh_key_path: str | None = None,
209
+ num_rewards_epochs: int = 10,
210
+ timeout: int = 60,
211
+ ) -> Result[dict[int, float]]:
212
+ solana_dir = _solana_dir(solana_dir)
213
+ cmd = f"{solana_dir}solana vote-account {address} --with-rewards --num-rewards-epochs={num_rewards_epochs} -u {url}"
214
+ cmd += " --output json 2>/dev/null"
215
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
216
+ data = {"cmd": cmd, "stdout": res.stdout, "stderr": res.stderr}
217
+ try:
218
+ rewards: dict[int, float] = {}
219
+ for r in reversed(json.loads(res.stdout)["epochRewards"]):
220
+ rewards[r["epoch"]] = r["amount"] / 10**9
221
+ return Ok(rewards, data=data)
222
+ except Exception as e:
223
+ return Err(e, data=data)
224
+
225
+
226
+ def get_stakes(
227
+ *,
228
+ vote_address: str = "",
229
+ solana_dir: str = "",
230
+ url: str = "localhost",
231
+ ssh_host: str | None = None,
232
+ ssh_key_path: str | None = None,
233
+ timeout: int = 60,
234
+ ) -> Result[list[Stake]]:
235
+ solana_dir = _solana_dir(solana_dir)
236
+ cmd = f"{solana_dir}solana stakes --output json -u {url} {vote_address}"
237
+ res = _exec_cmd(cmd, ssh_host, ssh_key_path, timeout)
238
+ data = {"stdout": res.stdout, "stderr": res.stderr}
239
+ try:
240
+ return Ok([Stake(**x) for x in json.loads(res.stdout)], data=data)
241
+ except Exception as e:
242
+ return Err(e, data=data)
243
+
244
+
245
+ def _exec_cmd(cmd: str, ssh_host: str | None, ssh_key_path: str | None, timeout: int) -> CommandResult:
246
+ if ssh_host:
247
+ return run_ssh_command(ssh_host, cmd, ssh_key_path, timeout=timeout)
248
+ return run_command(cmd, timeout=timeout)
249
+
250
+
251
+ def _solana_dir(solana_dir: str) -> str:
252
+ if solana_dir and not solana_dir.endswith("/"):
253
+ solana_dir += "/"
254
+ return solana_dir
mm_sol/token.py ADDED
@@ -0,0 +1,133 @@
1
+ from mm_std import Err, Ok, Result
2
+ from solana.exceptions import SolanaRpcException
3
+ from solana.rpc.types import TokenAccountOpts
4
+ from solders.pubkey import Pubkey
5
+
6
+ from mm_sol.types import Nodes, Proxies
7
+ from mm_sol.utils import get_client, get_node, get_proxy
8
+
9
+
10
+ def get_balance(
11
+ node: str,
12
+ owner_address: str,
13
+ token_mint_address: str,
14
+ token_account: str | None = None,
15
+ timeout: float = 10,
16
+ proxy: str | None = None,
17
+ no_token_accounts_return_zero: bool = True,
18
+ ) -> Result[int]:
19
+ try:
20
+ client = get_client(node, proxy=proxy, timeout=timeout)
21
+ if token_account:
22
+ res_balance = client.get_token_account_balance(Pubkey.from_string(token_account))
23
+ return Ok(int(res_balance.value.amount))
24
+
25
+ res_accounts = client.get_token_accounts_by_owner(
26
+ Pubkey.from_string(owner_address),
27
+ TokenAccountOpts(mint=Pubkey.from_string(token_mint_address)),
28
+ )
29
+
30
+ if no_token_accounts_return_zero and not res_accounts.value:
31
+ return Ok(0)
32
+ if not res_accounts.value:
33
+ return Err("no_token_accounts")
34
+
35
+ token_accounts = [a.pubkey for a in res_accounts.value]
36
+ balances = []
37
+ for token_account_ in token_accounts:
38
+ res = client.get_token_account_balance(token_account_)
39
+ if res.value: # type:ignore[truthy-bool]
40
+ balances.append(int(res.value.amount))
41
+
42
+ if len(balances) > 1:
43
+ return Err("there are many non empty token accounts, set token_account explicitly")
44
+ return Ok(balances[0])
45
+ except SolanaRpcException as e:
46
+ return Err(e.error_msg)
47
+ except Exception as e:
48
+ return Err(e)
49
+
50
+
51
+ def get_balance_with_retries(
52
+ nodes: Nodes,
53
+ owner_address: str,
54
+ token_mint_address: str,
55
+ retries: int,
56
+ token_account: str | None = None,
57
+ timeout: float = 10,
58
+ proxies: Proxies = None,
59
+ no_token_accounts_return_zero: bool = True,
60
+ ) -> Result[int]:
61
+ res: Result[int] = Err("not started yet")
62
+ for _ in range(retries):
63
+ res = get_balance(
64
+ get_node(nodes),
65
+ owner_address,
66
+ token_mint_address,
67
+ token_account,
68
+ timeout=timeout,
69
+ proxy=get_proxy(proxies),
70
+ no_token_accounts_return_zero=no_token_accounts_return_zero,
71
+ )
72
+ if res.is_ok():
73
+ return res
74
+ return res
75
+
76
+
77
+ def get_decimals(node: str, token_mint_address: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
78
+ try:
79
+ client = get_client(node, proxy=proxy, timeout=timeout)
80
+ res = client.get_token_supply(Pubkey.from_string(token_mint_address))
81
+ return Ok(res.value.decimals)
82
+ except Exception as e:
83
+ return Err(e)
84
+
85
+
86
+ def get_decimals_with_retries(
87
+ nodes: Nodes, token_mint_address: str, retries: int, timeout: float = 10, proxies: Proxies = None
88
+ ) -> Result[int]:
89
+ res: Result[int] = Err("not started yet")
90
+ for _ in range(retries):
91
+ res = get_decimals(get_node(nodes), token_mint_address, timeout=timeout, proxy=get_proxy(proxies))
92
+ if res.is_ok():
93
+ return res
94
+ return res
95
+
96
+
97
+ # def transfer_to_wallet_address(
98
+ # *,
99
+ # node: str,
100
+ # private_key: str,
101
+ # recipient_wallet_address: str,
102
+ # token_mint_address: str,
103
+ # amount: int,
104
+ # ) -> Result[str]:
105
+ # try:
106
+ # keypair = account.get_keypair(private_key)
107
+ # token_client = Token(Client(node), Pubkey.from_string(token_mint_address), program_id=TOKEN_PROGRAM_ID, payer=keypair)
108
+ #
109
+ # # get from_token_account
110
+ # res = token_client.get_accounts(keypair.public_key)
111
+ # token_accounts = res["result"]["value"]
112
+ # if len(token_accounts) > 1:
113
+ # return Result(error="many_from_token_accounts", data=res)
114
+ # from_token_account = Pubkey.from_string(token_accounts[0]["pubkey"])
115
+ #
116
+ # # get to_token_account
117
+ # res = token_client.get_accounts(Pubkey.from_string(recipient_wallet_address))
118
+ # token_accounts = res["result"]["value"]
119
+ # if len(token_accounts) > 1:
120
+ # return Result(error="many_to_token_accounts", data=res)
121
+ # elif len(token_accounts) == 1:
122
+ # to_token_account = Pubkey.from_string(token_accounts[0]["pubkey"])
123
+ # else: # create a new to_token_account
124
+ # to_token_account = token_client.create_account(owner=Pubkey.from_string(recipient_wallet_address))
125
+ #
126
+ # res = token_client.transfer(source=from_token_account, dest=to_token_account, owner=keypair, amount=amount)
127
+ # if res.get("result"):
128
+ # return Result(ok=res.get("result"), data=res)
129
+ # return Result(error="unknown_response", data=res)
130
+ # except RPCException as e:
131
+ # return Result(error="rcp_exception", data=str(e))
132
+ # except Exception as e:
133
+ # return Result(error="exception", data=str(e))
mm_sol/transfer.py ADDED
@@ -0,0 +1,80 @@
1
+ from decimal import Decimal
2
+
3
+ import pydash
4
+ from mm_std import Err, Ok, Result
5
+ from pydantic import BaseModel
6
+ from solana.rpc.api import Client
7
+ from solders.message import Message
8
+ from solders.pubkey import Pubkey
9
+ from solders.system_program import TransferParams, transfer
10
+ from solders.transaction import Transaction
11
+
12
+ from mm_sol import rpc, utils
13
+ from mm_sol.account import check_private_key, get_keypair
14
+
15
+
16
+ def transfer_sol(
17
+ *,
18
+ from_address: str,
19
+ private_key_base58: str,
20
+ recipient_address: str,
21
+ amount_sol: Decimal,
22
+ nodes: str | list[str] | None = None,
23
+ attempts: int = 3,
24
+ ) -> Result[str]:
25
+ acc = get_keypair(private_key_base58)
26
+ if not check_private_key(from_address, private_key_base58):
27
+ raise ValueError("from_address or private_key_base58 is invalid")
28
+
29
+ lamports = int(amount_sol * 10**9)
30
+ error = None
31
+ data = None
32
+ for _ in range(attempts):
33
+ try:
34
+ client = Client(utils.get_node(nodes))
35
+ # tx = Transaction(from_keypairs=[acc])
36
+ # ti = transfer(
37
+ # TransferParams(from_pubkey=acc.pubkey(), to_pubkey=Pubkey.from_string(recipient_address), lamports=lamports),
38
+ # )
39
+ # tx.add(ti)
40
+ # res = client.send_legacy_transaction(tx, acc)
41
+ ixns = [
42
+ transfer(
43
+ TransferParams(from_pubkey=acc.pubkey(), to_pubkey=Pubkey.from_string(recipient_address), lamports=lamports)
44
+ )
45
+ ]
46
+ msg = Message(ixns, acc.pubkey())
47
+ tx = Transaction([acc], msg, client.get_latest_blockhash().value.blockhash)
48
+ res = client.send_transaction(tx)
49
+ data = res.to_json()
50
+ return Ok(str(res.value), data=data)
51
+ except Exception as e:
52
+ error = e
53
+
54
+ return Err(error or "unknown", data=data)
55
+
56
+
57
+ class TransferInfo(BaseModel):
58
+ source: str
59
+ destination: str
60
+ lamports: int
61
+
62
+
63
+ def find_transfers(node: str, tx_signature: str) -> Result[list[TransferInfo]]:
64
+ res = rpc.get_transaction(node, tx_signature, encoding="jsonParsed")
65
+ if res.is_err():
66
+ return res # type: ignore[return-value]
67
+ result = []
68
+ try:
69
+ for ix in pydash.get(res.ok, "transaction.message.instructions"):
70
+ program_id = ix.get("programId")
71
+ ix_type = pydash.get(ix, "parsed.type")
72
+ if program_id == "11111111111111111111111111111111" and ix_type == "transfer":
73
+ source = pydash.get(ix, "parsed.info.source")
74
+ destination = pydash.get(ix, "parsed.info.destination")
75
+ lamports = pydash.get(ix, "parsed.info.lamports")
76
+ if source and destination and lamports:
77
+ result.append(TransferInfo(source=source, destination=destination, lamports=lamports))
78
+ return Ok(result, data=res.data)
79
+ except Exception as e:
80
+ return Err(e, res.data)
mm_sol/types.py ADDED
@@ -0,0 +1,4 @@
1
+ from collections.abc import Sequence
2
+
3
+ type Proxies = str | Sequence[str] | None
4
+ type Nodes = str | Sequence[str]