mm-eth 0.1.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.
Files changed (50) hide show
  1. mm_eth/__init__.py +0 -0
  2. mm_eth/abi/zksync.json +2092 -0
  3. mm_eth/abi.py +130 -0
  4. mm_eth/account.py +70 -0
  5. mm_eth/anvil.py +56 -0
  6. mm_eth/cli/__init__.py +0 -0
  7. mm_eth/cli/calcs.py +88 -0
  8. mm_eth/cli/cli.py +233 -0
  9. mm_eth/cli/cli_helpers.py +195 -0
  10. mm_eth/cli/cli_utils.py +150 -0
  11. mm_eth/cli/cmd/__init__.py +0 -0
  12. mm_eth/cli/cmd/balance_cmd.py +59 -0
  13. mm_eth/cli/cmd/balances_cmd.py +121 -0
  14. mm_eth/cli/cmd/call_contract_cmd.py +44 -0
  15. mm_eth/cli/cmd/config_example_cmd.py +9 -0
  16. mm_eth/cli/cmd/deploy_cmd.py +41 -0
  17. mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
  18. mm_eth/cli/cmd/mnemonic_cmd.py +27 -0
  19. mm_eth/cli/cmd/node_cmd.py +47 -0
  20. mm_eth/cli/cmd/private_key_cmd.py +10 -0
  21. mm_eth/cli/cmd/rpc_cmd.py +81 -0
  22. mm_eth/cli/cmd/send_contract_cmd.py +247 -0
  23. mm_eth/cli/cmd/solc_cmd.py +25 -0
  24. mm_eth/cli/cmd/token_cmd.py +29 -0
  25. mm_eth/cli/cmd/transfer_erc20_cmd.py +275 -0
  26. mm_eth/cli/cmd/transfer_eth_cmd.py +252 -0
  27. mm_eth/cli/cmd/vault_cmd.py +16 -0
  28. mm_eth/cli/config_examples/balances.yml +15 -0
  29. mm_eth/cli/config_examples/call_contract.yml +5 -0
  30. mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
  31. mm_eth/cli/config_examples/transfer_eth.yml +24 -0
  32. mm_eth/cli/validators.py +84 -0
  33. mm_eth/deploy.py +20 -0
  34. mm_eth/ens.py +16 -0
  35. mm_eth/erc20.py +240 -0
  36. mm_eth/ethernodes.py +34 -0
  37. mm_eth/py.typed +0 -0
  38. mm_eth/rpc.py +478 -0
  39. mm_eth/services/__init__.py +0 -0
  40. mm_eth/solc.py +34 -0
  41. mm_eth/tx.py +164 -0
  42. mm_eth/types.py +5 -0
  43. mm_eth/utils.py +245 -0
  44. mm_eth/vault.py +38 -0
  45. mm_eth/zksync.py +203 -0
  46. mm_eth-0.1.0.dist-info/METADATA +24 -0
  47. mm_eth-0.1.0.dist-info/RECORD +50 -0
  48. mm_eth-0.1.0.dist-info/WHEEL +5 -0
  49. mm_eth-0.1.0.dist-info/entry_points.txt +2 -0
  50. mm_eth-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+ import importlib.metadata
