mm-sol 0.5.9__tar.gz → 0.6.0__tar.gz

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.
Files changed (57) hide show
  1. {mm_sol-0.5.9 → mm_sol-0.6.0}/PKG-INFO +3 -2
  2. {mm_sol-0.5.9 → mm_sol-0.6.0}/pyproject.toml +8 -3
  3. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/account.py +0 -26
  4. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/calcs.py +15 -19
  5. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cli.py +16 -13
  6. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/balance_cmd.py +19 -22
  7. mm_sol-0.6.0/src/mm_sol/cli/cmd/balances_cmd.py +70 -0
  8. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/node_cmd.py +2 -2
  9. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/transfer_cmd.py +50 -55
  10. mm_sol-0.5.9/src/mm_sol/rpc_async.py → mm_sol-0.6.0/src/mm_sol/rpc.py +34 -18
  11. mm_sol-0.5.9/src/mm_sol/rpc.py → mm_sol-0.6.0/src/mm_sol/rpc_sync.py +22 -22
  12. mm_sol-0.6.0/src/mm_sol/spl_token.py +85 -0
  13. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/transfer.py +80 -76
  14. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/conftest.py +1 -1
  15. mm_sol-0.6.0/tests/test_rpc.py +10 -0
  16. mm_sol-0.5.9/tests/test_rpc.py → mm_sol-0.6.0/tests/test_rpc_sync.py +12 -19
  17. mm_sol-0.6.0/tests/test_spl_token.py +22 -0
  18. {mm_sol-0.5.9 → mm_sol-0.6.0}/uv.lock +44 -28
  19. mm_sol-0.5.9/src/mm_sol/async_rpc.py +0 -42
  20. mm_sol-0.5.9/src/mm_sol/balance.py +0 -158
  21. mm_sol-0.5.9/src/mm_sol/block.py +0 -58
  22. mm_sol-0.5.9/src/mm_sol/cli/cmd/balances_cmd.py +0 -71
  23. mm_sol-0.5.9/src/mm_sol/solana_cli.py +0 -252
  24. mm_sol-0.5.9/src/mm_sol/token.py +0 -33
  25. mm_sol-0.5.9/src/mm_sol/token_async.py +0 -40
  26. mm_sol-0.5.9/tests/test_async_rpc.py +0 -10
  27. mm_sol-0.5.9/tests/test_balance.py +0 -25
  28. mm_sol-0.5.9/tests/test_token.py +0 -22
  29. {mm_sol-0.5.9 → mm_sol-0.6.0}/.env.example +0 -0
  30. {mm_sol-0.5.9 → mm_sol-0.6.0}/.gitignore +0 -0
  31. {mm_sol-0.5.9 → mm_sol-0.6.0}/README.md +0 -0
  32. {mm_sol-0.5.9 → mm_sol-0.6.0}/dict.dic +0 -0
  33. {mm_sol-0.5.9 → mm_sol-0.6.0}/justfile +0 -0
  34. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/__init__.py +0 -0
  35. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/__init__.py +0 -0
  36. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cli_utils.py +0 -0
  37. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/__init__.py +0 -0
  38. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/example_cmd.py +0 -0
  39. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/wallet/__init__.py +0 -0
  40. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/wallet/keypair_cmd.py +0 -0
  41. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/cmd/wallet/mnemonic_cmd.py +0 -0
  42. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/examples/balances.toml +0 -0
  43. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/examples/transfer.toml +0 -0
  44. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/cli/validators.py +0 -0
  45. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/constants.py +0 -0
  46. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/converters.py +0 -0
  47. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/py.typed +0 -0
  48. {mm_sol-0.5.9 → mm_sol-0.6.0}/src/mm_sol/utils.py +0 -0
  49. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/__init__.py +0 -0
  50. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/cli/__init__.py +0 -0
  51. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/cli/cmd/__init__.py +0 -0
  52. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/cli/cmd/wallet/__init__.py +0 -0
  53. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/cli/cmd/wallet/test_keypair_cmd.py +0 -0
  54. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/cli/cmd/wallet/test_mnemonic_cmd.py +0 -0
  55. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/test_account.py +0 -0
  56. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/test_client.py +0 -0
  57. {mm_sol-0.5.9 → mm_sol-0.6.0}/tests/test_converters.py +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-sol
