mm-sol 0.2.5__py3-none-any.whl → 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mm_sol/account.py CHANGED
@@ -35,8 +35,10 @@ def get_keypair(private_key: str | list[int]) -> Keypair:
35
35
  return Keypair.from_bytes(private_key_)
36
36
 
37
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
38
+ def check_private_key(public_key: str | Pubkey, private_key: str | list[int]) -> bool:
39
+ if isinstance(public_key, str):
40
+ public_key = Pubkey.from_string(public_key)
41
+ return get_keypair(private_key).pubkey() == public_key
40
42
 
41
43
 
42
44
  def get_public_key(private_key: str) -> str:
mm_sol/balance.py CHANGED
@@ -1,18 +1,95 @@
1
- from mm_std import Err, Result
1
+ import httpx
2
+ from mm_crypto_utils import Nodes, Proxies, random_node, random_proxy
3
+ from mm_std import Err, Ok, Result
4
+ from solana.exceptions import SolanaRpcException
5
+ from solana.rpc.types import TokenAccountOpts
6
+ from solders.pubkey import Pubkey
2
7
 
3
8
  from mm_sol import rpc
4
- from mm_sol.types import Nodes, Proxies
5
- from mm_sol.utils import get_node, get_proxy
9
+ from mm_sol.utils import get_client
6
10
 
7
11
 
8
- def get_balance(node: str, address: str, timeout: int = 10, proxy: str | None = None) -> Result[int]:
12
+ def get_sol_balance(node: str, address: str, timeout: int = 10, proxy: str | None = None) -> Result[int]:
9
13
  return rpc.get_balance(node, address, timeout, proxy)
10
14
 
11
15
 
12
- def get_balance_with_retries(nodes: Nodes, address: str, retries: int, timeout: int = 10, proxies: Proxies = None) -> Result[int]:
16
+ def get_sol_balance_with_retries(
17
+ nodes: Nodes, address: str, retries: int, timeout: int = 10, proxies: Proxies = None
18
+ ) -> Result[int]:
13
19
  res: Result[int] = Err("not started yet")
14
20
  for _ in range(retries):
15
- res = get_balance(get_node(nodes), address, timeout=timeout, proxy=get_proxy(proxies))
21
+ res = get_sol_balance(random_node(nodes), address, timeout=timeout, proxy=random_proxy(proxies))
22
+ if res.is_ok():
23
+ return res
24
+ return res
25
+
26
+
27
+ def get_token_balance(
28
+ node: str,
29
+ owner_address: str,
30
+ token_mint_address: str,
31
+ token_account: str | None = None,
32
+ timeout: float = 10,
33
+ proxy: str | None = None,
34
+ no_token_accounts_return_zero: bool = True,
35
+ ) -> Result[int]:
36
+ data: list[object] = []
37
+ try:
38
+ client = get_client(node, proxy=proxy, timeout=timeout)
39
+ if token_account:
40
+ res_balance = client.get_token_account_balance(Pubkey.from_string(token_account))
41
+ data.append(res_balance)
42
+ return Ok(int(res_balance.value.amount))
43
+
44
+ res_accounts = client.get_token_accounts_by_owner(
45
+ Pubkey.from_string(owner_address),
46
+ TokenAccountOpts(mint=Pubkey.from_string(token_mint_address)),
47
+ )
48
+ data.append(res_accounts)
49
+
50
+ if no_token_accounts_return_zero and not res_accounts.value:
51
+ return Ok(0)
52
+ if not res_accounts.value:
53
+ return Err("no_token_accounts")
54
+
55
+ token_accounts = [a.pubkey for a in res_accounts.value]
56
+ balances = []
57
+ for token_account_ in token_accounts:
58
+ res = client.get_token_account_balance(token_account_)
59
+ data.append(res)
60
+ if res.value: # type:ignore[truthy-bool]
61
+ balances.append(int(res.value.amount))
62
+
63
+ return Ok(sum(balances))
64
+ except httpx.HTTPStatusError as e:
65
+ return Err(f"http error: {e}", data=data)
66
+ except SolanaRpcException as e:
67
+ return Err(e.error_msg, data=data)
68
+ except Exception as e:
69
+ return Err(e, data=data)
70
+
71
+
72
+ def get_token_balance_with_retries(
73
+ nodes: Nodes,
74
+ owner_address: str,
75
+ token_mint_address: str,
76
+ retries: int,
77
+ token_account: str | None = None,
78
+ timeout: float = 10,
79
+ proxies: Proxies = None,
80
+ no_token_accounts_return_zero: bool = True,
81
+ ) -> Result[int]:
82
+ res: Result[int] = Err("not started yet")
83
+ for _ in range(retries):
84
+ res = get_token_balance(
85
+ random_node(nodes),
86
+ owner_address,
87
+ token_mint_address,
88
+ token_account,
89
+ timeout=timeout,
90
+ proxy=random_proxy(proxies),
91
+ no_token_accounts_return_zero=no_token_accounts_return_zero,
92
+ )
16
93
  if res.is_ok():
17
94
  return res
18
95
  return res
