mm-sol 0.2.8__tar.gz → 0.2.10__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 (54) hide show
  1. {mm_sol-0.2.8 → mm_sol-0.2.10}/PKG-INFO +2 -2
  2. {mm_sol-0.2.8 → mm_sol-0.2.10}/pyproject.toml +2 -2
  3. mm_sol-0.2.10/src/mm_sol/cli/calcs.py +49 -0
  4. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cli.py +1 -0
  5. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cli_utils.py +25 -1
  6. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/balances_cmd.py +6 -18
  7. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/transfer_sol_cmd.py +21 -78
  8. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/transfer_token_cmd.py +54 -36
  9. mm_sol-0.2.10/src/mm_sol/cli/examples/transfer-sol.yml +9 -0
  10. mm_sol-0.2.8/src/mm_sol/cli/examples/transfer-sol.yml → mm_sol-0.2.10/src/mm_sol/cli/examples/transfer-token.yml +3 -3
  11. mm_sol-0.2.10/src/mm_sol/cli/validators.py +32 -0
  12. mm_sol-0.2.10/src/mm_sol/constants.py +1 -0
  13. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/converters.py +4 -0
  14. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/transfer.py +3 -6
  15. {mm_sol-0.2.8 → mm_sol-0.2.10}/uv.lock +4 -4
  16. mm_sol-0.2.8/src/mm_sol/cli/calcs.py +0 -101
  17. mm_sol-0.2.8/src/mm_sol/cli/validators.py +0 -17
  18. mm_sol-0.2.8/tests/cli/test_calcs.py +0 -28
  19. {mm_sol-0.2.8 → mm_sol-0.2.10}/.env.example +0 -0
  20. {mm_sol-0.2.8 → mm_sol-0.2.10}/.gitignore +0 -0
  21. {mm_sol-0.2.8 → mm_sol-0.2.10}/README.txt +0 -0
  22. {mm_sol-0.2.8 → mm_sol-0.2.10}/dict.dic +0 -0
  23. {mm_sol-0.2.8 → mm_sol-0.2.10}/justfile +0 -0
  24. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/__init__.py +0 -0
  25. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/account.py +0 -0
  26. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/balance.py +0 -0
  27. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/block.py +0 -0
  28. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/__init__.py +0 -0
  29. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/__init__.py +0 -0
  30. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/balance_cmd.py +0 -0
  31. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/example_cmd.py +0 -0
  32. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/node_cmd.py +0 -0
  33. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/wallet/__init__.py +0 -0
  34. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/wallet/keypair_cmd.py +0 -0
  35. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/cmd/wallet/new_cmd.py +0 -0
  36. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/cli/examples/balances.yml +0 -0
  37. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/py.typed +0 -0
  38. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/rpc.py +0 -0
  39. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/solana_cli.py +0 -0
  40. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/token.py +0 -0
  41. {mm_sol-0.2.8 → mm_sol-0.2.10}/src/mm_sol/utils.py +0 -0
  42. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/__init__.py +0 -0
  43. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/__init__.py +0 -0
  44. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/__init__.py +0 -0
  45. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/wallet/__init__.py +0 -0
  46. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/wallet/test_keypair_cmd.py +0 -0
  47. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/cli/cmd/wallet/test_new_cmd.py +0 -0
  48. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/conftest.py +0 -0
  49. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_account.py +0 -0
  50. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_balance.py +0 -0
  51. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_client.py +0 -0
  52. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_converters.py +0 -0
  53. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_rpc.py +0 -0
  54. {mm_sol-0.2.8 → mm_sol-0.2.10}/tests/test_token.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-sol
3
- Version: 0.2.8
3
+ Version: 0.2.10
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: base58~=2.1.1
6
6
  Requires-Dist: jinja2>=3.1.5
7
- Requires-Dist: mm-crypto-utils>=0.0.15
7
+ Requires-Dist: mm-crypto-utils>=0.0.19
8
8
  Requires-Dist: solana~=0.36.3
