mm-eth 0.3.1__py3-none-any.whl → 0.4.1__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/account.py +11 -11
- mm_eth/cli/calcs.py +2 -68
- mm_eth/cli/cli.py +28 -47
- mm_eth/cli/cli_utils.py +29 -1
- mm_eth/cli/cmd/balances_cmd.py +1 -1
- mm_eth/cli/cmd/call_contract_cmd.py +1 -1
- mm_eth/cli/cmd/deploy_cmd.py +1 -1
- mm_eth/cli/cmd/{config_example_cmd.py → example_cmd.py} +1 -1
- mm_eth/cli/cmd/send_contract_cmd.py +1 -1
- mm_eth/cli/cmd/transfer_cmd.py +311 -0
- mm_eth/cli/cmd/wallet/__init__.py +0 -0
- mm_eth/cli/cmd/{mnemonic_cmd.py → wallet/mnemonic_cmd.py} +4 -4
- mm_eth/cli/{config_examples → examples}/balances.toml +3 -0
- mm_eth/cli/{config_examples → examples}/call_contract.toml +4 -0
- mm_eth/cli/{config_examples/transfer_erc20.toml → examples/transfer.toml} +15 -3
- mm_eth/cli/validators.py +4 -0
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.1.dist-info}/METADATA +3 -3
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.1.dist-info}/RECORD +21 -22
- mm_eth/cli/cmd/transfer_erc20_cmd.py +0 -200
- mm_eth/cli/cmd/transfer_eth_cmd.py +0 -182
- mm_eth/cli/config_examples/transfer_eth.toml +0 -26
- /mm_eth/cli/cmd/{private_key_cmd.py → wallet/private_key_cmd.py} +0 -0
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.1.dist-info}/WHEEL +0 -0
- {mm_eth-0.3.1.dist-info → mm_eth-0.4.1.dist-info}/entry_points.txt +0 -0
mm_eth/account.py
CHANGED
|
@@ -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
|
|
mm_eth/cli/calcs.py
CHANGED
|
@@ -10,58 +10,8 @@ def calc_eth_expression(expression: str, var: VarInt | None = None) -> int:
|
|
|
10
10
|
return mm_crypto_utils.calc_int_expression(expression, var=var, suffix_decimals=SUFFIX_DECIMALS)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# raise TypeError(f"value is not str: {value}")
|
|
16
|
-
# try:
|
|
17
|
-
# var_name = var_name.lower()
|
|
18
|
-
# result = 0
|
|
19
|
-
# for token in split_on_plus_minus_tokens(value.lower()):
|
|
20
|
-
# operator = token[0]
|
|
21
|
-
# item = token[1:]
|
|
22
|
-
# if item.isdigit():
|
|
23
|
-
# item_value = int(item)
|
|
24
|
-
# elif item.endswith("eth"):
|
|
25
|
-
# item = item.removesuffix("eth")
|
|
26
|
-
# item_value = int(Decimal(item) * 10**18)
|
|
27
|
-
# elif item.endswith("ether"):
|
|
28
|
-
# item = item.removesuffix("ether")
|
|
29
|
-
# item_value = int(Decimal(item) * 10**18)
|
|
30
|
-
# elif item.endswith("gwei"):
|
|
31
|
-
# item = item.removesuffix("gwei")
|
|
32
|
-
# item_value = int(Decimal(item) * 10**9)
|
|
33
|
-
# elif item.endswith("t"):
|
|
34
|
-
# if decimals is None:
|
|
35
|
-
# raise ValueError("t without decimals")
|
|
36
|
-
# item = item.removesuffix("t")
|
|
37
|
-
# item_value = int(Decimal(item) * 10**decimals)
|
|
38
|
-
# elif item.endswith(var_name):
|
|
39
|
-
# if var_value is None:
|
|
40
|
-
# raise ValueError("var_value is not set")
|
|
41
|
-
# item = item.removesuffix(var_name)
|
|
42
|
-
# k = Decimal(item) if item else Decimal(1)
|
|
43
|
-
# item_value = int(k * var_value)
|
|
44
|
-
# elif item.startswith("random(") and item.endswith(")"):
|
|
45
|
-
# item = item.lstrip("random(").rstrip(")")
|
|
46
|
-
# arr = item.split(",")
|
|
47
|
-
# if len(arr) != 2:
|
|
48
|
-
# raise ValueError(f"wrong value, random part: {value}")
|
|
49
|
-
# from_value = to_wei(arr[0], decimals=decimals)
|
|
50
|
-
# to_value = to_wei(arr[1], decimals=decimals)
|
|
51
|
-
# if from_value > to_value:
|
|
52
|
-
# raise ValueError(f"wrong value, random part: {value}")
|
|
53
|
-
# item_value = random.randint(from_value, to_value)
|
|
54
|
-
# else:
|
|
55
|
-
# raise ValueError(f"wrong value: {value}")
|
|
56
|
-
#
|
|
57
|
-
# if operator == "+":
|
|
58
|
-
# result += item_value
|
|
59
|
-
# if operator == "-":
|
|
60
|
-
# result -= item_value
|
|
61
|
-
#
|
|
62
|
-
# return result
|
|
63
|
-
# except Exception as err:
|
|
64
|
-
# raise ValueError(f"illegal ee: {value}, error={err}") from err
|
|
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})
|
|
65
15
|
|
|
66
16
|
|
|
67
17
|
def calc_function_args(value: str) -> str:
|
|
@@ -75,19 +25,3 @@ def calc_function_args(value: str) -> str:
|
|
|
75
25
|
raise ValueError("wrong random(from,to) template")
|
|
76
26
|
rand_value = str(random.randint(random_range[0], random_range[1]))
|
|
77
27
|
value = value[0:start_index] + rand_value + value[stop_index + 1 :]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# def is_value_less_min_limit(
|
|
81
|
-
# value_min_limit: str | None,
|
|
82
|
-
# value: int,
|
|
83
|
-
# value_unit: str,
|
|
84
|
-
# decimals: int | None = None,
|
|
85
|
-
# log_prefix: str | None = None,
|
|
86
|
-
# ) -> bool:
|
|
87
|
-
# if value_min_limit is None:
|
|
88
|
-
# return False
|
|
89
|
-
# if value < calc_eth_expression(value_min_limit):
|
|
90
|
-
# prefix = get_log_prefix(log_prefix)
|
|
91
|
-
# logger.info("{}value is less min limit, value={}", prefix, from_wei_str(value, value_unit, decimals=decimals))
|
|
92
|
-
# return True
|
|
93
|
-
# return False
|
mm_eth/cli/cli.py
CHANGED
|
@@ -5,23 +5,22 @@ 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,
|
|
11
13
|
balances_cmd,
|
|
12
14
|
call_contract_cmd,
|
|
13
|
-
config_example_cmd,
|
|
14
15
|
deploy_cmd,
|
|
15
16
|
encode_input_data_cmd,
|
|
16
|
-
|
|
17
|
+
example_cmd,
|
|
17
18
|
node_cmd,
|
|
18
|
-
private_key_cmd,
|
|
19
19
|
rpc_cmd,
|
|
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,8 @@ 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.
|
|
31
|
+
from .cmd.transfer_cmd import TransferCmdParams
|
|
32
|
+
from .cmd.wallet import mnemonic_cmd, private_key_cmd
|
|
34
33
|
|
|
35
34
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
36
35
|
|
|
@@ -40,8 +39,7 @@ app.add_typer(wallet_app, name="w", hidden=True)
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
class ConfigExample(str, Enum):
|
|
43
|
-
|
|
44
|
-
TRANSFER_ERC20 = "transfer-erc20"
|
|
42
|
+
TRANSFER = "transfer"
|
|
45
43
|
BALANCES = "balances"
|
|
46
44
|
CALL_CONTRACT = "call-contract"
|
|
47
45
|
|
|
@@ -77,9 +75,10 @@ def node_command(
|
|
|
77
75
|
@wallet_app.command(name="mnemonic", help="Generate eth accounts based on a mnemonic")
|
|
78
76
|
def mnemonic_command( # nosec
|
|
79
77
|
mnemonic: Annotated[str, typer.Option("--mnemonic", "-m")] = "",
|
|
80
|
-
passphrase: Annotated[str, typer.Option("--passphrase", "-
|
|
78
|
+
passphrase: Annotated[str, typer.Option("--passphrase", "-p")] = "",
|
|
81
79
|
print_path: bool = typer.Option(False, "--print_path"),
|
|
82
|
-
|
|
80
|
+
derivation_path: Annotated[str, typer.Option("--path")] = DEFAULT_DERIVATION_PATH,
|
|
81
|
+
words: int = typer.Option(12, "--words", "-w", help="Number of mnemonic words"),
|
|
83
82
|
limit: int = typer.Option(10, "--limit", "-l"),
|
|
84
83
|
save_file: str = typer.Option("", "--save", "-s", help="Save private keys to a file"),
|
|
85
84
|
) -> None:
|
|
@@ -88,7 +87,8 @@ def mnemonic_command( # nosec
|
|
|
88
87
|
passphrase=passphrase,
|
|
89
88
|
print_path=print_path,
|
|
90
89
|
limit=limit,
|
|
91
|
-
|
|
90
|
+
words=words,
|
|
91
|
+
derivation_path=derivation_path,
|
|
92
92
|
save_file=save_file,
|
|
93
93
|
)
|
|
94
94
|
|
|
@@ -141,43 +141,24 @@ def tx_command(
|
|
|
141
141
|
tx_cmd.run(rpc_url, tx_hash, get_receipt)
|
|
142
142
|
|
|
143
143
|
|
|
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(
|
|
144
|
+
@app.command(
|
|
145
|
+
name="transfer", help="Transfers ETH or ERC20 tokens, supporting multiple routes, delays, and expression-based values"
|
|
146
|
+
)
|
|
147
|
+
def transfer_command(
|
|
167
148
|
config_path: Path,
|
|
168
149
|
print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
|
|
169
150
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
170
151
|
emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
|
|
171
|
-
|
|
152
|
+
skip_receipt: bool = typer.Option(False, "--skip-receipt", help="Don't wait for a tx receipt"),
|
|
172
153
|
debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
|
|
173
154
|
) -> None:
|
|
174
|
-
|
|
175
|
-
|
|
155
|
+
transfer_cmd.run(
|
|
156
|
+
TransferCmdParams(
|
|
176
157
|
config_path=config_path,
|
|
177
158
|
print_balances=print_balances,
|
|
178
|
-
|
|
159
|
+
print_config=print_config,
|
|
179
160
|
debug=debug,
|
|
180
|
-
|
|
161
|
+
skip_receipt=skip_receipt,
|
|
181
162
|
emulate=emulate,
|
|
182
163
|
)
|
|
183
164
|
)
|
|
@@ -196,7 +177,7 @@ def send_contract_command(
|
|
|
196
177
|
SendContractCmdParams(
|
|
197
178
|
config_path=config_path,
|
|
198
179
|
print_balances=print_balances,
|
|
199
|
-
|
|
180
|
+
print_config=print_config,
|
|
200
181
|
debug=debug,
|
|
201
182
|
no_receipt=no_receipt,
|
|
202
183
|
emulate=emulate,
|
|
@@ -211,7 +192,7 @@ def balances_command(
|
|
|
211
192
|
nonce: bool = typer.Option(False, "--nonce", "-n", help="Print nonce also"),
|
|
212
193
|
wei: bool = typer.Option(False, "--wei", "-w", help="Show balances in WEI"),
|
|
213
194
|
) -> None:
|
|
214
|
-
balances_cmd.run(BalancesCmdParams(config_path=config_path,
|
|
195
|
+
balances_cmd.run(BalancesCmdParams(config_path=config_path, print_config=print_config, wei=wei, show_nonce=nonce))
|
|
215
196
|
|
|
216
197
|
|
|
217
198
|
@app.command(name="call-contract", help="Call a method on a contract")
|
|
@@ -219,7 +200,7 @@ def call_contract_command(
|
|
|
219
200
|
config_path: Path,
|
|
220
201
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
221
202
|
) -> None:
|
|
222
|
-
call_contract_cmd.run(CallContractCmdParams(config_path=config_path,
|
|
203
|
+
call_contract_cmd.run(CallContractCmdParams(config_path=config_path, print_config=print_config))
|
|
223
204
|
|
|
224
205
|
|
|
225
206
|
@app.command(name="deploy", help="Deploy a smart contract onchain")
|
|
@@ -227,12 +208,12 @@ def deploy_command(
|
|
|
227
208
|
config_path: Path,
|
|
228
209
|
print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
|
|
229
210
|
) -> None:
|
|
230
|
-
deploy_cmd.run(DeployCmdParams(config_path=config_path,
|
|
211
|
+
deploy_cmd.run(DeployCmdParams(config_path=config_path, print_config=print_config))
|
|
231
212
|
|
|
232
213
|
|
|
233
|
-
@app.command(name="
|
|
234
|
-
def
|
|
235
|
-
|
|
214
|
+
@app.command(name="example", help="Displays an example configuration for a command")
|
|
215
|
+
def example_command(command: Annotated[ConfigExample, typer.Argument()]) -> None:
|
|
216
|
+
example_cmd.run(command)
|
|
236
217
|
|
|
237
218
|
|
|
238
219
|
@app.command(name="encode-input-data", help="Encode input data by a function signature")
|
mm_eth/cli/cli_utils.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import importlib.metadata
|
|
2
|
+
import time
|
|
2
3
|
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
3
5
|
|
|
6
|
+
from mm_crypto_utils import Nodes, Proxies
|
|
7
|
+
from mm_std import BaseConfig, print_json
|
|
4
8
|
from pydantic import BaseModel
|
|
5
9
|
|
|
10
|
+
from mm_eth import rpc
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
def get_version() -> str:
|
|
8
14
|
return importlib.metadata.version("mm-eth")
|
|
@@ -29,4 +35,26 @@ def public_rpc_url(url: str | None) -> str:
|
|
|
29
35
|
|
|
30
36
|
class BaseConfigParams(BaseModel):
|
|
31
37
|
config_path: Path
|
|
32
|
-
|
|
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"
|
mm_eth/cli/cmd/balances_cmd.py
CHANGED
|
@@ -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(
|
mm_eth/cli/cmd/deploy_cmd.py
CHANGED
|
@@ -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)
|
|
File without changes
|
|
@@ -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
|
|
@@ -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
|
|
@@ -2,25 +2,37 @@ routes = """
|
|
|
2
2
|
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
3
3
|
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
private_keys = """
|
|
6
7
|
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
7
8
|
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
8
9
|
file: /path/to/other_private_keys.txt
|
|
9
10
|
"""
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
|
|
13
|
+
|
|
12
14
|
max_fee = "1.2base_fee + 1gwei + random(1,200)" # supported var_name=base_fee
|
|
15
|
+
|
|
13
16
|
max_fee_limit = "10.1gwei - random(1, 10)" # optional
|
|
17
|
+
|
|
14
18
|
priority_fee = "1gwei + random(1, 12)"
|
|
19
|
+
|
|
15
20
|
gas = "estimate + random(100, 200) - 19" # supported var_name=estimate
|
|
16
|
-
|
|
21
|
+
|
|
22
|
+
value = "0.5balance - random(1.5t, 3t) + 11t" # supported var_name=balance. For ERC20 token use 't' suffix.
|
|
23
|
+
|
|
17
24
|
value_min_limit = "0.5t + random(1, 2) - 7"
|
|
18
25
|
|
|
19
26
|
delay = "random(1.123, 10) + 1" # secs, optional
|
|
27
|
+
|
|
20
28
|
log_debug = "/path/to/file_debug.log" # optional
|
|
29
|
+
|
|
21
30
|
log_info = "/path/to/file_info.log" # optional
|
|
31
|
+
|
|
22
32
|
round_ndigits = 6 # optional, default=5
|
|
33
|
+
|
|
23
34
|
chain_id = 421613
|
|
35
|
+
|
|
24
36
|
nodes = """
|
|
25
37
|
https://arbitrum-goerli.publicnode.com
|
|
26
38
|
https://rpc.goerli.arbitrum.gateway.fm
|
mm_eth/cli/validators.py
CHANGED
|
@@ -17,6 +17,10 @@ class Validators(ConfigValidators):
|
|
|
17
17
|
def valid_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
18
18
|
return ConfigValidators.valid_calc_int_expression(var_name, {"t": 6})
|
|
19
19
|
|
|
20
|
+
@staticmethod
|
|
21
|
+
def valid_eth_or_token_expression(var_name: str | None = None) -> Callable[[str], str]:
|
|
22
|
+
return ConfigValidators.valid_calc_int_expression(var_name, SUFFIX_DECIMALS | {"t": 6})
|
|
23
|
+
|
|
20
24
|
@staticmethod
|
|
21
25
|
def eth_routes() -> Callable[[str], list[TxRoute]]:
|
|
22
26
|
return ConfigValidators.routes(is_address, to_lower=True)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mm-eth
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
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,6 +1,6 @@
|
|
|
1
1
|
mm_eth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
mm_eth/abi.py,sha256=Qf-QOsR9QexyQM9XWKNeTMkRarIL3XQJbaDbJ8ifMrw,4856
|
|
3
|
-
mm_eth/account.py,sha256=
|
|
3
|
+
mm_eth/account.py,sha256=k0MNMatBe0zo1iKZiB_Tq6zyEIo22IncD6ewNUJp3dY,2075
|
|
4
4
|
mm_eth/anvil.py,sha256=98RCfI7dEpxFBTV6UErYvubWVP3n0ctUFn1--4kZ84U,1603
|
|
5
5
|
mm_eth/constants.py,sha256=Cy_G-IleBH4gAZ4ok8AGHHlqmdW0ZM7ZldyVpzAfWLs,54
|
|
6
6
|
mm_eth/deploy.py,sha256=SB3ruY808_5UnG8kHR34uVP66P3zOWZu0ImKD7UUv2s,691
|
|
@@ -15,35 +15,34 @@ mm_eth/tx.py,sha256=efSoMCoWkenbGdHo1_LX66_Edz1HvED5-J_i3wrHwMw,4051
|
|
|
15
15
|
mm_eth/utils.py,sha256=sSxt9GZEntZlT0RU8ht9Qon875HPhpd-1JjgqUBEfVo,7405
|
|
16
16
|
mm_eth/vault.py,sha256=h8NyiOQh5YFskh1lZA3KyvnJUnxl9769ME2ChplG0CM,1477
|
|
17
17
|
mm_eth/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
mm_eth/cli/calcs.py,sha256=
|
|
19
|
-
mm_eth/cli/cli.py,sha256=
|
|
20
|
-
mm_eth/cli/cli_utils.py,sha256=
|
|
18
|
+
mm_eth/cli/calcs.py,sha256=cLFTYNAN-I53tUiSg-zFfVr2afjIZPftDDjHj16FBz0,1068
|
|
19
|
+
mm_eth/cli/cli.py,sha256=0UYDtNrCXvJqs836tPxsQHs7yTKjvAGL3qgWiXVhnDU,9208
|
|
20
|
+
mm_eth/cli/cli_utils.py,sha256=yBIS3dGid75zyxw8crPOQHA4p3Krk5BoA2g01turKmQ,1712
|
|
21
21
|
mm_eth/cli/print_helpers.py,sha256=yOiOFjTKloumwf07AqNIHQswUo8t0yuT9bpeGBGl60Q,1470
|
|
22
22
|
mm_eth/cli/rpc_helpers.py,sha256=FMV-QVNM3v9X8H_-DP0hjNRqmm7KOnfzkw9bP17Qbz0,4499
|
|
23
|
-
mm_eth/cli/validators.py,sha256=
|
|
23
|
+
mm_eth/cli/validators.py,sha256=whnuA4CFkKbcNcbl4e2EwktTSTDGFBTA798E9F_3EM8,1682
|
|
24
24
|
mm_eth/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
mm_eth/cli/cmd/balance_cmd.py,sha256=gkwUa8VGe1wXH0GDsit3-1NmRiijojaNLygi5zNcYSY,2110
|
|
26
|
-
mm_eth/cli/cmd/balances_cmd.py,sha256=
|
|
27
|
-
mm_eth/cli/cmd/call_contract_cmd.py,sha256=
|
|
28
|
-
mm_eth/cli/cmd/
|
|
29
|
-
mm_eth/cli/cmd/deploy_cmd.py,sha256=H5p0K4p3wX9MGFHg-h__dYUw-X5ttNTEDMPCy_T5eHQ,1281
|
|
26
|
+
mm_eth/cli/cmd/balances_cmd.py,sha256=4UiWSNH9OCnkvcMGPTygEss8119do-rfG7QtsNMfWZs,4197
|
|
27
|
+
mm_eth/cli/cmd/call_contract_cmd.py,sha256=RbBPvyUEQ45hQINYDKkx1yWhPygdymVKlRL26xI31uk,1264
|
|
28
|
+
mm_eth/cli/cmd/deploy_cmd.py,sha256=0oBp_RZw_DIEtBFRc6QKdAw5oouwwpDIdeXvuyP9xdU,1272
|
|
30
29
|
mm_eth/cli/cmd/encode_input_data_cmd.py,sha256=9UQ1MKPEFQJ8j_COsP3KGKhwOf9tT3feBezI8vvxTlw,267
|
|
31
|
-
mm_eth/cli/cmd/
|
|
30
|
+
mm_eth/cli/cmd/example_cmd.py,sha256=o4NTll3fjmspbKjZ0sHGDRHTZ1RcFNHZDi9Ka0VNoDQ,264
|
|
32
31
|
mm_eth/cli/cmd/node_cmd.py,sha256=Ae5yPxxnNiHw3tZcojS7KwNLM4gEfLhsTfhZp_86rqU,1956
|
|
33
|
-
mm_eth/cli/cmd/private_key_cmd.py,sha256=Fv_2OLog1h32pIP7PJITwl_pHdy3BXvaDRcXZsxY1xo,241
|
|
34
32
|
mm_eth/cli/cmd/rpc_cmd.py,sha256=02q82YqgbPezfEBmV_QBCIeNReE7ktkPych8Xr9ann8,2186
|
|
35
|
-
mm_eth/cli/cmd/send_contract_cmd.py,sha256=
|
|
33
|
+
mm_eth/cli/cmd/send_contract_cmd.py,sha256=JX3L1Dz4CBySfa2Pf87E8grauPu0U6xIyyOus81285Y,7316
|
|
36
34
|
mm_eth/cli/cmd/solc_cmd.py,sha256=tBpeMdPfGs2iQIMaIJAAhMh1a3KyXHwyninfXPVpsgs,677
|
|
37
35
|
mm_eth/cli/cmd/token_cmd.py,sha256=4y6ZQpLOJ33_iNuKpm9tZXh4RntWhmPUcizgaNNBzaw,1102
|
|
38
|
-
mm_eth/cli/cmd/
|
|
39
|
-
mm_eth/cli/cmd/transfer_eth_cmd.py,sha256=egYx3yG29CCzKeZRzY5QmqSHQrypdHMKJ32rHdQpsyg,6980
|
|
36
|
+
mm_eth/cli/cmd/transfer_cmd.py,sha256=nC1Jiqe6zDJQvK9xVhB8mCbkEB1ZgMZmA393tDnZ1Dw,12176
|
|
40
37
|
mm_eth/cli/cmd/tx_cmd.py,sha256=PIenXYTT60Z2fqsivpzybCLI2Z_tlcz-asm3B0JLHgI,517
|
|
41
38
|
mm_eth/cli/cmd/vault_cmd.py,sha256=MOM1CILIaaqown1I-Fgo22ckqIMLtFt8t2D3fWNp798,606
|
|
42
|
-
mm_eth/cli/
|
|
43
|
-
mm_eth/cli/
|
|
44
|
-
mm_eth/cli/
|
|
45
|
-
mm_eth/cli/
|
|
46
|
-
mm_eth
|
|
47
|
-
mm_eth
|
|
48
|
-
mm_eth-0.
|
|
49
|
-
mm_eth-0.
|
|
39
|
+
mm_eth/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
+
mm_eth/cli/cmd/wallet/mnemonic_cmd.py,sha256=xE-5Ux9BdYsTZYBy0dMn9jupGhW4ced-AgYscy_wU_4,1007
|
|
41
|
+
mm_eth/cli/cmd/wallet/private_key_cmd.py,sha256=Fv_2OLog1h32pIP7PJITwl_pHdy3BXvaDRcXZsxY1xo,241
|
|
42
|
+
mm_eth/cli/examples/balances.toml,sha256=i_ALpiEcf8-0TFiUg1cgJhxxfHYeBl9x0b3tnUWjswU,421
|
|
43
|
+
mm_eth/cli/examples/call_contract.toml,sha256=ZQWK-409V_vLIZ2bsRD5RCWPPzShPz2KJTTRQY4YaGw,248
|
|
44
|
+
mm_eth/cli/examples/transfer.toml,sha256=HLVpkCNakZFE8qdFdDUoBPk7ZBdfEmk9TLZtooRsGAc,1198
|
|
45
|
+
mm_eth-0.4.1.dist-info/METADATA,sha256=aMEeaP5awNBZ0ZhRIxmhTl1jennR_TAdETFamw4F9HU,207
|
|
46
|
+
mm_eth-0.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
47
|
+
mm_eth-0.4.1.dist-info/entry_points.txt,sha256=aGhpsozl8NIrkuUcX5fSgURCcDhr3ShUdeTSIrJq4oc,46
|
|
48
|
+
mm_eth-0.4.1.dist-info/RECORD,,
|
|
@@ -1,200 +0,0 @@
|
|
|
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, Ok, fatal, utc_now
|
|
10
|
-
from pydantic import AfterValidator, BeforeValidator, model_validator
|
|
11
|
-
|
|
12
|
-
from mm_eth import erc20, rpc
|
|
13
|
-
from mm_eth.cli import cli_utils, print_helpers, rpc_helpers
|
|
14
|
-
from mm_eth.cli.calcs import calc_eth_expression
|
|
15
|
-
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
16
|
-
from mm_eth.cli.validators import Validators
|
|
17
|
-
from mm_eth.utils import from_wei_str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# noinspection DuplicatedCode
|
|
21
|
-
class Config(BaseConfig):
|
|
22
|
-
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
23
|
-
chain_id: int
|
|
24
|
-
routes: Annotated[list[TxRoute], BeforeValidator(Validators.eth_routes())]
|
|
25
|
-
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
26
|
-
token: Annotated[str, AfterValidator(Validators.eth_address())]
|
|
27
|
-
decimals: int
|
|
28
|
-
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
29
|
-
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
30
|
-
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
31
|
-
value: Annotated[str, AfterValidator(Validators.valid_token_expression("balance"))]
|
|
32
|
-
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_token_expression())] = None
|
|
33
|
-
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
34
|
-
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
35
|
-
round_ndigits: int = 5
|
|
36
|
-
log_debug: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
37
|
-
log_info: Annotated[Path | None, BeforeValidator(Validators.log_file())] = None
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def from_addresses(self) -> list[str]:
|
|
41
|
-
return [r.from_address for r in self.routes]
|
|
42
|
-
|
|
43
|
-
@model_validator(mode="after")
|
|
44
|
-
def final_validator(self) -> Self:
|
|
45
|
-
if not self.private_keys.contains_all_addresses(self.from_addresses):
|
|
46
|
-
raise ValueError("private keys are not set for all addresses")
|
|
47
|
-
|
|
48
|
-
return self
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class TransferErc20CmdParams(BaseConfigParams):
|
|
52
|
-
print_balances: bool
|
|
53
|
-
debug: bool
|
|
54
|
-
no_receipt: bool
|
|
55
|
-
emulate: bool
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# noinspection DuplicatedCode
|
|
59
|
-
def run(cli_params: TransferErc20CmdParams) -> None:
|
|
60
|
-
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
61
|
-
if cli_params.print_config_and_exit:
|
|
62
|
-
config.print_and_exit({"private_keys"})
|
|
63
|
-
|
|
64
|
-
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
65
|
-
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
66
|
-
|
|
67
|
-
# check decimals
|
|
68
|
-
res = erc20.get_decimals(config.nodes[0], config.token)
|
|
69
|
-
if isinstance(res, Err):
|
|
70
|
-
fatal(f"can't get token decimals: {res.err}")
|
|
71
|
-
if res.ok != config.decimals:
|
|
72
|
-
fatal(f"config.decimals is wrong: {config.decimals} != {res.ok}")
|
|
73
|
-
|
|
74
|
-
if cli_params.print_balances:
|
|
75
|
-
print_helpers.print_balances(
|
|
76
|
-
config.nodes,
|
|
77
|
-
config.from_addresses,
|
|
78
|
-
token_address=config.token,
|
|
79
|
-
token_decimals=config.decimals,
|
|
80
|
-
round_ndigits=config.round_ndigits,
|
|
81
|
-
)
|
|
82
|
-
sys.exit(0)
|
|
83
|
-
|
|
84
|
-
return _run_transfers(config, cli_params)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# noinspection DuplicatedCode
|
|
88
|
-
def _run_transfers(config: Config, cli_params: TransferErc20CmdParams) -> None:
|
|
89
|
-
logger.info(f"started at {utc_now()} UTC")
|
|
90
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
91
|
-
for i, route in enumerate(config.routes):
|
|
92
|
-
_transfer(route, config, cli_params)
|
|
93
|
-
if not cli_params.emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
94
|
-
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
95
|
-
logger.debug(f"delay {delay_value} seconds")
|
|
96
|
-
time.sleep(float(delay_value))
|
|
97
|
-
logger.info(f"finished at {utc_now()} UTC")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# noinspection DuplicatedCode
|
|
101
|
-
def _transfer(route: TxRoute, config: Config, cli_params: TransferErc20CmdParams) -> None:
|
|
102
|
-
log_prefix = f"{route.from_address}->{route.to_address}"
|
|
103
|
-
# get nonce
|
|
104
|
-
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, log_prefix)
|
|
105
|
-
if nonce is None:
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# get max_fee
|
|
109
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, log_prefix)
|
|
110
|
-
if max_fee is None:
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
# check max_fee_limit
|
|
114
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
# get gas
|
|
118
|
-
gas = rpc_helpers.calc_gas(
|
|
119
|
-
nodes=config.nodes,
|
|
120
|
-
gas_expression=config.gas,
|
|
121
|
-
from_address=route.from_address,
|
|
122
|
-
to_address=config.token,
|
|
123
|
-
data=erc20.encode_transfer_input_data(route.to_address, 1234),
|
|
124
|
-
log_prefix=log_prefix,
|
|
125
|
-
)
|
|
126
|
-
if gas is None:
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
# get value
|
|
130
|
-
value = rpc_helpers.calc_erc20_value_for_address(
|
|
131
|
-
nodes=config.nodes,
|
|
132
|
-
value_expression=config.value,
|
|
133
|
-
wallet_address=route.from_address,
|
|
134
|
-
token_address=config.token,
|
|
135
|
-
decimals=config.decimals,
|
|
136
|
-
log_prefix=log_prefix,
|
|
137
|
-
)
|
|
138
|
-
if value is None:
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
# value_min_limit
|
|
142
|
-
if config.value_min_limit is not None:
|
|
143
|
-
value_min_limit = mm_crypto_utils.calc_int_expression(config.value_min_limit, suffix_decimals={"t": config.decimals})
|
|
144
|
-
if value < value_min_limit:
|
|
145
|
-
value_str = from_wei_str(value, "t", config.round_ndigits, decimals=config.decimals)
|
|
146
|
-
logger.info(f"{log_prefix}value<value_min_limit, value={value_str}")
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
priority_fee = calc_eth_expression(config.priority_fee)
|
|
150
|
-
|
|
151
|
-
# emulate?
|
|
152
|
-
if cli_params.emulate:
|
|
153
|
-
msg = f"{log_prefix}: emulate,"
|
|
154
|
-
msg += f" value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)},"
|
|
155
|
-
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
156
|
-
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
157
|
-
msg += f" gas={gas}"
|
|
158
|
-
logger.info(msg)
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
debug_tx_params = {
|
|
162
|
-
"nonce": nonce,
|
|
163
|
-
"max_fee": max_fee,
|
|
164
|
-
"priority_fee": priority_fee,
|
|
165
|
-
"gas": gas,
|
|
166
|
-
"value": value,
|
|
167
|
-
"to": route.to_address,
|
|
168
|
-
"chain_id": config.chain_id,
|
|
169
|
-
}
|
|
170
|
-
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
171
|
-
signed_tx = erc20.sign_transfer_tx(
|
|
172
|
-
nonce=nonce,
|
|
173
|
-
max_fee_per_gas=max_fee,
|
|
174
|
-
max_priority_fee_per_gas=priority_fee,
|
|
175
|
-
gas_limit=gas,
|
|
176
|
-
private_key=config.private_keys[route.from_address],
|
|
177
|
-
chain_id=config.chain_id,
|
|
178
|
-
value=value,
|
|
179
|
-
token_address=config.token,
|
|
180
|
-
recipient_address=route.to_address,
|
|
181
|
-
)
|
|
182
|
-
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
183
|
-
if isinstance(res, Err):
|
|
184
|
-
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
185
|
-
return
|
|
186
|
-
tx_hash = res.ok
|
|
187
|
-
|
|
188
|
-
if cli_params.no_receipt:
|
|
189
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)}" # noqa: E501
|
|
190
|
-
logger.info(msg)
|
|
191
|
-
else:
|
|
192
|
-
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
193
|
-
while True: # TODO: infinite loop if receipt_res is err
|
|
194
|
-
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
195
|
-
if isinstance(receipt_res, Ok):
|
|
196
|
-
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
197
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)}, status={status}" # noqa: E501
|
|
198
|
-
logger.info(msg)
|
|
199
|
-
break
|
|
200
|
-
time.sleep(1)
|
|
@@ -1,182 +0,0 @@
|
|
|
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, Ok, utc_now
|
|
10
|
-
from pydantic import AfterValidator, BeforeValidator, model_validator
|
|
11
|
-
|
|
12
|
-
from mm_eth import rpc
|
|
13
|
-
from mm_eth.cli import cli_utils, print_helpers, rpc_helpers
|
|
14
|
-
from mm_eth.cli.calcs import calc_eth_expression
|
|
15
|
-
from mm_eth.cli.cli_utils import BaseConfigParams
|
|
16
|
-
from mm_eth.cli.validators import Validators as Validators
|
|
17
|
-
from mm_eth.tx import sign_tx
|
|
18
|
-
from mm_eth.utils import from_wei_str
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# noinspection DuplicatedCode
|
|
22
|
-
class Config(BaseConfig):
|
|
23
|
-
nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
|
|
24
|
-
chain_id: int
|
|
25
|
-
routes: Annotated[list[TxRoute], BeforeValidator(Validators.eth_routes())]
|
|
26
|
-
private_keys: Annotated[AddressToPrivate, BeforeValidator(Validators.eth_private_keys())]
|
|
27
|
-
max_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression("base_fee"))]
|
|
28
|
-
priority_fee: Annotated[str, AfterValidator(Validators.valid_eth_expression())]
|
|
29
|
-
max_fee_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
30
|
-
value: Annotated[str, AfterValidator(Validators.valid_eth_expression("balance"))]
|
|
31
|
-
value_min_limit: Annotated[str | None, AfterValidator(Validators.valid_eth_expression())] = None
|
|
32
|
-
gas: Annotated[str, AfterValidator(Validators.valid_eth_expression("estimate"))]
|
|
33
|
-
delay: Annotated[str | None, AfterValidator(Validators.valid_calc_decimal_value())] = None # in seconds
|
|
34
|
-
round_ndigits: int = 5
|
|
35
|
-
log_debug: Annotated[Path | None, AfterValidator(Validators.log_file())] = None
|
|
36
|
-
log_info: Annotated[Path | None, AfterValidator(Validators.log_file())] = None
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def from_addresses(self) -> list[str]:
|
|
40
|
-
return [r.from_address for r in self.routes]
|
|
41
|
-
|
|
42
|
-
@model_validator(mode="after")
|
|
43
|
-
def final_validator(self) -> Self:
|
|
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
|
-
return self
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class TransferEthCmdParams(BaseConfigParams):
|
|
51
|
-
print_balances: bool
|
|
52
|
-
debug: bool
|
|
53
|
-
no_receipt: bool
|
|
54
|
-
emulate: bool
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def run(cli_params: TransferEthCmdParams) -> None:
|
|
58
|
-
config = Config.read_toml_config_or_exit(cli_params.config_path)
|
|
59
|
-
if cli_params.print_config_and_exit:
|
|
60
|
-
config.print_and_exit({"private_keys"})
|
|
61
|
-
|
|
62
|
-
mm_crypto_utils.init_logger(cli_params.debug, config.log_debug, config.log_info)
|
|
63
|
-
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
64
|
-
|
|
65
|
-
if cli_params.print_balances:
|
|
66
|
-
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
67
|
-
sys.exit(0)
|
|
68
|
-
|
|
69
|
-
return _run_transfers(config, cli_params)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# noinspection DuplicatedCode
|
|
73
|
-
def _run_transfers(config: Config, cli_params: TransferEthCmdParams) -> None:
|
|
74
|
-
logger.info(f"started at {utc_now()} UTC")
|
|
75
|
-
logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
|
|
76
|
-
for i, route in enumerate(config.routes):
|
|
77
|
-
_transfer(route, config, cli_params)
|
|
78
|
-
if not cli_params.emulate and config.delay is not None and i < len(config.routes) - 1:
|
|
79
|
-
delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
|
|
80
|
-
logger.debug(f"delay {delay_value} seconds")
|
|
81
|
-
time.sleep(float(delay_value))
|
|
82
|
-
logger.info(f"finished at {utc_now()} UTC")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# noinspection DuplicatedCode
|
|
86
|
-
def _transfer(route: TxRoute, config: Config, cli_params: TransferEthCmdParams) -> None:
|
|
87
|
-
log_prefix = f"{route.from_address}->{route.to_address}"
|
|
88
|
-
# get nonce
|
|
89
|
-
nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, log_prefix)
|
|
90
|
-
if nonce is None:
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
# get max_fee
|
|
94
|
-
max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, log_prefix)
|
|
95
|
-
if max_fee is None:
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
# check max_fee_limit
|
|
99
|
-
if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, log_prefix):
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
# get gas
|
|
103
|
-
gas = rpc_helpers.calc_gas(
|
|
104
|
-
nodes=config.nodes,
|
|
105
|
-
gas_expression=config.gas,
|
|
106
|
-
from_address=route.from_address,
|
|
107
|
-
to_address=route.to_address,
|
|
108
|
-
value=123,
|
|
109
|
-
log_prefix=log_prefix,
|
|
110
|
-
)
|
|
111
|
-
if gas is None:
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
# get value
|
|
115
|
-
value = rpc_helpers.calc_eth_value_for_address(
|
|
116
|
-
nodes=config.nodes,
|
|
117
|
-
value_expression=config.value,
|
|
118
|
-
address=route.from_address,
|
|
119
|
-
gas=gas,
|
|
120
|
-
max_fee=max_fee,
|
|
121
|
-
log_prefix=log_prefix,
|
|
122
|
-
)
|
|
123
|
-
if value is None:
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
# value_min_limit
|
|
127
|
-
if config.value_min_limit is not None:
|
|
128
|
-
value_min_limit = calc_eth_expression(config.value_min_limit)
|
|
129
|
-
if value < value_min_limit:
|
|
130
|
-
logger.info(f"{log_prefix}value<value_min_limit, value={from_wei_str(value, 'eth', config.round_ndigits)}")
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
priority_fee = calc_eth_expression(config.priority_fee)
|
|
134
|
-
|
|
135
|
-
# emulate?
|
|
136
|
-
if cli_params.emulate:
|
|
137
|
-
msg = f"{log_prefix}: emulate, value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
138
|
-
msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
|
|
139
|
-
msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
|
|
140
|
-
msg += f" gas={gas}"
|
|
141
|
-
logger.info(msg)
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
debug_tx_params = {
|
|
145
|
-
"nonce": nonce,
|
|
146
|
-
"max_fee": max_fee,
|
|
147
|
-
"priority_fee": priority_fee,
|
|
148
|
-
"gas": gas,
|
|
149
|
-
"value": value,
|
|
150
|
-
"to": route.to_address,
|
|
151
|
-
"chain_id": config.chain_id,
|
|
152
|
-
}
|
|
153
|
-
logger.debug(f"{log_prefix}: tx_params={debug_tx_params}")
|
|
154
|
-
signed_tx = sign_tx(
|
|
155
|
-
nonce=nonce,
|
|
156
|
-
max_fee_per_gas=max_fee,
|
|
157
|
-
max_priority_fee_per_gas=priority_fee,
|
|
158
|
-
gas=gas,
|
|
159
|
-
private_key=config.private_keys[route.from_address],
|
|
160
|
-
chain_id=config.chain_id,
|
|
161
|
-
value=value,
|
|
162
|
-
to=route.to_address,
|
|
163
|
-
)
|
|
164
|
-
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
165
|
-
if isinstance(res, Err):
|
|
166
|
-
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
167
|
-
return
|
|
168
|
-
tx_hash = res.ok
|
|
169
|
-
|
|
170
|
-
if cli_params.no_receipt:
|
|
171
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 'ether', round_ndigits=config.round_ndigits)}"
|
|
172
|
-
logger.info(msg)
|
|
173
|
-
else:
|
|
174
|
-
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
175
|
-
while True: # TODO: infinite loop if receipt_res is err
|
|
176
|
-
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
177
|
-
if isinstance(receipt_res, Ok):
|
|
178
|
-
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
179
|
-
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 'ether', round_ndigits=config.round_ndigits)}, status={status}" # noqa: E501
|
|
180
|
-
logger.info(msg)
|
|
181
|
-
break
|
|
182
|
-
time.sleep(1)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
routes = """
|
|
2
|
-
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
3
|
-
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
4
|
-
file: /path/from_addresses.txt /path/to_addresses.txt
|
|
5
|
-
"""
|
|
6
|
-
private_keys = """
|
|
7
|
-
# 0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
8
|
-
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
9
|
-
file: /path/to/private_keys.txt
|
|
10
|
-
"""
|
|
11
|
-
max_fee = "1.2base + 1gwei + random(1, 200)" # supported var_name=base
|
|
12
|
-
max_fee_limit = "10.1gwei - random(1, 10)" # optional
|
|
13
|
-
priority_fee = "1gwei + random(1, 12)"
|
|
14
|
-
gas = "estimate + random(100, 200) - 19" # supported var_name=estimate
|
|
15
|
-
value = "balance - random(0.002eth, 0.0025eth) + 11gwei" # supported var_name=balance. If 'balance' is used, value=calc(value) - gas*max_fee_per_gas
|
|
16
|
-
value_min_limit = "0.001eth + random(1, 2) - 7gwei"
|
|
17
|
-
|
|
18
|
-
delay = "random(1.123,10) + 1" # secs
|
|
19
|
-
log_debug = "/path/to/file_debug.log" # optional
|
|
20
|
-
log_info = "/path/to/file_info.log" # optional
|
|
21
|
-
round_ndigits = 6
|
|
22
|
-
chain_id = 421613
|
|
23
|
-
nodes = """
|
|
24
|
-
https://arbitrum-goerli.publicnode.com
|
|
25
|
-
https://rpc.goerli.arbitrum.gateway.fm
|
|
26
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|