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,245 @@
1
+ """Compile consensus specs into executable Python modules."""
2
+
3
+ import json
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from ..logging import get_logger
9
+
10
+ logger = get_logger("compiler")
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CompiledSpec:
15
+ """A compiled spec module with extracted metadata."""
16
+
17
+ fork: str
18
+ module_path: Path
19
+ constants: dict[str, str | int]
20
+ functions: list[str]
21
+ types: list[str]
22
+
23
+
24
+ def compile_specs(consensus_dir: Path, output_dir: Path) -> list[CompiledSpec]:
25
+ """
26
+ Compile consensus specs using the spec build process.
27
+
28
+ The consensus-specs repo has a build system that compiles
29
+ the markdown + python into executable modules.
30
+
31
+ Args:
32
+ consensus_dir: Path to cloned consensus-specs repo
33
+ output_dir: Where to store compiled output
34
+
35
+ Returns:
36
+ List of compiled spec metadata
37
+ """
38
+ output_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ # Check if pyspec can be built
41
+ setup_py = consensus_dir / "setup.py"
42
+ if not setup_py.exists():
43
+ logger.warning("setup.py not found, using fallback extraction")
44
+ return _extract_specs_fallback(consensus_dir, output_dir)
45
+
46
+ # Try to build pyspec
47
+ try:
48
+ result = subprocess.run(
49
+ ["pip", "install", "-e", ".[test]"],
50
+ cwd=consensus_dir,
51
+ capture_output=True,
52
+ text=True,
53
+ timeout=300,
54
+ )
55
+ if result.returncode != 0:
56
+ logger.warning("pyspec build failed: %s", result.stderr)
57
+ return _extract_specs_fallback(consensus_dir, output_dir)
58
+
59
+ return _extract_from_pyspec(consensus_dir, output_dir)
60
+
61
+ except subprocess.TimeoutExpired:
62
+ logger.warning("pyspec build timed out, using fallback")
63
+ return _extract_specs_fallback(consensus_dir, output_dir)
64
+ except Exception as e:
65
+ logger.warning("pyspec build error: %s, using fallback", e)
66
+ return _extract_specs_fallback(consensus_dir, output_dir)
67
+
68
+
69
+ def _extract_from_pyspec(consensus_dir: Path, output_dir: Path) -> list[CompiledSpec]:
70
+ """Extract metadata from built pyspec modules."""
71
+ compiled = []
72
+
73
+ # Import and inspect pyspec modules
74
+ try:
75
+ import importlib
76
+ import inspect
77
+
78
+ forks = ["phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu"]
79
+
80
+ for fork in forks:
81
+ try:
82
+ module = importlib.import_module(f"eth2spec.{fork}.mainnet")
83
+
84
+ constants = {}
85
+ functions = []
86
+ types = []
87
+
88
+ for name, obj in inspect.getmembers(module):
89
+ if name.startswith("_"):
90
+ continue
91
+ if inspect.isfunction(obj):
92
+ functions.append(name)
93
+ elif inspect.isclass(obj):
94
+ types.append(name)
95
+ elif name.isupper(): # Constants are uppercase
96
+ constants[name] = obj
97
+
98
+ # Save to JSON
99
+ spec_data = {
100
+ "fork": fork,
101
+ "constants": {k: str(v) for k, v in constants.items()},
102
+ "functions": functions,
103
+ "types": types,
104
+ }
105
+
106
+ output_file = output_dir / f"{fork}_spec.json"
107
+ with open(output_file, "w") as f:
108
+ json.dump(spec_data, f, indent=2)
109
+
110
+ compiled.append(
111
+ CompiledSpec(
112
+ fork=fork,
113
+ module_path=output_file,
114
+ constants=constants,
115
+ functions=functions,
116
+ types=types,
117
+ )
118
+ )
119
+ logger.info(
120
+ "Extracted %s: %d constants, %d functions",
121
+ fork, len(constants), len(functions)
122
+ )
123
+
124
+ except ImportError:
125
+ logger.warning("Could not import %s spec", fork)
126
+ continue
127
+
128
+ except ImportError:
129
+ logger.warning("eth2spec not available, using fallback")
130
+ return _extract_specs_fallback(consensus_dir, output_dir)
131
+
132
+ return compiled
133
+
134
+
135
+ def _extract_specs_fallback(consensus_dir: Path, output_dir: Path) -> list[CompiledSpec]:
136
+ """
137
+ Fallback: extract specs directly from markdown/python files.
138
+
139
+ This doesn't give us executable code but extracts constants,
140
+ function signatures, and type definitions.
141
+ """
142
+ import re
143
+
144
+ compiled = []
145
+ specs_dir = consensus_dir / "specs"
146
+
147
+ # Known forks in order
148
+ forks = ["phase0", "altair", "bellatrix", "capella", "deneb", "electra", "fulu"]
149
+
150
+ for fork in forks:
151
+ fork_dir = specs_dir / fork
152
+ if not fork_dir.exists():
153
+ continue
154
+
155
+ constants = {}
156
+ functions = []
157
+ types = []
158
+
159
+ # Parse all markdown files in fork directory
160
+ for md_file in fork_dir.glob("*.md"):
161
+ content = md_file.read_text()
162
+
163
+ # Extract constants from tables (| Name | Value | pattern)
164
+ const_pattern = r"\|\s*`?([A-Z][A-Z0-9_]+)`?\s*\|\s*`?([^|`]+)`?\s*\|"
165
+ for match in re.finditer(const_pattern, content):
166
+ name, value = match.groups()
167
+ constants[name.strip()] = value.strip()
168
+
169
+ # Extract function definitions
170
+ func_pattern = r"^def\s+([a-z_][a-z0-9_]*)\s*\("
171
+ for match in re.finditer(func_pattern, content, re.MULTILINE):
172
+ functions.append(match.group(1))
173
+
174
+ # Extract class/type definitions
175
+ class_pattern = r"^class\s+([A-Z][a-zA-Z0-9_]*)"
176
+ for match in re.finditer(class_pattern, content, re.MULTILINE):
177
+ types.append(match.group(1))
178
+
179
+ if constants or functions or types:
180
+ spec_data = {
181
+ "fork": fork,
182
+ "constants": constants,
183
+ "functions": list(set(functions)),
184
+ "types": list(set(types)),
185
+ }
186
+
187
+ output_file = output_dir / f"{fork}_spec.json"
188
+ with open(output_file, "w") as f:
189
+ json.dump(spec_data, f, indent=2)
190
+
191
+ compiled.append(
192
+ CompiledSpec(
193
+ fork=fork,
194
+ module_path=output_file,
195
+ constants=constants,
196
+ functions=list(set(functions)),
197
+ types=list(set(types)),
198
+ )
199
+ )
200
+ logger.info(
201
+ "Extracted %s (fallback): %d constants, %d functions",
202
+ fork, len(constants), len(functions)
203
+ )
204
+
205
+ return compiled
206
+
207
+
208
+ def get_function_source(consensus_dir: Path, fork: str, function_name: str) -> str | None:
209
+ """
210
+ Extract the full source code of a function from spec markdown.
211
+
212
+ Args:
213
+ consensus_dir: Path to consensus-specs repo
214
+ fork: Fork name (e.g., 'electra')
215
+ function_name: Name of function to find
216
+
217
+ Returns:
218
+ Function source code or None if not found
219
+ """
220
+ import re
221
+
222
+ # Prevent path traversal - ensure fork doesn't escape specs directory
223
+ specs_dir = (consensus_dir / "specs" / fork).resolve()
224
+ base_specs = (consensus_dir / "specs").resolve()
225
+ if not str(specs_dir).startswith(str(base_specs)):
226
+ logger.warning("Path traversal attempt blocked: %s", fork)
227
+ return None
228
+
229
+ if not specs_dir.exists():
230
+ return None
231
+
232
+ for md_file in specs_dir.glob("*.md"):
233
+ content = md_file.read_text()
234
+
235
+ # Find function definition and extract full body
236
+ # Functions in specs are in ```python blocks
237
+ # Escape function_name to prevent regex injection (defense in depth)
238
+ safe_name = re.escape(function_name)
239
+ pattern = rf"```python\n(def\s+{safe_name}\s*\([^)]*\)[^`]*?)```"
240
+ match = re.search(pattern, content, re.DOTALL)
241
+
242
+ if match:
243
+ return match.group(1).strip()
244
+
245
+ return None