mm-eth 0.4.1__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mm_eth-0.4.1 → mm_eth-0.5.1}/.gitignore +1 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/PKG-INFO +2 -2
- {mm_eth-0.4.1 → mm_eth-0.5.1}/dict.dic +1 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/pyproject.toml +3 -3
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cli.py +21 -21
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/transfer_cmd.py +80 -55
- mm_eth-0.5.1/src/mm_eth/cli/examples/transfer.toml +45 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/validators.py +3 -3
- {mm_eth-0.4.1 → mm_eth-0.5.1}/uv.lock +18 -40
- mm_eth-0.4.1/src/mm_eth/cli/cmd/send_contract_cmd.py +0 -197
- mm_eth-0.4.1/src/mm_eth/cli/examples/transfer.toml +0 -39
- {mm_eth-0.4.1 → mm_eth-0.5.1}/README.txt +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/justfile +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/abi.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/account.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/anvil.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/calcs.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cli_utils.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/balance_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/balances_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/call_contract_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/deploy_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/encode_input_data_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/example_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/node_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/rpc_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/solc_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/token_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/tx_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/vault_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/wallet/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/wallet/mnemonic_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/cmd/wallet/private_key_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/examples/balances.toml +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/examples/call_contract.toml +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/print_helpers.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/cli/rpc_helpers.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/constants.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/deploy.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/ens.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/erc20.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/ethernodes.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/json_encoder.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/py.typed +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/rpc.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/solc.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/tx.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/utils.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/src/mm_eth/vault.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/cmd/__init__.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/cmd/test_balance_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/cmd/test_mnemonic_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/cmd/test_node_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/cmd/test_private_key_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/cmd/test_solc_cmd.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/cli/test_calcs.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/conftest.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/contracts/ERC20.sol +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/contracts/abi/ERC20.json +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/test_abi.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/test_account.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/test_ethernodes.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/test_rpc.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/test_tx.py +0 -0
- {mm_eth-0.4.1 → mm_eth-0.5.1}/tests/test_utils.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
|
-
Requires-Dist: mm-crypto-utils>=0.
|
|
5
|
+
Requires-Dist: mm-crypto-utils>=0.2.3
|
|
6
6
|
Requires-Dist: typer>=0.15.1
|
|
7
7
|
Requires-Dist: web3~=7.8.0
|
|
8
8
|
Requires-Dist: websocket-client~=1.8.0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-eth"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-crypto-utils>=0.
|
|
7
|
+
"mm-crypto-utils>=0.2.3",
|
|
8
8
|
"websocket-client~=1.8.0",
|
|
9
9
|
"web3~=7.8.0",
|
|
10
10
|
"typer>=0.15.1",
|
|
@@ -21,7 +21,7 @@ dev-dependencies = [
|
|
|
21
21
|
"pytest~=8.3.4",
|
|
22
22
|
"pytest-xdist~=3.6.1",
|
|
23
23
|
"ruff~=0.9.5",
|
|
24
|
-
"pip-audit~=2.
|
|
24
|
+
"pip-audit~=2.8.0",
|
|
25
25
|
"bandit~=1.8.2",
|
|
26
26
|
"mypy~=1.15.0",
|
|
27
27
|
"types-pyyaml>=6.0.12.20241230",
|
|
@@ -17,7 +17,6 @@ from .cmd import (
|
|
|
17
17
|
example_cmd,
|
|
18
18
|
node_cmd,
|
|
19
19
|
rpc_cmd,
|
|
20
|
-
send_contract_cmd,
|
|
21
20
|
solc_cmd,
|
|
22
21
|
token_cmd,
|
|
23
22
|
transfer_cmd,
|
|
@@ -27,7 +26,6 @@ from .cmd import (
|
|
|
27
26
|
from .cmd.balances_cmd import BalancesCmdParams
|
|
28
27
|
from .cmd.call_contract_cmd import CallContractCmdParams
|
|
29
28
|
from .cmd.deploy_cmd import DeployCmdParams
|
|
30
|
-
from .cmd.send_contract_cmd import SendContractCmdParams
|
|
31
29
|
from .cmd.transfer_cmd import TransferCmdParams
|
|
32
30
|
from .cmd.wallet import mnemonic_cmd, private_key_cmd
|
|
33
31
|
|
|
@@ -147,6 +145,7 @@ def tx_command(
|
|
|
147
145
|
def transfer_command(
|
|
148
146
|
config_path: Path,
|
|
149
147
|
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
148
|
+
print_transfers: bool = typer.Option(False, "--transfers", "-t", help="Print transfers (from, to, value) and exit"),
|
|
150
149
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
151
150
|
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
152
151
|
skip_receipt: bool = typer.Option(False, "--skip-receipt", help="Don't wait for a tx receipt"),
|
|
@@ -156,6 +155,7 @@ def transfer_command(
|
|
|
156
155
|
TransferCmdParams(
|
|
157
156
|
config_path=config_path,
|
|
158
157
|
print_balances=print_balances,
|
|
158
|
+
print_transfers=print_transfers,
|
|
159
159
|
print_config=print_config,
|
|
160
160
|
debug=debug,
|
|
161
161
|
skip_receipt=skip_receipt,
|
|
@@ -164,25 +164,25 @@ def transfer_command(
|
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
|
|
167
|
-
@app.command(name="send-contract", help="Send transactions to a contract")
|
|
168
|
-
def send_contract_command(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
) -> None:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
# @app.command(name="send-contract", help="Send transactions to a contract")
|
|
168
|
+
# def send_contract_command(
|
|
169
|
+
# config_path: Path,
|
|
170
|
+
# print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
171
|
+
# print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
172
|
+
# emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
173
|
+
# no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
174
|
+
# debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
175
|
+
# ) -> None:
|
|
176
|
+
# send_contract_cmd.run(
|
|
177
|
+
# SendContractCmdParams(
|
|
178
|
+
# config_path=config_path,
|
|
179
|
+
# print_balances=print_balances,
|
|
180
|
+
# print_config=print_config,
|
|
181
|
+
# debug=debug,
|
|
182
|
+
# no_receipt=no_receipt,
|
|
183
|
+
# emulate=emulate,
|
|
184
|
+
# )
|
|
185
|
+
# )
|
|
186
186
|
|
|
187
187
|
|
|
188
188
|
@app.command(name="balances", help="Print base and ERC20 token balances")
|
|
@@ -5,9 +5,10 @@ from typing import Annotated, Self
|
|
|
5
5
|
|
|
6
6
|
import mm_crypto_utils
|
|
7
7
|
from loguru import logger
|
|
8
|
-
from mm_crypto_utils import AddressToPrivate,
|
|
8
|
+
from mm_crypto_utils import AddressToPrivate, Transfer
|
|
9
9
|
from mm_std import BaseConfig, Err, fatal, utc_now
|
|
10
10
|
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
11
|
+
from rich.console import Console
|
|
11
12
|
from rich.live import Live
|
|
12
13
|
from rich.table import Table
|
|
13
14
|
|
|
@@ -23,14 +24,14 @@ from mm_eth.utils import from_wei_str
|
|
|
23
24
|
class Config(BaseConfig):
|
|
24
25
|
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
25
26
|
chain_id: int
|
|
26
|
-
|
|
27
|
+
transfers: Annotated[list[Transfer], BeforeValidator(Validators.eth_transfers())]
|
|
27
28
|
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
28
29
|
token: Annotated[str | None, AfterValidator(Validators.eth_address())] = None # if None, then eth transfer
|
|
29
30
|
token_decimals: int = -1
|
|
30
31
|
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
31
32
|
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
32
33
|
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
33
|
-
|
|
34
|
+
default_value: Annotated[str | None, AfterValidator(Validators.valid_eth_or_token_expression("balance"))] = None
|
|
34
35
|
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_or_token_expression())] = None
|
|
35
36
|
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
36
37
|
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
@@ -42,19 +43,28 @@ class Config(BaseConfig):
|
|
|
42
43
|
|
|
43
44
|
@property
|
|
44
45
|
def from_addresses(self) -> list[str]:
|
|
45
|
-
return [r.from_address for r in self.
|
|
46
|
+
return [r.from_address for r in self.transfers]
|
|
46
47
|
|
|
47
48
|
@model_validator(mode="after")
|
|
48
49
|
def final_validator(self) -> Self:
|
|
49
50
|
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
50
51
|
raise ValueError("private keys are not set for all addresses")
|
|
51
52
|
|
|
53
|
+
for transfer in self.transfers: # If value is not set for a transfer, then set it to the global value of the config.
|
|
54
|
+
if not transfer.value and self.default_value:
|
|
55
|
+
transfer.value = self.default_value
|
|
56
|
+
for transfer in self.transfers: # Check all transfers have a value.
|
|
57
|
+
if not transfer.value:
|
|
58
|
+
raise ValueError(f"{transfer.log_prefix}: value is not set")
|
|
59
|
+
|
|
52
60
|
if self.token:
|
|
53
|
-
|
|
61
|
+
if self.default_value:
|
|
62
|
+
Validators.valid_token_expression("balance")(self.default_value)
|
|
54
63
|
if self.value_min_limit:
|
|
55
64
|
Validators.valid_token_expression()(self.value_min_limit)
|
|
56
65
|
else:
|
|
57
|
-
|
|
66
|
+
if self.default_value:
|
|
67
|
+
Validators.valid_eth_expression("balance")(self.default_value)
|
|
58
68
|
if self.value_min_limit:
|
|
59
69
|
Validators.valid_eth_expression()(self.value_min_limit)
|
|
60
70
|
|
|
@@ -69,6 +79,7 @@ class Config(BaseConfig):
|
|
|
69
79
|
|
|
70
80
|
class TransferCmdParams(BaseConfigParams):
|
|
71
81
|
print_balances: bool
|
|
82
|
+
print_transfers: bool
|
|
72
83
|
debug: bool
|
|
73
84
|
skip_receipt: bool
|
|
74
85
|
emulate: bool
|
|
@@ -82,6 +93,10 @@ def run(cmd_params: TransferCmdParams) -> None:
|
|
|
82
93
|
|
|
83
94
|
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
84
95
|
|
|
96
|
+
if cmd_params.print_transfers:
|
|
97
|
+
_print_transfers(config)
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
85
100
|
if cmd_params.print_balances:
|
|
86
101
|
_print_balances(config)
|
|
87
102
|
sys.exit(0)
|
|
@@ -93,9 +108,9 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
93
108
|
mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
|
|
94
109
|
logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
|
|
95
110
|
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
96
|
-
for i,
|
|
97
|
-
_transfer(
|
|
98
|
-
if config.delay is not None and i < len(config.
|
|
111
|
+
for i, transfer in enumerate(config.transfers):
|
|
112
|
+
_transfer(transfer, config, cmd_params)
|
|
113
|
+
if config.delay is not None and i < len(config.transfers) - 1:
|
|
99
114
|
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
100
115
|
logger.info(f"delay {delay_value} seconds")
|
|
101
116
|
if not cmd_params.emulate:
|
|
@@ -103,34 +118,34 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
103
118
|
logger.info(f"finished at {utc_now()} UTC")
|
|
104
119
|
|
|
105
120
|
|
|
106
|
-
def _transfer(
|
|
107
|
-
nonce = rpc_helpers.get_nonce(config.nodes,
|
|
121
|
+
def _transfer(t: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
122
|
+
nonce = rpc_helpers.get_nonce(config.nodes, t.from_address, t.log_prefix)
|
|
108
123
|
if nonce is None:
|
|
109
124
|
return
|
|
110
125
|
|
|
111
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee,
|
|
126
|
+
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, t.log_prefix)
|
|
112
127
|
if max_fee is None:
|
|
113
128
|
return
|
|
114
129
|
|
|
115
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit,
|
|
130
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, t.log_prefix):
|
|
116
131
|
return
|
|
117
132
|
|
|
118
|
-
gas = _calc_gas(
|
|
133
|
+
gas = _calc_gas(t, config)
|
|
119
134
|
if gas is None:
|
|
120
135
|
return
|
|
121
136
|
|
|
122
|
-
value = _calc_value(
|
|
137
|
+
value = _calc_value(t, max_fee=max_fee, gas=gas, config=config)
|
|
123
138
|
if value is None:
|
|
124
139
|
return
|
|
125
140
|
|
|
126
|
-
if not _check_value_min_limit(
|
|
141
|
+
if not _check_value_min_limit(t, value, config):
|
|
127
142
|
return
|
|
128
143
|
|
|
129
144
|
priority_fee = calc_eth_expression(config.priority_fee)
|
|
130
145
|
|
|
131
146
|
# emulate?
|
|
132
147
|
if cmd_params.emulate:
|
|
133
|
-
msg = f"{
|
|
148
|
+
msg = f"{t.log_prefix}: emulate,"
|
|
134
149
|
msg += f" value={_value_with_suffix(value, config)},"
|
|
135
150
|
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
136
151
|
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
@@ -138,39 +153,39 @@ def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) ->
|
|
|
138
153
|
logger.info(msg)
|
|
139
154
|
return
|
|
140
155
|
|
|
141
|
-
tx_hash = _send_tx(
|
|
156
|
+
tx_hash = _send_tx(transfer=t, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
|
|
142
157
|
if tx_hash is None:
|
|
143
158
|
return
|
|
144
159
|
|
|
145
160
|
status = "UNKNOWN"
|
|
146
161
|
if not cmd_params.skip_receipt:
|
|
147
|
-
logger.debug(f"{
|
|
162
|
+
logger.debug(f"{t.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
|
|
148
163
|
status = cli_utils.wait_tx_status(config.nodes, config.proxies, tx_hash, config.wait_tx_timeout)
|
|
149
164
|
|
|
150
|
-
logger.info(f"{
|
|
165
|
+
logger.info(f"{t.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
|
|
151
166
|
|
|
152
167
|
|
|
153
|
-
def _calc_value(
|
|
168
|
+
def _calc_value(transfer: Transfer, max_fee: int, gas: int, config: Config) -> int | None:
|
|
154
169
|
if config.token:
|
|
155
170
|
return rpc_helpers.calc_erc20_value_for_address(
|
|
156
171
|
nodes=config.nodes,
|
|
157
|
-
value_expression=
|
|
158
|
-
wallet_address=
|
|
172
|
+
value_expression=transfer.value,
|
|
173
|
+
wallet_address=transfer.from_address,
|
|
159
174
|
token_address=config.token,
|
|
160
175
|
decimals=config.token_decimals,
|
|
161
|
-
log_prefix=
|
|
176
|
+
log_prefix=transfer.log_prefix,
|
|
162
177
|
)
|
|
163
178
|
return rpc_helpers.calc_eth_value_for_address(
|
|
164
179
|
nodes=config.nodes,
|
|
165
|
-
value_expression=
|
|
166
|
-
address=
|
|
180
|
+
value_expression=transfer.value,
|
|
181
|
+
address=transfer.from_address,
|
|
167
182
|
gas=gas,
|
|
168
183
|
max_fee=max_fee,
|
|
169
|
-
log_prefix=
|
|
184
|
+
log_prefix=transfer.log_prefix,
|
|
170
185
|
)
|
|
171
186
|
|
|
172
187
|
|
|
173
|
-
def _check_value_min_limit(
|
|
188
|
+
def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
|
|
174
189
|
"""Returns False if the transfer should be skipped."""
|
|
175
190
|
if config.value_min_limit:
|
|
176
191
|
if config.token:
|
|
@@ -178,21 +193,23 @@ def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
|
|
|
178
193
|
else:
|
|
179
194
|
value_min_limit = calcs.calc_eth_expression(config.value_min_limit)
|
|
180
195
|
if value < value_min_limit:
|
|
181
|
-
logger.info(f"{
|
|
196
|
+
logger.info(f"{transfer.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
|
|
182
197
|
return True
|
|
183
198
|
|
|
184
199
|
|
|
185
|
-
def _send_tx(
|
|
200
|
+
def _send_tx(
|
|
201
|
+
*, transfer: Transfer, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config
|
|
202
|
+
) -> str | None:
|
|
186
203
|
debug_tx_params = {
|
|
187
204
|
"nonce": nonce,
|
|
188
205
|
"max_fee": max_fee,
|
|
189
206
|
"priority_fee": priority_fee,
|
|
190
207
|
"gas": gas,
|
|
191
208
|
"value": value,
|
|
192
|
-
"to":
|
|
209
|
+
"to": transfer.to_address,
|
|
193
210
|
"chain_id": config.chain_id,
|
|
194
211
|
}
|
|
195
|
-
logger.debug(f"{
|
|
212
|
+
logger.debug(f"{transfer.log_prefix}: tx_params={debug_tx_params}")
|
|
196
213
|
|
|
197
214
|
if config.token:
|
|
198
215
|
signed_tx = erc20.sign_transfer_tx(
|
|
@@ -200,11 +217,11 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
|
|
|
200
217
|
max_fee_per_gas=max_fee,
|
|
201
218
|
max_priority_fee_per_gas=priority_fee,
|
|
202
219
|
gas_limit=gas,
|
|
203
|
-
private_key=config.private_keys[
|
|
220
|
+
private_key=config.private_keys[transfer.from_address],
|
|
204
221
|
chain_id=config.chain_id,
|
|
205
222
|
value=value,
|
|
206
223
|
token_address=config.token,
|
|
207
|
-
recipient_address=
|
|
224
|
+
recipient_address=transfer.to_address,
|
|
208
225
|
)
|
|
209
226
|
else:
|
|
210
227
|
signed_tx = sign_tx(
|
|
@@ -212,38 +229,46 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
|
|
|
212
229
|
max_fee_per_gas=max_fee,
|
|
213
230
|
max_priority_fee_per_gas=priority_fee,
|
|
214
231
|
gas=gas,
|
|
215
|
-
private_key=config.private_keys[
|
|
232
|
+
private_key=config.private_keys[transfer.from_address],
|
|
216
233
|
chain_id=config.chain_id,
|
|
217
234
|
value=value,
|
|
218
|
-
to=
|
|
235
|
+
to=transfer.to_address,
|
|
219
236
|
)
|
|
220
237
|
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
221
238
|
if isinstance(res, Err):
|
|
222
|
-
logger.info(f"{
|
|
239
|
+
logger.info(f"{transfer.log_prefix}: tx error {res.err}")
|
|
223
240
|
return None
|
|
224
241
|
return res.ok
|
|
225
242
|
|
|
226
243
|
|
|
227
|
-
def _calc_gas(
|
|
244
|
+
def _calc_gas(transfer: Transfer, config: Config) -> int | None:
|
|
228
245
|
if config.token:
|
|
229
246
|
return rpc_helpers.calc_gas(
|
|
230
247
|
nodes=config.nodes,
|
|
231
248
|
gas_expression=config.gas,
|
|
232
|
-
from_address=
|
|
249
|
+
from_address=transfer.from_address,
|
|
233
250
|
to_address=config.token,
|
|
234
|
-
data=erc20.encode_transfer_input_data(
|
|
235
|
-
log_prefix=
|
|
251
|
+
data=erc20.encode_transfer_input_data(transfer.to_address, 1234),
|
|
252
|
+
log_prefix=transfer.log_prefix,
|
|
236
253
|
)
|
|
237
254
|
return rpc_helpers.calc_gas(
|
|
238
255
|
nodes=config.nodes,
|
|
239
256
|
gas_expression=config.gas,
|
|
240
|
-
from_address=
|
|
241
|
-
to_address=
|
|
257
|
+
from_address=transfer.from_address,
|
|
258
|
+
to_address=transfer.to_address,
|
|
242
259
|
value=123,
|
|
243
|
-
log_prefix=
|
|
260
|
+
log_prefix=transfer.log_prefix,
|
|
244
261
|
)
|
|
245
262
|
|
|
246
263
|
|
|
264
|
+
def _print_transfers(config: Config) -> None:
|
|
265
|
+
table = Table("n", "from_address", "to_address", "value", title="transfers")
|
|
266
|
+
for count, transfer in enumerate(config.transfers, start=1):
|
|
267
|
+
table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
|
|
268
|
+
console = Console()
|
|
269
|
+
console.print(table)
|
|
270
|
+
|
|
271
|
+
|
|
247
272
|
def _print_balances(config: Config) -> None:
|
|
248
273
|
if config.token:
|
|
249
274
|
headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
|
|
@@ -251,24 +276,24 @@ def _print_balances(config: Config) -> None:
|
|
|
251
276
|
headers = ["n", "from_address", "nonce", "eth", "to_address", "nonce", "eth"]
|
|
252
277
|
table = Table(*headers, title="balances")
|
|
253
278
|
with Live(table, refresh_per_second=0.5):
|
|
254
|
-
for count,
|
|
255
|
-
from_nonce = _get_nonce_str(
|
|
256
|
-
to_nonce = _get_nonce_str(
|
|
279
|
+
for count, transfer in enumerate(config.transfers):
|
|
280
|
+
from_nonce = _get_nonce_str(transfer.from_address, config)
|
|
281
|
+
to_nonce = _get_nonce_str(transfer.to_address, config)
|
|
257
282
|
|
|
258
|
-
from_eth_balance = _get_eth_balance_str(
|
|
259
|
-
to_eth_balance = _get_eth_balance_str(
|
|
283
|
+
from_eth_balance = _get_eth_balance_str(transfer.from_address, config)
|
|
284
|
+
to_eth_balance = _get_eth_balance_str(transfer.to_address, config)
|
|
260
285
|
|
|
261
|
-
from_token_balance = _get_token_balance_str(
|
|
262
|
-
to_token_balance = _get_token_balance_str(
|
|
286
|
+
from_token_balance = _get_token_balance_str(transfer.from_address, config) if config.token else ""
|
|
287
|
+
to_token_balance = _get_token_balance_str(transfer.to_address, config) if config.token else ""
|
|
263
288
|
|
|
264
289
|
if config.token:
|
|
265
290
|
table.add_row(
|
|
266
291
|
str(count),
|
|
267
|
-
|
|
292
|
+
transfer.from_address,
|
|
268
293
|
from_nonce,
|
|
269
294
|
from_eth_balance,
|
|
270
295
|
from_token_balance,
|
|
271
|
-
|
|
296
|
+
transfer.to_address,
|
|
272
297
|
to_nonce,
|
|
273
298
|
to_eth_balance,
|
|
274
299
|
to_token_balance,
|
|
@@ -276,10 +301,10 @@ def _print_balances(config: Config) -> None:
|
|
|
276
301
|
else:
|
|
277
302
|
table.add_row(
|
|
278
303
|
str(count),
|
|
279
|
-
|
|
304
|
+
transfer.from_address,
|
|
280
305
|
from_nonce,
|
|
281
306
|
from_eth_balance,
|
|
282
|
-
|
|
307
|
+
transfer.to_address,
|
|
283
308
|
to_nonce,
|
|
284
309
|
to_eth_balance,
|
|
285
310
|
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# from_address to_address value -- is a format for each transfer, value is optional
|
|
2
|
+
# 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
|
+
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b 0.1t # comments are allowed here
|
|
6
|
+
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f 0.2balance-random(0.1t,0.5t)
|
|
7
|
+
0x10ecB8d838746643E613f6B5218C8e342593225c 0xE19242B72a4833eD86F1b2015d4E59052A2b278b
|
|
8
|
+
file: /path/to/other_transfers.txt # transfers from this file will be added
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
private_keys = """
|
|
12
|
+
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
13
|
+
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
14
|
+
file: /path/to/other_private_keys.txt
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
18
|
+
|
|
19
|
+
max_fee = "1.2base_fee+1gwei+random(1,200)" # supported var_name=base_fee
|
|
20
|
+
|
|
21
|
+
max_fee_limit = "10.1gwei-random(1,10)" # optional
|
|
22
|
+
|
|
23
|
+
priority_fee = "1gwei+random(1,12)"
|
|
24
|
+
|
|
25
|
+
gas = "estimate+random(100,200)-19" # supported var_name=estimate
|
|
26
|
+
|
|
27
|
+
# default_value is used if transfer.value is not set in transfers. It's optional.
|
|
28
|
+
default_value = "0.5balance-random(1.5t,3t)+11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
29
|
+
|
|
30
|
+
value_min_limit = "0.5t+random(1,2)-7" # don't transfer if transfer.value is less than this
|
|
31
|
+
|
|
32
|
+
delay = "random(1.123,10)+1" # secs, optional
|
|
33
|
+
|
|
34
|
+
log_debug = "/path/to/file_debug.log" # optional
|
|
35
|
+
|
|
36
|
+
log_info = "/path/to/file_info.log" # optional
|
|
37
|
+
|
|
38
|
+
round_ndigits = 6 # optional, default=5
|
|
39
|
+
|
|
40
|
+
chain_id = 421613
|
|
41
|
+
|
|
42
|
+
nodes = """
|
|
43
|
+
https://arbitrum-goerli.publicnode.com
|
|
44
|
+
https://rpc.goerli.arbitrum.gateway.fm
|
|
45
|
+
"""
|
|
@@ -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_eth.account import address_from_private, is_address
|
|
6
6
|
from mm_eth.constants import SUFFIX_DECIMALS
|
|
@@ -22,8 +22,8 @@ class Validators(ConfigValidators):
|
|
|
22
22
|
return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS | {"t": 6})
|
|
23
23
|
|
|
24
24
|
@staticmethod
|
|
25
|
-
def
|
|
26
|
-
return ConfigValidators.
|
|
25
|
+
def eth_transfers() -> Callable[[str], list[Transfer]]:
|
|
26
|
+
return ConfigValidators.transfers(is_address, to_lower=True)
|
|
27
27
|
|
|
28
28
|
@staticmethod
|
|
29
29
|
def eth_private_keys() -> Callable[[str], AddressToPrivate]:
|
|
@@ -602,19 +602,6 @@ wheels = [
|
|
|
602
602
|
{ url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 },
|
|
603
603
|
]
|
|
604
604
|
|
|
605
|
-
[[package]]
|
|
606
|
-
name = "html5lib"
|
|
607
|
-
version = "1.1"
|
|
608
|
-
source = { registry = "https://pypi.org/simple" }
|
|
609
|
-
dependencies = [
|
|
610
|
-
{ name = "six" },
|
|
611
|
-
{ name = "webencodings" },
|
|
612
|
-
]
|
|
613
|
-
sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 }
|
|
614
|
-
wheels = [
|
|
615
|
-
{ url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 },
|
|
616
|
-
]
|
|
617
|
-
|
|
618
605
|
[[package]]
|
|
619
606
|
name = "httpcore"
|
|
620
607
|
version = "1.0.7"
|
|
@@ -726,19 +713,19 @@ wheels = [
|
|
|
726
713
|
|
|
727
714
|
[[package]]
|
|
728
715
|
name = "mm-crypto-utils"
|
|
729
|
-
version = "0.
|
|
716
|
+
version = "0.2.3"
|
|
730
717
|
source = { registry = "https://pypi.org/simple" }
|
|
731
718
|
dependencies = [
|
|
732
719
|
{ name = "loguru" },
|
|
733
720
|
{ name = "mm-std" },
|
|
734
721
|
]
|
|
735
722
|
wheels = [
|
|
736
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
723
|
+
{ url = "https://files.pythonhosted.org/packages/78/dd/3cb0ddc4ab14a78a0e828fd1f7ca97298be8eff67b53b6198853e2013976/mm_crypto_utils-0.2.3-py3-none-any.whl", hash = "sha256:33ca42ef22c847c6f92973d7c799460926ee5c5bd7c872d3c7d19f31377e8833", size = 8257 },
|
|
737
724
|
]
|
|
738
725
|
|
|
739
726
|
[[package]]
|
|
740
727
|
name = "mm-eth"
|
|
741
|
-
version = "0.
|
|
728
|
+
version = "0.5.1"
|
|
742
729
|
source = { editable = "." }
|
|
743
730
|
dependencies = [
|
|
744
731
|
{ name = "mm-crypto-utils" },
|
|
@@ -760,7 +747,7 @@ dev = [
|
|
|
760
747
|
|
|
761
748
|
[package.metadata]
|
|
762
749
|
requires-dist = [
|
|
763
|
-
{ name = "mm-crypto-utils", specifier = ">=0.
|
|
750
|
+
{ name = "mm-crypto-utils", specifier = ">=0.2.3" },
|
|
764
751
|
{ name = "typer", specifier = ">=0.15.1" },
|
|
765
752
|
{ name = "web3", specifier = "~=7.8.0" },
|
|
766
753
|
{ name = "websocket-client", specifier = "~=1.8.0" },
|
|
@@ -770,7 +757,7 @@ requires-dist = [
|
|
|
770
757
|
dev = [
|
|
771
758
|
{ name = "bandit", specifier = "~=1.8.2" },
|
|
772
759
|
{ name = "mypy", specifier = "~=1.15.0" },
|
|
773
|
-
{ name = "pip-audit", specifier = "~=2.
|
|
760
|
+
{ name = "pip-audit", specifier = "~=2.8.0" },
|
|
774
761
|
{ name = "pytest", specifier = "~=8.3.4" },
|
|
775
762
|
{ name = "pytest-xdist", specifier = "~=3.6.1" },
|
|
776
763
|
{ name = "ruff", specifier = "~=0.9.5" },
|
|
@@ -959,22 +946,22 @@ wheels = [
|
|
|
959
946
|
|
|
960
947
|
[[package]]
|
|
961
948
|
name = "pip-audit"
|
|
962
|
-
version = "2.
|
|
949
|
+
version = "2.8.0"
|
|
963
950
|
source = { registry = "https://pypi.org/simple" }
|
|
964
951
|
dependencies = [
|
|
965
952
|
{ name = "cachecontrol", extra = ["filecache"] },
|
|
966
953
|
{ name = "cyclonedx-python-lib" },
|
|
967
|
-
{ name = "html5lib" },
|
|
968
954
|
{ name = "packaging" },
|
|
969
955
|
{ name = "pip-api" },
|
|
970
956
|
{ name = "pip-requirements-parser" },
|
|
957
|
+
{ name = "platformdirs" },
|
|
971
958
|
{ name = "requests" },
|
|
972
959
|
{ name = "rich" },
|
|
973
960
|
{ name = "toml" },
|
|
974
961
|
]
|
|
975
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
962
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e8/c8/44ccea85bd2024f1ebe55eb6cdaf1f2183359176689eed3c0b01926c24ad/pip_audit-2.8.0.tar.gz", hash = "sha256:9816cbd94de6f618a8965c117433006b3d565a708dc05d5a7be47ab65b66fa05", size = 51073 }
|
|
976
963
|
wheels = [
|
|
977
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
964
|
+
{ url = "https://files.pythonhosted.org/packages/11/0c/be5c42643284b2cfc5d9d36b576b7465268a163bd7df481a3979a3d87a0b/pip_audit-2.8.0-py3-none-any.whl", hash = "sha256:200f50d56cb6fba3a9189c20d53250354f72f161d63b6ef77ae5de2b53044566", size = 57002 },
|
|
978
965
|
]
|
|
979
966
|
|
|
980
967
|
[[package]]
|
|
@@ -990,6 +977,15 @@ wheels = [
|
|
|
990
977
|
{ url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648 },
|
|
991
978
|
]
|
|
992
979
|
|
|
980
|
+
[[package]]
|
|
981
|
+
name = "platformdirs"
|
|
982
|
+
version = "4.3.6"
|
|
983
|
+
source = { registry = "https://pypi.org/simple" }
|
|
984
|
+
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
|
|
985
|
+
wheels = [
|
|
986
|
+
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
|
987
|
+
]
|
|
988
|
+
|
|
993
989
|
[[package]]
|
|
994
990
|
name = "pluggy"
|
|
995
991
|
version = "1.5.0"
|
|
@@ -1359,15 +1355,6 @@ wheels = [
|
|
|
1359
1355
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
|
1360
1356
|
]
|
|
1361
1357
|
|
|
1362
|
-
[[package]]
|
|
1363
|
-
name = "six"
|
|
1364
|
-
version = "1.16.0"
|
|
1365
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1366
|
-
sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
|
|
1367
|
-
wheels = [
|
|
1368
|
-
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
|
|
1369
|
-
]
|
|
1370
|
-
|
|
1371
1358
|
[[package]]
|
|
1372
1359
|
name = "sniffio"
|
|
1373
1360
|
version = "1.3.1"
|
|
@@ -1504,15 +1491,6 @@ wheels = [
|
|
|
1504
1491
|
{ url = "https://files.pythonhosted.org/packages/b0/52/bc4a08811db59392e13bf56ada316517a83b9a6135c20d357c222c80be2d/web3-7.8.0-py3-none-any.whl", hash = "sha256:c8771b3d8772f7104a0462804449beb57d36cef7bd8b411140f95a92fc46b559", size = 1363475 },
|
|
1505
1492
|
]
|
|
1506
1493
|
|
|
1507
|
-
[[package]]
|
|
1508
|
-
name = "webencodings"
|
|
1509
|
-
version = "0.5.1"
|
|
1510
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1511
|
-
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 }
|
|
1512
|
-
wheels = [
|
|
1513
|
-
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 },
|
|
1514
|
-
]
|
|
1515
|
-
|
|
1516
1494
|
[[package]]
|
|
1517
1495
|
name = "websocket-client"
|
|
1518
1496
|
version = "1.8.0"
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
import time
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Annotated, Self
|
|
6
|
-
|
|
7
|
-
import mm_crypto_utils
|
|
8
|
-
from loguru import logger
|
|
9
|
-
from mm_crypto_utils import AddressToPrivate
|
|
10
|
-
from mm_std import BaseConfig, Err, Ok, utc_now
|
|
11
|
-
from pydantic import AfterValidator, BeforeValidator, StrictStr, model_validator
|
|
12
|
-
|
|
13
|
-
from mm_eth import abi, rpc
|
|
14
|
-
from mm_eth.cli import calcs, cli_utils, print_helpers, rpc_helpers, validators
|
|
15
|
-
from mm_eth.cli.calcs import calc_eth_expression
|
|
16
|
-
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
17
|
-
from mm_eth.cli.validators import Validators
|
|
18
|
-
from mm_eth.tx import sign_tx
|
|
19
|
-
from mm_eth.utils import from_wei_str
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Config(BaseConfig):
|
|
23
|
-
from_addresses: Annotated[list[str], BeforeValidator(Validators.eth_addresses(unique=True))]
|
|
24
|
-
contract_address: Annotated[str, BeforeValidator(Validators.eth_address())]
|
|
25
|
-
function_signature: str
|
|
26
|
-
function_args: StrictStr = "[]"
|
|
27
|
-
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
28
|
-
chain_id: int
|
|
29
|
-
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
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_expression("balance"))] # eth value
|
|
34
|
-
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
35
|
-
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
36
|
-
round_ndigits: int = 5
|
|
37
|
-
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
|
-
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
39
|
-
|
|
40
|
-
# noinspection DuplicatedCode
|
|
41
|
-
@model_validator(mode="after")
|
|
42
|
-
def final_validator(self) -> Self:
|
|
43
|
-
# check all private keys exist
|
|
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
|
-
# check that from_addresses is not empty
|
|
48
|
-
if not self.from_addresses:
|
|
49
|
-
raise ValueError("from_addresses is empty")
|
|
50
|
-
|
|
51
|
-
# function_args
|
|
52
|
-
if not validators.is_valid_calc_function_args(self.function_args):
|
|
53
|
-
raise ValueError(f"wrong function_args: {self.function_args}")
|
|
54
|
-
|
|
55
|
-
return self
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class SendContractCmdParams(BaseConfigParams):
|
|
59
|
-
print_balances: bool
|
|
60
|
-
debug: bool
|
|
61
|
-
no_receipt: bool
|
|
62
|
-
emulate: bool
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# noinspection DuplicatedCode
|
|
66
|
-
def run(cli_params: SendContractCmdParams) -> None:
|
|
67
|
-
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
68
|
-
if cli_params.print_config:
|
|
69
|
-
config.print_and_exit({"private_key"})
|
|
70
|
-
|
|
71
|
-
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
72
|
-
|
|
73
|
-
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
74
|
-
|
|
75
|
-
if cli_params.print_balances:
|
|
76
|
-
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
77
|
-
sys.exit(0)
|
|
78
|
-
|
|
79
|
-
_run_transfers(config, cli_params)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# noinspection DuplicatedCode
|
|
83
|
-
def _run_transfers(config: Config, cli_params: SendContractCmdParams) -> None:
|
|
84
|
-
logger.info(f"started at {utc_now()} UTC")
|
|
85
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
86
|
-
for i, from_address in enumerate(config.from_addresses):
|
|
87
|
-
_transfer(from_address, config, cli_params)
|
|
88
|
-
if not cli_params.emulate and config.delay is not None and i < len(config.from_addresses) - 1:
|
|
89
|
-
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
90
|
-
logger.debug(f"delay {delay_value} seconds")
|
|
91
|
-
time.sleep(float(delay_value))
|
|
92
|
-
logger.info(f"finished at {utc_now()} UTC")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# noinspection DuplicatedCode
|
|
96
|
-
def _transfer(from_address: str, config: Config, cli_params: SendContractCmdParams) -> None:
|
|
97
|
-
log_prefix = f"{from_address}"
|
|
98
|
-
# get nonce
|
|
99
|
-
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
100
|
-
if nonce is None:
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
# get max_fee
|
|
104
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, log_prefix)
|
|
105
|
-
if max_fee is None:
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# check max_fee_limit
|
|
109
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
priority_fee = calc_eth_expression(config.priority_fee)
|
|
113
|
-
|
|
114
|
-
# data
|
|
115
|
-
function_args = calcs.calc_function_args(config.function_args).replace("'", '"')
|
|
116
|
-
data = abi.encode_function_input_by_signature(config.function_signature, json.loads(function_args))
|
|
117
|
-
|
|
118
|
-
# get gas
|
|
119
|
-
gas = rpc_helpers.calc_gas(
|
|
120
|
-
nodes=config.nodes,
|
|
121
|
-
gas_expression=config.gas,
|
|
122
|
-
from_address=from_address,
|
|
123
|
-
to_address=config.contract_address,
|
|
124
|
-
value=None,
|
|
125
|
-
data=data,
|
|
126
|
-
log_prefix=log_prefix,
|
|
127
|
-
)
|
|
128
|
-
if gas is None:
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
# get value
|
|
132
|
-
value = None
|
|
133
|
-
if config.value is not None:
|
|
134
|
-
value = rpc_helpers.calc_eth_value_for_address(
|
|
135
|
-
nodes=config.nodes,
|
|
136
|
-
value_expression=config.value,
|
|
137
|
-
address=from_address,
|
|
138
|
-
gas=gas,
|
|
139
|
-
max_fee=max_fee,
|
|
140
|
-
log_prefix=log_prefix,
|
|
141
|
-
)
|
|
142
|
-
if value is None:
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
# emulate?
|
|
146
|
-
if cli_params.emulate:
|
|
147
|
-
msg = f"{log_prefix}: emulate,"
|
|
148
|
-
if value is not None:
|
|
149
|
-
msg += f" value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
150
|
-
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
151
|
-
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
152
|
-
msg += f" gas={gas}, "
|
|
153
|
-
msg += f" data={data}"
|
|
154
|
-
logger.info(msg)
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
debug_tx_params = {
|
|
158
|
-
"nonce": nonce,
|
|
159
|
-
"max_fee": max_fee,
|
|
160
|
-
"priority_fee": priority_fee,
|
|
161
|
-
"gas": gas,
|
|
162
|
-
"value": value,
|
|
163
|
-
"data": data,
|
|
164
|
-
"to": config.contract_address,
|
|
165
|
-
"chain_id": config.chain_id,
|
|
166
|
-
}
|
|
167
|
-
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
168
|
-
signed_tx = sign_tx(
|
|
169
|
-
nonce=nonce,
|
|
170
|
-
max_fee_per_gas=max_fee,
|
|
171
|
-
max_priority_fee_per_gas=priority_fee,
|
|
172
|
-
gas=gas,
|
|
173
|
-
private_key=config.private_keys[from_address],
|
|
174
|
-
chain_id=config.chain_id,
|
|
175
|
-
value=value,
|
|
176
|
-
data=data,
|
|
177
|
-
to=config.contract_address,
|
|
178
|
-
)
|
|
179
|
-
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
180
|
-
if isinstance(res, Err):
|
|
181
|
-
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
182
|
-
return
|
|
183
|
-
tx_hash = res.ok
|
|
184
|
-
|
|
185
|
-
if cli_params.no_receipt:
|
|
186
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}"
|
|
187
|
-
logger.info(msg)
|
|
188
|
-
else:
|
|
189
|
-
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
190
|
-
while True: # TODO: infinite loop if receipt_res is err
|
|
191
|
-
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
192
|
-
if isinstance(receipt_res, Ok):
|
|
193
|
-
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
194
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, status={status}"
|
|
195
|
-
logger.info(msg)
|
|
196
|
-
break
|
|
197
|
-
time.sleep(1)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
routes = """
|
|
2
|
-
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
3
|
-
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
private_keys = """
|
|
7
|
-
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
8
|
-
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
9
|
-
file: /path/to/other_private_keys.txt
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
13
|
-
|
|
14
|
-
max_fee = "1.2base_fee + 1gwei + random(1,200)" # supported var_name=base_fee
|
|
15
|
-
|
|
16
|
-
max_fee_limit = "10.1gwei - random(1, 10)" # optional
|
|
17
|
-
|
|
18
|
-
priority_fee = "1gwei + random(1, 12)"
|
|
19
|
-
|
|
20
|
-
gas = "estimate + random(100, 200) - 19" # supported var_name=estimate
|
|
21
|
-
|
|
22
|
-
value = "0.5balance - random(1.5t, 3t) + 11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
23
|
-
|
|
24
|
-
value_min_limit = "0.5t + random(1, 2) - 7"
|
|
25
|
-
|
|
26
|
-
delay = "random(1.123, 10) + 1" # secs, optional
|
|
27
|
-
|
|
28
|
-
log_debug = "/path/to/file_debug.log" # optional
|
|
29
|
-
|
|
30
|
-
log_info = "/path/to/file_info.log" # optional
|
|
31
|
-
|
|
32
|
-
round_ndigits = 6 # optional, default=5
|
|
33
|
-
|
|
34
|
-
chain_id = 421613
|
|
35
|
-
|
|
36
|
-
nodes = """
|
|
37
|
-
https://arbitrum-goerli.publicnode.com
|
|
38
|
-
https://rpc.goerli.arbitrum.gateway.fm
|
|
39
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|