zexus 1.7.1 → 1.7.2
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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3411 -573
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +7 -4
- package/src/zexus.egg-info/SOURCES.txt +116 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zexus Blockchain — JSON-RPC Server
|
|
3
|
+
|
|
4
|
+
Production-grade JSON-RPC 2.0 server providing external access to a
|
|
5
|
+
running BlockchainNode. Supports both HTTP and WebSocket transports.
|
|
6
|
+
|
|
7
|
+
Namespaces:
|
|
8
|
+
zx_* — Core blockchain methods (query blocks, accounts, state)
|
|
9
|
+
txpool_* — Mempool / transaction pool
|
|
10
|
+
net_* — Peer-to-peer networking
|
|
11
|
+
miner_* — Mining control
|
|
12
|
+
contract_* — Smart-contract deployment and interaction
|
|
13
|
+
admin_* — Node administration
|
|
14
|
+
|
|
15
|
+
The server follows the JSON-RPC 2.0 specification:
|
|
16
|
+
https://www.jsonrpc.org/specification
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
node = BlockchainNode(NodeConfig(rpc_enabled=True, rpc_port=8545))
|
|
20
|
+
await node.start() # RPC server starts automatically
|
|
21
|
+
|
|
22
|
+
Or standalone:
|
|
23
|
+
server = RPCServer(node, host="0.0.0.0", port=8545)
|
|
24
|
+
await server.start()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import time
|
|
33
|
+
import traceback
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from enum import IntEnum
|
|
36
|
+
from typing import Any, Callable, Coroutine, Dict, List, Optional, Set, Tuple, Union
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("zexus.blockchain.rpc")
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# JSON-RPC 2.0 error codes
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
class RPCErrorCode(IntEnum):
|
|
45
|
+
"""Standard JSON-RPC 2.0 error codes + Zexus-specific extensions."""
|
|
46
|
+
PARSE_ERROR = -32700
|
|
47
|
+
INVALID_REQUEST = -32600
|
|
48
|
+
METHOD_NOT_FOUND = -32601
|
|
49
|
+
INVALID_PARAMS = -32602
|
|
50
|
+
INTERNAL_ERROR = -32603
|
|
51
|
+
|
|
52
|
+
# Zexus-specific (-32000 to -32099 reserved for server errors)
|
|
53
|
+
CHAIN_ERROR = -32000
|
|
54
|
+
TX_REJECTED = -32001
|
|
55
|
+
INSUFFICIENT_FUNDS = -32002
|
|
56
|
+
NONCE_TOO_LOW = -32003
|
|
57
|
+
CONTRACT_ERROR = -32004
|
|
58
|
+
MINING_ERROR = -32005
|
|
59
|
+
NOT_FOUND = -32006
|
|
60
|
+
UNAUTHORIZED = -32007
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RPCError(Exception):
|
|
64
|
+
"""An error that maps directly to a JSON-RPC error response."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, code: RPCErrorCode, message: str, data: Any = None):
|
|
67
|
+
super().__init__(message)
|
|
68
|
+
self.code = int(code)
|
|
69
|
+
self.message = message
|
|
70
|
+
self.data = data
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
d: Dict[str, Any] = {"code": self.code, "message": self.message}
|
|
74
|
+
if self.data is not None:
|
|
75
|
+
d["data"] = self.data
|
|
76
|
+
return d
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Subscription manager (WebSocket push)
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class Subscription:
|
|
85
|
+
"""A single WebSocket subscription."""
|
|
86
|
+
sub_id: str
|
|
87
|
+
event: str # "newHeads", "newPendingTransactions", "logs"
|
|
88
|
+
ws: Any # aiohttp WebSocketResponse
|
|
89
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SubscriptionManager:
|
|
93
|
+
"""Manages WebSocket subscriptions for real-time event streaming."""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
self._subs: Dict[str, Subscription] = {}
|
|
97
|
+
self._counter = 0
|
|
98
|
+
|
|
99
|
+
def add(self, event: str, ws: Any, params: Optional[Dict] = None) -> str:
|
|
100
|
+
self._counter += 1
|
|
101
|
+
sub_id = f"0x{self._counter:016x}"
|
|
102
|
+
self._subs[sub_id] = Subscription(
|
|
103
|
+
sub_id=sub_id, event=event, ws=ws, params=params or {},
|
|
104
|
+
)
|
|
105
|
+
return sub_id
|
|
106
|
+
|
|
107
|
+
def remove(self, sub_id: str) -> bool:
|
|
108
|
+
return self._subs.pop(sub_id, None) is not None
|
|
109
|
+
|
|
110
|
+
def remove_all_for_ws(self, ws: Any) -> int:
|
|
111
|
+
to_remove = [sid for sid, s in self._subs.items() if s.ws is ws]
|
|
112
|
+
for sid in to_remove:
|
|
113
|
+
del self._subs[sid]
|
|
114
|
+
return len(to_remove)
|
|
115
|
+
|
|
116
|
+
async def notify(self, event: str, data: Any):
|
|
117
|
+
"""Push a notification to all subscribers of *event*."""
|
|
118
|
+
dead: List[str] = []
|
|
119
|
+
for sid, sub in self._subs.items():
|
|
120
|
+
if sub.event != event:
|
|
121
|
+
continue
|
|
122
|
+
msg = json.dumps({
|
|
123
|
+
"jsonrpc": "2.0",
|
|
124
|
+
"method": "zx_subscription",
|
|
125
|
+
"params": {"subscription": sid, "result": data},
|
|
126
|
+
})
|
|
127
|
+
try:
|
|
128
|
+
await sub.ws.send_str(msg)
|
|
129
|
+
except Exception:
|
|
130
|
+
dead.append(sid)
|
|
131
|
+
for sid in dead:
|
|
132
|
+
self._subs.pop(sid, None)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def count(self) -> int:
|
|
136
|
+
return len(self._subs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Rate limiter
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
class RateLimiter:
|
|
144
|
+
"""Simple token-bucket rate limiter per IP address."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, max_requests: int = 100, window_seconds: float = 1.0):
|
|
147
|
+
self.max_requests = max_requests
|
|
148
|
+
self.window = window_seconds
|
|
149
|
+
self._buckets: Dict[str, List[float]] = {}
|
|
150
|
+
|
|
151
|
+
def allow(self, ip: str) -> bool:
|
|
152
|
+
now = time.monotonic()
|
|
153
|
+
bucket = self._buckets.setdefault(ip, [])
|
|
154
|
+
# Prune old entries
|
|
155
|
+
bucket[:] = [t for t in bucket if now - t < self.window]
|
|
156
|
+
if len(bucket) >= self.max_requests:
|
|
157
|
+
return False
|
|
158
|
+
bucket.append(now)
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
def reset(self):
|
|
162
|
+
self._buckets.clear()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# RPC Method registry
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
# Type alias for an RPC handler: takes (params) -> result
|
|
170
|
+
RPCHandler = Callable[..., Any]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class RPCMethodInfo:
|
|
175
|
+
"""Metadata about a registered RPC method."""
|
|
176
|
+
name: str
|
|
177
|
+
handler: RPCHandler
|
|
178
|
+
is_async: bool = False
|
|
179
|
+
description: str = ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class RPCMethodRegistry:
|
|
183
|
+
"""Registry that maps JSON-RPC method names to Python handlers."""
|
|
184
|
+
|
|
185
|
+
def __init__(self):
|
|
186
|
+
self._methods: Dict[str, RPCMethodInfo] = {}
|
|
187
|
+
|
|
188
|
+
def register(self, name: str, handler: RPCHandler, description: str = ""):
|
|
189
|
+
is_async = asyncio.iscoroutinefunction(handler)
|
|
190
|
+
self._methods[name] = RPCMethodInfo(
|
|
191
|
+
name=name, handler=handler, is_async=is_async,
|
|
192
|
+
description=description,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def get(self, name: str) -> Optional[RPCMethodInfo]:
|
|
196
|
+
return self._methods.get(name)
|
|
197
|
+
|
|
198
|
+
def list_methods(self) -> List[str]:
|
|
199
|
+
return sorted(self._methods.keys())
|
|
200
|
+
|
|
201
|
+
def __contains__(self, name: str) -> bool:
|
|
202
|
+
return name in self._methods
|
|
203
|
+
|
|
204
|
+
def __len__(self) -> int:
|
|
205
|
+
return len(self._methods)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Utility helpers
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def _hex_int(value: int) -> str:
|
|
213
|
+
"""Encode an integer as a 0x-prefixed hex string."""
|
|
214
|
+
return hex(value)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _parse_hex_int(value: Any) -> int:
|
|
218
|
+
"""Parse a 0x-prefixed hex string or plain int."""
|
|
219
|
+
if isinstance(value, int):
|
|
220
|
+
return value
|
|
221
|
+
if isinstance(value, str):
|
|
222
|
+
if value.startswith("0x") or value.startswith("0X"):
|
|
223
|
+
return int(value, 16)
|
|
224
|
+
return int(value)
|
|
225
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, f"expected integer, got {type(value).__name__}")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _require_param(params: Any, key: Union[str, int], name: str = "") -> Any:
|
|
229
|
+
"""Extract a required parameter from dict or list."""
|
|
230
|
+
label = name or str(key)
|
|
231
|
+
try:
|
|
232
|
+
if isinstance(params, dict):
|
|
233
|
+
if key not in params:
|
|
234
|
+
raise KeyError(key)
|
|
235
|
+
return params[key]
|
|
236
|
+
if isinstance(params, (list, tuple)):
|
|
237
|
+
return params[key]
|
|
238
|
+
except (KeyError, IndexError, TypeError):
|
|
239
|
+
pass
|
|
240
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, f"missing required parameter: {label}")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _optional_param(params: Any, key: Union[str, int], default: Any = None) -> Any:
|
|
244
|
+
"""Extract an optional parameter."""
|
|
245
|
+
try:
|
|
246
|
+
if isinstance(params, dict):
|
|
247
|
+
return params.get(key, default)
|
|
248
|
+
if isinstance(params, (list, tuple)) and isinstance(key, int) and key < len(params):
|
|
249
|
+
return params[key]
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
return default
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _safe_serialize(obj: Any) -> Any:
|
|
256
|
+
"""Make an object JSON-safe (handle non-serializable types)."""
|
|
257
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
258
|
+
return obj
|
|
259
|
+
if isinstance(obj, dict):
|
|
260
|
+
return {str(k): _safe_serialize(v) for k, v in obj.items()}
|
|
261
|
+
if isinstance(obj, (list, tuple)):
|
|
262
|
+
return [_safe_serialize(v) for v in obj]
|
|
263
|
+
if isinstance(obj, bytes):
|
|
264
|
+
return "0x" + obj.hex()
|
|
265
|
+
if hasattr(obj, "to_dict"):
|
|
266
|
+
return _safe_serialize(obj.to_dict())
|
|
267
|
+
return str(obj)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# RPCServer — the main server class
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
class RPCServer:
|
|
275
|
+
"""
|
|
276
|
+
JSON-RPC 2.0 server for a Zexus BlockchainNode.
|
|
277
|
+
|
|
278
|
+
Supports:
|
|
279
|
+
- HTTP POST requests (standard JSON-RPC)
|
|
280
|
+
- WebSocket connections (subscriptions + regular calls)
|
|
281
|
+
- Batch requests
|
|
282
|
+
- CORS for browser-based dApps
|
|
283
|
+
- Per-IP rate limiting
|
|
284
|
+
- Method namespace: zx_, txpool_, net_, miner_, contract_, admin_
|
|
285
|
+
|
|
286
|
+
Usage::
|
|
287
|
+
|
|
288
|
+
from zexus.blockchain.node import BlockchainNode, NodeConfig
|
|
289
|
+
from zexus.blockchain.rpc import RPCServer
|
|
290
|
+
|
|
291
|
+
node = BlockchainNode(NodeConfig(rpc_enabled=True))
|
|
292
|
+
rpc = RPCServer(node, host="127.0.0.1", port=8545)
|
|
293
|
+
await rpc.start()
|
|
294
|
+
# ... node is now accessible via HTTP/WS at port 8545
|
|
295
|
+
await rpc.stop()
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(
|
|
299
|
+
self,
|
|
300
|
+
node: Any, # BlockchainNode — avoid circular import at module level
|
|
301
|
+
host: str = "0.0.0.0",
|
|
302
|
+
port: int = 8545,
|
|
303
|
+
cors_origins: str = "*",
|
|
304
|
+
max_request_size: int = 5 * 1024 * 1024, # 5 MB
|
|
305
|
+
rate_limit: int = 200, # requests per second per IP
|
|
306
|
+
):
|
|
307
|
+
self.node = node
|
|
308
|
+
self.host = host
|
|
309
|
+
self.port = port
|
|
310
|
+
self.cors_origins = cors_origins
|
|
311
|
+
self.max_request_size = max_request_size
|
|
312
|
+
|
|
313
|
+
# Components
|
|
314
|
+
self.registry = RPCMethodRegistry()
|
|
315
|
+
self.subscriptions = SubscriptionManager()
|
|
316
|
+
self.rate_limiter = RateLimiter(max_requests=rate_limit)
|
|
317
|
+
|
|
318
|
+
# Server state
|
|
319
|
+
self._app = None
|
|
320
|
+
self._runner = None
|
|
321
|
+
self._site = None
|
|
322
|
+
self._running = False
|
|
323
|
+
|
|
324
|
+
# Register all RPC methods
|
|
325
|
+
self._register_all_methods()
|
|
326
|
+
|
|
327
|
+
# ------------------------------------------------------------------
|
|
328
|
+
# Server lifecycle
|
|
329
|
+
# ------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
async def start(self):
|
|
332
|
+
"""Start the HTTP+WS server."""
|
|
333
|
+
if self._running:
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
from aiohttp import web
|
|
338
|
+
except ImportError:
|
|
339
|
+
logger.error(
|
|
340
|
+
"aiohttp is required for the RPC server. "
|
|
341
|
+
"Install with: pip install aiohttp"
|
|
342
|
+
)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
self._app = web.Application(client_max_size=self.max_request_size)
|
|
346
|
+
self._app.router.add_post("/", self._handle_http)
|
|
347
|
+
self._app.router.add_get("/", self._handle_ws_or_info)
|
|
348
|
+
self._app.router.add_get("/ws", self._handle_ws)
|
|
349
|
+
self._app.router.add_options("/", self._handle_cors_preflight)
|
|
350
|
+
self._app.router.add_get("/health", self._handle_health)
|
|
351
|
+
|
|
352
|
+
self._runner = web.AppRunner(self._app)
|
|
353
|
+
await self._runner.setup()
|
|
354
|
+
self._site = web.TCPSite(self._runner, self.host, self.port)
|
|
355
|
+
await self._site.start()
|
|
356
|
+
self._running = True
|
|
357
|
+
|
|
358
|
+
# Hook into node events for WebSocket subscriptions
|
|
359
|
+
self._wire_node_events()
|
|
360
|
+
|
|
361
|
+
logger.info("RPC server listening on http://%s:%d", self.host, self.port)
|
|
362
|
+
|
|
363
|
+
async def stop(self):
|
|
364
|
+
"""Stop the server gracefully."""
|
|
365
|
+
if not self._running:
|
|
366
|
+
return
|
|
367
|
+
self._running = False
|
|
368
|
+
if self._runner:
|
|
369
|
+
await self._runner.cleanup()
|
|
370
|
+
self._app = None
|
|
371
|
+
self._runner = None
|
|
372
|
+
self._site = None
|
|
373
|
+
logger.info("RPC server stopped")
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def is_running(self) -> bool:
|
|
377
|
+
return self._running
|
|
378
|
+
|
|
379
|
+
# ------------------------------------------------------------------
|
|
380
|
+
# HTTP handler
|
|
381
|
+
# ------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
async def _handle_http(self, request):
|
|
384
|
+
"""Handle a JSON-RPC POST request."""
|
|
385
|
+
from aiohttp import web
|
|
386
|
+
|
|
387
|
+
# CORS headers (Content-Type omitted — json_response sets it)
|
|
388
|
+
headers = {
|
|
389
|
+
"Access-Control-Allow-Origin": self.cors_origins,
|
|
390
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
391
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Rate limiting
|
|
395
|
+
ip = request.remote or "unknown"
|
|
396
|
+
if not self.rate_limiter.allow(ip):
|
|
397
|
+
return web.json_response(
|
|
398
|
+
{"jsonrpc": "2.0", "error": {"code": -32000, "message": "rate limit exceeded"}, "id": None},
|
|
399
|
+
status=429, headers=headers,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Parse body
|
|
403
|
+
try:
|
|
404
|
+
body = await request.json()
|
|
405
|
+
except Exception:
|
|
406
|
+
resp = self._error_response(None, RPCErrorCode.PARSE_ERROR, "Invalid JSON")
|
|
407
|
+
return web.json_response(resp, headers=headers)
|
|
408
|
+
|
|
409
|
+
# Batch request?
|
|
410
|
+
if isinstance(body, list):
|
|
411
|
+
if len(body) == 0:
|
|
412
|
+
resp = self._error_response(None, RPCErrorCode.INVALID_REQUEST, "Empty batch")
|
|
413
|
+
return web.json_response(resp, headers=headers)
|
|
414
|
+
if len(body) > 100:
|
|
415
|
+
resp = self._error_response(None, RPCErrorCode.INVALID_REQUEST, "Batch too large (max 100)")
|
|
416
|
+
return web.json_response(resp, headers=headers)
|
|
417
|
+
results = await asyncio.gather(
|
|
418
|
+
*[self._process_single_request(r) for r in body]
|
|
419
|
+
)
|
|
420
|
+
# Filter out notifications (no id)
|
|
421
|
+
results = [r for r in results if r is not None]
|
|
422
|
+
return web.json_response(results, headers=headers)
|
|
423
|
+
|
|
424
|
+
# Single request
|
|
425
|
+
result = await self._process_single_request(body)
|
|
426
|
+
if result is None:
|
|
427
|
+
return web.Response(status=204, headers=headers)
|
|
428
|
+
return web.json_response(result, headers=headers)
|
|
429
|
+
|
|
430
|
+
async def _handle_cors_preflight(self, request):
|
|
431
|
+
"""Handle CORS preflight OPTIONS requests."""
|
|
432
|
+
from aiohttp import web
|
|
433
|
+
return web.Response(
|
|
434
|
+
status=204,
|
|
435
|
+
headers={
|
|
436
|
+
"Access-Control-Allow-Origin": self.cors_origins,
|
|
437
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
438
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
439
|
+
"Access-Control-Max-Age": "86400",
|
|
440
|
+
},
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
async def _handle_health(self, request):
|
|
444
|
+
"""Health check endpoint."""
|
|
445
|
+
from aiohttp import web
|
|
446
|
+
return web.json_response({
|
|
447
|
+
"status": "ok",
|
|
448
|
+
"chain_id": self.node.config.chain_id,
|
|
449
|
+
"height": self.node.chain.height,
|
|
450
|
+
"peers": self.node.network.peer_count,
|
|
451
|
+
"rpc_methods": len(self.registry),
|
|
452
|
+
"subscriptions": self.subscriptions.count,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
async def _handle_ws_or_info(self, request):
|
|
456
|
+
"""GET / — upgrade to WS if requested, otherwise return node info."""
|
|
457
|
+
from aiohttp import web
|
|
458
|
+
if request.headers.get("Upgrade", "").lower() == "websocket":
|
|
459
|
+
return await self._handle_ws(request)
|
|
460
|
+
# Return a friendly info page for browsers
|
|
461
|
+
info = {
|
|
462
|
+
"jsonrpc": "2.0",
|
|
463
|
+
"server": "Zexus Blockchain RPC",
|
|
464
|
+
"chain_id": self.node.config.chain_id,
|
|
465
|
+
"height": self.node.chain.height,
|
|
466
|
+
"methods": self.registry.list_methods(),
|
|
467
|
+
}
|
|
468
|
+
return web.json_response(info, headers={
|
|
469
|
+
"Access-Control-Allow-Origin": self.cors_origins,
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
# ------------------------------------------------------------------
|
|
473
|
+
# WebSocket handler
|
|
474
|
+
# ------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
async def _handle_ws(self, request):
|
|
477
|
+
"""Handle a WebSocket connection (subscriptions + regular calls)."""
|
|
478
|
+
from aiohttp import web, WSMsgType
|
|
479
|
+
|
|
480
|
+
ws = web.WebSocketResponse()
|
|
481
|
+
await ws.prepare(request)
|
|
482
|
+
|
|
483
|
+
logger.debug("WebSocket client connected: %s", request.remote)
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
async for msg in ws:
|
|
487
|
+
if msg.type == WSMsgType.TEXT:
|
|
488
|
+
try:
|
|
489
|
+
body = json.loads(msg.data)
|
|
490
|
+
except json.JSONDecodeError:
|
|
491
|
+
await ws.send_json(
|
|
492
|
+
self._error_response(None, RPCErrorCode.PARSE_ERROR, "Invalid JSON")
|
|
493
|
+
)
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
if isinstance(body, list):
|
|
497
|
+
results = await asyncio.gather(
|
|
498
|
+
*[self._process_single_request(r, ws=ws) for r in body]
|
|
499
|
+
)
|
|
500
|
+
results = [r for r in results if r is not None]
|
|
501
|
+
if results:
|
|
502
|
+
await ws.send_json(results)
|
|
503
|
+
else:
|
|
504
|
+
result = await self._process_single_request(body, ws=ws)
|
|
505
|
+
if result is not None:
|
|
506
|
+
await ws.send_json(result)
|
|
507
|
+
|
|
508
|
+
elif msg.type in (WSMsgType.ERROR, WSMsgType.CLOSE):
|
|
509
|
+
break
|
|
510
|
+
finally:
|
|
511
|
+
removed = self.subscriptions.remove_all_for_ws(ws)
|
|
512
|
+
if removed:
|
|
513
|
+
logger.debug("Cleaned up %d subscriptions for disconnected client", removed)
|
|
514
|
+
|
|
515
|
+
return ws
|
|
516
|
+
|
|
517
|
+
# ------------------------------------------------------------------
|
|
518
|
+
# Request processing
|
|
519
|
+
# ------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
async def _process_single_request(
|
|
522
|
+
self, body: Any, ws: Any = None,
|
|
523
|
+
) -> Optional[Dict[str, Any]]:
|
|
524
|
+
"""Process a single JSON-RPC request and return the response dict."""
|
|
525
|
+
if not isinstance(body, dict):
|
|
526
|
+
return self._error_response(None, RPCErrorCode.INVALID_REQUEST, "Request must be an object")
|
|
527
|
+
|
|
528
|
+
req_id = body.get("id")
|
|
529
|
+
method = body.get("method")
|
|
530
|
+
params = body.get("params", [])
|
|
531
|
+
|
|
532
|
+
# Validate
|
|
533
|
+
if body.get("jsonrpc") != "2.0":
|
|
534
|
+
return self._error_response(req_id, RPCErrorCode.INVALID_REQUEST, "jsonrpc must be '2.0'")
|
|
535
|
+
if not method or not isinstance(method, str):
|
|
536
|
+
return self._error_response(req_id, RPCErrorCode.INVALID_REQUEST, "missing or invalid method")
|
|
537
|
+
|
|
538
|
+
# Special subscription methods that need WS
|
|
539
|
+
if method == "zx_subscribe" and ws is not None:
|
|
540
|
+
return await self._handle_subscribe(req_id, params, ws)
|
|
541
|
+
if method == "zx_unsubscribe" and ws is not None:
|
|
542
|
+
return await self._handle_unsubscribe(req_id, params)
|
|
543
|
+
|
|
544
|
+
# Dispatch to registered handler
|
|
545
|
+
info = self.registry.get(method)
|
|
546
|
+
if info is None:
|
|
547
|
+
return self._error_response(req_id, RPCErrorCode.METHOD_NOT_FOUND, f"method not found: {method}")
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
if info.is_async:
|
|
551
|
+
result = await info.handler(params)
|
|
552
|
+
else:
|
|
553
|
+
result = info.handler(params)
|
|
554
|
+
return self._success_response(req_id, _safe_serialize(result))
|
|
555
|
+
except RPCError as e:
|
|
556
|
+
return self._error_response(req_id, e.code, e.message, e.data)
|
|
557
|
+
except Exception as e:
|
|
558
|
+
logger.error("RPC handler error [%s]: %s\n%s", method, e, traceback.format_exc())
|
|
559
|
+
return self._error_response(req_id, RPCErrorCode.INTERNAL_ERROR, str(e))
|
|
560
|
+
|
|
561
|
+
# ------------------------------------------------------------------
|
|
562
|
+
# Subscriptions
|
|
563
|
+
# ------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
async def _handle_subscribe(self, req_id, params, ws) -> Dict:
|
|
566
|
+
"""Handle zx_subscribe."""
|
|
567
|
+
event = _require_param(params, 0, "event_name")
|
|
568
|
+
sub_params = _optional_param(params, 1, {})
|
|
569
|
+
valid_events = {"newHeads", "newPendingTransactions", "logs", "syncing"}
|
|
570
|
+
if event not in valid_events:
|
|
571
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS,
|
|
572
|
+
f"unknown subscription event: {event}. "
|
|
573
|
+
f"Valid: {', '.join(sorted(valid_events))}")
|
|
574
|
+
sub_id = self.subscriptions.add(event, ws, sub_params if isinstance(sub_params, dict) else {})
|
|
575
|
+
return self._success_response(req_id, sub_id)
|
|
576
|
+
|
|
577
|
+
async def _handle_unsubscribe(self, req_id, params) -> Dict:
|
|
578
|
+
"""Handle zx_unsubscribe."""
|
|
579
|
+
sub_id = _require_param(params, 0, "subscription_id")
|
|
580
|
+
removed = self.subscriptions.remove(sub_id)
|
|
581
|
+
return self._success_response(req_id, removed)
|
|
582
|
+
|
|
583
|
+
def _wire_node_events(self):
|
|
584
|
+
"""Connect node events to subscription notifications."""
|
|
585
|
+
def on_new_block(block):
|
|
586
|
+
if not self._running:
|
|
587
|
+
return
|
|
588
|
+
data = _safe_serialize(block.to_dict()) if hasattr(block, "to_dict") else {}
|
|
589
|
+
asyncio.run_coroutine_threadsafe(
|
|
590
|
+
self.subscriptions.notify("newHeads", data),
|
|
591
|
+
asyncio.get_event_loop(),
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def on_new_tx(tx):
|
|
595
|
+
if not self._running:
|
|
596
|
+
return
|
|
597
|
+
tx_data = _safe_serialize(tx.to_dict()) if hasattr(tx, "to_dict") else {"tx_hash": getattr(tx, "tx_hash", "")}
|
|
598
|
+
asyncio.run_coroutine_threadsafe(
|
|
599
|
+
self.subscriptions.notify("newPendingTransactions", tx_data),
|
|
600
|
+
asyncio.get_event_loop(),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
self.node.on("new_block", on_new_block)
|
|
604
|
+
self.node.on("mined", on_new_block)
|
|
605
|
+
self.node.on("new_tx", on_new_tx)
|
|
606
|
+
|
|
607
|
+
# ------------------------------------------------------------------
|
|
608
|
+
# Response builders
|
|
609
|
+
# ------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def _success_response(req_id: Any, result: Any) -> Dict[str, Any]:
|
|
613
|
+
return {"jsonrpc": "2.0", "result": result, "id": req_id}
|
|
614
|
+
|
|
615
|
+
@staticmethod
|
|
616
|
+
def _error_response(req_id: Any, code: int, message: str, data: Any = None) -> Dict[str, Any]:
|
|
617
|
+
err: Dict[str, Any] = {"code": int(code), "message": message}
|
|
618
|
+
if data is not None:
|
|
619
|
+
err["data"] = data
|
|
620
|
+
return {"jsonrpc": "2.0", "error": err, "id": req_id}
|
|
621
|
+
|
|
622
|
+
# ══════════════════════════════════════════════════════════════════
|
|
623
|
+
# RPC method registration
|
|
624
|
+
# ══════════════════════════════════════════════════════════════════
|
|
625
|
+
|
|
626
|
+
def _register_all_methods(self):
|
|
627
|
+
"""Register every RPC method."""
|
|
628
|
+
r = self.registry.register
|
|
629
|
+
|
|
630
|
+
# ── zx_* namespace: core blockchain queries ───────────────
|
|
631
|
+
r("zx_chainId", self._zx_chain_id, "Get the chain ID")
|
|
632
|
+
r("zx_blockNumber", self._zx_block_number, "Get current block height")
|
|
633
|
+
r("zx_getBlockByNumber", self._zx_get_block_by_number, "Get block by height")
|
|
634
|
+
r("zx_getBlockByHash", self._zx_get_block_by_hash, "Get block by hash")
|
|
635
|
+
r("zx_getBlockTransactionCount", self._zx_get_block_tx_count, "Get tx count in a block")
|
|
636
|
+
r("zx_getBalance", self._zx_get_balance, "Get account balance")
|
|
637
|
+
r("zx_getTransactionCount", self._zx_get_tx_count, "Get account nonce")
|
|
638
|
+
r("zx_getAccount", self._zx_get_account, "Get full account state")
|
|
639
|
+
r("zx_getTransactionByHash", self._zx_get_tx_by_hash, "Get a transaction by hash")
|
|
640
|
+
r("zx_getTransactionReceipt", self._zx_get_tx_receipt, "Get a transaction receipt")
|
|
641
|
+
r("zx_sendTransaction", self._zx_send_transaction, "Submit a signed transaction")
|
|
642
|
+
r("zx_sendRawTransaction", self._zx_send_raw_transaction, "Submit a raw transaction dict")
|
|
643
|
+
r("zx_gasPrice", self._zx_gas_price, "Get suggested gas price")
|
|
644
|
+
r("zx_estimateGas", self._zx_estimate_gas, "Estimate gas for a call")
|
|
645
|
+
r("zx_getChainInfo", self._zx_get_chain_info, "Get chain + network summary")
|
|
646
|
+
r("zx_validateChain", self._zx_validate_chain, "Validate full chain integrity")
|
|
647
|
+
r("zx_getCode", self._zx_get_code, "Get contract code at address")
|
|
648
|
+
r("zx_getLogs", self._zx_get_logs, "Get event logs (filtered)")
|
|
649
|
+
|
|
650
|
+
# ── txpool_* namespace: mempool ───────────────────────────
|
|
651
|
+
r("txpool_status", self._txpool_status, "Mempool size and stats")
|
|
652
|
+
r("txpool_content", self._txpool_content, "List pending transactions")
|
|
653
|
+
r("txpool_replaceByFee", self._txpool_replace_by_fee, "Replace tx with higher gas price (RBF)")
|
|
654
|
+
|
|
655
|
+
# ── net_* namespace: networking ───────────────────────────
|
|
656
|
+
r("net_version", self._net_version, "Get network / chain ID")
|
|
657
|
+
r("net_peerCount", self._net_peer_count, "Get connected peer count")
|
|
658
|
+
r("net_listening", self._net_listening, "Is node listening for connections")
|
|
659
|
+
r("net_peers", self._net_peers, "Get connected peer info")
|
|
660
|
+
|
|
661
|
+
# ── miner_* namespace: mining control ─────────────────────
|
|
662
|
+
r("miner_start", self._miner_start, "Start mining")
|
|
663
|
+
r("miner_stop", self._miner_stop, "Stop mining")
|
|
664
|
+
r("miner_status", self._miner_status, "Get mining status")
|
|
665
|
+
r("miner_setMinerAddress", self._miner_set_address, "Set miner reward address")
|
|
666
|
+
r("miner_mineBlock", self._miner_mine_block, "Mine one block synchronously")
|
|
667
|
+
|
|
668
|
+
# ── contract_* namespace: smart contracts ─────────────────
|
|
669
|
+
r("contract_deploy", self._contract_deploy, "Deploy a smart contract")
|
|
670
|
+
r("contract_call", self._contract_call, "Execute a contract action (state-changing)")
|
|
671
|
+
r("contract_staticCall", self._contract_static_call, "Read-only contract call")
|
|
672
|
+
|
|
673
|
+
# ── admin_* namespace: node administration ────────────────
|
|
674
|
+
r("admin_nodeInfo", self._admin_node_info, "Get full node information")
|
|
675
|
+
r("admin_fundAccount", self._admin_fund_account, "Fund an account (devnet)")
|
|
676
|
+
r("admin_exportChain", self._admin_export_chain, "Export full chain JSON")
|
|
677
|
+
r("admin_rpcMethods", self._admin_rpc_methods, "List all available RPC methods")
|
|
678
|
+
|
|
679
|
+
# ══════════════════════════════════════════════════════════════════
|
|
680
|
+
# zx_* handlers
|
|
681
|
+
# ══════════════════════════════════════════════════════════════════
|
|
682
|
+
|
|
683
|
+
def _zx_chain_id(self, params) -> str:
|
|
684
|
+
return self.node.config.chain_id
|
|
685
|
+
|
|
686
|
+
def _zx_block_number(self, params) -> str:
|
|
687
|
+
return _hex_int(self.node.chain.height)
|
|
688
|
+
|
|
689
|
+
def _zx_get_block_by_number(self, params) -> Optional[Dict]:
|
|
690
|
+
height = _parse_hex_int(_require_param(params, 0, "block_number"))
|
|
691
|
+
full_txs = _optional_param(params, 1, False)
|
|
692
|
+
block = self.node.get_block(height)
|
|
693
|
+
if block is None:
|
|
694
|
+
return None
|
|
695
|
+
if not full_txs:
|
|
696
|
+
# Return only tx hashes instead of full tx objects
|
|
697
|
+
block["transactions"] = [
|
|
698
|
+
tx.get("tx_hash", "") if isinstance(tx, dict) else tx
|
|
699
|
+
for tx in block.get("transactions", [])
|
|
700
|
+
]
|
|
701
|
+
return block
|
|
702
|
+
|
|
703
|
+
def _zx_get_block_by_hash(self, params) -> Optional[Dict]:
|
|
704
|
+
block_hash = _require_param(params, 0, "block_hash")
|
|
705
|
+
full_txs = _optional_param(params, 1, False)
|
|
706
|
+
block = self.node.get_block(block_hash)
|
|
707
|
+
if block is None:
|
|
708
|
+
return None
|
|
709
|
+
if not full_txs:
|
|
710
|
+
block["transactions"] = [
|
|
711
|
+
tx.get("tx_hash", "") if isinstance(tx, dict) else tx
|
|
712
|
+
for tx in block.get("transactions", [])
|
|
713
|
+
]
|
|
714
|
+
return block
|
|
715
|
+
|
|
716
|
+
def _zx_get_block_tx_count(self, params) -> str:
|
|
717
|
+
block_id = _require_param(params, 0, "block_number_or_hash")
|
|
718
|
+
block_id = _parse_hex_int(block_id) if isinstance(block_id, str) and block_id.startswith("0x") else block_id
|
|
719
|
+
block = self.node.get_block(block_id)
|
|
720
|
+
if block is None:
|
|
721
|
+
raise RPCError(RPCErrorCode.NOT_FOUND, "block not found")
|
|
722
|
+
return _hex_int(len(block.get("transactions", [])))
|
|
723
|
+
|
|
724
|
+
def _zx_get_balance(self, params) -> str:
|
|
725
|
+
address = _require_param(params, 0, "address")
|
|
726
|
+
balance = self.node.get_balance(address)
|
|
727
|
+
return _hex_int(balance)
|
|
728
|
+
|
|
729
|
+
def _zx_get_tx_count(self, params) -> str:
|
|
730
|
+
address = _require_param(params, 0, "address")
|
|
731
|
+
nonce = self.node.get_nonce(address)
|
|
732
|
+
return _hex_int(nonce)
|
|
733
|
+
|
|
734
|
+
def _zx_get_account(self, params) -> Dict:
|
|
735
|
+
address = _require_param(params, 0, "address")
|
|
736
|
+
return self.node.get_account(address)
|
|
737
|
+
|
|
738
|
+
def _zx_get_tx_by_hash(self, params) -> Optional[Dict]:
|
|
739
|
+
tx_hash = _require_param(params, 0, "tx_hash")
|
|
740
|
+
# Search in blocks
|
|
741
|
+
for block in self.node.chain.blocks:
|
|
742
|
+
for tx in block.transactions:
|
|
743
|
+
if tx.tx_hash == tx_hash:
|
|
744
|
+
result = tx.to_dict()
|
|
745
|
+
result["block_hash"] = block.hash
|
|
746
|
+
result["block_height"] = block.header.height
|
|
747
|
+
return result
|
|
748
|
+
# Search in mempool
|
|
749
|
+
for tx in self.node.mempool.get_pending():
|
|
750
|
+
if tx.tx_hash == tx_hash:
|
|
751
|
+
result = tx.to_dict()
|
|
752
|
+
result["block_hash"] = None
|
|
753
|
+
result["block_height"] = None
|
|
754
|
+
result["status"] = "pending"
|
|
755
|
+
return result
|
|
756
|
+
return None
|
|
757
|
+
|
|
758
|
+
def _zx_get_tx_receipt(self, params) -> Optional[Dict]:
|
|
759
|
+
tx_hash = _require_param(params, 0, "tx_hash")
|
|
760
|
+
for block in self.node.chain.blocks:
|
|
761
|
+
for receipt in block.receipts:
|
|
762
|
+
if receipt.tx_hash == tx_hash:
|
|
763
|
+
result = receipt.to_dict()
|
|
764
|
+
result["block_hash"] = block.hash
|
|
765
|
+
result["block_height"] = block.header.height
|
|
766
|
+
return result
|
|
767
|
+
# Check if tx is in this block (even with no explicit receipt)
|
|
768
|
+
for tx in block.transactions:
|
|
769
|
+
if tx.tx_hash == tx_hash:
|
|
770
|
+
return {
|
|
771
|
+
"tx_hash": tx_hash,
|
|
772
|
+
"block_hash": block.hash,
|
|
773
|
+
"block_height": block.header.height,
|
|
774
|
+
"status": 1,
|
|
775
|
+
"gas_used": tx.gas_limit,
|
|
776
|
+
}
|
|
777
|
+
return None
|
|
778
|
+
|
|
779
|
+
def _zx_send_transaction(self, params) -> str:
|
|
780
|
+
"""Create and submit a transaction from parameters."""
|
|
781
|
+
tx_data = _require_param(params, 0, "transaction")
|
|
782
|
+
if not isinstance(tx_data, dict):
|
|
783
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, "transaction must be an object")
|
|
784
|
+
|
|
785
|
+
from .chain import Transaction
|
|
786
|
+
tx = Transaction(
|
|
787
|
+
sender=tx_data.get("from", tx_data.get("sender", "")),
|
|
788
|
+
recipient=tx_data.get("to", tx_data.get("recipient", "")),
|
|
789
|
+
value=_parse_hex_int(tx_data.get("value", 0)),
|
|
790
|
+
data=tx_data.get("data", ""),
|
|
791
|
+
nonce=_parse_hex_int(tx_data.get("nonce", 0)),
|
|
792
|
+
gas_limit=_parse_hex_int(tx_data.get("gas", tx_data.get("gasLimit", tx_data.get("gas_limit", 21000)))),
|
|
793
|
+
gas_price=_parse_hex_int(tx_data.get("gasPrice", tx_data.get("gas_price", 1))),
|
|
794
|
+
timestamp=time.time(),
|
|
795
|
+
)
|
|
796
|
+
tx.compute_hash()
|
|
797
|
+
|
|
798
|
+
# Auto-fill nonce if not explicitly provided
|
|
799
|
+
if "nonce" not in tx_data:
|
|
800
|
+
acct = self.node.chain.get_account(tx.sender)
|
|
801
|
+
chain_nonce = acct["nonce"]
|
|
802
|
+
mempool_nonce = self.node.mempool._nonces.get(tx.sender, 0)
|
|
803
|
+
tx.nonce = max(chain_nonce, mempool_nonce)
|
|
804
|
+
tx.compute_hash()
|
|
805
|
+
|
|
806
|
+
# Set dev signature for RPC-submitted transactions
|
|
807
|
+
if not tx.signature:
|
|
808
|
+
tx.signature = "rpc_dev_" + tx.tx_hash[:56]
|
|
809
|
+
|
|
810
|
+
result = self.node.submit_transaction(tx)
|
|
811
|
+
if not result["success"]:
|
|
812
|
+
error_msg = result.get("error", "transaction rejected")
|
|
813
|
+
if "insufficient balance" in error_msg:
|
|
814
|
+
raise RPCError(RPCErrorCode.INSUFFICIENT_FUNDS, error_msg)
|
|
815
|
+
if "nonce too low" in error_msg:
|
|
816
|
+
raise RPCError(RPCErrorCode.NONCE_TOO_LOW, error_msg)
|
|
817
|
+
raise RPCError(RPCErrorCode.TX_REJECTED, error_msg)
|
|
818
|
+
return result["tx_hash"]
|
|
819
|
+
|
|
820
|
+
def _zx_send_raw_transaction(self, params) -> str:
|
|
821
|
+
"""Submit a pre-built transaction dict."""
|
|
822
|
+
tx_data = _require_param(params, 0, "raw_transaction")
|
|
823
|
+
if not isinstance(tx_data, dict):
|
|
824
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, "raw_transaction must be an object")
|
|
825
|
+
|
|
826
|
+
from .chain import Transaction
|
|
827
|
+
tx = Transaction(**{k: v for k, v in tx_data.items() if k in Transaction.__dataclass_fields__})
|
|
828
|
+
if not tx.tx_hash:
|
|
829
|
+
tx.compute_hash()
|
|
830
|
+
if not tx.signature:
|
|
831
|
+
tx.signature = "rpc_dev_" + tx.tx_hash[:56]
|
|
832
|
+
|
|
833
|
+
result = self.node.submit_transaction(tx)
|
|
834
|
+
if not result["success"]:
|
|
835
|
+
raise RPCError(RPCErrorCode.TX_REJECTED, result.get("error", "rejected"))
|
|
836
|
+
return result["tx_hash"]
|
|
837
|
+
|
|
838
|
+
def _zx_gas_price(self, params) -> str:
|
|
839
|
+
"""Return suggested gas price (simple: median of recent block txs)."""
|
|
840
|
+
prices = []
|
|
841
|
+
for block in self.node.chain.blocks[-10:]:
|
|
842
|
+
for tx in block.transactions:
|
|
843
|
+
if tx.gas_price > 0:
|
|
844
|
+
prices.append(tx.gas_price)
|
|
845
|
+
if prices:
|
|
846
|
+
prices.sort()
|
|
847
|
+
median = prices[len(prices) // 2]
|
|
848
|
+
return _hex_int(max(median, 1))
|
|
849
|
+
return _hex_int(1) # Minimum gas price
|
|
850
|
+
|
|
851
|
+
def _zx_estimate_gas(self, params) -> str:
|
|
852
|
+
"""Estimate gas for a transaction via dry-run execution.
|
|
853
|
+
|
|
854
|
+
For simple transfers: 21,000 gas.
|
|
855
|
+
For contract deployments (no recipient, has data): size-based estimate.
|
|
856
|
+
For contract calls: attempts dry-run via ContractVM.static_call,
|
|
857
|
+
returning actual gas_used + 20 % safety margin.
|
|
858
|
+
"""
|
|
859
|
+
tx_data = _optional_param(params, 0, {})
|
|
860
|
+
if not isinstance(tx_data, dict):
|
|
861
|
+
return _hex_int(21_000)
|
|
862
|
+
|
|
863
|
+
data = tx_data.get("data", "")
|
|
864
|
+
to = tx_data.get("to", tx_data.get("recipient", ""))
|
|
865
|
+
value = _parse_hex_int(tx_data.get("value", 0))
|
|
866
|
+
|
|
867
|
+
# Simple value transfer (no data, has recipient)
|
|
868
|
+
if not data and to:
|
|
869
|
+
return _hex_int(21_000)
|
|
870
|
+
|
|
871
|
+
# Contract deployment (has data, no recipient)
|
|
872
|
+
if data and not to:
|
|
873
|
+
# Base cost + per-byte cost (200 gas per byte of code)
|
|
874
|
+
base = 53_000
|
|
875
|
+
per_byte = len(data.encode("utf-8")) * 200
|
|
876
|
+
return _hex_int(base + per_byte)
|
|
877
|
+
|
|
878
|
+
# Contract call — try dry-run via static_call
|
|
879
|
+
if data and to:
|
|
880
|
+
try:
|
|
881
|
+
import json as _j
|
|
882
|
+
call_data = _j.loads(data) if isinstance(data, str) and data.startswith("{") else {}
|
|
883
|
+
action = call_data.get("action", "")
|
|
884
|
+
args = call_data.get("args", {})
|
|
885
|
+
caller = tx_data.get("from", tx_data.get("sender", ""))
|
|
886
|
+
if action and self.node.contract_vm:
|
|
887
|
+
receipt = self.node.contract_vm.static_call(
|
|
888
|
+
contract_address=to,
|
|
889
|
+
action=action,
|
|
890
|
+
args=args,
|
|
891
|
+
caller=caller,
|
|
892
|
+
)
|
|
893
|
+
gas_used = receipt.gas_used if receipt.gas_used > 0 else 50_000
|
|
894
|
+
# Add 20% safety margin
|
|
895
|
+
estimated = int(gas_used * 1.2)
|
|
896
|
+
return _hex_int(estimated)
|
|
897
|
+
except Exception:
|
|
898
|
+
pass
|
|
899
|
+
# Fallback for generic contract interaction
|
|
900
|
+
return _hex_int(500_000)
|
|
901
|
+
|
|
902
|
+
# Fallback: data but unclear intent
|
|
903
|
+
if data:
|
|
904
|
+
return _hex_int(500_000)
|
|
905
|
+
return _hex_int(21_000)
|
|
906
|
+
|
|
907
|
+
def _zx_get_chain_info(self, params) -> Dict:
|
|
908
|
+
return self.node.get_chain_info()
|
|
909
|
+
|
|
910
|
+
def _zx_validate_chain(self, params) -> Dict:
|
|
911
|
+
return self.node.validate_chain()
|
|
912
|
+
|
|
913
|
+
def _zx_get_code(self, params) -> str:
|
|
914
|
+
"""Get contract code/state at an address."""
|
|
915
|
+
address = _require_param(params, 0, "address")
|
|
916
|
+
# Check contract_state first (populated by contract_deploy)
|
|
917
|
+
cs = self.node.chain.contract_state.get(address, {})
|
|
918
|
+
if cs.get("code"):
|
|
919
|
+
return cs["code"]
|
|
920
|
+
acct = self.node.chain.get_account(address)
|
|
921
|
+
return acct.get("code", "")
|
|
922
|
+
|
|
923
|
+
def _zx_get_logs(self, params) -> List[Dict]:
|
|
924
|
+
"""Get event logs filtered by block range, address, topics, and event name.
|
|
925
|
+
|
|
926
|
+
Uses the EventIndex (if available) for fast indexed lookups,
|
|
927
|
+
otherwise falls back to scanning block receipts.
|
|
928
|
+
"""
|
|
929
|
+
filter_obj = _optional_param(params, 0, {})
|
|
930
|
+
if not isinstance(filter_obj, dict):
|
|
931
|
+
filter_obj = {}
|
|
932
|
+
|
|
933
|
+
from_block = _parse_hex_int(filter_obj.get("fromBlock", 0))
|
|
934
|
+
to_block = _parse_hex_int(filter_obj.get("toBlock", self.node.chain.height))
|
|
935
|
+
target_address = filter_obj.get("address")
|
|
936
|
+
topics = filter_obj.get("topics")
|
|
937
|
+
event_name = filter_obj.get("eventName")
|
|
938
|
+
limit = filter_obj.get("limit", 10_000)
|
|
939
|
+
|
|
940
|
+
# ── Fast path: use EventIndex if available ──
|
|
941
|
+
if self.node.event_index:
|
|
942
|
+
try:
|
|
943
|
+
from .events import LogFilter
|
|
944
|
+
filt = LogFilter(
|
|
945
|
+
from_block=from_block,
|
|
946
|
+
to_block=to_block,
|
|
947
|
+
address=target_address,
|
|
948
|
+
topics=topics,
|
|
949
|
+
event_name=event_name,
|
|
950
|
+
limit=limit,
|
|
951
|
+
)
|
|
952
|
+
results = self.node.event_index.get_logs(filt)
|
|
953
|
+
return [log.to_dict() for log in results]
|
|
954
|
+
except Exception:
|
|
955
|
+
pass # Fall through to scan
|
|
956
|
+
|
|
957
|
+
# ── Fallback: scan receipts directly ──
|
|
958
|
+
logs = []
|
|
959
|
+
for h in range(from_block, min(to_block + 1, self.node.chain.height + 1)):
|
|
960
|
+
block = self.node.chain.get_block(h)
|
|
961
|
+
if block is None:
|
|
962
|
+
continue
|
|
963
|
+
for receipt in block.receipts:
|
|
964
|
+
for log_entry in receipt.logs:
|
|
965
|
+
if target_address and log_entry.get("address") != target_address:
|
|
966
|
+
continue
|
|
967
|
+
if event_name and log_entry.get("event") != event_name:
|
|
968
|
+
continue
|
|
969
|
+
enriched = dict(log_entry)
|
|
970
|
+
enriched["block_hash"] = block.hash
|
|
971
|
+
enriched["block_height"] = block.header.height
|
|
972
|
+
enriched["tx_hash"] = receipt.tx_hash
|
|
973
|
+
logs.append(enriched)
|
|
974
|
+
if len(logs) >= limit:
|
|
975
|
+
return logs
|
|
976
|
+
return logs
|
|
977
|
+
|
|
978
|
+
# ══════════════════════════════════════════════════════════════════
|
|
979
|
+
# txpool_* handlers
|
|
980
|
+
# ══════════════════════════════════════════════════════════════════
|
|
981
|
+
|
|
982
|
+
def _txpool_status(self, params) -> Dict:
|
|
983
|
+
mp = self.node.mempool
|
|
984
|
+
return {
|
|
985
|
+
"pending": mp.size,
|
|
986
|
+
"queued": 0,
|
|
987
|
+
"rbf_enabled": mp.rbf_enabled,
|
|
988
|
+
"rbf_increment_pct": mp.rbf_increment_pct,
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
def _txpool_content(self, params) -> Dict:
|
|
992
|
+
pending = self.node.mempool.get_pending()
|
|
993
|
+
return {
|
|
994
|
+
"pending": {
|
|
995
|
+
tx.sender: {str(tx.nonce): tx.to_dict()} for tx in pending
|
|
996
|
+
},
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
def _txpool_replace_by_fee(self, params) -> Dict:
|
|
1000
|
+
"""Replace a pending tx with a higher-gas-price version (RBF)."""
|
|
1001
|
+
tx_data = _require_param(params, 0, "transaction")
|
|
1002
|
+
if not isinstance(tx_data, dict):
|
|
1003
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, "transaction must be an object")
|
|
1004
|
+
|
|
1005
|
+
from .chain import Transaction
|
|
1006
|
+
tx = Transaction(
|
|
1007
|
+
sender=tx_data.get("from", tx_data.get("sender", "")),
|
|
1008
|
+
recipient=tx_data.get("to", tx_data.get("recipient", "")),
|
|
1009
|
+
value=_parse_hex_int(tx_data.get("value", 0)),
|
|
1010
|
+
data=tx_data.get("data", ""),
|
|
1011
|
+
nonce=_parse_hex_int(tx_data.get("nonce", 0)),
|
|
1012
|
+
gas_limit=_parse_hex_int(tx_data.get("gas", tx_data.get("gasLimit", 21000))),
|
|
1013
|
+
gas_price=_parse_hex_int(tx_data.get("gasPrice", tx_data.get("gas_price", 1))),
|
|
1014
|
+
timestamp=time.time(),
|
|
1015
|
+
)
|
|
1016
|
+
tx.compute_hash()
|
|
1017
|
+
if not tx.signature:
|
|
1018
|
+
tx.signature = "rpc_dev_" + tx.tx_hash[:56]
|
|
1019
|
+
|
|
1020
|
+
result = self.node.mempool.replace_by_fee(tx)
|
|
1021
|
+
if not result["replaced"]:
|
|
1022
|
+
raise RPCError(RPCErrorCode.TX_REJECTED, result.get("error", "RBF failed"))
|
|
1023
|
+
return {
|
|
1024
|
+
"new_tx_hash": tx.tx_hash,
|
|
1025
|
+
"old_tx_hash": result["old_hash"],
|
|
1026
|
+
"gas_price": tx.gas_price,
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1030
|
+
# net_* handlers
|
|
1031
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1032
|
+
|
|
1033
|
+
def _net_version(self, params) -> str:
|
|
1034
|
+
return self.node.config.chain_id
|
|
1035
|
+
|
|
1036
|
+
def _net_peer_count(self, params) -> str:
|
|
1037
|
+
return _hex_int(self.node.network.peer_count)
|
|
1038
|
+
|
|
1039
|
+
def _net_listening(self, params) -> bool:
|
|
1040
|
+
return self.node.network.is_running
|
|
1041
|
+
|
|
1042
|
+
def _net_peers(self, params) -> List[Dict]:
|
|
1043
|
+
info = self.node.network.get_network_info()
|
|
1044
|
+
return info.get("peers", [])
|
|
1045
|
+
|
|
1046
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1047
|
+
# miner_* handlers
|
|
1048
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1049
|
+
|
|
1050
|
+
def _miner_start(self, params) -> bool:
|
|
1051
|
+
try:
|
|
1052
|
+
self.node.start_mining()
|
|
1053
|
+
return True
|
|
1054
|
+
except Exception as e:
|
|
1055
|
+
raise RPCError(RPCErrorCode.MINING_ERROR, str(e))
|
|
1056
|
+
|
|
1057
|
+
def _miner_stop(self, params) -> bool:
|
|
1058
|
+
self.node.stop_mining()
|
|
1059
|
+
return True
|
|
1060
|
+
|
|
1061
|
+
def _miner_status(self, params) -> Dict:
|
|
1062
|
+
return {
|
|
1063
|
+
"mining": self.node._mining,
|
|
1064
|
+
"miner_address": self.node.config.miner_address,
|
|
1065
|
+
"consensus": self.node.config.consensus,
|
|
1066
|
+
"height": self.node.chain.height,
|
|
1067
|
+
"mempool_size": self.node.mempool.size,
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
def _miner_set_address(self, params) -> bool:
|
|
1071
|
+
address = _require_param(params, 0, "address")
|
|
1072
|
+
self.node.config.miner_address = address
|
|
1073
|
+
return True
|
|
1074
|
+
|
|
1075
|
+
def _miner_mine_block(self, params) -> Optional[Dict]:
|
|
1076
|
+
"""Mine a single block synchronously."""
|
|
1077
|
+
block = self.node.mine_block_sync()
|
|
1078
|
+
if block is None:
|
|
1079
|
+
return None
|
|
1080
|
+
return block.to_dict()
|
|
1081
|
+
|
|
1082
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1083
|
+
# contract_* handlers
|
|
1084
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1085
|
+
|
|
1086
|
+
def _contract_deploy(self, params) -> Dict:
|
|
1087
|
+
"""Deploy a contract.
|
|
1088
|
+
|
|
1089
|
+
Expects: {code, deployer, gas_limit?, initial_value?}
|
|
1090
|
+
"""
|
|
1091
|
+
data = _require_param(params, 0, "deploy_params")
|
|
1092
|
+
if not isinstance(data, dict):
|
|
1093
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, "deploy_params must be an object")
|
|
1094
|
+
|
|
1095
|
+
code = _require_param(data, "code", "code")
|
|
1096
|
+
deployer = _require_param(data, "deployer", "deployer")
|
|
1097
|
+
gas_limit = data.get("gas_limit", data.get("gasLimit", 10_000_000))
|
|
1098
|
+
initial_value = data.get("initial_value", data.get("initialValue", 0))
|
|
1099
|
+
|
|
1100
|
+
if self.node.contract_vm is None:
|
|
1101
|
+
raise RPCError(RPCErrorCode.CONTRACT_ERROR, "ContractVM not available")
|
|
1102
|
+
|
|
1103
|
+
# Create a minimal contract object for the VM
|
|
1104
|
+
from .contract_vm import ContractVM
|
|
1105
|
+
import hashlib as _hl
|
|
1106
|
+
contract_address = "0x" + _hl.sha256(
|
|
1107
|
+
f"{deployer}:{code}:{time.time()}".encode()
|
|
1108
|
+
).hexdigest()[:40]
|
|
1109
|
+
|
|
1110
|
+
# Store the code in chain's contract state
|
|
1111
|
+
self.node.chain.contract_state[contract_address] = {
|
|
1112
|
+
"code": code,
|
|
1113
|
+
"deployer": deployer,
|
|
1114
|
+
"storage": {},
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return {
|
|
1118
|
+
"success": True,
|
|
1119
|
+
"address": contract_address,
|
|
1120
|
+
"deployer": deployer,
|
|
1121
|
+
"gas_used": 0,
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
def _contract_call(self, params) -> Dict:
|
|
1125
|
+
"""Execute a contract action (state-mutating)."""
|
|
1126
|
+
data = _require_param(params, 0, "call_params")
|
|
1127
|
+
if not isinstance(data, dict):
|
|
1128
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, "call_params must be an object")
|
|
1129
|
+
|
|
1130
|
+
address = _require_param(data, "address", "contract address")
|
|
1131
|
+
action = _require_param(data, "action", "action name")
|
|
1132
|
+
args = data.get("args", {})
|
|
1133
|
+
caller = data.get("caller", data.get("from", ""))
|
|
1134
|
+
gas_limit = data.get("gas_limit", data.get("gasLimit", 500_000))
|
|
1135
|
+
value = data.get("value", 0)
|
|
1136
|
+
|
|
1137
|
+
result = self.node.call_contract(
|
|
1138
|
+
contract_address=address,
|
|
1139
|
+
action=action,
|
|
1140
|
+
args=args,
|
|
1141
|
+
caller=caller,
|
|
1142
|
+
gas_limit=gas_limit,
|
|
1143
|
+
value=value,
|
|
1144
|
+
)
|
|
1145
|
+
if not result.get("success"):
|
|
1146
|
+
raise RPCError(RPCErrorCode.CONTRACT_ERROR, result.get("error", "contract call failed"))
|
|
1147
|
+
return result
|
|
1148
|
+
|
|
1149
|
+
def _contract_static_call(self, params) -> Dict:
|
|
1150
|
+
"""Execute a read-only contract call."""
|
|
1151
|
+
data = _require_param(params, 0, "call_params")
|
|
1152
|
+
if not isinstance(data, dict):
|
|
1153
|
+
raise RPCError(RPCErrorCode.INVALID_PARAMS, "call_params must be an object")
|
|
1154
|
+
|
|
1155
|
+
address = _require_param(data, "address", "contract address")
|
|
1156
|
+
action = _require_param(data, "action", "action name")
|
|
1157
|
+
args = data.get("args", {})
|
|
1158
|
+
caller = data.get("caller", data.get("from", ""))
|
|
1159
|
+
|
|
1160
|
+
result = self.node.static_call_contract(
|
|
1161
|
+
contract_address=address,
|
|
1162
|
+
action=action,
|
|
1163
|
+
args=args,
|
|
1164
|
+
caller=caller,
|
|
1165
|
+
)
|
|
1166
|
+
if not result.get("success"):
|
|
1167
|
+
raise RPCError(RPCErrorCode.CONTRACT_ERROR, result.get("error", "static call failed"))
|
|
1168
|
+
return result
|
|
1169
|
+
|
|
1170
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1171
|
+
# admin_* handlers
|
|
1172
|
+
# ══════════════════════════════════════════════════════════════════
|
|
1173
|
+
|
|
1174
|
+
def _admin_node_info(self, params) -> Dict:
|
|
1175
|
+
return {
|
|
1176
|
+
"chain_id": self.node.config.chain_id,
|
|
1177
|
+
"host": self.node.config.host,
|
|
1178
|
+
"port": self.node.config.port,
|
|
1179
|
+
"rpc_port": self.node.config.rpc_port,
|
|
1180
|
+
"consensus": self.node.config.consensus,
|
|
1181
|
+
"mining": self.node._mining,
|
|
1182
|
+
"miner_address": self.node.config.miner_address,
|
|
1183
|
+
"height": self.node.chain.height,
|
|
1184
|
+
"peers": self.node.network.peer_count,
|
|
1185
|
+
"mempool_size": self.node.mempool.size,
|
|
1186
|
+
"contract_vm_available": self.node.contract_vm is not None,
|
|
1187
|
+
"rpc_methods": self.registry.list_methods(),
|
|
1188
|
+
"subscriptions": self.subscriptions.count,
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
def _admin_fund_account(self, params) -> Dict:
|
|
1192
|
+
"""Fund an account (for testnets/devnets only)."""
|
|
1193
|
+
address = _require_param(params, 0, "address")
|
|
1194
|
+
amount = _parse_hex_int(_require_param(params, 1, "amount"))
|
|
1195
|
+
self.node.fund_account(address, amount)
|
|
1196
|
+
new_balance = self.node.get_balance(address)
|
|
1197
|
+
return {"address": address, "funded": amount, "new_balance": new_balance}
|
|
1198
|
+
|
|
1199
|
+
def _admin_export_chain(self, params) -> List[Dict]:
|
|
1200
|
+
return self.node.export_chain()
|
|
1201
|
+
|
|
1202
|
+
def _admin_rpc_methods(self, params) -> List[str]:
|
|
1203
|
+
return self.registry.list_methods()
|