mm-eth 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mm_eth/cli/calcs.py +2 -68
- mm_eth/cli/cli.py +20 -42
- mm_eth/cli/cli_utils.py +29 -1
- mm_eth/cli/cmd/balances_cmd.py +1 -1
- mm_eth/cli/cmd/call_contract_cmd.py +1 -1
- mm_eth/cli/cmd/deploy_cmd.py +1 -1
- mm_eth/cli/cmd/{config_example_cmd.py → example_cmd.py} +1 -1
- mm_eth/cli/cmd/send_contract_cmd.py +1 -1
- mm_eth/cli/cmd/transfer_cmd.py +311 -0
- mm_eth/cli/{config_examples → examples}/balances.toml +3 -0
- mm_eth/cli/{config_examples → examples}/call_contract.toml +4 -0
- mm_eth/cli/{config_examples/transfer_erc20.toml → examples/transfer.toml} +15 -3
- mm_eth/cli/validators.py +4 -0
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.0.dist-info}/METADATA +3 -3
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.0.dist-info}/RECORD +17 -19
- mm_eth/cli/cmd/transfer_erc20_cmd.py +0 -200
- mm_eth/cli/cmd/transfer_eth_cmd.py +0 -182
- mm_eth/cli/config_examples/transfer_eth.toml +0 -26
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.0.dist-info}/WHEEL +0 -0
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.0.dist-info}/entry_points.txt +0 -0
mm_eth/cli/calcs.py
CHANGED
|
@@ -10,58 +10,8 @@ def calc_eth_expression(expression: str, var: VarInt | None = None) -> int:
|
|
|
10
10
|
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals=SUFFIX_DECIMALS)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# raise TypeError(f"value is not str: {value}")
|
|
16
|
-
# try:
|
|
17
|
-
# var_name = var_name.lower()
|
|
18
|
-
# result = 0
|
|
19
|
-
# for token in split_on_plus_minus_tokens(value.lower()):
|
|
20
|
-
# operator = token[0]
|
|
21
|
-
# item = token[1:]
|
|
22
|
-
# if item.isdigit():
|
|
23
|
-
# item_value = int(item)
|
|
24
|
-
# elif item.endswith("eth"):
|
|
25
|
-
# item = item.removesuffix("eth")
|
|
26
|
-
# item_value = int(Decimal(item) * 10**18)
|
|
27
|
-
# elif item.endswith("ether"):
|
|
28
|
-
# item = item.removesuffix("ether")
|
|
29
|
-
# item_value = int(Decimal(item) * 10**18)
|
|
30
|
-
# elif item.endswith("gwei"):
|
|
31
|
-
# item = item.removesuffix("gwei")
|
|
32
|
-
# item_value = int(Decimal(item) * 10**9)
|
|
33
|
-
# elif item.endswith("t"):
|
|
34
|
-
# if decimals is None:
|
|
35
|
-
# raise ValueError("t without decimals")
|
|
36
|
-
# item = item.removesuffix("t")
|
|
37
|
-
# item_value = int(Decimal(item) * 10**decimals)
|
|
38
|
-
# elif item.endswith(var_name):
|
|
39
|
-
# if var_value is None:
|
|
40
|
-
# raise ValueError("var_value is not set")
|
|
41
|
-
# item = item.removesuffix(var_name)
|
|
42
|
-
# k = Decimal(item) if item else Decimal(1)
|
|
43
|
-
# item_value = int(k * var_value)
|
|
44
|
-
# elif item.startswith("random(") and item.endswith(")"):
|
|
45
|
-
# item = item.lstrip("random(").rstrip(")")
|
|
46
|
-
# arr = item.split(",")
|
|
47
|
-
# if len(arr) != 2:
|
|
48
|
-
# raise ValueError(f"wrong value, random part: {value}")
|
|
49
|
-
# from_value = to_wei(arr[0], decimals=decimals)
|
|
50
|
-
# to_value = to_wei(arr[1], decimals=decimals)
|
|
51
|
-
# if from_value > to_value:
|
|
52
|
-
# raise ValueError(f"wrong value, random part: {value}")
|
|
53
|
-
# item_value = random.randint(from_value, to_value)
|
|
54
|
-
# else:
|
|
55
|
-
# raise ValueError(f"wrong value: {value}")
|
|
56
|
-
#
|
|
57
|
-
# if operator == "+":
|
|
58
|
-
# result += item_value
|
|
59
|
-
# if operator == "-":
|
|
60
|
-
# result -= item_value
|
|
61
|
-
#
|
|
62
|
-
# return result
|
|
63
|
-
# except Exception as err:
|
|
64
|
-
# raise ValueError(f"illegal ee: {value}, error={err}") from err
|
|
13
|
+
def calc_token_expression(expression: str, token_decimals: int, var: VarInt | None = None) -> int:
|
|
14
|
+
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals={"t": token_decimals})
|
|
65
15
|
|
|
66
16
|
|
|
67
17
|
def calc_function_args(value: str) -> str:
|
|
@@ -75,19 +25,3 @@ def calc_function_args(value: str) -> str:
|
|
|
75
25
|
raise ValueError("wrong random(from,to) template")
|
|
76
26
|
rand_value = str(random.randint(random_range[0], random_range[1]))
|
|
77
27
|
value = value[0:start_index] + rand_value + value[stop_index + 1 :]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# def is_value_less_min_limit(
|
|
81
|
-
# value_min_limit: str | None,
|
|
82
|
-
# value: int,
|
|
83
|
-
# value_unit: str,
|
|
84
|
-
# decimals: int | None = None,
|
|
85
|
-
# log_prefix: str | None = None,
|
|
86
|
-
# ) -> bool:
|
|
87
|
-
# if value_min_limit is None:
|
|
88
|
-
# return False
|
|
89
|
-
# if value < calc_eth_expression(value_min_limit):
|
|
90
|
-
# prefix = get_log_prefix(log_prefix)
|
|
91
|
-
# logger.info("{}value is less min limit, value={}", prefix, from_wei_str(value, value_unit, decimals=decimals))
|
|
92
|
-
# return True
|
|
93
|
-
# return False
|
mm_eth/cli/cli.py
CHANGED
|
@@ -10,9 +10,9 @@ from .cmd import (
|
|
|
10
10
|
balance_cmd,
|
|
11
11
|
balances_cmd,
|
|
12
12
|
call_contract_cmd,
|
|
13
|
-
config_example_cmd,
|
|
14
13
|
deploy_cmd,
|
|
15
14
|
encode_input_data_cmd,
|
|
15
|
+
example_cmd,
|
|
16
16
|
mnemonic_cmd,
|
|
17
17
|
node_cmd,
|
|
18
18
|
private_key_cmd,
|
|
@@ -20,8 +20,7 @@ from .cmd import (
|
|
|
20
20
|
send_contract_cmd,
|
|
21
21
|
solc_cmd,
|
|
22
22
|
token_cmd,
|
|
23
|
-
|
|
24
|
-
transfer_eth_cmd,
|
|
23
|
+
transfer_cmd,
|
|
25
24
|
tx_cmd,
|
|
26
25
|
vault_cmd,
|
|
27
26
|
)
|
|
@@ -29,8 +28,7 @@ from .cmd.balances_cmd import BalancesCmdParams
|
|
|
29
28
|
from .cmd.call_contract_cmd import CallContractCmdParams
|
|
30
29
|
from .cmd.deploy_cmd import DeployCmdParams
|
|
31
30
|
from .cmd.send_contract_cmd import SendContractCmdParams
|
|
32
|
-
from .cmd.
|
|
33
|
-
from .cmd.transfer_eth_cmd import TransferEthCmdParams
|
|
31
|
+
from .cmd.transfer_cmd import TransferCmdParams
|
|
34
32
|
|
|
35
33
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
36
34
|
|
|
@@ -40,8 +38,7 @@ app.add_typer(wallet_app, name="w", hidden=True)
|
|
|
40
38
|
|
|
41
39
|
|
|
42
40
|
class ConfigExample(str, Enum):
|
|
43
|
-
|
|
44
|
-
TRANSFER_ERC20 = "transfer-erc20"
|
|
41
|
+
TRANSFER = "transfer"
|
|
45
42
|
BALANCES = "balances"
|
|
46
43
|
CALL_CONTRACT = "call-contract"
|
|
47
44
|
|
|
@@ -141,43 +138,24 @@ def tx_command(
|
|
|
141
138
|
tx_cmd.run(rpc_url, tx_hash, get_receipt)
|
|
142
139
|
|
|
143
140
|
|
|
144
|
-
@app.command(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
149
|
-
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
150
|
-
no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
151
|
-
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
152
|
-
) -> None:
|
|
153
|
-
transfer_eth_cmd.run(
|
|
154
|
-
TransferEthCmdParams(
|
|
155
|
-
config_path=config_path,
|
|
156
|
-
print_balances=print_balances,
|
|
157
|
-
print_config_and_exit=print_config,
|
|
158
|
-
debug=debug,
|
|
159
|
-
no_receipt=no_receipt,
|
|
160
|
-
emulate=emulate,
|
|
161
|
-
)
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@app.command(name="transfer-erc20", help="Transfer ERC20 token from one or many accounts")
|
|
166
|
-
def transfer_erc20_command(
|
|
141
|
+
@app.command(
|
|
142
|
+
name="transfer", help="Transfers ETH or ERC20 tokens, supporting multiple routes, delays, and expression-based values"
|
|
143
|
+
)
|
|
144
|
+
def transfer_command(
|
|
167
145
|
config_path: Path,
|
|
168
146
|
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
169
147
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
170
148
|
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
171
|
-
|
|
149
|
+
skip_receipt: bool = typer.Option(False, "--skip-receipt", help="Don't wait for a tx receipt"),
|
|
172
150
|
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
173
151
|
) -> None:
|
|
174
|
-
|
|
175
|
-
|
|
152
|
+
transfer_cmd.run(
|
|
153
|
+
TransferCmdParams(
|
|
176
154
|
config_path=config_path,
|
|
177
155
|
print_balances=print_balances,
|
|
178
|
-
|
|
156
|
+
print_config=print_config,
|
|
179
157
|
debug=debug,
|
|
180
|
-
|
|
158
|
+
skip_receipt=skip_receipt,
|
|
181
159
|
emulate=emulate,
|
|
182
160
|
)
|
|
183
161
|
)
|
|
@@ -196,7 +174,7 @@ def send_contract_command(
|
|
|
196
174
|
SendContractCmdParams(
|
|
197
175
|
config_path=config_path,
|
|
198
176
|
print_balances=print_balances,
|
|
199
|
-
|
|
177
|
+
print_config=print_config,
|
|
200
178
|
debug=debug,
|
|
201
179
|
no_receipt=no_receipt,
|
|
202
180
|
emulate=emulate,
|
|
@@ -211,7 +189,7 @@ def balances_command(
|
|
|
211
189
|
nonce: bool = typer.Option(False, "--nonce", "-n", help="Print nonce also"),
|
|
212
190
|
wei: bool = typer.Option(False, "--wei", "-w", help="Show balances in WEI"),
|
|
213
191
|
) -> None:
|
|
214
|
-
balances_cmd.run(BalancesCmdParams(config_path=config_path,
|
|
192
|
+
balances_cmd.run(BalancesCmdParams(config_path=config_path, print_config=print_config, wei=wei, show_nonce=nonce))
|
|
215
193
|
|
|
216
194
|
|
|
217
195
|
@app.command(name="call-contract", help="Call a method on a contract")
|
|
@@ -219,7 +197,7 @@ def call_contract_command(
|
|
|
219
197
|
config_path: Path,
|
|
220
198
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
221
199
|
) -> None:
|
|
222
|
-
call_contract_cmd.run(CallContractCmdParams(config_path=config_path,
|
|
200
|
+
call_contract_cmd.run(CallContractCmdParams(config_path=config_path, print_config=print_config))
|
|
223
201
|
|
|
224
202
|
|
|
225
203
|
@app.command(name="deploy", help="Deploy a smart contract onchain")
|
|
@@ -227,12 +205,12 @@ def deploy_command(
|
|
|
227
205
|
config_path: Path,
|
|
228
206
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
229
207
|
) -> None:
|
|
230
|
-
deploy_cmd.run(DeployCmdParams(config_path=config_path,
|
|
208
|
+
deploy_cmd.run(DeployCmdParams(config_path=config_path, print_config=print_config))
|
|
231
209
|
|
|
232
210
|
|
|
233
|
-
@app.command(name="
|
|
234
|
-
def
|
|
235
|
-
|
|
211
|
+
@app.command(name="example", help="Displays an example configuration for a command")
|
|
212
|
+
def example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
|
|
213
|
+
example_cmd.run(command)
|
|
236
214
|
|
|
237
215
|
|
|
238
216
|
@app.command(name="encode-input-data", help="Encode input data by a function signature")
|
mm_eth/cli/cli_utils.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
|
+
import time
|
|
2
3
|
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
3
5
|
|
|
6
|
+
from mm_crypto_utils import Nodes, Proxies
|
|
7
|
+
from mm_std import BaseConfig, print_json
|
|
4
8
|
from pydantic import BaseModel
|
|
5
9
|
|
|
10
|
+
from mm_eth import rpc
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
def get_version() -> str:
|
|
8
14
|
return importlib.metadata.version("mm-eth")
|
|
@@ -29,4 +35,26 @@ def public_rpc_url(url: str | None) -> str:
|
|
|
29
35
|
|
|
30
36
|
class BaseConfigParams(BaseModel):
|
|
31
37
|
config_path: Path
|
|
32
|
-
|
|
38
|
+
print_config: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_config(config: BaseConfig, exclude: set[str] | None = None, count: set[str] | None = None) -> None:
|
|
42
|
+
data = config.model_dump(exclude=exclude)
|
|
43
|
+
if count:
|
|
44
|
+
for k in count:
|
|
45
|
+
data[k] = len(data[k])
|
|
46
|
+
print_json(data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def wait_tx_status(nodes: Nodes, proxies: Proxies, tx_hash: str, timeout: int) -> Literal["OK", "FAIL", "TIMEOUT"]:
|
|
50
|
+
started_at = time.perf_counter()
|
|
51
|
+
count = 0
|
|
52
|
+
while True:
|
|
53
|
+
res = rpc.get_tx_status(nodes, tx_hash, proxies=proxies, attempts=5)
|
|
54
|
+
if res.is_ok():
|
|
55
|
+
return "OK" if res.ok == 1 else "FAIL"
|
|
56
|
+
|
|
57
|
+
time.sleep(1)
|
|
58
|
+
count += 1
|
|
59
|
+
if time.perf_counter() - started_at > timeout:
|
|
60
|
+
return "TIMEOUT"
|
mm_eth/cli/cmd/balances_cmd.py
CHANGED
|
@@ -33,7 +33,7 @@ class BalancesCmdParams(BaseConfigParams):
|
|
|
33
33
|
|
|
34
34
|
def run(params: BalancesCmdParams) -> None:
|
|
35
35
|
config = Config.read_toml_config_or_exit(params.config_path)
|
|
36
|
-
if params.
|
|
36
|
+
if params.print_config:
|
|
37
37
|
config.print_and_exit()
|
|
38
38
|
|
|
39
39
|
tokens = _get_tokens_info(config)
|
|
@@ -22,7 +22,7 @@ class CallContractCmdParams(BaseConfigParams):
|
|
|
22
22
|
|
|
23
23
|
def run(cli_params: CallContractCmdParams) -> None:
|
|
24
24
|
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
25
|
-
if cli_params.
|
|
25
|
+
if cli_params.print_config:
|
|
26
26
|
config.print_and_exit()
|
|
27
27
|
|
|
28
28
|
input_data = abi.encode_function_input_by_signature(
|
mm_eth/cli/cmd/deploy_cmd.py
CHANGED
|
@@ -27,7 +27,7 @@ class DeployCmdParams(BaseConfigParams):
|
|
|
27
27
|
|
|
28
28
|
def run(cli_params: DeployCmdParams) -> None:
|
|
29
29
|
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
30
|
-
if cli_params.
|
|
30
|
+
if cli_params.print_config:
|
|
31
31
|
config.print_and_exit({"private_key"})
|
|
32
32
|
|
|
33
33
|
constructor_types = yaml.full_load(config.constructor_types)
|
|
@@ -5,5 +5,5 @@ from mm_std import print_plain
|
|
|
5
5
|
|
|
6
6
|
def run(command: str) -> None:
|
|
7
7
|
command = command.replace("-", "_")
|
|
8
|
-
example_file = Path(Path(__file__).parent.absolute(), "../
|
|
8
|
+
example_file = Path(Path(__file__).parent.absolute(), "../examples", f"{command}.toml")
|
|
9
9
|
print_plain(example_file.read_text())
|
|
@@ -65,7 +65,7 @@ class SendContractCmdParams(BaseConfigParams):
|
|
|
65
65
|
# noinspection DuplicatedCode
|
|
66
66
|
def run(cli_params: SendContractCmdParams) -> None:
|
|
67
67
|
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
68
|
-
if cli_params.
|
|
68
|
+
if cli_params.print_config:
|
|
69
69
|
config.print_and_exit({"private_key"})
|
|
70
70
|
|
|
71
71
|
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
@@ -0,0 +1,311 @@
|
|
|
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_eth import erc20, rpc
|
|
15
|
+
from mm_eth.cli import calcs, cli_utils, rpc_helpers
|
|
16
|
+
from mm_eth.cli.calcs import calc_eth_expression
|
|
17
|
+
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
18
|
+
from mm_eth.cli.validators import Validators
|
|
19
|
+
from mm_eth.tx import sign_tx
|
|
20
|
+
from mm_eth.utils import from_wei_str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Config(BaseConfig):
|
|
24
|
+
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
25
|
+
chain_id: int
|
|
26
|
+
routes: Annotated[list[TxRoute], BeforeValidator(Validators.eth_routes())]
|
|
27
|
+
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
28
|
+
token: Annotated[str | None, AfterValidator(Validators.eth_address())] = None # if None, then eth transfer
|
|
29
|
+
token_decimals: int = -1
|
|
30
|
+
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
31
|
+
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
32
|
+
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
33
|
+
value: Annotated[str, AfterValidator(Validators.valid_eth_or_token_expression("balance"))]
|
|
34
|
+
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_or_token_expression())] = None
|
|
35
|
+
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
36
|
+
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
37
|
+
round_ndigits: int = 5
|
|
38
|
+
proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
|
|
39
|
+
wait_tx_timeout: int = 120
|
|
40
|
+
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
41
|
+
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def from_addresses(self) -> list[str]:
|
|
45
|
+
return [r.from_address for r in self.routes]
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def final_validator(self) -> Self:
|
|
49
|
+
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
50
|
+
raise ValueError("private keys are not set for all addresses")
|
|
51
|
+
|
|
52
|
+
if self.token:
|
|
53
|
+
Validators.valid_token_expression("balance")(self.value)
|
|
54
|
+
if self.value_min_limit:
|
|
55
|
+
Validators.valid_token_expression()(self.value_min_limit)
|
|
56
|
+
else:
|
|
57
|
+
Validators.valid_eth_expression("balance")(self.value)
|
|
58
|
+
if self.value_min_limit:
|
|
59
|
+
Validators.valid_eth_expression()(self.value_min_limit)
|
|
60
|
+
|
|
61
|
+
if self.token:
|
|
62
|
+
res = erc20.get_decimals(self.nodes, self.token, proxies=self.proxies, attempts=5)
|
|
63
|
+
if isinstance(res, Err):
|
|
64
|
+
fatal(f"can't get token decimals: {res.err}")
|
|
65
|
+
self.token_decimals = res.ok
|
|
66
|
+
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TransferCmdParams(BaseConfigParams):
|
|
71
|
+
print_balances: bool
|
|
72
|
+
debug: bool
|
|
73
|
+
skip_receipt: bool
|
|
74
|
+
emulate: bool
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run(cmd_params: TransferCmdParams) -> None:
|
|
78
|
+
config = Config.read_toml_config_or_exit(cmd_params.config_path)
|
|
79
|
+
if cmd_params.print_config:
|
|
80
|
+
cli_utils.print_config(config, exclude={"private_keys"}, count=None if cmd_params.debug else {"proxies"})
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
84
|
+
|
|
85
|
+
if cmd_params.print_balances:
|
|
86
|
+
_print_balances(config)
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
_run_transfers(config, cmd_params)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
93
|
+
mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
|
|
94
|
+
logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
|
|
95
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
96
|
+
for i, route in enumerate(config.routes):
|
|
97
|
+
_transfer(route, config, cmd_params)
|
|
98
|
+
if config.delay is not None and i < len(config.routes) - 1:
|
|
99
|
+
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
100
|
+
logger.info(f"delay {delay_value} seconds")
|
|
101
|
+
if not cmd_params.emulate:
|
|
102
|
+
time.sleep(float(delay_value))
|
|
103
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
107
|
+
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, route.log_prefix)
|
|
108
|
+
if nonce is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, route.log_prefix)
|
|
112
|
+
if max_fee is None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, route.log_prefix):
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
gas = _calc_gas(route, config)
|
|
119
|
+
if gas is None:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
value = _calc_value(route, max_fee=max_fee, gas=gas, config=config)
|
|
123
|
+
if value is None:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if not _check_value_min_limit(route, value, config):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
priority_fee = calc_eth_expression(config.priority_fee)
|
|
130
|
+
|
|
131
|
+
# emulate?
|
|
132
|
+
if cmd_params.emulate:
|
|
133
|
+
msg = f"{route.log_prefix}: emulate,"
|
|
134
|
+
msg += f" value={_value_with_suffix(value, config)},"
|
|
135
|
+
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
136
|
+
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
137
|
+
msg += f" gas={gas}"
|
|
138
|
+
logger.info(msg)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
tx_hash = _send_tx(route=route, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
|
|
142
|
+
if tx_hash is None:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
status = "UNKNOWN"
|
|
146
|
+
if not cmd_params.skip_receipt:
|
|
147
|
+
logger.debug(f"{route.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
|
|
148
|
+
status = cli_utils.wait_tx_status(config.nodes, config.proxies, tx_hash, config.wait_tx_timeout)
|
|
149
|
+
|
|
150
|
+
logger.info(f"{route.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _calc_value(route: TxRoute, max_fee: int, gas: int, config: Config) -> int | None:
|
|
154
|
+
if config.token:
|
|
155
|
+
return rpc_helpers.calc_erc20_value_for_address(
|
|
156
|
+
nodes=config.nodes,
|
|
157
|
+
value_expression=config.value,
|
|
158
|
+
wallet_address=route.from_address,
|
|
159
|
+
token_address=config.token,
|
|
160
|
+
decimals=config.token_decimals,
|
|
161
|
+
log_prefix=route.log_prefix,
|
|
162
|
+
)
|
|
163
|
+
return rpc_helpers.calc_eth_value_for_address(
|
|
164
|
+
nodes=config.nodes,
|
|
165
|
+
value_expression=config.value,
|
|
166
|
+
address=route.from_address,
|
|
167
|
+
gas=gas,
|
|
168
|
+
max_fee=max_fee,
|
|
169
|
+
log_prefix=route.log_prefix,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
|
|
174
|
+
"""Returns False if the transfer should be skipped."""
|
|
175
|
+
if config.value_min_limit:
|
|
176
|
+
if config.token:
|
|
177
|
+
value_min_limit = calcs.calc_token_expression(config.value_min_limit, config.token_decimals)
|
|
178
|
+
else:
|
|
179
|
+
value_min_limit = calcs.calc_eth_expression(config.value_min_limit)
|
|
180
|
+
if value < value_min_limit:
|
|
181
|
+
logger.info(f"{route.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config) -> str | None:
|
|
186
|
+
debug_tx_params = {
|
|
187
|
+
"nonce": nonce,
|
|
188
|
+
"max_fee": max_fee,
|
|
189
|
+
"priority_fee": priority_fee,
|
|
190
|
+
"gas": gas,
|
|
191
|
+
"value": value,
|
|
192
|
+
"to": route.to_address,
|
|
193
|
+
"chain_id": config.chain_id,
|
|
194
|
+
}
|
|
195
|
+
logger.debug(f"{route.log_prefix}: tx_params={debug_tx_params}")
|
|
196
|
+
|
|
197
|
+
if config.token:
|
|
198
|
+
signed_tx = erc20.sign_transfer_tx(
|
|
199
|
+
nonce=nonce,
|
|
200
|
+
max_fee_per_gas=max_fee,
|
|
201
|
+
max_priority_fee_per_gas=priority_fee,
|
|
202
|
+
gas_limit=gas,
|
|
203
|
+
private_key=config.private_keys[route.from_address],
|
|
204
|
+
chain_id=config.chain_id,
|
|
205
|
+
value=value,
|
|
206
|
+
token_address=config.token,
|
|
207
|
+
recipient_address=route.to_address,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
signed_tx = sign_tx(
|
|
211
|
+
nonce=nonce,
|
|
212
|
+
max_fee_per_gas=max_fee,
|
|
213
|
+
max_priority_fee_per_gas=priority_fee,
|
|
214
|
+
gas=gas,
|
|
215
|
+
private_key=config.private_keys[route.from_address],
|
|
216
|
+
chain_id=config.chain_id,
|
|
217
|
+
value=value,
|
|
218
|
+
to=route.to_address,
|
|
219
|
+
)
|
|
220
|
+
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
221
|
+
if isinstance(res, Err):
|
|
222
|
+
logger.info(f"{route.log_prefix}: tx error {res.err}")
|
|
223
|
+
return None
|
|
224
|
+
return res.ok
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _calc_gas(route: TxRoute, config: Config) -> int | None:
|
|
228
|
+
if config.token:
|
|
229
|
+
return rpc_helpers.calc_gas(
|
|
230
|
+
nodes=config.nodes,
|
|
231
|
+
gas_expression=config.gas,
|
|
232
|
+
from_address=route.from_address,
|
|
233
|
+
to_address=config.token,
|
|
234
|
+
data=erc20.encode_transfer_input_data(route.to_address, 1234),
|
|
235
|
+
log_prefix=route.log_prefix,
|
|
236
|
+
)
|
|
237
|
+
return rpc_helpers.calc_gas(
|
|
238
|
+
nodes=config.nodes,
|
|
239
|
+
gas_expression=config.gas,
|
|
240
|
+
from_address=route.from_address,
|
|
241
|
+
to_address=route.to_address,
|
|
242
|
+
value=123,
|
|
243
|
+
log_prefix=route.log_prefix,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _print_balances(config: Config) -> None:
|
|
248
|
+
if config.token:
|
|
249
|
+
headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
|
|
250
|
+
else:
|
|
251
|
+
headers = ["n", "from_address", "nonce", "eth", "to_address", "nonce", "eth"]
|
|
252
|
+
table = Table(*headers, title="balances")
|
|
253
|
+
with Live(table, refresh_per_second=0.5):
|
|
254
|
+
for count, route in enumerate(config.routes):
|
|
255
|
+
from_nonce = _get_nonce_str(route.from_address, config)
|
|
256
|
+
to_nonce = _get_nonce_str(route.to_address, config)
|
|
257
|
+
|
|
258
|
+
from_eth_balance = _get_eth_balance_str(route.from_address, config)
|
|
259
|
+
to_eth_balance = _get_eth_balance_str(route.to_address, config)
|
|
260
|
+
|
|
261
|
+
from_token_balance = _get_token_balance_str(route.from_address, config) if config.token else ""
|
|
262
|
+
to_token_balance = _get_token_balance_str(route.to_address, config) if config.token else ""
|
|
263
|
+
|
|
264
|
+
if config.token:
|
|
265
|
+
table.add_row(
|
|
266
|
+
str(count),
|
|
267
|
+
route.from_address,
|
|
268
|
+
from_nonce,
|
|
269
|
+
from_eth_balance,
|
|
270
|
+
from_token_balance,
|
|
271
|
+
route.to_address,
|
|
272
|
+
to_nonce,
|
|
273
|
+
to_eth_balance,
|
|
274
|
+
to_token_balance,
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
table.add_row(
|
|
278
|
+
str(count),
|
|
279
|
+
route.from_address,
|
|
280
|
+
from_nonce,
|
|
281
|
+
from_eth_balance,
|
|
282
|
+
route.to_address,
|
|
283
|
+
to_nonce,
|
|
284
|
+
to_eth_balance,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _get_nonce_str(address: str, config: Config) -> str:
|
|
289
|
+
return str(rpc.eth_get_transaction_count(config.nodes, address, proxies=config.proxies, attempts=5).ok_or_err())
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _get_eth_balance_str(address: str, config: Config) -> str:
|
|
293
|
+
return rpc.eth_get_balance(config.nodes, address, proxies=config.proxies, attempts=5).map_or_else(
|
|
294
|
+
lambda err: err,
|
|
295
|
+
lambda ok: from_wei_str(ok, "eth", config.round_ndigits),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _get_token_balance_str(address: str, config: Config) -> str:
|
|
300
|
+
if not config.token:
|
|
301
|
+
raise ValueError("token is not set")
|
|
302
|
+
return erc20.get_balance(config.nodes, config.token, address, proxies=config.proxies, attempts=5).map_or_else(
|
|
303
|
+
lambda err: err,
|
|
304
|
+
lambda ok: from_wei_str(ok, "t", decimals=config.token_decimals, round_ndigits=config.round_ndigits),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _value_with_suffix(value: int, config: Config) -> str:
|
|
309
|
+
if config.token:
|
|
310
|
+
return from_wei_str(value, "t", config.round_ndigits, decimals=config.token_decimals)
|
|
311
|
+
return from_wei_str(value, "eth", config.round_ndigits)
|
|
@@ -3,13 +3,16 @@ addresses = """
|
|
|
3
3
|
0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
4
4
|
0x97C77B548aE0d4925F5C201220fC6d8996424309
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
tokens = """
|
|
7
8
|
0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # USDT
|
|
8
9
|
0x6a55fe4884DE7E1d904BdC47A3BA092240ae9B39 # USDC
|
|
9
10
|
"""
|
|
11
|
+
|
|
10
12
|
nodes = """
|
|
11
13
|
https://arb1.arbitrum.io/rpc
|
|
12
14
|
https://rpc.arb1.arbitrum.gateway.fm
|
|
13
15
|
https://arbitrum-one.publicnode.com
|
|
14
16
|
"""
|
|
17
|
+
|
|
15
18
|
round_ndigits = 3
|
|
@@ -2,25 +2,37 @@ routes = """
|
|
|
2
2
|
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
3
3
|
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
private_keys = """
|
|
6
7
|
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
7
8
|
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
8
9
|
file: /path/to/other_private_keys.txt
|
|
9
10
|
"""
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
13
|
+
|
|
12
14
|
max_fee = "1.2base_fee + 1gwei + random(1,200)" # supported var_name=base_fee
|
|
15
|
+
|
|
13
16
|
max_fee_limit = "10.1gwei - random(1, 10)" # optional
|
|
17
|
+
|
|
14
18
|
priority_fee = "1gwei + random(1, 12)"
|
|
19
|
+
|
|
15
20
|
gas = "estimate + random(100, 200) - 19" # supported var_name=estimate
|
|
16
|
-
|
|
21
|
+
|
|
22
|
+
value = "0.5balance - random(1.5t, 3t) + 11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
23
|
+
|
|
17
24
|
value_min_limit = "0.5t + random(1, 2) - 7"
|
|
18
25
|
|
|
19
26
|
delay = "random(1.123, 10) + 1" # secs, optional
|
|
27
|
+
|
|
20
28
|
log_debug = "/path/to/file_debug.log" # optional
|
|
29
|
+
|
|
21
30
|
log_info = "/path/to/file_info.log" # optional
|
|
31
|
+
|
|
22
32
|
round_ndigits = 6 # optional, default=5
|
|
33
|
+
|
|
23
34
|
chain_id = 421613
|
|
35
|
+
|
|
24
36
|
nodes = """
|
|
25
37
|
https://arbitrum-goerli.publicnode.com
|
|
26
38
|
https://rpc.goerli.arbitrum.gateway.fm
|
mm_eth/cli/validators.py
CHANGED
|
@@ -17,6 +17,10 @@ class Validators(ConfigValidators):
|
|
|
17
17
|
def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
18
18
|
return ConfigValidators.valid_calc_int_expression(var_name, {"t": 6})
|
|
19
19
|
|
|
20
|
+
@staticmethod
|
|
21
|
+
def valid_eth_or_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
22
|
+
return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS | {"t": 6})
|
|
23
|
+
|
|
20
24
|
@staticmethod
|
|
21
25
|
def eth_routes() -> Callable[[str], list[TxRoute]]:
|
|
22
26
|
return ConfigValidators.routes(is_address, to_lower=True)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
|
-
Requires-Dist: mm-crypto-utils>=0.1.
|
|
5
|
+
Requires-Dist: mm-crypto-utils>=0.1.5
|
|
6
6
|
Requires-Dist: typer>=0.15.1
|
|
7
|
-
Requires-Dist: web3~=7.
|
|
7
|
+
Requires-Dist: web3~=7.8.0
|
|
8
8
|
Requires-Dist: websocket-client~=1.8.0
|
|
@@ -15,35 +15,33 @@ mm_eth/tx.py,sha256=efSoMCoWkenbGdHo1_LX66_Edz1HvED5-J_i3wrHwMw,4051
|
|
|
15
15
|
mm_eth/utils.py,sha256=sSxt9GZEntZlT0RU8ht9Qon875HPhpd-1JjgqUBEfVo,7405
|
|
16
16
|
mm_eth/vault.py,sha256=h8NyiOQh5YFskh1lZA3KyvnJUnxl9769ME2ChplG0CM,1477
|
|
17
17
|
mm_eth/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
mm_eth/cli/calcs.py,sha256=
|
|
19
|
-
mm_eth/cli/cli.py,sha256=
|
|
20
|
-
mm_eth/cli/cli_utils.py,sha256=
|
|
18
|
+
mm_eth/cli/calcs.py,sha256=cLFTYNAN-I53tUiSg-zFfVr2afjIZPftDDjHj16FBz0,1068
|
|
19
|
+
mm_eth/cli/cli.py,sha256=SdfOq4hSI2V8rgbSG0LTXqicE8dmIdLDBfSstSPhDfA,9019
|
|
20
|
+
mm_eth/cli/cli_utils.py,sha256=yBIS3dGid75zyxw8crPOQHA4p3Krk5BoA2g01turKmQ,1712
|
|
21
21
|
mm_eth/cli/print_helpers.py,sha256=yOiOFjTKloumwf07AqNIHQswUo8t0yuT9bpeGBGl60Q,1470
|
|
22
22
|
mm_eth/cli/rpc_helpers.py,sha256=FMV-QVNM3v9X8H_-DP0hjNRqmm7KOnfzkw9bP17Qbz0,4499
|
|
23
|
-
mm_eth/cli/validators.py,sha256=
|
|
23
|
+
mm_eth/cli/validators.py,sha256=whnuA4CFkKbcNcbl4e2EwktTSTDGFBTA798E9F_3EM8,1682
|
|
24
24
|
mm_eth/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
mm_eth/cli/cmd/balance_cmd.py,sha256=gkwUa8VGe1wXH0GDsit3-1NmRiijojaNLygi5zNcYSY,2110
|
|
26
|
-
mm_eth/cli/cmd/balances_cmd.py,sha256=
|
|
27
|
-
mm_eth/cli/cmd/call_contract_cmd.py,sha256=
|
|
28
|
-
mm_eth/cli/cmd/
|
|
29
|
-
mm_eth/cli/cmd/deploy_cmd.py,sha256=H5p0K4p3wX9MGFHg-h__dYUw-X5ttNTEDMPCy_T5eHQ,1281
|
|
26
|
+
mm_eth/cli/cmd/balances_cmd.py,sha256=4UiWSNH9OCnkvcMGPTygEss8119do-rfG7QtsNMfWZs,4197
|
|
27
|
+
mm_eth/cli/cmd/call_contract_cmd.py,sha256=RbBPvyUEQ45hQINYDKkx1yWhPygdymVKlRL26xI31uk,1264
|
|
28
|
+
mm_eth/cli/cmd/deploy_cmd.py,sha256=0oBp_RZw_DIEtBFRc6QKdAw5oouwwpDIdeXvuyP9xdU,1272
|
|
30
29
|
mm_eth/cli/cmd/encode_input_data_cmd.py,sha256=9UQ1MKPEFQJ8j_COsP3KGKhwOf9tT3feBezI8vvxTlw,267
|
|
30
|
+
mm_eth/cli/cmd/example_cmd.py,sha256=o4NTll3fjmspbKjZ0sHGDRHTZ1RcFNHZDi9Ka0VNoDQ,264
|
|
31
31
|
mm_eth/cli/cmd/mnemonic_cmd.py,sha256=Mb0H0inSNCa93GEPxGYQi7BnPe11mnLjnspfe7h54I4,972
|
|
32
32
|
mm_eth/cli/cmd/node_cmd.py,sha256=Ae5yPxxnNiHw3tZcojS7KwNLM4gEfLhsTfhZp_86rqU,1956
|
|
33
33
|
mm_eth/cli/cmd/private_key_cmd.py,sha256=Fv_2OLog1h32pIP7PJITwl_pHdy3BXvaDRcXZsxY1xo,241
|
|
34
34
|
mm_eth/cli/cmd/rpc_cmd.py,sha256=02q82YqgbPezfEBmV_QBCIeNReE7ktkPych8Xr9ann8,2186
|
|
35
|
-
mm_eth/cli/cmd/send_contract_cmd.py,sha256=
|
|
35
|
+
mm_eth/cli/cmd/send_contract_cmd.py,sha256=JX3L1Dz4CBySfa2Pf87E8grauPu0U6xIyyOus81285Y,7316
|
|
36
36
|
mm_eth/cli/cmd/solc_cmd.py,sha256=tBpeMdPfGs2iQIMaIJAAhMh1a3KyXHwyninfXPVpsgs,677
|
|
37
37
|
mm_eth/cli/cmd/token_cmd.py,sha256=4y6ZQpLOJ33_iNuKpm9tZXh4RntWhmPUcizgaNNBzaw,1102
|
|
38
|
-
mm_eth/cli/cmd/
|
|
39
|
-
mm_eth/cli/cmd/transfer_eth_cmd.py,sha256=egYx3yG29CCzKeZRzY5QmqSHQrypdHMKJ32rHdQpsyg,6980
|
|
38
|
+
mm_eth/cli/cmd/transfer_cmd.py,sha256=nC1Jiqe6zDJQvK9xVhB8mCbkEB1ZgMZmA393tDnZ1Dw,12176
|
|
40
39
|
mm_eth/cli/cmd/tx_cmd.py,sha256=PIenXYTT60Z2fqsivpzybCLI2Z_tlcz-asm3B0JLHgI,517
|
|
41
40
|
mm_eth/cli/cmd/vault_cmd.py,sha256=MOM1CILIaaqown1I-Fgo22ckqIMLtFt8t2D3fWNp798,606
|
|
42
|
-
mm_eth/cli/
|
|
43
|
-
mm_eth/cli/
|
|
44
|
-
mm_eth/cli/
|
|
45
|
-
mm_eth/
|
|
46
|
-
mm_eth-0.
|
|
47
|
-
mm_eth-0.
|
|
48
|
-
mm_eth-0.
|
|
49
|
-
mm_eth-0.3.1.dist-info/RECORD,,
|
|
41
|
+
mm_eth/cli/examples/balances.toml,sha256=i_ALpiEcf8-0TFiUg1cgJhxxfHYeBl9x0b3tnUWjswU,421
|
|
42
|
+
mm_eth/cli/examples/call_contract.toml,sha256=ZQWK-409V_vLIZ2bsRD5RCWPPzShPz2KJTTRQY4YaGw,248
|
|
43
|
+
mm_eth/cli/examples/transfer.toml,sha256=HLVpkCNakZFE8qdFdDUoBPk7ZBdfEmk9TLZtooRsGAc,1198
|
|
44
|
+
mm_eth-0.4.0.dist-info/METADATA,sha256=WjvTV6bG2379Zk0cJkuMX6xhAlVSi_YeqVhS4IyBFfI,207
|
|
45
|
+
mm_eth-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
46
|
+
mm_eth-0.4.0.dist-info/entry_points.txt,sha256=aGhpsozl8NIrkuUcX5fSgURCcDhr3ShUdeTSIrJq4oc,46
|
|
47
|
+
mm_eth-0.4.0.dist-info/RECORD,,
|
|
@@ -1,200 +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, Ok, fatal, utc_now
|
|
10
|
-
from pydantic import AfterValidator, BeforeValidator, model_validator
|
|
11
|
-
|
|
12
|
-
from mm_eth import erc20, rpc
|
|
13
|
-
from mm_eth.cli import cli_utils, print_helpers, rpc_helpers
|
|
14
|
-
from mm_eth.cli.calcs import calc_eth_expression
|
|
15
|
-
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
16
|
-
from mm_eth.cli.validators import Validators
|
|
17
|
-
from mm_eth.utils import from_wei_str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# noinspection DuplicatedCode
|
|
21
|
-
class Config(BaseConfig):
|
|
22
|
-
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
23
|
-
chain_id: int
|
|
24
|
-
routes: Annotated[list[TxRoute], BeforeValidator(Validators.eth_routes())]
|
|
25
|
-
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
26
|
-
token: Annotated[str, AfterValidator(Validators.eth_address())]
|
|
27
|
-
decimals: int
|
|
28
|
-
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
29
|
-
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
30
|
-
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
31
|
-
value: Annotated[str, AfterValidator(Validators.valid_token_expression("balance"))]
|
|
32
|
-
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_token_expression())] = None
|
|
33
|
-
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
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.routes]
|
|
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
|
-
return self
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class TransferErc20CmdParams(BaseConfigParams):
|
|
52
|
-
print_balances: bool
|
|
53
|
-
debug: bool
|
|
54
|
-
no_receipt: bool
|
|
55
|
-
emulate: bool
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# noinspection DuplicatedCode
|
|
59
|
-
def run(cli_params: TransferErc20CmdParams) -> None:
|
|
60
|
-
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
61
|
-
if cli_params.print_config_and_exit:
|
|
62
|
-
config.print_and_exit({"private_keys"})
|
|
63
|
-
|
|
64
|
-
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
65
|
-
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
66
|
-
|
|
67
|
-
# check decimals
|
|
68
|
-
res = erc20.get_decimals(config.nodes[0], config.token)
|
|
69
|
-
if isinstance(res, Err):
|
|
70
|
-
fatal(f"can't get token decimals: {res.err}")
|
|
71
|
-
if res.ok != config.decimals:
|
|
72
|
-
fatal(f"config.decimals is wrong: {config.decimals} != {res.ok}")
|
|
73
|
-
|
|
74
|
-
if cli_params.print_balances:
|
|
75
|
-
print_helpers.print_balances(
|
|
76
|
-
config.nodes,
|
|
77
|
-
config.from_addresses,
|
|
78
|
-
token_address=config.token,
|
|
79
|
-
token_decimals=config.decimals,
|
|
80
|
-
round_ndigits=config.round_ndigits,
|
|
81
|
-
)
|
|
82
|
-
sys.exit(0)
|
|
83
|
-
|
|
84
|
-
return _run_transfers(config, cli_params)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# noinspection DuplicatedCode
|
|
88
|
-
def _run_transfers(config: Config, cli_params: TransferErc20CmdParams) -> None:
|
|
89
|
-
logger.info(f"started at {utc_now()} UTC")
|
|
90
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
91
|
-
for i, route in enumerate(config.routes):
|
|
92
|
-
_transfer(route, config, cli_params)
|
|
93
|
-
if not cli_params.emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
94
|
-
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
95
|
-
logger.debug(f"delay {delay_value} seconds")
|
|
96
|
-
time.sleep(float(delay_value))
|
|
97
|
-
logger.info(f"finished at {utc_now()} UTC")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# noinspection DuplicatedCode
|
|
101
|
-
def _transfer(route: TxRoute, config: Config, cli_params: TransferErc20CmdParams) -> None:
|
|
102
|
-
log_prefix = f"{route.from_address}->{route.to_address}"
|
|
103
|
-
# get nonce
|
|
104
|
-
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, log_prefix)
|
|
105
|
-
if nonce is None:
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# get max_fee
|
|
109
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, log_prefix)
|
|
110
|
-
if max_fee is None:
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
# check max_fee_limit
|
|
114
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
# get gas
|
|
118
|
-
gas = rpc_helpers.calc_gas(
|
|
119
|
-
nodes=config.nodes,
|
|
120
|
-
gas_expression=config.gas,
|
|
121
|
-
from_address=route.from_address,
|
|
122
|
-
to_address=config.token,
|
|
123
|
-
data=erc20.encode_transfer_input_data(route.to_address, 1234),
|
|
124
|
-
log_prefix=log_prefix,
|
|
125
|
-
)
|
|
126
|
-
if gas is None:
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
# get value
|
|
130
|
-
value = rpc_helpers.calc_erc20_value_for_address(
|
|
131
|
-
nodes=config.nodes,
|
|
132
|
-
value_expression=config.value,
|
|
133
|
-
wallet_address=route.from_address,
|
|
134
|
-
token_address=config.token,
|
|
135
|
-
decimals=config.decimals,
|
|
136
|
-
log_prefix=log_prefix,
|
|
137
|
-
)
|
|
138
|
-
if value is None:
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
# value_min_limit
|
|
142
|
-
if config.value_min_limit is not None:
|
|
143
|
-
value_min_limit = mm_crypto_utils.calc_int_expression(config.value_min_limit, suffix_decimals={"t": config.decimals})
|
|
144
|
-
if value < value_min_limit:
|
|
145
|
-
value_str = from_wei_str(value, "t", config.round_ndigits, decimals=config.decimals)
|
|
146
|
-
logger.info(f"{log_prefix}value<value_min_limit, value={value_str}")
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
priority_fee = calc_eth_expression(config.priority_fee)
|
|
150
|
-
|
|
151
|
-
# emulate?
|
|
152
|
-
if cli_params.emulate:
|
|
153
|
-
msg = f"{log_prefix}: emulate,"
|
|
154
|
-
msg += f" value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)},"
|
|
155
|
-
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
156
|
-
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
157
|
-
msg += f" gas={gas}"
|
|
158
|
-
logger.info(msg)
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
debug_tx_params = {
|
|
162
|
-
"nonce": nonce,
|
|
163
|
-
"max_fee": max_fee,
|
|
164
|
-
"priority_fee": priority_fee,
|
|
165
|
-
"gas": gas,
|
|
166
|
-
"value": value,
|
|
167
|
-
"to": route.to_address,
|
|
168
|
-
"chain_id": config.chain_id,
|
|
169
|
-
}
|
|
170
|
-
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
171
|
-
signed_tx = erc20.sign_transfer_tx(
|
|
172
|
-
nonce=nonce,
|
|
173
|
-
max_fee_per_gas=max_fee,
|
|
174
|
-
max_priority_fee_per_gas=priority_fee,
|
|
175
|
-
gas_limit=gas,
|
|
176
|
-
private_key=config.private_keys[route.from_address],
|
|
177
|
-
chain_id=config.chain_id,
|
|
178
|
-
value=value,
|
|
179
|
-
token_address=config.token,
|
|
180
|
-
recipient_address=route.to_address,
|
|
181
|
-
)
|
|
182
|
-
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
183
|
-
if isinstance(res, Err):
|
|
184
|
-
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
185
|
-
return
|
|
186
|
-
tx_hash = res.ok
|
|
187
|
-
|
|
188
|
-
if cli_params.no_receipt:
|
|
189
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)}" # noqa: E501
|
|
190
|
-
logger.info(msg)
|
|
191
|
-
else:
|
|
192
|
-
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
193
|
-
while True: # TODO: infinite loop if receipt_res is err
|
|
194
|
-
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
195
|
-
if isinstance(receipt_res, Ok):
|
|
196
|
-
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
197
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)}, status={status}" # noqa: E501
|
|
198
|
-
logger.info(msg)
|
|
199
|
-
break
|
|
200
|
-
time.sleep(1)
|
|
@@ -1,182 +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, Ok, utc_now
|
|
10
|
-
from pydantic import AfterValidator, BeforeValidator, model_validator
|
|
11
|
-
|
|
12
|
-
from mm_eth import rpc
|
|
13
|
-
from mm_eth.cli import cli_utils, print_helpers, rpc_helpers
|
|
14
|
-
from mm_eth.cli.calcs import calc_eth_expression
|
|
15
|
-
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
16
|
-
from mm_eth.cli.validators import Validators as Validators
|
|
17
|
-
from mm_eth.tx import sign_tx
|
|
18
|
-
from mm_eth.utils import from_wei_str
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# noinspection DuplicatedCode
|
|
22
|
-
class Config(BaseConfig):
|
|
23
|
-
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
24
|
-
chain_id: int
|
|
25
|
-
routes: Annotated[list[TxRoute], BeforeValidator(Validators.eth_routes())]
|
|
26
|
-
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
27
|
-
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
28
|
-
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
29
|
-
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
30
|
-
value: Annotated[str, AfterValidator(Validators.valid_eth_expression("balance"))]
|
|
31
|
-
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
32
|
-
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
33
|
-
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
34
|
-
round_ndigits: int = 5
|
|
35
|
-
log_debug: Annotated[Path | None, AfterValidator(Validators.log_file())] = None
|
|
36
|
-
log_info: Annotated[Path | None, AfterValidator(Validators.log_file())] = None
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def from_addresses(self) -> list[str]:
|
|
40
|
-
return [r.from_address for r in self.routes]
|
|
41
|
-
|
|
42
|
-
@model_validator(mode="after")
|
|
43
|
-
def final_validator(self) -> Self:
|
|
44
|
-
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
45
|
-
raise ValueError("private keys are not set for all addresses")
|
|
46
|
-
|
|
47
|
-
return self
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class TransferEthCmdParams(BaseConfigParams):
|
|
51
|
-
print_balances: bool
|
|
52
|
-
debug: bool
|
|
53
|
-
no_receipt: bool
|
|
54
|
-
emulate: bool
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def run(cli_params: TransferEthCmdParams) -> None:
|
|
58
|
-
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
59
|
-
if cli_params.print_config_and_exit:
|
|
60
|
-
config.print_and_exit({"private_keys"})
|
|
61
|
-
|
|
62
|
-
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
63
|
-
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
64
|
-
|
|
65
|
-
if cli_params.print_balances:
|
|
66
|
-
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
67
|
-
sys.exit(0)
|
|
68
|
-
|
|
69
|
-
return _run_transfers(config, cli_params)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# noinspection DuplicatedCode
|
|
73
|
-
def _run_transfers(config: Config, cli_params: TransferEthCmdParams) -> None:
|
|
74
|
-
logger.info(f"started at {utc_now()} UTC")
|
|
75
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
76
|
-
for i, route in enumerate(config.routes):
|
|
77
|
-
_transfer(route, config, cli_params)
|
|
78
|
-
if not cli_params.emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
79
|
-
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
80
|
-
logger.debug(f"delay {delay_value} seconds")
|
|
81
|
-
time.sleep(float(delay_value))
|
|
82
|
-
logger.info(f"finished at {utc_now()} UTC")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# noinspection DuplicatedCode
|
|
86
|
-
def _transfer(route: TxRoute, config: Config, cli_params: TransferEthCmdParams) -> None:
|
|
87
|
-
log_prefix = f"{route.from_address}->{route.to_address}"
|
|
88
|
-
# get nonce
|
|
89
|
-
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, log_prefix)
|
|
90
|
-
if nonce is None:
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
# get max_fee
|
|
94
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, log_prefix)
|
|
95
|
-
if max_fee is None:
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
# check max_fee_limit
|
|
99
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
# get gas
|
|
103
|
-
gas = rpc_helpers.calc_gas(
|
|
104
|
-
nodes=config.nodes,
|
|
105
|
-
gas_expression=config.gas,
|
|
106
|
-
from_address=route.from_address,
|
|
107
|
-
to_address=route.to_address,
|
|
108
|
-
value=123,
|
|
109
|
-
log_prefix=log_prefix,
|
|
110
|
-
)
|
|
111
|
-
if gas is None:
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
# get value
|
|
115
|
-
value = rpc_helpers.calc_eth_value_for_address(
|
|
116
|
-
nodes=config.nodes,
|
|
117
|
-
value_expression=config.value,
|
|
118
|
-
address=route.from_address,
|
|
119
|
-
gas=gas,
|
|
120
|
-
max_fee=max_fee,
|
|
121
|
-
log_prefix=log_prefix,
|
|
122
|
-
)
|
|
123
|
-
if value is None:
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
# value_min_limit
|
|
127
|
-
if config.value_min_limit is not None:
|
|
128
|
-
value_min_limit = calc_eth_expression(config.value_min_limit)
|
|
129
|
-
if value < value_min_limit:
|
|
130
|
-
logger.info(f"{log_prefix}value<value_min_limit, value={from_wei_str(value, 'eth', config.round_ndigits)}")
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
priority_fee = calc_eth_expression(config.priority_fee)
|
|
134
|
-
|
|
135
|
-
# emulate?
|
|
136
|
-
if cli_params.emulate:
|
|
137
|
-
msg = f"{log_prefix}: emulate, value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
138
|
-
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
139
|
-
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
140
|
-
msg += f" gas={gas}"
|
|
141
|
-
logger.info(msg)
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
debug_tx_params = {
|
|
145
|
-
"nonce": nonce,
|
|
146
|
-
"max_fee": max_fee,
|
|
147
|
-
"priority_fee": priority_fee,
|
|
148
|
-
"gas": gas,
|
|
149
|
-
"value": value,
|
|
150
|
-
"to": route.to_address,
|
|
151
|
-
"chain_id": config.chain_id,
|
|
152
|
-
}
|
|
153
|
-
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
154
|
-
signed_tx = sign_tx(
|
|
155
|
-
nonce=nonce,
|
|
156
|
-
max_fee_per_gas=max_fee,
|
|
157
|
-
max_priority_fee_per_gas=priority_fee,
|
|
158
|
-
gas=gas,
|
|
159
|
-
private_key=config.private_keys[route.from_address],
|
|
160
|
-
chain_id=config.chain_id,
|
|
161
|
-
value=value,
|
|
162
|
-
to=route.to_address,
|
|
163
|
-
)
|
|
164
|
-
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
165
|
-
if isinstance(res, Err):
|
|
166
|
-
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
167
|
-
return
|
|
168
|
-
tx_hash = res.ok
|
|
169
|
-
|
|
170
|
-
if cli_params.no_receipt:
|
|
171
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 'ether', round_ndigits=config.round_ndigits)}"
|
|
172
|
-
logger.info(msg)
|
|
173
|
-
else:
|
|
174
|
-
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
175
|
-
while True: # TODO: infinite loop if receipt_res is err
|
|
176
|
-
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
177
|
-
if isinstance(receipt_res, Ok):
|
|
178
|
-
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
179
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 'ether', round_ndigits=config.round_ndigits)}, status={status}" # noqa: E501
|
|
180
|
-
logger.info(msg)
|
|
181
|
-
break
|
|
182
|
-
time.sleep(1)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
routes = """
|
|
2
|
-
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
3
|
-
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
4
|
-
file: /path/from_addresses.txt /path/to_addresses.txt
|
|
5
|
-
"""
|
|
6
|
-
private_keys = """
|
|
7
|
-
# 0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
8
|
-
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
9
|
-
file: /path/to/private_keys.txt
|
|
10
|
-
"""
|
|
11
|
-
max_fee = "1.2base + 1gwei + random(1, 200)" # supported var_name=base
|
|
12
|
-
max_fee_limit = "10.1gwei - random(1, 10)" # optional
|
|
13
|
-
priority_fee = "1gwei + random(1, 12)"
|
|
14
|
-
gas = "estimate + random(100, 200) - 19" # supported var_name=estimate
|
|
15
|
-
value = "balance - random(0.002eth, 0.0025eth) + 11gwei" # supported var_name=balance. If 'balance' is used, value=calc(value) - gas*max_fee_per_gas
|
|
16
|
-
value_min_limit = "0.001eth + random(1, 2) - 7gwei"
|
|
17
|
-
|
|
18
|
-
delay = "random(1.123,10) + 1" # secs
|
|
19
|
-
log_debug = "/path/to/file_debug.log" # optional
|
|
20
|
-
log_info = "/path/to/file_info.log" # optional
|
|
21
|
-
round_ndigits = 6
|
|
22
|
-
chain_id = 421613
|
|
23
|
-
nodes = """
|
|
24
|
-
https://arbitrum-goerli.publicnode.com
|
|
25
|
-
https://rpc.goerli.arbitrum.gateway.fm
|
|
26
|
-
"""
|
|
File without changes
|
|
File without changes
|