9
9
  Requires-Dist: typer>=0.15.1
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "mm-sol"
3
- version = "0.2.8"
3
+ version = "0.2.10"
4
4
  description = ""
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
7
- "mm-crypto-utils>=0.0.15",
7
+ "mm-crypto-utils>=0.0.19",
8
8
  "solana~=0.36.3",
9
9
  "base58~=2.1.1",
10
10
  "typer>=0.15.1",
@@ -0,0 +1,49 @@
1
+ import mm_crypto_utils
2
+ from mm_crypto_utils import Nodes, Proxies, VarInt
3
+ from mm_std import Err, Ok, Result
4
+
5
+ from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
6
+ from mm_sol.constants import SUFFIX_DECIMALS
7
+
8
+
9
+ def calc_sol_expression(expression: str, var: VarInt | None = None) -> int:
10
+ return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals=SUFFIX_DECIMALS)
11
+
12
+
13
+ def calc_token_expression(expression: str, token_decimals: int, var: VarInt | None = None) -> int:
14
+ return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals={"t": token_decimals})
15
+
16
+
17
+ def calc_sol_value_for_address(*, nodes: Nodes, value_expression: str, address: str, proxies: Proxies, fee: int) -> Result[int]:
18
+ value_expression = value_expression.lower()
19
+ var = None
20
+ if "balance" in value_expression:
21
+ res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
22
+ if isinstance(res, Err):
23
+ return res
24
+ var = VarInt("balance", res.ok)
25
+
26
+ value = calc_sol_expression(value_expression, var)
27
+ if "balance" in value_expression:
28
+ value = value - fee
29
+ return Ok(value)
30
+
31
+
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
+ ) -> Result[int]:
35
+ var = None
36
+ value_expression = value_expression.lower()
37
+ 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):
46
+ return res
47
+ var = VarInt("balance", res.ok)
48
+ value = calc_token_expression(value_expression, token_decimals, var)
49
+ return Ok(value)
@@ -29,6 +29,7 @@ def main(_version: bool = typer.Option(None, "--version", callback=version_callb
29
29
  class ConfigExample(str, Enum):
30
30
  balances = "balances"
31
31
  transfer_sol = "transfer-sol"
32
+ transfer_token = "transfer-token" # noqa: S105 # nosec
32
33
 
33
34
 
34
35
  @app.command(name="example", help="Print an example of config for a command")
@@ -1,11 +1,16 @@
1
1
  import importlib.metadata
2
+ import time
2
3
 
3
- from mm_crypto_utils import Proxies
4
+ import mm_crypto_utils
5
+ from loguru import logger
6
+ from mm_crypto_utils import Nodes, Proxies
4
7
  from rich.live import Live
5
8
  from rich.table import Table
9
+ from solders.signature import Signature
6
10
 
7
11
  from mm_sol.balance import get_sol_balance_with_retries
8
12
  from mm_sol.converters import lamports_to_sol
13
+ from mm_sol.utils import get_client
9
14
 
10
15
 
11
16
  def get_version() -> str:
@@ -46,3 +51,22 @@ def print_balances(
46
51
  )
47
52
  row: list[str] = [str(count), address, balance]
48
53
  table.add_row(*row)
54
+
55
+
56
+ def wait_confirmation(nodes: Nodes, proxies: Proxies, signature: Signature, log_prefix: str) -> bool:
57
+ count = 0
58
+ while True:
59
+ try:
60
+ node = mm_crypto_utils.random_node(nodes)
61
+ proxy = mm_crypto_utils.random_proxy(proxies)
62
+ client = get_client(node, proxy=proxy)
63
+ res = client.get_transaction(signature)
64
+ if res.value and res.value.slot: # check for tx error
65
+ return True
66
+ except Exception as e:
67
+ logger.error(f"{log_prefix}: can't get confirmation, error={e}")
68
+ time.sleep(1)
69
+ count += 1
70
+ if count > 30:
71
+ logger.error(f"{log_prefix}: can't get confirmation, timeout")
72
+ return False
@@ -1,39 +1,27 @@
1
- import os
2
1
  import random
3
2
  from decimal import Decimal
4
- from typing import Annotated, Any, Self
3
+ from typing import Annotated, Any
5
4
 
6
- import mm_crypto_utils
7
5
  from mm_crypto_utils import ConfigValidators
8
6
  from mm_std import BaseConfig, print_json
9
- from pydantic import BeforeValidator, Field, model_validator
7
+ from pydantic import BeforeValidator
10
8
 
11
9
  import mm_sol.converters
12
10
  from mm_sol import balance
13
- from mm_sol.account import is_address
14
11
  from mm_sol.balance import get_token_balance_with_retries
12
+ from mm_sol.cli.validators import Validators
15
13
 
16
14
 
17
15
  class Config(BaseConfig):
18
- accounts: Annotated[list[str], BeforeValidator(ConfigValidators.addresses(unique=True, is_address=is_address))]
16
+ accounts: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
17
+ tokens: Annotated[list[str], BeforeValidator(Validators.sol_addresses(unique=True))]
19
18
  nodes: Annotated[list[str], BeforeValidator(ConfigValidators.nodes())]
20
- tokens: Annotated[list[str], BeforeValidator(ConfigValidators.addresses(unique=True, is_address=is_address))]
21
- proxies_url: str | None = None
22
- proxies: list[str] = Field(default_factory=list)
19
+ proxies: Annotated[list[str], BeforeValidator(Validators.proxies())]
23
20
 
24
21
  @property
25
22
  def random_node(self) -> str:
26
23
  return random.choice(self.nodes)
27
24
 
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
-
37
25
 
38
26
  def run(config_path: str, print_config: bool) -> None:
39
27
  config = Config.read_config_or_exit(config_path)
@@ -1,4 +1,3 @@
1
- import os
2
1
  import sys
3
2
  import time
4
3
  from pathlib import Path
@@ -8,31 +7,24 @@ import mm_crypto_utils
8
7
  from loguru import logger
9
8
  from mm_crypto_utils import AddressToPrivate, TxRoute
10
9
  from mm_std import BaseConfig, Err, utc_now
11
- from pydantic import BeforeValidator, Field, model_validator
12
- from solders.signature import Signature
10
+ from pydantic import AfterValidator, BeforeValidator, model_validator
13
11
 
14
12
  from mm_sol import transfer
15
- from mm_sol.account import get_public_key, is_address
16
- from mm_sol.cli import calcs, cli_utils, validators
13
+ from mm_sol.cli import calcs, cli_utils
14
+ from mm_sol.cli.calcs import calc_sol_expression
17
15
  from mm_sol.cli.validators import Validators
18
16
  from mm_sol.converters import lamports_to_sol
19
- from mm_sol.utils import get_client
20
17
 
21
18
 
19
+ # noinspection DuplicatedCode
22
20
  class Config(BaseConfig):
23
21
  nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
24
- routes: Annotated[list[TxRoute], BeforeValidator(Validators.routes(is_address))]
25
- routes_from_file: Path | None = None
26
- routes_to_file: Path | None = None
27
- private_keys: Annotated[
28
- AddressToPrivate, Field(default_factory=AddressToPrivate), BeforeValidator(Validators.private_keys(get_public_key))
29
- ]
30
- private_keys_file: Path | None = None
31
- proxies_url: str | None = None
32
- proxies: list[str] = Field(default_factory=list)
33
- value: str
34
- value_min_limit: str | None = None
35
- delay: str | None = None # in seconds
22
+ routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
23
+ private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
24
+ proxies: Annotated[list[str], BeforeValidator(Validators.proxies())]
25
+ value: Annotated[str, AfterValidator(Validators.valid_sol_expression("balance"))]
26
+ value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_sol_expression())] = None
27
+ delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
36
28
  round_ndigits: int = 5
