mm-eth 0.3.1__tar.gz → 0.4.0__tar.gz
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-0.3.1 → mm_eth-0.4.0}/PKG-INFO +3 -3
- {mm_eth-0.3.1 → mm_eth-0.4.0}/pyproject.toml +6 -6
- mm_eth-0.4.0/src/mm_eth/cli/calcs.py +27 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cli.py +20 -42
- mm_eth-0.4.0/src/mm_eth/cli/cli_utils.py +60 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/balances_cmd.py +1 -1
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/call_contract_cmd.py +1 -1
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/deploy_cmd.py +1 -1
- mm_eth-0.3.1/src/mm_eth/cli/cmd/config_example_cmd.py → mm_eth-0.4.0/src/mm_eth/cli/cmd/example_cmd.py +1 -1
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/send_contract_cmd.py +1 -1
- mm_eth-0.4.0/src/mm_eth/cli/cmd/transfer_cmd.py +311 -0
- {mm_eth-0.3.1/src/mm_eth/cli/config_examples → mm_eth-0.4.0/src/mm_eth/cli/examples}/balances.toml +3 -0
- {mm_eth-0.3.1/src/mm_eth/cli/config_examples → mm_eth-0.4.0/src/mm_eth/cli/examples}/call_contract.toml +4 -0
- mm_eth-0.3.1/src/mm_eth/cli/config_examples/transfer_erc20.toml → mm_eth-0.4.0/src/mm_eth/cli/examples/transfer.toml +15 -3
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/validators.py +4 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/uv.lock +47 -47
- mm_eth-0.3.1/src/mm_eth/cli/calcs.py +0 -93
- mm_eth-0.3.1/src/mm_eth/cli/cli_utils.py +0 -32
- mm_eth-0.3.1/src/mm_eth/cli/cmd/transfer_erc20_cmd.py +0 -200
- mm_eth-0.3.1/src/mm_eth/cli/cmd/transfer_eth_cmd.py +0 -182
- mm_eth-0.3.1/src/mm_eth/cli/config_examples/transfer_eth.toml +0 -26
- {mm_eth-0.3.1 → mm_eth-0.4.0}/.gitignore +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/README.txt +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/dict.dic +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/justfile +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/__init__.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/abi.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/account.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/anvil.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/__init__.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/__init__.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/balance_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/encode_input_data_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/mnemonic_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/node_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/private_key_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/rpc_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/solc_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/token_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/tx_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/cmd/vault_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/print_helpers.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/cli/rpc_helpers.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/constants.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/deploy.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/ens.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/erc20.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/ethernodes.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/json_encoder.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/py.typed +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/rpc.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/solc.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/tx.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/utils.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/src/mm_eth/vault.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/__init__.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/__init__.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/cmd/__init__.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/cmd/test_balance_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/cmd/test_mnemonic_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/cmd/test_node_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/cmd/test_private_key_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/cmd/test_solc_cmd.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/cli/test_calcs.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/conftest.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/contracts/ERC20.sol +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/contracts/abi/ERC20.json +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/test_abi.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/test_account.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/test_ethernodes.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/test_rpc.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/test_tx.py +0 -0
- {mm_eth-0.3.1 → mm_eth-0.4.0}/tests/test_utils.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
|
-
Requires-Dist: mm-crypto-utils>=0.1.
|
|
5
|
+
Requires-Dist: mm-crypto-utils>=0.1.5
|
|
6
6
|
Requires-Dist: typer>=0.15.1
|
|
7
|
-
Requires-Dist: web3~=7.
|
|
7
|
+
Requires-Dist: web3~=7.8.0
|
|
8
8
|
Requires-Dist: websocket-client~=1.8.0
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-eth"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-crypto-utils>=0.1.
|
|
7
|
+
"mm-crypto-utils>=0.1.5",
|
|
8
8
|
"websocket-client~=1.8.0",
|
|
9
|
-
"web3~=7.
|
|
9
|
+
"web3~=7.8.0",
|
|
10
10
|
"typer>=0.15.1",
|
|
11
11
|
]
|
|
12
12
|
[project.scripts]
|
|
@@ -20,11 +20,11 @@ build-backend = "hatchling.build"
|
|
|
20
20
|
dev-dependencies = [
|
|
21
21
|
"pytest~=8.3.4",
|
|
22
22
|
"pytest-xdist~=3.6.1",
|
|
23
|
-
"ruff~=0.9.
|
|
23
|
+
"ruff~=0.9.5",
|
|
24
24
|
"pip-audit~=2.7.3",
|
|
25
25
|
"bandit~=1.8.2",
|
|
26
|
-
"mypy~=1.
|
|
27
|
-
"types-
|
|
26
|
+
"mypy~=1.15.0",
|
|
27
|
+
"types-pyyaml>=6.0.12.20241230",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
[tool.mypy]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
import mm_crypto_utils
|
|
4
|
+
from mm_crypto_utils import VarInt
|
|
5
|
+
|
|
6
|
+
from mm_eth.constants import SUFFIX_DECIMALS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calc_eth_expression(expression: str, var: VarInt | None = None) -> int:
|
|
10
|
+
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals=SUFFIX_DECIMALS)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def calc_token_expression(expression: str, token_decimals: int, var: VarInt | None = None) -> int:
|
|
14
|
+
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals={"t": token_decimals})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calc_function_args(value: str) -> str:
|
|
18
|
+
while True:
|
|
19
|
+
if "random(" not in value:
|
|
20
|
+
return value
|
|
21
|
+
start_index = value.index("random(")
|
|
22
|
+
stop_index = value.index(")", start_index)
|
|
23
|
+
random_range = [int(v.strip()) for v in value[start_index + 7 : stop_index].split(",")]
|
|
24
|
+
if len(random_range) != 2:
|
|
25
|
+
raise ValueError("wrong random(from,to) template")
|
|
26
|
+
rand_value = str(random.randint(random_range[0], random_range[1]))
|
|
27
|
+
value = value[0:start_index] + rand_value + value[stop_index + 1 :]
|
|
@@ -10,9 +10,9 @@ from .cmd import (
|
|
|
10
10
|
balance_cmd,
|
|
11
11
|
balances_cmd,
|
|
12
12
|
call_contract_cmd,
|
|
13
|
-
config_example_cmd,
|
|
14
13
|
deploy_cmd,
|
|
15
14
|
encode_input_data_cmd,
|
|
15
|
+
example_cmd,
|
|
16
16
|
mnemonic_cmd,
|
|
17
17
|
node_cmd,
|
|
18
18
|
private_key_cmd,
|
|
@@ -20,8 +20,7 @@ from .cmd import (
|
|
|
20
20
|
send_contract_cmd,
|
|
21
21
|
solc_cmd,
|
|
22
22
|
token_cmd,
|
|
23
|
-
|
|
24
|
-
transfer_eth_cmd,
|
|
23
|
+
transfer_cmd,
|
|
25
24
|
tx_cmd,
|
|
26
25
|
vault_cmd,
|
|
27
26
|
)
|
|
@@ -29,8 +28,7 @@ from .cmd.balances_cmd import BalancesCmdParams
|
|
|
29
28
|
from .cmd.call_contract_cmd import CallContractCmdParams
|
|
30
29
|
from .cmd.deploy_cmd import DeployCmdParams
|
|
31
30
|
from .cmd.send_contract_cmd import SendContractCmdParams
|
|
32
|
-
from .cmd.
|
|
33
|
-
from .cmd.transfer_eth_cmd import TransferEthCmdParams
|
|
31
|
+
from .cmd.transfer_cmd import TransferCmdParams
|
|
34
32
|
|
|
35
33
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
36
34
|
|
|
@@ -40,8 +38,7 @@ app.add_typer(wallet_app, name="w", hidden=True)
|
|
|
40
38
|
|
|
41
39
|
|
|
42
40
|
class ConfigExample(str, Enum):
|
|
43
|
-
|
|
44
|
-
TRANSFER_ERC20 = "transfer-erc20"
|
|
41
|
+
TRANSFER = "transfer"
|
|
45
42
|
BALANCES = "balances"
|
|
46
43
|
CALL_CONTRACT = "call-contract"
|
|
47
44
|
|
|
@@ -141,43 +138,24 @@ def tx_command(
|
|
|
141
138
|
tx_cmd.run(rpc_url, tx_hash, get_receipt)
|
|
142
139
|
|
|
143
140
|
|
|
144
|
-
@app.command(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
149
|
-
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
150
|
-
no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
151
|
-
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
152
|
-
) -> None:
|
|
153
|
-
transfer_eth_cmd.run(
|
|
154
|
-
TransferEthCmdParams(
|
|
155
|
-
config_path=config_path,
|
|
156
|
-
print_balances=print_balances,
|
|
157
|
-
print_config_and_exit=print_config,
|
|
158
|
-
debug=debug,
|
|
159
|
-
no_receipt=no_receipt,
|
|
160
|
-
emulate=emulate,
|
|
161
|
-
)
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@app.command(name="transfer-erc20", help="Transfer ERC20 token from one or many accounts")
|
|
166
|
-
def transfer_erc20_command(
|
|
141
|
+
@app.command(
|
|
142
|
+
name="transfer", help="Transfers ETH or ERC20 tokens, supporting multiple routes, delays, and expression-based values"
|
|
143
|
+
)
|
|
144
|
+
def transfer_command(
|
|
167
145
|
config_path: Path,
|
|
168
146
|
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
169
147
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
170
148
|
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
171
|
-
|
|
149
|
+
skip_receipt: bool = typer.Option(False, "--skip-receipt", help="Don't wait for a tx receipt"),
|
|
172
150
|
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
173
151
|
) -> None:
|
|
174
|
-
|
|
175
|
-
|
|
152
|
+
transfer_cmd.run(
|
|
153
|
+
TransferCmdParams(
|
|
176
154
|
config_path=config_path,
|
|
177
155
|
print_balances=print_balances,
|
|
178
|
-
|
|
156
|
+
print_config=print_config,
|
|
179
157
|
debug=debug,
|
|
180
|
-
|
|
158
|
+
skip_receipt=skip_receipt,
|
|
181
159
|
emulate=emulate,
|
|
182
160
|
)
|
|
183
161
|
)
|
|
@@ -196,7 +174,7 @@ def send_contract_command(
|
|
|
196
174
|
SendContractCmdParams(
|
|
197
175
|
config_path=config_path,
|
|
198
176
|
print_balances=print_balances,
|
|
199
|
-
|
|
177
|
+
print_config=print_config,
|
|
200
178
|
debug=debug,
|
|
201
179
|
no_receipt=no_receipt,
|
|
202
180
|
emulate=emulate,
|
|
@@ -211,7 +189,7 @@ def balances_command(
|
|
|
211
189
|
nonce: bool = typer.Option(False, "--nonce", "-n", help="Print nonce also"),
|
|
212
190
|
wei: bool = typer.Option(False, "--wei", "-w", help="Show balances in WEI"),
|
|
213
191
|
) -> None:
|
|
214
|
-
balances_cmd.run(BalancesCmdParams(config_path=config_path,
|
|
192
|
+
balances_cmd.run(BalancesCmdParams(config_path=config_path, print_config=print_config, wei=wei, show_nonce=nonce))
|
|
215
193
|
|
|
216
194
|
|
|
217
195
|
@app.command(name="call-contract", help="Call a method on a contract")
|
|
@@ -219,7 +197,7 @@ def call_contract_command(
|
|
|
219
197
|
config_path: Path,
|
|
220
198
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
221
199
|
) -> None:
|
|
222
|
-
call_contract_cmd.run(CallContractCmdParams(config_path=config_path,
|
|
200
|
+
call_contract_cmd.run(CallContractCmdParams(config_path=config_path, print_config=print_config))
|
|
223
201
|
|
|
224
202
|
|
|
225
203
|
@app.command(name="deploy", help="Deploy a smart contract onchain")
|
|
@@ -227,12 +205,12 @@ def deploy_command(
|
|
|
227
205
|
config_path: Path,
|
|
228
206
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
229
207
|
) -> None:
|
|
230
|
-
deploy_cmd.run(DeployCmdParams(config_path=config_path,
|
|
208
|
+
deploy_cmd.run(DeployCmdParams(config_path=config_path, print_config=print_config))
|
|
231
209
|
|
|
232
210
|
|
|
233
|
-
@app.command(name="
|
|
234
|
-
def
|
|
235
|
-
|
|
211
|
+
@app.command(name="example", help="Displays an example configuration for a command")
|
|
212
|
+
def example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
|
|
213
|
+
example_cmd.run(command)
|
|
236
214
|
|
|
237
215
|
|
|
238
216
|
@app.command(name="encode-input-data", help="Encode input data by a function signature")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from mm_crypto_utils import Nodes, Proxies
|
|
7
|
+
from mm_std import BaseConfig, print_json
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from mm_eth import rpc
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_version() -> str:
|
|
14
|
+
return importlib.metadata.version("mm-eth")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def public_rpc_url(url: str | None) -> str:
|
|
18
|
+
if not url or url == "1":
|
|
19
|
+
return "https://ethereum.publicnode.com"
|
|
20
|
+
if url.startswith(("http://", "https://", "ws://", "wss://")):
|
|
21
|
+
return url
|
|
22
|
+
|
|
23
|
+
match url.lower():
|
|
24
|
+
case "mainnet" | "1":
|
|
25
|
+
return "https://ethereum.publicnode.com"
|
|
26
|
+
case "opbnb" | "204":
|
|
27
|
+
return "https://opbnb-mainnet-rpc.bnbchain.org"
|
|
28
|
+
case "base" | "8453":
|
|
29
|
+
return "https://mainnet.base.org"
|
|
30
|
+
case "base-sepolia" | "84532":
|
|
31
|
+
return "https://sepolia.base.org"
|
|
32
|
+
case _:
|
|
33
|
+
return url
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseConfigParams(BaseModel):
|
|
37
|
+
config_path: Path
|
|
38
|
+
print_config: bool
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_config(config: BaseConfig, exclude: set[str] | None = None, count: set[str] | None = None) -> None:
|
|
42
|
+
data = config.model_dump(exclude=exclude)
|
|
43
|
+
if count:
|
|
44
|
+
for k in count:
|
|
45
|
+
data[k] = len(data[k])
|
|
46
|
+
print_json(data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def wait_tx_status(nodes: Nodes, proxies: Proxies, tx_hash: str, timeout: int) -> Literal["OK", "FAIL", "TIMEOUT"]:
|
|
50
|
+
started_at = time.perf_counter()
|
|
51
|
+
count = 0
|
|
52
|
+
while True:
|
|
53
|
+
res = rpc.get_tx_status(nodes, tx_hash, proxies=proxies, attempts=5)
|
|
54
|
+
if res.is_ok():
|
|
55
|
+
return "OK" if res.ok == 1 else "FAIL"
|
|
56
|
+
|
|
57
|
+
time.sleep(1)
|
|
58
|
+
count += 1
|
|
59
|
+
if time.perf_counter() - started_at > timeout:
|
|
60
|
+
return "TIMEOUT"
|
|
@@ -33,7 +33,7 @@ class BalancesCmdParams(BaseConfigParams):
|
|
|
33
33
|
|
|
34
34
|
def run(params: BalancesCmdParams) -> None:
|
|
35
35
|
config = Config.read_toml_config_or_exit(params.config_path)
|
|
36
|
-
if params.
|
|
36
|
+
if params.print_config:
|
|
37
37
|
config.print_and_exit()
|
|
38
38
|
|
|
39
39
|
tokens = _get_tokens_info(config)
|
|
@@ -22,7 +22,7 @@ class CallContractCmdParams(BaseConfigParams):
|
|
|
22
22
|
|
|
23
23
|
def run(cli_params: CallContractCmdParams) -> None:
|
|
24
24
|
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
25
|
-
if cli_params.
|
|
25
|
+
if cli_params.print_config:
|
|
26
26
|
config.print_and_exit()
|
|
27
27
|
|
|
28
28
|
input_data = abi.encode_function_input_by_signature(
|
|
@@ -27,7 +27,7 @@ class DeployCmdParams(BaseConfigParams):
|
|
|
27
27
|
|
|
28
28
|
def run(cli_params: DeployCmdParams) -> None:
|
|
29
29
|
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
30
|
-
if cli_params.
|
|
30
|
+
if cli_params.print_config:
|
|
31
31
|
config.print_and_exit({"private_key"})
|
|
32
32
|
|
|
33
33
|
constructor_types = yaml.full_load(config.constructor_types)
|
|
@@ -5,5 +5,5 @@ from mm_std import print_plain
|
|
|
5
5
|
|
|
6
6
|
def run(command: str) -> None:
|
|
7
7
|
command = command.replace("-", "_")
|
|
8
|
-
example_file = Path(Path(__file__).parent.absolute(), "../
|
|
8
|
+
example_file = Path(Path(__file__).parent.absolute(), "../examples", f"{command}.toml")
|
|
9
9
|
print_plain(example_file.read_text())
|
|
@@ -65,7 +65,7 @@ class SendContractCmdParams(BaseConfigParams):
|
|
|
65
65
|
# noinspection DuplicatedCode
|
|
66
66
|
def run(cli_params: SendContractCmdParams) -> None:
|
|
67
67
|
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
68
|
-
if cli_params.
|
|
68
|
+
if cli_params.print_config:
|
|
69
69
|
config.print_and_exit({"private_key"})
|
|
70
70
|
|
|
71
71
|
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Self
|
|
5
|
+
|
|
6
|
+
import mm_crypto_utils
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from mm_crypto_utils import AddressToPrivate, TxRoute
|
|
9
|
+
from mm_std import BaseConfig, Err, fatal, utc_now
|
|
10
|
+
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
11
|
+
from rich.live import Live
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from mm_eth import erc20, rpc
|
|
15
|
+
from mm_eth.cli import calcs, cli_utils, rpc_helpers
|
|
16
|
+
from mm_eth.cli.calcs import calc_eth_expression
|
|
17
|
+
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
18
|
+
from mm_eth.cli.validators import Validators
|
|
19
|
+
from mm_eth.tx import sign_tx
|
|
20
|
+
from mm_eth.utils import from_wei_str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Config(BaseConfig):
|
|
24
|
+
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
25
|
+
chain_id: int
|
|
26
|
+
routes: Annotated[list[TxRoute], BeforeValidator(Validators.eth_routes())]
|
|
27
|
+
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
28
|
+
token: Annotated[str | None, AfterValidator(Validators.eth_address())] = None # if None, then eth transfer
|
|
29
|
+
token_decimals: int = -1
|
|
30
|
+
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
31
|
+
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
32
|
+
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
33
|
+
value: Annotated[str, AfterValidator(Validators.valid_eth_or_token_expression("balance"))]
|
|
34
|
+
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_or_token_expression())] = None
|
|
35
|
+
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
36
|
+
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
37
|
+
round_ndigits: int = 5
|
|
38
|
+
proxies: Annotated[list[str], Field(default_factory=list), BeforeValidator(Validators.proxies())]
|
|
39
|
+
wait_tx_timeout: int = 120
|
|
40
|
+
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
41
|
+
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def from_addresses(self) -> list[str]:
|
|
45
|
+
return [r.from_address for r in self.routes]
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def final_validator(self) -> Self:
|
|
49
|
+
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
50
|
+
raise ValueError("private keys are not set for all addresses")
|
|
51
|
+
|
|
52
|
+
if self.token:
|
|
53
|
+
Validators.valid_token_expression("balance")(self.value)
|
|
54
|
+
if self.value_min_limit:
|
|
55
|
+
Validators.valid_token_expression()(self.value_min_limit)
|
|
56
|
+
else:
|
|
57
|
+
Validators.valid_eth_expression("balance")(self.value)
|
|
58
|
+
if self.value_min_limit:
|
|
59
|
+
Validators.valid_eth_expression()(self.value_min_limit)
|
|
60
|
+
|
|
61
|
+
if self.token:
|
|
62
|
+
res = erc20.get_decimals(self.nodes, self.token, proxies=self.proxies, attempts=5)
|
|
63
|
+
if isinstance(res, Err):
|
|
64
|
+
fatal(f"can't get token decimals: {res.err}")
|
|
65
|
+
self.token_decimals = res.ok
|
|
66
|
+
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TransferCmdParams(BaseConfigParams):
|
|
71
|
+
print_balances: bool
|
|
72
|
+
debug: bool
|
|
73
|
+
skip_receipt: bool
|
|
74
|
+
emulate: bool
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run(cmd_params: TransferCmdParams) -> None:
|
|
78
|
+
config = Config.read_toml_config_or_exit(cmd_params.config_path)
|
|
79
|
+
if cmd_params.print_config:
|
|
80
|
+
cli_utils.print_config(config, exclude={"private_keys"}, count=None if cmd_params.debug else {"proxies"})
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
84
|
+
|
|
85
|
+
if cmd_params.print_balances:
|
|
86
|
+
_print_balances(config)
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
_run_transfers(config, cmd_params)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
93
|
+
mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
|
|
94
|
+
logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
|
|
95
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
96
|
+
for i, route in enumerate(config.routes):
|
|
97
|
+
_transfer(route, config, cmd_params)
|
|
98
|
+
if config.delay is not None and i < len(config.routes) - 1:
|
|
99
|
+
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
100
|
+
logger.info(f"delay {delay_value} seconds")
|
|
101
|
+
if not cmd_params.emulate:
|
|
102
|
+
time.sleep(float(delay_value))
|
|
103
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
107
|
+
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, route.log_prefix)
|
|
108
|
+
if nonce is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, route.log_prefix)
|
|
112
|
+
if max_fee is None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, route.log_prefix):
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
gas = _calc_gas(route, config)
|
|
119
|
+
if gas is None:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
value = _calc_value(route, max_fee=max_fee, gas=gas, config=config)
|
|
123
|
+
if value is None:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if not _check_value_min_limit(route, value, config):
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
priority_fee = calc_eth_expression(config.priority_fee)
|
|
130
|
+
|
|
131
|
+
# emulate?
|
|
132
|
+
if cmd_params.emulate:
|
|
133
|
+
msg = f"{route.log_prefix}: emulate,"
|
|
134
|
+
msg += f" value={_value_with_suffix(value, config)},"
|
|
135
|
+
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
136
|
+
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
137
|
+
msg += f" gas={gas}"
|
|
138
|
+
logger.info(msg)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
tx_hash = _send_tx(route=route, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
|
|
142
|
+
if tx_hash is None:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
status = "UNKNOWN"
|
|
146
|
+
if not cmd_params.skip_receipt:
|
|
147
|
+
logger.debug(f"{route.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
|
|
148
|
+
status = cli_utils.wait_tx_status(config.nodes, config.proxies, tx_hash, config.wait_tx_timeout)
|
|
149
|
+
|
|
150
|
+
logger.info(f"{route.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _calc_value(route: TxRoute, max_fee: int, gas: int, config: Config) -> int | None:
|
|
154
|
+
if config.token:
|
|
155
|
+
return rpc_helpers.calc_erc20_value_for_address(
|
|
156
|
+
nodes=config.nodes,
|
|
157
|
+
value_expression=config.value,
|
|
158
|
+
wallet_address=route.from_address,
|
|
159
|
+
token_address=config.token,
|
|
160
|
+
decimals=config.token_decimals,
|
|
161
|
+
log_prefix=route.log_prefix,
|
|
162
|
+
)
|
|
163
|
+
return rpc_helpers.calc_eth_value_for_address(
|
|
164
|
+
nodes=config.nodes,
|
|
165
|
+
value_expression=config.value,
|
|
166
|
+
address=route.from_address,
|
|
167
|
+
gas=gas,
|
|
168
|
+
max_fee=max_fee,
|
|
169
|
+
log_prefix=route.log_prefix,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
|
|
174
|
+
"""Returns False if the transfer should be skipped."""
|
|
175
|
+
if config.value_min_limit:
|
|
176
|
+
if config.token:
|
|
177
|
+
value_min_limit = calcs.calc_token_expression(config.value_min_limit, config.token_decimals)
|
|
178
|
+
else:
|
|
179
|
+
value_min_limit = calcs.calc_eth_expression(config.value_min_limit)
|
|
180
|
+
if value < value_min_limit:
|
|
181
|
+
logger.info(f"{route.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config) -> str | None:
|
|
186
|
+
debug_tx_params = {
|
|
187
|
+
"nonce": nonce,
|
|
188
|
+
"max_fee": max_fee,
|
|
189
|
+
"priority_fee": priority_fee,
|
|
190
|
+
"gas": gas,
|
|
191
|
+
"value": value,
|
|
192
|
+
"to": route.to_address,
|
|
193
|
+
"chain_id": config.chain_id,
|
|
194
|
+
}
|
|
195
|
+
logger.debug(f"{route.log_prefix}: tx_params={debug_tx_params}")
|
|
196
|
+
|
|
197
|
+
if config.token:
|
|
198
|
+
signed_tx = erc20.sign_transfer_tx(
|
|
199
|
+
nonce=nonce,
|
|
200
|
+
max_fee_per_gas=max_fee,
|
|
201
|
+
max_priority_fee_per_gas=priority_fee,
|
|
202
|
+
gas_limit=gas,
|
|
203
|
+
private_key=config.private_keys[route.from_address],
|
|
204
|
+
chain_id=config.chain_id,
|
|
205
|
+
value=value,
|
|
206
|
+
token_address=config.token,
|
|
207
|
+
recipient_address=route.to_address,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
signed_tx = sign_tx(
|
|
211
|
+
nonce=nonce,
|
|
212
|
+
max_fee_per_gas=max_fee,
|
|
213
|
+
max_priority_fee_per_gas=priority_fee,
|
|
214
|
+
gas=gas,
|
|
215
|
+
private_key=config.private_keys[route.from_address],
|
|
216
|
+
chain_id=config.chain_id,
|
|
217
|
+
value=value,
|
|
218
|
+
to=route.to_address,
|
|
219
|
+
)
|
|
220
|
+
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
221
|
+
if isinstance(res, Err):
|
|
222
|
+
logger.info(f"{route.log_prefix}: tx error {res.err}")
|
|
223
|
+
return None
|
|
224
|
+
return res.ok
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _calc_gas(route: TxRoute, config: Config) -> int | None:
|
|
228
|
+
if config.token:
|
|
229
|
+
return rpc_helpers.calc_gas(
|
|
230
|
+
nodes=config.nodes,
|
|
231
|
+
gas_expression=config.gas,
|
|
232
|
+
from_address=route.from_address,
|
|
233
|
+
to_address=config.token,
|
|
234
|
+
data=erc20.encode_transfer_input_data(route.to_address, 1234),
|
|
235
|
+
log_prefix=route.log_prefix,
|
|
236
|
+
)
|
|
237
|
+
return rpc_helpers.calc_gas(
|
|
238
|
+
nodes=config.nodes,
|
|
239
|
+
gas_expression=config.gas,
|
|
240
|
+
from_address=route.from_address,
|
|
241
|
+
to_address=route.to_address,
|
|
242
|
+
value=123,
|
|
243
|
+
log_prefix=route.log_prefix,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _print_balances(config: Config) -> None:
|
|
248
|
+
if config.token:
|
|
249
|
+
headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
|
|
250
|
+
else:
|
|
251
|
+
headers = ["n", "from_address", "nonce", "eth", "to_address", "nonce", "eth"]
|
|
252
|
+
table = Table(*headers, title="balances")
|
|
253
|
+
with Live(table, refresh_per_second=0.5):
|
|
254
|
+
for count, route in enumerate(config.routes):
|
|
255
|
+
from_nonce = _get_nonce_str(route.from_address, config)
|
|
256
|
+
to_nonce = _get_nonce_str(route.to_address, config)
|
|
257
|
+
|
|
258
|
+
from_eth_balance = _get_eth_balance_str(route.from_address, config)
|
|
259
|
+
to_eth_balance = _get_eth_balance_str(route.to_address, config)
|
|
260
|
+
|
|
261
|
+
from_token_balance = _get_token_balance_str(route.from_address, config) if config.token else ""
|
|
262
|
+
to_token_balance = _get_token_balance_str(route.to_address, config) if config.token else ""
|
|
263
|
+
|
|
264
|
+
if config.token:
|
|
265
|
+
table.add_row(
|
|
266
|
+
str(count),
|
|
267
|
+
route.from_address,
|
|
268
|
+
from_nonce,
|
|
269
|
+
from_eth_balance,
|
|
270
|
+
from_token_balance,
|
|
271
|
+
route.to_address,
|
|
272
|
+
to_nonce,
|
|
273
|
+
to_eth_balance,
|
|
274
|
+
to_token_balance,
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
table.add_row(
|
|
278
|
+
str(count),
|
|
279
|
+
route.from_address,
|
|
280
|
+
from_nonce,
|
|
281
|
+
from_eth_balance,
|
|
282
|
+
route.to_address,
|
|
283
|
+
to_nonce,
|
|
284
|
+
to_eth_balance,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _get_nonce_str(address: str, config: Config) -> str:
|
|
289
|
+
return str(rpc.eth_get_transaction_count(config.nodes, address, proxies=config.proxies, attempts=5).ok_or_err())
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _get_eth_balance_str(address: str, config: Config) -> str:
|
|
293
|
+
return rpc.eth_get_balance(config.nodes, address, proxies=config.proxies, attempts=5).map_or_else(
|
|
294
|
+
lambda err: err,
|
|
295
|
+
lambda ok: from_wei_str(ok, "eth", config.round_ndigits),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _get_token_balance_str(address: str, config: Config) -> str:
|
|
300
|
+
if not config.token:
|
|
301
|
+
raise ValueError("token is not set")
|
|
302
|
+
return erc20.get_balance(config.nodes, config.token, address, proxies=config.proxies, attempts=5).map_or_else(
|
|
303
|
+
lambda err: err,
|
|
304
|
+
lambda ok: from_wei_str(ok, "t", decimals=config.token_decimals, round_ndigits=config.round_ndigits),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _value_with_suffix(value: int, config: Config) -> str:
|
|
309
|
+
if config.token:
|
|
310
|
+
return from_wei_str(value, "t", config.round_ndigits, decimals=config.token_decimals)
|
|
311
|
+
return from_wei_str(value, "eth", config.round_ndigits)
|
{mm_eth-0.3.1/src/mm_eth/cli/config_examples → mm_eth-0.4.0/src/mm_eth/cli/examples}/balances.toml
RENAMED
|
@@ -3,13 +3,16 @@ addresses = """
|
|
|
3
3
|
0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
4
4
|
0x97C77B548aE0d4925F5C201220fC6d8996424309
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
tokens = """
|
|
7
8
|
0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # USDT
|
|
8
9
|
0x6a55fe4884DE7E1d904BdC47A3BA092240ae9B39 # USDC
|
|
9
10
|
"""
|
|
11
|
+
|
|
10
12
|
nodes = """
|
|
11
13
|
https://arb1.arbitrum.io/rpc
|
|
12
14
|
https://rpc.arb1.arbitrum.gateway.fm
|
|
13
15
|
https://arbitrum-one.publicnode.com
|
|
14
16
|
"""
|
|
17
|
+
|
|
15
18
|
round_ndigits = 3
|