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.
Files changed (43) hide show
  1. mm_eth/abi.py +3 -3
  2. mm_eth/account.py +1 -1
  3. mm_eth/anvil.py +2 -2
  4. mm_eth/cli/__init__.py +0 -0
  5. mm_eth/cli/calcs.py +112 -0
  6. mm_eth/cli/cli.py +237 -0
  7. mm_eth/cli/cli_utils.py +105 -0
  8. mm_eth/cli/cmd/__init__.py +0 -0
  9. mm_eth/cli/cmd/balance_cmd.py +51 -0
  10. mm_eth/cli/cmd/balances_cmd.py +124 -0
  11. mm_eth/cli/cmd/call_contract_cmd.py +43 -0
  12. mm_eth/cli/cmd/config_example_cmd.py +9 -0
  13. mm_eth/cli/cmd/deploy_cmd.py +43 -0
  14. mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
  15. mm_eth/cli/cmd/mnemonic_cmd.py +26 -0
  16. mm_eth/cli/cmd/node_cmd.py +47 -0
  17. mm_eth/cli/cmd/private_key_cmd.py +10 -0
  18. mm_eth/cli/cmd/rpc_cmd.py +78 -0
  19. mm_eth/cli/cmd/send_contract_cmd.py +251 -0
  20. mm_eth/cli/cmd/solc_cmd.py +24 -0
  21. mm_eth/cli/cmd/token_cmd.py +29 -0
  22. mm_eth/cli/cmd/transfer_erc20_cmd.py +277 -0
  23. mm_eth/cli/cmd/transfer_eth_cmd.py +254 -0
  24. mm_eth/cli/cmd/vault_cmd.py +16 -0
  25. mm_eth/cli/config_examples/balances.yml +15 -0
  26. mm_eth/cli/config_examples/call_contract.yml +5 -0
  27. mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
  28. mm_eth/cli/config_examples/transfer_eth.yml +24 -0
  29. mm_eth/cli/print_helpers.py +37 -0
  30. mm_eth/cli/rpc_helpers.py +140 -0
  31. mm_eth/cli/validators.py +85 -0
  32. mm_eth/erc20.py +8 -7
  33. mm_eth/rpc.py +8 -8
  34. mm_eth/solc.py +2 -3
  35. mm_eth/tx.py +3 -5
  36. mm_eth/utils.py +11 -16
  37. mm_eth/vault.py +5 -5
  38. mm_eth-0.2.2.dist-info/METADATA +9 -0
  39. mm_eth-0.2.2.dist-info/RECORD +47 -0
  40. mm_eth-0.2.2.dist-info/entry_points.txt +2 -0
  41. mm_eth-0.2.0.dist-info/METADATA +0 -7
  42. mm_eth-0.2.0.dist-info/RECORD +0 -18
  43. {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,5 @@
1
+ contract_address: "0xBa985cad26658EB00eA42aCc7516aed52e7a8AcC"
2
+ function_signature: balanceOf(address)
3
+ function_args: "['0x83aC43147BA5dAA5abc4ccEA84F2B8000bA82f26']"
4
+ outputs_types: uint256 # optional
5
+ node: https://rpc.eth.gateway.fm
@@ -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)