37
29
  log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
38
30
  log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
@@ -43,37 +35,9 @@ class Config(BaseConfig):
43
35
 
44
36
  @model_validator(mode="after")
45
37
  def final_validator(self) -> Self:
46
- # routes_files
47
- if self.routes_from_file and self.routes_to_file:
48
- self.routes += TxRoute.from_files(self.routes_from_file, self.routes_to_file, is_address)
49
- if not self.routes:
50
- raise ValueError("routes is empty")
51
-
52
- # load private keys from file
53
- if self.private_keys_file:
54
- self.private_keys.update(AddressToPrivate.from_file(self.private_keys_file, get_public_key))
55
-
56
- # check all private keys exist
57
38
  if not self.private_keys.contains_all_addresses(self.from_addresses):
58
39
  raise ValueError("private keys are not set for all addresses")
59
40
 
60
- # fetch proxies from proxies_url
61
- proxies_url = self.proxies_url or os.getenv("MM_PROXIES_URL", "")
62
- if proxies_url:
63
- self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
64
-
65
- # value
66
- if not validators.is_valid_var_lamports(self.value, "balance"):
67
- raise ValueError(f"wrong value: {self.value}")
68
-
69
- # value_min_limit
70
- if not validators.is_valid_var_lamports(self.value_min_limit):
71
- raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
72
-
73
- # delay
74
- if not validators.is_valid_var_lamports(self.delay):
75
- raise ValueError(f"wrong delay: {self.delay}")
76
-
77
41
  return self
