mm-sol 0.5.9__tar.gz → 0.6.1__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.
- {mm_sol-0.5.9 → mm_sol-0.6.1}/PKG-INFO +2 -2
- {mm_sol-0.5.9 → mm_sol-0.6.1}/justfile +7 -4
- {mm_sol-0.5.9 → mm_sol-0.6.1}/pyproject.toml +8 -3
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/account.py +0 -26
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/calcs.py +15 -19
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cli.py +16 -13
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/balance_cmd.py +19 -23
- mm_sol-0.6.1/src/mm_sol/cli/cmd/balances_cmd.py +71 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/node_cmd.py +2 -2
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/transfer_cmd.py +53 -58
- mm_sol-0.6.1/src/mm_sol/retry.py +108 -0
- mm_sol-0.5.9/src/mm_sol/rpc_async.py → mm_sol-0.6.1/src/mm_sol/rpc.py +20 -19
- mm_sol-0.5.9/src/mm_sol/rpc.py → mm_sol-0.6.1/src/mm_sol/rpc_sync.py +16 -33
- mm_sol-0.6.1/src/mm_sol/spl_token.py +48 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/transfer.py +27 -84
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/conftest.py +1 -1
- mm_sol-0.6.1/tests/test_rpc.py +10 -0
- mm_sol-0.6.1/tests/test_rpc_sync.py +50 -0
- mm_sol-0.6.1/tests/test_spl_token.py +22 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/uv.lock +46 -31
- mm_sol-0.5.9/src/mm_sol/async_rpc.py +0 -42
- mm_sol-0.5.9/src/mm_sol/balance.py +0 -158
- mm_sol-0.5.9/src/mm_sol/block.py +0 -58
- mm_sol-0.5.9/src/mm_sol/cli/cmd/balances_cmd.py +0 -71
- mm_sol-0.5.9/src/mm_sol/solana_cli.py +0 -252
- mm_sol-0.5.9/src/mm_sol/token.py +0 -33
- mm_sol-0.5.9/src/mm_sol/token_async.py +0 -40
- mm_sol-0.5.9/tests/test_async_rpc.py +0 -10
- mm_sol-0.5.9/tests/test_balance.py +0 -25
- mm_sol-0.5.9/tests/test_rpc.py +0 -62
- mm_sol-0.5.9/tests/test_token.py +0 -22
- {mm_sol-0.5.9 → mm_sol-0.6.1}/.env.example +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/.gitignore +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/README.md +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/dict.dic +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cli_utils.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/example_cmd.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/wallet/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/wallet/keypair_cmd.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/cmd/wallet/mnemonic_cmd.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/examples/balances.toml +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/examples/transfer.toml +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/cli/validators.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/constants.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/converters.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/py.typed +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/src/mm_sol/utils.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/cli/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/cli/cmd/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/cli/cmd/wallet/__init__.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/cli/cmd/wallet/test_keypair_cmd.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/cli/cmd/wallet/test_mnemonic_cmd.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/test_account.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/test_client.py +0 -0
- {mm_sol-0.5.9 → mm_sol-0.6.1}/tests/test_converters.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-sol
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
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.
|
|
7
|
+
Requires-Dist: mm-crypto-utils>=0.3.4
|
|
8
8
|
Requires-Dist: mnemonic==0.21
|
|
9
9
|
Requires-Dist: socksio>=1.0.0
|
|
10
10
|
Requires-Dist: solana~=0.36.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
set dotenv-load
|
|
2
|
-
version := `uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])'`
|
|
1
|
+
set dotenv-load := true
|
|
3
2
|
|
|
3
|
+
version := `uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])'`
|
|
4
4
|
|
|
5
5
|
clean:
|
|
6
6
|
rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage dist build src/*.egg-info
|
|
@@ -19,6 +19,9 @@ lint: format
|
|
|
19
19
|
uv run ruff check src tests
|
|
20
20
|
uv run mypy src
|
|
21
21
|
|
|
22
|
+
lint-fix: format
|
|
23
|
+
uv run ruff check --fix src tests
|
|
24
|
+
|
|
22
25
|
audit:
|
|
23
26
|
uv run pip-audit
|
|
24
27
|
uv run bandit -r -c "pyproject.toml" src
|
|
@@ -26,8 +29,8 @@ audit:
|
|
|
26
29
|
publish: build
|
|
27
30
|
git diff-index --quiet HEAD
|
|
28
31
|
uvx twine upload dist/**
|
|
29
|
-
git tag -a 'v{{version}}' -m 'v{{version}}'
|
|
30
|
-
git push origin v{{version}}
|
|
32
|
+
git tag -a 'v{{ version }}' -m 'v{{ version }}'
|
|
33
|
+
git push origin v{{ version }}
|
|
31
34
|
|
|
32
35
|
sync:
|
|
33
36
|
uv sync
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-sol"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.1"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-crypto-utils>=0.
|
|
7
|
+
"mm-crypto-utils>=0.3.4",
|
|
8
8
|
"solana~=0.36.6",
|
|
9
9
|
"solders~=0.26.0",
|
|
10
10
|
"base58~=2.1.1",
|
|
@@ -24,10 +24,11 @@ build-backend = "hatchling.build"
|
|
|
24
24
|
dev-dependencies = [
|
|
25
25
|
"pytest~=8.3.5",
|
|
26
26
|
"pytest-xdist~=3.6.1",
|
|
27
|
-
"ruff~=0.11.
|
|
27
|
+
"ruff~=0.11.6",
|
|
28
28
|
"pip-audit~=2.9.0",
|
|
29
29
|
"bandit~=1.8.3",
|
|
30
30
|
"mypy~=1.15.0",
|
|
31
|
+
"pytest-asyncio~=0.26.0",
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
[tool.mypy]
|
|
@@ -78,3 +79,7 @@ indent-style = "space"
|
|
|
78
79
|
[tool.bandit]
|
|
79
80
|
exclude_dirs = ["tests"]
|
|
80
81
|
skips = ["B311"]
|
|
82
|
+
|
|
83
|
+
[tool.pytest.ini_options]
|
|
84
|
+
asyncio_mode = "auto"
|
|
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
|
|
3
|
+
from mm_std import Result
|
|
4
4
|
|
|
5
|
-
from mm_sol
|
|
5
|
+
from mm_sol import retry
|
|
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(
|
|
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 =
|
|
22
|
-
if
|
|
23
|
+
res = await retry.get_sol_balance(5, nodes, proxies, address=address)
|
|
24
|
+
if res.is_err():
|
|
23
25
|
return res
|
|
24
|
-
var = VarInt("balance", res.
|
|
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
|
|
31
|
+
return Result.ok(value)
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
def calc_token_value_for_address(
|
|
33
|
-
*, nodes: Nodes, value_expression: str,
|
|
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 =
|
|
39
|
-
|
|
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 retry.get_token_balance(5, nodes, proxies, owner=owner, token=token)
|
|
41
|
+
if res.is_err():
|
|
46
42
|
return res
|
|
47
|
-
var = VarInt("balance", res.
|
|
43
|
+
var = VarInt("balance", res.unwrap())
|
|
48
44
|
value = calc_token_expression(value_expression, token_decimals, var)
|
|
49
|
-
return
|
|
45
|
+
return Result.ok(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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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,11 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
3
|
import mm_crypto_utils
|
|
4
|
-
from mm_std import
|
|
4
|
+
from mm_std import print_json
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
-
import mm_sol.
|
|
8
|
-
from mm_sol import
|
|
7
|
+
import mm_sol.retry
|
|
8
|
+
from mm_sol import retry
|
|
9
9
|
from mm_sol.cli import cli_utils
|
|
10
10
|
|
|
11
11
|
|
|
@@ -32,7 +32,7 @@ class BalanceResult(BaseModel):
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def run(
|
|
35
|
+
async def run(
|
|
36
36
|
rpc_url: str,
|
|
37
37
|
wallet_address: str,
|
|
38
38
|
token_address: str | None,
|
|
@@ -42,34 +42,30 @@ def run(
|
|
|
42
42
|
result = BalanceResult()
|
|
43
43
|
|
|
44
44
|
rpc_url = cli_utils.public_rpc_url(rpc_url)
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
proxies = await mm_crypto_utils.fetch_proxies_or_fatal(proxies_url) if proxies_url else None
|
|
46
47
|
|
|
47
48
|
# sol balance
|
|
48
|
-
sol_balance_res =
|
|
49
|
-
if
|
|
50
|
-
result.sol_balance = sol_balance_res.
|
|
49
|
+
sol_balance_res = await retry.get_sol_balance(3, rpc_url, proxies, address=wallet_address)
|
|
50
|
+
if sol_balance_res.is_ok():
|
|
51
|
+
result.sol_balance = sol_balance_res.unwrap()
|
|
51
52
|
else:
|
|
52
|
-
result.errors.append("sol_balance: " + sol_balance_res.
|
|
53
|
+
result.errors.append("sol_balance: " + sol_balance_res.unwrap_error())
|
|
53
54
|
|
|
54
55
|
# token balance
|
|
55
56
|
if token_address:
|
|
56
|
-
token_balance_res = mm_sol.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
retries=3,
|
|
61
|
-
proxies=proxies,
|
|
62
|
-
)
|
|
63
|
-
if isinstance(token_balance_res, Ok):
|
|
64
|
-
result.token_balance = token_balance_res.ok
|
|
57
|
+
token_balance_res = await mm_sol.retry.get_token_balance(3, rpc_url, proxies, owner=wallet_address, token=token_address)
|
|
58
|
+
|
|
59
|
+
if token_balance_res.is_ok():
|
|
60
|
+
result.token_balance = token_balance_res.unwrap()
|
|
65
61
|
else:
|
|
66
|
-
result.errors.append("token_balance: " + token_balance_res.
|
|
62
|
+
result.errors.append("token_balance: " + token_balance_res.unwrap_error())
|
|
67
63
|
|
|
68
|
-
decimals_res =
|
|
69
|
-
if
|
|
70
|
-
result.token_decimals = decimals_res.
|
|
64
|
+
decimals_res = await mm_sol.retry.get_token_decimals(3, rpc_url, proxies, token=token_address)
|
|
65
|
+
if decimals_res.is_ok():
|
|
66
|
+
result.token_decimals = decimals_res.unwrap()
|
|
71
67
|
else:
|
|
72
|
-
result.errors.append("token_decimals: " + decimals_res.
|
|
68
|
+
result.errors.append("token_decimals: " + decimals_res.unwrap_error())
|
|
73
69
|
|
|
74
70
|
if lamport:
|
|
75
71
|
print_json(result)
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
import mm_sol.retry
|
|
11
|
+
from mm_sol import converters, retry
|
|
12
|
+
from mm_sol.cli.validators import Validators
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Config(BaseConfig):
|
|
16
|
+
accounts: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
|
|
17
|
+
tokens: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
|
|
18
|
+
nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
|
|
19
|
+
proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def random_node(self) -> str:
|
|
23
|
+
return random.choice(self.nodes)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def run(config_path: Path, print_config: bool) -> None:
|
|
27
|
+
config = Config.read_toml_config_or_exit(config_path)
|
|
28
|
+
if print_config:
|
|
29
|
+
config.print_and_exit()
|
|
30
|
+
|
|
31
|
+
result: dict[str, Any] = {"sol": await _get_sol_balances(config.accounts, config)}
|
|
32
|
+
result["sol_sum"] = sum([v for v in result["sol"].values() if v is not None])
|
|
33
|
+
|
|
34
|
+
if config.tokens:
|
|
35
|
+
for token_address in config.tokens:
|
|
36
|
+
res = await mm_sol.retry.get_token_decimals(3, config.nodes, config.proxies, token=token_address)
|
|
37
|
+
if res.is_err():
|
|
38
|
+
fatal(f"Failed to get decimals for token {token_address}: {res.unwrap_error()}")
|
|
39
|
+
|
|
40
|
+
token_decimals = res.unwrap()
|
|
41
|
+
result[token_address] = await _get_token_balances(token_address, token_decimals, config.accounts, config)
|
|
42
|
+
result[token_address + "_decimals"] = token_decimals
|
|
43
|
+
result[token_address + "_sum"] = sum([v for v in result[token_address].values() if v is not None])
|
|
44
|
+
|
|
45
|
+
print_json(result)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _get_token_balances(
|
|
49
|
+
token_address: str, token_decimals: int, accounts: list[str], config: Config
|
|
50
|
+
) -> dict[str, Decimal | None]:
|
|
51
|
+
result: dict[str, Decimal | None] = {}
|
|
52
|
+
for account in accounts:
|
|
53
|
+
result[account] = (
|
|
54
|
+
(await mm_sol.retry.get_token_balance(3, config.nodes, config.proxies, owner=account, token=token_address))
|
|
55
|
+
.map(lambda v: converters.to_token(v, token_decimals))
|
|
56
|
+
.value
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _get_sol_balances(accounts: list[str], config: Config) -> dict[str, Decimal | None]:
|
|
63
|
+
result = {}
|
|
64
|
+
for account in accounts:
|
|
65
|
+
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
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
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).
|
|
10
|
+
result[url] = (await rpc.get_block_height(url, proxy=proxy, timeout=10)).ok_or_error()
|
|
11
11
|
print_json(data=result)
|
|
@@ -1,25 +1,24 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import sys
|
|
2
|
-
import time
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Annotated
|
|
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,
|
|
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
|
-
|
|
16
|
+
import mm_sol.retry
|
|
17
|
+
from mm_sol import retry
|
|
17
18
|
from mm_sol.cli import calcs, cli_utils
|
|
18
19
|
from mm_sol.cli.cli_utils import BaseConfigParams
|
|
19
20
|
from mm_sol.cli.validators import Validators
|
|
20
21
|
from mm_sol.converters import lamports_to_sol, to_token
|
|
21
|
-
from mm_sol.token import get_decimals_with_retries
|
|
22
|
-
from mm_sol.transfer import transfer_sol_with_retries, transfer_token_with_retries
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
class Config(BaseConfig):
|
|
@@ -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) ->
|
|
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 =
|
|
68
|
-
if
|
|
69
|
-
fatal(f"can't get decimals for token={self.token}, error={res.
|
|
70
|
-
self.token_decimals = res.
|
|
66
|
+
res = await mm_sol.retry.get_token_decimals(3, self.nodes, self.proxies, token=self.token)
|
|
67
|
+
if res.is_err():
|
|
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.
|
|
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,49 +92,49 @@ 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
|
-
|
|
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
|
-
|
|
120
|
+
owner=transfer.from_address,
|
|
122
121
|
proxies=config.proxies,
|
|
123
|
-
|
|
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.
|
|
135
|
-
if
|
|
136
|
-
logger.info(f"{transfer.log_prefix}: calc value error, {value_res.
|
|
133
|
+
logger.debug(f"{transfer.log_prefix}: value={value_res.ok_or_error()}")
|
|
134
|
+
if value_res.is_err():
|
|
135
|
+
logger.info(f"{transfer.log_prefix}: calc value error, {value_res.unwrap_error()}")
|
|
137
136
|
|
|
138
|
-
return value_res.
|
|
137
|
+
return value_res.value
|
|
139
138
|
|
|
140
139
|
|
|
141
140
|
def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
|
|
@@ -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 =
|
|
163
|
-
|
|
161
|
+
res = await retry.transfer_token(
|
|
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 =
|
|
175
|
-
|
|
173
|
+
res = await retry.transfer_sol(
|
|
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
|
|
185
|
-
logger.info(f"{transfer.log_prefix}: tx error {res.
|
|
183
|
+
if res.is_err():
|
|
184
|
+
logger.info(f"{transfer.log_prefix}: tx error {res.unwrap_error()}")
|
|
186
185
|
return None
|
|
187
|
-
return res.
|
|
186
|
+
return res.value
|
|
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
|
-
|
|
260
|
-
|
|
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 retry.get_sol_balance(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
|
-
|
|
269
|
-
|
|
270
|
-
lambda ok: str(to_token(ok, config.token_decimals, ndigits=config.round_ndigits)),
|
|
271
|
-
)
|
|
265
|
+
res = await mm_sol.retry.get_token_balance(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()
|