3
- Version: 0.5.9
3
+ Version: 0.6.0
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: base58~=2.1.1
6
6
  Requires-Dist: jinja2>=3.1.6
7
- Requires-Dist: mm-crypto-utils>=0.2.15
7
+ Requires-Dist: mm-crypto-utils>=0.3.3
8
+ Requires-Dist: mm-std>=0.4.5
8
9
  Requires-Dist: mnemonic==0.21
9
10
  Requires-Dist: socksio>=1.0.0
10
11
  Requires-Dist: solana~=0.36.6
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "mm-sol"
3
- version = "0.5.9"
3
+ version = "0.6.0"
4
4
  description = ""
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
7
- "mm-crypto-utils>=0.2.15",
7
+ "mm-crypto-utils>=0.3.3",
8
8
  "solana~=0.36.6",
9
9
  "solders~=0.26.0",
10
10
  "base58~=2.1.1",
@@ -12,6 +12,7 @@ dependencies = [
12
12
  "typer>=0.15.2",
13
13
  "jinja2>=3.1.6",
14
14
  "socksio>=1.0.0",
15
+ "mm-std>=0.4.5",
15
16
  ]
16
17
  [project.scripts]
17
18
  mm-sol = "mm_sol.cli.cli:app"
@@ -24,10 +25,11 @@ build-backend = "hatchling.build"
24
25
  dev-dependencies = [
25
26
  "pytest~=8.3.5",
26
27
  "pytest-xdist~=3.6.1",
27
- "ruff~=0.11.2",
28
+ "ruff~=0.11.6",
28
29
  "pip-audit~=2.9.0",
29
30
  "bandit~=1.8.3",
30
31
  "mypy~=1.15.0",
32
+ "pytest-asyncio~=0.26.0",
31
33
  ]
32
34
 
33
35
  [tool.mypy]
@@ -78,3 +80,6 @@ indent-style = "space"
78
80
  [tool.bandit]
79
81
  exclude_dirs = ["tests"]
80
82
  skips = ["B311"]
83
+
84
+ [tool.pytest.ini_options]
85
+ asyncio_default_fixture_loop_scope = "function"
@@ -1,16 +1,11 @@
1
1
  import contextlib
2
- import random
3
2
  from dataclasses import dataclass
4
3
 
5
4
  import base58
6
- import pydash
7
- from mm_std import Err, Ok, Result
8
5
  from mnemonic import Mnemonic
9
6
  from pydantic import BaseModel
10
- from solana.rpc.api import Client
11
7
  from solders.keypair import Keypair
12
8
  from solders.pubkey import Pubkey
13
- from solders.rpc.responses import GetAccountInfoResp
14
9
 
15
10
  PHANTOM_DERIVATION_PATH = "m/44'/501'/{i}'/0'"
16
11
  WORD_STRENGTH = {12: 128, 15: 160, 18: 192, 21: 224, 24: 256}
@@ -105,27 +100,6 @@ def get_private_key_arr_str(private_key: str) -> str:
105
100
  return f"[{','.join(str(x) for x in get_private_key_arr(private_key))}]"
106
101
 
107
102
 
108
- def is_empty_account(*, address: str, node: str | None = None, nodes: list[str] | None = None, attempts: int = 3) -> Result[bool]:
109
- if not node and not nodes:
110
- raise ValueError("node or nodes must be set")
111
- error = None
112
- data = None
113
- for _ in range(attempts):
114
- try:
115
- client = Client(node or random.choice(nodes)) # type: ignore[arg-type]
116
- res: GetAccountInfoResp = client.get_account_info(Pubkey.from_string(address))
117
- data = res
118
- slot = pydash.get(res, "result.context.slot")
119
- value = pydash.get(res, "result.value")
120
- if slot and value is None:
121
- return Ok(True, data=data)
122
- if slot and value:
123
- return Ok(False, data=data)
124
- except Exception as e:
125
- error = str(e)
126
- return Err(error or "unknown response", data=data)
127
-
128
-
129
103
  def is_address(pubkey: str) -> bool:
