mm-eth 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mm_eth/cli/cli.py +21 -21
- mm_eth/cli/cmd/transfer_cmd.py +75 -52
- mm_eth/cli/examples/transfer.toml +13 -10
- mm_eth/cli/validators.py +3 -3
- {mm_eth-0.4.1.dist-info → mm_eth-0.5.0.dist-info}/METADATA +2 -2
- {mm_eth-0.4.1.dist-info → mm_eth-0.5.0.dist-info}/RECORD +8 -9
- mm_eth/cli/cmd/send_contract_cmd.py +0 -197
- {mm_eth-0.4.1.dist-info → mm_eth-0.5.0.dist-info}/WHEEL +0 -0
- {mm_eth-0.4.1.dist-info → mm_eth-0.5.0.dist-info}/entry_points.txt +0 -0
mm_eth/cli/cli.py
CHANGED
|
@@ -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")
|
mm_eth/cli/cmd/transfer_cmd.py
CHANGED
|
@@ -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,7 +24,7 @@ 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
|
|
@@ -42,13 +43,20 @@ 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:
|
|
55
|
+
transfer.value = self.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
|
Validators.valid_token_expression("balance")(self.value)
|
|
54
62
|
if self.value_min_limit:
|
|
@@ -69,6 +77,7 @@ class Config(BaseConfig):
|
|
|
69
77
|
|
|
70
78
|
class TransferCmdParams(BaseConfigParams):
|
|
71
79
|
print_balances: bool
|
|
80
|
+
print_transfers: bool
|
|
72
81
|
debug: bool
|
|
73
82
|
skip_receipt: bool
|
|
74
83
|
emulate: bool
|
|
@@ -82,6 +91,10 @@ def run(cmd_params: TransferCmdParams) -> None:
|
|
|
82
91
|
|
|
83
92
|
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
84
93
|
|
|
94
|
+
if cmd_params.print_transfers:
|
|
95
|
+
_print_transfers(config)
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
|
|
85
98
|
if cmd_params.print_balances:
|
|
86
99
|
_print_balances(config)
|
|
87
100
|
sys.exit(0)
|
|
@@ -93,9 +106,9 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
93
106
|
mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
|
|
94
107
|
logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
|
|
95
108
|
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.
|
|
109
|
+
for i, transfer in enumerate(config.transfers):
|
|
110
|
+
_transfer(transfer, config, cmd_params)
|
|
111
|
+
if config.delay is not None and i < len(config.transfers) - 1:
|
|
99
112
|
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
100
113
|
logger.info(f"delay {delay_value} seconds")
|
|
101
114
|
if not cmd_params.emulate:
|
|
@@ -103,34 +116,34 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
103
116
|
logger.info(f"finished at {utc_now()} UTC")
|
|
104
117
|
|
|
105
118
|
|
|
106
|
-
def _transfer(
|
|
107
|
-
nonce = rpc_helpers.get_nonce(config.nodes,
|
|
119
|
+
def _transfer(t: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
120
|
+
nonce = rpc_helpers.get_nonce(config.nodes, t.from_address, t.log_prefix)
|
|
108
121
|
if nonce is None:
|
|
109
122
|
return
|
|
110
123
|
|
|
111
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee,
|
|
124
|
+
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, t.log_prefix)
|
|
112
125
|
if max_fee is None:
|
|
113
126
|
return
|
|
114
127
|
|
|
115
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit,
|
|
128
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, t.log_prefix):
|
|
116
129
|
return
|
|
117
130
|
|
|
118
|
-
gas = _calc_gas(
|
|
131
|
+
gas = _calc_gas(t, config)
|
|
119
132
|
if gas is None:
|
|
120
133
|
return
|
|
121
134
|
|
|
122
|
-
value = _calc_value(
|
|
135
|
+
value = _calc_value(t, max_fee=max_fee, gas=gas, config=config)
|
|
123
136
|
if value is None:
|
|
124
137
|
return
|
|
125
138
|
|
|
126
|
-
if not _check_value_min_limit(
|
|
139
|
+
if not _check_value_min_limit(t, value, config):
|
|
127
140
|
return
|
|
128
141
|
|
|
129
142
|
priority_fee = calc_eth_expression(config.priority_fee)
|
|
130
143
|
|
|
131
144
|
# emulate?
|
|
132
145
|
if cmd_params.emulate:
|
|
133
|
-
msg = f"{
|
|
146
|
+
msg = f"{t.log_prefix}: emulate,"
|
|
134
147
|
msg += f" value={_value_with_suffix(value, config)},"
|
|
135
148
|
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
136
149
|
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
@@ -138,39 +151,39 @@ def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) ->
|
|
|
138
151
|
logger.info(msg)
|
|
139
152
|
return
|
|
140
153
|
|
|
141
|
-
tx_hash = _send_tx(
|
|
154
|
+
tx_hash = _send_tx(transfer=t, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
|
|
142
155
|
if tx_hash is None:
|
|
143
156
|
return
|
|
144
157
|
|
|
145
158
|
status = "UNKNOWN"
|
|
146
159
|
if not cmd_params.skip_receipt:
|
|
147
|
-
logger.debug(f"{
|
|
160
|
+
logger.debug(f"{t.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
|
|
148
161
|
status = cli_utils.wait_tx_status(config.nodes, config.proxies, tx_hash, config.wait_tx_timeout)
|
|
149
162
|
|
|
150
|
-
logger.info(f"{
|
|
163
|
+
logger.info(f"{t.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
|
|
151
164
|
|
|
152
165
|
|
|
153
|
-
def _calc_value(
|
|
166
|
+
def _calc_value(transfer: Transfer, max_fee: int, gas: int, config: Config) -> int | None:
|
|
154
167
|
if config.token:
|
|
155
168
|
return rpc_helpers.calc_erc20_value_for_address(
|
|
156
169
|
nodes=config.nodes,
|
|
157
|
-
value_expression=
|
|
158
|
-
wallet_address=
|
|
170
|
+
value_expression=transfer.value,
|
|
171
|
+
wallet_address=transfer.from_address,
|
|
159
172
|
token_address=config.token,
|
|
160
173
|
decimals=config.token_decimals,
|
|
161
|
-
log_prefix=
|
|
174
|
+
log_prefix=transfer.log_prefix,
|
|
162
175
|
)
|
|
163
176
|
return rpc_helpers.calc_eth_value_for_address(
|
|
164
177
|
nodes=config.nodes,
|
|
165
|
-
value_expression=
|
|
166
|
-
address=
|
|
178
|
+
value_expression=transfer.value,
|
|
179
|
+
address=transfer.from_address,
|
|
167
180
|
gas=gas,
|
|
168
181
|
max_fee=max_fee,
|
|
169
|
-
log_prefix=
|
|
182
|
+
log_prefix=transfer.log_prefix,
|
|
170
183
|
)
|
|
171
184
|
|
|
172
185
|
|
|
173
|
-
def _check_value_min_limit(
|
|
186
|
+
def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
|
|
174
187
|
"""Returns False if the transfer should be skipped."""
|
|
175
188
|
if config.value_min_limit:
|
|
176
189
|
if config.token:
|
|
@@ -178,21 +191,23 @@ def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
|
|
|
178
191
|
else:
|
|
179
192
|
value_min_limit = calcs.calc_eth_expression(config.value_min_limit)
|
|
180
193
|
if value < value_min_limit:
|
|
181
|
-
logger.info(f"{
|
|
194
|
+
logger.info(f"{transfer.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
|
|
182
195
|
return True
|
|
183
196
|
|
|
184
197
|
|
|
185
|
-
def _send_tx(
|
|
198
|
+
def _send_tx(
|
|
199
|
+
*, transfer: Transfer, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config
|
|
200
|
+
) -> str | None:
|
|
186
201
|
debug_tx_params = {
|
|
187
202
|
"nonce": nonce,
|
|
188
203
|
"max_fee": max_fee,
|
|
189
204
|
"priority_fee": priority_fee,
|
|
190
205
|
"gas": gas,
|
|
191
206
|
"value": value,
|
|
192
|
-
"to":
|
|
207
|
+
"to": transfer.to_address,
|
|
193
208
|
"chain_id": config.chain_id,
|
|
194
209
|
}
|
|
195
|
-
logger.debug(f"{
|
|
210
|
+
logger.debug(f"{transfer.log_prefix}: tx_params={debug_tx_params}")
|
|
196
211
|
|
|
197
212
|
if config.token:
|
|
198
213
|
signed_tx = erc20.sign_transfer_tx(
|
|
@@ -200,11 +215,11 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
|
|
|
200
215
|
max_fee_per_gas=max_fee,
|
|
201
216
|
max_priority_fee_per_gas=priority_fee,
|
|
202
217
|
gas_limit=gas,
|
|
203
|
-
private_key=config.private_keys[
|
|
218
|
+
private_key=config.private_keys[transfer.from_address],
|
|
204
219
|
chain_id=config.chain_id,
|
|
205
220
|
value=value,
|
|
206
221
|
token_address=config.token,
|
|
207
|
-
recipient_address=
|
|
222
|
+
recipient_address=transfer.to_address,
|
|
208
223
|
)
|
|
209
224
|
else:
|
|
210
225
|
signed_tx = sign_tx(
|
|
@@ -212,38 +227,46 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
|
|
|
212
227
|
max_fee_per_gas=max_fee,
|
|
213
228
|
max_priority_fee_per_gas=priority_fee,
|
|
214
229
|
gas=gas,
|
|
215
|
-
private_key=config.private_keys[
|
|
230
|
+
private_key=config.private_keys[transfer.from_address],
|
|
216
231
|
chain_id=config.chain_id,
|
|
217
232
|
value=value,
|
|
218
|
-
to=
|
|
233
|
+
to=transfer.to_address,
|
|
219
234
|
)
|
|
220
235
|
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
221
236
|
if isinstance(res, Err):
|
|
222
|
-
logger.info(f"{
|
|
237
|
+
logger.info(f"{transfer.log_prefix}: tx error {res.err}")
|
|
223
238
|
return None
|
|
224
239
|
return res.ok
|
|
225
240
|
|
|
226
241
|
|
|
227
|
-
def _calc_gas(
|
|
242
|
+
def _calc_gas(transfer: Transfer, config: Config) -> int | None:
|
|
228
243
|
if config.token:
|
|
229
244
|
return rpc_helpers.calc_gas(
|
|
230
245
|
nodes=config.nodes,
|
|
231
246
|
gas_expression=config.gas,
|
|
232
|
-
from_address=
|
|
247
|
+
from_address=transfer.from_address,
|
|
233
248
|
to_address=config.token,
|
|
234
|
-
data=erc20.encode_transfer_input_data(
|
|
235
|
-
log_prefix=
|
|
249
|
+
data=erc20.encode_transfer_input_data(transfer.to_address, 1234),
|
|
250
|
+
log_prefix=transfer.log_prefix,
|
|
236
251
|
)
|
|
237
252
|
return rpc_helpers.calc_gas(
|
|
238
253
|
nodes=config.nodes,
|
|
239
254
|
gas_expression=config.gas,
|
|
240
|
-
from_address=
|
|
241
|
-
to_address=
|
|
255
|
+
from_address=transfer.from_address,
|
|
256
|
+
to_address=transfer.to_address,
|
|
242
257
|
value=123,
|
|
243
|
-
log_prefix=
|
|
258
|
+
log_prefix=transfer.log_prefix,
|
|
244
259
|
)
|
|
245
260
|
|
|
246
261
|
|
|
262
|
+
def _print_transfers(config: Config) -> None:
|
|
263
|
+
table = Table("n", "from_address", "to_address", "value", title="transfers")
|
|
264
|
+
for count, transfer in enumerate(config.transfers, start=1):
|
|
265
|
+
table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
|
|
266
|
+
console = Console()
|
|
267
|
+
console.print(table)
|
|
268
|
+
|
|
269
|
+
|
|
247
270
|
def _print_balances(config: Config) -> None:
|
|
248
271
|
if config.token:
|
|
249
272
|
headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
|
|
@@ -251,24 +274,24 @@ def _print_balances(config: Config) -> None:
|
|
|
251
274
|
headers = ["n", "from_address", "nonce", "eth", "to_address", "nonce", "eth"]
|
|
252
275
|
table = Table(*headers, title="balances")
|
|
253
276
|
with Live(table, refresh_per_second=0.5):
|
|
254
|
-
for count,
|
|
255
|
-
from_nonce = _get_nonce_str(
|
|
256
|
-
to_nonce = _get_nonce_str(
|
|
277
|
+
for count, transfer in enumerate(config.transfers):
|
|
278
|
+
from_nonce = _get_nonce_str(transfer.from_address, config)
|
|
279
|
+
to_nonce = _get_nonce_str(transfer.to_address, config)
|
|
257
280
|
|
|
258
|
-
from_eth_balance = _get_eth_balance_str(
|
|
259
|
-
to_eth_balance = _get_eth_balance_str(
|
|
281
|
+
from_eth_balance = _get_eth_balance_str(transfer.from_address, config)
|
|
282
|
+
to_eth_balance = _get_eth_balance_str(transfer.to_address, config)
|
|
260
283
|
|
|
261
|
-
from_token_balance = _get_token_balance_str(
|
|
262
|
-
to_token_balance = _get_token_balance_str(
|
|
284
|
+
from_token_balance = _get_token_balance_str(transfer.from_address, config) if config.token else ""
|
|
285
|
+
to_token_balance = _get_token_balance_str(transfer.to_address, config) if config.token else ""
|
|
263
286
|
|
|
264
287
|
if config.token:
|
|
265
288
|
table.add_row(
|
|
266
289
|
str(count),
|
|
267
|
-
|
|
290
|
+
transfer.from_address,
|
|
268
291
|
from_nonce,
|
|
269
292
|
from_eth_balance,
|
|
270
293
|
from_token_balance,
|
|
271
|
-
|
|
294
|
+
transfer.to_address,
|
|
272
295
|
to_nonce,
|
|
273
296
|
to_eth_balance,
|
|
274
297
|
to_token_balance,
|
|
@@ -276,10 +299,10 @@ def _print_balances(config: Config) -> None:
|
|
|
276
299
|
else:
|
|
277
300
|
table.add_row(
|
|
278
301
|
str(count),
|
|
279
|
-
|
|
302
|
+
transfer.from_address,
|
|
280
303
|
from_nonce,
|
|
281
304
|
from_eth_balance,
|
|
282
|
-
|
|
305
|
+
transfer.to_address,
|
|
283
306
|
to_nonce,
|
|
284
307
|
to_eth_balance,
|
|
285
308
|
)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
transfers = """
|
|
2
|
+
# from_address to_address value
|
|
3
|
+
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b 0.1t # comments are allowed here
|
|
4
|
+
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f 0.2balance-random(0.1t,0.5t)
|
|
5
|
+
0x10ecB8d838746643E613f6B5218C8e342593225c 0xE19242B72a4833eD86F1b2015d4E59052A2b278b # if the value is not set, config.value will be used
|
|
6
|
+
file: /path/to/other_transfers.txt # transfers from this file will be added
|
|
4
7
|
"""
|
|
5
8
|
|
|
6
9
|
private_keys = """
|
|
@@ -11,19 +14,19 @@ file: /path/to/other_private_keys.txt
|
|
|
11
14
|
|
|
12
15
|
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
13
16
|
|
|
14
|
-
max_fee = "1.2base_fee
|
|
17
|
+
max_fee = "1.2base_fee+1gwei+random(1,200)" # supported var_name=base_fee
|
|
15
18
|
|
|
16
|
-
max_fee_limit = "10.1gwei
|
|
19
|
+
max_fee_limit = "10.1gwei-random(1,10)" # optional
|
|
17
20
|
|
|
18
|
-
priority_fee = "1gwei
|
|
21
|
+
priority_fee = "1gwei+random(1,12)"
|
|
19
22
|
|
|
20
|
-
gas = "estimate
|
|
23
|
+
gas = "estimate+random(100,200)-19" # supported var_name=estimate
|
|
21
24
|
|
|
22
|
-
value = "0.5balance
|
|
25
|
+
value = "0.5balance-random(1.5t,3t)+11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
23
26
|
|
|
24
|
-
value_min_limit = "0.5t
|
|
27
|
+
value_min_limit = "0.5t+random(1,2)-7"
|
|
25
28
|
|
|
26
|
-
delay = "random(1.123,
|
|
29
|
+
delay = "random(1.123,10)+1" # secs, optional
|
|
27
30
|
|
|
28
31
|
log_debug = "/path/to/file_debug.log" # optional
|
|
29
32
|
|
mm_eth/cli/validators.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
|
|
3
|
-
from mm_crypto_utils import AddressToPrivate, ConfigValidators,
|
|
3
|
+
from mm_crypto_utils import AddressToPrivate, ConfigValidators, Transfer
|
|
4
4
|
|
|
5
5
|
from mm_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]:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -16,11 +16,11 @@ 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
18
|
mm_eth/cli/calcs.py,sha256=cLFTYNAN-I53tUiSg-zFfVr2afjIZPftDDjHj16FBz0,1068
|
|
19
|
-
mm_eth/cli/cli.py,sha256=
|
|
19
|
+
mm_eth/cli/cli.py,sha256=nSOxvfjEu6HBXz6qFOCuiLFgw381pzeNB2tUGxdJIgA,9332
|
|
20
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=KIAQUohl4_KKDvynbeqIeywtNnMWhTKYlnTdaxcjn6U,1690
|
|
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
26
|
mm_eth/cli/cmd/balances_cmd.py,sha256=4UiWSNH9OCnkvcMGPTygEss8119do-rfG7QtsNMfWZs,4197
|
|
@@ -30,10 +30,9 @@ mm_eth/cli/cmd/encode_input_data_cmd.py,sha256=9UQ1MKPEFQJ8j_COsP3KGKhwOf9tT3feB
|
|
|
30
30
|
mm_eth/cli/cmd/example_cmd.py,sha256=o4NTll3fjmspbKjZ0sHGDRHTZ1RcFNHZDi9Ka0VNoDQ,264
|
|
31
31
|
mm_eth/cli/cmd/node_cmd.py,sha256=Ae5yPxxnNiHw3tZcojS7KwNLM4gEfLhsTfhZp_86rqU,1956
|
|
32
32
|
mm_eth/cli/cmd/rpc_cmd.py,sha256=02q82YqgbPezfEBmV_QBCIeNReE7ktkPych8Xr9ann8,2186
|
|
33
|
-
mm_eth/cli/cmd/send_contract_cmd.py,sha256=JX3L1Dz4CBySfa2Pf87E8grauPu0U6xIyyOus81285Y,7316
|
|
34
33
|
mm_eth/cli/cmd/solc_cmd.py,sha256=tBpeMdPfGs2iQIMaIJAAhMh1a3KyXHwyninfXPVpsgs,677
|
|
35
34
|
mm_eth/cli/cmd/token_cmd.py,sha256=4y6ZQpLOJ33_iNuKpm9tZXh4RntWhmPUcizgaNNBzaw,1102
|
|
36
|
-
mm_eth/cli/cmd/transfer_cmd.py,sha256=
|
|
35
|
+
mm_eth/cli/cmd/transfer_cmd.py,sha256=r8mZK9WyqXdR-Ce7Xb4hhOOHEKaJhPfbmYigC_4HzqU,13151
|
|
37
36
|
mm_eth/cli/cmd/tx_cmd.py,sha256=PIenXYTT60Z2fqsivpzybCLI2Z_tlcz-asm3B0JLHgI,517
|
|
38
37
|
mm_eth/cli/cmd/vault_cmd.py,sha256=MOM1CILIaaqown1I-Fgo22ckqIMLtFt8t2D3fWNp798,606
|
|
39
38
|
mm_eth/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -41,8 +40,8 @@ mm_eth/cli/cmd/wallet/mnemonic_cmd.py,sha256=xE-5Ux9BdYsTZYBy0dMn9jupGhW4ced-AgY
|
|
|
41
40
|
mm_eth/cli/cmd/wallet/private_key_cmd.py,sha256=Fv_2OLog1h32pIP7PJITwl_pHdy3BXvaDRcXZsxY1xo,241
|
|
42
41
|
mm_eth/cli/examples/balances.toml,sha256=i_ALpiEcf8-0TFiUg1cgJhxxfHYeBl9x0b3tnUWjswU,421
|
|
43
42
|
mm_eth/cli/examples/call_contract.toml,sha256=ZQWK-409V_vLIZ2bsRD5RCWPPzShPz2KJTTRQY4YaGw,248
|
|
44
|
-
mm_eth/cli/examples/transfer.toml,sha256=
|
|
45
|
-
mm_eth-0.
|
|
46
|
-
mm_eth-0.
|
|
47
|
-
mm_eth-0.
|
|
48
|
-
mm_eth-0.
|
|
43
|
+
mm_eth/cli/examples/transfer.toml,sha256=OKpojNWV2DQ68hX8jQ84cAMevKmBt-l20ij3ROTO9G0,1463
|
|
44
|
+
mm_eth-0.5.0.dist-info/METADATA,sha256=5vJwdr_CVo2u2CDobOUKNK2pnQi1sDksc2qQd9XBNaE,207
|
|
45
|
+
mm_eth-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
46
|
+
mm_eth-0.5.0.dist-info/entry_points.txt,sha256=aGhpsozl8NIrkuUcX5fSgURCcDhr3ShUdeTSIrJq4oc,46
|
|
47
|
+
mm_eth-0.5.0.dist-info/RECORD,,
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|