mm-sol 0.3.6__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {mm_sol-0.3.6 → mm_sol-0.5.0}/.gitignore +1 -0
  2. {mm_sol-0.3.6 → mm_sol-0.5.0}/PKG-INFO +3 -3
  3. {mm_sol-0.3.6 → mm_sol-0.5.0}/pyproject.toml +5 -6
  4. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cli.py +24 -35
  5. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cli_utils.py +16 -0
  6. mm_sol-0.5.0/src/mm_sol/cli/cmd/transfer_cmd.py +271 -0
  7. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/examples/balances.toml +2 -0
  8. mm_sol-0.5.0/src/mm_sol/cli/examples/transfer.toml +35 -0
  9. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/validators.py +7 -3
  10. {mm_sol-0.3.6 → mm_sol-0.5.0}/uv.lock +44 -66
  11. mm_sol-0.3.6/src/mm_sol/cli/cmd/transfer_sol_cmd.py +0 -159
  12. mm_sol-0.3.6/src/mm_sol/cli/cmd/transfer_token_cmd.py +0 -188
  13. mm_sol-0.3.6/src/mm_sol/cli/examples/transfer-sol.toml +0 -9
  14. mm_sol-0.3.6/src/mm_sol/cli/examples/transfer-token.toml +0 -9
  15. {mm_sol-0.3.6 → mm_sol-0.5.0}/.env.example +0 -0
  16. {mm_sol-0.3.6 → mm_sol-0.5.0}/README.md +0 -0
  17. {mm_sol-0.3.6 → mm_sol-0.5.0}/dict.dic +0 -0
  18. {mm_sol-0.3.6 → mm_sol-0.5.0}/justfile +0 -0
  19. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/__init__.py +0 -0
  20. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/account.py +0 -0
  21. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/balance.py +0 -0
  22. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/block.py +0 -0
  23. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/__init__.py +0 -0
  24. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/calcs.py +0 -0
  25. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/__init__.py +0 -0
  26. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/balance_cmd.py +0 -0
  27. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/balances_cmd.py +0 -0
  28. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/example_cmd.py +0 -0
  29. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/node_cmd.py +0 -0
  30. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/wallet/__init__.py +0 -0
  31. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/wallet/keypair_cmd.py +0 -0
  32. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/cli/cmd/wallet/mnemonic_cmd.py +0 -0
  33. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/constants.py +0 -0
  34. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/converters.py +0 -0
  35. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/py.typed +0 -0
  36. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/rpc.py +0 -0
  37. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/solana_cli.py +0 -0
  38. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/token.py +0 -0
  39. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/transfer.py +0 -0
  40. {mm_sol-0.3.6 → mm_sol-0.5.0}/src/mm_sol/utils.py +0 -0
  41. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/__init__.py +0 -0
  42. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/cli/__init__.py +0 -0
  43. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/cli/cmd/__init__.py +0 -0
  44. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/cli/cmd/wallet/__init__.py +0 -0
  45. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/cli/cmd/wallet/test_keypair_cmd.py +0 -0
  46. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/cli/cmd/wallet/test_mnemonic_cmd.py +0 -0
  47. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/conftest.py +0 -0
  48. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/test_account.py +0 -0
  49. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/test_balance.py +0 -0
  50. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/test_client.py +0 -0
  51. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/test_converters.py +0 -0
  52. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/test_rpc.py +0 -0
  53. {mm_sol-0.3.6 → mm_sol-0.5.0}/tests/test_token.py +0 -0
@@ -1,4 +1,5 @@
1
1
  .idea
2
+ .vscode
2
3
  .venv
3
4
  .env
4
5
  .coverage
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-sol
3
- Version: 0.3.6
3
+ Version: 0.5.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.2.3
8
8
  Requires-Dist: mnemonic==0.21
9
- Requires-Dist: solana~=0.36.3
9
+ Requires-Dist: solana~=0.36.5
10
10
  Requires-Dist: typer>=0.15.1
@@ -1,16 +1,15 @@
1
1
  [project]
2
2
  name = "mm-sol"
3
- version = "0.3.6"
3
+ version = "0.5.0"
4
4
  description = ""
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
7
- "mm-crypto-utils>=0.1.4",
8
- "solana~=0.36.3",
7
+ "mm-crypto-utils>=0.2.3",
8
+ "solana~=0.36.5",
9
9
  "base58~=2.1.1",
10
10
  "mnemonic==0.21",
11
11
  "typer>=0.15.1",
12
12
  "jinja2>=3.1.5",
13
-
14
13
  ]
15
14
  [project.scripts]
16
15
  mm-sol = "mm_sol.cli.cli:app"
@@ -23,8 +22,8 @@ build-backend = "hatchling.build"
23
22
  dev-dependencies = [
24
23
  "pytest~=8.3.4",
25
24
  "pytest-xdist~=3.6.1",
26
- "ruff~=0.9.4",
27
- "pip-audit~=2.7.3",
25
+ "ruff~=0.9.6",
26
+ "pip-audit~=2.8.0",
28
27
  "bandit~=1.8.2",
29
28
  "mypy~=1.15.0",
30
29
  ]
@@ -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,39 @@ 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"),
67
+ print_transfers: bool = typer.Option(False, "--transfers", "-t", help="Print transfers (from, to, value) and exit"),
65
68
  print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
69
+ config_verbose: bool = typer.Option(False, "--config-verbose", help="Print config in verbose mode and exit"),
66
70
  emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
67
71
  no_confirmation: bool = typer.Option(False, "--no-confirmation", "-nc", help="Do not wait for confirmation"),
68
72
  debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
69
73
  ) -> 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,
74
+ transfer_cmd.run(
75
+ TransferCmdParams(
76
+ config_path=config_path,
77
+ print_balances=print_balances,
78
+ print_transfers=print_transfers,
79
+ debug=debug,
80
+ no_confirmation=no_confirmation,
81
+ emulate=emulate,
82
+ print_config_and_exit=print_config or config_verbose,
83
+ print_config_verbose=config_verbose,
84
+ )
77
85
  )
78
86
 
79
87
 
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")
88
+ @app.command(name="node", help="Checks RPC URLs for availability and status")
100
89
  def node_command(
101
90
  urls: Annotated[list[str], typer.Argument()],
102
91
  proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
@@ -1,9 +1,12 @@
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
8
+ from mm_std import BaseConfig, print_json
9
+ from pydantic import BaseModel
7
10
  from solders.signature import Signature
8
11
 
9
12
  from mm_sol.utils import get_client
@@ -13,6 +16,19 @@ def get_version() -> str:
13
16
  return importlib.metadata.version("mm-sol")
14
17
 
15
18
 
19
+ class BaseConfigParams(BaseModel):
20
+ config_path: Path
21
+ print_config_and_exit: bool
22
+
23
+
24
+ def print_config(config: BaseConfig, exclude: set[str] | None = None, count: set[str] | None = None) -> None:
25
+ data = config.model_dump(exclude=exclude)
26
+ if count:
27
+ for k in count:
28
+ data[k] = len(data[k])
29
+ print_json(data)
30
+
31
+
16
32
  def public_rpc_url(url: str | None) -> str:
17
33
  if not url:
18
34
  return "https://api.mainnet-beta.solana.com"
@@ -0,0 +1,271 @@
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, Transfer
9
+ from mm_std import BaseConfig, Err, fatal, utc_now
10
+ from pydantic import AfterValidator, BeforeValidator, Field, model_validator
11
+ from rich.console import Console
12
+ from rich.live import Live
13
+ from rich.table import Table
14
+ from solders.signature import Signature
15
+
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
+ from mm_sol.transfer import transfer_sol_with_retries, transfer_token_with_retries
23
+
24
+
25
+ class Config(BaseConfig):
26
+ nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
27
+ transfers: Annotated[list[Transfer], BeforeValidator(Validators.sol_transfers())]
28
+ private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
29
+ proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
30
+ token: Annotated[str | None, AfterValidator(Validators.sol_address())] = None
31
+ token_decimals: int = -1
32
+ default_value: Annotated[str | None, AfterValidator(Validators.valid_sol_or_token_expression("balance"))] = None
33
+ value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_sol_or_token_expression())] = None
34
+ delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
35
+ round_ndigits: int = 5
36
+ log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
37
+ log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
38
+
39
+ @property
40
+ def from_addresses(self) -> list[str]:
41
+ return [r.from_address for r in self.transfers]
42
+
43
+ @model_validator(mode="after")
44
+ def final_validator(self) -> Self:
45
+ if not self.private_keys.contains_all_addresses(self.from_addresses):
46
+ raise ValueError("private keys are not set for all addresses")
47
+
48
+ for transfer in self.transfers: # If value is not set for a transfer, then set it to the global value of the config.
49
+ if not transfer.value and self.default_value:
50
+ transfer.value = self.default_value
51
+ for transfer in self.transfers: # Check all transfers have a value.
52
+ if not transfer.value:
53
+ raise ValueError(f"{transfer.log_prefix}: value is not set")
54
+
55
+ if self.token:
56
+ if self.default_value:
57
+ Validators.valid_token_expression("balance")(self.default_value)
58
+ if self.value_min_limit:
59
+ Validators.valid_token_expression()(self.value_min_limit)
60
+ else:
61
+ if self.default_value:
62
+ Validators.valid_sol_expression("balance")(self.default_value)
63
+ if self.value_min_limit:
64
+ Validators.valid_sol_expression()(self.value_min_limit)
65
+
66
+ if self.token:
67
+ res = get_decimals_with_retries(self.nodes, self.token, retries=3, proxies=self.proxies)
68
+ if isinstance(res, Err):
69
+ fatal(f"can't get decimals for token={self.token}, error={res.err}")
70
+ self.token_decimals = res.ok
71
+
72
+ return self
73
+
74
+
75
+ class TransferCmdParams(BaseConfigParams):
76
+ print_balances: bool
77
+ print_transfers: bool
78
+ debug: bool
79
+ no_confirmation: bool
80
+ emulate: bool
81
+ print_config_verbose: bool
82
+
83
+
84
+ def run(cmd_params: TransferCmdParams) -> None:
85
+ config = Config.read_toml_config_or_exit(cmd_params.config_path)
86
+
87
+ if cmd_params.print_config_and_exit:
88
+ cli_utils.print_config(config, exclude={"private_keys"}, count=None if cmd_params.debug else {"proxies"})
89
+ sys.exit(0)
90
+
91
+ if cmd_params.print_transfers:
92
+ _print_transfers(config)
93
+ sys.exit(0)
94
+
95
+ if cmd_params.print_balances:
96
+ _print_balances(config)
97
+ sys.exit(0)
98
+
99
+ _run_transfers(config, cmd_params)
100
+
101
+
102
+ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
103
+ mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
104
+ logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
105
+ logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
106
+ for i, route in enumerate(config.transfers):
107
+ _transfer(route, config, cmd_params)
108
+ if config.delay is not None and i < len(config.transfers) - 1:
109
+ delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
110
+ logger.info(f"delay {delay_value} seconds")
111
+ if not cmd_params.emulate:
112
+ time.sleep(float(delay_value))
113
+ logger.info(f"transfer {cmd_params.config_path}: finished at {utc_now()} UTC")
114
+
115
+
116
+ def _calc_value(transfer: Transfer, config: Config, transfer_sol_fee: int) -> int | None:
117
+ if config.token:
118
+ value_res = calcs.calc_token_value_for_address(
119
+ nodes=config.nodes,
120
+ value_expression=transfer.value,
121
+ wallet_address=transfer.from_address,
122
+ proxies=config.proxies,
123
+ token_mint_address=config.token,
124
+ token_decimals=config.token_decimals,
125
+ )
126
+ else:
127
+ value_res = calcs.calc_sol_value_for_address(
128
+ nodes=config.nodes,
129
+ value_expression=transfer.value,
130
+ address=transfer.from_address,
131
+ proxies=config.proxies,
132
+ fee=transfer_sol_fee,
133
+ )
134
+ logger.debug(f"{transfer.log_prefix}: value={value_res.ok_or_err()}")
135
+ if isinstance(value_res, Err):
136
+ logger.info(f"{transfer.log_prefix}: calc value error, {value_res.err}")
137
+
138
+ return value_res.ok
139
+
140
+
141
+ def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
142
+ """Returns False if the transfer should be skipped."""
143
+ if config.value_min_limit:
144
+ if config.token:
145
+ value_min_limit = calcs.calc_token_expression(config.value_min_limit, config.token_decimals)
146
+ else:
147
+ value_min_limit = calcs.calc_sol_expression(config.value_min_limit)
148
+ if value < value_min_limit:
149
+ logger.info(f"{transfer.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
150
+ return True
151
+
152
+
153
+ def _value_with_suffix(value: int, config: Config) -> str:
154
+ if config.token:
155
+ return f"{to_token(value, decimals=config.token_decimals, ndigits=config.round_ndigits)}t"
156
+ return f"{lamports_to_sol(value, config.round_ndigits)}sol"
157
+
158
+
159
+ def _send_tx(transfer: Transfer, value: int, config: Config) -> Signature | None:
160
+ logger.debug(f"{transfer.log_prefix}: value={_value_with_suffix(value, config)}")
161
+ if config.token:
162
+ res = transfer_token_with_retries(
163
+ nodes=config.nodes,
164
+ token_mint_address=config.token,
165
+ from_address=transfer.from_address,
166
+ private_key=config.private_keys[transfer.from_address],
167
+ to_address=transfer.to_address,
168
+ amount=value,
169
+ decimals=config.token_decimals,
170
+ proxies=config.proxies,
171
+ retries=3,
172
+ )
173
+ else:
174
+ res = transfer_sol_with_retries(
175
+ nodes=config.nodes,
176
+ from_address=transfer.from_address,
177
+ private_key=config.private_keys[transfer.from_address],
178
+ to_address=transfer.to_address,
179
+ lamports=value,
180
+ proxies=config.proxies,
181
+ retries=3,
182
+ )
183
+
184
+ if isinstance(res, Err):
185
+ logger.info(f"{transfer.log_prefix}: tx error {res.err}")
186
+ return None
187
+ return res.ok
188
+
189
+
190
+ def _transfer(transfer: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
191
+ transfer_sol_fee = 5000
192
+
193
+ value = _calc_value(transfer, config, transfer_sol_fee)
194
+ if value is None:
195
+ return
196
+
197
+ if not _check_value_min_limit(transfer, value, config):
198
+ return
199
+
200
+ if cmd_params.emulate:
201
+ logger.info(f"{transfer.log_prefix}: emulate, value={_value_with_suffix(value, config)}")
202
+ return
203
+
204
+ signature = _send_tx(transfer, value, config)
205
+ if signature is None:
206
+ return
207
+
208
+ status = "UNKNOWN"
209
+ if not cmd_params.no_confirmation:
210
+ logger.debug(f"{transfer.log_prefix}: waiting for confirmation, sig={signature}")
211
+ if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, transfer.log_prefix):
212
+ status = "OK"
213
+
214
+ logger.info(f"{transfer.log_prefix}: sig={signature}, value={_value_with_suffix(value, config)}, status={status}")
215
+
216
+
217
+ def _print_transfers(config: Config) -> None:
218
+ table = Table("n", "from_address", "to_address", "value", title="transfers")
219
+ for count, transfer in enumerate(config.transfers, start=1):
220
+ table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
221
+ console = Console()
222
+ console.print(table)
223
+
224
+
225
+ def _print_balances(config: Config) -> None:
226
+ if config.token:
227
+ headers = ["n", "from_address", "sol", "t", "to_address", "sol", "t"]
228
+ else:
229
+ headers = ["n", "from_address", "sol", "to_address", "sol"]
230
+ table = Table(*headers, title="balances")
231
+ with Live(table, refresh_per_second=0.5):
232
+ for count, route in enumerate(config.transfers):
233
+ from_sol_balance = _get_sol_balance_str(route.from_address, config)
234
+ to_sol_balance = _get_sol_balance_str(route.to_address, config)
235
+ from_t_balance = _get_token_balance_str(route.from_address, config) if config.token else ""
236
+ to_t_balance = _get_token_balance_str(route.to_address, config) if config.token else ""
237
+
238
+ if config.token:
239
+ table.add_row(
240
+ str(count),
241
+ route.from_address,
242
+ from_sol_balance,
243
+ from_t_balance,
244
+ route.to_address,
245
+ to_sol_balance,
246
+ to_t_balance,
247
+ )
248
+ else:
249
+ table.add_row(
250
+ str(count),
251
+ route.from_address,
252
+ from_sol_balance,
253
+ route.to_address,
254
+ to_sol_balance,
255
+ )
256
+
257
+
258
+ def _get_sol_balance_str(address: str, config: Config) -> str:
259
+ return get_sol_balance_with_retries(config.nodes, address, proxies=config.proxies, retries=5).map_or_else(
260
+ lambda err: err,
261
+ lambda ok: str(lamports_to_sol(ok, config.round_ndigits)),
262
+ )
263
+
264
+
265
+ def _get_token_balance_str(address: str, config: Config) -> str:
266
+ if not config.token:
267
+ raise ValueError("token is not set")
268
+ return get_token_balance_with_retries(config.nodes, address, config.token, proxies=config.proxies, retries=5).map_or_else(
269
+ lambda err: err,
270
+ lambda ok: str(to_token(ok, config.token_decimals, ndigits=config.round_ndigits)),
271
+ )
@@ -2,9 +2,11 @@ accounts = """
2
2
  9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM # binance
3
3
  61aq585V8cR2sZBeawJFt2NPqmN7zDi1sws4KLs5xHXV # Jupiter cold wallet
4
4
  """
5
+
5
6
  tokens = """
6
7
  EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v # USDC
7
8
  JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN # JUP
8
9
  """
10
+
9
11
  nodes = "https://api.mainnet-beta.solana.com"
10
12
  # proxies = "env_url: MM_SOL_PROXIES_URL"
@@ -0,0 +1,35 @@
1
+ # Each line is a transfer instruction, with format: from_address to_address [value]
2
+ # Value is optional. If value is not set, default_value will be used
3
+ # value is an expression that can contain variable 'balance' and 'random' function
4
+ transfers = """
5
+ # for sol transfer use 'sol' suffix, example: 1.2sol. For other tokens use 't' suffix, example: 1.2t
6
+ Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj 1.2t
7
+ Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur 0.6balance-random(1t,2.50t)-100
8
+ 6TW4WswDgGmEB5HELBtEdwC1tQJq9Xa1msDjPw94sgai GGb7gb5EzW8GZZWX552eiC9r1SY4Pqtgbqf9UMrBrEzy # default_value will be used
9
+ file: /path/to/other/transfers.txt # transfers from this file will be added
10
+ """
11
+
12
+ token = "6VBTMLgv256c7scudNf8T5GoTJcEE8WfgcJhxbGYPQ8G" # if token is not specified, then SOL is used
13
+
14
+ private_keys = """
15
+ 5VTfgpKKkckrsK33vcw6cEgv8SjLiwaorU8sd2ftjo2sx4tCV6N44dF4P9VigLaKNT2vpX3VuiFAiNpEBnMq3CiB
16
+ DE9poAKvs6tENFbADZ25W1zfKeiCbuDnFbafkBgo4rT28ZGkemqnF1zAqX9WGvBKUXSRVhXgX1RHe3qn11xfjR8
17
+ file: ./path/to/privates.txt
18
+ # Extra keys are not a problem, nor is their order.
19
+ """
20
+
21
+ # default_value is used if transfer.value is not set in transfers. It's optional.
22
+ default_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.
23
+
24
+ delay = "random(10, 100)" # seconds, optional
25
+
26
+ proxies = """
27
+ http://usr:pass@123.123.123.123:1234
28
+ env_url: MM_SOL_PROXIES_URL
29
+ url: https://site.com/api/proxies
30
+ """
31
+
32
+ nodes = """
33
+ https://api.devnet.solana.com
34
+ http://localhost:8899
35
+ """
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
2
 
3
- from mm_crypto_utils import AddressToPrivate, ConfigValidators, TxRoute
3
+ from mm_crypto_utils import AddressToPrivate, ConfigValidators, Transfer
4
4
 
5
5
  from mm_sol.account import get_public_key, is_address
6
6
  from mm_sol.constants import SUFFIX_DECIMALS
@@ -16,8 +16,8 @@ class Validators(ConfigValidators):
16
16
  return ConfigValidators.addresses(unique, is_address=is_address)
17
17
 
18
18
  @staticmethod
19
- def sol_routes() -> Callable[[str], list[TxRoute]]:
20
- return ConfigValidators.routes(is_address)
19
+ def sol_transfers() -> Callable[[str], list[Transfer]]:
20
+ return ConfigValidators.transfers(is_address)
21
21
 
22
22
  @staticmethod
23
23
  def sol_private_keys() -> Callable[[str], AddressToPrivate]:
@@ -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})
@@ -298,19 +298,6 @@ wheels = [
298
298
  { url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 },
299
299
  ]
300
300
 
301
- [[package]]
302
- name = "html5lib"
303
- version = "1.1"
304
- source = { registry = "https://pypi.org/simple" }
305
- dependencies = [
306
- { name = "six" },
307
- { name = "webencodings" },
308
- ]
309
- sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 }
310
- wheels = [
311
- { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 },
312
- ]
313
-
314
301
  [[package]]
315
302
  name = "httpcore"
316
303
  version = "1.0.7"
@@ -481,19 +468,19 @@ wheels = [
481
468
 
482
469
  [[package]]
483
470
  name = "mm-crypto-utils"
484
- version = "0.1.4"
471
+ version = "0.2.3"
485
472
  source = { registry = "https://pypi.org/simple" }
486
473
  dependencies = [
487
474
  { name = "loguru" },
488
475
  { name = "mm-std" },
489
476
  ]
490
477
  wheels = [
491
- { url = "https://files.pythonhosted.org/packages/b8/29/fb0a382faadf0ba377747a551e9136c0ad7f64286163de5d0e1e64ae3456/mm_crypto_utils-0.1.4-py3-none-any.whl", hash = "sha256:3b2e011959fa70f3498ea27b0e741d5e3915be771ec1de63729d8bd97b4d0ff4", size = 7733 },
478
+ { url = "https://files.pythonhosted.org/packages/78/dd/3cb0ddc4ab14a78a0e828fd1f7ca97298be8eff67b53b6198853e2013976/mm_crypto_utils-0.2.3-py3-none-any.whl", hash = "sha256:33ca42ef22c847c6f92973d7c799460926ee5c5bd7c872d3c7d19f31377e8833", size = 8257 },
492
479
  ]
493
480
 
494
481
  [[package]]
495
482
  name = "mm-sol"
496
- version = "0.3.6"
483
+ version = "0.5.0"
497
484
  source = { editable = "." }
498
485
  dependencies = [
499
486
  { name = "base58" },
@@ -518,9 +505,9 @@ dev = [
518
505
  requires-dist = [
519
506
  { name = "base58", specifier = "~=2.1.1" },
520
507
  { name = "jinja2", specifier = ">=3.1.5" },
521
- { name = "mm-crypto-utils", specifier = ">=0.1.4" },
508
+ { name = "mm-crypto-utils", specifier = ">=0.2.3" },
522
509
  { name = "mnemonic", specifier = "==0.21" },
523
- { name = "solana", specifier = "~=0.36.3" },
510
+ { name = "solana", specifier = "~=0.36.5" },
524
511
  { name = "typer", specifier = ">=0.15.1" },
525
512
  ]
526
513
 
@@ -528,10 +515,10 @@ requires-dist = [
528
515
  dev = [
529
516
  { name = "bandit", specifier = "~=1.8.2" },
530
517
  { name = "mypy", specifier = "~=1.15.0" },
531
- { name = "pip-audit", specifier = "~=2.7.3" },
518
+ { name = "pip-audit", specifier = "~=2.8.0" },
532
519
  { name = "pytest", specifier = "~=8.3.4" },
533
520
  { name = "pytest-xdist", specifier = "~=3.6.1" },
534
- { name = "ruff", specifier = "~=0.9.4" },
521
+ { name = "ruff", specifier = "~=0.9.6" },
535
522
  ]
536
523
 
537
524
  [[package]]
@@ -674,22 +661,22 @@ wheels = [
674
661
 
675
662
  [[package]]
676
663
  name = "pip-audit"
677
- version = "2.7.3"
664
+ version = "2.8.0"
678
665
  source = { registry = "https://pypi.org/simple" }
679
666
  dependencies = [
680
667
  { name = "cachecontrol", extra = ["filecache"] },
681
668
  { name = "cyclonedx-python-lib" },
682
- { name = "html5lib" },
683
669
  { name = "packaging" },
684
670
  { name = "pip-api" },
685
671
  { name = "pip-requirements-parser" },
672
+ { name = "platformdirs" },
686
673
  { name = "requests" },
687
674
  { name = "rich" },
688
675
  { name = "toml" },
689
676
  ]
690
- sdist = { url = "https://files.pythonhosted.org/packages/46/2f/d030d0d3a50b776f910dd87dc1d57dd4a27bfad176b85882f463632e4747/pip_audit-2.7.3.tar.gz", hash = "sha256:08891bbf179bffe478521f150818112bae998424f58bf9285c0078965aef38bc", size = 50365 }
677
+ sdist = { url = "https://files.pythonhosted.org/packages/e8/c8/44ccea85bd2024f1ebe55eb6cdaf1f2183359176689eed3c0b01926c24ad/pip_audit-2.8.0.tar.gz", hash = "sha256:9816cbd94de6f618a8965c117433006b3d565a708dc05d5a7be47ab65b66fa05", size = 51073 }
691
678
  wheels = [
692
- { url = "https://files.pythonhosted.org/packages/4d/4a/c908ec8a527698a6539b431d70454e18aef04d4190ff48107ed4d3df99ff/pip_audit-2.7.3-py3-none-any.whl", hash = "sha256:46a11faee3323f76adf7987de8171daeb660e8f57d8088cc27fb1c1e5c7747b0", size = 56266 },
679
+ { url = "https://files.pythonhosted.org/packages/11/0c/be5c42643284b2cfc5d9d36b576b7465268a163bd7df481a3979a3d87a0b/pip_audit-2.8.0-py3-none-any.whl", hash = "sha256:200f50d56cb6fba3a9189c20d53250354f72f161d63b6ef77ae5de2b53044566", size = 57002 },
693
680
  ]
694
681
 
695
682
  [[package]]
@@ -705,6 +692,15 @@ wheels = [
705
692
  { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648 },
706
693
  ]
707
694
 
695
+ [[package]]
696
+ name = "platformdirs"
697
+ version = "4.3.6"
698
+ source = { registry = "https://pypi.org/simple" }
699
+ sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
700
+ wheels = [
701
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
702
+ ]
703
+
708
704
  [[package]]
709
705
  name = "pluggy"
710
706
  version = "1.5.0"
@@ -911,27 +907,27 @@ wheels = [
911
907
 
912
908
  [[package]]
913
909
  name = "ruff"
914
- version = "0.9.4"
915
- source = { registry = "https://pypi.org/simple" }
916
- sdist = { url = "https://files.pythonhosted.org/packages/c0/17/529e78f49fc6f8076f50d985edd9a2cf011d1dbadb1cdeacc1d12afc1d26/ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7", size = 3599458 }
917
- wheels = [
918
- { url = "https://files.pythonhosted.org/packages/b6/f8/3fafb7804d82e0699a122101b5bee5f0d6e17c3a806dcbc527bb7d3f5b7a/ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706", size = 11668400 },
919
- { url = "https://files.pythonhosted.org/packages/2e/a6/2efa772d335da48a70ab2c6bb41a096c8517ca43c086ea672d51079e3d1f/ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf", size = 11628395 },
920
- { url = "https://files.pythonhosted.org/packages/dc/d7/cd822437561082f1c9d7225cc0d0fbb4bad117ad7ac3c41cd5d7f0fa948c/ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b", size = 11090052 },
921
- { url = "https://files.pythonhosted.org/packages/9e/67/3660d58e893d470abb9a13f679223368ff1684a4ef40f254a0157f51b448/ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137", size = 11882221 },
922
- { url = "https://files.pythonhosted.org/packages/79/d1/757559995c8ba5f14dfec4459ef2dd3fcea82ac43bc4e7c7bf47484180c0/ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e", size = 11424862 },
923
- { url = "https://files.pythonhosted.org/packages/c0/96/7915a7c6877bb734caa6a2af424045baf6419f685632469643dbd8eb2958/ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec", size = 12626735 },
924
- { url = "https://files.pythonhosted.org/packages/0e/cc/dadb9b35473d7cb17c7ffe4737b4377aeec519a446ee8514123ff4a26091/ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b", size = 13255976 },
925
- { url = "https://files.pythonhosted.org/packages/5f/c3/ad2dd59d3cabbc12df308cced780f9c14367f0321e7800ca0fe52849da4c/ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a", size = 12752262 },
926
- { url = "https://files.pythonhosted.org/packages/c7/17/5f1971e54bd71604da6788efd84d66d789362b1105e17e5ccc53bba0289b/ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214", size = 14401648 },
927
- { url = "https://files.pythonhosted.org/packages/30/24/6200b13ea611b83260501b6955b764bb320e23b2b75884c60ee7d3f0b68e/ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231", size = 12414702 },
928
- { url = "https://files.pythonhosted.org/packages/34/cb/f5d50d0c4ecdcc7670e348bd0b11878154bc4617f3fdd1e8ad5297c0d0ba/ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b", size = 11859608 },
929
- { url = "https://files.pythonhosted.org/packages/d6/f4/9c8499ae8426da48363bbb78d081b817b0f64a9305f9b7f87eab2a8fb2c1/ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6", size = 11485702 },
930
- { url = "https://files.pythonhosted.org/packages/18/59/30490e483e804ccaa8147dd78c52e44ff96e1c30b5a95d69a63163cdb15b/ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c", size = 12067782 },
931
- { url = "https://files.pythonhosted.org/packages/3d/8c/893fa9551760b2f8eb2a351b603e96f15af167ceaf27e27ad873570bc04c/ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0", size = 12483087 },
932
- { url = "https://files.pythonhosted.org/packages/23/15/f6751c07c21ca10e3f4a51ea495ca975ad936d780c347d9808bcedbd7182/ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402", size = 9852302 },
933
- { url = "https://files.pythonhosted.org/packages/12/41/2d2d2c6a72e62566f730e49254f602dfed23019c33b5b21ea8f8917315a1/ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e", size = 10850051 },
934
- { url = "https://files.pythonhosted.org/packages/c6/e6/3d6ec3bc3d254e7f005c543a661a41c3e788976d0e52a1ada195bd664344/ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41", size = 10078251 },
910
+ version = "0.9.6"
911
+ source = { registry = "https://pypi.org/simple" }
912
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
913
+ wheels = [
914
+ { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
915
+ { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
916
+ { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
917
+ { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
918
+ { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
919
+ { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
920
+ { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
921
+ { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
922
+ { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
923
+ { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
924
+ { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
925
+ { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
926
+ { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
927
+ { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
928
+ { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
929
+ { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
930
+ { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
935
931
  ]
936
932
 
937
933
  [[package]]
@@ -943,15 +939,6 @@ wheels = [
943
939
  { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
944
940
  ]
945
941
 
946
- [[package]]
947
- name = "six"
948
- version = "1.16.0"
949
- source = { registry = "https://pypi.org/simple" }
950
- sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
951
- wheels = [
952
- { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
953
- ]
954
-
955
942
  [[package]]
956
943
  name = "sniffio"
957
944
  version = "1.3.1"
@@ -972,7 +959,7 @@ wheels = [
972
959
 
973
960
  [[package]]
974
961
  name = "solana"
975
- version = "0.36.3"
962
+ version = "0.36.5"
976
963
  source = { registry = "https://pypi.org/simple" }
977
964
  dependencies = [
978
965
  { name = "construct-typing" },
@@ -981,9 +968,9 @@ dependencies = [
981
968
  { name = "typing-extensions" },
982
969
  { name = "websockets" },
983
970
  ]
984
- sdist = { url = "https://files.pythonhosted.org/packages/34/d6/5c8b044ab5d4566a74bd2880785ec0d8e5b5e0c5f90dc2a5245b114788c9/solana-0.36.3.tar.gz", hash = "sha256:b0d46ea1e196425945d78e50f541fd67fe3c116f411b950bbc8f33dd5aef5f19", size = 52018 }
971
+ sdist = { url = "https://files.pythonhosted.org/packages/c8/e7/18ffc3d25e8f772f6b20d8dfd27a0d275576403e4d52def0a94021741506/solana-0.36.5.tar.gz", hash = "sha256:8c81de111903d01b3b5659130840ee1711092ab02982cf5e1ea851ed9ddcb011", size = 52108 }
985
972
  wheels = [
986
- { url = "https://files.pythonhosted.org/packages/a9/38/9495868167e8101e82ff2ec5b9db4e48561be9605b8f7c6bad9c84c8d530/solana-0.36.3-py3-none-any.whl", hash = "sha256:8d7c7e65d735b6e059cfc35d8fbde8edf6953cf9a9cc511d9db3a05953712625", size = 62180 },
973
+ { url = "https://files.pythonhosted.org/packages/f4/3f/9c6a8bff32b7b622ac1566c52bde16239290b9d1e042c1164bdba22fef8b/solana-0.36.5-py3-none-any.whl", hash = "sha256:6b2023ba8623841ccac96e73e374a9d0e0d450436d368178fdd0537ed308d04c", size = 62217 },
987
974
  ]
988
975
 
989
976
  [[package]]
@@ -1069,15 +1056,6 @@ wheels = [
1069
1056
  { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
1070
1057
  ]
1071
1058
 
1072
- [[package]]
1073
- name = "webencodings"
1074
- version = "0.5.1"
1075
- source = { registry = "https://pypi.org/simple" }
1076
- sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 }
1077
- wheels = [
1078
- { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 },
1079
- ]
1080
-
1081
1059
  [[package]]
1082
1060
  name = "websockets"
1083
1061
  version = "11.0.3"
@@ -1,159 +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
- from rich.live import Live
12
- from rich.table import Table
13
-
14
- from mm_sol import transfer
15
- from mm_sol.balance import get_sol_balance_with_retries
16
- from mm_sol.cli import calcs, cli_utils
17
- from mm_sol.cli.calcs import calc_sol_expression
18
- from mm_sol.cli.validators import Validators
19
- from mm_sol.converters import lamports_to_sol
20
-
21
-
22
- # noinspection DuplicatedCode
23
- class Config(BaseConfig):
24
- nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
25
- routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
26
- private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
27
- proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
28
- value: Annotated[str, AfterValidator(Validators.valid_sol_expression("balance"))]
29
- value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_sol_expression())] = None
30
- delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
31
- round_ndigits: int = 5
32
- log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
33
- log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
34
-
35
- @property
36
- def from_addresses(self) -> list[str]:
37
- return [r.from_address for r in self.routes]
38
-
39
- @model_validator(mode="after")
40
- def final_validator(self) -> Self:
41
- if not self.private_keys.contains_all_addresses(self.from_addresses):
42
- raise ValueError("private keys are not set for all addresses")
43
-
44
- return self
45
-
46
-
47
- def run(
48
- config_path: Path,
49
- *,
50
- print_balances: bool,
51
- print_config: bool,
52
- debug: bool,
53
- no_confirmation: bool,
54
- emulate: bool,
55
- ) -> None:
56
- config = Config.read_toml_config_or_exit(config_path)
57
-
58
- if print_config:
59
- config.print_and_exit({"private_keys"})
60
-
61
- mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
62
-
63
- if print_balances:
64
- _print_balances(config)
65
- sys.exit(0)
66
-
67
- _run_transfers(config, no_confirmation=no_confirmation, emulate=emulate)
68
-
69
-
70
- def _run_transfers(config: Config, *, no_confirmation: bool, emulate: bool) -> None:
71
- logger.info(f"started at {utc_now()} UTC")
72
- logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
73
- for i, route in enumerate(config.routes):
74
- _transfer(
75
- from_address=route.from_address,
76
- to_address=route.to_address,
77
- config=config,
78
- no_confirmation=no_confirmation,
79
- emulate=emulate,
80
- )
81
- if not emulate and config.delay is not None and i < len(config.routes) - 1:
82
- delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
83
- logger.debug(f"delay {delay_value} seconds")
84
- time.sleep(float(delay_value))
85
- logger.info(f"finished at {utc_now()} UTC")
86
-
87
-
88
- def _transfer(*, from_address: str, to_address: str, config: Config, no_confirmation: bool, emulate: bool) -> None:
89
- log_prefix = f"{from_address}->{to_address}"
90
- fee = 5000
91
- # get value
92
- value_res = calcs.calc_sol_value_for_address(
93
- nodes=config.nodes, value_expression=config.value, address=from_address, proxies=config.proxies, fee=fee
94
- )
95
- logger.debug(f"{log_prefix}value={value_res.ok_or_err()}")
96
- if isinstance(value_res, Err):
97
- logger.info(f"{log_prefix}calc value error, {value_res.err}")
98
- return
99
- value = value_res.ok
100
-
101
- # value_min_limit
102
- if config.value_min_limit:
103
- value_min_limit = calc_sol_expression(config.value_min_limit)
104
- if value < value_min_limit:
105
- logger.info(f"{log_prefix}: value<value_min_limit, value={lamports_to_sol(value, config.round_ndigits)}sol")
106
- return
107
-
108
- # emulate?
109
- if emulate:
110
- msg = f"{log_prefix}: emulate, value={lamports_to_sol(value, config.round_ndigits)}SOL,"
111
- msg += f" fee={fee}"
112
- logger.info(msg)
113
- return
114
-
115
- debug_tx_params = {"fee": fee, "value": value, "to": to_address}
116
- logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
117
-
118
- res = transfer.transfer_sol_with_retries(
119
- nodes=config.nodes,
120
- from_address=from_address,
121
- private_key=config.private_keys[from_address],
122
- to_address=to_address,
123
- lamports=value,
124
- proxies=config.proxies,
125
- retries=3,
126
- )
127
-
128
- if isinstance(res, Err):
129
- logger.info(f"{log_prefix}: send_error: {res.err}")
130
- return
131
- signature = res.ok
132
-
133
- if no_confirmation:
134
- msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}"
135
- logger.info(msg)
136
- else:
137
- logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
138
- status = "UNKNOWN"
139
- if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
140
- status = "OK"
141
- msg = f"{log_prefix}: sig={signature}, value={lamports_to_sol(value, config.round_ndigits)}, status={status}"
142
- logger.info(msg)
143
-
144
-
145
- def _print_balances(config: Config) -> None:
146
- table = Table("n", "from_address", "sol", "to_address", "sol", title="balances")
147
- with Live(table, refresh_per_second=0.5):
148
- for count, route in enumerate(config.routes):
149
- from_balance = _get_sol_balance_str(route.from_address, config)
150
- to_balance = _get_sol_balance_str(route.to_address, config)
151
- row: list[str] = [str(count), route.from_address, from_balance, route.to_address, to_balance]
152
- table.add_row(*row)
153
-
154
-
155
- def _get_sol_balance_str(address: str, config: Config) -> str:
156
- return get_sol_balance_with_retries(config.nodes, address, proxies=config.proxies, retries=5).map_or_else(
157
- lambda err: err,
158
- lambda ok: str(lamports_to_sol(ok, config.round_ndigits)),
159
- )
@@ -1,188 +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, 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
-
14
- from mm_sol import transfer
15
- from mm_sol.balance import get_sol_balance_with_retries, get_token_balance_with_retries
16
- from mm_sol.cli import calcs, cli_utils
17
- from mm_sol.cli.validators import Validators
18
- from mm_sol.converters import lamports_to_sol, to_token
19
- from mm_sol.token import get_decimals_with_retries
20
-
21
-
22
- # noinspection DuplicatedCode
23
- class Config(BaseConfig):
24
- nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
25
- routes: Annotated[list[TxRoute], BeforeValidator(Validators.sol_routes())]
26
- private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.sol_private_keys())]
27
- proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
28
- token: Annotated[str, AfterValidator(Validators.sol_address())]
29
- value: Annotated[str, AfterValidator(Validators.valid_token_expression("balance"))]
30
- value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_token_expression())] = None
31
- delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
32
- round_ndigits: int = 5
33
- log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
34
- log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
35
-
36
- @property
37
- def from_addresses(self) -> list[str]:
38
- return [r.from_address for r in self.routes]
39
-
40
- @model_validator(mode="after")
41
- def final_validator(self) -> Self:
42
- if not self.private_keys.contains_all_addresses(self.from_addresses):
43
- raise ValueError("private keys are not set for all addresses")
44
-
45
- return self
46
-
47
-
48
- def run(
49
- config_path: Path,
50
- *,
51
- print_balances: bool,
52
- print_config: bool,
53
- debug: bool,
54
- no_confirmation: bool,
55
- emulate: bool,
56
- ) -> None:
57
- config = Config.read_toml_config_or_exit(config_path)
58
-
59
- if print_config:
60
- config.print_and_exit({"private_keys"})
61
-
62
- mm_crypto_utils.init_logger(debug, config.log_debug, config.log_info)
63
-
64
- decimals_res = get_decimals_with_retries(config.nodes, config.token, retries=3, proxies=config.proxies)
65
- if isinstance(decimals_res, Err):
66
- fatal(f"can't get decimals for token={config.token}, error={decimals_res.err}")
67
-
68
- token_decimals = decimals_res.ok
69
- logger.debug(f"token decimals={token_decimals}")
70
-
71
- if print_balances:
72
- _print_balances(config, token_decimals)
73
- sys.exit(0)
74
-
75
- _run_transfers(config, token_decimals, no_confirmation=no_confirmation, emulate=emulate)
76
-
77
-
78
- def _run_transfers(config: Config, token_decimals: int, *, no_confirmation: bool, emulate: bool) -> None:
79
- logger.info(f"started at {utc_now()} UTC")
80
- logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
81
- for i, route in enumerate(config.routes):
82
- _transfer(
83
- route=route,
84
- token_decimals=token_decimals,
85
- config=config,
86
- no_confirmation=no_confirmation,
87
- emulate=emulate,
88
- )
89
- if not emulate and config.delay is not None and i < len(config.routes) - 1:
90
- delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
91
- logger.debug(f"delay {delay_value} seconds")
92
- time.sleep(float(delay_value))
93
- logger.info(f"finished at {utc_now()} UTC")
94
-
95
-
96
- def _transfer(*, route: TxRoute, config: Config, token_decimals: int, no_confirmation: bool, emulate: bool) -> None:
97
- log_prefix = f"{route.from_address}->{route.to_address}"
98
- fee = 5000
99
-
100
- # get value
101
- value_res = calcs.calc_token_value_for_address(
102
- nodes=config.nodes,
103
- value_expression=config.value,
104
- wallet_address=route.from_address,
105
- proxies=config.proxies,
106
- token_mint_address=config.token,
107
- token_decimals=token_decimals,
108
- )
109
- logger.debug(f"{log_prefix}: value={value_res.ok_or_err()}")
110
- if isinstance(value_res, Err):
111
- logger.info(f"{log_prefix}: calc value error, {value_res.err}")
112
- return
113
- value = value_res.ok
114
- value_t = f"{to_token(value, decimals=token_decimals, ndigits=config.round_ndigits)}t"
115
-
116
- # value_min_limit
117
- if config.value_min_limit:
118
- value_min_limit = calcs.calc_token_expression(config.value_min_limit, token_decimals)
119
- if value < value_min_limit:
120
- logger.info(f"{log_prefix}: value<value_min_limit, value={value_t}")
121
- return
122
-
123
- if emulate:
124
- logger.info(f"{log_prefix}: emulate, value={value_t}, fee={fee}lamports")
125
- return
126
-
127
- logger.debug(f"{log_prefix}: value={to_token(value, decimals=token_decimals)}t, fee={fee}lamports")
128
- res = transfer.transfer_token_with_retries(
129
- nodes=config.nodes,
130
- token_mint_address=config.token,
131
- from_address=route.from_address,
132
- private_key=config.private_keys[route.from_address],
133
- to_address=route.to_address,
134
- amount=value,
135
- decimals=token_decimals,
136
- proxies=config.proxies,
137
- retries=3,
138
- )
139
-
140
- if isinstance(res, Err):
141
- logger.info(f"{log_prefix}: send_error: {res.err}")
142
- return
143
- signature = res.ok
144
-
145
- if no_confirmation:
146
- msg = f"{log_prefix}: sig={signature}, value={value_t}"
147
- logger.info(msg)
148
- else:
149
- logger.debug(f"{log_prefix}: sig={signature}, waiting for confirmation")
150
- status = "UNKNOWN"
151
- if cli_utils.wait_confirmation(config.nodes, config.proxies, signature, log_prefix):
152
- status = "OK"
153
- msg = f"{log_prefix}: sig={signature}, value={value_t}, status={status}"
154
- logger.info(msg)
155
-
156
-
157
- def _print_balances(config: Config, token_decimals: int) -> None:
158
- table = Table("n", "from_address", "sol", "t", "to_address", "sol", "t", title="balances")
159
- with Live(table, refresh_per_second=0.5):
160
- for count, route in enumerate(config.routes):
161
- from_sol_balance = _get_sol_balance_str(route.from_address, config)
162
- to_sol_balance = _get_sol_balance_str(route.to_address, config)
163
- from_t_balance = _get_token_balance_str(route.from_address, config, token_decimals)
164
- to_t_balance = _get_token_balance_str(route.to_address, config, token_decimals)
165
- row: list[str] = [
166
- str(count),
167
- route.from_address,
168
- from_sol_balance,
169
- from_t_balance,
170
- route.to_address,
171
- to_sol_balance,
172
- to_t_balance,
173
- ]
174
- table.add_row(*row)
175
-
176
-
177
- def _get_sol_balance_str(address: str, config: Config) -> str:
178
- return get_sol_balance_with_retries(config.nodes, address, proxies=config.proxies, retries=5).map_or_else(
179
- lambda err: err,
180
- lambda ok: str(lamports_to_sol(ok, config.round_ndigits)),
181
- )
182
-
183
-
184
- def _get_token_balance_str(address: str, config: Config, token_decimals: int) -> str:
185
- return get_token_balance_with_retries(config.nodes, address, config.token, proxies=config.proxies, retries=5).map_or_else(
186
- lambda err: err,
187
- lambda ok: str(to_token(ok, token_decimals, ndigits=config.round_ndigits)),
188
- )
@@ -1,9 +0,0 @@
1
- routes = """
2
- Bd8CxCTLez2ckVTqEJjuZkWjYFSRbo8fA1qYbd7yFVP9 Eaft9xXzfgbRqsHd65WspoaxTtH7pkznM9YA8tsDKGwj
3
- Fc2TRJVCpFZpRz56mFnQETctib1zwFnwHcS7HoQSgUzZ EVJctTWikt29rUXBf49tyQdK87x837HtvpCwqeSjp1Ur
4
- """
5
- private_keys = "file: ./path/to/privates.txt"
6
- value = "0.5balance - 0.012sol"
7
- delay = "random(5,10)"
8
- proxies = "url: https://site.com/api/get-proxies"
9
- 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
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
File without changes
File without changes
File without changes