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 CHANGED
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  import eth_utils
4
4
  from eth_account import Account
5
5
  from eth_account.hdaccount import Mnemonic
6
+ from eth_account.signers.local import LocalAccount
6
7
  from eth_account.types import Language
7
8
  from eth_keys import KeyAPI
8
9
  from eth_typing import ChecksumAddress
@@ -39,13 +40,17 @@ def generate_accounts( # nosec
39
40
  for i in range(limit):
40
41
  path = f"{path_prefix}/{i}"
41
42
  acc = Account.from_mnemonic(mnemonic=mnemonic, account_path=path, passphrase=passphrase)
42
- private_key = acc.key.hex().lower()
43
- if not private_key.startswith("0x"):
44
- private_key = f"0x{private_key}"
43
+ private_key = acc.key.to_0x_hex().lower()
45
44
  result.append(NewAccount(path, acc.address, private_key))
46
45
  return result
47
46
 
48
47
 
48
+ def address_from_private(private_key: str) -> str:
49
+ """returns address in lower case"""
50
+ acc: LocalAccount = Account.from_key(private_key)
51
+ return acc.address.lower()
52
+
53
+
49
54
  def private_to_address(private_key: str) -> str | None:
50
55
  """returns address in lower case"""
51
56
  try:
@@ -54,19 +59,13 @@ def private_to_address(private_key: str) -> str | None:
54
59
  return None
55
60
 
56
61
 
57
- def create_private_keys_dict(private_keys: list[str]) -> dict[str, str]: # address in lower
58
- result = {}
59
- for private_key in private_keys:
60
- address = private_to_address(private_key)
61
- if address is None:
62
- raise ValueError("wrong private key")
63
- result[address.lower()] = private_key
64
- return result
65
-
66
-
67
62
  def is_private_key(private_key: str) -> bool:
68
63
  try:
69
64
  key_api.PrivateKey(decode_hex(private_key)).public_key.to_address()
70
65
  return True # noqa: TRY300
71
66
  except Exception:
72
67
  return False
68
+
69
+
70
+ def is_address(address: str) -> bool:
71
+ return eth_utils.is_address(address)
mm_eth/cli/calcs.py CHANGED
@@ -1,79 +1,67 @@
1
1
  import random
2
- from decimal import Decimal
3
2
 
4
- from loguru import logger
5
- from mm_std.random_ import random_decimal
6
- from mm_std.str import split_on_plus_minus_tokens
3
+ import mm_crypto_utils
4
+ from mm_crypto_utils import VarInt
7
5
 
8
- from mm_eth.utils import from_wei_str, to_wei
6
+ from mm_eth.constants import SUFFIX_DECIMALS
9
7
 
10
8
 
11
- def calc_var_wei_value(value: str, *, var_name: str = "var", var_value: int | None = None, decimals: int | None = None) -> int:
12
- if not isinstance(value, str):
13
- raise TypeError(f"value is not str: {value}")
14
- try:
15
- var_name = var_name.lower()
16
- result = 0
17
- for token in split_on_plus_minus_tokens(value.lower()):
18
- operator = token[0]
19
- item = token[1:]
20
- if item.isdigit():
21
- item_value = int(item)
22
- elif item.endswith("eth"):
23
- item = item.removesuffix("eth")
24
- item_value = int(Decimal(item) * 10**18)
25
- elif item.endswith("ether"):
26
- item = item.removesuffix("ether")
27
- item_value = int(Decimal(item) * 10**18)
28
- elif item.endswith("gwei"):
29
- item = item.removesuffix("gwei")
30
- item_value = int(Decimal(item) * 10**9)
31
- elif item.endswith("t"):
32
- if decimals is None:
33
- raise ValueError("t without decimals") # noqa: TRY301
34
- item = item.removesuffix("t")
35
- item_value = int(Decimal(item) * 10**decimals)
36
- elif item.endswith(var_name):
37
- if var_value is None:
38
- raise ValueError("base value is not set") # noqa: TRY301
39
- item = item.removesuffix(var_name)
40
- k = Decimal(item) if item else Decimal(1)
41
- item_value = int(k * var_value)
42
- elif item.startswith("random(") and item.endswith(")"):
43
- item = item.lstrip("random(").rstrip(")")
44
- arr = item.split(",")
45
- if len(arr) != 2:
46
- raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
47
- from_value = to_wei(arr[0], decimals=decimals)
48
- to_value = to_wei(arr[1], decimals=decimals)
49
- if from_value > to_value:
50
- raise ValueError(f"wrong value, random part: {value}") # noqa: TRY301
51
- item_value = random.randint(from_value, to_value)
52
- else:
53
- raise ValueError(f"wrong value: {value}") # noqa: TRY301
9
+ def calc_eth_expression(expression: str, var: VarInt | None = None) -> int:
10
+ return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals=SUFFIX_DECIMALS)
54
11
 