130
104
  with contextlib.suppress(Exception):
131
105
  Pubkey.from_string(pubkey)
@@ -1,8 +1,8 @@
1
1
  import mm_crypto_utils
2
2
  from mm_crypto_utils import Nodes, Proxies, VarInt
3
- from mm_std import Err, Ok, Result
3
+ from mm_std import Result
4
4
 
5
- from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
5
+ from mm_sol import rpc, spl_token
6
6
  from mm_sol.constants import SUFFIX_DECIMALS
7
7
 
8
8
 
@@ -14,36 +14,32 @@ def calc_token_expression(expression: str, token_decimals: int, var: VarInt | No
14
14
  return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals={"t": token_decimals})
15
15
 
16
16
 
17
- def calc_sol_value_for_address(*, nodes: Nodes, value_expression: str, address: str, proxies: Proxies, fee: int) -> Result[int]:
17
+ async def calc_sol_value_for_address(
18
+ *, nodes: Nodes, value_expression: str, address: str, proxies: Proxies, fee: int
19
+ ) -> Result[int]:
18
20
  value_expression = value_expression.lower()
19
21
  var = None
20
22
  if "balance" in value_expression:
21
- res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
22
- if isinstance(res, Err):
23
+ res = await rpc.get_balance_with_retries(5, nodes, proxies, address=address)
24
+ if res.is_error():
23
25
  return res
24
- var = VarInt("balance", res.ok)
26
+ var = VarInt("balance", res.unwrap())
25
27
 
26
28
  value = calc_sol_expression(value_expression, var)
27
29
  if "balance" in value_expression:
28
30
  value = value - fee
29
- return Ok(value)
31
+ return Result.success(value)
30
32
 
31
33
 
