mm-sol 0.3.5__py3-none-any.whl → 0.4.0__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/cli.py CHANGED
@@ -8,12 +8,15 @@ from mm_std import print_plain
8
8
  from mm_sol.account import PHANTOM_DERIVATION_PATH
9
9
 
10
10
  from . import cli_utils
11
- from .cmd import balance_cmd, balances_cmd, example_cmd, node_cmd, transfer_sol_cmd, transfer_token_cmd
11
+ from .cmd import balance_cmd, balances_cmd, example_cmd, node_cmd, transfer_cmd
12
+ from .cmd.transfer_cmd import TransferCmdParams
12
13
  from .cmd.wallet import keypair_cmd, mnemonic_cmd
13
14
 
14
15
  app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
15
16
 
16
- wallet_app = typer.Typer(no_args_is_help=True, help="Wallet commands: generate new accounts, private to address")
17
+ wallet_app = typer.Typer(
18
+ no_args_is_help=True, help="Wallet-related commands: generate new accounts, derive addresses from private keys, and more"
19
+ )
17
20
  app.add_typer(wallet_app, name="wallet")
18
21
  app.add_typer(wallet_app, name="w", hidden=True)
19
22
 
@@ -31,11 +34,10 @@ def main(_version: bool = typer.Option(None, "--version", callback=version_callb
31
34
 
32
35
  class ConfigExample(str, Enum):
33
36
  balances = "balances"
34
- transfer_sol = "transfer-sol"
35
- transfer_token = "transfer-token" # noqa: S105 # nosec
37
+ transfer = "transfer"
36
38
 
37
39
 
38
- @app.command(name="example", help="Print an example of config for a command")
40
+ @app.command(name="example", help="Displays an example configuration for a command")
39
41
  def example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
40
42
  example_cmd.run(command.value)
41
43
 
@@ -51,52 +53,37 @@ def balance_command(
51
53
  balance_cmd.run(rpc_url, wallet_address, token_address, lamport, proxies_url)
52
54
 
53
55
 
54
- @app.command(name="balances", help="Print SOL and token balances for accounts")
56
+ @app.command(name="balances", help="Displays SOL and token balances for multiple accounts")
55
57
  def balances_command(
56
58
  config_path: Path, print_config: Annotated[bool, typer.Option("--config", "-c", help="Print config and exit")] = False
57
59
  ) -> None:
58
60
  balances_cmd.run(config_path, print_config)
59
61
 
60
62
 
61
- @app.command(name="transfer-sol", help="Transfer SOL from one or many accounts")
62
- def transfer_sol_command(
63
+ @app.command(name="transfer", help="Transfers SOL or SPL tokens, supporting multiple routes, delays, and expression-based values")
64
+ def transfer_command(
63
65
  config_path: Path,
64
66
  print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
65
67
  print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
68
+ config_verbose: bool = typer.Option(False, "--config-verbose", help="Print config in verbose mode and exit"),
66
69
  emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
67
70
  no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
68
71
  debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
69
72
  ) -> None:
70
- transfer_sol_cmd.run(
71
- config_path,
72
- print_balances=print_balances,
73
- print_config=print_config,
74
- debug=debug,
75
- no_confirmation=no_confirmation,
76
- emulate=emulate,
73
+ transfer_cmd.run(
74
+ TransferCmdParams(
75
+ config_path=config_path,
76
+ print_balances=print_balances,
77
+ debug=debug,
78
+ no_confirmation=no_confirmation,
79
+ emulate=emulate,
80
+ print_config_and_exit=print_config or config_verbose,
81
+ print_config_verbose=config_verbose,
82
+ )
77
83
  )
78
84
 
79
85
 
80
- @app.command(name="transfer-token", help="Transfer token from one or many accounts")
81
- def transfer_token_command(
82
- config_path: Path,
83
- print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
84
- print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
85
- emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
86
- no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
87
- debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
88
- ) -> None:
89
- transfer_token_cmd.run(
90
- config_path,
91
- print_balances=print_balances,
92
- print_config=print_config,
93
- debug=debug,
94
- no_confirmation=no_confirmation,
95
- emulate=emulate,
96
- )
97
-
98
-
99
- @app.command(name="node", help="Check RPC urls")
86
+ @app.command(name="node", help="Checks RPC URLs for availability and status")
100
87
  def node_command(
101
88
  urls: Annotated[list[str], typer.Argument()],
102
89
  proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
mm_sol/cli/cli_utils.py CHANGED
@@ -1,15 +1,14 @@
1
1
  import importlib.metadata
2
2
  import time
3
+ from pathlib import Path
3
4
 
4
5
  import mm_crypto_utils
5
6
  from loguru import logger
6
7
  from mm_crypto_utils import Nodes, Proxies
7
- from rich.live import Live
8
- from rich.table import Table
8
+ from mm_std import BaseConfig, print_json
9
+ from pydantic import BaseModel
9
10
  from solders.signature import Signature
10
11
 
11
- from mm_sol.balance import get_sol_balance_with_retries
12
- from mm_sol.converters import lamports_to_sol
13
12
  from mm_sol.utils import get_client
14
13
 
15
14
 
@@ -17,6 +16,21 @@ def get_version() -> str:
17
16
  return importlib.metadata.version("mm-sol")
18
17
 
19
18
 
19
+ class BaseConfigParams(BaseModel):
20
+ config_path: Path
21
+ print_config_and_exit: bool
22
+
23
+
24
+ def print_config(
25
+ config: BaseConfig, verbose: bool, exclude_fields: set[str] | None = None, count_fields: set[str] | None = None
26
+ ) -> None:
27
+ data = config.model_dump(exclude=exclude_fields)
28
+ if not verbose and count_fields:
29
+ for k in count_fields:
30
+ data[k] = len(data[k])
31
+ print_json(data)
32
+
33
+
20
34
  def public_rpc_url(url: str | None) -> str:
21
35
  if not url:
22
36
  return "https://api.mainnet-beta.solana.com"
@@ -32,27 +46,6 @@ def public_rpc_url(url: str | None) -> str:
32
46
  return url
33
47
 
34
48
 
35
- def print_balances(
36
- rpc_nodes: list[str],
37
- addresses: list[str],
38
- *,
39
- proxies: Proxies = None,
40
- round_ndigits: int = 5,
41
- ) -> None:
42
- table = Table(title="balances")
43
- table.add_column("n")
44
- table.add_column("address")
45
- table.add_column("balance, sol")
46
- with Live(table, refresh_per_second=0.5):
47
- for count, address in enumerate(addresses):
48
- balance = get_sol_balance_with_retries(rpc_nodes, address, proxies=proxies, retries=5).map_or_else(
49
- lambda err: err,
50
- lambda ok: str(lamports_to_sol(ok, round_ndigits)),
51
- )
52
- row: list[str] = [str(count), address, balance]
53
- table.add_row(*row)
54
-
55
-
56
49
  def wait_confirmation(nodes: Nodes, proxies: Proxies, signature: Signature, log_prefix: str) -> bool:
57
50
  count = 0
58
51
  while True:
@@ -0,0 +1,248 @@
1
+ import sys
2
+ import time
3
+ from pathlib import Path
4
+ from typing import Annotated, Self
5
+
6
+ import mm_crypto_utils
7
+ from loguru import logger
8
+ from mm_crypto_utils import AddressToPrivate, TxRoute
9
+ from mm_std import BaseConfig, Err, fatal, utc_now
10
+ from pydantic import AfterValidator, BeforeValidator, Field, model_validator
11
+ from rich.live import Live
12
+ from rich.table import Table
13
+ from solders.signature import Signature
14
+
15
+ from mm_sol import transfer
16
+ from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
17
+ from mm_sol.cli import calcs, cli_utils
18
+ from mm_sol.cli.cli_utils import BaseConfigParams
19
+ from mm_sol.cli.validators import Validators
20
+ from mm_sol.converters import lamports_to_sol, to_token
21
+ from mm_sol.token import get_decimals_with_retries
22
+
23
+
24
+ class Config(BaseConfig):
25
+ nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
26
+ routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
27
+ private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
28
+ proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
29
+ token: Annotated[str | None, AfterValidator(Validators.sol_address())] = None
30
+ token_decimals: int = -1
31
+ value: Annotated[str, AfterValidator(Validators.valid_sol_or_token_expression("balance"))]
32
+ value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_sol_or_token_expression())] = None
33
+ delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
34
+ round_ndigits: int = 5
35
+ log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
36
+ log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
37
+
38
+ @property
39
+ def from_addresses(self) -> list[str]:
40
+ return [r.from_address for r in self.routes]
41
+
42
+ @model_validator(mode="after")
43
+ def final_validator(self) -> Self:
44
+ if not self.private_keys.contains_all_addresses(self.from_addresses):
45
+ raise ValueError("private keys are not set for all addresses")
46
+
47
+ if self.token:
48
+ Validators.valid_token_expression("balance")(self.value)
49
+ if self.value_min_limit:
50
+ Validators.valid_token_expression()(self.value_min_limit)
51
+ else:
52
+ Validators.valid_sol_expression("balance")(self.value)
53
+ if self.value_min_limit:
54
+ Validators.valid_sol_expression()(self.value_min_limit)
55
+
56
+ if self.token:
57
+ res = get_decimals_with_retries(self.nodes, self.token, retries=3, proxies=self.proxies)
58
+ if isinstance(res, Err):
59
+ fatal(f"can't get decimals for token={self.token}, error={res.err}")
60
+ self.token_decimals = res.ok
61
+
62
+ return self
63
+
64
+
65
+ class TransferCmdParams(BaseConfigParams):
66
+ print_balances: bool
67
+ debug: bool
68
+ no_confirmation: bool
69
+ emulate: bool
70
+ print_config_verbose: bool
71
+
72
+
73
+ def run(cmd_params: TransferCmdParams) -> None:
74
+ config = Config.read_toml_config_or_exit(cmd_params.config_path)
75
+
76
+ if cmd_params.print_config_and_exit:
77
+ cli_utils.print_config(config, cmd_params.print_config_verbose, {"private_keys"}, {"proxies"})
78
+ sys.exit(0)
79
+
80
+ mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
81
+
82
+ if cmd_params.print_balances:
83
+ _print_balances(config)
84
+ sys.exit(0)
85
+
86
+ _run_transfers(config, cmd_params)
87
+
88
+
89
+ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
90
+ logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
91
+ logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
92
+ for i, route in enumerate(config.routes):
93
+ _transfer(route, config, cmd_params)
94
+ if not cmd_params.emulate and config.delay is not None and i < len(config.routes) - 1:
95
+ delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
96
+ logger.debug(f"delay {delay_value} seconds")
97
+ time.sleep(float(delay_value))
98
+ logger.info(f"transfer {cmd_params.config_path}: finished at {utc_now()} UTC")
99
+
100
+
101
+ def _calc_value(route: TxRoute, config: Config, transfer_sol_fee: int) -> int | None:
102
+ if config.token:
103
+ value_res = calcs.calc_token_value_for_address(
104
+ nodes=config.nodes,
105
+ value_expression=config.value,
106
+ wallet_address=route.from_address,
107
+ proxies=config.proxies,
108
+ token_mint_address=config.token,
109
+ token_decimals=config.token_decimals,
110
+ )
111
+ else:
112
+ value_res = calcs.calc_sol_value_for_address(
113
+ nodes=config.nodes,
114
+ value_expression=config.value,
115
+ address=route.from_address,
116
+ proxies=config.proxies,
117
+ fee=transfer_sol_fee,
118
+ )
119
+ logger.debug(f"{route.log_prefix}: value={value_res.ok_or_err()}")
120
+ if isinstance(value_res, Err):
121
+ logger.info(f"{route.log_prefix}: calc value error, {value_res.err}")
122
+
123
+ return value_res.ok
124
+
125
+
126
+ def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
127
+ """Returns False if the transfer should be skipped."""
128
+ if config.value_min_limit:
129
+ if config.token:
130
+ value_min_limit = calcs.calc_token_expression(config.value_min_limit, config.token_decimals)
131
+ else:
132
+ value_min_limit = calcs.calc_sol_expression(config.value_min_limit)
133
+ if value < value_min_limit:
134
+ logger.info(f"{route.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
135
+ return True
136
+
137
+
138
+ def _value_with_suffix(value: int, config: Config) -> str:
139
+ if config.token:
140
+ return f"{to_token(value, decimals=config.token_decimals, ndigits=config.round_ndigits)}t"
141
+ return f"{lamports_to_sol(value, config.round_ndigits)}sol"
142
+
143
+
144
+ def _send_tx(route: TxRoute, value: int, config: Config) -> Signature | None:
145
+ logger.debug(f"{route.log_prefix}: value={_value_with_suffix(value, config)}")
146
+ if config.token:
147
+ res = transfer.transfer_token_with_retries(
148
+ nodes=config.nodes,
149
+ token_mint_address=config.token,
150
+ from_address=route.from_address,
151
+ private_key=config.private_keys[route.from_address],
152
+ to_address=route.to_address,
153
+ amount=value,
154
+ decimals=config.token_decimals,
155
+ proxies=config.proxies,
156
+ retries=3,
157
+ )
158
+ else:
159
+ res = transfer.transfer_sol_with_retries(
160
+ nodes=config.nodes,
161
+ from_address=route.from_address,
162
+ private_key=config.private_keys[route.from_address],
163
+ to_address=route.to_address,
164
+ lamports=value,
165
+ proxies=config.proxies,
166
+ retries=3,
167
+ )
168
+
169
+ if isinstance(res, Err):
170
+ logger.info(f"{route.log_prefix}: tx error {res.err}")
171
+ return None
172
+ return res.ok
173
+
174
+
175
+ def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) -> None:
176
+ transfer_sol_fee = 5000
177
+
178
+ value = _calc_value(route, config, transfer_sol_fee)
179
+ if value is None:
180
+ return
181
+
182
+ if not _check_value_min_limit(route, value, config):
183
+ return
184
+
185
+ if cmd_params.emulate:
186
+ logger.info(f"{route.log_prefix}: emulate, value={_value_with_suffix(value, config)}")
187
+ return
188
+
189
+ signature = _send_tx(route, value, config)
190
+ if signature is None:
191
+ return
192
+
193
+ status = "UNKNOWN"
194
+ if not cmd_params.no_confirmation:
195
+ logger.debug(f"{route.log_prefix}: waiting for confirmation, sig={signature}")
196
+ if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, route.log_prefix):
197
+ status = "OK"
198
+
199
+ logger.info(f"{route.log_prefix}: sig={signature}, value={_value_with_suffix(value, config)}, status={status}")
200
+
201
+
202
+ def _print_balances(config: Config) -> None:
203
+ if config.token:
204
+ headers = ["n", "from_address", "sol", "t", "to_address", "sol", "t"]
205
+ else:
206
+ headers = ["n", "from_address", "sol", "to_address", "sol"]
207
+ table = Table(*headers, title="balances")
208
+ with Live(table, refresh_per_second=0.5):
209
+ for count, route in enumerate(config.routes):
210
+ from_sol_balance = _get_sol_balance_str(route.from_address, config)
211
+ to_sol_balance = _get_sol_balance_str(route.to_address, config)
212
+ from_t_balance = _get_token_balance_str(route.from_address, config) if config.token else ""
213
+ to_t_balance = _get_token_balance_str(route.to_address, config) if config.token else ""
214
+
215
+ if config.token:
216
+ table.add_row(
217
+ str(count),
218
+ route.from_address,
219
+ from_sol_balance,
220
+ from_t_balance,
221
+ route.to_address,
222
+ to_sol_balance,
223
+ to_t_balance,
224
+ )
225
+ else:
226
+ table.add_row(
227
+ str(count),
228
+ route.from_address,
229
+ from_sol_balance,
230
+ route.to_address,
231
+ to_sol_balance,
232
+ )
233
+
234
+
235
+ def _get_sol_balance_str(address: str, config: Config) -> str:
236
+ return get_sol_balance_with_retries(config.nodes, address, proxies=config.proxies, retries=5).map_or_else(
237
+ lambda err: err,
238
+ lambda ok: str(lamports_to_sol(ok, config.round_ndigits)),
239
+ )
240
+
241
+
242
+ def _get_token_balance_str(address: str, config: Config) -> str:
243
+ if not config.token:
244
+ raise ValueError("token is not set")
245
+ return get_token_balance_with_retries(config.nodes, address, config.token, proxies=config.proxies, retries=5).map_or_else(
246
+ lambda err: err,
247
+ lambda ok: str(to_token(ok, config.token_decimals, ndigits=config.round_ndigits)),
248
+ )
@@ -0,0 +1,23 @@
1
+ routes = """
2
+ Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj # comments are allowed
3
+ Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
+ file: /from/addresses.txt /to/addresses.txt
5
+ """
6
+ token = "6VBTMLgv256c7scudNf8T5GoTJcEE8WfgcJhxbGYPQ8G" # if token is not specified, then SOL is used
7
+ private_keys = """
8
+ 5VTfgpKKkckrsK33vcw6cEgv8SjLiwaorU8sd2ftjo2sx4tCV6N44dF4P9VigLaKNT2vpX3VuiFAiNpEBnMq3CiB
9
+ DE9poAKvs6tENFbADZ25W1zfKeiCbuDnFbafkBgo4rT28ZGkemqnF1zAqX9WGvBKUXSRVhXgX1RHe3qn11xfjR8
10
+ file: ./path/to/privates.txt
11
+ # Extra keys are not a problem, nor is their order.
12
+ """
13
+ value = "0.2balance + random(1t, 2.50t) - 100" # If transferring SOL, the suffix ‘sol’ is used. For any other tokens, the suffix ‘t’ is used.
14
+ delay = "random(10, 100)" # seconds, optional
15
+ proxies = """
16
+ http://usr:pass@123.123.123.123:1234
17
+ env_url: MM_SOL_PROXIES_URL
18
+ url: https://site.com/api/proxies
19
+ """
20
+ nodes = """
21
+ https://api.devnet.solana.com
22
+ http://localhost:8899
23
+ """
mm_sol/cli/validators.py CHANGED
@@ -30,3 +30,7 @@ class Validators(ConfigValidators):
30
30
  @staticmethod
31
31
  def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
32
32
  return ConfigValidators.valid_calc_int_expression(var_name, {"t": 6})
33
+
34
+ @staticmethod
35
+ def valid_sol_or_token_expression(var_name: str | None = None) -> Callable[[str], str]:
36
+ return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS | {"t": 6})
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-sol
3
- Version: 0.3.5
3
+ Version: 0.4.0
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.1.4
7
+ Requires-Dist: mm-crypto-utils>=0.1.5
8
8
  Requires-Dist: mnemonic==0.21
9
9
  Requires-Dist: solana~=0.36.3
10
10
  Requires-Dist: typer>=0.15.1
@@ -12,23 +12,21 @@ mm_sol/transfer.py,sha256=taf2NTLpo-bxISeaILARXEGLldUvqvP-agp5IDva7Hw,5825
12
12
  mm_sol/utils.py,sha256=NE0G564GiT9d7rW_lPPxUb1eq62WiXh28xtvtzNQIqw,556
13
13
  mm_sol/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  mm_sol/cli/calcs.py,sha256=-r9RlsQyOziTDf84uIsvTgZmsUdNrVeazu3vTj9hhNA,1887
15
- mm_sol/cli/cli.py,sha256=OlgBas-dWWZJ_WCbHJGLX6GxfILvIy7Q0VhgRo_nytE,4945
16
- mm_sol/cli/cli_utils.py,sha256=Skj0SCHPWMHGr2ag-em1JtIK9Qdh7xeJafMzvCgChnc,2254
17
- mm_sol/cli/validators.py,sha256=HLCFRrBOdyAMj_ibdAkkP36y9Zm2Dt3gMKoyPOcaiQE,1164
15
+ mm_sol/cli/cli.py,sha256=_6Fv3_RC20Yllee3NUXBO_DUfdaAD0vvL_jW_YVcrFo,4444
16
+ mm_sol/cli/cli_utils.py,sha256=rYcunR8w3uyFX44zs8GT4vNZbkrcMtQWgjRi7gkBCos,1912
17
+ mm_sol/cli/validators.py,sha256=q9Na86x4w21CX4_1DawKDHd8JF7HHtkAOZdsaDo9_zk,1371
18
18
  mm_sol/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  mm_sol/cli/cmd/balance_cmd.py,sha256=DfUZY3-Hr-F7Y0xp1faol0yLnPu6iDU3b839VhdwAbw,2587
20
20
  mm_sol/cli/cmd/balances_cmd.py,sha256=gMRZXAnCOPESsKRqz33ALPzSOqGGeZF225kMRO6sV-c,2876
21
21
  mm_sol/cli/cmd/example_cmd.py,sha256=M5dvRg9jvpVUwxvMRiRnO77aKR7c57bMY9booxMAswM,222
22
22
  mm_sol/cli/cmd/node_cmd.py,sha256=2AEAjq2M9f8-RZiI0rif6wITdns9QUb4Kr34QPsI2CA,238
23
- mm_sol/cli/cmd/transfer_sol_cmd.py,sha256=3kdukRKQYRW56jzFmNU30JJByq0igTMwbhipg2CEXZA,5349
24
- mm_sol/cli/cmd/transfer_token_cmd.py,sha256=BZYOBzROfHNn0jvFBUTgFcgJQyB5nR8iZ80ZohRwh2c,5947
23
+ mm_sol/cli/cmd/transfer_cmd.py,sha256=7LEQ2e-aTeawwTXkNwtsB044zHFsNEHXj3cCbp5rb-Q,9832
25
24
  mm_sol/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
25
  mm_sol/cli/cmd/wallet/keypair_cmd.py,sha256=cRHVVTs9zNYmUozZ8ZlJoutn9V6r8I1AEHBrszR7dTE,538
27
26
  mm_sol/cli/cmd/wallet/mnemonic_cmd.py,sha256=IiON_fJT5AFfIr_E1LR6_iDYZ3c_jWCFc-wSYqk61V8,648
28
27
  mm_sol/cli/examples/balances.toml,sha256=jyfUXusAWe6suuuwnPIfqWdU2kfT3hfMjkZ6uYMg8s0,347
29
- mm_sol/cli/examples/transfer-sol.toml,sha256=1LdkhSBC0y5oMJXjm8MVIMyZruIrYSIm3LBDGmcS5Iw,353
30
- mm_sol/cli/examples/transfer-token.toml,sha256=I_Wof-APv-h6xeYVq0zbWfLbpDny2kz9U0xJifVNEtU,401
31
- mm_sol-0.3.5.dist-info/METADATA,sha256=RYUzypxIkw8xXA9xtI0T38rFYq2SmmEtRSbDEe3ogB4,259
32
- mm_sol-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
- mm_sol-0.3.5.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
34
- mm_sol-0.3.5.dist-info/RECORD,,
28
+ mm_sol/cli/examples/transfer.toml,sha256=C8cHd0BMmD4KPqU4OWLrV8Deu7VVEOJ71H2YZxHhuLg,1027
29
+ mm_sol-0.4.0.dist-info/METADATA,sha256=t249_PGnuxYLN2_2GSoIrIQHSp-whVl_SMarQtufQ9Y,259
30
+ mm_sol-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ mm_sol-0.4.0.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
32
+ mm_sol-0.4.0.dist-info/RECORD,,
@@ -1,139 +0,0 @@
1
- import sys
2
- import time
3
- from pathlib import Path
4
- from typing import Annotated, Self
5
-
6
- import mm_crypto_utils
7
- from loguru import logger
8
- from mm_crypto_utils import AddressToPrivate, TxRoute
9
- from mm_std import BaseConfig, Err, utc_now
10
- from pydantic import AfterValidator, BeforeValidator, Field, model_validator
11
-
12
- from mm_sol import transfer
13
- from mm_sol.cli import calcs, cli_utils
14
- from mm_sol.cli.calcs import calc_sol_expression
15
- from mm_sol.cli.validators import Validators
16
- from mm_sol.converters import lamports_to_sol
17
-
18
-
19
- # noinspection DuplicatedCode
20
- class Config(BaseConfig):
21
- nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
22
- routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
23
- private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
24
- proxies: Annotated[list[str], Field(default_factory=list), 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
28
- round_ndigits: int = 5
29
- log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
30
- log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
31
-
32
- @property
33
- def from_addresses(self) -> list[str]:
34
- return [r.from_address for r in self.routes]
35
-
36
- @model_validator(mode="after")
37
- def final_validator(self) -> Self:
38
- if not self.private_keys.contains_all_addresses(self.from_addresses):
39
- raise ValueError("private keys are not set for all addresses")
40
-
41
- return self
42
-
43
-
44
- def run(
45
- config_path: Path,
46
- *,
47
- print_balances: bool,
48
- print_config: bool,
49
- debug: bool,
50
- no_confirmation: bool,
51
- emulate: bool,
52
- ) -> None:
53
- config = Config.read_toml_config_or_exit(config_path)
54
-
55
- if print_config:
56
- config.print_and_exit({"private_keys"})
57
-
58
- mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
59
-
60
- if print_balances:
61
- cli_utils.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits, proxies=config.proxies)
62
- sys.exit(0)
63
-
64
- _run_transfers(config, no_confirmation=no_confirmation, emulate=emulate)
65
-
66
-
67
- def _run_transfers(config: Config, *, no_confirmation: bool, emulate: bool) -> None:
68
- logger.info(f"started at {utc_now()} UTC")
69
- logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
70
- for i, route in enumerate(config.routes):
71
- _transfer(
72
- from_address=route.from_address,
73
- to_address=route.to_address,
74
- config=config,
75
- no_confirmation=no_confirmation,
76
- emulate=emulate,
77
- )
78
- if not emulate and config.delay is not None and i < len(config.routes) - 1:
79
- delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
80
- logger.debug(f"delay {delay_value} seconds")
81
- time.sleep(float(delay_value))
82
- logger.info(f"finished at {utc_now()} UTC")
83
-
84
-
85
- def _transfer(*, from_address: str, to_address: str, config: Config, no_confirmation: bool, emulate: bool) -> None:
86
- log_prefix = f"{from_address}->{to_address}"
87
- fee = 5000
88
- # get value
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
91
- )
92
- logger.debug(f"{log_prefix}value={value_res.ok_or_err()}")
93
- if isinstance(value_res, Err):
94
- logger.info(f"{log_prefix}calc value error, {value_res.err}")
95
- return
96
- value = value_res.ok
97
-
98
- # value_min_limit
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
104
-
105
- # emulate?
106
- if emulate:
107
- msg = f"{log_prefix}: emulate, value={lamports_to_sol(value, config.round_ndigits)}SOL,"
108
- msg += f" fee={fee}"
109
- logger.info(msg)
110
- return
111
-
112
- debug_tx_params = {"fee": fee, "value": value, "to": to_address}
113
- logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
114
-
115
- res = transfer.transfer_sol_with_retries(
116
- nodes=config.nodes,
117
- from_address=from_address,
118
- private_key=config.private_keys[from_address],
119
- to_address=to_address,
120
- lamports=value,
121
- proxies=config.proxies,
122
- retries=3,
123
- )
124
-
125
- if isinstance(res, Err):
126
- logger.info(f"{log_prefix}: send_error: {res.err}")
127
- return
128
- signature = res.ok
129
-
130
- if no_confirmation:
131
- msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}"
132
- logger.info(msg)
133
- else:
134
- logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
135
- status = "UNKNOWN"
136
- if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
137
- status = "OK"
138
- msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
139
- logger.info(msg)
@@ -1,153 +0,0 @@
1
- import sys
2
- import time
3
- from pathlib import Path
4
- from typing import Annotated, Self
5
-
6
- import mm_crypto_utils
7
- import typer
8
- from loguru import logger
9
- from mm_crypto_utils import AddressToPrivate, TxRoute
10
- from mm_std import BaseConfig, Err, fatal, utc_now
11
- from pydantic import AfterValidator, BeforeValidator, Field, model_validator
12
-
13
- from mm_sol import transfer
14
- from mm_sol.cli import calcs, cli_utils
15
- from mm_sol.cli.validators import Validators
16
- from mm_sol.converters import to_token
17
- from mm_sol.token import get_decimals_with_retries
18
-
19
-
20
- # noinspection DuplicatedCode
21
- class Config(BaseConfig):
22
- nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
23
- routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
24
- private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
25
- proxies: Annotated[list[str], Field(default_factory=list), 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
30
- round_ndigits: int = 5
31
- log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
32
- log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
33
-
34
- @property
35
- def from_addresses(self) -> list[str]:
36
- return [r.from_address for r in self.routes]
37
-
38
- @model_validator(mode="after")
39
- def final_validator(self) -> Self:
40
- if not self.private_keys.contains_all_addresses(self.from_addresses):
41
- raise ValueError("private keys are not set for all addresses")
42
-
43
- return self
44
-
45
-
46
- def run(
47
- config_path: Path,
48
- *,
49
- print_balances: bool,
50
- print_config: bool,
51
- debug: bool,
52
- no_confirmation: bool,
53
- emulate: bool,
54
- ) -> None:
55
- config = Config.read_toml_config_or_exit(config_path)
56
-
57
- if print_config:
58
- config.print_and_exit({"private_keys"})
59
-
60
- mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
61
-
62
- decimals_res = get_decimals_with_retries(config.nodes, config.token, retries=3, proxies=config.proxies)
63
- if isinstance(decimals_res, Err):
64
- fatal(f"can't get decimals for token={config.token}, error={decimals_res.err}")
65
-
66
- token_decimals = decimals_res.ok
67
- logger.debug(f"token decimals={token_decimals}")
68
-
69
- if print_balances:
70
- # cli_utils.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits, proxies=config.proxies) # noqa: E501
71
- typer.echo("Not implemented yet")
72
- sys.exit(0)
73
-
74
- _run_transfers(config, token_decimals, no_confirmation=no_confirmation, emulate=emulate)
75
-
76
-
77
- def _run_transfers(config: Config, token_decimals: int, *, no_confirmation: bool, emulate: bool) -> None:
78
- logger.info(f"started at {utc_now()} UTC")
79
- logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
80
- for i, route in enumerate(config.routes):
81
- _transfer(
82
- route=route,
83
- token_decimals=token_decimals,
84
- config=config,
85
- no_confirmation=no_confirmation,
86
- emulate=emulate,
87
- )
88
- if not emulate and config.delay is not None and i < len(config.routes) - 1:
89
- delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
90
- logger.debug(f"delay {delay_value} seconds")
91
- time.sleep(float(delay_value))
92
- logger.info(f"finished at {utc_now()} UTC")
93
-
94
-
95
- def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirmation: bool, emulate: bool) -> None:
96
- log_prefix = f"{route.from_address}->{route.to_address}"
97
- fee = 5000
98
-
99
- # get value
100
- value_res = calcs.calc_token_value_for_address(
101
- nodes=config.nodes,
102
- value_expression=config.value,
103
- wallet_address=route.from_address,
104
- proxies=config.proxies,
105
- token_mint_address=config.token,
106
- token_decimals=token_decimals,
107
- )
108
- logger.debug(f"{log_prefix}: value={value_res.ok_or_err()}")
109
- if isinstance(value_res, Err):
110
- logger.info(f"{log_prefix}: calc value error, {value_res.err}")
111
- return
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
125
-
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)
@@ -1,8 +0,0 @@
1
- routes = """
2
- Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
- Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
- """
5
- private_keys = "file: ./path/to/privates.txt"
6
- value = "0.012 sol"
7
- proxies = "url: https://site.com/api/get-proxies"
8
- nodes = "https://api.devnet.solana.com"
@@ -1,9 +0,0 @@
1
- routes = """
2
- Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
- Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
- """
5
- token = "6VBTMLgv256c7scudNf8T5GoTJcEE8WfgcJhxbGYPQ8G"
6
- private_keys = "file: ./path/to/privates.txt"
7
- value = "0.012 t"
8
- proxies = "https://site.com/api/get-proxies"
9
- nodes = "https://api.devnet.solana.com"
File without changes