55
- if operator == "+":
56
- result += item_value
57
- if operator == "-":
58
- result -= item_value
59
12
 
60
- return result # noqa: TRY300
61
- except Exception as err:
62
- raise ValueError(f"wrong value: {value}, error={err}") from err
63
-
64
-
65
- def calc_decimal_value(value: str) -> Decimal:
66
- value = value.lower().strip()
67
- if value.startswith("random(") and value.endswith(")"):
68
- arr = value.lstrip("random(").rstrip(")").split(",")
69
- if len(arr) != 2:
70
- raise ValueError(f"wrong value, random part: {value}")
71
- from_value = Decimal(arr[0])
72
- to_value = Decimal(arr[1])
73
- if from_value > to_value:
74
- raise ValueError(f"wrong value, random part: {value}")
75
- return random_decimal(from_value, to_value)
76
- return Decimal(value)
13
+ # def calc_var_value(value: str, *, var_name: str = "var", var_value: int | None = None, decimals: int | None = None) -> int:
14
+ # if not isinstance(value, str):
15
+ # raise TypeError(f"value is not str: {value}")
16
+ # try:
17
+ # var_name = var_name.lower()
18
+ # result = 0
19
+ # for token in split_on_plus_minus_tokens(value.lower()):
20
+ # operator = token[0]
21
+ # item = token[1:]
22
+ # if item.isdigit():
23
+ # item_value = int(item)
24
+ # elif item.endswith("eth"):
25
+ # item = item.removesuffix("eth")
26
+ # item_value = int(Decimal(item) * 10**18)
27
+ # elif item.endswith("ether"):
28
+ # item = item.removesuffix("ether")
29
+ # item_value = int(Decimal(item) * 10**18)
30
+ # elif item.endswith("gwei"):
31
+ # item = item.removesuffix("gwei")
32
+ # item_value = int(Decimal(item) * 10**9)
33
+ # elif item.endswith("t"):
34
+ # if decimals is None:
35
+ # raise ValueError("t without decimals")
36
+ # item = item.removesuffix("t")
37
+ # item_value = int(Decimal(item) * 10**decimals)
38
+ # elif item.endswith(var_name):
39
+ # if var_value is None:
40
+ # raise ValueError("var_value is not set")
41
+ # item = item.removesuffix(var_name)
42
+ # k = Decimal(item) if item else Decimal(1)
43
+ # item_value = int(k * var_value)
44
+ # elif item.startswith("random(") and item.endswith(")"):
45
+ # item = item.lstrip("random(").rstrip(")")
46
+ # arr = item.split(",")
47
+ # if len(arr) != 2:
48
+ # raise ValueError(f"wrong value, random part: {value}")
49
+ # from_value = to_wei(arr[0], decimals=decimals)
50
+ # to_value = to_wei(arr[1], decimals=decimals)
51
+ # if from_value > to_value:
52
+ # raise ValueError(f"wrong value, random part: {value}")
53
+ # item_value = random.randint(from_value, to_value)
54
+ # else:
55
+ # raise ValueError(f"wrong value: {value}")
56
+ #
57
+ # if operator == "+":
58
+ # result += item_value
59
+ # if operator == "-":
60
+ # result -= item_value
61
+ #
62
+ # return result
63
+ # except Exception as err:
64
+ # raise ValueError(f"illegal ee: {value}, error={err}") from err
77
65
 
