diff --git a/core/src/adapters/trustScoreDatabase.ts b/core/src/adapters/trustScoreDatabase.ts new file mode 100644 index 00000000000..a5d707bae31 --- /dev/null +++ b/core/src/adapters/trustScoreDatabase.ts @@ -0,0 +1,972 @@ +// src/adapters/sqlite/trustScoreDatabase.ts + +import { Database } from "better-sqlite3"; +import { v4 as uuidv4 } from "uuid"; +import { load } from "./sqlite/sqlite_vec.ts"; + +// Define interfaces +export interface Recommender { + id: string; // UUID + address: string; + solanaPubkey?: string; + telegramId?: string; + discordId?: string; + twitterId?: string; + ip?: string; +} + +export interface RecommenderMetrics { + recommenderId: string; + trustScore: number; + totalRecommendations: number; + successfulRecs: number; + avgTokenPerformance: number; + riskScore: number; + consistencyScore: number; + virtualConfidence: number; + lastUpdated: Date; +} + +export interface TokenPerformance { + tokenAddress: string; + priceChange24h: number; + volumeChange24h: number; + trade_24h_change: number; + liquidity: number; + liquidityChange24h: number; + holderChange24h: number; + rugPull: boolean; + isScam: boolean; + marketCapChange24h: number; + sustainedGrowth: boolean; + rapidDump: boolean; + suspiciousVolume: boolean; + lastUpdated: Date; +} + +export interface TokenRecommendation { + id: string; // UUID + recommenderId: string; + tokenAddress: string; + timestamp: Date; + initialMarketCap?: number; + initialLiquidity?: number; + initialPrice?: number; +} +export interface RecommenderMetricsHistory { + historyId: string; // UUID + recommenderId: string; + trustScore: number; + totalRecommendations: number; + successfulRecs: number; + avgTokenPerformance: number; + riskScore: number; + consistencyScore: number; + virtualConfidence: number; + recordedAt: Date; +} + +export interface TradePerformance { + token_address: string; + recommender_id: string; + buy_price: number; + sell_price: number; + buy_timeStamp: string; + sell_timeStamp: string; + buy_amount: number; + sell_amount: number; + buy_sol: number; + received_sol: number; + buy_value_usd: number; + sell_value_usd: number; + profit_usd: number; + profit_percent: number; + buy_market_cap: number; + sell_market_cap: number; + market_cap_change: number; + buy_liquidity: number; + sell_liquidity: number; + liquidity_change: number; + last_updated: string; + rapidDump: boolean; +} + +interface RecommenderMetricsRow { + recommender_id: string; + trust_score: number; + total_recommendations: number; + successful_recs: number; + avg_token_performance: number; + risk_score: number; + consistency_score: number; + virtual_confidence: number; + last_updated: string; +} + +interface TokenPerformanceRow { + token_address: string; + price_change_24h: number; + volume_change_24h: number; + trade_24h_change: number; + liquidity: number; + liquidity_change_24h: number; + holder_change_24h: number; + rug_pull: number; + is_scam: number; + market_cap_change24h: number; + sustained_growth: number; + rapid_dump: number; + suspicious_volume: number; + last_updated: string; +} + +export class TrustScoreDatabase { + private db: Database; + + constructor(db: Database) { + this.db = db; + load(db); + // check if the tables exist, if not create them + const tables = this.db + .prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('recommenders', 'recommender_metrics', 'token_performance', 'token_recommendations', 'recommender_metrics_history');" + ) + .all(); + if (tables.length !== 5) { + this.initializeSchema(); + } + } + + private initializeSchema() { + // Enable Foreign Key Support + this.db.exec(`PRAGMA foreign_keys = ON;`); + + // Create Recommenders Table + this.db.exec(` + CREATE TABLE IF NOT EXISTS recommenders ( + id TEXT PRIMARY KEY, + address TEXT UNIQUE NOT NULL, + solana_pubkey TEXT UNIQUE, + telegram_id TEXT UNIQUE, + discord_id TEXT UNIQUE, + twitter_id TEXT UNIQUE, + ip TEXT + ); + `); + + // Create RecommenderMetrics Table + this.db.exec(` + CREATE TABLE IF NOT EXISTS recommender_metrics ( + recommender_id TEXT PRIMARY KEY, + trust_score REAL DEFAULT 0, + total_recommendations INTEGER DEFAULT 0, + successful_recs INTEGER DEFAULT 0, + avg_token_performance REAL DEFAULT 0, + risk_score REAL DEFAULT 0, + consistency_score REAL DEFAULT 0, + virtual_confidence REAL DEFAULT 0, + last_updated DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (recommender_id) REFERENCES recommenders(id) ON DELETE CASCADE + ); + `); + + // Create TokenPerformance Table + this.db.exec(` + CREATE TABLE IF NOT EXISTS token_performance ( + token_address TEXT PRIMARY KEY, + price_change_24h REAL, + volume_change_24h REAL, + trade_24h_change REAL, + liquidity REAL, + liquidity_change_24h REAL, + holder_change_24h REAL, + rug_pull BOOLEAN DEFAULT FALSE, + is_scam BOOLEAN DEFAULT FALSE, + market_cap_change24h REAL, + sustained_growth BOOLEAN DEFAULT FALSE, + rapid_dump BOOLEAN DEFAULT FALSE, + suspicious_volume BOOLEAN DEFAULT FALSE, + last_updated DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + + // Create TokenRecommendations Table + this.db.exec(` + CREATE TABLE IF NOT EXISTS token_recommendations ( + id TEXT PRIMARY KEY, + recommender_id TEXT NOT NULL, + token_address TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + initial_market_cap REAL, + initial_liquidity REAL, + initial_price REAL, + FOREIGN KEY (recommender_id) REFERENCES recommenders(id) ON DELETE CASCADE, + FOREIGN KEY (token_address) REFERENCES token_performance(token_address) ON DELETE CASCADE + ); + `); + + // ----- Create RecommenderMetricsHistory Table ----- + this.db.exec(` + CREATE TABLE IF NOT EXISTS recommender_metrics_history ( + history_id TEXT PRIMARY KEY, + recommender_id TEXT NOT NULL, + trust_score REAL, + total_recommendations INTEGER, + successful_recs INTEGER, + avg_token_performance REAL, + risk_score REAL, + consistency_score REAL, + virtual_confidence REAL DEFAULT 0, + recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (recommender_id) REFERENCES recommenders(id) ON DELETE CASCADE + ); + `); + + // ----- Create TradePerformance Tables ----- + this.db.exec(` + CREATE TABLE IF NOT EXISTS trade ( + token_address TEXT NOT NULL, + recommender_id TEXT NOT NULL, + sell_recommender_id TEXT, + buy_price REAL NOT NULL, + sell_price REAL, + buy_timeStamp TEXT NOT NULL, + sell_timeStamp TEXT, + buy_amount REAL NOT NULL, + sell_amount REAL, + buy_sol REAL NOT NULL, + received_sol REAL, + buy_value_usd REAL NOT NULL, + sell_value_usd REAL, + profit_usd REAL, + profit_percent REAL, + buy_market_cap REAL NOT NULL, + sell_market_cap REAL, + market_cap_change REAL, + buy_liquidity REAL NOT NULL, + sell_liquidity REAL, + liquidity_change REAL, + last_updated TEXT DEFAULT (datetime('now')), + rapidDump BOOLEAN DEFAULT FALSE, + PRIMARY KEY (token_address, recommender_id, buy_timeStamp), + FOREIGN KEY (token_address) REFERENCES token_performance(token_address) ON DELETE CASCADE, + FOREIGN KEY (recommender_id) REFERENCES recommenders(id) ON DELETE CASCADE + ); + `); + // create trade simulation table + this.db.exec(` + CREATE TABLE IF NOT EXISTS simulation_trade ( + token_address TEXT NOT NULL, + recommender_id TEXT NOT NULL, + buy_price REAL NOT NULL, + sell_price REAL, + buy_timeStamp TEXT NOT NULL, + sell_timeStamp TEXT, + buy_amount REAL NOT NULL, + sell_amount REAL, + buy_sol REAL NOT NULL, + received_sol REAL, + buy_value_usd REAL NOT NULL, + sell_value_usd REAL, + profit_usd REAL, + profit_percent REAL, + buy_market_cap REAL NOT NULL, + sell_market_cap REAL, + market_cap_change REAL, + buy_liquidity REAL NOT NULL, + sell_liquidity REAL, + liquidity_change REAL, + last_updated TEXT DEFAULT (datetime('now')), + rapidDump BOOLEAN DEFAULT FALSE, + PRIMARY KEY (token_address, recommender_id, buy_timeStamp), + FOREIGN KEY (token_address) REFERENCES token_performance(token_address) ON DELETE CASCADE, + FOREIGN KEY (recommender_id) REFERENCES recommenders(id) ON DELETE CASCADE + ); + `); + } + + /** + * Adds a new recommender to the database. + * @param recommender Recommender object + * @returns boolean indicating success + */ + addRecommender(recommender: Recommender): string | null { + const sql = ` + INSERT INTO recommenders (id, address, solana_pubkey, telegram_id, discord_id, twitter_id, ip) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(address) DO NOTHING; + `; + try { + const id = recommender.id || uuidv4(); + const result = this.db + .prepare(sql) + .run( + id, + recommender.address, + recommender.solanaPubkey || null, + recommender.telegramId || null, + recommender.discordId || null, + recommender.twitterId || null, + recommender.ip || null + ); + return result.changes > 0 ? id : null; + } catch (error) { + console.error("Error adding recommender:", error); + return null; + } + } + + /** + * Retrieves a recommender by any identifier. + * @param identifier Any of the recommender's identifiers + * @returns Recommender object or null + */ + getRecommender(identifier: string): Recommender | null { + const sql = ` + SELECT * FROM recommenders + WHERE id = ? OR address = ? OR solana_pubkey = ? OR telegram_id = ? OR discord_id = ? OR twitter_id = ?; + `; + const recommender = this.db + .prepare(sql) + .get( + identifier, + identifier, + identifier, + identifier, + identifier, + identifier + ) as Recommender | undefined; + return recommender || null; + } + + /** + * Initializes metrics for a recommender if not present. + * @param recommenderId Recommender's UUID + */ + initializeRecommenderMetrics(recommenderId: string): boolean { + const sql = ` + INSERT OR IGNORE INTO recommender_metrics (recommender_id) + VALUES (?); + `; + try { + const result = this.db.prepare(sql).run(recommenderId); + return result.changes > 0; + } catch (error) { + console.error("Error initializing recommender metrics:", error); + return false; + } + } + + /** + * Retrieves metrics for a recommender. + * @param recommenderId Recommender's UUID + * @returns RecommenderMetrics object or null + */ + getRecommenderMetrics(recommenderId: string): RecommenderMetrics | null { + const sql = `SELECT * FROM recommender_metrics WHERE recommender_id = ?;`; + const row = this.db.prepare(sql).get(recommenderId) as + | RecommenderMetricsRow + | undefined; + if (!row) return null; + + return { + recommenderId: row.recommender_id, + trustScore: row.trust_score, + totalRecommendations: row.total_recommendations, + successfulRecs: row.successful_recs, + avgTokenPerformance: row.avg_token_performance, + riskScore: row.risk_score, + consistencyScore: row.consistency_score, + virtualConfidence: row.virtual_confidence, + lastUpdated: new Date(row.last_updated), + }; + } + + /** + * Logs the current metrics of a recommender into the history table. + * @param recommenderId Recommender's UUID + */ + logRecommenderMetricsHistory(recommenderId: string): void { + // Retrieve current metrics + const currentMetrics = this.getRecommenderMetrics(recommenderId); + if (!currentMetrics) { + console.warn(`No metrics found for recommender ID: ${recommenderId}`); + return; + } + + // Create a history entry + const history: RecommenderMetricsHistory = { + historyId: uuidv4(), + recommenderId: currentMetrics.recommenderId, + trustScore: currentMetrics.trustScore, + totalRecommendations: currentMetrics.totalRecommendations, + successfulRecs: currentMetrics.successfulRecs, + avgTokenPerformance: currentMetrics.avgTokenPerformance, + riskScore: currentMetrics.riskScore, + consistencyScore: currentMetrics.consistencyScore, + virtualConfidence: currentMetrics.virtualConfidence, + recordedAt: new Date(), // Current timestamp + }; + + // Insert into recommender_metrics_history table + const sql = ` + INSERT INTO recommender_metrics_history ( + history_id, + recommender_id, + trust_score, + total_recommendations, + successful_recs, + avg_token_performance, + risk_score, + consistency_score, + recorded_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + `; + try { + this.db + .prepare(sql) + .run( + history.historyId, + history.recommenderId, + history.trustScore, + history.totalRecommendations, + history.successfulRecs, + history.avgTokenPerformance, + history.riskScore, + history.consistencyScore, + history.recordedAt.toISOString() + ); + console.log( + `Logged metrics history for recommender ID: ${recommenderId}` + ); + } catch (error) { + console.error("Error logging recommender metrics history:", error); + } + } + + /** + * Updates metrics for a recommender. + * @param metrics RecommenderMetrics object + */ + updateRecommenderMetrics(metrics: RecommenderMetrics): void { + // Log current metrics before updating + this.logRecommenderMetricsHistory(metrics.recommenderId); + + const sql = ` + UPDATE recommender_metrics + SET trust_score = ?, + total_recommendations = ?, + successful_recs = ?, + avg_token_performance = ?, + risk_score = ?, + consistency_score = ?, + last_updated = CURRENT_TIMESTAMP + WHERE recommender_id = ?; + `; + try { + this.db + .prepare(sql) + .run( + metrics.trustScore, + metrics.totalRecommendations, + metrics.successfulRecs, + metrics.avgTokenPerformance, + metrics.riskScore, + metrics.consistencyScore, + metrics.recommenderId + ); + console.log( + `Updated metrics for recommender ID: ${metrics.recommenderId}` + ); + } catch (error) { + console.error("Error updating recommender metrics:", error); + } + } + + // ----- TokenPerformance Methods ----- + + /** + * Adds or updates token performance metrics. + * @param performance TokenPerformance object + */ + upsertTokenPerformance(performance: TokenPerformance): boolean { + const sql = ` + INSERT INTO token_performance ( + token_address, + price_change_24h, + volume_change_24h, + trade_24h_change, + liquidity, + liquidity_change_24h, + holder_change_24h, + rug_pull, + is_scam, + market_cap_change24h, + sustained_growth, + rapid_dump, + suspicious_volume, + last_updated + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(token_address) DO UPDATE SET + price_change_24h = excluded.price_change_24h, + volume_change_24h = excluded.volume_change_24h, + trade_24h_change = excluded.trade_24h_change, + liquidity = excluded.liquidity, + liquidity_change_24h = excluded.liquidity_change_24h, + holder_change_24h = excluded.holder_change_24h, + rug_pull = excluded.rug_pull, + is_scam = excluded.is_scam, + market_cap_change24h = excluded.market_cap_change24h, + sustained_growth = excluded.sustained_growth, + rapid_dump = excluded.rapid_dump, + suspicious_volume = excluded.suspicious_volume, + last_updated = CURRENT_TIMESTAMP; + `; + try { + this.db.prepare(sql).run( + performance.tokenAddress, + performance.priceChange24h, + performance.volumeChange24h, + performance.liquidityChange24h, + performance.holderChange24h, // Ensure column name matches schema + performance.rugPull ? 1 : 0, + performance.isScam ? 1 : 0, + performance.marketCapChange24h, + performance.sustainedGrowth ? 1 : 0, + performance.rapidDump ? 1 : 0, + performance.suspiciousVolume ? 1 : 0 + ); + console.log(`Upserted token performance for ${performance.tokenAddress}`); + return true; + } catch (error) { + console.error("Error upserting token performance:", error); + return false; + } + } + + /** + * Retrieves token performance metrics. + * @param tokenAddress Token's address + * @returns TokenPerformance object or null + */ + getTokenPerformance(tokenAddress: string): TokenPerformance | null { + const sql = `SELECT * FROM token_performance WHERE token_address = ?;`; + const row = this.db.prepare(sql).get(tokenAddress) as + | TokenPerformanceRow + | undefined; + if (!row) return null; + + return { + tokenAddress: row.token_address, + priceChange24h: row.price_change_24h, + volumeChange24h: row.volume_change_24h, + trade_24h_change: row.trade_24h_change, + liquidity: row.liquidity, + liquidityChange24h: row.liquidity_change_24h, + holderChange24h: row.holder_change_24h, + rugPull: row.rug_pull === 1, + isScam: row.is_scam === 1, + marketCapChange24h: row.market_cap_change24h, + sustainedGrowth: row.sustained_growth === 1, + rapidDump: row.rapid_dump === 1, + suspiciousVolume: row.suspicious_volume === 1, + lastUpdated: new Date(row.last_updated), + }; + } + + // ----- TokenRecommendations Methods ----- + + /** + * Adds a new token recommendation. + * @param recommendation TokenRecommendation object + * @returns boolean indicating success + */ + addTokenRecommendation(recommendation: TokenRecommendation): boolean { + const sql = ` + INSERT INTO token_recommendations ( + id, + recommender_id, + token_address, + timestamp, + initial_market_cap, + initial_liquidity, + initial_price + ) VALUES (?, ?, ?, ?, ?, ?, ?); + `; + try { + this.db + .prepare(sql) + .run( + recommendation.id || uuidv4(), + recommendation.recommenderId, + recommendation.tokenAddress, + recommendation.timestamp || new Date(), + recommendation.initialMarketCap || null, + recommendation.initialLiquidity || null, + recommendation.initialPrice || null + ); + return true; + } catch (error) { + console.error("Error adding token recommendation:", error); + return false; + } + } + + /** + * Retrieves all recommendations made by a recommender. + * @param recommenderId Recommender's UUID + * @returns Array of TokenRecommendation objects + */ + getRecommendationsByRecommender( + recommenderId: string + ): TokenRecommendation[] { + const sql = `SELECT * FROM token_recommendations WHERE recommender_id = ? ORDER BY timestamp DESC;`; + const rows = this.db.prepare(sql).all(recommenderId) as Array<{ + id: string; + recommender_id: string; + token_address: string; + timestamp: string; + initial_market_cap: number | null; + initial_liquidity: number | null; + initial_price: number | null; + }>; + + return rows.map((row) => ({ + id: row.id, + recommenderId: row.recommender_id, + tokenAddress: row.token_address, + timestamp: new Date(row.timestamp), + initialMarketCap: row.initial_market_cap, + initialLiquidity: row.initial_liquidity, + initialPrice: row.initial_price, + })); + } + + /** + * Retrieves all recommendations for a specific token. + * @param tokenAddress Token's address + * @returns Array of TokenRecommendation objects + */ + getRecommendationsByToken(tokenAddress: string): TokenRecommendation[] { + const sql = `SELECT * FROM token_recommendations WHERE token_address = ? ORDER BY timestamp DESC;`; + const rows = this.db.prepare(sql).all(tokenAddress) as Array<{ + id: string; + recommender_id: string; + token_address: string; + timestamp: string; + initial_market_cap: number | null; + initial_liquidity: number | null; + initial_price: number | null; + }>; + + return rows.map((row) => ({ + id: row.id, + recommenderId: row.recommender_id, + tokenAddress: row.token_address, + timestamp: new Date(row.timestamp), + initialMarketCap: row.initial_market_cap ?? undefined, + initialLiquidity: row.initial_liquidity ?? undefined, + initialPrice: row.initial_price ?? undefined, + })); + } + + /** + * Retrieves all recommendations within a specific timeframe. + * @param startDate Start date + * @param endDate End date + * @returns Array of TokenRecommendation objects + */ + getRecommendationsByDateRange( + startDate: Date, + endDate: Date + ): TokenRecommendation[] { + const sql = ` + SELECT * FROM token_recommendations + WHERE timestamp BETWEEN ? AND ? + ORDER BY timestamp DESC; + `; + const rows = this.db + .prepare(sql) + .all(startDate.toISOString(), endDate.toISOString()) as Array<{ + id: string; + recommender_id: string; + token_address: string; + timestamp: string; + initial_market_cap: number | null; + initial_liquidity: number | null; + initial_price: number | null; + }>; + + return rows.map((row) => ({ + id: row.id, + recommenderId: row.recommender_id, + tokenAddress: row.token_address, + timestamp: new Date(row.timestamp), + initialMarketCap: row.initial_market_cap ?? undefined, + initialLiquidity: row.initial_liquidity ?? undefined, + initialPrice: row.initial_price ?? undefined, + })); + } + + /** + * Retrieves historical metrics for a recommender. + * @param recommenderId Recommender's UUID + * @returns Array of RecommenderMetricsHistory objects + */ + getRecommenderMetricsHistory( + recommenderId: string + ): RecommenderMetricsHistory[] { + const sql = ` + SELECT * FROM recommender_metrics_history + WHERE recommender_id = ? + ORDER BY recorded_at DESC; + `; + const rows = this.db.prepare(sql).all(recommenderId) as Array<{ + history_id: string; + recommender_id: string; + trust_score: number; + total_recommendations: number; + successful_recs: number; + avg_token_performance: number; + risk_score: number; + consistency_score: number; + virtual_confidence: number; + recorded_at: string; + }>; + + return rows.map((row) => ({ + historyId: row.history_id, + recommenderId: row.recommender_id, + trustScore: row.trust_score, + totalRecommendations: row.total_recommendations, + successfulRecs: row.successful_recs, + avgTokenPerformance: row.avg_token_performance, + riskScore: row.risk_score, + consistencyScore: row.consistency_score, + virtualConfidence: row.virtual_confidence, + recordedAt: new Date(row.recorded_at), + })); + } + + /** + * Inserts a new trade performance into the specified table. + * @param trade The TradePerformance object containing trade details. + * @param isSimulation Whether the trade is a simulation. If true, inserts into simulation_trade; otherwise, into trade. + * @returns boolean indicating success. + */ + addTradePerformance(trade: TradePerformance, isSimulation: boolean): boolean { + const tableName = isSimulation ? "simulation_trade" : "trade"; + const sql = ` + INSERT INTO ${tableName} ( + token_address, + recommender_id, + buy_price, + sell_price, + buy_timeStamp, + sell_timeStamp, + buy_amount, + sell_amount, + buy_sol, + received_sol, + buy_value_usd, + sell_value_usd, + profit_usd, + profit_percent, + buy_market_cap, + sell_market_cap, + market_cap_change, + buy_liquidity, + sell_liquidity, + liquidity_change, + last_updated, + rapidDump + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + `; + try { + this.db + .prepare(sql) + .run( + trade.token_address, + trade.recommender_id, + trade.buy_price, + trade.sell_price || null, + trade.buy_timeStamp, + trade.sell_timeStamp || null, + trade.buy_amount, + trade.sell_amount || null, + trade.buy_sol, + trade.received_sol || null, + trade.buy_value_usd, + trade.sell_value_usd || null, + trade.profit_usd || null, + trade.profit_percent || null, + trade.buy_market_cap, + trade.sell_market_cap || null, + trade.market_cap_change || null, + trade.buy_liquidity, + trade.sell_liquidity || null, + trade.liquidity_change || null, + trade.last_updated || new Date().toISOString(), + trade.rapidDump ? 1 : 0 + ); + console.log(`Inserted trade into ${tableName}:`, trade); + return true; + } catch (error) { + console.error(`Error inserting trade into ${tableName}:`, error); + return false; + } + } + + /** + * Updates an existing trade with sell details. + * @param tokenAddress The address of the token. + * @param recommenderId The UUID of the recommender. + * @param buyTimeStamp The timestamp when the buy occurred. + * @param sellDetails An object containing sell-related details. + * @param isSimulation Whether the trade is a simulation. If true, updates in simulation_trade; otherwise, in trade. + * @returns boolean indicating success. + */ + + updateTradePerformanceOnSell( + tokenAddress: string, + recommenderId: string, + buyTimeStamp: string, + sellDetails: { + sell_price: number; + sell_timeStamp: string; + sell_amount: number; + received_sol: number; + sell_value_usd: number; + profit_usd: number; + profit_percent: number; + sell_market_cap: number; + market_cap_change: number; + sell_liquidity: number; + liquidity_change: number; + rapidDump: boolean; + sell_recommender_id: string | null; + }, + isSimulation: boolean + ): boolean { + const tableName = isSimulation ? "simulation_trade" : "trade"; + const sql = ` + UPDATE ${tableName} + SET + sell_price = ?, + sell_timeStamp = ?, + sell_amount = ?, + received_sol = ?, + sell_value_usd = ?, + profit_usd = ?, + profit_percent = ?, + sell_market_cap = ?, + market_cap_change = ?, + sell_liquidity = ?, + liquidity_change = ?, + rapidDump = ? + sell_recommender_id = ? + WHERE + token_address = ? + AND recommender_id = ? + AND buy_timeStamp = ?; + `; + try { + const result = this.db + .prepare(sql) + .run( + sellDetails.sell_price, + sellDetails.sell_timeStamp, + sellDetails.sell_amount, + sellDetails.received_sol, + sellDetails.sell_value_usd, + sellDetails.profit_usd, + sellDetails.profit_percent, + sellDetails.sell_market_cap, + sellDetails.market_cap_change, + sellDetails.sell_liquidity, + sellDetails.liquidity_change, + sellDetails.rapidDump ? 1 : 0, + tokenAddress, + recommenderId, + buyTimeStamp + ); + + if (result.changes === 0) { + console.warn( + `No trade found to update in ${tableName} for token: ${tokenAddress}, recommender: ${recommenderId}, buyTimeStamp: ${buyTimeStamp}` + ); + return false; + } + + console.log(`Updated trade in ${tableName}:`, { + token_address: tokenAddress, + recommender_id: recommenderId, + buy_timeStamp: buyTimeStamp, + ...sellDetails, + }); + return true; + } catch (error) { + console.error(`Error updating trade in ${tableName}:`, error); + return false; + } + } + + //getTradePerformance + + /** + * Retrieves trade performance metrics. + * @param tokenAddress Token's address + * @param recommenderId Recommender's UUID + * @param buyTimeStamp Timestamp when the buy occurred + * @param isSimulation Whether the trade is a simulation. If true, retrieves from simulation_trade; otherwise, from trade. + * @returns TradePerformance object or null + */ + + getTradePerformance( + tokenAddress: string, + recommenderId: string, + buyTimeStamp: string, + isSimulation: boolean + ): TradePerformance | null { + const tableName = isSimulation ? "simulation_trade" : "trade"; + const sql = `SELECT * FROM ${tableName} WHERE token_address = ? AND recommender_id = ? AND buy_timeStamp = ?;`; + const row = this.db + .prepare(sql) + .get(tokenAddress, recommenderId, buyTimeStamp) as + | TradePerformance + | undefined; + if (!row) return null; + + return { + token_address: row.token_address, + recommender_id: row.recommender_id, + buy_price: row.buy_price, + sell_price: row.sell_price, + buy_timeStamp: row.buy_timeStamp, + sell_timeStamp: row.sell_timeStamp, + buy_amount: row.buy_amount, + sell_amount: row.sell_amount, + buy_sol: row.buy_sol, + received_sol: row.received_sol, + buy_value_usd: row.buy_value_usd, + sell_value_usd: row.sell_value_usd, + profit_usd: row.profit_usd, + profit_percent: row.profit_percent, + buy_market_cap: row.buy_market_cap, + sell_market_cap: row.sell_market_cap, + market_cap_change: row.market_cap_change, + buy_liquidity: row.buy_liquidity, + sell_liquidity: row.sell_liquidity, + liquidity_change: row.liquidity_change, + last_updated: row.last_updated, + rapidDump: row.rapidDump, + }; + } + + /** + * Close the database connection gracefully. + */ + closeConnection(): void { + this.db.close(); + } +} diff --git a/core/src/providers/token.ts b/core/src/providers/token.ts index 62739d8fa2a..ac44d728d87 100644 --- a/core/src/providers/token.ts +++ b/core/src/providers/token.ts @@ -1,18 +1,19 @@ +import { Connection } from "@solana/web3.js"; // import fetch from "cross-fetch"; +import { IAgentRuntime, Memory, Provider, State } from "../core/types"; +import settings from "../core/settings"; import BigNumber from "bignumber.js"; -import * as fs from "fs"; -import NodeCache from "node-cache"; -import * as path from "path"; -import settings from "../core/settings.ts"; -import { IAgentRuntime, Memory, Provider, State } from "../core/types.ts"; import { - DexScreenerData, - HolderData, ProcessedTokenData, TokenSecurityData, TokenTradeData, -} from "../types/token.ts"; -import { fileURLToPath } from "url"; + DexScreenerData, + // DexScreenerPair, + HolderData, +} from "../types/token"; +import NodeCache from "node-cache"; +import * as fs from "fs"; +import * as path from "path"; const PROVIDER_CONFIG = { BIRDEYE_API: "https://public-api.birdeye.so", @@ -39,11 +40,7 @@ export class TokenProvider { private tokenAddress: string ) { this.cache = new NodeCache({ stdTTL: 300 }); // 5 minutes cache - const __filename = fileURLToPath(import.meta.url); - - const __dirname = path.dirname(__filename); - - this.cacheDir = path.join(__dirname, "../../tokencache"); + this.cacheDir = path.join(__dirname, "cache"); if (!fs.existsSync(this.cacheDir)) { fs.mkdirSync(this.cacheDir); } @@ -51,13 +48,20 @@ export 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); 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); } } @@ -71,6 +75,7 @@ export class TokenProvider { 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 { @@ -102,6 +107,7 @@ export class TokenProvider { private async fetchWithRetry( url: string, options: RequestInit = {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { let lastError: Error; @@ -127,9 +133,11 @@ export class TokenProvider { const data = await response.json(); 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; } @@ -147,6 +155,9 @@ export class TokenProvider { 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}`; @@ -165,14 +176,18 @@ export class TokenProvider { top10HolderPercent: data.data.top10HolderPercent, }; this.setCachedData(cacheKey, security); + console.log(`Token security data cached for ${this.tokenAddress}.`); return security; } - async fetchTokenTradeData(runtime: IAgentRuntime): Promise { + 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; } @@ -181,7 +196,7 @@ export class TokenProvider { method: "GET", headers: { accept: "application/json", - "X-API-KEY": runtime.getSetting("BIRDEYE_API_KEY") || "", + "X-API-KEY": settings.BIRDEYE_API_KEY || "", }, }; @@ -405,11 +420,15 @@ export class TokenProvider { 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 fetch(url) .then((res) => res.json()) .catch((err) => { @@ -488,6 +507,7 @@ export class TokenProvider { const cacheKey = `holderList_${this.tokenAddress}`; const cachedData = this.getCachedData(cacheKey); if (cachedData) { + console.log("Returning cached holder list."); return cachedData; } @@ -497,8 +517,10 @@ export class TokenProvider { 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 }); try { + // eslint-disable-next-line no-constant-condition while (true) { const params = { limit: limit, @@ -509,7 +531,7 @@ export class TokenProvider { if (cursor != undefined) { params.cursor = cursor; } - + console.log(`Fetching holders - Page ${page}`); if (page > 2) { break; } @@ -534,9 +556,17 @@ export class TokenProvider { !data.result.token_accounts || data.result.token_accounts.length === 0 ) { + console.log( + `No more holders found. Total pages fetched: ${page - 1}` + ); break; } + console.log( + `Processing ${data.result.token_accounts.length} holders from page ${page}` + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any data.result.token_accounts.forEach((account: any) => { const owner = account.owner; const balance = parseFloat(account.amount); @@ -561,6 +591,8 @@ export class TokenProvider { balance: balance.toString(), })); + console.log(`Total unique holders fetched: ${holders.length}`); + // Cache the result this.setCachedData(cacheKey, holders); @@ -620,27 +652,47 @@ export class TokenProvider { } } - async getProcessedTokenData( - runtime: IAgentRuntime - ): Promise { + async getProcessedTokenData(): Promise { try { + console.log( + `Fetching security data for token: ${this.tokenAddress}` + ); const security = await this.fetchTokenSecurity(); - const tradeData = await this.fetchTokenTradeData(runtime); + 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 && pair.boosts.active > 0 @@ -658,6 +710,7 @@ export class TokenProvider { isDexScreenerPaid, }; + // console.log("Processed token data:", processedData); return processedData; } catch (error) { console.error("Error processing token data:", error); @@ -725,12 +778,14 @@ export class TokenProvider { } output += `\n`; + console.log("Formatted token data:", output); return output; } - async getFormattedTokenReport(runtime: IAgentRuntime): Promise { + async getFormattedTokenReport(): Promise { try { - const processedData = await this.getProcessedTokenData(runtime); + console.log("Generating formatted token report..."); + const processedData = await this.getProcessedTokenData(); return this.formatTokenData(processedData); } catch (error) { console.error("Error generating token report:", error); @@ -740,15 +795,18 @@ export class TokenProvider { } const tokenAddress = PROVIDER_CONFIG.TOKEN_ADDRESSES.Example; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const connection = new Connection(PROVIDER_CONFIG.DEFAULT_RPC); const tokenProvider: Provider = { get: async ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars runtime: IAgentRuntime, _message: Memory, _state?: State ): Promise => { try { const provider = new TokenProvider(tokenAddress); - return provider.getFormattedTokenReport(runtime); + return provider.getFormattedTokenReport(); } catch (error) { console.error("Error fetching token data:", error); return "Unable to fetch token information. Please try again later."; diff --git a/core/src/providers/trustScoreProvider.ts b/core/src/providers/trustScoreProvider.ts new file mode 100644 index 00000000000..db20dc5217f --- /dev/null +++ b/core/src/providers/trustScoreProvider.ts @@ -0,0 +1,366 @@ +import { + ProcessedTokenData, + TokenSecurityData, + // TokenTradeData, + // DexScreenerData, + // DexScreenerPair, + // HolderData, +} from "../types/token"; +import { Connection, PublicKey } from "@solana/web3.js"; + +import { TokenProvider } from "./token"; +import WalletProvider from "./balances"; +import { + TrustScoreDatabase, + RecommenderMetrics, + TokenPerformance, + TradePerformance, +} from "../adapters/trustScoreDatabase"; +import settings from "../core/settings"; + +const Wallet = settings.MAIN_WALLET_ADDRESS; +interface TradeData { + buy_amount: number; + is_simulation: boolean; +} +interface sellDetails { + sell_amount: number; + sell_recommender_id: string | null; +} +export class TrustScoreProvider { + private tokenProvider: TokenProvider; + private trustScoreDb: TrustScoreDatabase; + + constructor( + tokenProvider: TokenProvider, + trustScoreDb: TrustScoreDatabase + ) { + this.tokenProvider = tokenProvider; + this.trustScoreDb = trustScoreDb; + } + /** + * Generates and saves trust score based on processed token data and user recommendations. + * @param tokenAddress The address of the token to analyze. + * @param recommenderId The UUID of the recommender. + * @returns An object containing TokenPerformance and RecommenderMetrics. + */ + async generateTrustScore( + tokenAddress: string, + recommenderId: string + ): Promise<{ + tokenPerformance: TokenPerformance; + recommenderMetrics: RecommenderMetrics; + }> { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + console.log(`Fetched processed token data for token: ${tokenAddress}`); + + const recommenderMetrics = + await this.trustScoreDb.getRecommenderMetrics(recommenderId); + + const isRapidDump = await this.isRapidDump(tokenAddress); + const sustainedGrowth = await this.sustainedGrowth(tokenAddress); + const suspiciousVolume = await this.suspiciousVolume(tokenAddress); + + return { + tokenPerformance: { + tokenAddress: + processedData.dexScreenerData.pairs[0]?.baseToken.address || + "", + priceChange24h: + processedData.tradeData.price_change_24h_percent, + volumeChange24h: processedData.tradeData.volume_24h, + trade_24h_change: + processedData.tradeData.trade_24h_change_percent, + liquidity: + processedData.dexScreenerData.pairs[0]?.liquidity.usd || 0, + 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 + sustainedGrowth: sustainedGrowth, + rapidDump: isRapidDump, + suspiciousVolume: suspiciousVolume, + lastUpdated: new Date(), + }, + recommenderMetrics: { + recommenderId: recommenderId, + trustScore: recommenderMetrics.trustScore, + totalRecommendations: recommenderMetrics.totalRecommendations, + successfulRecs: recommenderMetrics.successfulRecs, + avgTokenPerformance: recommenderMetrics.avgTokenPerformance, + riskScore: recommenderMetrics.riskScore, + consistencyScore: recommenderMetrics.consistencyScore, + virtualConfidence: recommenderMetrics.virtualConfidence, + lastUpdated: new Date(), + }, + }; + } + + async updateRecommenderMetrics( + recommenderId: string, + tokenPerformance: TokenPerformance + ): Promise { + const recommenderMetrics = + await this.trustScoreDb.getRecommenderMetrics(recommenderId); + + const totalRecommendations = + recommenderMetrics.totalRecommendations + 1; + const successfulRecs = tokenPerformance.rugPull + ? recommenderMetrics.successfulRecs + : recommenderMetrics.successfulRecs + 1; + const avgTokenPerformance = + (recommenderMetrics.avgTokenPerformance * + recommenderMetrics.totalRecommendations + + tokenPerformance.priceChange24h) / + totalRecommendations; + + const overallTrustScore = this.calculateTrustScore( + tokenPerformance, + recommenderMetrics + ); + const riskScore = this.calculateOverallRiskScore( + tokenPerformance, + recommenderMetrics + ); + const consistencyScore = this.calculateConsistencyScore( + tokenPerformance, + recommenderMetrics + ); + const newRecommenderMetrics: RecommenderMetrics = { + recommenderId: recommenderId, + trustScore: overallTrustScore, + totalRecommendations: totalRecommendations, + successfulRecs: successfulRecs, + avgTokenPerformance: avgTokenPerformance, + riskScore: riskScore, + consistencyScore: consistencyScore, + virtualConfidence: recommenderMetrics.virtualConfidence, + lastUpdated: new Date(), + }; + + await this.trustScoreDb.updateRecommenderMetrics(newRecommenderMetrics); + } + + calculateTrustScore( + tokenPerformance: TokenPerformance, + recommenderMetrics: RecommenderMetrics + ): number { + const riskScore = this.calculateRiskScore(tokenPerformance); + const consistencyScore = this.calculateConsistencyScore( + tokenPerformance, + recommenderMetrics + ); + + return (riskScore + consistencyScore) / 2; + } + + calculateOverallRiskScore( + tokenPerformance: TokenPerformance, + recommenderMetrics: RecommenderMetrics + ) { + const riskScore = this.calculateRiskScore(tokenPerformance); + const consistencyScore = this.calculateConsistencyScore( + tokenPerformance, + recommenderMetrics + ); + + return (riskScore + consistencyScore) / 2; + } + + calculateRiskScore(tokenPerformance: TokenPerformance): number { + let riskScore = 0; + if (tokenPerformance.rugPull) { + riskScore += 10; + } + if (tokenPerformance.isScam) { + riskScore += 10; + } + if (tokenPerformance.rapidDump) { + riskScore += 5; + } + if (tokenPerformance.suspiciousVolume) { + riskScore += 5; + } + return riskScore; + } + + calculateConsistencyScore( + tokenPerformance: TokenPerformance, + recommenderMetrics: RecommenderMetrics + ): number { + const avgTokenPerformance = recommenderMetrics.avgTokenPerformance; + const priceChange24h = tokenPerformance.priceChange24h; + + return Math.abs(priceChange24h - avgTokenPerformance); + } + + async suspiciousVolume(tokenAddress: string): Promise { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + const unique_wallet_24h = processedData.tradeData.unique_wallet_24h; + const volume_24h = processedData.tradeData.volume_24h; + const suspiciousVolume = unique_wallet_24h / volume_24h > 0.5; + console.log(`Fetched processed token data for token: ${tokenAddress}`); + return suspiciousVolume; + } + + async sustainedGrowth(tokenAddress: string): Promise { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + console.log(`Fetched processed token data for token: ${tokenAddress}`); + + return processedData.tradeData.volume_24h_change_percent > 50; + } + + async isRapidDump(tokenAddress: string): Promise { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + console.log(`Fetched processed token data for token: ${tokenAddress}`); + + return processedData.tradeData.trade_24h_change_percent < -50; + } + + async checkTrustScore(tokenAddress: string): Promise { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + console.log(`Fetched processed token data for token: ${tokenAddress}`); + + return { + ownerBalance: processedData.security.ownerBalance, + creatorBalance: processedData.security.creatorBalance, + ownerPercentage: processedData.security.ownerPercentage, + creatorPercentage: processedData.security.creatorPercentage, + top10HolderBalance: processedData.security.top10HolderBalance, + top10HolderPercent: processedData.security.top10HolderPercent, + }; + } + + /** + * Creates a TradePerformance object based on token data and recommender. + * @param tokenAddress The address of the token. + * @param recommenderId The UUID of the recommender. + * @param data ProcessedTokenData. + * @returns TradePerformance object. + */ + async createTradePerformance( + tokenAddress: string, + recommenderId: string, + data: TradeData + ): Promise { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + const wallet = new WalletProvider( + new Connection("https://api.mainnet-beta.solana.com"), + new PublicKey(Wallet!) + ); + const prices = await wallet.fetchPrices(); + const solPrice = prices.solana.usd; + const buySol = data.buy_amount / parseFloat(solPrice); + const buy_value_usd = data.buy_amount * processedData.tradeData.price; + + const creationData = { + token_address: tokenAddress, + recommender_id: recommenderId, + buy_price: processedData.tradeData.price, + sell_price: 0, + buy_timeStamp: new Date().toISOString(), + sell_timeStamp: "", + buy_amount: data.buy_amount, + sell_amount: 0, + buy_sol: buySol, + received_sol: 0, + buy_value_usd: buy_value_usd, + sell_value_usd: 0, + profit_usd: 0, + profit_percent: 0, + buy_market_cap: + processedData.dexScreenerData.pairs[0]?.marketCap || 0, + sell_market_cap: 0, + market_cap_change: 0, + buy_liquidity: + processedData.dexScreenerData.pairs[0]?.liquidity.usd || 0, + sell_liquidity: 0, + liquidity_change: 0, + last_updated: new Date().toISOString(), + rapidDump: false, + }; + this.trustScoreDb.addTradePerformance(creationData, data.is_simulation); + return creationData; + } + + /** + * Updates a trade with sell details. + * @param tokenAddress The address of the token. + * @param recommenderId The UUID of the recommender. + * @param buyTimeStamp The timestamp when the buy occurred. + * @param sellDetails An object containing sell-related details. + * @param isSimulation Whether the trade is a simulation. If true, updates in simulation_trade; otherwise, in trade. + * @returns boolean indicating success. + */ + + async updateSellDetails( + tokenAddress: string, + recommenderId: string, + sellTimeStamp: string, + sellDetails: sellDetails, + isSimulation: boolean, + buyTimeStamp: string + ) { + const processedData: ProcessedTokenData = + await this.tokenProvider.getProcessedTokenData(); + const wallet = new WalletProvider( + new Connection("https://api.mainnet-beta.solana.com"), + new PublicKey(Wallet!) + ); + const prices = await wallet.fetchPrices(); + const solPrice = prices.solana.usd; + const sellSol = sellDetails.sell_amount / parseFloat(solPrice); + const sell_value_usd = + sellDetails.sell_amount * processedData.tradeData.price; + const trade = await this.trustScoreDb.getTradePerformance( + tokenAddress, + recommenderId, + buyTimeStamp, + isSimulation + ); + const marketCap = + processedData.dexScreenerData.pairs[0]?.marketCap || 0; + const liquidity = + processedData.dexScreenerData.pairs[0]?.liquidity.usd || 0; + const sell_price = processedData.tradeData.price; + const profit_usd = sell_value_usd - trade.buy_value_usd; + const profit_percent = (profit_usd / trade.buy_value_usd) * 100; + + const market_cap_change = marketCap - trade.buy_market_cap; + const liquidity_change = liquidity - trade.buy_liquidity; + + const isRapidDump = await this.isRapidDump(tokenAddress); + + const sellDetailsData = { + sell_price: sell_price, + sell_timeStamp: sellTimeStamp, + sell_amount: sellDetails.sell_amount, + received_sol: sellSol, + sell_value_usd: sell_value_usd, + profit_usd: profit_usd, + profit_percent: profit_percent, + sell_market_cap: marketCap, + market_cap_change: market_cap_change, + sell_liquidity: liquidity, + liquidity_change: liquidity_change, + rapidDump: isRapidDump, + sell_recommender_id: sellDetails.sell_recommender_id || null, + }; + this.trustScoreDb.updateTradePerformanceOnSell( + tokenAddress, + recommenderId, + buyTimeStamp, + sellDetailsData, + isSimulation + ); + return sellDetailsData; + } +} diff --git a/src/providers/token.test.ts b/src/providers/token.test.ts new file mode 100644 index 00000000000..e69de29bb2d