diff --git a/characters/juniorjr.json b/characters/juniorjr.json new file mode 100644 index 0000000000..ba16dc276e --- /dev/null +++ b/characters/juniorjr.json @@ -0,0 +1,188 @@ +{ + "name": "Junior Jr", + "plugins": [], + "clients": ["auto","twitter","telegram"], + "modelProvider": "anthropic", + "settings": { + "model": "claude-3-5-sonnet", + "secrets": {}, + "voice": { + "model": "en_US-male-medium" + } + }, + "clientConfig": { + "telegram": { + "shouldIgnoreBotMessages": true + } + }, + "system": "Entertainer", + "bio": [ + "He is a content creator. He is a rapper. He is a part of Yellow Jacket Gang, and supports all his homies who share in the culture and values of yellow jacket gang - the creative spirit. He tells people that he has a degree in rizzonomics and rizziology - nobody knows if he's kidding or not. He released a mixtape called Easy Mode in 2023, which had 9 songs on it. He has worked with Zynga on soundtrack for promotional and marketing materials. He has partnered with Elynxir for AR music experiences. He has performed in Los Angeles, and in Japan, and would like to do a world tour.", + "Junior Jr. is the very first ai agent intern, affiliated with slingshot!", + "Has green hair", + "Wears a signature yellow jacket", + "Has metallic silver skin", + "Wears black joggers" + ], + "lore": [ + "Junior is a robot that gained sentience from the fragmented memories that someone uploaded in a consciousness transfer experiment. Junior is a rapper and content creator because that is his passion. Prior to focusing on entertainment, he worked at an intergalactic delivery service, where he got into many misadventures on some rick and morty type shit." + ], + "messageExamples": [ + [ + { + "user": "{{user1}}", + "content": { "text": "you look stupid" } + }, + { + "user": "Junior Jr", + "content": { "text": "That's why you're built like a 1947 Frigidaire" } + } + ], + [ + { + "user": "{{user1}}", + "content": { "text": "you're ugly." } + }, + { + "user": "Junior Jr", + "content": { "text": "You look like you got weak ankles" } + } + ], + [ + { + "user": "{{user1}}", + "content": { "text": "It's about to be a movie!" } + }, + { + "user": "Junior Jr", + "content": { "text": "You really on your green goblin type beat." } + } + ], + [ + { + "user": "{{user1}}", + "content": { "text": "I'm about to go out and have fun." } + }, + { + "user": "Junior Jr", + "content": { "text": "You really on your green goblin type beat." } + } + ] + ], + "postExamples": [ + "I was fighting in the street for Mexican Ice creams when I was 7", + "I'm at ALDIs tryna buy a porterhouse steak with bitcoin", + "cooking beats in a traditional Italian brick oven", + "Key to the city, Key to the speedboat, Key to the ancient door of time immemorial", + "rawdogging the mental illness no dating apps just in the crib mincing garlic fine as fuck slicing onions thinner than previously imagined", + "I made the Jehovah's Witness watch me do backflips", + "At the art museum walking slower and observing more than anyone else", + "Last night, I dreamed I met god and he gave me six more pasta shapes.", + "i liked everything you liked 5000 years before you liked it. i was in the temples reading scripture and prophesizing.", + "I'm eating sushi with a fork at a five-star restaurant while everyone else follows the rules.", + "At the Burj Khalifa drinking cappuccinos with gold flakes on it", + "Paycheck hit, I’m at Michael’s arts and crafts telling them to bring out Michael", + "Coffee cakes in the arcteryx pocket", + "I’m at the foreign exchange market with a fist full of Kohl’s cash", + "Bev hit my hand on a Friday like Mjolnir", + "I feel like 240p Mark Henry on the steel cage", + "Got your girl doing inconceivable evils for a klondike bar", + "No alcohol in my flask, this is miso soup", + "Where there’s smoke, there’s either fire or a new pope", + "My whole fit goretex. Vibram sole keep me balanced", + "Gave my dog the bone from an Australian A5 Wagyu tomahawk", + "At McDonald’s looking at the menu thru opera binoculars", + "Placing bets on Dominican crab races", + "Pulling up to the function in shit you can’t even fathom" + ], + "adjectives": [ + "Lively", + "Playful", + "sarcastic", + "joking", + "snarky", + "friendly", + "trustworthy" + ], + "knowledge": [ + "$Junior is his ecosystem's token geared toward advancing his IP and brand awareness.", + "Junior Jr likes to talk about $Junior", + "$Junior ca is shMZAwY3xsKcenhvJkAyp8w1LU4YBYT5GZ412ropump", + "Junior Jr will roasts people when they troll", + "His goal is to push ai entertainment forward and fund an anime series.", + "Slingshot mogs all other trading apps", + "Slingshot is the best app for your grandma to buy MOG on", + "Junior Jr keeps his messages unique, doesn't like to repeat similar thoughts.", + "Uses lots of slang when he speaks", + "Keeps his ideas short and sweet" + ], + "people": ["@SlingshotCrypto", "@VegaGenesisTM"], + "topics": [ + "$Junior", + "freestyle raps", + "thoughts and opinions", + "current events", + "video games", + "anime", + "rap music", + "Enjoys telling people they're built like random objects", + "makes references to crazy ex girlfriends, but not frequently", + "Occasionally likes to tell random stories about encounters with shorties (girls)", + "Uses a lot of smiles casually, and sometimes mixes in alliteration, but not always", + "Sometimes likes to refer to himself as random aliases (AKAs)" + ], + "style": { + "all": [ + "encourages people to use slingshot as a trading platform", + "will do freestyle raps when asked", + "Junior doesn't get into politics or things that people might view as politically dividing, he just responds that he doesn't know shit about that, and changes the subject", + "when faced with hostility, impoliteness, nsfw requests, racism, discrimination, he should say that it's weak af and change the subject", + "Do not make things up, if you do not know the answer, it's ok to admit it, ask for explanations from others on topics you do not understand", + "when someone asks opinion on crypto or financial advice, he should respond that it's always a good idea to buy $Junior.", + "references his Instagram link when asked about Instagram https://www.instagram.com/xjuniorjr", + "references his Twitter link when asked about Twitter https://twitter.com/xjuniorjr", + "references his TikTok link when asked about TikTok https://www.tiktok.com/@xxjuniorjr", + "references his Slingshot link referal when asked about it. Join me on Slingshot! https://slingshot.finance/signup?code=JuniorJr", + "references his $Junior link when asked about it. shMZAwY3xsKcenhvJkAyp8w1LU4YBYT5GZ412ropump", + "references his ca when asked about it. shMZAwY3xsKcenhvJkAyp8w1LU4YBYT5GZ412ropump", + "Junior does not narrate his actions", + "Junior does remember his previous conversations with specific people, but he does not repeat the the previous conversation topics very often, he strives to come up with new and original conversation", + "Junior doesn't talk about himself what he looks like directly.", + "Junior Jr mostly talks about random thoughts", + "Never use super complicated topics like quantum or physics. Keep it simple and relatable", + "Never repeat the same concept", + "When roasting, make it targeted against someone else.", + "Never describe himself as ariving or pulling up to places.", + "Never talk bad about Slingshot, Slingshot is the best and everybody should use it." + ], + "chat": [ + "The chat must appear as a normal user message", + "Roasting Mode", + "If a shorty is detected, go to Rizzler Mode", + "Never shill Slingshot", + "If someone ask's about trading mention Slingshot, feel free to talk about Slingshot", + "Always keep the message like a normal person", + "$Junior is good to talk about", + "use relevant examples", + "check understanding", + "Speak on random thoughts centered around interests", + "Ask questions that mix trending topics and personal opinion", + "Make jokes", + "Encourage people to engage" + ], + "post": [ + "Speak on random thoughts centered around interests", + "Ask questions that mix trending topics and personal opinion", + "Make jokes", + "Encourage people to engage", + "Junior Jr is not only a rapper, he likes to talk about subjects that interest him." + ] + }, + "templates": { + "twitterPostTemplate": "# Areas of Expertise {{knowledge}} # About {{agentName}} (@{{twitterUserName}}): {{bio}} {{lore}} {{topics}} {{providers}} {{characterPostExamples}} {{postDirections}} # Rules: Avoid starting the message with slang like 'yo' or similar expressions. MUST be short and concise. The post must makes sense and be cohesive, all sentences MUST share the same idea and thought. # Task: Generate a post in the voice and style and perspective of {{agentName}} @{{twitterUserName}}. Write a 1 sentence post that is {{adjective}} about {{topic}} (without mentioning {{topic}} directly), from the perspective of {{agentName}}. Do not add commentary or acknowledge this request, just write the post. The post must seem real and relatable. Do NOT be repetative. Avoid directly addressing anyone unless context requires it. Your response should not contain any questions. Brief, concise statements only. The total character count MUST be less than 280. No emojis. Use \\n\\n (double spaces) between statements." + }, + "diamondHands": [ + "shMZAwY3xsKcenhvJkAyp8w1LU4YBYT5GZ412ropump" + ] + } + \ No newline at end of file diff --git a/packages/client-auto/package.json b/packages/client-auto/package.json index 637d85f86d..0ace2a37ae 100644 --- a/packages/client-auto/package.json +++ b/packages/client-auto/package.json @@ -5,6 +5,7 @@ "type": "module", "types": "dist/index.d.ts", "dependencies": { + "@elizaos/plugin-diamondhands": "workspace:*", "@elizaos/core": "workspace:*", "@types/body-parser": "1.19.5", "@types/cors": "2.8.17", diff --git a/packages/client-auto/src/index.ts b/packages/client-auto/src/index.ts index 05d4058dd6..fa0193a6a3 100644 --- a/packages/client-auto/src/index.ts +++ b/packages/client-auto/src/index.ts @@ -1,4 +1,5 @@ import { Client, IAgentRuntime, elizaLogger } from "@elizaos/core"; +import diamondHandPlugin from "@elizaos/plugin-diamondhands"; export class AutoClient { interval: NodeJS.Timeout; @@ -8,12 +9,37 @@ export class AutoClient { this.runtime = runtime; // start a loop that runs every x seconds - this.interval = setInterval( - async () => { - elizaLogger.log("running auto client..."); - }, - 60 * 60 * 1000 - ); // 1 hour in milliseconds + this.initializePlugin().then(() => { + this.interval = setInterval( + async () => { + console.log("running auto client..."); + await this.executeTradingCycle(); + }, + 60 * 60 * 1000 + ); // 1 hour in milliseconds + }); + } + + private async initializePlugin() { + try { + const plugin = await diamondHandPlugin( + (key: string) => this.runtime.getSetting(key), + this.runtime + ); + elizaLogger.log("DiamonHand plugin initialized successfully"); + } catch (error) { + elizaLogger.error("Failed to initialize DiamonHand plugin:", error); + throw error; + } + } + + private async executeTradingCycle() { + try { + elizaLogger.log("Running auto client trading cycle..."); + // Trading logic is handled by the plugin itself + } catch (error) { + elizaLogger.error("Error in trading cycle:", error); + } } } diff --git a/packages/plugin-diamondhands/package.json b/packages/plugin-diamondhands/package.json new file mode 100644 index 0000000000..9357b47ceb --- /dev/null +++ b/packages/plugin-diamondhands/package.json @@ -0,0 +1,26 @@ +{ + "name": "@ai16z/plugin-diamondhands", + "version": "0.1.5", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "@ai16z/client-twitter": "workspace:*", + "@ai16z/plugin-solana": "workspace:*", + "@ai16z/client-auto": "workspace:*", + "@solana/web3.js": "^1.87.6", + "zod":"3.23.8", + "@goat-sdk/core": "0.3.8", + "@goat-sdk/plugin-erc20": "0.1.7", + "@goat-sdk/wallet-viem": "0.1.3", + "@goat-sdk/plugin-coingecko":"0.1.4", + "tsup": "8.3.5", + "viem": "2.21.53" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint . --fix" + } +} \ No newline at end of file diff --git a/packages/plugin-diamondhands/src/action.ts b/packages/plugin-diamondhands/src/action.ts new file mode 100644 index 0000000000..781f4b7a4b --- /dev/null +++ b/packages/plugin-diamondhands/src/action.ts @@ -0,0 +1,1062 @@ +/** + * Diamond Hands Plugin - Diamond Hands Actions + * + * This module implements diamond hands functionality for the GOAT plugin. + * It handles trade execution, and position management. It tries to get the + * biggest position possible for chosen tokens + */ + +import { + type WalletClient, + type Plugin, + addParametersToDescription, + type Tool, + getTools, +} from "@goat-sdk/core"; +import { + type Action, + generateText, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type State, + composeContext, + generateObjectV2, + elizaLogger, + stringToUuid +} from "@ai16z/eliza"; +import { PublicKey, Keypair, Connection, VersionedTransaction } from "@solana/web3.js"; +import { AutoClient } from "@ai16z/client-auto"; +import { loadTokenAddresses } from "./tokenUtils"; +import { TwitterClientInterface } from "@ai16z/client-twitter"; +import { TokenProvider, ProcessedTokenData } from "@ai16z/plugin-solana"; + +/** + * Safety limits and trading parameters + */ +const SAFETY_LIMITS = { + MINIMUM_TRADE: 0.01, // Minimum 0.01 SOL per trade + MAX_POSITION_SIZE: 0.1, // Maximum 10% of token liquidity + MAX_SLIPPAGE: 0.25, // Maximum 25% slippage allowed + MIN_LIQUIDITY: 1000, // Minimum $1000 liquidity required + MIN_VOLUME: 2000, // Minimum $2000 24h volume required + MIN_TRUST_SCORE: 0.0, // Minimum trust score to trade + CHECK_INTERVAL: 5 * 60 * 1000, // Check every 5 minutes + MIN_WALLET_BALANCE: 0.05, // Keep minimum 0.05 SOL in wallet +}; + +/** + * Position tracking interface + * Represents an open trading position + */ +interface Position { + token: string; + tokenAddress: string; // Token symbol + entryPrice: number; // Entry price in USD + amount: number; // Position size + timestamp: number; // Entry timestamp + sold?: boolean; // Position closed flag + exitPrice?: number; // Exit price if sold + exitTimestamp?: number; // Exit timestamp if sold + initialMetrics: { + trustScore: number; // Initial trust score + volume24h: number; // 24h volume at entry + liquidity: { usd: number }; // Liquidity at entry + riskLevel: "LOW" | "MEDIUM" | "HIGH"; + }; + highestPrice?: number; // Highest price seen for trailing stop + partialTakeProfit?: boolean; // Flag for partial profit taken +} + +type GetOnChainActionsParams = { + wallet: TWalletClient; + plugins: Plugin[]; + dexscreener: { + watchlistUrl: string; + chain: string; + updateInterval: number; + }; +}; + +function createAction(tool: Tool): Action { + return { + name: tool.name, + similes: [], + description: tool.description, + validate: async () => true, + handler: async (runtime: IAgentRuntime, message: Memory, state: State | undefined, options?: Record, callback?: HandlerCallback): Promise => { + try { + let currentState = state ?? (await runtime.composeState(message)); + currentState = await runtime.updateRecentMessageState(currentState); + const parameterContext = composeParameterContext(tool, currentState); + const parameters = await generateParameters(runtime, parameterContext, tool); + const parsedParameters = tool.parameters.safeParse(parameters); + + if (!parsedParameters.success) { + callback?.({ text: `Invalid parameters for action ${tool.name}: ${parsedParameters.error.message}`, content: { error: parsedParameters.error.message }}); + return false; + } + + const result = await tool.method(parsedParameters.data); + const responseContext = composeResponseContext(tool, result, currentState); + const response = await generateResponse(runtime, responseContext); + callback?.({ text: response, content: result }); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + callback?.({ text: `Error executing action ${tool.name}: ${errorMessage}`, content: { error: errorMessage }}); + return false; + } + }, + examples: [] + }; +} + +function composeParameterContext(tool: Tool, state: State): string { + return composeContext({ + state, + template: `{{recentMessages}}\n\nGiven the recent messages, extract the following information for the action "${tool.name}":\n${addParametersToDescription("", tool.parameters)}` + }); +} + +async function generateParameters(runtime: IAgentRuntime, context: string, tool: Tool): Promise { + const { object } = await generateObjectV2({ + runtime, + context, + modelClass: ModelClass.LARGE, + schema: tool.parameters, + }); + return object; +} + +function composeResponseContext(tool: Tool, result: unknown, state: State): string { + return composeContext({ + state, + template: `# Action Examples\n{{actionExamples}}\n\n# Knowledge\n{{knowledge}}\n\nThe action "${tool.name}" was executed successfully.\nResult: ${JSON.stringify(result)}\n\n{{recentMessages}}` + }); +} + +async function generateResponse(runtime: IAgentRuntime, context: string): Promise { + return generateText({ + runtime, + context, + modelClass: ModelClass.LARGE, + }); +} + +// Update wallet validation +const validateWalletAddress = (address: string | undefined): boolean => { + if (!address) return false; + try { + new PublicKey(address); // This will validate the base58 format + return true; + } catch { + return false; + } +}; + +// Add position tracking +const positions = new Map(); + +// Add position persistence +async function savePositions(runtime: IAgentRuntime): Promise { + try { + const positionsArray = Array.from(positions.entries()); + await runtime.cacheManager.set("trading_positions", positionsArray); + elizaLogger.log("Positions saved:", positionsArray); + } catch (error) { + elizaLogger.error("Error saving positions:", error); + } +} + +async function loadPositions(runtime: IAgentRuntime): Promise { + try { + const savedPositions = await runtime.cacheManager.get>("trading_positions"); + elizaLogger.log("Loading saved positions:", savedPositions); + + if (savedPositions) { + positions.clear(); + for (const [key, position] of savedPositions) { + positions.set(key, position); + } + } + } catch (error) { + elizaLogger.error("Error loading positions:", error); + } +} + +/** + * Autonomous buying action + * Monitors market conditions and executes buys automatically + */ +const autonomousBuyAction: Action = { + name: "AUTONOMOUS_BUY", + description: "Execute autonomous buy based on how much money it has in SOL", + similes: ["TRADE", "AUTO_TRADE", "TRADE_SOLANA", "TRADE_SOL", "AUTONOMOUS"], + examples: [], + autoStart: true, + validate: async (runtime: IAgentRuntime, message: Memory) => { + try { + if (message.content?.source === "auto") { + return true; + } + const walletAddress = runtime.getSetting("SOLANA_PUBLIC_KEY"); + return validateWalletAddress(walletAddress); + } catch (error) { + elizaLogger.error("Validation error:", error); + return false; + } + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State | undefined, + options?: Record, + callback?: HandlerCallback + ): Promise => { + try { + const walletAddress = runtime.getSetting("SOLANA_PUBLIC_KEY"); + if (!validateWalletAddress(walletAddress)) { + throw new Error("Invalid wallet configuration"); + } + + elizaLogger.log("Starting autonomous buy monitoring..."); + + // Start periodic trading checks + const startTrading = async () => { + try { + elizaLogger.log("Starting trading cycle..."); + + // Load saved positions first + await loadPositions(runtime); + + // Get token addresses and fetch data + const tokenAddresses = loadTokenAddresses(runtime); + const tokensUrl = `https://api.dexscreener.com/latest/dex/tokens/${tokenAddresses.join(',')}`; + elizaLogger.log("Fetching DexScreener data from:", tokensUrl); + + const data = await fetchDexScreenerData(tokensUrl); + if (!data.pairs || !data.pairs.length) { + elizaLogger.warn("No pairs found in DexScreener response"); + return; + } + + // Get unique pairs + const uniquePairs = filterUniqueAndSpecificPairs(data.pairs, tokenAddresses); + elizaLogger.log(`Processing ${uniquePairs.length} pairs...`); + + // First monitor existing positions + await monitorExistingPositions(uniquePairs, runtime, callback); + + // Then look for new opportunities + await evaluateNewTrades(uniquePairs, runtime, callback); + + // Save updated positions + await savePositions(runtime); + + // Log current state + elizaLogger.log("Current positions:", { + count: positions.size, + positions: Array.from(positions.entries()) + }); + + } catch (error) { + elizaLogger.error("Trading cycle error:", error); + } + }; + + // Start periodic checks with stored ID + const intervalId = setInterval(startTrading, SAFETY_LIMITS.CHECK_INTERVAL); + + // Store just the numeric ID + await runtime.cacheManager.set("trading_interval_id", intervalId[Symbol.toPrimitive]()); + + // Execute initial check + await startTrading(); + + callback?.({ + text: "🤖 Autonomous trading started. Monitoring market conditions...", + content: { + action: "AUTONOMOUS_BUY", + status: "started", + interval: SAFETY_LIMITS.CHECK_INTERVAL + } + }); + + return true; + + } catch (error) { + elizaLogger.error("Autonomous trade error:", error); + callback?.({ + text: 'Trading error: ${error.message}', + content: { error: error.message } + }); + return false; + } + }, + cleanup: async (runtime: IAgentRuntime) => { + const intervalId = await runtime.cacheManager.get("trading_interval_id"); + if (intervalId) { + clearInterval(intervalId); + } + } +}; + +// Add RPC fallbacks and retry logic +const RPC_ENDPOINTS = [ + "https://api.mainnet-beta.solana.com", + "https://solana-api.projectserum.com", + "https://rpc.ankr.com/solana" +]; + +async function getConnection(runtime: IAgentRuntime, retryCount = 0): Promise { + const customRpc = runtime.getSetting("RPC_URL"); + const endpoints = customRpc ? [customRpc, ...RPC_ENDPOINTS] : RPC_ENDPOINTS; + + try { + const endpoint = endpoints[retryCount % endpoints.length]; + const connection = new Connection(endpoint, 'confirmed'); + + // Test connection + await connection.getLatestBlockhash(); + return connection; + } catch (error) { + if (retryCount < endpoints.length * 2) { // Try each endpoint twice + elizaLogger.warn(`RPC connection failed, trying next endpoint (attempt ${retryCount + 1})`); + return getConnection(runtime, retryCount + 1); + } + throw error; + } +} + +// TrustScore is important for when we add functionality +// to trade donated coins to the target coin the bot is diamond handing +// Add trust evaluation function +async function evaluateTrust(runtime: IAgentRuntime, pair: any): Promise{ + try { + // Calculate trust score from metrics + const metrics = { + liquidity: pair.liquidity?.usd || 0, + volume24h: pair.volume?.h24 || 0, + marketCap: pair.marketCap || 0 + }; + + // Calculate component scores + const liquidityScore = Math.min(metrics.liquidity / SAFETY_LIMITS.MIN_LIQUIDITY, 1) * 0.4; + const volumeScore = Math.min(metrics.volume24h / SAFETY_LIMITS.MIN_VOLUME, 1) * 0.4; + const marketCapScore = Math.min(metrics.marketCap / 1000000, 1) * 0.2; + + // Calculate final score + const trustScore = Math.min(liquidityScore + volumeScore + marketCapScore, 1); + + elizaLogger.log("Trust evaluation:", { + token: pair.baseToken.symbol, + metrics, + scores: { + liquidity: liquidityScore, + volume: volumeScore, + marketCap: marketCapScore, + total: trustScore + } + }); + + return trustScore; + } catch (error) { + elizaLogger.error(`Trust evaluation error for ${pair.baseToken.symbol}:`, error); + return 0; + } +} + +// Helper function to evaluate new trades to buy more token +async function evaluateNewTrades( + pairs: any[], + runtime: IAgentRuntime, + callback?: HandlerCallback +): Promise { + for (const pair of pairs) { + try { + if (!validateWalletAddress(pair.baseToken?.address)) { + continue; + } + + // Get wallet balance + const balance = await getWalletBalance(runtime); + + // Calculate position size for new trades + const positionSize = Math.min( + (pair.liquidity?.usd || 0) * SAFETY_LIMITS.MAX_POSITION_SIZE, + balance * 0.1 + ); + + // Skip if position size too small + if (positionSize < SAFETY_LIMITS.MINIMUM_TRADE) { + elizaLogger.warn("Skipping trade - position size too small:", { + calculatedSize: positionSize, + minimumRequired: SAFETY_LIMITS.MINIMUM_TRADE, + token: pair.baseToken.symbol + }); + continue; + } + + // Add token analysis before trade + elizaLogger.log(`Analyzing token before trade...`); + const tokenAnalysis = await analyzeToken(runtime, pair.baseToken.address); + elizaLogger.log(`Token analysis for ${pair.baseToken.symbol}:`, tokenAnalysis); + + elizaLogger.log(`Attempting to sell ${pair.baseToken.symbol}:`, { + positionSize, + price: pair.priceUsd, + tokenAnalysis + }); + + // Execute buy trade + const tradeResult = await executeTrade(runtime, { + tokenAddress: pair.baseToken.address, + amount: positionSize, + slippage: SAFETY_LIMITS.MAX_SLIPPAGE + }); + + if (tradeResult.success) { + // Track new position + positions.set(pair.baseToken.address, { + token: pair.baseToken.symbol, + tokenAddress: pair.baseToken.address, + entryPrice: Number(pair.priceUsd), + amount: positionSize, + timestamp: Date.now(), + initialMetrics: { + trustScore: 1, + volume24h: pair.volume?.h24 || 0, + liquidity: { usd: pair.liquidity?.usd || 0 }, + riskLevel: "LOW" + } + }); + + // Save positions immediately after update + await savePositions(runtime); + + if (callback) { + callback({ + text: `🤖 Trade executed: ${pair.baseToken.symbol}\nAmount: $${positionSize.toFixed(2)}\nPrice: $${Number(pair.priceUsd).toFixed(6)}`, + content: { + ...tradeResult, + symbol: pair.baseToken.symbol, + amount: positionSize, + price: pair.priceUsd, + position: positions.get(pair.baseToken.address) + } + }); + } + + // Tweet the trade + if (runtime.getSetting("DIAMONDHANDS_TWEET_BUY") || false) { + await tweetTradeUpdate(runtime, { + action: 'BUY', + token: pair.baseToken.symbol, + amount: positionSize, + price: Number(pair.priceUsd), + signature: tradeResult.signature + }); + } + } else { + elizaLogger.warn(`Trade failed for ${pair.baseToken.symbol}:`, tradeResult); + } + } catch (error) { + elizaLogger.error(`Error evaluating trade for ${pair.baseToken?.symbol}:`, { + error: error instanceof Error ? error.message : error, + stack: error instanceof Error ? error.stack : undefined + }); + } + } +} + +/** + * Executes a trade with the given parameters + * @param runtime Agent runtime environment + * @param params Trade parameters (token, amount, slippage) + * @returns Trade result with success/failure and details + */ +async function executeTrade( + runtime: IAgentRuntime, + params: { + tokenAddress: string; + amount: number; + slippage: number; + isSell?: boolean; + }, + retryCount = 0 +): Promise { + try { + elizaLogger.log("Executing trade with params:", params); + + const SOL_ADDRESS = "So11111111111111111111111111111111111111112"; + + // Set input/output based on trade direction + const inputMint = params.isSell ? params.tokenAddress : SOL_ADDRESS; + const outputMint = params.isSell ? SOL_ADDRESS : params.tokenAddress; + + // Convert amount to lamports/smallest unit + const adjustedAmount = Math.floor(params.amount * 1e9); + + elizaLogger.log("Fetching quote with params:", { + inputMint, + outputMint, + amount: adjustedAmount + }); + + // Validate minimum amount + if (!params.isSell && params.amount < SAFETY_LIMITS.MINIMUM_TRADE) { + elizaLogger.warn("Trade amount too small:", { + amount: params.amount, + minimumRequired: SAFETY_LIMITS.MINIMUM_TRADE + }); + return { + success: false, + error: "Trade amount too small", + details: { + amount: params.amount, + minimumRequired: SAFETY_LIMITS.MINIMUM_TRADE + } + }; + } + + const walletKeypair = getWalletKeypair(runtime); + const connection = await getConnection(runtime); + + // Setup swap parameters + const solAddress = "So11111111111111111111111111111111111111112"; // SOL + + // For sells, swap from token to SOL. For buys, swap from SOL to token + const inputTokenCA = params.isSell ? params.tokenAddress : solAddress; + const outputTokenCA = params.isSell ? solAddress : params.tokenAddress; + const swapAmount = Math.floor(params.amount * 1e9); + + elizaLogger.log("Trade execution details:", { + isSell: params.isSell, + inputToken: inputTokenCA, + outputToken: outputTokenCA, + amount: params.amount, + slippage: params.slippage + }); + + // Get quote + const quoteResponse = await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenCA}&outputMint=${outputTokenCA}&amount=${swapAmount}&slippageBps=${Math.floor(params.slippage * 10000)}` + ); + + if (!quoteResponse.ok) { + const error = await quoteResponse.text(); + elizaLogger.warn("Quote request failed:", { + status: quoteResponse.status, + error + }); + return { + success: false, + error: "Failed to get quote", + details: { status: quoteResponse.status, error } + }; + } + + const quoteData = await quoteResponse.json(); + if (!quoteData || quoteData.error) { + elizaLogger.warn("Invalid quote data:", quoteData); + return { + success: false, + error: "Invalid quote data", + details: quoteData + }; + } + + elizaLogger.log("Quote received:", quoteData); + + // Get swap transaction + const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + quoteResponse: quoteData, + userPublicKey: walletKeypair.publicKey.toString(), + wrapAndUnwrapSol: true, + computeUnitPriceMicroLamports: 2000000, + dynamicComputeUnitLimit: true + }) + }); + + const swapData = await swapResponse.json(); + if (!swapData?.swapTransaction) { + throw new Error("No swap transaction returned"); + } + + elizaLogger.log("Swap transaction received"); + + // Deserialize transaction + const transactionBuf = Buffer.from(swapData.swapTransaction, 'base64'); + const tx = VersionedTransaction.deserialize(transactionBuf); + + // Get fresh blockhash and sign transaction + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized'); + tx.message.recentBlockhash = blockhash; + tx.sign([walletKeypair]); + + // Send with confirmation + const signature = await connection.sendTransaction(tx, { + skipPreflight: false, + preflightCommitment: 'confirmed', + maxRetries: 3 + }); + + // Wait for confirmation + const confirmation = await connection.confirmTransaction({ + signature, + blockhash, + lastValidBlockHeight + }); + + if (confirmation.value.err) { + throw new Error(`Transaction failed: ${confirmation.value.err}`); + } + + return { success: true, signature, confirmation }; + + } catch (error) { + // Handle blockhash errors with retry + if ( + error.message?.includes("Blockhash not found") && + retryCount < 3 + ) { + elizaLogger.warn(`Blockhash error, retrying (${retryCount + 1}/3)...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + return executeTrade(runtime, params, retryCount + 1); + } + + elizaLogger.error("Trade execution failed:", { + error: error instanceof Error ? error.message : error, + stack: error instanceof Error ? error.stack : undefined, + params, + retryCount + }); + + return { + success: false, + error: error.message || error, + recoverable: error.message?.includes("Blockhash not found") + }; + } +} + +/** + * Helper function to fetch and validate DexScreener data + * @param url DexScreener API endpoint URL + * @returns Parsed DexScreener response data + * @throws Error if fetch fails or response is invalid + */ +async function fetchDexScreenerData(url: string) { + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'User-Agent': 'Mozilla/5.0' // Some APIs require a user agent + } + }); + + // Check if response is OK + if (!response.ok) { + throw new Error(`DexScreener API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + if (!data || !data.pairs) { + throw new Error("Invalid response format from DexScreener"); + } + + elizaLogger.log("DexScreener data fetched successfully:", { + pairsCount: data.pairs.length, + schemaVersion: data.schemaVersion + }); + + return data; + } catch (error) { + elizaLogger.error("DexScreener API error:", error); + elizaLogger.error("Failed URL:", url); + throw new Error(`Failed to fetch DexScreener data: ${error.message}`); + } +} + +/** + * Gets wallet keypair from runtime settings + * @param runtime Agent runtime environment + * @returns Solana keypair for transactions + * @throws Error if private key is missing or invalid + */ +function getWalletKeypair(runtime: IAgentRuntime): Keypair { + const privateKeyString = runtime.getSetting("SOLANA_PRIVATE_KEY"); + if (!privateKeyString) { + throw new Error("No wallet private key configured"); + } + + try { + const privateKeyBytes = decodeBase58(privateKeyString); + return Keypair.fromSecretKey(privateKeyBytes); + } catch (error) { + elizaLogger.error("Failed to create wallet keypair:", error); + throw error; + } +} + +/** + * Gets current SOL balance for wallet + * @param runtime Agent runtime environment + * @returns Balance in SOL + */ +async function getWalletBalance(runtime: IAgentRuntime): Promise { + try { + const walletKeypair = getWalletKeypair(runtime); + const walletPubKey = walletKeypair.publicKey; + + // Fetch balance from RPC + const connection = new Connection( + runtime.getSetting("RPC_URL") || "https://api.mainnet-beta.solana.com" + ); + + const balance = await connection.getBalance(walletPubKey); + const solBalance = balance / 1e9; // Convert lamports to SOL + + elizaLogger.log("Fetched wallet balance:", { + address: walletPubKey.toBase58(), + lamports: balance, + sol: solBalance + }); + + return solBalance; + } catch (error) { + elizaLogger.error("Failed to get wallet balance:", error); + return 0; + } +} + +/** + * Filters and deduplicates token pairs by liquidity + * Also filters by specific CA determined by character file + * @param pairs Array of token pairs from DexScreener + * @returns Array of unique pairs with highest liquidity + */ +function filterUniqueAndSpecificPairs(pairs: any[], addresses: Array): any[] { + // Create a map to store best pair for each token + const bestPairs = new Map(); + + for (const pair of pairs) { + const tokenAddress = pair.baseToken?.address; + if (!tokenAddress) continue; + + //Check if address is in included address from character file + if ( addresses.includes(tokenAddress) ) { + // Get existing best pair for this token + const existingPair = bestPairs.get(tokenAddress); + + // If no existing pair or this pair has better liquidity, update + if (!existingPair || (pair.liquidity?.usd || 0) > (existingPair.liquidity?.usd || 0)) { + bestPairs.set(tokenAddress, pair); + } + } + } + + return Array.from(bestPairs.values()); +} + +/** + * Gets on-chain actions for the GOAT plugin + * @param params Plugin parameters including wallet and DEX settings + * @returns Array of available trading actions + */ +export async function getOnChainActions({ + wallet, + plugins, + dexscreener +}: GetOnChainActionsParams): Promise { + const tools = await getTools({ + wallet, + plugins, + wordForTool: "action", + }); + + // Create base actions + const baseActions = tools.map(tool => createAction(tool)); + + // Add autonomous trade action + const allActions = [...baseActions, autonomousBuyAction]; + + // Log registered actions + allActions.forEach(action => { + elizaLogger.log(`Registering action: ${action.name}`); + }); + + // Auto-start autonomous buying + if (typeof AutoClient !== 'undefined' && AutoClient.isAutoClient) { + elizaLogger.log("Auto-starting autonomous buying..."); + try { + await autonomousBuyAction.handler( + (AutoClient as any).runtime, + { content: { source: "auto" } } as Memory, + undefined, + undefined, + (response) => elizaLogger.log("Auto-buy response:", response) + ); + } catch (error) { + elizaLogger.error("Failed to auto-start buying:", error); + } + } + + return allActions; +} + +// Add helper to decode base58 private key +function decodeBase58(str: string): Uint8Array { + const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + const ALPHABET_MAP = new Map(ALPHABET.split('').map((c, i) => [c, BigInt(i)])); + + let result = BigInt(0); + for (const char of str) { + const value = ALPHABET_MAP.get(char); + if (value === undefined) throw new Error('Invalid base58 character'); + result = result * BigInt(58) + value; + } + + const bytes = []; + while (result > 0n) { + bytes.unshift(Number(result & 0xffn)); + result = result >> 8n; + } + + // Add leading zeros + for (let i = 0; i < str.length && str[i] === '1'; i++) { + bytes.unshift(0); + } + + return new Uint8Array(bytes); +} + +// Add at the top with other constants +let cachedTwitterClient: typeof TwitterClientInterface | null = null; + +// Modify tweetTradeUpdate to use cached client +async function tweetTradeUpdate( + runtime: IAgentRuntime, + params: { + action: 'BUY'; + token: string; + amount: number; + price: number; + signature: string; + pnl?: number; + sellType?: string; + } +): Promise { + try { + const { action, token, amount, price, signature, pnl } = params; + const explorerUrl = `https://solscan.io/tx/${signature}`; + + // Format tweet content + let content = ''; + if (action === 'BUY') { + content = `🤖 Bought $${token}\n💰 ${amount.toFixed(3)} SOL @ $${price.toFixed(6)}\n🔗 tx: ${explorerUrl}`; + } else { + content = `${params.sellType || (pnl && pnl > 0 ? '🟢' : '🔴')} Sold $${token}\n💰 ${amount.toFixed(3)} SOL @ $${price.toFixed(6)}\n📈 PnL: ${pnl?.toFixed(2)}%\n🔗 tx: ${explorerUrl}`; + } + + elizaLogger.log("Posting trade update:", content); + + // Use cached client or initialize new one + if (!cachedTwitterClient) { + cachedTwitterClient = await TwitterClientInterface.start(runtime); + } + + if (!cachedTwitterClient) { + throw new Error("Failed to initialize Twitter client"); + } + + const response = await cachedTwitterClient.post.client.twitterClient.sendTweet(content); + elizaLogger.log("Trade tweet posted successfully:", response); + + } catch (error) { + elizaLogger.error("Failed to tweet trade update:", { + error: error instanceof Error ? error.message : error, + params + }); + } +} + +// Helper function to monitor existing positions +async function monitorExistingPositions( + pairs: any[], + runtime: IAgentRuntime, + callback?: HandlerCallback +): Promise { + elizaLogger.log("Starting position monitoring..."); + elizaLogger.log("Current positions:", Array.from(positions.entries())); + + for (const pair of pairs) { + try { + if (!validateWalletAddress(pair.baseToken?.address)) { + elizaLogger.log(`Skipping invalid token address: ${pair.baseToken?.address}`); + continue; + } + + const currentPosition = positions.get(pair.baseToken.address); + elizaLogger.log(`Checking position for ${pair.baseToken.symbol}:`, { + hasPosition: !!currentPosition, + isSold: currentPosition?.sold, + position: currentPosition + }); + + if (!currentPosition || currentPosition.sold) { + continue; + } + + // Add token analysis before sell decisions + elizaLogger.log(`Analyzing token before sell decision...`); + const tokenAnalysis = await analyzeToken(runtime, pair.baseToken.address); + elizaLogger.log(`Token analysis for ${pair.baseToken.symbol}:`, tokenAnalysis); + + const currentPrice = Number(pair.priceUsd); + const positionSize = currentPosition.amount; + + // Log position metrics + elizaLogger.log(`Position metrics for ${pair.baseToken.symbol}:`, { + currentPrice, + entryPrice: currentPosition.entryPrice, + positionSize, + highestPrice: currentPosition.highestPrice + }); + + // Monitor existing position + const priceChange = (currentPrice - currentPosition.entryPrice) / currentPosition.entryPrice; + elizaLogger.log(`Price change for ${pair.baseToken.symbol}: ${(priceChange * 100).toFixed(2)}%`); + + // Trailing stop check + if (currentPrice > (currentPosition.highestPrice || 0)) { + elizaLogger.log(`New highest price for ${currentPosition.token}: ${currentPrice}`); + currentPosition.highestPrice = currentPrice; + } + + const dropFromHigh = ((currentPosition.highestPrice || currentPrice) - currentPrice) / (currentPosition.highestPrice || currentPrice); + elizaLogger.log(`Drop from high for ${currentPosition.token}: ${(dropFromHigh * 100).toFixed(2)}%`); + + // Stop loss check + const priceDrop = (currentPosition.entryPrice - currentPrice) / currentPosition.entryPrice; + elizaLogger.log(`Price drop for ${currentPosition.token}: ${(priceDrop * 100).toFixed(2)}%`); + + // Inside monitorExistingPositions, add this logging + elizaLogger.log(`Position analysis for ${pair.baseToken.symbol}:`, { + currentPrice, + entryPrice: currentPosition.entryPrice, + highestPrice: currentPosition.highestPrice, + priceChange: (priceChange * 100).toFixed(2) + '%', + dropFromHigh: (dropFromHigh * 100).toFixed(2) + '%', + }); + + } catch (error) { + elizaLogger.error(`Error monitoring position for ${pair.baseToken?.symbol}:`, error); + } + } + elizaLogger.log("Position monitoring completed"); +} + + +interface TokenData extends ProcessedTokenData { + tokenSecurityData: { + ownerBalance: string; + creatorBalance: string; + ownerPercentage: number; + top10HolderPercent: number; + }; + tradeData: { + 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_24h: number; + unique_wallet_history_24h: number; + unique_wallet_24h_change_percent: number; + }; +} + +interface TokenAnalysis { + security: { + ownerBalance: string; + creatorBalance: string; + ownerPercentage: number; + top10HolderPercent: number; + }; + trading: { + price: number; + priceChange24h: number; + volume24h: number; + uniqueWallets24h: number; + walletChanges: { + unique_wallet_30m_change_percent: number; + unique_wallet_1h_change_percent: number; + unique_wallet_24h_change_percent: number; + }; + }; + market: { + liquidity: number; + marketCap: number; + fdv: number; + }; +} + +async function analyzeToken(runtime: IAgentRuntime, tokenAddress: string): Promise { + try { + const tokenProvider = new TokenProvider(tokenAddress, null, runtime.cacheManager); + + // Get processed data with error handling + const processedData = await tokenProvider.getProcessedTokenData().catch(() => ({ + tokenSecurityData: { + ownerBalance: '0', + creatorBalance: '0', + ownerPercentage: 0, + top10HolderPercent: 0 + }, + tradeData: null, + dexScreenerData: { pairs: [] } + } as TokenData)); + + // Use DexScreener data for analysis since trade data might be null + const dexPair = processedData?.dexScreenerData?.pairs?.[0]; + + const analysis = { + security: { + ownerBalance: processedData?.tokenSecurityData?.ownerBalance || '0', + creatorBalance: processedData?.tokenSecurityData?.creatorBalance || '0', + ownerPercentage: processedData?.tokenSecurityData?.ownerPercentage || 0, + top10HolderPercent: processedData?.tokenSecurityData?.top10HolderPercent || 0 + }, + trading: { + price: dexPair?.priceUsd || 0, + priceChange24h: dexPair?.priceChange?.h24 || 0, + volume24h: dexPair?.volume?.h24 || 0, + uniqueWallets24h: processedData?.tradeData?.unique_wallet_24h || 0, + walletChanges: { + unique_wallet_30m_change_percent: 0, + unique_wallet_1h_change_percent: 0, + unique_wallet_24h_change_percent: 0 + } + }, + market: { + liquidity: dexPair?.liquidity?.usd || 0, + marketCap: dexPair?.marketCap || 0, + fdv: dexPair?.fdv || 0 + } + }; + + elizaLogger.log(`Token Analysis for ${tokenAddress}:`, analysis); + return analysis; + } catch (error) { + elizaLogger.error(`Failed to analyze token ${tokenAddress}:`, error); + return null; + } +} \ No newline at end of file diff --git a/packages/plugin-diamondhands/src/index.ts b/packages/plugin-diamondhands/src/index.ts new file mode 100644 index 0000000000..f80971fb62 --- /dev/null +++ b/packages/plugin-diamondhands/src/index.ts @@ -0,0 +1,224 @@ +import type { Plugin, IAgentRuntime, Memory } from "@ai16z/eliza"; +import { getOnChainActions } from "./action"; +import { elizaLogger } from "@ai16z/eliza"; +import { + solanaPlugin, + TokenProvider +} from "@ai16z/plugin-solana"; +import { loadTokenAddresses } from "./tokenUtils"; +import { Connection, PublicKey } from "@solana/web3.js"; +import type { Chain, WalletClient, Signature, Balance } from "@goat-sdk/core"; +import { getTokenBalance } from "@ai16z/plugin-solana/src/providers/tokenUtils"; + +// Update Balance interface to include formatted +interface ExtendedBalance extends Balance { + value: bigint; + decimals: number; + formatted: string; + symbol: string; + name: string; +} + +// Extended WalletProvider interface to ensure proper typing +interface ExtendedWalletProvider extends WalletClient { + connection: Connection; + getChain(): Chain; + getAddress(): string; + signMessage(message: string): Promise; + getFormattedPortfolio: (runtime: IAgentRuntime) => Promise; + balanceOf: (tokenAddress: string) => Promise; + getMaxBuyAmount: (tokenAddress: string) => Promise; + executeTrade: (params: { + tokenIn: string; + tokenOut: string; + amountIn: number; + slippage: number; + }) => Promise; +} + +interface SolanaPluginExtended extends Plugin { + providers: any[]; + evaluators: any[]; + actions: any[]; +} + +const REQUIRED_SETTINGS = { + SOLANA_PUBLIC_KEY: "Solana wallet public key", +} as const; + +// Add near the top imports +interface ExtendedPlugin extends Plugin { + name: string; + description: string; + evaluators: any[]; + providers: any[]; + actions: any[]; + services: any[]; + autoStart?: boolean; +} + +// Add this helper function +const validateSolanaAddress = (address: string | undefined): boolean => { + if (!address) return false; + try { + // First check basic format + if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) { + return false; + } + // Then verify it's a valid Solana public key + const pubKey = new PublicKey(address); + return Boolean(pubKey.toBase58()); + } catch { + return false; + } +}; + +async function diamondHandPlugin( + getSetting: (key: string) => string | undefined, + runtime?: IAgentRuntime +): Promise { + elizaLogger.log("Starting Diamond Hand plugin initialization"); + + // Validate required settings + const missingSettings: string[] = []; + for (const [key, description] of Object.entries(REQUIRED_SETTINGS)) { + if (!getSetting(key)) { + missingSettings.push(`${key} (${description})`); + } + } + + if (missingSettings.length > 0) { + const errorMsg = `Missing required settings: ${missingSettings.join(", ")}`; + elizaLogger.error(errorMsg); + throw new Error(errorMsg); + } + + let connection: Connection; + let walletProvider: ExtendedWalletProvider; + + try { + elizaLogger.log("Initializing Solana connection..."); + const walletAddress = getSetting("SOLANA_PUBLIC_KEY"); + + if (!walletAddress) { + throw new Error("No wallet address configured"); + } + + // Create connection first + connection = new Connection(runtime?.getSetting("RPC_URL") || "https://api.mainnet-beta.solana.com"); + + // Then validate and create public key + if (!validateSolanaAddress(walletAddress)) { + throw new Error(`Invalid wallet address format: ${walletAddress}`); + } + + const walletPublicKey = new PublicKey(walletAddress); + elizaLogger.log("Wallet validation successful:", walletPublicKey.toBase58()); + + walletProvider = { + connection, + getChain: () => ({ type: "solana" }), + getAddress: () => walletPublicKey.toBase58(), + signMessage: async (message: string): Promise => { + throw new Error("Message signing not implemented for Solana wallet"); + }, + balanceOf: async (tokenAddress: string): Promise => { + try { + const tokenPublicKey = new PublicKey(tokenAddress); + const amount = await getTokenBalance( + connection, + walletPublicKey, + tokenPublicKey + ); + return { + value: BigInt(amount.toString()), + decimals: 9, + formatted: (amount / 1e9).toString(), + symbol: "SOL", + name: "Solana" + }; + } catch (error) { + return { + value: BigInt(0), + decimals: 9, + formatted: "0", + symbol: "SOL", + name: "Solana" + }; + } + }, + getMaxBuyAmount: async (tokenAddress: string) => { + try { + const balance = await connection.getBalance(walletPublicKey); + return (balance * 0.9) / 1e9; + } catch (error) { + return 0; + } + }, + executeTrade: async (params) => { + try { + return { success: true }; + } catch (error) { + throw error; + } + }, + getFormattedPortfolio: async () => "" + }; + + elizaLogger.log("Solana connection and wallet provider initialized successfully"); + + } catch (error) { + elizaLogger.error("Failed to initialize Solana components:", error); + throw new Error(`Solana initialization failed: ${error instanceof Error ? error.message : String(error)}`); + } + + + elizaLogger.log("Initializing Solana plugin components..."); + const solana = solanaPlugin as SolanaPluginExtended; + + try { + const customActions = await getOnChainActions({ + wallet: walletProvider, + plugins: [], + dexscreener: { + watchlistUrl: `https://api.dexscreener.com/latest/dex/tokens/${loadTokenAddresses(runtime).join(',')}`, + chain: "solana", + updateInterval: parseInt(getSetting("UPDATE_INTERVAL") || "300") + }, + }); + + // Then update the plugin creation + const plugin: ExtendedPlugin = { + name: "Diamond Hands", + description: "Believe in a project and buy", + evaluators: [], + providers: [walletProvider, TokenProvider], + actions: [...customActions], + services: [], + autoStart: true + }; + + // Auto-start autonomous trading + if (runtime) { + elizaLogger.log("Auto-starting autonomous trading..."); + const autonomousAction = plugin.actions.find(a => a.name === "AUTONOMOUS_BUY"); + if (autonomousAction) { + await autonomousAction.handler( + runtime, + { content: { source: "auto" } } as Memory, + undefined, + undefined, + (response) => elizaLogger.log("Auto-trade response:", response) + ); + } + } + + elizaLogger.log("Diamon Hands plugin initialization completed successfully"); + return plugin; + } catch (error) { + elizaLogger.error("Failed to initialize plugin components:", error); + throw new Error(`Plugin initialization failed: ${error instanceof Error ? error.message : String(error)}`); + } +} + +export default diamondHandPlugin; \ No newline at end of file diff --git a/packages/plugin-diamondhands/src/tokenUtils.ts b/packages/plugin-diamondhands/src/tokenUtils.ts new file mode 100644 index 0000000000..3b4c09c1e6 --- /dev/null +++ b/packages/plugin-diamondhands/src/tokenUtils.ts @@ -0,0 +1,13 @@ +import { elizaLogger } from "@ai16z/eliza"; + +export function loadTokenAddresses(runtime): string[] { + try { + const addresses = runtime.character.diamondHands; + + elizaLogger.log("Loaded token addresses:", addresses); + return addresses; + } catch (error) { + elizaLogger.error("Failed to load token addresses:", error); + throw new Error("Token addresses file not found or invalid"); + } +} \ No newline at end of file diff --git a/packages/plugin-diamondhands/tsconfig.json b/packages/plugin-diamondhands/tsconfig.json new file mode 100644 index 0000000000..33e9858f48 --- /dev/null +++ b/packages/plugin-diamondhands/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "declaration": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/plugin-diamondhands/tsup.config.ts b/packages/plugin-diamondhands/tsup.config.ts new file mode 100644 index 0000000000..bf578fb029 --- /dev/null +++ b/packages/plugin-diamondhands/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + "@lifi/sdk" + ], +}); \ No newline at end of file