mm-eth 0.7.3__py3-none-any.whl → 0.8.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 +1 -0
- mm_eth/abi.py +17 -2
- mm_eth/account.py +6 -43
- mm_eth/anvil.py +11 -0
- mm_eth/cli/__init__.py +1 -0
- mm_eth/cli/calcs.py +4 -0
- mm_eth/cli/cli.py +14 -3
- mm_eth/cli/cli_utils.py +21 -4
- mm_eth/cli/cmd/__init__.py +1 -0
- mm_eth/cli/cmd/balance_cmd.py +10 -7
- mm_eth/cli/cmd/balances_cmd.py +13 -4
- mm_eth/cli/cmd/deploy_cmd.py +9 -2
- mm_eth/cli/cmd/node_cmd.py +14 -2
- mm_eth/cli/cmd/solc_cmd.py +11 -7
- mm_eth/cli/cmd/transfer_cmd.py +25 -1
- mm_eth/cli/cmd/wallet/__init__.py +1 -0
- mm_eth/cli/cmd/wallet/mnemonic_cmd.py +5 -2
- mm_eth/cli/cmd/wallet/private_key_cmd.py +7 -3
- mm_eth/cli/rpc_helpers.py +6 -0
- mm_eth/cli/validators.py +12 -0
- mm_eth/converters.py +4 -0
- mm_eth/deploy.py +4 -0
- mm_eth/erc20.py +4 -0
- mm_eth/retry.py +17 -0
- mm_eth/rpc.py +32 -9
- mm_eth/solc.py +6 -1
- mm_eth/tx.py +13 -0
- mm_eth/utils.py +4 -0
- mm_eth-0.8.0.dist-info/METADATA +7 -0
- mm_eth-0.8.0.dist-info/RECORD +33 -0
- {mm_eth-0.7.3.dist-info → mm_eth-0.8.0.dist-info}/WHEEL +1 -1
- mm_eth-0.7.3.dist-info/METADATA +0 -7
- mm_eth-0.7.3.dist-info/RECORD +0 -33
- {mm_eth-0.7.3.dist-info → mm_eth-0.8.0.dist-info}/entry_points.txt +0 -0
mm_eth/cli/cmd/solc_cmd.py
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
+
"""CLI command: compile Solidity contracts."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
|
|
4
|
-
import
|
|
6
|
+
from mm_print import print_json, print_plain
|
|
5
7
|
|
|
6
8
|
from mm_eth.cli.cli import PrintFormat
|
|
9
|
+
from mm_eth.cli.cli_utils import fatal
|
|
7
10
|
from mm_eth.solc import solc
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
def run(contract_path: Path, tmp_dir: Path, print_format: PrintFormat) -> None:
|
|
14
|
+
"""Compile a Solidity file and print the ABI and bytecode."""
|
|
11
15
|
contract_name = contract_path.stem
|
|
12
16
|
res = solc(contract_name, contract_path, tmp_dir)
|
|
13
17
|
if res.is_err():
|
|
14
|
-
|
|
18
|
+
fatal(res.unwrap_err())
|
|
15
19
|
|
|
16
20
|
bin_ = res.unwrap().bin
|
|
17
21
|
abi = res.unwrap().abi
|
|
18
22
|
|
|
19
23
|
if print_format == PrintFormat.JSON:
|
|
20
|
-
|
|
24
|
+
print_json({"bin": bin_, "abi": json.loads(abi)})
|
|
21
25
|
else:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
print_plain("bin:")
|
|
27
|
+
print_plain(bin_)
|
|
28
|
+
print_plain("abi:")
|
|
29
|
+
print_plain(abi)
|
mm_eth/cli/cmd/transfer_cmd.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""CLI command: transfer ETH or ERC-20 tokens."""
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import sys
|
|
3
5
|
import time
|
|
@@ -20,6 +22,8 @@ from mm_eth.converters import from_wei
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class Config(Web3CliConfig):
|
|
25
|
+
"""Configuration for the transfer command."""
|
|
26
|
+
|
|
23
27
|
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
24
28
|
chain_id: int
|
|
25
29
|
transfers: Annotated[list[Transfer], BeforeValidator(Validators.eth_transfers())]
|
|
@@ -41,10 +45,12 @@ class Config(Web3CliConfig):
|
|
|
41
45
|
|
|
42
46
|
@property
|
|
43
47
|
def from_addresses(self) -> list[str]:
|
|
48
|
+
"""Return the list of sender addresses from all transfers."""
|
|
44
49
|
return [r.from_address for r in self.transfers]
|
|
45
50
|
|
|
46
51
|
@model_validator(mode="after")
|
|
47
52
|
def final_validator(self) -> Self:
|
|
53
|
+
"""Validate private keys coverage, set default values, and check expression types."""
|
|
48
54
|
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
49
55
|
raise ValueError("private keys are not set for all addresses")
|
|
50
56
|
|
|
@@ -69,6 +75,7 @@ class Config(Web3CliConfig):
|
|
|
69
75
|
return self
|
|
70
76
|
|
|
71
77
|
async def async_init(self) -> None:
|
|
78
|
+
"""Fetch token decimals from the network if a token address is configured."""
|
|
72
79
|
if self.token:
|
|
73
80
|
self.token_decimals = (await retry.erc20_decimals(5, self.nodes, self.proxies, token=self.token)).unwrap(
|
|
74
81
|
"can't get token decimals"
|
|
@@ -76,6 +83,8 @@ class Config(Web3CliConfig):
|
|
|
76
83
|
|
|
77
84
|
|
|
78
85
|
class TransferCmdParams(BaseConfigParams):
|
|
86
|
+
"""Parameters for the transfer command."""
|
|
87
|
+
|
|
79
88
|
print_balances: bool
|
|
80
89
|
print_transfers: bool
|
|
81
90
|
debug: bool
|
|
@@ -84,6 +93,7 @@ class TransferCmdParams(BaseConfigParams):
|
|
|
84
93
|
|
|
85
94
|
|
|
86
95
|
async def run(params: TransferCmdParams) -> None:
|
|
96
|
+
"""Read config and execute transfers, or print balances/transfers for inspection."""
|
|
87
97
|
config = await Config.read_toml_config_or_exit_async(params.config_path)
|
|
88
98
|
await config.async_init()
|
|
89
99
|
if params.print_config:
|
|
@@ -103,6 +113,7 @@ async def run(params: TransferCmdParams) -> None:
|
|
|
103
113
|
|
|
104
114
|
|
|
105
115
|
async def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
116
|
+
"""Execute all configured transfers sequentially with optional delays."""
|
|
106
117
|
init_loguru(cmd_params.debug, config.log_debug, config.log_info)
|
|
107
118
|
logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
|
|
108
119
|
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
@@ -117,6 +128,7 @@ async def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
117
128
|
|
|
118
129
|
|
|
119
130
|
async def _get_nonce(t: Transfer, config: Config) -> int | None:
|
|
131
|
+
"""Fetch the nonce for the transfer's sender address."""
|
|
120
132
|
res = await retry.eth_get_transaction_count(5, config.nodes, config.proxies, address=t.from_address)
|
|
121
133
|
if res.is_err():
|
|
122
134
|
logger.error(f"{t.log_prefix}: nonce error: {res.unwrap_err()}")
|
|
@@ -126,6 +138,7 @@ async def _get_nonce(t: Transfer, config: Config) -> int | None:
|
|
|
126
138
|
|
|
127
139
|
|
|
128
140
|
async def _calc_max_fee(t: Transfer, config: Config) -> int | None:
|
|
141
|
+
"""Calculate the max fee, fetching base_fee from the network if needed."""
|
|
129
142
|
if "base_fee" in config.max_fee.lower():
|
|
130
143
|
base_fee_res = await retry.get_base_fee_per_gas(5, config.nodes, config.proxies)
|
|
131
144
|
if base_fee_res.is_err():
|
|
@@ -137,6 +150,7 @@ async def _calc_max_fee(t: Transfer, config: Config) -> int | None:
|
|
|
137
150
|
|
|
138
151
|
|
|
139
152
|
def check_max_fee_limit(t: Transfer, config: Config, max_fee: int) -> bool:
|
|
153
|
+
"""Return False if max_fee exceeds the configured limit."""
|
|
140
154
|
if config.max_fee_limit:
|
|
141
155
|
max_fee_limit = calcs.calc_eth_expression(config.max_fee_limit)
|
|
142
156
|
if max_fee > max_fee_limit:
|
|
@@ -148,6 +162,7 @@ def check_max_fee_limit(t: Transfer, config: Config, max_fee: int) -> bool:
|
|
|
148
162
|
|
|
149
163
|
|
|
150
164
|
async def _calc_gas(t: Transfer, config: Config) -> int | None:
|
|
165
|
+
"""Calculate the gas limit, optionally using on-chain estimation."""
|
|
151
166
|
variables: dict[str, int] | None = None
|
|
152
167
|
if "estimate" in config.gas.lower():
|
|
153
168
|
if config.token:
|
|
@@ -172,6 +187,7 @@ async def _calc_gas(t: Transfer, config: Config) -> int | None:
|
|
|
172
187
|
|
|
173
188
|
|
|
174
189
|
async def _calc_eth_value(t: Transfer, max_fee: int, gas: int, config: Config) -> int | None:
|
|
190
|
+
"""Calculate the ETH transfer value, subtracting gas costs if using balance."""
|
|
175
191
|
value_expression = t.value.lower()
|
|
176
192
|
variables: dict[str, int] | None = None
|
|
177
193
|
if "balance" in value_expression:
|
|
@@ -189,6 +205,7 @@ async def _calc_eth_value(t: Transfer, max_fee: int, gas: int, config: Config) -
|
|
|
189
205
|
|
|
190
206
|
|
|
191
207
|
async def _calc_token_value(t: Transfer, config: Config) -> int | None:
|
|
208
|
+
"""Calculate the ERC-20 token transfer value from a value expression."""
|
|
192
209
|
value_expression = t.value.lower()
|
|
193
210
|
variables: dict[str, int] | None = None
|
|
194
211
|
if "balance" in value_expression:
|
|
@@ -202,13 +219,14 @@ async def _calc_token_value(t: Transfer, config: Config) -> int | None:
|
|
|
202
219
|
|
|
203
220
|
|
|
204
221
|
async def _calc_value(t: Transfer, max_fee: int, gas: int, config: Config) -> int | None:
|
|
222
|
+
"""Calculate the transfer value, dispatching to ETH or token calculation."""
|
|
205
223
|
if config.token:
|
|
206
224
|
return await _calc_token_value(t, config)
|
|
207
225
|
return await _calc_eth_value(t, max_fee, gas, config)
|
|
208
226
|
|
|
209
227
|
|
|
210
228
|
def _check_value_min_limit(t: Transfer, value: int, config: Config) -> bool:
|
|
211
|
-
"""
|
|
229
|
+
"""Return False if the transfer should be skipped."""
|
|
212
230
|
if config.value_min_limit:
|
|
213
231
|
if config.token:
|
|
214
232
|
value_min_limit = calcs.calc_token_expression(config.value_min_limit, config.token_decimals)
|
|
@@ -220,6 +238,7 @@ def _check_value_min_limit(t: Transfer, value: int, config: Config) -> bool:
|
|
|
220
238
|
|
|
221
239
|
|
|
222
240
|
async def _transfer(t: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
241
|
+
"""Execute a single transfer: calculate params, sign, send, and wait for receipt."""
|
|
223
242
|
nonce = await _get_nonce(t, config)
|
|
224
243
|
if nonce is None:
|
|
225
244
|
return
|
|
@@ -268,6 +287,7 @@ async def _transfer(t: Transfer, config: Config, cmd_params: TransferCmdParams)
|
|
|
268
287
|
|
|
269
288
|
|
|
270
289
|
async def wait_tx_status(t: Transfer, tx_hash: str, config: Config) -> Literal["OK", "FAIL", "TIMEOUT"]:
|
|
290
|
+
"""Poll for a transaction receipt until success, failure, or timeout."""
|
|
271
291
|
logger.debug(f"{t.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
|
|
272
292
|
started_at = time.perf_counter()
|
|
273
293
|
count = 0
|
|
@@ -286,6 +306,7 @@ async def wait_tx_status(t: Transfer, tx_hash: str, config: Config) -> Literal["
|
|
|
286
306
|
async def _send_tx(
|
|
287
307
|
*, transfer: Transfer, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config
|
|
288
308
|
) -> str | None:
|
|
309
|
+
"""Sign and broadcast a transaction, returning the tx hash or None on failure."""
|
|
289
310
|
debug_tx_params = {
|
|
290
311
|
"nonce": nonce,
|
|
291
312
|
"max_fee": max_fee,
|
|
@@ -330,6 +351,7 @@ async def _send_tx(
|
|
|
330
351
|
|
|
331
352
|
|
|
332
353
|
def _print_transfers(config: Config) -> None:
|
|
354
|
+
"""Print the configured transfers as a table."""
|
|
333
355
|
table = Table("n", "from_address", "to_address", "value", title="transfers")
|
|
334
356
|
for count, transfer in enumerate(config.transfers, start=1):
|
|
335
357
|
table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
|
|
@@ -338,6 +360,7 @@ def _print_transfers(config: Config) -> None:
|
|
|
338
360
|
|
|
339
361
|
|
|
340
362
|
async def _print_balances(config: Config) -> None:
|
|
363
|
+
"""Fetch and print ETH (and token) balances for all transfer routes."""
|
|
341
364
|
if config.token:
|
|
342
365
|
headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
|
|
343
366
|
else:
|
|
@@ -414,6 +437,7 @@ async def _print_balances(config: Config) -> None:
|
|
|
414
437
|
|
|
415
438
|
|
|
416
439
|
def _value_with_suffix(value: int, config: Config) -> str:
|
|
440
|
+
"""Format a value with its unit suffix (eth or t) for display."""
|
|
417
441
|
if config.token:
|
|
418
442
|
return f"{from_wei(value, 't', config.round_ndigits, decimals=config.token_decimals)}t"
|
|
419
443
|
return f"{from_wei(value, 'eth', config.round_ndigits)}eth"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Wallet-related CLI commands."""
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
"""CLI command: generate or derive accounts from a mnemonic."""
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
4
|
-
import
|
|
6
|
+
from mm_print import print_json
|
|
5
7
|
|
|
6
8
|
from mm_eth.account import derive_accounts, generate_mnemonic
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
def run(mnemonic: str, passphrase: str, words: int, derivation_path: str, limit: int, print_path: bool, save_file: str) -> None: # nosec
|
|
12
|
+
"""Generate a mnemonic (if not provided) and derive accounts from it."""
|
|
10
13
|
result: dict[str, Any] = {}
|
|
11
14
|
if not mnemonic:
|
|
12
15
|
mnemonic = generate_mnemonic(num_words=words)
|
|
@@ -19,7 +22,7 @@ def run(mnemonic: str, passphrase: str, words: int, derivation_path: str, limit:
|
|
|
19
22
|
if print_path:
|
|
20
23
|
new_account["path"] = acc.path
|
|
21
24
|
result["accounts"].append(new_account)
|
|
22
|
-
|
|
25
|
+
print_json(result)
|
|
23
26
|
|
|
24
27
|
if save_file:
|
|
25
28
|
data = [acc["address"] + "\t" + acc["private"] for acc in result["accounts"]]
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
"""CLI command: derive address from a private key."""
|
|
2
|
+
|
|
3
|
+
from mm_print import print_plain
|
|
2
4
|
|
|
3
5
|
from mm_eth import account
|
|
6
|
+
from mm_eth.cli.cli_utils import fatal
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def run(private_key: str) -> None:
|
|
10
|
+
"""Print the Ethereum address for the given private key."""
|
|
7
11
|
res = account.private_to_address(private_key)
|
|
8
12
|
if res.is_ok():
|
|
9
|
-
|
|
13
|
+
print_plain(res.unwrap())
|
|
10
14
|
else:
|
|
11
|
-
|
|
15
|
+
fatal(f"invalid private key: '{private_key}'")
|
mm_eth/cli/rpc_helpers.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""RPC helper functions with logging for CLI commands."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
|
|
3
5
|
from mm_web3 import Nodes, Proxies
|
|
@@ -11,6 +13,7 @@ logger = logging.getLogger(__name__)
|
|
|
11
13
|
async def get_nonce_with_logging(
|
|
12
14
|
log_prefix: str | None, retries: int, nodes: Nodes, proxies: Proxies, *, address: str
|
|
13
15
|
) -> int | None:
|
|
16
|
+
"""Fetch the nonce for an address, logging errors and debug info."""
|
|
14
17
|
res = await retry.eth_get_transaction_count(retries, nodes, proxies, address=address)
|
|
15
18
|
prefix = log_prefix or address
|
|
16
19
|
if res.is_err():
|
|
@@ -21,6 +24,7 @@ async def get_nonce_with_logging(
|
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
async def get_base_fee_with_logging(log_prefix: str | None, retries: int, nodes: Nodes, proxies: Proxies) -> int | None:
|
|
27
|
+
"""Fetch the base fee, logging errors and debug info."""
|
|
24
28
|
prefix = get_log_prefix(log_prefix)
|
|
25
29
|
res = await retry.get_base_fee_per_gas(retries, nodes, proxies)
|
|
26
30
|
if res.is_err():
|
|
@@ -34,6 +38,7 @@ async def get_base_fee_with_logging(log_prefix: str | None, retries: int, nodes:
|
|
|
34
38
|
async def calc_max_fee_with_logging(
|
|
35
39
|
log_prefix: str | None, retries: int, nodes: Nodes, proxies: Proxies, *, max_fee_expression: str
|
|
36
40
|
) -> int | None:
|
|
41
|
+
"""Evaluate a max fee expression, fetching base_fee from the network if needed."""
|
|
37
42
|
if "base_fee" in max_fee_expression.lower():
|
|
38
43
|
base_fee = await get_base_fee_with_logging(log_prefix, retries, nodes, proxies)
|
|
39
44
|
if base_fee is None:
|
|
@@ -44,6 +49,7 @@ async def calc_max_fee_with_logging(
|
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
def get_log_prefix(log_prefix: str | None) -> str:
|
|
52
|
+
"""Format a log prefix string, appending ': ' if non-empty."""
|
|
47
53
|
prefix = log_prefix or ""
|
|
48
54
|
if prefix:
|
|
49
55
|
prefix += ": "
|
mm_eth/cli/validators.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Pydantic validators for Ethereum addresses, keys, and expressions."""
|
|
2
|
+
|
|
1
3
|
from collections.abc import Callable
|
|
2
4
|
|
|
3
5
|
import eth_utils
|
|
@@ -9,6 +11,7 @@ SUFFIX_DECIMALS = {"eth": 18, "gwei": 9, "ether": 18}
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
def address_from_private(private_key: str) -> str:
|
|
14
|
+
"""Derive a lowercase Ethereum address from a private key."""
|
|
12
15
|
res = account.private_to_address(private_key)
|
|
13
16
|
if res.is_err():
|
|
14
17
|
raise ValueError("invalid private key")
|
|
@@ -16,30 +19,39 @@ def address_from_private(private_key: str) -> str:
|
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class Validators(ConfigValidators):
|
|
22
|
+
"""Ethereum-specific config validators for addresses, keys, and expressions."""
|
|
23
|
+
|
|
19
24
|
@staticmethod
|
|
20
25
|
def valid_eth_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
26
|
+
"""Return a validator for expressions with ETH unit suffixes."""
|
|
21
27
|
return ConfigValidators.expression_with_vars(var_name, SUFFIX_DECIMALS)
|
|
22
28
|
|
|
23
29
|
@staticmethod
|
|
24
30
|
def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
31
|
+
"""Return a validator for expressions with token unit suffix."""
|
|
25
32
|
return ConfigValidators.expression_with_vars(var_name, {"t": 6})
|
|
26
33
|
|
|
27
34
|
@staticmethod
|
|
28
35
|
def valid_eth_or_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
36
|
+
"""Return a validator for expressions with ETH or token unit suffixes."""
|
|
29
37
|
return ConfigValidators.expression_with_vars(var_name, SUFFIX_DECIMALS | {"t": 6})
|
|
30
38
|
|
|
31
39
|
@staticmethod
|
|
32
40
|
def eth_transfers() -> Callable[[str], list[Transfer]]:
|
|
41
|
+
"""Return a validator for Ethereum transfer definitions."""
|
|
33
42
|
return ConfigValidators.transfers(is_address=eth_utils.is_address, lowercase=True)
|
|
34
43
|
|
|
35
44
|
@staticmethod
|
|
36
45
|
def eth_private_keys() -> Callable[[str], PrivateKeyMap]:
|
|
46
|
+
"""Return a validator for Ethereum private key mappings."""
|
|
37
47
|
return ConfigValidators.private_keys(address_from_private)
|
|
38
48
|
|
|
39
49
|
@staticmethod
|
|
40
50
|
def eth_address() -> Callable[[str], str]:
|
|
51
|
+
"""Return a validator for a single Ethereum address."""
|
|
41
52
|
return ConfigValidators.address(eth_utils.is_address, lowercase=True)
|
|
42
53
|
|
|
43
54
|
@staticmethod
|
|
44
55
|
def eth_addresses(unique: bool) -> Callable[[str], list[str]]:
|
|
56
|
+
"""Return a validator for a list of Ethereum addresses."""
|
|
45
57
|
return ConfigValidators.addresses(unique, lowercase=True, is_address=eth_utils.is_address)
|
mm_eth/converters.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Wei/ether/gwei/token unit conversion utilities."""
|
|
2
|
+
|
|
1
3
|
from decimal import Decimal, localcontext
|
|
2
4
|
from typing import cast
|
|
3
5
|
|
|
@@ -6,6 +8,7 @@ from web3.types import Wei
|
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def to_wei(value: str | int | Decimal, decimals: int | None = None) -> Wei:
|
|
11
|
+
"""Convert a value with optional unit suffix (gwei, ether, eth, t) to wei."""
|
|
9
12
|
if isinstance(value, int):
|
|
10
13
|
return Wei(value)
|
|
11
14
|
if isinstance(value, Decimal):
|
|
@@ -36,6 +39,7 @@ def to_wei(value: str | int | Decimal, decimals: int | None = None) -> Wei:
|
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def from_wei(value: int, unit: str, round_ndigits: int | None = None, decimals: int | None = None) -> Decimal:
|
|
42
|
+
"""Convert a wei value to the specified unit (eth, gwei, t) with optional rounding."""
|
|
39
43
|
if value == 0:
|
|
40
44
|
return Decimal(0)
|
|
41
45
|
|
mm_eth/deploy.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Smart contract deployment helpers."""
|
|
2
|
+
|
|
1
3
|
import eth_utils
|
|
2
4
|
import rlp
|
|
3
5
|
from eth_utils import keccak
|
|
@@ -6,6 +8,7 @@ from mm_eth import abi
|
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def get_deploy_contract_data(contract_bin: str, constructor_types: list[str], constructor_values: list[object]) -> str:
|
|
11
|
+
"""Build deployment bytecode by appending ABI-encoded constructor arguments to contract binary."""
|
|
9
12
|
constructor_data = ""
|
|
10
13
|
if constructor_types and constructor_values:
|
|
11
14
|
constructor_data = abi.encode_data(constructor_types, constructor_values)[2:]
|
|
@@ -13,6 +16,7 @@ def get_deploy_contract_data(contract_bin: str, constructor_types: list[str], co
|
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def get_contract_address(sender_address: str, nonce: int) -> str:
|
|
19
|
+
"""Compute the contract address that would be created by a given sender and nonce."""
|
|
16
20
|
sender_bytes = eth_utils.to_bytes(hexstr=sender_address)
|
|
17
21
|
raw = rlp.encode([sender_bytes, nonce])
|
|
18
22
|
h = keccak(raw)
|
mm_eth/erc20.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""ERC-20 token transfer encoding and signing."""
|
|
2
|
+
|
|
1
3
|
import eth_abi
|
|
2
4
|
import eth_utils
|
|
3
5
|
from eth_typing import HexStr
|
|
@@ -10,6 +12,7 @@ TRANSFER_TOPIC = HexStr("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def encode_transfer_input_data(recipient: str, value: int) -> str:
|
|
15
|
+
"""Encode ERC-20 transfer(address,uint256) call data."""
|
|
13
16
|
recipient = eth_utils.to_checksum_address(recipient)
|
|
14
17
|
input_data = eth_utils.to_bytes(hexstr=HexStr(TRANSFER_METHOD)) + eth_abi.encode(["address", "uint256"], [recipient, value])
|
|
15
18
|
return eth_utils.to_hex(input_data)
|
|
@@ -27,6 +30,7 @@ def sign_transfer_tx(
|
|
|
27
30
|
private_key: str,
|
|
28
31
|
chain_id: int,
|
|
29
32
|
) -> SignedTx:
|
|
33
|
+
"""Sign an ERC-20 transfer transaction (EIP-1559)."""
|
|
30
34
|
input_data = encode_transfer_input_data(recipient_address, value)
|
|
31
35
|
return tx.sign_tx(
|
|
32
36
|
nonce=nonce,
|
mm_eth/retry.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Retry-enabled wrappers around RPC calls using node and proxy rotation."""
|
|
2
|
+
|
|
1
3
|
from typing import Any, Literal
|
|
2
4
|
|
|
3
5
|
from eth_typing import BlockIdentifier
|
|
@@ -14,18 +16,21 @@ TIMEOUT = 5.0
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
async def eth_block_number(retries: int, nodes: Nodes, proxies: Proxies, *, timeout: float = TIMEOUT) -> Result[int]:
|
|
19
|
+
"""Return the current block number with retry logic."""
|
|
17
20
|
return await retry_with_node_and_proxy(
|
|
18
21
|
retries, nodes, proxies, lambda node, proxy: rpc.eth_block_number(node, timeout, proxy)
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
async def eth_get_balance(retries: int, nodes: Nodes, proxies: Proxies, *, address: str, timeout: float = TIMEOUT) -> Result[int]:
|
|
26
|
+
"""Return the ETH balance of an address with retry logic."""
|
|
23
27
|
return await retry_with_node_and_proxy(
|
|
24
28
|
retries, nodes, proxies, lambda node, proxy: rpc.eth_get_balance(node, address, timeout, proxy)
|
|
25
29
|
)
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
async def eth_chain_id(retries: int, nodes: Nodes, proxies: Proxies, *, timeout: float = TIMEOUT) -> Result[int]:
|
|
33
|
+
"""Return the chain ID with retry logic."""
|
|
29
34
|
return await retry_with_node_and_proxy(retries, nodes, proxies, lambda node, proxy: rpc.eth_chain_id(node, timeout, proxy))
|
|
30
35
|
|
|
31
36
|
|
|
@@ -38,6 +43,7 @@ async def eth_get_block_by_number(
|
|
|
38
43
|
full_transaction: bool = False,
|
|
39
44
|
timeout: float = TIMEOUT,
|
|
40
45
|
) -> Result[dict[str, Any]]:
|
|
46
|
+
"""Return block data for a given block number with retry logic."""
|
|
41
47
|
return await retry_with_node_and_proxy(
|
|
42
48
|
retries,
|
|
43
49
|
nodes,
|
|
@@ -49,6 +55,7 @@ async def eth_get_block_by_number(
|
|
|
49
55
|
async def eth_get_transaction_count(
|
|
50
56
|
retries: int, nodes: Nodes, proxies: Proxies, *, address: str, timeout: float = TIMEOUT
|
|
51
57
|
) -> Result[int]:
|
|
58
|
+
"""Return the nonce for an address with retry logic."""
|
|
52
59
|
return await retry_with_node_and_proxy(
|
|
53
60
|
retries, nodes, proxies, lambda node, proxy: rpc.eth_get_transaction_count(node, address, timeout, proxy)
|
|
54
61
|
)
|
|
@@ -66,6 +73,7 @@ async def eth_estimate_gas(
|
|
|
66
73
|
type_: Literal["0x0", "0x2"] | None = None,
|
|
67
74
|
timeout: float = TIMEOUT,
|
|
68
75
|
) -> Result[int]:
|
|
76
|
+
"""Estimate gas for a transaction with retry logic."""
|
|
69
77
|
return await retry_with_node_and_proxy(
|
|
70
78
|
retries, nodes, proxies, lambda node, proxy: rpc.eth_estimate_gas(node, from_, to, value, data, type_, timeout, proxy)
|
|
71
79
|
)
|
|
@@ -74,6 +82,7 @@ async def eth_estimate_gas(
|
|
|
74
82
|
async def eth_send_raw_transaction(
|
|
75
83
|
retries: int, nodes: Nodes, proxies: Proxies, *, raw_tx: str, timeout: float = TIMEOUT
|
|
76
84
|
) -> Result[str]:
|
|
85
|
+
"""Broadcast a raw transaction with retry logic."""
|
|
77
86
|
return await retry_with_node_and_proxy(
|
|
78
87
|
retries, nodes, proxies, lambda node, proxy: rpc.eth_send_raw_transaction(node, raw_tx, timeout, proxy)
|
|
79
88
|
)
|
|
@@ -82,6 +91,7 @@ async def eth_send_raw_transaction(
|
|
|
82
91
|
async def eth_get_transaction_receipt(
|
|
83
92
|
retries: int, nodes: Nodes, proxies: Proxies, *, tx_hash: str, timeout: float = TIMEOUT
|
|
84
93
|
) -> Result[TxReceipt]:
|
|
94
|
+
"""Fetch a transaction receipt with retry logic."""
|
|
85
95
|
return await retry_with_node_and_proxy(
|
|
86
96
|
retries, nodes, proxies, lambda node, proxy: rpc.eth_get_transaction_receipt(node, tx_hash, timeout, proxy)
|
|
87
97
|
)
|
|
@@ -96,6 +106,7 @@ async def eth_get_transaction_receipt(
|
|
|
96
106
|
async def erc20_balance(
|
|
97
107
|
retries: int, nodes: Nodes, proxies: Proxies, *, token: str, wallet: str, timeout: float = TIMEOUT
|
|
98
108
|
) -> Result[int]:
|
|
109
|
+
"""Return an ERC-20 token balance with retry logic."""
|
|
99
110
|
return await retry_with_node_and_proxy(
|
|
100
111
|
retries,
|
|
101
112
|
nodes,
|
|
@@ -105,18 +116,21 @@ async def erc20_balance(
|
|
|
105
116
|
|
|
106
117
|
|
|
107
118
|
async def erc20_name(retries: int, nodes: Nodes, proxies: Proxies, *, token: str, timeout: float = TIMEOUT) -> Result[str]:
|
|
119
|
+
"""Return the name of an ERC-20 token with retry logic."""
|
|
108
120
|
return await retry_with_node_and_proxy(
|
|
109
121
|
retries, nodes, proxies, lambda node, proxy: rpc.erc20_name(node, token, timeout, proxy)
|
|
110
122
|
)
|
|
111
123
|
|
|
112
124
|
|
|
113
125
|
async def erc20_symbol(retries: int, nodes: Nodes, proxies: Proxies, *, token: str, timeout: float = TIMEOUT) -> Result[str]:
|
|
126
|
+
"""Return the symbol of an ERC-20 token with retry logic."""
|
|
114
127
|
return await retry_with_node_and_proxy(
|
|
115
128
|
retries, nodes, proxies, lambda node, proxy: rpc.erc20_symbol(node, token, timeout, proxy)
|
|
116
129
|
)
|
|
117
130
|
|
|
118
131
|
|
|
119
132
|
async def erc20_decimals(retries: int, nodes: Nodes, proxies: Proxies, *, token: str, timeout: float = TIMEOUT) -> Result[int]:
|
|
133
|
+
"""Return the decimals of an ERC-20 token with retry logic."""
|
|
120
134
|
return await retry_with_node_and_proxy(
|
|
121
135
|
retries, nodes, proxies, lambda node, proxy: rpc.erc20_decimals(node, token, timeout, proxy)
|
|
122
136
|
)
|
|
@@ -128,6 +142,7 @@ async def erc20_decimals(retries: int, nodes: Nodes, proxies: Proxies, *, token:
|
|
|
128
142
|
|
|
129
143
|
|
|
130
144
|
async def ens_name(retries: int, nodes: Nodes, proxies: Proxies, *, address: str, timeout: float = TIMEOUT) -> Result[str | None]:
|
|
145
|
+
"""Perform reverse ENS resolution with retry logic."""
|
|
131
146
|
return await retry_with_node_and_proxy(
|
|
132
147
|
retries, nodes, proxies, lambda node, proxy: rpc.ens_name(node, address, timeout, proxy)
|
|
133
148
|
)
|
|
@@ -139,12 +154,14 @@ async def ens_name(retries: int, nodes: Nodes, proxies: Proxies, *, address: str
|
|
|
139
154
|
|
|
140
155
|
|
|
141
156
|
async def get_base_fee_per_gas(retries: int, nodes: Nodes, proxies: Proxies, *, timeout: float = TIMEOUT) -> Result[int]:
|
|
157
|
+
"""Return the base fee per gas from the latest block with retry logic."""
|
|
142
158
|
return await retry_with_node_and_proxy(
|
|
143
159
|
retries, nodes, proxies, lambda node, proxy: rpc.get_base_fee_per_gas(node, timeout, proxy)
|
|
144
160
|
)
|
|
145
161
|
|
|
146
162
|
|
|
147
163
|
async def get_tx_status(retries: int, nodes: Nodes, proxies: Proxies, *, tx_hash: str, timeout: float = TIMEOUT) -> Result[int]:
|
|
164
|
+
"""Return the transaction status (0=fail, 1=success) with retry logic."""
|
|
148
165
|
return await retry_with_node_and_proxy(
|
|
149
166
|
retries, nodes, proxies, lambda node, proxy: rpc.get_tx_status(node, tx_hash, timeout, proxy)
|
|
150
167
|
)
|