mm-eth 0.2.5__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.
@@ -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}.yml")
8
+ example_file = Path(Path(__file__).parent.absolute(), "../config_examples", f"{command}.toml")
9
9
  print_plain(example_file.read_text())
@@ -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 cli_utils, rpc_helpers
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
- def run(config_path: str, *, print_config: bool) -> None:
24
- config = Config.read_config_or_exit(config_path)
25
- cli_utils.print_config_and_exit(print_config, config, {"private_key"})
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)
@@ -44,4 +44,4 @@ def run(urls: list[str], print_format: PrintFormat, proxy: str | None) -> None:
44
44
  print_plain(f"base_fee: {base_fee}", print_format)
45
45
  print_plain("", print_format)
46
46
 
47
- print_json(json_result, print_format)
47
+ print_json(json_result, print_format=print_format)
@@ -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 mm_std import BaseConfig, Err, Ok, str_to_list, utc_now
9
- from pydantic import Field, StrictStr, field_validator, model_validator
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
- contract_address: str
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[StrictStr]
27
+ nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
23
28
  chain_id: int
24
- private_keys: dict[str, str] = Field(default_factory=dict)
25
- private_keys_file: str | None = None
26
- max_fee_per_gas: str
27
- max_fee_per_gas_limit: str | None = None
28
- max_priority_fee_per_gas: str
29
- value: str | None = None
30
- gas: str
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: str | None = None
35
- log_info: str | None = None
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
- # load private keys from file
61
- if self.private_keys_file:
62
- file = Path(self.private_keys_file).expanduser()
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: str,
110
- *,
111
- print_balances: bool,
112
- print_config: bool,
113
- debug: bool,
114
- no_receipt: bool,
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, no_receipt=no_receipt, emulate=emulate)
79
+ _run_transfers(config, cli_params)
128
80
 
129
81
 
130
82
  # noinspection DuplicatedCode
131
- def _run_transfers(config: Config, *, no_receipt: bool, emulate: bool) -> None:
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=from_address, config=config, no_receipt=no_receipt, emulate=emulate)
137
- if not emulate and config.delay is not None and i < len(config.from_addresses) - 1:
138
- delay_value = calcs.calc_decimal_value(config.delay)
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(*, from_address: str, config: Config, no_receipt: bool, emulate: bool) -> None:
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 max_fee_per_gas
153
- max_fee_per_gas = rpc_helpers.calc_max_fee_per_gas(config.nodes, config.max_fee_per_gas, log_prefix)
154
- if max_fee_per_gas is None:
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 max_fee_per_gas_limit
158
- if rpc_helpers.is_max_fee_per_gas_limit_exceeded(max_fee_per_gas, config.max_fee_per_gas_limit, log_prefix):
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
- max_priority_fee_per_gas = calcs.calc_var_wei_value(config.max_priority_fee_per_gas)
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
- gas=config.gas,
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.calc_eth_value(
134
+ value = rpc_helpers.calc_eth_value_for_address(
184
135
  nodes=config.nodes,
185
- value_str=config.value,
136
+ value_expression=config.value,
186
137
  address=from_address,
187
138
  gas=gas,
188
- max_fee_per_gas=max_fee_per_gas,
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" max_fee_per_gas={from_wei_str(max_fee_per_gas, 'gwei', config.round_ndigits)},"
210
- msg += f" max_priority_fee_per_gas={from_wei_str(max_priority_fee_per_gas, 'gwei', config.round_ndigits)},"
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
- logger.debug(f"{log_prefix}: tx_params={tx_params}")
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=max_fee_per_gas,
219
- max_priority_fee_per_gas=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"