mm-eth 0.5.17__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. mm_eth/abi.py +2 -4
  2. mm_eth/account.py +51 -18
  3. mm_eth/anvil.py +8 -8
  4. mm_eth/cli/calcs.py +1 -16
  5. mm_eth/cli/cli.py +55 -159
  6. mm_eth/cli/cli_utils.py +14 -27
  7. mm_eth/cli/cmd/balance_cmd.py +15 -16
  8. mm_eth/cli/cmd/balances_cmd.py +35 -36
  9. mm_eth/cli/cmd/deploy_cmd.py +9 -11
  10. mm_eth/cli/cmd/node_cmd.py +20 -15
  11. mm_eth/cli/cmd/solc_cmd.py +7 -6
  12. mm_eth/cli/cmd/transfer_cmd.py +210 -128
  13. mm_eth/cli/cmd/wallet/private_key_cmd.py +5 -4
  14. mm_eth/cli/rpc_helpers.py +32 -115
  15. mm_eth/cli/validators.py +13 -16
  16. mm_eth/converters.py +56 -0
  17. mm_eth/erc20.py +6 -224
  18. mm_eth/retry.py +143 -0
  19. mm_eth/rpc.py +226 -428
  20. mm_eth/solc.py +30 -17
  21. mm_eth/tx.py +8 -9
  22. mm_eth/utils.py +0 -224
  23. {mm_eth-0.5.17.dist-info → mm_eth-0.6.0.dist-info}/METADATA +3 -2
  24. mm_eth-0.6.0.dist-info/RECORD +32 -0
  25. mm_eth/async_rpc.py +0 -94
  26. mm_eth/cli/cmd/call_contract_cmd.py +0 -44
  27. mm_eth/cli/cmd/encode_input_data_cmd.py +0 -10
  28. mm_eth/cli/cmd/example_cmd.py +0 -9
  29. mm_eth/cli/cmd/rpc_cmd.py +0 -78
  30. mm_eth/cli/cmd/token_cmd.py +0 -29
  31. mm_eth/cli/cmd/tx_cmd.py +0 -16
  32. mm_eth/cli/cmd/vault_cmd.py +0 -19
  33. mm_eth/cli/examples/balances.toml +0 -18
  34. mm_eth/cli/examples/call_contract.toml +0 -9
  35. mm_eth/cli/examples/transfer.toml +0 -46
  36. mm_eth/cli/print_helpers.py +0 -37
  37. mm_eth/constants.py +0 -1
  38. mm_eth/ens.py +0 -106
  39. mm_eth/ethernodes.py +0 -34
  40. mm_eth/json_encoder.py +0 -15
  41. mm_eth/py.typed +0 -0
  42. mm_eth/rpc_async.py +0 -160
  43. mm_eth/vault.py +0 -38
  44. mm_eth-0.5.17.dist-info/RECORD +0 -49
  45. {mm_eth-0.5.17.dist-info → mm_eth-0.6.0.dist-info}/WHEEL +0 -0
  46. {mm_eth-0.5.17.dist-info → mm_eth-0.6.0.dist-info}/entry_points.txt +0 -0
mm_eth/solc.py CHANGED
@@ -1,9 +1,10 @@
1
1
  import random
2
+ import re
2
3
  import shutil
3
4
  from dataclasses import dataclass
4
5
  from pathlib import Path
5
6
 
6
- from mm_std import Err, Ok, Result, run_command
7
+ from mm_std import Result, run_command
7
8
 
8
9
 
9
10
  @dataclass
@@ -12,22 +13,34 @@ class SolcResult:
12
13
  abi: str
13
14
 
14
15
 
15
- def solc(contract_name: str, contract_path: str, tmp_dir: str) -> Result[SolcResult]:
16
- if tmp_dir.startswith("~"):
17
- tmp_dir = Path(tmp_dir).expanduser().as_posix()
18
- if contract_path.startswith("~"):
19
- contract_path = Path(contract_path).expanduser().as_posix()
20
- work_dir = f"{tmp_dir}/solc_{contract_name}_{random.randint(0, 100_000_000)}"
21
- abi_path = f"{work_dir}/{contract_name}.abi"
22
- bin_path = f"{work_dir}/{contract_name}.bin"
16
+ def solc(contract_name: str, contract_path: Path, tmp_dir: Path) -> Result[SolcResult]:
17
+ # Sanitize contract name to avoid unsafe characters in directory name
18
+ safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", contract_name)
19
+
20
+ # Expand ~ in paths if present
21
+ contract_path = contract_path.expanduser().resolve()
22
+ tmp_dir = tmp_dir.expanduser().resolve()
23
+
24
+ work_dir = tmp_dir / f"solc_{safe_name}_{random.randint(0, 100_000_000)}"
25
+ abi_path = work_dir / f"{contract_name}.abi"
26
+ bin_path = work_dir / f"{contract_name}.bin"
27
+
28
+ work_dir_created = False
23
29
  try:
