diff --git a/apps/api/index.ts b/apps/api/index.ts index e791ed7..d38e6f3 100644 --- a/apps/api/index.ts +++ b/apps/api/index.ts @@ -1,43 +1,50 @@ -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"); +import { + Transaction, + SystemProgram, + Connection, + PublicKey, + Keypair, + sendAndConfirmTransaction, +} from "@solana/web3.js"; + +const connection = new Connection("https://api.devnet.solana.com"); +const publicKey = new PublicKey(process.env.PUBLIC_KEY!); const app = express(); 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, + const data = await prismaClient.website.create({ + data: { + userId, url } }) - res.json({ + 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 websiteId = req.query.websiteId! as unknown as string; + const userId = req.userId; - const data = await prismaClient.website.findFirst({ - where: { - id: websiteId, - userId, + const data = await prismaClient.website.findFirst({ + where: { + id: websiteId, + userId, disabled: false - }, - include: { + }, + include: { ticks: true } }) @@ -47,44 +54,102 @@ app.get("/api/v1/website/status", authMiddleware, async (req, res) => { }) app.get("/api/v1/websites", authMiddleware, async (req, res) => { - const userId = req.userId!; + const userId = req.userId!; - const websites = await prismaClient.website.findMany({ - where: { - userId, + const websites = await prismaClient.website.findMany({ + where: { + userId, disabled: false - }, - include: { + }, + include: { ticks: true } }) - res.json({ + res.json({ websites }) }) app.delete("/api/v1/website/", authMiddleware, async (req, res) => { - const websiteId = req.body.websiteId; - const userId = req.userId!; + const websiteId = req.body.websiteId; + const userId = req.userId!; - await prismaClient.website.update({ - where: { - id: websiteId, + await prismaClient.website.update({ + where: { + id: websiteId, userId - }, - data: { + }, + data: { disabled: true } }) - res.json({ + res.json({ message: "Deleted website successfully" }) }) app.post("/api/v1/payout/:validatorId", async (req, res) => { - -}) + const validatorId = req.params.validatorId! as unknown as string; + console.log("Payout request received", validatorId); + + const validator = await prismaClient.validator.findFirst({ + where: { + id: validatorId, + }, + }); + + if (!validator) { + return res.status(400).json({ + message: "Validator not found", + }); + } + + if (validator.isAmountClaimed) { + return res.status(400).json({ + message: "Amount already claimed", + }); + } + + // claim the amount + await prismaClient.validator.update({ + where: { + id: validatorId, + }, + data: { + isAmountClaimed: true, + }, + }); + // transfer the SOL to the validator + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: new PublicKey(validator.publicKey), + lamports: validator.pendingPayouts, + }) + ); + + const signer = Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!)) + ); + + const signature = await sendAndConfirmTransaction(connection, transaction, [ + signer, + ]); + + await prismaClient.solanaTransaction.create({ + data: { + validatorId, + amount: validator.pendingPayouts, + createdAt: new Date(), + signature, + }, + }); + + res.json({ + message: "Payout successful", + }); +}); app.listen(8080); diff --git a/apps/poller/.gitignore b/apps/poller/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/apps/poller/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/apps/poller/README.md b/apps/poller/README.md new file mode 100644 index 0000000..7d9691f --- /dev/null +++ b/apps/poller/README.md @@ -0,0 +1,15 @@ +# poller + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.5. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/apps/poller/index.ts b/apps/poller/index.ts new file mode 100644 index 0000000..7f96229 --- /dev/null +++ b/apps/poller/index.ts @@ -0,0 +1,53 @@ +import { prismaClient } from "db/client"; +import { Connection } from "@solana/web3.js"; + +const connection = new Connection("https://api.devnet.solana.com"); + +async function resolvePendingPayouts() { + console.log("Resolving pending payouts"); + // get top 10 transactions + const transactions = await prismaClient.solanaTransaction.findMany({ + orderBy: { + createdAt: "asc", + }, + take: 10, + }); + + const txStatuses = await connection.getSignatureStatuses( + transactions.map((t) => t.signature) + ); + + for (let index = 0; index < txStatuses.value.length; index++) { + const txStatus = txStatuses.value[index]; + + if (txStatus?.confirmationStatus) { + console.log(`Transaction Status:${txStatus.confirmationStatus}`); + + if (txStatus.confirmationStatus === "finalized") { + await prismaClient.$transaction(async (tx) => { + // delete Transaction + await tx.solanaTransaction.delete({ + where: { + id: transactions[index]!.id, + }, + }); + // make `isAmountClaimed` = false and pendingPayouts = 0 + await tx.validator.update({ + where: { + id: transactions[index]!.validatorId, + }, + data: { + isAmountClaimed: false, + pendingPayouts: 0, + }, + }); + }); + console.log( + `Transactions of ${transactions[index]!.validatorId} resolved` + ); + } + } + } +} + +setInterval(resolvePendingPayouts, 1000 * 60); diff --git a/apps/poller/package.json b/apps/poller/package.json new file mode 100644 index 0000000..f0c9319 --- /dev/null +++ b/apps/poller/package.json @@ -0,0 +1,13 @@ +{ + "name": "poller", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest", + "db": "*" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/apps/poller/tsconfig.json b/apps/poller/tsconfig.json new file mode 100644 index 0000000..ab0f0b0 --- /dev/null +++ b/apps/poller/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["esnext"], + "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, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/db/prisma/migrations/20250316173459_add_solana_transaction_table/migration.sql b/packages/db/prisma/migrations/20250316173459_add_solana_transaction_table/migration.sql new file mode 100644 index 0000000..9b4367c --- /dev/null +++ b/packages/db/prisma/migrations/20250316173459_add_solana_transaction_table/migration.sql @@ -0,0 +1,16 @@ +-- AlterTable +ALTER TABLE "Validator" ADD COLUMN "isAmountClaimed" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "SolanaTransaction" ( + "id" TEXT NOT NULL, + "validatorId" TEXT NOT NULL, + "amount" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL, + "signature" TEXT NOT NULL, + + CONSTRAINT "SolanaTransaction_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "SolanaTransaction" ADD CONSTRAINT "SolanaTransaction_validatorId_fkey" FOREIGN KEY ("validatorId") REFERENCES "Validator"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index a3b9521..c18917a 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -27,26 +27,38 @@ model Website { } model Validator { - id String @id @default(uuid()) - publicKey String - location String - ip String - pendingPayouts Int @default(0) - ticks WebsiteTick[] + id String @id @default(uuid()) + publicKey String + location String + ip String + pendingPayouts Int @default(0) + ticks WebsiteTick[] + isAmountClaimed Boolean @default(false) + solanaTransactions SolanaTransaction[] } model WebsiteTick { - id String @id @default(uuid()) - websiteId String - validatorId String - createdAt DateTime - status WebsiteStatus - latency Float - website Website @relation(fields: [websiteId], references: [id]) - validator Validator @relation(fields: [validatorId], references: [id]) + id String @id @default(uuid()) + websiteId String + validatorId String + createdAt DateTime + status WebsiteStatus + latency Float + website Website @relation(fields: [websiteId], references: [id]) + validator Validator @relation(fields: [validatorId], references: [id]) +} + + +model SolanaTransaction { + id String @id @default(uuid()) + validatorId String + amount Int + createdAt DateTime + validator Validator @relation(fields: [validatorId], references: [id]) + signature String } enum WebsiteStatus { Good Bad -} \ No newline at end of file +}