From 78fd1bf843621c72a1a334586a98cedfe1930f4a Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Sat, 22 Mar 2025 15:56:17 +0545 Subject: [PATCH 1/7] added github workflow and improvement --- .github/workflows/deploy_api.yml | 24 ++ .github/workflows/deploy_frontend.yml | 24 ++ .github/workflows/deploy_hub.yml | 24 ++ .github/workflows/deploy_validator.yml | 24 ++ apps/api/config.ts | 21 +- apps/api/index.ts | 133 ++++++----- apps/api/middleware.ts | 38 ++- apps/api/package.json | 5 +- apps/api/types.d.ts | 6 - apps/frontend/app/layout.tsx | 14 +- apps/frontend/hooks/useWebsites.tsx | 58 ++--- apps/hub/index.ts | 306 ++++++++++++++----------- apps/hub/package.json | 5 +- apps/validator/.env.example | 2 + apps/validator/index.ts | 195 ++++++++++------ apps/validator/package.json | 6 +- bun.lock | 16 +- docker/Dockerfile.api | 17 ++ docker/Dockerfile.frontend | 15 ++ docker/Dockerfile.hub | 17 ++ docker/Dockerfile.validator | 13 ++ package.json | 9 +- packages/common/index.ts | 62 ++--- packages/db/.env.example | 0 packages/db/package.json | 1 + packages/db/prisma/seed.ts | 133 ++++++----- 26 files changed, 750 insertions(+), 418 deletions(-) create mode 100644 .github/workflows/deploy_api.yml create mode 100644 .github/workflows/deploy_frontend.yml create mode 100644 .github/workflows/deploy_hub.yml create mode 100644 .github/workflows/deploy_validator.yml create mode 100644 apps/validator/.env.example create mode 100644 docker/Dockerfile.api create mode 100644 docker/Dockerfile.frontend create mode 100644 docker/Dockerfile.hub create mode 100644 docker/Dockerfile.validator create mode 100644 packages/db/.env.example diff --git a/.github/workflows/deploy_api.yml b/.github/workflows/deploy_api.yml new file mode 100644 index 0000000..4f3123d --- /dev/null +++ b/.github/workflows/deploy_api.yml @@ -0,0 +1,24 @@ +name: Docker push Api +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-actions@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB.TOKEN }} + - name: Build and push worker + uses: docker/build-push-actions@v5 + with: + context: . + file: ./docker/Dockerfile.api + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} diff --git a/.github/workflows/deploy_frontend.yml b/.github/workflows/deploy_frontend.yml new file mode 100644 index 0000000..7a5ec3a --- /dev/null +++ b/.github/workflows/deploy_frontend.yml @@ -0,0 +1,24 @@ +name: Docker push Frontend +on: + push: + branches: [development] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to DockerHub + uses: docker/login-actions@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB.TOKEN }} + - name: Build and push frontend + uses: docker/build-push-actions@v5 + with: + context: . + file: ./docker/Dockerfile.frontend + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} diff --git a/.github/workflows/deploy_hub.yml b/.github/workflows/deploy_hub.yml new file mode 100644 index 0000000..6a38783 --- /dev/null +++ b/.github/workflows/deploy_hub.yml @@ -0,0 +1,24 @@ +name: Docker push Hub +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-actions@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB.TOKEN }} + - name: Build and push Hub + uses: docker/build-push-actions@v5 + with: + context: . + file: ./docker/Dockerfile.hub + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} diff --git a/.github/workflows/deploy_validator.yml b/.github/workflows/deploy_validator.yml new file mode 100644 index 0000000..26f08f8 --- /dev/null +++ b/.github/workflows/deploy_validator.yml @@ -0,0 +1,24 @@ +name: Docker push Validator +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to DockerHub + uses: docker/login-actions@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB.TOKEN }} + - name: Build and push Validator + uses: docker/build-push-actions@v5 + with: + context: . + file: ./docker/Dockerfile.validator + push: true + tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} diff --git a/apps/api/config.ts b/apps/api/config.ts index 9ac5e49..5d00f97 100644 --- a/apps/api/config.ts +++ b/apps/api/config.ts @@ -1,11 +1,12 @@ - -export const JWT_PUBLIC_KEY = process.env.JWT_PUBLIC_KEY || `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxYRjjCo+ZhdDrsVduRaj -o4U9XNYbUxG4LBgtrHUpvMrRda6yXXfuecC0zwdF4O16ZNp9LMyy2bllXG8HOWyw -ac/r/9Zt98A9jMUEKq/AhRvaRaavr9jMFceJNTebFMOnzPRZGIhMWWOGPsrwqWLa -JbMtnAd2IlQc4MNJmns2NeaPmujDysZDmN5BwLd0LzTaktOFsrVCFYu7MiL7sYi9 -229OovGxu55Y1qpekwt42nuipbpZEx59FdAIpJCgjsp8GeqavC5ySWuhRbKGTO+e -qiBV0S9m134FuGRc67HEYcr7BkTSFwF4pD/L2culHEBaRedilGdRbNAGFAQhN4mS -yQIDAQAB +export const JWT_PUBLIC_KEY = + process.env.JWT_PUBLIC_KEY || + `-----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqhRzktxm2Z/vQlmwl4Md + JpOoXSeT9RQWbE+XHRHp2UGaRVHhuZg5Tu2trXmn40R7BfZlWUuIgaKyqwYw1UkX + pCYrpfX8vqwIELY33hm6Ssxt/nI6fXcyM6F/w0c/tCNxbSQj+dn5R0Oa3nY/zJc1 + RopATCkB2ZPT1obOERyEMNjnfrxcmIlK0tmOLsvkYXAjfDYNH29JrD/DGY7m0/Am + VHWaJM/lIkL+Lwq++mmO2cWD3ya4WxUgKtIDUw9PX5WOUiDigC5OK/Px04glFIyS + VG3I4BtsRt0s+G61DQjYZgrUHudJj7zVCGGHayMUrMCl37LWk1ply7RGrZlL78/M + MQIDAQAB -----END PUBLIC KEY----- -`; \ No newline at end of file +`; diff --git a/apps/api/index.ts b/apps/api/index.ts index e791ed7..217ae38 100644 --- a/apps/api/index.ts +++ b/apps/api/index.ts @@ -1,10 +1,9 @@ -import express from "express" +import express from "express"; import { authMiddleware } from "./middleware"; import { prismaClient } from "db/client"; import cors from "cors"; import { Transaction, SystemProgram, Connection } from "@solana/web3.js"; - const connection = new Connection("https://api.mainnet-beta.solana.com"); const app = express(); @@ -12,79 +11,77 @@ app.use(cors()); app.use(express.json()); app.post("/api/v1/website", authMiddleware, async (req, res) => { - const userId = req.userId!; - const { url } = req.body; + const userId = req.userId!; + const { url } = req.body; - const data = await prismaClient.website.create({ - data: { - userId, - url - } - }) + const data = await prismaClient.website.create({ + data: { + userId, + url, + }, + }); - res.json({ - id: data.id - }) -}) + res.json({ + id: data.id, + }); +}); app.get("/api/v1/website/status", authMiddleware, async (req, res) => { - const websiteId = req.query.websiteId! as unknown as string; - const userId = req.userId; - - const data = await prismaClient.website.findFirst({ - where: { - id: websiteId, - userId, - disabled: false - }, - include: { - ticks: true - } - }) - - res.json(data) - -}) + const websiteId = req.query.websiteId! as unknown as string; + const userId = req.userId; + + const data = await prismaClient.website.findFirst({ + where: { + id: websiteId, + userId, + disabled: false, + }, + include: { + ticks: true, + }, + }); + + res.json(data); +}); app.get("/api/v1/websites", authMiddleware, async (req, res) => { - const userId = req.userId!; - - const websites = await prismaClient.website.findMany({ - where: { - userId, - disabled: false - }, - include: { - ticks: true - } - }) - - res.json({ - websites - }) -}) + const userId = req.userId!; + console.log("userId :", userId); + + const websites = await prismaClient.website.findMany({ + where: { + userId, + disabled: false, + }, + include: { + ticks: true, + }, + }); + + res.json({ + websites, + }); +}); app.delete("/api/v1/website/", authMiddleware, async (req, res) => { - const websiteId = req.body.websiteId; - const userId = req.userId!; - - await prismaClient.website.update({ - where: { - id: websiteId, - userId - }, - data: { - disabled: true - } - }) - - res.json({ - message: "Deleted website successfully" - }) -}) - -app.post("/api/v1/payout/:validatorId", async (req, res) => { - -}) + const websiteId = req.body.websiteId; + const userId = req.userId!; + + await prismaClient.website.update({ + where: { + id: websiteId, + userId, + }, + data: { + disabled: true, + }, + }); + + res.json({ + message: "Deleted website successfully", + }); +}); + +app.post("/api/v1/payout/:validatorId", async (req, res) => {}); app.listen(8080); diff --git a/apps/api/middleware.ts b/apps/api/middleware.ts index 5217ce0..d6e40fe 100644 --- a/apps/api/middleware.ts +++ b/apps/api/middleware.ts @@ -2,19 +2,39 @@ import type { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; import { JWT_PUBLIC_KEY } from "./config"; -export function authMiddleware(req: Request, res: Response, next: NextFunction) { - const token = req.headers['authorization']; - if (!token) { - return res.status(401).json({ error: 'Unauthorized' }); +declare global { + namespace Express { + interface Request { + userId?: string; } + } +} +export function authMiddleware( + req: Request, + res: Response, + next: NextFunction, +) { + const authHeader = req.headers["authorization"]; + if (!authHeader) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + + const token = authHeader.split(" ")[1]; + + try { const decoded = jwt.verify(token, JWT_PUBLIC_KEY); - console.log(decoded); if (!decoded || !decoded.sub) { - return res.status(401).json({ error: 'Unauthorized' }); + res.status(401).json({ error: "Invalid token" }); + return; } req.userId = decoded.sub as string; - - next() -} \ No newline at end of file + + next(); + } catch (err) { + res.status(401).json({ message: "Invalid token" }); + return; + } +} diff --git a/apps/api/package.json b/apps/api/package.json index 1901ff9..3f14872 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -2,6 +2,9 @@ "name": "api", "module": "index.ts", "type": "module", + "scripts": { + "dev": "bun index.ts" + }, "devDependencies": { "@types/bun": "latest" }, @@ -19,4 +22,4 @@ "jsonwebtoken": "^9.0.2", "jwt": "^0.2.0" } -} \ No newline at end of file +} diff --git a/apps/api/types.d.ts b/apps/api/types.d.ts index 9bdaea1..e69de29 100644 --- a/apps/api/types.d.ts +++ b/apps/api/types.d.ts @@ -1,6 +0,0 @@ - -declare namespace Express { - interface Request { - userId?: string - } -} \ No newline at end of file diff --git a/apps/frontend/app/layout.tsx b/apps/frontend/app/layout.tsx index c159be5..9a293ad 100644 --- a/apps/frontend/app/layout.tsx +++ b/apps/frontend/app/layout.tsx @@ -1,10 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import { - ClerkProvider -} from '@clerk/nextjs' -import { Appbar } from "../components/Appbar"; +import { ClerkProvider } from "@clerk/nextjs"; +import { Appbar } from "@/components/Appbar"; import { ThemeProvider } from "@/components/theme-provider"; const geistSans = Geist({ @@ -28,12 +26,16 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - + {children} diff --git a/apps/frontend/hooks/useWebsites.tsx b/apps/frontend/hooks/useWebsites.tsx index e61d0e6..f154a61 100644 --- a/apps/frontend/hooks/useWebsites.tsx +++ b/apps/frontend/hooks/useWebsites.tsx @@ -5,41 +5,45 @@ import axios from "axios"; import { useEffect, useState } from "react"; interface Website { + id: string; + url: string; + ticks: { id: string; - url: string; - ticks: { - id: string; - createdAt: string; - status: string; - latency: number; - }[]; + createdAt: string; + status: string; + latency: number; + }[]; } export function useWebsites() { - const { getToken } = useAuth(); - const [websites, setWebsites] = useState([]); + const { getToken } = useAuth(); + const [websites, setWebsites] = useState([]); - async function refreshWebsites() { - const token = await getToken(); - const response = await axios.get(`${API_BACKEND_URL}/api/v1/websites`, { - headers: { - Authorization: token, - }, - }); + async function refreshWebsites() { + const token = await getToken(); + const response = await axios.get(`${API_BACKEND_URL}/api/v1/websites`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); - setWebsites(response.data.websites); - } + console.log("response: ", response); - useEffect(() => { - refreshWebsites(); + setWebsites(response.data.websites); + } - const interval = setInterval(() => { - refreshWebsites(); - }, 1000 * 60 * 1); + useEffect(() => { + refreshWebsites(); - return () => clearInterval(interval); - }, []); + const interval = setInterval( + () => { + refreshWebsites(); + }, + 1000 * 60 * 1, + ); - return { websites, refreshWebsites }; + return () => clearInterval(interval); + }, []); -} \ No newline at end of file + return { websites, refreshWebsites }; +} diff --git a/apps/hub/index.ts b/apps/hub/index.ts index e8fac06..b0a782a 100644 --- a/apps/hub/index.ts +++ b/apps/hub/index.ts @@ -5,154 +5,202 @@ import { PublicKey } from "@solana/web3.js"; import nacl from "tweetnacl"; import nacl_util from "tweetnacl-util"; -const availableValidators: { validatorId: string, socket: ServerWebSocket, publicKey: string }[] = []; +const availableValidators: { + validatorId: string; + socket: ServerWebSocket; + publicKey: string; +}[] = []; -const CALLBACKS : { [callbackId: string]: (data: IncomingMessage) => void } = {} +const CALLBACKS: { [callbackId: string]: (data: IncomingMessage) => void } = {}; const COST_PER_VALIDATION = 100; // in lamports Bun.serve({ - fetch(req, server) { - if (server.upgrade(req)) { - return; + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + port: 8081, + websocket: { + async message(ws: ServerWebSocket, message: string) { + const data: IncomingMessage = JSON.parse(message); + + if (data.type === "signup") { + const verified = await verifyMessage( + `Signed message for ${data.data.callbackId}, ${data.data.publicKey}`, + data.data.publicKey, + data.data.signedMessage, + ); + if (verified) { + await signupHandler(ws, data.data); + } + } else if (data.type === "validate") { + CALLBACKS[data.data.callbackId](data); + delete CALLBACKS[data.data.callbackId]; } - return new Response("Upgrade failed", { status: 500 }); }, - port: 8081, - websocket: { - async message(ws: ServerWebSocket, message: string) { - const data: IncomingMessage = JSON.parse(message); - - if (data.type === 'signup') { - - const verified = await verifyMessage( - `Signed message for ${data.data.callbackId}, ${data.data.publicKey}`, - data.data.publicKey, - data.data.signedMessage - ); - if (verified) { - await signupHandler(ws, data.data); - } - } else if (data.type === 'validate') { - CALLBACKS[data.data.callbackId](data); - delete CALLBACKS[data.data.callbackId]; - } - }, - async close(ws: ServerWebSocket) { - availableValidators.splice(availableValidators.findIndex(v => v.socket === ws), 1); - } + async close(ws: ServerWebSocket) { + availableValidators.splice( + availableValidators.findIndex((v) => v.socket === ws), + 1, + ); }, + }, }); -async function signupHandler(ws: ServerWebSocket, { ip, publicKey, signedMessage, callbackId }: SignupIncomingMessage) { - const validatorDb = await prismaClient.validator.findFirst({ - where: { - publicKey, - }, - }); +async function signupHandler( + ws: ServerWebSocket, + { ip, publicKey, signedMessage, callbackId }: SignupIncomingMessage, +) { + const validatorDb = await prismaClient.validator.findFirst({ + where: { + publicKey, + }, + }); - if (validatorDb) { - ws.send(JSON.stringify({ - type: 'signup', - data: { - validatorId: validatorDb.id, - callbackId, - }, - })); - - availableValidators.push({ - validatorId: validatorDb.id, - socket: ws, - publicKey: validatorDb.publicKey, - }); - return; - } - - //TODO: Given the ip, return the location - const validator = await prismaClient.validator.create({ + if (validatorDb) { + ws.send( + JSON.stringify({ + type: "signup", data: { - ip, - publicKey, - location: 'unknown', + validatorId: validatorDb.id, + callbackId, }, + }), + ); + + availableValidators.push({ + validatorId: validatorDb.id, + socket: ws, + publicKey: validatorDb.publicKey, }); + return; + } + + async function getIpLocation(ip: string): Promise { + try { + const res = await fetch(`http://ip-api.com/json/${ip}`); + if (!res.ok) throw new Error("Failed to fetch location"); + const data = await res.json(); + console.log(data); + if (data.status !== "success") + throw new Error("Failed to fetch location"); + return JSON.stringify({ + country: data.country, + region: data.regionName, + lat: data.lat, + lon: data.lon, + }); + } catch (error) { + console.log(error); + return JSON.stringify({ + country: "unknown", + region: "unknown", + lat: 0, + lon: 0, + }); + } + } - ws.send(JSON.stringify({ - type: 'signup', - data: { - validatorId: validator.id, - callbackId, - }, - })); + const validatorLocation = await getIpLocation(ip); - availableValidators.push({ + //TODO: Given the ip, return the location + const validator = await prismaClient.validator.create({ + data: { + ip, + publicKey, + location: validatorLocation, + }, + }); + + ws.send( + JSON.stringify({ + type: "signup", + data: { validatorId: validator.id, - socket: ws, - publicKey: validator.publicKey, - }); + callbackId, + }, + }), + ); + + availableValidators.push({ + validatorId: validator.id, + socket: ws, + publicKey: validator.publicKey, + }); } -async function verifyMessage(message: string, publicKey: string, signature: string) { - const messageBytes = nacl_util.decodeUTF8(message); - const result = nacl.sign.detached.verify( - messageBytes, - new Uint8Array(JSON.parse(signature)), - new PublicKey(publicKey).toBytes(), - ); - - return result; +async function verifyMessage( + message: string, + publicKey: string, + signature: string, +) { + const messageBytes = nacl_util.decodeUTF8(message); + const result = nacl.sign.detached.verify( + messageBytes, + new Uint8Array(JSON.parse(signature)), + new PublicKey(publicKey).toBytes(), + ); + + return result; } setInterval(async () => { - const websitesToMonitor = await prismaClient.website.findMany({ - where: { - disabled: false, - }, + const websitesToMonitor = await prismaClient.website.findMany({ + where: { + disabled: false, + }, + }); + + for (const website of websitesToMonitor) { + availableValidators.forEach((validator) => { + const callbackId = randomUUIDv7(); + console.log( + `Sending validate to ${validator.validatorId} ${website.url}`, + ); + validator.socket.send( + JSON.stringify({ + type: "validate", + data: { + url: website.url, + callbackId, + }, + }), + ); + + CALLBACKS[callbackId] = async (data: IncomingMessage) => { + if (data.type === "validate") { + const { validatorId, status, latency, signedMessage } = data.data; + const verified = await verifyMessage( + `Replying to ${callbackId}`, + validator.publicKey, + signedMessage, + ); + if (!verified) { + return; + } + + await prismaClient.$transaction(async (tx) => { + await tx.websiteTick.create({ + data: { + websiteId: website.id, + validatorId, + status, + latency, + createdAt: new Date(), + }, + }); + + await tx.validator.update({ + where: { id: validatorId }, + data: { + pendingPayouts: { increment: COST_PER_VALIDATION }, + }, + }); + }); + } + }; }); - - for (const website of websitesToMonitor) { - availableValidators.forEach(validator => { - const callbackId = randomUUIDv7(); - console.log(`Sending validate to ${validator.validatorId} ${website.url}`); - validator.socket.send(JSON.stringify({ - type: 'validate', - data: { - url: website.url, - callbackId - }, - })); - - CALLBACKS[callbackId] = async (data: IncomingMessage) => { - if (data.type === 'validate') { - const { validatorId, status, latency, signedMessage } = data.data; - const verified = await verifyMessage( - `Replying to ${callbackId}`, - validator.publicKey, - signedMessage - ); - if (!verified) { - return; - } - - await prismaClient.$transaction(async (tx) => { - await tx.websiteTick.create({ - data: { - websiteId: website.id, - validatorId, - status, - latency, - createdAt: new Date(), - }, - }); - - await tx.validator.update({ - where: { id: validatorId }, - data: { - pendingPayouts: { increment: COST_PER_VALIDATION }, - }, - }); - }); - } - }; - }); - } -}, 60 * 1000); \ No newline at end of file + } +}, 60 * 1000); diff --git a/apps/hub/package.json b/apps/hub/package.json index d9e5b19..5ccf3a4 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -2,6 +2,9 @@ "name": "hub", "module": "index.ts", "type": "module", + "scripts": { + "dev": "bun index.ts" + }, "devDependencies": { "@types/bun": "latest", "common": "*", @@ -15,4 +18,4 @@ "@solana/web3.js": "^1.98.0", "tweetnacl": "^1.0.3" } -} \ No newline at end of file +} diff --git a/apps/validator/.env.example b/apps/validator/.env.example new file mode 100644 index 0000000..d85d04d --- /dev/null +++ b/apps/validator/.env.example @@ -0,0 +1,2 @@ +# PRIVATE_KEY=`[]` +PRIVATE_KEY= diff --git a/apps/validator/index.ts b/apps/validator/index.ts index 1736c7e..1160f2a 100644 --- a/apps/validator/index.ts +++ b/apps/validator/index.ts @@ -1,97 +1,144 @@ import { randomUUIDv7 } from "bun"; -import type { OutgoingMessage, SignupOutgoingMessage, ValidateOutgoingMessage } from "common/types"; +import type { + OutgoingMessage, + SignupOutgoingMessage, + ValidateOutgoingMessage, +} from "common/types"; import { Keypair } from "@solana/web3.js"; import nacl from "tweetnacl"; import nacl_util from "tweetnacl-util"; +import bs58 from "bs58"; -const CALLBACKS: {[callbackId: string]: (data: SignupOutgoingMessage) => void} = {} +const CALLBACKS: { + [callbackId: string]: (data: SignupOutgoingMessage) => void; +} = {}; let validatorId: string | null = null; async function main() { - const keypair = Keypair.fromSecretKey( - Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!)) - ); - const ws = new WebSocket("ws://localhost:8081"); - - ws.onmessage = async (event) => { - const data: OutgoingMessage = JSON.parse(event.data); - if (data.type === 'signup') { - CALLBACKS[data.data.callbackId]?.(data.data) - delete CALLBACKS[data.data.callbackId]; - } else if (data.type === 'validate') { - await validateHandler(ws, data.data, keypair); - } + // const keypair = Keypair.fromSecretKey( + // Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!)) + // ); + + const keypair = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!)); + + interface IpInfo { + ip: string; + city?: string; + region?: string; + country?: string; + loc?: string; // Latitude and longitude ("37.7749,-122.4194") + } + async function getIpAndLocation(): Promise { + try { + const response = await fetch("https://ipinfo.io/json"); + const data = await response.json(); + return data as IpInfo; + } catch (error) { + const response = await fetch("http://ip-api.com/json/"); + const data = await response.json(); + return { + ip: data.query, + city: data.city, + region: data.regionName, + country: data.country, + loc: `${data.lat},${data.lon}`, + }; } + } - ws.onopen = async () => { - const callbackId = randomUUIDv7(); - CALLBACKS[callbackId] = (data: SignupOutgoingMessage) => { - validatorId = data.validatorId; - } - const signedMessage = await signMessage(`Signed message for ${callbackId}, ${keypair.publicKey}`, keypair); - - ws.send(JSON.stringify({ - type: 'signup', - data: { - callbackId, - ip: '127.0.0.1', - publicKey: keypair.publicKey, - signedMessage, - }, - })); + const ws = new WebSocket("ws://localhost:8081"); + + ws.onmessage = async (event) => { + const data: OutgoingMessage = JSON.parse(event.data); + if (data.type === "signup") { + CALLBACKS[data.data.callbackId]?.(data.data); + delete CALLBACKS[data.data.callbackId]; + } else if (data.type === "validate") { + await validateHandler(ws, data.data, keypair); } + }; + + ws.onopen = async () => { + const ipInfo = await getIpAndLocation(); + const callbackId = randomUUIDv7(); + CALLBACKS[callbackId] = (data: SignupOutgoingMessage) => { + validatorId = data.validatorId; + }; + const signedMessage = await signMessage( + `Signed message for ${callbackId}, ${keypair.publicKey}`, + keypair, + ); + + ws.send( + JSON.stringify({ + type: "signup", + data: { + callbackId, + ip: ipInfo.ip, + publicKey: keypair.publicKey, + signedMessage, + }, + }), + ); + }; } -async function validateHandler(ws: WebSocket, { url, callbackId, websiteId }: ValidateOutgoingMessage, keypair: Keypair) { - console.log(`Validating ${url}`); - const startTime = Date.now(); - const signature = await signMessage(`Replying to ${callbackId}`, keypair); +async function validateHandler( + ws: WebSocket, + { url, callbackId, websiteId }: ValidateOutgoingMessage, + keypair: Keypair, +) { + console.log(`Validating ${url}`); + const startTime = Date.now(); + const signature = await signMessage(`Replying to ${callbackId}`, keypair); - try { - const response = await fetch(url); - const endTime = Date.now(); - const latency = endTime - startTime; - const status = response.status; - - console.log(url); - console.log(status); - ws.send(JSON.stringify({ - type: 'validate', - data: { - callbackId, - status: status === 200 ? 'Good' : 'Bad', - latency, - websiteId, - validatorId, - signedMessage: signature, - }, - })); - } catch (error) { - ws.send(JSON.stringify({ - type: 'validate', - data: { - callbackId, - status:'Bad', - latency: 1000, - websiteId, - validatorId, - signedMessage: signature, - }, - })); - console.error(error); - } + try { + const response = await fetch(url); + const endTime = Date.now(); + const latency = endTime - startTime; + const status = response.status; + + console.log(url); + console.log(status); + ws.send( + JSON.stringify({ + type: "validate", + data: { + callbackId, + status: status === 200 ? "Good" : "Bad", + latency, + websiteId, + validatorId, + signedMessage: signature, + }, + }), + ); + } catch (error) { + ws.send( + JSON.stringify({ + type: "validate", + data: { + callbackId, + status: "Bad", + latency: 1000, + websiteId, + validatorId, + signedMessage: signature, + }, + }), + ); + console.error(error); + } } async function signMessage(message: string, keypair: Keypair) { - const messageBytes = nacl_util.decodeUTF8(message); - const signature = nacl.sign.detached(messageBytes, keypair.secretKey); + const messageBytes = nacl_util.decodeUTF8(message); + const signature = nacl.sign.detached(messageBytes, keypair.secretKey); - return JSON.stringify(Array.from(signature)); + return JSON.stringify(Array.from(signature)); } main(); -setInterval(async () => { - -}, 10000); \ No newline at end of file +setInterval(async () => {}, 1000); diff --git a/apps/validator/package.json b/apps/validator/package.json index 8b0102e..ae95d88 100644 --- a/apps/validator/package.json +++ b/apps/validator/package.json @@ -2,6 +2,9 @@ "name": "validator", "module": "index.ts", "type": "module", + "scripts": { + "dev": "bun index.ts" + }, "devDependencies": { "@types/bun": "latest", "common": "*" @@ -12,7 +15,8 @@ "dependencies": { "@solana/kit": "^2.1.0", "@solana/web3.js": "^1.98.0", + "bs58": "^6.0.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" } -} \ No newline at end of file +} diff --git a/bun.lock b/bun.lock index 1b01488..217e878 100644 --- a/bun.lock +++ b/bun.lock @@ -82,6 +82,7 @@ "dependencies": { "@solana/kit": "^2.1.0", "@solana/web3.js": "^1.98.0", + "bs58": "^6.0.0", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1", }, @@ -105,6 +106,7 @@ "packages/db": { "name": "db", "dependencies": { + "@prisma/client": "6.5.0", "prisma": "^6.5.0", }, "devDependencies": { @@ -327,6 +329,8 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + "@prisma/client": ["@prisma/client@6.5.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw=="], + "@prisma/config": ["@prisma/config@6.5.0", "", { "dependencies": { "esbuild": ">=0.12 <1", "esbuild-register": "3.6.0" } }, "sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw=="], "@prisma/debug": ["@prisma/debug@6.5.0", "", {}, "sha512-fc/nusYBlJMzDmDepdUtH9aBsJrda2JNErP9AzuHbgUEQY0/9zQYZdNlXmKoIWENtio+qarPNe/+DQtrX5kMcQ=="], @@ -613,7 +617,7 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + "base-x": ["base-x@5.0.1", "", {}, "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -635,7 +639,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + "bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], @@ -1647,6 +1651,8 @@ "@solana/rpc-transport-http/undici-types": ["undici-types@7.5.0", "", {}, "sha512-CxNFga24pkqrtk9aO4jV78tWXLZhVVU9J2/EAhBGwqJ1+tsLydMI2Vaq7wj3ba/SZL7BL8aq5rflf75DhbgkhA=="], + "@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@turbo/gen/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1669,6 +1675,8 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "borsh/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -1733,6 +1741,8 @@ "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "@solana/web3.js/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + "@turbo/gen/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@turbo/workspaces/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1745,6 +1755,8 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "borsh/bs58/base-x": ["base-x@3.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api new file mode 100644 index 0000000..39da083 --- /dev/null +++ b/docker/Dockerfile.api @@ -0,0 +1,17 @@ +FROM oven/bun:latest + +WORKDIR /app + +COPY ./apps/api ./apps/api +COPY ./packages/ ./packages +COPY package* . +COPY turbo.json . +COPY bun.lock . + +RUN bun install + +RUN bun run db:generate + +EXPOSE 8080 + +CMD ["bun", "run", "api"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend new file mode 100644 index 0000000..b492edc --- /dev/null +++ b/docker/Dockerfile.frontend @@ -0,0 +1,15 @@ +FROM oven/bun:latest + +WORKDIR /app + +COPY ./apps/frontend/ ./apps/frontend +COPY ./packages/ ./packages +COPY package* . +COPY turbo.json . +COPY bun.lock . + +RUN bun install + +EXPOSE 3000 + +CMD ["bun", "run", "frontend"] diff --git a/docker/Dockerfile.hub b/docker/Dockerfile.hub new file mode 100644 index 0000000..eaa0ccc --- /dev/null +++ b/docker/Dockerfile.hub @@ -0,0 +1,17 @@ +FROM oven/bun:latest + +WORKDIR /app + +COPY ./apps/hub ./apps/hub +COPY ./packages/ ./packages +COPY package* . +COPY turbo.json . +COPY bun.lock . + +RUN bun install + +RUN bun run db:generate + +EXPOSE 8081 + +CMD ["bun", "run", "hub"] diff --git a/docker/Dockerfile.validator b/docker/Dockerfile.validator new file mode 100644 index 0000000..3a08b94 --- /dev/null +++ b/docker/Dockerfile.validator @@ -0,0 +1,13 @@ +FROM oven/bun:latest + +WORKDIR /app + +COPY ./apps/validator ./apps/validator +COPY ./packages/ ./packages +COPY package* . +COPY turbo.json . +COPY bun.lock . + +RUN bun install + +CMD ["bun", "run", "validator"] diff --git a/package.json b/package.json index 17d11e2..78168f6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,14 @@ "dev": "turbo run dev", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"", - "check-types": "turbo run check-types" + "check-types": "turbo run check-types", + "api": "cd ./apps/api && bun run dev", + "hub": "cd ./apps/hub && bun run dev", + "validator": "cd ./apps/validator && bun run dev", + "db:migrate": "cd ./packages/db && bunx prisma migrate dev && cd ../..", + "db:generate": "cd ./packages/db && bunx prisma generate && cd ../..", + "frontend": "cd ./apps/frontend && bun run dev", + "frontend:prod": "cd ./apps/frontend && bun run start" }, "devDependencies": { "prettier": "^3.5.3", diff --git a/packages/common/index.ts b/packages/common/index.ts index 4d4e6d6..a2c0291 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -1,42 +1,46 @@ export interface SignupIncomingMessage { - ip: string; - publicKey: string; - signedMessage: string; - callbackId: string; + ip: string; + publicKey: string; + signedMessage: string; + callbackId: string; } export interface ValidateIncomingMessage { - callbackId: string; - signedMessage: string; - status: 'Good' | 'Bad'; - latency: number; - websiteId: string; - validatorId: string; + callbackId: string; + signedMessage: string; + status: "Good" | "Bad"; + latency: number; + websiteId: string; + validatorId: string; } export interface SignupOutgoingMessage { - validatorId: string; - callbackId: string; + validatorId: string; + callbackId: string; } export interface ValidateOutgoingMessage { - url: string, - callbackId: string, - websiteId: string; + url: string; + callbackId: string; + websiteId: string; } -export type IncomingMessage = { - type: 'signup' - data: SignupIncomingMessage -} | { - type: 'validate' - data: ValidateIncomingMessage -} +export type IncomingMessage = + | { + type: "signup"; + data: SignupIncomingMessage; + } + | { + type: "validate"; + data: ValidateIncomingMessage; + }; -export type OutgoingMessage = { - type: 'signup' - data: SignupOutgoingMessage -} | { - type: 'validate' - data: ValidateOutgoingMessage -} +export type OutgoingMessage = + | { + type: "signup"; + data: SignupOutgoingMessage; + } + | { + type: "validate"; + data: ValidateOutgoingMessage; + }; diff --git a/packages/db/.env.example b/packages/db/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/packages/db/package.json b/packages/db/package.json index 833e905..9d56099 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -12,6 +12,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "@prisma/client": "6.5.0", "prisma": "^6.5.0" }, "prisma": { diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts index 379553d..8dd127c 100644 --- a/packages/db/prisma/seed.ts +++ b/packages/db/prisma/seed.ts @@ -1,60 +1,85 @@ - import { prismaClient } from "../src"; -const USER_ID = "4"; +const RANDOM_NUMBER = Math.random() * 10; async function seed() { - await prismaClient.user.create({ - data: { - id: USER_ID, - email: "test@test.com", - } - }) - - const website = await prismaClient.website.create({ - data: { - url: "https://test.com", - userId: USER_ID - } - }) - - const validator = await prismaClient.validator.create({ - data: { - publicKey: "0x12341223123", - location: "Delhi", - ip: "127.0.0.1", - } - }) - - await prismaClient.websiteTick.create({ - data: { - websiteId: website.id, - status: "Good", - createdAt: new Date(), - latency: 100, - validatorId: validator.id - } - }) - - await prismaClient.websiteTick.create({ - data: { - websiteId: website.id, - status: "Good", - createdAt: new Date(Date.now() - 1000 * 60 *10), - latency: 100, - validatorId: validator.id - } - }) - - await prismaClient.websiteTick.create({ - data: { - websiteId: website.id, - status: "Bad", - createdAt: new Date(Date.now() - 1000 * 60 * 20), - latency: 100, - validatorId: validator.id - } - }) + const user = await prismaClient.user.create({ + data: { + email: `test${RANDOM_NUMBER}@test.com`, + }, + }); + + const website = await prismaClient.website.create({ + data: { + url: `https://test${RANDOM_NUMBER}.com`, + userId: user.id, + }, + }); + + interface IpInfo { + ip: string; + city?: string; + region?: string; + country?: string; + loc?: string; // Latitude and longitude ("37.7749,-122.4194") + } + async function getIpAndLocation(): Promise { + try { + const response = await fetch("https://ipinfo.io/json"); + const data = await response.json(); + return data as IpInfo; + } catch (error) { + const response = await fetch("http://ip-api.com/json/"); + const data = await response.json(); + return { + ip: data.query, + city: data.city, + region: data.regionName, + country: data.country, + loc: `${data.lat},${data.lon}`, + }; + } + } + + const ipInfo = await getIpAndLocation(); + + const validator = await prismaClient.validator.create({ + data: { + publicKey: "0x12341223123", + location: ipInfo.city!, + ip: ipInfo.ip, + }, + }); + + await prismaClient.websiteTick.create({ + data: { + websiteId: website.id, + status: "Good", + createdAt: new Date(), + latency: 100, + validatorId: validator.id, + }, + }); + + await prismaClient.websiteTick.create({ + data: { + websiteId: website.id, + status: "Good", + createdAt: new Date(Date.now() - 1000 * 60 * 10), + latency: 100, + validatorId: validator.id, + }, + }); + + await prismaClient.websiteTick.create({ + data: { + websiteId: website.id, + status: "Bad", + createdAt: new Date(Date.now() - 1000 * 60 * 20), + latency: 100, + validatorId: validator.id, + }, + }); } -seed(); \ No newline at end of file +seed(); From df5ca5d459e482dfa0f270c3205237e3dec4ac82 Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Sun, 23 Mar 2025 18:33:16 +0545 Subject: [PATCH 2/7] added missing todos and and many more --- .github/workflows/deploy_api.yml | 29 ++- .github/workflows/deploy_frontend.yml | 6 +- .github/workflows/deploy_hub.yml | 6 +- .github/workflows/deploy_validator.yml | 6 +- apps/api/.env.example | 1 + apps/api/index.ts | 75 +++++++- bun.lock | 26 +++ packages/metrics/.gitignore | 175 ++++++++++++++++++ packages/metrics/README.md | 15 ++ packages/metrics/activeRequests.ts | 6 + packages/metrics/index.ts | 39 ++++ packages/metrics/package.json | 19 ++ packages/metrics/requestCount.ts | 7 + packages/metrics/requestTime.ts | 8 + packages/metrics/tsconfig.json | 27 +++ packages/typescript-config/base.json | 19 -- packages/typescript-config/nextjs.json | 12 -- packages/typescript-config/package.json | 9 - packages/typescript-config/react-library.json | 7 - prometheus.yml | 8 + 20 files changed, 438 insertions(+), 62 deletions(-) create mode 100644 apps/api/.env.example create mode 100644 packages/metrics/.gitignore create mode 100644 packages/metrics/README.md create mode 100644 packages/metrics/activeRequests.ts create mode 100644 packages/metrics/index.ts create mode 100644 packages/metrics/package.json create mode 100644 packages/metrics/requestCount.ts create mode 100644 packages/metrics/requestTime.ts create mode 100644 packages/metrics/tsconfig.json delete mode 100644 packages/typescript-config/base.json delete mode 100644 packages/typescript-config/nextjs.json delete mode 100644 packages/typescript-config/package.json delete mode 100644 packages/typescript-config/react-library.json create mode 100644 prometheus.yml diff --git a/.github/workflows/deploy_api.yml b/.github/workflows/deploy_api.yml index 4f3123d..7fa1d48 100644 --- a/.github/workflows/deploy_api.yml +++ b/.github/workflows/deploy_api.yml @@ -1,4 +1,5 @@ name: Docker push Api + on: push: branches: [main] @@ -8,17 +9,37 @@ jobs: build-and-push: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Check Out Repo + uses: actions/checkout@v4 - name: Login to Docker Hub - uses: docker/login-actions@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB.TOKEN }} - name: Build and push worker - uses: docker/build-push-actions@v5 + uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.api push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha}} + + - name: Verify Pushed Image + run: | + docker pull ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha }} + + - name: Deploy to EC2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_KEY }} + scripts: | + sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha }} + sudo docker stop api-backend || true + sudo docekr rm api-backend || true + sudo docker run -d \ + --name api-backend \ + -p 8080:8080 \ + ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha }} diff --git a/.github/workflows/deploy_frontend.yml b/.github/workflows/deploy_frontend.yml index 7a5ec3a..7102700 100644 --- a/.github/workflows/deploy_frontend.yml +++ b/.github/workflows/deploy_frontend.yml @@ -11,14 +11,14 @@ jobs: - uses: actions/checkout@v4 - name: Login to DockerHub - uses: docker/login-actions@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB.TOKEN }} - name: Build and push frontend - uses: docker/build-push-actions@v5 + uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.frontend push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/dpin-frontend:${{ github.sha}} diff --git a/.github/workflows/deploy_hub.yml b/.github/workflows/deploy_hub.yml index 6a38783..5d8bd17 100644 --- a/.github/workflows/deploy_hub.yml +++ b/.github/workflows/deploy_hub.yml @@ -11,14 +11,14 @@ jobs: - uses: actions/checkout@v4 - name: Login to Docker Hub - uses: docker/login-actions@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB.TOKEN }} - name: Build and push Hub - uses: docker/build-push-actions@v5 + uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.hub push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/hub-backend:${{ github.sha}} diff --git a/.github/workflows/deploy_validator.yml b/.github/workflows/deploy_validator.yml index 26f08f8..4ac3d27 100644 --- a/.github/workflows/deploy_validator.yml +++ b/.github/workflows/deploy_validator.yml @@ -11,14 +11,14 @@ jobs: - uses: actions/checkout@v4 - name: Login to DockerHub - uses: docker/login-actions@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB.TOKEN }} - name: Build and push Validator - uses: docker/build-push-actions@v5 + uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.validator push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}:${{ github.sha}} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/validator-backend:${{ github.sha}} diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..85b5838 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1 @@ +TREASURY_WALLET= \ No newline at end of file diff --git a/apps/api/index.ts b/apps/api/index.ts index 217ae38..571a869 100644 --- a/apps/api/index.ts +++ b/apps/api/index.ts @@ -2,12 +2,14 @@ import express from "express"; import { authMiddleware } from "./middleware"; import { prismaClient } from "db/client"; import cors from "cors"; -import { Transaction, SystemProgram, Connection } from "@solana/web3.js"; +import { metricsMiddleware } from "metrics/metrics"; +import { Transaction, SystemProgram, Connection, PublicKey } from "@solana/web3.js"; const connection = new Connection("https://api.mainnet-beta.solana.com"); const app = express(); app.use(cors()); +app.use(metricsMiddleware); app.use(express.json()); app.post("/api/v1/website", authMiddleware, async (req, res) => { @@ -82,6 +84,75 @@ app.delete("/api/v1/website/", authMiddleware, async (req, res) => { }); }); -app.post("/api/v1/payout/:validatorId", async (req, res) => {}); +app.post("/api/v1/payout/:validatorId", async (req, res) => { + try { + const {amount} = req.body; + const validatorId = req.params.validatorId; + + if (!amount || amount <= 0) { + res.status(400).json({ error: "Invalid amount"}) + return + } + + // database transaction + const result = await prismaClient.$transaction(async (tx) => { + const validator = await tx.validator.findUnique({ + where: { id: validatorId } + }); + + if (!validator) { + throw new Error("Validator not found"); + } + + if (amount > validator.pendingPayouts) { + throw new Error("Payout amount exceeds pending balance"); + } + + const transaction = new Transaction(); + transaction.add( + SystemProgram.transfer({ + fromPubkey: new PublicKey(process.env.TREASURY_WALLET!), + toPubkey: new PublicKey(validator.publicKey!), + lamports: amount + }) + ); + + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + + // Update pending payouts within the transaction + await tx.validator.update({ + where: { id: validatorId }, + data: { + pendingPayouts: validator.pendingPayouts - amount + } + }); + + return { + transaction: transaction.serialize({requireAllSignatures: false}), + remainingPendingAmount: validator.pendingPayouts - amount + }; + }); + + res.json({ + message: "Transaction created successfully", + ...result + }); + + } catch (error) { + console.log("payout error: ", error); + if (error instanceof Error) { + if (error.message === "Validator not found") { + res.status(404).json({ error: "Validator not found" }); + } else if (error.message === "Payout amount exceeds pending balance") { + res.status(400).json({ error: "Payout amount exceeds pending balance" }); + } else { + res.status(500).json({ error: "Failed to create payout" }); + } + } else { + res.status(500).json({ error: "Failed to create payout" }); + } + } +}); app.listen(8080); diff --git a/bun.lock b/bun.lock index 217e878..7a9df34 100644 --- a/bun.lock +++ b/bun.lock @@ -133,6 +133,20 @@ "typescript-eslint": "^8.26.0", }, }, + "packages/metrics": { + "name": "metrics", + "dependencies": { + "@types/express": "^5.0.1", + "express": "^4.21.2", + "prom-client": "^15.1.3", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, "packages/typescript-config": { "name": "@repo/typescript-config", "version": "0.0.0", @@ -329,6 +343,8 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@prisma/client": ["@prisma/client@6.5.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw=="], "@prisma/config": ["@prisma/config@6.5.0", "", { "dependencies": { "esbuild": ">=0.12 <1", "esbuild-register": "3.6.0" } }, "sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw=="], @@ -627,6 +643,8 @@ "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bintrees": ["bintrees@1.0.2", "", {}, "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], @@ -1205,6 +1223,8 @@ "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "metrics": ["metrics@workspace:packages/metrics"], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], @@ -1327,6 +1347,8 @@ "prisma": ["prisma@6.5.0", "", { "dependencies": { "@prisma/config": "6.5.0", "@prisma/engines": "6.5.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg=="], + "prom-client": ["prom-client@15.1.3", "", { "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" } }, "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -1503,6 +1525,8 @@ "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + "tdigest": ["tdigest@0.1.2", "", { "dependencies": { "bintrees": "1.0.2" } }, "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA=="], + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], @@ -1711,6 +1735,8 @@ "log-symbols/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + "metrics/@types/express": ["@types/express@5.0.1", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "node-plop/inquirer": ["inquirer@7.3.3", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.19", "mute-stream": "0.0.8", "run-async": "^2.4.0", "rxjs": "^6.6.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" } }, "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA=="], diff --git a/packages/metrics/.gitignore b/packages/metrics/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/packages/metrics/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/metrics/README.md b/packages/metrics/README.md new file mode 100644 index 0000000..8a98c7c --- /dev/null +++ b/packages/metrics/README.md @@ -0,0 +1,15 @@ +# metrics + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/metrics/activeRequests.ts b/packages/metrics/activeRequests.ts new file mode 100644 index 0000000..d9bb07b --- /dev/null +++ b/packages/metrics/activeRequests.ts @@ -0,0 +1,6 @@ +import client from "prom-client"; + +export const activeRequestGauge = new client.Gauge({ + name: "active-requests", + help: "Number of active requests", +}); diff --git a/packages/metrics/index.ts b/packages/metrics/index.ts new file mode 100644 index 0000000..97574eb --- /dev/null +++ b/packages/metrics/index.ts @@ -0,0 +1,39 @@ +import type { Request, Response, NextFunction } from "express"; +import { activeRequestGauge } from "./activeRequests"; +import { requestCounter } from "./requestCount"; +import { httpRequestDurationMicroseconds } from "./requestTime"; + +export const metricsMiddleware = ( + req: Request, + res: Response, + next: NextFunction, +) => { + const startTime = Date.now(); + + activeRequestGauge.inc(); + + res.on("finish", () => { + const endTime = Date.now(); + const duration = endTime - startTime; + + //increment request counter + requestCounter.inc({ + method: req.method, + route: req.route ? req.route.path : req.path, + status_code: res.statusCode, + }); + + httpRequestDurationMicroseconds.observe( + { + methods: req.method, + route: req.route ? req.route.path : req.path, + code: res.statusCode, + }, + duration, + ); + + activeRequestGauge.dec(); + }); + + next(); +}; diff --git a/packages/metrics/package.json b/packages/metrics/package.json new file mode 100644 index 0000000..8c68eca --- /dev/null +++ b/packages/metrics/package.json @@ -0,0 +1,19 @@ +{ + "name": "metrics", + "module": "index.ts", + "type": "module", + "exports": { + "./metrics": "./index.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@types/express": "^5.0.1", + "express": "^4.21.2", + "prom-client": "^15.1.3" + } +} diff --git a/packages/metrics/requestCount.ts b/packages/metrics/requestCount.ts new file mode 100644 index 0000000..48e1a69 --- /dev/null +++ b/packages/metrics/requestCount.ts @@ -0,0 +1,7 @@ +import client from "prom-client"; + +export const requestCounter = new client.Counter({ + name: "http_requests_total", + help: "Total number of HTTP requests", + labelNames: ["method", "route", "status_code"], +}); diff --git a/packages/metrics/requestTime.ts b/packages/metrics/requestTime.ts new file mode 100644 index 0000000..53ce49a --- /dev/null +++ b/packages/metrics/requestTime.ts @@ -0,0 +1,8 @@ +import client from "prom-client"; + +export const httpRequestDurationMicroseconds = new client.Histogram({ + name: "http_request_duration_ms", + help: "Duration of HTTP requests in ms", + labelNames: ["methods", "route", "code"], + buckets: [0.1, 5, 15, 50, 100, 300], +}); diff --git a/packages/metrics/tsconfig.json b/packages/metrics/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/packages/metrics/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json deleted file mode 100644 index 5117f2a..0000000 --- a/packages/typescript-config/base.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "incremental": false, - "isolatedModules": true, - "lib": ["es2022", "DOM", "DOM.Iterable"], - "module": "NodeNext", - "moduleDetection": "force", - "moduleResolution": "NodeNext", - "noUncheckedIndexedAccess": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "ES2022" - } -} diff --git a/packages/typescript-config/nextjs.json b/packages/typescript-config/nextjs.json deleted file mode 100644 index e6defa4..0000000 --- a/packages/typescript-config/nextjs.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "module": "ESNext", - "moduleResolution": "Bundler", - "allowJs": true, - "jsx": "preserve", - "noEmit": true - } -} diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json deleted file mode 100644 index 27c0e60..0000000 --- a/packages/typescript-config/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@repo/typescript-config", - "version": "0.0.0", - "private": true, - "license": "MIT", - "publishConfig": { - "access": "public" - } -} diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json deleted file mode 100644 index c3a1b26..0000000 --- a/packages/typescript-config/react-library.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "jsx": "react-jsx" - } -} diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..e1dd92a --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,8 @@ +--- +global: + scrape_interval: 15s + +scrape_configs: + - job_name: "api-backend" + static_configs: + - targets: ["api:8080"] From c083832bbb87946d43acd76bf6f76e3ba723267e Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Mon, 24 Mar 2025 02:17:17 +0545 Subject: [PATCH 3/7] imporvement in code --- .github/workflows/deploy_api.yml | 16 ++++++++-------- .github/workflows/deploy_frontend.yml | 8 +++++--- .github/workflows/deploy_hub.yml | 6 +++--- .github/workflows/deploy_validator.yml | 8 +++++--- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy_api.yml b/.github/workflows/deploy_api.yml index 7fa1d48..5aa1760 100644 --- a/.github/workflows/deploy_api.yml +++ b/.github/workflows/deploy_api.yml @@ -1,9 +1,9 @@ +--- name: Docker push Api on: push: branches: [main] - workflow_dispatch: jobs: build-and-push: @@ -16,18 +16,18 @@ jobs: uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB.TOKEN }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push worker uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.api push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha}} + tags: io.pankaj/api-backend:${{ github.sha}} - name: Verify Pushed Image run: | - docker pull ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha }} + docker pull io.pankaj/api-backend:${{ github.sha }} - name: Deploy to EC2 uses: appleboy/ssh-action@master @@ -36,10 +36,10 @@ jobs: username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} scripts: | - sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha }} - sudo docker stop api-backend || true - sudo docekr rm api-backend || true + sudo docker pull io.pankaj/api-backend:${{ github.sha }} + sudo docker stop api-backend || true + sudo docker rm api-backend || true sudo docker run -d \ --name api-backend \ -p 8080:8080 \ - ${{ secrets.DOCKERHUB_USERNAME }}/api-backend:${{ github.sha }} + io.pankaj/api-backend:${{ github.sha }} diff --git a/.github/workflows/deploy_frontend.yml b/.github/workflows/deploy_frontend.yml index 7102700..8eb826a 100644 --- a/.github/workflows/deploy_frontend.yml +++ b/.github/workflows/deploy_frontend.yml @@ -1,8 +1,9 @@ +--- name: Docker push Frontend + on: push: branches: [development] - workflow_dispatch: jobs: build-and-push: @@ -14,11 +15,12 @@ jobs: uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB.TOKEN }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push frontend uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.frontend push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/dpin-frontend:${{ github.sha}} + tags: io.pankaj/dpin-frontend:${{ github.sha}} diff --git a/.github/workflows/deploy_hub.yml b/.github/workflows/deploy_hub.yml index 5d8bd17..5167de4 100644 --- a/.github/workflows/deploy_hub.yml +++ b/.github/workflows/deploy_hub.yml @@ -1,8 +1,8 @@ +--- name: Docker push Hub on: push: branches: [main] - workflow_dispatch: jobs: build-and-push: @@ -14,11 +14,11 @@ jobs: uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB.TOKEN }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Hub uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.hub push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/hub-backend:${{ github.sha}} + tags: io.pankaj/hub-backend:${{ github.sha}} diff --git a/.github/workflows/deploy_validator.yml b/.github/workflows/deploy_validator.yml index 4ac3d27..533e069 100644 --- a/.github/workflows/deploy_validator.yml +++ b/.github/workflows/deploy_validator.yml @@ -1,8 +1,10 @@ +--- name: Docker push Validator + on: push: branches: [main] - workflow_dispatch: + jobs: build-and-push: @@ -14,11 +16,11 @@ jobs: uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB.TOKEN }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Validator uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.validator push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/validator-backend:${{ github.sha}} + tags: io.pankaj/validator-backend:${{ github.sha}} From e9791e6564cb067d03a28bb23090a458f1d76575 Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Tue, 25 Mar 2025 04:19:12 +0545 Subject: [PATCH 4/7] github workflow --- .github/workflows/deploy_api.yml | 24 +-- .github/workflows/deploy_frontend.yml | 54 +++---- .github/workflows/deploy_hub.yml | 2 +- .github/workflows/deploy_validator.yml | 3 +- apps/api/package.json | 1 + apps/hub/index.ts | 138 +++++++++++------- apps/validator/index.ts | 84 ++++++----- bun.lock | 5 +- docker/Dockerfile.api | 24 +-- docker/Dockerfile.frontend | 31 ++-- docker/Dockerfile.hub | 25 ++-- docker/Dockerfile.validator | 24 ++- packages/common/index.ts | 4 +- packages/typescript-config/base.json | 19 +++ packages/typescript-config/nextjs.json | 12 ++ packages/typescript-config/package.json | 9 ++ packages/typescript-config/react-library.json | 7 + 17 files changed, 293 insertions(+), 173 deletions(-) create mode 100644 packages/typescript-config/base.json create mode 100644 packages/typescript-config/nextjs.json create mode 100644 packages/typescript-config/package.json create mode 100644 packages/typescript-config/react-library.json diff --git a/.github/workflows/deploy_api.yml b/.github/workflows/deploy_api.yml index 5aa1760..e480c67 100644 --- a/.github/workflows/deploy_api.yml +++ b/.github/workflows/deploy_api.yml @@ -4,6 +4,7 @@ name: Docker push Api on: push: branches: [main] + workflow_dispatch: jobs: build-and-push: @@ -17,29 +18,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push worker + + - name: Build and push api uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.api push: true - tags: io.pankaj/api-backend:${{ github.sha}} - - - name: Verify Pushed Image - run: | - docker pull io.pankaj/api-backend:${{ github.sha }} + tags: 100xdevs/dpin-uptime-api:${{ github.sha}} - - name: Deploy to EC2 - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.SSH_HOST }} - username: ${{ secrets.SSH_USERNAME }} - key: ${{ secrets.SSH_KEY }} - scripts: | - sudo docker pull io.pankaj/api-backend:${{ github.sha }} - sudo docker stop api-backend || true - sudo docker rm api-backend || true - sudo docker run -d \ - --name api-backend \ - -p 8080:8080 \ - io.pankaj/api-backend:${{ github.sha }} diff --git a/.github/workflows/deploy_frontend.yml b/.github/workflows/deploy_frontend.yml index 8eb826a..dccebc5 100644 --- a/.github/workflows/deploy_frontend.yml +++ b/.github/workflows/deploy_frontend.yml @@ -1,26 +1,30 @@ --- -name: Docker push Frontend - -on: - push: - branches: [development] - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push frontend - uses: docker/build-push-action@v5 - with: - context: . - file: ./docker/Dockerfile.frontend - push: true - tags: io.pankaj/dpin-frontend:${{ github.sha}} + name: Continuous Deployment web + on: + push: + branches: [ main ] + + jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Docker login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./docker/Dockerfile.frontend + push: true + tags: 100xdevs/dpin-uptime-frontend:${{ github.sha }} + build-args: | + DATABASE_URL=${{ secrets.DATABASE_URL }} \ No newline at end of file diff --git a/.github/workflows/deploy_hub.yml b/.github/workflows/deploy_hub.yml index 5167de4..0ce4a4a 100644 --- a/.github/workflows/deploy_hub.yml +++ b/.github/workflows/deploy_hub.yml @@ -21,4 +21,4 @@ jobs: context: . file: ./docker/Dockerfile.hub push: true - tags: io.pankaj/hub-backend:${{ github.sha}} + tags: 100xdevs/dpin-uptime-hub:${{ github.sha}} diff --git a/.github/workflows/deploy_validator.yml b/.github/workflows/deploy_validator.yml index 533e069..6218577 100644 --- a/.github/workflows/deploy_validator.yml +++ b/.github/workflows/deploy_validator.yml @@ -17,10 +17,11 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push Validator uses: docker/build-push-action@v5 with: context: . file: ./docker/Dockerfile.validator push: true - tags: io.pankaj/validator-backend:${{ github.sha}} + tags: 100xdevs/dpin-uptime-validator:${{ github.sha}} diff --git a/apps/api/package.json b/apps/api/package.json index 3f14872..bc5f02b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -18,6 +18,7 @@ "@types/jsonwebtoken": "^9.0.9", "cors": "^2.8.5", "db": "*", + "common": "*", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "jwt": "^0.2.0" diff --git a/apps/hub/index.ts b/apps/hub/index.ts index b0a782a..8647366 100644 --- a/apps/hub/index.ts +++ b/apps/hub/index.ts @@ -16,7 +16,7 @@ const COST_PER_VALIDATION = 100; // in lamports Bun.serve({ fetch(req, server) { - if (server.upgrade(req)) { + if (server.upgrade(req)) { return; } return new Response("Upgrade failed", { status: 500 }); @@ -146,61 +146,93 @@ async function verifyMessage( return result; } -setInterval(async () => { - const websitesToMonitor = await prismaClient.website.findMany({ - where: { - disabled: false, +const BATCH_SIZE = 100; +const WEBSITES_PER_VALIDATOR = 3; +let lastProcessedId: string | null = null; + +async function distributeWebsites() { + // Skip distribution if no validators are available + if (availableValidators.length === 0) return; + + + // Fetch just one batch of websites + const websiteBatch = await prismaClient.website.findMany({ + where: { disabled: false , + ...(lastProcessedId ? {id: {gt: lastProcessedId } } : {} ) }, + take: BATCH_SIZE, + orderBy: { + id: 'asc' + } }); - for (const website of websitesToMonitor) { - availableValidators.forEach((validator) => { - const callbackId = randomUUIDv7(); - console.log( - `Sending validate to ${validator.validatorId} ${website.url}`, - ); - validator.socket.send( - JSON.stringify({ - type: "validate", - data: { - url: website.url, - callbackId, - }, - }), - ); + if(websiteBatch.length === 0 ){ + lastProcessedId = null; + return; + } - CALLBACKS[callbackId] = async (data: IncomingMessage) => { - if (data.type === "validate") { - const { validatorId, status, latency, signedMessage } = data.data; - const verified = await verifyMessage( - `Replying to ${callbackId}`, - validator.publicKey, - signedMessage, - ); - if (!verified) { - return; - } - - await prismaClient.$transaction(async (tx) => { - await tx.websiteTick.create({ - data: { - websiteId: website.id, - validatorId, - status, - latency, - createdAt: new Date(), - }, - }); - - await tx.validator.update({ - where: { id: validatorId }, - data: { - pendingPayouts: { increment: COST_PER_VALIDATION }, - }, - }); - }); + // Distribute websites round-robin style to validators + let validatorIndex = 0; + let websiteCount = 0; + + for (const website of websiteBatch) { + // If this validator has enough websites, move to next validator + if (websiteCount >= WEBSITES_PER_VALIDATOR) { + validatorIndex = (validatorIndex + 1) % availableValidators.length; + websiteCount = 0; + } + + const validator = availableValidators[validatorIndex]; + + const callbackId = randomUUIDv7(); + console.log( + `Sending validate to ${validator.validatorId} ${website.url}`, + ); + validator.socket.send( + JSON.stringify({ + type: "validate", + data: { + url: website.url, + callbackId, + }, + }), + ); + + CALLBACKS[callbackId] = async (data: IncomingMessage) => { + if (data.type === "validate") { + const { validatorId, status, latency, signedMessage } = data.data; + const verified = await verifyMessage( + `Replying to ${callbackId}`, + validator.publicKey, + signedMessage, + ); + if (!verified) { + return; } - }; - }); + + await prismaClient.$transaction(async (tx) => { + await tx.websiteTick.create({ + data: { + websiteId: website.id, + validatorId, + status, + latency, + createdAt: new Date(), + }, + }); + + await tx.validator.update({ + where: { id: validatorId }, + data: { + pendingPayouts: { increment: COST_PER_VALIDATION }, + }, + }); + }); + } + }; + + websiteCount++; } -}, 60 * 1000); +} + +setInterval(distributeWebsites, 60 * 10000); diff --git a/apps/validator/index.ts b/apps/validator/index.ts index 1160f2a..101bc7f 100644 --- a/apps/validator/index.ts +++ b/apps/validator/index.ts @@ -15,39 +15,14 @@ const CALLBACKS: { let validatorId: string | null = null; -async function main() { - // const keypair = Keypair.fromSecretKey( - // Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!)) - // ); - - const keypair = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!)); +let ws: WebSocket | null = null; - interface IpInfo { - ip: string; - city?: string; - region?: string; - country?: string; - loc?: string; // Latitude and longitude ("37.7749,-122.4194") - } - async function getIpAndLocation(): Promise { - try { - const response = await fetch("https://ipinfo.io/json"); - const data = await response.json(); - return data as IpInfo; - } catch (error) { - const response = await fetch("http://ip-api.com/json/"); - const data = await response.json(); - return { - ip: data.query, - city: data.city, - region: data.regionName, - country: data.country, - loc: `${data.lat},${data.lon}`, - }; - } +async function connectToHub(keypair: Keypair) { + if (ws?.readyState === WebSocket.OPEN) { + return; // Already connected } - const ws = new WebSocket("ws://localhost:8081"); + ws = new WebSocket("ws://localhost:8081"); ws.onmessage = async (event) => { const data: OutgoingMessage = JSON.parse(event.data); @@ -55,7 +30,7 @@ async function main() { CALLBACKS[data.data.callbackId]?.(data.data); delete CALLBACKS[data.data.callbackId]; } else if (data.type === "validate") { - await validateHandler(ws, data.data, keypair); + await validateHandler(ws!, data.data, keypair); } }; @@ -70,20 +45,39 @@ async function main() { keypair, ); - ws.send( + ws!.send( JSON.stringify({ type: "signup", data: { callbackId, ip: ipInfo.ip, publicKey: keypair.publicKey, + // loc: ipInfo.loc, signedMessage, }, }), ); }; + + ws.onclose = () => { + setTimeout(() => connectToHub(keypair), 5000); + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + ws?.close(); + }; } +async function main() { + const keypair = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!)); + await connectToHub(keypair); +} + +main(); + + + async function validateHandler( ws: WebSocket, { url, callbackId, websiteId }: ValidateOutgoingMessage, @@ -139,6 +133,28 @@ async function signMessage(message: string, keypair: Keypair) { return JSON.stringify(Array.from(signature)); } -main(); +interface IpInfo { + ip: string; + city?: string; + region?: string; + country?: string; + loc?: string; // Latitude and longitude ("37.7749,-122.4194") +} -setInterval(async () => {}, 1000); +async function getIpAndLocation(): Promise { + try { + const response = await fetch("https://ipinfo.io/json"); + const data = await response.json(); + return data as IpInfo; + } catch (error) { + const response = await fetch("http://ip-api.com/json/"); + const data = await response.json(); + return { + ip: data.query, + city: data.city, + region: data.regionName, + country: data.country, + loc: `${data.lat},${data.lon}`, + }; + } +} diff --git a/bun.lock b/bun.lock index 7a9df34..960dd93 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jsonwebtoken": "^9.0.9", + "common": "*", "cors": "^2.8.5", "db": "*", "express": "^4.21.2", @@ -503,7 +504,7 @@ "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], - "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], + "@types/express": ["@types/express@5.0.1", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ=="], "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="], @@ -1735,8 +1736,6 @@ "log-symbols/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "metrics/@types/express": ["@types/express@5.0.1", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "node-plop/inquirer": ["inquirer@7.3.3", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.19", "mute-stream": "0.0.8", "run-async": "^2.4.0", "rxjs": "^6.6.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" } }, "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA=="], diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api index 39da083..7373dc3 100644 --- a/docker/Dockerfile.api +++ b/docker/Dockerfile.api @@ -1,17 +1,23 @@ -FROM oven/bun:latest +FROM oven/bun:1 -WORKDIR /app +WORKDIR /usr/src/app +COPY ./packages ./packages + +COPY ./bun.lock ./bun.lock +COPY ./package.json ./package.json + +COPY ./package*.json ./package*.json + +COPY turbo.json turbo.json + +RUN bun install COPY ./apps/api ./apps/api -COPY ./packages/ ./packages -COPY package* . -COPY turbo.json . -COPY bun.lock . -RUN bun install +RUN bun db:generate + -RUN bun run db:generate EXPOSE 8080 -CMD ["bun", "run", "api"] +CMD [ "bun", "run", "api" ] \ No newline at end of file diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend index b492edc..3b70327 100644 --- a/docker/Dockerfile.frontend +++ b/docker/Dockerfile.frontend @@ -1,15 +1,28 @@ -FROM oven/bun:latest +FROM oven/bun:1 +ARG DATABASE_URL -WORKDIR /app +WORKDIR /usr/src/app -COPY ./apps/frontend/ ./apps/frontend -COPY ./packages/ ./packages -COPY package* . -COPY turbo.json . -COPY bun.lock . +COPY ./packages ./packages + +COPY ./bun.lock ./bun.lock +COPY ./package.json ./package.json + +COPY ./package*.json ./package*.json + +COPY turbo.json turbo.json + + +RUN bun install + +COPY ./apps/frontend ./apps/frontend + +RUN echo "DATABASE_URL: ${DATABASE_URL}" + +RUN DATABASE_URL=${DATABASE_URL} bun db:generate +RUN DATABASE_URL=${DATABASE_URL} bun run build -RUN bun install EXPOSE 3000 -CMD ["bun", "run", "frontend"] +CMD [ "bun", "run", "frontend:prod" ] \ No newline at end of file diff --git a/docker/Dockerfile.hub b/docker/Dockerfile.hub index eaa0ccc..f3b5400 100644 --- a/docker/Dockerfile.hub +++ b/docker/Dockerfile.hub @@ -1,17 +1,24 @@ -FROM oven/bun:latest +FROM oven/bun:1 -WORKDIR /app +WORKDIR /usr/src/app +COPY ./packages ./packages + +COPY ./bun.lock ./bun.lock +COPY ./package.json ./package.json + +COPY ./package*.json ./package*.json + +COPY turbo.json turbo.json + +RUN bun install COPY ./apps/hub ./apps/hub -COPY ./packages/ ./packages -COPY package* . -COPY turbo.json . -COPY bun.lock . -RUN bun install +RUN bun db:generate + + -RUN bun run db:generate EXPOSE 8081 -CMD ["bun", "run", "hub"] +CMD [ "bun", "run", "hub" ] \ No newline at end of file diff --git a/docker/Dockerfile.validator b/docker/Dockerfile.validator index 3a08b94..dac4c70 100644 --- a/docker/Dockerfile.validator +++ b/docker/Dockerfile.validator @@ -1,13 +1,21 @@ -FROM oven/bun:latest +FROM oven/bun:1 -WORKDIR /app +WORKDIR /usr/src/app +COPY ./packages ./packages + +COPY ./bun.lock ./bun.lock +COPY ./package.json ./package.json + +COPY ./package*.json ./package*.json + +COPY turbo.json turbo.json + + +RUN bun install COPY ./apps/validator ./apps/validator -COPY ./packages/ ./packages -COPY package* . -COPY turbo.json . -COPY bun.lock . +RUN bun db:generate + -RUN bun install -CMD ["bun", "run", "validator"] +CMD [ "bun", "run", "validator" ] \ No newline at end of file diff --git a/packages/common/index.ts b/packages/common/index.ts index a2c0291..7b8f5f8 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -3,6 +3,7 @@ export interface SignupIncomingMessage { publicKey: string; signedMessage: string; callbackId: string; + // loc: string; } export interface ValidateIncomingMessage { @@ -33,7 +34,8 @@ export type IncomingMessage = | { type: "validate"; data: ValidateIncomingMessage; - }; + } + export type OutgoingMessage = | { diff --git a/packages/typescript-config/base.json b/packages/typescript-config/base.json new file mode 100644 index 0000000..ce564e8 --- /dev/null +++ b/packages/typescript-config/base.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": ["es2022", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + } + } \ No newline at end of file diff --git a/packages/typescript-config/nextjs.json b/packages/typescript-config/nextjs.json new file mode 100644 index 0000000..3ca488b --- /dev/null +++ b/packages/typescript-config/nextjs.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "plugins": [{ "name": "next" }], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "jsx": "preserve", + "noEmit": true + } + } \ No newline at end of file diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json new file mode 100644 index 0000000..7f8d139 --- /dev/null +++ b/packages/typescript-config/package.json @@ -0,0 +1,9 @@ +{ + "name": "@repo/typescript-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } + } \ No newline at end of file diff --git a/packages/typescript-config/react-library.json b/packages/typescript-config/react-library.json new file mode 100644 index 0000000..599cb70 --- /dev/null +++ b/packages/typescript-config/react-library.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx" + } + } \ No newline at end of file From 8e28aa06f25fff1077c8ac4d4afa7d4c6807418e Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Tue, 25 Mar 2025 21:33:51 +0545 Subject: [PATCH 5/7] final working docker build and pushing to docker hub --- apps/frontend/app/page.tsx | 10 ++++++---- bun.lock | 16 ++++++++++++++-- docker/Dockerfile.frontend | 8 ++++---- package.json | 1 + .../20250314012859_added_disabled/migration.sql | 2 -- .../20250315042613_added_payout/migration.sql | 2 -- .../20250315042714_fix_payout/migration.sql | 11 ----------- .../migration.sql | 2 ++ 8 files changed, 27 insertions(+), 25 deletions(-) delete mode 100644 packages/db/prisma/migrations/20250314012859_added_disabled/migration.sql delete mode 100644 packages/db/prisma/migrations/20250315042613_added_payout/migration.sql delete mode 100644 packages/db/prisma/migrations/20250315042714_fix_payout/migration.sql rename packages/db/prisma/migrations/{20250314011543_init => 20250325141250_init}/migration.sql (92%) diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/page.tsx index 0032ee7..e475f37 100644 --- a/apps/frontend/app/page.tsx +++ b/apps/frontend/app/page.tsx @@ -1,10 +1,10 @@ "use client" import React, { useEffect, useState } from 'react'; -import { Activity, Bell, Clock, Server, ArrowRight, Check, Moon, Sun } from 'lucide-react'; +import { Activity, Bell, Clock, Server, ArrowRight, Check} from 'lucide-react'; import { useRouter } from 'next/navigation'; function App() { - const [darkMode, setDarkMode] = useState(false); + const [darkMode] = useState(false); const router = useRouter(); useEffect(() => { @@ -90,6 +90,7 @@ function App() { "5 team members", "24h data retention" ]} + featured={false} /> @@ -167,7 +169,7 @@ function App() { ); } -function FeatureCard({ icon, title, description }) { +function FeatureCard({ icon, title, description }: {icon: React.ReactNode, title: string, description: string}) { return (
{icon}
@@ -177,7 +179,7 @@ function FeatureCard({ icon, title, description }) { ); } -function PricingCard({ title, price, features, featured = false }) { +function PricingCard({ title, price, features, featured = false }: { title: string, price: string, features: string[] , featured: boolean}) { return (
Date: Sun, 30 Mar 2025 17:50:05 +0545 Subject: [PATCH 6/7] major update --- .github/workflows/deploy_api.yml | 14 ++ .github/workflows/deploy_frontend.yml | 18 ++- .github/workflows/deploy_hub.yml | 15 ++ .github/workflows/deploy_validator.yml | 4 + apps/api/index.ts | 189 ++++++++++++++++------- caddy_configs/Caddyfile.frontend-api.hub | 8 + caddy_configs/Caddyfile.hub | 3 + docker/Dockerfile.frontend | 3 +- 8 files changed, 191 insertions(+), 63 deletions(-) create mode 100644 caddy_configs/Caddyfile.frontend-api.hub create mode 100644 caddy_configs/Caddyfile.hub diff --git a/.github/workflows/deploy_api.yml b/.github/workflows/deploy_api.yml index e480c67..d056e41 100644 --- a/.github/workflows/deploy_api.yml +++ b/.github/workflows/deploy_api.yml @@ -27,3 +27,17 @@ jobs: push: true tags: 100xdevs/dpin-uptime-api:${{ github.sha}} + - name: Deploy to api ec2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.API_EC2_IP }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + #login to Docker Hub in case of private repo + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_TOKEN }} + #stop and remove existing api container + docker stop betteruptime-api || true + docker rm betteruptime-api || true + docker pull 100xdevs.com/dpin-uptime-api:${{ github.sha }} + docker run -d --name betteruptime-api -p 3000:3000 100xdevs/dpin-uptime-api:${{ github.sha }} diff --git a/.github/workflows/deploy_frontend.yml b/.github/workflows/deploy_frontend.yml index dccebc5..7e0dccd 100644 --- a/.github/workflows/deploy_frontend.yml +++ b/.github/workflows/deploy_frontend.yml @@ -27,4 +27,20 @@ push: true tags: 100xdevs/dpin-uptime-frontend:${{ github.sha }} build-args: | - DATABASE_URL=${{ secrets.DATABASE_URL }} \ No newline at end of file + DATABASE_URL=${{ secrets.DATABASE_URL }} + + - name: Deploy to Frontend ec2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.FRONTEND_EC2_IP }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + #login to Docker Hub in case of private repo + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_TOKEN }} + #stop and remove existing frontend container + docker stop betteruptime-frontend || true + docker rm betteruptime-frontend || true + docker pull 100xdevs.com/dpin-uptime-frontend:${{ github.sha }} + docker run -d --name betteruptime-frontend -p 3000:3000 100xdevs/dpin-uptime-frontend:${{ github.sha }} + \ No newline at end of file diff --git a/.github/workflows/deploy_hub.yml b/.github/workflows/deploy_hub.yml index 0ce4a4a..3614992 100644 --- a/.github/workflows/deploy_hub.yml +++ b/.github/workflows/deploy_hub.yml @@ -22,3 +22,18 @@ jobs: file: ./docker/Dockerfile.hub push: true tags: 100xdevs/dpin-uptime-hub:${{ github.sha}} + + - name: Deploy to Frontend ec2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HUB_EC2_IP }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + #login to Docker Hub in case of private repo + docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_TOKEN }} + #stop and remove existing hub container + docker stop betteruptime-hub || true + docker rm betteruptime-hub || true + docker pull 100xdevs.com/dpin-uptime-hub:${{ github.sha }} + docker run -d --name betteruptime-hub -p 3000:3000 100xdevs/dpin-uptime-hub:${{ github.sha }} diff --git a/.github/workflows/deploy_validator.yml b/.github/workflows/deploy_validator.yml index 6218577..7309236 100644 --- a/.github/workflows/deploy_validator.yml +++ b/.github/workflows/deploy_validator.yml @@ -25,3 +25,7 @@ jobs: file: ./docker/Dockerfile.validator push: true tags: 100xdevs/dpin-uptime-validator:${{ github.sha}} + + + #no need to push to ec2 as every validator will run accouring to them + # we will just publish docker imageUrl diff --git a/apps/api/index.ts b/apps/api/index.ts index 571a869..e041e85 100644 --- a/apps/api/index.ts +++ b/apps/api/index.ts @@ -13,77 +13,146 @@ app.use(metricsMiddleware); app.use(express.json()); app.post("/api/v1/website", authMiddleware, async (req, res) => { - const userId = req.userId!; - const { url } = req.body; - - const data = await prismaClient.website.create({ - data: { - userId, - url, - }, - }); - - res.json({ - id: data.id, - }); + try { + const userId = req.userId!; + const { url } = req.body; + + const data = await prismaClient.website.create({ + data: { + userId, + url, + }, + }); + + res.json({ + id: data.id, + }); + } catch (error) { + console.error("Error creating website:", error); + res.status(500).json({ error: "Failed to create website" }); + } }); app.get("/api/v1/website/status", authMiddleware, async (req, res) => { - const websiteId = req.query.websiteId! as unknown as string; - const userId = req.userId; - - const data = await prismaClient.website.findFirst({ - where: { - id: websiteId, - userId, - disabled: false, - }, - include: { - ticks: true, - }, - }); - - res.json(data); + try { + const websiteId = req.query.websiteId! as unknown as string; + const userId = req.userId; + + const data = await prismaClient.website.findFirst({ + where: { + id: websiteId, + userId, + disabled: false, + }, + include: { + ticks: true, + }, + }); + + res.json(data); + } catch (error) { + console.error("Error fetching website status:", error); + res.status(500).json({ error: "Failed to fetch website status" }); + } }); app.get("/api/v1/websites", authMiddleware, async (req, res) => { - const userId = req.userId!; - console.log("userId :", userId); - - const websites = await prismaClient.website.findMany({ - where: { - userId, - disabled: false, - }, - include: { - ticks: true, - }, - }); - - res.json({ - websites, - }); + try { + const userId = req.userId!; + console.log("userId :", userId); + + const websites = await prismaClient.website.findMany({ + where: { + userId, + disabled: false, + }, + include: { + ticks: true, + }, + }); + + res.json({ + websites, + }); + } catch (error) { + console.error("Error fetching websites:", error); + res.status(500).json({ error: "Failed to fetch websites" }); + } }); app.delete("/api/v1/website/", authMiddleware, async (req, res) => { - const websiteId = req.body.websiteId; - const userId = req.userId!; - - await prismaClient.website.update({ - where: { - id: websiteId, - userId, - }, - data: { - disabled: true, - }, - }); - - res.json({ - message: "Deleted website successfully", - }); + try { + const websiteId = req.body.websiteId; + const userId = req.userId!; + + await prismaClient.website.update({ + where: { + id: websiteId, + userId, + }, + data: { + disabled: true, + }, + }); + + res.json({ + message: "Deleted website successfully", + }); + } catch (error) { + console.error("Error deleting website:", error); + res.status(500).json({ error: "Failed to delete website" }); + } +}); + +//getting total amount need to pay at this moment +app.get("/api/v1/payout", authMiddleware, async (req, res) => { + try { + const totalPending = await prismaClient.validator.aggregate({ + _sum: { + pendingPayouts: true + } + }); + + res.json({ + totalPendingAmount: totalPending._sum.pendingPayouts || 0 + }); + } catch (error) { + console.error("Error fetching total pending payouts:", error); + res.status(500).json({ error: "Failed to fetch total pending payouts" }); + } +}); + +//getting how much needed to pay to any validator +app.get("/api/v1/payout/:validatorId", authMiddleware, async(req, res) => { + try { + const validatorId = req.params.validatorId; + + const validator = await prismaClient.validator.findUnique({ + where: { id: validatorId }, + select: { + id: true, + publicKey: true, + pendingPayouts: true + } + }); + + if (!validator) { + res.status(404).json({ error: "Validator not found" }); + return; + } + + res.json({ + validatorId: validator.id, + publicKey: validator.publicKey, + pendingAmount: validator.pendingPayouts + }); + } catch (error) { + console.error("Error fetching validator payout:", error); + res.status(500).json({ error: "Failed to fetch validator payout details" }); + } }); +//paying to any validator app.post("/api/v1/payout/:validatorId", async (req, res) => { try { const {amount} = req.body; diff --git a/caddy_configs/Caddyfile.frontend-api.hub b/caddy_configs/Caddyfile.frontend-api.hub new file mode 100644 index 0000000..ec7633a --- /dev/null +++ b/caddy_configs/Caddyfile.frontend-api.hub @@ -0,0 +1,8 @@ +betteruptime.10kdevs.com { + reverse_proxy localhost:3000 +} + +betteruptime-api.10kdevs.com { + reverse_proxy localhost:8000 +} + diff --git a/caddy_configs/Caddyfile.hub b/caddy_configs/Caddyfile.hub new file mode 100644 index 0000000..8c2efc9 --- /dev/null +++ b/caddy_configs/Caddyfile.hub @@ -0,0 +1,3 @@ +betteruptime-hub.10kdevs.com { + reverse_proxy localhost:8081 +} \ No newline at end of file diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend index 96dc75b..823271f 100644 --- a/docker/Dockerfile.frontend +++ b/docker/Dockerfile.frontend @@ -1,4 +1,4 @@ -FROM oven/bun:1 +FROM oven/bun:1 ARG DATABASE_URL @@ -22,7 +22,6 @@ RUN DATABASE_URL=$DATABASE_URL bun run db:generate RUN DATABASE_URL=$DATABASE_URL bun run frontend:build - EXPOSE 3000 CMD [ "bun", "run", "frontend:prod" ] \ No newline at end of file From 51c73edbf21119428b8d77429d1fcdf566891858 Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Mon, 31 Mar 2025 02:12:07 +0545 Subject: [PATCH 7/7] added monitoring, alter validator schema --- apps/frontend/app/admin/page.tsx | 177 +++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 apps/frontend/app/admin/page.tsx diff --git a/apps/frontend/app/admin/page.tsx b/apps/frontend/app/admin/page.tsx new file mode 100644 index 0000000..9f71523 --- /dev/null +++ b/apps/frontend/app/admin/page.tsx @@ -0,0 +1,177 @@ +"use client"; +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { API_BACKEND_URL } from '@/config'; +import { useAuth } from '@clerk/nextjs'; + +interface Validator { + id: string; + publicKey: string; + pendingPayouts: number; + isOnline: boolean; +} + +function AdminDashboard() { + const [validators, setValidators] = useState([]); + const [selectedValidator, setSelectedValidator] = useState(null); + const [payoutAmount, setPayoutAmount] = useState(0); + const { getToken } = useAuth(); + + // Fetch validators + const fetchValidators = async () => { + try { + const response = await axios.get(`${API_BACKEND_URL}/api/v1/validators`); + setValidators(response.data.validators); + } catch (error) { + console.error('Error fetching validators:', error); + } + }; + + // Process payout + const handlePayout = async (validatorId: string) => { + try { + const token = await getToken(); + const response = await axios.post( + `${API_BACKEND_URL}/api/v1/payout/${validatorId}`, + { amount: payoutAmount }, + { + headers: { Authorization: token }, + } + ); + + // Refresh validators after successful payout + fetchValidators(); + + // Reset form + setPayoutAmount(0); + setSelectedValidator(null); + + // You might want to handle the transaction signature here + console.log('Payout successful:', response.data); + } catch (error) { + console.error('Error processing payout:', error); + } + }; + + useEffect(() => { + fetchValidators(); + }, []); + + return ( +
+
+

+ Validator Management +

+ + {/* Validators Table */} +
+ + + + + + + + + + + {validators.map((validator) => ( + + + + + + + ))} + +
+ Validator + + Status + + Pending Payouts + + Actions +
+
+ {validator.publicKey.slice(0, 8)}...{validator.publicKey.slice(-8)} +
+
+ + {validator.isOnline ? 'Online' : 'Offline'} + + + {validator.pendingPayouts} SOL + + {selectedValidator === validator.id ? ( +
+ setPayoutAmount(Number(e.target.value))} + className="w-24 px-2 py-1 border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white" + min="0" + step="0.1" + /> + + +
+ ) : ( + + )} +
+
+ + {/* Summary Statistics */} +
+
+

+ Total Validators +

+

+ {validators.length} +

+
+
+

+ Active Validators +

+

+ {validators.filter(v => v.isOnline).length} +

+
+
+

+ Total Pending Payouts +

+

+ {validators.reduce((sum, v) => sum + v.pendingPayouts, 0)} SOL +

+
+
+
+
+ ); +} + +export default AdminDashboard; \ No newline at end of file