mm-sol 0.7.3__py3-none-any.whl → 0.8.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/__init__.py +1 -0
- mm_sol/account.py +19 -0
- mm_sol/cli/__init__.py +1 -0
- mm_sol/cli/calcs.py +6 -0
- mm_sol/cli/cli.py +19 -5
- mm_sol/cli/cli_utils.py +18 -2
- mm_sol/cli/cmd/__init__.py +1 -0
- mm_sol/cli/cmd/balance_cmd.py +11 -3
- mm_sol/cli/cmd/balances_cmd.py +13 -6
- mm_sol/cli/cmd/example_cmd.py +5 -2
- mm_sol/cli/cmd/node_cmd.py +5 -2
- mm_sol/cli/cmd/transfer_cmd.py +25 -8
- mm_sol/cli/cmd/wallet/__init__.py +1 -0
- mm_sol/cli/cmd/wallet/keypair_cmd.py +5 -2
- mm_sol/cli/cmd/wallet/mnemonic_cmd.py +5 -2
- mm_sol/cli/validators.py +11 -0
- mm_sol/constants.py +3 -0
- mm_sol/converters.py +6 -0
- mm_sol/retry.py +7 -0
- mm_sol/rpc.py +8 -2
- mm_sol/rpc_sync.py +35 -3
- mm_sol/spl_token.py +4 -0
- mm_sol/transfer.py +8 -1
- mm_sol/utils.py +5 -0
- mm_sol-0.8.0.dist-info/METADATA +10 -0
- mm_sol-0.8.0.dist-info/RECORD +31 -0
- {mm_sol-0.7.3.dist-info → mm_sol-0.8.0.dist-info}/WHEEL +1 -1
- mm_sol-0.7.3.dist-info/METADATA +0 -11
- mm_sol-0.7.3.dist-info/RECORD +0 -31
- {mm_sol-0.7.3.dist-info → mm_sol-0.8.0.dist-info}/entry_points.txt +0 -0
mm_sol/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Library for interacting with Solana blockchain."""
|
mm_sol/account.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Solana account management: key generation, derivation, and validation."""
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
4
|
from dataclasses import dataclass
|
|
3
5
|
|
|
@@ -8,10 +10,15 @@ from solders.keypair import Keypair
|
|
|
8
10
|
from solders.pubkey import Pubkey
|
|
9
11
|
|
|
10
12
|
PHANTOM_DERIVATION_PATH = "m/44'/501'/{i}'/0'"
|
|
13
|
+
"""Default Phantom wallet derivation path template."""
|
|
14
|
+
|
|
11
15
|
WORD_STRENGTH = {12: 128, 15: 160, 18: 192, 21: 224, 24: 256}
|
|
16
|
+
"""Mapping of mnemonic word count to entropy bits."""
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
class NewAccount(BaseModel):
|
|
20
|
+
"""Newly generated Solana account with public and private keys."""
|
|
21
|
+
|
|
15
22
|
public_key: str
|
|
16
23
|
private_key_base58: str
|
|
17
24
|
private_key_arr: list[int]
|
|
@@ -19,6 +26,8 @@ class NewAccount(BaseModel):
|
|
|
19
26
|
|
|
20
27
|
@dataclass
|
|
21
28
|
class DerivedAccount:
|
|
29
|
+
"""Account derived from a mnemonic at a specific derivation path."""
|
|
30
|
+
|
|
22
31
|
index: int
|
|
23
32
|
path: str
|
|
24
33
|
address: str
|
|
@@ -26,6 +35,7 @@ class DerivedAccount:
|
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
def generate_mnemonic(num_words: int = 24) -> str:
|
|
38
|
+
"""Generate a BIP39 mnemonic phrase with the specified number of words."""
|
|
29
39
|
if num_words not in WORD_STRENGTH:
|
|
30
40
|
raise ValueError(f"num_words must be one of {list(WORD_STRENGTH.keys())}")
|
|
31
41
|
mnemonic = Mnemonic("english")
|
|
@@ -33,6 +43,7 @@ def generate_mnemonic(num_words: int = 24) -> str:
|
|
|
33
43
|
|
|
34
44
|
|
|
35
45
|
def derive_accounts(mnemonic: str, passphrase: str, derivation_path: str, limit: int) -> list[DerivedAccount]:
|
|
46
|
+
"""Derive multiple accounts from a mnemonic using the given derivation path template."""
|
|
36
47
|
if "{i}" not in derivation_path:
|
|
37
48
|
raise ValueError("derivation_path must contain {i}, for example: m/44'/501'/{i}'/0'")
|
|
38
49
|
|
|
@@ -54,6 +65,7 @@ def derive_accounts(mnemonic: str, passphrase: str, derivation_path: str, limit:
|
|
|
54
65
|
|
|
55
66
|
|
|
56
67
|
def generate_account() -> NewAccount:
|
|
68
|
+
"""Generate a new random Solana keypair and return it as a NewAccount."""
|
|
57
69
|
keypair = Keypair()
|
|
58
70
|
public_key = str(keypair.pubkey())
|
|
59
71
|
private_key_base58 = base58.b58encode(bytes(keypair.to_bytes())).decode("utf-8")
|
|
@@ -62,6 +74,7 @@ def generate_account() -> NewAccount:
|
|
|
62
74
|
|
|
63
75
|
|
|
64
76
|
def get_keypair(private_key: str | list[int]) -> Keypair:
|
|
77
|
+
"""Create a Keypair from a base58 string, JSON array string, or integer list."""
|
|
65
78
|
if isinstance(private_key, str):
|
|
66
79
|
if "[" in private_key:
|
|
67
80
|
private_key_ = [int(x) for x in private_key.replace("[", "").replace("]", "").split(",")]
|
|
@@ -73,12 +86,14 @@ def get_keypair(private_key: str | list[int]) -> Keypair:
|
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
def check_private_key(public_key: str | Pubkey, private_key: str | list[int]) -> bool:
|
|
89
|
+
"""Check whether a private key corresponds to the given public key."""
|
|
76
90
|
if isinstance(public_key, str):
|
|
77
91
|
public_key = Pubkey.from_string(public_key)
|
|
78
92
|
return get_keypair(private_key).pubkey() == public_key
|
|
79
93
|
|
|
80
94
|
|
|
81
95
|
def get_public_key(private_key: str) -> str:
|
|
96
|
+
"""Derive the public key address from a private key string."""
|
|
82
97
|
if "[" in private_key:
|
|
83
98
|
private_key_ = [int(x) for x in private_key.replace("[", "").replace("]", "").split(",")]
|
|
84
99
|
else:
|
|
@@ -87,20 +102,24 @@ def get_public_key(private_key: str) -> str:
|
|
|
87
102
|
|
|
88
103
|
|
|
89
104
|
def get_private_key_base58(private_key: str) -> str:
|
|
105
|
+
"""Convert a private key to base58 encoding."""
|
|
90
106
|
keypair = get_keypair(private_key)
|
|
91
107
|
return base58.b58encode(bytes(keypair.to_bytes())).decode("utf-8")
|
|
92
108
|
|
|
93
109
|
|
|
94
110
|
def get_private_key_arr(private_key: str) -> list[int]:
|
|
111
|
+
"""Convert a private key to a list of byte integers."""
|
|
95
112
|
keypair = get_keypair(private_key)
|
|
96
113
|
return list(x for x in keypair.to_bytes()) # noqa: C400
|
|
97
114
|
|
|
98
115
|
|
|
99
116
|
def get_private_key_arr_str(private_key: str) -> str:
|
|
117
|
+
"""Convert a private key to a JSON-style array string."""
|
|
100
118
|
return f"[{','.join(str(x) for x in get_private_key_arr(private_key))}]"
|
|
101
119
|
|
|
102
120
|
|
|
103
121
|
def is_address(pubkey: str) -> bool:
|
|
122
|
+
"""Check whether a string is a valid Solana address."""
|
|
104
123
|
with contextlib.suppress(Exception):
|
|
105
124
|
Pubkey.from_string(pubkey)
|
|
106
125
|
return True
|
mm_sol/cli/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI interface for mm-sol."""
|
mm_sol/cli/calcs.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Value expression calculators for SOL and token amounts with optional balance variables."""
|
|
2
|
+
|
|
1
3
|
from mm_result import Result
|
|
2
4
|
from mm_web3 import Nodes, Proxies
|
|
3
5
|
from mm_web3.calcs import calc_expression_with_vars
|
|
@@ -7,16 +9,19 @@ from mm_sol.constants import UNIT_DECIMALS
|
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def calc_sol_expression(expression: str, variables: dict[str, int] | None = None) -> int:
|
|
12
|
+
"""Evaluate a SOL expression string into lamports."""
|
|
10
13
|
return calc_expression_with_vars(expression, variables, unit_decimals=UNIT_DECIMALS)
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def calc_token_expression(expression: str, token_decimals: int, variables: dict[str, int] | None = None) -> int:
|
|
17
|
+
"""Evaluate a token expression string into smallest units."""
|
|
14
18
|
return calc_expression_with_vars(expression, variables, unit_decimals={"t": token_decimals})
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
async def calc_sol_value_for_address(
|
|
18
22
|
*, nodes: Nodes, value_expression: str, address: str, proxies: Proxies, fee: int
|
|
19
23
|
) -> Result[int]:
|
|
24
|
+
"""Calculate SOL value in lamports for an address, resolving 'balance' variable if used."""
|
|
20
25
|
value_expression = value_expression.lower()
|
|
21
26
|
variables: dict[str, int] | None = None
|
|
22
27
|
if "balance" in value_expression:
|
|
@@ -34,6 +39,7 @@ async def calc_sol_value_for_address(
|
|
|
34
39
|
async def calc_token_value_for_address(
|
|
35
40
|
*, nodes: Nodes, value_expression: str, owner: str, token: str, token_decimals: int, proxies: Proxies
|
|
36
41
|
) -> Result[int]:
|
|
42
|
+
"""Calculate token value in smallest units for an address, resolving 'balance' variable if used."""
|
|
37
43
|
variables: dict[str, int] | None = None
|
|
38
44
|
value_expression = value_expression.lower()
|
|
39
45
|
if "balance" in value_expression:
|
mm_sol/cli/cli.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
"""Main CLI entry point and command definitions for mm-sol."""
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
|
-
from enum import
|
|
4
|
+
from enum import StrEnum
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Annotated
|
|
5
7
|
|
|
6
|
-
import mm_print
|
|
7
8
|
import typer
|
|
9
|
+
from mm_print import print_plain
|
|
8
10
|
|
|
9
11
|
from mm_sol.account import PHANTOM_DERIVATION_PATH
|
|
10
12
|
|
|
@@ -13,8 +15,10 @@ from .cmd import balance_cmd, balances_cmd, example_cmd, node_cmd, transfer_cmd
|
|
|
13
15
|
from .cmd.transfer_cmd import TransferCmdParams
|
|
14
16
|
from .cmd.wallet import keypair_cmd, mnemonic_cmd
|
|
15
17
|
|
|
18
|
+
"""Main CLI application."""
|
|
16
19
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
17
20
|
|
|
21
|
+
"""Wallet subcommand group."""
|
|
18
22
|
wallet_app = typer.Typer(
|
|
19
23
|
no_args_is_help=True, help="Wallet-related commands: generate new accounts, derive addresses from private keys, and more"
|
|
20
24
|
)
|
|
@@ -23,23 +27,27 @@ app.add_typer(wallet_app, name="w", hidden=True)
|
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
def version_callback(value: bool) -> None:
|
|
30
|
+
"""Print version and exit when --version is passed."""
|
|
26
31
|
if value:
|
|
27
|
-
|
|
32
|
+
print_plain(f"mm-sol: {cli_utils.get_version()}")
|
|
28
33
|
raise typer.Exit
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
@app.callback()
|
|
32
37
|
def main(_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True)) -> None:
|
|
33
|
-
|
|
38
|
+
"""Solana CLI tool."""
|
|
39
|
+
|
|
34
40
|
|
|
41
|
+
class ConfigExample(StrEnum):
|
|
42
|
+
"""Available example configuration names."""
|
|
35
43
|
|
|
36
|
-
class ConfigExample(str, Enum):
|
|
37
44
|
balances = "balances"
|
|
38
45
|
transfer = "transfer"
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
@app.command(name="example", help="Displays an example configuration for a command")
|
|
42
49
|
def example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
|
|
50
|
+
"""Display an example configuration for the given command."""
|
|
43
51
|
example_cmd.run(command.value)
|
|
44
52
|
|
|
45
53
|
|
|
@@ -51,6 +59,7 @@ def balance_command(
|
|
|
51
59
|
proxies_url: Annotated[str, typer.Option("--proxies-url", envvar="MM_SOL_PROXIES_URL")] = "", # nosec
|
|
52
60
|
lamport: bool = typer.Option(False, "--lamport", "-l", help="Print balances in lamports"),
|
|
53
61
|
) -> None:
|
|
62
|
+
"""Fetch and print SOL and optional token balance for an account."""
|
|
54
63
|
asyncio.run(balance_cmd.run(rpc_url, wallet_address, token_address, lamport, proxies_url))
|
|
55
64
|
|
|
56
65
|
|
|
@@ -58,6 +67,7 @@ def balance_command(
|
|
|
58
67
|
def balances_command(
|
|
59
68
|
config_path: Path, print_config: Annotated[bool, typer.Option("--config", "-c", help="Print config and exit")] = False
|
|
60
69
|
) -> None:
|
|
70
|
+
"""Display SOL and token balances for multiple accounts from a config file."""
|
|
61
71
|
asyncio.run(balances_cmd.run(config_path, print_config))
|
|
62
72
|
|
|
63
73
|
|
|
@@ -72,6 +82,7 @@ def transfer_command(
|
|
|
72
82
|
no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
|
|
73
83
|
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
74
84
|
) -> None:
|
|
85
|
+
"""Execute SOL or SPL token transfers based on a config file."""
|
|
75
86
|
asyncio.run(
|
|
76
87
|
transfer_cmd.run(
|
|
77
88
|
TransferCmdParams(
|
|
@@ -93,6 +104,7 @@ def node_command(
|
|
|
93
104
|
urls: Annotated[list[str], typer.Argument()],
|
|
94
105
|
proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
|
|
95
106
|
) -> None:
|
|
107
|
+
"""Check RPC node availability by fetching block height."""
|
|
96
108
|
asyncio.run(node_cmd.run(urls, proxy))
|
|
97
109
|
|
|
98
110
|
|
|
@@ -105,11 +117,13 @@ def wallet_mnemonic_command( # nosec
|
|
|
105
117
|
words: int = typer.Option(12, "--words", "-w", help="Number of mnemonic words"),
|
|
106
118
|
limit: int = typer.Option(5, "--limit", "-l"),
|
|
107
119
|
) -> None:
|
|
120
|
+
"""Generate or derive accounts from a mnemonic phrase."""
|
|
108
121
|
mnemonic_cmd.run(mnemonic, passphrase, words, derivation_path, limit)
|
|
109
122
|
|
|
110
123
|
|
|
111
124
|
@wallet_app.command(name="keypair", help="Print public, private_base58, private_arr by a private key")
|
|
112
125
|
def keypair_command(private_key: str) -> None:
|
|
126
|
+
"""Print keypair details from a private key."""
|
|
113
127
|
keypair_cmd.run(private_key)
|
|
114
128
|
|
|
115
129
|
|
mm_sol/cli/cli_utils.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
"""Shared CLI utilities: config printing, RPC URL resolution, and helpers."""
|
|
2
|
+
|
|
1
3
|
import importlib.metadata
|
|
2
4
|
import time
|
|
3
5
|
from pathlib import Path
|
|
6
|
+
from typing import NoReturn
|
|
4
7
|
|
|
5
|
-
import
|
|
8
|
+
import typer
|
|
6
9
|
from loguru import logger
|
|
10
|
+
from mm_print import print_json
|
|
7
11
|
from mm_web3 import Nodes, Proxies, Web3CliConfig, random_node, random_proxy
|
|
8
12
|
from pydantic import BaseModel
|
|
9
13
|
from solders.signature import Signature
|
|
@@ -12,23 +16,28 @@ from mm_sol.utils import get_client
|
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
def get_version() -> str:
|
|
19
|
+
"""Return the installed mm-sol package version."""
|
|
15
20
|
return importlib.metadata.version("mm-sol")
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
class BaseConfigParams(BaseModel):
|
|
24
|
+
"""Base parameters shared by CLI commands that read a config file."""
|
|
25
|
+
|
|
19
26
|
config_path: Path
|
|
20
27
|
print_config_and_exit: bool
|
|
21
28
|
|
|
22
29
|
|
|
23
30
|
def print_config(config: Web3CliConfig, exclude: set[str] | None = None, count: set[str] | None = None) -> None:
|
|
31
|
+
"""Print a config as JSON, optionally replacing list fields with their counts."""
|
|
24
32
|
data = config.model_dump(exclude=exclude)
|
|
25
33
|
if count:
|
|
26
34
|
for k in count:
|
|
27
35
|
data[k] = len(data[k])
|
|
28
|
-
|
|
36
|
+
print_json(data)
|
|
29
37
|
|
|
30
38
|
|
|
31
39
|
def public_rpc_url(url: str | None) -> str:
|
|
40
|
+
"""Resolve a shorthand network name (mainnet/testnet/devnet) to its full RPC URL."""
|
|
32
41
|
if not url:
|
|
33
42
|
return "https://api.mainnet-beta.solana.com"
|
|
34
43
|
|
|
@@ -44,6 +53,7 @@ def public_rpc_url(url: str | None) -> str:
|
|
|
44
53
|
|
|
45
54
|
|
|
46
55
|
def wait_confirmation(nodes: Nodes, proxies: Proxies, signature: Signature, log_prefix: str) -> bool:
|
|
56
|
+
"""Poll for transaction confirmation, returning True if confirmed within 30 seconds."""
|
|
47
57
|
count = 0
|
|
48
58
|
while True:
|
|
49
59
|
try:
|
|
@@ -60,3 +70,9 @@ def wait_confirmation(nodes: Nodes, proxies: Proxies, signature: Signature, log_
|
|
|
60
70
|
if count > 30:
|
|
61
71
|
logger.error(f"{log_prefix}: can't get confirmation, timeout")
|
|
62
72
|
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def fatal(message: str) -> NoReturn:
|
|
76
|
+
"""Print an error message and exit with code 1."""
|
|
77
|
+
typer.echo(message)
|
|
78
|
+
raise typer.Exit(1)
|
mm_sol/cli/cmd/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations."""
|
mm_sol/cli/cmd/balance_cmd.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
"""Single account balance query command."""
|
|
2
|
+
|
|
1
3
|
from decimal import Decimal
|
|
2
4
|
|
|
3
|
-
import
|
|
5
|
+
from mm_print import print_json
|
|
4
6
|
from mm_web3 import fetch_proxies
|
|
5
7
|
from pydantic import BaseModel, Field
|
|
6
8
|
|
|
@@ -10,6 +12,8 @@ from mm_sol.cli import cli_utils
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class HumanReadableBalanceResult(BaseModel):
|
|
15
|
+
"""Balance result with SOL and token values in human-readable decimals."""
|
|
16
|
+
|
|
13
17
|
sol_balance: Decimal | None
|
|
14
18
|
token_balance: Decimal | None
|
|
15
19
|
token_decimals: int | None
|
|
@@ -17,12 +21,15 @@ class HumanReadableBalanceResult(BaseModel):
|
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class BalanceResult(BaseModel):
|
|
24
|
+
"""Balance result with SOL and token values in smallest units."""
|
|
25
|
+
|
|
20
26
|
sol_balance: int | None = None
|
|
21
27
|
token_balance: int | None = None
|
|
22
28
|
token_decimals: int | None = None
|
|
23
29
|
errors: list[str] = Field(default_factory=list)
|
|
24
30
|
|
|
25
31
|
def to_human_readable(self) -> HumanReadableBalanceResult:
|
|
32
|
+
"""Convert balances from smallest units to human-readable decimals."""
|
|
26
33
|
sol_balance = Decimal(self.sol_balance) / 10**9 if self.sol_balance is not None else None
|
|
27
34
|
token_balance = None
|
|
28
35
|
if self.token_balance is not None and self.token_decimals is not None:
|
|
@@ -39,6 +46,7 @@ async def run(
|
|
|
39
46
|
lamport: bool,
|
|
40
47
|
proxies_url: str | None,
|
|
41
48
|
) -> None:
|
|
49
|
+
"""Fetch and print SOL and optional token balance for a single account."""
|
|
42
50
|
result = BalanceResult()
|
|
43
51
|
|
|
44
52
|
rpc_url = cli_utils.public_rpc_url(rpc_url)
|
|
@@ -68,6 +76,6 @@ async def run(
|
|
|
68
76
|
result.errors.append("token_decimals: " + decimals_res.unwrap_err())
|
|
69
77
|
|
|
70
78
|
if lamport:
|
|
71
|
-
|
|
79
|
+
print_json(result)
|
|
72
80
|
else:
|
|
73
|
-
|
|
81
|
+
print_json(result.to_human_readable())
|
mm_sol/cli/cmd/balances_cmd.py
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
"""Multi-account balances query command."""
|
|
2
|
+
|
|
1
3
|
import random
|
|
2
4
|
from decimal import Decimal
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Annotated, Any
|
|
5
7
|
|
|
6
|
-
import
|
|
8
|
+
from mm_print import print_json
|
|
7
9
|
from mm_web3 import ConfigValidators, Web3CliConfig
|
|
8
10
|
from pydantic import BeforeValidator, Field
|
|
9
11
|
|
|
10
12
|
import mm_sol.retry
|
|
11
13
|
from mm_sol import converters, retry
|
|
14
|
+
from mm_sol.cli.cli_utils import fatal
|
|
12
15
|
from mm_sol.cli.validators import Validators
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class Config(Web3CliConfig):
|
|
19
|
+
"""Configuration for the balances command."""
|
|
20
|
+
|
|
16
21
|
accounts: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
|
|
17
22
|
tokens: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
|
|
18
23
|
nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
|
|
@@ -20,10 +25,12 @@ class Config(Web3CliConfig):
|
|
|
20
25
|
|
|
21
26
|
@property
|
|
22
27
|
def random_node(self) -> str:
|
|
28
|
+
"""Return a randomly selected RPC node URL."""
|
|
23
29
|
return random.choice(self.nodes)
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
async def run(config_path: Path, print_config: bool) -> None:
|
|
33
|
+
"""Fetch and print SOL and token balances for all configured accounts."""
|
|
27
34
|
config = Config.read_toml_config_or_exit(config_path)
|
|
28
35
|
if print_config:
|
|
29
36
|
config.print_and_exit()
|
|
@@ -35,19 +42,20 @@ async def run(config_path: Path, print_config: bool) -> None:
|
|
|
35
42
|
for token_address in config.tokens:
|
|
36
43
|
res = await mm_sol.retry.get_token_decimals(3, config.nodes, config.proxies, token=token_address)
|
|
37
44
|
if res.is_err():
|
|
38
|
-
|
|
45
|
+
fatal(f"Failed to get decimals for token {token_address}: {res.unwrap_err()}")
|
|
39
46
|
|
|
40
47
|
token_decimals = res.unwrap()
|
|
41
48
|
result[token_address] = await _get_token_balances(token_address, token_decimals, config.accounts, config)
|
|
42
49
|
result[token_address + "_decimals"] = token_decimals
|
|
43
50
|
result[token_address + "_sum"] = sum([v for v in result[token_address].values() if v is not None])
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
print_json(result)
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
async def _get_token_balances(
|
|
49
56
|
token_address: str, token_decimals: int, accounts: list[str], config: Config
|
|
50
57
|
) -> dict[str, Decimal | None]:
|
|
58
|
+
"""Fetch token balances for all accounts, returning a dict of address to balance."""
|
|
51
59
|
result: dict[str, Decimal | None] = {}
|
|
52
60
|
for account in accounts:
|
|
53
61
|
result[account] = (
|
|
@@ -60,12 +68,11 @@ async def _get_token_balances(
|
|
|
60
68
|
|
|
61
69
|
|
|
62
70
|
async def _get_sol_balances(accounts: list[str], config: Config) -> dict[str, Decimal | None]:
|
|
71
|
+
"""Fetch SOL balances for all accounts, returning a dict of address to balance."""
|
|
63
72
|
result = {}
|
|
64
73
|
for account in accounts:
|
|
65
74
|
result[account] = (
|
|
66
|
-
(await retry.get_sol_balance(3, config.nodes, config.proxies, address=account))
|
|
67
|
-
.map(lambda v: converters.lamports_to_sol(v))
|
|
68
|
-
.value
|
|
75
|
+
(await retry.get_sol_balance(3, config.nodes, config.proxies, address=account)).map(converters.lamports_to_sol).value
|
|
69
76
|
)
|
|
70
77
|
|
|
71
78
|
return result
|
mm_sol/cli/cmd/example_cmd.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
"""Example config display command."""
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
|
-
import
|
|
5
|
+
from mm_print import print_toml
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def run(module: str) -> None:
|
|
9
|
+
"""Print the example TOML configuration for the given command module."""
|
|
7
10
|
example_file = Path(Path(__file__).parent.absolute(), "../examples", f"{module}.toml")
|
|
8
|
-
|
|
11
|
+
print_toml(example_file.read_text())
|
mm_sol/cli/cmd/node_cmd.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"""RPC node health check command."""
|
|
2
|
+
|
|
3
|
+
from mm_print import print_json
|
|
2
4
|
|
|
3
5
|
from mm_sol import rpc
|
|
4
6
|
from mm_sol.cli import cli_utils
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
async def run(urls: list[str], proxy: str | None) -> None:
|
|
10
|
+
"""Check each RPC URL by fetching block height and print results."""
|
|
8
11
|
result = {}
|
|
9
12
|
for url in [cli_utils.public_rpc_url(u) for u in urls]:
|
|
10
13
|
result[url] = (await rpc.get_block_height(url, proxy=proxy, timeout=10)).value_or_error()
|
|
11
|
-
|
|
14
|
+
print_json(data=result)
|
mm_sol/cli/cmd/transfer_cmd.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
"""SOL and SPL token transfer command with multi-route support."""
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import sys
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Annotated
|
|
5
7
|
|
|
6
|
-
import mm_print
|
|
7
8
|
from loguru import logger
|
|
8
|
-
from mm_std import
|
|
9
|
+
from mm_std import utc
|
|
9
10
|
from mm_web3 import Web3CliConfig
|
|
10
11
|
from mm_web3.account import PrivateKeyMap
|
|
11
12
|
from mm_web3.calcs import calc_decimal_expression
|
|
@@ -20,12 +21,14 @@ from solders.signature import Signature
|
|
|
20
21
|
import mm_sol.retry
|
|
21
22
|
from mm_sol import retry
|
|
22
23
|
from mm_sol.cli import calcs, cli_utils
|
|
23
|
-
from mm_sol.cli.cli_utils import BaseConfigParams
|
|
24
|
+
from mm_sol.cli.cli_utils import BaseConfigParams, fatal
|
|
24
25
|
from mm_sol.cli.validators import Validators
|
|
25
26
|
from mm_sol.converters import lamports_to_sol, to_token
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class Config(Web3CliConfig):
|
|
30
|
+
"""Transfer command configuration with routes, keys, and value expressions."""
|
|
31
|
+
|
|
29
32
|
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
30
33
|
transfers: Annotated[list[Transfer], BeforeValidator(Validators.sol_transfers())]
|
|
31
34
|
private_keys: Annotated[PrivateKeyMap, BeforeValidator(Validators.sol_private_keys())]
|
|
@@ -41,10 +44,12 @@ class Config(Web3CliConfig):
|
|
|
41
44
|
|
|
42
45
|
@property
|
|
43
46
|
def from_addresses(self) -> list[str]:
|
|
47
|
+
"""Return the list of sender addresses from all transfer routes."""
|
|
44
48
|
return [r.from_address for r in self.transfers]
|
|
45
49
|
|
|
46
50
|
@model_validator(mode="after") # type: ignore[misc]
|
|
47
|
-
async def final_validator(self) ->
|
|
51
|
+
async def final_validator(self) -> Config:
|
|
52
|
+
"""Validate private keys, transfer values, and fetch token decimals if needed."""
|
|
48
53
|
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
49
54
|
raise ValueError("private keys are not set for all addresses")
|
|
50
55
|
|
|
@@ -69,13 +74,15 @@ class Config(Web3CliConfig):
|
|
|
69
74
|
if self.token:
|
|
70
75
|
res = await mm_sol.retry.get_token_decimals(3, self.nodes, self.proxies, token=self.token)
|
|
71
76
|
if res.is_err():
|
|
72
|
-
|
|
77
|
+
fatal(f"can't get decimals for token={self.token}, error={res.unwrap_err()}")
|
|
73
78
|
self.token_decimals = res.unwrap()
|
|
74
79
|
|
|
75
80
|
return self
|
|
76
81
|
|
|
77
82
|
|
|
78
83
|
class TransferCmdParams(BaseConfigParams):
|
|
84
|
+
"""CLI parameters for the transfer command."""
|
|
85
|
+
|
|
79
86
|
print_balances: bool
|
|
80
87
|
print_transfers: bool
|
|
81
88
|
debug: bool
|
|
@@ -85,6 +92,7 @@ class TransferCmdParams(BaseConfigParams):
|
|
|
85
92
|
|
|
86
93
|
|
|
87
94
|
async def run(cmd_params: TransferCmdParams) -> None:
|
|
95
|
+
"""Execute the transfer command: print config/balances/transfers or run transfers."""
|
|
88
96
|
config = await Config.read_toml_config_or_exit_async(cmd_params.config_path)
|
|
89
97
|
|
|
90
98
|
if cmd_params.print_config_and_exit:
|
|
@@ -103,8 +111,9 @@ async def run(cmd_params: TransferCmdParams) -> None:
|
|
|
103
111
|
|
|
104
112
|
|
|
105
113
|
async def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
114
|
+
"""Execute all configured transfer routes sequentially with optional delays."""
|
|
106
115
|
init_loguru(cmd_params.debug, config.log_debug, config.log_info)
|
|
107
|
-
logger.info(f"transfer {cmd_params.config_path}: started at {
|
|
116
|
+
logger.info(f"transfer {cmd_params.config_path}: started at {utc()} UTC")
|
|
108
117
|
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
109
118
|
for i, route in enumerate(config.transfers):
|
|
110
119
|
await _transfer(route, config, cmd_params)
|
|
@@ -113,10 +122,11 @@ async def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
113
122
|
logger.info(f"delay {delay_value} seconds")
|
|
114
123
|
if not cmd_params.emulate:
|
|
115
124
|
await asyncio.sleep(float(delay_value))
|
|
116
|
-
logger.info(f"transfer {cmd_params.config_path}: finished at {
|
|
125
|
+
logger.info(f"transfer {cmd_params.config_path}: finished at {utc()} UTC")
|
|
117
126
|
|
|
118
127
|
|
|
119
128
|
async def _calc_value(transfer: Transfer, config: Config, transfer_sol_fee: int) -> int | None:
|
|
129
|
+
"""Calculate the transfer value in smallest units, resolving balance-based expressions."""
|
|
120
130
|
if config.token:
|
|
121
131
|
value_res = await calcs.calc_token_value_for_address(
|
|
122
132
|
nodes=config.nodes,
|
|
@@ -142,7 +152,7 @@ async def _calc_value(transfer: Transfer, config: Config, transfer_sol_fee: int)
|
|
|
142
152
|
|
|
143
153
|
|
|
144
154
|
def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
|
|
145
|
-
"""
|
|
155
|
+
"""Return False if the transfer value is below the configured minimum limit."""
|
|
146
156
|
if config.value_min_limit:
|
|
147
157
|
if config.token:
|
|
148
158
|
value_min_limit = calcs.calc_token_expression(config.value_min_limit, config.token_decimals)
|
|
@@ -154,12 +164,14 @@ def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bo
|
|
|
154
164
|
|
|
155
165
|
|
|
156
166
|
def _value_with_suffix(value: int, config: Config) -> str:
|
|
167
|
+
"""Format a value with its unit suffix (sol or t)."""
|
|
157
168
|
if config.token:
|
|
158
169
|
return f"{to_token(value, decimals=config.token_decimals, ndigits=config.round_ndigits)}t"
|
|
159
170
|
return f"{lamports_to_sol(value, config.round_ndigits)}sol"
|
|
160
171
|
|
|
161
172
|
|
|
162
173
|
async def _send_tx(transfer: Transfer, value: int, config: Config) -> Signature | None:
|
|
174
|
+
"""Submit a SOL or token transfer transaction with retries."""
|
|
163
175
|
logger.debug(f"{transfer.log_prefix}: value={_value_with_suffix(value, config)}")
|
|
164
176
|
if config.token:
|
|
165
177
|
res = await retry.transfer_token(
|
|
@@ -191,6 +203,7 @@ async def _send_tx(transfer: Transfer, value: int, config: Config) -> Signature
|
|
|
191
203
|
|
|
192
204
|
|
|
193
205
|
async def _transfer(transfer: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
206
|
+
"""Execute a single transfer route: calculate value, check limits, send, and confirm."""
|
|
194
207
|
transfer_sol_fee = 5000
|
|
195
208
|
|
|
196
209
|
value = await _calc_value(transfer, config, transfer_sol_fee)
|
|
@@ -218,6 +231,7 @@ async def _transfer(transfer: Transfer, config: Config, cmd_params: TransferCmdP
|
|
|
218
231
|
|
|
219
232
|
|
|
220
233
|
def _print_transfers(config: Config) -> None:
|
|
234
|
+
"""Print a table of all configured transfer routes."""
|
|
221
235
|
table = Table("n", "from_address", "to_address", "value", title="transfers")
|
|
222
236
|
for count, transfer in enumerate(config.transfers, start=1):
|
|
223
237
|
table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
|
|
@@ -226,6 +240,7 @@ def _print_transfers(config: Config) -> None:
|
|
|
226
240
|
|
|
227
241
|
|
|
228
242
|
async def _print_balances(config: Config) -> None:
|
|
243
|
+
"""Print a live-updating table of SOL and token balances for all transfer routes."""
|
|
229
244
|
if config.token:
|
|
230
245
|
headers = ["n", "from_address", "sol", "t", "to_address", "sol", "t"]
|
|
231
246
|
else:
|
|
@@ -259,11 +274,13 @@ async def _print_balances(config: Config) -> None:
|
|
|
259
274
|
|
|
260
275
|
|
|
261
276
|
async def _get_sol_balance_str(address: str, config: Config) -> str:
|
|
277
|
+
"""Fetch SOL balance and return it as a formatted string."""
|
|
262
278
|
res = await retry.get_sol_balance(5, config.nodes, config.proxies, address=address)
|
|
263
279
|
return res.map(lambda ok: str(lamports_to_sol(ok, config.round_ndigits))).value_or_error()
|
|
264
280
|
|
|
265
281
|
|
|
266
282
|
async def _get_token_balance_str(address: str, config: Config) -> str:
|
|
283
|
+
"""Fetch token balance and return it as a formatted string."""
|
|
267
284
|
if not config.token:
|
|
268
285
|
raise ValueError("token is not set")
|
|
269
286
|
res = await mm_sol.retry.get_token_balance(5, config.nodes, config.proxies, owner=address, token=config.token)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Wallet-related CLI commands."""
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
"""Keypair details display command."""
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
|
-
import
|
|
5
|
+
from mm_print import print_json
|
|
4
6
|
|
|
5
7
|
from mm_sol.account import (
|
|
6
8
|
get_private_key_arr_str,
|
|
@@ -10,10 +12,11 @@ from mm_sol.account import (
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def run(private_key: str) -> None:
|
|
15
|
+
"""Print public key, base58 private key, and array private key from a private key input."""
|
|
13
16
|
if (file := Path(private_key)).is_file():
|
|
14
17
|
private_key = file.read_text()
|
|
15
18
|
|
|
16
19
|
public = get_public_key(private_key)
|
|
17
20
|
private_base58 = get_private_key_base58(private_key)
|
|
18
21
|
private_arr = get_private_key_arr_str(private_key)
|
|
19
|
-
|
|
22
|
+
print_json({"public": public, "private_base58": private_base58, "private_arr": private_arr})
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
"""Mnemonic generation and account derivation command."""
|
|
2
|
+
|
|
1
3
|
from dataclasses import asdict
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
4
|
-
import
|
|
6
|
+
from mm_print import print_json
|
|
5
7
|
|
|
6
8
|
from mm_sol.account import derive_accounts, generate_mnemonic
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def run(mnemonic: str, passphrase: str, words: int, derivation_path: str, limit: int) -> None: # nosec
|
|
12
|
+
"""Generate or use a mnemonic to derive accounts and print results."""
|
|
10
13
|
result: dict[str, Any] = {}
|
|
11
14
|
if not mnemonic:
|
|
12
15
|
mnemonic = generate_mnemonic(words)
|
|
@@ -16,4 +19,4 @@ def run(mnemonic: str, passphrase: str, words: int, derivation_path: str, limit:
|
|
|
16
19
|
accounts = derive_accounts(mnemonic=mnemonic, passphrase=passphrase, derivation_path=derivation_path, limit=limit)
|
|
17
20
|
|
|
18
21
|
result["accounts"] = [asdict(acc) for acc in accounts]
|
|
19
|
-
|
|
22
|
+
print_json(result)
|
mm_sol/cli/validators.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Solana-specific config validators for CLI commands."""
|
|
2
|
+
|
|
1
3
|
from collections.abc import Callable
|
|
2
4
|
|
|
3
5
|
from mm_web3 import ConfigValidators
|
|
@@ -9,30 +11,39 @@ from mm_sol.constants import UNIT_DECIMALS
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class Validators(ConfigValidators):
|
|
14
|
+
"""Pydantic validators for Solana addresses, private keys, and value expressions."""
|
|
15
|
+
|
|
12
16
|
@staticmethod
|
|
13
17
|
def sol_address() -> Callable[[str], str]:
|
|
18
|
+
"""Return a validator for a single Solana address."""
|
|
14
19
|
return ConfigValidators.address(is_address)
|
|
15
20
|
|
|
16
21
|
@staticmethod
|
|
17
22
|
def sol_addresses(unique: bool) -> Callable[[str], list[str]]:
|
|
23
|
+
"""Return a validator for a list of Solana addresses."""
|
|
18
24
|
return ConfigValidators.addresses(unique, is_address=is_address)
|
|
19
25
|
|
|
20
26
|
@staticmethod
|
|
21
27
|
def sol_transfers() -> Callable[[str], list[Transfer]]:
|
|
28
|
+
"""Return a validator for transfer routes with Solana addresses."""
|
|
22
29
|
return ConfigValidators.transfers(is_address)
|
|
23
30
|
|
|
24
31
|
@staticmethod
|
|
25
32
|
def sol_private_keys() -> Callable[[str], PrivateKeyMap]:
|
|
33
|
+
"""Return a validator for private key mappings."""
|
|
26
34
|
return ConfigValidators.private_keys(get_public_key)
|
|
27
35
|
|
|
28
36
|
@staticmethod
|
|
29
37
|
def valid_sol_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
38
|
+
"""Return a validator for SOL value expressions."""
|
|
30
39
|
return ConfigValidators.expression_with_vars(var_name, UNIT_DECIMALS)
|
|
31
40
|
|
|
32
41
|
@staticmethod
|
|
33
42
|
def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
43
|
+
"""Return a validator for token value expressions with 6 decimal places."""
|
|
34
44
|
return ConfigValidators.expression_with_vars(var_name, {"t": 6})
|
|
35
45
|
|
|
36
46
|
@staticmethod
|
|
37
47
|
def valid_sol_or_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
48
|
+
"""Return a validator accepting both SOL and token value expressions."""
|
|
38
49
|
return ConfigValidators.expression_with_vars(var_name, UNIT_DECIMALS | {"t": 6})
|
mm_sol/constants.py
CHANGED
mm_sol/converters.py
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
|
+
"""Conversion utilities between SOL, lamports, and token smallest units."""
|
|
2
|
+
|
|
1
3
|
from decimal import Decimal
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
|
|
7
|
+
"""Convert lamports to SOL with the specified decimal precision."""
|
|
5
8
|
if lamports == 0:
|
|
6
9
|
return Decimal(0)
|
|
7
10
|
return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
def to_token(smallest_unit_value: int, decimals: int, ndigits: int = 4) -> Decimal:
|
|
14
|
+
"""Convert a token's smallest unit value to a human-readable Decimal."""
|
|
11
15
|
if smallest_unit_value == 0:
|
|
12
16
|
return Decimal(0)
|
|
13
17
|
return Decimal(str(round(smallest_unit_value / 10**decimals, ndigits=ndigits)))
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def sol_to_lamports(sol: Decimal) -> int:
|
|
21
|
+
"""Convert SOL amount to lamports."""
|
|
17
22
|
return int(sol * 10**9)
|
|
18
23
|
|
|
19
24
|
|
|
20
25
|
def to_lamports(value: str | int | Decimal, decimals: int | None = None) -> int:
|
|
26
|
+
"""Parse a value into lamports. Supports raw int, Decimal, or string with 'sol'/'t' suffix."""
|
|
21
27
|
if isinstance(value, int):
|
|
22
28
|
return value
|
|
23
29
|
if isinstance(value, Decimal):
|
mm_sol/retry.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Retry wrappers for Solana RPC calls with node and proxy rotation."""
|
|
2
|
+
|
|
1
3
|
from mm_result import Result
|
|
2
4
|
from mm_web3 import Nodes, Proxies, retry_with_node_and_proxy
|
|
3
5
|
from solders.solders import Pubkey, Signature
|
|
@@ -6,6 +8,7 @@ from mm_sol import rpc, spl_token, transfer
|
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
async def get_sol_balance(retries: int, nodes: Nodes, proxies: Proxies, *, address: str, timeout: float = 5) -> Result[int]:
|
|
11
|
+
"""Fetch SOL balance in lamports with retries across nodes and proxies."""
|
|
9
12
|
return await retry_with_node_and_proxy(
|
|
10
13
|
retries,
|
|
11
14
|
nodes,
|
|
@@ -24,6 +27,7 @@ async def get_token_balance(
|
|
|
24
27
|
token_account: str | None = None,
|
|
25
28
|
timeout: float = 5,
|
|
26
29
|
) -> Result[int]:
|
|
30
|
+
"""Fetch SPL token balance with retries across nodes and proxies."""
|
|
27
31
|
return await retry_with_node_and_proxy(
|
|
28
32
|
retries,
|
|
29
33
|
nodes,
|
|
@@ -53,6 +57,7 @@ async def transfer_token(
|
|
|
53
57
|
timeout: float = 10,
|
|
54
58
|
create_token_account_if_not_exists: bool = True,
|
|
55
59
|
) -> Result[Signature]:
|
|
60
|
+
"""Transfer SPL tokens with retries across nodes and proxies."""
|
|
56
61
|
return await retry_with_node_and_proxy(
|
|
57
62
|
retries,
|
|
58
63
|
nodes,
|
|
@@ -83,6 +88,7 @@ async def transfer_sol(
|
|
|
83
88
|
lamports: int,
|
|
84
89
|
timeout: float = 10,
|
|
85
90
|
) -> Result[Signature]:
|
|
91
|
+
"""Transfer SOL with retries across nodes and proxies."""
|
|
86
92
|
return await retry_with_node_and_proxy(
|
|
87
93
|
retries,
|
|
88
94
|
nodes,
|
|
@@ -100,6 +106,7 @@ async def transfer_sol(
|
|
|
100
106
|
|
|
101
107
|
|
|
102
108
|
async def get_token_decimals(retries: int, nodes: Nodes, proxies: Proxies, *, token: str, timeout: float = 5) -> Result[int]:
|
|
109
|
+
"""Fetch token decimals with retries across nodes and proxies."""
|
|
103
110
|
return await retry_with_node_and_proxy(
|
|
104
111
|
retries,
|
|
105
112
|
nodes,
|
mm_sol/rpc.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Async Solana JSON-RPC client with HTTP and WebSocket support."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
from collections.abc import Sequence
|
|
3
5
|
from typing import Any
|
|
@@ -15,6 +17,7 @@ async def rpc_call(
|
|
|
15
17
|
proxy: str | None,
|
|
16
18
|
id_: int = 1,
|
|
17
19
|
) -> Result[Any]:
|
|
20
|
+
"""Send a JSON-RPC request to a Solana node via HTTP or WebSocket."""
|
|
18
21
|
data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
|
|
19
22
|
if node.startswith("http"):
|
|
20
23
|
return await _http_call(node, data, timeout, proxy)
|
|
@@ -22,11 +25,12 @@ async def rpc_call(
|
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
async def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
|
|
28
|
+
"""Execute an RPC call over HTTP."""
|
|
25
29
|
res = await http_request(node, method="POST", proxy=proxy, timeout=timeout, json=data)
|
|
26
30
|
if res.is_err():
|
|
27
31
|
return res.to_result_err()
|
|
28
32
|
try:
|
|
29
|
-
parsed_body = res.
|
|
33
|
+
parsed_body = res.json_body().unwrap("invalid_json")
|
|
30
34
|
err = parsed_body.get("error", {}).get("message", "")
|
|
31
35
|
if err:
|
|
32
36
|
return res.to_result_err(f"service_error: {err}")
|
|
@@ -38,6 +42,7 @@ async def _http_call(node: str, data: dict[str, object], timeout: float, proxy:
|
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
async def _ws_call(node: str, data: dict[str, object], timeout: float) -> Result[Any]:
|
|
45
|
+
"""Execute an RPC call over WebSocket."""
|
|
41
46
|
response = None
|
|
42
47
|
try:
|
|
43
48
|
async with websockets.connect(node, open_timeout=timeout) as ws:
|
|
@@ -57,11 +62,12 @@ async def _ws_call(node: str, data: dict[str, object], timeout: float) -> Result
|
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
async def get_block_height(node: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
65
|
+
"""Return the current block height."""
|
|
60
66
|
return await rpc_call(node=node, method="getBlockHeight", params=[], timeout=timeout, proxy=proxy)
|
|
61
67
|
|
|
62
68
|
|
|
63
69
|
async def get_balance(node: str, address: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
64
|
-
"""
|
|
70
|
+
"""Return balance in lamports."""
|
|
65
71
|
return (await rpc_call(node=node, method="getBalance", params=[address], timeout=timeout, proxy=proxy)).map(
|
|
66
72
|
lambda r: r["value"]
|
|
67
73
|
)
|
mm_sol/rpc_sync.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Synchronous Solana JSON-RPC client and response models."""
|
|
2
|
+
|
|
1
3
|
from typing import Any
|
|
2
4
|
|
|
3
5
|
import pydash
|
|
@@ -6,10 +8,15 @@ from mm_result import Result
|
|
|
6
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
7
9
|
|
|
8
10
|
DEFAULT_MAINNET_RPC = "https://api.mainnet-beta.solana.com"
|
|
11
|
+
"""Default Solana mainnet RPC endpoint."""
|
|
12
|
+
|
|
9
13
|
DEFAULT_TESTNET_RPC = "https://api.testnet.solana.com"
|
|
14
|
+
"""Default Solana testnet RPC endpoint."""
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class EpochInfo(BaseModel):
|
|
18
|
+
"""Solana epoch information from getEpochInfo RPC call."""
|
|
19
|
+
|
|
13
20
|
model_config = ConfigDict(populate_by_name=True)
|
|
14
21
|
|
|
15
22
|
epoch: int
|
|
@@ -21,10 +28,13 @@ class EpochInfo(BaseModel):
|
|
|
21
28
|
|
|
22
29
|
@property
|
|
23
30
|
def progress(self) -> float:
|
|
31
|
+
"""Return epoch progress as a percentage."""
|
|
24
32
|
return round(self.slot_index / self.slots_in_epoch * 100, 2)
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
class ClusterNode(BaseModel):
|
|
36
|
+
"""A node in the Solana cluster from getClusterNodes."""
|
|
37
|
+
|
|
28
38
|
pubkey: str
|
|
29
39
|
version: str | None
|
|
30
40
|
gossip: str | None
|
|
@@ -32,7 +42,11 @@ class ClusterNode(BaseModel):
|
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
class VoteAccount(BaseModel):
|
|
45
|
+
"""Validator vote account with stake and credit info."""
|
|
46
|
+
|
|
35
47
|
class EpochCredits(BaseModel):
|
|
48
|
+
"""Credits earned by a validator in a single epoch."""
|
|
49
|
+
|
|
36
50
|
epoch: int
|
|
37
51
|
credits: int
|
|
38
52
|
previous_credits: int
|
|
@@ -49,7 +63,11 @@ class VoteAccount(BaseModel):
|
|
|
49
63
|
|
|
50
64
|
|
|
51
65
|
class BlockProduction(BaseModel):
|
|
66
|
+
"""Block production statistics for a slot range."""
|
|
67
|
+
|
|
52
68
|
class Leader(BaseModel):
|
|
69
|
+
"""Per-leader block production stats."""
|
|
70
|
+
|
|
53
71
|
address: str
|
|
54
72
|
produced: int
|
|
55
73
|
skipped: int
|
|
@@ -61,14 +79,18 @@ class BlockProduction(BaseModel):
|
|
|
61
79
|
|
|
62
80
|
@property
|
|
63
81
|
def total_produced(self) -> int:
|
|
82
|
+
"""Return total blocks produced across all leaders."""
|
|
64
83
|
return sum(leader.produced for leader in self.leaders)
|
|
65
84
|
|
|
66
85
|
@property
|
|
67
86
|
def total_skipped(self) -> int:
|
|
87
|
+
"""Return total blocks skipped across all leaders."""
|
|
68
88
|
return sum(leader.skipped for leader in self.leaders)
|
|
69
89
|
|
|
70
90
|
|
|
71
91
|
class StakeActivation(BaseModel):
|
|
92
|
+
"""Stake activation status for a stake account."""
|
|
93
|
+
|
|
72
94
|
state: str
|
|
73
95
|
active: int
|
|
74
96
|
inactive: int
|
|
@@ -83,6 +105,7 @@ def rpc_call(
|
|
|
83
105
|
timeout: float = 5,
|
|
84
106
|
proxy: str | None = None,
|
|
85
107
|
) -> Result[Any]:
|
|
108
|
+
"""Send a synchronous JSON-RPC request to a Solana node."""
|
|
86
109
|
data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
|
|
87
110
|
if node.startswith("http"):
|
|
88
111
|
return _http_call(node, data, timeout, proxy)
|
|
@@ -90,12 +113,13 @@ def rpc_call(
|
|
|
90
113
|
|
|
91
114
|
|
|
92
115
|
def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
|
|
116
|
+
"""Execute a synchronous RPC call over HTTP."""
|
|
93
117
|
res = http_request_sync(node, method="POST", proxy=proxy, timeout=timeout, json=data)
|
|
94
118
|
try:
|
|
95
119
|
if res.is_err():
|
|
96
120
|
return res.to_result_err()
|
|
97
121
|
|
|
98
|
-
json_body = res.
|
|
122
|
+
json_body = res.json_body().unwrap("invalid_json")
|
|
99
123
|
err = pydash.get(json_body, "error.message")
|
|
100
124
|
if err:
|
|
101
125
|
return res.to_result_err(f"service_error: {err}")
|
|
@@ -108,35 +132,40 @@ def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str |
|
|
|
108
132
|
|
|
109
133
|
|
|
110
134
|
def get_balance(node: str, address: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
111
|
-
"""
|
|
135
|
+
"""Return balance in lamports."""
|
|
112
136
|
return rpc_call(node=node, method="getBalance", params=[address], timeout=timeout, proxy=proxy).map(lambda r: r["value"])
|
|
113
137
|
|
|
114
138
|
|
|
115
139
|
def get_block_height(node: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
116
|
-
"""
|
|
140
|
+
"""Return the current block height."""
|
|
117
141
|
return rpc_call(node=node, method="getBlockHeight", params=[], timeout=timeout, proxy=proxy)
|
|
118
142
|
|
|
119
143
|
|
|
120
144
|
def get_slot(node: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
145
|
+
"""Return the current slot number."""
|
|
121
146
|
return rpc_call(node=node, method="getSlot", params=[], timeout=timeout, proxy=proxy)
|
|
122
147
|
|
|
123
148
|
|
|
124
149
|
def get_epoch_info(node: str, epoch: int | None = None, timeout: float = 5, proxy: str | None = None) -> Result[EpochInfo]:
|
|
150
|
+
"""Return epoch information, optionally for a specific epoch."""
|
|
125
151
|
params = [epoch] if epoch else []
|
|
126
152
|
return rpc_call(node=node, method="getEpochInfo", params=params, timeout=timeout, proxy=proxy).map(lambda r: EpochInfo(**r))
|
|
127
153
|
|
|
128
154
|
|
|
129
155
|
def get_health(node: str, timeout: float = 5, proxy: str | None = None) -> Result[bool]:
|
|
156
|
+
"""Check whether the node is healthy."""
|
|
130
157
|
return rpc_call(node=node, method="getHealth", params=[], timeout=timeout, proxy=proxy).map(lambda r: r == "ok")
|
|
131
158
|
|
|
132
159
|
|
|
133
160
|
def get_cluster_nodes(node: str, timeout: float = 5, proxy: str | None = None) -> Result[list[ClusterNode]]:
|
|
161
|
+
"""Return the list of cluster nodes."""
|
|
134
162
|
return rpc_call(node=node, method="getClusterNodes", timeout=timeout, proxy=proxy, params=[]).map(
|
|
135
163
|
lambda r: [ClusterNode(**n) for n in r],
|
|
136
164
|
)
|
|
137
165
|
|
|
138
166
|
|
|
139
167
|
def get_vote_accounts(node: str, timeout: float = 5, proxy: str | None = None) -> Result[list[VoteAccount]]:
|
|
168
|
+
"""Return current and delinquent vote accounts."""
|
|
140
169
|
res = rpc_call(node=node, method="getVoteAccounts", timeout=timeout, proxy=proxy, params=[])
|
|
141
170
|
if res.is_err():
|
|
142
171
|
return res
|
|
@@ -186,6 +215,7 @@ def get_leader_scheduler(
|
|
|
186
215
|
timeout: float = 5,
|
|
187
216
|
proxy: str | None = None,
|
|
188
217
|
) -> Result[dict[str, list[int]]]:
|
|
218
|
+
"""Return the leader schedule, optionally for a specific slot."""
|
|
189
219
|
return rpc_call(
|
|
190
220
|
node=node,
|
|
191
221
|
method="getLeaderSchedule",
|
|
@@ -196,6 +226,7 @@ def get_leader_scheduler(
|
|
|
196
226
|
|
|
197
227
|
|
|
198
228
|
def get_stake_activation(node: str, address: str, timeout: float = 60, proxy: str | None = None) -> Result[StakeActivation]:
|
|
229
|
+
"""Return stake activation status for a stake account address."""
|
|
199
230
|
return rpc_call(node=node, method="getStakeActivation", timeout=timeout, proxy=proxy, params=[address]).map(
|
|
200
231
|
lambda ok: StakeActivation(**ok),
|
|
201
232
|
)
|
|
@@ -209,6 +240,7 @@ def get_transaction(
|
|
|
209
240
|
timeout: float = 5,
|
|
210
241
|
proxy: str | None = None,
|
|
211
242
|
) -> Result[dict[str, object] | None]:
|
|
243
|
+
"""Return transaction details for a given signature."""
|
|
212
244
|
if max_supported_transaction_version is not None:
|
|
213
245
|
params = [signature, {"maxSupportedTransactionVersion": max_supported_transaction_version, "encoding": encoding}]
|
|
214
246
|
else:
|
mm_sol/spl_token.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""SPL token balance and metadata queries via Solana RPC."""
|
|
2
|
+
|
|
1
3
|
from mm_result import Result
|
|
2
4
|
from solana.exceptions import SolanaRpcException
|
|
3
5
|
from solana.rpc.core import RPCException
|
|
@@ -14,6 +16,7 @@ async def get_balance(
|
|
|
14
16
|
timeout: float = 5,
|
|
15
17
|
proxy: str | None = None,
|
|
16
18
|
) -> Result[int]:
|
|
19
|
+
"""Return the token balance in smallest units for the owner's associated token account."""
|
|
17
20
|
response = None
|
|
18
21
|
try:
|
|
19
22
|
client = get_async_client(node, proxy=proxy, timeout=timeout)
|
|
@@ -38,6 +41,7 @@ async def get_balance(
|
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
async def get_decimals(node: str, token: str, timeout: float = 5, proxy: str | None = None) -> Result[int]:
|
|
44
|
+
"""Return the number of decimals for a token mint."""
|
|
41
45
|
response = None
|
|
42
46
|
try:
|
|
43
47
|
client = get_async_client(node, proxy=proxy, timeout=timeout)
|
mm_sol/transfer.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""SOL and SPL token transfer operations."""
|
|
2
|
+
|
|
1
3
|
import pydash
|
|
2
4
|
from mm_result import Result
|
|
3
5
|
from pydantic import BaseModel
|
|
@@ -27,6 +29,7 @@ async def transfer_token(
|
|
|
27
29
|
timeout: float = 10,
|
|
28
30
|
create_token_account_if_not_exists: bool = True,
|
|
29
31
|
) -> Result[Signature]:
|
|
32
|
+
"""Transfer SPL tokens, optionally creating the recipient's token account."""
|
|
30
33
|
# TODO: try/except this function!!!
|
|
31
34
|
acc = get_keypair(private_key)
|
|
32
35
|
if not check_private_key(from_address, private_key):
|
|
@@ -74,6 +77,7 @@ async def transfer_sol(
|
|
|
74
77
|
proxy: str | None = None,
|
|
75
78
|
timeout: float = 10,
|
|
76
79
|
) -> Result[Signature]:
|
|
80
|
+
"""Transfer SOL from one account to another."""
|
|
77
81
|
acc = get_keypair(private_key)
|
|
78
82
|
if not check_private_key(from_address, private_key):
|
|
79
83
|
return Result.err("invalid_private_key")
|
|
@@ -93,12 +97,15 @@ async def transfer_sol(
|
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
class SolTransferInfo(BaseModel):
|
|
100
|
+
"""Parsed SOL transfer from a transaction's instructions."""
|
|
101
|
+
|
|
96
102
|
source: str
|
|
97
103
|
destination: str
|
|
98
104
|
lamports: int
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
def find_sol_transfers(node: str, tx_signature: str) -> Result[list[SolTransferInfo]]:
|
|
108
|
+
"""Parse SOL transfer instructions from a transaction signature."""
|
|
102
109
|
res = rpc_sync.get_transaction(node, tx_signature, encoding="jsonParsed")
|
|
103
110
|
if res.is_err():
|
|
104
111
|
return res # type: ignore[return-value]
|
|
@@ -115,4 +122,4 @@ def find_sol_transfers(node: str, tx_signature: str) -> Result[list[SolTransferI
|
|
|
115
122
|
result.append(SolTransferInfo(source=source, destination=destination, lamports=lamports))
|
|
116
123
|
return res.with_value(result)
|
|
117
124
|
except Exception as e:
|
|
118
|
-
return Result.err(e, res.
|
|
125
|
+
return Result.err(e, res.context)
|
mm_sol/utils.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Utility functions for creating Solana RPC clients and converting pubkeys."""
|
|
2
|
+
|
|
1
3
|
from solana.rpc.api import Client
|
|
2
4
|
from solana.rpc.async_api import AsyncClient
|
|
3
5
|
from solana.rpc.commitment import Commitment
|
|
@@ -11,6 +13,7 @@ def get_client(
|
|
|
11
13
|
proxy: str | None = None,
|
|
12
14
|
timeout: float = 10,
|
|
13
15
|
) -> Client:
|
|
16
|
+
"""Create a synchronous Solana RPC client."""
|
|
14
17
|
return Client(endpoint, commitment=commitment, extra_headers=extra_headers, timeout=timeout, proxy=proxy)
|
|
15
18
|
|
|
16
19
|
|
|
@@ -21,10 +24,12 @@ def get_async_client(
|
|
|
21
24
|
proxy: str | None = None,
|
|
22
25
|
timeout: float = 10,
|
|
23
26
|
) -> AsyncClient:
|
|
27
|
+
"""Create an asynchronous Solana RPC client."""
|
|
24
28
|
return AsyncClient(endpoint, commitment=commitment, extra_headers=extra_headers, timeout=timeout, proxy=proxy)
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def pubkey(value: str | Pubkey) -> Pubkey:
|
|
32
|
+
"""Convert a string or Pubkey to a Pubkey instance."""
|
|
28
33
|
if isinstance(value, Pubkey):
|
|
29
34
|
return value
|
|
30
35
|
return Pubkey.from_string(value)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mm-sol
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Requires-Python: >=3.14
|
|
5
|
+
Requires-Dist: base58~=2.1.1
|
|
6
|
+
Requires-Dist: jinja2~=3.1.6
|
|
7
|
+
Requires-Dist: mm-web3~=0.6.2
|
|
8
|
+
Requires-Dist: mnemonic==0.21
|
|
9
|
+
Requires-Dist: solana~=0.36.11
|
|
10
|
+
Requires-Dist: typer~=0.21.1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
mm_sol/__init__.py,sha256=fUeHKoD3cV4rOVWUrfNuugxIe71FcaWQkhnoyvtZrZg,54
|
|
2
|
+
mm_sol/account.py,sha256=ytwiAzxvrjNryFaCDfIAuMMchluxm8gmNTX0w09HBVE,4538
|
|
3
|
+
mm_sol/constants.py,sha256=2Lbzqi6hA_2edJ-AYgJszPGH2r9tOyXNWlabqE19uUM,106
|
|
4
|
+
mm_sol/converters.py,sha256=NhgSEYdkz63Ph7G52vCCoSTlqD4ncO4xRtK7eXT_EQE,1797
|
|
5
|
+
mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
mm_sol/retry.py,sha256=RILvfBtpHjDwacrX2aCdtSkAvLB9Cn59SoQa9Dv3VQE,3315
|
|
7
|
+
mm_sol/rpc.py,sha256=o3PEx4at5JCZ4qKkZTMDozr4kt1ZKeGPBKeJxk2owFk,2768
|
|
8
|
+
mm_sol/rpc_sync.py,sha256=XVs6vA1o5Y1I1MvMWv0ePDBcwVdj7rMWX7QJULfEB8Y,8531
|
|
9
|
+
mm_sol/spl_token.py,sha256=yw-tIH-wbfrTf7WdjMr39ZbAO4Ip-T0wTd8YBP3K-0w,2159
|
|
10
|
+
mm_sol/transfer.py,sha256=7roDEqE6qyUZCsw4J4MBMZwBlEVPTnls0vX5AupFEB0,4733
|
|
11
|
+
mm_sol/utils.py,sha256=ZeTESNwYEcMfGl523WwnMwzWIGT-SEh5WsAz62kEIE8,1165
|
|
12
|
+
mm_sol/cli/__init__.py,sha256=AoiQ74zt-QmDnw1wLOfMW5HLNtZrj6Ut9DExNQ0CLDA,32
|
|
13
|
+
mm_sol/cli/calcs.py,sha256=jb6AnmXQDm3fgYxOhjmN5E6caCdBGkpm0IiN7WwWL00,2184
|
|
14
|
+
mm_sol/cli/cli.py,sha256=-y2uvJAS__4pSK9z61R42pWcuGPBjV8dcb5z7SGbHdI,5457
|
|
15
|
+
mm_sol/cli/cli_utils.py,sha256=UdRN-NTvxyJjWS3c2dzznK9TCg69ptP_3V3acgy3jfA,2471
|
|
16
|
+
mm_sol/cli/validators.py,sha256=cMANe5jbMrw9Bb2PrCvciaKmEjWpYEykdice29x9f1s,2037
|
|
17
|
+
mm_sol/cli/cmd/__init__.py,sha256=GYdoNv80KS2p7bCThuoRbhiLmIZW6ddPqPEFEx567nQ,35
|
|
18
|
+
mm_sol/cli/cmd/balance_cmd.py,sha256=mEfpcL4LD3do9mwIwSXg-qz2gZ7YT32zYhDLSXx5cpw,2816
|
|
19
|
+
mm_sol/cli/cmd/balances_cmd.py,sha256=MnZZNc6cSAneaDyTZ9BxZjUlUgRg1TG84RyGzd9HwCg,3072
|
|
20
|
+
mm_sol/cli/cmd/example_cmd.py,sha256=8DM8Zbuj8FMzzd04ywfALXfzpYxvUy3sS-r5oj7vz3M,338
|
|
21
|
+
mm_sol/cli/cmd/node_cmd.py,sha256=DukE_9VuoPEWxlberIRwZ9kUhXgbQv1n0xGsl-cc8kI,463
|
|
22
|
+
mm_sol/cli/cmd/transfer_cmd.py,sha256=A1bfeUvFvU16SFfWt7i0FfswIhA-PH5fld4oHOQAI1w,12143
|
|
23
|
+
mm_sol/cli/cmd/wallet/__init__.py,sha256=et8EPGCXsVukSYnO29E1Wn_O0_Sf8FKxc-09UuFL_gk,35
|
|
24
|
+
mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=XsToFLoQjxbK4q9-UZZtLa7U7ZGBDS7-HMrWSyufhUA,676
|
|
25
|
+
mm_sol/cli/cmd/wallet/mnemonic_cmd.py,sha256=XzCpmpaiKgrButhEmPe6Ww51Pmml_Q727654dU7i6Wo,784
|
|
26
|
+
mm_sol/cli/examples/balances.toml,sha256=UxX5FXKhH1lwjniiOQW-hZP0C731W96o5-1V1yqg0cQ,350
|
|
27
|
+
mm_sol/cli/examples/transfer.toml,sha256=-7inhWntAHKYqr0spmRYyNflUrYWscv_xMoWwytS61E,1623
|
|
28
|
+
mm_sol-0.8.0.dist-info/METADATA,sha256=t6wCPNR7ZD5kVFn4xV_Pbb4r2jMSj5GTmdQM06f8q6o,252
|
|
29
|
+
mm_sol-0.8.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
30
|
+
mm_sol-0.8.0.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
|
|
31
|
+
mm_sol-0.8.0.dist-info/RECORD,,
|
mm_sol-0.7.3.dist-info/METADATA
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mm-sol
|
|
3
|
-
Version: 0.7.3
|
|
4
|
-
Requires-Python: >=3.13
|
|
5
|
-
Requires-Dist: base58~=2.1.1
|
|
6
|
-
Requires-Dist: jinja2>=3.1.6
|
|
7
|
-
Requires-Dist: mm-web3~=0.5.4
|
|
8
|
-
Requires-Dist: mnemonic==0.21
|
|
9
|
-
Requires-Dist: socksio>=1.0.0
|
|
10
|
-
Requires-Dist: solana~=0.36.9
|
|
11
|
-
Requires-Dist: typer>=0.16.1
|
mm_sol-0.7.3.dist-info/RECORD
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
mm_sol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
mm_sol/account.py,sha256=cVcxRQBuV_Gfm2WgQIwaYuAQijeIJqDDxLC22PN0XSs,3493
|
|
3
|
-
mm_sol/constants.py,sha256=9XKF8Z2dyY6L82_Bm0_naSQp5U0mIaOeqbZNBj25D2E,27
|
|
4
|
-
mm_sol/converters.py,sha256=rBxe3SIADZS8hG7TYl4FgjmvKH-ykaTmNbnWWQDiFZ4,1430
|
|
5
|
-
mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
mm_sol/retry.py,sha256=IjaKvIe13_kRkwUxEpgNaZ0MNAW9bSOW5H33BL8GNA4,2889
|
|
7
|
-
mm_sol/rpc.py,sha256=TFgU6H5la7ctEgPyQ0CW8SxipCycx8FFCu1baR6F_mM,2473
|
|
8
|
-
mm_sol/rpc_sync.py,sha256=ryS6ZuPrrZsfQUuizVHbgcr-IX_jS9hF4J4B1ya2FAU,7190
|
|
9
|
-
mm_sol/spl_token.py,sha256=Bd5zXhlVhFd5n3WwunRoeM_yYo-0f1hNR7_1zww81A8,1944
|
|
10
|
-
mm_sol/transfer.py,sha256=kfNGRjF4aNSLAyBdm0jHyCXENFTMwib4pqIhKRJGCCQ,4413
|
|
11
|
-
mm_sol/utils.py,sha256=oD06NsMSMhN6lqsM6mSgLTtiKwA1uAsen9WR82ofRTE,923
|
|
12
|
-
mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
mm_sol/cli/calcs.py,sha256=lgXF5SJkwOEY2AjygBO9CxP-LpyvOWB7j5HQn6p-1tU,1765
|
|
14
|
-
mm_sol/cli/cli.py,sha256=TxAAQ-QX9j9mG6d6el9SLYxF3QeIeWal7AdW_GTzPZQ,4723
|
|
15
|
-
mm_sol/cli/cli_utils.py,sha256=1_ikUjwHkxJleNwhVLaI_-6m4JyCKZFH2WgvsZ5EGMk,1799
|
|
16
|
-
mm_sol/cli/validators.py,sha256=DnK2C68MgceqQ6Zs90WZ6Co0LYMiNmhxZILX-2GKhZM,1401
|
|
17
|
-
mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
mm_sol/cli/cmd/balance_cmd.py,sha256=6h7ryybMyegy8EsV1limWo2gcYL9_dczK9JRiHxHLzg,2452
|
|
19
|
-
mm_sol/cli/cmd/balances_cmd.py,sha256=u8dSexKVVExC-5U-8vPUDFUBF2r-j-vsSmmOBM-Tsqc,2673
|
|
20
|
-
mm_sol/cli/cmd/example_cmd.py,sha256=HFEjAPxIPAss3u--i3mLpvVttCkmY1Ps4aEfNLPp8bw,209
|
|
21
|
-
mm_sol/cli/cmd/node_cmd.py,sha256=octbG9IwCUKyaFgGrPf_2MJlzHKncMQhGlyi-1rqw-E,339
|
|
22
|
-
mm_sol/cli/cmd/transfer_cmd.py,sha256=6q5LbWWl6AvQI2u4kh3qMQpZndPkdturTn_1wgGflNo,11023
|
|
23
|
-
mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=HndbANn2XbuHWxD-8ggKEGUK1OT1tjiVTtR5G6F8XFw,527
|
|
25
|
-
mm_sol/cli/cmd/wallet/mnemonic_cmd.py,sha256=xT0wzBtj9IeJHvrzst0jFjXP4pj_k17JsKX_PBNjIO4,637
|
|
26
|
-
mm_sol/cli/examples/balances.toml,sha256=UxX5FXKhH1lwjniiOQW-hZP0C731W96o5-1V1yqg0cQ,350
|
|
27
|
-
mm_sol/cli/examples/transfer.toml,sha256=-7inhWntAHKYqr0spmRYyNflUrYWscv_xMoWwytS61E,1623
|
|
28
|
-
mm_sol-0.7.3.dist-info/METADATA,sha256=PnrdtEVYgYTzz9jSHbMsLiL3qJ4BEYg_-aLEc7AsiwI,281
|
|
29
|
-
mm_sol-0.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
-
mm_sol-0.7.3.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
|
|
31
|
-
mm_sol-0.7.3.dist-info/RECORD,,
|
|
File without changes
|