mm-eth 0.2.4__py3-none-any.whl → 0.3.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/account.py +12 -13
- mm_eth/cli/calcs.py +71 -90
- mm_eth/cli/cli.py +60 -37
- mm_eth/cli/cli_utils.py +11 -81
- mm_eth/cli/cmd/balance_cmd.py +1 -1
- mm_eth/cli/cmd/balances_cmd.py +23 -23
- mm_eth/cli/cmd/call_contract_cmd.py +9 -4
- mm_eth/cli/cmd/config_example_cmd.py +1 -1
- mm_eth/cli/cmd/deploy_cmd.py +10 -4
- mm_eth/cli/cmd/node_cmd.py +1 -1
- mm_eth/cli/cmd/send_contract_cmd.py +74 -122
- mm_eth/cli/cmd/transfer_erc20_cmd.py +86 -158
- mm_eth/cli/cmd/transfer_eth_cmd.py +85 -152
- mm_eth/cli/cmd/tx_cmd.py +16 -0
- mm_eth/cli/cmd/vault_cmd.py +6 -3
- mm_eth/cli/config_examples/{balances.yml → balances.toml} +7 -7
- mm_eth/cli/config_examples/call_contract.toml +5 -0
- mm_eth/cli/config_examples/transfer_erc20.toml +27 -0
- mm_eth/cli/config_examples/transfer_eth.toml +26 -0
- mm_eth/cli/rpc_helpers.py +43 -50
- mm_eth/cli/validators.py +23 -64
- mm_eth/constants.py +1 -0
- mm_eth/ens.py +2 -2
- mm_eth/erc20.py +3 -7
- mm_eth/json_encoder.py +15 -0
- mm_eth/rpc.py +4 -7
- mm_eth/tx.py +3 -2
- mm_eth/utils.py +1 -15
- {mm_eth-0.2.4.dist-info → mm_eth-0.3.0.dist-info}/METADATA +2 -3
- mm_eth-0.3.0.dist-info/RECORD +49 -0
- mm_eth/cli/config_examples/call_contract.yml +0 -5
- mm_eth/cli/config_examples/transfer_erc20.yml +0 -26
- mm_eth/cli/config_examples/transfer_eth.yml +0 -24
- mm_eth/types.py +0 -4
- mm_eth-0.2.4.dist-info/RECORD +0 -47
- {mm_eth-0.2.4.dist-info → mm_eth-0.3.0.dist-info}/WHEEL +0 -0
- {mm_eth-0.2.4.dist-info → mm_eth-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,136 +1,67 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import time
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Self
|
|
4
|
+
from typing import Annotated, Self
|
|
5
5
|
|
|
6
|
+
import mm_crypto_utils
|
|
6
7
|
from loguru import logger
|
|
7
|
-
from
|
|
8
|
-
from
|
|
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
|
|
9
11
|
|
|
10
12
|
from mm_eth import erc20, rpc
|
|
11
|
-
from mm_eth.
|
|
12
|
-
from mm_eth.cli import
|
|
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
|
|
13
17
|
from mm_eth.utils import from_wei_str
|
|
14
18
|
|
|
15
19
|
|
|
20
|
+
# noinspection DuplicatedCode
|
|
16
21
|
class Config(BaseConfig):
|
|
17
|
-
|
|
18
|
-
from_address: str
|
|
19
|
-
to_address: str
|
|
20
|
-
|
|
21
|
-
token: str
|
|
22
|
-
decimals: int
|
|
23
|
-
nodes: list[StrictStr]
|
|
22
|
+
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
24
23
|
chain_id: int
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
delay: str | None = None # in seconds
|
|
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
|
|
37
35
|
round_ndigits: int = 5
|
|
38
|
-
log_debug:
|
|
39
|
-
log_info:
|
|
40
|
-
txs: list[Tx] = Field(default_factory=list)
|
|
36
|
+
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
37
|
+
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
41
38
|
|
|
42
39
|
@property
|
|
43
40
|
def from_addresses(self) -> list[str]:
|
|
44
|
-
return [
|
|
45
|
-
|
|
46
|
-
@field_validator("log_debug", "log_info", mode="before")
|
|
47
|
-
def log_validator(cls, v: str | None) -> str | None:
|
|
48
|
-
return validators.log_validator(v)
|
|
49
|
-
|
|
50
|
-
@field_validator("nodes", mode="before")
|
|
51
|
-
def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
52
|
-
return validators.nodes_validator(v)
|
|
53
|
-
|
|
54
|
-
@field_validator("private_keys", mode="before")
|
|
55
|
-
def private_keys_validator(cls, v: str | list[str] | None) -> dict[str, str]:
|
|
56
|
-
if v is None:
|
|
57
|
-
return {}
|
|
58
|
-
if isinstance(v, str):
|
|
59
|
-
return create_private_keys_dict(str_to_list(v, unique=True, remove_comments=True))
|
|
60
|
-
return create_private_keys_dict(v)
|
|
41
|
+
return [r.from_address for r in self.routes]
|
|
61
42
|
|
|
62
|
-
# noinspection DuplicatedCode
|
|
63
43
|
@model_validator(mode="after")
|
|
64
44
|
def final_validator(self) -> Self:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
file = Path(self.private_keys_file).expanduser()
|
|
68
|
-
if not file.is_file():
|
|
69
|
-
raise ValueError("can't read private_keys_file")
|
|
70
|
-
for line in file.read_text().strip().split("\n"):
|
|
71
|
-
line = line.strip() # noqa: PLW2901
|
|
72
|
-
address = private_to_address(line)
|
|
73
|
-
if address is None:
|
|
74
|
-
raise ValueError("there is not a private key in private_keys_file")
|
|
75
|
-
self.private_keys[address.lower()] = line
|
|
45
|
+
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
46
|
+
raise ValueError("private keys are not set for all addresses")
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas, "base"):
|
|
79
|
-
raise ValueError(f"wrong max_fee_per_gas: {self.max_fee_per_gas}")
|
|
80
|
-
|
|
81
|
-
# max_fee_per_gas_limit
|
|
82
|
-
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas_limit, "base"):
|
|
83
|
-
raise ValueError(f"wrong max_fee_per_gas_limit: {self.max_fee_per_gas_limit}")
|
|
84
|
-
|
|
85
|
-
# max_priority_fee_per_gas
|
|
86
|
-
if not validators.is_valid_calc_var_wei_value(self.max_priority_fee_per_gas):
|
|
87
|
-
raise ValueError(f"wrong max_priority_fee_per_gas: {self.max_priority_fee_per_gas}")
|
|
88
|
-
|
|
89
|
-
# value
|
|
90
|
-
if not validators.is_valid_calc_var_wei_value(self.value, "balance", decimals=self.decimals):
|
|
91
|
-
raise ValueError(f"wrong value: {self.value}")
|
|
92
|
-
|
|
93
|
-
# value_min_limit
|
|
94
|
-
if not validators.is_valid_calc_var_wei_value(self.value_min_limit, decimals=self.decimals):
|
|
95
|
-
raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
|
|
96
|
-
|
|
97
|
-
# gas
|
|
98
|
-
if not validators.is_valid_calc_var_wei_value(self.gas, "estimate"):
|
|
99
|
-
raise ValueError(f"wrong gas: {self.gas}")
|
|
100
|
-
|
|
101
|
-
# delay
|
|
102
|
-
if not validators.is_valid_calc_decimal_value(self.delay):
|
|
103
|
-
raise ValueError(f"wrong delay: {self.delay}")
|
|
48
|
+
return self
|
|
104
49
|
|
|
105
|
-
# txs
|
|
106
|
-
if self.addresses_map:
|
|
107
|
-
for tx in cli_utils.load_tx_addresses_from_str(self.addresses_map):
|
|
108
|
-
self.txs.append(Config.Tx(from_address=tx[0], to_address=tx[1]))
|
|
109
|
-
if self.addresses_from_file and self.addresses_to_file:
|
|
110
|
-
self.txs.extend(
|
|
111
|
-
Config.Tx(from_address=tx[0], to_address=tx[1])
|
|
112
|
-
for tx in cli_utils.load_tx_addresses_from_files(self.addresses_from_file, self.addresses_to_file)
|
|
113
|
-
)
|
|
114
|
-
if not self.txs:
|
|
115
|
-
raise ValueError("txs is empty")
|
|
116
50
|
|
|
117
|
-
|
|
51
|
+
class TransferErc20CmdParams(BaseConfigParams):
|
|
52
|
+
print_balances: bool
|
|
53
|
+
debug: bool
|
|
54
|
+
no_receipt: bool
|
|
55
|
+
emulate: bool
|
|
118
56
|
|
|
119
57
|
|
|
120
58
|
# noinspection DuplicatedCode
|
|
121
|
-
def run(
|
|
122
|
-
config_path
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
print_config: bool,
|
|
126
|
-
debug: bool,
|
|
127
|
-
no_receipt: bool,
|
|
128
|
-
emulate: bool,
|
|
129
|
-
) -> None:
|
|
130
|
-
config = Config.read_config_or_exit(config_path)
|
|
131
|
-
cli_utils.print_config_and_exit(print_config, config, {"private_key", "addresses_map"})
|
|
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"})
|
|
132
63
|
|
|
133
|
-
|
|
64
|
+
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
134
65
|
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
135
66
|
|
|
136
67
|
# check decimals
|
|
@@ -140,7 +71,7 @@ def run(
|
|
|
140
71
|
if res.ok != config.decimals:
|
|
141
72
|
fatal(f"config.decimals is wrong: {config.decimals} != {res.ok}")
|
|
142
73
|
|
|
143
|
-
if print_balances:
|
|
74
|
+
if cli_params.print_balances:
|
|
144
75
|
print_helpers.print_balances(
|
|
145
76
|
config.nodes,
|
|
146
77
|
config.from_addresses,
|
|
@@ -150,57 +81,56 @@ def run(
|
|
|
150
81
|
)
|
|
151
82
|
sys.exit(0)
|
|
152
83
|
|
|
153
|
-
return _run_transfers(config,
|
|
84
|
+
return _run_transfers(config, cli_params)
|
|
154
85
|
|
|
155
86
|
|
|
156
87
|
# noinspection DuplicatedCode
|
|
157
|
-
def _run_transfers(config: Config,
|
|
88
|
+
def _run_transfers(config: Config, cli_params: TransferErc20CmdParams) -> None:
|
|
158
89
|
logger.info(f"started at {utc_now()} UTC")
|
|
159
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
delay_value = calcs.calc_decimal_value(config.delay)
|
|
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)
|
|
165
95
|
logger.debug(f"delay {delay_value} seconds")
|
|
166
96
|
time.sleep(float(delay_value))
|
|
167
97
|
logger.info(f"finished at {utc_now()} UTC")
|
|
168
98
|
|
|
169
99
|
|
|
170
100
|
# noinspection DuplicatedCode
|
|
171
|
-
def _transfer(
|
|
172
|
-
log_prefix = f"{from_address}->{to_address}"
|
|
101
|
+
def _transfer(route: TxRoute, config: Config, cli_params: TransferErc20CmdParams) -> None:
|
|
102
|
+
log_prefix = f"{route.from_address}->{route.to_address}"
|
|
173
103
|
# get nonce
|
|
174
|
-
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
104
|
+
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, log_prefix)
|
|
175
105
|
if nonce is None:
|
|
176
106
|
return
|
|
177
107
|
|
|
178
|
-
# get
|
|
179
|
-
|
|
180
|
-
if
|
|
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:
|
|
181
111
|
return
|
|
182
112
|
|
|
183
|
-
# check
|
|
184
|
-
if rpc_helpers.
|
|
113
|
+
# check max_fee_limit
|
|
114
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
185
115
|
return
|
|
186
116
|
|
|
187
117
|
# get gas
|
|
188
118
|
gas = rpc_helpers.calc_gas(
|
|
189
119
|
nodes=config.nodes,
|
|
190
|
-
|
|
191
|
-
from_address=from_address,
|
|
120
|
+
gas_expression=config.gas,
|
|
121
|
+
from_address=route.from_address,
|
|
192
122
|
to_address=config.token,
|
|
193
|
-
data=erc20.encode_transfer_input_data(to_address, 1234),
|
|
123
|
+
data=erc20.encode_transfer_input_data(route.to_address, 1234),
|
|
194
124
|
log_prefix=log_prefix,
|
|
195
125
|
)
|
|
196
126
|
if gas is None:
|
|
197
127
|
return
|
|
198
128
|
|
|
199
129
|
# get value
|
|
200
|
-
value = rpc_helpers.
|
|
130
|
+
value = rpc_helpers.calc_erc20_value_for_address(
|
|
201
131
|
nodes=config.nodes,
|
|
202
|
-
|
|
203
|
-
wallet_address=from_address,
|
|
132
|
+
value_expression=config.value,
|
|
133
|
+
wallet_address=route.from_address,
|
|
204
134
|
token_address=config.token,
|
|
205
135
|
decimals=config.decimals,
|
|
206
136
|
log_prefix=log_prefix,
|
|
@@ -209,47 +139,45 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_receipt:
|
|
|
209
139
|
return
|
|
210
140
|
|
|
211
141
|
# value_min_limit
|
|
212
|
-
if
|
|
213
|
-
config.value_min_limit,
|
|
214
|
-
value
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
):
|
|
219
|
-
return
|
|
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
|
|
220
148
|
|
|
221
|
-
|
|
222
|
-
tx_params = {
|
|
223
|
-
"nonce": nonce,
|
|
224
|
-
"max_fee_per_gas": max_fee_per_gas,
|
|
225
|
-
"max_priority_fee_per_gas": max_priority_fee_per_gas,
|
|
226
|
-
"gas": gas,
|
|
227
|
-
"value": value,
|
|
228
|
-
"to": to_address,
|
|
229
|
-
"chain_id": config.chain_id,
|
|
230
|
-
}
|
|
149
|
+
priority_fee = calc_eth_expression(config.priority_fee)
|
|
231
150
|
|
|
232
151
|
# emulate?
|
|
233
|
-
if emulate:
|
|
152
|
+
if cli_params.emulate:
|
|
234
153
|
msg = f"{log_prefix}: emulate,"
|
|
235
154
|
msg += f" value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)},"
|
|
236
|
-
msg += f"
|
|
237
|
-
msg += f"
|
|
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)},"
|
|
238
157
|
msg += f" gas={gas}"
|
|
239
158
|
logger.info(msg)
|
|
240
159
|
return
|
|
241
160
|
|
|
242
|
-
|
|
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}")
|
|
243
171
|
signed_tx = erc20.sign_transfer_tx(
|
|
244
172
|
nonce=nonce,
|
|
245
|
-
max_fee_per_gas=
|
|
246
|
-
max_priority_fee_per_gas=
|
|
173
|
+
max_fee_per_gas=max_fee,
|
|
174
|
+
max_priority_fee_per_gas=priority_fee,
|
|
247
175
|
gas_limit=gas,
|
|
248
|
-
private_key=config.private_keys[from_address],
|
|
176
|
+
private_key=config.private_keys[route.from_address],
|
|
249
177
|
chain_id=config.chain_id,
|
|
250
178
|
value=value,
|
|
251
179
|
token_address=config.token,
|
|
252
|
-
recipient_address=to_address,
|
|
180
|
+
recipient_address=route.to_address,
|
|
253
181
|
)
|
|
254
182
|
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
255
183
|
if isinstance(res, Err):
|
|
@@ -257,12 +185,12 @@ def _transfer(*, from_address: str, to_address: str, config: Config, no_receipt:
|
|
|
257
185
|
return
|
|
258
186
|
tx_hash = res.ok
|
|
259
187
|
|
|
260
|
-
if no_receipt:
|
|
188
|
+
if cli_params.no_receipt:
|
|
261
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
|
|
262
190
|
logger.info(msg)
|
|
263
191
|
else:
|
|
264
192
|
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
265
|
-
while True:
|
|
193
|
+
while True: # TODO: infinite loop if receipt_res is err
|
|
266
194
|
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
267
195
|
if isinstance(receipt_res, Ok):
|
|
268
196
|
status = "OK" if receipt_res.ok == 1 else "FAIL"
|