mm_sol/cli/calcs.py ADDED
@@ -0,0 +1,93 @@
1
+ import random
2
+ from decimal import Decimal
3
+
4
+ import mm_crypto_utils
5
+ from loguru import logger
6
+ from mm_crypto_utils import Nodes, Proxies
7
+ from mm_std import Err
8
+ from mm_std.str import split_on_plus_minus_tokens
9
+
10
+ from mm_sol.balance import get_sol_balance_with_retries
11
+ from mm_sol.converters import lamports_to_sol, sol_to_lamports, to_lamports
12
+
13
+
14
+ def calc_var_value(value: str, *, var_name: str = "var", var_value: int | None = None, decimals: int | None = None) -> int:
15
+ if not isinstance(value, str):
16
+ raise TypeError(f"value is not str: {value}")
17
+ try:
18
+ var_name = var_name.lower()
19
+ result = 0
20
+ for token in split_on_plus_minus_tokens(value.lower()):
21
+ operator = token[0]
22
+ item = token[1:]
23
+ if item.isdigit():
24
+ item_value = int(item)
25
+ elif item.endswith("sol"):
26
+ item = item.removesuffix("sol")
27
+ item_value = sol_to_lamports(Decimal(item))
28
+ elif item.endswith("t"):
29
+ if decimals is None:
30
+ raise ValueError("t without decimals") # noqa: TRY301
31
+ item = item.removesuffix("t")
32
+ item_value = int(Decimal(item) * 10**decimals)
33
+ elif item.endswith(var_name):
34
+ if var_value is None:
35
+ raise ValueError("base value is not set") # noqa: TRY301
36
+ item = item.removesuffix(var_name)
37
+ k = Decimal(item) if item else Decimal(1)
38
+ item_value = int(k * var_value)
39
+ elif item.startswith("random(") and item.endswith(")"):
40
+ item = item.lstrip("random(").rstrip(")")
41
+ arr = item.split(",")
42
+ if len(arr) != 2:
43
+ raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
44
+ from_value = to_lamports(arr[0], decimals=decimals)
45
+ to_value = to_lamports(arr[1], decimals=decimals)
46
+ if from_value > to_value:
47
+ raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
48
+ item_value = random.randint(from_value, to_value)
49
+ else:
50
+ raise ValueError(f"wrong value: {value}") # noqa: TRY301
51
+
52
+ if operator == "+":
53
+ result += item_value
54
+ if operator == "-":
55
+ result -= item_value
56
+
57
+ return result # noqa: TRY300
58
+ except Exception as err:
59
+ raise ValueError(f"wrong value: {value}, error={err}") from err
60
+
61
+
62
+ def is_sol_value_less_min_limit(value_min_limit: str | None, value: int, log_prefix: str | None = None) -> bool:
63
+ if value_min_limit is None:
64
+ return False
65
+ if value < calc_var_value(value_min_limit):
66
+ prefix = mm_crypto_utils.get_log_prefix(log_prefix)
67
+ logger.info("{}value is less min limit, value={}", prefix, lamports_to_sol(value))
68
+ return True
69
+ return False
70
+
71
+
72
+ def calc_sol_value(
73
+ *,
74
+ nodes: Nodes,
75
+ value_str: str,
76
+ address: str,
77
+ proxies: Proxies,
78
+ fee: int = 5000,
79
+ log_prefix: str | None = None,
80
+ ) -> int | None:
81
+ balance_value = None
82
+ if "balance" in value_str.lower():
83
+ prefix = mm_crypto_utils.get_log_prefix(log_prefix)
84
+ res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
85
+ logger.debug(f"{prefix}balance={res.ok_or_err()}")
86
+ if isinstance(res, Err):
87
+ logger.info(f"{prefix}balance error, {res.err}")
88
+ return None
89
+ balance_value = res.ok
90
+ value = calc_var_value(value_str, var_name="balance", var_value=balance_value)
91
+ if "balance" in value_str.lower():
92
+ value = value - fee
93
+ return value
mm_sol/cli/cli.py CHANGED
@@ -4,6 +4,7 @@ from typing import Annotated
4
4
  import typer
5
5
  from mm_std import print_plain
6
6
 
7
+ from . import cli_utils
7
8
  from .cmd import balance_cmd, balances_cmd, example_cmd, node_cmd, transfer_sol_cmd
8
9
  from .cmd.wallet import keypair_cmd, new_cmd
9
10
 
@@ -16,9 +17,7 @@ app.add_typer(wallet_app, name="w", hidden=True)
16
17
 
17
18
  def version_callback(value: bool) -> None:
18
19
  if value:
19
- import importlib.metadata
20
-
21
- print_plain(f"mm-sol: {importlib.metadata.version('mm-sol')}")
20
+ print_plain(f"mm-sol: {cli_utils.get_version()}")
22
21
  raise typer.Exit
23
22
 
24
23
 
@@ -55,11 +54,23 @@ def balances_command(
55
54
  balances_cmd.run(config_path, print_config)
56
55
 
57
56
 
58
- @app.command(name="transfer-sol", help="Transfer SOL")
57
+ @app.command(name="transfer-sol", help="Transfer SOL from one or many accounts")
59
58
  def transfer_sol_command(
60
- config_path: str, print_config: Annotated[bool, typer.Option("--config", "-c", help="Print config and exit")] = False
59
+ config_path: str,
60
+ print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
61
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
62
+ emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
63
+ no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
64
+ debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
61
65
  ) -> None:
62
- transfer_sol_cmd.run(config_path, print_config)
66
+ transfer_sol_cmd.run(
67
+ config_path,
68
+ print_balances=print_balances,
69
+ print_config=print_config,
70
+ debug=debug,
71
+ no_confirmation=no_confirmation,
72
+ emulate=emulate,
73
+ )
63
74
 
64
75
 
65
76
  @app.command(name="node", help="Check RPC urls")
mm_sol/cli/cli_utils.py CHANGED
@@ -1,13 +1,15 @@
1
- import sys
1
+ import importlib.metadata
2
2
 
3
- import pydash
4
- from mm_std import BaseConfig, fatal, hr, print_json
3
+ from mm_crypto_utils import Proxies
4
+ from rich.live import Live
5
+ from rich.table import Table
5
6
 
7
+ from mm_sol.balance import get_sol_balance_with_retries
8
+ from mm_sol.converters import lamports_to_sol
6
9
 
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)
10
+
11
+ def get_version() -> str:
12
+ return importlib.metadata.version("mm-sol")
11
13
 
12
14
 
13
15
  def public_rpc_url(url: str | None) -> str:
@@ -25,12 +27,22 @@ def public_rpc_url(url: str | None) -> str:
25
27
  return url
26
28
 
27
29
 
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}")
30
+ def print_balances(
31
+ rpc_nodes: list[str],
32
+ addresses: list[str],
33
+ *,
34
+ proxies: Proxies = None,
35
+ round_ndigits: int = 5,
36
+ ) -> None:
37
+ table = Table(title="balances")
38
+ table.add_column("n")
39
+ table.add_column("address")
40
+ table.add_column("balance, sol")
41
+ with Live(table, refresh_per_second=0.5):
42
+ for count, address in enumerate(addresses):
43
+ balance = get_sol_balance_with_retries(rpc_nodes, address, proxies=proxies, retries=5).map_or_else(
44
+ lambda err: err,
45
+ lambda ok: str(lamports_to_sol(ok, round_ndigits)),
46
+ )
47
+ row: list[str] = [str(count), address, balance]
48
+ table.add_row(*row)
@@ -1,8 +1,10 @@
1
1
  from decimal import Decimal
