mm-eth 0.2.0__py3-none-any.whl → 0.2.2__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.
Files changed (43) hide show
  1. mm_eth/abi.py +3 -3
  2. mm_eth/account.py +1 -1
  3. mm_eth/anvil.py +2 -2
  4. mm_eth/cli/__init__.py +0 -0
  5. mm_eth/cli/calcs.py +112 -0
  6. mm_eth/cli/cli.py +237 -0
  7. mm_eth/cli/cli_utils.py +105 -0
  8. mm_eth/cli/cmd/__init__.py +0 -0
  9. mm_eth/cli/cmd/balance_cmd.py +51 -0
  10. mm_eth/cli/cmd/balances_cmd.py +124 -0
  11. mm_eth/cli/cmd/call_contract_cmd.py +43 -0
  12. mm_eth/cli/cmd/config_example_cmd.py +9 -0
  13. mm_eth/cli/cmd/deploy_cmd.py +43 -0
  14. mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
  15. mm_eth/cli/cmd/mnemonic_cmd.py +26 -0
  16. mm_eth/cli/cmd/node_cmd.py +47 -0
  17. mm_eth/cli/cmd/private_key_cmd.py +10 -0
  18. mm_eth/cli/cmd/rpc_cmd.py +78 -0
  19. mm_eth/cli/cmd/send_contract_cmd.py +251 -0
  20. mm_eth/cli/cmd/solc_cmd.py +24 -0
  21. mm_eth/cli/cmd/token_cmd.py +29 -0
  22. mm_eth/cli/cmd/transfer_erc20_cmd.py +277 -0
  23. mm_eth/cli/cmd/transfer_eth_cmd.py +254 -0
  24. mm_eth/cli/cmd/vault_cmd.py +16 -0
  25. mm_eth/cli/config_examples/balances.yml +15 -0
  26. mm_eth/cli/config_examples/call_contract.yml +5 -0
  27. mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
  28. mm_eth/cli/config_examples/transfer_eth.yml +24 -0
  29. mm_eth/cli/print_helpers.py +37 -0
  30. mm_eth/cli/rpc_helpers.py +140 -0
  31. mm_eth/cli/validators.py +85 -0
  32. mm_eth/erc20.py +8 -7
  33. mm_eth/rpc.py +8 -8
  34. mm_eth/solc.py +2 -3
  35. mm_eth/tx.py +3 -5
  36. mm_eth/utils.py +11 -16
  37. mm_eth/vault.py +5 -5
  38. mm_eth-0.2.2.dist-info/METADATA +9 -0
  39. mm_eth-0.2.2.dist-info/RECORD +47 -0
  40. mm_eth-0.2.2.dist-info/entry_points.txt +2 -0
  41. mm_eth-0.2.0.dist-info/METADATA +0 -7
  42. mm_eth-0.2.0.dist-info/RECORD +0 -18
  43. {mm_eth-0.2.0.dist-info → mm_eth-0.2.2.dist-info}/WHEEL +0 -0
mm_eth/abi.py CHANGED
@@ -64,7 +64,7 @@ def decode_function_input(contract_abi: ABI, tx_input: str) -> FunctionInput:
64
64
 
65
65
 
66
66
  def get_function_abi(contr_abi: ABI, fn_name: str) -> ABIFunction:
67
- abi = pydash.find(contr_abi, lambda x: x.get("name", None) == fn_name and x.get("type", None) == "function") # type:ignore
67
+ abi = pydash.find(contr_abi, lambda x: x.get("name", None) == fn_name and x.get("type", None) == "function") # type: ignore[call-overload, attr-defined]
68
68
  if not abi:
69
69
  raise ValueError("can't find abi for function: " + fn_name)
70
70
  return cast(ABIFunction, abi)
@@ -122,8 +122,8 @@ def parse_function_signatures(contract_abi: ABI) -> dict[str, str]:
122
122
  result: dict[str, str] = {}
123
123
  for item in contract_abi:
124
124
  if item.get("type", None) == "function":