32
- def calc_token_value_for_address(
33
- *, nodes: Nodes, value_expression: str, wallet_address: str, token_mint_address: str, token_decimals: int, proxies: Proxies
34
+ async def calc_token_value_for_address(
35
+ *, nodes: Nodes, value_expression: str, owner: str, token: str, token_decimals: int, proxies: Proxies
34
36
  ) -> Result[int]:
35
37
  var = None
36
38
  value_expression = value_expression.lower()
37
39
  if "balance" in value_expression:
38
- res = get_token_balance_with_retries(
39
- nodes=nodes,
40
- owner_address=wallet_address,
41
- token_mint_address=token_mint_address,
42
- proxies=proxies,
43
- retries=5,
44
- )
45
- if isinstance(res, Err):
40
+ res = await spl_token.get_balance_with_retries(5, nodes, proxies, owner=owner, token=token)
41
+ if res.is_error():
46
42
  return res
47
- var = VarInt("balance", res.ok)
43
+ var = VarInt("balance", res.unwrap())
48
44
  value = calc_token_expression(value_expression, token_decimals, var)
49
- return Ok(value)
45
+ return Result.success(value)
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from enum import Enum
2
3
  from pathlib import Path
3
4
  from typing import Annotated
@@ -50,14 +51,14 @@ def balance_command(
50
51
  proxies_url: Annotated[str, typer.Option("--proxies-url", envvar="MM_SOL_PROXIES_URL")] = "", # nosec
51
52
  lamport: bool = typer.Option(False, "--lamport", "-l", help="Print balances in lamports"),
52
53
  ) -> None:
53
- balance_cmd.run(rpc_url, wallet_address, token_address, lamport, proxies_url)
54
+ asyncio.run(balance_cmd.run(rpc_url, wallet_address, token_address, lamport, proxies_url))
54
55
 
55
56
 
56
57
  @app.command(name="balances", help="Displays SOL and token balances for multiple accounts")
57
58
  def balances_command(
58
59
  config_path: Path, print_config: Annotated[bool, typer.Option("--config", "-c", help="Print config and exit")] = False
59
60
  ) -> None:
60
- balances_cmd.run(config_path, print_config)
61
+ asyncio.run(balances_cmd.run(config_path, print_config))
61
62
 
62
63
 
63
64
  @app.command(name="transfer", help="Transfers SOL or SPL tokens, supporting multiple routes, delays, and expression-based values")
@@ -71,16 +72,18 @@ def transfer_command(
71
72
  no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
72
73
  debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
73
74
  ) -> None:
74
- transfer_cmd.run(
75
- TransferCmdParams(
76
- config_path=config_path,
77
- print_balances=print_balances,
78
- print_transfers=print_transfers,
79
- debug=debug,
80
- no_confirmation=no_confirmation,
81
- emulate=emulate,
82
- print_config_and_exit=print_config or config_verbose,
83
- print_config_verbose=config_verbose,
75
+ asyncio.run(
76
+ transfer_cmd.run(
77
+ TransferCmdParams(
78
+ config_path=config_path,
79
+ print_balances=print_balances,
80
+ print_transfers=print_transfers,
81
+ debug=debug,
82
+ no_confirmation=no_confirmation,
83
+ emulate=emulate,
84
+ print_config_and_exit=print_config or config_verbose,
85
+ print_config_verbose=config_verbose,
86
+ )
84
87
  )
85
88
  )
86
89
 
@@ -90,7 +93,7 @@ def node_command(
90
93
  urls: Annotated[list[str], typer.Argument()],
91
94
  proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
92
95
  ) -> None:
93
- node_cmd.run(urls, proxy)
96
+ asyncio.run(node_cmd.run(urls, proxy))
94
97
 
95
98
 
96
99
  @wallet_app.command(name="mnemonic", help="Derive accounts from a mnemonic")
@@ -1,11 +1,10 @@
1
1
  from decimal import Decimal
2
2
 
3
3
  import mm_crypto_utils
4
- from mm_std import Ok, print_json
4
+ from mm_std import print_json
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- import mm_sol.balance
8
- from mm_sol import balance, token
7
+ from mm_sol import rpc, spl_token
9
8
  from mm_sol.cli import cli_utils
10
9
 
11
10
 
@@ -32,7 +31,7 @@ class BalanceResult(BaseModel):
32
31
  )
33
32
 
34
33
 
35
- def run(
34
+ async def run(
36
35
  rpc_url: str,
37
36
  wallet_address: str,
38
37
  token_address: str | None,
@@ -42,34 +41,32 @@ def run(
42
41
  result = BalanceResult()
43
42
 
44
43
  rpc_url = cli_utils.public_rpc_url(rpc_url)
45
- proxies = mm_crypto_utils.fetch_proxies_or_fatal(proxies_url) if proxies_url else None
44
+
45
+ proxies = await mm_crypto_utils.fetch_proxies_or_fatal(proxies_url) if proxies_url else None
46
46
 
47
47
  # sol balance
48
- sol_balance_res = balance.get_sol_balance_with_retries(rpc_url, wallet_address, retries=3, proxies=proxies)
49
- if isinstance(sol_balance_res, Ok):
50
- result.sol_balance = sol_balance_res.ok
48
+ sol_balance_res = await rpc.get_balance_with_retries(3, rpc_url, proxies, address=wallet_address)
49
+ if sol_balance_res.is_ok():
50
+ result.sol_balance = sol_balance_res.unwrap()
51
51
  else:
52
- result.errors.append("sol_balance: " + sol_balance_res.err)
52
+ result.errors.append("sol_balance: " + sol_balance_res.unwrap_error())
53
53
 
54
54
  # token balance
55
55
  if token_address:
56
- token_balance_res = mm_sol.balance.get_token_balance_with_retries(
57
- nodes=rpc_url,
58
- owner_address=wallet_address,
59
- token_mint_address=token_address,
60
- retries=3,
61
- proxies=proxies,
56
+ token_balance_res = await spl_token.get_balance_with_retries(
57
+ 3, rpc_url, proxies, owner=wallet_address, token=token_address
62
58
  )
63
- if isinstance(token_balance_res, Ok):
64
- result.token_balance = token_balance_res.ok
59
+
60
+ if token_balance_res.is_ok():
61
+ result.token_balance = token_balance_res.unwrap()
65
62
  else:
66
- result.errors.append("token_balance: " + token_balance_res.err)
63
+ result.errors.append("token_balance: " + token_balance_res.unwrap_error())
67
64
 
68
- decimals_res = token.get_decimals_with_retries(rpc_url, token_address, retries=3, proxies=proxies)
69
- if isinstance(decimals_res, Ok):
70
- result.token_decimals = decimals_res.ok
65
+ decimals_res = await spl_token.get_decimals_with_retries(3, rpc_url, proxies, token=token_address)
66
+ if decimals_res.is_ok():
67
+ result.token_decimals = decimals_res.unwrap()
71
68
  else:
72
- result.errors.append("token_decimals: " + decimals_res.err)
69
+ result.errors.append("token_decimals: " + decimals_res.unwrap_error())
73
70
 
74
71
  if lamport:
75
72
  print_json(result)
@@ -0,0 +1,70 @@
1
+ import random
2
+ from decimal import Decimal
3
+ from pathlib import Path
4
+ from typing import Annotated, Any
5
+
6
+ from mm_crypto_utils import ConfigValidators
7
+ from mm_std import BaseConfig, fatal, print_json
8
+ from pydantic import BeforeValidator, Field
9
+
10
+ from mm_sol import converters, rpc, spl_token
11
+ from mm_sol.cli.validators import Validators
12
+
13
+
14
+ class Config(BaseConfig):
15
+ accounts: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
16
+ tokens: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
17
+ nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
18
+ proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
19
+
20
+ @property
21
+ def random_node(self) -> str:
22
+ return random.choice(self.nodes)
23
+
24
+
25
+ async def run(config_path: Path, print_config: bool) -> None:
26
+ config = Config.read_toml_config_or_exit(config_path)
27
+ if print_config:
28
+ config.print_and_exit()
29
+
30
+ result: dict[str, Any] = {"sol": await _get_sol_balances(config.accounts, config)}
31
+ result["sol_sum"] = sum([v for v in result["sol"].values() if v is not None])
32
+
33
+ if config.tokens:
34
+ for token_address in config.tokens:
35
+ res = await spl_token.get_decimals_with_retries(3, config.nodes, config.proxies, token=token_address)
36
+ if res.is_error():
37
+ fatal(f"Failed to get decimals for token {token_address}: {res.unwrap_error()}")
38
+
39
+ token_decimals = res.unwrap()
40
+ result[token_address] = await _get_token_balances(token_address, token_decimals, config.accounts, config)
41
+ result[token_address + "_decimals"] = token_decimals
42
+ result[token_address + "_sum"] = sum([v for v in result[token_address].values() if v is not None])
43
+
44
+ print_json(result)
45
+
46
+
47
+ async def _get_token_balances(
48
+ token_address: str, token_decimals: int, accounts: list[str], config: Config
49
+ ) -> dict[str, Decimal | None]:
50
+ result: dict[str, Decimal | None] = {}
51
+ for account in accounts:
52
+ result[account] = (
53
+ (await spl_token.get_balance_with_retries(3, config.nodes, config.proxies, owner=account, token=token_address))
54
+ .map(lambda v: converters.to_token(v, token_decimals))
55
+ .ok
56
+ )
57
+
58
+ return result
59
+
60
+
61
+ async def _get_sol_balances(accounts: list[str], config: Config) -> dict[str, Decimal | None]:
62
+ result = {}
63
+ for account in accounts:
64
+ result[account] = (
65
+ (await rpc.get_balance_with_retries(3, config.nodes, config.proxies, address=account))
66
+ .map(lambda v: converters.lamports_to_sol(v))
67
+ .ok
68
+ )
69
+
70
+ return result
@@ -4,8 +4,8 @@ from mm_sol import rpc
4
4
  from mm_sol.cli import cli_utils
5
5
 
6
6
 
7
- def run(urls: list[str], proxy: str | None) -> None:
7
+ async def run(urls: list[str], proxy: str | None) -> None:
8
8
  result = {}
9
9
  for url in [cli_utils.public_rpc_url(u) for u in urls]:
10
- result[url] = rpc.get_block_height(url, proxy=proxy, timeout=10).ok_or_err()
10
+ result[url] = (await rpc.get_block_height(url, proxy=proxy, timeout=10)).ok_or_error()
11
11
  print_json(data=result)
@@ -1,24 +1,23 @@
1
+ import asyncio
1
2
  import sys
2
- import time
3
3
  from pathlib import Path
4
- from typing import Annotated, Self
4
+ from typing import Annotated
5
5
 
6
6
  import mm_crypto_utils
7
7
  from loguru import logger
8
8
  from mm_crypto_utils import AddressToPrivate, Transfer
9
- from mm_std import BaseConfig, Err, fatal, utc_now
9
+ from mm_std import BaseConfig, fatal, utc_now
10
10
  from pydantic import AfterValidator, BeforeValidator, Field, model_validator
11
11
  from rich.console import Console
12
12
  from rich.live import Live
13
13
  from rich.table import Table
14
14
  from solders.signature import Signature
15
15
 
16
- from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
16
+ from mm_sol import rpc, spl_token
17
17
  from mm_sol.cli import calcs, cli_utils
18
18
  from mm_sol.cli.cli_utils import BaseConfigParams
19
19
  from mm_sol.cli.validators import Validators
20
20
  from mm_sol.converters import lamports_to_sol, to_token
21
- from mm_sol.token import get_decimals_with_retries
22
21
  from mm_sol.transfer import transfer_sol_with_retries, transfer_token_with_retries
23
22
 
24
23
 
@@ -40,8 +39,8 @@ class Config(BaseConfig):
40
39
  def from_addresses(self) -> list[str]:
41
40
  return [r.from_address for r in self.transfers]
42
41
 
43
- @model_validator(mode="after")
44
- def final_validator(self) -> Self:
42
+ @model_validator(mode="after") # type: ignore[misc]
43
+ async def final_validator(self) -> "Config":
45
44
  if not self.private_keys.contains_all_addresses(self.from_addresses):
46
45
  raise ValueError("private keys are not set for all addresses")
47
46
 
@@ -64,10 +63,10 @@ class Config(BaseConfig):
64
63
  Validators.valid_sol_expression()(self.value_min_limit)
65
64
 
66
65
  if self.token:
67
- res = get_decimals_with_retries(self.nodes, self.token, retries=3, proxies=self.proxies)
68
- if isinstance(res, Err):
69
- fatal(f"can't get decimals for token={self.token}, error={res.err}")
70
- self.token_decimals = res.ok
66
+ res = await spl_token.get_decimals_with_retries(3, self.nodes, self.proxies, token=self.token)
67
+ if res.is_error():
68
+ fatal(f"can't get decimals for token={self.token}, error={res.unwrap_error()}")
69
+ self.token_decimals = res.unwrap()
71
70
 
72
71
  return self
73
72
 
@@ -81,8 +80,8 @@ class TransferCmdParams(BaseConfigParams):
81
80
  print_config_verbose: bool
82
81
 
83
82
 
84
- def run(cmd_params: TransferCmdParams) -> None:
85
- config = Config.read_toml_config_or_exit(cmd_params.config_path)
83
+ async def run(cmd_params: TransferCmdParams) -> None:
84
+ config = await Config.read_toml_config_or_exit_async(cmd_params.config_path)
86
85
 
87
86
  if cmd_params.print_config_and_exit:
88
87
  cli_utils.print_config(config, exclude={"private_keys"}, count=None if cmd_params.debug else {"proxies"})
@@ -93,47 +92,47 @@ def run(cmd_params: TransferCmdParams) -> None:
93
92
  sys.exit(0)
94
93
 
95
94
  if cmd_params.print_balances:
96
- _print_balances(config)
95
+ await _print_balances(config)
97
96
  sys.exit(0)
98
97
 
99
- _run_transfers(config, cmd_params)
98
+ await _run_transfers(config, cmd_params)
100
99
 
101
100
 
102
- def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
101
+ async def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
103
102
  mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
104
103
  logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
105
104
  logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
106
105
  for i, route in enumerate(config.transfers):
107
- _transfer(route, config, cmd_params)
106
+ await _transfer(route, config, cmd_params)
108
107
  if config.delay is not None and i < len(config.transfers) - 1:
109
108
  delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
110
109
  logger.info(f"delay {delay_value} seconds")
111
110
  if not cmd_params.emulate:
112
- time.sleep(float(delay_value))
111
+ await asyncio.sleep(float(delay_value))
113
112
  logger.info(f"transfer {cmd_params.config_path}: finished at {utc_now()} UTC")
114
113
 
115
114
 
116
- def _calc_value(transfer: Transfer, config: Config, transfer_sol_fee: int) -> int | None:
115
+ async def _calc_value(transfer: Transfer, config: Config, transfer_sol_fee: int) -> int | None:
117
116
  if config.token:
118
- value_res = calcs.calc_token_value_for_address(
117
+ value_res = await calcs.calc_token_value_for_address(
119
118
  nodes=config.nodes,
120
119
  value_expression=transfer.value,
121
- wallet_address=transfer.from_address,
120
+ owner=transfer.from_address,
122
121
  proxies=config.proxies,
123
- token_mint_address=config.token,
122
+ token=config.token,
124
123
  token_decimals=config.token_decimals,
125
124
  )
126
125
  else:
127
- value_res = calcs.calc_sol_value_for_address(
126
+ value_res = await calcs.calc_sol_value_for_address(
128
127
  nodes=config.nodes,
129
128
  value_expression=transfer.value,
130
129
  address=transfer.from_address,
131
130
  proxies=config.proxies,
132
131
  fee=transfer_sol_fee,
133
132
  )
134
- logger.debug(f"{transfer.log_prefix}: value={value_res.ok_or_err()}")
135
- if isinstance(value_res, Err):
136
- logger.info(f"{transfer.log_prefix}: calc value error, {value_res.err}")
133
+ logger.debug(f"{transfer.log_prefix}: value={value_res.ok_or_error()}")
134
+ if value_res.is_error():
135
+ logger.info(f"{transfer.log_prefix}: calc value error, {value_res.unwrap_error()}")
137
136
 
138
137
  return value_res.ok
139
138
 
@@ -156,41 +155,41 @@ def _value_with_suffix(value: int, config: Config) -> str:
156
155
  return f"{lamports_to_sol(value, config.round_ndigits)}sol"
157
156
 
158
157
 
159
- def _send_tx(transfer: Transfer, value: int, config: Config) -> Signature | None:
158
+ async def _send_tx(transfer: Transfer, value: int, config: Config) -> Signature | None:
160
159
  logger.debug(f"{transfer.log_prefix}: value={_value_with_suffix(value, config)}")
161
160
  if config.token:
162
- res = transfer_token_with_retries(
163
- nodes=config.nodes,
161
+ res = await transfer_token_with_retries(
162
+ 3,
163
+ config.nodes,
164
+ config.proxies,
164
165
  token_mint_address=config.token,
165
166
  from_address=transfer.from_address,
166
167
  private_key=config.private_keys[transfer.from_address],
167
168
  to_address=transfer.to_address,
168
169
  amount=value,
169
170
  decimals=config.token_decimals,
170
- proxies=config.proxies,
171
- retries=3,
172
171
  )
173
172
  else:
174
- res = transfer_sol_with_retries(
175
- nodes=config.nodes,
173
+ res = await transfer_sol_with_retries(
174
+ 3,
175
+ config.nodes,
176
+ config.proxies,
176
177
  from_address=transfer.from_address,
177
178
  private_key=config.private_keys[transfer.from_address],
178
179
  to_address=transfer.to_address,
179
180
  lamports=value,
180
- proxies=config.proxies,
181
- retries=3,
182
181
  )
183
182
 
184
- if isinstance(res, Err):
185
- logger.info(f"{transfer.log_prefix}: tx error {res.err}")
183
+ if res.is_error():
184
+ logger.info(f"{transfer.log_prefix}: tx error {res.unwrap_error()}")
186
185
  return None
187
186
  return res.ok
188
187
 
189
188
 
190
- def _transfer(transfer: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
189
+ async def _transfer(transfer: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
191
190
  transfer_sol_fee = 5000
192
191
 
193
- value = _calc_value(transfer, config, transfer_sol_fee)
192
+ value = await _calc_value(transfer, config, transfer_sol_fee)
194
193
  if value is None:
195
194
  return
196
195
 
@@ -201,7 +200,7 @@ def _transfer(transfer: Transfer, config: Config, cmd_params: TransferCmdParams)
201
200
  logger.info(f"{transfer.log_prefix}: emulate, value={_value_with_suffix(value, config)}")
202
201
  return
203
202
 
204
- signature = _send_tx(transfer, value, config)
203
+ signature = await _send_tx(transfer, value, config)
205
204
  if signature is None:
206
205
  return
207
206
 
@@ -222,7 +221,7 @@ def _print_transfers(config: Config) -> None:
222
221
  console.print(table)
223
222
 
224
223
 
225
- def _print_balances(config: Config) -> None:
224
+ async def _print_balances(config: Config) -> None:
226
225
  if config.token:
227
226
  headers = ["n", "from_address", "sol", "t", "to_address", "sol", "t"]
228
227
  else:
@@ -230,10 +229,10 @@ def _print_balances(config: Config) -> None:
230
229
  table = Table(*headers, title="balances")
231
230
  with Live(table, refresh_per_second=0.5):
232
231
  for count, route in enumerate(config.transfers):
233
- from_sol_balance = _get_sol_balance_str(route.from_address, config)
234
- to_sol_balance = _get_sol_balance_str(route.to_address, config)
235
- from_t_balance = _get_token_balance_str(route.from_address, config) if config.token else ""
236
- to_t_balance = _get_token_balance_str(route.to_address, config) if config.token else ""
232
+ from_sol_balance = await _get_sol_balance_str(route.from_address, config)
233
+ to_sol_balance = await _get_sol_balance_str(route.to_address, config)
234
+ from_t_balance = await _get_token_balance_str(route.from_address, config) if config.token else ""
235
+ to_t_balance = await _get_token_balance_str(route.to_address, config) if config.token else ""
237
236
 
238
237
  if config.token:
239
238
  table.add_row(
@@ -255,17 +254,13 @@ def _print_balances(config: Config) -> None:
255
254
  )
256
255
 
257
256
 
258
- def _get_sol_balance_str(address: str, config: Config) -> str:
259
- return get_sol_balance_with_retries(config.nodes, address, proxies=config.proxies, retries=5).map_or_else(
260
- lambda err: err,
261
- lambda ok: str(lamports_to_sol(ok, config.round_ndigits)),
262
- )
257
+ async def _get_sol_balance_str(address: str, config: Config) -> str:
258
+ res = await rpc.get_balance_with_retries(5, config.nodes, config.proxies, address=address)
259
+ return res.map(lambda ok: str(lamports_to_sol(ok, config.round_ndigits))).ok_or_error()
263
260
 
264
261
 
265
- def _get_token_balance_str(address: str, config: Config) -> str:
262
+ async def _get_token_balance_str(address: str, config: Config) -> str:
266
263
  if not config.token:
267
264
  raise ValueError("token is not set")
268
- return get_token_balance_with_retries(config.nodes, address, config.token, proxies=config.proxies, retries=5).map_or_else(
269
- lambda err: err,
270
- lambda ok: str(to_token(ok, config.token_decimals, ndigits=config.round_ndigits)),
271
- )
265
+ res = await spl_token.get_balance_with_retries(5, config.nodes, config.proxies, owner=address, token=config.token)
266
+ return res.map(lambda ok: str(to_token(ok, config.token_decimals, ndigits=config.round_ndigits))).ok_or_error()