2
2
 
3
+ import mm_crypto_utils
3
4
  from mm_std import Ok, print_json
4
5
  from pydantic import BaseModel, Field
5
6
 
7
+ import mm_sol.balance
6
8
  from mm_sol import balance, token
7
9
  from mm_sol.cli import cli_utils
8
10
 
@@ -40,10 +42,10 @@ def run(
40
42
  result = BalanceResult()
41
43
 
42
44
  rpc_url = cli_utils.public_rpc_url(rpc_url)
43
- proxies = cli_utils.load_proxies_from_url(proxies_url) if proxies_url else None
45
+ proxies = mm_crypto_utils.fetch_proxies_or_fatal(proxies_url) if proxies_url else None
44
46
 
45
47
  # sol balance
46
- sol_balance_res = balance.get_balance_with_retries(rpc_url, wallet_address, retries=3, proxies=proxies)
48
+ sol_balance_res = balance.get_sol_balance_with_retries(rpc_url, wallet_address, retries=3, proxies=proxies)
47
49
  if isinstance(sol_balance_res, Ok):
48
50
  result.sol_balance = sol_balance_res.ok
49
51
  else:
@@ -51,7 +53,7 @@ def run(
51
53
 
52
54
  # token balance
53
55
  if token_address:
54
- token_balance_res = token.get_balance_with_retries(
56
+ token_balance_res = mm_sol.balance.get_token_balance_with_retries(
55
57
  nodes=rpc_url,
56
58
  owner_address=wallet_address,
57
59
  token_mint_address=token_address,
@@ -1,62 +1,72 @@
1
+ import os
1
2
  import random
2
3
  from decimal import Decimal
3
- from typing import Any
4
+ from typing import Annotated, Any, Self
4
5
 
5
- from mm_std import BaseConfig, print_console, str_to_list
6
- from pydantic import StrictStr, field_validator
6
+ import mm_crypto_utils
7
+ from mm_crypto_utils import ConfigValidators
8
+ from mm_std import BaseConfig, print_json
9
+ from pydantic import BeforeValidator, Field, model_validator
7
10
 
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
+ import mm_sol.converters
12
+ from mm_sol import balance
13
+ from mm_sol.account import is_valid_pubkey
14
+ from mm_sol.balance import get_token_balance_with_retries
11
15
 
12
16
 
13
17
  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)
18
+ accounts: Annotated[list[str], BeforeValidator(ConfigValidators.addresses(unique=True, is_address=is_valid_pubkey))]
19
+ nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
20
+ tokens: Annotated[list[str], BeforeValidator(ConfigValidators.addresses(unique=True, is_address=is_valid_pubkey))]
21
+ proxies_url: str | None = None
22
+ proxies: list[str] = Field(default_factory=list)
21
23
 
22
24
  @property
23
25
  def random_node(self) -> str:
24
26
  return random.choice(self.nodes)
25
27
 
28
+ @model_validator(mode="after")
29
+ def final_validator(self) -> Self:
30
+ # fetch proxies from proxies_url
31
+ proxies_url = self.proxies_url or os.getenv("MM_SOL_PROXIES_URL", "")
32
+ if proxies_url:
33
+ self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
34
+
35
+ return self
36
+
26
37
 
27
38
  def run(config_path: str, print_config: bool) -> None:
28
39
  config = Config.read_config_or_exit(config_path)
29
- cli_utils.print_config_and_exit(print_config, config)
40
+ if print_config:
41
+ config.print_and_exit()
30
42
 
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)}
43
+ result: dict[str, Any] = {"sol": _get_sol_balances(config.accounts, config)}
34
44
  result["sol_sum"] = sum([v for v in result["sol"].values() if v is not None])
35
45
 
36
46
  if config.tokens:
37
47
  for token in config.tokens:
38
- result[token] = _get_token_balances(token, config.accounts, config.nodes)
48
+ result[token] = _get_token_balances(token, config.accounts, config)
39
49
  result[token + "_sum"] = sum([v for v in result[token].values() if v is not None])
40
50
 
41
- print_console(result, print_json=True)
51
+ print_json(result)
42
52
 
43
53
 
44
- def _get_token_balances(token: str, accounts: list[str], nodes: list[str]) -> dict[str, int | None]:
54
+ def _get_token_balances(token: str, accounts: list[str], config: Config) -> dict[str, int | None]:
45
55
  result = {}
46
56
  for account in accounts:
47
- # result[account] = _get_token_balance(token, account, nodes)
48
- result[account] = get_balance_with_retries(
49
- nodes=nodes,
57
+ result[account] = get_token_balance_with_retries(
58
+ nodes=config.nodes,
50
59
  owner_address=account,
51
60
  token_mint_address=token,
52
61
  retries=3,
62
+ proxies=config.proxies,
53
63
  ).ok_or_none()
54
64
  return result
55
65
 
56
66
 
57
- def _get_sol_balances(accounts: list[str], nodes: list[str]) -> dict[str, Decimal | None]:
67
+ def _get_sol_balances(accounts: list[str], config: Config) -> dict[str, Decimal | None]:
58
68
  result = {}
59
69
  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
70
+ res = balance.get_sol_balance_with_retries(nodes=config.nodes, address=account, retries=3, proxies=config.proxies)
71
+ result[account] = mm_sol.converters.lamports_to_sol(res.unwrap(), ndigits=2) if res.is_ok() else None
62
72
  return result
@@ -1,4 +1,4 @@
1
- from mm_std import print_console
1
+ from mm_std import print_json
2
2
 
3
3
  from mm_sol import rpc
4
4
 
@@ -6,4 +6,4 @@ from mm_sol import rpc
6
6
  def run(urls: list[str], proxy: str | None) -> None:
7
7
  for url in urls:
8
8
  res = rpc.get_block_height(url, proxy=proxy, timeout=10)
9
- print_console(url, res.ok_or_err())
9
+ print_json({url: res.ok_or_err()})
@@ -1,41 +1,194 @@
1
- import random
2
- from decimal import Decimal
1
+ import os
2
+ import sys
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Annotated, Self
3
6
 
4
- from mm_std import BaseConfig, print_console, str_to_list
5
- from pydantic import StrictStr, field_validator
7
+ import mm_crypto_utils
8
+ from loguru import logger
9
+ from mm_crypto_utils import AddressToPrivate, ConfigValidators, TxRoute
10
+ from mm_std import BaseConfig, Err, utc_now
11
+ from pydantic import BeforeValidator, Field, model_validator
12
+ from solders.signature import Signature
6
13
 
7
- from mm_sol.cli import cli_utils
8
- from mm_sol.transfer import transfer_sol
14
+ from mm_sol import transfer
15
+ from mm_sol.account import get_public_key, is_valid_pubkey
16
+ from mm_sol.cli import calcs, cli_utils, validators
17
+ from mm_sol.converters import lamports_to_sol
18
+ from mm_sol.utils import get_client
9
19
 
10
20
 
11
21
  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)
22
+ nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
23
+ routes: Annotated[list[TxRoute], BeforeValidator(ConfigValidators.routes(is_valid_pubkey))]
24
+ routes_from_file: Path | None = None
25
+ routes_to_file: Path | None = None
26
+ private_keys: Annotated[
27
+ AddressToPrivate, Field(default_factory=AddressToPrivate), BeforeValidator(ConfigValidators.private_keys(get_public_key))
28
+ ]
29
+ private_keys_file: Path | None = None
30
+ proxies_url: str | None = None
31
+ proxies: list[str] = Field(default_factory=list)
32
+ value: str
33
+ value_min_limit: str | None = None
34
+ delay: str | None = None # in seconds
35
+ round_ndigits: int = 5
36
+ log_debug: Annotated[Path | None, BeforeValidator(ConfigValidators.log_file())] = None
37
+ log_info: Annotated[Path | None, BeforeValidator(ConfigValidators.log_file())] = None
21
38
 
22
39
  @property
23
- def random_node(self) -> str:
24
- return random.choice(self.nodes)
40
+ def from_addresses(self) -> list[str]:
41
+ return [r.from_address for r in self.routes]
42
+
43
+ @model_validator(mode="after")
44
+ def final_validator(self) -> Self:
45
+ # routes_files
46
+ if self.routes_from_file and self.routes_to_file:
47
+ self.routes += TxRoute.from_files(self.routes_from_file, self.routes_to_file, is_valid_pubkey)
48
+ if not self.routes:
49
+ raise ValueError("routes is empty")
50
+
51
+ # load private keys from file
52
+ if self.private_keys_file:
53
+ self.private_keys.update(AddressToPrivate.from_file(self.private_keys_file, get_public_key))
54
+
55
+ # check all private keys exist
56
+ if not self.private_keys.contains_all_addresses(self.from_addresses):
57
+ raise ValueError("private keys are not set for all addresses")
58
+
59
+ # fetch proxies from proxies_url
60
+ proxies_url = self.proxies_url or os.getenv("MM_PROXIES_URL", "")
61
+ if proxies_url:
62
+ self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
63
+
64
+ # value
65
+ if not validators.is_valid_var_lamports(self.value, "balance"):
66
+ raise ValueError(f"wrong value: {self.value}")
67
+
68
+ # value_min_limit
69
+ if not validators.is_valid_var_lamports(self.value_min_limit):
70
+ raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
25
71
 
72
+ # delay
73
+ if not validators.is_valid_var_lamports(self.delay):
74
+ raise ValueError(f"wrong delay: {self.delay}")
26
75
 
27
- def run(config_path: str, print_config: bool) -> None:
76
+ return self
77
+
78
+
79
+ def run(
80
+ config_path: str,
81
+ *,
82
+ print_balances: bool,
83
+ print_config: bool,
84
+ debug: bool,
85
+ no_confirmation: bool,
86
+ emulate: bool,
87
+ ) -> None:
28
88
  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,
89
+
90
+ if print_config:
91
+ config.print_and_exit({"private_keys", "proxies"})
92
+
93
+ mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
94
+
95
+ if print_balances:
96
+ cli_utils.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits, proxies=config.proxies)
97
+ sys.exit(0)
98
+
99
+ _run_transfers(config, no_confirmation=no_confirmation, emulate=emulate)
100
+
101
+
102
+ def _run_transfers(config: Config, *, no_confirmation: bool, emulate: bool) -> None:
103
+ logger.info(f"started at {utc_now()} UTC")
104
+ logger.debug(
105
+ f"config={config.model_dump(exclude={'private_keys', 'addresses_map', 'proxies'}) | {'version': cli_utils.get_version()}}"
106
+ )
107
+ for i, route in enumerate(config.routes):
108
+ _transfer(
109
+ from_address=route.from_address,
110
+ to_address=route.to_address,
111
+ config=config,
112
+ no_confirmation=no_confirmation,
113
+ emulate=emulate,
39
114
  )