125
- function_name = item["name"] # type: ignore
126
- types = ",".join([i["type"] for i in item["inputs"]]) # type: ignore
125
+ function_name = item["name"] # type: ignore[typeddict-item]
126
+ types = ",".join([i["type"] for i in item["inputs"]]) # type: ignore[typeddict-item]
127
127
  function_name_and_types = f"{function_name}({types})"
128
128
  result[function_name_and_types] = encode_function_signature(function_name_and_types)
129
129
  return result
mm_eth/account.py CHANGED
@@ -67,6 +67,6 @@ def create_private_keys_dict(private_keys: list[str]) -> dict[str, str]: # addr
67
67
  def is_private_key(private_key: str) -> bool:
68
68
  try:
69
69
  key_api.PrivateKey(decode_hex(private_key)).public_key.to_address()
70
- return True
70
+ return True # noqa: TRY300
71
71
  except Exception:
72
72
  return False
mm_eth/anvil.py CHANGED
@@ -10,7 +10,7 @@ from mm_eth import account, rpc
10
10
 
11
11
 
12
12
  class Anvil:
13
- def __init__(self, *, chain_id: int, port: int, mnemonic: str):
13
+ def __init__(self, *, chain_id: int, port: int, mnemonic: str) -> None:
14
14
  self.chain_id = chain_id
15
15
  self.port = port
16
16
  self.mnemonic = mnemonic
@@ -18,7 +18,7 @@ class Anvil:
18
18
 
19
19
  def start_process(self) -> None:
20
20
  cmd = f"anvil -m '{self.mnemonic}' -p {self.port} --chain-id {self.chain_id}"
21
- self.process = Popen(cmd, shell=True) # nosec
21
+ self.process = Popen(cmd, shell=True) # noqa: S602 # nosec
22
22
  time.sleep(3)
23
23
 
24
24
  def stop(self) -> None:
mm_eth/cli/__init__.py ADDED
File without changes
mm_eth/cli/calcs.py ADDED
@@ -0,0 +1,112 @@
1
+ import random
2
+ from decimal import Decimal
3
+
4
+ from loguru import logger
5
+ from mm_std.random_ import random_decimal
6
+ from mm_std.str import split_on_plus_minus_tokens
7
+
8
+ from mm_eth.utils import from_wei_str, to_wei
9
+
10
+
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
54
+
55
+ if operator == "+":
56
+ result += item_value
57
+ if operator == "-":
58
+ result -= item_value
59
+
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)
77
+
78
+
79
+ def calc_function_args(value: str) -> str:
80
+ while True:
81
+ if "random(" not in value:
82
+ return value
83
+ start_index = value.index("random(")
84
+ stop_index = value.index(")", start_index)
85
+ random_range = [int(v.strip()) for v in value[start_index + 7 : stop_index].split(",")]
86
+ if len(random_range) != 2:
87
+ raise ValueError("wrong random(from,to) template")
88
+ rand_value = str(random.randint(random_range[0], random_range[1]))
89
+ value = value[0:start_index] + rand_value + value[stop_index + 1 :]
90
+
91
+
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
mm_eth/cli/cli.py ADDED
@@ -0,0 +1,237 @@
1
+ from enum import Enum
2
+ from typing import Annotated
3
+
4
+ import typer
5
+ from mm_std import PrintFormat, print_plain
6
+
7
+ from . import cli_utils
8
+ from .cmd import (
9
+ balance_cmd,
10
+ balances_cmd,
11
+ call_contract_cmd,
12
+ config_example_cmd,
13
+ deploy_cmd,
14
+ encode_input_data_cmd,
15
+ mnemonic_cmd,
16
+ node_cmd,
17
+ private_key_cmd,
18
+ rpc_cmd,
19
+ send_contract_cmd,
20
+ solc_cmd,
21
+ token_cmd,
22
+ transfer_erc20_cmd,
23
+ transfer_eth_cmd,
24
+ vault_cmd,
25
+ )
26
+
27
+ app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
28
+
29
+ wallet_app = typer.Typer(no_args_is_help=True, help="Wallet commands: generate mnemonic, private to address")
30
+ app.add_typer(wallet_app, name="wallet")
31
+ app.add_typer(wallet_app, name="w", hidden=True)
32
+
33
+
34
+ class ConfigExample(str, Enum):
35
+ TRANSFER_ETH = "transfer-eth"
36
+ TRANSFER_ERC20 = "transfer-erc20"
37
+ BALANCES = "balances"
38
+ CALL_CONTRACT = "call-contract"
39
+
40
+
41
+ @app.command(name="balance", help="Gen account balance")
42
+ def balance_command(
43
+ wallet_address: Annotated[str, typer.Argument()],
44
+ token_address: Annotated[str | None, typer.Option("--token", "-t")] = None,
45
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL")] = "", # nosec
46
+ wei: bool = typer.Option(False, "--wei", "-w", help="Print balances in wei units"),
47
+ print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.PLAIN,
48
+ ) -> None:
49
+ balance_cmd.run(rpc_url, wallet_address, token_address, wei, print_format)
50
+
51
+
52
+ @app.command(name="token", help="Get token info")
53
+ def token_command(
54
+ token_address: Annotated[str, typer.Argument()],
55
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL")] = "",
56
+ ) -> None:
57
+ token_cmd.run(rpc_url, token_address)
58
+
59
+
60
+ @app.command(name="node", help="Check RPC url")
61
+ def node_command(
62
+ urls: Annotated[list[str], typer.Argument()],
63
+ print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.TABLE,
64
+ proxy: Annotated[str | None, typer.Option("--proxy", "-p", help="Proxy")] = None,
65
+ ) -> None:
66
+ node_cmd.run(urls, print_format, proxy)
67
+
68
+
69
+ @wallet_app.command(name="mnemonic", help="Generate eth accounts based on a mnemonic")
70
+ def mnemonic_command( # nosec
71
+ mnemonic: Annotated[str, typer.Option("--mnemonic", "-m")] = "",
72
+ passphrase: Annotated[str, typer.Option("--passphrase", "-pass")] = "",
73
+ print_path: bool = typer.Option(False, "--print_path"),
74
+ path_prefix: Annotated[str, typer.Option("--path")] = "m/44'/60'/0'/0",
75
+ limit: int = typer.Option(10, "--limit", "-l"),
76
+ save_file: str = typer.Option("", "--save", "-s", help="Save private keys to a file"),
77
+ ) -> None:
78
+ mnemonic_cmd.run(
79
+ mnemonic,
80
+ passphrase=passphrase,
81
+ print_path=print_path,
82
+ limit=limit,
83
+ path_prefix=path_prefix,
84
+ save_file=save_file,
85
+ )
86
+
87
+
88
+ @wallet_app.command(name="private-key", help="Print an address for a private key")
89
+ def private_key_command(private_key: str) -> None:
90
+ private_key_cmd.run(private_key)
91
+
92
+
93
+ @app.command(name="solc", help="Compile a solidity file")
94
+ def solc_command(
95
+ contract_path: str,
96
+ tmp_dir: str = "/tmp", # noqa: S108 # nosec
97
+ print_format: Annotated[PrintFormat, typer.Option("--format", "-f", help="Print format")] = PrintFormat.PLAIN,
98
+ ) -> None:
99
+ solc_cmd.run(contract_path, tmp_dir, print_format)
100
+
101
+
102
+ @app.command(name="vault", help="Save private keys to vault")
103
+ def vault_command(
104
+ keys_url: Annotated[
105
+ str,
106
+ typer.Option(..., "--url", "-u", help="Url to keys, for example https://vault.site.com:8200/v1/kv/keys1"),
107
+ ],
108
+ vault_token: Annotated[
109
+ str,
110
+ typer.Option(..., "--token", "-t", prompt=True, hide_input=True, prompt_required=False, help="A vault token"),
111
+ ],
112
+ keys_file: Annotated[str, typer.Option(..., "--file", "-f", help="Path to a file with private keys")],
113
+ ) -> None:
114
+ vault_cmd.run(keys_url, vault_token, keys_file)
115
+
116
+
117
+ @app.command(name="rpc", help="Call a JSON-RPC method")
118
+ def rpc_command(
119
+ method: Annotated[str, typer.Argument()] = "",
120
+ params: Annotated[str, typer.Argument()] = "[]",
121
+ rpc_url: Annotated[str, typer.Option("--url", "-u", envvar="ETH_RPC_URL", help="RPC node url")] = "",
122
+ hex2dec: Annotated[bool, typer.Option("--hex2dec", "-d", help="Print result in decimal value")] = False,
123
+ ) -> None:
124
+ rpc_cmd.run(rpc_url, method, params, hex2dec)
125
+
126
+
127
+ @app.command(name="transfer-eth", help="Transfer eth / base token from one or many accounts")
128
+ def transfer_eth_command(
129
+ config_path: str,
130
+ print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
131
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
132
+ emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
133
+ no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
134
+ debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
135
+ ) -> None:
136
+ 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,
143
+ )
144
+
145
+
146
+ @app.command(name="transfer-erc20", help="Transfer ERC20 token from one or many accounts")
147
+ def transfer_erc20_command(
148
+ config_path: str,
149
+ print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
150
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
151
+ emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
152
+ no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
153
+ debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
154
+ ) -> None:
155
+ 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,
162
+ )
163
+
164
+
165
+ @app.command(name="send-contract", help="Send transactions to a contract")
166
+ def send_contract_command(
167
+ config_path: str,
168
+ print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
169
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
170
+ emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
171
+ no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
172
+ debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
173
+ ) -> None:
174
+ 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,
181
+ )
182
+
183
+
184
+ @app.command(name="balances", help="Print base and ERC20 token balances")
185
+ def balances_command(
186
+ config_path: str,
187
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
188
+ nonce: bool = typer.Option(False, "--nonce", "-n", help="Print nonce also"),
189
+ wei: bool = typer.Option(False, "--wei", "-w", help="Show balances in WEI"),
190
+ ) -> None:
191
+ balances_cmd.run(config_path, print_config, wei, nonce)
192
+
193
+
194
+ @app.command(name="call-contract", help="Call a method on a contract")
195
+ def call_contract_command(
196
+ config_path: str,
197
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
198
+ ) -> None:
199
+ call_contract_cmd.run(config_path, print_config)
200
+
201
+
202
+ @app.command(name="deploy", help="Deploy a smart contract onchain")
203
+ def deploy_command(
204
+ config_path: str,
205
+ print_config: bool = typer.Option(False, "--config", "-c", help="Print config only and exit"),
206
+ ) -> None:
207
+ deploy_cmd.run(config_path, print_config=print_config)
208
+
209
+
210
+ @app.command(name="config-example", help="Print an example of config for a command")
211
+ def config_example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
212
+ config_example_cmd.run(command)
213
+
214
+
215
+ @app.command(name="encode-input-data", help="Encode input data by a function signature")
216
+ def encode_input_data(
217
+ funtion_signature: str = typer.Argument(help="Function signature, for example: transfer(address, uint256)"),
218
+ args_str: str = typer.Argument(
219
+ help="""Function arguments, as an array string. For example: '["0xA659FB44eB5d4bFaC1074Cb426b1b11D58D28308", 123]' """,
220
+ ),
221
+ ) -> None:
222
+ encode_input_data_cmd.run(funtion_signature, args_str)
223
+
224
+
225
+ def version_callback(value: bool) -> None:
226
+ if value:
227
+ print_plain(f"mm-eth-cli version: {cli_utils.get_version()}")
228
+ raise typer.Exit
229
+
230
+
231
+ @app.callback()
232
+ def main(_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True)) -> None:
233
+ pass
234
+
235
+
236
+ if __name__ == "__main_":
237
+ app()
@@ -0,0 +1,105 @@
1
+ import importlib.metadata
2
+ import sys
3
+ from pathlib import Path
4
+
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
11
+
12
+
13
+ def get_version() -> str:
14
+ return importlib.metadata.version("mm-eth-cli")
15
+
16
+
17
+ def public_rpc_url(url: str | None) -> str:
18
+ if not url or url == "1":
19
+ return "https://ethereum.publicnode.com"
20
+ if url.startswith(("http://", "https://", "ws://", "wss://")):
21
+ return url
22
+
23
+ match url.lower():
24
+ case "opbnb" | "204":
25
+ return "https://opbnb-mainnet-rpc.bnbchain.org"
26
+ case _:
27
+ return url
28
+
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 read_config[T: BaseConfig](config_type: type[T], config_path: Path) -> T:
100
+ res = config_type.read_config(config_path)
101
+ if res.is_ok():
102
+ return res.unwrap()
103
+
104
+ print_json(res.err)
105
+ sys.exit(1)
File without changes
@@ -0,0 +1,51 @@
1
+ from mm_std import Err, Ok, PrintFormat, fatal, print_json, print_plain
2
+
3
+ from mm_eth import erc20, rpc
4
+ from mm_eth.cli.cli_utils import public_rpc_url
5
+ from mm_eth.utils import from_wei_str
6
+
7
+
8
+ def run(rpc_url: str, wallet_address: str, token_address: str | None, wei: bool, print_format: PrintFormat) -> None:
9
+ rpc_url = public_rpc_url(rpc_url)
10
+ json_result: dict[str, object] = {}
11
+
12
+ # nonce
13
+ nonce = rpc.eth_get_transaction_count(rpc_url, wallet_address).ok_or_err()
14
+ print_plain(f"nonce: {nonce}", print_format)
15
+ json_result["nonce"] = nonce
16
+
17
+ # balance
18
+ balance_res = rpc.eth_get_balance(rpc_url, wallet_address)
19
+ if isinstance(balance_res, Ok):
20
+ balance = str(balance_res.ok) if wei else from_wei_str(balance_res.ok, "eth")
21
+ else:
22
+ balance = balance_res.err
23
+ print_plain(f"eth_balance: {balance}", print_format)
24
+ json_result["eth_balance"] = balance
25
+
26
+ if token_address is not None:
27
+ # token decimal
28
+ decimals_res = erc20.get_decimals(rpc_url, token_address)
29
+ if isinstance(decimals_res, Err):
30
+ fatal(f"error: can't get token decimals: {decimals_res.err}")
31
+ decimals = decimals_res.ok
32
+ print_plain(f"token_decimal: {decimals}", print_format)
33
+ json_result["token_decimal"] = decimals
34
+
35
+ # token symbol
36
+ symbol_res = erc20.get_symbol(rpc_url, token_address)
37
+ if isinstance(symbol_res, Err):
38
+ fatal(f"error: can't get token symbol: {symbol_res.err}")
39
+ symbol = symbol_res.ok
40
+ print_plain(f"token_symbol: {symbol}", print_format)
41
+ json_result["token_symbol"] = symbol
42
+
43
+ # token balance
44
+ balance_res = erc20.get_balance(rpc_url, token_address, wallet_address)
45
+ if isinstance(balance_res, Err):
46
+ fatal(f"error: can't get token balance: {balance_res.err}")
47
+ balance = str(balance_res.ok) if wei else from_wei_str(balance_res.ok, "t", decimals=decimals)
48
+ print_plain(f"token_balance: {balance}", print_format)
49
+ json_result["token_balance"] = balance
50
+
51
+ print_json(json_result, print_format)
@@ -0,0 +1,124 @@
1
+ import sys
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+ from mm_std import BaseConfig, Err, Ok, fatal, print_json
6
+ from pydantic import Field, field_validator
7
+ from rich.live import Live
8
+ from rich.table import Table
9
+
10
+ from mm_eth import erc20, rpc
11
+ from mm_eth.cli import cli_utils, validators
12
+ from mm_eth.utils import from_token_wei_str, from_wei_str
13
+
14
+
15
+ class Config(BaseConfig):
16
+ addresses: list[str]
17
+ tokens: list[str] = Field(default_factory=list)
18
+ nodes: list[str]
19
+ round_ndigits: int = 5
20
+
21
+ @field_validator("nodes", mode="before")
22
+ @classmethod
23
+ def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
24
+ return validators.nodes_validator(v)
25
+
26
+ @field_validator("tokens", "addresses", mode="before")
27
+ @classmethod
28
+ def addresses_validator(cls, v: str | list[str] | None) -> list[str]:
29
+ return validators.addresses_validator(v)
30
+
31
+
32
+ @dataclass
33
+ class Token:
34
+ address: str
35
+ decimals: int
36
+ symbol: str
37
+
38
+
39
+ def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> None:
40
+ config = cli_utils.read_config(Config, Path(config_path))
41
+ if print_config:
42
+ print_json(config.model_dump())
43
+ sys.exit(0)
44
+
45
+ tokens = _get_tokens_info(config)
46
+
47
+ table = Table(title="balances")
48
+ table.add_column("address")
49
+ if show_nonce:
50
+ table.add_column("nonce")
51
+ table.add_column("wei" if wei else "eth")
52
+ for t in tokens:
53
+ table.add_column(t.symbol)
54
+
55
+ base_sum = 0
56
+ token_sum: dict[str, int] = {t.address: 0 for t in tokens}
57
+ with Live(table, refresh_per_second=0.5):
58
+ for address in config.addresses:
59
+ row = [address]
60
+ if show_nonce:
61
+ row.append(str(rpc.eth_get_transaction_count(config.nodes, address, attempts=5).ok_or_err()))
62
+
63
+ base_balance_res = rpc.eth_get_balance(config.nodes, address, attempts=5)
64
+ if isinstance(base_balance_res, Ok):
65
+ base_sum += base_balance_res.ok
66
+ if wei:
67
+ row.append(str(base_balance_res.ok))
68
+ else:
69
+ row.append(
70
+ from_wei_str(base_balance_res.ok, "eth", round_ndigits=config.round_ndigits, print_unit_name=False),
71
+ )
72
+ else:
73
+ row.append(base_balance_res.err)
74
+
75
+ for t in tokens:
76
+ token_balance_res = erc20.get_balance(config.nodes, t.address, address, attempts=5)
77
+ if isinstance(token_balance_res, Ok):
78
+ token_sum[t.address] += token_balance_res.ok
79
+ if wei:
80
+ row.append(str(token_balance_res.ok))
81
+ else:
82
+ row.append(
83
+ from_token_wei_str(
84
+ token_balance_res.ok,
85
+ decimals=t.decimals,
86
+ round_ndigits=config.round_ndigits,
87
+ ),
88
+ )
89
+ else:
90
+ row.append(token_balance_res.err)
91
+
92
+ table.add_row(*row)
93
+
94
+ sum_row = ["sum"]
95
+ if show_nonce:
96
+ sum_row.append("")
97
+ if wei:
98
+ sum_row.append(str(base_sum))
99
+ sum_row.extend([str(token_sum[t.address]) for t in tokens])
100
+ else:
101
+ sum_row.append(from_wei_str(base_sum, "eth", round_ndigits=config.round_ndigits, print_unit_name=False))
102
+ sum_row.extend(
103
+ [from_token_wei_str(token_sum[t.address], t.decimals, round_ndigits=config.round_ndigits) for t in tokens]
104
+ )
105
+
106
+ table.add_row(*sum_row)
107
+
108
+
109
+ def _get_tokens_info(config: Config) -> list[Token]:
110
+ result: list[Token] = []
111
+ for address in config.tokens:
112
+ decimals_res = erc20.get_decimals(config.nodes, address, attempts=5)
113
+ if isinstance(decimals_res, Err):
114
+ fatal(f"can't get token {address} decimals: {decimals_res.err}")
115
+ decimal = decimals_res.ok
116
+
117
+ symbols_res = erc20.get_symbol(config.nodes, address, attempts=5)
118
+ if isinstance(symbols_res, Err):
119
+ fatal(f"can't get token {address} symbol: {symbols_res.err}")
120
+ symbol = symbols_res.ok
121
+
122
+ result.append(Token(address=address, decimals=decimal, symbol=symbol))
123
+
124
+ return result