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/__init__.py +0 -0
- mm_sol/account.py +90 -0
- mm_sol/balance.py +18 -0
- mm_sol/block.py +58 -0
- mm_sol/cli/__init__.py +0 -0
- mm_sol/cli/cli.py +87 -0
- mm_sol/cli/cli_utils.py +36 -0
- mm_sol/cli/cmd/__init__.py +0 -0
- mm_sol/cli/cmd/balance_cmd.py +75 -0
- mm_sol/cli/cmd/balances_cmd.py +62 -0
- mm_sol/cli/cmd/example_cmd.py +8 -0
- mm_sol/cli/cmd/node_cmd.py +9 -0
- mm_sol/cli/cmd/transfer_sol_cmd.py +41 -0
- mm_sol/cli/cmd/wallet/__init__.py +0 -0
- mm_sol/cli/cmd/wallet/keypair_cmd.py +19 -0
- mm_sol/cli/cmd/wallet/new_cmd.py +14 -0
- mm_sol/cli/examples/balances.yml +11 -0
- mm_sol/cli/examples/transfer-sol.yml +8 -0
- mm_sol/py.typed +0 -0
- mm_sol/rpc.py +232 -0
- mm_sol/solana_cli.py +254 -0
- mm_sol/token.py +133 -0
- mm_sol/transfer.py +80 -0
- mm_sol/types.py +4 -0
- mm_sol/utils.py +38 -0
- mm_sol-0.2.5.dist-info/METADATA +9 -0
- mm_sol-0.2.5.dist-info/RECORD +29 -0
- mm_sol-0.2.5.dist-info/WHEEL +4 -0
- mm_sol-0.2.5.dist-info/entry_points.txt +2 -0
mm_sol/__init__.py
ADDED
|
File without changes
|
mm_sol/account.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
import base58
|
|
4
|
+
import pydash
|
|
5
|
+
from mm_std import Err, Ok, Result
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from solana.rpc.api import Client
|
|
8
|
+
from solders.keypair import Keypair
|
|
9
|
+
from solders.pubkey import Pubkey
|
|
10
|
+
from solders.rpc.responses import GetAccountInfoResp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NewAccount(BaseModel):
|
|
14
|
+
public_key: str
|
|
15
|
+
private_key_base58: str
|
|
16
|
+
private_key_arr: list[int]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_account() -> NewAccount:
|
|
20
|
+
keypair = Keypair()
|
|
21
|
+
public_key = str(keypair.pubkey())
|
|
22
|
+
private_key_base58 = base58.b58encode(bytes(keypair.to_bytes_array())).decode("utf-8")
|
|
23
|
+
private_key_arr = list(keypair.to_bytes_array())
|
|
24
|
+
return NewAccount(public_key=public_key, private_key_base58=private_key_base58, private_key_arr=private_key_arr)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_keypair(private_key: str | list[int]) -> Keypair:
|
|
28
|
+
if isinstance(private_key, str):
|
|
29
|
+
if "[" in private_key:
|
|
30
|
+
private_key_ = [int(x) for x in private_key.replace("[", "").replace("]", "").split(",")]
|
|
31
|
+
else:
|
|
32
|
+
private_key_ = base58.b58decode(private_key) # type: ignore[assignment]
|
|
33
|
+
else:
|
|
34
|
+
private_key_ = private_key
|
|
35
|
+
return Keypair.from_bytes(private_key_)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_private_key(public_key_base58: str, private_key: str | list[int]) -> bool:
|
|
39
|
+
return str(get_keypair(private_key).pubkey()) == public_key_base58
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_public_key(private_key: str) -> str:
|
|
43
|
+
if "[" in private_key:
|
|
44
|
+
private_key_ = [int(x) for x in private_key.replace("[", "").replace("]", "").split(",")]
|
|
45
|
+
else:
|
|
46
|
+
private_key_ = base58.b58decode(private_key) # type: ignore[assignment]
|
|
47
|
+
return str(Keypair.from_bytes(private_key_).pubkey())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_private_key_base58(private_key: str) -> str:
|
|
51
|
+
keypair = get_keypair(private_key)
|
|
52
|
+
return base58.b58encode(bytes(keypair.to_bytes_array())).decode("utf-8")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_private_key_arr(private_key: str) -> list[int]:
|
|
56
|
+
keypair = get_keypair(private_key)
|
|
57
|
+
return list(x for x in keypair.to_bytes_array()) # noqa: C400
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_private_key_arr_str(private_key: str) -> str:
|
|
61
|
+
return f"[{','.join(str(x) for x in get_private_key_arr(private_key))}]"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_empty_account(*, address: str, node: str | None = None, nodes: list[str] | None = None, attempts: int = 3) -> Result[bool]:
|
|
65
|
+
if not node and not nodes:
|
|
66
|
+
raise ValueError("node or nodes must be set")
|
|
67
|
+
error = None
|
|
68
|
+
data = None
|
|
69
|
+
for _ in range(attempts):
|
|
70
|
+
try:
|
|
71
|
+
client = Client(node or random.choice(nodes)) # type: ignore[arg-type]
|
|
72
|
+
res: GetAccountInfoResp = client.get_account_info(Pubkey.from_string(address))
|
|
73
|
+
data = res
|
|
74
|
+
slot = pydash.get(res, "result.context.slot")
|
|
75
|
+
value = pydash.get(res, "result.value")
|
|
76
|
+
if slot and value is None:
|
|
77
|
+
return Ok(True, data=data)
|
|
78
|
+
if slot and value:
|
|
79
|
+
return Ok(False, data=data)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
error = str(e)
|
|
82
|
+
return Err(error or "unknown response", data=data)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_valid_pubkey(pubkey: str) -> bool:
|
|
86
|
+
try:
|
|
87
|
+
Pubkey.from_string(pubkey)
|
|
88
|
+
return True # noqa: TRY300
|
|
89
|
+
except Exception:
|
|
90
|
+
return False
|
mm_sol/balance.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from mm_std import Err, Result
|
|
2
|
+
|
|
3
|
+
from mm_sol import rpc
|
|
4
|
+
from mm_sol.types import Nodes, Proxies
|
|
5
|
+
from mm_sol.utils import get_node, get_proxy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_balance(node: str, address: str, timeout: int = 10, proxy: str | None = None) -> Result[int]:
|
|
9
|
+
return rpc.get_balance(node, address, timeout, proxy)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_balance_with_retries(nodes: Nodes, address: str, retries: int, timeout: int = 10, proxies: Proxies = None) -> Result[int]:
|
|
13
|
+
res: Result[int] = Err("not started yet")
|
|
14
|
+
for _ in range(retries):
|
|
15
|
+
res = get_balance(get_node(nodes), address, timeout=timeout, proxy=get_proxy(proxies))
|
|
16
|
+
if res.is_ok():
|
|
17
|
+
return res
|
|
18
|
+
return res
|
mm_sol/block.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from mm_std import Err, Ok, Result
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from mm_sol.rpc import rpc_call
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BlockTxCount(BaseModel):
|
|
8
|
+
slot: int
|
|
9
|
+
block_time: int | None
|
|
10
|
+
vote_tx_ok: int
|
|
11
|
+
vote_tx_error: int
|
|
12
|
+
non_vote_tx_ok: int
|
|
13
|
+
non_vote_tx_error: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def calc_block_tx_count(node: str, slot: int, timeout: int = 10, proxy: str | None = None) -> Result[BlockTxCount]:
|
|
17
|
+
res = rpc_call(node=node, method="getBlock", params=[slot], timeout=timeout, proxy=proxy)
|
|
18
|
+
if res.is_err():
|
|
19
|
+
return res
|
|
20
|
+
vote_tx_ok = 0
|
|
21
|
+
vote_tx_error = 0
|
|
22
|
+
non_vote_tx_ok = 0
|
|
23
|
+
non_vote_tx_error = 0
|
|
24
|
+
vote_tx_keys = [
|
|
25
|
+
"SysvarS1otHashes111111111111111111111111111",
|
|
26
|
+
"SysvarC1ock11111111111111111111111111111111",
|
|
27
|
+
"Vote111111111111111111111111111111111111111",
|
|
28
|
+
]
|
|
29
|
+
try:
|
|
30
|
+
res_ok = res.unwrap()
|
|
31
|
+
txs = res_ok["transactions"]
|
|
32
|
+
block_time = res_ok["blockTime"]
|
|
33
|
+
for tx in txs:
|
|
34
|
+
is_error = tx["meta"]["err"] is not None
|
|
35
|
+
account_keys = tx["transaction"]["message"]["accountKeys"]
|
|
36
|
+
if len(account_keys) == 5 and vote_tx_keys == account_keys[2:]:
|
|
37
|
+
if is_error:
|
|
38
|
+
vote_tx_error += 1
|
|
39
|
+
else:
|
|
40
|
+
vote_tx_ok += 1
|
|
41
|
+
elif is_error:
|
|
42
|
+
non_vote_tx_error += 1
|
|
43
|
+
else:
|
|
44
|
+
non_vote_tx_ok += 1
|
|
45
|
+
|
|
46
|
+
return Ok(
|
|
47
|
+
BlockTxCount(
|
|
48
|
+
slot=slot,
|
|
49
|
+
vote_tx_ok=vote_tx_ok,
|
|
50
|
+
vote_tx_error=vote_tx_error,
|
|
51
|
+
non_vote_tx_ok=non_vote_tx_ok,
|
|
52
|
+
non_vote_tx_error=non_vote_tx_error,
|
|
53
|
+
block_time=block_time,
|
|
54
|
+
),
|
|
55
|
+
res.data,
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
return Err(e, data=res.data)
|
mm_sol/cli/__init__.py
ADDED
|
File without changes
|
mm_sol/cli/cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from mm_std import print_plain
|
|
6
|
+
|
|
7
|
+
from .cmd import balance_cmd, balances_cmd, example_cmd, node_cmd, transfer_sol_cmd
|
|
8
|
+
from .cmd.wallet import keypair_cmd, new_cmd
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
11
|
+
|
|
12
|
+
wallet_app = typer.Typer(no_args_is_help=True, help="Wallet commands: generate new accounts, private to address")
|
|
13
|
+
app.add_typer(wallet_app, name="wallet")
|
|
14
|
+
app.add_typer(wallet_app, name="w", hidden=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def version_callback(value: bool) -> None:
|
|
18
|
+
if value:
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
|
|
21
|
+
print_plain(f"mm-sol: {importlib.metadata.version('mm-sol')}")
|
|
22
|
+
raise typer.Exit
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.callback()
|
|
26
|
+
def main(_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True)) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigExample(str, Enum):
|
|
31
|
+
balances = "balances"
|
|
32
|
+
transfer_sol = "transfer-sol"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command(name="example", help="Print an example of config for a command")
|
|
36
|
+
def example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
|
|
37
|
+
example_cmd.run(command.value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command(name="balance", help="Gen account balance")
|
|
41
|
+
def balance_command(
|
|
42
|
+
wallet_address: Annotated[str, typer.Argument()],
|
|
43
|
+
token_address: Annotated[str | None, typer.Option("--token", "-t")] = None,
|
|
44
|
+
rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="MM_SOL_RPC_URL")] = "", # nosec
|
|
45
|
+
proxies_url: Annotated[str, typer.Option("--proxies-url", envvar="MM_SOL_PROXIES_URL")] = "", # nosec
|
|
46
|
+
lamport: bool = typer.Option(False, "--lamport", "-l", help="Print balances in lamports"),
|
|
47
|
+
) -> None:
|
|
48
|
+
balance_cmd.run(rpc_url, wallet_address, token_address, lamport, proxies_url)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command(name="balances", help="Print SOL and token balances for accounts")
|
|
52
|
+
def balances_command(
|
|
53
|
+
config_path: str, print_config: Annotated[bool, typer.Option("--config", "-c", help="Print config and exit")] = False
|
|
54
|
+
) -> None:
|
|
55
|
+
balances_cmd.run(config_path, print_config)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command(name="transfer-sol", help="Transfer SOL")
|
|
59
|
+
def transfer_sol_command(
|
|
60
|
+
config_path: str, print_config: Annotated[bool, typer.Option("--config", "-c", help="Print config and exit")] = False
|
|
61
|
+
) -> None:
|
|
62
|
+
transfer_sol_cmd.run(config_path, print_config)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command(name="node", help="Check RPC urls")
|
|
66
|
+
def node_command(
|
|
67
|
+
urls: Annotated[list[str], typer.Argument()],
|
|
68
|
+
proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
node_cmd.run(urls, proxy)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@wallet_app.command(name="new", help="Generate new accounts")
|
|
74
|
+
def generate_accounts_command(
|
|
75
|
+
limit: Annotated[int, typer.Option("--limit", "-l")] = 5,
|
|
76
|
+
array: Annotated[bool, typer.Option("--array", help="Print private key in the array format.")] = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
new_cmd.run(limit, array)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@wallet_app.command(name="keypair", help="Print public, private_base58, private_arr by a private key")
|
|
82
|
+
def keypair_command(private_key: str) -> None:
|
|
83
|
+
keypair_cmd.run(private_key)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main_":
|
|
87
|
+
app()
|
mm_sol/cli/cli_utils.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import pydash
|
|
4
|
+
from mm_std import BaseConfig, fatal, hr, print_json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def print_config_and_exit(exit_: bool, config: BaseConfig, exclude: set[str] | None = None) -> None:
|
|
8
|
+
if exit_:
|
|
9
|
+
print_json(config.model_dump(exclude=exclude))
|
|
10
|
+
sys.exit(0)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def public_rpc_url(url: str | None) -> str:
|
|
14
|
+
if not url:
|
|
15
|
+
return "https://api.mainnet-beta.solana.com"
|
|
16
|
+
|
|
17
|
+
match url.lower():
|
|
18
|
+
case "mainnet":
|
|
19
|
+
return "https://api.mainnet-beta.solana.com"
|
|
20
|
+
case "testnet":
|
|
21
|
+
return "https://api.testnet.solana.com"
|
|
22
|
+
case "devnet":
|
|
23
|
+
return "https://api.devnet.solana.com"
|
|
24
|
+
|
|
25
|
+
return url
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_proxies_from_url(proxies_url: str) -> list[str]:
|
|
29
|
+
try:
|
|
30
|
+
res = hr(proxies_url)
|
|
31
|
+
if res.is_error():
|
|
32
|
+
fatal(f"Can't get proxies: {res.error}")
|
|
33
|
+
proxies = [p.strip() for p in res.body.splitlines() if p.strip()]
|
|
34
|
+
return pydash.uniq(proxies)
|
|
35
|
+
except Exception as err:
|
|
36
|
+
fatal(f"Can't get proxies from the url: {err}")
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from mm_std import Ok, print_json
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
from mm_sol import balance, token
|
|
7
|
+
from mm_sol.cli import cli_utils
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HumanReadableBalanceResult(BaseModel):
|
|
11
|
+
sol_balance: Decimal | None
|
|
12
|
+
token_balance: Decimal | None
|
|
13
|
+
token_decimals: int | None
|
|
14
|
+
errors: list[str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BalanceResult(BaseModel):
|
|
18
|
+
sol_balance: int | None = None
|
|
19
|
+
token_balance: int | None = None
|
|
20
|
+
token_decimals: int | None = None
|
|
21
|
+
errors: list[str] = Field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
def to_human_readable(self) -> HumanReadableBalanceResult:
|
|
24
|
+
sol_balance = Decimal(self.sol_balance) / 10**9 if self.sol_balance is not None else None
|
|
25
|
+
token_balance = None
|
|
26
|
+
if self.token_balance is not None and self.token_decimals is not None:
|
|
27
|
+
token_balance = Decimal(self.token_balance) / 10**self.token_decimals
|
|
28
|
+
return HumanReadableBalanceResult(
|
|
29
|
+
sol_balance=sol_balance, token_balance=token_balance, token_decimals=self.token_decimals, errors=self.errors
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(
|
|
34
|
+
rpc_url: str,
|
|
35
|
+
wallet_address: str,
|
|
36
|
+
token_address: str | None,
|
|
37
|
+
lamport: bool,
|
|
38
|
+
proxies_url: str | None,
|
|
39
|
+
) -> None:
|
|
40
|
+
result = BalanceResult()
|
|
41
|
+
|
|
42
|
+
rpc_url = cli_utils.public_rpc_url(rpc_url)
|
|
43
|
+
proxies = cli_utils.load_proxies_from_url(proxies_url) if proxies_url else None
|
|
44
|
+
|
|
45
|
+
# sol balance
|
|
46
|
+
sol_balance_res = balance.get_balance_with_retries(rpc_url, wallet_address, retries=3, proxies=proxies)
|
|
47
|
+
if isinstance(sol_balance_res, Ok):
|
|
48
|
+
result.sol_balance = sol_balance_res.ok
|
|
49
|
+
else:
|
|
50
|
+
result.errors.append("sol_balance: " + sol_balance_res.err)
|
|
51
|
+
|
|
52
|
+
# token balance
|
|
53
|
+
if token_address:
|
|
54
|
+
token_balance_res = token.get_balance_with_retries(
|
|
55
|
+
nodes=rpc_url,
|
|
56
|
+
owner_address=wallet_address,
|
|
57
|
+
token_mint_address=token_address,
|
|
58
|
+
retries=3,
|
|
59
|
+
proxies=proxies,
|
|
60
|
+
)
|
|
61
|
+
if isinstance(token_balance_res, Ok):
|
|
62
|
+
result.token_balance = token_balance_res.ok
|
|
63
|
+
else:
|
|
64
|
+
result.errors.append("token_balance: " + token_balance_res.err)
|
|
65
|
+
|
|
66
|
+
decimals_res = token.get_decimals_with_retries(rpc_url, token_address, retries=3, proxies=proxies)
|
|
67
|
+
if isinstance(decimals_res, Ok):
|
|
68
|
+
result.token_decimals = decimals_res.ok
|
|
69
|
+
else:
|
|
70
|
+
result.errors.append("token_decimals: " + decimals_res.err)
|
|
71
|
+
|
|
72
|
+
if lamport:
|
|
73
|
+
print_json(result)
|
|
74
|
+
else:
|
|
75
|
+
print_json(result.to_human_readable())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mm_std import BaseConfig, print_console, str_to_list
|
|
6
|
+
from pydantic import StrictStr, field_validator
|
|
7
|
+
|
|
8
|
+
from mm_sol import balance, utils
|
|
9
|
+
from mm_sol.cli import cli_utils
|
|
10
|
+
from mm_sol.token import get_balance_with_retries
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Config(BaseConfig):
|
|
14
|
+
accounts: list[StrictStr]
|
|
15
|
+
nodes: list[StrictStr]
|
|
16
|
+
tokens: list[StrictStr] | None = None
|
|
17
|
+
|
|
18
|
+
@field_validator("accounts", "nodes", "tokens", mode="before")
|
|
19
|
+
def to_list_validator(cls, v: list[str] | str | None) -> list[str]:
|
|
20
|
+
return str_to_list(v)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def random_node(self) -> str:
|
|
24
|
+
return random.choice(self.nodes)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(config_path: str, print_config: bool) -> None:
|
|
28
|
+
config = Config.read_config_or_exit(config_path)
|
|
29
|
+
cli_utils.print_config_and_exit(print_config, config)
|
|
30
|
+
|
|
31
|
+
# config = parse_config(ctx, config_path, Config)
|
|
32
|
+
# print_config_and_exit(ctx, config)
|
|
33
|
+
result: dict[str, Any] = {"sol": _get_sol_balances(config.accounts, config.nodes)}
|
|
34
|
+
result["sol_sum"] = sum([v for v in result["sol"].values() if v is not None])
|
|
35
|
+
|
|
36
|
+
if config.tokens:
|
|
37
|
+
for token in config.tokens:
|
|
38
|
+
result[token] = _get_token_balances(token, config.accounts, config.nodes)
|
|
39
|
+
result[token + "_sum"] = sum([v for v in result[token].values() if v is not None])
|
|
40
|
+
|
|
41
|
+
print_console(result, print_json=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_token_balances(token: str, accounts: list[str], nodes: list[str]) -> dict[str, int | None]:
|
|
45
|
+
result = {}
|
|
46
|
+
for account in accounts:
|
|
47
|
+
# result[account] = _get_token_balance(token, account, nodes)
|
|
48
|
+
result[account] = get_balance_with_retries(
|
|
49
|
+
nodes=nodes,
|
|
50
|
+
owner_address=account,
|
|
51
|
+
token_mint_address=token,
|
|
52
|
+
retries=3,
|
|
53
|
+
).ok_or_none()
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_sol_balances(accounts: list[str], nodes: list[str]) -> dict[str, Decimal | None]:
|
|
58
|
+
result = {}
|
|
59
|
+
for account in accounts:
|
|
60
|
+
res = balance.get_balance_with_retries(nodes=nodes, address=account, retries=3)
|
|
61
|
+
result[account] = utils.lamports_to_sol(res.unwrap(), ndigits=2) if res.is_ok() else None
|
|
62
|
+
return result
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from mm_std import BaseConfig, print_console, str_to_list
|
|
5
|
+
from pydantic import StrictStr, field_validator
|
|
6
|
+
|
|
7
|
+
from mm_sol.cli import cli_utils
|
|
8
|
+
from mm_sol.transfer import transfer_sol
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Config(BaseConfig):
|
|
12
|
+
from_address: StrictStr
|
|
13
|
+
private_key: StrictStr
|
|
14
|
+
recipients: list[StrictStr]
|
|
15
|
+
nodes: list[StrictStr]
|
|
16
|
+
amount: Decimal
|
|
17
|
+
|
|
18
|
+
@field_validator("recipients", "nodes", mode="before")
|
|
19
|
+
def to_list_validator(cls, v: list[str] | str | None) -> list[str]:
|
|
20
|
+
return str_to_list(v)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def random_node(self) -> str:
|
|
24
|
+
return random.choice(self.nodes)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(config_path: str, print_config: bool) -> None:
|
|
28
|
+
config = Config.read_config_or_exit(config_path)
|
|
29
|
+
cli_utils.print_config_and_exit(print_config, config)
|
|
30
|
+
|
|
31
|
+
result = {}
|
|
32
|
+
for recipient in config.recipients:
|
|
33
|
+
res = transfer_sol(
|
|
34
|
+
from_address=config.from_address,
|
|
35
|
+
private_key_base58=config.private_key,
|
|
36
|
+
recipient_address=recipient,
|
|
37
|
+
amount_sol=config.amount,
|
|
38
|
+
nodes=config.nodes,
|
|
39
|
+
)
|
|
40
|
+
result[recipient] = res.ok_or_err()
|
|
41
|
+
print_console(result, print_json=True)
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from mm_std import print_console
|
|
4
|
+
|
|
5
|
+
from mm_sol.account import (
|
|
6
|
+
get_private_key_arr_str,
|
|
7
|
+
get_private_key_base58,
|
|
8
|
+
get_public_key,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(private_key: str) -> None:
|
|
13
|
+
if (file := Path(private_key)).is_file():
|
|
14
|
+
private_key = file.read_text()
|
|
15
|
+
|
|
16
|
+
public = get_public_key(private_key)
|
|
17
|
+
private_base58 = get_private_key_base58(private_key)
|
|
18
|
+
private_arr = get_private_key_arr_str(private_key)
|
|
19
|
+
print_console({"public": public, "private_base58": private_base58, "private_arr": private_arr}, print_json=True)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from mm_std import print_console
|
|
2
|
+
|
|
3
|
+
from mm_sol.account import generate_account, get_private_key_arr_str
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run(limit: int, array: bool) -> None:
|
|
7
|
+
result = {}
|
|
8
|
+
for _ in range(limit):
|
|
9
|
+
acc = generate_account()
|
|
10
|
+
private_key = acc.private_key_base58
|
|
11
|
+
if array:
|
|
12
|
+
private_key = get_private_key_arr_str(acc.private_key_base58)
|
|
13
|
+
result[acc.public_key] = private_key
|
|
14
|
+
print_console(result, print_json=True)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
accounts: |
|
|
2
|
+
5BJ9ViMj4gi2BBX3wbCEJ4p4vpVWKpG6ja2aQP2ACBUv
|
|
3
|
+
HbSAUDjzpr44fWzf9Ynj3V1jq3wBsgg88N88P4vzeGPX
|
|
4
|
+
|
|
5
|
+
tokens: |
|
|
6
|
+
7XtmNSHJDHTZdx2K1S8D529kHsBbt3Civxt6vUHrGxBR
|
|
7
|
+
65FKbLPtrssmc8aVn9DEBY8VTGB85vsgadrmaW9H4nEB
|
|
8
|
+
|
|
9
|
+
nodes: |
|
|
10
|
+
https://api.mainnet-beta.solana.com
|
|
11
|
+
# https://rpc.ankr.com/solana
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from_address: ERB7SPx1XxH35pLu4Lkg9ChPQgsnwJGLZG8RsEE87XNw
|
|
2
|
+
private_key: 3h92ZLyibvqQ9f923s66eN7V1iyG5WRRo3y9nciH4swUiLrkrYRdRM8q3DrtJ9JjTjiU9BT2r2qNzuiCmcZPVvqV
|
|
3
|
+
amount: 1.2 # in SOL
|
|
4
|
+
recipients:
|
|
5
|
+
- Rjg3K9PPDm1B5bmUP7eXZNfmApgvDPF9SrTh12dLRH9
|
|
6
|
+
|
|
7
|
+
nodes: |
|
|
8
|
+
https://api.mainnet-beta.solana.com
|
mm_sol/py.typed
ADDED
|
File without changes
|