mm-eth 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

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