mm-sol 0.3.6__py3-none-any.whl → 0.5.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 +24 -35
- mm_sol/cli/cli_utils.py +16 -0
- mm_sol/cli/cmd/transfer_cmd.py +271 -0
- mm_sol/cli/examples/balances.toml +2 -0
- mm_sol/cli/examples/transfer.toml +35 -0
- mm_sol/cli/validators.py +7 -3
- {mm_sol-0.3.6.dist-info → mm_sol-0.5.0.dist-info}/METADATA +3 -3
- {mm_sol-0.3.6.dist-info → mm_sol-0.5.0.dist-info}/RECORD +10 -12
- mm_sol/cli/cmd/transfer_sol_cmd.py +0 -159
- mm_sol/cli/cmd/transfer_token_cmd.py +0 -188
- mm_sol/cli/examples/transfer-sol.toml +0 -9
- mm_sol/cli/examples/transfer-token.toml +0 -9
- {mm_sol-0.3.6.dist-info → mm_sol-0.5.0.dist-info}/WHEEL +0 -0
- {mm_sol-0.3.6.dist-info → mm_sol-0.5.0.dist-info}/entry_points.txt +0 -0
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,
|
|
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(
|
|
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
|
-
|
|
35
|
-
transfer_token = "transfer-token" # noqa: S105 # nosec
|
|
37
|
+
transfer = "transfer"
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
@app.command(name="example", help="
|
|
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="
|
|
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
|
|
62
|
-
def
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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="
|
|
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,
|
mm_sol/cli/cli_utils.py
CHANGED
|
@@ -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
|
+
"""
|
mm_sol/cli/validators.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
|
|
3
|
-
from mm_crypto_utils import AddressToPrivate, ConfigValidators,
|
|
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
|
|
20
|
-
return ConfigValidators.
|
|
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})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-sol
|
|
3
|
-
Version: 0.
|
|
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.
|
|
7
|
+
Requires-Dist: mm-crypto-utils>=0.2.3
|
|
8
8
|
Requires-Dist: mnemonic==0.21
|
|
9
|
-
Requires-Dist: solana~=0.36.
|
|
9
|
+
Requires-Dist: solana~=0.36.5
|
|
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=
|
|
16
|
-
mm_sol/cli/cli_utils.py,sha256=
|
|
17
|
-
mm_sol/cli/validators.py,sha256=
|
|
15
|
+
mm_sol/cli/cli.py,sha256=a28Cy1X2bRSIAAJu2HHoPoTJYOPvff-anVcL0BJnwRI,4610
|
|
16
|
+
mm_sol/cli/cli_utils.py,sha256=nFdY8tJFZxyssEBEFCc3VTNJt447e6vMnugx4GBPL4o,1840
|
|
17
|
+
mm_sol/cli/validators.py,sha256=M_Rr7JoG3TUYTDAGkjQLDH6l9i9FOrSpss30KdY3UlM,1379
|
|
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/
|
|
24
|
-
mm_sol/cli/cmd/transfer_token_cmd.py,sha256=gepB8dds1xnGMYT1UBwV8EgpKmST3SvWS99lXnc2X3E,7470
|
|
23
|
+
mm_sol/cli/cmd/transfer_cmd.py,sha256=eQzD7LispR4KFMUlXdXMBGbXs7b0A-iHJHs7VkxH1UA,11021
|
|
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
|
-
mm_sol/cli/examples/balances.toml,sha256=
|
|
29
|
-
mm_sol/cli/examples/transfer
|
|
30
|
-
mm_sol
|
|
31
|
-
mm_sol-0.
|
|
32
|
-
mm_sol-0.
|
|
33
|
-
mm_sol-0.
|
|
34
|
-
mm_sol-0.3.6.dist-info/RECORD,,
|
|
27
|
+
mm_sol/cli/examples/balances.toml,sha256=333g2EkyYBDW7OWFGMIWVZGkdFQMMo0Ag-bg-BvS4Zg,349
|
|
28
|
+
mm_sol/cli/examples/transfer.toml,sha256=kOCdmuwmhlOam4LVtlcYTKF0PoZYHWMlv9gWxNSXMOk,1624
|
|
29
|
+
mm_sol-0.5.0.dist-info/METADATA,sha256=oszZ4Bv24rOTDdMZf6y5FugE0yCiSqANtii1_NluRKc,259
|
|
30
|
+
mm_sol-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
mm_sol-0.5.0.dist-info/entry_points.txt,sha256=MrYnosumy9nsITSAw5TiR3WXDwsdoF0YvUIlZ38TLLs,46
|
|
32
|
+
mm_sol-0.5.0.dist-info/RECORD,,
|
|
@@ -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
|