mm-eth 0.2.5__py3-none-any.whl → 0.3.1__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 +11 -10
- mm_eth/cli/calcs.py +71 -90
- mm_eth/cli/cli.py +60 -37
- mm_eth/cli/cli_utils.py +6 -80
- 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 +1 -1
- mm_eth/json_encoder.py +15 -0
- mm_eth/rpc.py +2 -2
- mm_eth/utils.py +1 -15
- {mm_eth-0.2.5.dist-info → mm_eth-0.3.1.dist-info}/METADATA +2 -3
- mm_eth-0.3.1.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.5.dist-info/RECORD +0 -47
- {mm_eth-0.2.5.dist-info → mm_eth-0.3.1.dist-info}/WHEEL +0 -0
- {mm_eth-0.2.5.dist-info → mm_eth-0.3.1.dist-info}/entry_points.txt +0 -0
|
@@ -5,5 +5,5 @@ from mm_std import print_plain
|
|
|
5
5
|
|
|
6
6
|
def run(command: str) -> None:
|
|
7
7
|
command = command.replace("-", "_")
|
|
8
|
-
example_file = Path(Path(__file__).parent.absolute(), "../config_examples", f"{command}.
|
|
8
|
+
example_file = Path(Path(__file__).parent.absolute(), "../config_examples", f"{command}.toml")
|
|
9
9
|
print_plain(example_file.read_text())
|
mm_eth/cli/cmd/deploy_cmd.py
CHANGED
|
@@ -3,7 +3,8 @@ from mm_std import BaseConfig, fatal
|
|
|
3
3
|
from pydantic import StrictStr
|
|
4
4
|
|
|
5
5
|
from mm_eth import account, deploy
|
|
6
|
-
from mm_eth.cli import
|
|
6
|
+
from mm_eth.cli import rpc_helpers
|
|
7
|
+
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class Config(BaseConfig):
|
|
@@ -20,9 +21,14 @@ class Config(BaseConfig):
|
|
|
20
21
|
node: str
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
class DeployCmdParams(BaseConfigParams):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(cli_params: DeployCmdParams) -> None:
|
|
29
|
+
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
30
|
+
if cli_params.print_config_and_exit:
|
|
31
|
+
config.print_and_exit({"private_key"})
|
|
26
32
|
|
|
27
33
|
constructor_types = yaml.full_load(config.constructor_types)
|
|
28
34
|
constructor_values = yaml.full_load(config.constructor_values)
|
mm_eth/cli/cmd/node_cmd.py
CHANGED
|
@@ -2,101 +2,52 @@ import json
|
|
|
2
2
|
import sys
|
|
3
3
|
import time
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Self
|
|
5
|
+
from typing import Annotated, Self
|
|
6
6
|
|
|
7
|
+
import mm_crypto_utils
|
|
7
8
|
from loguru import logger
|
|
8
|
-
from
|
|
9
|
-
from
|
|
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
|
|
10
12
|
|
|
11
13
|
from mm_eth import abi, rpc
|
|
12
|
-
from mm_eth.account import create_private_keys_dict, private_to_address
|
|
13
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
|
|
14
18
|
from mm_eth.tx import sign_tx
|
|
15
19
|
from mm_eth.utils import from_wei_str
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
class Config(BaseConfig):
|
|
19
|
-
|
|
23
|
+
from_addresses: Annotated[list[str], BeforeValidator(Validators.eth_addresses(unique=True))]
|
|
24
|
+
contract_address: Annotated[str, BeforeValidator(Validators.eth_address())]
|
|
20
25
|
function_signature: str
|
|
21
26
|
function_args: StrictStr = "[]"
|
|
22
|
-
nodes: list[
|
|
27
|
+
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
23
28
|
chain_id: int
|
|
24
|
-
private_keys:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
from_addresses: list[str]
|
|
32
|
-
delay: str | None = None # in seconds
|
|
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
|
|
33
36
|
round_ndigits: int = 5
|
|
34
|
-
log_debug:
|
|
35
|
-
log_info:
|
|
36
|
-
|
|
37
|
-
@field_validator("log_debug", "log_info", mode="before")
|
|
38
|
-
def log_validator(cls, v: str | None) -> str | None:
|
|
39
|
-
return validators.log_validator(v)
|
|
40
|
-
|
|
41
|
-
@field_validator("nodes", "from_addresses", mode="before")
|
|
42
|
-
def list_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
43
|
-
return validators.nodes_validator(v)
|
|
44
|
-
|
|
45
|
-
@field_validator("from_addresses", mode="before")
|
|
46
|
-
def from_addresses_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
47
|
-
return str_to_list(v, remove_comments=True, lower=True)
|
|
48
|
-
|
|
49
|
-
@field_validator("private_keys", mode="before")
|
|
50
|
-
def private_keys_validator(cls, v: str | list[str] | None) -> dict[str, str]:
|
|
51
|
-
if v is None:
|
|
52
|
-
return {}
|
|
53
|
-
if isinstance(v, str):
|
|
54
|
-
return create_private_keys_dict(str_to_list(v, unique=True, remove_comments=True))
|
|
55
|
-
return create_private_keys_dict(v)
|
|
37
|
+
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
|
+
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
56
39
|
|
|
57
40
|
# noinspection DuplicatedCode
|
|
58
41
|
@model_validator(mode="after")
|
|
59
42
|
def final_validator(self) -> Self:
|
|
60
|
-
#
|
|
61
|
-
if self.
|
|
62
|
-
|
|
63
|
-
if not file.is_file():
|
|
64
|
-
raise ValueError("can't read private_keys_file")
|
|
65
|
-
for line in file.read_text().strip().split("\n"):
|
|
66
|
-
line = line.strip() # noqa: PLW2901
|
|
67
|
-
address = private_to_address(line)
|
|
68
|
-
if address is None:
|
|
69
|
-
raise ValueError("there is not a private key in private_keys_file")
|
|
70
|
-
self.private_keys[address.lower()] = line
|
|
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")
|
|
71
46
|
|
|
72
47
|
# check that from_addresses is not empty
|
|
73
48
|
if not self.from_addresses:
|
|
74
49
|
raise ValueError("from_addresses is empty")
|
|
75
50
|
|
|
76
|
-
# max_fee_per_gas
|
|
77
|
-
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas, "base"):
|
|
78
|
-
raise ValueError(f"wrong max_fee_per_gas: {self.max_fee_per_gas}")
|
|
79
|
-
|
|
80
|
-
# max_fee_per_gas_limit
|
|
81
|
-
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas_limit, "base"):
|
|
82
|
-
raise ValueError(f"wrong max_fee_per_gas_limit: {self.max_fee_per_gas_limit}")
|
|
83
|
-
|
|
84
|
-
# max_priority_fee_per_gas
|
|
85
|
-
if not validators.is_valid_calc_var_wei_value(self.max_priority_fee_per_gas):
|
|
86
|
-
raise ValueError(f"wrong max_priority_fee_per_gas: {self.max_priority_fee_per_gas}")
|
|
87
|
-
|
|
88
|
-
# value
|
|
89
|
-
if self.value is not None and not validators.is_valid_calc_var_wei_value(self.value, "balance"):
|
|
90
|
-
raise ValueError(f"wrong value: {self.value}")
|
|
91
|
-
|
|
92
|
-
# gas
|
|
93
|
-
if not validators.is_valid_calc_var_wei_value(self.gas, "estimate"):
|
|
94
|
-
raise ValueError(f"wrong gas: {self.gas}")
|
|
95
|
-
|
|
96
|
-
# delay
|
|
97
|
-
if not validators.is_valid_calc_decimal_value(self.delay):
|
|
98
|
-
raise ValueError(f"wrong delay: {self.delay}")
|
|
99
|
-
|
|
100
51
|
# function_args
|
|
101
52
|
if not validators.is_valid_calc_function_args(self.function_args):
|
|
102
53
|
raise ValueError(f"wrong function_args: {self.function_args}")
|
|
@@ -104,61 +55,61 @@ class Config(BaseConfig):
|
|
|
104
55
|
return self
|
|
105
56
|
|
|
106
57
|
|
|
58
|
+
class SendContractCmdParams(BaseConfigParams):
|
|
59
|
+
print_balances: bool
|
|
60
|
+
debug: bool
|
|
61
|
+
no_receipt: bool
|
|
62
|
+
emulate: bool
|
|
63
|
+
|
|
64
|
+
|
|
107
65
|
# noinspection DuplicatedCode
|
|
108
|
-
def run(
|
|
109
|
-
config_path
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
debug
|
|
114
|
-
|
|
115
|
-
emulate: bool,
|
|
116
|
-
) -> None:
|
|
117
|
-
config = Config.read_config_or_exit(config_path)
|
|
118
|
-
cli_utils.print_config_and_exit(print_config, config, {"private_key"})
|
|
119
|
-
|
|
120
|
-
cli_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
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_and_exit:
|
|
69
|
+
config.print_and_exit({"private_key"})
|
|
70
|
+
|
|
71
|
+
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
72
|
+
|
|
121
73
|
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
122
74
|
|
|
123
|
-
if print_balances:
|
|
75
|
+
if cli_params.print_balances:
|
|
124
76
|
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
125
77
|
sys.exit(0)
|
|
126
78
|
|
|
127
|
-
_run_transfers(config,
|
|
79
|
+
_run_transfers(config, cli_params)
|
|
128
80
|
|
|
129
81
|
|
|
130
82
|
# noinspection DuplicatedCode
|
|
131
|
-
def _run_transfers(config: Config,
|
|
83
|
+
def _run_transfers(config: Config, cli_params: SendContractCmdParams) -> None:
|
|
132
84
|
logger.info(f"started at {utc_now()} UTC")
|
|
133
85
|
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
134
|
-
cli_utils.check_private_keys(config.from_addresses, config.private_keys)
|
|
135
86
|
for i, from_address in enumerate(config.from_addresses):
|
|
136
|
-
_transfer(from_address
|
|
137
|
-
if not emulate and config.delay is not None and i < len(config.from_addresses) - 1:
|
|
138
|
-
delay_value =
|
|
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)
|
|
139
90
|
logger.debug(f"delay {delay_value} seconds")
|
|
140
91
|
time.sleep(float(delay_value))
|
|
141
92
|
logger.info(f"finished at {utc_now()} UTC")
|
|
142
93
|
|
|
143
94
|
|
|
144
95
|
# noinspection DuplicatedCode
|
|
145
|
-
def _transfer(
|
|
96
|
+
def _transfer(from_address: str, config: Config, cli_params: SendContractCmdParams) -> None:
|
|
146
97
|
log_prefix = f"{from_address}"
|
|
147
98
|
# get nonce
|
|
148
99
|
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
149
100
|
if nonce is None:
|
|
150
101
|
return
|
|
151
102
|
|
|
152
|
-
# get
|
|
153
|
-
|
|
154
|
-
if
|
|
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:
|
|
155
106
|
return
|
|
156
107
|
|
|
157
|
-
# check
|
|
158
|
-
if rpc_helpers.
|
|
108
|
+
# check max_fee_limit
|
|
109
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
159
110
|
return
|
|
160
111
|
|
|
161
|
-
|
|
112
|
+
priority_fee = calc_eth_expression(config.priority_fee)
|
|
162
113
|
|
|
163
114
|
# data
|
|
164
115
|
function_args = calcs.calc_function_args(config.function_args).replace("'", '"')
|
|
@@ -167,7 +118,7 @@ def _transfer(*, from_address: str, config: Config, no_receipt: bool, emulate: b
|
|
|
167
118
|
# get gas
|
|
168
119
|
gas = rpc_helpers.calc_gas(
|
|
169
120
|
nodes=config.nodes,
|
|
170
|
-
|
|
121
|
+
gas_expression=config.gas,
|
|
171
122
|
from_address=from_address,
|
|
172
123
|
to_address=config.contract_address,
|
|
173
124
|
value=None,
|
|
@@ -180,43 +131,44 @@ def _transfer(*, from_address: str, config: Config, no_receipt: bool, emulate: b
|
|
|
180
131
|
# get value
|
|
181
132
|
value = None
|
|
182
133
|
if config.value is not None:
|
|
183
|
-
value = rpc_helpers.
|
|
134
|
+
value = rpc_helpers.calc_eth_value_for_address(
|
|
184
135
|
nodes=config.nodes,
|
|
185
|
-
|
|
136
|
+
value_expression=config.value,
|
|
186
137
|
address=from_address,
|
|
187
138
|
gas=gas,
|
|
188
|
-
|
|
139
|
+
max_fee=max_fee,
|
|
189
140
|
log_prefix=log_prefix,
|
|
190
141
|
)
|
|
191
142
|
if value is None:
|
|
192
143
|
return
|
|
193
144
|
|
|
194
|
-
tx_params = {
|
|
195
|
-
"nonce": nonce,
|
|
196
|
-
"max_fee_per_gas": max_fee_per_gas,
|
|
197
|
-
"max_priority_fee_per_gas": max_priority_fee_per_gas,
|
|
198
|
-
"gas": gas,
|
|
199
|
-
"value": value,
|
|
200
|
-
"to": config.contract_address,
|
|
201
|
-
"chain_id": config.chain_id,
|
|
202
|
-
}
|
|
203
|
-
|
|
204
145
|
# emulate?
|
|
205
|
-
if emulate:
|
|
146
|
+
if cli_params.emulate:
|
|
206
147
|
msg = f"{log_prefix}: emulate,"
|
|
207
148
|
if value is not None:
|
|
208
149
|
msg += f" value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
209
|
-
msg += f"
|
|
210
|
-
msg += f"
|
|
211
|
-
msg += f" gas={gas}"
|
|
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}"
|
|
212
154
|
logger.info(msg)
|
|
213
155
|
return
|
|
214
156
|
|
|
215
|
-
|
|
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}")
|
|
216
168
|
signed_tx = sign_tx(
|
|
217
169
|
nonce=nonce,
|
|
218
|
-
max_fee_per_gas=
|
|
219
|
-
max_priority_fee_per_gas=
|
|
170
|
+
max_fee_per_gas=max_fee,
|
|
171
|
+
max_priority_fee_per_gas=priority_fee,
|
|
220
172
|
gas=gas,
|
|
221
173
|
private_key=config.private_keys[from_address],
|
|
222
174
|
chain_id=config.chain_id,
|
|
@@ -230,12 +182,12 @@ def _transfer(*, from_address: str, config: Config, no_receipt: bool, emulate: b
|
|
|
230
182
|
return
|
|
231
183
|
tx_hash = res.ok
|
|
232
184
|
|
|
233
|
-
if no_receipt:
|
|
185
|
+
if cli_params.no_receipt:
|
|
234
186
|
msg = f"{log_prefix}: tx_hash={tx_hash}"
|
|
235
187
|
logger.info(msg)
|
|
236
188
|
else:
|
|
237
189
|
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
238
|
-
while True:
|
|
190
|
+
while True: # TODO: infinite loop if receipt_res is err
|
|
239
191
|
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
240
192
|
if isinstance(receipt_res, Ok):
|
|
241
193
|
status = "OK" if receipt_res.ok == 1 else "FAIL"
|