diff --git a/apps/api/index.ts b/apps/api/index.ts index e791ed7..4836853 100644 --- a/apps/api/index.ts +++ b/apps/api/index.ts @@ -2,10 +2,10 @@ 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 { SystemProgram, Connection, PublicKey, VersionedTransaction, TransactionMessage, Keypair, clusterApiUrl } from "@solana/web3.js"; +import { redis } from "cache/client"; - -const connection = new Connection("https://api.mainnet-beta.solana.com"); +const connection = new Connection(clusterApiUrl("devnet")); const app = express(); app.use(cors()); @@ -84,7 +84,184 @@ app.delete("/api/v1/website/", authMiddleware, async (req, res) => { }) app.post("/api/v1/payout/:validatorId", async (req, res) => { - + + const validatorId = req.params.validatorId; + + const validator = await prismaClient.validator.findFirst({ + where:{ + id: validatorId, + } + }); + + if(!validator){ + res.json({ + message: "Validator not found" + }) + return; + } + + if(validator.payoutLocked){ + res.json({ + message: "Payout is locked, Please wait for some time before attempting to payout again" + }) + return; + } + + await redis.lpush("payouts", validatorId); + + res.json({ + message: "Payout queued successfully" + }) + + return; + }) + app.listen(8080); + + +async function main(){ + // Avoid Rate Limiting + setInterval(async ()=>{ + await sendPayouts(); + await verifyTxs(); + },3000) +} + +main(); + +async function sendPayouts(){ + + const validatorId = await redis.rpop("payouts"); + + try { + + if(validatorId){ + + const validator = await prismaClient.validator.findFirst({ + where:{ + id: validatorId, + } + }); + + if(validator && !validator.payoutLocked){ + + await prismaClient.validator.update({ + where: { + id: validatorId + }, + data: { + payoutLocked: true + } + }); + + // create tx and push it to txVerification queue + + const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.PRIVATE_KEY!))); + + const lamports = validator.pendingPayouts; + + const transferIx = SystemProgram.transfer({ + fromPubkey: keypair.publicKey, + toPubkey: new PublicKey(validator.publicKey), + lamports, + }); + + const {blockhash} = await connection.getLatestBlockhash(); + + const messageV0 = new TransactionMessage({ + payerKey:keypair.publicKey, + recentBlockhash: blockhash, + instructions : [transferIx], + }).compileToV0Message(); + + const versionedTx = new VersionedTransaction(messageV0); + versionedTx.sign([keypair]); + + console.log(`Sending Payout to validator ${validator.publicKey} with ID ${validatorId} of Amount: ${validator.pendingPayouts}`); + + const tx = await connection.sendTransaction(versionedTx); + const payload = {validatorId, tx, payoutAmount: validator.pendingPayouts} + await redis.lpush("txVerification", JSON.stringify(payload)); + } + } + + } catch (error) { + console.log("Error while sending payouts"); + console.error(error); + } + + +} + +async function verifyTxs(){ + const payload = await redis.rpop("txVerification"); + + if(payload){ + const {validatorId, tx, payoutAmount} : {validatorId: string, tx: string, payoutAmount: number} = JSON.parse(payload); + + // verify the tx + const result = await connection.getParsedTransaction(tx, {maxSupportedTransactionVersion:0, commitment:"confirmed"}); + + console.log("Transaction Result for ", tx); + console.dir(result, {depth:null}); + + // The tx may not be updated immmediately + if(!result){ + await redis.lpush("txVerification", JSON.stringify({validatorId, tx, payoutAmount})); + return; + } + + const preBalances = result?.meta?.preBalances; + const postBalances = result?.meta?.postBalances; + + // verify this based on the tx + let isPaidSuccessfully = false; + + const fee = result.meta?.fee; + + if(preBalances && postBalances && fee && postBalances[1] - preBalances[1] === payoutAmount && preBalances[0] - postBalances[0] === (fee + payoutAmount)){ + isPaidSuccessfully = true; + console.log("Transaction verified successfully for ", tx); + } + + if(!isPaidSuccessfully){ + return; + } + else{ + + await prismaClient.$transaction(async tx =>{ + + const validator = await tx.validator.findUnique({ + where:{ + id:validatorId, + } + }); + + // This will not happen , just to satisfy TS + if(!validator){ + console.log("Validator ID not found in DB", validator) + return; + } + + // When We set to 0, The amount after locked will be lost, so to handle that + const balanceToSet = validator.pendingPayouts - payoutAmount ; + + await tx.validator.update({ + where: { + id: validatorId + }, + data: { + payoutLocked: false, + pendingPayouts: balanceToSet, + } + }); + + }) + } + + } + + +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 1901ff9..4170725 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -15,6 +15,7 @@ "@types/jsonwebtoken": "^9.0.9", "cors": "^2.8.5", "db": "*", + "cache":"*", "express": "^4.21.2", "jsonwebtoken": "^9.0.2", "jwt": "^0.2.0" diff --git a/bun.lock b/bun.lock index 1b01488..d461578 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", + "cache": "*", "cors": "^2.8.5", "db": "*", "express": "^4.21.2", @@ -93,6 +94,18 @@ "typescript": "^5.0.0", }, }, + "packages/cache": { + "name": "cache", + "dependencies": { + "ioredis": "^5.6.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, "packages/common": { "name": "common", "devDependencies": { @@ -105,6 +118,7 @@ "packages/db": { "name": "db", "dependencies": { + "@prisma/client": "6.5.0", "prisma": "^6.5.0", }, "devDependencies": { @@ -289,6 +303,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], @@ -327,6 +343,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=="], @@ -649,6 +667,8 @@ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cache": ["cache@workspace:packages/cache"], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -683,6 +703,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -751,6 +773,8 @@ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -1003,6 +1027,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ioredis": ["ioredis@5.6.0", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg=="], + "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1155,10 +1181,14 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + "lodash.get": ["lodash.get@4.4.2", "", {}, "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], @@ -1351,6 +1381,10 @@ "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], @@ -1449,6 +1483,8 @@ "stable-hash": ["stable-hash@0.0.4", "", {}, "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "std-env": ["std-env@3.8.1", "", {}, "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA=="], diff --git a/packages/cache/.gitignore b/packages/cache/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/packages/cache/.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/packages/cache/README.md b/packages/cache/README.md new file mode 100644 index 0000000..7110092 --- /dev/null +++ b/packages/cache/README.md @@ -0,0 +1,15 @@ +# cache + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.4. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/cache/index.ts b/packages/cache/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/packages/cache/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 0000000..cfe526d --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,18 @@ +{ + "name": "cache", + "module": "index.ts", + "type": "module", + "private": true, + "exports": { + "./client": "./src/index.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "ioredis": "^5.6.0" + } +} \ No newline at end of file diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts new file mode 100644 index 0000000..0f394a8 --- /dev/null +++ b/packages/cache/src/index.ts @@ -0,0 +1,3 @@ +import Redis from "ioredis"; + +export const redis = new Redis() diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/packages/cache/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/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/migrations/20250317133211_add_payout_locked_cloumn_validator/migration.sql b/packages/db/prisma/migrations/20250317133211_add_payout_locked_cloumn_validator/migration.sql new file mode 100644 index 0000000..decdd8f --- /dev/null +++ b/packages/db/prisma/migrations/20250317133211_add_payout_locked_cloumn_validator/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Validator" ADD COLUMN "payoutLocked" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index a3b9521..079d50f 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -32,6 +32,7 @@ model Validator { location String ip String pendingPayouts Int @default(0) + payoutLocked Boolean @default(false) ticks WebsiteTick[] }