78
42
 
79
43
 
@@ -89,7 +53,7 @@ def run(
89
53
  config = Config.read_config_or_exit(config_path)
90
54
 
91
55
  if print_config:
92
- config.print_and_exit({"private_keys", "proxies"})
56
+ config.print_and_exit({"private_keys"})
93
57
 
94
58
  mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
95
59
 
@@ -122,8 +86,8 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
122
86
  log_prefix = f"{from_address}->{to_address}"
123
87
  fee = 5000
124
88
  # get value
125
- value_res = calcs.calc_sol_value(
126
- nodes=config.nodes, value_str=config.value, address=from_address, proxies=config.proxies, fee=fee
89
+ value_res = calcs.calc_sol_value_for_address(
90
+ nodes=config.nodes, value_expression=config.value, address=from_address, proxies=config.proxies, fee=fee
127
91
  )
128
92
  logger.debug(f"{log_prefix}value={value_res.ok_or_err()}")
129
93
  if isinstance(value_res, Err):
@@ -132,14 +96,11 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
132
96
  value = value_res.ok
133
97
 
134
98
  # value_min_limit
135
- if calcs.is_sol_value_less_min_limit(config.value_min_limit, value, log_prefix=log_prefix):
136
- return
137
-
138
- tx_params = {
139
- "fee": fee,
140
- "value": value,
141
- "to": to_address,
142
- }
99
+ if config.value_min_limit:
100
+ value_min_limit = calc_sol_expression(config.value_min_limit)
101
+ if value < value_min_limit:
102
+ logger.info(f"{log_prefix}: value<value_min_limit, value={lamports_to_sol(value, config.round_ndigits)}sol")
103
+ return
143
104
 
144
105
  # emulate?
145
106
  if emulate:
@@ -148,7 +109,8 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
148
109
  logger.info(msg)
149
110
  return
150
111
 
151
- logger.debug(f"{log_prefix}: tx_params={tx_params}")
112
+ debug_tx_params = {"fee": fee, "value": value, "to": to_address}
113
+ logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
152
114
 
153
115
  res = transfer.transfer_sol_with_retries(
154
116
  nodes=config.nodes,
@@ -171,26 +133,7 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
171
133
  else:
172
134
  logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
173
135
  status = "UNKNOWN"
174
- if _wait_confirmation(config, signature, log_prefix):
136
+ if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
175
137
  status = "OK"
176
138
  msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
177
139
  logger.info(msg)
178
-
179
-
180
- def _wait_confirmation(config: Config, signature: Signature, log_prefix: str) -> bool:
181
- count = 0
182
- while True:
183
- try:
184
- node = mm_crypto_utils.random_node(config.nodes)
185
- proxy = mm_crypto_utils.random_proxy(config.proxies)
186
- client = get_client(node, proxy=proxy)
187
- res = client.get_transaction(signature)
188
- if res.value and res.value.slot: # check for tx error
189
- return True
190
- except Exception as e:
191
- logger.error(f"{log_prefix}: can't get confirmation, error={e}")
192
- time.sleep(1)
193
- count += 1
194
- if count > 30:
195
- logger.error(f"{log_prefix}: can't get confirmation, timeout")
196
- return False
@@ -1,4 +1,3 @@
1
- import os
2
1
  import sys
3
2
  import time
4
3
  from pathlib import Path
@@ -9,29 +8,25 @@ import typer
9
8
  from loguru import logger
10
9
  from mm_crypto_utils import AddressToPrivate, TxRoute
11
10
  from mm_std import BaseConfig, Err, fatal, utc_now
12
- from pydantic import AfterValidator, BeforeValidator, Field, model_validator
11
+ from pydantic import AfterValidator, BeforeValidator, model_validator
13
12
 
14
- from mm_sol.account import get_public_key, is_address
13
+ from mm_sol import transfer
15
14
  from mm_sol.cli import calcs, cli_utils
16
15
  from mm_sol.cli.validators import Validators
16
+ from mm_sol.converters import to_token
17
17
  from mm_sol.token import get_decimals_with_retries
18
18
 
19
19
 
20
+ # noinspection DuplicatedCode
20
21
  class Config(BaseConfig):
21
22
  nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
22
- routes: Annotated[list[TxRoute], BeforeValidator(Validators.routes(is_address))]
23
- routes_from_file: Path | None = None
24
- routes_to_file: Path | None = None
25
- private_keys: Annotated[
26
- AddressToPrivate, Field(default_factory=AddressToPrivate), BeforeValidator(Validators.private_keys(get_public_key))
27
- ]
28
- private_keys_file: Path | None = None
29
- proxies_url: str | None = None
30
- proxies: list[str] = Field(default_factory=list)
31
- token: Annotated[str, AfterValidator(Validators.address(is_address))]
32
- value: str
33
- value_min_limit: str | None = None
34
- delay: str | None = None # in seconds
23
+ routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
24
+ private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
25
+ proxies: Annotated[list[str], BeforeValidator(Validators.proxies())]
26
+ token: Annotated[str, AfterValidator(Validators.sol_address())]
27
+ value: Annotated[str, AfterValidator(Validators.valid_token_expression("balance"))]
28
+ value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_token_expression())] = None
29
+ delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
35
30
  round_ndigits: int = 5
36
31
  log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
37
32
  log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
@@ -42,25 +37,9 @@ class Config(BaseConfig):
42
37
 
43
38
  @model_validator(mode="after")
44
39
  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_address)
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
40
  if not self.private_keys.contains_all_addresses(self.from_addresses):
