mm-eth 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {mm_eth-0.4.0 → mm_eth-0.5.0}/.gitignore +1 -0
  2. {mm_eth-0.4.0 → mm_eth-0.5.0}/PKG-INFO +2 -2
  3. {mm_eth-0.4.0 → mm_eth-0.5.0}/dict.dic +1 -0
  4. {mm_eth-0.4.0 → mm_eth-0.5.0}/pyproject.toml +3 -3
  5. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/account.py +11 -11
  6. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cli.py +29 -26
  7. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/transfer_cmd.py +75 -52
  8. {mm_eth-0.4.0/src/mm_eth/cli/cmd → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/mnemonic_cmd.py +4 -4
  9. mm_eth-0.5.0/src/mm_eth/cli/examples/transfer.toml +42 -0
  10. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/validators.py +3 -3
  11. mm_eth-0.5.0/tests/cli/cmd/__init__.py +0 -0
  12. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_account.py +9 -1
  13. {mm_eth-0.4.0 → mm_eth-0.5.0}/uv.lock +18 -40
  14. mm_eth-0.4.0/src/mm_eth/cli/cmd/send_contract_cmd.py +0 -197
  15. mm_eth-0.4.0/src/mm_eth/cli/examples/transfer.toml +0 -39
  16. {mm_eth-0.4.0 → mm_eth-0.5.0}/README.txt +0 -0
  17. {mm_eth-0.4.0 → mm_eth-0.5.0}/justfile +0 -0
  18. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/__init__.py +0 -0
  19. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/abi.py +0 -0
  20. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/anvil.py +0 -0
  21. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/__init__.py +0 -0
  22. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/calcs.py +0 -0
  23. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cli_utils.py +0 -0
  24. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/__init__.py +0 -0
  25. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/balance_cmd.py +0 -0
  26. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/balances_cmd.py +0 -0
  27. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/call_contract_cmd.py +0 -0
  28. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/deploy_cmd.py +0 -0
  29. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/encode_input_data_cmd.py +0 -0
  30. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/example_cmd.py +0 -0
  31. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/node_cmd.py +0 -0
  32. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/rpc_cmd.py +0 -0
  33. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/solc_cmd.py +0 -0
  34. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/token_cmd.py +0 -0
  35. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/tx_cmd.py +0 -0
  36. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/cmd/vault_cmd.py +0 -0
  37. {mm_eth-0.4.0/tests → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/__init__.py +0 -0
  38. {mm_eth-0.4.0/src/mm_eth/cli/cmd → mm_eth-0.5.0/src/mm_eth/cli/cmd/wallet}/private_key_cmd.py +0 -0
  39. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/examples/balances.toml +0 -0
  40. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/examples/call_contract.toml +0 -0
  41. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/print_helpers.py +0 -0
  42. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/cli/rpc_helpers.py +0 -0
  43. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/constants.py +0 -0
  44. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/deploy.py +0 -0
  45. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/ens.py +0 -0
  46. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/erc20.py +0 -0
  47. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/ethernodes.py +0 -0
  48. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/json_encoder.py +0 -0
  49. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/py.typed +0 -0
  50. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/rpc.py +0 -0
  51. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/solc.py +0 -0
  52. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/tx.py +0 -0
  53. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/utils.py +0 -0
  54. {mm_eth-0.4.0 → mm_eth-0.5.0}/src/mm_eth/vault.py +0 -0
  55. {mm_eth-0.4.0/tests/cli → mm_eth-0.5.0/tests}/__init__.py +0 -0
  56. {mm_eth-0.4.0/tests/cli/cmd → mm_eth-0.5.0/tests/cli}/__init__.py +0 -0
  57. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_balance_cmd.py +0 -0
  58. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_mnemonic_cmd.py +0 -0
  59. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_node_cmd.py +0 -0
  60. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_private_key_cmd.py +0 -0
  61. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/cmd/test_solc_cmd.py +0 -0
  62. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/cli/test_calcs.py +0 -0
  63. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/conftest.py +0 -0
  64. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/contracts/ERC20.sol +0 -0
  65. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/contracts/abi/ERC20.json +0 -0
  66. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_abi.py +0 -0
  67. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_ethernodes.py +0 -0
  68. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_rpc.py +0 -0
  69. {mm_eth-0.4.0 → mm_eth-0.5.0}/tests/test_tx.py +0 -0
  70. {mm_eth-0.4.0 → mm_eth-0.5.0}/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.0
3
+ Version: 0.5.0
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.0"
3
+ version = "0.5.0"
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",
@@ -13,9 +13,12 @@ Account.enable_unaudited_hdwallet_features()
13
13
 
14
14
  key_api = KeyAPI()
15
15
 
16
+ DEFAULT_DERIVATION_PATH = "m/44'/60'/0'/0/{i}"
17
+
16
18
 
17
19
  @dataclass
18
- class NewAccount:
20
+ class DerivedAccount:
21
+ index: int
19
22
  path: str
20
23
  address: str
21
24
  private_key: str
@@ -30,18 +33,15 @@ def generate_mnemonic(num_words: int = 24) -> str:
30
33
  return mnemonic.generate(num_words=num_words)
31
34
 
32
35
 
33
- def generate_accounts( # nosec
34
- mnemonic: str,
35
- passphrase: str = "",
36
- path_prefix: str = "m/44'/60'/0'/0",
37
- limit: int = 12,
38
- ) -> list[NewAccount]:
39
- result: list[NewAccount] = []
36
+ def derive_accounts(mnemonic: str, passphrase: str, derivation_path: str, limit: int) -> list[DerivedAccount]:
37
+ if "{i}" not in derivation_path:
38
+ raise ValueError("derivation_path must contain {i}, for example: " + DEFAULT_DERIVATION_PATH)
39
+ result: list[DerivedAccount] = []
40
40
  for i in range(limit):
41
- path = f"{path_prefix}/{i}"
42
- acc = Account.from_mnemonic(mnemonic=mnemonic, account_path=path, passphrase=passphrase)
41
+ path = derivation_path.replace("{i}", str(i))
42
+ acc = Account.from_mnemonic(mnemonic, passphrase, path)
43
43
  private_key = acc.key.to_0x_hex().lower()
44
- result.append(NewAccount(path, acc.address, private_key))
44
+ result.append(DerivedAccount(i, path, acc.address, private_key))
45
45
  return result
46
46
 
47
47
 
@@ -5,6 +5,8 @@ from typing import Annotated
5
5
  import typer
6
6
  from mm_std import PrintFormat, print_plain
7
7
 
8
+ from mm_eth.account import DEFAULT_DERIVATION_PATH
9
+
8
10
  from . import cli_utils
9
11
  from .cmd import (
10
12
  balance_cmd,
@@ -13,11 +15,8 @@ from .cmd import (
13
15
  deploy_cmd,
14
16
  encode_input_data_cmd,
15
17
  example_cmd,
16
- mnemonic_cmd,
17
18
  node_cmd,
18
- private_key_cmd,
19
19
  rpc_cmd,
20
- send_contract_cmd,
21
20
  solc_cmd,
22
21
  token_cmd,
23
22
  transfer_cmd,
@@ -27,8 +26,8 @@ from .cmd import (
27
26
  from .cmd.balances_cmd import BalancesCmdParams
28
27
  from .cmd.call_contract_cmd import CallContractCmdParams
29
28
  from .cmd.deploy_cmd import DeployCmdParams
30
- from .cmd.send_contract_cmd import SendContractCmdParams
31
29
  from .cmd.transfer_cmd import TransferCmdParams
30
+ from .cmd.wallet import mnemonic_cmd, private_key_cmd
32
31
 
33
32
  app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
34
33
 
@@ -74,9 +73,10 @@ def node_command(
74
73
  @wallet_app.command(name="mnemonic", help="Generate eth accounts based on a mnemonic")
75
74
  def mnemonic_command( # nosec
76
75
  mnemonic: Annotated[str, typer.Option("--mnemonic", "-m")] = "",
77
- passphrase: Annotated[str, typer.Option("--passphrase", "-pass")] = "",
76
+ passphrase: Annotated[str, typer.Option("--passphrase", "-p")] = "",
78
77
  print_path: bool = typer.Option(False, "--print_path"),
79
- path_prefix: Annotated[str, typer.Option("--path")] = "m/44'/60'/0'/0",
78
+ derivation_path: Annotated[str, typer.Option("--path")] = DEFAULT_DERIVATION_PATH,
79
+ words: int = typer.Option(12, "--words", "-w", help="Number of mnemonic words"),
80
80
  limit: int = typer.Option(10, "--limit", "-l"),
81
81
  save_file: str = typer.Option("", "--save", "-s", help="Save private keys to a file"),
82
82
  ) -> None:
@@ -85,7 +85,8 @@ def mnemonic_command( # nosec
85
85
  passphrase=passphrase,
86
86
  print_path=print_path,
87
87
  limit=limit,
88
- path_prefix=path_prefix,
88
+ words=words,
89
+ derivation_path=derivation_path,
89
90
  save_file=save_file,
90
91
  )
91
92
 
@@ -144,6 +145,7 @@ def tx_command(
144
145
  def transfer_command(
145
146
  config_path: Path,
146
147
  print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
148
+ print_transfers: bool = typer.Option(False, "--transfers", "-t", help="Print transfers (from, to, value) and exit"),
147
149
  print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
148
150
  emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
149
151
  skip_receipt: bool = typer.Option(False, "--skip-receipt", help="Don't wait for a tx receipt"),
@@ -153,6 +155,7 @@ def transfer_command(
153
155
  TransferCmdParams(
154
156
  config_path=config_path,
155
157
  print_balances=print_balances,
158
+ print_transfers=print_transfers,
156
159
  print_config=print_config,
157
160
  debug=debug,
158
161
  skip_receipt=skip_receipt,
@@ -161,25 +164,25 @@ def transfer_command(
161
164
  )
162
165
 
163
166
 
164
- @app.command(name="send-contract", help="Send transactions to a contract")
165
- def send_contract_command(
166
- config_path: Path,
167
- print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
168
- print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
169
- emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
170
- no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
171
- debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
172
- ) -> None:
173
- send_contract_cmd.run(
174
- SendContractCmdParams(
175
- config_path=config_path,
176
- print_balances=print_balances,
177
- print_config=print_config,
178
- debug=debug,
179
- no_receipt=no_receipt,
180
- emulate=emulate,
181
- )
182
- )
167
+ # @app.command(name="send-contract", help="Send transactions to a contract")
168
+ # def send_contract_command(
169
+ # config_path: Path,
170
+ # print_balances: bool = typer.Option(False, "--balances", "-b", help="Print balances and exit"),
171
+ # print_config: bool = typer.Option(False, "--config", "-c", help="Print config and exit"),
172
+ # emulate: bool = typer.Option(False, "--emulate", "-e", help="Emulate transaction posting"),
173
+ # no_receipt: bool = typer.Option(False, "--no-receipt", "-nr", help="Don't wait for a tx receipt"),
174
+ # debug: bool = typer.Option(False, "--debug", "-d", help="Print debug info"),
175
+ # ) -> None:
176
+ # send_contract_cmd.run(
177
+ # SendContractCmdParams(
178
+ # config_path=config_path,
179
+ # print_balances=print_balances,
180
+ # print_config=print_config,
181
+ # debug=debug,
182
+ # no_receipt=no_receipt,
183
+ # emulate=emulate,
184
+ # )
185
+ # )
183
186
 
184
187
 
185
188
  @app.command(name="balances", help="Print base and ERC20 token balances")
@@ -5,9 +5,10 @@ from typing import Annotated, Self
5
5
 
6
6
  import mm_crypto_utils
7
7
  from loguru import logger
8
- from mm_crypto_utils import AddressToPrivate, 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,7 +24,7 @@ from mm_eth.utils import from_wei_str
23
24
  class Config(BaseConfig):
24
25
  nodes: Annotated[list[str], BeforeValidator(Validators.nodes())]
25
26
  chain_id: int
26
- 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
@@ -42,13 +43,20 @@ class Config(BaseConfig):
42
43
 
43
44
  @property
44
45
  def from_addresses(self) -> list[str]:
45
- return [r.from_address for r in self.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:
55
+ transfer.value = self.value
56
+ for transfer in self.transfers: # Check all transfers have a value.
57
+ if not transfer.value:
58
+ raise ValueError(f"{transfer.log_prefix}: value is not set")
59
+
52
60
  if self.token:
53
61
  Validators.valid_token_expression("balance")(self.value)
54
62
  if self.value_min_limit:
@@ -69,6 +77,7 @@ class Config(BaseConfig):
69
77
 
70
78
  class TransferCmdParams(BaseConfigParams):
71
79
  print_balances: bool
80
+ print_transfers: bool
72
81
  debug: bool
73
82
  skip_receipt: bool
74
83
  emulate: bool
@@ -82,6 +91,10 @@ def run(cmd_params: TransferCmdParams) -> None:
82
91
 
83
92
  rpc_helpers.check_nodes_for_chain_id(config.nodes, config.chain_id)
84
93
 
94
+ if cmd_params.print_transfers:
95
+ _print_transfers(config)
96
+ sys.exit(0)
97
+
85
98
  if cmd_params.print_balances:
86
99
  _print_balances(config)
87
100
  sys.exit(0)
@@ -93,9 +106,9 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
93
106
  mm_crypto_utils.init_logger(cmd_params.debug, config.log_debug, config.log_info)
94
107
  logger.info(f"transfer {cmd_params.config_path}: started at {utc_now()} UTC")
95
108
  logger.debug(f"config={config.model_dump(exclude={'private_keys'}) | {'version': cli_utils.get_version()}}")
96
- for i, route in enumerate(config.routes):
97
- _transfer(route, config, cmd_params)
98
- if config.delay is not None and i < len(config.routes) - 1:
109
+ for i, transfer in enumerate(config.transfers):
110
+ _transfer(transfer, config, cmd_params)
111
+ if config.delay is not None and i < len(config.transfers) - 1:
99
112
  delay_value = mm_crypto_utils.calc_decimal_value(config.delay)
100
113
  logger.info(f"delay {delay_value} seconds")
101
114
  if not cmd_params.emulate:
@@ -103,34 +116,34 @@ def _run_transfers(config: Config, cmd_params: TransferCmdParams) -> None:
103
116
  logger.info(f"finished at {utc_now()} UTC")
104
117
 
105
118
 
106
- def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) -> None:
107
- nonce = rpc_helpers.get_nonce(config.nodes, route.from_address, route.log_prefix)
119
+ def _transfer(t: Transfer, config: Config, cmd_params: TransferCmdParams) -> None:
120
+ nonce = rpc_helpers.get_nonce(config.nodes, t.from_address, t.log_prefix)
108
121
  if nonce is None:
109
122
  return
110
123
 
111
- max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, route.log_prefix)
124
+ max_fee = rpc_helpers.calc_max_fee(config.nodes, config.max_fee, t.log_prefix)
112
125
  if max_fee is None:
113
126
  return
114
127
 
115
- if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, route.log_prefix):
128
+ if rpc_helpers.is_max_fee_limit_exceeded(max_fee, config.max_fee_limit, t.log_prefix):
116
129
  return
117
130
 
118
- gas = _calc_gas(route, config)
131
+ gas = _calc_gas(t, config)
119
132
  if gas is None:
120
133
  return
121
134
 
122
- value = _calc_value(route, max_fee=max_fee, gas=gas, config=config)
135
+ value = _calc_value(t, max_fee=max_fee, gas=gas, config=config)
123
136
  if value is None:
124
137
  return
125
138
 
126
- if not _check_value_min_limit(route, value, config):
139
+ if not _check_value_min_limit(t, value, config):
127
140
  return
128
141
 
129
142
  priority_fee = calc_eth_expression(config.priority_fee)
130
143
 
131
144
  # emulate?
132
145
  if cmd_params.emulate:
133
- msg = f"{route.log_prefix}: emulate,"
146
+ msg = f"{t.log_prefix}: emulate,"
134
147
  msg += f" value={_value_with_suffix(value, config)},"
135
148
  msg += f" max_fee={from_wei_str(max_fee, 'gwei', config.round_ndigits)},"
136
149
  msg += f" priority_fee={from_wei_str(priority_fee, 'gwei', config.round_ndigits)},"
@@ -138,39 +151,39 @@ def _transfer(route: TxRoute, config: Config, cmd_params: TransferCmdParams) ->
138
151
  logger.info(msg)
139
152
  return
140
153
 
141
- tx_hash = _send_tx(route=route, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
154
+ tx_hash = _send_tx(transfer=t, nonce=nonce, max_fee=max_fee, priority_fee=priority_fee, gas=gas, value=value, config=config)
142
155
  if tx_hash is None:
143
156
  return
144
157
 
145
158
  status = "UNKNOWN"
146
159
  if not cmd_params.skip_receipt:
147
- logger.debug(f"{route.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
160
+ logger.debug(f"{t.log_prefix}: waiting for receipt, tx_hash={tx_hash}")
148
161
  status = cli_utils.wait_tx_status(config.nodes, config.proxies, tx_hash, config.wait_tx_timeout)
149
162
 
150
- logger.info(f"{route.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
163
+ logger.info(f"{t.log_prefix}: tx_hash={tx_hash}, value={_value_with_suffix(value, config)}, status={status}")
151
164
 
152
165
 
153
- def _calc_value(route: TxRoute, max_fee: int, gas: int, config: Config) -> int | None:
166
+ def _calc_value(transfer: Transfer, max_fee: int, gas: int, config: Config) -> int | None:
154
167
  if config.token:
155
168
  return rpc_helpers.calc_erc20_value_for_address(
156
169
  nodes=config.nodes,
157
- value_expression=config.value,
158
- wallet_address=route.from_address,
170
+ value_expression=transfer.value,
171
+ wallet_address=transfer.from_address,
159
172
  token_address=config.token,
160
173
  decimals=config.token_decimals,
161
- log_prefix=route.log_prefix,
174
+ log_prefix=transfer.log_prefix,
162
175
  )
163
176
  return rpc_helpers.calc_eth_value_for_address(
164
177
  nodes=config.nodes,
165
- value_expression=config.value,
166
- address=route.from_address,
178
+ value_expression=transfer.value,
179
+ address=transfer.from_address,
167
180
  gas=gas,
168
181
  max_fee=max_fee,
169
- log_prefix=route.log_prefix,
182
+ log_prefix=transfer.log_prefix,
170
183
  )
171
184
 
172
185
 
173
- def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
186
+ def _check_value_min_limit(transfer: Transfer, value: int, config: Config) -> bool:
174
187
  """Returns False if the transfer should be skipped."""
175
188
  if config.value_min_limit:
176
189
  if config.token:
@@ -178,21 +191,23 @@ def _check_value_min_limit(route: TxRoute, value: int, config: Config) -> bool:
178
191
  else:
179
192
  value_min_limit = calcs.calc_eth_expression(config.value_min_limit)
180
193
  if value < value_min_limit:
181
- logger.info(f"{route.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
194
+ logger.info(f"{transfer.log_prefix}: value<value_min_limit, value={_value_with_suffix(value, config)}")
182
195
  return True
183
196
 
184
197
 
185
- def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config) -> str | None:
198
+ def _send_tx(
199
+ *, transfer: Transfer, nonce: int, max_fee: int, priority_fee: int, gas: int, value: int, config: Config
200
+ ) -> str | None:
186
201
  debug_tx_params = {
187
202
  "nonce": nonce,
188
203
  "max_fee": max_fee,
189
204
  "priority_fee": priority_fee,
190
205
  "gas": gas,
191
206
  "value": value,
192
- "to": route.to_address,
207
+ "to": transfer.to_address,
193
208
  "chain_id": config.chain_id,
194
209
  }
195
- logger.debug(f"{route.log_prefix}: tx_params={debug_tx_params}")
210
+ logger.debug(f"{transfer.log_prefix}: tx_params={debug_tx_params}")
196
211
 
197
212
  if config.token:
198
213
  signed_tx = erc20.sign_transfer_tx(
@@ -200,11 +215,11 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
200
215
  max_fee_per_gas=max_fee,
201
216
  max_priority_fee_per_gas=priority_fee,
202
217
  gas_limit=gas,
203
- private_key=config.private_keys[route.from_address],
218
+ private_key=config.private_keys[transfer.from_address],
204
219
  chain_id=config.chain_id,
205
220
  value=value,
206
221
  token_address=config.token,
207
- recipient_address=route.to_address,
222
+ recipient_address=transfer.to_address,
208
223
  )
209
224
  else:
210
225
  signed_tx = sign_tx(
@@ -212,38 +227,46 @@ def _send_tx(*, route: TxRoute, nonce: int, max_fee: int, priority_fee: int, gas
212
227
  max_fee_per_gas=max_fee,
213
228
  max_priority_fee_per_gas=priority_fee,
214
229
  gas=gas,
215
- private_key=config.private_keys[route.from_address],
230
+ private_key=config.private_keys[transfer.from_address],
216
231
  chain_id=config.chain_id,
217
232
  value=value,
218
- to=route.to_address,
233
+ to=transfer.to_address,
219
234
  )
220
235
  res = rpc.eth_send_raw_transaction(config.nodes, signed_tx.raw_tx, attempts=5)
221
236
  if isinstance(res, Err):
222
- logger.info(f"{route.log_prefix}: tx error {res.err}")
237
+ logger.info(f"{transfer.log_prefix}: tx error {res.err}")
223
238
  return None
224
239
  return res.ok
225
240
 
226
241
 
227
- def _calc_gas(route: TxRoute, config: Config) -> int | None:
242
+ def _calc_gas(transfer: Transfer, config: Config) -> int | None:
228
243
  if config.token:
229
244
  return rpc_helpers.calc_gas(
230
245
  nodes=config.nodes,
231
246
  gas_expression=config.gas,
232
- from_address=route.from_address,
247
+ from_address=transfer.from_address,
233
248
  to_address=config.token,
234
- data=erc20.encode_transfer_input_data(route.to_address, 1234),
235
- log_prefix=route.log_prefix,
249
+ data=erc20.encode_transfer_input_data(transfer.to_address, 1234),
250
+ log_prefix=transfer.log_prefix,
236
251
  )
237
252
  return rpc_helpers.calc_gas(
238
253
  nodes=config.nodes,
239
254
  gas_expression=config.gas,
240
- from_address=route.from_address,
241
- to_address=route.to_address,
255
+ from_address=transfer.from_address,
256
+ to_address=transfer.to_address,
242
257
  value=123,
243
- log_prefix=route.log_prefix,
258
+ log_prefix=transfer.log_prefix,
244
259
  )
245
260
 
246
261
 
262
+ def _print_transfers(config: Config) -> None:
263
+ table = Table("n", "from_address", "to_address", "value", title="transfers")
264
+ for count, transfer in enumerate(config.transfers, start=1):
265
+ table.add_row(str(count), transfer.from_address, transfer.to_address, transfer.value)
266
+ console = Console()
267
+ console.print(table)
268
+
269
+
247
270
  def _print_balances(config: Config) -> None:
248
271
  if config.token:
249
272
  headers = ["n", "from_address", "nonce", "eth", "t", "to_address", "nonce", "eth", "t"]
@@ -251,24 +274,24 @@ def _print_balances(config: Config) -> None:
251
274
  headers = ["n", "from_address", "nonce", "eth", "to_address", "nonce", "eth"]
252
275
  table = Table(*headers, title="balances")
253
276
  with Live(table, refresh_per_second=0.5):
254
- for count, 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)
277
+ for count, transfer in enumerate(config.transfers):
278
+ from_nonce = _get_nonce_str(transfer.from_address, config)
279
+ to_nonce = _get_nonce_str(transfer.to_address, config)
257
280
 
258
- from_eth_balance = _get_eth_balance_str(route.from_address, config)
259
- to_eth_balance = _get_eth_balance_str(route.to_address, config)
281
+ from_eth_balance = _get_eth_balance_str(transfer.from_address, config)
282
+ to_eth_balance = _get_eth_balance_str(transfer.to_address, config)
260
283
 
261
- from_token_balance = _get_token_balance_str(route.from_address, config) if config.token else ""
262
- to_token_balance = _get_token_balance_str(route.to_address, config) if config.token else ""
284
+ from_token_balance = _get_token_balance_str(transfer.from_address, config) if config.token else ""
285
+ to_token_balance = _get_token_balance_str(transfer.to_address, config) if config.token else ""
263
286
 
264
287
  if config.token:
265
288
  table.add_row(
266
289
  str(count),
267
- route.from_address,
290
+ transfer.from_address,
268
291
  from_nonce,
269
292
  from_eth_balance,
270
293
  from_token_balance,
271
- route.to_address,
294
+ transfer.to_address,
272
295
  to_nonce,
273
296
  to_eth_balance,
274
297
  to_token_balance,
@@ -276,10 +299,10 @@ def _print_balances(config: Config) -> None:
276
299
  else:
277
300
  table.add_row(
278
301
  str(count),
279
- route.from_address,
302
+ transfer.from_address,
280
303
  from_nonce,
281
304
  from_eth_balance,
282
- route.to_address,
305
+ transfer.to_address,
283
306
  to_nonce,
284
307
  to_eth_balance,
285
308
  )
@@ -3,18 +3,18 @@ from typing import Any
3
3
 
4
4
  from mm_std import print_json
5
5
 
6
- from mm_eth.account import generate_accounts, generate_mnemonic
6
+ from mm_eth.account import derive_accounts, generate_mnemonic
7
7
 
8
8
 
9
- def run(mnemonic: str, passphrase: str, limit: int, print_path: bool, path_prefix: str, save_file: str) -> None: # nosec
9
+ def run(mnemonic: str, passphrase: str, words: int, derivation_path: str, limit: int, print_path: bool, save_file: str) -> None: # nosec
10
10
  result: dict[str, Any] = {}
11
11
  if not mnemonic:
12
- mnemonic = generate_mnemonic()
12
+ mnemonic = generate_mnemonic(num_words=words)
13
13
  result["mnemonic"] = mnemonic
14
14
  if passphrase:
15
15
  result["passphrase"] = passphrase
16
16
  result["accounts"] = []
17
- for acc in generate_accounts(mnemonic=mnemonic, passphrase=passphrase, limit=limit, path_prefix=path_prefix):
17
+ for acc in derive_accounts(mnemonic=mnemonic, passphrase=passphrase, limit=limit, derivation_path=derivation_path):
18
18
  new_account = {"address": acc.address, "private": acc.private_key}
19
19
  if print_path:
20
20
  new_account["path"] = acc.path
@@ -0,0 +1,42 @@
1
+ transfers = """
2
+ # from_address to_address value
3
+ 0x10fd602Bff689e64D4720D1DCCCD3494f1f16623 0x58487485c3858109f5A37e42546FE87473f79a4b 0.1t # comments are allowed here
4
+ 0x97C77B548aE0d4925F5C201220fC6d8996424309 0x7EdF3b8579c21A8820b4C0B8352541c1CE04045f 0.2balance-random(0.1t,0.5t)
5
+ 0x10ecB8d838746643E613f6B5218C8e342593225c 0xE19242B72a4833eD86F1b2015d4E59052A2b278b # if the value is not set, config.value will be used
6
+ file: /path/to/other_transfers.txt # transfers from this file will be added
7
+ """
8
+
9
+ private_keys = """
10
+ 0x7bb5b9c0ba991275f84b796b4d25fd3a8d7320911f50fade85410e7a2b000632
11
+ 0xb7e0b671e176b04ceb0897a698d34771bfe9acf29273dc52a141be6e97145a00
12
+ file: /path/to/other_private_keys.txt
13
+ """
14
+
15
+ token = "0x60631C856303731BE4deb81C0303F80B652aA5b4" # If not specified, it ETH transfers
16
+
17
+ max_fee = "1.2base_fee+1gwei+random(1,200)" # supported var_name=base_fee
18
+
19
+ max_fee_limit = "10.1gwei-random(1,10)" # optional
20
+
21
+ priority_fee = "1gwei+random(1,12)"
22
+
23
+ gas = "estimate+random(100,200)-19" # supported var_name=estimate
24
+
25
+ value = "0.5balance-random(1.5t,3t)+11t" # supported var_name=balance. For ERC20 token use 't' suffix.
26
+
27
+ value_min_limit = "0.5t+random(1,2)-7"
28
+
29
+ delay = "random(1.123,10)+1" # secs, optional
30
+
31
+ log_debug = "/path/to/file_debug.log" # optional
32
+
33
+ log_info = "/path/to/file_info.log" # optional
34
+
35
+ round_ndigits = 6 # optional, default=5
36
+
37
+ chain_id = 421613
38
+
39
+ nodes = """
40
+ https://arbitrum-goerli.publicnode.com
41
+ https://rpc.goerli.arbitrum.gateway.fm
42
+ """
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
2
 
3
- from mm_crypto_utils import AddressToPrivate, ConfigValidators, 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]:
File without changes
@@ -1,4 +1,5 @@
1
1
  from mm_eth import account
2
+ from mm_eth.account import DEFAULT_DERIVATION_PATH
2
3
 
3
4
 
4
5
  def test_generate_mnemonic():
@@ -8,7 +9,14 @@ def test_generate_mnemonic():
8
9
 
9
10
 
10
11
  def test_generate_accounts():
11
- assert len(account.generate_accounts(account.generate_mnemonic(), limit=17)) == 17
12
+ mnemonic = "tuition skin amateur sail oak bone panel concert horse need panel balance"
13
+ passphrase = "pass-secret"
14
+ res = account.derive_accounts(mnemonic, passphrase, DEFAULT_DERIVATION_PATH, 7)
15
+ assert len(res) == 7
16
+ assert res[3].index == 3
17
+ assert res[3].path == "m/44'/60'/0'/0/3"
18
+ assert res[3].address == "0x2F9e1b9f4D11756E84d4b6D2f6B107FA37feB701"
19
+ assert res[3].private_key == "0x7b222a59ac8496b4f1f623bc86d15e889af8406f796037888ddee1290b933183"
12
20
 
13
21
 
14
22
  def test_to_checksum_address():
@@ -602,19 +602,6 @@ wheels = [
602
602
  { url = "https://files.pythonhosted.org/packages/d5/34/e8b383f35b77c402d28563d2b8f83159319b509bc5f760b15d60b0abf165/hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c", size = 32611 },
603
603
  ]
604
604
 
605
- [[package]]
606
- name = "html5lib"
607
- version = "1.1"
608
- source = { registry = "https://pypi.org/simple" }
609
- dependencies = [
610
- { name = "six" },
611
- { name = "webencodings" },
612
- ]
613
- sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 }
614
- wheels = [
615
- { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 },
616
- ]
617
-
618
605
  [[package]]
619
606
  name = "httpcore"
620
607
  version = "1.0.7"
@@ -726,19 +713,19 @@ wheels = [
726
713
 
727
714
  [[package]]
728
715
  name = "mm-crypto-utils"
729
- version = "0.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.0"
728
+ version = "0.5.0"
742
729
  source = { editable = "." }
743
730
  dependencies = [
744
731
  { name = "mm-crypto-utils" },
@@ -760,7 +747,7 @@ dev = [
760
747
 
761
748
  [package.metadata]
762
749
  requires-dist = [
763
- { name = "mm-crypto-utils", specifier = ">=0.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