From ea1cd3321e226a04d9414301efb637f4620959c2 Mon Sep 17 00:00:00 2001 From: MarcoMandar Date: Thu, 24 Oct 2024 03:08:10 +0300 Subject: [PATCH 1/5] Token Provider Signed-off-by: MarcoMandar --- .env.example | 3 +- package-lock.json | 22 ++ package.json | 1 + src/providers/token.ts | 746 +++++++++++++++++++++++++++++++++++++++++ src/types/token.ts | 274 +++++++++++++++ 5 files changed, 1045 insertions(+), 1 deletion(-) create mode 100644 src/providers/token.ts create mode 100644 src/types/token.ts diff --git a/.env.example b/.env.example index d6dab99d58..2e95273e43 100644 --- a/.env.example +++ b/.env.example @@ -35,4 +35,5 @@ WALLET_PUBLIC_KEY=DM1fSD9KfdJ2jaSmR9NGpPPVcDzBwsYg1STttYc5Bvay SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com \ No newline at end of file +RPC_URL=https://api.mainnet-beta.solana.com +HELIUS_API_KEY= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dc284bf4f2..81b06be8a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "kuromoji": "^0.1.2", "libsodium-wrappers": "^0.7.13", "multer": "^1.4.5-lts.1", + "node-cache": "^5.1.2", "node-llama-cpp": "^3.0.0-beta.44", "node-wav": "^0.0.2", "nodejs-whisper": "^0.1.18", @@ -5953,6 +5954,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "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/clone-deep": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", @@ -12947,6 +12957,18 @@ "node": ">=v0.6.5" } }, + "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-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", diff --git a/package.json b/package.json index 6c488e5820..a2c6eb6466 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "kuromoji": "^0.1.2", "libsodium-wrappers": "^0.7.13", "multer": "^1.4.5-lts.1", + "node-cache": "^5.1.2", "node-llama-cpp": "^3.0.0-beta.44", "node-wav": "^0.0.2", "nodejs-whisper": "^0.1.18", diff --git a/src/providers/token.ts b/src/providers/token.ts new file mode 100644 index 0000000000..722114078a --- /dev/null +++ b/src/providers/token.ts @@ -0,0 +1,746 @@ +import { Connection, PublicKey, ParsedAccountData } from "@solana/web3.js"; +import fetch from "cross-fetch"; +import { IAgentRuntime, Memory, Provider, State } from "../core/types"; +import settings from "../core/settings.ts"; +import BigNumber from "bignumber.js"; +import { TOKEN_PROGRAM_ID, AccountLayout } from "@solana/spl-token"; +import { + ProcessedTokenData, + TokenSecurityData, + TokenTradeData, + DexScreenerData, + DexScreenerPair, + HolderData, +} from "../types/token"; +import NodeCache from "node-cache"; +import fs from "fs"; +import path from "path"; + +const PROVIDER_CONFIG = { + BIRDEYE_API: "https://public-api.birdeye.so", + MAX_RETRIES: 3, + RETRY_DELAY: 2000, + DEFAULT_RPC: "https://api.mainnet-beta.solana.com", + TOKEN_ADDRESSES: { + SOL: "So11111111111111111111111111111111111111112", + BTC: "qfnqNqs3nCAHjnyCgLRDbBtq4p2MtHZxw8YjSyYhPoL", + ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + Example: "2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh", + }, + TOKEN_SECURITY_ENDPOINT: "/defi/token_security?address=", + TOKEN_TRADE_DATA_ENDPOINT: "/defi/v3/token/trade-data/single?address=", + DEX_SCREENER_API: "https://api.dexscreener.com/latest/dex/tokens/", // Example endpoint +}; + +class TokenProvider { + private cache: NodeCache; + private cacheDir: string; + + constructor( + private connection: Connection, + private tokenAddress: string + ) { + this.cache = new NodeCache({ stdTTL: 300 }); // 5 minutes cache + this.cacheDir = path.join(__dirname, "cache"); + if (!fs.existsSync(this.cacheDir)) { + fs.mkdirSync(this.cacheDir); + } + } + + private readCacheFromFile(cacheKey: string): T | null { + const filePath = path.join(this.cacheDir, `${cacheKey}.json`); + if (fs.existsSync(filePath)) { + const fileContent = fs.readFileSync(filePath, "utf-8"); + const parsed = JSON.parse(fileContent); + const now = Date.now(); + if (now < parsed.expiry) { + console.log(`Reading cached data from file for key: ${cacheKey}`); + return parsed.data as T; + } else { + console.log(`Cache expired for key: ${cacheKey}. Deleting file.`); + fs.unlinkSync(filePath); + } + } + return null; + } + + private writeCacheToFile(cacheKey: string, data: T): void { + const filePath = path.join(this.cacheDir, `${cacheKey}.json`); + const cacheData = { + data: data, + expiry: Date.now() + 300000, // 5 minutes in milliseconds + }; + fs.writeFileSync(filePath, JSON.stringify(cacheData), "utf-8"); + console.log(`Cached data written to file for key: ${cacheKey}`); + } + + private getCachedData(cacheKey: string): T | null { + // Check in-memory cache first + const cachedData = this.cache.get(cacheKey); + if (cachedData) { + return cachedData; + } + + // Check file-based cache + const fileCachedData = this.readCacheFromFile(cacheKey); + if (fileCachedData) { + // Populate in-memory cache + this.cache.set(cacheKey, fileCachedData); + return fileCachedData; + } + + return null; + } + + private setCachedData(cacheKey: string, data: T): void { + // Set in-memory cache + this.cache.set(cacheKey, data); + + // Write to file-based cache + this.writeCacheToFile(cacheKey, data); + } + + private async fetchWithRetry( + url: string, + options: RequestInit = {} + ): Promise { + let lastError: Error; + + for (let i = 0; i < PROVIDER_CONFIG.MAX_RETRIES; i++) { + try { + console.log(`Attempt ${i + 1}: Fetching data from ${url}`); + const response = await fetch(url, { + ...options, + headers: { + Accept: "application/json", + "x-chain": "solana", + "X-API-KEY": settings.BIRDEYE_API_KEY || "", + ...options.headers, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `HTTP error! status: ${response.status}, message: ${errorText}` + ); + } + + const data = await response.json(); + console.log(`Attempt ${i + 1}: Data fetched successfully`, data); + return data; + } catch (error) { + console.error(`Attempt ${i + 1} failed:`, error); + lastError = error as Error; + if (i < PROVIDER_CONFIG.MAX_RETRIES - 1) { + const delay = PROVIDER_CONFIG.RETRY_DELAY * Math.pow(2, i); + console.log(`Waiting ${delay}ms before retrying...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + } + } + + console.error("All attempts failed. Throwing the last error:", lastError); + throw lastError; + } + + async fetchTokenSecurity(): Promise { + const cacheKey = `tokenSecurity_${this.tokenAddress}`; + const cachedData = this.getCachedData(cacheKey); + if (cachedData) { + console.log( + `Returning cached token security data for ${this.tokenAddress}.` + ); + return cachedData; + } + const url = `${PROVIDER_CONFIG.BIRDEYE_API}${PROVIDER_CONFIG.TOKEN_SECURITY_ENDPOINT}${this.tokenAddress}`; + const data = await this.fetchWithRetry(url); + + if (!data?.success || !data?.data) { + throw new Error("No token security data available"); + } + + const security: TokenSecurityData = { + ownerBalance: data.data.ownerBalance, + creatorBalance: data.data.creatorBalance, + ownerPercentage: data.data.ownerPercentage, + creatorPercentage: data.data.creatorPercentage, + top10HolderBalance: data.data.top10HolderBalance, + top10HolderPercent: data.data.top10HolderPercent, + }; + this.setCachedData(cacheKey, security); + console.log(`Token security data cached for ${this.tokenAddress}.`); + + return security; + } + + async fetchTokenTradeData(): Promise { + const cacheKey = `tokenTradeData_${this.tokenAddress}`; + const cachedData = this.getCachedData(cacheKey); + if (cachedData) { + console.log( + `Returning cached token trade data for ${this.tokenAddress}.` + ); + return cachedData; + } + + const url = `${PROVIDER_CONFIG.BIRDEYE_API}${PROVIDER_CONFIG.TOKEN_TRADE_DATA_ENDPOINT}${this.tokenAddress}`; + const data = await this.fetchWithRetry(url); + + if (!data?.success || !data?.data) { + throw new Error("No token trade data available"); + } + + const tradeData: TokenTradeData = { + address: data.data.address, + holder: data.data.holder, + market: data.data.market, + last_trade_unix_time: data.data.last_trade_unix_time, + last_trade_human_time: data.data.last_trade_human_time, + price: data.data.price, + history_30m_price: data.data.history_30m_price, + price_change_30m_percent: data.data.price_change_30m_percent, + history_1h_price: data.data.history_1h_price, + price_change_1h_percent: data.data.price_change_1h_percent, + history_2h_price: data.data.history_2h_price, + price_change_2h_percent: data.data.price_change_2h_percent, + history_4h_price: data.data.history_4h_price, + price_change_4h_percent: data.data.price_change_4h_percent, + history_6h_price: data.data.history_6h_price, + price_change_6h_percent: data.data.price_change_6h_percent, + history_8h_price: data.data.history_8h_price, + price_change_8h_percent: data.data.price_change_8h_percent, + history_12h_price: data.data.history_12h_price, + price_change_12h_percent: data.data.price_change_12h_percent, + history_24h_price: data.data.history_24h_price, + price_change_24h_percent: data.data.price_change_24h_percent, + unique_wallet_30m: data.data.unique_wallet_30m, + unique_wallet_history_30m: data.data.unique_wallet_history_30m, + unique_wallet_30m_change_percent: + data.data.unique_wallet_30m_change_percent, + unique_wallet_1h: data.data.unique_wallet_1h, + unique_wallet_history_1h: data.data.unique_wallet_history_1h, + unique_wallet_1h_change_percent: + data.data.unique_wallet_1h_change_percent, + unique_wallet_2h: data.data.unique_wallet_2h, + unique_wallet_history_2h: data.data.unique_wallet_history_2h, + unique_wallet_2h_change_percent: + data.data.unique_wallet_2h_change_percent, + unique_wallet_4h: data.data.unique_wallet_4h, + unique_wallet_history_4h: data.data.unique_wallet_history_4h, + unique_wallet_4h_change_percent: + data.data.unique_wallet_4h_change_percent, + unique_wallet_8h: data.data.unique_wallet_8h, + unique_wallet_history_8h: data.data.unique_wallet_history_8h, + unique_wallet_8h_change_percent: + data.data.unique_wallet_8h_change_percent, + unique_wallet_24h: data.data.unique_wallet_24h, + unique_wallet_history_24h: data.data.unique_wallet_history_24h, + unique_wallet_24h_change_percent: + data.data.unique_wallet_24h_change_percent, + trade_30m: data.data.trade_30m, + trade_history_30m: data.data.trade_history_30m, + trade_30m_change_percent: data.data.trade_30m_change_percent, + sell_30m: data.data.sell_30m, + sell_history_30m: data.data.sell_history_30m, + sell_30m_change_percent: data.data.sell_30m_change_percent, + buy_30m: data.data.buy_30m, + buy_history_30m: data.data.buy_history_30m, + buy_30m_change_percent: data.data.buy_30m_change_percent, + volume_30m: data.data.volume_30m, + volume_30m_usd: data.data.volume_30m_usd, + volume_history_30m: data.data.volume_history_30m, + volume_history_30m_usd: data.data.volume_history_30m_usd, + volume_30m_change_percent: data.data.volume_30m_change_percent, + volume_buy_30m: data.data.volume_buy_30m, + volume_buy_30m_usd: data.data.volume_buy_30m_usd, + volume_buy_history_30m: data.data.volume_buy_history_30m, + volume_buy_history_30m_usd: data.data.volume_buy_history_30m_usd, + volume_buy_30m_change_percent: data.data.volume_buy_30m_change_percent, + volume_sell_30m: data.data.volume_sell_30m, + volume_sell_30m_usd: data.data.volume_sell_30m_usd, + volume_sell_history_30m: data.data.volume_sell_history_30m, + volume_sell_history_30m_usd: data.data.volume_sell_history_30m_usd, + volume_sell_30m_change_percent: data.data.volume_sell_30m_change_percent, + trade_1h: data.data.trade_1h, + trade_history_1h: data.data.trade_history_1h, + trade_1h_change_percent: data.data.trade_1h_change_percent, + sell_1h: data.data.sell_1h, + sell_history_1h: data.data.sell_history_1h, + sell_1h_change_percent: data.data.sell_1h_change_percent, + buy_1h: data.data.buy_1h, + buy_history_1h: data.data.buy_history_1h, + buy_1h_change_percent: data.data.buy_1h_change_percent, + volume_1h: data.data.volume_1h, + volume_1h_usd: data.data.volume_1h_usd, + volume_history_1h: data.data.volume_history_1h, + volume_history_1h_usd: data.data.volume_history_1h_usd, + volume_1h_change_percent: data.data.volume_1h_change_percent, + volume_buy_1h: data.data.volume_buy_1h, + volume_buy_1h_usd: data.data.volume_buy_1h_usd, + volume_buy_history_1h: data.data.volume_buy_history_1h, + volume_buy_history_1h_usd: data.data.volume_buy_history_1h_usd, + volume_buy_1h_change_percent: data.data.volume_buy_1h_change_percent, + volume_sell_1h: data.data.volume_sell_1h, + volume_sell_1h_usd: data.data.volume_sell_1h_usd, + volume_sell_history_1h: data.data.volume_sell_history_1h, + volume_sell_history_1h_usd: data.data.volume_sell_history_1h_usd, + volume_sell_1h_change_percent: data.data.volume_sell_1h_change_percent, + trade_2h: data.data.trade_2h, + trade_history_2h: data.data.trade_history_2h, + trade_2h_change_percent: data.data.trade_2h_change_percent, + sell_2h: data.data.sell_2h, + sell_history_2h: data.data.sell_history_2h, + sell_2h_change_percent: data.data.sell_2h_change_percent, + buy_2h: data.data.buy_2h, + buy_history_2h: data.data.buy_history_2h, + buy_2h_change_percent: data.data.buy_2h_change_percent, + volume_2h: data.data.volume_2h, + volume_2h_usd: data.data.volume_2h_usd, + volume_history_2h: data.data.volume_history_2h, + volume_history_2h_usd: data.data.volume_history_2h_usd, + volume_2h_change_percent: data.data.volume_2h_change_percent, + volume_buy_2h: data.data.volume_buy_2h, + volume_buy_2h_usd: data.data.volume_buy_2h_usd, + volume_buy_history_2h: data.data.volume_buy_history_2h, + volume_buy_history_2h_usd: data.data.volume_buy_history_2h_usd, + volume_buy_2h_change_percent: data.data.volume_buy_2h_change_percent, + volume_sell_2h: data.data.volume_sell_2h, + volume_sell_2h_usd: data.data.volume_sell_2h_usd, + volume_sell_history_2h: data.data.volume_sell_history_2h, + volume_sell_history_2h_usd: data.data.volume_sell_history_2h_usd, + volume_sell_2h_change_percent: data.data.volume_sell_2h_change_percent, + trade_4h: data.data.trade_4h, + trade_history_4h: data.data.trade_history_4h, + trade_4h_change_percent: data.data.trade_4h_change_percent, + sell_4h: data.data.sell_4h, + sell_history_4h: data.data.sell_history_4h, + sell_4h_change_percent: data.data.sell_4h_change_percent, + buy_4h: data.data.buy_4h, + buy_history_4h: data.data.buy_history_4h, + buy_4h_change_percent: data.data.buy_4h_change_percent, + volume_4h: data.data.volume_4h, + volume_4h_usd: data.data.volume_4h_usd, + volume_history_4h: data.data.volume_history_4h, + volume_history_4h_usd: data.data.volume_history_4h_usd, + volume_4h_change_percent: data.data.volume_4h_change_percent, + volume_buy_4h: data.data.volume_buy_4h, + volume_buy_4h_usd: data.data.volume_buy_4h_usd, + volume_buy_history_4h: data.data.volume_buy_history_4h, + volume_buy_history_4h_usd: data.data.volume_buy_history_4h_usd, + volume_buy_4h_change_percent: data.data.volume_buy_4h_change_percent, + volume_sell_4h: data.data.volume_sell_4h, + volume_sell_4h_usd: data.data.volume_sell_4h_usd, + volume_sell_history_4h: data.data.volume_sell_history_4h, + volume_sell_history_4h_usd: data.data.volume_sell_history_4h_usd, + volume_sell_4h_change_percent: data.data.volume_sell_4h_change_percent, + trade_8h: data.data.trade_8h, + trade_history_8h: data.data.trade_history_8h, + trade_8h_change_percent: data.data.trade_8h_change_percent, + sell_8h: data.data.sell_8h, + sell_history_8h: data.data.sell_history_8h, + sell_8h_change_percent: data.data.sell_8h_change_percent, + buy_8h: data.data.buy_8h, + buy_history_8h: data.data.buy_history_8h, + buy_8h_change_percent: data.data.buy_8h_change_percent, + volume_8h: data.data.volume_8h, + volume_8h_usd: data.data.volume_8h_usd, + volume_history_8h: data.data.volume_history_8h, + volume_history_8h_usd: data.data.volume_history_8h_usd, + volume_8h_change_percent: data.data.volume_8h_change_percent, + volume_buy_8h: data.data.volume_buy_8h, + volume_buy_8h_usd: data.data.volume_buy_8h_usd, + volume_buy_history_8h: data.data.volume_buy_history_8h, + volume_buy_history_8h_usd: data.data.volume_buy_history_8h_usd, + volume_buy_8h_change_percent: data.data.volume_buy_8h_change_percent, + volume_sell_8h: data.data.volume_sell_8h, + volume_sell_8h_usd: data.data.volume_sell_8h_usd, + volume_sell_history_8h: data.data.volume_sell_history_8h, + volume_sell_history_8h_usd: data.data.volume_sell_history_8h_usd, + volume_sell_8h_change_percent: data.data.volume_sell_8h_change_percent, + trade_24h: data.data.trade_24h, + trade_history_24h: data.data.trade_history_24h, + trade_24h_change_percent: data.data.trade_24h_change_percent, + sell_24h: data.data.sell_24h, + sell_history_24h: data.data.sell_history_24h, + sell_24h_change_percent: data.data.sell_24h_change_percent, + buy_24h: data.data.buy_24h, + buy_history_24h: data.data.buy_history_24h, + buy_24h_change_percent: data.data.buy_24h_change_percent, + volume_24h: data.data.volume_24h, + volume_24h_usd: data.data.volume_24h_usd, + volume_history_24h: data.data.volume_history_24h, + volume_history_24h_usd: data.data.volume_history_24h_usd, + volume_24h_change_percent: data.data.volume_24h_change_percent, + volume_buy_24h: data.data.volume_buy_24h, + volume_buy_24h_usd: data.data.volume_buy_24h_usd, + volume_buy_history_24h: data.data.volume_buy_history_24h, + volume_buy_history_24h_usd: data.data.volume_buy_history_24h_usd, + volume_buy_24h_change_percent: data.data.volume_buy_24h_change_percent, + volume_sell_24h: data.data.volume_sell_24h, + volume_sell_24h_usd: data.data.volume_sell_24h_usd, + volume_sell_history_24h: data.data.volume_sell_history_24h, + volume_sell_history_24h_usd: data.data.volume_sell_history_24h_usd, + volume_sell_24h_change_percent: data.data.volume_sell_24h_change_percent, + }; + this.setCachedData(cacheKey, tradeData); + return tradeData; + } + + async fetchDexScreenerData(): Promise { + const cacheKey = `dexScreenerData_${this.tokenAddress}`; + const cachedData = this.getCachedData(cacheKey); + if (cachedData) { + console.log("Returning cached DexScreener data."); + return cachedData; + } + + const url = `https://api.dexscreener.com/latest/dex/search?q=${this.tokenAddress}`; + try { + console.log(`Fetching DexScreener data for token: ${this.tokenAddress}`); + const data = await this.fetchWithRetry(url); + + if (!data || !data.pairs) { + throw new Error("No DexScreener data available"); + } + + const dexData: DexScreenerData = { + schemaVersion: data.schemaVersion, + pairs: data.pairs, + }; + + // Cache the result + this.setCachedData(cacheKey, dexData); + + return dexData; + } catch (error) { + console.error(`Error fetching DexScreener data:`, error); + return { + schemaVersion: "1.0.0", + pairs: [], + }; + } + } + + async analyzeHolderDistribution(tradeData: TokenTradeData): Promise { + // Define the time intervals to consider (e.g., 30m, 1h, 2h) + const intervals = [ + { period: "30m", change: tradeData.unique_wallet_30m_change_percent }, + { period: "1h", change: tradeData.unique_wallet_1h_change_percent }, + { period: "2h", change: tradeData.unique_wallet_2h_change_percent }, + { period: "4h", change: tradeData.unique_wallet_4h_change_percent }, + { period: "8h", change: tradeData.unique_wallet_8h_change_percent }, + { period: "24h", change: tradeData.unique_wallet_24h_change_percent }, + ]; + + // Calculate the average change percentage + const validChanges = intervals + .map((interval) => interval.change) + .filter((change) => change !== null && change !== undefined) as number[]; + + if (validChanges.length === 0) { + return "stable"; + } + + const averageChange = + validChanges.reduce((acc, curr) => acc + curr, 0) / validChanges.length; + + const increaseThreshold = 10; // e.g., average change > 10% + const decreaseThreshold = -10; // e.g., average change < -10% + + if (averageChange > increaseThreshold) { + return "increasing"; + } else if (averageChange < decreaseThreshold) { + return "decreasing"; + } else { + return "stable"; + } + } + + async fetchHolderList(): Promise { + const cacheKey = `holderList_${this.tokenAddress}`; + const cachedData = this.getCachedData(cacheKey); + if (cachedData) { + console.log("Returning cached holder list."); + return cachedData; + } + + const allHoldersMap = new Map(); + let page = 1; + const limit = 1000; + + const heliusUrl = `https://mainnet.helius-rpc.com/?api-key=${settings.HELIUS_API_KEY}`; + + try { + while (true) { + console.log(`Fetching holders - Page ${page}`); + + const response = await fetch(heliusUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "getTokenAccounts", + page: page, + limit: limit, + mint: this.tokenAddress, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Helius API error: ${response.status} ${response.statusText} - ${errorText}` + ); + } + + const data = await response.json(); + + if (!data || !data.token_accounts || data.token_accounts.length === 0) { + console.log( + `No more holders found. Total pages fetched: ${page - 1}` + ); + break; + } + + console.log( + `Processing ${data.token_accounts.length} holders from page ${page}` + ); + + data.token_accounts.forEach((account: any) => { + const owner = account.owner; + const balance = parseFloat(account.amount); + + if (allHoldersMap.has(owner)) { + allHoldersMap.set(owner, allHoldersMap.get(owner)! + balance); + } else { + allHoldersMap.set(owner, balance); + } + }); + + page++; + } + + const holders: HolderData[] = Array.from(allHoldersMap.entries()).map( + ([address, balance]) => ({ + address, + balance: balance.toString(), + }) + ); + + console.log(`Total unique holders fetched: ${holders.length}`); + + // Cache the result + this.setCachedData(cacheKey, holders); + + return holders; + } catch (error) { + console.error("Error fetching holder list from Helius:", error); + throw new Error("Failed to fetch holder list from Helius."); + } + } + + async filterHighValueHolders( + tradeData: TokenTradeData + ): Promise> { + const holdersData = await this.fetchHolderList(); + + const tokenPriceUsd = new BigNumber(tradeData.price); + + const highValueHolders = holdersData + .filter((holder) => { + const balanceUsd = new BigNumber(holder.balance).multipliedBy( + tokenPriceUsd + ); + return balanceUsd.isGreaterThan(5); + }) + .map((holder) => ({ + holderAddress: holder.address, + balanceUsd: new BigNumber(holder.balance) + .multipliedBy(tokenPriceUsd) + .toFixed(2), + })); + + return highValueHolders; + } + + async checkRecentTrades(tradeData: TokenTradeData): Promise { + return new BigNumber(tradeData.volume_24h_usd).isGreaterThan(0); + } + + async countHighSupplyHolders( + securityData: TokenSecurityData + ): Promise { + try { + const ownerBalance = new BigNumber(securityData.ownerBalance); + const totalSupply = ownerBalance.plus(securityData.creatorBalance); + + const highSupplyHolders = await this.fetchHolderList(); + const highSupplyHoldersCount = highSupplyHolders.filter((holder) => { + const balance = new BigNumber(holder.balance); + return balance.dividedBy(totalSupply).isGreaterThan(0.02); + }).length; + return highSupplyHoldersCount; + } catch (error) { + console.error("Error counting high supply holders:", error); + return 0; + } + } + + async getProcessedTokenData(): Promise { + try { + console.log(`Fetching security data for token: ${this.tokenAddress}`); + const security = await this.fetchTokenSecurity(); + + console.log(`Fetching trade data for token: ${this.tokenAddress}`); + const tradeData = await this.fetchTokenTradeData(); + + console.log(`Fetching DexScreener data for token: ${this.tokenAddress}`); + const dexData = await this.fetchDexScreenerData(); + + console.log( + `Analyzing holder distribution for token: ${this.tokenAddress}` + ); + const holderDistributionTrend = + await this.analyzeHolderDistribution(tradeData); + + console.log( + `Filtering high-value holders for token: ${this.tokenAddress}` + ); + const highValueHolders = await this.filterHighValueHolders(tradeData); + + console.log(`Checking recent trades for token: ${this.tokenAddress}`); + const recentTrades = await this.checkRecentTrades(tradeData); + + console.log( + `Counting high-supply holders for token: ${this.tokenAddress}` + ); + const highSupplyHoldersCount = + await this.countHighSupplyHolders(security); + + console.log( + `Determining DexScreener listing status for token: ${this.tokenAddress}` + ); + const isDexScreenerListed = dexData.pairs.length > 0; + const isDexScreenerPaid = dexData.pairs.some( + (pair) => pair.boosts.active > 0 + ); + + const processedData: ProcessedTokenData = { + security, + tradeData, + holderDistributionTrend, + highValueHolders, + recentTrades, + highSupplyHoldersCount, + dexScreenerData: dexData, + isDexScreenerListed, + isDexScreenerPaid, + }; + + console.log("Processed token data:", processedData); + return processedData; + } catch (error) { + console.error("Error processing token data:", error); + throw error; + } + } + + formatTokenData(data: ProcessedTokenData): string { + let output = `**Token Security and Trade Report**\n`; + output += `Token Address: ${this.tokenAddress}\n\n`; + + // Security Data + output += `**Ownership Distribution:**\n`; + output += `- Owner Balance: ${data.security.ownerBalance}\n`; + output += `- Creator Balance: ${data.security.creatorBalance}\n`; + output += `- Owner Percentage: ${data.security.ownerPercentage}%\n`; + output += `- Creator Percentage: ${data.security.creatorPercentage}%\n`; + output += `- Top 10 Holders Balance: ${data.security.top10HolderBalance}\n`; + output += `- Top 10 Holders Percentage: ${data.security.top10HolderPercent}%\n\n`; + + // Trade Data + output += `**Trade Data:**\n`; + output += `- Holders: ${data.tradeData.holder}\n`; + output += `- Unique Wallets (24h): ${data.tradeData.unique_wallet_24h}\n`; + output += `- Price Change (24h): ${data.tradeData.price_change_24h_percent}%\n`; + output += `- Price Change (12h): ${data.tradeData.price_change_12h_percent}%\n`; + output += `- Volume (24h USD): $${new BigNumber(data.tradeData.volume_24h_usd).toFixed(2)}\n`; + output += `- Current Price: $${new BigNumber(data.tradeData.price).toFixed(2)}\n\n`; + + // Holder Distribution Trend + output += `**Holder Distribution Trend:** ${data.holderDistributionTrend}\n\n`; + + // High-Value Holders + output += `**High-Value Holders (>$5 USD):**\n`; + if (data.highValueHolders.length === 0) { + output += `- No high-value holders found or data not available.\n`; + } else { + data.highValueHolders.forEach((holder) => { + output += `- ${holder.holderAddress}: $${holder.balanceUsd}\n`; + }); + } + output += `\n`; + + // Recent Trades + output += `**Recent Trades (Last 24h):** ${data.recentTrades ? "Yes" : "No"}\n\n`; + + // High-Supply Holders + output += `**Holders with >2% Supply:** ${data.highSupplyHoldersCount}\n\n`; + + // DexScreener Status + output += `**DexScreener Listing:** ${data.isDexScreenerListed ? "Yes" : "No"}\n`; + if (data.isDexScreenerListed) { + output += `- Listing Type: ${data.isDexScreenerPaid ? "Paid" : "Free"}\n`; + output += `- Number of DexPairs: ${data.dexScreenerData.pairs.length}\n\n`; + output += `**DexScreener Pairs:**\n`; + data.dexScreenerData.pairs.forEach((pair, index) => { + output += `\n**Pair ${index + 1}:**\n`; + output += `- DEX: ${pair.dexId}\n`; + output += `- URL: ${pair.url}\n`; + output += `- Price USD: $${new BigNumber(pair.priceUsd).toFixed(6)}\n`; + output += `- Volume (24h USD): $${new BigNumber(pair.volume.h24).toFixed(2)}\n`; + output += `- Boosts Active: ${pair.boosts.active}\n`; + output += `- Liquidity USD: $${new BigNumber(pair.liquidity.usd).toFixed(2)}\n`; + }); + } + output += `\n`; + + console.log("Formatted token data:", output); + return output; + } + + async getFormattedTokenReport(): Promise { + try { + console.log("Generating formatted token report..."); + const processedData = await this.getProcessedTokenData(); + return this.formatTokenData(processedData); + } catch (error) { + console.error("Error generating token report:", error); + return "Unable to fetch token information. Please try again later."; + } + } +} + +const tokenAddress = PROVIDER_CONFIG.TOKEN_ADDRESSES.Example; +const connection = new Connection(PROVIDER_CONFIG.DEFAULT_RPC); +const tokenProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise => { + try { + const provider = new TokenProvider(connection, tokenAddress); + return provider.getFormattedTokenReport(); + } catch (error) { + console.error("Error fetching token data:", error); + return "Unable to fetch token information. Please try again later."; + } + }, +}; + +export default tokenProvider; diff --git a/src/types/token.ts b/src/types/token.ts new file mode 100644 index 0000000000..95751e7b5e --- /dev/null +++ b/src/types/token.ts @@ -0,0 +1,274 @@ +export interface TokenSecurityData { + ownerBalance: string; + creatorBalance: string; + ownerPercentage: number; + creatorPercentage: number; + top10HolderBalance: string; + top10HolderPercent: number; +} + +export interface TokenTradeData { + address: string; + holder: number; + market: number; + last_trade_unix_time: number; + last_trade_human_time: string; + price: number; + history_30m_price: number; + price_change_30m_percent: number; + history_1h_price: number; + price_change_1h_percent: number; + history_2h_price: number; + price_change_2h_percent: number; + history_4h_price: number; + price_change_4h_percent: number; + history_6h_price: number; + price_change_6h_percent: number; + history_8h_price: number; + price_change_8h_percent: number; + history_12h_price: number; + price_change_12h_percent: number; + history_24h_price: number; + price_change_24h_percent: number; + unique_wallet_30m: number; + unique_wallet_history_30m: number; + unique_wallet_30m_change_percent: number; + unique_wallet_1h: number; + unique_wallet_history_1h: number; + unique_wallet_1h_change_percent: number; + unique_wallet_2h: number; + unique_wallet_history_2h: number; + unique_wallet_2h_change_percent: number; + unique_wallet_4h: number; + unique_wallet_history_4h: number; + unique_wallet_4h_change_percent: number; + unique_wallet_8h: number; + unique_wallet_history_8h: number | null; + unique_wallet_8h_change_percent: number | null; + unique_wallet_24h: number; + unique_wallet_history_24h: number | null; + unique_wallet_24h_change_percent: number | null; + trade_30m: number; + trade_history_30m: number; + trade_30m_change_percent: number; + sell_30m: number; + sell_history_30m: number; + sell_30m_change_percent: number; + buy_30m: number; + buy_history_30m: number; + buy_30m_change_percent: number; + volume_30m: number; + volume_30m_usd: number; + volume_history_30m: number; + volume_history_30m_usd: number; + volume_30m_change_percent: number; + volume_buy_30m: number; + volume_buy_30m_usd: number; + volume_buy_history_30m: number; + volume_buy_history_30m_usd: number; + volume_buy_30m_change_percent: number; + volume_sell_30m: number; + volume_sell_30m_usd: number; + volume_sell_history_30m: number; + volume_sell_history_30m_usd: number; + volume_sell_30m_change_percent: number; + trade_1h: number; + trade_history_1h: number; + trade_1h_change_percent: number; + sell_1h: number; + sell_history_1h: number; + sell_1h_change_percent: number; + buy_1h: number; + buy_history_1h: number; + buy_1h_change_percent: number; + volume_1h: number; + volume_1h_usd: number; + volume_history_1h: number; + volume_history_1h_usd: number; + volume_1h_change_percent: number; + volume_buy_1h: number; + volume_buy_1h_usd: number; + volume_buy_history_1h: number; + volume_buy_history_1h_usd: number; + volume_buy_1h_change_percent: number; + volume_sell_1h: number; + volume_sell_1h_usd: number; + volume_sell_history_1h: number; + volume_sell_history_1h_usd: number; + volume_sell_1h_change_percent: number; + trade_2h: number; + trade_history_2h: number; + trade_2h_change_percent: number; + sell_2h: number; + sell_history_2h: number; + sell_2h_change_percent: number; + buy_2h: number; + buy_history_2h: number; + buy_2h_change_percent: number; + volume_2h: number; + volume_2h_usd: number; + volume_history_2h: number; + volume_history_2h_usd: number; + volume_2h_change_percent: number; + volume_buy_2h: number; + volume_buy_2h_usd: number; + volume_buy_history_2h: number; + volume_buy_history_2h_usd: number; + volume_buy_2h_change_percent: number; + volume_sell_2h: number; + volume_sell_2h_usd: number; + volume_sell_history_2h: number; + volume_sell_history_2h_usd: number; + volume_sell_2h_change_percent: number; + trade_4h: number; + trade_history_4h: number; + trade_4h_change_percent: number; + sell_4h: number; + sell_history_4h: number; + sell_4h_change_percent: number; + buy_4h: number; + buy_history_4h: number; + buy_4h_change_percent: number; + volume_4h: number; + volume_4h_usd: number; + volume_history_4h: number; + volume_history_4h_usd: number; + volume_4h_change_percent: number; + volume_buy_4h: number; + volume_buy_4h_usd: number; + volume_buy_history_4h: number; + volume_buy_history_4h_usd: number; + volume_buy_4h_change_percent: number; + volume_sell_4h: number; + volume_sell_4h_usd: number; + volume_sell_history_4h: number; + volume_sell_history_4h_usd: number; + volume_sell_4h_change_percent: number; + trade_8h: number; + trade_history_8h: number | null; + trade_8h_change_percent: number | null; + sell_8h: number; + sell_history_8h: number | null; + sell_8h_change_percent: number | null; + buy_8h: number; + buy_history_8h: number | null; + buy_8h_change_percent: number | null; + volume_8h: number; + volume_8h_usd: number; + volume_history_8h: number; + volume_history_8h_usd: number; + volume_8h_change_percent: number | null; + volume_buy_8h: number; + volume_buy_8h_usd: number; + volume_buy_history_8h: number; + volume_buy_history_8h_usd: number; + volume_buy_8h_change_percent: number | null; + volume_sell_8h: number; + volume_sell_8h_usd: number; + volume_sell_history_8h: number; + volume_sell_history_8h_usd: number; + volume_sell_8h_change_percent: number | null; + trade_24h: number; + trade_history_24h: number; + trade_24h_change_percent: number | null; + sell_24h: number; + sell_history_24h: number; + sell_24h_change_percent: number | null; + buy_24h: number; + buy_history_24h: number; + buy_24h_change_percent: number | null; + volume_24h: number; + volume_24h_usd: number; + volume_history_24h: number; + volume_history_24h_usd: number; + volume_24h_change_percent: number | null; + volume_buy_24h: number; + volume_buy_24h_usd: number; + volume_buy_history_24h: number; + volume_buy_history_24h_usd: number; + volume_buy_24h_change_percent: number | null; + volume_sell_24h: number; + volume_sell_24h_usd: number; + volume_sell_history_24h: number; + volume_sell_history_24h_usd: number; + volume_sell_24h_change_percent: number | null; +} + +export interface HolderData { + address: string; + balance: string; +} + +export interface ProcessedTokenData { + security: TokenSecurityData; + tradeData: TokenTradeData; + holderDistributionTrend: string; // 'increasing' | 'decreasing' | 'stable' + highValueHolders: Array<{ + holderAddress: string; + balanceUsd: string; + }>; + recentTrades: boolean; + highSupplyHoldersCount: number; + dexScreenerData: DexScreenerData; + + isDexScreenerListed: boolean; + isDexScreenerPaid: boolean; +} + +export interface DexScreenerPair { + chainId: string; + dexId: string; + url: string; + pairAddress: string; + baseToken: { + address: string; + name: string; + symbol: string; + }; + quoteToken: { + address: string; + name: string; + symbol: string; + }; + priceNative: string; + priceUsd: string; + txns: { + m5: { buys: number; sells: number }; + h1: { buys: number; sells: number }; + h6: { buys: number; sells: number }; + h24: { buys: number; sells: number }; + }; + volume: { + h24: number; + h6: number; + h1: number; + m5: number; + }; + priceChange: { + m5: number; + h1: number; + h6: number; + h24: number; + }; + liquidity: { + usd: number; + base: number; + quote: number; + }; + fdv: number; + marketCap: number; + pairCreatedAt: number; + info: { + imageUrl: string; + websites: { label: string; url: string }[]; + socials: { type: string; url: string }[]; + }; + boosts: { + active: number; + }; +} + +export interface DexScreenerData { + schemaVersion: string; + pairs: DexScreenerPair[]; +} From 0384b2f584eeeac0da4dc2fde76dcfbd366b5a82 Mon Sep 17 00:00:00 2001 From: MarcoMandar Date: Thu, 24 Oct 2024 13:45:57 +0300 Subject: [PATCH 2/5] fix for helius holders Signed-off-by: MarcoMandar --- src/providers/token.test.ts | 65 +++++++++++++++++++++++++++++++++++++ src/providers/token.ts | 58 +++++++++++++++++++-------------- 2 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 src/providers/token.test.ts diff --git a/src/providers/token.test.ts b/src/providers/token.test.ts new file mode 100644 index 0000000000..5592bdd177 --- /dev/null +++ b/src/providers/token.test.ts @@ -0,0 +1,65 @@ +import { TokenProvider } from "./token"; +import NodeCache from "node-cache"; + +// Mock the dependencies +jest.mock("cross-fetch"); +jest.mock("fs"); +jest.mock("node-cache"); + +describe("TokenProvider Tests", () => { + // let connection: Connection; + let tokenProvider: TokenProvider; + + beforeEach(() => { + // Initialize the connection and token provider before each test + // connection = new Connection("https://api.mainnet-beta.solana.com"); + tokenProvider = new TokenProvider( + "2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh" + ); + }); + + test("should fetch token security data", async () => { + // Mock the response for the fetchTokenSecurity call + const mockFetchResponse = { + success: true, + data: { + ownerBalance: "100", + creatorBalance: "50", + ownerPercentage: 10, + creatorPercentage: 5, + top10HolderBalance: "200", + top10HolderPercent: 20, + }, + }; + + // Mock fetchWithRetry function + const fetchSpy = jest + .spyOn(tokenProvider as any, "fetchWithRetry") + .mockResolvedValue(mockFetchResponse); + + // Run the fetchTokenSecurity method + const securityData = await tokenProvider.fetchTokenSecurity(); + + // Check if the data returned is correct + expect(securityData).toEqual({ + ownerBalance: "100", + creatorBalance: "50", + ownerPercentage: 10, + creatorPercentage: 5, + top10HolderBalance: "200", + top10HolderPercent: 20, + }); + //console.log the securityData + console.log({ securityData }); + + const holderList = await tokenProvider.fetchHolderList(); + + console.log({ holderList }); + + // const tokenReport = await tokenProvider.getFormattedTokenReport(); + // console.log({ tokenReport }); + + // Ensure the mock was called + expect(fetchSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/providers/token.ts b/src/providers/token.ts index 722114078a..a5e26155d4 100644 --- a/src/providers/token.ts +++ b/src/providers/token.ts @@ -1,7 +1,7 @@ import { Connection, PublicKey, ParsedAccountData } from "@solana/web3.js"; -import fetch from "cross-fetch"; +// import fetch from "cross-fetch"; import { IAgentRuntime, Memory, Provider, State } from "../core/types"; -import settings from "../core/settings.ts"; +import settings from "../core/settings"; import BigNumber from "bignumber.js"; import { TOKEN_PROGRAM_ID, AccountLayout } from "@solana/spl-token"; import { @@ -13,8 +13,8 @@ import { HolderData, } from "../types/token"; import NodeCache from "node-cache"; -import fs from "fs"; -import path from "path"; +import * as fs from "fs"; +import * as path from "path"; const PROVIDER_CONFIG = { BIRDEYE_API: "https://public-api.birdeye.so", @@ -32,12 +32,12 @@ const PROVIDER_CONFIG = { DEX_SCREENER_API: "https://api.dexscreener.com/latest/dex/tokens/", // Example endpoint }; -class TokenProvider { +export class TokenProvider { private cache: NodeCache; private cacheDir: string; constructor( - private connection: Connection, + // private connection: Connection, private tokenAddress: string ) { this.cache = new NodeCache({ stdTTL: 300 }); // 5 minutes cache @@ -49,6 +49,7 @@ class TokenProvider { private readCacheFromFile(cacheKey: string): T | null { const filePath = path.join(this.cacheDir, `${cacheKey}.json`); + console.log({ filePath }); if (fs.existsSync(filePath)) { const fileContent = fs.readFileSync(filePath, "utf-8"); const parsed = JSON.parse(fileContent); @@ -469,37 +470,46 @@ class TokenProvider { const allHoldersMap = new Map(); let page = 1; const limit = 1000; + let cursor; - const heliusUrl = `https://mainnet.helius-rpc.com/?api-key=${settings.HELIUS_API_KEY}`; + const url = `https://mainnet.helius-rpc.com/?api-key=${settings.HELIOUS_API_KEY || ""}`; + console.log({ url }); try { while (true) { + let params = { + limit: limit, + displayOptions: {}, + mint: this.tokenAddress, + cursor: cursor, + }; + if (cursor != undefined) { + params.cursor = cursor; + } console.log(`Fetching holders - Page ${page}`); - - const response = await fetch(heliusUrl, { + if (page > 2) { + break; + } + const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", + id: "helius-test", method: "getTokenAccounts", - page: page, - limit: limit, - mint: this.tokenAddress, + params: params, }), }); - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `Helius API error: ${response.status} ${response.statusText} - ${errorText}` - ); - } - const data = await response.json(); - if (!data || !data.token_accounts || data.token_accounts.length === 0) { + if ( + !data || + !data.result.token_accounts || + data.result.token_accounts.length === 0 + ) { console.log( `No more holders found. Total pages fetched: ${page - 1}` ); @@ -507,10 +517,10 @@ class TokenProvider { } console.log( - `Processing ${data.token_accounts.length} holders from page ${page}` + `Processing ${data.result.token_accounts.length} holders from page ${page}` ); - data.token_accounts.forEach((account: any) => { + data.result.token_accounts.forEach((account: any) => { const owner = account.owner; const balance = parseFloat(account.amount); @@ -520,7 +530,7 @@ class TokenProvider { allHoldersMap.set(owner, balance); } }); - + cursor = data.result.cursor; page++; } @@ -734,7 +744,7 @@ const tokenProvider: Provider = { _state?: State ): Promise => { try { - const provider = new TokenProvider(connection, tokenAddress); + const provider = new TokenProvider(/*connection,*/ tokenAddress); return provider.getFormattedTokenReport(); } catch (error) { console.error("Error fetching token data:", error); From d305fd64c357c2072c532ba3e15c95218d9f3f9d Mon Sep 17 00:00:00 2001 From: MarcoMandar Date: Thu, 24 Oct 2024 13:52:08 +0300 Subject: [PATCH 3/5] generate tokenreport Signed-off-by: MarcoMandar --- src/providers/token.test.ts | 8 ++++---- src/providers/token.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/providers/token.test.ts b/src/providers/token.test.ts index 5592bdd177..2b25b6fe91 100644 --- a/src/providers/token.test.ts +++ b/src/providers/token.test.ts @@ -52,12 +52,12 @@ describe("TokenProvider Tests", () => { //console.log the securityData console.log({ securityData }); - const holderList = await tokenProvider.fetchHolderList(); + // const holderList = await tokenProvider.fetchHolderList(); - console.log({ holderList }); + // console.log({ holderList }); - // const tokenReport = await tokenProvider.getFormattedTokenReport(); - // console.log({ tokenReport }); + const tokenReport = await tokenProvider.getFormattedTokenReport(); + console.log({ tokenReport }); // Ensure the mock was called expect(fetchSpy).toHaveBeenCalled(); diff --git a/src/providers/token.ts b/src/providers/token.ts index a5e26155d4..a8456d9aab 100644 --- a/src/providers/token.ts +++ b/src/providers/token.ts @@ -507,6 +507,7 @@ export class TokenProvider { if ( !data || + !data.result || !data.result.token_accounts || data.result.token_accounts.length === 0 ) { From d71031d2dbe1fcd2d26768c436cbdbf8802070d6 Mon Sep 17 00:00:00 2001 From: MarcoMandar Date: Thu, 24 Oct 2024 15:53:55 +0300 Subject: [PATCH 4/5] finish token report Signed-off-by: MarcoMandar --- .env.example | 2 +- src/providers/token.test.ts | 26 ++++++++++++++++---------- src/providers/token.ts | 29 ++++++++++++++++++++++++----- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 2e95273e43..6f1bbd91b7 100644 --- a/.env.example +++ b/.env.example @@ -31,7 +31,7 @@ ANTHROPIC_API_KEY= WALLET_SECRET_KEY=2eETRBeJFNfxAmPzTxfRynebRjTYK9WBLeAE5JhfxdzAxjJG8ZCbmHX1WadTRdcEpE7HRELVp6cbCfZFY6Qw9BgR WALLET_PUBLIC_KEY=DM1fSD9KfdJ2jaSmR9NGpPPVcDzBwsYg1STttYc5Bvay - +BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 diff --git a/src/providers/token.test.ts b/src/providers/token.test.ts index 2b25b6fe91..18e40215ac 100644 --- a/src/providers/token.test.ts +++ b/src/providers/token.test.ts @@ -38,24 +38,30 @@ describe("TokenProvider Tests", () => { .mockResolvedValue(mockFetchResponse); // Run the fetchTokenSecurity method - const securityData = await tokenProvider.fetchTokenSecurity(); + // const securityData = await tokenProvider.fetchTokenSecurity(); // Check if the data returned is correct - expect(securityData).toEqual({ - ownerBalance: "100", - creatorBalance: "50", - ownerPercentage: 10, - creatorPercentage: 5, - top10HolderBalance: "200", - top10HolderPercent: 20, - }); + // expect(securityData).toEqual({ + // ownerBalance: "100", + // creatorBalance: "50", + // ownerPercentage: 10, + // creatorPercentage: 5, + // top10HolderBalance: "200", + // top10HolderPercent: 20, + // }); //console.log the securityData - console.log({ securityData }); + // console.log({ securityData }); // const holderList = await tokenProvider.fetchHolderList(); // console.log({ holderList }); + // const tradeData = await tokenProvider.fetchTokenTradeData(); + // console.log({ tradeData }); + + // const dexScreenerData = await tokenProvider.fetchDexScreenerData(); + // console.log({ dexScreenerData }); + const tokenReport = await tokenProvider.getFormattedTokenReport(); console.log({ tokenReport }); diff --git a/src/providers/token.ts b/src/providers/token.ts index a8456d9aab..64a5beabf6 100644 --- a/src/providers/token.ts +++ b/src/providers/token.ts @@ -120,6 +120,8 @@ export class TokenProvider { }, }); + console.log({ response }); + if (!response.ok) { const errorText = await response.text(); throw new Error( @@ -157,6 +159,7 @@ export class TokenProvider { } const url = `${PROVIDER_CONFIG.BIRDEYE_API}${PROVIDER_CONFIG.TOKEN_SECURITY_ENDPOINT}${this.tokenAddress}`; const data = await this.fetchWithRetry(url); + console.log({ data }); if (!data?.success || !data?.data) { throw new Error("No token security data available"); @@ -187,7 +190,19 @@ export class TokenProvider { } const url = `${PROVIDER_CONFIG.BIRDEYE_API}${PROVIDER_CONFIG.TOKEN_TRADE_DATA_ENDPOINT}${this.tokenAddress}`; - const data = await this.fetchWithRetry(url); + const options = { + method: "GET", + headers: { + accept: "application/json", + "X-API-KEY": settings.BIRDEYE_API_KEY || "", + }, + }; + + const data = await fetch(url, options) + .then((res) => res.json()) + .catch((err) => console.error(err)); + + console.log({ data }); if (!data?.success || !data?.data) { throw new Error("No token trade data available"); @@ -400,7 +415,11 @@ export class TokenProvider { const url = `https://api.dexscreener.com/latest/dex/search?q=${this.tokenAddress}`; try { console.log(`Fetching DexScreener data for token: ${this.tokenAddress}`); - const data = await this.fetchWithRetry(url); + const data = await fetch(url) + .then((res) => res.json()) + .catch((err) => { + console.error(err); + }); if (!data || !data.pairs) { throw new Error("No DexScreener data available"); @@ -637,7 +656,7 @@ export class TokenProvider { ); const isDexScreenerListed = dexData.pairs.length > 0; const isDexScreenerPaid = dexData.pairs.some( - (pair) => pair.boosts.active > 0 + (pair) => pair.boosts && pair.boosts.active > 0 ); const processedData: ProcessedTokenData = { @@ -652,7 +671,7 @@ export class TokenProvider { isDexScreenerPaid, }; - console.log("Processed token data:", processedData); + // console.log("Processed token data:", processedData); return processedData; } catch (error) { console.error("Error processing token data:", error); @@ -714,7 +733,7 @@ export class TokenProvider { output += `- URL: ${pair.url}\n`; output += `- Price USD: $${new BigNumber(pair.priceUsd).toFixed(6)}\n`; output += `- Volume (24h USD): $${new BigNumber(pair.volume.h24).toFixed(2)}\n`; - output += `- Boosts Active: ${pair.boosts.active}\n`; + output += `- Boosts Active: ${pair.boosts && pair.boosts.active}\n`; output += `- Liquidity USD: $${new BigNumber(pair.liquidity.usd).toFixed(2)}\n`; }); } From ab1b85c0ae11127b15869f807396808a9ce762be Mon Sep 17 00:00:00 2001 From: MarcoMandar Date: Thu, 24 Oct 2024 16:00:19 +0300 Subject: [PATCH 5/5] update Signed-off-by: MarcoMandar --- src/providers/token.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/token.ts b/src/providers/token.ts index 64a5beabf6..343a5b5179 100644 --- a/src/providers/token.ts +++ b/src/providers/token.ts @@ -29,7 +29,7 @@ const PROVIDER_CONFIG = { }, TOKEN_SECURITY_ENDPOINT: "/defi/token_security?address=", TOKEN_TRADE_DATA_ENDPOINT: "/defi/v3/token/trade-data/single?address=", - DEX_SCREENER_API: "https://api.dexscreener.com/latest/dex/tokens/", // Example endpoint + DEX_SCREENER_API: "https://api.dexscreener.com/latest/dex/tokens/", }; export class TokenProvider { @@ -490,7 +490,7 @@ export class TokenProvider { let page = 1; const limit = 1000; let cursor; - + //HELIOUS_API_KEY needs to be added const url = `https://mainnet.helius-rpc.com/?api-key=${settings.HELIOUS_API_KEY || ""}`; console.log({ url });