diff --git a/package-lock.json b/package-lock.json index cc1c9d9..d3a950b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "express": "^4.20.0", "express-rate-limit": "^7.4.1", "fuse.js": "^7.0.0", - "helmet": "^8.0.0" + "helmet": "^8.0.0", + "node-cache": "^5.1.2", + "react": ">=16" }, "devDependencies": { "@eslint/js": "^9.10.0", @@ -33,9 +35,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -399,9 +401,9 @@ } }, "node_modules/@ref-finance/ref-sdk": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ref-finance/ref-sdk/-/ref-sdk-1.4.0.tgz", - "integrity": "sha512-lx14ixf7x5xkQO+qqme+0Pz816Gx+k20sEm7NqA4UaAd89FIPtZND+PtOYFELPIsrt6HvO6i+2DNN/5ZpM9R+A==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@ref-finance/ref-sdk/-/ref-sdk-1.4.6.tgz", + "integrity": "sha512-HVmcV+lhE+4+RwlDkgnFHwymrplHFlwsIwYZASE2XbGQjSY0sF3wceJkz671II3Us/KcRl1wp23ASSzza+/pbg==", "license": "MIT", "dependencies": { "@near-wallet-selector/core": "^7.0.0", @@ -458,9 +460,9 @@ "license": "MIT" }, "node_modules/@types/bn.js": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", - "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.6.tgz", + "integrity": "sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -538,9 +540,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", "license": "MIT" }, "node_modules/@types/mime": { @@ -1057,9 +1059,9 @@ } }, "node_modules/big.js": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz", - "integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", "license": "MIT", "engines": { "node": "*" @@ -1233,6 +1235,15 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1254,16 +1265,16 @@ "license": "MIT" }, "node_modules/complex.js": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", - "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", + "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==", "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, "node_modules/concat-map": { @@ -2497,8 +2508,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2598,7 +2608,6 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -2826,6 +2835,18 @@ "node": ">= 0.6" } }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -3143,7 +3164,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3630,9 +3650,9 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tweetnacl": { diff --git a/package.json b/package.json index 97c2ac4..acba48f 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "description": "", "dependencies": { "@ref-finance/ref-sdk": "^1.4.0", + "react": ">=16", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.20.0", - "express-rate-limit": "^7.4.1", "fuse.js": "^7.0.0", + "node-cache": "^5.1.2", + "express-rate-limit": "^7.4.1", "helmet": "^8.0.0" }, "devDependencies": { diff --git a/src/server.ts b/src/server.ts index ba93acc..ec64bea 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,12 +6,8 @@ import cors from "cors"; import Big from "big.js"; import * as dotenv from "dotenv"; import helmet from "helmet"; -import rateLimit from 'express-rate-limit'; -import { - BalanceResp, - SmartRouter, - Token, -} from "./utils/interface"; +import rateLimit from "express-rate-limit"; +import { BalanceResp, SmartRouter, Token } from "./utils/interface"; import { swapFromServer, unWrapNear, wrapNear } from "./utils/lib"; dotenv.config(); @@ -29,6 +25,9 @@ app.use(helmet()); app.use(express.json()); app.use("/api/", apiLimiter); +const NodeCache = require("node-cache"); +const cache = new NodeCache({ stdTTL: 600, checkperiod: 120 }); // Cache for 10 min + app.get("/api/token-metadata", async (req: Request, res: Response) => { try { const { token } = req.query as { token: string }; @@ -106,7 +105,10 @@ app.get("/api/whitelist-tokens", async (req: Request, res: Response) => { decimals: token.decimals, parsedBalance, balance, - price: priceData.price !== "N/A" ? Big(priceData.price ?? "").toFixed(4) : priceData.price, + price: + priceData.price !== "N/A" + ? Big(priceData.price ?? "").toFixed(4) + : priceData.price, symbol: token.symbol, name: token.name, icon: token.icon, @@ -184,7 +186,6 @@ app.get("/api/swap", async (req: Request, res: Response) => { amountIn: amountIn, accountId: accountId, swapsToDoServer: swapRes.result_data, - }); return res.json({ @@ -199,7 +200,158 @@ app.get("/api/swap", async (req: Request, res: Response) => { } }); +app.get("/api/token-balance-history", async (req: Request, res: Response) => { + const { account_id, period, token_id, interval } = req.query; + const cachekey = `${account_id}:${period}:${interval}:${token_id}`; + const cachedData = cache.get(cachekey); + + if (cachedData) { + console.log( + ` cached response for key: ${account_id}:${period}:${interval}:${token_id}` + ); + return res.json(cachedData); + } + + const RPC_URL = "https://archival-rpc.mainnet.near.org"; + const balanceHistory = []; + const parsedInterval = parseInt(interval as string); + const parsedPeriod = parseFloat(period as string); + + const filePath = path.join(__dirname, "tokens.json"); + const data = await fs.readFile(filePath, "utf-8"); + const tokens: Record = JSON.parse(data); + let balance; + + function convertFTBalance(value: string, decimals: number) { + return (parseFloat(value) / Math.pow(10, decimals)).toFixed(2); + } + + try { + const blockResponse = await fetch(RPC_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "block", + params: { finality: "final" }, + }), + }); + + const blockData = await blockResponse.json(); + if (!blockData.result) { + console.error("Failed to fetch latest block"); + return; + } + + const endBlock = blockData.result.header.height; + const BLOCKS_IN_ONE_HOUR = 3200; + const BLOCKS_IN_PERIOD = Math.floor(BLOCKS_IN_ONE_HOUR * parsedPeriod); + + for (let i = 0; i < parsedInterval; i++) { + const block_id = endBlock - BLOCKS_IN_PERIOD * i; + if (block_id < 0) break; + + const blockResponse = await fetch(RPC_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: block_id, + method: "block", + params: { block_id }, + }), + }); + const blockData = await blockResponse.json(); + if (!blockData.result) { + console.error("Failed to fetch block " + block_id); + continue; + } + + if (token_id !== "near") { + const accountResponse = await fetch(RPC_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: "dontcare", // Arbitrary ID for matching responses + method: "query", + params: { + request_type: "call_function", + block_id, + account_id: token_id, + method_name: "ft_balance_of", + args_base64: btoa(JSON.stringify({ account_id })), // Base64 encode arguments + }, + }), + }); + const accountData = await accountResponse.json(); + if (accountData.result) { + balance = String.fromCharCode(...accountData.result.result); + // @ts-ignore + balance = balance ? balance.replaceAll('"', "") : "0"; + } else + console.error("Failed to fetch account state for block " + block_id); + } else { + const accountResponse = await fetch(RPC_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "query", + params: { + request_type: "view_account", + block_id, + account_id, + }, + }), + }); + const accountData = await accountResponse.json(); + if (accountData.result) balance = accountData.result.amount.toString(); + else + console.error("Failed to fetch account state for block " + block_id); + } + + balanceHistory.push({ + timestamp: blockData.result.header.timestamp / 1e6, + date: + parsedPeriod <= 1 + ? new Date( + blockData.result.header.timestamp / 1e6 + ).toLocaleTimeString( + "en-US", + parsedPeriod < 1 + ? { hour: "numeric", minute: "numeric" } + : { hour: "numeric" } + ) + : new Date( + blockData.result.header.timestamp / 1e6 + ).toLocaleDateString( + "en-US", + parsedPeriod < 24 * 31 + ? { month: "short", day: "2-digit" } + : { year: "numeric", month: "short" } + ), + balance: + balance && token_id + ? convertFTBalance(balance, tokens[token_id as string].decimals) + : 0, + }); + } + + const respData = balanceHistory.reverse(); + + cache.set(cachekey, respData); + return res.json(respData); + } catch (error) { + cache.del(cachekey); + console.error("Error fetching balance history:", error); + throw error; + } +}); + // Start the server app.listen(port, hostname, 100, () => { console.log(`Server is running on http://${hostname}:${port}`); -}); \ No newline at end of file +});