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.
@@ -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 mm_std import BaseConfig, Err, Ok, fatal, str_to_list, utc_now
8
- from pydantic import Field, StrictStr, field_validator, model_validator
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.account import create_private_keys_dict, private_to_address
12
- from mm_eth.cli import calcs, cli_utils, print_helpers, rpc_helpers, validators
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
- class Tx(BaseConfig):
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
- private_keys: dict[str, str] = Field(default_factory=dict)
26
- private_keys_file: str | None = None
27
- max_fee_per_gas: str
28
- max_fee_per_gas_limit: str | None = None
29
- max_priority_fee_per_gas: str
30
- value: str
31
- value_min_limit: str | None = None
32
- gas: str
33
- addresses_map: str | None = None
34
- addresses_from_file: str | None = None
35
- addresses_to_file: str | None = None
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: str | None = None
39
- log_info: str | None = None
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 [tx.from_address for tx in self.txs]
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
- # load private keys from file
66
- if self.private_keys_file:
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
- # max_fee_per_gas
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
- return self
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: str,
123
- *,
124
- print_balances: bool,
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
- cli_utils.init_logger(debug, config.log_debug, config.log_info)
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, no_receipt=no_receipt, emulate=emulate)
84
+ return _run_transfers(config, cli_params)
154
85
 
155
86
 
156
87
  # noinspection DuplicatedCode
157
- def _run_transfers(config: Config, *, no_receipt: bool, emulate: bool) -> None:
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', 'addresses_map'}) | {'version': cli_utils.get_version()}}")
160
- cli_utils.check_private_keys(config.from_addresses, config.private_keys)
161
- for i, tx in enumerate(config.txs):
162
- _transfer(from_address=tx.from_address, to_address=tx.to_address, config=config, no_receipt=no_receipt, emulate=emulate)
163
- if not emulate and config.delay is not None and i < len(config.txs) - 1:
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(*, from_address: str, to_address: str, config: Config, no_receipt: bool, emulate: bool) -> None:
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 max_fee_per_gas
179
- max_fee_per_gas = rpc_helpers.calc_max_fee_per_gas(config.nodes, config.max_fee_per_gas, log_prefix)
180
- if max_fee_per_gas is None:
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 max_fee_per_gas_limit
184
- if rpc_helpers.is_max_fee_per_gas_limit_exceeded(max_fee_per_gas, config.max_fee_per_gas_limit, log_prefix):
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
- gas=config.gas,
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.calc_erc20_value(
130
+ value = rpc_helpers.calc_erc20_value_for_address(
201
131
  nodes=config.nodes,
202
- value_str=config.value,
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 calcs.is_value_less_min_limit(
213
- config.value_min_limit,
214
- value,
215
- "t",
216
- decimals=config.decimals,
217
- log_prefix=log_prefix,
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
- max_priority_fee_per_gas = calcs.calc_var_wei_value(config.max_priority_fee_per_gas)
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" max_fee_per_gas={from_wei_str(max_fee_per_gas, 'gwei', config.round_ndigits)},"
237
- msg += f" max_priority_fee_per_gas={from_wei_str(max_priority_fee_per_gas, 'gwei', config.round_ndigits)},"
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
- logger.debug(f"{log_prefix}: tx_params={tx_params}")
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=max_fee_per_gas,
246
- max_priority_fee_per_gas=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"