mm-eth 0.2.1__py3-none-any.whl → 0.2.3__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/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,102 @@
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 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)
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,120 @@
1
+ from dataclasses import dataclass
2
+
3
+ from mm_std import BaseConfig, Err, Ok, fatal
4
+ from pydantic import Field, field_validator
5
+ from rich.live import Live
6
+ from rich.table import Table
7
+
8
+ from mm_eth import erc20, rpc
9
+ from mm_eth.cli import cli_utils, validators
10
+ from mm_eth.utils import from_token_wei_str, from_wei_str
11
+
12
+
13
+ class Config(BaseConfig):
14
+ addresses: list[str]
15
+ tokens: list[str] = Field(default_factory=list)
16
+ nodes: list[str]
17
+ round_ndigits: int = 5
18
+
19
+ @field_validator("nodes", mode="before")
20
+ @classmethod
21
+ def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
22
+ return validators.nodes_validator(v)
23
+
24
+ @field_validator("tokens", "addresses", mode="before")
25
+ @classmethod
26
+ def addresses_validator(cls, v: str | list[str] | None) -> list[str]:
27
+ return validators.addresses_validator(v)
28
+
29
+
30
+ @dataclass
31
+ class Token:
32
+ address: str
33
+ decimals: int
34
+ symbol: str
35
+
36
+
37
+ def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> None:
38
+ config = Config.read_config_or_exit(config_path)
39
+ cli_utils.print_config_and_exit(print_config, config)
40
+
41
+ tokens = _get_tokens_info(config)
42
+
43
+ table = Table(title="balances")
44
+ table.add_column("address")
45
+ if show_nonce:
46
+ table.add_column("nonce")
47
+ table.add_column("wei" if wei else "eth")
48
+ for t in tokens:
49
+ table.add_column(t.symbol)
50
+
51
+ base_sum = 0
52
+ token_sum: dict[str, int] = {t.address: 0 for t in tokens}
53
+ with Live(table, refresh_per_second=0.5):
54
+ for address in config.addresses:
55
+ row = [address]
56
+ if show_nonce:
57
+ row.append(str(rpc.eth_get_transaction_count(config.nodes, address, attempts=5).ok_or_err()))
58
+
59
+ base_balance_res = rpc.eth_get_balance(config.nodes, address, attempts=5)
60
+ if isinstance(base_balance_res, Ok):
61
+ base_sum += base_balance_res.ok
62
+ if wei:
63
+ row.append(str(base_balance_res.ok))
64
+ else:
65
+ row.append(
66
+ from_wei_str(base_balance_res.ok, "eth", round_ndigits=config.round_ndigits, print_unit_name=False),
67
+ )
68
+ else:
69
+ row.append(base_balance_res.err)
70
+
71
+ for t in tokens:
72
+ token_balance_res = erc20.get_balance(config.nodes, t.address, address, attempts=5)
73
+ if isinstance(token_balance_res, Ok):
74
+ token_sum[t.address] += token_balance_res.ok
75
+ if wei:
76
+ row.append(str(token_balance_res.ok))
77
+ else:
78
+ row.append(
79
+ from_token_wei_str(
80
+ token_balance_res.ok,
81
+ decimals=t.decimals,
82
+ round_ndigits=config.round_ndigits,
83
+ ),
84
+ )
85
+ else:
86
+ row.append(token_balance_res.err)
87
+
88
+ table.add_row(*row)
89
+
90
+ sum_row = ["sum"]
91
+ if show_nonce:
92
+ sum_row.append("")
93
+ if wei:
94
+ sum_row.append(str(base_sum))
95
+ sum_row.extend([str(token_sum[t.address]) for t in tokens])
96
+ else:
97
+ sum_row.append(from_wei_str(base_sum, "eth", round_ndigits=config.round_ndigits, print_unit_name=False))
98
+ sum_row.extend(
99
+ [from_token_wei_str(token_sum[t.address], t.decimals, round_ndigits=config.round_ndigits) for t in tokens]
100
+ )
101
+
102
+ table.add_row(*sum_row)
103
+
104
+
105
+ def _get_tokens_info(config: Config) -> list[Token]:
106
+ result: list[Token] = []
107
+ for address in config.tokens:
108
+ decimals_res = erc20.get_decimals(config.nodes, address, attempts=5)
109
+ if isinstance(decimals_res, Err):
110
+ fatal(f"can't get token {address} decimals: {decimals_res.err}")
111
+ decimal = decimals_res.ok
112
+
113
+ symbols_res = erc20.get_symbol(config.nodes, address, attempts=5)
114
+ if isinstance(symbols_res, Err):
115
+ fatal(f"can't get token {address} symbol: {symbols_res.err}")
116
+ symbol = symbols_res.ok
117
+
118
+ result.append(Token(address=address, decimals=decimal, symbol=symbol))
119
+
120
+ return result
@@ -0,0 +1,39 @@
1
+ import json
2
+ from logging import fatal
3
+
4
+ from mm_std import BaseConfig, Err, print_plain
5
+ from pydantic import StrictStr
6
+
7
+ from mm_eth import abi, rpc
8
+ from mm_eth.cli import cli_utils
9
+
10
+
11
+ class Config(BaseConfig):
12
+ contract_address: StrictStr
13
+ function_signature: str
14
+ function_args: StrictStr = "[]"
15
+ outputs_types: str | None = None
16
+ node: str
17
+
18
+
19
+ def run(config_path: str, print_config: bool) -> None:
20
+ config = Config.read_config_or_exit(config_path)
21
+ cli_utils.print_config_and_exit(print_config, config)
22
+
23
+ input_data = abi.encode_function_input_by_signature(
24
+ config.function_signature,
25
+ json.loads(config.function_args.replace("'", '"')),
26
+ )
27
+ res = rpc.eth_call(config.node, config.contract_address, input_data)
28
+ if isinstance(res, Err):
29
+ return fatal(f"error: {res.err}")
30
+
31
+ result = res.ok
32
+ if config.outputs_types is not None:
33
+ decode_res = abi.decode_data(_get_types(config.outputs_types), result)
34
+ result = decode_res[0] if len(decode_res) == 1 else str(decode_res)
35
+ print_plain(result)
36
+
37
+
38
+ def _get_types(data: str) -> list[str]:
39
+ return [t.strip() for t in data.split(",") if t.strip()]
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ from mm_std import print_plain
4
+
5
+
6
+ def run(command: str) -> None:
7
+ command = command.replace("-", "_")
8
+ example_file = Path(Path(__file__).parent.absolute(), "../config_examples", f"{command}.yml")
9
+ print_plain(example_file.read_text())