2
+ import sys
3
+ from typing import NoReturn
4
+
5
+ from loguru import logger
6
+ from mm_std import Err
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 calcs
12
+ from mm_eth.utils import from_wei_str
13
+
14
+
15
+ def get_nonce(nodes: list[str] | str, address: str, log_prefix: str | None = None) -> int | None:
16
+ res = rpc.eth_get_transaction_count(nodes, address, attempts=5)
17
+ prefix = log_prefix or address
18
+ logger.debug(f"{prefix}: nonce={res.ok_or_err()}")
19
+ if isinstance(res, Err):
20
+ logger.info(f"{prefix}: nonce error, {res.err}")
21
+ return None
22
+ return res.ok
23
+
24
+
25
+ def get_base_fee(nodes: list[str], log_prefix: str | None = None) -> int | None:
26
+ res = rpc.get_base_fee_per_gas(nodes)
27
+ prefix = _get_prefix(log_prefix)
28
+ logger.debug(f"{prefix}base_fee={res.ok_or_err()}")
29
+ if isinstance(res, Err):
30
+ logger.info(f"{prefix}base_fee error, {res.err}")
31
+ return None
32
+ return res.ok
33
+
34
+
35
+ def calc_max_fee_per_gas(nodes: list[str], max_fee_per_gas: str, log_prefix: str | None = None) -> int | None:
36
+ if "base" in max_fee_per_gas.lower():
37
+ base_fee = get_base_fee(nodes, log_prefix)
38
+ if base_fee is None:
39
+ return None
40
+ return calcs.calc_var_wei_value(max_fee_per_gas, var_name="base", var_value=base_fee)
41
+ return calcs.calc_var_wei_value(max_fee_per_gas)
42
+
43
+
44
+ def is_max_fee_per_gas_limit_exceeded(
45
+ max_fee_per_gas: int,
46
+ max_fee_per_gas_limit: str | None,
47
+ log_prefix: str | None = None,
48
+ ) -> bool:
49
+ if max_fee_per_gas_limit is None:
50
+ return False
51
+ max_fee_per_gas_limit_value = calcs.calc_var_wei_value(max_fee_per_gas_limit)
52
+ if max_fee_per_gas > max_fee_per_gas_limit_value:
53
+ prefix = _get_prefix(log_prefix)
54
+ logger.info(
55
+ "{}max_fee_per_gas_limit is exeeded, max_fee_per_gas={}, max_fee_per_gas_limit={}",
56
+ prefix,
57
+ from_wei_str(max_fee_per_gas, "gwei"),
58
+ from_wei_str(max_fee_per_gas_limit_value, "gwei"),
59
+ )
60
+ return True
61
+ return False
62
+
63
+
64
+ def is_value_less_min_limit(
65
+ value_min_limit: str | None,
66
+ value: int,
67
+ value_unit: str,
68
+ decimals: int | None = None,
69
+ log_prefix: str | None = None,
70
+ ) -> bool:
71
+ if value_min_limit is None:
72
+ return False
73
+ if value < calcs.calc_var_wei_value(value_min_limit, decimals=decimals):
74
+ prefix = _get_prefix(log_prefix)
75
+ logger.info("{}value is less min limit, value={}", prefix, from_wei_str(value, value_unit, decimals=decimals))
76
+ return True
77
+ return False
78
+
79
+
80
+ def calc_gas(
81
+ *,
82
+ nodes: list[str],
83
+ gas: str,
84
+ from_address: str,
85
+ to_address: str,
86
+ value: int | None = None,
87
+ data: str | None = None,
88
+ log_prefix: str | None = None,
89
+ ) -> int | None:
90
+ estimate_value = None
91
+ if "estimate" in gas.lower():
92
+ prefix = _get_prefix(log_prefix)
93
+ res = rpc.eth_estimate_gas(nodes, from_address, to_address, data=data, value=value, attempts=5)
94
+ logger.debug(f"{prefix}gas_estimate={res.ok_or_err()}")
95
+ if isinstance(res, Err):
96
+ logger.info(f"{prefix}estimate_gas error, {res.err}")
97
+ return None
98
+ estimate_value = res.ok
99
+ return calcs.calc_var_wei_value(gas, var_name="estimate", var_value=estimate_value)
100
+
101
+
102
+ def calc_eth_value(
103
+ *,
104
+ nodes: list[str],
105
+ value_str: str,
106
+ address: str,
107
+ gas: int | None = None,
108
+ max_fee_per_gas: int | None = None,
109
+ log_prefix: str | None = None,
110
+ ) -> int | None:
111
+ balance_value = None
112
+ if "balance" in value_str.lower():
113
+ prefix = _get_prefix(log_prefix)
114
+ res = rpc.eth_get_balance(nodes, address, attempts=5)
115
+ logger.debug(f"{prefix}balance={res.ok_or_err()}")
116
+ if isinstance(res, Err):
117
+ logger.info(f"{prefix}balance error, {res.err}")
118
+ return None
119
+ balance_value = res.ok
120
+ value = calcs.calc_var_wei_value(value_str, var_name="balance", var_value=balance_value)
121
+ if "balance" in value_str.lower() and gas is not None and max_fee_per_gas is not None:
122
+ value = value - gas * max_fee_per_gas
123
+ return value
124
+
125
+
126
+ def calc_erc20_value(
127
+ *,
128
+ nodes: list[str],
129
+ value_str: str,
130
+ wallet_address: str,
131
+ token_address: str,
132
+ decimals: int,
133
+ log_prefix: str | None = None,
134
+ ) -> int | None:
135
+ value_str = value_str.lower()
136
+ balance_value = None
137
+ if "balance" in value_str:
138
+ prefix = _get_prefix(log_prefix)
139
+ res = erc20.get_balance(nodes, token_address, wallet_address, attempts=5)
140
+ logger.debug(f"{prefix}balance={res.ok_or_err()}")
141
+ if isinstance(res, Err):
142
+ logger.info(f"{prefix}balance error, {res.err}")
143
+ return None
144
+ balance_value = res.ok
145
+ return calcs.calc_var_wei_value(value_str, var_name="balance", var_value=balance_value, decimals=decimals)
146
+
147
+
148
+ def print_balances(
149
+ rpc_nodes: list[str],
150
+ addresses: list[str],
151
+ *,
152
+ token_address: str | None = None,
153
+ token_decimals: int | None = None,
154
+ round_ndigits: int = 5,
155
+ ) -> None:
156
+ table = Table(title="balances")
157
+ table.add_column("n")
158
+ table.add_column("address")
159
+ table.add_column("nonce")
160
+ table.add_column("balance, eth")
161
+ if token_address is not None and token_decimals is not None:
162
+ table.add_column("token, t")
163
+ with Live(table, refresh_per_second=0.5):
164
+ count = 0
165
+ for address in addresses:
166
+ count += 1
167
+ nonce = str(rpc.eth_get_transaction_count(rpc_nodes, address, attempts=5).ok_or_err())
168
+ balance = rpc.eth_get_balance(rpc_nodes, address, attempts=5).map_or_else(
169
+ lambda err: err,
170
+ lambda ok: from_wei_str(ok, "eth", round_ndigits),
171
+ )
172
+ row: list[str] = [str(count), address, nonce, balance]
173
+ if token_address is not None and token_decimals is not None:
174
+ erc20_balance = erc20.get_balance(rpc_nodes, token_address, address, attempts=5).map_or_else(
175
+ lambda err: err,
176
+ lambda ok: from_wei_str(ok, "t", decimals=token_decimals, round_ndigits=round_ndigits),
177
+ )
178
+ row.append(erc20_balance)
179
+ table.add_row(*row)
180
+
181
+
182
+ def fatal(message: str) -> NoReturn:
183
+ print(f"error: {message}", file=sys.stderr) # noqa: T201
184
+ sys.exit(1)
185
+
186
+
187
+ def get_version() -> str:
188
+ return importlib.metadata.version("mm-eth")
189
+
190
+
191
+ def _get_prefix(log_prefix: str | None) -> str:
192
+ prefix = log_prefix or ""
193
+ if prefix:
194
+ prefix += ": "
195
+ return prefix
@@ -0,0 +1,150 @@
1
+ import sys
2
+ import time
3
+ from pathlib import Path
4
+ from typing import TypeVar
5
+
6
+ import eth_utils
7
+ import yaml
8
+ from loguru import logger
9
+ from mm_std import Err, fatal, str_to_list, utc_now
10
+ from pydantic import BaseModel, ConfigDict, ValidationError
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from mm_eth import account, rpc
15
+ from mm_eth.account import is_private_key
16
+ from mm_eth.cli import calcs
17
+
18
+
19
+ class BaseConfig(BaseModel):
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+
23
+ def check_nodes_for_chain_id(nodes: list[str], chain_id: int) -> None:
24
+ for node in nodes:
25
+ res = rpc.eth_chain_id(node, timeout=7)
26
+ if isinstance(res, Err):
27
+ fatal(f"can't get chain_id for {node}, error={res.err}")
28
+ if res.ok != chain_id:
29
+ fatal(f"node {node} has a wrong chain_id: {res.ok}")
30
+
31
+
32
+ def check_private_keys(addresses: list[str], private_keys: dict[str, str]) -> None:
33
+ for address in addresses:
34
+ address = address.lower()
35
+ if address not in private_keys:
36
+ fatal(f"no private key for {address}")
37
+ if account.private_to_address(private_keys[address]) != address:
38
+ fatal(f"no private key for {address}")
39
+
40
+
41
+ def delay(value: str | None) -> None:
42
+ if value is None:
43
+ return
44
+ time.sleep(float(calcs.calc_decimal_value(value)))
45
+
46
+
47
+ T = TypeVar("T")
48
+
49
+
50
+ def read_config(config_cls: type[T], config_path: str) -> T:
51
+ try:
52
+ with open(config_path) as f:
53
+ config = config_cls(**yaml.full_load(f))
54
+ return config
55
+ except ValidationError as err:
56
+ table = Table(title="config validation errors")
57
+ table.add_column("field")
58
+ table.add_column("message")
59
+ for e in err.errors():
60
+ loc = e["loc"]
61
+ field = str(loc[0]) if len(loc) > 0 else ""
62
+ table.add_row(field, e["msg"])
63
+ console = Console()
64
+ console.print(table)
65
+ exit(1)
66
+
67
+
68
+ def log(log_path: str | None, *messages: object) -> None:
69
+ if log_path is None:
70
+ return
71
+ message = ", ".join([str(m) for m in messages])
72
+ message = f"{utc_now()}, {message}\n"
73
+ with open(Path(log_path).expanduser(), "a") as f:
74
+ f.write(message)
75
+
76
+
77
+ def load_tx_addresses_from_str(v: str | None) -> list[tuple[str, str]]:
78
+ result: list[tuple[str, str]] = []
79
+ if v is None:
80
+ return result
81
+ for line in str_to_list(v, remove_comments=True):
82
+ arr = line.split()
83
+ if len(arr) == 2 and eth_utils.is_address(arr[0]) and eth_utils.is_address(arr[1]):
84
+ result.append((arr[0].lower(), arr[1].lower()))
85
+ return result
86
+
87
+
88
+ def load_tx_addresses_from_files(addresses_from_file: str, addresses_to_file: str) -> list[tuple[str, str]]:
89
+ from_file = Path(addresses_from_file).expanduser()
90
+ to_file = Path(addresses_to_file).expanduser()
91
+ if not from_file.is_file():
92
+ raise ValueError(f"can't read addresses from 'addresses_from_file={addresses_from_file}")
93
+ if not to_file.is_file():
94
+ raise ValueError(f"can't read addresses from 'addresses_to_file={addresses_to_file}")
95
+
96
+ # get addresses_from
97
+ addresses_from = []
98
+ for line in from_file.read_text().strip().split("\n"):
99
+ if not eth_utils.is_address(line):
100
+ raise ValueError(f"illigal address in addresses_from_file: {line}")
101
+ addresses_from.append(line.lower())
102
+
103
+ # get addresses_to
104
+ addresses_to = []
105
+ for line in to_file.read_text().strip().split("\n"):
106
+ if not eth_utils.is_address(line):
107
+ raise ValueError(f"illigal address in addresses_to_file: {line}")
108
+ addresses_to.append(line.lower())
109
+
110
+ if len(addresses_from) != len(addresses_to):
111
+ raise ValueError("len(addresses_from) != len(addresses_to)")
112
+
113
+ return list(zip(addresses_from, addresses_to, strict=True))
114
+
115
+
116
+ def load_private_keys_from_file(private_keys_file: str) -> list[str]:
117
+ result: list[str] = []
118
+ for item in Path(private_keys_file).expanduser().read_text().split():
119
+ if is_private_key(item):
120
+ result.append(item)
121
+ return result
122
+
123
+
124
+ def init_logger(debug: bool, log_debug_file: str | None, log_info_file: str | None) -> None:
125
+ if debug:
126
+ level = "DEBUG"
127
+ format_ = "<green>{time:YYYY-MM-DD HH:mm:ss}</green> <level>{level}</level> {message}"
128
+ else:
129
+ level = "INFO"
130
+ format_ = "{message}"
131
+
132
+ logger.remove()
133
+ logger.add(sys.stderr, format=format_, colorize=True, level=level)
134
+ if log_debug_file:
135
+ logger.add(Path(log_debug_file).expanduser(), format="{time:YYYY-MM-DD HH:mm:ss} {level} {message}")
136
+ if log_info_file:
137
+ logger.add(Path(log_info_file).expanduser(), format="{message}", level="INFO")
138
+
139
+
140
+ def public_rpc_url(url: str | None) -> str:
141
+ if not url or url == "1":
142
+ return "https://ethereum.publicnode.com"
143
+ if url.startswith(("http://", "https://", "ws://", "wss://")):
144
+ return url
145
+
146
+ match url.lower():
147
+ case "opbnb" | "204":
148
+ return "https://opbnb-mainnet-rpc.bnbchain.org"
149
+ case _:
150
+ return url
File without changes
@@ -0,0 +1,59 @@
1
+ from mm_std import Err, Ok, print_json, print_plain
2
+
3
+ from mm_eth import erc20, rpc
4
+ from mm_eth.cli import cli_helpers
5
+ from mm_eth.cli.cli import PrintFormat
6
+ from mm_eth.cli.cli_utils import public_rpc_url
7
+ from mm_eth.utils import from_wei_str
8
+
9
+
10
+ def run(rpc_url: str, wallet_address: str, token_address: str | None, wei: bool, print_format: PrintFormat) -> None:
11
+ rpc_url = public_rpc_url(rpc_url)
12
+ json_result: dict[str, object] = {}
13
+
14
+ # nonce
15
+ nonce = rpc.eth_get_transaction_count(rpc_url, wallet_address).ok_or_err()
16
+ print_plain(f"nonce: {nonce}", print_format)
17
+ json_result["nonce"] = nonce
18
+
19
+ # balance
20
+ balance_res = rpc.eth_get_balance(rpc_url, wallet_address)
21
+ if isinstance(balance_res, Ok):
22
+ if wei:
23
+ balance = str(balance_res.ok)
24
+ else:
25
+ balance = from_wei_str(balance_res.ok, "eth")
26
+ else:
27
+ balance = balance_res.err
28
+ print_plain(f"eth_balance: {balance}", print_format)
29
+ json_result["eth_balance"] = balance
30
+
31
+ if token_address is not None:
32
+ # token decimal
33
+ decimals_res = erc20.get_decimals(rpc_url, token_address)
34
+ if isinstance(decimals_res, Err):
35
+ cli_helpers.fatal(f"can't get token decimals: {decimals_res.err}")
36
+ decimals = decimals_res.ok
37
+ print_plain(f"token_decimal: {decimals}", print_format)
38
+ json_result["token_decimal"] = decimals
39
+
40
+ # token symbol
41
+ symbol_res = erc20.get_symbol(rpc_url, token_address)
42
+ if isinstance(symbol_res, Err):
43
+ cli_helpers.fatal(f"can't get token symbol: {symbol_res.err}")
44
+ symbol = symbol_res.ok
45
+ print_plain(f"token_symbol: {symbol}", print_format)
46
+ json_result["token_symbol"] = symbol
47
+
48
+ # token balance
49
+ balance_res = erc20.get_balance(rpc_url, token_address, wallet_address)
50
+ if isinstance(balance_res, Err):
51
+ cli_helpers.fatal(f"can't get token balance: {balance_res.err}")
52
+ if wei:
53
+ balance = str(balance_res.ok)
54
+ else:
55
+ balance = from_wei_str(balance_res.ok, "t", decimals=decimals)
56
+ print_plain(f"token_balance: {balance}", print_format)
57
+ json_result["token_balance"] = balance
58
+
59
+ print_json(json_result, print_format)
@@ -0,0 +1,121 @@
1
+ from dataclasses import dataclass
2
+
3
+ from mm_std import Err, Ok, fatal, json_dumps
4
+ from pydantic import BaseModel, Field, field_validator
5
+ from rich.console import Console
6
+ from rich.live import Live
7
+ from rich.table import Table
8
+
9
+ from mm_eth import erc20, rpc
10
+ from mm_eth.cli import cli_utils, validators
11
+ from mm_eth.utils import from_token_wei_str, from_wei_str
12
+
13
+
14
+ class Config(BaseModel):
15
+ addresses: list[str]
16
+ tokens: list[str] = Field(default_factory=list)
17
+ nodes: list[str]
18
+ round_ndigits: int = 5
19
+
20
+ @field_validator("nodes", mode="before")
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
+ def addresses_validator(cls, v: str | list[str] | None) -> list[str]:
26
+ return validators.addresses_validator(v)
27
+
28
+
29
+ @dataclass
30
+ class Token:
31
+ address: str
32
+ decimals: int
33
+ symbol: str
34
+
35
+
36
+ def run(config_path: str, print_config: bool, wei: bool, show_nonce: bool) -> None:
37
+ config = cli_utils.read_config(Config, config_path)
38
+ if print_config:
39
+ console = Console()
40
+ console.print_json(json_dumps(config.model_dump()))
41
+ exit(0)
42
+
43
+ tokens = _get_tokens_info(config)
44
+
45
+ table = Table(title="balances")
46
+ table.add_column("address")
47
+ if show_nonce:
48
+ table.add_column("nonce")
49
+ table.add_column("wei" if wei else "eth")
50
+ for t in tokens:
51
+ table.add_column(t.symbol)
52
+
53
+ base_sum = 0
54
+ token_sum: dict[str, int] = {t.address: 0 for t in tokens}
55
+ with Live(table, refresh_per_second=0.5):
56
+ for address in config.addresses:
57
+ row = [address]
58
+ if show_nonce:
59
+ row.append(str(rpc.eth_get_transaction_count(config.nodes, address, attempts=5).ok_or_err()))
60
+
61
+ base_balance_res = rpc.eth_get_balance(config.nodes, address, attempts=5)
62
+ if isinstance(base_balance_res, Ok):
63
+ base_sum += base_balance_res.ok
64
+ if wei:
65
+ row.append(str(base_balance_res.ok))
66
+ else:
67
+ row.append(
68
+ from_wei_str(base_balance_res.ok, "eth", round_ndigits=config.round_ndigits, print_unit_name=False),
69
+ )
70
+ else:
71
+ row.append(base_balance_res.err)
72
+
73
+ for t in tokens:
74
+ token_balance_res = erc20.get_balance(config.nodes, t.address, address, attempts=5)
75
+ if isinstance(token_balance_res, Ok):
76
+ token_sum[t.address] += token_balance_res.ok
77
+ if wei:
78
+ row.append(str(token_balance_res.ok))
79
+ else:
80
+ row.append(
81
+ from_token_wei_str(
82
+ token_balance_res.ok,
83
+ decimals=t.decimals,
84
+ round_ndigits=config.round_ndigits,
85
+ ),
86
+ )
87
+ else:
88
+ row.append(token_balance_res.err)
89
+
90
+ table.add_row(*row)
91
+
92
+ sum_row = ["sum"]
93
+ if show_nonce:
94
+ sum_row.append("")
95
+ if wei:
96
+ sum_row.append(str(base_sum))
97
+ for t in tokens:
98
+ sum_row.append(str(token_sum[t.address]))
99
+ else:
100
+ sum_row.append(from_wei_str(base_sum, "eth", round_ndigits=config.round_ndigits, print_unit_name=False))
101
+ for t in tokens:
102
+ sum_row.append(from_token_wei_str(token_sum[t.address], t.decimals, round_ndigits=config.round_ndigits))
103
+ table.add_row(*sum_row)
104
+
105
+
106
+ def _get_tokens_info(config: Config) -> list[Token]:
107
+ result: list[Token] = []
108
+ for address in config.tokens:
109
+ decimals_res = erc20.get_decimals(config.nodes, address, attempts=5)
110
+ if isinstance(decimals_res, Err):
111
+ fatal(f"can't get token {address} decimals: {decimals_res.err}")
112
+ decimal = decimals_res.ok
113
+
114
+ symbols_res = erc20.get_symbol(config.nodes, address, attempts=5)
115
+ if isinstance(symbols_res, Err):
116
+ fatal(f"can't get token {address} symbol: {symbols_res.err}")
117
+ symbol = symbols_res.ok
118
+
119
+ result.append(Token(address=address, decimals=decimal, symbol=symbol))
120
+
121
+ return result
@@ -0,0 +1,44 @@
1
+ import json
2
+ from logging import fatal
3
+
4
+ from mm_std import Err, print_json, 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(cli_utils.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 = cli_utils.read_config(Config, config_path)
21
+ if print_config:
22
+ print_json(config.model_dump())
23
+ exit(0)
24
+
25
+ input_data = abi.encode_function_input_by_signature(
26
+ config.function_signature,
27
+ json.loads(config.function_args.replace("'", '"')),
28
+ )
29
+ res = rpc.eth_call(config.node, config.contract_address, input_data)
30
+ if isinstance(res, Err):
31
+ return fatal(f"error: {res.err}")
32
+
33
+ result = res.ok
34
+ if config.outputs_types is not None:
35
+ decode_res = abi.decode_data(_get_types(config.outputs_types), result)
36
+ if len(decode_res) == 1:
37
+ result = decode_res[0]
38
+ else:
39
+ result = str(decode_res)
40
+ print_plain(result)
41
+
42
+
43
+ def _get_types(data: str) -> list[str]:
44
+ 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())
@@ -0,0 +1,41 @@
1
+ import yaml
2
+ from mm_std import print_json
3
+ from pydantic import StrictStr
4
+
5
+ from mm_eth import account, deploy
6
+ from mm_eth.cli import cli_helpers, cli_utils
7
+ from mm_eth.cli.cli_utils import BaseConfig
8
+
9
+
10
+ class Config(BaseConfig):
11
+ private_key: StrictStr
12
+ nonce: int | None = None
13
+ gas: StrictStr
14
+ max_fee_per_gas: str
15
+ max_priority_fee_per_gas: str
16
+ value: str | None = None
17
+ contract_bin: StrictStr
18
+ constructor_types: StrictStr = "[]"
19
+ constructor_values: StrictStr = "[]"
20
+ chain_id: int
21
+ node: str
22
+
23
+
24
+ def run(config_path: str, *, print_config: bool) -> None:
25
+ config = cli_utils.read_config(Config, config_path)
26
+ if print_config:
27
+ print_json(config.model_dump(exclude={"private_key"}))
28
+ exit(0)
29
+
30
+ constructor_types = yaml.full_load(config.constructor_types)
31
+ constructor_values = yaml.full_load(config.constructor_values)
32
+
33
+ sender_address = account.private_to_address(config.private_key)
34
+ if sender_address is None:
35
+ cli_utils.fatal("private address is wrong")
36
+
37
+ nonce = cli_helpers.get_nonce(config.node, sender_address)
38
+ if nonce is None:
39
+ cli_utils.fatal("can't get nonce")
40
+
41
+ deploy.get_deploy_contract_data(config.contract_bin, constructor_types, constructor_values)
@@ -0,0 +1,10 @@
1
+ import json
2
+
3
+ from mm_std import print_plain
4
+
5
+ from mm_eth import abi
6
+
7
+
8
+ def run(function_signature: str, args_str: str) -> None:
9
+ args_str = args_str.replace("'", '"')
10
+ print_plain(abi.encode_function_input_by_signature(function_signature, json.loads(args_str)))
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ from mm_std import print_json
5
+
6
+ from mm_eth.account import generate_accounts, generate_mnemonic
7
+
8
+
9
+ def run(mnemonic: str, passphrase: str, limit: int, print_path: bool, path_prefix: str, save_file: str) -> None: # nosec
10
+ result: dict[str, Any] = {}
11
+ if not mnemonic:
12
+ mnemonic = generate_mnemonic()
13
+ result["mnemonic"] = mnemonic
14
+ if passphrase:
15
+ result["passphrase"] = passphrase
16
+ result["accounts"] = []
17
+ for acc in generate_accounts(mnemonic=mnemonic, passphrase=passphrase, limit=limit, path_prefix=path_prefix):
18
+ new_account = {"address": acc.address, "private": acc.private_key}
19
+ if print_path:
20
+ new_account["path"] = acc.path
21
+ result["accounts"].append(new_account)
22
+ print_json(result)
23
+
24
+ if save_file:
25
+ with open(Path(save_file).expanduser(), "w") as f:
26
+ for account in result["accounts"]:
27
+ f.write(f"{account['address']}\t{account['private']}\n")