diff --git a/packages/plugin-solana/src/providers/simulationSellingService.ts b/packages/plugin-solana/src/providers/simulationSellingService.ts index 88b8729f5cd..a0a24ca6a26 100644 --- a/packages/plugin-solana/src/providers/simulationSellingService.ts +++ b/packages/plugin-solana/src/providers/simulationSellingService.ts @@ -18,7 +18,7 @@ interface SellDetails { sell_recommender_id: string | null; } -export class simulationSellingService { +export class SimulationSellingService { private trustScoreDb: TrustScoreDatabase; private walletProvider: WalletProvider; private connection: Connection; @@ -178,7 +178,7 @@ export class simulationSellingService { await this.startListeners(); } - private async startListeners() { + public async startListeners() { // scanning recommendations and selling console.log("Scanning for token performances..."); const tokenPerformances = @@ -203,25 +203,60 @@ export class simulationSellingService { this.walletProvider, this.runtime.cacheManager ); - const shouldTrade = await tokenProvider.shouldTradeToken(); - if (shouldTrade) { - const balance = tokenPerformance.balance; - const sell_recommender_id = tokenPerformance.recommenderId; - const tokenAddress = tokenPerformance.tokenAddress; - const process = await this.startProcessInTheSonarBackend( - tokenAddress, - balance, - true, - sell_recommender_id, - tokenPerformance.initial_mc - ); - if (process) { - this.runningProcesses.add(tokenAddress); - } + // const shouldTrade = await tokenProvider.shouldTradeToken(); + // if (shouldTrade) { + const balance = tokenPerformance.balance; + const sell_recommender_id = tokenPerformance.recommenderId; + const tokenAddress = tokenPerformance.tokenAddress; + const process = await this.startProcessInTheSonarBackend( + tokenAddress, + balance, + true, + sell_recommender_id, + tokenPerformance.initial_mc + ); + if (process) { + this.runningProcesses.add(tokenAddress); } + // } }); } + public processTokenPerformance(tokenAddress: string) { + try { + const runningProcesses = this.runningProcesses; + // check if token is already being processed + if (runningProcesses.has(tokenAddress)) { + console.log(`Token ${tokenAddress} is already being processed`); + return; + } + const tokenPerformance = + this.trustScoreDb.getTokenPerformance(tokenAddress); + const tokenProvider = new TokenProvider( + tokenPerformance.tokenAddress, + this.walletProvider, + this.runtime.cacheManager + ); + const balance = tokenPerformance.balance; + const sell_recommender_id = tokenPerformance.recommenderId; + const process = this.startProcessInTheSonarBackend( + tokenAddress, + balance, + true, + sell_recommender_id, + tokenPerformance.initial_mc + ); + if (process) { + this.runningProcesses.add(tokenAddress); + } + } catch (error) { + console.error( + `Error getting token performance for token ${tokenAddress}:`, + error + ); + } + } + private async startProcessInTheSonarBackend( tokenAddress: string, balance: number, diff --git a/packages/plugin-solana/src/providers/token.ts b/packages/plugin-solana/src/providers/token.ts index 174f47fd290..37c5377ab5d 100644 --- a/packages/plugin-solana/src/providers/token.ts +++ b/packages/plugin-solana/src/providers/token.ts @@ -9,12 +9,14 @@ import { TokenTradeData, CalculatedBuyAmounts, Prices, + TokenCodex, } from "../types/token.ts"; import NodeCache from "node-cache"; import * as path from "path"; import { toBN } from "../bignumber.ts"; import { WalletProvider, Item } from "./wallet.ts"; import { Connection, PublicKey } from "@solana/web3.js"; +import axios from "axios"; const PROVIDER_CONFIG = { BIRDEYE_API: "https://public-api.birdeye.so", @@ -36,6 +38,8 @@ const PROVIDER_CONFIG = { export class TokenProvider { private cache: NodeCache; private cacheKey: string = "solana/tokens"; + private NETWORK_ID = 1399811149; + private GRAPHQL_ENDPOINT = "https://graph.codex.io/graphql"; constructor( // private connection: Connection, @@ -155,6 +159,89 @@ export class TokenProvider { } } + async fetchTokenCodex(): Promise { + try { + const cacheKey = `token_${this.tokenAddress}`; + const cachedData = this.getCachedData(cacheKey); + if (cachedData) { + console.log( + `Returning cached token data for ${this.tokenAddress}.` + ); + return cachedData; + } + const query = ` + query Token($address: String!, $networkId: Int!) { + token(input: { address: $address, networkId: $networkId }) { + id + address + cmcId + decimals + name + symbol + totalSupply + isScam + info { + circulatingSupply + imageThumbUrl + } + explorerData { + blueCheckmark + description + tokenType + } + } + } + `; + + const variables = { + address: this.tokenAddress, + networkId: this.NETWORK_ID, // Replace with your network ID + }; + + const response = await axios.post( + this.GRAPHQL_ENDPOINT, + { + query, + variables, + }, + { + headers: { + "Content-Type": "application/json", + Authorization: settings.CODEX_API_KEY, + }, + } + ); + + const token = response.data?.data?.token; + + if (!token) { + throw new Error(`No data returned for token ${tokenAddress}`); + } + + this.setCachedData(cacheKey, token); + + return { + id: token.id, + address: token.address, + cmcId: token.cmcId, + decimals: token.decimals, + name: token.name, + symbol: token.symbol, + totalSupply: token.totalSupply, + circulatingSupply: token.info?.circulatingSupply, + imageThumbUrl: token.info?.imageThumbUrl, + blueCheckmark: token.explorerData?.blueCheckmark, + isScam: token.isScam ? true : false, + }; + } catch (error) { + console.error( + "Error fetching token data from Codex:", + error.message + ); + return {} as TokenCodex; + } + } + async fetchPrices(): Promise { try { const cacheKey = "prices"; @@ -813,6 +900,8 @@ export class TokenProvider { ); const security = await this.fetchTokenSecurity(); + const tokenCodex = await this.fetchTokenCodex(); + console.log(`Fetching trade data for token: ${this.tokenAddress}`); const tradeData = await this.fetchTokenTradeData(); @@ -862,6 +951,7 @@ export class TokenProvider { dexScreenerData: dexData, isDexScreenerListed, isDexScreenerPaid, + tokenCodex, }; // console.log("Processed token data:", processedData); diff --git a/packages/plugin-solana/src/providers/trustScoreProvider.ts b/packages/plugin-solana/src/providers/trustScoreProvider.ts index d09e1126248..a2537d26fb6 100644 --- a/packages/plugin-solana/src/providers/trustScoreProvider.ts +++ b/packages/plugin-solana/src/providers/trustScoreProvider.ts @@ -10,6 +10,7 @@ import { Connection, PublicKey } from "@solana/web3.js"; import { getAssociatedTokenAddress } from "@solana/spl-token"; import { TokenProvider } from "./token.ts"; import { WalletProvider } from "./wallet.ts"; +import { SimulationSellingService } from "./simulationSellingService.ts"; import { TrustScoreDatabase, RecommenderMetrics, @@ -53,6 +54,7 @@ interface TokenRecommendationSummary { export class TrustScoreManager { private tokenProvider: TokenProvider; private trustScoreDb: TrustScoreDatabase; + private simulationSellingService: SimulationSellingService; private connection: Connection; private baseMint: PublicKey; private DECAY_RATE = 0.95; @@ -73,6 +75,10 @@ export class TrustScoreManager { ); this.backend = runtime.getSetting("BACKEND_URL"); this.backendToken = runtime.getSetting("BACKEND_TOKEN"); + this.simulationSellingService = new SimulationSellingService( + runtime, + this.tokenProvider + ); } //getRecommenederBalance @@ -147,9 +153,9 @@ export class TrustScoreManager { liquidityChange24h: 0, holderChange24h: processedData.tradeData.unique_wallet_24h_change_percent, - rugPull: false, // TODO: Implement rug pull detection - isScam: false, // TODO: Implement scam detection - marketCapChange24h: 0, // TODO: Implement market cap change + rugPull: false, + isScam: processedData.tokenCodex.isScam, + marketCapChange24h: 0, sustainedGrowth: sustainedGrowth, rapidDump: isRapidDump, suspiciousVolume: suspiciousVolume, @@ -362,6 +368,7 @@ export class TrustScoreManager { const buySol = data.buy_amount / parseFloat(solPrice); const buy_value_usd = data.buy_amount * processedData.tradeData.price; const token = await this.tokenProvider.fetchTokenTradeData(); + const tokenCodex = await this.tokenProvider.fetchTokenCodex(); const tokenPrice = token.price; tokensBalance = buy_value_usd / tokenPrice; @@ -418,7 +425,7 @@ export class TrustScoreManager { holderChange24h: processedData.tradeData.unique_wallet_24h_change_percent, rugPull: false, - isScam: false, + isScam: tokenCodex.isScam, marketCapChange24h: 0, sustainedGrowth: false, rapidDump: false, @@ -446,6 +453,7 @@ export class TrustScoreManager { }; this.trustScoreDb.addTransaction(transaction); } + this.simulationSellingService.processTokenPerformance(tokenAddress); // api call to update trade performance this.createTradeInBe(tokenAddress, recommenderId, username, data); return creationData; diff --git a/packages/plugin-solana/src/providers/wallet.ts b/packages/plugin-solana/src/providers/wallet.ts index 22d9f050bf6..842d2aab930 100644 --- a/packages/plugin-solana/src/providers/wallet.ts +++ b/packages/plugin-solana/src/providers/wallet.ts @@ -2,12 +2,14 @@ import { IAgentRuntime, Memory, Provider, State } from "@ai16z/eliza"; import { Connection, PublicKey } from "@solana/web3.js"; import BigNumber from "bignumber.js"; import NodeCache from "node-cache"; +import axios from "axios"; // Provider configuration const PROVIDER_CONFIG = { BIRDEYE_API: "https://public-api.birdeye.so", MAX_RETRIES: 3, RETRY_DELAY: 2000, DEFAULT_RPC: "https://api.mainnet-beta.solana.com", + GRAPHQL_ENDPOINT: "https://graph.codex.io/graphql", TOKEN_ADDRESSES: { SOL: "So11111111111111111111111111111111111111112", BTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", @@ -160,6 +162,105 @@ export class WalletProvider { } } + async fetchPortfolioValueCodex(runtime): Promise { + try { + const cacheKey = `portfolio-${this.walletPublicKey.toBase58()}`; + const cachedValue = await this.cache.get(cacheKey); + + if (cachedValue) { + console.log("Cache hit for fetchPortfolioValue"); + return cachedValue; + } + console.log("Cache miss for fetchPortfolioValue"); + + const query = ` + query Balances($walletId: String!, $cursor: String) { + balances(input: { walletId: $walletId, cursor: $cursor }) { + cursor + items { + walletId + tokenId + balance + shiftedBalance + } + } + } + `; + + const variables = { + walletId: `${this.walletPublicKey.toBase58()}:${1399811149}`, + cursor: null, + }; + + const response = await axios.post( + PROVIDER_CONFIG.GRAPHQL_ENDPOINT, + { + query, + variables, + }, + { + headers: { + "Content-Type": "application/json", + Authorization: + runtime.getSetting("CODEX_API_KEY", "") || "", + }, + } + ); + + const data = response.data?.data?.balances?.items; + + if (!data || data.length === 0) { + console.error("No portfolio data available", data); + throw new Error("No portfolio data available"); + } + + // Fetch token prices + const prices = await this.fetchPrices(runtime); + const solPriceInUSD = new BigNumber(prices.solana.usd.toString()); + + // Reformat items + const items: Item[] = data.map((item: any) => { + return { + name: "Unknown", + address: item.tokenId.split(":")[0], + symbol: item.tokenId.split(":")[0], + decimals: 6, + balance: item.balance, + uiAmount: item.shiftedBalance.toString(), + priceUsd: "", + valueUsd: "", + valueSol: "", + }; + }); + + // Calculate total portfolio value + const totalUsd = items.reduce( + (sum, item) => sum.plus(new BigNumber(item.valueUsd)), + new BigNumber(0) + ); + + const totalSol = totalUsd.div(solPriceInUSD); + + const portfolio: WalletPortfolio = { + totalUsd: totalUsd.toFixed(6), + totalSol: totalSol.toFixed(6), + items: items.sort((a, b) => + new BigNumber(b.valueUsd) + .minus(new BigNumber(a.valueUsd)) + .toNumber() + ), + }; + + // Cache the portfolio for future requests + await this.cache.set(cacheKey, portfolio, 60 * 1000); // Cache for 1 minute + + return portfolio; + } catch (error) { + console.error("Error fetching portfolio:", error); + throw error; + } + } + async fetchPrices(runtime): Promise { try { const cacheKey = "prices"; diff --git a/packages/plugin-solana/src/types/token.ts b/packages/plugin-solana/src/types/token.ts index 4f8d3e355e4..1fca4c37c32 100644 --- a/packages/plugin-solana/src/types/token.ts +++ b/packages/plugin-solana/src/types/token.ts @@ -7,6 +7,20 @@ export interface TokenSecurityData { top10HolderPercent: number; } +export interface TokenCodex { + id: string; + address: string; + cmcId: number; + decimals: number; + name: string; + symbol: string; + totalSupply: string; + circulatingSupply: string; + imageThumbUrl: string; + blueCheckmark: boolean; + isScam: boolean; +} + export interface TokenTradeData { address: string; holder: number; @@ -213,6 +227,7 @@ export interface ProcessedTokenData { isDexScreenerListed: boolean; isDexScreenerPaid: boolean; + tokenCodex: TokenCodex; } export interface DexScreenerPair {