forked from sagnik/Project_Astral
221 lines
9.0 KiB
TypeScript
221 lines
9.0 KiB
TypeScript
import axios from "axios";
|
|
import { JsonRpcProvider, isAddress } from "ethers";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
type CheckResult = {
|
|
component: string;
|
|
ok: boolean;
|
|
detail: string;
|
|
};
|
|
|
|
const ROOT_DIR = process.cwd();
|
|
const RPC_URL = process.env.RPC_URL ?? "http://127.0.0.1:8545";
|
|
const BLOCKCHAIN_CONFIG_PATH = path.join(ROOT_DIR, "blockchain", "blockchain_config.json");
|
|
const OPENCLAW_URL = process.env.OPENCLAW_URL ?? "http://127.0.0.1:3001/skills/astral-guard/generate";
|
|
const COMFY_STATS_URL = process.env.COMFY_STATS_URL ?? "http://127.0.0.1:8188/system_stats";
|
|
const COMFY_OBJECT_INFO_URL = process.env.COMFY_OBJECT_INFO_URL ?? "http://127.0.0.1:8188/object_info";
|
|
const TIMEOUT_MS = 8000;
|
|
|
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
|
|
let timer: NodeJS.Timeout | null = null;
|
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
});
|
|
|
|
try {
|
|
return await Promise.race([promise, timeoutPromise]);
|
|
} finally {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizeBytes(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (typeof value === "string") {
|
|
const parsed = Number(value);
|
|
if (Number.isFinite(parsed)) {
|
|
return parsed;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function extractDeviceEntries(stats: unknown): Array<Record<string, unknown>> {
|
|
if (!stats || typeof stats !== "object") {
|
|
return [];
|
|
}
|
|
|
|
const devices = (stats as Record<string, unknown>).devices;
|
|
if (Array.isArray(devices)) {
|
|
return devices.filter((d): d is Record<string, unknown> => !!d && typeof d === "object");
|
|
}
|
|
|
|
if (devices && typeof devices === "object") {
|
|
return Object.values(devices as Record<string, unknown>).filter(
|
|
(d): d is Record<string, unknown> => !!d && typeof d === "object"
|
|
);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function hasNode(objectInfo: unknown, targetKeys: string[]): boolean {
|
|
if (!objectInfo || typeof objectInfo !== "object") {
|
|
return false;
|
|
}
|
|
|
|
const keys = Object.keys(objectInfo as Record<string, unknown>).map((k) => k.toLowerCase());
|
|
return targetKeys.some((target) => keys.includes(target.toLowerCase()));
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const checks: CheckResult[] = [];
|
|
|
|
const provider = new JsonRpcProvider(RPC_URL, undefined, { staticNetwork: true });
|
|
let rpcHealthy = false;
|
|
|
|
try {
|
|
const blockNumber = await withTimeout(provider.getBlockNumber(), TIMEOUT_MS, "Geth RPC timeout");
|
|
if (Number.isInteger(blockNumber) && blockNumber > 0) {
|
|
rpcHealthy = true;
|
|
checks.push({ component: "Geth RPC", ok: true, detail: `Geth Mining at Block #${blockNumber}` });
|
|
} else {
|
|
rpcHealthy = true;
|
|
checks.push({ component: "Geth RPC", ok: false, detail: `Connected but block height is ${blockNumber} (genesis or stalled)` });
|
|
}
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
checks.push({ component: "Geth RPC", ok: false, detail: `Connection failed: ${msg}` });
|
|
}
|
|
|
|
try {
|
|
if (!fs.existsSync(BLOCKCHAIN_CONFIG_PATH)) {
|
|
checks.push({ component: "AstralAccess Contract", ok: false, detail: `Missing file: ${BLOCKCHAIN_CONFIG_PATH}` });
|
|
} else {
|
|
const parsed = JSON.parse(fs.readFileSync(BLOCKCHAIN_CONFIG_PATH, "utf8")) as { contractAddress?: string };
|
|
const contractAddress = parsed.contractAddress;
|
|
|
|
if (!contractAddress || !isAddress(contractAddress)) {
|
|
checks.push({ component: "AstralAccess Contract", ok: false, detail: "Invalid or missing contractAddress in blockchain_config.json" });
|
|
} else if (!rpcHealthy) {
|
|
checks.push({ component: "AstralAccess Contract", ok: false, detail: "RPC unavailable; cannot verify bytecode" });
|
|
} else {
|
|
const code = await withTimeout(provider.getCode(contractAddress), TIMEOUT_MS, "Contract code lookup timeout");
|
|
if (code && code !== "0x") {
|
|
checks.push({ component: "AstralAccess Contract", ok: true, detail: `Smart Contract verified at ${contractAddress}` });
|
|
} else {
|
|
checks.push({ component: "AstralAccess Contract", ok: false, detail: `Contract code is 0x at ${contractAddress}` });
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
checks.push({ component: "AstralAccess Contract", ok: false, detail: `Verification failed: ${msg}` });
|
|
}
|
|
|
|
try {
|
|
const response = await axios.post(
|
|
OPENCLAW_URL,
|
|
{ user: "0xTest000000000000000000000000000000000000", action: "ping" },
|
|
{ timeout: TIMEOUT_MS, validateStatus: () => true }
|
|
);
|
|
|
|
if (response.status === 200) {
|
|
checks.push({ component: "OpenClaw Middleware", ok: true, detail: "AstralGuard endpoint returned HTTP 200" });
|
|
} else if (response.status >= 201 && response.status < 500) {
|
|
checks.push({ component: "OpenClaw Middleware", ok: true, detail: `AstralGuard endpoint reachable (HTTP ${response.status})` });
|
|
} else {
|
|
checks.push({ component: "OpenClaw Middleware", ok: false, detail: `Unhealthy HTTP ${response.status} from AstralGuard endpoint` });
|
|
}
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
checks.push({ component: "OpenClaw Middleware", ok: false, detail: `Connection failed: ${msg}` });
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(COMFY_STATS_URL, { timeout: TIMEOUT_MS, validateStatus: () => true });
|
|
if (response.status !== 200 || !response.data || typeof response.data !== "object") {
|
|
checks.push({ component: "ComfyUI system_stats", ok: false, detail: `Unexpected HTTP ${response.status}` });
|
|
} else {
|
|
const devices = extractDeviceEntries(response.data);
|
|
const hasDevices = devices.length > 0;
|
|
const maxVramBytes = devices
|
|
.map((d) => normalizeBytes(d.vram_total ?? d.vram_total_bytes ?? d.total_vram ?? d.total_memory))
|
|
.filter((v): v is number => v !== null)
|
|
.reduce((max, cur) => Math.max(max, cur), 0);
|
|
|
|
const gpuNameJoined = devices
|
|
.map((d) => String(d.name ?? d.device_name ?? d.type ?? "").toLowerCase())
|
|
.join(" ");
|
|
const hasRtxPro6000 = gpuNameJoined.includes("rtx") && gpuNameJoined.includes("6000");
|
|
const has40GBPlus = maxVramBytes > 40 * 1024 * 1024 * 1024;
|
|
|
|
if (hasDevices) {
|
|
checks.push({ component: "ComfyUI system_stats", ok: true, detail: "ComfyUI reachable and returned device stats" });
|
|
} else {
|
|
checks.push({ component: "ComfyUI system_stats", ok: false, detail: "ComfyUI reachable but no devices reported" });
|
|
}
|
|
|
|
if (hasRtxPro6000 && has40GBPlus) {
|
|
checks.push({ component: "ComfyUI Hardware", ok: true, detail: `RTX Pro 6000-class GPU detected with ${(maxVramBytes / (1024 ** 3)).toFixed(1)} GB VRAM` });
|
|
} else if (!hasRtxPro6000 && has40GBPlus) {
|
|
checks.push({ component: "ComfyUI Hardware", ok: false, detail: `>=40GB VRAM detected but RTX Pro 6000 name not found (max ${(maxVramBytes / (1024 ** 3)).toFixed(1)} GB)` });
|
|
} else {
|
|
checks.push({ component: "ComfyUI Hardware", ok: false, detail: `Required GPU/VRAM threshold not met (max ${(maxVramBytes / (1024 ** 3)).toFixed(1)} GB)` });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
checks.push({ component: "ComfyUI system_stats", ok: false, detail: `Connection failed: ${msg}` });
|
|
checks.push({ component: "ComfyUI Hardware", ok: false, detail: "Skipped because system_stats check failed" });
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(COMFY_OBJECT_INFO_URL, { timeout: TIMEOUT_MS, validateStatus: () => true });
|
|
if (response.status !== 200 || !response.data || typeof response.data !== "object") {
|
|
checks.push({ component: "ComfyUI Nodes", ok: false, detail: `Unexpected HTTP ${response.status}` });
|
|
} else {
|
|
const hasRequiredNode = hasNode(response.data, ["LTX_VideoLoader", "CheckpointLoaderSimple"]);
|
|
if (hasRequiredNode) {
|
|
checks.push({ component: "ComfyUI Nodes", ok: true, detail: "LTX_VideoLoader or CheckpointLoaderSimple is available" });
|
|
} else {
|
|
checks.push({ component: "ComfyUI Nodes", ok: false, detail: "Required nodes missing: LTX_VideoLoader/CheckpointLoaderSimple" });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
checks.push({ component: "ComfyUI Nodes", ok: false, detail: `Connection failed: ${msg}` });
|
|
}
|
|
|
|
provider.destroy();
|
|
|
|
const passed = checks.filter((c) => c.ok).length;
|
|
const failed = checks.filter((c) => !c.ok).length;
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
timestamp: new Date().toISOString(),
|
|
passed,
|
|
failed,
|
|
checks
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error(JSON.stringify({ fatal: true, message }, null, 2));
|
|
process.exitCode = 1;
|
|
});
|