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.
- mm_eth/__init__.py +0 -0
- mm_eth/abi/zksync.json +2092 -0
- mm_eth/abi.py +130 -0
- mm_eth/account.py +70 -0
- mm_eth/anvil.py +56 -0
- mm_eth/cli/__init__.py +0 -0
- mm_eth/cli/calcs.py +88 -0
- mm_eth/cli/cli.py +233 -0
- mm_eth/cli/cli_helpers.py +195 -0
- mm_eth/cli/cli_utils.py +150 -0
- mm_eth/cli/cmd/__init__.py +0 -0
- mm_eth/cli/cmd/balance_cmd.py +59 -0
- mm_eth/cli/cmd/balances_cmd.py +121 -0
- mm_eth/cli/cmd/call_contract_cmd.py +44 -0
- mm_eth/cli/cmd/config_example_cmd.py +9 -0
- mm_eth/cli/cmd/deploy_cmd.py +41 -0
- mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
- mm_eth/cli/cmd/mnemonic_cmd.py +27 -0
- mm_eth/cli/cmd/node_cmd.py +47 -0
- mm_eth/cli/cmd/private_key_cmd.py +10 -0
- mm_eth/cli/cmd/rpc_cmd.py +81 -0
- mm_eth/cli/cmd/send_contract_cmd.py +247 -0
- mm_eth/cli/cmd/solc_cmd.py +25 -0
- mm_eth/cli/cmd/token_cmd.py +29 -0
- mm_eth/cli/cmd/transfer_erc20_cmd.py +275 -0
- mm_eth/cli/cmd/transfer_eth_cmd.py +252 -0
- mm_eth/cli/cmd/vault_cmd.py +16 -0
- mm_eth/cli/config_examples/balances.yml +15 -0
- mm_eth/cli/config_examples/call_contract.yml +5 -0
- mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
- mm_eth/cli/config_examples/transfer_eth.yml +24 -0
- mm_eth/cli/validators.py +84 -0
- mm_eth/deploy.py +20 -0
- mm_eth/ens.py +16 -0
- mm_eth/erc20.py +240 -0
- mm_eth/ethernodes.py +34 -0
- mm_eth/py.typed +0 -0
- mm_eth/rpc.py +478 -0
- mm_eth/services/__init__.py +0 -0
- mm_eth/solc.py +34 -0
- mm_eth/tx.py +164 -0
- mm_eth/types.py +5 -0
- mm_eth/utils.py +245 -0
- mm_eth/vault.py +38 -0
- mm_eth/zksync.py +203 -0
- mm_eth-0.1.0.dist-info/METADATA +24 -0
- mm_eth-0.1.0.dist-info/RECORD +50 -0
- mm_eth-0.1.0.dist-info/WHEEL +5 -0
- mm_eth-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
mm_eth/cli/cli_utils.py
ADDED
|
@@ -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")
|