78
66
 
79
67
  def calc_function_args(value: str) -> str:
@@ -89,24 +77,17 @@ def calc_function_args(value: str) -> str:
89
77
  value = value[0:start_index] + rand_value + value[stop_index + 1 :]
90
78
 
91
79
 
92
- def is_value_less_min_limit(
93
- value_min_limit: str | None,
94
- value: int,
95
- value_unit: str,
96
- decimals: int | None = None,
97
- log_prefix: str | None = None,
98
- ) -> bool:
99
- if value_min_limit is None:
100
- return False
101
- if value < calc_var_wei_value(value_min_limit, decimals=decimals):
102
- prefix = _get_prefix(log_prefix)
103
- logger.info("{}value is less min limit, value={}", prefix, from_wei_str(value, value_unit, decimals=decimals))
104
- return True
105
- return False
106
-
107
-
108
- def _get_prefix(log_prefix: str | None) -> str:
109
- prefix = log_prefix or ""
110
- if prefix:
111
- prefix += ": "
112
- return prefix
80
+ # def is_value_less_min_limit(
81
+ # value_min_limit: str | None,
82
+ # value: int,
83
+ # value_unit: str,
84
+ # decimals: int | None = None,
85
+ # log_prefix: str | None = None,
86
+ # ) -> bool:
87
+ # if value_min_limit is None:
88
+ # return False
89
+ # if value < calc_eth_expression(value_min_limit):
90
+ # prefix = get_log_prefix(log_prefix)
91
+ # logger.info("{}value is less min limit, value={}", prefix, from_wei_str(value, value_unit, decimals=decimals))
92
+ # return True
93
+ # return False
mm_eth/cli/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from enum import Enum
2
+ from pathlib import Path
2
3
  from typing import Annotated
3
4
 
4
5
  import typer
@@ -21,8 +22,15 @@ from .cmd import (
21
22
  token_cmd,
22
23
  transfer_erc20_cmd,
23
24
  transfer_eth_cmd,
25
+ tx_cmd,
24
26
  vault_cmd,
25
27
  )
28
+ from .cmd.balances_cmd import BalancesCmdParams
29
+ from .cmd.call_contract_cmd import CallContractCmdParams
30
+ from .cmd.deploy_cmd import DeployCmdParams
31
+ from .cmd.send_contract_cmd import SendContractCmdParams
32
+ from .cmd.transfer_erc20_cmd import TransferErc20CmdParams
33
+ from .cmd.transfer_eth_cmd import TransferEthCmdParams
26
34
 
27
35
  app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
28
36
 
@@ -42,7 +50,7 @@ class ConfigExample(str, Enum):
42
50
  def balance_command(
43
51
  wallet_address: Annotated[str, typer.Argument()],
44
52
  token_address: Annotated[str | None, typer.Option("--token", "-t")] = None,
45
- rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL")] = "", # nosec
53
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="MM_ETH_RPC_URL")] = "", # nosec
46
54
  wei: bool = typer.Option(False, "--wei", "-w", help="Print balances in wei units"),
47
55
  print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.PLAIN,
48
56
  ) -> None:
@@ -52,7 +60,7 @@ def balance_command(
52
60
  @app.command(name="token", help="Get token info")
53
61
  def token_command(
54
62
  token_address: Annotated[str, typer.Argument()],
55
- rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL")] = "",
63
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="MM_ETH_RPC_URL")] = "",
56
64
  ) -> None:
57
65
  token_cmd.run(rpc_url, token_address)
58
66
 
@@ -109,7 +117,7 @@ def vault_command(
109
117
  str,
110
118
  typer.Option(..., "--token", "-t", prompt=True, hide_input=True, prompt_required=False, help="A vault token"),
111
119
  ],
112
- keys_file: Annotated[str, typer.Option(..., "--file", "-f", help="Path to a file with private keys")],
120
+ keys_file: Annotated[Path, typer.Option(..., "--file", "-f", help="Path to a file with private keys")],
113
121
  ) -> None:
114
122
  vault_cmd.run(keys_url, vault_token, keys_file)
115
123
 
@@ -118,15 +126,24 @@ def vault_command(
118
126
  def rpc_command(
119
127
  method: Annotated[str, typer.Argument()] = "",
120
128
  params: Annotated[str, typer.Argument()] = "[]",
121
- rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL", help="RPC node url")] = "",
129
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="MM_ETH_RPC_URL", help="RPC node url")] = "",
122
130
  hex2dec: Annotated[bool, typer.Option("--hex2dec", "-d", help="Print result in decimal value")] = False,
123
131
  ) -> None:
124
132
  rpc_cmd.run(rpc_url, method, params, hex2dec)
125
133
 
126
134
 
135
+ @app.command(name="tx", help="Get transaction info by hash")
136
+ def tx_command(
137
+ tx_hash: Annotated[str, typer.Argument()],
138
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="MM_ETH_RPC_URL", help="RPC node url")] = "",
139
+ get_receipt: Annotated[bool, typer.Option("--receipt", "-r", help="Get receipt")] = False,
140
+ ) -> None:
141
+ tx_cmd.run(rpc_url, tx_hash, get_receipt)
142
+
143
+
127
144
  @app.command(name="transfer-eth", help="Transfer eth / base token from one or many accounts")
