mm-eth 0.4.1__tar.gz → 0.5.1__tar.gz

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