eth-mcp 0.2.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.
@@ -0,0 +1,521 @@
1
+ """Download Ethereum specs, EIPs, client source code, and MEV infrastructure from GitHub.
2
+
3
+ Repositories:
4
+ - ethereum/consensus-specs: Consensus layer specifications
5
+ - ethereum/EIPs: Ethereum Improvement Proposals
6
+
7
+ Execution Layer Clients:
8
+ - paradigmxyz/reth: Rust execution client (Paradigm)
9
+ - ethereum/go-ethereum: Go execution client (Geth)
10
+ - NethermindEth/nethermind: C# execution client
11
+ - ledgerwatch/erigon: Go execution client (archive-focused)
12
+
13
+ Consensus Layer Clients:
14
+ - sigp/lighthouse: Rust consensus client (Sigma Prime)
15
+ - prysmaticlabs/prysm: Go consensus client
16
+ - ConsenSys/teku: Java consensus client
17
+ - status-im/nimbus-eth2: Nim consensus client
18
+
19
+ MEV Infrastructure (Flashbots):
20
+ - flashbots/mev-boost: MEV-boost middleware (connects validators to builders)
21
+ - flashbots/builder: Block builder reference implementation
22
+ - flashbots/mev-boost-relay: Relay implementation
23
+ - flashbots/flashbots-protect-rpc: RPC for private transactions
24
+ - ethereum/builder-specs: Builder API specifications
25
+ """
26
+
27
+ import shutil
28
+ import subprocess
29
+ from collections.abc import Callable
30
+ from dataclasses import dataclass
31
+ from pathlib import Path
32
+
33
+ from git import Repo
34
+
35
+ from ..logging import get_logger
36
+
37
+ logger = get_logger("downloader")
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class SpecsConfig:
42
+ """Configuration for specs download."""
43
+
44
+ consensus_specs_url: str = "https://github.com/ethereum/consensus-specs.git"
45
+ eips_url: str = "https://github.com/ethereum/EIPs.git"
46
+ builder_specs_url: str = "https://github.com/ethereum/builder-specs.git"
47
+ consensus_specs_branch: str = "master"
48
+ eips_branch: str = "master"
49
+ builder_specs_branch: str = "main"
50
+
51
+
52
+ # Client repositories to index
53
+ # Using sparse checkout for large repos to only get relevant code
54
+ CLIENT_REPOS = {
55
+ # Execution Layer Clients
56
+ "reth": {
57
+ "url": "https://github.com/paradigmxyz/reth.git",
58
+ "branch": "main",
59
+ "language": "rust",
60
+ "layer": "execution",
61
+ "sparse_paths": [
62
+ "crates/consensus",
63
+ "crates/engine",
64
+ "crates/evm",
65
+ "crates/execution",
66
+ "crates/net",
67
+ "crates/payload",
68
+ "crates/primitives",
69
+ "crates/rpc",
70
+ "crates/stages",
71
+ "crates/storage",
72
+ "crates/transaction-pool",
73
+ "crates/trie",
74
+ ],
75
+ },
76
+ "go-ethereum": {
77
+ "url": "https://github.com/ethereum/go-ethereum.git",
78
+ "branch": "master",
79
+ "language": "go",
80
+ "layer": "execution",
81
+ "sparse_paths": [
82
+ "consensus",
83
+ "core",
84
+ "eth",
85
+ "ethdb",
86
+ "miner",
87
+ "node",
88
+ "p2p",
89
+ "params",
90
+ "rpc",
91
+ "trie",
92
+ ],
93
+ },
94
+ "nethermind": {
95
+ "url": "https://github.com/NethermindEth/nethermind.git",
96
+ "branch": "master",
97
+ "language": "csharp",
98
+ "layer": "execution",
99
+ "sparse_paths": [
100
+ "src/Nethermind/Nethermind.Consensus",
101
+ "src/Nethermind/Nethermind.Core",
102
+ "src/Nethermind/Nethermind.Evm",
103
+ "src/Nethermind/Nethermind.JsonRpc",
104
+ "src/Nethermind/Nethermind.Merge.Plugin",
105
+ "src/Nethermind/Nethermind.Network",
106
+ "src/Nethermind/Nethermind.State",
107
+ "src/Nethermind/Nethermind.Trie",
108
+ ],
109
+ },
110
+ "erigon": {
111
+ "url": "https://github.com/ledgerwatch/erigon.git",
112
+ "branch": "main",
113
+ "language": "go",
114
+ "layer": "execution",
115
+ "sparse_paths": [
116
+ "consensus",
117
+ "core",
118
+ "erigon-lib",
119
+ "eth",
120
+ "ethdb",
121
+ "turbo",
122
+ ],
123
+ },
124
+ # Consensus Layer Clients
125
+ "lighthouse": {
126
+ "url": "https://github.com/sigp/lighthouse.git",
127
+ "branch": "stable",
128
+ "language": "rust",
129
+ "layer": "consensus",
130
+ "sparse_paths": [
131
+ "beacon_node/beacon_chain",
132
+ "beacon_node/client",
133
+ "beacon_node/execution_layer",
134
+ "beacon_node/lighthouse_network",
135
+ "beacon_node/network",
136
+ "beacon_node/store",
137
+ "consensus/fork_choice",
138
+ "consensus/state_processing",
139
+ "consensus/types",
140
+ "slasher",
141
+ "validator_client",
142
+ ],
143
+ },
144
+ "prysm": {
145
+ "url": "https://github.com/prysmaticlabs/prysm.git",
146
+ "branch": "develop",
147
+ "language": "go",
148
+ "layer": "consensus",
149
+ "sparse_paths": [
150
+ "beacon-chain/blockchain",
151
+ "beacon-chain/core",
152
+ "beacon-chain/db",
153
+ "beacon-chain/execution",
154
+ "beacon-chain/forkchoice",
155
+ "beacon-chain/operations",
156
+ "beacon-chain/p2p",
157
+ "beacon-chain/slasher",
158
+ "beacon-chain/state",
159
+ "beacon-chain/sync",
160
+ "proto",
161
+ "validator",
162
+ ],
163
+ },
164
+ "teku": {
165
+ "url": "https://github.com/ConsenSys/teku.git",
166
+ "branch": "master",
167
+ "language": "java",
168
+ "layer": "consensus",
169
+ "sparse_paths": [
170
+ "ethereum/spec",
171
+ "ethereum/statetransition",
172
+ "ethereum/executionclient",
173
+ "ethereum/executionlayer",
174
+ "ethereum/networks",
175
+ "ethereum/pow",
176
+ "networking",
177
+ "storage",
178
+ "validator",
179
+ ],
180
+ },
181
+ "nimbus-eth2": {
182
+ "url": "https://github.com/status-im/nimbus-eth2.git",
183
+ "branch": "stable",
184
+ "language": "nim",
185
+ "layer": "consensus",
186
+ "sparse_paths": [
187
+ "beacon_chain",
188
+ "ncli",
189
+ "research",
190
+ ],
191
+ },
192
+ # ===================
193
+ # MEV Infrastructure
194
+ # ===================
195
+ "mev-boost": {
196
+ "url": "https://github.com/flashbots/mev-boost.git",
197
+ "branch": "develop",
198
+ "language": "go",
199
+ "layer": "mev",
200
+ "sparse_paths": None, # Small repo, full clone
201
+ "description": "MEV-boost middleware connecting validators to block builders",
202
+ },
203
+ "flashbots-builder": {
204
+ "url": "https://github.com/flashbots/builder.git",
205
+ "branch": "main",
206
+ "language": "go",
207
+ "layer": "mev",
208
+ "sparse_paths": [
209
+ "builder",
210
+ "core",
211
+ "eth",
212
+ "miner",
213
+ "flashbotsextra",
214
+ ],
215
+ "description": "Flashbots block builder (geth fork)",
216
+ },
217
+ "mev-boost-relay": {
218
+ "url": "https://github.com/flashbots/mev-boost-relay.git",
219
+ "branch": "main",
220
+ "language": "go",
221
+ "layer": "mev",
222
+ "sparse_paths": None, # Full clone
223
+ "description": "MEV-boost relay for connecting builders to proposers",
224
+ },
225
+ "builder-specs": {
226
+ "url": "https://github.com/ethereum/builder-specs.git",
227
+ "branch": "main",
228
+ "language": "markdown",
229
+ "layer": "mev",
230
+ "sparse_paths": None, # Specs repo, full clone
231
+ "description": "Builder API specifications for PBS",
232
+ },
233
+ "mev-share-node": {
234
+ "url": "https://github.com/flashbots/mev-share-node.git",
235
+ "branch": "main",
236
+ "language": "go",
237
+ "layer": "mev",
238
+ "sparse_paths": None,
239
+ "description": "MEV-Share node for orderflow auctions",
240
+ },
241
+ "rbuilder": {
242
+ "url": "https://github.com/flashbots/rbuilder.git",
243
+ "branch": "develop",
244
+ "language": "rust",
245
+ "layer": "mev",
246
+ "sparse_paths": [
247
+ "crates/rbuilder",
248
+ "crates/op-rbuilder",
249
+ ],
250
+ "description": "Rust block builder by Flashbots (high performance)",
251
+ },
252
+ }
253
+
254
+
255
+ def run_git(args: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess:
256
+ """Run a git command."""
257
+ result = subprocess.run(
258
+ ["git"] + args,
259
+ cwd=cwd,
260
+ capture_output=True,
261
+ text=True,
262
+ )
263
+ return result
264
+
265
+
266
+ def clone_client_repo(
267
+ name: str,
268
+ url: str,
269
+ dest: Path,
270
+ branch: str = "main",
271
+ sparse_paths: list[str] | None = None,
272
+ progress_callback: Callable[[str], None] | None = None,
273
+ ) -> bool:
274
+ """
275
+ Clone a client repository with sparse checkout.
276
+
277
+ Args:
278
+ name: Repository name for logging
279
+ url: Git URL to clone
280
+ dest: Destination path
281
+ branch: Branch to checkout
282
+ sparse_paths: Paths to checkout (sparse checkout)
283
+ progress_callback: Optional callback for progress updates
284
+
285
+ Returns:
286
+ True if successful, False otherwise
287
+ """
288
+ def log(msg: str):
289
+ if progress_callback:
290
+ progress_callback(msg)
291
+ else:
292
+ logger.info(msg)
293
+
294
+ if dest.exists():
295
+ log(f" {name}: Already exists, pulling latest...")
296
+ result = run_git(["pull", "--ff-only"], cwd=dest)
297
+ if result.returncode != 0:
298
+ log(f" {name}: Pull failed, trying fetch + reset...")
299
+ run_git(["fetch", "origin"], cwd=dest)
300
+ run_git(["reset", "--hard", f"origin/{branch}"], cwd=dest)
301
+ return True
302
+
303
+ log(f" {name}: Cloning from {url}...")
304
+
305
+ if sparse_paths:
306
+ # Sparse checkout for large repos
307
+ dest.mkdir(parents=True, exist_ok=True)
308
+
309
+ # Initialize repo
310
+ run_git(["init"], cwd=dest)
311
+ run_git(["remote", "add", "origin", url], cwd=dest)
312
+
313
+ # Configure sparse checkout
314
+ run_git(["config", "core.sparseCheckout", "true"], cwd=dest)
315
+
316
+ # Write sparse-checkout file
317
+ sparse_file = dest / ".git" / "info" / "sparse-checkout"
318
+ sparse_file.parent.mkdir(parents=True, exist_ok=True)
319
+ sparse_file.write_text("\n".join(sparse_paths) + "\n")
320
+
321
+ # Fetch and checkout
322
+ log(f" {name}: Fetching (sparse checkout: {len(sparse_paths)} paths)...")
323
+ result = run_git(["fetch", "--depth=1", "origin", branch], cwd=dest)
324
+ if result.returncode != 0:
325
+ log(f" {name}: Fetch failed: {result.stderr}")
326
+ return False
327
+
328
+ result = run_git(["checkout", branch], cwd=dest)
329
+ if result.returncode != 0:
330
+ log(f" {name}: Checkout failed: {result.stderr}")
331
+ return False
332
+ else:
333
+ # Full clone for small repos
334
+ result = run_git(["clone", "--depth=1", "--branch", branch, url, str(dest)])
335
+ if result.returncode != 0:
336
+ log(f" {name}: Clone failed: {result.stderr}")
337
+ return False
338
+
339
+ log(f" {name}: Done")
340
+ return True
341
+
342
+
343
+ def download_specs(
344
+ data_dir: Path,
345
+ config: SpecsConfig | None = None,
346
+ force: bool = False,
347
+ ) -> tuple[Path, Path]:
348
+ """
349
+ Download consensus specs and EIPs repositories.
350
+
351
+ Args:
352
+ data_dir: Directory to store downloaded repos
353
+ config: Optional configuration override
354
+ force: If True, re-download even if exists
355
+
356
+ Returns:
357
+ Tuple of (consensus_specs_path, eips_path)
358
+ """
359
+ config = config or SpecsConfig()
360
+ data_dir = Path(data_dir)
361
+ data_dir.mkdir(parents=True, exist_ok=True)
362
+
363
+ consensus_dir = data_dir / "consensus-specs"
364
+ eips_dir = data_dir / "EIPs"
365
+
366
+ # Download consensus specs
367
+ if force and consensus_dir.exists():
368
+ shutil.rmtree(consensus_dir)
369
+
370
+ if not consensus_dir.exists():
371
+ logger.info("Cloning consensus-specs to %s...", consensus_dir)
372
+ Repo.clone_from(
373
+ config.consensus_specs_url,
374
+ consensus_dir,
375
+ branch=config.consensus_specs_branch,
376
+ depth=1,
377
+ )
378
+ else:
379
+ logger.info("Consensus specs already exists at %s, pulling latest...", consensus_dir)
380
+ repo = Repo(consensus_dir)
381
+ repo.remotes.origin.pull()
382
+
383
+ # Download EIPs
384
+ if force and eips_dir.exists():
385
+ shutil.rmtree(eips_dir)
386
+
387
+ if not eips_dir.exists():
388
+ logger.info("Cloning EIPs to %s...", eips_dir)
389
+ Repo.clone_from(
390
+ config.eips_url,
391
+ eips_dir,
392
+ branch=config.eips_branch,
393
+ depth=1,
394
+ )
395
+ else:
396
+ logger.info("EIPs already exists at %s, pulling latest...", eips_dir)
397
+ repo = Repo(eips_dir)
398
+ repo.remotes.origin.pull()
399
+
400
+ # Download builder-specs
401
+ builder_specs_dir = data_dir / "builder-specs"
402
+ if force and builder_specs_dir.exists():
403
+ shutil.rmtree(builder_specs_dir)
404
+
405
+ if not builder_specs_dir.exists():
406
+ logger.info("Cloning builder-specs to %s...", builder_specs_dir)
407
+ Repo.clone_from(
408
+ config.builder_specs_url,
409
+ builder_specs_dir,
410
+ branch=config.builder_specs_branch,
411
+ depth=1,
412
+ )
413
+ else:
414
+ logger.info("Builder specs already exists at %s, pulling latest...", builder_specs_dir)
415
+ repo = Repo(builder_specs_dir)
416
+ repo.remotes.origin.pull()
417
+
418
+ return consensus_dir, eips_dir, builder_specs_dir
419
+
420
+
421
+ def download_clients(
422
+ data_dir: Path,
423
+ clients: list[str] | None = None,
424
+ progress_callback: Callable[[str], None] | None = None,
425
+ ) -> dict[str, bool]:
426
+ """
427
+ Download Ethereum client source code.
428
+
429
+ Args:
430
+ data_dir: Directory to store downloaded repos
431
+ clients: List of client names to download (default: all)
432
+ progress_callback: Optional callback for progress updates
433
+
434
+ Returns:
435
+ Dict mapping client name to success status
436
+ """
437
+ data_dir = Path(data_dir)
438
+ clients_dir = data_dir / "clients"
439
+ clients_dir.mkdir(parents=True, exist_ok=True)
440
+
441
+ if clients is None:
442
+ clients = list(CLIENT_REPOS.keys())
443
+
444
+ results = {}
445
+ for name in clients:
446
+ if name not in CLIENT_REPOS:
447
+ if progress_callback:
448
+ progress_callback(f" {name}: Unknown client, skipping")
449
+ results[name] = False
450
+ continue
451
+
452
+ config = CLIENT_REPOS[name]
453
+ dest = clients_dir / name
454
+
455
+ success = clone_client_repo(
456
+ name=name,
457
+ url=config["url"],
458
+ dest=dest,
459
+ branch=config["branch"],
460
+ sparse_paths=config.get("sparse_paths"),
461
+ progress_callback=progress_callback,
462
+ )
463
+ results[name] = success
464
+
465
+ return results
466
+
467
+
468
+ def list_downloaded_clients(data_dir: Path) -> dict[str, dict]:
469
+ """List all downloaded client repositories with their status."""
470
+ clients_dir = data_dir / "clients"
471
+
472
+ status = {}
473
+ for name, config in CLIENT_REPOS.items():
474
+ path = clients_dir / name
475
+ if path.exists():
476
+ result = run_git(["rev-parse", "HEAD"], cwd=path)
477
+ version = result.stdout.strip()[:12] if result.returncode == 0 else None
478
+ status[name] = {
479
+ "path": str(path),
480
+ "version": version,
481
+ "exists": True,
482
+ "language": config["language"],
483
+ "layer": config["layer"],
484
+ }
485
+ else:
486
+ status[name] = {
487
+ "path": str(path),
488
+ "version": None,
489
+ "exists": False,
490
+ "language": config["language"],
491
+ "layer": config["layer"],
492
+ }
493
+
494
+ return status
495
+
496
+
497
+ def get_spec_files(consensus_dir: Path) -> list[Path]:
498
+ """Get all markdown spec files organized by fork."""
499
+ specs_dir = consensus_dir / "specs"
500
+ if not specs_dir.exists():
501
+ raise FileNotFoundError(f"Specs directory not found: {specs_dir}")
502
+
503
+ return list(specs_dir.rglob("*.md"))
504
+
505
+
506
+ def get_eip_files(eips_dir: Path) -> list[Path]:
507
+ """Get all EIP markdown files."""
508
+ eips_content_dir = eips_dir / "EIPS"
509
+ if not eips_content_dir.exists():
510
+ raise FileNotFoundError(f"EIPs directory not found: {eips_content_dir}")
511
+
512
+ return list(eips_content_dir.glob("eip-*.md"))
513
+
514
+
515
+ def get_builder_spec_files(builder_specs_dir: Path) -> list[Path]:
516
+ """Get all builder-specs markdown files."""
517
+ specs_dir = builder_specs_dir / "specs"
518
+ if not specs_dir.exists():
519
+ raise FileNotFoundError(f"Builder specs directory not found: {specs_dir}")
520
+
521
+ return list(specs_dir.rglob("*.md"))