diff --git a/contract/Anchor.toml b/contract/Anchor.toml index 681164d..350dcd5 100644 --- a/contract/Anchor.toml +++ b/contract/Anchor.toml @@ -6,7 +6,7 @@ resolution = true skip-lint = false [programs.localnet] -contract = "J7nyNjMR7p9Xi8ohzkNAFmnAeVUBb1AMpGKTFGtFvVjJ" +contract = "BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc" [registry] url = "https://api.apr.dev" diff --git a/contract/README.md b/contract/README.md index 9803182..fd737b6 100644 --- a/contract/README.md +++ b/contract/README.md @@ -2,7 +2,7 @@ Anchor framework program managing VM rental sessions, escrow payments, and DePIN host registration. -**Program ID (devnet):** `J7nyNjMR7p9Xi8ohzkNAFmnAeVUBb1AMpGKTFGtFvVjJ` +**Program ID (devnet):** `BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc` ## Instructions diff --git a/contract/programs/contract/src/lib.rs b/contract/programs/contract/src/lib.rs index 2c17be9..42380b5 100644 --- a/contract/programs/contract/src/lib.rs +++ b/contract/programs/contract/src/lib.rs @@ -9,7 +9,7 @@ pub mod depin; use instructions::*; use depin::*; -declare_id!("J7nyNjMR7p9Xi8ohzkNAFmnAeVUBb1AMpGKTFGtFvVjJ"); +declare_id!("BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc"); #[program] pub mod contract { diff --git a/indexer/src/config.rs b/indexer/src/config.rs index b07f0c3..33e8edf 100644 --- a/indexer/src/config.rs +++ b/indexer/src/config.rs @@ -32,7 +32,7 @@ impl Config { grpc_endpoint: env::var("GRPC_ENDPOINT") .unwrap_or_else(|_| "http://127.0.0.1:10000".to_string()), program_id: env::var("PROGRAM_ID") - .unwrap_or_else(|_| "J7nyNjMR7p9Xi8ohzkNAFmnAeVUBb1AMpGKTFGtFvVjJ".to_string()), + .unwrap_or_else(|_| "BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc".to_string()), backend_webhook_url: env::var("BACKEND_WEBHOOK_URL") .unwrap_or_else(|_| "http://localhost:3000/api/v2/indexer/webhook".to_string()), ws_relayer_url: env::var("WS_RELAYER_URL") diff --git a/ops/deployment.yml b/ops/deployment.yml index b432e1b..3136385 100644 --- a/ops/deployment.yml +++ b/ops/deployment.yml @@ -25,6 +25,20 @@ spec: image: krishanand01/axion-frontend:v1.2.11 ports: - containerPort: 80 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 resources: requests: cpu: "5m" @@ -68,6 +82,20 @@ spec: image: krishanand01/axion-backend:v1.2.11 ports: - containerPort: 3000 + livenessProbe: + httpGet: + path: /api/v2/health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/v2/health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 resources: requests: cpu: "5m" @@ -159,13 +187,29 @@ spec: containers: - name: axion-worker image: krishanand01/axion-worker:v1.2.11 + ports: + - containerPort: 9094 + livenessProbe: + httpGet: + path: /health + port: 9094 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 9094 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 resources: requests: cpu: "5m" memory: "32Mi" limits: cpu: "100m" - memory: "512Mi" + memory: "1Gi" volumeMounts: - name: env-file mountPath: /app/apps/worker/.env @@ -208,6 +252,20 @@ spec: image: krishanand01/axion-ws-relayer:v1.2.11 ports: - containerPort: 9093 + livenessProbe: + httpGet: + path: /health + port: 9093 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 9093 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 resources: requests: cpu: "5m" @@ -252,6 +310,20 @@ spec: image: krishanand01/axion-depin-ws-relayer:v1.2.11 ports: - containerPort: 8080 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 resources: requests: cpu: "5m" @@ -289,6 +361,18 @@ spec: image: redis:7-alpine ports: - containerPort: 6379 + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 volumeMounts: - name: redis-data mountPath: /data diff --git a/web-services/apps/backend/index.ts b/web-services/apps/backend/index.ts index 3de07ed..4d232d8 100644 --- a/web-services/apps/backend/index.ts +++ b/web-services/apps/backend/index.ts @@ -1,6 +1,7 @@ import express from "express"; import cors from "cors"; import type { NextFunction, Request, Response } from "express"; +import type { Server } from "http"; import { sendError, logger } from "@axion/utilities"; import { redisConnection } from "@axion/utilities/redis"; import { @@ -9,6 +10,9 @@ import { indexerLimiter, healthLimiter, depinVerificationLimiter, + deployLimiter, + registerLimiter, + claimSOLLimiter, } from "@axion/utilities/rateLimiter"; import prisma from "@axion/db"; import UserRouter from "./routes/user"; @@ -36,12 +40,16 @@ app.use(cors()); app.use("/api/v2/user/signup", publicLimiter); app.use("/api/v2/user/login", publicLimiter); -app.use("/api/v2/user/depin/depinVerification", depinVerificationLimiter); -app.use("/api/v2/indexer", indexerLimiter); -app.use("/api/v2/user", authLimiter); +app.use("/api/v2/user/me", authLimiter); +app.use("/api/v2/user/checkTimeout", authLimiter); app.use("/api/v2/vmInstance", authLimiter); app.use("/api/v2/vm", authLimiter); app.use("/api/v2/user/depin", authLimiter); +app.use("/api/v2/user/depin/depinVerification", depinVerificationLimiter); +app.use("/api/v2/user/depin/deploy", deployLimiter); +app.use("/api/v2/user/depin/register", registerLimiter); +app.use("/api/v2/user/depin/claimSOL", claimSOLLimiter); +app.use("/api/v2/indexer", indexerLimiter); app.use("/api/v2/user", UserRouter); app.use("/api/v2/vmInstance", vmInstance); @@ -83,6 +91,24 @@ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { sendError(res, err); }); -app.listen(3000, () => { +const server: Server = app.listen(3000, () => { logger.info("Backend server started", { port: 3000 }); }); + +function gracefulShutdown(signal: string) { + logger.info(`Received ${signal}, shutting down gracefully...`); + server.close(() => { + logger.info("HTTP server closed"); + prisma.$disconnect().finally(() => { + redisConnection.quit(); + process.exit(0); + }); + }); + setTimeout(() => { + logger.error("Forced shutdown after timeout"); + process.exit(1); + }, 10_000); +} + +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); diff --git a/web-services/apps/backend/routes/depinVm.ts b/web-services/apps/backend/routes/depinVm.ts index 0828491..6fef547 100644 --- a/web-services/apps/backend/routes/depinVm.ts +++ b/web-services/apps/backend/routes/depinVm.ts @@ -1,3 +1,4 @@ +import { z } from "zod"; import { Router } from "express"; import { authMiddleware, logger } from "@axion/utilities"; import prisma from "@axion/db"; @@ -30,15 +31,13 @@ let wsReconnectTimer: ReturnType | null = null; function connectDepinWs() { ws = new WebSocket(DEPIN_WS_URL); ws.addEventListener("open", () => { - console.log("[depin-ws] Connected"); + logger.info("[depin-ws] Connected"); if (wsReconnectTimer) { clearTimeout(wsReconnectTimer); wsReconnectTimer = null; } }); - ws.addEventListener("error", (err) => - console.error("[depin-ws] Error:", err), - ); + ws.addEventListener("error", (err) => logger.error("[depin-ws] Error", err)); ws.addEventListener("close", () => { console.warn("[depin-ws] Disconnected, reconnecting in 3s..."); wsReconnectTimer = setTimeout(connectDepinWs, 3000); @@ -51,7 +50,7 @@ function wsSend(payload: object): boolean { ws.send(JSON.stringify(payload)); return true; } - console.error("[depin-ws] Cannot send, not connected"); + logger.error("[depin-ws] Cannot send, not connected"); return false; } @@ -79,8 +78,10 @@ function getCF() { } depinVM.post("/findVM", authMiddleware, async (req, res) => { - const user = await getUserOr404(res, req.userId); - if (!user) return; + if (!req.userId) { + fail(res, 400, "User ID is required"); + return; + } const parseData = FindVmSchema.safeParse(req.body); if (!parseData.success) { @@ -98,39 +99,53 @@ depinVM.post("/findVM", authMiddleware, async (req, res) => { `library/${dockerImage.split(":")[0]}`, dockerImage.split(":")[1] || "latest", ]; - const registryRes = await fetch( - `https://hub.docker.com/v2/repositories/${repo}/tags/${tag}`, - ); + + const [registryRes, findVm] = await Promise.all([ + fetch(`https://hub.docker.com/v2/repositories/${repo}/tags/${tag}`), + prisma.depinHostMachine.findFirst({ + where: { + isActive: true, + cpu: { gte: parseInt(cpu) }, + ram: { gte: parseInt(ram) }, + diskSize: { gte: parseInt(diskSize) }, + isOccupied: false, + verified: true, + perHourPrice: { gt: 0 }, + }, + select: { + id: true, + ipAddress: true, + region: true, + os: true, + tunnelId: true, + userPublicKey: true, + }, + }), + ]); if (!registryRes.ok) { fail(res, 400, `Docker image '${dockerImage}' not found on Docker Hub`); return; } - - const findVm = await prisma.depinHostMachine.findFirst({ - where: { - isActive: true, - cpu: { gte: parseInt(cpu) }, - ram: { gte: parseInt(ram) }, - diskSize: { gte: parseInt(diskSize) }, - isOccupied: false, - verified: true, - perHourPrice: { gt: 0 }, - }, - }); if (!findVm) { fail(res, 404, "No suitable VM found for deployment"); return; } ok(res, { message: "Deployment request sent successfully", vm: findVm }); } catch (error) { - console.error("Error deploying image:", error); + logger.error("Error deploying image", error as Error); fail(res, 500, "Internal server error"); } }); depinVM.post("/deploy", authMiddleware, async (req, res) => { - const user = await getUserOr404(res, req.userId); - if (!user) return; + const user = await prisma.user.findUnique({ + where: { id: req.userId }, + select: { publicKey: true }, + }); + if (!user) { + fail(res, 404, "User not found"); + return; + } const parseData = DepinDeployVmSchema.safeParse(req.body); if (!parseData.success) { @@ -155,49 +170,60 @@ depinVM.post("/deploy", authMiddleware, async (req, res) => { } = parseData.data; const findVm = await prisma.depinHostMachine.findFirst({ where: { id: VmId, isActive: true, isOccupied: false }, + select: { + id: true, + userPublicKey: true, + region: true, + ipAddress: true, + os: true, + tunnelId: true, + }, }); if (!findVm) { fail(res, 404, "No suitable VM found for deployment"); return; } - const txn = await prisma.$transaction(async (tx) => { - const portList = ports[0] - ? ports[0] - .split(",") - .map((p) => parseInt(p.trim())) - .filter((p) => !isNaN(p)) - : []; - const token = signToken({ id: req.userId!, machineId: findVm.id }); - const cf = getCF(); - const containerPort = portList[0] || 80; - const subdomain = `${id}-depin.${cf?.domain || "axion.krishlabs.tech"}`; - wsSend({ - type: "start-job", - jobId: id, - dockerImage, - containerPort, - subdomain, - env: parseEnvVars(envVars), - machineId: findVm.id, - token, - }); + const portList = ports[0] + ? ports[0] + .split(",") + .map((p) => parseInt(p.trim())) + .filter((p) => !isNaN(p)) + : []; + const cf = getCF(); + const containerPort = portList[0] || 80; + const subdomain = `${id}-depin.${cf?.domain || "axion.krishlabs.tech"}`; - await tx.depinHostMachine.update({ - where: { id: findVm.id }, - data: { isOccupied: true }, - }); - await activateHostQueue.add("changeVMStatus", { + const [job] = await Promise.all([ + terminateDepinVMQueue.add( + "terminate-depin-vm", + { pubKey: user.publicKey, id: findVm.id }, + { delay: endTime * MINUTE_MS }, + ), + activateHostQueue.add("changeVMStatus", { id: findVm.id, userPubKey: findVm.userPublicKey, status: true, - }); + }), + ]); - const job = await terminateDepinVMQueue.add( - "terminate-depin-vm", - { pubKey: user.publicKey, id: findVm.id }, - { delay: endTime * MINUTE_MS }, - ); + const token = signToken({ id: req.userId!, machineId: findVm.id }); + wsSend({ + type: "start-job", + jobId: id, + dockerImage, + containerPort, + subdomain, + env: parseEnvVars(envVars), + machineId: findVm.id, + token, + }); + + const txn = await prisma.$transaction(async (tx) => { + await tx.depinHostMachine.update({ + where: { id: findVm.id }, + data: { isOccupied: true }, + }); const config = await tx.vMInstance.create({ data: { @@ -233,17 +259,18 @@ depinVM.post("/deploy", authMiddleware, async (req, res) => { }, }); - // Create Cloudflare DNS record - if (cf && findVm.tunnelId) { - try { - await cf.createDNSRecord(id, findVm.tunnelId); - } catch (err) { - console.error("Error creating DNS record:", err); - } - } return config; }); + // Create Cloudflare DNS record after DB transaction + if (cf && findVm.tunnelId) { + try { + await cf.createDNSRecord(id, findVm.tunnelId); + } catch (err) { + logger.error("Error creating DNS record", err as Error); + } + } + ok(res, { message: "Deployment request sent successfully", id: txn.id, @@ -256,13 +283,19 @@ depinVM.post("/deploy", authMiddleware, async (req, res) => { }); depinVM.delete("/terminate/:id", authMiddleware, async (req, res) => { - const user = await getUserOr404(res, req.userId); - if (!user) return; + const user = await prisma.user.findUnique({ + where: { id: req.userId }, + select: { publicKey: true }, + }); + if (!user) { + fail(res, 404, "User not found"); + return; + } const vmId = req.params.id; const vmInstance = await prisma.vMInstance.findFirst({ where: { id: vmId, userId: req.userId }, - include: { VMImage: true }, + select: { id: true, VMImage: { select: { depinHostMachineId: true } } }, }); if (!vmInstance) { fail(res, 404, "VM instance not found"); @@ -275,9 +308,9 @@ depinVM.delete("/terminate/:id", authMiddleware, async (req, res) => { } try { + const token = signToken({ id: req.userId!, machineId }, "5Mins"); + wsSend({ type: "end-job", jobId: vmId, machineId, token }); await prisma.$transaction(async (tx) => { - const token = signToken({ id: req.userId!, machineId }, "5Mins"); - wsSend({ type: "end-job", jobId: vmId, machineId, token }); await tx.vMInstance.update({ where: { id: vmId }, data: { status: "TERMINATED" }, @@ -300,7 +333,7 @@ depinVM.delete("/terminate/:id", authMiddleware, async (req, res) => { try { await cf.deleteDNSRecord(vmId); } catch (err) { - console.error("Error deleting DNS record:", err); + logger.error("Error deleting DNS record", err as Error); } } @@ -321,7 +354,10 @@ depinVM.post("/depinVerification", async (req, res) => { try { const { os, cpu_cores, ram_gb, disk_gb, ip_address, wallet, key } = parseData.data; - const user = await prisma.user.findFirst({ where: { publicKey: wallet } }); + const user = await prisma.user.findFirst({ + where: { publicKey: wallet }, + select: { name: true }, + }); if (!user) { fail(res, 404, "User not found"); return; @@ -329,6 +365,16 @@ depinVM.post("/depinVerification", async (req, res) => { const vm = await prisma.depinHostMachine.findFirst({ where: { ipAddress: ip_address }, + select: { + id: true, + Key: true, + os: true, + cpu: true, + ram: true, + diskSize: true, + machineType: true, + tunnelId: true, + }, }); if (!vm) { fail(res, 404, "VM not found"); @@ -392,7 +438,7 @@ depinVM.post("/depinVerification", async (req, res) => { }; if (tokenBody.success) tunnelToken = tokenBody.result; } catch (err) { - console.error("Error fetching tunnel token:", err); + logger.error("Error fetching tunnel token", err as Error); } } @@ -438,7 +484,7 @@ depinVM.post("/register", authMiddleware, async (req, res) => { region, os, userPublicKey, - Key: bcrypt.hashSync(Key, 10), + Key: await bcrypt.hash(Key, 10), }, }); @@ -455,7 +501,7 @@ depinVM.post("/register", authMiddleware, async (req, res) => { }, }); } catch (err) { - console.error("Error creating Cloudflare tunnel:", err); + logger.error("Error creating Cloudflare tunnel", err as Error); } } @@ -477,6 +523,7 @@ depinVM.post("/changeVisibility", authMiddleware, async (req, res) => { try { const vm = await prisma.depinHostMachine.findFirst({ where: { id, userPublicKey: pubKey }, + select: { id: true, Key: true }, }); if (!vm) { fail(res, 404, "VM not found"); @@ -501,7 +548,7 @@ depinVM.post("/changeVisibility", authMiddleware, async (req, res) => { wsSend({ type: "end-job", jobId: "all", machineId: id, token }); } } catch (e) { - console.error("WS send error:", e); + logger.error("WS send error", e as Error); } ok(res, { message: "VM visibility updated successfully" }); @@ -512,11 +559,14 @@ depinVM.post("/changeVisibility", authMiddleware, async (req, res) => { }); depinVM.get("/getAll", authMiddleware, async (req, res) => { - const userPublicKey = req.query.userPublicKey as string; - if (!userPublicKey) { + const parsed = z + .object({ userPublicKey: z.string().min(1) }) + .safeParse(req.query); + if (!parsed.success) { fail(res, 400, "User public key is required"); return; } + const { userPublicKey } = parsed.data; try { const vms = await prisma.depinHostMachine.findMany({ @@ -530,11 +580,12 @@ depinVM.get("/getAll", authMiddleware, async (req, res) => { }); depinVM.get("/getById", authMiddleware, async (req, res) => { - const id = req.query.id as string; - if (!id) { + const parsed = z.object({ id: z.string().min(1) }).safeParse(req.query); + if (!parsed.success) { fail(res, 400, "VM ID is required"); return; } + const { id } = parsed.data; try { const vm = await prisma.depinHostMachine.findFirst({ where: { id } }); diff --git a/web-services/apps/backend/routes/indexer.ts b/web-services/apps/backend/routes/indexer.ts index 0d120da..0e20d75 100644 --- a/web-services/apps/backend/routes/indexer.ts +++ b/web-services/apps/backend/routes/indexer.ts @@ -33,24 +33,20 @@ interface IndexerEvent { router.post("/webhook", async (req: Request, res: Response) => { const token = req.headers["x-indexer-token"]; if (token !== INDEXER_TOKEN) { - res - .status(401) - .json({ - success: false, - error: { code: "UNAUTHORIZED", message: "Unauthorized" }, - }); + res.status(401).json({ + success: false, + error: { code: "UNAUTHORIZED", message: "Unauthorized" }, + }); return; } const event: IndexerEvent = req.body; if (!event.instruction || !event.signature) { - res - .status(400) - .json({ - success: false, - error: { code: "INVALID_PAYLOAD", message: "Invalid event payload" }, - }); + res.status(400).json({ + success: false, + error: { code: "INVALID_PAYLOAD", message: "Invalid event payload" }, + }); return; } @@ -59,7 +55,7 @@ router.post("/webhook", async (req: Request, res: Response) => { return; } - console.log( + logger.info( `[Indexer] ${event.instruction} | sig=${event.signature.slice(0, 16)}... | success=${event.success} | args=${JSON.stringify(event.args)}`, ); @@ -73,12 +69,10 @@ router.post("/webhook", async (req: Request, res: Response) => { res.status(200).json({ received: true }); } catch (error) { logger.error(`[Indexer] Error handling ${event.instruction}`, error); - res - .status(500) - .json({ - success: false, - error: { code: "INDEXER_ERROR", message: "Failed to process event" }, - }); + res.status(500).json({ + success: false, + error: { code: "INDEXER_ERROR", message: "Failed to process event" }, + }); } }); @@ -177,61 +171,40 @@ async function handleInstruction(event: IndexerEvent) { const diskSize = args?.disk_size as number; const solPerHour = args?.sol_per_hour as number; - const host = await prisma.depinHostMachine.findFirst({ + await prisma.depinHostMachine.updateMany({ where: { userPublicKey: userPubKey }, + data: { pdaAddress: accounts[2] }, }); - - if (host) { - await prisma.depinHostMachine.update({ - where: { id: host.id }, - data: { pdaAddress: accounts[2] }, - }); - } break; } case "activate_host": { const hostPubKey = accounts[1]; - const host = await prisma.depinHostMachine.findFirst({ + await prisma.depinHostMachine.updateMany({ where: { userPublicKey: hostPubKey }, + data: { isActive: true }, }); - if (host) { - await prisma.depinHostMachine.update({ - where: { id: host.id }, - data: { isActive: true }, - }); - } break; } case "deactivate_host": { const hostPubKey = accounts[1]; - const host = await prisma.depinHostMachine.findFirst({ + await prisma.depinHostMachine.updateMany({ where: { userPublicKey: hostPubKey }, + data: { isActive: false }, }); - if (host) { - await prisma.depinHostMachine.update({ - where: { id: host.id }, - data: { isActive: false }, - }); - } break; } case "penalize_host": { const hostPubKey = accounts[1]; - const host = await prisma.depinHostMachine.findFirst({ + await prisma.depinHostMachine.updateMany({ where: { userPublicKey: hostPubKey }, + data: { isActive: false, verified: false }, }); - if (host) { - await prisma.depinHostMachine.update({ - where: { id: host.id }, - data: { isActive: false, verified: false }, - }); - } break; } @@ -244,7 +217,7 @@ async function handleInstruction(event: IndexerEvent) { } default: - console.log(`[Indexer] Unhandled: ${instruction}`); + logger.info(`[Indexer] Unhandled: ${instruction}`); } } diff --git a/web-services/apps/backend/routes/vm.ts b/web-services/apps/backend/routes/vm.ts index 92ecac2..82ca04e 100644 --- a/web-services/apps/backend/routes/vm.ts +++ b/web-services/apps/backend/routes/vm.ts @@ -1,4 +1,5 @@ import "dotenv/config"; +import { z } from "zod"; import prisma from "@axion/db"; import { Router } from "express"; import axios from "axios"; @@ -16,11 +17,24 @@ const vm = Router(); vm.get("/calculatePrice", authMiddleware, async (req, res) => { try { - const machineType = req.query.machineType as string; - const diskSize = parseInt(req.query.diskSize as string, 10); - const basePrice = await prisma.vMTypes.findFirst({ - where: { machineType }, - }); + const query = z + .object({ + machineType: z.string().min(1), + diskSize: z.coerce.number().int().min(0), + }) + .safeParse(req.query); + if (!query.success) { + fail(res, 400, "Invalid query parameters"); + return; + } + const { machineType, diskSize } = query.data; + const [basePrice, solPrice] = await Promise.all([ + prisma.vMTypes.findFirst({ + where: { machineType }, + select: { priceMonthlyUSD: true }, + }), + getSolPrice(), + ]); if (!basePrice) { fail(res, 404, "Machine type not found"); return; @@ -30,7 +44,6 @@ vm.get("/calculatePrice", authMiddleware, async (req, res) => { ? (diskSize - FREE_DISK_GB) * DISK_COST_PER_GB : 0; const totalPrice = basePrice.priceMonthlyUSD + additionalCost; - const solPrice = await getSolPrice(); ok(res, { price: totalPrice / solPrice }); } catch (error) { logger.error("Error calculating price", error); @@ -49,7 +62,12 @@ vm.get("/getVMTypes", authMiddleware, async (req, res) => { }); vm.get("/getAll", authMiddleware, async (req, res) => { - const adminKey = req.query.adminKey as string; + const parsed = z.object({ adminKey: z.string().min(1) }).safeParse(req.query); + if (!parsed.success) { + fail(res, 400, "adminKey is required"); + return; + } + const adminKey = parsed.data.adminKey; if (adminKey !== process.env.ADMIN_KEY) { fail(res, 403, "Forbidden"); return; @@ -66,14 +84,18 @@ vm.get("/getAll", authMiddleware, async (req, res) => { }); vm.get("/checkNameAvailability", authMiddleware, async (req, res) => { - const name = req.query.name as string; - if (!name) { + const parsed = z + .object({ name: z.string().min(1).max(100) }) + .safeParse(req.query); + if (!parsed.success) { fail(res, 400, "Name is required"); return; } + const { name } = parsed.data; try { const existingVM = await prisma.vMInstance.findFirst({ where: { name, status: { not: "DELETED" } }, + select: { id: true }, }); ok(res, { available: !existingVM }); } catch (error) { @@ -88,13 +110,28 @@ vm.post("/topup", authMiddleware, async (req, res) => { fail(res, 400, "Invalid request data"); return; } - const user = await getUserOr404(res, req.userId); - if (!user) return; + const user = await prisma.user.findUnique({ + where: { id: req.userId }, + select: { publicKey: true }, + }); + if (!user) { + fail(res, 404, "User not found"); + return; + } try { const { id, amount, additionalEscrowDuration } = parsedData.data; const vmInstance = await prisma.vMInstance.findFirst({ where: { id, userId: req.userId }, + select: { + PaymentType: true, + endTime: true, + jobId: true, + provider: true, + instanceId: true, + region: true, + id: true, + }, }); if (!vmInstance) { fail(res, 404, "VM instance not found"); diff --git a/web-services/apps/backend/routes/vmInstance.ts b/web-services/apps/backend/routes/vmInstance.ts index cb2e651..ede3735 100644 --- a/web-services/apps/backend/routes/vmInstance.ts +++ b/web-services/apps/backend/routes/vmInstance.ts @@ -1,4 +1,5 @@ import "dotenv/config"; +import { z } from "zod"; import { Router } from "express"; import { authMiddleware, logger } from "@axion/utilities"; import { VmInstanceSchema } from "@axion/types"; @@ -26,8 +27,14 @@ vmInstance.post("/create", authMiddleware, async (req, res) => { return; } - const user = await getUserOr404(res, req.userId); - if (!user) return; + const user = await prisma.user.findUnique({ + where: { id: req.userId }, + select: { timeoutAt: true, publicKey: true }, + }); + if (!user) { + fail(res, 404, "User not found"); + return; + } if (user.timeoutAt) { const elapsed = Date.now() - new Date(user.timeoutAt).getTime(); @@ -54,6 +61,7 @@ vmInstance.post("/create", authMiddleware, async (req, res) => { const existingVm = await prisma.vMInstance.findFirst({ where: { name, userId: req.userId, status: { not: "DELETED" } }, + select: { id: true }, }); if (existingVm) { fail(res, 409, "VM with this name already exists"); @@ -131,16 +139,19 @@ vmInstance.post("/create", authMiddleware, async (req, res) => { }); vmInstance.get("/pollStatus", authMiddleware, async (req, res) => { - const instanceId = req.query.instanceId as string; - const vmId = req.query.id as string; - if (!instanceId || !vmId) { - fail(res, 400, "ID is required"); + const parsed = z + .object({ instanceId: z.string().min(1), id: z.string().min(1) }) + .safeParse(req.query); + if (!parsed.success) { + fail(res, 400, "instanceId and id are required"); return; } + const { instanceId, id: vmId } = parsed.data; try { const vmInstance = await prisma.vMInstance.findUnique({ where: { id: vmId, instanceId }, + select: { instanceId: true, region: true }, }); if (!vmInstance) { fail(res, 404, "VM instance not found"); @@ -176,17 +187,23 @@ vmInstance.get("/pollStatus", authMiddleware, async (req, res) => { }); vmInstance.delete("/destroy", authMiddleware, async (req, res) => { - const instanceId = req.query.instanceId as string; - const vmId = req.query.vmId as string; - const zone = req.query.zone as string; - if (!instanceId || !vmId || !zone) { - fail(res, 400, "instance Id, VM ID, and zone are required"); + const parsed = z + .object({ + instanceId: z.string().min(1), + vmId: z.string().min(1), + zone: z.string().min(1), + }) + .safeParse(req.query); + if (!parsed.success) { + fail(res, 400, "instanceId, vmId, and zone are required"); return; } + const { instanceId, vmId, zone } = parsed.data; try { const vmInstance = await prisma.vMInstance.findFirst({ where: { id: vmId, instanceId }, + select: { endTime: true }, }); if (!vmInstance) { fail(res, 404, "VM instance not found"); @@ -213,7 +230,20 @@ vmInstance.get("/getAll", authMiddleware, async (req, res) => { try { const vms = await prisma.vMInstance.findMany({ where: { userId: req.userId }, - include: { VMConfig: true, VMImage: true }, + select: { + id: true, + name: true, + status: true, + ipAddress: true, + price: true, + region: true, + startTime: true, + endTime: true, + provider: true, + instanceId: true, + VMConfig: { select: { os: true, machineType: true, diskSize: true } }, + VMImage: { select: { applicationUrl: true } }, + }, orderBy: { createdAt: "desc" }, }); ok(res, { vms }); @@ -224,16 +254,44 @@ vmInstance.get("/getAll", authMiddleware, async (req, res) => { }); vmInstance.get("/getDetails", authMiddleware, async (req, res) => { - const id = req.query.id as string; - if (!id) { + const parsed = z.object({ id: z.string().min(1) }).safeParse(req.query); + if (!parsed.success) { fail(res, 400, "VM ID is required"); return; } + const { id } = parsed.data; try { const vmInstance = await prisma.vMInstance.findFirst({ where: { id }, - include: { VMConfig: true, VMImage: true }, + select: { + id: true, + name: true, + status: true, + ipAddress: true, + price: true, + region: true, + startTime: true, + endTime: true, + provider: true, + instanceId: true, + publicKey: true, + jobId: true, + VMConfig: { select: { os: true, machineType: true, diskSize: true } }, + VMImage: { + select: { + name: true, + description: true, + dockerImage: true, + cpu: true, + ram: true, + diskSize: true, + os: true, + applicationUrl: true, + applicationPort: true, + }, + }, + }, }); if (!vmInstance) { fail(res, 404, "VM instance not found"); diff --git a/web-services/apps/depin-ws-relayer/index.ts b/web-services/apps/depin-ws-relayer/index.ts index 3ecddd4..2e5b91a 100644 --- a/web-services/apps/depin-ws-relayer/index.ts +++ b/web-services/apps/depin-ws-relayer/index.ts @@ -3,10 +3,23 @@ import jwt, { type JwtPayload } from "jsonwebtoken"; import prisma from "@axion/db"; import { createQueue } from "@axion/utilities/redis"; -const JWT_SECRET = process.env.JWT_SECRET || "secret123"; -const HEARTBEAT_TIMEOUT_MS = 90_000; // 3 missed heartbeats (30s each) +function log(level: string, msg: string, meta?: unknown) { + console.log( + JSON.stringify({ + level, + msg, + ...(meta ? { meta } : {}), + timestamp: new Date().toISOString(), + }), + ); +} + +const JWT_SECRET = process.env.JWT_SECRET; +const HEARTBEAT_TIMEOUT_MS = 90_000; const HEARTBEAT_CHECK_INTERVAL_MS = 30_000; +if (!JWT_SECRET) throw new Error("JWT_SECRET is required"); + // --- Queues --- const penalizeQueue = createQueue("penalize-host"); const activateHostQueue = createQueue("changeVMStatus"); @@ -22,11 +35,12 @@ interface HostConnection { const activeConnections = new Map(); // --- Heartbeat Checker --- -setInterval(() => { +let heartbeatTimer: ReturnType; +heartbeatTimer = setInterval(() => { const now = Date.now(); for (const [id, conn] of activeConnections) { if (now - conn.lastHeartbeat > HEARTBEAT_TIMEOUT_MS) { - console.log(`[heartbeat] Host ${id} timed out, penalizing...`); + log("info", `[heartbeat] Host ${id} timed out, penalizing...`); handleHostTimeout(id, conn); } } @@ -45,7 +59,9 @@ async function handleHostTimeout(machineId: string, conn: HostConnection) { }); conn.ws.close(4001, "Heartbeat timeout"); } catch (err) { - console.error(`[heartbeat] Error penalizing ${machineId}:`, err); + log("error", `[heartbeat] Error penalizing ${machineId}`, { + error: (err as Error).message, + }); } } @@ -71,7 +87,7 @@ interface WsMessage { } // --- WebSocket Server --- -Bun.serve({ +const depinServer = Bun.serve({ port: 8080, fetch(req, server) { const url = new URL(req.url); @@ -113,7 +129,7 @@ Bun.serve({ break; } } catch (err) { - console.error("[ws] Error:", err); + log("error", "[ws] Error", { error: (err as Error).message }); ws.send(JSON.stringify({ type: "error", message: "Internal error" })); } }, @@ -133,9 +149,11 @@ Bun.serve({ prisma.depinHostMachine .update({ where: { id }, data: { isActive: false } }) .catch((err) => - console.error(`[close] Error deactivating ${id}:`, err), + log("error", `[close] Error deactivating ${id}`, { + error: (err as Error).message, + }), ); - console.log(`[close] Host ${id} disconnected`); + log("info", `[close] Host ${id} disconnected`); break; } } @@ -143,7 +161,7 @@ Bun.serve({ }, }); -console.log("[depin-ws-relayer] Running on port 8080"); +log("info", "[depin-ws-relayer] Running on port 8080"); // --- Handlers --- @@ -169,7 +187,7 @@ async function handleSubscribe(ws: ServerWebSocket, data: WsMessage) { data: { isActive: true }, }); - console.log(`[subscribe] Host ${id} connected`); + log("info", `[subscribe] Host ${id} connected`); ws.send(JSON.stringify({ type: "subscribed", machineId: id })); } @@ -189,7 +207,7 @@ async function handleUnsubscribe(data: WsMessage) { userPubKey: payload.userPublicKey, status: false, }); - console.log(`[unsubscribe] Host ${payload.id} unsubscribed`); + log("info", `[unsubscribe] Host ${payload.id} unsubscribed`); } } @@ -262,7 +280,9 @@ async function handleStatus(data: WsMessage) { data: { status: data.status }, }); } catch (err) { - console.error(`[status] Error updating job ${data.jobId}:`, err); + log("error", `[status] Error updating job ${data.jobId}`, { + error: (err as Error).message, + }); } } @@ -272,7 +292,7 @@ function verifyHostToken( token: string, ): { id: string; userPublicKey: string } | null { try { - const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; + const decoded = jwt.verify(token, JWT_SECRET!) as JwtPayload; if (!decoded?.id || !decoded?.userPublicKey) return null; return { id: decoded.id, userPublicKey: decoded.userPublicKey }; } catch { @@ -282,9 +302,26 @@ function verifyHostToken( function verifyUserToken(token: string): boolean { try { - const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; + const decoded = jwt.verify(token, JWT_SECRET!) as JwtPayload; return !!(decoded?.id && decoded?.machineId); } catch { return false; } } + +function gracefulShutdown(signal: string) { + log( + "info", + `[depin-ws-relayer] Received ${signal}, shutting down gracefully...`, + ); + clearInterval(heartbeatTimer); + for (const [, conn] of activeConnections) { + conn.ws.close(1001, "Server shutting down"); + } + activeConnections.clear(); + depinServer.stop(); + process.exit(0); +} + +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); diff --git a/web-services/apps/frontend/.gitignore b/web-services/apps/frontend/.gitignore index 12b134e..54f07af 100644 --- a/web-services/apps/frontend/.gitignore +++ b/web-services/apps/frontend/.gitignore @@ -21,5 +21,4 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? -idl \ No newline at end of file +*.sw? \ No newline at end of file diff --git a/web-services/apps/frontend/idl/contract.json b/web-services/apps/frontend/idl/contract.json new file mode 100644 index 0000000..952cf86 --- /dev/null +++ b/web-services/apps/frontend/idl/contract.json @@ -0,0 +1,2026 @@ +{ + "address": "BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc", + "metadata": { + "name": "contract", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "activate_host", + "discriminator": [ + 45, + 216, + 251, + 148, + 114, + 223, + 124, + 93 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "host" + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "claim_rewards", + "discriminator": [ + 4, + 144, + 132, + 71, + 116, + 23, + 151, + 80 + ], + "accounts": [ + { + "name": "host", + "writable": true, + "signer": true + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "const", + "value": [ + 97, + 120, + 105, + 111, + 110, + 95, + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "deactivate_host", + "discriminator": [ + 186, + 199, + 26, + 132, + 15, + 107, + 167, + 120 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "host" + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "end_rental_session", + "discriminator": [ + 137, + 30, + 27, + 15, + 38, + 234, + 219, + 145 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "arg", + "path": "_user_pub_key" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "_user_pub_key", + "type": "pubkey" + } + ] + }, + { + "name": "finalise_rental_with_escrow", + "discriminator": [ + 169, + 123, + 93, + 73, + 3, + 33, + 124, + 33 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "force_terminate_rental", + "discriminator": [ + 222, + 155, + 74, + 196, + 43, + 39, + 24, + 51 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "fund_vault", + "discriminator": [ + 26, + 33, + 207, + 242, + 119, + 108, + 134, + 73 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "initialise_host_registration", + "discriminator": [ + 52, + 10, + 15, + 93, + 246, + 199, + 29, + 130 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "user_key" + }, + { + "name": "host_machine_registration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "user_key" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "host_name", + "type": "string" + }, + { + "name": "machine_type", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "disk_size", + "type": "u64" + }, + { + "name": "sol_per_hour", + "type": "u64" + } + ] + }, + { + "name": "initialize_vault", + "discriminator": [ + 48, + 191, + 163, + 44, + 71, + 129, + 63, + 164 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "penalize_host", + "discriminator": [ + 85, + 159, + 25, + 178, + 64, + 97, + 181, + 21 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "settle_depin_job", + "discriminator": [ + 171, + 255, + 10, + 181, + 121, + 250, + 77, + 219 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "renter", + "writable": true + }, + { + "name": "host", + "writable": true + }, + { + "name": "platform_vault", + "writable": true + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "renter" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "renter" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "renter" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "host_earned", + "type": "u64" + }, + { + "name": "platform_fee_bps", + "type": "u16" + } + ] + }, + { + "name": "start_rental_with_escrow", + "discriminator": [ + 194, + 199, + 159, + 147, + 205, + 206, + 29, + 97 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "top_up_escrow", + "discriminator": [ + 48, + 220, + 80, + 92, + 122, + 91, + 195, + 169 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "transfer_from_vault", + "discriminator": [ + 85, + 166, + 47, + 107, + 26, + 20, + 192, + 23 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "transfer_to_vault_and_rent", + "discriminator": [ + 248, + 150, + 240, + 174, + 139, + 94, + 113, + 234 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "duration_seconds", + "type": "i64" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "withdraw_funds", + "discriminator": [ + 241, + 36, + 29, + 111, + 208, + 31, + 104, + 217 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "secret_key", + "type": "string" + } + ] + } + ], + "accounts": [ + { + "name": "EscrowSession", + "discriminator": [ + 23, + 195, + 112, + 163, + 237, + 163, + 231, + 187 + ] + }, + { + "name": "HostMachineRegistration", + "discriminator": [ + 93, + 141, + 154, + 94, + 126, + 29, + 140, + 108 + ] + }, + { + "name": "RentalSession", + "discriminator": [ + 104, + 56, + 37, + 247, + 197, + 131, + 84, + 28 + ] + }, + { + "name": "VaultAccount", + "discriminator": [ + 230, + 251, + 241, + 83, + 139, + 202, + 93, + 28 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "HostMachineRegistrationNotFound", + "msg": "Host machine registration not found" + }, + { + "code": 6001, + "name": "HostMachineRegistrationAlreadyExists", + "msg": "Host machine registration already exists" + }, + { + "code": 6002, + "name": "InvalidHostMachineRegistrationData", + "msg": "Invalid host machine registration data" + }, + { + "code": 6003, + "name": "UnauthorizedHostMachineAccess", + "msg": "Unauthorized access to host machine registration" + }, + { + "code": 6004, + "name": "HostMachineRegistrationNotActive", + "msg": "Host machine registration is not active" + }, + { + "code": 6005, + "name": "InvalidHostMachineRegistrationId", + "msg": "Host machine registration ID is invalid" + }, + { + "code": 6006, + "name": "InvalidHostMachineRegistrationOS", + "msg": "Host machine registration OS is invalid" + }, + { + "code": 6007, + "name": "InvalidHostMachineRegistrationDiskSize", + "msg": "Host machine registration disk size is invalid" + }, + { + "code": 6008, + "name": "InvalidHostMachineRegistrationName", + "msg": "Host machine registration name is invalid" + }, + { + "code": 6009, + "name": "KeyDoesNotMatchPDA", + "msg": "key does not match with pda" + }, + { + "code": 6010, + "name": "UnauthorizedAdmin", + "msg": "Unauthorized admin access" + }, + { + "code": 6011, + "name": "HostKeyMismatch", + "msg": "Host key mismatch" + }, + { + "code": 6012, + "name": "HostMachineRegistrationNotActiveLongEnough", + "msg": "Host machine registration not active long enough" + }, + { + "code": 6013, + "name": "HostMachineAlreadyStarted", + "msg": "Host machine already started" + }, + { + "code": 6014, + "name": "HostMachineRegistrationNotFoundForId", + "msg": "Host machine registration not found for the given ID" + }, + { + "code": 6015, + "name": "HostMachinePenalized", + "msg": "Host machine penalized" + }, + { + "code": 6016, + "name": "HostMachineAlreadyActive", + "msg": "Host machine is already active" + }, + { + "code": 6017, + "name": "HostMachineShouldNotBeActiveForClaiming", + "msg": "Host machine should not be active for claiming earned rewards" + } + ], + "types": [ + { + "name": "EscrowSession", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "is_active", + "type": "bool" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "host", + "type": "pubkey" + }, + { + "name": "id", + "type": "string" + } + ] + } + }, + { + "name": "HostMachineRegistration", + "type": { + "kind": "struct", + "fields": [ + { + "name": "is_active", + "type": "bool" + }, + { + "name": "host_key", + "type": "pubkey" + }, + { + "name": "host_name", + "type": "string" + }, + { + "name": "machine_type", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "disk_size", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "earned", + "type": "u64" + }, + { + "name": "started_at", + "type": "i64" + }, + { + "name": "penalized", + "type": "bool" + }, + { + "name": "sol_per_hour", + "type": "u64" + } + ] + } + }, + { + "name": "RentalSession", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "amount_paid", + "type": "u64" + }, + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "duration_seconds", + "type": "i64" + }, + { + "name": "is_active", + "type": "bool" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "VaultAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/web-services/apps/frontend/src/contractidl.ts b/web-services/apps/frontend/src/contractidl.ts index 9eaaead..bdd33da 100644 --- a/web-services/apps/frontend/src/contractidl.ts +++ b/web-services/apps/frontend/src/contractidl.ts @@ -1,7 +1,7 @@ import type { Idl } from "@coral-xyz/anchor"; export const idl = { - address: "J7nyNjMR7p9Xi8ohzkNAFmnAeVUBb1AMpGKTFGtFvVjJ", + address: "BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc", metadata: { name: "contract", version: "0.1.0", diff --git a/web-services/apps/frontend/src/lib/useIndexerEvents.ts b/web-services/apps/frontend/src/lib/useIndexerEvents.ts index 25b94ee..136ed4e 100644 --- a/web-services/apps/frontend/src/lib/useIndexerEvents.ts +++ b/web-services/apps/frontend/src/lib/useIndexerEvents.ts @@ -43,6 +43,8 @@ export function useWSConnectionStatus(): WSConnectionState { return useSyncExternalStore(subscribeToState, getConnectionState); } +export { connect as connectWs }; + // ── Backoff ─────────────────────────────────────────────────────────── const BACKOFF_BASE = 1000; const BACKOFF_MAX = 30000; diff --git a/web-services/apps/frontend/src/pages/Status.tsx b/web-services/apps/frontend/src/pages/Status.tsx index 7c87b23..d7fefd0 100644 --- a/web-services/apps/frontend/src/pages/Status.tsx +++ b/web-services/apps/frontend/src/pages/Status.tsx @@ -1,7 +1,8 @@ +import { useEffect } from "react"; import { motion } from "motion/react"; import { BackgroundGlow } from "@/components/BackgroundGlow"; import { useHealth } from "@/hooks/useHealth"; -import { useWSConnectionStatus } from "@/lib/useIndexerEvents"; +import { useWSConnectionStatus, connectWs } from "@/lib/useIndexerEvents"; import { CheckCircle, AlertCircle, Loader2 } from "lucide-react"; type ServiceStatus = "ok" | "degraded" | "down" | "connecting"; @@ -30,6 +31,10 @@ export default function Status() { const health = useHealth(); const wsStatus = useWSConnectionStatus(); + useEffect(() => { + connectWs(); + }, []); + const overall: ServiceStatus = (() => { if (health.error && wsStatus === "disconnected") return "down"; if (health.data?.status === "degraded" || wsStatus === "disconnected") diff --git a/web-services/apps/worker/.gitignore b/web-services/apps/worker/.gitignore index 3c824c2..4cbaf13 100644 --- a/web-services/apps/worker/.gitignore +++ b/web-services/apps/worker/.gitignore @@ -32,5 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store -vm-manager.json -idl \ No newline at end of file +vm-manager.json \ No newline at end of file diff --git a/web-services/apps/worker/contract.ts b/web-services/apps/worker/contract.ts index b2fae41..b544ad2 100644 --- a/web-services/apps/worker/contract.ts +++ b/web-services/apps/worker/contract.ts @@ -9,6 +9,7 @@ import { AnchorProvider, Program, type Idl } from "@coral-xyz/anchor"; import { existsSync, readFileSync } from "node:fs"; import bs58 from "bs58"; import BN from "bn.js"; +import { logger } from "@axion/utilities"; import fallbackIdl from "./contractIdl"; const connection = new Connection(clusterApiUrl("devnet")); @@ -94,7 +95,7 @@ export async function endRentalSession( return tx; } } catch (error) { - console.error("Error ending rental session:", error); + logger.error("Error ending rental session", error as Error); throw error; } } @@ -132,7 +133,7 @@ export async function InitialiseHostPDA( await connection.confirmTransaction(tx); return { hostMachinePda }; } catch (error) { - console.error("Error initializing host PDA:", error); + logger.error("Error initializing host PDA", error as Error); throw error; } } @@ -154,7 +155,7 @@ export async function deActivateHost( await connection.confirmTransaction(tx); return tx; } catch (error) { - console.error("Error deactivating host:", error); + logger.error("Error deactivating host", error as Error); throw error; } } @@ -176,7 +177,7 @@ export async function activateHost( await connection.confirmTransaction(tx); return tx; } catch (error) { - console.error("Error activating host:", error); + logger.error("Error activating host", error as Error); throw error; } } @@ -200,7 +201,7 @@ export async function claimRewards( await connection.confirmTransaction(tx); return tx; } catch (error) { - console.error("Error claiming rewards:", error); + logger.error("Error claiming rewards", error as Error); throw error; } } @@ -235,7 +236,7 @@ export async function settleDepinJob( await connection.confirmTransaction(tx); return tx; } catch (error) { - console.error("Error settling DePIN job:", error); + logger.error("Error settling DePIN job", error as Error); throw error; } } @@ -257,7 +258,7 @@ export async function penalizeHost( await connection.confirmTransaction(tx); return tx; } catch (error) { - console.error("Error penalizing host:", error); + logger.error("Error penalizing host", error as Error); throw error; } } diff --git a/web-services/apps/worker/contractIdl.ts b/web-services/apps/worker/contractIdl.ts index f64b880..4703606 100644 --- a/web-services/apps/worker/contractIdl.ts +++ b/web-services/apps/worker/contractIdl.ts @@ -1,7 +1,7 @@ import type { Idl } from "@coral-xyz/anchor"; const idl = { - address: "J7nyNjMR7p9Xi8ohzkNAFmnAeVUBb1AMpGKTFGtFvVjJ", + address: "BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc", metadata: { name: "contract", version: "0.1.0", diff --git a/web-services/apps/worker/idl/contract.json b/web-services/apps/worker/idl/contract.json new file mode 100644 index 0000000..952cf86 --- /dev/null +++ b/web-services/apps/worker/idl/contract.json @@ -0,0 +1,2026 @@ +{ + "address": "BD8qpWm9WWLcqQu5PKJ3Lew4BZ6nh6n96FMZv3DJ54sc", + "metadata": { + "name": "contract", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "activate_host", + "discriminator": [ + 45, + 216, + 251, + 148, + 114, + 223, + 124, + 93 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "host" + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "claim_rewards", + "discriminator": [ + 4, + 144, + 132, + 71, + 116, + 23, + 151, + 80 + ], + "accounts": [ + { + "name": "host", + "writable": true, + "signer": true + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "const", + "value": [ + 97, + 120, + 105, + 111, + 110, + 95, + 118, + 97, + 117, + 108, + 116 + ] + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "deactivate_host", + "discriminator": [ + 186, + 199, + 26, + 132, + 15, + 107, + 167, + 120 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "host" + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "end_rental_session", + "discriminator": [ + 137, + 30, + 27, + 15, + 38, + 234, + 219, + 145 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "arg", + "path": "_user_pub_key" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "_user_pub_key", + "type": "pubkey" + } + ] + }, + { + "name": "finalise_rental_with_escrow", + "discriminator": [ + 169, + 123, + 93, + 73, + 3, + 33, + 124, + 33 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "force_terminate_rental", + "discriminator": [ + 222, + 155, + 74, + 196, + 43, + 39, + 24, + 51 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "fund_vault", + "discriminator": [ + 26, + 33, + 207, + 242, + 119, + 108, + 134, + 73 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "initialise_host_registration", + "discriminator": [ + 52, + 10, + 15, + 93, + 246, + 199, + 29, + 130 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "user_key" + }, + { + "name": "host_machine_registration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "user_key" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "host_name", + "type": "string" + }, + { + "name": "machine_type", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "disk_size", + "type": "u64" + }, + { + "name": "sol_per_hour", + "type": "u64" + } + ] + }, + { + "name": "initialize_vault", + "discriminator": [ + 48, + 191, + 163, + 44, + 71, + 129, + 63, + 164 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "penalize_host", + "discriminator": [ + 85, + 159, + 25, + 178, + 64, + 97, + 181, + 21 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "user" + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + } + ], + "args": [ + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "settle_depin_job", + "discriminator": [ + 171, + 255, + 10, + 181, + 121, + 250, + 77, + 219 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "renter", + "writable": true + }, + { + "name": "host", + "writable": true + }, + { + "name": "platform_vault", + "writable": true + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "renter" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "renter" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "renter" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "host_machine", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 115, + 116, + 95, + 109, + 97, + 99, + 104, + 105, + 110, + 101 + ] + }, + { + "kind": "account", + "path": "host" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "host_earned", + "type": "u64" + }, + { + "name": "platform_fee_bps", + "type": "u16" + } + ] + }, + { + "name": "start_rental_with_escrow", + "discriminator": [ + 194, + 199, + 159, + 147, + 205, + 206, + 29, + 97 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "id", + "type": "string" + } + ] + }, + { + "name": "top_up_escrow", + "discriminator": [ + 48, + 220, + 80, + 92, + 122, + 91, + 195, + 169 + ], + "accounts": [ + { + "name": "user", + "writable": true, + "signer": true + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "escrow_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "user" + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "id", + "type": "string" + }, + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "transfer_from_vault", + "discriminator": [ + 85, + 166, + 47, + 107, + 26, + 20, + 192, + 23 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "_id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "transfer_to_vault_and_rent", + "discriminator": [ + 248, + 150, + 240, + 174, + 139, + 94, + 113, + 234 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "admin" + }, + { + "name": "rental_session", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 114, + 101, + 110, + 116, + 97, + 108, + 95, + 115, + 101, + 115, + 115, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "payer" + }, + { + "kind": "arg", + "path": "id" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "duration_seconds", + "type": "i64" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "secret_key", + "type": "string" + } + ] + }, + { + "name": "withdraw_funds", + "discriminator": [ + 241, + 36, + 29, + 111, + 208, + 31, + 104, + 217 + ], + "accounts": [ + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "vault_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "admin" + }, + { + "kind": "arg", + "path": "_secret_key" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "secret_key", + "type": "string" + } + ] + } + ], + "accounts": [ + { + "name": "EscrowSession", + "discriminator": [ + 23, + 195, + 112, + 163, + 237, + 163, + 231, + 187 + ] + }, + { + "name": "HostMachineRegistration", + "discriminator": [ + 93, + 141, + 154, + 94, + 126, + 29, + 140, + 108 + ] + }, + { + "name": "RentalSession", + "discriminator": [ + 104, + 56, + 37, + 247, + 197, + 131, + 84, + 28 + ] + }, + { + "name": "VaultAccount", + "discriminator": [ + 230, + 251, + 241, + 83, + 139, + 202, + 93, + 28 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "HostMachineRegistrationNotFound", + "msg": "Host machine registration not found" + }, + { + "code": 6001, + "name": "HostMachineRegistrationAlreadyExists", + "msg": "Host machine registration already exists" + }, + { + "code": 6002, + "name": "InvalidHostMachineRegistrationData", + "msg": "Invalid host machine registration data" + }, + { + "code": 6003, + "name": "UnauthorizedHostMachineAccess", + "msg": "Unauthorized access to host machine registration" + }, + { + "code": 6004, + "name": "HostMachineRegistrationNotActive", + "msg": "Host machine registration is not active" + }, + { + "code": 6005, + "name": "InvalidHostMachineRegistrationId", + "msg": "Host machine registration ID is invalid" + }, + { + "code": 6006, + "name": "InvalidHostMachineRegistrationOS", + "msg": "Host machine registration OS is invalid" + }, + { + "code": 6007, + "name": "InvalidHostMachineRegistrationDiskSize", + "msg": "Host machine registration disk size is invalid" + }, + { + "code": 6008, + "name": "InvalidHostMachineRegistrationName", + "msg": "Host machine registration name is invalid" + }, + { + "code": 6009, + "name": "KeyDoesNotMatchPDA", + "msg": "key does not match with pda" + }, + { + "code": 6010, + "name": "UnauthorizedAdmin", + "msg": "Unauthorized admin access" + }, + { + "code": 6011, + "name": "HostKeyMismatch", + "msg": "Host key mismatch" + }, + { + "code": 6012, + "name": "HostMachineRegistrationNotActiveLongEnough", + "msg": "Host machine registration not active long enough" + }, + { + "code": 6013, + "name": "HostMachineAlreadyStarted", + "msg": "Host machine already started" + }, + { + "code": 6014, + "name": "HostMachineRegistrationNotFoundForId", + "msg": "Host machine registration not found for the given ID" + }, + { + "code": 6015, + "name": "HostMachinePenalized", + "msg": "Host machine penalized" + }, + { + "code": 6016, + "name": "HostMachineAlreadyActive", + "msg": "Host machine is already active" + }, + { + "code": 6017, + "name": "HostMachineShouldNotBeActiveForClaiming", + "msg": "Host machine should not be active for claiming earned rewards" + } + ], + "types": [ + { + "name": "EscrowSession", + "type": { + "kind": "struct", + "fields": [ + { + "name": "start_time", + "type": "i64" + }, + { + "name": "is_active", + "type": "bool" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "host", + "type": "pubkey" + }, + { + "name": "id", + "type": "string" + } + ] + } + }, + { + "name": "HostMachineRegistration", + "type": { + "kind": "struct", + "fields": [ + { + "name": "is_active", + "type": "bool" + }, + { + "name": "host_key", + "type": "pubkey" + }, + { + "name": "host_name", + "type": "string" + }, + { + "name": "machine_type", + "type": "string" + }, + { + "name": "os", + "type": "string" + }, + { + "name": "disk_size", + "type": "u64" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "earned", + "type": "u64" + }, + { + "name": "started_at", + "type": "i64" + }, + { + "name": "penalized", + "type": "bool" + }, + { + "name": "sol_per_hour", + "type": "u64" + } + ] + } + }, + { + "name": "RentalSession", + "type": { + "kind": "struct", + "fields": [ + { + "name": "user", + "type": "pubkey" + }, + { + "name": "id", + "type": "string" + }, + { + "name": "amount_paid", + "type": "u64" + }, + { + "name": "start_time", + "type": "i64" + }, + { + "name": "end_time", + "type": "i64" + }, + { + "name": "duration_seconds", + "type": "i64" + }, + { + "name": "is_active", + "type": "bool" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "VaultAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/web-services/apps/worker/index.ts b/web-services/apps/worker/index.ts index 5cb5581..6f07d06 100644 --- a/web-services/apps/worker/index.ts +++ b/web-services/apps/worker/index.ts @@ -1,21 +1,11 @@ import { Worker } from "bullmq"; -import compute from "@google-cloud/compute"; import prisma from "@axion/db"; import { logger } from "@axion/utilities"; import { redisConnection as connection } from "@axion/utilities/redis"; -import { - activateHost, - claimRewards, - deActivateHost, - endRentalSession, - InitialiseHostPDA, - penalizeHost, - settleDepinJob, -} from "./contract"; const HEALTH_PORT = Number(process.env.HEALTH_PORT || "9094"); -Bun.serve({ +const healthServer = Bun.serve({ port: HEALTH_PORT, fetch(req) { const url = new URL(req.url); @@ -33,11 +23,25 @@ const projectId = process.env.PROJECT_ID; const PLATFORM_VAULT_PUBKEY = process.env.PLATFORM_VAULT_PUBKEY || ""; const PLATFORM_FEE_BPS = Number(process.env.PLATFORM_FEE_BPS || "1000"); -const ws = new WebSocket(process.env.WS_URL || "ws://localhost:8080"); +const WS_URL = process.env.WS_URL || "ws://localhost:8080"; + +let ws: WebSocket | undefined; +let contractModulePromise: Promise | undefined; + +function getContractModule() { + contractModulePromise ??= import("./contract"); + return contractModulePromise; +} + +function getWebSocket() { + ws ??= new WebSocket(WS_URL); + return ws; +} const worker = new Worker( "vm-termination", async (job) => { + const { endRentalSession } = await getContractModule(); logger.info(`Processing job ${job.id} for VM instance`, { vmId: job.data.vmId, }); @@ -62,7 +66,7 @@ const worker = new Worker( }); logger.info(`VM instance ${instanceId} deleted and rental session ended`); }, - { connection }, + { connection, concurrency: 1 }, ); worker.on("completed", (job) => { @@ -76,6 +80,7 @@ worker.on("failed", (job, err) => { const DepinWorker = new Worker( "initialise-host-pda", async (job) => { + const { InitialiseHostPDA } = await getContractModule(); const { id, hostName, @@ -103,7 +108,7 @@ const DepinWorker = new Worker( data: { pdaAddress: tx.hostMachinePda.toBase58() }, }); }, - { connection }, + { connection, concurrency: 1 }, ); DepinWorker.on("completed", (job) => { @@ -116,6 +121,7 @@ DepinWorker.on("failed", (job, err) => { const changeVmStatus = new Worker( "changeVMStatus", async (job) => { + const { activateHost, deActivateHost } = await getContractModule(); const { id, userPubKey, status } = job.data; if (status === false) { await deActivateHost(id, userPubKey); @@ -124,7 +130,7 @@ const changeVmStatus = new Worker( } logger.info(`Status change processed for ${id}`, { status }); }, - { connection }, + { connection, concurrency: 1 }, ); changeVmStatus.on("completed", (job) => { @@ -137,6 +143,7 @@ changeVmStatus.on("failed", (job, err) => { const terminateDepinVm = new Worker( "terminate-depin-vm", async (job) => { + const { settleDepinJob } = await getContractModule(); const { pubKey, id } = job.data; const findVm = await prisma.depinHostMachine.findFirst({ where: { id }, @@ -162,7 +169,7 @@ const terminateDepinVm = new Worker( return; } - ws.send( + getWebSocket().send( JSON.stringify({ type: "end-job", machineId: findVm.id, @@ -219,7 +226,7 @@ const terminateDepinVm = new Worker( logger.info(`DePIN job ${id} settled successfully`); }, - { connection }, + { connection, concurrency: 1 }, ); terminateDepinVm.on("completed", (job) => { @@ -229,9 +236,19 @@ terminateDepinVm.on("failed", (job, err) => { logger.error(`Terminate depin VM ${job?.id} failed`, err); }); +let computeClient: InstanceType | null = null; + +async function getInstancesClient() { + if (!computeClient) { + const { default: compute } = await import("@google-cloud/compute"); + computeClient = new compute.InstancesClient(); + } + return computeClient; +} + async function deleteInstance(zone: string, instanceId: string) { - const instancesClient = new compute.InstancesClient(); - await instancesClient.delete({ + const client = await getInstancesClient(); + await client.delete({ project: projectId, zone, instance: instanceId, @@ -242,6 +259,7 @@ async function deleteInstance(zone: string, instanceId: string) { const claimRewardsWorker = new Worker( "claim-rewards", async (job) => { + const { claimRewards } = await getContractModule(); const { id, userPubKey } = job.data; const tx = await claimRewards(id, userPubKey); if (!tx) { @@ -249,7 +267,7 @@ const claimRewardsWorker = new Worker( } logger.info(`Rewards claimed for ${id}`, { tx }); }, - { connection }, + { connection, concurrency: 1 }, ); claimRewardsWorker.on("completed", (job) => { @@ -262,6 +280,7 @@ claimRewardsWorker.on("failed", (job, err) => { const penalizeHostWorker = new Worker( "penalize-host", async (job) => { + const { penalizeHost } = await getContractModule(); const { id, userPubKey } = job.data; const tx = await penalizeHost(id, userPubKey); if (!tx) { @@ -269,7 +288,7 @@ const penalizeHostWorker = new Worker( } logger.info(`Host ${id} penalized`, { tx }); }, - { connection }, + { connection, concurrency: 1 }, ); penalizeHostWorker.on("completed", (job) => { @@ -278,3 +297,22 @@ penalizeHostWorker.on("completed", (job) => { penalizeHostWorker.on("failed", (job, err) => { logger.error(`Penalize host ${job?.id} failed`, err); }); + +async function gracefulShutdown(signal: string) { + logger.info(`Received ${signal}, shutting down gracefully...`); + healthServer.stop(); + await Promise.all([ + worker.close(), + DepinWorker.close(), + changeVmStatus.close(), + terminateDepinVm.close(), + claimRewardsWorker.close(), + penalizeHostWorker.close(), + ]); + logger.info("All workers closed"); + await prisma.$disconnect(); + process.exit(0); +} + +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); diff --git a/web-services/apps/worker/package.json b/web-services/apps/worker/package.json index e1cdd51..3e31e7e 100644 --- a/web-services/apps/worker/package.json +++ b/web-services/apps/worker/package.json @@ -19,7 +19,6 @@ "@coral-xyz/anchor": "^0.32.1", "@google-cloud/compute": "^6.11.0", "@solana/web3.js": "^1.98.4", - "bn.js": "^5.2.2", "bs58": "^6.0.0", "bullmq": "^5.56.5", "ioredis": "^5.6.1", diff --git a/web-services/apps/ws-relayer/index.ts b/web-services/apps/ws-relayer/index.ts index e1dea54..15601dc 100644 --- a/web-services/apps/ws-relayer/index.ts +++ b/web-services/apps/ws-relayer/index.ts @@ -3,6 +3,17 @@ import { Client as SSHClient } from "ssh2"; import { type JwtPayload } from "jsonwebtoken"; import jwt from "jsonwebtoken"; +function log(level: string, msg: string, meta?: unknown) { + console.log( + JSON.stringify({ + level, + msg, + ...(meta ? { meta } : {}), + timestamp: new Date().toISOString(), + }), + ); +} + interface Session { userId: string; allowedVM: string; @@ -40,10 +51,13 @@ type incomingMessage = { const SSH_READY_TIMEOUT_MS = 10000; -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; -const INDEXER_TOKEN = process.env.INDEXER_TOKEN || "changeme"; +const JWT_SECRET = process.env.JWT_SECRET; +const INDEXER_TOKEN = process.env.INDEXER_TOKEN; -Bun.serve({ +if (!JWT_SECRET) throw new Error("JWT_SECRET is required"); +if (!INDEXER_TOKEN) throw new Error("INDEXER_TOKEN is required"); + +const server = Bun.serve({ fetch(req, server) { const url = new URL(req.url); @@ -67,7 +81,8 @@ Bun.serve({ } } - console.log( + log( + "info", `[WS-Relayer] Broadcasting ${body.instruction} to ${targetClients.size} clients`, ); @@ -129,7 +144,9 @@ Bun.serve({ disconnectFromVM(ws as ServerWebSocket); } } catch (err) { - console.error("Error processing WebSocket message:", err); + log("error", "Error processing WebSocket message", { + error: (err as Error).message, + }); ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" })); } }, @@ -156,7 +173,7 @@ Bun.serve({ function authenticateUser(ws: ServerWebSocket, token: string) { try { - const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; + const decoded = jwt.verify(token, JWT_SECRET!) as JwtPayload; if ( !decoded || !decoded.userId || @@ -328,3 +345,20 @@ function disconnectFromVM(ws: ServerWebSocket) { userSessions.delete(ws); } + +function gracefulShutdown(signal: string) { + log("info", `[ws-relayer] Received ${signal}, shutting down gracefully...`); + for (const ws of allClients) { + ws.close(1001, "Server shutting down"); + } + for (const [, subs] of pubkeySubscriptions) { + subs.clear(); + } + pubkeySubscriptions.clear(); + allClients.clear(); + server.stop(); + process.exit(0); +} + +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGINT", () => gracefulShutdown("SIGINT")); diff --git a/web-services/bun.lock b/web-services/bun.lock index 7324e7b..8881ac3 100644 --- a/web-services/bun.lock +++ b/web-services/bun.lock @@ -140,7 +140,6 @@ "@coral-xyz/anchor": "^0.32.1", "@google-cloud/compute": "^6.11.0", "@solana/web3.js": "^1.98.4", - "bn.js": "^5.2.2", "bs58": "^6.0.0", "bullmq": "^5.56.5", "ioredis": "^5.6.1", diff --git a/web-services/docker/worker.dockerfile b/web-services/docker/worker.dockerfile index a904482..f7b0c8a 100644 --- a/web-services/docker/worker.dockerfile +++ b/web-services/docker/worker.dockerfile @@ -60,4 +60,4 @@ COPY --from=builder /app/packages/utilities ./packages/utilities WORKDIR /app/apps/worker -CMD ["bun", "index.ts"] +CMD ["bun", "dist/index.js"] diff --git a/web-services/packages/db/index.ts b/web-services/packages/db/index.ts index 95612c3..7af9067 100644 --- a/web-services/packages/db/index.ts +++ b/web-services/packages/db/index.ts @@ -8,7 +8,12 @@ if (!connectionString) { throw new Error("DATABASE_URL is required for Prisma initialization"); } -const pool = new Pool({ connectionString }); +const pool = new Pool({ + connectionString, + max: 10, + connectionTimeoutMillis: 5000, + idleTimeoutMillis: 30000, +}); const adapter = new PrismaPg(pool); const prisma = new PrismaClient({ adapter }); diff --git a/web-services/packages/utilities/authMiddleware.ts b/web-services/packages/utilities/authMiddleware.ts index bbed7e6..63c0f98 100644 --- a/web-services/packages/utilities/authMiddleware.ts +++ b/web-services/packages/utilities/authMiddleware.ts @@ -1,5 +1,6 @@ import type { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; +import { logger } from "./logger"; declare global { namespace Express { @@ -44,7 +45,10 @@ export async function authMiddleware( req.userId = decoded.userId; next(); } catch (error) { - console.error("Auth error:", error); + logger.error( + "Auth error", + error instanceof Error ? error : new Error(String(error)), + ); if (error instanceof jwt.TokenExpiredError) { res.status(401).json({ success: false, diff --git a/web-services/packages/utilities/index.ts b/web-services/packages/utilities/index.ts index f9e55d6..8d99ad1 100644 --- a/web-services/packages/utilities/index.ts +++ b/web-services/packages/utilities/index.ts @@ -23,4 +23,7 @@ export { indexerLimiter, healthLimiter, depinVerificationLimiter, + deployLimiter, + registerLimiter, + claimSOLLimiter, } from "./rateLimiter"; diff --git a/web-services/packages/utilities/rateLimiter.ts b/web-services/packages/utilities/rateLimiter.ts index e18eb60..5456336 100644 --- a/web-services/packages/utilities/rateLimiter.ts +++ b/web-services/packages/utilities/rateLimiter.ts @@ -60,3 +60,21 @@ export const depinVerificationLimiter = createRateLimiter({ max: 30, message: "Too many verification requests, please try again later", }); + +export const deployLimiter = createRateLimiter({ + windowMs: 60_000, + max: 10, + message: "Too many deploy requests, please try again later", +}); + +export const registerLimiter = createRateLimiter({ + windowMs: 60_000, + max: 5, + message: "Too many registration requests, please try again later", +}); + +export const claimSOLLimiter = createRateLimiter({ + windowMs: 60_000, + max: 3, + message: "Too many claim requests, please try again later", +}); diff --git a/web-services/packages/utilities/redis.ts b/web-services/packages/utilities/redis.ts index e6a6bce..215bb66 100644 --- a/web-services/packages/utilities/redis.ts +++ b/web-services/packages/utilities/redis.ts @@ -5,6 +5,13 @@ const connection = new IORedis({ host: process.env.REDIS_HOST || "localhost", port: 6379, maxRetriesPerRequest: null, + connectTimeout: 10_000, + keepAlive: 30_000, + retryStrategy(times) { + if (times > 10) return null; + return Math.min(times * 200, 2000); + }, + lazyConnect: true, }); export function getRedisConnection(): IORedis { diff --git a/web-services/packages/utilities/response.ts b/web-services/packages/utilities/response.ts index 25f8222..4af49fc 100644 --- a/web-services/packages/utilities/response.ts +++ b/web-services/packages/utilities/response.ts @@ -1,6 +1,7 @@ import type { Response } from "express"; import type { ZodError } from "zod"; import { AppError, ValidationError } from "./errors"; +import { logger } from "./logger"; export interface ApiResponse { success: boolean; @@ -24,7 +25,10 @@ export function sendError(res: Response, err: unknown) { } satisfies ApiResponse); return; } - console.error("Unhandled error:", err); + logger.error( + "Unhandled error", + err instanceof Error ? err : new Error(String(err)), + ); res.status(500).json({ success: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" },