128
145
  def transfer_eth_command(
129
- config_path: str,
146
+ config_path: Path,
130
147
  print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
131
148
  print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
132
149
  emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
@@ -134,18 +151,20 @@ def transfer_eth_command(
134
151
  debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
135
152
  ) -> None:
136
153
  transfer_eth_cmd.run(
137
- config_path,
138
- print_balances=print_balances,
139
- print_config=print_config,
140
- debug=debug,
141
- no_receipt=no_receipt,
142
- emulate=emulate,
154
+ TransferEthCmdParams(
155
+ config_path=config_path,
156
+ print_balances=print_balances,
157
+ print_config_and_exit=print_config,
158
+ debug=debug,
159
+ no_receipt=no_receipt,
160
+ emulate=emulate,
161
+ )
143
162
  )
144
163
 
145
164
 
146
165
  @app.command(name="transfer-erc20", help="Transfer ERC20 token from one or many accounts")
147
166
  def transfer_erc20_command(
148
- config_path: str,
167
+ config_path: Path,
149
168
  print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
150
169
  print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
151
170
  emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
@@ -153,18 +172,20 @@ def transfer_erc20_command(
153
172
  debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
154
173
  ) -> None:
155
174
  transfer_erc20_cmd.run(
156
- config_path,
157
- print_balances=print_balances,
158
- print_config=print_config,
159
- debug=debug,
160
- no_receipt=no_receipt,
161
- emulate=emulate,
175
+ TransferErc20CmdParams(
176
+ config_path=config_path,
177
+ print_balances=print_balances,
178
+ print_config_and_exit=print_config,
179
+ debug=debug,
180
+ no_receipt=no_receipt,
181
+ emulate=emulate,
182
+ )
162
183
  )
163
184
 
164
185
 
165
186
  @app.command(name="send-contract", help="Send transactions to a contract")
166
187
  def send_contract_command(
167
- config_path: str,
188
+ config_path: Path,
168
189
  print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
169
190
  print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
170
191
  emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
@@ -172,39 +193,41 @@ def send_contract_command(
172
193
  debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
173
194
  ) -> None:
174
195
  send_contract_cmd.run(
175
- config_path,
176
- print_balances=print_balances,
177
- print_config=print_config,
178
- debug=debug,
179
- no_receipt=no_receipt,
180
- emulate=emulate,
196
+ SendContractCmdParams(
197
+ config_path=config_path,
198
+ print_balances=print_balances,
199
+ print_config_and_exit=print_config,
200
+ debug=debug,
201
+ no_receipt=no_receipt,
202
+ emulate=emulate,
203
+ )
181
204
  )
182
205
 
183
206
 
184
207
  @app.command(name="balances", help="Print base and ERC20 token balances")
185
208
  def balances_command(
186
- config_path: str,
187
- print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
209
+ config_path: Path,
210
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
188
211
  nonce: bool = typer.Option(False, "--nonce", "-n", help="Print nonce also"),
189
212
  wei: bool = typer.Option(False, "--wei", "-w", help="Show balances in WEI"),
190
213
  ) -> None:
191
- balances_cmd.run(config_path, print_config, wei, nonce)
214
+ balances_cmd.run(BalancesCmdParams(config_path=config_path, print_config_and_exit=print_config, wei=wei, show_nonce=nonce))
192
215
 
193
216
 
194
217
  @app.command(name="call-contract", help="Call a method on a contract")
195
218
  def call_contract_command(
196
- config_path: str,
197
- print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
219
+ config_path: Path,
220
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
198
221
  ) -> None:
199
- call_contract_cmd.run(config_path, print_config)
222
+ call_contract_cmd.run(CallContractCmdParams(config_path=config_path, print_config_and_exit=print_config))
200
223
 
201
224
 
202
225
  @app.command(name="deploy", help="Deploy a smart contract onchain")
203
226
  def deploy_command(
204
- config_path: str,
205
- print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
227
+ config_path: Path,
228
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
206
229
  ) -> None:
207
- deploy_cmd.run(config_path, print_config=print_config)
230
+ deploy_cmd.run(DeployCmdParams(config_path=config_path, print_config_and_exit=print_config))
208
231
 
209
232
 
210
233
  @app.command(name="config-example", help="Print an example of config for a command")
@@ -214,17 +237,17 @@ def config_example_command(command: Annotated[ConfigExample, typer.Argument()])
214
237
 
215
238
  @app.command(name="encode-input-data", help="Encode input data by a function signature")
216
239
  def encode_input_data(
217
- funtion_signature: str = typer.Argument(help="Function signature, for example: transfer(address, uint256)"),
240
+ function_signature: str = typer.Argument(help="Function signature, for example: transfer(address, uint256)"),
218
241
  args_str: str = typer.Argument(
219
242
  help="""Function arguments, as an array string. For example: '["0xA659FB44eB5d4bFaC1074Cb426b1b11D58D28308", 123]' """,
220
243
  ),
221
244
  ) -> None:
222
- encode_input_data_cmd.run(funtion_signature, args_str)
245
+ encode_input_data_cmd.run(function_signature, args_str)
223
246
 
224
247
 
225
248
  def version_callback(value: bool) -> None:
226
249
  if value:
227
- print_plain(f"mm-eth-cli version: {cli_utils.get_version()}")
250
+ print_plain(f"mm-eth: {cli_utils.get_version()}")
228
251
  raise typer.Exit
229
252
 
230
253
 
mm_eth/cli/cli_utils.py CHANGED
@@ -1,17 +1,11 @@
1
1
  import importlib.metadata
2
- import sys
3
2
  from pathlib import Path
4
3
 
5
- import eth_utils
6
- from loguru import logger
7
- from mm_std import BaseConfig, fatal, print_json, str_to_list
8
-
9
- from mm_eth import account
10
- from mm_eth.account import is_private_key
4
+ from pydantic import BaseModel
11
5
 
12
6
 
13
7
  def get_version() -> str:
14
- return importlib.metadata.version("mm-eth-cli")
8
+ return importlib.metadata.version("mm-eth")
15
9
 
16
10
 
17
11
  def public_rpc_url(url: str | None) -> str:
@@ -21,82 +15,18 @@ def public_rpc_url(url: str | None) -> str:
21
15
  return url
22
16
 
23
17
  match url.lower():
18
+ case "mainnet" | "1":
19
+ return "https://ethereum.publicnode.com"
24
20
  case "opbnb" | "204":
25
21
  return "https://opbnb-mainnet-rpc.bnbchain.org"
22
+ case "base" | "8453":
23
+ return "https://mainnet.base.org"
24
+ case "base-sepolia" | "84532":
25
+ return "https://sepolia.base.org"
26
26
  case _:
27
27
  return url
28
28
 
29
29
 
30
- def init_logger(debug: bool, log_debug_file: str | None, log_info_file: str | None) -> None:
31
- if debug:
32
- level = "DEBUG"
33
- format_ = "<green>{time:YYYY-MM-DD HH:mm:ss}</green> <level>{level}</level> {message}"
34
- else:
35
- level = "INFO"
36
- format_ = "{message}"
37
-
38
- logger.remove()
39
- logger.add(sys.stderr, format=format_, colorize=True, level=level)
40
- if log_debug_file:
41
- logger.add(Path(log_debug_file).expanduser(), format="{time:YYYY-MM-DD HH:mm:ss} {level} {message}")
42
- if log_info_file:
43
- logger.add(Path(log_info_file).expanduser(), format="{message}", level="INFO")
44
-
45
-
46
- def check_private_keys(addresses: list[str], private_keys: dict[str, str]) -> None:
47
- for address in addresses:
48
- address = address.lower() # noqa: PLW2901
49
- if address not in private_keys:
50
- fatal(f"no private key for {address}")
51
- if account.private_to_address(private_keys[address]) != address:
52
- fatal(f"no private key for {address}")
53
-
54
-
55
- def load_tx_addresses_from_str(v: str | None) -> list[tuple[str, str]]:
56
- result: list[tuple[str, str]] = []
57
- if v is None:
58
- return result
59
- for line in str_to_list(v, remove_comments=True):
60
- arr = line.split()
61
- if len(arr) == 2 and eth_utils.is_address(arr[0]) and eth_utils.is_address(arr[1]):
62
- result.append((arr[0].lower(), arr[1].lower()))
63
- return result
64
-
65
-
66
- def load_tx_addresses_from_files(addresses_from_file: str, addresses_to_file: str) -> list[tuple[str, str]]:
67
- from_file = Path(addresses_from_file).expanduser()
68
- to_file = Path(addresses_to_file).expanduser()
69
- if not from_file.is_file():
70
- raise ValueError(f"can't read addresses from 'addresses_from_file={addresses_from_file}")
71
- if not to_file.is_file():
72
- raise ValueError(f"can't read addresses from 'addresses_to_file={addresses_to_file}")
73
-
74
- # get addresses_from
75
- addresses_from = []
76
- for line in from_file.read_text().strip().split("\n"):
77
- if not eth_utils.is_address(line):
78
- raise ValueError(f"illigal address in addresses_from_file: {line}")
79
- addresses_from.append(line.lower())
80
-
81
- # get addresses_to
82
- addresses_to = []
83
- for line in to_file.read_text().strip().split("\n"):
84
- if not eth_utils.is_address(line):
85
- raise ValueError(f"illigal address in addresses_to_file: {line}")
86
- addresses_to.append(line.lower())
87
-
88
- if len(addresses_from) != len(addresses_to):
89
- raise ValueError("len(addresses_from) != len(addresses_to)")
90
-
91
- return list(zip(addresses_from, addresses_to, strict=True))
92
-
93
-
94
- def load_private_keys_from_file(private_keys_file: str) -> list[str]:
95
- lines = Path(private_keys_file).expanduser().read_text().split()
96
- return [line for line in lines if is_private_key(line)]
97
-
98
-
99
- def print_config_and_exit(exit_: bool, config: BaseConfig, exclude: set[str] | None = None) -> None:
100
- if exit_:
101
- print_json(config.model_dump(exclude=exclude))
102
- sys.exit(0)
30
+ class BaseConfigParams(BaseModel):
31
+ config_path: Path
32
+ print_config_and_exit: bool
@@ -48,4 +48,4 @@ def run(rpc_url: str, wallet_address: str, token_address: str | None, wei: bool,
48
48
  print_plain(f"token_balance: {balance}", print_format)
49
49
  json_result["token_balance"] = balance
50
50
 
51
- print_json(json_result, print_format)
51
+ print_json(json_result, print_format=print_format)
@@ -1,29 +1,23 @@
1
1
  from dataclasses import dataclass
2
+ from typing import Annotated
2
3
 
3
4
  from mm_std import BaseConfig, Err, Ok, fatal
4
- from pydantic import Field, field_validator
5
+ from pydantic import BeforeValidator
5
6
  from rich.live import Live
6
7
  from rich.table import Table
7
8
 
8
9
  from mm_eth import erc20, rpc
9
- from mm_eth.cli import cli_utils, validators
10
+ from mm_eth.cli.cli_utils import BaseConfigParams
11
+ from mm_eth.cli.validators import Validators
10
12
  from mm_eth.utils import from_token_wei_str, from_wei_str
11
13
 
12
14
 
13
15
  class Config(BaseConfig):
14
- addresses: list[str]
15
- tokens: list[str] = Field(default_factory=list)
16
- nodes: list[str]
16
+ addresses: Annotated[list[str], BeforeValidator(Validators.eth_addresses(unique=True))]
17
+ tokens: Annotated[list[str], BeforeValidator(Validators.eth_addresses(unique=True))]
18
+ nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
17
19
  round_ndigits: int = 5
18
20
 
19
- @field_validator("nodes", mode="before")
20
- def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
21
- return validators.nodes_validator(v)
22
-
23
- @field_validator("tokens", "addresses", mode="before")
24
- def addresses_validator(cls, v: str | list[str] | None) -> list[str]:
25
- return validators.addresses_validator(v)
26
-
27
21
 
28
22
  @dataclass
29
23
  class Token:
@@ -32,17 +26,23 @@ class Token:
32
26
  symbol: str
33
27
 
34
28
 
35
- def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> None:
36
- config = Config.read_config_or_exit(config_path)
37
- cli_utils.print_config_and_exit(print_config, config)
29
+ class BalancesCmdParams(BaseConfigParams):
30
+ wei: bool
31
+ show_nonce: bool
32
+
33
+
34
+ def run(params: BalancesCmdParams) -> None:
35
+ config = Config.read_toml_config_or_exit(params.config_path)
36
+ if params.print_config_and_exit:
37
+ config.print_and_exit()
38
38
 
39
39
  tokens = _get_tokens_info(config)
40
40
 
41
41
  table = Table(title="balances")
42
42
  table.add_column("address")
43
- if show_nonce:
43
+ if params.show_nonce:
44
44
  table.add_column("nonce")
45
- table.add_column("wei" if wei else "eth")
45
+ table.add_column("wei" if params.wei else "eth")
46
46
  for t in tokens:
47
47
  table.add_column(t.symbol)
48
48
 
@@ -51,13 +51,13 @@ def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> No
51
51
  with Live(table, refresh_per_second=0.5):
52
52
  for address in config.addresses:
53
53
  row = [address]
54
- if show_nonce:
54
+ if params.show_nonce:
55
55
  row.append(str(rpc.eth_get_transaction_count(config.nodes, address, attempts=5).ok_or_err()))
56
56
 
57
57
  base_balance_res = rpc.eth_get_balance(config.nodes, address, attempts=5)
58
58
  if isinstance(base_balance_res, Ok):
59
59
  base_sum += base_balance_res.ok
60
- if wei:
60
+ if params.wei:
61
61
  row.append(str(base_balance_res.ok))
62
62
  else:
63
63
  row.append(
@@ -70,7 +70,7 @@ def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> No
70
70
  token_balance_res = erc20.get_balance(config.nodes, t.address, address, attempts=5)
71
71
  if isinstance(token_balance_res, Ok):
72
72
  token_sum[t.address] += token_balance_res.ok
73
- if wei:
73
+ if params.wei:
74
74
  row.append(str(token_balance_res.ok))
75
75
  else:
76
76
  row.append(
@@ -86,9 +86,9 @@ def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> No
86
86
  table.add_row(*row)
87
87
 
88
88
  sum_row = ["sum"]
89
- if show_nonce:
89
+ if params.show_nonce:
90
90
  sum_row.append("")
91
- if wei:
91
+ if params.wei:
92
92
  sum_row.append(str(base_sum))
93
93
  sum_row.extend([str(token_sum[t.address]) for t in tokens])
94
94
  else: