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,47 @@
1
+ from mm_std import Ok, PrintFormat, print_json, print_plain
2
+ from rich.live import Live
3
+ from rich.table import Table
4
+
5
+ from mm_eth import rpc
6
+ from mm_eth.utils import from_wei_str, name_network
7
+
8
+
9
+ def run(urls: list[str], print_format: PrintFormat, proxy: str | None) -> None:
10
+ json_result: dict[str, object] = {}
11
+ table = Table(title="nodes")
12
+ if print_format == PrintFormat.TABLE:
13
+ table.add_column("url")
14
+ table.add_column("chain_id")
15
+ table.add_column("chain_name")
16
+ table.add_column("block_number")
17
+ table.add_column("base_fee")
18
+
19
+ with Live(table, refresh_per_second=0.5):
20
+ for url in urls:
21
+ chain_id_res = rpc.eth_chain_id(url, timeout=10, proxies=proxy)
22
+ chain_id = chain_id_res.ok_or_err()
23
+ chain_name = ""
24
+ if isinstance(chain_id_res, Ok):
25
+ chain_name = name_network(chain_id_res.ok)
26
+ block_number = rpc.eth_block_number(url, timeout=10, proxies=proxy).ok_or_err()
27
+ base_fee = rpc.get_base_fee_per_gas(url, timeout=10, proxies=proxy).map_or_else(
28
+ lambda err: err,
29
+ lambda ok: from_wei_str(ok, "gwei"),
30
+ )
31
+
32
+ json_result[url] = {
33
+ "chain_id": chain_id,
34
+ "chain_name": chain_name,
35
+ "block_number": block_number,
36
+ "base_fee": base_fee,
37
+ }
38
+ if print_format == PrintFormat.TABLE:
39
+ table.add_row(url, str(chain_id), chain_name, str(block_number), base_fee)
40
+ print_plain(f"url: {url}", print_format)
41
+ print_plain(f"chain_id: {chain_id}", print_format)
42
+ print_plain(f"chain_name: {chain_name}", print_format)
43
+ print_plain(f"block_number: {block_number}", print_format)
44
+ print_plain(f"base_fee: {base_fee}", print_format)
45
+ print_plain("", print_format)
46
+
47
+ print_json(json_result, print_format)
@@ -0,0 +1,10 @@
1
+ from mm_std import fatal, print_plain
2
+
3
+ from mm_eth import account
4
+
5
+
6
+ def run(private_key: str) -> None:
7
+ try:
8
+ print_plain(account.private_to_address(private_key))
9
+ except Exception as e:
10
+ fatal(f"wrong private key: {e}")
@@ -0,0 +1,81 @@
1
+ import json
2
+ from typing import cast
3
+
4
+ from mm_std import fatal, hr, print_console, str_starts_with_any
5
+ from rich import print_json
6
+
7
+ from mm_eth.cli.cli_utils import public_rpc_url
8
+
9
+
10
+ def run(rpc_url: str, method: str, params: str, hex2dec: bool) -> None:
11
+ rpc_url = public_rpc_url(rpc_url)
12
+ if not method:
13
+ return list_all_methods()
14
+ if not str_starts_with_any(rpc_url, ["http://", "https://"]):
15
+ fatal(f"invalid rpc_url: {rpc_url}")
16
+ params = params.replace("'", '"')
17
+ data = {"jsonrpc": "2.0", "method": method, "params": parse_method_params(method, params), "id": 1}
18
+ res = hr(rpc_url, method="POST", params=data, json_params=True)
19
+ if res.json:
20
+ print_json(data=res.json)
21
+ result_value: str = res.json.get("result", "")
22
+ if hex2dec and result_value.startswith(("0x", "0X")):
23
+ print_console("hex2dec", int(result_value, 16))
24
+ else:
25
+ fatal(str(res))
26
+
27
+
28
+ def parse_method_params(method: str, params_str: str) -> list[object]:
29
+ if params_str.startswith("["):
30
+ params = json.loads(params_str)
31
+ else:
32
+ params = params_str.split()
33
+ if method in ["eth_getBalance", "eth_getTransactionCount", "eth_getCode"] and len(params) == 1:
34
+ params.append("latest")
35
+ return cast(list[object], params)
36
+
37
+
38
+ def list_all_methods() -> None:
39
+ all_methods = """
40
+ web3_clientVersion
41
+ web3_sha3
42
+ net_version
43
+ net_listening
44
+ net_peerCount
45
+ eth_protocolVersion
46
+ eth_syncing
47
+ eth_chainId
48
+ eth_gasPrice
49
+ eth_accounts
50
+ eth_blockNumber
51
+ eth_getBalance
52
+ eth_getStorageAt
53
+ eth_getTransactionCount
54
+ eth_getBlockTransactionCountByHash
55
+ eth_getBlockTransactionCountByNumber
56
+ eth_getUncleCountByBlockHash
57
+ eth_getUncleCountByBlockNumber
58
+ eth_getCode
59
+ eth_sign
60
+ eth_signTransaction
61
+ eth_sendTransaction
62
+ eth_sendRawTransaction
63
+ eth_call
64
+ eth_estimateGas
65
+ eth_getBlockByHash
66
+ eth_getBlockByNumber
67
+ eth_getTransactionByHash
68
+ eth_getTransactionByBlockHashAndIndex
69
+ eth_getTransactionByBlockNumberAndIndex
70
+ eth_getTransactionReceipt
71
+ eth_getUncleByBlockHashAndIndex
72
+ eth_getUncleByBlockNumberAndIndex
73
+ eth_newFilter
74
+ eth_newBlockFilter
75
+ eth_newPendingTransactionFilter
76
+ eth_uninstallFilter
77
+ eth_getFilterChanges
78
+ eth_getFilterLogs
79
+ eth_getLogs
80
+ """.strip()
81
+ print_console(all_methods)
@@ -0,0 +1,247 @@
1
+ import json
2
+ import time
3
+ from pathlib import Path
4
+ from typing import Self
5
+
6
+ from loguru import logger
7
+ from mm_std import Err, Ok, print_json, str_to_list, utc_now
8
+ from pydantic import Field, StrictStr, field_validator, model_validator
9
+
10
+ from mm_eth import abi, rpc
11
+ from mm_eth.account import create_private_keys_dict, private_to_address
12
+ from mm_eth.cli import calcs, cli_helpers, cli_utils, validators
13
+ from mm_eth.cli.cli_helpers import get_version
14
+ from mm_eth.tx import sign_tx
15
+ from mm_eth.utils import from_wei_str
16
+
17
+
18
+ class Config(cli_utils.BaseConfig):
19
+ contract_address: str
20
+ function_signature: str
21
+ function_args: StrictStr = "[]"
22
+ nodes: list[StrictStr]
23
+ chain_id: int
24
+ private_keys: dict[str, str] = Field(default_factory=dict)
25
+ private_keys_file: str | None = None
26
+ max_fee_per_gas: str
27
+ max_fee_per_gas_limit: str | None = None
28
+ max_priority_fee_per_gas: str
29
+ value: str | None = None
30
+ gas: str
31
+ from_addresses: list[str]
32
+ delay: str | None = None # in seconds
33
+ round_ndigits: int = 5
34
+ log_debug: str | None = None
35
+ log_info: str | None = None
36
+
37
+ @field_validator("log_debug", "log_info", mode="before")
38
+ def log_validator(cls, v: str | None) -> str | None:
39
+ return validators.log_validator(v)
40
+
41
+ @field_validator("nodes", "from_addresses", mode="before")
42
+ def list_validator(cls, v: str | list[str] | None) -> list[str]:
43
+ return validators.nodes_validator(v)
44
+
45
+ @field_validator("from_addresses", mode="before")
46
+ def from_addresses_validator(cls, v: str | list[str] | None) -> list[str]:
47
+ return str_to_list(v, remove_comments=True, lower=True)
48
+
49
+ @field_validator("private_keys", mode="before")
50
+ def private_keys_validator(cls, v: str | list[str] | None) -> dict[str, str]:
51
+ if v is None:
52
+ return {}
53
+ if isinstance(v, str):
54
+ return create_private_keys_dict(str_to_list(v, unique=True, remove_comments=True))
55
+ return create_private_keys_dict(v)
56
+
57
+ # noinspection DuplicatedCode
58
+ @model_validator(mode="after")
59
+ def final_validator(self) -> Self:
60
+ # load private keys from file
61
+ if self.private_keys_file:
62
+ file = Path(self.private_keys_file).expanduser()
63
+ if not file.is_file():
64
+ raise ValueError("can't read private_keys_file")
65
+ for line in file.read_text().strip().split("\n"):
66
+ line = line.strip()
67
+ address = private_to_address(line)
68
+ if address is None:
69
+ raise ValueError("there is not a private key in private_keys_file")
70
+ self.private_keys[address.lower()] = line
71
+
72
+ # check that from_addresses is not empty
73
+ if not self.from_addresses:
74
+ raise ValueError("from_addresses is empty")
75
+
76
+ # max_fee_per_gas
77
+ if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas, "base"):
78
+ raise ValueError(f"wrong max_fee_per_gas: {self.max_fee_per_gas}")
79
+
80
+ # max_fee_per_gas_limit
81
+ if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas_limit, "base"):
82
+ raise ValueError(f"wrong max_fee_per_gas_limit: {self.max_fee_per_gas_limit}")
83
+
84
+ # max_priority_fee_per_gas
85
+ if not validators.is_valid_calc_var_wei_value(self.max_priority_fee_per_gas):
86
+ raise ValueError(f"wrong max_priority_fee_per_gas: {self.max_priority_fee_per_gas}")
87
+
88
+ # value
89
+ if self.value is not None and not validators.is_valid_calc_var_wei_value(self.value, "balance"):
90
+ raise ValueError(f"wrong value: {self.value}")
91
+
92
+ # gas
93
+ if not validators.is_valid_calc_var_wei_value(self.gas, "estimate"):
94
+ raise ValueError(f"wrong gas: {self.gas}")
95
+
96
+ # delay
97
+ if not validators.is_valid_calc_decimal_value(self.delay):
98
+ raise ValueError(f"wrong delay: {self.delay}")
99
+
100
+ # function_args
101
+ if not validators.is_valid_calc_function_args(self.function_args):
102
+ raise ValueError(f"wrong function_args: {self.function_args}")
103
+
104
+ return self
105
+
106
+
107
+ # noinspection DuplicatedCode
108
+ def run(
109
+ config_path: str,
110
+ *,
111
+ print_balances: bool,
112
+ print_config: bool,
113
+ debug: bool,
114
+ no_receipt: bool,
115
+ emulate: bool,
116
+ ) -> None:
117
+ config = cli_utils.read_config(Config, config_path)
118
+ if print_config:
119
+ print_json(config.model_dump(exclude={"private_keys"}))
120
+ exit(0)
121
+
122
+ cli_utils.init_logger(debug, config.log_debug, config.log_info)
123
+ cli_utils.check_nodes_for_chain_id(config.nodes, config.chain_id)
124
+
125
+ if print_balances:
126
+ cli_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
127
+ exit(0)
128
+
129
+ _run_transfers(config, no_receipt=no_receipt, emulate=emulate)
130
+
131
+
132
+ # noinspection DuplicatedCode
133
+ def _run_transfers(config: Config, *, no_receipt: bool, emulate: bool) -> None:
134
+ logger.info(f"started at {utc_now()} UTC")
135
+ logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version':get_version()}}")
136
+ cli_utils.check_private_keys(config.from_addresses, config.private_keys)
137
+ for i, from_address in enumerate(config.from_addresses):
138
+ _transfer(from_address=from_address, config=config, no_receipt=no_receipt, emulate=emulate)
139
+ if not emulate and config.delay is not None and i < len(config.from_addresses) - 1:
140
+ delay_value = calcs.calc_decimal_value(config.delay)
141
+ logger.debug(f"delay {delay_value} seconds")
142
+ time.sleep(float(delay_value))
143
+ logger.info(f"finished at {utc_now()} UTC")
144
+
145
+
146
+ # noinspection DuplicatedCode
147
+ def _transfer(*, from_address: str, config: Config, no_receipt: bool, emulate: bool) -> None:
148
+ log_prefix = f"{from_address}"
149
+ # get nonce
150
+ nonce = cli_helpers.get_nonce(config.nodes, from_address, log_prefix)
151
+ if nonce is None:
152
+ return
153
+
154
+ # get max_fee_per_gas
155
+ max_fee_per_gas = cli_helpers.calc_max_fee_per_gas(config.nodes, config.max_fee_per_gas, log_prefix)
156
+ if max_fee_per_gas is None:
157
+ return
158
+
159
+ # check max_fee_per_gas_limit
160
+ if cli_helpers.is_max_fee_per_gas_limit_exceeded(max_fee_per_gas, config.max_fee_per_gas_limit, log_prefix):
161
+ return
162
+
163
+ max_priority_fee_per_gas = calcs.calc_var_wei_value(config.max_priority_fee_per_gas)
164
+
165
+ # data
166
+ function_args = calcs.calc_function_args(config.function_args).replace("'", '"')
167
+ data = abi.encode_function_input_by_signature(config.function_signature, json.loads(function_args))
168
+
169
+ # get gas
170
+ gas = cli_helpers.calc_gas(
171
+ nodes=config.nodes,
172
+ gas=config.gas,
173
+ from_address=from_address,
174
+ to_address=config.contract_address,
175
+ value=None,
176
+ data=data,
177
+ log_prefix=log_prefix,
178
+ )
179
+ if gas is None:
180
+ return
181
+
182
+ # get value
183
+ value = None
184
+ if config.value is not None:
185
+ value = cli_helpers.calc_eth_value(
186
+ nodes=config.nodes,
187
+ value_str=config.value,
188
+ address=from_address,
189
+ gas=gas,
190
+ max_fee_per_gas=max_fee_per_gas,
191
+ log_prefix=log_prefix,
192
+ )
193
+ if value is None:
194
+ return
195
+
196
+ tx_params = {
197
+ "nonce": nonce,
198
+ "max_fee_per_gas": max_fee_per_gas,
199
+ "max_priority_fee_per_gas": max_priority_fee_per_gas,
200
+ "gas": gas,
201
+ "value": value,
202
+ "to": config.contract_address,
203
+ "chain_id": config.chain_id,
204
+ }
205
+
206
+ # emulate?
207
+ if emulate:
208
+ msg = f"{log_prefix}: emulate,"
209
+ if value is not None:
210
+ msg += f" value={from_wei_str(value, 'eth', config.round_ndigits)},"
211
+ msg += f" max_fee_per_gas={from_wei_str(max_fee_per_gas, 'gwei', config.round_ndigits)},"
212
+ msg += f" max_priority_fee_per_gas={from_wei_str(max_priority_fee_per_gas, 'gwei', config.round_ndigits)},"
213
+ msg += f" gas={gas}"
214
+ logger.info(msg)
215
+ return
216
+
217
+ logger.debug(f"{log_prefix}: tx_params={tx_params}")
218
+ signed_tx = sign_tx(
219
+ nonce=nonce,
220
+ max_fee_per_gas=max_fee_per_gas,
221
+ max_priority_fee_per_gas=max_priority_fee_per_gas,
222
+ gas=gas,
223
+ private_key=config.private_keys[from_address],
224
+ chain_id=config.chain_id,
225
+ value=value,
226
+ data=data,
227
+ to=config.contract_address,
228
+ )
229
+ res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
230
+ if isinstance(res, Err):
231
+ logger.info(f"{log_prefix}: send_error: {res.err}")
232
+ return
233
+ tx_hash = res.ok
234
+
235
+ if no_receipt:
236
+ msg = f"{log_prefix}: tx_hash={tx_hash}"
237
+ logger.info(msg)
238
+ else:
239
+ logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
240
+ while True:
241
+ receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
242
+ if isinstance(receipt_res, Ok):
243
+ status = "OK" if receipt_res.ok == 1 else "FAIL"
244
+ msg = f"{log_prefix}: tx_hash={tx_hash}, status={status}"
245
+ logger.info(msg)
246
+ break
247
+ time.sleep(1)
@@ -0,0 +1,25 @@
1
+ import json
2
+
3
+ from mm_std import Err, PrintFormat, print_json, print_plain
4
+ from mm_std.fs import get_filename_without_extension
5
+
6
+ from mm_eth.cli import cli_helpers
7
+ from mm_eth.solc import solc
8
+
9
+
10
+ def run(contract_path: str, tmp_dir: str, print_format: PrintFormat) -> None:
11
+ contract_name = get_filename_without_extension(contract_path)
12
+ res = solc(contract_name, contract_path, tmp_dir)
13
+ if isinstance(res, Err):
14
+ cli_helpers.fatal(res.err)
15
+
16
+ bin_ = res.ok.bin
17
+ abi = res.ok.abi
18
+
19
+ if print_format == PrintFormat.JSON:
20
+ print_json({"bin": bin_, "abi": json.loads(abi)})
21
+ else:
22
+ print_plain("bin:")
23
+ print_plain(bin_)
24
+ print_plain("abi:")
25
+ print_plain(abi)
@@ -0,0 +1,29 @@
1
+ from mm_std import Err, Ok, print_plain
2
+
3
+ from mm_eth import erc20, rpc
4
+ from mm_eth.cli.cli_utils import public_rpc_url
5
+
6
+
7
+ def run(rpc_url: str, token_address: str) -> None:
8
+ rpc_url = public_rpc_url(rpc_url)
9
+ name = erc20.get_name(rpc_url, token_address).ok_or_err()
10
+ symbol = erc20.get_symbol(rpc_url, token_address).ok_or_err()
11
+ decimals = erc20.get_decimals(rpc_url, token_address).ok_or_err()
12
+ transfer_count = _calc_transfer_events(rpc_url, 100, token_address)
13
+
14
+ print_plain(f"name: {name}")
15
+ print_plain(f"symbol: {symbol}")
16
+ print_plain(f"decimals: {decimals}")
17
+ print_plain(f"transfer_count: {transfer_count}")
18
+
19
+
20
+ def _calc_transfer_events(rpc_url: str, last_block_limit: int, token_address: str) -> int | str:
21
+ current_block_res = rpc.eth_block_number(rpc_url)
22
+ if isinstance(current_block_res, Err):
23
+ return current_block_res.err
24
+ current_block = current_block_res.ok
25
+
26
+ res = erc20.get_transfer_event_logs(rpc_url, token_address, current_block - last_block_limit, current_block)
27
+ if isinstance(res, Ok):
28
+ return len(res.ok)
29
+ return res.err