24
- Path(work_dir).mkdir(parents=True)
25
- cmd = f"solc -o '{work_dir}' --abi --bin --optimize {contract_path}"
26
- run_command(cmd)
27
- abi = Path(abi_path).read_text()
28
- bin_ = Path(bin_path).read_text()
29
- return Ok(SolcResult(bin=bin_, abi=abi))
30
+ work_dir.mkdir(parents=True, exist_ok=False)
31
+ work_dir_created = True
32
+
33
+ cmd = f"solc -o '{work_dir}' --abi --bin --optimize '{contract_path}'"
34
+ result = run_command(cmd)
35
+ if result.code != 0:
36
+ return Result.err(f"solc error: {result.stderr}")
37
+
38
+ abi = abi_path.read_text()
39
+ bin_ = bin_path.read_text()
40
+
41
+ return Result.ok(SolcResult(bin=bin_, abi=abi))
30
42
  except Exception as e:
31
- return Err(f"exception: {e}")
43
+ return Result.err(e)
32
44
  finally:
33
- shutil.rmtree(work_dir, ignore_errors=True)
45
+ if work_dir_created:
46
+ shutil.rmtree(work_dir, ignore_errors=True)
mm_eth/tx.py CHANGED
@@ -2,15 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
+ import eth_utils
5
6
  import rlp
6
7
  from eth_account import Account
8
+ from eth_typing import HexStr
7
9
  from eth_utils import keccak, to_hex
8
10
  from pydantic import BaseModel
9
11
  from rlp.sedes import Binary, big_endian_int, binary
10
12
  from web3 import Web3
11
- from web3.auto import w3
12
-
13
- from mm_eth.utils import hex_to_bytes
14
13
 
15
14
 
16
15
  class SignedTx(BaseModel):
@@ -45,9 +44,9 @@ class RPLTransaction(rlp.Serializable): # type: ignore[misc]
45
44
  to: str | None = None,
46
45
  ) -> RPLTransaction:
47
46
  if to:
48
- to = hex_to_bytes(to) # type: ignore[assignment]
47
+ to = eth_utils.to_bytes(hexstr=HexStr(to)) # type:ignore[assignment]
49
48
  if data:
50
- data = hex_to_bytes(data) # type: ignore[assignment]
49
+ data = eth_utils.to_bytes(hexstr=HexStr(data)) # type:ignore[assignment]
51
50
  if not value:
52
51
  value = 0
53
52
  return RPLTransaction(nonce, gas_price, gas, to, value, data, v, int(r, 16), int(s, 16))