57
41
  raise ValueError("private keys are not set for all addresses")
58
42
 
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
43
  return self
65
44
 
66
45
 
@@ -76,7 +55,7 @@ def run(
76
55
  config = Config.read_config_or_exit(config_path)
77
56
 
78
57
  if print_config:
79
- config.print_and_exit({"private_keys", "proxies"})
58
+ config.print_and_exit({"private_keys"})
80
59
 
81
60
  mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
82
61
 
@@ -118,9 +97,9 @@ def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirm
118
97
  fee = 5000
119
98
 
120
99
  # get value
121
- value_res = calcs.calc_token_value(
100
+ value_res = calcs.calc_token_value_for_address(
122
101
  nodes=config.nodes,
123
- value_str=config.value,
102
+ value_expression=config.value,
124
103
  wallet_address=route.from_address,
125
104
  proxies=config.proxies,
126
105
  token_mint_address=config.token,
@@ -131,5 +110,44 @@ def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirm
131
110
  logger.info(f"{log_prefix}: calc value error, {value_res.err}")
132
111
  return
133
112
  value = value_res.ok
113
+ value_t = f"{to_token(value, decimals=token_decimals, ndigits=config.round_ndigits)}t"
114
+
115
+ # value_min_limit
116
+ if config.value_min_limit:
117
+ value_min_limit = calcs.calc_token_expression(config.value_min_limit, token_decimals)
118
+ if value < value_min_limit:
119
+ logger.info(f"{log_prefix}: value<value_min_limit, value={value_t}")
120
+ return
121
+
122
+ if emulate:
123
+ logger.info(f"{log_prefix}: emulate, value={value_t}, fee={fee}lamports")
124
+ return
134
125
 
135
- logger.debug(f"{log_prefix}: value={value}, fee={fee}, no_confirmation={no_confirmation}, emulate={emulate}")
126
+ logger.debug(f"{log_prefix}: value={to_token(value, decimals=token_decimals)}t, fee={fee}lamports")
127
+ res = transfer.transfer_token_with_retries(
128
+ nodes=config.nodes,
129
+ token_mint_address=config.token,
130
+ from_address=route.from_address,
131
+ private_key=config.private_keys[route.from_address],
132
+ to_address=route.to_address,
133
+ amount=value,
134
+ decimals=token_decimals,
135
+ proxies=config.proxies,
136
+ retries=3,
137
+ )
138
+
139
+ if isinstance(res, Err):
140
+ logger.info(f"{log_prefix}: send_error: {res.err}")
141
+ return
142
+ signature = res.ok
143
+
144
+ if no_confirmation:
145
+ msg = f"{log_prefix}: sig={signature}, value={value_t}"
146
+ logger.info(msg)
147
+ else:
148
+ logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
149
+ status = "UNKNOWN"
150
+ if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
151
+ status = "OK"
152
+ msg = f"{log_prefix}: sig={signature}, value={value_t}, status={status}"
153
+ logger.info(msg)
@@ -0,0 +1,9 @@
1
+ routes: |
2
+ Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
+ Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
+ private_keys: "file: ./path/to/privates.txt"
5
+ value: 0.012 sol
6
+
7
+ proxies: "url: https://site.com/api/get-proxies"
8
+ nodes: |
9
+ https://api.devnet.solana.com
@@ -1,10 +1,10 @@
1
- tx_routes: |
1
+ routes: |
2
2
  Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
3
  Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
-
4
+ token: 6VBTMLgv256c7scudNf8T5GoTJcEE8WfgcJhxbGYPQ8G
5
5
 
6
6
  private_keys_file: ./path/to/privates.txt
7
- value: 0.012 sol
7
+ value: 0.012 t
8
8
 
9
9
  proxies_url: https://site.com/api/get-proxies
10
10
  nodes: |
@@ -0,0 +1,32 @@
1
+ from collections.abc import Callable
2
+
3
+ from mm_crypto_utils import AddressToPrivate, ConfigValidators, TxRoute
4
+
5
+ from mm_sol.account import get_public_key, is_address
6
+ from mm_sol.constants import SUFFIX_DECIMALS
7
+
8
+
9
+ class Validators(ConfigValidators):
10
+ @staticmethod
11
+ def sol_address() -> Callable[[str], str]:
12
+ return ConfigValidators.address(is_address)
13
+
14
+ @staticmethod
15
+ def sol_addresses(unique: bool) -> Callable[[str | list[str] | None], list[str]]:
16
+ return ConfigValidators.addresses(unique, is_address=is_address)
17
+
18
+ @staticmethod
19
+ def sol_routes() -> Callable[[str | None], list[TxRoute]]:
20
+ return ConfigValidators.routes(is_address)
21
+
22
+ @staticmethod
23
+ def sol_private_keys() -> Callable[[str | list[str] | None], AddressToPrivate]:
24
+ return ConfigValidators.private_keys(get_public_key)
25
+
26
+ @staticmethod
27
+ def valid_sol_expression(var_name: str | None = None) -> Callable[[str], str]:
28
+ return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS)
29
+
30
+ @staticmethod
31
+ def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
32
+ return ConfigValidators.valid_calc_int_expression(var_name, {"t": 6})
@@ -0,0 +1 @@
1
+ SUFFIX_DECIMALS = {"sol": 9}
@@ -5,6 +5,10 @@ def lamports_to_sol(lamports: int, ndigits: int = 4) -> Decimal:
5
5
  return Decimal(str(round(lamports / 10**9, ndigits=ndigits)))
6
6
 
7
7
 
8
+ def to_token(smallest_unit_value: int, decimals: int, ndigits: int = 4) -> Decimal:
9
+ return Decimal(str(round(smallest_unit_value / 10**decimals, ndigits=ndigits)))
10
+
11
+
8
12
  def sol_to_lamports(sol: Decimal) -> int:
9
13
  return int(sol * 10**9)
10
14
 
@@ -1,5 +1,3 @@
1
- from decimal import Decimal
2
-
3
1
  import mm_crypto_utils
4
2
  import pydash
5
3
  from mm_crypto_utils import Nodes, Proxies
@@ -14,7 +12,6 @@ from spl.token.client import Token
14
12
  from spl.token.constants import TOKEN_PROGRAM_ID
15
13
  from spl.token.instructions import get_associated_token_address
16
14
 
17
- import mm_sol.converters
18
15
  from mm_sol import rpc, utils
19
16
  from mm_sol.account import check_private_key, get_keypair
20
17
 
@@ -26,7 +23,7 @@ def transfer_token(
26
23
  from_address: str | Pubkey,
27
24
  private_key: str,
28
25
  to_address: str | Pubkey,
29
- amount: Decimal,
26
+ amount: int, # smallest unit
30
27
  decimals: int,
31
28
  proxy: str | None = None,
32
29
  timeout: float = 10,
@@ -60,7 +57,7 @@ def transfer_token(
60
57
  source=from_token_account,
61
58
  dest=recipient_token_account,
62
59
  owner=from_address,
63
- amount=mm_sol.converters.sol_to_lamports(amount),
60
+ amount=amount,
64
61
  decimals=decimals,
65
62
  )
66
63
  data.append(res)
@@ -75,7 +72,7 @@ def transfer_token_with_retries(
75
72
  from_address: str | Pubkey,
76
73
  private_key: str,
77
74
  to_address: str | Pubkey,
78
- amount: Decimal,
75
+ amount: int, # smallest unit
79
76
  decimals: int,
80
77
  proxies: Proxies = None,
81
78
  timeout: float = 10,
@@ -481,19 +481,19 @@ wheels = [
481
481
 
482
482
  [[package]]
483
483
  name = "mm-crypto-utils"
484
- version = "0.0.15"
484
+ version = "0.0.19"
485
485
  source = { registry = "https://pypi.org/simple" }
486
486
  dependencies = [
487
487
  { name = "loguru" },
488
488
  { name = "mm-std" },
489
489
  ]
490
490
  wheels = [
491
- { url = "https://files.pythonhosted.org/packages/22/50/1bbb9d11d0f799b71ae2a1a6f2d3724288e3d3698f0de5832f8ac86db482/mm_crypto_utils-0.0.15-py3-none-any.whl", hash = "sha256:950f7c11e43c17e9de8574def0e1b5d0a2d21db6027a77c8bbd91bded1be43c7", size = 6121 },
491
+ { url = "https://files.pythonhosted.org/packages/49/0b/408f661aba09bfc3625652a6cf2ba516874aaaa4f6c9eaaeca450cf70004/mm_crypto_utils-0.0.19-py3-none-any.whl", hash = "sha256:b51184a53aa78cb70253c3c2e0ad56a98a214a854ffb74a4130a4401cd025538", size = 6992 },
492
492
  ]
493
493
 
494
494
  [[package]]
495
495
  name = "mm-sol"
496
- version = "0.2.8"
496
+ version = "0.2.10"
497
497
  source = { editable = "." }
498
498
  dependencies = [
499
499
  { name = "base58" },
@@ -518,7 +518,7 @@ dev = [
518
518
  requires-dist = [
519
519
  { name = "base58", specifier = "~=2.1.1" },
520
520
  { name = "jinja2", specifier = ">=3.1.5" },
521
- { name = "mm-crypto-utils", specifier = ">=0.0.15" },
521
+ { name = "mm-crypto-utils", specifier = ">=0.0.19" },
522
522
  { name = "solana", specifier = "~=0.36.3" },
523
523
  { name = "typer", specifier = ">=0.15.1" },
524
524
  ]
@@ -1,101 +0,0 @@
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 Ok, Result
8
- from mm_std.str import split_on_plus_minus_tokens
9
-
10
- from mm_sol.balance import get_sol_balance_with_retries, get_token_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(*, nodes: Nodes, value_str: str, address: str, proxies: Proxies, fee: int = 5000) -> Result[int]:
73
- balance_value = None
74
- if "balance" in value_str.lower():
75
- res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
76
- if res.is_err():
77
- return res
78
- balance_value = res.ok
79
- value = calc_var_value(value_str, var_name="balance", var_value=balance_value)
80
- if "balance" in value_str.lower():
81
- value = value - fee
82
- return Ok(value)
83
-
84
-
85
- def calc_token_value(
86
- *, nodes: Nodes, value_str: str, wallet_address: str, token_mint_address: str, token_decimals: int, proxies: Proxies
87
- ) -> Result[int]:
88
- balance_value = None
89
- if "balance" in value_str.lower():
90
- res = get_token_balance_with_retries(
91
- nodes=nodes,
92
- owner_address=wallet_address,
93
- token_mint_address=token_mint_address,
94
- proxies=proxies,
95
- retries=5,
96
- )
97
- if res.is_err():
98
- return res
99
- balance_value = res.ok
100
- value = calc_var_value(value_str, var_name="balance", var_value=balance_value, decimals=token_decimals)
101
- return Ok(value)
@@ -1,17 +0,0 @@
1
- from mm_crypto_utils import ConfigValidators
2
-
3
- from mm_sol.cli import calcs
4
-
5
-
6
- def is_valid_var_lamports(value: str | None, base_name: str = "var", decimals: int | None = None) -> bool:
7
- if value is None:
8
- return True # check for None on BaseModel.field type level
9
- try:
10
- calcs.calc_var_value(value, var_value=123, var_name=base_name, decimals=decimals)
11
- return True # noqa: TRY300
12
- except ValueError:
13
- return False
14
-
15
-
16
- class Validators(ConfigValidators):
17
- pass
@@ -1,28 +0,0 @@
1
- import pytest
2
-
3
- from mm_sol.cli import calcs
4
- from mm_sol.converters import to_lamports
5
-
6
-
7
- def test_calc_var_value():
8
- assert calcs.calc_var_value("100") == 100
9
- assert calcs.calc_var_value("10 + 2 - 5") == 7
10
- assert calcs.calc_var_value("10 - random(2,2)") == 8
11
- assert calcs.calc_var_value("1.5base + 1", var_value=10, var_name="base") == 16
12
- assert calcs.calc_var_value("1.5estimate + 1", var_value=10, var_name="estimate") == 16
13
- assert calcs.calc_var_value("12.2 sol") == to_lamports("12.2sol")
14
- assert calcs.calc_var_value("12.2 t", decimals=6) == 12.2 * 10**6
15
-
16
- with pytest.raises(ValueError):
17
- calcs.calc_var_value("fff")
18
- with pytest.raises(ValueError):
19
- calcs.calc_var_value("12.3 sol + base", var_name="base")
20
- with pytest.raises(ValueError):
21
- calcs.calc_var_value("1.5estimate + 1", var_value=10)
22
- with pytest.raises(ValueError):
23
- calcs.calc_var_value("1.1t")
24
-
25
-
26
- # def test_calc_function_args():
27
- # res = calcs.calc_function_args('["xxx", random(100,200), 100, "aaa", random(1,3)]')
28
- # assert json.loads(res)[1] >= 100
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes