mm-sol 0.2.8__py3-none-any.whl → 0.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mm_sol/cli/calcs.py CHANGED
@@ -1,92 +1,40 @@
1
- import random
2
- from decimal import Decimal
3
-
4
1
  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
2
+ from mm_crypto_utils import Nodes, Proxies, VarInt
3
+ from mm_std import Err, Ok, Result
9
4
 
10
5
  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
-
6
+ from mm_sol.constants import SUFFIX_DECIMALS
13
7
 
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
8
 
52
- if operator == "+":
53
- result += item_value
54
- if operator == "-":
55
- result -= item_value
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)
56
11
 
57
- return result # noqa: TRY300
58
- except Exception as err:
59
- raise ValueError(f"wrong value: {value}, error={err}") from err
60
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})
61
15
 
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
16
 
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():
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:
75
21
  res = get_sol_balance_with_retries(nodes, address, proxies=proxies, retries=5)
76
- if res.is_err():
22
+ if isinstance(res, Err):
77
23
  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():
24
+ var = VarInt("balance", res.ok)
25
+
26
+ value = calc_sol_expression(value_expression, var)
27
+ if "balance" in value_expression:
81
28
  value = value - fee
82
29
  return Ok(value)
83
30
 
84
31
 
85
- def calc_token_value(
86
- *, nodes: Nodes, value_str: str, wallet_address: str, token_mint_address: str, token_decimals: int, proxies: Proxies
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
87
34
  ) -> Result[int]:
88
- balance_value = None
89
- if "balance" in value_str.lower():
35
+ var = None
36
+ value_expression = value_expression.lower()
37
+ if "balance" in value_expression:
90
38
  res = get_token_balance_with_retries(
91
39
  nodes=nodes,
92
40
  owner_address=wallet_address,
@@ -94,8 +42,8 @@ def calc_token_value(
94
42
  proxies=proxies,
95
43
  retries=5,
96
44
  )
97
- if res.is_err():
45
+ if isinstance(res, Err):
98
46
  return res
99
- balance_value = res.ok
100
- value = calc_var_value(value_str, var_name="balance", var_value=balance_value, decimals=token_decimals)
47
+ var = VarInt("balance", res.ok)
48
+ value = calc_token_expression(value_expression, token_decimals, var)
101
49
  return Ok(value)
mm_sol/cli/cli.py CHANGED
@@ -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")
mm_sol/cli/cli_utils.py CHANGED
@@ -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,38 +1,30 @@
1
- import os
2
1
  import sys
3
2
  import time
4
3
  from pathlib import Path
5
- from typing import Annotated, Self
4
+ from typing import Annotated
6
5
 
7
6
  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
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
@@ -41,41 +33,6 @@ class Config(BaseConfig):
41
33
  def from_addresses(self) -> list[str]:
42
34
  return [r.from_address for r in self.routes]
43
35
 
44
- @model_validator(mode="after")
45
- 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
- if not self.private_keys.contains_all_addresses(self.from_addresses):
58
- raise ValueError("private keys are not set for all addresses")
59
-
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
- return self
78
-
79
36
 
80
37
  def run(
81
38
  config_path: str,
@@ -89,7 +46,7 @@ def run(
89
46
  config = Config.read_config_or_exit(config_path)
90
47
 
91
48
  if print_config:
92
- config.print_and_exit({"private_keys", "proxies"})
49
+ config.print_and_exit({"private_keys"})
93
50
 
94
51
  mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
95
52
 
@@ -122,8 +79,8 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
122
79
  log_prefix = f"{from_address}->{to_address}"
123
80
  fee = 5000
124
81
  # 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
82
+ value_res = calcs.calc_sol_value_for_address(
83
+ nodes=config.nodes, value_expression=config.value, address=from_address, proxies=config.proxies, fee=fee
127
84
  )
128
85
  logger.debug(f"{log_prefix}value={value_res.ok_or_err()}")
129
86
  if isinstance(value_res, Err):
@@ -132,14 +89,11 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
132
89
  value = value_res.ok
133
90
 
134
91
  # 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
- }
92
+ if config.value_min_limit:
93
+ value_min_limit = calc_sol_expression(config.value_min_limit)
94
+ if value < value_min_limit:
95
+ logger.info(f"{log_prefix}: value<value_min_limit, value={lamports_to_sol(value, config.round_ndigits)}sol")
96
+ return
143
97
 
144
98
  # emulate?
145
99
  if emulate:
@@ -148,7 +102,8 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
148
102
  logger.info(msg)
149
103
  return
150
104
 
151
- logger.debug(f"{log_prefix}: tx_params={tx_params}")
105
+ debug_tx_params = {"fee": fee, "value": value, "to": to_address}
106
+ logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
152
107
 
153
108
  res = transfer.transfer_sol_with_retries(
154
109
  nodes=config.nodes,
@@ -171,26 +126,7 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_confirma
171
126
  else:
172
127
  logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
173
128
  status = "UNKNOWN"
174
- if _wait_confirmation(config, signature, log_prefix):
129
+ if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
175
130
  status = "OK"
176
131
  msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
177
132
  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,37 +1,32 @@
1
- import os
2
1
  import sys
3
2
  import time
4
3
  from pathlib import Path
5
- from typing import Annotated, Self
4
+ from typing import Annotated
6
5
 
7
6
  import mm_crypto_utils
8
7
  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
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
@@ -40,29 +35,6 @@ class Config(BaseConfig):
40
35
  def from_addresses(self) -> list[str]:
41
36
  return [r.from_address for r in self.routes]
42
37
 
43
- @model_validator(mode="after")
44
- def final_validator(self) -> Self:
45
- # routes_files
46
- if self.routes_from_file and self.routes_to_file:
47
- self.routes += TxRoute.from_files(self.routes_from_file, self.routes_to_file, is_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
- if not self.private_keys.contains_all_addresses(self.from_addresses):
57
- raise ValueError("private keys are not set for all addresses")
58
-
59
- # fetch proxies from proxies_url
60
- proxies_url = self.proxies_url or os.getenv("MM_PROXIES_URL", "")
61
- if proxies_url:
62
- self.proxies += mm_crypto_utils.fetch_proxies_or_fatal(proxies_url)
63
-
64
- return self
65
-
66
38
 
67
39
  def run(
68
40
  config_path: str,
@@ -76,7 +48,7 @@ def run(
76
48
  config = Config.read_config_or_exit(config_path)
77
49
 
78
50
  if print_config:
79
- config.print_and_exit({"private_keys", "proxies"})
51
+ config.print_and_exit({"private_keys"})
80
52
 
81
53
  mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
82
54
 
@@ -118,9 +90,9 @@ def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirm
118
90
  fee = 5000
119
91
 
120
92
  # get value
121
- value_res = calcs.calc_token_value(
93
+ value_res = calcs.calc_token_value_for_address(
122
94
  nodes=config.nodes,
123
- value_str=config.value,
95
+ value_expression=config.value,
124
96
  wallet_address=route.from_address,
125
97
  proxies=config.proxies,
126
98
  token_mint_address=config.token,
@@ -131,5 +103,44 @@ def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirm
131
103
  logger.info(f"{log_prefix}: calc value error, {value_res.err}")
132
104
  return
133
105
  value = value_res.ok
106
+ value_t = f"{to_token(value, decimals=token_decimals, ndigits=config.round_ndigits)}t"
107
+
108
+ # value_min_limit
109
+ if config.value_min_limit:
110
+ value_min_limit = calcs.calc_token_expression(config.value_min_limit, token_decimals)
111
+ if value < value_min_limit:
112
+ logger.info(f"{log_prefix}: value<value_min_limit, value={value_t}")
113
+ return
114
+
115
+ if emulate:
116
+ logger.info(f"{log_prefix}: emulate, value={value_t}, fee={fee}lamports")
117
+ return
134
118
 
135
- logger.debug(f"{log_prefix}: value={value}, fee={fee}, no_confirmation={no_confirmation}, emulate={emulate}")
119
+ logger.debug(f"{log_prefix}: value={to_token(value, decimals=token_decimals)}t, fee={fee}lamports")
120
+ res = transfer.transfer_token_with_retries(
121
+ nodes=config.nodes,
122
+ token_mint_address=config.token,
123
+ from_address=route.from_address,
124
+ private_key=config.private_keys[route.from_address],
125
+ to_address=route.to_address,
126
+ amount=value,
127
+ decimals=token_decimals,
128
+ proxies=config.proxies,
129
+ retries=3,
130
+ )
131
+
132
+ if isinstance(res, Err):
133
+ logger.info(f"{log_prefix}: send_error: {res.err}")
134
+ return
135
+ signature = res.ok
136
+
137
+ if no_confirmation:
138
+ msg = f"{log_prefix}: sig={signature}, value={value_t}"
139
+ logger.info(msg)
140
+ else:
141
+ logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
142
+ status = "UNKNOWN"
143
+ if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
144
+ status = "OK"
145
+ msg = f"{log_prefix}: sig={signature}, value={value_t}, status={status}"
146
+ logger.info(msg)
@@ -1,11 +1,9 @@
1
- tx_routes: |
1
+ routes: |
2
2
  Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
3
  Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
-
5
-
6
- private_keys_file: ./path/to/privates.txt
4
+ private_keys: "file: ./path/to/privates.txt"
7
5
  value: 0.012 sol
8
6
 
9
- proxies_url: https://site.com/api/get-proxies
7
+ proxies: "url: https://site.com/api/get-proxies"
10
8
  nodes: |
11
9
  https://api.devnet.solana.com
@@ -0,0 +1,11 @@
1
+ routes: |
2
+ Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
+ Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
+ token: 6VBTMLgv256c7scudNf8T5GoTJcEE8WfgcJhxbGYPQ8G
5
+
6
+ private_keys_file: ./path/to/privates.txt
7
+ value: 0.012 t
8
+
9
+ proxies_url: https://site.com/api/get-proxies
10
+ nodes: |
11
+ https://api.devnet.solana.com
mm_sol/cli/validators.py CHANGED
@@ -1,17 +1,32 @@
1
- from mm_crypto_utils import ConfigValidators
1
+ from collections.abc import Callable
2
2
 
3
- from mm_sol.cli import calcs
3
+ from mm_crypto_utils import AddressToPrivate, ConfigValidators, TxRoute
4
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
5
+ from mm_sol.account import get_public_key, is_address
6
+ from mm_sol.constants import SUFFIX_DECIMALS
14
7
 
15
8
 
16
9
  class Validators(ConfigValidators):
17
- pass
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})
mm_sol/constants.py ADDED
@@ -0,0 +1 @@
1
+ SUFFIX_DECIMALS = {"sol": 9}
mm_sol/converters.py CHANGED
@@ -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
 
mm_sol/transfer.py CHANGED
@@ -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,
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-sol
3
- Version: 0.2.8
3
+ Version: 0.2.9
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
@@ -2,31 +2,33 @@ mm_sol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  mm_sol/account.py,sha256=32PR3kAoZJLEtKzwfWeQXnMxZeFO8NVXjG8HnHpnJgM,3225
3
3
  mm_sol/balance.py,sha256=Idx7h9yhRLbrIEDFCBI5QSHE7OT2pYWEZrbHj9XFrkM,3147
4
4
  mm_sol/block.py,sha256=4Lc4TANgpGvPflVumC9MR-3vIl1dedGyci3cgzczuds,1794
5
- mm_sol/converters.py,sha256=jY3wZeo1326z8M-AMS4OEzspRaGt4Tbk0y2SPI-oDFE,1153
5
+ mm_sol/constants.py,sha256=WSpfz5_cq_8XbIrNFJGu9okwbfPTL00zsyR_k9-7O0o,29
6
+ mm_sol/converters.py,sha256=CevrVjQHkAbtllrig8_-hU0go0YlOkWa8YebxUpqXJk,1323
6
7
  mm_sol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
8
  mm_sol/rpc.py,sha256=TspD_KZQp_KJDQzRknxaT8DR03okok26UWDQWF7Zflg,8031
8
9
  mm_sol/solana_cli.py,sha256=ig3OoTvmkrl7MFQSZjRHIraLSmtse0_9kn5Nsw8_zb0,8258
9
10
  mm_sol/token.py,sha256=O8z3UE3iZGYLWw8fnd9weYMcoQO0m88noqbRO_jntGg,1092
10
- mm_sol/transfer.py,sha256=-76gSHbQGOZeOgVgo10QWjAcv_9mnGrBNvpRCvXBlZQ,5872
11
+ mm_sol/transfer.py,sha256=Gu6UYEjY5OECSusKJO-VA8C3B7W6ZJGjegUv67EQdfs,5809
11
12
  mm_sol/utils.py,sha256=NE0G564GiT9d7rW_lPPxUb1eq62WiXh28xtvtzNQIqw,556
12
13
  mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- mm_sol/cli/calcs.py,sha256=jtFTODBHOkeSR5SARksQ7dDw2BATQYdhc8PtUskfEPk,4152
14
- mm_sol/cli/cli.py,sha256=SZwvxbiEXHgnhZ_Zg7GsgOm0Y2-WYHSQSuaO2xTaOnk,4496
15
- mm_sol/cli/cli_utils.py,sha256=EJTWdccXg8mvkK7E-Dt7zmULzc1WoqFlbu8ASGT2SVI,1381
16
- mm_sol/cli/validators.py,sha256=uKAZfKx4a11kLnxnD98duCMUEswj9UTJkWVVjlW0cDw,500
14
+ mm_sol/cli/calcs.py,sha256=-r9RlsQyOziTDf84uIsvTgZmsUdNrVeazu3vTj9hhNA,1887
15
+ mm_sol/cli/cli.py,sha256=p0urdRiho7Q8DgvUdH79wiujnnf3VIG24uKcFpmF-Yw,4556
16
+ mm_sol/cli/cli_utils.py,sha256=Skj0SCHPWMHGr2ag-em1JtIK9Qdh7xeJafMzvCgChnc,2254
17
+ mm_sol/cli/validators.py,sha256=_HEMOG7ojGKNp8AHX9tXpo9ZDCeRJ85coI-5MO3u95s,1209
17
18
  mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  mm_sol/cli/cmd/balance_cmd.py,sha256=DfUZY3-Hr-F7Y0xp1faol0yLnPu6iDU3b839VhdwAbw,2587
19
- mm_sol/cli/cmd/balances_cmd.py,sha256=yfiRMR3inxz0eXKR4rC9Ii8XsED_3mn46LX_C6Zf0MA,2624
20
+ mm_sol/cli/cmd/balances_cmd.py,sha256=P2WRCqJXKo36Pqfe-ZrpguggVmpXTMXr-hQeji6-Bb0,2184
20
21
  mm_sol/cli/cmd/example_cmd.py,sha256=bK_z4du0UPGAoiHnYdi6iaZim_kKlYw4NKBbzvyes28,221
21
22
  mm_sol/cli/cmd/node_cmd.py,sha256=2AEAjq2M9f8-RZiI0rif6wITdns9QUb4Kr34QPsI2CA,238
22
- mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=BmPCKr9D1rc7EQ9fuuShlrhiYYli1v1EHY3gkygrFJU,7083
23
- mm_sol/cli/cmd/transfer_token_cmd.py,sha256=bfDhy0s1i9qaXeGWLk1CnSnxS6wJKdDAM4f3f07RWJA,5148
23
+ mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=eRLKkr8ur3tNt_OG8pQapFqSLrzW7u9irKIpNbDWFuM,5035
24
+ mm_sol/cli/cmd/transfer_token_cmd.py,sha256=8d_cTsMP1NnwdCBgM-hVxDjwXsTe3CHgoL2JEuknvWQ,5633
24
25
  mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=cRHVVTs9zNYmUozZ8ZlJoutn9V6r8I1AEHBrszR7dTE,538
26
27
  mm_sol/cli/cmd/wallet/new_cmd.py,sha256=hcP9NZPkwYkTBEvT5rBntFtCIvx1QnPGn5FUDnvz5sM,425
27
28
  mm_sol/cli/examples/balances.yml,sha256=SoFcf_IhgA2zrbihrVpqm-ikes80wLFGVzafrjO00UY,290
28
- mm_sol/cli/examples/transfer-sol.yml,sha256=YFIM36NhaSEPeck6yQMvcgy3MWxgsfDqmMLRqz6P1dk,346
29
- mm_sol-0.2.8.dist-info/METADATA,sha256=ijsNaal8AmUCRafckp2F6XPP_D1Ejn9RU4pwdq839cA,230
30
- mm_sol-0.2.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- mm_sol-0.2.8.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
32
- mm_sol-0.2.8.dist-info/RECORD,,
29
+ mm_sol/cli/examples/transfer-sol.yml,sha256=d0kO2FHNEXMD6WSjOk6X7PtDuaGAAQWn098k8z5bgbs,347
30
+ mm_sol/cli/examples/transfer-token.yml,sha256=d8-3GE6NeM5zXo9S6WA8OP6pHjoEZvf7mtu9JwS5SZY,392
31
+ mm_sol-0.2.9.dist-info/METADATA,sha256=YqSKksvBVs1tF2FlFiGe8rrPjqR3rbaftUHr4NKEtlQ,230
32
+ mm_sol-0.2.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ mm_sol-0.2.9.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
34
+ mm_sol-0.2.9.dist-info/RECORD,,
File without changes