mm-eth 0.4.0__tar.gz → 0.5.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.4.0 → mm_eth-0.5.0}/.gitignore +1 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/PKG-INFO +2 -2
- {mm_eth-0.4.0 → mm_eth-0.5.0}/dict.dic +1 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/pyproject.toml +3 -3
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/account.py +11 -11
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cli.py +29 -26
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/transfer_cmd.py +75 -52
- {mm_eth-0.4.0/src/mm_eth/cli/cmd → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/mnemonic_cmd.py +4 -4
- mm_eth-0.5.0/src/mm_eth/cli/examples/transfer.toml +42 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/validators.py +3 -3
- mm_eth-0.5.0/tests/cli/cmd/__init__.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_account.py +9 -1
- {mm_eth-0.4.0 → mm_eth-0.5.0}/uv.lock +18 -40
- mm_eth-0.4.0/src/mm_eth/cli/cmd/send_contract_cmd.py +0 -197
- mm_eth-0.4.0/src/mm_eth/cli/examples/transfer.toml +0 -39
- {mm_eth-0.4.0 → mm_eth-0.5.0}/README.txt +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/justfile +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/__init__.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/abi.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/anvil.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/__init__.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/calcs.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cli_utils.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/__init__.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/balance_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/balances_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/call_contract_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/deploy_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/encode_input_data_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/example_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/node_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/rpc_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/solc_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/token_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/tx_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/vault_cmd.py +0 -0
- {mm_eth-0.4.0/tests → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/__init__.py +0 -0
- {mm_eth-0.4.0/src/mm_eth/cli/cmd → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/private_key_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/examples/balances.toml +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/examples/call_contract.toml +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/print_helpers.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/rpc_helpers.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/constants.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/deploy.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/ens.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/erc20.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/ethernodes.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/json_encoder.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/py.typed +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/rpc.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/solc.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/tx.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/utils.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/vault.py +0 -0
- {mm_eth-0.4.0/tests/cli → mm_eth-0.5.0/tests}/__init__.py +0 -0
- {mm_eth-0.4.0/tests/cli/cmd → mm_eth-0.5.0/tests/cli}/__init__.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_balance_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_mnemonic_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_node_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_private_key_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_solc_cmd.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/test_calcs.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/conftest.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/contracts/ERC20.sol +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/contracts/abi/ERC20.json +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_abi.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_ethernodes.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_rpc.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_tx.py +0 -0
- {mm_eth-0.4.0 → mm_eth-0.5.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.5.0
|
|
4
4
|
Requires-Python: >=3.12
|
|
5
|
-
Requires-Dist: mm-crypto-utils>=0.
|
|
5
|
+
Requires-Dist: mm-crypto-utils>=0.2.3
|
|
6
6
|
Requires-Dist: typer>=0.15.1
|
|
7
7
|
Requires-Dist: web3~=7.8.0
|
|
8
8
|
Requires-Dist: websocket-client~=1.8.0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-eth"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = ""
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-crypto-utils>=0.
|
|
7
|
+
"mm-crypto-utils>=0.2.3",
|
|
8
8
|
"websocket-client~=1.8.0",
|
|
9
9
|
"web3~=7.8.0",
|
|
10
10
|
"typer>=0.15.1",
|
|
@@ -21,7 +21,7 @@ dev-dependencies = [
|
|
|
21
21
|
"pytest~=8.3.4",
|
|
22
22
|
"pytest-xdist~=3.6.1",
|
|
23
23
|
"ruff~=0.9.5",
|
|
24
|
-
"pip-audit~=2.
|
|
24
|
+
"pip-audit~=2.8.0",
|
|
25
25
|
"bandit~=1.8.2",
|
|
26
26
|
"mypy~=1.15.0",
|
|
27
27
|
"types-pyyaml>=6.0.12.20241230",
|
|
@@ -13,9 +13,12 @@ Account.enable_unaudited_hdwallet_features()
|
|
|
13
13
|
|
|
14
14
|
key_api = KeyAPI()
|
|
15
15
|
|
|
16
|
+
DEFAULT_DERIVATION_PATH = "m/44'/60'/0'/0/{i}"
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
@dataclass
|
|
18
|
-
class
|
|
20
|
+
class DerivedAccount:
|
|
21
|
+
index: int
|
|
19
22
|
path: str
|
|
20
23
|
address: str
|
|
21
24
|
private_key: str
|
|
@@ -30,18 +33,15 @@ def generate_mnemonic(num_words: int = 24) -> str:
|
|
|
30
33
|
return mnemonic.generate(num_words=num_words)
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
limit: int = 12,
|
|
38
|
-
) -> list[NewAccount]:
|
|
39
|
-
result: list[NewAccount] = []
|
|
36
|
+
def derive_accounts(mnemonic: str, passphrase: str, derivation_path: str, limit: int) -> list[DerivedAccount]:
|
|
37
|
+
if "{i}" not in derivation_path:
|
|
38
|
+
raise ValueError("derivation_path must contain {i}, for example: " + DEFAULT_DERIVATION_PATH)
|
|
39
|
+
result: list[DerivedAccount] = []
|
|
40
40
|
for i in range(limit):
|
|
41
|
-
path =
|
|
42
|
-
acc = Account.from_mnemonic(mnemonic
|
|
41
|
+
path = derivation_path.replace("{i}", str(i))
|
|
42
|
+
acc = Account.from_mnemonic(mnemonic, passphrase, path)
|
|
43
43
|
private_key = acc.key.to_0x_hex().lower()
|
|
44
|
-
result.append(
|
|
44
|
+
result.append(DerivedAccount(i, path, acc.address, private_key))
|
|
45
45
|
return result
|
|
46
46
|
|
|
47
47
|
|
|
@@ -5,6 +5,8 @@ from typing import Annotated
|
|
|
5
5
|
import typer
|
|
6
6
|
from mm_std import PrintFormat, print_plain
|
|
7
7
|
|
|
8
|
+
from mm_eth.account import DEFAULT_DERIVATION_PATH
|
|
9
|
+
|
|
8
10
|
from . import cli_utils
|
|
9
11
|
from .cmd import (
|
|
10
12
|
balance_cmd,
|
|
@@ -13,11 +15,8 @@ from .cmd import (
|
|
|
13
15
|
deploy_cmd,
|
|
14
16
|
encode_input_data_cmd,
|
|
15
17
|
example_cmd,
|
|
16
|
-
mnemonic_cmd,
|
|
17
18
|
node_cmd,
|
|
18
|
-
private_key_cmd,
|
|
19
19
|
rpc_cmd,
|
|
20
|
-
send_contract_cmd,
|
|
21
20
|
solc_cmd,
|
|
22
21
|
token_cmd,
|
|
23
22
|
transfer_cmd,
|
|
@@ -27,8 +26,8 @@ from .cmd import (
|
|
|
27
26
|
from .cmd.balances_cmd import BalancesCmdParams
|
|
28
27
|
from .cmd.call_contract_cmd import CallContractCmdParams
|
|
29
28
|
from .cmd.deploy_cmd import DeployCmdParams
|
|
30
|
-
from .cmd.send_contract_cmd import SendContractCmdParams
|
|
31
29
|
from .cmd.transfer_cmd import TransferCmdParams
|
|
30
|
+
from .cmd.wallet import mnemonic_cmd, private_key_cmd
|
|
32
31
|
|
|
33
32
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
34
33
|
|
|
@@ -74,9 +73,10 @@ def node_command(
|
|
|
74
73
|
@wallet_app.command(name="mnemonic", help="Generate eth accounts based on a mnemonic")
|
|
75
74
|
def mnemonic_command( # nosec
|
|
76
75
|
mnemonic: Annotated[str, typer.Option("--mnemonic", "-m")] = "",
|
|
77
|
-
passphrase: Annotated[str, typer.Option("--passphrase", "-
|
|
76
|
+
passphrase: Annotated[str, typer.Option("--passphrase", "-p")] = "",
|
|
78
77
|
print_path: bool = typer.Option(False, "--print_path"),
|
|
79
|
-
|
|
78
|
+
derivation_path: Annotated[str, typer.Option("--path")] = DEFAULT_DERIVATION_PATH,
|
|
79
|
+
words: int = typer.Option(12, "--words", "-w", help="Number of mnemonic words"),
|
|
80
80
|
limit: int = typer.Option(10, "--limit", "-l"),
|
|
81
81
|
save_file: str = typer.Option("", "--save", "-s", help="Save private keys to a file"),
|
|
82
82
|
) -> None:
|
|
@@ -85,7 +85,8 @@ def mnemonic_command( # nosec
|
|
|
85
85
|
passphrase=passphrase,
|
|
86
86
|
print_path=print_path,
|
|
87
87
|
limit=limit,
|
|
88
|
-
|
|
88
|
+
words=words,
|
|
89
|
+
derivation_path=derivation_path,
|
|
89
90
|
save_file=save_file,
|
|
90
91
|
)
|
|
91
92
|
|
|
@@ -144,6 +145,7 @@ def tx_command(
|
|
|
144
145
|
def transfer_command(
|
|
145
146
|
config_path: Path,
|
|
146
147
|
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
148
|
+
print_transfers: bool = typer.Option(False, "--transfers", "-t", help="Print transfers (from, to, value) and exit"),
|
|
147
149
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
148
150
|
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
149
151
|
skip_receipt: bool = typer.Option(False, "--skip-receipt", help="Don't wait for a tx receipt"),
|
|
@@ -153,6 +155,7 @@ def transfer_command(
|
|
|
153
155
|
TransferCmdParams(
|
|
154
156
|
config_path=config_path,
|
|
155
157
|
print_balances=print_balances,
|
|
158
|
+
print_transfers=print_transfers,
|
|
156
159
|
print_config=print_config,
|
|
157
160
|
debug=debug,
|
|
158
161
|
skip_receipt=skip_receipt,
|
|
@@ -161,25 +164,25 @@ def transfer_command(
|
|
|
161
164
|
)
|
|
162
165
|
|
|
163
166
|
|
|
164
|
-
@app.command(name="send-contract", help="Send transactions to a contract")
|
|
165
|
-
def send_contract_command(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
) -> None:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
167
|
+
# @app.command(name="send-contract", help="Send transactions to a contract")
|
|
168
|
+
# def send_contract_command(
|
|
169
|
+
# config_path: Path,
|
|
170
|
+
# print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
171
|
+
# print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
172
|
+
# emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
173
|
+
# no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
|
|
174
|
+
# debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
175
|
+
# ) -> None:
|
|
176
|
+
# send_contract_cmd.run(
|
|
177
|
+
# SendContractCmdParams(
|
|
178
|
+
# config_path=config_path,
|
|
179
|
+
# print_balances=print_balances,
|
|
180
|
+
# print_config=print_config,
|
|
181
|
+
# debug=debug,
|
|
182
|
+
# no_receipt=no_receipt,
|
|
183
|
+
# emulate=emulate,
|
|
184
|
+
# )
|
|
185
|
+
# )
|
|
183
186
|
|
|
184
187
|
|
|
185
188
|
@app.command(name="balances", help="Print base and ERC20 token balances")
|
|
@@ -5,9 +5,10 @@ from typing import Annotated, Self
|
|
|
5
5
|
|
|
6
6
|
import mm_crypto_utils
|
|
7
7
|
from loguru import logger
|
|
8
|
-
from mm_crypto_utils import AddressToPrivate,
|
|
8
|
+
from mm_crypto_utils import AddressToPrivate, Transfer
|
|
9
9
|
from mm_std import BaseConfig, Err, fatal, utc_now
|
|
10
10
|
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
11
|
+
from rich.console import Console
|
|
11
12
|
from rich.live import Live
|
|
12
13
|
from rich.table import Table
|
|
13
14
|
|
|
@@ -23,7 +24,7 @@ from mm_eth.utils import from_wei_str
|
|
|
23
24
|
class Config(BaseConfig):
|
|
24
25
|
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
25
26
|
chain_id: int
|
|
26
|
-
|
|
27
|
+
transfers: Annotated[list[Transfer], BeforeValidator(Validators.eth_transfers())]
|
|
27
28
|
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
28
29
|
token: Annotated[str | None, AfterValidator(Validators.eth_address())] = None # if None, then eth transfer
|
|
29
30
|
token_decimals: int = -1
|
|
@@ -42,13 +43,20 @@ class Config(BaseConfig):
|
|
|
42
43
|
|
|
43
44
|
@property
|
|
44
45
|
def from_addresses(self) -> list[str]:
|
|
45
|
-
return [r.from_address for r in self.
|
|
46
|
+
return [r.from_address for r in self.transfers]
|
|
46
47
|
|
|
47
48
|
@model_validator(mode="after")
|
|
48
49
|
def final_validator(self) -> Self:
|
|
49
50
|
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
50
51
|
raise ValueError("private keys are not set for all addresses")
|
|
51
52
|
|
|
53
|
+
for transfer in self.transfers: # If value is not set for a transfer, then set it to the global value of the config.
|
|
54
|
+
if not transfer.value:
|
|
55
|
+
transfer.value = self.value
|
|
56
|
+
for transfer in self.transfers: # Check all transfers have a value.
|
|
57
|
+
if not transfer.value:
|
|
58
|
+
raise ValueError(f"{transfer.log_prefix}: value is not set")
|
|
59
|
+
|
|
52
60
|
if self.token:
|
|
53
61
|
Validators.valid_token_expression("balance")(self.value)
|
|
54
62
|
if self.value_min_limit:
|
|
@@ -69,6 +77,7 @@ class Config(BaseConfig):
|
|
|
69
77
|
|
|
70
78
|
class TransferCmdParams(BaseConfigParams):
|
|
71
79
|
print_balances: bool
|
|
80
|
+
print_transfers: bool
|
|
72
81
|
debug: bool
|
|
73
82
|
skip_receipt: bool
|
|
74
83
|
emulate: bool
|
|
@@ -82,6 +91,10 @@ def run(cmd_params: TransferCmdParams) -> None:
|
|
|
82
91
|
|
|
83
92
|
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
84
93
|
|
|
94
|
+
if cmd_params.print_transfers:
|
|
95
|
+
_print_transfers(config)
|
|
96
|
+
sys.exit(0)
|
|
97
|
+
|
|
85
98
|
if cmd_params.print_balances:
|
|
86
99
|
_print_balances(config)
|
|
87
100
|
sys.exit(0)
|
|
@@ -93,9 +106,9 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
93
106
|
mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
|
|
94
107
|
logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
|
|
95
108
|
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
96
|
-
for i,
|
|
97
|
-
_transfer(
|
|
98
|
-
if config.delay is not None and i < len(config.
|
|
109
|
+
for i, transfer in enumerate(config.transfers):
|
|
110
|
+
_transfer(transfer, config, cmd_params)
|
|
111
|
+
if config.delay is not None and i < len(config.transfers) - 1:
|
|
99
112
|
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
100
113
|
logger.info(f"delay {delay_value} seconds")
|
|
101
114
|
if not cmd_params.emulate:
|
|
@@ -103,34 +116,34 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
|
|
|
103
116
|
logger.info(f"finished at {utc_now()} UTC")
|
|
104
117
|
|
|
105
118
|
|
|
106
|
-
def _transfer(
|
|
107
|
-
nonce = rpc_helpers.get_nonce(config.nodes,
|
|
119
|
+
def _transfer(t: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
|
|
120
|
+
nonce = rpc_helpers.get_nonce(config.nodes, t.from_address, t.log_prefix)
|
|
108
121
|
if nonce is None:
|
|
109
122
|
return
|
|
110
123
|
|
|
111
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee,
|
|
124
|
+
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, t.log_prefix)
|
|
112
125
|
if max_fee is None:
|
|
113
126
|
return
|
|
114
127
|
|
|
115
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit,
|
|
128
|
+
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, t.log_prefix):
|
|
116
129
|
return
|
|
117
130
|
|
|
118
|
-
gas = _calc_gas(
|
|
131
|
+
gas = _calc_gas(t, config)
|
|
119
132
|
if gas is None:
|
|
120
133
|
return
|
|
121
134
|
|
|
122
|
-
value = _calc_value(
|
|
135
|
+
value = _calc_value(t, max_fee=max_fee, gas=gas, config=config)
|
|
123
136
|
if value is None:
|
|
124
137
|
return
|
|
125
138
|
|
|
126
|
-
if not _check_value_min_limit(
|
|
139
|
+
if not _check_value_min_limit(t, value, config):
|
|
127
140
|
return
|
|
128
141
|
|
|
129
142
|
priority_fee = calc_eth_expression(config.priority_fee)
|
|
130
143
|
|
|
131
144
|
# emulate?
|
|
132
145
|
if cmd_params.emulate:
|
|
133
|
-
msg = f"{
|
|
146
|
+
msg = f"{t.log_prefix}: emulate,"
|
|
134
147
|
msg += f" value={_value_with_suffix(value, config)},"
|
|
135
148
|
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
136
149
|
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
@@ -138,39 +151,39 @@ def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) ->
|
|
|
138
151
|
logger.info(msg)
|
|
139
152
|
return
|
|
140
153
|
|
|
141
|
-
tx_hash = _send_tx(
|
|
154
|
+
tx_hash = _send_tx(transfer=t, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
|
|
142
155
|
if tx_hash is None:
|
|
143
156
|
return
|
|
144
157
|
|
|
145
158
|
status = "UNKNOWN"
|
|
146
159
|
if not cmd_params.skip_receipt:
|
|
147
|
-
logger.debug(f"{
|
|
160
|
+
logger.debug(f"{t.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
|
|
148
161
|
status = cli_utils.wait_tx_status(config.nodes, config.proxies, tx_hash, config.wait_tx_timeout)
|
|
149
162
|
|
|
150
|
-
logger.info(f"{
|
|
163
|
+
logger.info(f"{t.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
|
|
151
164
|
|
|
152
165
|
|
|
153
|
-
def _calc_value(
|
|
166
|
+
def _calc_value(transfer: Transfer, max_fee: int, gas: int, config: Config) -> int | None:
|
|
154
167
|
if config.token:
|
|
155
168
|
return rpc_helpers.calc_erc20_value_for_address(
|
|
156
169
|
nodes=config.nodes,
|
|
157
|
-
value_expression=
|
|
158
|
-
wallet_address=
|
|
170
|
+
value_expression=transfer.value,
|
|
171
|
+
wallet_address=transfer.from_address,
|
|
159
172
|
token_address=config.token,
|
|
160
173
|
decimals=config.token_decimals,
|
|
161
|
-
log_prefix=
|
|
174
|
+
log_prefix=transfer.log_prefix,
|
|
162
175
|
)
|
|
163
176
|
return rpc_helpers.calc_eth_value_for_address(
|
|
164
177
|
nodes=config.nodes,
|
|
165
|
-
value_expression=
|
|
166
|
-
address=
|
|
178
|
+
value_expression=transfer.value,
|
|
179
|
+
address=transfer.from_address,
|
|
167
180
|
gas=gas,
|
|
168
181
|
max_fee=max_fee,
|
|
169
|
-
log_prefix=
|
|
182
|
+
log_prefix=transfer.log_prefix,
|
|
170
183
|
)
|
|
171
184
|
|
|
172
185
|
|
|
173
|
-
def _check_value_min_limit(
|
|
186
|
+
def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
|
|
174
187
|
"""Returns False if the transfer should be skipped."""
|
|
175
188
|
if config.value_min_limit:
|
|
176
189
|
if config.token:
|
|
@@ -178,21 +191,23 @@ def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
|
|
|
178
191
|
else:
|
|
179
192
|
value_min_limit = calcs.calc_eth_expression(config.value_min_limit)
|
|
180
193
|
if value < value_min_limit:
|
|
181
|
-
logger.info(f"{
|
|
194
|
+
logger.info(f"{transfer.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
|
|
182
195
|
return True
|
|
183
196
|
|
|
184
197
|
|
|
185
|
-
def _send_tx(
|
|
198
|
+
def _send_tx(
|
|
199
|
+
*, transfer: Transfer, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config
|
|
200
|
+
) -> str | None:
|
|
186
201
|
debug_tx_params = {
|
|
187
202
|
"nonce": nonce,
|
|
188
203
|
"max_fee": max_fee,
|
|
189
204
|
"priority_fee": priority_fee,
|
|
190
205
|
"gas": gas,
|
|
191
206
|
"value": value,
|
|
192
|
-
"to":
|
|
207
|
+
"to": transfer.to_address,
|
|
193
208
|
"chain_id": config.chain_id,
|
|
194
209
|
}
|
|
195
|
-
logger.debug(f"{
|
|
210
|
+
logger.debug(f"{transfer.log_prefix}: tx_params={debug_tx_params}")
|
|
196
211
|
|
|
197
212
|
if config.token:
|
|
198
213
|
signed_tx = erc20.sign_transfer_tx(
|
|
@@ -200,11 +215,11 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
|
|
|
200
215
|
max_fee_per_gas=max_fee,
|
|
201
216
|
max_priority_fee_per_gas=priority_fee,
|
|
202
217
|
gas_limit=gas,
|
|
203
|
-
private_key=config.private_keys[
|
|
218
|
+
private_key=config.private_keys[transfer.from_address],
|
|
204
219
|
chain_id=config.chain_id,
|
|
205
220
|
value=value,
|
|
206
221
|
token_address=config.token,
|
|
207
|
-
recipient_address=
|
|
222
|
+
recipient_address=transfer.to_address,
|
|
208
223
|
)
|
|
209
224
|
else:
|
|
210
225
|
signed_tx = sign_tx(
|
|
@@ -212,38 +227,46 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
|
|
|
212
227
|
max_fee_per_gas=max_fee,
|
|
213
228
|
max_priority_fee_per_gas=priority_fee,
|
|
214
229
|
gas=gas,
|
|
215
|
-
private_key=config.private_keys[
|
|
230
|
+
private_key=config.private_keys[transfer.from_address],
|
|
216
231
|
chain_id=config.chain_id,
|
|
217
232
|
value=value,
|
|
218
|
-
to=
|
|
233
|
+
to=transfer.to_address,
|
|
219
234
|
)
|
|
220
235
|
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
221
236
|
if isinstance(res, Err):
|
|
222
|
-
logger.info(f"{
|
|
237
|
+
logger.info(f"{transfer.log_prefix}: tx error {res.err}")
|
|
223
238
|
return None
|
|
224
239
|
return res.ok
|
|
225
240
|
|
|
226
241
|
|
|
227
|
-
def _calc_gas(
|
|
242
|
+
def _calc_gas(transfer: Transfer, config: Config) -> int | None:
|
|
228
243
|
if config.token:
|
|
229
244
|
return rpc_helpers.calc_gas(
|
|
230
245
|
nodes=config.nodes,
|
|
231
246
|
gas_expression=config.gas,
|
|
232
|
-
from_address=
|
|
247
|
+
from_address=transfer.from_address,
|
|
233
248
|
to_address=config.token,
|
|
234
|
-
data=erc20.encode_transfer_input_data(
|
|
235
|
-
log_prefix=
|
|
249
|
+
data=erc20.encode_transfer_input_data(transfer.to_address, 1234),
|
|
250
|
+
log_prefix=transfer.log_prefix,
|
|
236
251
|
)
|
|
237
252
|
return rpc_helpers.calc_gas(
|
|
238
253
|
nodes=config.nodes,
|
|
239
254
|
gas_expression=config.gas,
|
|
240
|
-
from_address=
|
|
241
|
-
to_address=
|
|
255
|
+
from_address=transfer.from_address,
|
|
256
|
+
to_address=transfer.to_address,
|
|
242
257
|
value=123,
|
|
243
|
-
log_prefix=
|
|
258
|
+
log_prefix=transfer.log_prefix,
|
|
244
259
|
)
|
|
245
260
|
|
|
246
261
|
|
|
262
|
+
def _print_transfers(config: Config) -> None:
|
|
263
|
+
table = Table("n", "from_address", "to_address", "value", title="transfers")
|
|
264
|
+
for count, transfer in enumerate(config.transfers, start=1):
|
|
265
|
+
table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
|
|
266
|
+
console = Console()
|
|
267
|
+
console.print(table)
|
|
268
|
+
|
|
269
|
+
|
|
247
270
|
def _print_balances(config: Config) -> None:
|
|
248
271
|
if config.token:
|
|
249
272
|
headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
|
|
@@ -251,24 +274,24 @@ def _print_balances(config: Config) -> None:
|
|
|
251
274
|
headers = ["n", "from_address", "nonce", "eth", "to_address", "nonce", "eth"]
|
|
252
275
|
table = Table(*headers, title="balances")
|
|
253
276
|
with Live(table, refresh_per_second=0.5):
|
|
254
|
-
for count,
|
|
255
|
-
from_nonce = _get_nonce_str(
|
|
256
|
-
to_nonce = _get_nonce_str(
|
|
277
|
+
for count, transfer in enumerate(config.transfers):
|
|
278
|
+
from_nonce = _get_nonce_str(transfer.from_address, config)
|
|
279
|
+
to_nonce = _get_nonce_str(transfer.to_address, config)
|
|
257
280
|
|
|
258
|
-
from_eth_balance = _get_eth_balance_str(
|
|
259
|
-
to_eth_balance = _get_eth_balance_str(
|
|
281
|
+
from_eth_balance = _get_eth_balance_str(transfer.from_address, config)
|
|
282
|
+
to_eth_balance = _get_eth_balance_str(transfer.to_address, config)
|
|
260
283
|
|
|
261
|
-
from_token_balance = _get_token_balance_str(
|
|
262
|
-
to_token_balance = _get_token_balance_str(
|
|
284
|
+
from_token_balance = _get_token_balance_str(transfer.from_address, config) if config.token else ""
|
|
285
|
+
to_token_balance = _get_token_balance_str(transfer.to_address, config) if config.token else ""
|
|
263
286
|
|
|
264
287
|
if config.token:
|
|
265
288
|
table.add_row(
|
|
266
289
|
str(count),
|
|
267
|
-
|
|
290
|
+
transfer.from_address,
|
|
268
291
|
from_nonce,
|
|
269
292
|
from_eth_balance,
|
|
270
293
|
from_token_balance,
|
|
271
|
-
|
|
294
|
+
transfer.to_address,
|
|
272
295
|
to_nonce,
|
|
273
296
|
to_eth_balance,
|
|
274
297
|
to_token_balance,
|
|
@@ -276,10 +299,10 @@ def _print_balances(config: Config) -> None:
|
|
|
276
299
|
else:
|
|
277
300
|
table.add_row(
|
|
278
301
|
str(count),
|
|
279
|
-
|
|
302
|
+
transfer.from_address,
|
|
280
303
|
from_nonce,
|
|
281
304
|
from_eth_balance,
|
|
282
|
-
|
|
305
|
+
transfer.to_address,
|
|
283
306
|
to_nonce,
|
|
284
307
|
to_eth_balance,
|
|
285
308
|
)
|
|
@@ -3,18 +3,18 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from mm_std import print_json
|
|
5
5
|
|
|
6
|
-
from mm_eth.account import
|
|
6
|
+
from mm_eth.account import derive_accounts, generate_mnemonic
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def run(mnemonic: str, passphrase: str,
|
|
9
|
+
def run(mnemonic: str, passphrase: str, words: int, derivation_path: str, limit: int, print_path: bool, save_file: str) -> None: # nosec
|
|
10
10
|
result: dict[str, Any] = {}
|
|
11
11
|
if not mnemonic:
|
|
12
|
-
mnemonic = generate_mnemonic()
|
|
12
|
+
mnemonic = generate_mnemonic(num_words=words)
|
|
13
13
|
result["mnemonic"] = mnemonic
|
|
14
14
|
if passphrase:
|
|
15
15
|
result["passphrase"] = passphrase
|
|
16
16
|
result["accounts"] = []
|
|
17
|
-
for acc in
|
|
17
|
+
for acc in derive_accounts(mnemonic=mnemonic, passphrase=passphrase, limit=limit, derivation_path=derivation_path):
|
|
18
18
|
new_account = {"address": acc.address, "private": acc.private_key}
|
|
19
19
|
if print_path:
|
|
20
20
|
new_account["path"] = acc.path
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
transfers = """
|
|
2
|
+
# from_address to_address value
|
|
3
|
+
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b 0.1t # comments are allowed here
|
|
4
|
+
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f 0.2balance-random(0.1t,0.5t)
|
|
5
|
+
0x10ecB8d838746643E613f6B5218C8e342593225c 0xE19242B72a4833eD86F1b2015d4E59052A2b278b # if the value is not set, config.value will be used
|
|
6
|
+
file: /path/to/other_transfers.txt # transfers from this file will be added
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
private_keys = """
|
|
10
|
+
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
11
|
+
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
12
|
+
file: /path/to/other_private_keys.txt
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
16
|
+
|
|
17
|
+
max_fee = "1.2base_fee+1gwei+random(1,200)" # supported var_name=base_fee
|
|
18
|
+
|
|
19
|
+
max_fee_limit = "10.1gwei-random(1,10)" # optional
|
|
20
|
+
|
|
21
|
+
priority_fee = "1gwei+random(1,12)"
|
|
22
|
+
|
|
23
|
+
gas = "estimate+random(100,200)-19" # supported var_name=estimate
|
|
24
|
+
|
|
25
|
+
value = "0.5balance-random(1.5t,3t)+11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
26
|
+
|
|
27
|
+
value_min_limit = "0.5t+random(1,2)-7"
|
|
28
|
+
|
|
29
|
+
delay = "random(1.123,10)+1" # secs, optional
|
|
30
|
+
|
|
31
|
+
log_debug = "/path/to/file_debug.log" # optional
|
|
32
|
+
|
|
33
|
+
log_info = "/path/to/file_info.log" # optional
|
|
34
|
+
|
|
35
|
+
round_ndigits = 6 # optional, default=5
|
|
36
|
+
|
|
37
|
+
chain_id = 421613
|
|
38
|
+
|
|
39
|
+
nodes = """
|
|
40
|
+
https://arbitrum-goerli.publicnode.com
|
|
41
|
+
https://rpc.goerli.arbitrum.gateway.fm
|
|
42
|
+
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
|
|
3
|
-
from mm_crypto_utils import AddressToPrivate, ConfigValidators,
|
|
3
|
+
from mm_crypto_utils import AddressToPrivate, ConfigValidators, Transfer
|
|
4
4
|
|
|
5
5
|
from mm_eth.account import address_from_private, is_address
|
|
6
6
|
from mm_eth.constants import SUFFIX_DECIMALS
|
|
@@ -22,8 +22,8 @@ class Validators(ConfigValidators):
|
|
|
22
22
|
return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS | {"t": 6})
|
|
23
23
|
|
|
24
24
|
@staticmethod
|
|
25
|
-
def
|
|
26
|
-
return ConfigValidators.
|
|
25
|
+
def eth_transfers() -> Callable[[str], list[Transfer]]:
|
|
26
|
+
return ConfigValidators.transfers(is_address, to_lower=True)
|
|
27
27
|
|
|
28
28
|
@staticmethod
|
|
29
29
|
def eth_private_keys() -> Callable[[str], AddressToPrivate]:
|
|
File without changes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from mm_eth import account
|
|
2
|
+
from mm_eth.account import DEFAULT_DERIVATION_PATH
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def test_generate_mnemonic():
|
|
@@ -8,7 +9,14 @@ def test_generate_mnemonic():
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def test_generate_accounts():
|
|
11
|
-
|
|
12
|
+
mnemonic = "tuition skin amateur sail oak bone panel concert horse need panel balance"
|
|
13
|
+
passphrase = "pass-secret"
|
|
14
|
+
res = account.derive_accounts(mnemonic, passphrase, DEFAULT_DERIVATION_PATH, 7)
|
|
15
|
+
assert len(res) == 7
|
|
16
|
+
assert res[3].index == 3
|
|
17
|
+
assert res[3].path == "m/44'/60'/0'/0/3"
|
|
18
|
+
assert res[3].address == "0x2F9e1b9f4D11756E84d4b6D2f6B107FA37feB701"
|
|
19
|
+
assert res[3].private_key == "0x7b222a59ac8496b4f1f623bc86d15e889af8406f796037888ddee1290b933183"
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
def test_to_checksum_address():
|
|
@@ -602,19 +602,6 @@ wheels = [
|
|
|
602
602
|
{ url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 },
|
|
603
603
|
]
|
|
604
604
|
|
|
605
|
-
[[package]]
|
|
606
|
-
name = "html5lib"
|
|
607
|
-
version = "1.1"
|
|
608
|
-
source = { registry = "https://pypi.org/simple" }
|
|
609
|
-
dependencies = [
|
|
610
|
-
{ name = "six" },
|
|
611
|
-
{ name = "webencodings" },
|
|
612
|
-
]
|
|
613
|
-
sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 }
|
|
614
|
-
wheels = [
|
|
615
|
-
{ url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 },
|
|
616
|
-
]
|
|
617
|
-
|
|
618
605
|
[[package]]
|
|
619
606
|
name = "httpcore"
|
|
620
607
|
version = "1.0.7"
|
|
@@ -726,19 +713,19 @@ wheels = [
|
|
|
726
713
|
|
|
727
714
|
[[package]]
|
|
728
715
|
name = "mm-crypto-utils"
|
|
729
|
-
version = "0.
|
|
716
|
+
version = "0.2.3"
|
|
730
717
|
source = { registry = "https://pypi.org/simple" }
|
|
731
718
|
dependencies = [
|
|
732
719
|
{ name = "loguru" },
|
|
733
720
|
{ name = "mm-std" },
|
|
734
721
|
]
|
|
735
722
|
wheels = [
|
|
736
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
723
|
+
{ url = "https://files.pythonhosted.org/packages/78/dd/3cb0ddc4ab14a78a0e828fd1f7ca97298be8eff67b53b6198853e2013976/mm_crypto_utils-0.2.3-py3-none-any.whl", hash = "sha256:33ca42ef22c847c6f92973d7c799460926ee5c5bd7c872d3c7d19f31377e8833", size = 8257 },
|
|
737
724
|
]
|
|
738
725
|
|
|
739
726
|
[[package]]
|
|
740
727
|
name = "mm-eth"
|
|
741
|
-
version = "0.
|
|
728
|
+
version = "0.5.0"
|
|
742
729
|
source = { editable = "." }
|
|
743
730
|
dependencies = [
|
|
744
731
|
{ name = "mm-crypto-utils" },
|
|
@@ -760,7 +747,7 @@ dev = [
|
|
|
760
747
|
|
|
761
748
|
[package.metadata]
|
|
762
749
|
requires-dist = [
|
|
763
|
-
{ name = "mm-crypto-utils", specifier = ">=0.
|
|
750
|
+
{ name = "mm-crypto-utils", specifier = ">=0.2.3" },
|
|
764
751
|
{ name = "typer", specifier = ">=0.15.1" },
|
|
765
752
|
{ name = "web3", specifier = "~=7.8.0" },
|
|
766
753
|
{ name = "websocket-client", specifier = "~=1.8.0" },
|
|
@@ -770,7 +757,7 @@ requires-dist = [
|
|
|
770
757
|
dev = [
|
|
771
758
|
{ name = "bandit", specifier = "~=1.8.2" },
|
|
772
759
|
{ name = "mypy", specifier = "~=1.15.0" },
|
|
773
|
-
{ name = "pip-audit", specifier = "~=2.
|
|
760
|
+
{ name = "pip-audit", specifier = "~=2.8.0" },
|
|
774
761
|
{ name = "pytest", specifier = "~=8.3.4" },
|
|
775
762
|
{ name = "pytest-xdist", specifier = "~=3.6.1" },
|
|
776
763
|
{ name = "ruff", specifier = "~=0.9.5" },
|
|
@@ -959,22 +946,22 @@ wheels = [
|
|
|
959
946
|
|
|
960
947
|
[[package]]
|
|
961
948
|
name = "pip-audit"
|
|
962
|
-
version = "2.
|
|
949
|
+
version = "2.8.0"
|
|
963
950
|
source = { registry = "https://pypi.org/simple" }
|
|
964
951
|
dependencies = [
|
|
965
952
|
{ name = "cachecontrol", extra = ["filecache"] },
|
|
966
953
|
{ name = "cyclonedx-python-lib" },
|
|
967
|
-
{ name = "html5lib" },
|
|
968
954
|
{ name = "packaging" },
|
|
969
955
|
{ name = "pip-api" },
|
|
970
956
|
{ name = "pip-requirements-parser" },
|
|
957
|
+
{ name = "platformdirs" },
|
|
971
958
|
{ name = "requests" },
|
|
972
959
|
{ name = "rich" },
|
|
973
960
|
{ name = "toml" },
|
|
974
961
|
]
|
|
975
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
962
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e8/c8/44ccea85bd2024f1ebe55eb6cdaf1f2183359176689eed3c0b01926c24ad/pip_audit-2.8.0.tar.gz", hash = "sha256:9816cbd94de6f618a8965c117433006b3d565a708dc05d5a7be47ab65b66fa05", size = 51073 }
|
|
976
963
|
wheels = [
|
|
977
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
964
|
+
{ url = "https://files.pythonhosted.org/packages/11/0c/be5c42643284b2cfc5d9d36b576b7465268a163bd7df481a3979a3d87a0b/pip_audit-2.8.0-py3-none-any.whl", hash = "sha256:200f50d56cb6fba3a9189c20d53250354f72f161d63b6ef77ae5de2b53044566", size = 57002 },
|
|
978
965
|
]
|
|
979
966
|
|
|
980
967
|
[[package]]
|
|
@@ -990,6 +977,15 @@ wheels = [
|
|
|
990
977
|
{ url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648 },
|
|
991
978
|
]
|
|
992
979
|
|
|
980
|
+
[[package]]
|
|
981
|
+
name = "platformdirs"
|
|
982
|
+
version = "4.3.6"
|
|
983
|
+
source = { registry = "https://pypi.org/simple" }
|
|
984
|
+
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
|
|
985
|
+
wheels = [
|
|
986
|
+
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
|
987
|
+
]
|
|
988
|
+
|
|
993
989
|
[[package]]
|
|
994
990
|
name = "pluggy"
|
|
995
991
|
version = "1.5.0"
|
|
@@ -1359,15 +1355,6 @@ wheels = [
|
|
|
1359
1355
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
|
1360
1356
|
]
|
|
1361
1357
|
|
|
1362
|
-
[[package]]
|
|
1363
|
-
name = "six"
|
|
1364
|
-
version = "1.16.0"
|
|
1365
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1366
|
-
sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
|
|
1367
|
-
wheels = [
|
|
1368
|
-
{ url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
|
|
1369
|
-
]
|
|
1370
|
-
|
|
1371
1358
|
[[package]]
|
|
1372
1359
|
name = "sniffio"
|
|
1373
1360
|
version = "1.3.1"
|
|
@@ -1504,15 +1491,6 @@ wheels = [
|
|
|
1504
1491
|
{ url = "https://files.pythonhosted.org/packages/b0/52/bc4a08811db59392e13bf56ada316517a83b9a6135c20d357c222c80be2d/web3-7.8.0-py3-none-any.whl", hash = "sha256:c8771b3d8772f7104a0462804449beb57d36cef7bd8b411140f95a92fc46b559", size = 1363475 },
|
|
1505
1492
|
]
|
|
1506
1493
|
|
|
1507
|
-
[[package]]
|
|
1508
|
-
name = "webencodings"
|
|
1509
|
-
version = "0.5.1"
|
|
1510
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1511
|
-
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 }
|
|
1512
|
-
wheels = [
|
|
1513
|
-
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 },
|
|
1514
|
-
]
|
|
1515
|
-
|
|
1516
1494
|
[[package]]
|
|
1517
1495
|
name = "websocket-client"
|
|
1518
1496
|
version = "1.8.0"
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
import time
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Annotated, Self
|
|
6
|
-
|
|
7
|
-
import mm_crypto_utils
|
|
8
|
-
from loguru import logger
|
|
9
|
-
from mm_crypto_utils import AddressToPrivate
|
|
10
|
-
from mm_std import BaseConfig, Err, Ok, utc_now
|
|
11
|
-
from pydantic import AfterValidator, BeforeValidator, StrictStr, model_validator
|
|
12
|
-
|
|
13
|
-
from mm_eth import abi, rpc
|
|
14
|
-
from mm_eth.cli import calcs, cli_utils, print_helpers, rpc_helpers, validators
|
|
15
|
-
from mm_eth.cli.calcs import calc_eth_expression
|
|
16
|
-
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
17
|
-
from mm_eth.cli.validators import Validators
|
|
18
|
-
from mm_eth.tx import sign_tx
|
|
19
|
-
from mm_eth.utils import from_wei_str
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Config(BaseConfig):
|
|
23
|
-
from_addresses: Annotated[list[str], BeforeValidator(Validators.eth_addresses(unique=True))]
|
|
24
|
-
contract_address: Annotated[str, BeforeValidator(Validators.eth_address())]
|
|
25
|
-
function_signature: str
|
|
26
|
-
function_args: StrictStr = "[]"
|
|
27
|
-
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
28
|
-
chain_id: int
|
|
29
|
-
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
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_expression("balance"))] # eth value
|
|
34
|
-
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
35
|
-
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
36
|
-
round_ndigits: int = 5
|
|
37
|
-
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
|
-
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
39
|
-
|
|
40
|
-
# noinspection DuplicatedCode
|
|
41
|
-
@model_validator(mode="after")
|
|
42
|
-
def final_validator(self) -> Self:
|
|
43
|
-
# check all private keys exist
|
|
44
|
-
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
45
|
-
raise ValueError("private keys are not set for all addresses")
|
|
46
|
-
|
|
47
|
-
# check that from_addresses is not empty
|
|
48
|
-
if not self.from_addresses:
|
|
49
|
-
raise ValueError("from_addresses is empty")
|
|
50
|
-
|
|
51
|
-
# function_args
|
|
52
|
-
if not validators.is_valid_calc_function_args(self.function_args):
|
|
53
|
-
raise ValueError(f"wrong function_args: {self.function_args}")
|
|
54
|
-
|
|
55
|
-
return self
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class SendContractCmdParams(BaseConfigParams):
|
|
59
|
-
print_balances: bool
|
|
60
|
-
debug: bool
|
|
61
|
-
no_receipt: bool
|
|
62
|
-
emulate: bool
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# noinspection DuplicatedCode
|
|
66
|
-
def run(cli_params: SendContractCmdParams) -> None:
|
|
67
|
-
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
68
|
-
if cli_params.print_config:
|
|
69
|
-
config.print_and_exit({"private_key"})
|
|
70
|
-
|
|
71
|
-
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
72
|
-
|
|
73
|
-
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
74
|
-
|
|
75
|
-
if cli_params.print_balances:
|
|
76
|
-
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
77
|
-
sys.exit(0)
|
|
78
|
-
|
|
79
|
-
_run_transfers(config, cli_params)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# noinspection DuplicatedCode
|
|
83
|
-
def _run_transfers(config: Config, cli_params: SendContractCmdParams) -> None:
|
|
84
|
-
logger.info(f"started at {utc_now()} UTC")
|
|
85
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
86
|
-
for i, from_address in enumerate(config.from_addresses):
|
|
87
|
-
_transfer(from_address, config, cli_params)
|
|
88
|
-
if not cli_params.emulate and config.delay is not None and i < len(config.from_addresses) - 1:
|
|
89
|
-
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
90
|
-
logger.debug(f"delay {delay_value} seconds")
|
|
91
|
-
time.sleep(float(delay_value))
|
|
92
|
-
logger.info(f"finished at {utc_now()} UTC")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# noinspection DuplicatedCode
|
|
96
|
-
def _transfer(from_address: str, config: Config, cli_params: SendContractCmdParams) -> None:
|
|
97
|
-
log_prefix = f"{from_address}"
|
|
98
|
-
# get nonce
|
|
99
|
-
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
100
|
-
if nonce is None:
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
# get max_fee
|
|
104
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, log_prefix)
|
|
105
|
-
if max_fee is None:
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# check max_fee_limit
|
|
109
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
priority_fee = calc_eth_expression(config.priority_fee)
|
|
113
|
-
|
|
114
|
-
# data
|
|
115
|
-
function_args = calcs.calc_function_args(config.function_args).replace("'", '"')
|
|
116
|
-
data = abi.encode_function_input_by_signature(config.function_signature, json.loads(function_args))
|
|
117
|
-
|
|
118
|
-
# get gas
|
|
119
|
-
gas = rpc_helpers.calc_gas(
|
|
120
|
-
nodes=config.nodes,
|
|
121
|
-
gas_expression=config.gas,
|
|
122
|
-
from_address=from_address,
|
|
123
|
-
to_address=config.contract_address,
|
|
124
|
-
value=None,
|
|
125
|
-
data=data,
|
|
126
|
-
log_prefix=log_prefix,
|
|
127
|
-
)
|
|
128
|
-
if gas is None:
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
# get value
|
|
132
|
-
value = None
|
|
133
|
-
if config.value is not None:
|
|
134
|
-
value = rpc_helpers.calc_eth_value_for_address(
|
|
135
|
-
nodes=config.nodes,
|
|
136
|
-
value_expression=config.value,
|
|
137
|
-
address=from_address,
|
|
138
|
-
gas=gas,
|
|
139
|
-
max_fee=max_fee,
|
|
140
|
-
log_prefix=log_prefix,
|
|
141
|
-
)
|
|
142
|
-
if value is None:
|
|
143
|
-
return
|
|
144
|
-
|
|
145
|
-
# emulate?
|
|
146
|
-
if cli_params.emulate:
|
|
147
|
-
msg = f"{log_prefix}: emulate,"
|
|
148
|
-
if value is not None:
|
|
149
|
-
msg += f" value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
150
|
-
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
151
|
-
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
152
|
-
msg += f" gas={gas}, "
|
|
153
|
-
msg += f" data={data}"
|
|
154
|
-
logger.info(msg)
|
|
155
|
-
return
|
|
156
|
-
|
|
157
|
-
debug_tx_params = {
|
|
158
|
-
"nonce": nonce,
|
|
159
|
-
"max_fee": max_fee,
|
|
160
|
-
"priority_fee": priority_fee,
|
|
161
|
-
"gas": gas,
|
|
162
|
-
"value": value,
|
|
163
|
-
"data": data,
|
|
164
|
-
"to": config.contract_address,
|
|
165
|
-
"chain_id": config.chain_id,
|
|
166
|
-
}
|
|
167
|
-
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
168
|
-
signed_tx = sign_tx(
|
|
169
|
-
nonce=nonce,
|
|
170
|
-
max_fee_per_gas=max_fee,
|
|
171
|
-
max_priority_fee_per_gas=priority_fee,
|
|
172
|
-
gas=gas,
|
|
173
|
-
private_key=config.private_keys[from_address],
|
|
174
|
-
chain_id=config.chain_id,
|
|
175
|
-
value=value,
|
|
176
|
-
data=data,
|
|
177
|
-
to=config.contract_address,
|
|
178
|
-
)
|
|
179
|
-
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
180
|
-
if isinstance(res, Err):
|
|
181
|
-
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
182
|
-
return
|
|
183
|
-
tx_hash = res.ok
|
|
184
|
-
|
|
185
|
-
if cli_params.no_receipt:
|
|
186
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}"
|
|
187
|
-
logger.info(msg)
|
|
188
|
-
else:
|
|
189
|
-
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
190
|
-
while True: # TODO: infinite loop if receipt_res is err
|
|
191
|
-
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
192
|
-
if isinstance(receipt_res, Ok):
|
|
193
|
-
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
194
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, status={status}"
|
|
195
|
-
logger.info(msg)
|
|
196
|
-
break
|
|
197
|
-
time.sleep(1)
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
routes = """
|
|
2
|
-
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
3
|
-
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
private_keys = """
|
|
7
|
-
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
8
|
-
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
9
|
-
file: /path/to/other_private_keys.txt
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
13
|
-
|
|
14
|
-
max_fee = "1.2base_fee + 1gwei + random(1,200)" # supported var_name=base_fee
|
|
15
|
-
|
|
16
|
-
max_fee_limit = "10.1gwei - random(1, 10)" # optional
|
|
17
|
-
|
|
18
|
-
priority_fee = "1gwei + random(1, 12)"
|
|
19
|
-
|
|
20
|
-
gas = "estimate + random(100, 200) - 19" # supported var_name=estimate
|
|
21
|
-
|
|
22
|
-
value = "0.5balance - random(1.5t, 3t) + 11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
23
|
-
|
|
24
|
-
value_min_limit = "0.5t + random(1, 2) - 7"
|
|
25
|
-
|
|
26
|
-
delay = "random(1.123, 10) + 1" # secs, optional
|
|
27
|
-
|
|
28
|
-
log_debug = "/path/to/file_debug.log" # optional
|
|
29
|
-
|
|
30
|
-
log_info = "/path/to/file_info.log" # optional
|
|
31
|
-
|
|
32
|
-
round_ndigits = 6 # optional, default=5
|
|
33
|
-
|
|
34
|
-
chain_id = 421613
|
|
35
|
-
|
|
36
|
-
nodes = """
|
|
37
|
-
https://arbitrum-goerli.publicnode.com
|
|
38
|
-
https://rpc.goerli.arbitrum.gateway.fm
|
|
39
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mm_eth-0.4.0/src/mm_eth/cli/cmd → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/private_key_cmd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|