40
- result[recipient] = res.ok_or_err()
41
- print_console(result, print_json=True)
115
+ if not emulate and config.delay is not None and i < len(config.routes) - 1:
116
+ delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
117
+ logger.debug(f"delay {delay_value} seconds")
118
+ time.sleep(float(delay_value))
119
+ logger.info(f"finished at {utc_now()} UTC")
120
+
121
+
122
+ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirmation: bool, emulate: bool) -> None:
123
+ log_prefix = f"{from_address}->{to_address}"
124
+ fee = 5000
125
+ # get value
126
+ value = calcs.calc_sol_value(
127
+ nodes=config.nodes, value_str=config.value, address=from_address, proxies=config.proxies, log_prefix=log_prefix, fee=fee
128
+ )
129
+ if value is None:
130
+ return
131
+
132
+ # value_min_limit
133
+ if calcs.is_sol_value_less_min_limit(config.value_min_limit, value, log_prefix=log_prefix):
134
+ return
135
+
136
+ tx_params = {
137
+ "fee": fee,
138
+ "value": value,
139
+ "to": to_address,
140
+ }
141
+
142
+ # emulate?
143
+ if emulate:
144
+ msg = f"{log_prefix}: emulate, value={lamports_to_sol(value, config.round_ndigits)}SOL,"
145
+ msg += f" fee={fee}"
146
+ logger.info(msg)
147
+ return
148
+
149
+ logger.debug(f"{log_prefix}: tx_params={tx_params}")
150
+
151
+ res = transfer.transfer_sol_with_retries(
152
+ nodes=config.nodes,
153
+ from_address=from_address,
154
+ private_key=config.private_keys[from_address],
155
+ to_address=to_address,
156
+ lamports=value,
157
+ proxies=config.proxies,
158
+ retries=3,
159
+ )
160
+
161
+ if isinstance(res, Err):
162
+ logger.info(f"{log_prefix}: send_error: {res.err}")
163
+ return
164
+ signature = res.ok
165
+
166
+ if no_confirmation:
167
+ msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}"
168
+ logger.info(msg)
169
+ else:
170
+ logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
171
+ status = "UNKNOWN"
172
+ if _wait_confirmation(config, signature, log_prefix):
173
+ status = "OK"
174
+ msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
175
+ logger.info(msg)
176
+
177
+
178
+ def _wait_confirmation(config: Config, signature: Signature, log_prefix: str) -> bool:
179
+ count = 0
180
+ while True:
181
+ try:
182
+ node = mm_crypto_utils.random_node(config.nodes)
183
+ proxy = mm_crypto_utils.random_proxy(config.proxies)
184
+ client = get_client(node, proxy=proxy)
185
+ res = client.get_transaction(signature)
186
+ if res.value and res.value.slot: # check for tx error
187
+ return True
188
+ except Exception as e:
189
+ logger.error(f"{log_prefix}: can't get confirmation, error={e}")
190
+ time.sleep(1)
191
+ count += 1
192
+ if count > 30:
193
+ logger.error(f"{log_prefix}: can't get confirmation, timeout")
194
+ return False
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from mm_std import print_console
3
+ from mm_std import print_json
4
4
 
5
5
  from mm_sol.account import (
6
6
  get_private_key_arr_str,
@@ -16,4 +16,4 @@ def run(private_key: str) -> None:
16
16
  public = get_public_key(private_key)
17
17
  private_base58 = get_private_key_base58(private_key)
18
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)
19
+ print_json({"public": public, "private_base58": private_base58, "private_arr": private_arr})
@@ -1,4 +1,4 @@
1
- from mm_std import print_console
1
+ from mm_std import print_json
2
2
 
3
3
  from mm_sol.account import generate_account, get_private_key_arr_str
4
4
 
@@ -11,4 +11,4 @@ def run(limit: int, array: bool) -> None:
11
11
  if array:
12
12
  private_key = get_private_key_arr_str(acc.private_key_base58)
13
13
  result[acc.public_key] = private_key
14
- print_console(result, print_json=True)
14
+ print_json(result)
@@ -1,8 +1,11 @@
1
- from_address: ERB7SPx1XxH35pLu4Lkg9ChPQgsnwJGLZG8RsEE87XNw
2
- private_key: 3h92ZLyibvqQ9f923s66eN7V1iyG5WRRo3y9nciH4swUiLrkrYRdRM8q3DrtJ9JjTjiU9BT2r2qNzuiCmcZPVvqV
3
- amount: 1.2 # in SOL
4
- recipients:
5
- - Rjg3K9PPDm1B5bmUP7eXZNfmApgvDPF9SrTh12dLRH9
1
+ tx_routes: |
2
+ Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
+ Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
6
4
 
5
+
6
+ private_keys_file: ./path/to/privates.txt
7
+ value: 0.012 sol
8
+
9
+ proxies_url: https://site.com/api/get-proxies
7
10
  nodes: |
8
- https://api.mainnet-beta.solana.com
11
+ https://api.devnet.solana.com
@@ -0,0 +1,11 @@
1
+ from mm_sol.cli import calcs
2
+
3
+
4
+ def is_valid_var_lamports(value: str | None, base_name: str = "var", decimals: int | None = None) -> bool:
5
+ if value is None:
6
+ return True # check for None on BaseModel.field type level
7
+ try:
8
+ calcs.calc_var_value(value, var_value=123, var_name=base_name, decimals=decimals)
9
+ return True # noqa: TRY300
10
+ except ValueError:
11
+ return False
mm_sol/converters.py ADDED
@@ -0,0 +1,33 @@
1
+ from decimal import Decimal
2
+
3
+
4
+ def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
5
+ return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
6
+
7
+
8
+ def sol_to_lamports(sol: Decimal) -> int:
9
+ return int(sol * 10**9)
10
+
11
+
12
+ def to_lamports(value: str | int | Decimal, decimals: int | None = None) -> int:
13
+ if isinstance(value, int):
14
+ return value
15
+ if isinstance(value, Decimal):
16
+ if value != value.to_integral_value():
17
+ raise ValueError(f"value must be integral number: {value}")
18
+ return int(value)
19
+ if isinstance(value, str):
20
+ value = value.lower().replace(" ", "").strip()
21
+ if value.endswith("sol"):
22
+ value = value.replace("sol", "")
23
+ return sol_to_lamports(Decimal(value))
24
+ if value.endswith("t"):
25
+ if decimals is None:
26
+ raise ValueError("t without decimals")
27
+ value = value.removesuffix("t")
28
+ return int(Decimal(value) * 10**decimals)
29
+ if value.isdigit():
30
+ return int(value)
31
+ raise ValueError("wrong value " + value)
32
+
33
+ raise ValueError(f"value has a wrong type: {type(value)}")
mm_sol/solana_cli.py CHANGED
@@ -26,7 +26,6 @@ class StakeAccount(BaseModel):
26
26
  vote: str | None = Field(None, alias="delegatedVoteAccountAddress")
27
27
 
28
28
  @field_validator("balance")
29
- @classmethod
30
29
  def from_lamports_to_sol(cls, v: int | None) -> float | None:
31
30
  if v:
32
31
  return v / 1_000_000_000
@@ -44,7 +43,6 @@ class Stake(BaseModel):
44
43
  lock_time: int | None = Field(None, alias="unixTimestamp")
45
44
 
46
45
  @field_validator("balance", "delegated", "active")
47
- @classmethod
48
46
  def from_lamports_to_sol(cls, v: int | None) -> float | None:
49
47
  if v:
50
48
  return v / 1_000_000_000
mm_sol/token.py CHANGED
@@ -1,77 +1,9 @@
1
+ import mm_crypto_utils
2
+ from mm_crypto_utils import Nodes, Proxies
1
3
  from mm_std import Err, Ok, Result
2
- from solana.exceptions import SolanaRpcException
3
- from solana.rpc.types import TokenAccountOpts
4
4
  from solders.pubkey import Pubkey
5
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
6
+ from mm_sol.utils import get_client
75
7
 
76
8
 
77
9
  def get_decimals(node: str, token_mint_address: str, timeout: float = 10, proxy: str | None = None) -> Result[int]:
@@ -88,46 +20,12 @@ def get_decimals_with_retries(
88
20
  ) -> Result[int]:
89
21
  res: Result[int] = Err("not started yet")
90
22
  for _ in range(retries):
91
- res = get_decimals(get_node(nodes), token_mint_address, timeout=timeout, proxy=get_proxy(proxies))
23
+ res = get_decimals(
24
+ node=mm_crypto_utils.random_node(nodes),
25
+ token_mint_address=token_mint_address,
26
+ timeout=timeout,
27
+ proxy=mm_crypto_utils.random_proxy(proxies),
28
+ )
92
29
  if res.is_ok():
93
30
  return res
94
31
  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 CHANGED
@@ -1,66 +1,165 @@
1
1
  from decimal import Decimal
2
2
 
3
+ import mm_crypto_utils
3
4
  import pydash
5
+ from mm_crypto_utils import Nodes, Proxies
4
6
  from mm_std import Err, Ok, Result
5
7
  from pydantic import BaseModel
6
- from solana.rpc.api import Client
7
8
  from solders.message import Message
8
9
  from solders.pubkey import Pubkey
10
+ from solders.signature import Signature
9
11
  from solders.system_program import TransferParams, transfer
10
12
  from solders.transaction import Transaction
13
+ from spl.token.client import Token
14
+ from spl.token.constants import TOKEN_PROGRAM_ID
15
+ from spl.token.instructions import get_associated_token_address
11
16
 
17
+ import mm_sol.converters
12
18
  from mm_sol import rpc, utils
13
19
  from mm_sol.account import check_private_key, get_keypair
14
20
 
15
21
 
22
+ def transfer_token(
23
+ *,
24
+ node: str,
25
+ token_mint_address: str | Pubkey,
26
+ from_address: str | Pubkey,
27
+ private_key: str,
28
+ to_address: str | Pubkey,
29
+ amount: Decimal,
30
+ decimals: int,
31
+ proxy: str | None = None,
32
+ timeout: float = 10,
33
+ create_token_account_if_not_exists: bool = True,
34
+ ) -> Result[Signature]:
35
+ acc = get_keypair(private_key)
36
+ if not check_private_key(from_address, private_key):
37
+ return Err("invalid_private_key")
38
+
39
+ from_address = utils.pubkey(from_address)
40
+ token_mint_address = utils.pubkey(token_mint_address)
41
+ to_address = utils.pubkey(to_address)
42
+
43
+ client = utils.get_client(node, proxy=proxy, timeout=timeout)
44
+ token_client = Token(conn=client, pubkey=token_mint_address, program_id=TOKEN_PROGRAM_ID, payer=acc)
45
+
46
+ recipient_token_account = get_associated_token_address(to_address, token_mint_address, token_program_id=TOKEN_PROGRAM_ID)
47
+ from_token_account = get_associated_token_address(from_address, token_mint_address, token_program_id=TOKEN_PROGRAM_ID)
48
+
49
+ data: list[object] = []
50
+
51
+ account_info_res = client.get_account_info(recipient_token_account)
52
+ if account_info_res.value is None:
53
+ if create_token_account_if_not_exists:
54
+ create_account_res = token_client.create_account(to_address, skip_confirmation=False)
55
+ data.append(create_account_res)
56
+ else:
57
+ return Err("no_token_account")
58
+
59
+ res = token_client.transfer_checked(
60
+ source=from_token_account,
61
+ dest=recipient_token_account,
62
+ owner=from_address,
63
+ amount=mm_sol.converters.sol_to_lamports(amount),
64
+ decimals=decimals,
65
+ )
66
+ data.append(res)
67
+
68
+ return Ok(res.value, data=data)
69
+
70
+
71
+ def transfer_token_with_retries(
72
+ *,
73
+ nodes: Nodes,
74
+ token_mint_address: str | Pubkey,
75
+ from_address: str | Pubkey,
76
+ private_key: str,
77
+ to_address: str | Pubkey,
78
+ amount: Decimal,
79
+ decimals: int,
80
+ proxies: Proxies = None,
81
+ timeout: float = 10,
82
+ retries: int = 3,
83
+ ) -> Result[Signature]:
84
+ res: Result[Signature] = Err("not started yet")
85
+ for _ in range(retries):
86
+ res = transfer_token(
87
+ node=mm_crypto_utils.random_node(nodes),
88
+ token_mint_address=token_mint_address,
89
+ from_address=from_address,
90
+ private_key=private_key,
91
+ to_address=to_address,
92
+ amount=amount,
93
+ decimals=decimals,
94
+ proxy=mm_crypto_utils.random_proxy(proxies),
95
+ timeout=timeout,
96
+ )
97
+ if res.is_ok():
98
+ return res
99
+ return res
100
+
101
+
16
102
  def transfer_sol(
17
103
  *,
104
+ node: str,
18
105
  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
106
+ private_key: str,
107
+ to_address: str,
108
+ lamports: int,
109
+ proxy: str | None = None,
110
+ timeout: float = 10,
111
+ ) -> Result[Signature]:
112
+ acc = get_keypair(private_key)
113
+ if not check_private_key(from_address, private_key):
114
+ return Err("invalid_private_key")
115
+
116
+ client = utils.get_client(node, proxy=proxy, timeout=timeout)
31
117
  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):
118
+ try:
119
+ ixs = [transfer(TransferParams(from_pubkey=acc.pubkey(), to_pubkey=Pubkey.from_string(to_address), lamports=lamports))]
120
+ msg = Message(ixs, acc.pubkey())
121
+ tx = Transaction([acc], msg, client.get_latest_blockhash().value.blockhash)
122
+ res = client.send_transaction(tx)
123
+ data = res.to_json()
124
+ return Ok(res.value, data=data)
125
+ except Exception as e:
126
+ return Err(e, data=data)
127
+
128
+
129
+ def transfer_sol_with_retries(
130
+ *,
131
+ nodes: Nodes,
132
+ from_address: str,
133
+ private_key: str,
134
+ to_address: str,
135
+ lamports: int,
136
+ proxies: Proxies = None,
137
+ timeout: float = 10,
138
+ retries: int = 3,
139
+ ) -> Result[Signature]:
140
+ res: Result[Signature] = Err("not started yet")
141
+ for _ in range(retries):
142
+ res = transfer_sol(
143
+ node=mm_crypto_utils.random_node(nodes),
144
+ from_address=from_address,
145
+ private_key=private_key,
146
+ to_address=to_address,
147
+ lamports=lamports,
148
+ proxy=mm_crypto_utils.random_proxy(proxies),
149
+ timeout=timeout,
150
+ )
151
+ if res.is_ok():
152
+ return res
153
+ return res
154
+
155
+
156
+ class SolTransferInfo(BaseModel):
58
157
  source: str
59
158
  destination: str
60
159
  lamports: int
61
160
 
62
161
 
63
- def find_transfers(node: str, tx_signature: str) -> Result[list[TransferInfo]]:
162
+ def find_sol_transfers(node: str, tx_signature: str) -> Result[list[SolTransferInfo]]:
64
163
  res = rpc.get_transaction(node, tx_signature, encoding="jsonParsed")
65
164
  if res.is_err():
66
165
  return res # type: ignore[return-value]
@@ -74,7 +173,7 @@ def find_transfers(node: str, tx_signature: str) -> Result[list[TransferInfo]]:
74
173
  destination = pydash.get(ix, "parsed.info.destination")
75
174
  lamports = pydash.get(ix, "parsed.info.lamports")
76
175
  if source and destination and lamports:
77
- result.append(TransferInfo(source=source, destination=destination, lamports=lamports))
176
+ result.append(SolTransferInfo(source=source, destination=destination, lamports=lamports))
78
177
  return Ok(result, data=res.data)
79
178
  except Exception as e:
80
179
  return Err(e, res.data)
mm_sol/utils.py CHANGED
@@ -1,31 +1,6 @@
1
- import random
2
- from decimal import Decimal
3
-
4
1
  from solana.rpc.api import Client
5
2
  from solana.rpc.commitment import Commitment
6
-
7
- from mm_sol.rpc import DEFAULT_MAINNET_RPC
8
- from mm_sol.types import Nodes, Proxies
9
-
10
-
11
- def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
12
- return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
13
-
14
-
15
- def get_node(nodes: Nodes | None = None) -> str:
16
- if nodes is None:
17
- return DEFAULT_MAINNET_RPC
18
- if isinstance(nodes, str):
19
- return nodes
20
- return random.choice(nodes)
21
-
22
-
23
- def get_proxy(proxies: Proxies) -> str | None:
24
- if not proxies:
25
- return None
26
- if isinstance(proxies, str):
27
- return proxies
28
- return random.choice(proxies)
3
+ from solders.pubkey import Pubkey
29
4
 
30
5
 
31
6
  def get_client(
@@ -36,3 +11,9 @@ def get_client(
36
11
  timeout: float = 10,
37
12
  ) -> Client:
38
13
  return Client(endpoint, commitment=commitment, extra_headers=extra_headers, timeout=timeout, proxy=proxy)
14
+
15
+
16
+ def pubkey(value: str | Pubkey) -> Pubkey:
17
+ if isinstance(value, Pubkey):
18
+ return value
19
+ return Pubkey.from_string(value)
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-sol
3
- Version: 0.2.5
4
- Requires-Python: >=3.12
3
+ Version: 0.2.6
4
+ Requires-Python: >=3.13
5
5
  Requires-Dist: base58~=2.1.1
6
6
  Requires-Dist: jinja2>=3.1.5
7
- Requires-Dist: mm-std~=0.1.12
8
- Requires-Dist: solana~=0.36.2
7
+ Requires-Dist: mm-crypto-utils>=0.0.13
8
+ Requires-Dist: solana~=0.36.3
9
9
  Requires-Dist: typer>=0.15.1
@@ -0,0 +1,31 @@
1
+ mm_sol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mm_sol/account.py,sha256=a0pt1tVdY1O2a4qmgfUd5nP-ftqv3ey0T-2ru5qtUWE,3222
3
+ mm_sol/balance.py,sha256=Idx7h9yhRLbrIEDFCBI5QSHE7OT2pYWEZrbHj9XFrkM,3147
4
+ mm_sol/block.py,sha256=4Lc4TANgpGvPflVumC9MR-3vIl1dedGyci3cgzczuds,1794
5
+ mm_sol/converters.py,sha256=jY3wZeo1326z8M-AMS4OEzspRaGt4Tbk0y2SPI-oDFE,1153
6
+ mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ mm_sol/rpc.py,sha256=TspD_KZQp_KJDQzRknxaT8DR03okok26UWDQWF7Zflg,8031
8
+ mm_sol/solana_cli.py,sha256=ig3OoTvmkrl7MFQSZjRHIraLSmtse0_9kn5Nsw8_zb0,8258
9
+ mm_sol/token.py,sha256=RpXHeM3KBH7q9SUyY_U2dRAgLPC7YgumOTAqxxf_JtI,1046
10
+ mm_sol/transfer.py,sha256=-76gSHbQGOZeOgVgo10QWjAcv_9mnGrBNvpRCvXBlZQ,5872
11
+ mm_sol/utils.py,sha256=NE0G564GiT9d7rW_lPPxUb1eq62WiXh28xtvtzNQIqw,556
12
+ mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ mm_sol/cli/calcs.py,sha256=1IQF1j6c9mRBH7UAnX-MwrCC4PJ3Vp1pg9haa2kG22I,3698
14
+ mm_sol/cli/cli.py,sha256=9rWEDAd1iAwJYnC7voTo4ltqMtRd9IuHoGTmqwmDL8Q,3627
15
+ mm_sol/cli/cli_utils.py,sha256=EJTWdccXg8mvkK7E-Dt7zmULzc1WoqFlbu8ASGT2SVI,1381
16
+ mm_sol/cli/validators.py,sha256=RFbkSl67g6CXJJhgkNi0wvx_iXjXyQEqX5GmNYatshk,407
17
+ mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ mm_sol/cli/cmd/balance_cmd.py,sha256=DfUZY3-Hr-F7Y0xp1faol0yLnPu6iDU3b839VhdwAbw,2587
19
+ mm_sol/cli/cmd/balances_cmd.py,sha256=rnsomOKu1_BsRGYvbjTSXVKQ1pqzv4F0fIAnHMjp9kk,2639
20
+ mm_sol/cli/cmd/example_cmd.py,sha256=bK_z4du0UPGAoiHnYdi6iaZim_kKlYw4NKBbzvyes28,221
21
+ mm_sol/cli/cmd/node_cmd.py,sha256=2AEAjq2M9f8-RZiI0rif6wITdns9QUb4Kr34QPsI2CA,238
22
+ mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=vz2_yHmx0cFCD60ubgwru9seABJN71146vW8c-fPtew,6991
23
+ mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=cRHVVTs9zNYmUozZ8ZlJoutn9V6r8I1AEHBrszR7dTE,538
25
+ mm_sol/cli/cmd/wallet/new_cmd.py,sha256=hcP9NZPkwYkTBEvT5rBntFtCIvx1QnPGn5FUDnvz5sM,425
26
+ mm_sol/cli/examples/balances.yml,sha256=SoFcf_IhgA2zrbihrVpqm-ikes80wLFGVzafrjO00UY,290
27
+ mm_sol/cli/examples/transfer-sol.yml,sha256=YFIM36NhaSEPeck6yQMvcgy3MWxgsfDqmMLRqz6P1dk,346
28
+ mm_sol-0.2.6.dist-info/METADATA,sha256=UVHWer8SaDg55zgUGXeoXXORcQuuakRCfiFGQ9msbYY,230
29
+ mm_sol-0.2.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ mm_sol-0.2.6.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
31
+ mm_sol-0.2.6.dist-info/RECORD,,
mm_sol/types.py DELETED
@@ -1,4 +0,0 @@
1
- from collections.abc import Sequence
2
-
3
- type Proxies = str | Sequence[str] | None
4
- type Nodes = str | Sequence[str]
@@ -1,29 +0,0 @@
1
- mm_sol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- mm_sol/account.py,sha256=WlcbPsRpseC507J3E_G-A2-PJGUToNqzSGjrg0pc-B8,3144
3
- mm_sol/balance.py,sha256=QzLjzW2fCGH9Uo8gjxerUnwCStsbkVrguXdPs5N6R5I,667
4
- mm_sol/block.py,sha256=4Lc4TANgpGvPflVumC9MR-3vIl1dedGyci3cgzczuds,1794
5
- mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- mm_sol/rpc.py,sha256=TspD_KZQp_KJDQzRknxaT8DR03okok26UWDQWF7Zflg,8031
7
- mm_sol/solana_cli.py,sha256=odXNPe8G7dvsGcZxujAFjkd0vWM6fdVvt_mzPulgDZQ,8292
8
- mm_sol/token.py,sha256=LpFnmTzbtKo_Z5hWX96k1Z153640N8VzXHCvsH5zArs,4903
9
- mm_sol/transfer.py,sha256=Bdh3amO-sO180Clq5RVgB9y7NXHhu1EbFNgEsGzHjFY,2911
10
- mm_sol/types.py,sha256=vXXP5Dc72BpHv5tsyws0KDZebG1W1-5HH0UjL7N2Mgc,113
11
- mm_sol/utils.py,sha256=tjV2d6ZZ-Z61cS0WWTPX2yZ2VoJUQv11CNW0p3nO54M,1016
12
- mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- mm_sol/cli/cli.py,sha256=l3AKhrgO6GEFrwh3gov9_cfjyFZY0VbZGc0o7NqUWrU,3078
14
- mm_sol/cli/cli_utils.py,sha256=bOrU1LFgndBv4hlNNviHQOob3NfiOsS5YWWDm6fzpCs,1032
15
- mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- mm_sol/cli/cmd/balance_cmd.py,sha256=IryMXq_8yGF9lDXO5kmUo6TRGgedl_9rS3bE1iRPTw0,2516
17
- mm_sol/cli/cmd/balances_cmd.py,sha256=x6PdXqZgkSmHAsbxim06T4_iPbihUIB4GLy0ZPy5t0s,2168
18
- mm_sol/cli/cmd/example_cmd.py,sha256=bK_z4du0UPGAoiHnYdi6iaZim_kKlYw4NKBbzvyes28,221
19
- mm_sol/cli/cmd/node_cmd.py,sha256=UKlZZzd8MVfQHqAjigJRwPj95Mwq2cpHfWgH8BVuwaw,242
20
- mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=Ta3H4TXxlCCKgcedphN6PU90iYQiZvmYYuKPO044ciw,1196
21
- mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=7Dk2eT6hfWRLI0uHO_rAQS8xRwY2S6Uk4iBEIO9aQ_w,561
23
- mm_sol/cli/cmd/wallet/new_cmd.py,sha256=nvFy5lkk8XUM5jchx0rn0aHHQ0LGVgbwpn7-kej9ccI,448
24
- mm_sol/cli/examples/balances.yml,sha256=SoFcf_IhgA2zrbihrVpqm-ikes80wLFGVzafrjO00UY,290
25
- mm_sol/cli/examples/transfer-sol.yml,sha256=fEf5lxnotLwQHt85JzwN0o6kax_Am88VKH5e-n1HP94,290
26
- mm_sol-0.2.5.dist-info/METADATA,sha256=CARYWN8yylvN_Py6sDi3DBhOcrMlXja6IPlK-LxoiF4,221
27
- mm_sol-0.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- mm_sol-0.2.5.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
29
- mm_sol-0.2.5.dist-info/RECORD,,
File without changes