mm-eth 0.1.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.
- mm_eth/__init__.py +0 -0
- mm_eth/abi/zksync.json +2092 -0
- mm_eth/abi.py +130 -0
- mm_eth/account.py +70 -0
- mm_eth/anvil.py +56 -0
- mm_eth/cli/__init__.py +0 -0
- mm_eth/cli/calcs.py +88 -0
- mm_eth/cli/cli.py +233 -0
- mm_eth/cli/cli_helpers.py +195 -0
- mm_eth/cli/cli_utils.py +150 -0
- mm_eth/cli/cmd/__init__.py +0 -0
- mm_eth/cli/cmd/balance_cmd.py +59 -0
- mm_eth/cli/cmd/balances_cmd.py +121 -0
- mm_eth/cli/cmd/call_contract_cmd.py +44 -0
- mm_eth/cli/cmd/config_example_cmd.py +9 -0
- mm_eth/cli/cmd/deploy_cmd.py +41 -0
- mm_eth/cli/cmd/encode_input_data_cmd.py +10 -0
- mm_eth/cli/cmd/mnemonic_cmd.py +27 -0
- mm_eth/cli/cmd/node_cmd.py +47 -0
- mm_eth/cli/cmd/private_key_cmd.py +10 -0
- mm_eth/cli/cmd/rpc_cmd.py +81 -0
- mm_eth/cli/cmd/send_contract_cmd.py +247 -0
- mm_eth/cli/cmd/solc_cmd.py +25 -0
- mm_eth/cli/cmd/token_cmd.py +29 -0
- mm_eth/cli/cmd/transfer_erc20_cmd.py +275 -0
- mm_eth/cli/cmd/transfer_eth_cmd.py +252 -0
- mm_eth/cli/cmd/vault_cmd.py +16 -0
- mm_eth/cli/config_examples/balances.yml +15 -0
- mm_eth/cli/config_examples/call_contract.yml +5 -0
- mm_eth/cli/config_examples/transfer_erc20.yml +26 -0
- mm_eth/cli/config_examples/transfer_eth.yml +24 -0
- mm_eth/cli/validators.py +84 -0
- mm_eth/deploy.py +20 -0
- mm_eth/ens.py +16 -0
- mm_eth/erc20.py +240 -0
- mm_eth/ethernodes.py +34 -0
- mm_eth/py.typed +0 -0
- mm_eth/rpc.py +478 -0
- mm_eth/services/__init__.py +0 -0
- mm_eth/solc.py +34 -0
- mm_eth/tx.py +164 -0
- mm_eth/types.py +5 -0
- mm_eth/utils.py +245 -0
- mm_eth/vault.py +38 -0
- mm_eth/zksync.py +203 -0
- mm_eth-0.1.0.dist-info/METADATA +24 -0
- mm_eth-0.1.0.dist-info/RECORD +50 -0
- mm_eth-0.1.0.dist-info/WHEEL +5 -0
- mm_eth-0.1.0.dist-info/entry_points.txt +2 -0
- mm_eth-0.1.0.dist-info/top_level.txt +1 -0
mm_eth/rpc.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
import websocket
|
|
8
|
+
from mm_std import Err, Ok, Result, hr, random_choice
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from web3.types import BlockIdentifier
|
|
11
|
+
|
|
12
|
+
from mm_eth.types import Nodes, Proxies
|
|
13
|
+
from mm_eth.utils import hex_str_to_int, random_node, random_proxy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class TxReceipt:
|
|
18
|
+
tx_hash: str
|
|
19
|
+
tx_index: int
|
|
20
|
+
block_number: int
|
|
21
|
+
from_address: str
|
|
22
|
+
to_address: str | None
|
|
23
|
+
contract_address: str | None
|
|
24
|
+
status: int | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Log:
|
|
29
|
+
address: str
|
|
30
|
+
block_hash: str
|
|
31
|
+
block_number: int
|
|
32
|
+
data: str
|
|
33
|
+
log_index: int
|
|
34
|
+
removed: bool
|
|
35
|
+
topics: list[str]
|
|
36
|
+
transaction_hash: str
|
|
37
|
+
transaction_index: int
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_json_rpc_dict(cls, data: dict[str, Any]) -> Result[Log]:
|
|
41
|
+
try:
|
|
42
|
+
return Ok(
|
|
43
|
+
Log(
|
|
44
|
+
address=data["address"],
|
|
45
|
+
block_hash=data["blockHash"],
|
|
46
|
+
block_number=int(data["blockNumber"], 16),
|
|
47
|
+
data=data["data"],
|
|
48
|
+
log_index=int(data["logIndex"], 16),
|
|
49
|
+
removed=data["removed"],
|
|
50
|
+
topics=data["topics"],
|
|
51
|
+
transaction_hash=data["transactionHash"],
|
|
52
|
+
transaction_index=int(data["transactionIndex"], 16),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
except Exception as err:
|
|
56
|
+
return Err(f"exception: {err}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TxData(BaseModel):
|
|
60
|
+
block_number: int | None # for pending tx it can be none
|
|
61
|
+
from_: str
|
|
62
|
+
to: str | None
|
|
63
|
+
gas: int
|
|
64
|
+
gas_price: int
|
|
65
|
+
value: int
|
|
66
|
+
hash: str
|
|
67
|
+
input: str
|
|
68
|
+
nonce: int
|
|
69
|
+
v: int
|
|
70
|
+
r: str
|
|
71
|
+
s: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def rpc_call(
|
|
75
|
+
*,
|
|
76
|
+
nodes: Nodes,
|
|
77
|
+
method: str,
|
|
78
|
+
params: list[object],
|
|
79
|
+
id_: int = 1,
|
|
80
|
+
timeout: int = 10,
|
|
81
|
+
proxies: Proxies = None,
|
|
82
|
+
attempts: int = 1,
|
|
83
|
+
) -> Result[Any]:
|
|
84
|
+
data = {"jsonrpc": "2.0", "method": method, "params": params, "id": id_}
|
|
85
|
+
res: Result[Any] = Err("not started yet")
|
|
86
|
+
for _ in range(attempts):
|
|
87
|
+
node = random_node(nodes)
|
|
88
|
+
if node.startswith("http"):
|
|
89
|
+
res = _http_call(node, data, timeout, random_proxy(proxies))
|
|
90
|
+
else:
|
|
91
|
+
res = _ws_call(node, data, timeout)
|
|
92
|
+
if isinstance(res, Ok):
|
|
93
|
+
return res
|
|
94
|
+
return res
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _http_call(node: str, data: dict[str, object], timeout: int, proxy: str | None) -> Result[Any]:
|
|
98
|
+
res = hr(node, method="POST", proxy=proxy, timeout=timeout, params=data, json_params=True)
|
|
99
|
+
try:
|
|
100
|
+
if res.is_error():
|
|
101
|
+
return res.to_err_result()
|
|
102
|
+
|
|
103
|
+
err = res.json.get("error", {}).get("message", "")
|
|
104
|
+
if err:
|
|
105
|
+
return res.to_err_result(f"service_error: {err}")
|
|
106
|
+
if "result" in res.json:
|
|
107
|
+
return res.to_ok_result(res.json["result"])
|
|
108
|
+
|
|
109
|
+
return res.to_err_result("unknown_response")
|
|
110
|
+
except Exception as err:
|
|
111
|
+
return res.to_err_result(f"exception: {err}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _ws_call(node: str, data: dict[str, object], timeout: int) -> Result[Any]:
|
|
115
|
+
try:
|
|
116
|
+
ws = websocket.create_connection(node, timeout=timeout)
|
|
117
|
+
ws.send(json.dumps(data))
|
|
118
|
+
response = json.loads(ws.recv())
|
|
119
|
+
ws.close()
|
|
120
|
+
err = response.get("error", {}).get("message", "")
|
|
121
|
+
if err:
|
|
122
|
+
return Err(f"service_error: {err}")
|
|
123
|
+
if "result" in response:
|
|
124
|
+
return Ok(response["result"])
|
|
125
|
+
return Err(f"unknown_response: {response}")
|
|
126
|
+
except TimeoutError:
|
|
127
|
+
return Err("timeout")
|
|
128
|
+
except Exception as err:
|
|
129
|
+
return Err(f"exception: {err}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def eth_block_number(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[int]:
|
|
133
|
+
return rpc_call(
|
|
134
|
+
nodes=rpc_urls,
|
|
135
|
+
method="eth_blockNumber",
|
|
136
|
+
params=[],
|
|
137
|
+
timeout=timeout,
|
|
138
|
+
proxies=proxies,
|
|
139
|
+
attempts=attempts,
|
|
140
|
+
).and_then(hex_str_to_int)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def eth_chain_id(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[int]:
|
|
144
|
+
return rpc_call(
|
|
145
|
+
nodes=rpc_urls,
|
|
146
|
+
method="eth_chainId",
|
|
147
|
+
params=[],
|
|
148
|
+
timeout=timeout,
|
|
149
|
+
proxies=proxies,
|
|
150
|
+
attempts=attempts,
|
|
151
|
+
).and_then(hex_str_to_int)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def net_peer_count(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[int]:
|
|
155
|
+
return rpc_call(
|
|
156
|
+
nodes=rpc_urls,
|
|
157
|
+
method="net_peerCount",
|
|
158
|
+
params=[],
|
|
159
|
+
timeout=timeout,
|
|
160
|
+
proxies=proxies,
|
|
161
|
+
attempts=attempts,
|
|
162
|
+
).and_then(hex_str_to_int)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def web3_client_version(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[str]:
|
|
166
|
+
return rpc_call(
|
|
167
|
+
nodes=rpc_urls,
|
|
168
|
+
method="web3_clientVersion",
|
|
169
|
+
params=[],
|
|
170
|
+
timeout=timeout,
|
|
171
|
+
proxies=proxies,
|
|
172
|
+
attempts=attempts,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def net_version(nodes: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[str]:
|
|
177
|
+
return rpc_call(nodes=nodes, method="net_version", params=[], timeout=timeout, proxies=proxies, attempts=attempts)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def eth_get_code(rpc_urls: Nodes, address: str, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[str]:
|
|
181
|
+
return rpc_call(
|
|
182
|
+
nodes=rpc_urls,
|
|
183
|
+
method="eth_getCode",
|
|
184
|
+
params=[address, "latest"],
|
|
185
|
+
timeout=timeout,
|
|
186
|
+
proxies=proxies,
|
|
187
|
+
attempts=attempts,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def eth_send_raw_transaction(
|
|
192
|
+
rpc_urls: Nodes,
|
|
193
|
+
raw_tx: str,
|
|
194
|
+
timeout: int = 10,
|
|
195
|
+
proxies: Proxies = None,
|
|
196
|
+
attempts: int = 1,
|
|
197
|
+
) -> Result[str]:
|
|
198
|
+
return rpc_call(
|
|
199
|
+
nodes=rpc_urls,
|
|
200
|
+
method="eth_sendRawTransaction",
|
|
201
|
+
params=[raw_tx],
|
|
202
|
+
timeout=timeout,
|
|
203
|
+
proxies=proxies,
|
|
204
|
+
attempts=attempts,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def eth_get_balance(rpc_urls: Nodes, address: str, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[int]:
|
|
209
|
+
return rpc_call(
|
|
210
|
+
nodes=rpc_urls,
|
|
211
|
+
method="eth_getBalance",
|
|
212
|
+
params=[address, "latest"],
|
|
213
|
+
timeout=timeout,
|
|
214
|
+
proxies=proxies,
|
|
215
|
+
attempts=attempts,
|
|
216
|
+
).and_then(hex_str_to_int)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def eth_get_transaction_count(
|
|
220
|
+
rpc_urls: Nodes,
|
|
221
|
+
address: str,
|
|
222
|
+
timeout: int = 10,
|
|
223
|
+
proxies: Proxies = None,
|
|
224
|
+
attempts: int = 1,
|
|
225
|
+
) -> Result[int]:
|
|
226
|
+
return rpc_call(
|
|
227
|
+
nodes=rpc_urls,
|
|
228
|
+
method="eth_getTransactionCount",
|
|
229
|
+
params=[address, "latest"],
|
|
230
|
+
timeout=timeout,
|
|
231
|
+
proxies=proxies,
|
|
232
|
+
attempts=attempts,
|
|
233
|
+
).and_then(hex_str_to_int)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def eth_get_block_by_number(
|
|
237
|
+
rpc_urls: Nodes,
|
|
238
|
+
block_number: BlockIdentifier,
|
|
239
|
+
full_transaction: bool = False,
|
|
240
|
+
timeout: int = 10,
|
|
241
|
+
proxies: Proxies = None,
|
|
242
|
+
attempts: int = 1,
|
|
243
|
+
) -> Result[dict[str, Any]]:
|
|
244
|
+
return rpc_call(
|
|
245
|
+
nodes=rpc_urls,
|
|
246
|
+
method="eth_getBlockByNumber",
|
|
247
|
+
params=[hex(block_number) if isinstance(block_number, int) else block_number, full_transaction],
|
|
248
|
+
timeout=timeout,
|
|
249
|
+
proxies=proxies,
|
|
250
|
+
attempts=attempts,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def eth_get_logs(
|
|
255
|
+
rpc_urls: Nodes,
|
|
256
|
+
*,
|
|
257
|
+
address: str | None = None,
|
|
258
|
+
topics: list[str] | None = None,
|
|
259
|
+
from_block: BlockIdentifier | None = None,
|
|
260
|
+
to_block: BlockIdentifier | None = None,
|
|
261
|
+
timeout: int = 10,
|
|
262
|
+
proxies: Proxies = None,
|
|
263
|
+
attempts: int = 1,
|
|
264
|
+
) -> Result[list[Log]]:
|
|
265
|
+
params: dict[str, object] = {}
|
|
266
|
+
if address:
|
|
267
|
+
params["address"] = address
|
|
268
|
+
if isinstance(from_block, int):
|
|
269
|
+
params["fromBlock"] = hex(from_block)
|
|
270
|
+
else:
|
|
271
|
+
params["fromBlock"] = "earliest"
|
|
272
|
+
if isinstance(to_block, int):
|
|
273
|
+
params["toBlock"] = hex(to_block)
|
|
274
|
+
if topics:
|
|
275
|
+
params["topics"] = topics
|
|
276
|
+
|
|
277
|
+
res = rpc_call(nodes=rpc_urls, method="eth_getLogs", params=[params], proxies=proxies, attempts=attempts, timeout=timeout)
|
|
278
|
+
if isinstance(res, Err):
|
|
279
|
+
return res
|
|
280
|
+
|
|
281
|
+
result: list[Log] = []
|
|
282
|
+
for log_data in res.ok:
|
|
283
|
+
log_res = Log.from_json_rpc_dict(log_data)
|
|
284
|
+
if isinstance(log_res, Err):
|
|
285
|
+
return Err(log_res.err, data=res.data)
|
|
286
|
+
result.append(log_res.ok)
|
|
287
|
+
return Ok(result, data=res.data)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def eth_get_transaction_receipt(
|
|
291
|
+
rpc_urls: Nodes,
|
|
292
|
+
tx_hash: str,
|
|
293
|
+
timeout: int = 10,
|
|
294
|
+
proxies: Proxies = None,
|
|
295
|
+
attempts: int = 1,
|
|
296
|
+
) -> Result[TxReceipt]:
|
|
297
|
+
res = rpc_call(
|
|
298
|
+
nodes=rpc_urls,
|
|
299
|
+
method="eth_getTransactionReceipt",
|
|
300
|
+
params=[tx_hash],
|
|
301
|
+
timeout=timeout,
|
|
302
|
+
proxies=proxies,
|
|
303
|
+
attempts=attempts,
|
|
304
|
+
)
|
|
305
|
+
if isinstance(res, Err):
|
|
306
|
+
return res
|
|
307
|
+
|
|
308
|
+
if res.ok is None:
|
|
309
|
+
return Err("no_receipt", data=res.data)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
status = None
|
|
313
|
+
receipt = cast(dict[str, Any], res.ok)
|
|
314
|
+
if "status" in receipt:
|
|
315
|
+
status = int(receipt["status"], 16)
|
|
316
|
+
return Ok(
|
|
317
|
+
TxReceipt(
|
|
318
|
+
tx_hash=tx_hash,
|
|
319
|
+
tx_index=int(receipt["transactionIndex"], 16),
|
|
320
|
+
block_number=int(receipt["blockNumber"], 16),
|
|
321
|
+
from_address=receipt["from"],
|
|
322
|
+
to_address=receipt.get("to"),
|
|
323
|
+
contract_address=receipt.get("contractAddress"),
|
|
324
|
+
status=status,
|
|
325
|
+
),
|
|
326
|
+
data=res.data,
|
|
327
|
+
)
|
|
328
|
+
except Exception as err:
|
|
329
|
+
return Err(f"exception: {err}", data=res.data)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def eth_get_transaction_by_hash(
|
|
333
|
+
rpc_urls: Nodes,
|
|
334
|
+
tx_hash: str,
|
|
335
|
+
timeout: int = 10,
|
|
336
|
+
proxies: Proxies = None,
|
|
337
|
+
attempts: int = 1,
|
|
338
|
+
) -> Result[TxData]:
|
|
339
|
+
res = rpc_call(
|
|
340
|
+
nodes=rpc_urls,
|
|
341
|
+
method="eth_getTransactionByHash",
|
|
342
|
+
params=[tx_hash],
|
|
343
|
+
timeout=timeout,
|
|
344
|
+
proxies=proxies,
|
|
345
|
+
attempts=attempts,
|
|
346
|
+
)
|
|
347
|
+
if isinstance(res, Err):
|
|
348
|
+
return res
|
|
349
|
+
if res.ok is None:
|
|
350
|
+
return Err("not_found", data=res.data)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
tx = res.ok
|
|
354
|
+
return Ok(
|
|
355
|
+
TxData(
|
|
356
|
+
block_number=int(tx["blockNumber"], 16) if tx["blockNumber"] is not None else None,
|
|
357
|
+
from_=tx["from"],
|
|
358
|
+
to=tx.get("to"),
|
|
359
|
+
gas=int(tx["gas"], 16),
|
|
360
|
+
gas_price=int(tx["gasPrice"], 16),
|
|
361
|
+
value=int(tx["value"], 16),
|
|
362
|
+
nonce=int(tx["nonce"], 16),
|
|
363
|
+
input=tx["input"],
|
|
364
|
+
hash=tx_hash,
|
|
365
|
+
v=int(tx["v"], 16),
|
|
366
|
+
r=tx.get("r"),
|
|
367
|
+
s=tx.get("s"),
|
|
368
|
+
),
|
|
369
|
+
data=res.data,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
except Exception as err:
|
|
373
|
+
return Err(f"exception: {err}", data=res.data)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def eth_call(
|
|
377
|
+
rpc_urls: Nodes,
|
|
378
|
+
to: str,
|
|
379
|
+
data: str,
|
|
380
|
+
timeout: int = 10,
|
|
381
|
+
proxies: Proxies = None,
|
|
382
|
+
attempts: int = 1,
|
|
383
|
+
) -> Result[str]:
|
|
384
|
+
return rpc_call(
|
|
385
|
+
nodes=rpc_urls,
|
|
386
|
+
method="eth_call",
|
|
387
|
+
params=[{"to": to, "data": data}, "latest"],
|
|
388
|
+
timeout=timeout,
|
|
389
|
+
proxies=proxies,
|
|
390
|
+
attempts=attempts,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def eth_estimate_gas(
|
|
395
|
+
rpc_urls: Nodes,
|
|
396
|
+
from_: str,
|
|
397
|
+
to: str | None = None,
|
|
398
|
+
value: int | None = 0,
|
|
399
|
+
data: str | None = None,
|
|
400
|
+
type_: Literal["0x0", "0x2"] | None = None,
|
|
401
|
+
timeout: int = 10,
|
|
402
|
+
proxies: Proxies = None,
|
|
403
|
+
attempts: int = 1,
|
|
404
|
+
) -> Result[int]:
|
|
405
|
+
params: dict[str, Any] = {"from": from_}
|
|
406
|
+
if to:
|
|
407
|
+
params["to"] = to
|
|
408
|
+
if data:
|
|
409
|
+
params["data"] = data
|
|
410
|
+
if value:
|
|
411
|
+
params["value"] = hex(value)
|
|
412
|
+
if type_:
|
|
413
|
+
params["type"] = type_
|
|
414
|
+
return rpc_call(
|
|
415
|
+
nodes=rpc_urls,
|
|
416
|
+
method="eth_estimateGas",
|
|
417
|
+
params=[params],
|
|
418
|
+
timeout=timeout,
|
|
419
|
+
proxies=proxies,
|
|
420
|
+
attempts=attempts,
|
|
421
|
+
).and_then(hex_str_to_int)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def eth_gas_price(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[int]:
|
|
425
|
+
return rpc_call(
|
|
426
|
+
nodes=rpc_urls,
|
|
427
|
+
method="eth_gasPrice",
|
|
428
|
+
params=[],
|
|
429
|
+
timeout=timeout,
|
|
430
|
+
proxies=proxies,
|
|
431
|
+
attempts=attempts,
|
|
432
|
+
).and_then(hex_str_to_int)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def eth_syncing(rpc_urls: Nodes, timeout: int = 10, proxies: Proxies = None, attempts: int = 1) -> Result[bool | dict[str, int]]:
|
|
436
|
+
res = rpc_call(nodes=rpc_urls, method="eth_syncing", params=[], timeout=timeout, proxies=proxies, attempts=attempts)
|
|
437
|
+
if isinstance(res, Err):
|
|
438
|
+
return res
|
|
439
|
+
|
|
440
|
+
if isinstance(res.ok, dict):
|
|
441
|
+
result = {}
|
|
442
|
+
for k, v in res.ok.items():
|
|
443
|
+
if v:
|
|
444
|
+
result[k] = int(v, 16)
|
|
445
|
+
else:
|
|
446
|
+
result[k] = v
|
|
447
|
+
if result.get("currentBlock", None) and result.get("highestBlock", None):
|
|
448
|
+
result["remaining"] = result["highestBlock"] - result["currentBlock"]
|
|
449
|
+
return Ok(result, res.data)
|
|
450
|
+
|
|
451
|
+
return res
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def get_tx_status(rpc_urls: Nodes, tx_hash: str, timeout: int = 5, proxies: Proxies = None, attempts: int = 5) -> Result[int]:
|
|
455
|
+
res: Result[int] = Err("not started yet")
|
|
456
|
+
for _ in range(attempts):
|
|
457
|
+
node = cast(str, random_choice(rpc_urls))
|
|
458
|
+
cast(str | None, random_choice(proxies))
|
|
459
|
+
receipt_res = eth_get_transaction_receipt(node, tx_hash, timeout, proxies=proxies, attempts=1)
|
|
460
|
+
if isinstance(receipt_res, Err) and receipt_res.err == "no_receipt":
|
|
461
|
+
return receipt_res
|
|
462
|
+
if isinstance(receipt_res, Ok) and receipt_res.ok.status is None:
|
|
463
|
+
return Err("no_status", data=res.data)
|
|
464
|
+
|
|
465
|
+
if isinstance(receipt_res, Ok):
|
|
466
|
+
return Ok(cast(int, receipt_res.ok.status), data=receipt_res.data)
|
|
467
|
+
res = receipt_res
|
|
468
|
+
|
|
469
|
+
return res
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def get_base_fee_per_gas(rpc_urls: Nodes, timeout: int = 5, proxies: Proxies = None, attempts: int = 5) -> Result[int]:
|
|
473
|
+
res = eth_get_block_by_number(rpc_urls, "latest", False, timeout=timeout, proxies=proxies, attempts=attempts)
|
|
474
|
+
if isinstance(res, Err):
|
|
475
|
+
return res
|
|
476
|
+
if "baseFeePerGas" in res.ok:
|
|
477
|
+
return Ok(int(res.ok["baseFeePerGas"], 16), data=res.data)
|
|
478
|
+
return Err("no_base_fee_per_gas", data=res.data)
|
|
File without changes
|
mm_eth/solc.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import random
|
|
3
|
+
import shutil
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mm_std import Err, Ok, Result, run_command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SolcResult:
|
|
12
|
+
bin: str
|
|
13
|
+
abi: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def solc(contract_name: str, contract_path: str, tmp_dir: str) -> Result[SolcResult]:
|
|
17
|
+
if tmp_dir.startswith("~"):
|
|
18
|
+
tmp_dir = os.path.expanduser(tmp_dir)
|
|
19
|
+
if contract_path.startswith("~"):
|
|
20
|
+
contract_path = os.path.expanduser(contract_path)
|
|
21
|
+
work_dir = f"{tmp_dir}/solc_{contract_name}_{random.randint(0, 100_000_000)}"
|
|
22
|
+
abi_path = f"{work_dir}/{contract_name}.abi"
|
|
23
|
+
bin_path = f"{work_dir}/{contract_name}.bin"
|
|
24
|
+
try:
|
|
25
|
+
Path(work_dir).mkdir(parents=True)
|
|
26
|
+
cmd = f"solc -o '{work_dir}' --abi --bin --optimize {contract_path}"
|
|
27
|
+
run_command(cmd)
|
|
28
|
+
abi = Path(abi_path).read_text()
|
|
29
|
+
bin_ = Path(bin_path).read_text()
|
|
30
|
+
return Ok(SolcResult(bin=bin_, abi=abi))
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return Err(f"exception: {e}")
|
|
33
|
+
finally:
|
|
34
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
mm_eth/tx.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import rlp
|
|
6
|
+
from eth_utils import keccak
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from rlp.sedes import Binary, big_endian_int, binary
|
|
9
|
+
from web3 import Web3
|
|
10
|
+
from web3.auto import w3
|
|
11
|
+
|
|
12
|
+
from mm_eth.utils import hex_to_bytes
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SignedTx(BaseModel):
|
|
16
|
+
tx_hash: str
|
|
17
|
+
raw_tx: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RPLTransaction(rlp.Serializable): # type: ignore[misc]
|
|
21
|
+
fields = [ # noqa: RUF012
|
|
22
|
+
("nonce", big_endian_int),
|
|
23
|
+
("gas_price", big_endian_int),
|
|
24
|
+
("gas", big_endian_int),
|
|
25
|
+
("to", Binary.fixed_length(20, allow_empty=True)),
|
|
26
|
+
("value", big_endian_int),
|
|
27
|
+
("data", binary),
|
|
28
|
+
("v", big_endian_int),
|
|
29
|
+
("r", big_endian_int),
|
|
30
|
+
("s", big_endian_int),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def new_tx(
|
|
35
|
+
*,
|
|
36
|
+
nonce: int,
|
|
37
|
+
gas_price: int,
|
|
38
|
+
gas: int,
|
|
39
|
+
v: int,
|
|
40
|
+
r: str,
|
|
41
|
+
s: str,
|
|
42
|
+
data: str | None = None,
|
|
43
|
+
value: int | None = None,
|
|
44
|
+
to: str | None = None,
|
|
45
|
+
) -> RPLTransaction:
|
|
46
|
+
if to:
|
|
47
|
+
to = hex_to_bytes(to) # type:ignore
|
|
48
|
+
if data:
|
|
49
|
+
data = hex_to_bytes(data) # type:ignore
|
|
50
|
+
if not value:
|
|
51
|
+
value = 0
|
|
52
|
+
r = int(r, 16) # type:ignore
|
|
53
|
+
s = int(s, 16) # type:ignore
|
|
54
|
+
return RPLTransaction(nonce, gas_price, gas, to, value, data, v, r, s)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DecodedRawTx(BaseModel):
|
|
58
|
+
tx_hash: str
|
|
59
|
+
from_: str
|
|
60
|
+
to: str | None
|
|
61
|
+
nonce: int
|
|
62
|
+
gas: int
|
|
63
|
+
gas_price: int
|
|
64
|
+
value: int
|
|
65
|
+
data: str
|
|
66
|
+
chain_id: int
|
|
67
|
+
r: str
|
|
68
|
+
s: str
|
|
69
|
+
v: int
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def encode_raw_tx_with_signature(
|
|
73
|
+
*,
|
|
74
|
+
nonce: int,
|
|
75
|
+
gas_price: int,
|
|
76
|
+
gas: int,
|
|
77
|
+
v: int,
|
|
78
|
+
r: str,
|
|
79
|
+
s: str,
|
|
80
|
+
data: str | None = None,
|
|
81
|
+
value: int | None = None,
|
|
82
|
+
to: str | None = None,
|
|
83
|
+
) -> str:
|
|
84
|
+
tx = RPLTransaction.new_tx(nonce=nonce, gas_price=gas_price, gas=gas, v=v, r=r, s=s, data=data, value=value, to=to)
|
|
85
|
+
return Web3.to_hex(rlp.encode(tx))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sign_legacy_tx(
|
|
89
|
+
*,
|
|
90
|
+
nonce: int,
|
|
91
|
+
gas_price: int,
|
|
92
|
+
gas: int,
|
|
93
|
+
private_key: str,
|
|
94
|
+
chain_id: int,
|
|
95
|
+
data: str | None = None,
|
|
96
|
+
value: int | None = None,
|
|
97
|
+
to: str | None = None,
|
|
98
|
+
) -> SignedTx:
|
|
99
|
+
tx: dict[str, Any] = {"gas": gas, "gasPrice": gas_price, "nonce": nonce, "chainId": chain_id}
|
|
100
|
+
if to:
|
|
101
|
+
tx["to"] = Web3.to_checksum_address(to)
|
|
102
|
+
if value:
|
|
103
|
+
tx["value"] = value
|
|
104
|
+
if data:
|
|
105
|
+
tx["data"] = data
|
|
106
|
+
|
|
107
|
+
signed = w3.eth.account.sign_transaction(tx, private_key)
|
|
108
|
+
return SignedTx(tx_hash=signed.hash.hex(), raw_tx=signed.rawTransaction.hex())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def sign_tx(
|
|
112
|
+
*,
|
|
113
|
+
nonce: int,
|
|
114
|
+
max_fee_per_gas: int,
|
|
115
|
+
max_priority_fee_per_gas: int,
|
|
116
|
+
gas: int,
|
|
117
|
+
private_key: str,
|
|
118
|
+
chain_id: int,
|
|
119
|
+
data: str | None = None,
|
|
120
|
+
value: int | None = None,
|
|
121
|
+
to: str | None = None,
|
|
122
|
+
) -> SignedTx:
|
|
123
|
+
tx: dict[str, Any] = {
|
|
124
|
+
"type": "0x2",
|
|
125
|
+
"gas": gas,
|
|
126
|
+
"maxFeePerGas": max_fee_per_gas,
|
|
127
|
+
"maxPriorityFeePerGas": max_priority_fee_per_gas,
|
|
128
|
+
"nonce": nonce,
|
|
129
|
+
"chainId": chain_id,
|
|
130
|
+
}
|
|
131
|
+
if value:
|
|
132
|
+
tx["value"] = value
|
|
133
|
+
if data:
|
|
134
|
+
tx["data"] = data
|
|
135
|
+
if to:
|
|
136
|
+
tx["to"] = Web3.to_checksum_address(to)
|
|
137
|
+
|
|
138
|
+
signed = w3.eth.account.sign_transaction(tx, private_key)
|
|
139
|
+
return SignedTx(tx_hash=signed.hash.hex(), raw_tx=signed.rawTransaction.hex())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def decode_raw_tx(raw_tx: str) -> DecodedRawTx:
|
|
143
|
+
tx: Any = rlp.decode(hex_to_bytes(raw_tx), RPLTransaction)
|
|
144
|
+
tx_hash = Web3.to_hex(keccak(hex_to_bytes(raw_tx)))
|
|
145
|
+
from_ = w3.eth.account.recover_transaction(raw_tx)
|
|
146
|
+
to = Web3.to_checksum_address(tx.to) if tx.to else None
|
|
147
|
+
data = Web3.to_hex(tx.data)
|
|
148
|
+
r = hex(tx.r)
|
|
149
|
+
s = hex(tx.s)
|
|
150
|
+
chain_id = (tx.v - 35) // 2 if tx.v % 2 else (tx.v - 36) // 2
|
|
151
|
+
return DecodedRawTx(
|
|
152
|
+
tx_hash=tx_hash,
|
|
153
|
+
from_=from_,
|
|
154
|
+
to=to,
|
|
155
|
+
data=data,
|
|
156
|
+
chain_id=chain_id,
|
|
157
|
+
r=r,
|
|
158
|
+
s=s,
|
|
159
|
+
v=tx.v,
|
|
160
|
+
gas=tx.gas,
|
|
161
|
+
gas_price=tx.gas_price,
|
|
162
|
+
value=tx.value,
|
|
163
|
+
nonce=tx.nonce,
|
|
164
|
+
)
|