mm-eth 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mm_eth/abi.py +3 -3
- mm_eth/account.py +1 -1
- mm_eth/anvil.py +2 -2
- mm_eth/cli/__init__.py +0 -0
- mm_eth/cli/calcs.py +112 -0
- mm_eth/cli/cli.py +237 -0
- mm_eth/cli/cli_utils.py +105 -0
- mm_eth/cli/cmd/__init__.py +0 -0
- mm_eth/cli/cmd/balance_cmd.py +51 -0
- mm_eth/cli/cmd/balances_cmd.py +124 -0
- mm_eth/cli/cmd/call_contract_cmd.py +43 -0
- mm_eth/cli/cmd/config_example_cmd.py +9 -0
- mm_eth/cli/cmd/deploy_cmd.py +43 -0
- mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
- mm_eth/cli/cmd/mnemonic_cmd.py +26 -0
- mm_eth/cli/cmd/node_cmd.py +47 -0
- mm_eth/cli/cmd/private_key_cmd.py +10 -0
- mm_eth/cli/cmd/rpc_cmd.py +78 -0
- mm_eth/cli/cmd/send_contract_cmd.py +251 -0
- mm_eth/cli/cmd/solc_cmd.py +24 -0
- mm_eth/cli/cmd/token_cmd.py +29 -0
- mm_eth/cli/cmd/transfer_erc20_cmd.py +277 -0
- mm_eth/cli/cmd/transfer_eth_cmd.py +254 -0
- mm_eth/cli/cmd/vault_cmd.py +16 -0
- mm_eth/cli/config_examples/balances.yml +15 -0
- mm_eth/cli/config_examples/call_contract.yml +5 -0
- mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
- mm_eth/cli/config_examples/transfer_eth.yml +24 -0
- mm_eth/cli/print_helpers.py +37 -0
- mm_eth/cli/rpc_helpers.py +140 -0
- mm_eth/cli/validators.py +85 -0
- mm_eth/erc20.py +8 -7
- mm_eth/rpc.py +8 -8
- mm_eth/solc.py +2 -3
- mm_eth/tx.py +3 -5
- mm_eth/utils.py +11 -16
- mm_eth/vault.py +5 -5
- mm_eth-0.2.2.dist-info/METADATA +9 -0
- mm_eth-0.2.2.dist-info/RECORD +47 -0
- mm_eth-0.2.2.dist-info/entry_points.txt +2 -0
- mm_eth-0.2.0.dist-info/METADATA +0 -7
- mm_eth-0.2.0.dist-info/RECORD +0 -18
- {mm_eth-0.2.0.dist-info → mm_eth-0.2.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from mm_std import BaseConfig, Err, Ok, fatal, print_json, str_to_list, utc_now
|
|
8
|
+
from pydantic import Field, StrictStr, field_validator, model_validator
|
|
9
|
+
|
|
10
|
+
from mm_eth import erc20, rpc
|
|
11
|
+
from mm_eth.account import create_private_keys_dict, private_to_address
|
|
12
|
+
from mm_eth.cli import calcs, cli_utils, print_helpers, rpc_helpers, validators
|
|
13
|
+
from mm_eth.utils import from_wei_str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Config(BaseConfig):
|
|
17
|
+
class Tx(BaseConfig):
|
|
18
|
+
from_address: str
|
|
19
|
+
to_address: str
|
|
20
|
+
|
|
21
|
+
token: str
|
|
22
|
+
decimals: int
|
|
23
|
+
nodes: list[StrictStr]
|
|
24
|
+
chain_id: int
|
|
25
|
+
private_keys: dict[str, str] = Field(default_factory=dict)
|
|
26
|
+
private_keys_file: str | None = None
|
|
27
|
+
max_fee_per_gas: str
|
|
28
|
+
max_fee_per_gas_limit: str | None = None
|
|
29
|
+
max_priority_fee_per_gas: str
|
|
30
|
+
value: str
|
|
31
|
+
value_min_limit: str | None = None
|
|
32
|
+
gas: str
|
|
33
|
+
addresses_map: str | None = None
|
|
34
|
+
addresses_from_file: str | None = None
|
|
35
|
+
addresses_to_file: str | None = None
|
|
36
|
+
delay: str | None = None # in seconds
|
|
37
|
+
round_ndigits: int = 5
|
|
38
|
+
log_debug: str | None = None
|
|
39
|
+
log_info: str | None = None
|
|
40
|
+
txs: list[Tx] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def from_addresses(self) -> list[str]:
|
|
44
|
+
return [tx.from_address for tx in self.txs]
|
|
45
|
+
|
|
46
|
+
@field_validator("log_debug", "log_info", mode="before")
|
|
47
|
+
@classmethod
|
|
48
|
+
def log_validator(cls, v: str | None) -> str | None:
|
|
49
|
+
return validators.log_validator(v)
|
|
50
|
+
|
|
51
|
+
@field_validator("nodes", mode="before")
|
|
52
|
+
@classmethod
|
|
53
|
+
def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
54
|
+
return validators.nodes_validator(v)
|
|
55
|
+
|
|
56
|
+
@field_validator("private_keys", mode="before")
|
|
57
|
+
@classmethod
|
|
58
|
+
def private_keys_validator(cls, v: str | list[str] | None) -> dict[str, str]:
|
|
59
|
+
if v is None:
|
|
60
|
+
return {}
|
|
61
|
+
if isinstance(v, str):
|
|
62
|
+
return create_private_keys_dict(str_to_list(v, unique=True, remove_comments=True))
|
|
63
|
+
return create_private_keys_dict(v)
|
|
64
|
+
|
|
65
|
+
# noinspection DuplicatedCode
|
|
66
|
+
@model_validator(mode="after")
|
|
67
|
+
def final_validator(self) -> Self:
|
|
68
|
+
# load private keys from file
|
|
69
|
+
if self.private_keys_file:
|
|
70
|
+
file = Path(self.private_keys_file).expanduser()
|
|
71
|
+
if not file.is_file():
|
|
72
|
+
raise ValueError("can't read private_keys_file")
|
|
73
|
+
for line in file.read_text().strip().split("\n"):
|
|
74
|
+
line = line.strip() # noqa: PLW2901
|
|
75
|
+
address = private_to_address(line)
|
|
76
|
+
if address is None:
|
|
77
|
+
raise ValueError("there is not a private key in private_keys_file")
|
|
78
|
+
self.private_keys[address.lower()] = line
|
|
79
|
+
|
|
80
|
+
# max_fee_per_gas
|
|
81
|
+
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas, "base"):
|
|
82
|
+
raise ValueError(f"wrong max_fee_per_gas: {self.max_fee_per_gas}")
|
|
83
|
+
|
|
84
|
+
# max_fee_per_gas_limit
|
|
85
|
+
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas_limit, "base"):
|
|
86
|
+
raise ValueError(f"wrong max_fee_per_gas_limit: {self.max_fee_per_gas_limit}")
|
|
87
|
+
|
|
88
|
+
# max_priority_fee_per_gas
|
|
89
|
+
if not validators.is_valid_calc_var_wei_value(self.max_priority_fee_per_gas):
|
|
90
|
+
raise ValueError(f"wrong max_priority_fee_per_gas: {self.max_priority_fee_per_gas}")
|
|
91
|
+
|
|
92
|
+
# value
|
|
93
|
+
if not validators.is_valid_calc_var_wei_value(self.value, "balance", decimals=self.decimals):
|
|
94
|
+
raise ValueError(f"wrong value: {self.value}")
|
|
95
|
+
|
|
96
|
+
# value_min_limit
|
|
97
|
+
if not validators.is_valid_calc_var_wei_value(self.value_min_limit, decimals=self.decimals):
|
|
98
|
+
raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
|
|
99
|
+
|
|
100
|
+
# gas
|
|
101
|
+
if not validators.is_valid_calc_var_wei_value(self.gas, "estimate"):
|
|
102
|
+
raise ValueError(f"wrong gas: {self.gas}")
|
|
103
|
+
|
|
104
|
+
# delay
|
|
105
|
+
if not validators.is_valid_calc_decimal_value(self.delay):
|
|
106
|
+
raise ValueError(f"wrong delay: {self.delay}")
|
|
107
|
+
|
|
108
|
+
# txs
|
|
109
|
+
if self.addresses_map:
|
|
110
|
+
for tx in cli_utils.load_tx_addresses_from_str(self.addresses_map):
|
|
111
|
+
self.txs.append(Config.Tx(from_address=tx[0], to_address=tx[1]))
|
|
112
|
+
if self.addresses_from_file and self.addresses_to_file:
|
|
113
|
+
self.txs.extend(
|
|
114
|
+
Config.Tx(from_address=tx[0], to_address=tx[1])
|
|
115
|
+
for tx in cli_utils.load_tx_addresses_from_files(self.addresses_from_file, self.addresses_to_file)
|
|
116
|
+
)
|
|
117
|
+
if not self.txs:
|
|
118
|
+
raise ValueError("txs is empty")
|
|
119
|
+
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# noinspection DuplicatedCode
|
|
124
|
+
def run(
|
|
125
|
+
config_path: str,
|
|
126
|
+
*,
|
|
127
|
+
print_balances: bool,
|
|
128
|
+
print_config: bool,
|
|
129
|
+
debug: bool,
|
|
130
|
+
no_receipt: bool,
|
|
131
|
+
emulate: bool,
|
|
132
|
+
) -> None:
|
|
133
|
+
config = cli_utils.read_config(Config, Path(config_path))
|
|
134
|
+
if print_config:
|
|
135
|
+
print_json(config.model_dump(exclude={"private_key", "addresses_map"}))
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
cli_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
139
|
+
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
140
|
+
|
|
141
|
+
# check decimals
|
|
142
|
+
res = erc20.get_decimals(config.nodes[0], config.token)
|
|
143
|
+
if isinstance(res, Err):
|
|
144
|
+
fatal(f"can't get token decimals: {res.err}")
|
|
145
|
+
if res.ok != config.decimals:
|
|
146
|
+
fatal(f"config.decimals is wrong: {config.decimals} != {res.ok}")
|
|
147
|
+
|
|
148
|
+
if print_balances:
|
|
149
|
+
print_helpers.print_balances(
|
|
150
|
+
config.nodes,
|
|
151
|
+
config.from_addresses,
|
|
152
|
+
token_address=config.token,
|
|
153
|
+
token_decimals=config.decimals,
|
|
154
|
+
round_ndigits=config.round_ndigits,
|
|
155
|
+
)
|
|
156
|
+
sys.exit(0)
|
|
157
|
+
|
|
158
|
+
return _run_transfers(config, no_receipt=no_receipt, emulate=emulate)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# noinspection DuplicatedCode
|
|
162
|
+
def _run_transfers(config: Config, *, no_receipt: bool, emulate: bool) -> None:
|
|
163
|
+
logger.info(f"started at {utc_now()} UTC")
|
|
164
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys', 'addresses_map'}) | {'version': cli_utils.get_version()}}")
|
|
165
|
+
cli_utils.check_private_keys(config.from_addresses, config.private_keys)
|
|
166
|
+
for i, tx in enumerate(config.txs):
|
|
167
|
+
_transfer(from_address=tx.from_address, to_address=tx.to_address, config=config, no_receipt=no_receipt, emulate=emulate)
|
|
168
|
+
if not emulate and config.delay is not None and i < len(config.txs) - 1:
|
|
169
|
+
delay_value = calcs.calc_decimal_value(config.delay)
|
|
170
|
+
logger.debug(f"delay {delay_value} seconds")
|
|
171
|
+
time.sleep(float(delay_value))
|
|
172
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# noinspection DuplicatedCode
|
|
176
|
+
def _transfer(*, from_address: str, to_address: str, config: Config, no_receipt: bool, emulate: bool) -> None:
|
|
177
|
+
log_prefix = f"{from_address}->{to_address}"
|
|
178
|
+
# get nonce
|
|
179
|
+
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
180
|
+
if nonce is None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# get max_fee_per_gas
|
|
184
|
+
max_fee_per_gas = rpc_helpers.calc_max_fee_per_gas(config.nodes, config.max_fee_per_gas, log_prefix)
|
|
185
|
+
if max_fee_per_gas is None:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# check max_fee_per_gas_limit
|
|
189
|
+
if rpc_helpers.is_max_fee_per_gas_limit_exceeded(max_fee_per_gas, config.max_fee_per_gas_limit, log_prefix):
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# get gas
|
|
193
|
+
gas = rpc_helpers.calc_gas(
|
|
194
|
+
nodes=config.nodes,
|
|
195
|
+
gas=config.gas,
|
|
196
|
+
from_address=from_address,
|
|
197
|
+
to_address=config.token,
|
|
198
|
+
data=erc20.encode_transfer_input_data(to_address, 1234),
|
|
199
|
+
log_prefix=log_prefix,
|
|
200
|
+
)
|
|
201
|
+
if gas is None:
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# get value
|
|
205
|
+
value = rpc_helpers.calc_erc20_value(
|
|
206
|
+
nodes=config.nodes,
|
|
207
|
+
value_str=config.value,
|
|
208
|
+
wallet_address=from_address,
|
|
209
|
+
token_address=config.token,
|
|
210
|
+
decimals=config.decimals,
|
|
211
|
+
log_prefix=log_prefix,
|
|
212
|
+
)
|
|
213
|
+
if value is None:
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
# value_min_limit
|
|
217
|
+
if calcs.is_value_less_min_limit(
|
|
218
|
+
config.value_min_limit,
|
|
219
|
+
value,
|
|
220
|
+
"t",
|
|
221
|
+
decimals=config.decimals,
|
|
222
|
+
log_prefix=log_prefix,
|
|
223
|
+
):
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
max_priority_fee_per_gas = calcs.calc_var_wei_value(config.max_priority_fee_per_gas)
|
|
227
|
+
tx_params = {
|
|
228
|
+
"nonce": nonce,
|
|
229
|
+
"max_fee_per_gas": max_fee_per_gas,
|
|
230
|
+
"max_priority_fee_per_gas": max_priority_fee_per_gas,
|
|
231
|
+
"gas": gas,
|
|
232
|
+
"value": value,
|
|
233
|
+
"to": to_address,
|
|
234
|
+
"chain_id": config.chain_id,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# emulate?
|
|
238
|
+
if emulate:
|
|
239
|
+
msg = f"{log_prefix}: emulate,"
|
|
240
|
+
msg += f" value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)},"
|
|
241
|
+
msg += f" max_fee_per_gas={from_wei_str(max_fee_per_gas, 'gwei', config.round_ndigits)},"
|
|
242
|
+
msg += f" max_priority_fee_per_gas={from_wei_str(max_priority_fee_per_gas, 'gwei', config.round_ndigits)},"
|
|
243
|
+
msg += f" gas={gas}"
|
|
244
|
+
logger.info(msg)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
logger.debug(f"{log_prefix}: tx_params={tx_params}")
|
|
248
|
+
signed_tx = erc20.sign_transfer_tx(
|
|
249
|
+
nonce=nonce,
|
|
250
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
251
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
252
|
+
gas_limit=gas,
|
|
253
|
+
private_key=config.private_keys[from_address],
|
|
254
|
+
chain_id=config.chain_id,
|
|
255
|
+
value=value,
|
|
256
|
+
token_address=config.token,
|
|
257
|
+
recipient_address=to_address,
|
|
258
|
+
)
|
|
259
|
+
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
260
|
+
if isinstance(res, Err):
|
|
261
|
+
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
262
|
+
return
|
|
263
|
+
tx_hash = res.ok
|
|
264
|
+
|
|
265
|
+
if no_receipt:
|
|
266
|
+
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 't', decimals=config.decimals, round_ndigits=config.round_ndigits)}" # noqa: E501
|
|
267
|
+
logger.info(msg)
|
|
268
|
+
else:
|
|
269
|
+
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
270
|
+
while True:
|
|
271
|
+
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
272
|
+
if isinstance(receipt_res, Ok):
|
|
273
|
+
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
274
|
+
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
|
|
275
|
+
logger.info(msg)
|
|
276
|
+
break
|
|
277
|
+
time.sleep(1)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from mm_std import BaseConfig, Err, Ok, print_json, str_to_list, utc_now
|
|
8
|
+
from pydantic import Field, StrictStr, field_validator, model_validator
|
|
9
|
+
|
|
10
|
+
from mm_eth import rpc
|
|
11
|
+
from mm_eth.account import create_private_keys_dict, private_to_address
|
|
12
|
+
from mm_eth.cli import calcs, cli_utils, print_helpers, rpc_helpers, validators
|
|
13
|
+
from mm_eth.tx import sign_tx
|
|
14
|
+
from mm_eth.utils import from_wei_str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Config(BaseConfig):
|
|
18
|
+
class Tx(BaseConfig):
|
|
19
|
+
from_address: str
|
|
20
|
+
to_address: str
|
|
21
|
+
|
|
22
|
+
nodes: list[StrictStr]
|
|
23
|
+
chain_id: int
|
|
24
|
+
private_keys: dict[str, str] = Field(default_factory=dict)
|
|
25
|
+
private_keys_file: str | None = None
|
|
26
|
+
max_fee_per_gas: str
|
|
27
|
+
max_fee_per_gas_limit: str | None = None
|
|
28
|
+
max_priority_fee_per_gas: str
|
|
29
|
+
value: str
|
|
30
|
+
value_min_limit: str | None = None
|
|
31
|
+
gas: str
|
|
32
|
+
addresses_map: str | None = None
|
|
33
|
+
addresses_from_file: str | None = None
|
|
34
|
+
addresses_to_file: str | None = None
|
|
35
|
+
delay: str | None = None # in seconds
|
|
36
|
+
round_ndigits: int = 5
|
|
37
|
+
log_debug: str | None = None
|
|
38
|
+
log_info: str | None = None
|
|
39
|
+
txs: list[Tx] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def from_addresses(self) -> list[str]:
|
|
43
|
+
return [tx.from_address for tx in self.txs]
|
|
44
|
+
|
|
45
|
+
@field_validator("log_debug", "log_info", mode="before")
|
|
46
|
+
@classmethod
|
|
47
|
+
def log_validator(cls, v: str | None) -> str | None:
|
|
48
|
+
return validators.log_validator(v)
|
|
49
|
+
|
|
50
|
+
@field_validator("nodes", mode="before")
|
|
51
|
+
@classmethod
|
|
52
|
+
def nodes_validator(cls, v: str | list[str] | None) -> list[str]:
|
|
53
|
+
return validators.nodes_validator(v)
|
|
54
|
+
|
|
55
|
+
@field_validator("private_keys", mode="before")
|
|
56
|
+
@classmethod
|
|
57
|
+
def private_keys_validator(cls, v: str | list[str] | None) -> dict[str, str]:
|
|
58
|
+
if v is None:
|
|
59
|
+
return {}
|
|
60
|
+
if isinstance(v, str):
|
|
61
|
+
return create_private_keys_dict(str_to_list(v, unique=True, remove_comments=True))
|
|
62
|
+
return create_private_keys_dict(v)
|
|
63
|
+
|
|
64
|
+
# noinspection DuplicatedCode
|
|
65
|
+
@model_validator(mode="after")
|
|
66
|
+
def final_validator(self) -> Self:
|
|
67
|
+
# load private keys from file
|
|
68
|
+
if self.private_keys_file:
|
|
69
|
+
file = Path(self.private_keys_file).expanduser()
|
|
70
|
+
if not file.is_file():
|
|
71
|
+
raise ValueError("can't read private_keys_file")
|
|
72
|
+
for line in file.read_text().strip().split("\n"):
|
|
73
|
+
line = line.strip() # noqa: PLW2901
|
|
74
|
+
address = private_to_address(line)
|
|
75
|
+
if address is None:
|
|
76
|
+
raise ValueError("there is not a private key in private_keys_file")
|
|
77
|
+
self.private_keys[address.lower()] = line
|
|
78
|
+
|
|
79
|
+
# max_fee_per_gas
|
|
80
|
+
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas, "base"):
|
|
81
|
+
raise ValueError(f"wrong max_fee_per_gas: {self.max_fee_per_gas}")
|
|
82
|
+
|
|
83
|
+
# max_fee_per_gas_limit
|
|
84
|
+
if not validators.is_valid_calc_var_wei_value(self.max_fee_per_gas_limit, "base"):
|
|
85
|
+
raise ValueError(f"wrong max_fee_per_gas_limit: {self.max_fee_per_gas_limit}")
|
|
86
|
+
|
|
87
|
+
# max_priority_fee_per_gas
|
|
88
|
+
if not validators.is_valid_calc_var_wei_value(self.max_priority_fee_per_gas):
|
|
89
|
+
raise ValueError(f"wrong max_priority_fee_per_gas: {self.max_priority_fee_per_gas}")
|
|
90
|
+
|
|
91
|
+
# value
|
|
92
|
+
if not validators.is_valid_calc_var_wei_value(self.value, "balance"):
|
|
93
|
+
raise ValueError(f"wrong value: {self.value}")
|
|
94
|
+
|
|
95
|
+
# value_min_limit
|
|
96
|
+
if not validators.is_valid_calc_var_wei_value(self.value_min_limit):
|
|
97
|
+
raise ValueError(f"wrong value_min_limit: {self.value_min_limit}")
|
|
98
|
+
|
|
99
|
+
# gas
|
|
100
|
+
if not validators.is_valid_calc_var_wei_value(self.gas, "estimate"):
|
|
101
|
+
raise ValueError(f"wrong gas: {self.gas}")
|
|
102
|
+
|
|
103
|
+
# delay
|
|
104
|
+
if not validators.is_valid_calc_decimal_value(self.delay):
|
|
105
|
+
raise ValueError(f"wrong delay: {self.delay}")
|
|
106
|
+
|
|
107
|
+
# txs
|
|
108
|
+
if self.addresses_map:
|
|
109
|
+
for tx in cli_utils.load_tx_addresses_from_str(self.addresses_map):
|
|
110
|
+
self.txs.append(Config.Tx(from_address=tx[0], to_address=tx[1]))
|
|
111
|
+
if self.addresses_from_file and self.addresses_to_file:
|
|
112
|
+
self.txs.extend(
|
|
113
|
+
Config.Tx(from_address=tx[0], to_address=tx[1])
|
|
114
|
+
for tx in cli_utils.load_tx_addresses_from_files(self.addresses_from_file, self.addresses_to_file)
|
|
115
|
+
)
|
|
116
|
+
if not self.txs:
|
|
117
|
+
raise ValueError("txs is empty")
|
|
118
|
+
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def run(
|
|
123
|
+
config_path: str,
|
|
124
|
+
*,
|
|
125
|
+
print_balances: bool,
|
|
126
|
+
print_config: bool,
|
|
127
|
+
debug: bool,
|
|
128
|
+
no_receipt: bool,
|
|
129
|
+
emulate: bool,
|
|
130
|
+
) -> None:
|
|
131
|
+
config = cli_utils.read_config(Config, Path(config_path))
|
|
132
|
+
if print_config:
|
|
133
|
+
print_json(config.model_dump(exclude={"private_key", "addresses_map"}))
|
|
134
|
+
sys.exit(0)
|
|
135
|
+
|
|
136
|
+
cli_utils.init_logger(debug, config.log_debug, config.log_info)
|
|
137
|
+
rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
|
|
138
|
+
|
|
139
|
+
if print_balances:
|
|
140
|
+
print_helpers.print_balances(config.nodes, config.from_addresses, round_ndigits=config.round_ndigits)
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
|
|
143
|
+
return _run_transfers(config, no_receipt=no_receipt, emulate=emulate)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# noinspection DuplicatedCode
|
|
147
|
+
def _run_transfers(config: Config, *, no_receipt: bool, emulate: bool) -> None:
|
|
148
|
+
logger.info(f"started at {utc_now()} UTC")
|
|
149
|
+
logger.debug(f"config={config.model_dump(exclude={'private_keys', 'addresses_map'}) | {'version': cli_utils.get_version()}}")
|
|
150
|
+
cli_utils.check_private_keys(config.from_addresses, config.private_keys)
|
|
151
|
+
for i, tx in enumerate(config.txs):
|
|
152
|
+
_transfer(from_address=tx.from_address, to_address=tx.to_address, config=config, no_receipt=no_receipt, emulate=emulate)
|
|
153
|
+
if not emulate and config.delay is not None and i < len(config.txs) - 1:
|
|
154
|
+
delay_value = calcs.calc_decimal_value(config.delay)
|
|
155
|
+
logger.debug(f"delay {delay_value} seconds")
|
|
156
|
+
time.sleep(float(delay_value))
|
|
157
|
+
logger.info(f"finished at {utc_now()} UTC")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# noinspection DuplicatedCode
|
|
161
|
+
def _transfer(*, from_address: str, to_address: str, config: Config, no_receipt: bool, emulate: bool) -> None:
|
|
162
|
+
log_prefix = f"{from_address}->{to_address}"
|
|
163
|
+
# get nonce
|
|
164
|
+
nonce = rpc_helpers.get_nonce(config.nodes, from_address, log_prefix)
|
|
165
|
+
if nonce is None:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# get max_fee_per_gas
|
|
169
|
+
max_fee_per_gas = rpc_helpers.calc_max_fee_per_gas(config.nodes, config.max_fee_per_gas, log_prefix)
|
|
170
|
+
if max_fee_per_gas is None:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# check max_fee_per_gas_limit
|
|
174
|
+
if rpc_helpers.is_max_fee_per_gas_limit_exceeded(max_fee_per_gas, config.max_fee_per_gas_limit, log_prefix):
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# get gas
|
|
178
|
+
gas = rpc_helpers.calc_gas(
|
|
179
|
+
nodes=config.nodes,
|
|
180
|
+
gas=config.gas,
|
|
181
|
+
from_address=from_address,
|
|
182
|
+
to_address=to_address,
|
|
183
|
+
value=123,
|
|
184
|
+
log_prefix=log_prefix,
|
|
185
|
+
)
|
|
186
|
+
if gas is None:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# get value
|
|
190
|
+
value = rpc_helpers.calc_eth_value(
|
|
191
|
+
nodes=config.nodes,
|
|
192
|
+
value_str=config.value,
|
|
193
|
+
address=from_address,
|
|
194
|
+
gas=gas,
|
|
195
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
196
|
+
log_prefix=log_prefix,
|
|
197
|
+
)
|
|
198
|
+
if value is None:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
# value_min_limit
|
|
202
|
+
if calcs.is_value_less_min_limit(config.value_min_limit, value, "eth", log_prefix=log_prefix):
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
max_priority_fee_per_gas = calcs.calc_var_wei_value(config.max_priority_fee_per_gas)
|
|
206
|
+
tx_params = {
|
|
207
|
+
"nonce": nonce,
|
|
208
|
+
"max_fee_per_gas": max_fee_per_gas,
|
|
209
|
+
"max_priority_fee_per_gas": max_priority_fee_per_gas,
|
|
210
|
+
"gas": gas,
|
|
211
|
+
"value": value,
|
|
212
|
+
"to": to_address,
|
|
213
|
+
"chain_id": config.chain_id,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# emulate?
|
|
217
|
+
if emulate:
|
|
218
|
+
msg = f"{log_prefix}: emulate, value={from_wei_str(value, 'eth', config.round_ndigits)},"
|
|
219
|
+
msg += f" max_fee_per_gas={from_wei_str(max_fee_per_gas, 'gwei', config.round_ndigits)},"
|
|
220
|
+
msg += f" max_priority_fee_per_gas={from_wei_str(max_priority_fee_per_gas, 'gwei', config.round_ndigits)},"
|
|
221
|
+
msg += f" gas={gas}"
|
|
222
|
+
logger.info(msg)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
logger.debug(f"{log_prefix}: tx_params={tx_params}")
|
|
226
|
+
signed_tx = sign_tx(
|
|
227
|
+
nonce=nonce,
|
|
228
|
+
max_fee_per_gas=max_fee_per_gas,
|
|
229
|
+
max_priority_fee_per_gas=max_priority_fee_per_gas,
|
|
230
|
+
gas=gas,
|
|
231
|
+
private_key=config.private_keys[from_address],
|
|
232
|
+
chain_id=config.chain_id,
|
|
233
|
+
value=value,
|
|
234
|
+
to=to_address,
|
|
235
|
+
)
|
|
236
|
+
res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
|
|
237
|
+
if isinstance(res, Err):
|
|
238
|
+
logger.info(f"{log_prefix}: send_error: {res.err}")
|
|
239
|
+
return
|
|
240
|
+
tx_hash = res.ok
|
|
241
|
+
|
|
242
|
+
if no_receipt:
|
|
243
|
+
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 'ether', round_ndigits=config.round_ndigits)}"
|
|
244
|
+
logger.info(msg)
|
|
245
|
+
else:
|
|
246
|
+
logger.debug(f"{log_prefix}: tx_hash={tx_hash}, wait receipt")
|
|
247
|
+
while True:
|
|
248
|
+
receipt_res = rpc.get_tx_status(config.nodes, tx_hash)
|
|
249
|
+
if isinstance(receipt_res, Ok):
|
|
250
|
+
status = "OK" if receipt_res.ok == 1 else "FAIL"
|
|
251
|
+
msg = f"{log_prefix}: tx_hash={tx_hash}, value={from_wei_str(value, 'ether', round_ndigits=config.round_ndigits)}, status={status}" # noqa: E501
|
|
252
|
+
logger.info(msg)
|
|
253
|
+
break
|
|
254
|
+
time.sleep(1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from mm_std import fatal, print_plain
|
|
2
|
+
|
|
3
|
+
from mm_eth import vault
|
|
4
|
+
from mm_eth.cli import cli_utils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run(keys_url: str, vault_token: str, keys_file: str) -> None:
|
|
8
|
+
private_keys = cli_utils.load_private_keys_from_file(keys_file)
|
|
9
|
+
if not private_keys:
|
|
10
|
+
fatal("private keys not found")
|
|
11
|
+
|
|
12
|
+
res = vault.set_keys_from_vault(keys_url, vault_token, private_keys)
|
|
13
|
+
if res.is_ok() and res.ok is True:
|
|
14
|
+
print_plain(f"saved {len(private_keys)} private keys to the vault")
|
|
15
|
+
else:
|
|
16
|
+
fatal(f"error: {res.err}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
addresses: |
|
|
2
|
+
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623
|
|
3
|
+
0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
4
|
+
0x97C77B548aE0d4925F5C201220fC6d8996424309
|
|
5
|
+
|
|
6
|
+
tokens: |
|
|
7
|
+
0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # USDT
|
|
8
|
+
0x6a55fe4884DE7E1d904BdC47A3BA092240ae9B39 # USDC
|
|
9
|
+
|
|
10
|
+
nodes: |
|
|
11
|
+
https://arb1.arbitrum.io/rpc
|
|
12
|
+
https://rpc.arb1.arbitrum.gateway.fm
|
|
13
|
+
https://arbitrum-one.publicnode.com
|
|
14
|
+
|
|
15
|
+
round_ndigits: 3
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
token: "0x60631C856303731BE4deb81C0303F80B652aA5b4" # USDC
|
|
2
|
+
decimals: 6
|
|
3
|
+
max_fee_per_gas: 1.2base + 1gwei + random(1,200) # supported var_name=base
|
|
4
|
+
max_fee_per_gas_limit: 10.1gwei - random(1,10) # optional
|
|
5
|
+
max_priority_fee_per_gas: 1gwei + random(1,12)
|
|
6
|
+
gas: estimate + random(100,200) - 19 # supported var_name=estimate
|
|
7
|
+
value: 0.5balance - random(1.5t,3t) + 11gwei # supported var_name=balance
|
|
8
|
+
value_min_limit: 0.5t + random(1,2) - 7
|
|
9
|
+
addresses_map: |
|
|
10
|
+
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
11
|
+
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
12
|
+
# and here
|
|
13
|
+
addresses_from_file: ~/path/from.txt
|
|
14
|
+
addresses_to_file: ~/path/to.txt
|
|
15
|
+
delay: random(1.123,10) + 1 # secs, optional
|
|
16
|
+
private_keys: | # optional, private_keys or private_keys_file must be used
|
|
17
|
+
0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
18
|
+
0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
19
|
+
private_keys_file: ~/path/private_keys.txt # optional, private_keys or private_keys_file must be used
|
|
20
|
+
log_debug: /path/to/file_debug.log # optional
|
|
21
|
+
log_info: /path/to/file_info.log # optional
|
|
22
|
+
round_ndigits: 6 # optional, default=5
|
|
23
|
+
chain_id: 421613
|
|
24
|
+
nodes: |
|
|
25
|
+
https://arbitrum-goerli.publicnode.com
|
|
26
|
+
https://rpc.goerli.arbitrum.gateway.fm
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
max_fee_per_gas: 1.2base + 1gwei + random(1,200) # supported var_name=base
|
|
2
|
+
max_fee_per_gas_limit: 10.1gwei - random(1,10) # optional
|
|
3
|
+
max_priority_fee_per_gas: 1gwei + random(1,12)
|
|
4
|
+
gas: estimate + random(100,200) - 19 # supported var_name=estimate
|
|
5
|
+
value: balance - random(0.002eth,0.0025eth) + 11gwei # supported var_name=balance. If 'balance' is used, value=calc(value) - gas*max_fee_per_gas
|
|
6
|
+
value_min_limit: 0.001eth + random(1,2) - 7
|
|
7
|
+
addresses_map: |
|
|
8
|
+
0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b
|
|
9
|
+
0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f # can comment here
|
|
10
|
+
# and here
|
|
11
|
+
#addresses_from_file: ~/path/from.txt
|
|
12
|
+
#addresses_to_file: ~/path/to.txt
|
|
13
|
+
delay: random(1.123,10) + 1 # secs
|
|
14
|
+
#private_keys: |
|
|
15
|
+
# 0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
|
|
16
|
+
# 0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
|
|
17
|
+
private_keys_file: ~/path/private_keys.txt
|
|
18
|
+
log_debug: /path/to/file_debug.log # optional
|
|
19
|
+
log_info: /path/to/file_info.log # optional
|
|
20
|
+
round_ndigits: 6
|
|
21
|
+
chain_id: 421613
|
|
22
|
+
nodes: |
|
|
23
|
+
https://arbitrum-goerli.publicnode.com
|
|
24
|
+
https://rpc.goerli.arbitrum.gateway.fm
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from rich.live import Live
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
|
|
4
|
+
from mm_eth import erc20, rpc
|
|
5
|
+
from mm_eth.utils import from_wei_str
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_balances(
|
|
9
|
+
rpc_nodes: list[str],
|
|
10
|
+
addresses: list[str],
|
|
11
|
+
*,
|
|
12
|
+
token_address: str | None = None,
|
|
13
|
+
token_decimals: int | None = None,
|
|
14
|
+
round_ndigits: int = 5,
|
|
15
|
+
) -> None:
|
|
16
|
+
table = Table(title="balances")
|
|
17
|
+
table.add_column("n")
|
|
18
|
+
table.add_column("address")
|
|
19
|
+
table.add_column("nonce")
|
|
20
|
+
table.add_column("balance, eth")
|
|
21
|
+
if token_address is not None and token_decimals is not None:
|
|
22
|
+
table.add_column("token, t")
|
|
23
|
+
with Live(table, refresh_per_second=0.5):
|
|
24
|
+
for count, address in enumerate(addresses):
|
|
25
|
+
nonce = str(rpc.eth_get_transaction_count(rpc_nodes, address, attempts=5).ok_or_err())
|
|
26
|
+
balance = rpc.eth_get_balance(rpc_nodes, address, attempts=5).map_or_else(
|
|
27
|
+
lambda err: err,
|
|
28
|
+
lambda ok: from_wei_str(ok, "eth", round_ndigits),
|
|
29
|
+
)
|
|
30
|
+
row: list[str] = [str(count), address, nonce, balance]
|
|
31
|
+
if token_address is not None and token_decimals is not None:
|
|
32
|
+
erc20_balance = erc20.get_balance(rpc_nodes, token_address, address, attempts=5).map_or_else(
|
|
33
|
+
lambda err: err,
|
|
34
|
+
lambda ok: from_wei_str(ok, "t", decimals=token_decimals, round_ndigits=round_ndigits),
|
|
35
|
+
)
|
|
36
|
+
row.append(erc20_balance)
|
|
37
|
+
table.add_row(*row)
|