diff --git a/package-lock.json b/package-lock.json index 1e7704d..8c719c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "helmet": "^8.0.0", "js-sha256": "^0.11.0", "node-cache": "^5.1.2", + "node-cron": "^4.2.1", "react": ">=16" }, "devDependencies": { @@ -3242,6 +3243,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3744,6 +3758,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3833,13 +3861,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -3853,6 +3878,33 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -4503,13 +4555,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4653,16 +4707,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4681,6 +4740,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4857,12 +4929,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4916,10 +4988,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4928,11 +5000,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -6226,6 +6301,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mathjs": { "version": "9.5.2", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-9.5.2.tgz", @@ -6474,6 +6558,15 @@ "node": ">= 8.0.0" } }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", diff --git a/package.json b/package.json index d6061d0..1cbd0b2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "helmet": "^8.0.0", "js-sha256": "^0.11.0", "node-cache": "^5.1.2", + "node-cron": "^4.2.1", "react": ">=16" }, "devDependencies": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 20acf8e..b9164a5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,7 +41,7 @@ model RpcRequest { model FTToken { id String @id @default(uuid()) - account_id String + account_id String @unique totalCumulativeAmt Float fts Json timestamp DateTime @default(now()) diff --git a/src/all-token-balance-history.ts b/src/all-token-balance-history.ts index 6cab7e7..8ef3f87 100644 --- a/src/all-token-balance-history.ts +++ b/src/all-token-balance-history.ts @@ -290,30 +290,34 @@ export async function getAllTokenBalanceHistory( ); if (prev) { - await prisma.tokenBalanceHistory.update({ - where: { - account_id_token_id_period: { + prisma.tokenBalanceHistory + .update({ + where: { + account_id_token_id_period: { + account_id, + token_id, + period, + }, + }, + data: { + balance_history: finalHistory, + toBlock: currentBlock, + }, + }) + .catch((e) => console.error("DB write failed:", e.message)); + } else { + prisma.tokenBalanceHistory + .create({ + data: { account_id, token_id, period, + balance_history: finalHistory, + fromBlock: blockHeights[0], + toBlock: currentBlock, }, - }, - data: { - balance_history: finalHistory, - toBlock: currentBlock, - }, - }); - } else { - await prisma.tokenBalanceHistory.create({ - data: { - account_id, - token_id, - period, - balance_history: finalHistory, - fromBlock: blockHeights[0], - toBlock: currentBlock, - }, - }); + }) + .catch((e) => console.error("DB write failed:", e.message)); } return { period, data: finalHistory }; diff --git a/src/ft-tokens.ts b/src/ft-tokens.ts index f9cb413..9fa9032 100644 --- a/src/ft-tokens.ts +++ b/src/ft-tokens.ts @@ -88,26 +88,30 @@ export async function getFTTokens(account_id: string, cache: FTCache) { const nearblocksFts = nearblocksRes?.data?.inventory?.fts || []; const fastnearFts = fastnearRes?.data?.tokens || []; - const nearblocksMap = new Map((nearblocksFts as FtsToken[]).map((ft) => [ft.contract, ft])); - - const mergedFts = await Promise.all(fastnearFts.map(async (ft: any) => { - const meta = nearblocksMap.get(ft.contract_id) as FtsToken | undefined; - if (meta && meta.ft_meta) { - return { - contract: ft.contract_id, - amount: ft.balance, // use FastNear balance - ft_meta: meta.ft_meta, - } as FtsToken; - } else { - // Fetch metadata if not found in Nearblocks - const fetched = await fetchFtMeta(ft.contract_id); - if (fetched) { - fetched.amount = ft.balance; - return fetched; + const nearblocksMap = new Map( + (nearblocksFts as FtsToken[]).map((ft) => [ft.contract, ft]) + ); + + const mergedFts = (await Promise.all( + fastnearFts.map(async (ft: any) => { + const meta = nearblocksMap.get(ft.contract_id) as FtsToken | undefined; + if (meta && meta.ft_meta) { + return { + contract: ft.contract_id, + amount: ft.balance, // use FastNear balance + ft_meta: meta.ft_meta, + } as FtsToken; + } else { + // Fetch metadata if not found in Nearblocks + const fetched = await fetchFtMeta(ft.contract_id); + if (fetched) { + fetched.amount = ft.balance; + return fetched; + } + return null; } - return null; - } - })) as FtsToken[]; + }) + )) as FtsToken[]; const updatedFts = mergedFts.filter(Boolean) as FtsToken[]; @@ -142,11 +146,18 @@ export async function getFTTokens(account_id: string, cache: FTCache) { // Save to DB prisma.fTToken - .create({ - data: { + .upsert({ + where: { account_id }, + update: { + totalCumulativeAmt: parseFloat(total.toFixed(2)), + fts: finalFts as any, + timestamp: new Date(), + }, + create: { account_id, totalCumulativeAmt: parseFloat(total.toFixed(2)), fts: finalFts as any, + timestamp: new Date(), }, }) .catch((e) => console.error("DB write failed:", e.message)); @@ -163,4 +174,3 @@ export async function getFTTokens(account_id: string, cache: FTCache) { throw new Error("Failed to fetch FT tokens"); } } - diff --git a/src/near-price.ts b/src/near-price.ts index c6ded92..9b726d4 100644 --- a/src/near-price.ts +++ b/src/near-price.ts @@ -38,12 +38,22 @@ export async function getNearPrice( if (price) { console.log(`Fetched price from ${endpoint}: $${price}`); - await prisma.nearPrice.create({ - data: { - price, - source: endpoint, - }, - }); + prisma.nearPrice + .upsert({ + where: { id: "latest" }, + update: { + price, + source: endpoint, + timestamp: new Date(), + }, + create: { + id: "latest", + price, + source: endpoint, + timestamp: new Date(), + }, + }) + .catch((e) => console.error("DB write failed:", e.message)); cache.set(cacheKey, price, 50); // for 50 seconds return price; } diff --git a/src/server.ts b/src/server.ts index 62198da..d51eee4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -17,6 +17,7 @@ import prisma from "./prisma"; import { tokens } from "./constants/tokens"; import axios from "axios"; import treasuryRoutes from "./routes/metrics"; +import cron from "node-cron"; dotenv.config(); @@ -353,15 +354,14 @@ app.get("/api/validators", async (req: Request, res: Response) => { ); const validators = data?.map((item: any) => { const numerator = new Big(item.fees.numerator || 0); - const denominator = new Big(item.fees.denominator || 1); + const denominator = new Big(item.fees.denominator || 1); const feePercent = numerator.div(denominator).times(100); const isWholeNumber = feePercent.mod(1).eq(0); - + return { pool_id: item.account_id, fee: isWholeNumber ? feePercent.toFixed(0) : feePercent.toFixed(2), }; - }); cache.set(cacheKey, validators, 60 * 60 * 24); // 1 day return res.send(validators); @@ -371,24 +371,6 @@ app.get("/api/validators", async (req: Request, res: Response) => { } }); -app.delete("/api/rpc-request-db", async (req: Request, res: Response) => { - try { - // Delete all rows from RpcRequest table - await prisma.rpcRequest.deleteMany(); - - // Delete all rows from AccountBlockExistence table - await prisma.accountBlockExistence.deleteMany(); - - res.status(200).send({ - message: - "RpcRequest and AccountBlockExistence tables cleared successfully.", - }); - } catch (error) { - console.error("Error clearing tables:", error); - res.status(500).send({ error: "Failed to clear tables" }); - } -}); - app.get("/api/search-ft", async (req: Request, res: Response) => { try { const { query } = req.query; @@ -425,6 +407,33 @@ app.get("/headers", (req, res) => { res.json({ headers: req.headers }); }); +// Schedule a job to clear RpcRequest and AccountBlockExistence every day at 6:30 AM UTC +let cleanupJob: any = null; + +if (process.env.NODE_ENV !== "test") { + cleanupJob = cron.schedule( + "30 6 * * *", + async () => { + try { + const deletedRpc = await prisma.rpcRequest.deleteMany(); + const deletedAccountBlock = + await prisma.accountBlockExistence.deleteMany(); + console.log( + `[CRON] Cleared RpcRequest table: ${deletedRpc.count} rows deleted` + ); + console.log( + `[CRON] Cleared AccountBlockExistence table: ${deletedAccountBlock.count} rows deleted` + ); + } catch (error) { + console.error("[CRON] Error clearing tables:", error); + } + }, + { + timezone: "UTC", + } + ); +} + // Start the server if (process.env.NODE_ENV !== "test") { app.listen(port, hostname, () => { diff --git a/src/transactions-transfer-history.ts b/src/transactions-transfer-history.ts index c361da6..4c8bc14 100644 --- a/src/transactions-transfer-history.ts +++ b/src/transactions-transfer-history.ts @@ -62,11 +62,13 @@ export async function getTransactionsTransferHistory( if (isUpdated) { cachedData = deduplicateByTimestamp([...allData, ...cachedData]); - await prisma.transferHistory.upsert({ - where: { cacheKey }, - update: { data: cachedData, timestamp: new Date() }, - create: { cacheKey, data: cachedData }, - }); + prisma.transferHistory + .upsert({ + where: { cacheKey }, + update: { data: cachedData, timestamp: new Date() }, + create: { cacheKey, data: cachedData }, + }) + .catch((e) => console.error("DB write failed:", e.message)); } } @@ -92,11 +94,13 @@ export async function getTransactionsTransferHistory( cachedData = deduplicateByTimestamp([...cachedData, ...moreData]); - await prisma.transferHistory.upsert({ - where: { cacheKey }, - update: { data: cachedData, timestamp: new Date() }, - create: { cacheKey, data: cachedData }, - }); + prisma.transferHistory + .upsert({ + where: { cacheKey }, + update: { data: cachedData, timestamp: new Date() }, + create: { cacheKey, data: cachedData }, + }) + .catch((e) => console.error("DB write failed:", e.message)); } } diff --git a/src/utils/fetch-from-rpc.ts b/src/utils/fetch-from-rpc.ts index be0d4b0..caf134f 100644 --- a/src/utils/fetch-from-rpc.ts +++ b/src/utils/fetch-from-rpc.ts @@ -134,14 +134,16 @@ export async function fetchFromRPC( } // Store successful response in cache - await prisma.rpcRequest.create({ - data: { - requestHash, - endpoint: endpoint, - requestBody: body, - responseBody: data, - }, - }); + prisma.rpcRequest + .create({ + data: { + requestHash, + endpoint: endpoint, + requestBody: body, + responseBody: data, + }, + }) + .catch((e) => console.error("DB write failed:", e.message)); return data; } catch (error: any) {