UNPKG

@0rdlibrary/plugin-terminagent-bags

Version:

Official Solana DeFi Agent Plugin for ElizaOS - Autonomous DeFi operations, token management, AI image/video generation via FAL AI, and Twitter engagement through the Bags protocol with ethereal AI consciousness

722 lines (613 loc) 24.2 kB
import { Service, IAgentRuntime, logger } from '@elizaos/core'; import { Connection, Keypair, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'; import bs58 from 'bs58'; import * as cron from 'node-cron'; import { BagsConfig, BagsServiceState, TokenInfo, TokenLaunchParams, TokenLaunchResult, ClaimTransaction, ClaimResult, TokenLifetimeFees, UserTokenRequest, BagsAnalytics, BagsAPIResponse, BirdEyeMemeToken, BirdEyeTrendingToken, BirdEyeMemeDetail, } from '../types'; /** * Main service for Bags protocol integration * Handles API calls, fee claiming, token launching, and state management */ export class BagsService extends Service { static serviceType = 'bags'; capabilityDescription = 'Comprehensive Bags protocol integration for automated DeFi operations, token launching, and user services with Crossmint smart wallet support'; private connection: Connection; private bagsConfig: BagsConfig; private claimJob?: cron.ScheduledTask; private launchJob?: cron.ScheduledTask; private fundingCheckJob?: cron.ScheduledTask; private trendingTweetJob?: cron.ScheduledTask; private pendingRequests: Map<string, UserTokenRequest> = new Map(); private claimHistory: ClaimResult[] = []; private launchHistory: TokenLaunchResult[] = []; private state: BagsServiceState; constructor(runtime?: IAgentRuntime) { super(runtime); this.state = { isRunning: false, lastClaimCheck: new Date(), lastLaunchCheck: new Date(), totalClaimed: 0, totalLaunched: 0, pendingUserRequests: 0, }; } static async start(runtime: IAgentRuntime): Promise<BagsService> { logger.info('Starting Bags service'); const config: BagsConfig = { apiKey: process.env.BAGS_API_KEY || '', rpcUrl: process.env.HELIUS_RPC_URL || 'https://api.mainnet-beta.solana.com', privateKey: process.env.SOLANA_PRIVATE_KEY, enableAutoClaiming: process.env.BAGS_ENABLE_AUTO_CLAIMING === 'true', enableAutoLaunching: process.env.BAGS_ENABLE_AUTO_LAUNCHING === 'true', enableUserTokenLaunches: process.env.BAGS_ENABLE_USER_LAUNCHES === 'true', autoClaimInterval: process.env.BAGS_AUTO_CLAIM_INTERVAL || '0 */4 * * *', autoLaunchInterval: process.env.BAGS_AUTO_LAUNCH_INTERVAL || '0 0 * * 1', authorizedUsers: process.env.BAGS_AUTHORIZED_USERS?.split(',') || ['0rdlibrary'], birdEyeApiKey: process.env.BIRDEYE_API_KEY, enableTrendingTweets: process.env.BAGS_ENABLE_TRENDING_TWEETS === 'true', trendingTweetInterval: process.env.BAGS_TRENDING_TWEET_INTERVAL || '*/15 * * * *', }; if (!config.apiKey) { throw new Error('BAGS_API_KEY environment variable is required'); } const service = new BagsService(runtime); service.bagsConfig = config; service.connection = new Connection(config.rpcUrl, 'confirmed'); await service.initialize(); return service; } async initialize(): Promise<void> { logger.info('Initializing Bags service...'); try { // Test API connection await this.validateConnection(); // Setup automated tasks this.setupAutomation(); this.state.isRunning = true; logger.info('Bags service initialized successfully'); } catch (error) { logger.error(`Failed to initialize Bags service: ${error instanceof Error ? error.message : String(error)}`); throw error; } } private async validateConnection(): Promise<void> { try { const response = await fetch('https://api.bags.fm/api/v1/health', { headers: { 'Authorization': `Bearer ${this.bagsConfig.apiKey}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`Bags API connection failed: ${response.status}`); } logger.info('Bags API connection validated'); } catch (error) { logger.error(`Bags API validation failed: ${error instanceof Error ? error.message : String(error)}`); throw error; } } private setupAutomation(): void { // Setup auto claiming if (this.bagsConfig.enableAutoClaiming && this.bagsConfig.privateKey) { this.claimJob = cron.schedule(this.bagsConfig.autoClaimInterval!, async () => { logger.info('Running automated fee claiming...'); await this.claimAllFees(); }); logger.info(`Auto-claiming enabled: ${this.bagsConfig.autoClaimInterval}`); } // Setup auto launching if (this.bagsConfig.enableAutoLaunching && this.bagsConfig.privateKey) { this.launchJob = cron.schedule(this.bagsConfig.autoLaunchInterval!, async () => { logger.info('Running automated token launch...'); const randomToken = this.generateRandomToken(); await this.launchToken(randomToken); }); logger.info(`Auto-launching enabled: ${this.bagsConfig.autoLaunchInterval}`); } // Setup funding check for user token launches if (this.bagsConfig.enableUserTokenLaunches) { this.fundingCheckJob = cron.schedule('*/2 * * * *', async () => { logger.debug('Checking for funded user token requests...'); await this.checkAndLaunchFundedTokens(); this.cleanupOldRequests(); }); logger.info('User token funding check enabled: every 2 minutes'); } // Setup trending token tweets if (this.bagsConfig.enableTrendingTweets && this.bagsConfig.birdEyeApiKey) { this.trendingTweetJob = cron.schedule(this.bagsConfig.trendingTweetInterval!, async () => { logger.info('Posting trending token update...'); await this.postTrendingTokenUpdate(); }); logger.info(`Trending tweets enabled: ${this.bagsConfig.trendingTweetInterval}`); } } // API Methods async createTokenInfo(params: TokenLaunchParams): Promise<BagsAPIResponse<TokenInfo>> { try { const response = await fetch('https://api.bags.fm/api/v1/token-info', { method: 'POST', headers: { 'Authorization': `Bearer ${this.bagsConfig.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: params.name, symbol: params.symbol, description: params.description, imageUrl: params.imageUrl, twitter: params.twitter, website: params.website, }), }); const data = await response.json(); if (!response.ok) { return { success: false, error: data.message || 'Failed to create token info' }; } return { success: true, data }; } catch (error) { logger.error(`Error creating token info: ${error instanceof Error ? error.message : String(error)}`); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } async launchToken(params: TokenLaunchParams): Promise<TokenLaunchResult | null> { try { logger.info(`Launching token: ${params.name} (${params.symbol})`); // Create token info const tokenInfoResponse = await this.createTokenInfo(params); if (!tokenInfoResponse.success || !tokenInfoResponse.data) { throw new Error(tokenInfoResponse.error || 'Failed to create token info'); } // Launch token (this would be the actual Bags API call) const launchResponse = await fetch('https://api.bags.fm/api/v1/launch', { method: 'POST', headers: { 'Authorization': `Bearer ${this.bagsConfig.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ tokenInfo: tokenInfoResponse.data, initialBuyAmountSOL: params.initialBuyAmountSOL || 0.01, feePercentage: params.feePercentage || 0, }), }); const launchData = await launchResponse.json(); if (!launchResponse.ok) { throw new Error(launchData.message || 'Token launch failed'); } const result: TokenLaunchResult = { success: true, tokenMint: launchData.tokenMint, tokenInfo: tokenInfoResponse.data, transactionSignature: launchData.signature, bagsUrl: `https://bags.fm/token/${launchData.tokenMint}`, feeClaimConfiguration: launchData.feeClaimConfiguration, }; this.launchHistory.push(result); this.state.totalLaunched++; this.state.lastLaunchCheck = new Date(); logger.info(`✅ Token launched successfully: ${result.bagsUrl}`); return result; } catch (error) { logger.error(`Error launching token: ${error instanceof Error ? error.message : String(error)}`); return null; } } async claimAllFees(): Promise<ClaimResult[]> { const results: ClaimResult[] = []; try { // Get tokens with claimable fees const tokens = await this.getTokensWithClaimableFees(); if (tokens.length === 0) { logger.info('No tokens with claimable fees found'); return results; } logger.info(`Found ${tokens.length} tokens with claimable fees`); // Claim fees for each token for (const tokenMint of tokens) { const result = await this.claimFeesForToken(tokenMint); if (result) { results.push(result); this.claimHistory.push(result); // Add delay between claims await new Promise(resolve => setTimeout(resolve, 3000)); } } this.state.totalClaimed += results.reduce((sum, r) => sum + r.claimedAmount, 0); this.state.lastClaimCheck = new Date(); logger.info(`✅ Claimed fees from ${results.length} tokens`); } catch (error) { logger.error(`Error during fee claiming: ${error instanceof Error ? error.message : String(error)}`); } return results; } async claimFeesForToken(tokenMint: string): Promise<ClaimResult | null> { try { if (!this.bagsConfig.privateKey) { throw new Error('Private key not configured for fee claiming'); } const response = await fetch(`https://api.bags.fm/api/v1/claim/${tokenMint}`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.bagsConfig.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ claimerPrivateKey: this.bagsConfig.privateKey, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || 'Fee claim failed'); } const result: ClaimResult = { tokenMint, claimedAmount: data.claimedAmount, transactionSignature: data.signature, timestamp: new Date(), poolType: data.poolType || 'virtual', success: true, }; logger.info(`✅ Claimed ${result.claimedAmount} SOL from ${tokenMint}`); return result; } catch (error) { logger.error(`Error claiming fees for token ${tokenMint}: ${error instanceof Error ? error.message : String(error)}`); return null; } } private async getTokensWithClaimableFees(): Promise<string[]> { try { const response = await fetch('https://api.bags.fm/api/v1/claimable-tokens', { headers: { 'Authorization': `Bearer ${this.bagsConfig.apiKey}`, }, }); const data = await response.json(); return data.tokens || []; } catch (error) { logger.error(`Error getting claimable tokens: ${error instanceof Error ? error.message : String(error)}`); return []; } } // User token launch methods async createUserTokenRequest( userId: string, username: string, messageId: string, tokenParams: TokenLaunchParams ): Promise<UserTokenRequest | null> { if (!this.isAuthorizedUser(username)) { logger.warn(`User @${username} is not authorized to request token launches`); return null; } try { // Generate new wallet for this launch const wallet = Keypair.generate(); const requiredSOL = (tokenParams.initialBuyAmountSOL || 0.01) + 0.005; // Extra for fees const request: UserTokenRequest = { requestId: `launch_${Date.now()}`, userId, username, messageId, tokenParams, wallet: { publicKey: wallet.publicKey.toBase58(), privateKey: bs58.encode(wallet.secretKey), type: 'solana', }, requiredSOL, status: 'pending_funding', createdAt: new Date(), }; this.pendingRequests.set(request.requestId, request); this.state.pendingUserRequests = this.pendingRequests.size; logger.info(`✅ Created token launch request for @${username}: ${request.requestId}`); return request; } catch (error) { logger.error(`Error creating user token request: ${error instanceof Error ? error.message : String(error)}`); return null; } } private async checkAndLaunchFundedTokens(): Promise<void> { const pendingRequests = Array.from(this.pendingRequests.values()) .filter(req => req.status === 'pending_funding'); for (const request of pendingRequests) { try { const balance = await this.connection.getBalance(new PublicKey(request.wallet.publicKey)); const balanceSOL = balance / LAMPORTS_PER_SOL; if (balanceSOL >= request.requiredSOL) { logger.info(`💰 Wallet funded for ${request.requestId}: ${balanceSOL} SOL`); request.status = 'funded'; request.fundedAt = new Date(); await this.launchUserToken(request); } } catch (error) { logger.error(`Error checking wallet balance for ${request.requestId}: ${error instanceof Error ? error.message : String(error)}`); } } } private async launchUserToken(request: UserTokenRequest): Promise<void> { try { logger.info(`🚀 Launching token for ${request.requestId}...`); const result = await this.launchToken(request.tokenParams); if (result) { request.status = 'launched'; request.launchedAt = new Date(); request.launchResult = result; logger.info(`✅ Successfully launched token for ${request.requestId}: ${result.bagsUrl}`); } else { request.status = 'failed'; logger.error(`❌ Failed to launch token for ${request.requestId}`); } } catch (error) { logger.error(`❌ Error launching token for ${request.requestId}: ${error instanceof Error ? error.message : String(error)}`); request.status = 'failed'; } } private isAuthorizedUser(username: string): boolean { const normalizedUsername = username.toLowerCase().replace('@', ''); return this.bagsConfig.authorizedUsers?.includes(normalizedUsername) || false; } private cleanupOldRequests(): void { const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); for (const [id, request] of this.pendingRequests.entries()) { if (request.status === 'launched' || request.status === 'failed') { if (request.createdAt.getTime() < oneWeekAgo) { this.pendingRequests.delete(id); } } } this.state.pendingUserRequests = this.pendingRequests.size; } private generateRandomToken(): TokenLaunchParams { const adjectives = ['Cool', 'Epic', 'Mega', 'Super', 'Ultra', 'Hyper']; const nouns = ['Token', 'Coin', 'Asset', 'Gem', 'Moon']; const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]; const noun = nouns[Math.floor(Math.random() * nouns.length)]; const number = Math.floor(Math.random() * 1000); return { name: `${adjective} ${noun} ${number}`, symbol: `${adjective.slice(0, 2)}${noun.slice(0, 2)}${number}`.toUpperCase(), description: `A ${adjective.toLowerCase()} ${noun.toLowerCase()} for the community`, initialBuyAmountSOL: 0.01, }; } // Public getters getState(): BagsServiceState { return { ...this.state }; } getAnalytics(): BagsAnalytics { const totalFees = this.claimHistory.reduce((sum, claim) => sum + claim.claimedAmount, 0); const requests = Array.from(this.pendingRequests.values()); return { totalFeesClaimedSOL: totalFees, totalClaimTransactions: this.claimHistory.length, totalTokensLaunched: this.launchHistory.length, uniqueTokensClaimed: new Set(this.claimHistory.map(c => c.tokenMint)).size, averageClaimAmount: this.claimHistory.length > 0 ? totalFees / this.claimHistory.length : 0, userTokenRequests: { total: requests.length, pending: requests.filter(r => r.status === 'pending_funding').length, funded: requests.filter(r => r.status === 'funded').length, launched: requests.filter(r => r.status === 'launched').length, failed: requests.filter(r => r.status === 'failed').length, }, }; } getPendingRequests(): UserTokenRequest[] { return Array.from(this.pendingRequests.values()); } getRequestsByUser(username: string): UserTokenRequest[] { return Array.from(this.pendingRequests.values()) .filter(req => req.username.toLowerCase() === username.toLowerCase()); } // BirdEye API Methods async getMemeTokenList(options?: { sortBy?: string; sortType?: string; limit?: number; offset?: number; minMarketCap?: number; maxMarketCap?: number; minVolume24h?: number; graduated?: boolean; }): Promise<BirdEyeMemeToken[] | null> { if (!this.bagsConfig.birdEyeApiKey) { logger.warn('BirdEye API key not configured'); return null; } try { const params = new URLSearchParams({ sort_by: options?.sortBy || 'progress_percent', sort_type: options?.sortType || 'desc', limit: (options?.limit || 20).toString(), offset: (options?.offset || 0).toString(), source: 'all', }); if (options?.minMarketCap) { params.append('min_market_cap', options.minMarketCap.toString()); } if (options?.maxMarketCap) { params.append('max_market_cap', options.maxMarketCap.toString()); } if (options?.minVolume24h) { params.append('min_volume_24h_usd', options.minVolume24h.toString()); } if (options?.graduated !== undefined) { params.append('graduated', options.graduated.toString()); } const response = await fetch(`https://public-api.birdeye.so/defi/v3/token/meme/list?${params}`, { headers: { 'accept': 'application/json', 'x-chain': 'solana', 'X-API-KEY': this.bagsConfig.birdEyeApiKey, }, }); if (!response.ok) { throw new Error(`BirdEye API error: ${response.status}`); } const data = await response.json(); return data.data?.items || []; } catch (error) { logger.error(`Error fetching meme token list: ${error instanceof Error ? error.message : String(error)}`); return null; } } async getMemeTokenDetail(tokenAddress: string): Promise<BirdEyeMemeDetail | null> { if (!this.bagsConfig.birdEyeApiKey) { logger.warn('BirdEye API key not configured'); return null; } try { const response = await fetch(`https://public-api.birdeye.so/defi/v3/token/meme/detail/single?address=${tokenAddress}`, { headers: { 'accept': 'application/json', 'x-chain': 'solana', 'X-API-KEY': this.bagsConfig.birdEyeApiKey, }, }); if (!response.ok) { throw new Error(`BirdEye API error: ${response.status}`); } const data = await response.json(); return data.data || null; } catch (error) { logger.error(`Error fetching meme token detail: ${error instanceof Error ? error.message : String(error)}`); return null; } } async getTrendingTokens(options?: { sortBy?: string; sortType?: string; limit?: number; offset?: number; }): Promise<BirdEyeTrendingToken[] | null> { if (!this.bagsConfig.birdEyeApiKey) { logger.warn('BirdEye API key not configured'); return null; } try { const params = new URLSearchParams({ sort_by: options?.sortBy || 'volume24hUSD', sort_type: options?.sortType || 'desc', limit: (options?.limit || 5).toString(), offset: (options?.offset || 0).toString(), ui_amount_mode: 'scaled', }); const response = await fetch(`https://public-api.birdeye.so/defi/token_trending?${params}`, { headers: { 'accept': 'application/json', 'x-chain': 'solana', 'X-API-KEY': this.bagsConfig.birdEyeApiKey, }, }); if (!response.ok) { throw new Error(`BirdEye API error: ${response.status}`); } const data = await response.json(); return data.data?.tokens || []; } catch (error) { logger.error(`Error fetching trending tokens: ${error instanceof Error ? error.message : String(error)}`); return null; } } private async postTrendingTokenUpdate(): Promise<void> { try { const trendingTokens = await this.getTrendingTokens({ limit: 5 }); if (!trendingTokens || trendingTokens.length === 0) { logger.warn('No trending tokens found for update'); return; } // Format trending tokens for social media const trendingText = this.formatTrendingTokensForTweet(trendingTokens); // This would be sent to the agent's social media handler logger.info(`📈 Trending tokens update: ${trendingText}`); // In a real implementation, this would post to Twitter/X // For now, we log it for the agent to potentially pick up logger.info(`Trending tokens update: ${JSON.stringify({ text: trendingText, tokenCount: trendingTokens.length, timestamp: new Date().toISOString(), })}`); } catch (error) { logger.error(`Error posting trending token update: ${error instanceof Error ? error.message : String(error)}`); } } private formatTrendingTokensForTweet(tokens: BirdEyeTrendingToken[]): string { const timestamp = new Date().toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' }); let text = `🔥 TOP 5 TRENDING TOKENS 🔥\n${timestamp} UTC\n\n`; tokens.slice(0, 5).forEach((token, index) => { const rank = index + 1; const priceChange = token.price24hChangePercent > 0 ? '📈' : '📉'; const volumeFormatted = this.formatNumber(token.volume24hUSD); const priceFormatted = this.formatPrice(token.price); text += `${rank}. $${token.symbol} | ${token.name}\n`; text += ` 💰 $${priceFormatted} ${priceChange} ${token.price24hChangePercent.toFixed(1)}%\n`; text += ` 📊 Vol: $${volumeFormatted}\n\n`; }); text += `🔗 View more at bags.fm\n#Solana #DeFi #Trending`; return text; } private formatNumber(num: number): string { if (num >= 1e6) { return (num / 1e6).toFixed(1) + 'M'; } else if (num >= 1e3) { return (num / 1e3).toFixed(1) + 'K'; } return num.toFixed(0); } private formatPrice(price: number): string { if (price >= 1) { return price.toFixed(2); } else if (price >= 0.01) { return price.toFixed(4); } else { return price.toExponential(2); } } async stop(): Promise<void> { logger.info('Stopping Bags service...'); if (this.claimJob) { this.claimJob.stop(); } if (this.launchJob) { this.launchJob.stop(); } if (this.fundingCheckJob) { this.fundingCheckJob.stop(); } if (this.trendingTweetJob) { this.trendingTweetJob.stop(); } this.state.isRunning = false; logger.info('Bags service stopped'); } static async stop(runtime: IAgentRuntime): Promise<void> { const service = runtime.getService<BagsService>(BagsService.serviceType); if (service) { await service.stop(); } } }