-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.py
More file actions
124 lines (102 loc) · 3.98 KB
/
bootstrap.py
File metadata and controls
124 lines (102 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import json
import logging
import os
from typing import Any, Dict, List
import httpx
from config import Config
logger = logging.getLogger(__name__)
STAKEWIZ_VALIDATORS_URL = "https://api.stakewiz.com/validators"
def _parse_sui_ip(address_str: str) -> str:
"""Extract IP from multiaddr-style string like '/ip4/52.12.34.56/udp/8084'."""
if not address_str:
return ""
parts = address_str.split("/")
try:
idx = parts.index("ip4")
return parts[idx + 1]
except (ValueError, IndexError):
return ""
async def bootstrap_solana(config: Config, client: httpx.AsyncClient) -> None:
"""Fetch all validators from Stakewiz and write to stakewiz_cache.json."""
try:
resp = await client.get(STAKEWIZ_VALIDATORS_URL, timeout=30)
resp.raise_for_status()
validators = resp.json()
except Exception as e:
logger.error("Stakewiz fetch failed: %s", e)
return
with open(config.stakewiz_cache_path, "w") as f:
json.dump(validators, f)
logger.info("Written %d validators to %s", len(validators), config.stakewiz_cache_path)
async def bootstrap_sui(config: Config, client: httpx.AsyncClient) -> None:
"""Fetch Sui system state and merge validator metadata into known_operators.json."""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getLatestSuiSystemState",
"params": [],
}
try:
resp = await client.post(config.sui_rpc_url, json=payload, timeout=30)
resp.raise_for_status()
data = resp.json()
except Exception as e:
logger.error("Sui system state fetch failed: %s", e)
return
validators = data.get("result", {}).get("activeValidators", [])
# Load existing known_operators.json
try:
with open(config.operators_path) as f:
operators = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
operators = {}
if "sui" not in operators:
operators["sui"] = {}
if "solana" not in operators:
operators["solana"] = {}
if "ethereum" not in operators:
operators["ethereum"] = {}
for v in validators:
address = v.get("suiAddress", "")
if not address:
continue
ip = _parse_sui_ip(v.get("p2pAddress", ""))
operators["sui"][address] = {
"name": v.get("name", ""),
"project_url": v.get("projectUrl", ""),
"ip": ip,
}
with open(config.operators_path, "w") as f:
json.dump(operators, f, indent=2)
logger.info("Merged %d Sui validators into %s", len(validators), config.operators_path)
async def import_scans(scanned_validators_path: str, scan_export_path: str) -> None:
"""Merge a scan export JSON array into scanned_validators.json."""
# Load existing
try:
with open(scanned_validators_path) as f:
existing: List[dict] = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
existing = []
existing_index: Dict[str, dict] = {e["validator_pubkey"]: e for e in existing if e.get("validator_pubkey")}
# Load new export
try:
with open(scan_export_path) as f:
new_entries: List[dict] = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.error("Could not read scan export: %s", e)
return
for entry in new_entries:
pubkey = entry.get("validator_pubkey", "")
if not pubkey:
continue
existing_entry = existing_index.get(pubkey)
if existing_entry is None:
existing_index[pubkey] = entry
else:
# Keep newer by ISO date string comparison
if entry.get("scan_date", "") > existing_entry.get("scan_date", ""):
existing_index[pubkey] = entry
merged = list(existing_index.values())
with open(scanned_validators_path, "w") as f:
json.dump(merged, f, indent=2)
logger.info("Scan index now has %d entries at %s", len(merged), scanned_validators_path)