@@ -103,7 +102,7 @@ def sign_legacy_tx(
103
102
  if data:
104
103
  tx["data"] = data
105
104
 
106
- signed = w3.eth.account.sign_transaction(tx, private_key)
105
+ signed = Account.sign_transaction(tx, private_key)
107
106
  return SignedTx(tx_hash=to_hex(signed.hash), raw_tx=to_hex(signed.raw_transaction))
108
107
 
109
108
 
@@ -139,9 +138,9 @@ def sign_tx(
139
138
 
140
139
 
141
140
  def decode_raw_tx(raw_tx: str) -> DecodedRawTx:
142
- tx: Any = rlp.decode(hex_to_bytes(raw_tx), RPLTransaction)
143
- tx_hash = Web3.to_hex(keccak(hex_to_bytes(raw_tx)))
144
- from_ = w3.eth.account.recover_transaction(raw_tx)
141
+ tx: Any = rlp.decode(eth_utils.to_bytes(hexstr=HexStr(raw_tx)), RPLTransaction)
142
+ tx_hash = Web3.to_hex(keccak(eth_utils.to_bytes(hexstr=HexStr(raw_tx))))
143
+ from_ = Account.recover_transaction(raw_tx)
145
144
  to = Web3.to_checksum_address(tx.to) if tx.to else None
146
145
  data = Web3.to_hex(tx.data)
147
146
  r = hex(tx.r)
mm_eth/utils.py CHANGED
@@ -1,216 +1,3 @@
1
- import re
2
- from decimal import Decimal, localcontext
3
- from typing import Any, cast
4
-
5
- import aiohttp
6
- import eth_utils
7
- import pydash
8
- from aiohttp_socks import ProxyConnector
9
- from eth_typing import HexStr
10
- from hexbytes import HexBytes
11
- from mm_std import Err, Ok, Result, number_with_separator
12
- from pydantic import BaseModel
13
- from web3 import AsyncWeb3, Web3
14
- from web3.types import Wei
15
-
16
-
17
- def parse_addresses(data: str) -> list[str]:
18
- result = []
19
- for word in data.lower().split():
20
- if len(word) == 42 and re.match("0x[a-f0-9]{40}", word):
21
- result.append(word) # noqa: PERF401
22
- return pydash.uniq(result)
23
-
24
-
25
- def to_token_wei(value: str | int, decimals: int) -> int:
26
- if isinstance(value, int):
27
- return value
28
- if isinstance(value, str):
29
- value = value.lower().replace(" ", "").strip()
30
- if value.endswith("t"):
31
- value = value.replace("t", "")
32
- return int(Decimal(value) * 10**decimals)
33
- if value.isdigit():
34
- return int(value)
35
-
36
- raise ValueError("wrong value" + value)
37
-
38
-
39
- def to_wei(value: str | int | Decimal, decimals: int | None = None) -> Wei:
40
- if isinstance(value, int):
41
- return Wei(value)
42
- if isinstance(value, Decimal):
43
- if value != value.to_integral_value():
44
- raise ValueError(f"value must be integral number: {value}")
45
- return Wei(int(value))
46
- if isinstance(value, str):
47
- value = value.lower().replace(" ", "").strip()
48
- if value.endswith("navax"): # https://snowtrace.io/unitconverter
49
- value = value.replace("navax", "")
50
- return Wei(int(Decimal(value) * 10**9))
51
- if value.endswith("gwei"):
52
- value = value.replace("gwei", "")
53
- return Wei(int(Decimal(value) * 1000000000))
54
- if value.endswith("ether"):
55
- value = value.replace("ether", "")
56
- return Wei(int(Decimal(value) * 1000000000000000000))
57
- if value.endswith("eth"):
58
- value = value.replace("eth", "")
59
- return Wei(int(Decimal(value) * 1000000000000000000))
60
- if value.endswith("t"):
61
- if decimals is None:
62
- raise ValueError("t without decimals")
63
- value = value.removesuffix("t")
64
- return Wei(int(Decimal(value) * 10**decimals))
65
- if value.isdigit():
66
- return Wei(int(value))
67
- raise ValueError("wrong value " + value)
68
-
69
- raise ValueError(f"value has a wrong type: {type(value)}")
70
-
71
-
72
- def from_wei(
73
- value: int,
74
- unit: str,
75
- round_ndigits: int | None = None,
76
- decimals: int | None = None,
77
- ) -> Decimal:
78
- if value == 0:
79
- return Decimal(0)
80
-
81
- is_negative = value < 0
82
- if unit.lower() == "eth":
83
- unit = "ether"
84
-
85
- if unit.lower() == "t":
86
- if decimals is None:
87
- raise ValueError("t without decimals")
88
- with localcontext() as ctx:
89
- ctx.prec = 999
90
- res = Decimal(value=abs(value), context=ctx) / Decimal(10**decimals)
91
- else:
92
- res = cast(Decimal, eth_utils.from_wei(abs(value), unit))
93
- if round_ndigits is not None:
94
- res = round(res, ndigits=round_ndigits)
95
- return -1 * res if is_negative else res
96
-
97
-
98
- def from_wei_str(
99
- value: int,
100
- unit: str,
101
- round_ndigits: int | None = None,
102
- print_unit_name: bool = True,
103
- decimals: int | None = None,
104
- ) -> str:
105
- res = format(from_wei(value, unit, round_ndigits, decimals=decimals), "f")
106
- if unit == "ether":
107
- unit = "eth"
108
- if print_unit_name:
109
- res += unit
110
- return res
111
-
112
-
113
- def from_token_wei_str(value: int, decimals: int, symbol: str = "", round_ndigits: int | None = None) -> str:
114
- res = value / 10**decimals
115
- if round_ndigits is not None:
116
- res = round(res, ndigits=round_ndigits)
117
- if symbol:
118
- res = f"{res} {symbol}"
119
- return str(res)
120
-
121
-
122
- def to_wei_token(value: str | int | Decimal, symbol: str, decimals: int) -> int:
123
- if isinstance(value, int):
124
- return value
125
- if isinstance(value, Decimal):
126
- if value != value.to_integral_value():
127
- raise ValueError(f"value must be integral number: {value}")
128
- return int(value)
129
- if isinstance(value, str):
130
- value = value.lower().replace(" ", "").strip()
131
- if value.isdigit():
132
- return int(value)
133
- try:
134
- return int(Decimal(value.replace(symbol.lower(), "").strip()) * (10**decimals))
135
- except Exception as e:
136
- raise ValueError from e
137
- else:
138
- raise TypeError(f"value has a wrong type: {type(value)}")
139
-
140
-
141
- def to_checksum_address(address: str) -> str:
142
- return Web3.to_checksum_address(address)
143
-
144
-
145
- def hex_to_bytes(data: str) -> bytes:
146
- return eth_utils.to_bytes(hexstr=HexStr(data))
147
-
148
-
149
- def get_chain_name(chain_id: int | str) -> str:
150
- chain_id = str(chain_id)
151
- if chain_id == "1":
152
- return "mainnet"
153
- if chain_id == "3":
154
- return "ropsten"
155
- if chain_id == "5":
156
- return "goerli"
157
- return chain_id
158
-
159
-
160
- def to_human_readable_tx(tx: dict[str, Any] | BaseModel) -> dict[str, object]:
161
- if isinstance(tx, BaseModel):
162
- tx = tx.model_dump()
163
- tx["human_readable"] = {}
164
- tx["human_readable"]["gas_price"] = str(tx["gas_price"] / 10**9) + " gwei"
165
- tx["human_readable"]["value"] = str(tx["value"] / 10**18) + " ether"
166
- tx["human_readable"]["gas"] = number_with_separator(tx["gas"])
167
- if tx.get("chain_id") is not None:
168
- tx["human_readable"]["chain_id"] = get_chain_name(tx["chain_id"])
169
-
170
- return tx
171
-
172
-
173
- def truncate_hex_str(hex_str: str, digits: int = 4, replace_str: str = "...") -> str:
174
- if not hex_str.startswith("0x") and not hex_str.startswith("0X"):
175
- raise ValueError("truncate_hex_str: hex_str must start with 0x")
176
- if digits <= 0:
177
- raise ValueError("truncate_hex_str: digits must be more than zero")
178
- hex_str = hex_str.removeprefix("0x").removeprefix("0X")
179
- if digits * 2 >= len(hex_str):
180
- raise ValueError("truncate_hex_str: digits is too large")
181
- return "0x" + hex_str[:digits] + replace_str + hex_str[-1 * digits :]
182
-
183
-
184
- def log_topic_to_address(topic: HexBytes | str) -> str:
185
- result = topic.hex()[-40:] if isinstance(topic, HexBytes) else topic[-40:]
186
- if not result.startswith("0x"):
187
- result = f"0x{result}"
188
- return result
189
-
190
-
191
- def get_w3(rpc_url: str, timeout: float | None = None, proxy: str | None = None) -> Web3:
192
- request_kwargs: dict[str, object] = {"timeout": timeout}
193
- if proxy:
194
- request_kwargs["proxies"] = {"http": proxy, "https": proxy}
195
- return Web3(Web3.HTTPProvider(rpc_url, request_kwargs=request_kwargs))
196
-
197
-
198
- async def get_async_w3(rpc_url: str, timeout: float | None = None, proxy: str | None = None) -> AsyncWeb3:
199
- # TODO: Don't use async w3. AsyncHTTPProvider uses threads
200
- # check its constructor: self._request_session_manager = HTTPSessionManager()
201
- request_kwargs: dict[str, object] = {"timeout": timeout}
202
- if proxy and proxy.startswith("http"):
203
- request_kwargs["proxy"] = proxy
204
- provider = AsyncWeb3.AsyncHTTPProvider(rpc_url, request_kwargs=request_kwargs, exception_retry_configuration=None)
205
- w3 = AsyncWeb3(provider)
206
-
207
- if proxy and proxy.startswith("socks"):
208
- session = aiohttp.ClientSession(connector=ProxyConnector.from_url(proxy))
209
- await provider.cache_async_session(session)
210
-
211
- return w3
212
-
213
-
214
1
  def name_network(chain_id: int) -> str:
215
2
  match chain_id:
216
3
  case 1:
@@ -235,14 +22,3 @@ def name_network(chain_id: int) -> str:
235
22
  return "Arbitrum Goerli"
236
23
  case _:
237
24
  return ""
238
-
239
-
240
- def hex_str_to_int(value: str) -> Result[int]:
241
- try:
242
- return Ok(int(value, 16))
243
- except Exception:
244
- return Err(f"can't convert to int: {value}")
245
-
246
-
247
- def to_hex(data: bytes | int | bool) -> str:
248
- return Web3.to_hex(data)
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-eth
3
- Version: 0.5.17
3
+ Version: 0.6.0
4
4
  Requires-Python: >=3.12
5
5
  Requires-Dist: aiohttp-socks~=0.10.1
6
- Requires-Dist: mm-crypto-utils>=0.2.15
6
+ Requires-Dist: mm-crypto-utils>=0.3.5
7
+ Requires-Dist: mm-std~=0.4.10
7
8
  Requires-Dist: typer>=0.15.2
8
9
  Requires-Dist: web3~=7.10.0
9
10
  Requires-Dist: websocket-client~=1.8.0
@@ -0,0 +1,32 @@
1
+ mm_eth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mm_eth/abi.py,sha256=gsIsaq5V8QGdYY424c_t_uVRfg0of7Gk9h0LcW3tkjU,4806
3
+ mm_eth/account.py,sha256=syA89ihOBTA2FiDK3o55oLBVEgPN1rQT_3bUQN05vuk,3044
4
+ mm_eth/anvil.py,sha256=NY1TxYnamfm_47HqCbgUCRdQxPgmAcJ-9YkK3NZ0310,1631
5
+ mm_eth/converters.py,sha256=smL3Bsky1pYEre2kPhsb4arXoQC_u80P5ilU9NRvr44,2043
6
+ mm_eth/deploy.py,sha256=SB3ruY808_5UnG8kHR34uVP66P3zOWZu0ImKD7UUv2s,691
7
+ mm_eth/erc20.py,sha256=Pxs_w95flqEUF4pJMoaHTfvud8x5Fb2UwU7iwMjdGCw,1143
8
+ mm_eth/retry.py,sha256=93MzazJnDZraLavtkBSPoUT-ZZWsLYq4HMf42OlLlBw,4799
9
+ mm_eth/rpc.py,sha256=3LE7ZwefYqwfb1RHcAhdNK99--0r1SW5Rfr9Wg9h50Q,9816
10
+ mm_eth/solc.py,sha256=2FGCJpbkCwIs8hGc8APpvC62gyAXGLeyQMrn4f9Bnow,1348
11
+ mm_eth/tx.py,sha256=zf5fieuKB-CTOiNNjz3siXU7yga7tlh0vkZTDAHvhnQ,4102
12
+ mm_eth/utils.py,sha256=TLyapZZ1lp_kW3vdkHmgR8gUtIQ1aEy_0GScxB6v5IQ,659
13
+ mm_eth/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ mm_eth/cli/calcs.py,sha256=N063Qw85iBNFbDtIjOSYJKHL0vU6wyfRfYAlI9TKeK0,496
15
+ mm_eth/cli/cli.py,sha256=Qac0HizKAyF1wocVvNK2578ntB2JT9kLHr1U5jfQj6E,5580
16
+ mm_eth/cli/cli_utils.py,sha256=jYDuSo-osvKPkd_OJFCHZDfGlFy3LKs-MVKHEnkCPdM,1416
17
+ mm_eth/cli/rpc_helpers.py,sha256=5fxiwFb16V0udY2MOb3bTpUrkf3Lhn-mMXFrdbmAKo4,1669
18
+ mm_eth/cli/validators.py,sha256=74AxIzryQ-D3f5xJYrOnh4AMFugTpsYhUJ_pzhHd2ek,1675
19
+ mm_eth/cli/cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ mm_eth/cli/cmd/balance_cmd.py,sha256=hujMv4Fvt4ZMZ3iPOZKdI2u2iN-ClYBmfMn_2Y4lRew,1946
21
+ mm_eth/cli/cmd/balances_cmd.py,sha256=VGCqcPCKPFF2O3WNlryg0nFnCkkKRAaubvpbtH0XTV0,4230
22
+ mm_eth/cli/cmd/deploy_cmd.py,sha256=IItdoOgYt1Q6SpSTxlOecxWikTn3b0_w1-MZkYIBKKI,1266
23
+ mm_eth/cli/cmd/node_cmd.py,sha256=JJSiKGG5OL9p7FuHhNeerjfes6Eo_StgLgpZEy3EUU0,2559
24
+ mm_eth/cli/cmd/solc_cmd.py,sha256=HDLThGAxk8MlJh_OBTsivoXYCKDlGnljRFp-BbYDlU0,714
25
+ mm_eth/cli/cmd/transfer_cmd.py,sha256=kKNoU_kNl2blz9ydbvhlEY1Kck0fCMv4RKwU84HyOSw,16933
26
+ mm_eth/cli/cmd/wallet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ mm_eth/cli/cmd/wallet/mnemonic_cmd.py,sha256=xE-5Ux9BdYsTZYBy0dMn9jupGhW4ced-AgYscy_wU_4,1007
28
+ mm_eth/cli/cmd/wallet/private_key_cmd.py,sha256=wuW_LvmIZiT9T4R8ir0wH1VQ_CEPjmrLgS-vzf0yv70,272
29
+ mm_eth-0.6.0.dist-info/METADATA,sha256=cLC3hqliuUgipT2KS3MSt4JYA-fEZX43oNaY-f2iRuc,275
30
+ mm_eth-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ mm_eth-0.6.0.dist-info/entry_points.txt,sha256=aGhpsozl8NIrkuUcX5fSgURCcDhr3ShUdeTSIrJq4oc,46
32
+ mm_eth-0.6.0.dist-info/RECORD,,
mm_eth/async_rpc.py DELETED
@@ -1,94 +0,0 @@
1
- import json
2
- from collections.abc import Sequence
3
- from typing import Any
4
-
5
- import websockets
6
- from mm_crypto_utils import Nodes, Proxies, random_node, random_proxy
7
- from mm_std import Err, Ok, Result, hra
8
-
9
- from mm_eth.utils import hex_str_to_int
10
-
11
-
12
- async def rpc_call(
13
- *,
14
- nodes: Nodes,
15
- method: str,
16
- params: Sequence[object],
17
- id_: int = 1,
18
- timeout: float = 10,
19
- proxies: Proxies = None,
20
- attempts: int = 1,
21
- ) -> Result[Any]:
22
- data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
23
- res: Result[Any] = Err("not started yet")
24
- for _ in range(attempts):
25
- node = random_node(nodes)
26
- res = (
27
- await _http_call(node, data, timeout, random_proxy(proxies))
28
- if node.startswith("http")
29
- else await _ws_call(node, data, timeout)
30
- )
31
- if isinstance(res, Ok):
32
- return res
33
- return res
34
-
35
-
36
- async def _http_call(node: str, data: dict[str, object], timeout: float, proxy: str | None) -> Result[Any]:
37
- res = await hra(node, method="POST", proxy=proxy, timeout=timeout, params=data, json_params=True)
38
- if res.is_error():
39
- return res.to_err_result()
40
- try:
41
- err = res.json.get("error", {}).get("message", "")
42
- if err:
43
- return res.to_err_result(f"service_error: {err}")
44
- if "result" in res.json:
45
- return res.to_ok_result(res.json["result"])
46
- return res.to_err_result("unknown_response")
47
- except Exception as err:
48
- return res.to_err_result(f"exception: {err}")
49
-
50
-
51
- async def _ws_call(node: str, data: dict[str, object], timeout: float) -> Result[Any]:
52
- try:
53
- async with websockets.connect(node, timeout=timeout) as ws:
54
- await ws.send(json.dumps(data))
55
- response = json.loads(await ws.recv())
56
-
57
- err = response.get("error", {}).get("message", "")
58
- if err:
59
- return Err(f"service_error: {err}")
60
- if "result" in response:
61
- return Ok(response["result"], response)
62
- return Err(f"unknown_response: {response}")
63
- except TimeoutError:
64
- return Err("timeout")
65
- except Exception as err:
66
- return Err(f"exception: {err}")
67
-
68
-
69
- async def eth_block_number(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[int]:
70
- return (
71
- await rpc_call(
72
- nodes=rpc_urls,
73
- method="eth_blockNumber",
74
- params=[],
75
- timeout=timeout,
76
- proxies=proxies,
77
- attempts=attempts,
78
- )
79
- ).and_then(hex_str_to_int)
80
-
81
-
82
- async def eth_get_balance(
83
- rpc_urls: Nodes, address: str, timeout: int = 10, proxies: Proxies = None, attempts: int = 1
84
- ) -> Result[int]:
85
- return (
86
- await rpc_call(
87
- nodes=rpc_urls,
88
- method="eth_getBalance",
89
- params=[address, "latest"],
90
- timeout=timeout,
91
- proxies=proxies,
92
- attempts=attempts,
93
- )
94
- ).and_then(hex_str_to_int)
@@ -1,44 +0,0 @@
1
- import json
2
- from logging import fatal
3
-
4
- from mm_std import BaseConfig, Err, print_plain
5
- from pydantic import StrictStr
6
-
7
- from mm_eth import abi, rpc
8
- from mm_eth.cli.cli_utils import BaseConfigParams
9
-
10
-
11
- class Config(BaseConfig):
12
- contract_address: StrictStr
13
- function_signature: str
14
- function_args: StrictStr = "[]"
15
- outputs_types: str | None = None
16
- node: str
17
-
18
-
19
- class CallContractCmdParams(BaseConfigParams):
20
- pass
21
-
22
-
23
- def run(cli_params: CallContractCmdParams) -> None:
24
- config = Config.read_toml_config_or_exit(cli_params.config_path)
25
- if cli_params.print_config:
26
- config.print_and_exit()
27
-
28
- input_data = abi.encode_function_input_by_signature(
29
- config.function_signature,
30
- json.loads(config.function_args.replace("'", '"')),
31
- )
32
- res = rpc.eth_call(config.node, config.contract_address, input_data)
33
- if isinstance(res, Err):
34
- return fatal(f"error: {res.err}")
35
-
36
- result = res.ok
37
- if config.outputs_types is not None:
38
- decode_res = abi.decode_data(_get_types(config.outputs_types), result)
39
- result = decode_res[0] if len(decode_res) == 1 else str(decode_res)
40
- print_plain(result)
41
-
42
-
43
- def _get_types(data: str) -> list[str]:
44
- return [t.strip() for t in data.split(",") if t.strip()]
@@ -1,10 +0,0 @@
1
- import json
2
-
3
- from mm_std import print_plain
4
-
5
- from mm_eth import abi
6
-
7
-
8
- def run(function_signature: str, args_str: str) -> None:
9
- args_str = args_str.replace("'", '"')
10
- print_plain(abi.encode_function_input_by_signature(function_signature, json.loads(args_str)))
@@ -1,9 +0,0 @@
1
- from pathlib import Path
2
-
3
- from mm_std import pretty_print_toml
4
-
5
-
6
- def run(command: str) -> None:
7
- command = command.replace("-", "_")
8
- example_file = Path(Path(__file__).parent.absolute(), "../examples", f"{command}.toml")
9
- pretty_print_toml(example_file.read_text())
mm_eth/cli/cmd/rpc_cmd.py DELETED
@@ -1,78 +0,0 @@
1
- import json
2
- from typing import cast
3
-
4
- from mm_std import fatal, hr, print_console, str_starts_with_any
5
- from rich import print_json
6
-
7
- from mm_eth.cli.cli_utils import public_rpc_url
8
-
9
-
10
- def run(rpc_url: str, method: str, params: str, hex2dec: bool) -> None:
11
- rpc_url = public_rpc_url(rpc_url)
12
- if not method:
13
- return list_all_methods()
14
- if not str_starts_with_any(rpc_url, ["http://", "https://"]):
15
- fatal(f"invalid rpc_url: {rpc_url}")
16
- params = params.replace("'", '"')
17
- data = {"jsonrpc": "2.0", "method": method, "params": parse_method_params(method, params), "id": 1}
18
- res = hr(rpc_url, method="POST", params=data, json_params=True)
19
- if res.json:
20
- print_json(data=res.json)
21
- result_value: str = res.json.get("result", "")
22
- if hex2dec and result_value.startswith(("0x", "0X")):
23
- print_console("hex2dec", int(result_value, 16))
24
- else:
25
- fatal(str(res))
26
-
27
-
28
- def parse_method_params(method: str, params_str: str) -> list[object]:
29
- params = json.loads(params_str) if params_str.startswith("[") else params_str.split()
30
- if method in ["eth_getBalance", "eth_getTransactionCount", "eth_getCode"] and len(params) == 1:
31
- params.append("latest")
32
- return cast(list[object], params)
33
-
34
-
35
- def list_all_methods() -> None:
36
- all_methods = """
37
- web3_clientVersion
38
- web3_sha3
39
- net_version
40
- net_listening
41
- net_peerCount
42
- eth_protocolVersion
43
- eth_syncing
44
- eth_chainId
45
- eth_gasPrice
46
- eth_accounts
47
- eth_blockNumber
48
- eth_getBalance
49
- eth_getStorageAt
50
- eth_getTransactionCount
51
- eth_getBlockTransactionCountByHash
52
- eth_getBlockTransactionCountByNumber
53
- eth_getUncleCountByBlockHash
54
- eth_getUncleCountByBlockNumber
55
- eth_getCode
56
- eth_sign
57
- eth_signTransaction
58
- eth_sendTransaction
59
- eth_sendRawTransaction
60
- eth_call
61
- eth_estimateGas
62
- eth_getBlockByHash
63
- eth_getBlockByNumber
64
- eth_getTransactionByHash
65
- eth_getTransactionByBlockHashAndIndex
66
- eth_getTransactionByBlockNumberAndIndex
67
- eth_getTransactionReceipt
68
- eth_getUncleByBlockHashAndIndex
69
- eth_getUncleByBlockNumberAndIndex
70
- eth_newFilter
71
- eth_newBlockFilter
72
- eth_newPendingTransactionFilter
73
- eth_uninstallFilter
74
- eth_getFilterChanges
75
- eth_getFilterLogs
76
- eth_getLogs
77
- """.strip()
78
- print_console(all_methods)
@@ -1,29 +0,0 @@
1
- from mm_std import Err, Ok, print_plain
2
-
3
- from mm_eth import erc20, rpc
4
- from mm_eth.cli import cli_utils
5
-
6
-
7
- def run(rpc_url: str, token_address: str) -> None:
8
- rpc_url = cli_utils.public_rpc_url(rpc_url)
9
- name = erc20.get_name(rpc_url, token_address).ok_or_err()
10
- symbol = erc20.get_symbol(rpc_url, token_address).ok_or_err()
11
- decimals = erc20.get_decimals(rpc_url, token_address).ok_or_err()
12
- transfer_count = _calc_transfer_events(rpc_url, 100, token_address)
13
-
14
- print_plain(f"name: {name}")
15
- print_plain(f"symbol: {symbol}")
16
- print_plain(f"decimals: {decimals}")
17
- print_plain(f"transfer_count: {transfer_count}")
18
-
19
-
20
- def _calc_transfer_events(rpc_url: str, last_block_limit: int, token_address: str) -> int | str:
21
- current_block_res = rpc.eth_block_number(rpc_url)
22
- if isinstance(current_block_res, Err):
23
- return current_block_res.err
24
- current_block = current_block_res.ok
25
-
26
- res = erc20.get_transfer_event_logs(rpc_url, token_address, current_block - last_block_limit, current_block)
27
- if isinstance(res, Ok):
28
- return len(res.ok)
29
- return res.err
mm_eth/cli/cmd/tx_cmd.py DELETED
@@ -1,16 +0,0 @@
1
- from eth_typing import HexStr
2
- from rich.pretty import pprint
3
- from web3 import Web3
4
-
5
- from mm_eth.cli.cli_utils import public_rpc_url
6
-
7
-
8
- def run(rpc_url: str, tx_hash: str, get_receipt: bool) -> None:
9
- rpc_url = public_rpc_url(rpc_url)
10
- w3 = Web3(Web3.HTTPProvider(rpc_url))
11
- transaction = w3.eth.get_transaction(HexStr(tx_hash))
12
- pprint(dict(transaction), expand_all=True)
13
-
14
- if get_receipt:
15
- receipt = w3.eth.get_transaction_receipt(HexStr(tx_hash))
16
- pprint(dict(receipt), expand_all=True)