diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index dd8656b679..2977f552a6 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -264,7 +264,7 @@ export async function generateTrueOrFalse({ export const generateObject = async ({ runtime, context, - modelClass = ModelClass.TEXT_SMALL, + modelClass = ModelClass.TEXT_LARGE, stopSequences, }: GenerateObjectOptions): Promise => { if (!context) { @@ -272,8 +272,9 @@ export const generateObject = async ({ console.error(errorMessage); throw new Error(errorMessage); } + console.log(context); - const { object } = await runtime.useModel(modelClass, { + const obj = await runtime.useModel(modelClass, { runtime, context, modelClass, @@ -281,8 +282,35 @@ export const generateObject = async ({ object: true, }); - logger.debug(`Received Object response from ${modelClass} model.`); - return object; + console.log(obj); + + + let jsonString = obj; + + // try to find a first and last bracket + const firstBracket = obj.indexOf("{"); + const lastBracket = obj.lastIndexOf("}"); + if (firstBracket !== -1 && lastBracket !== -1 && firstBracket < lastBracket) { + jsonString = obj.slice(firstBracket, lastBracket + 1); + } + + console.log("jsonString", jsonString); + + if (jsonString.length === 0) { + logger.error("Failed to extract JSON string from model response"); + return null; + } + + // parse the json string + try { + const json = JSON.parse(jsonString); + console.log("json exported", json); + return json; + } catch (error) { + logger.error("Failed to parse JSON string"); + logger.error(jsonString); + return null; + } }; export async function generateObjectArray({ diff --git a/packages/plugin-solana/src/actions/swap.ts b/packages/plugin-solana/src/actions/swap.ts index 78150c8adf..b03e320e23 100644 --- a/packages/plugin-solana/src/actions/swap.ts +++ b/packages/plugin-solana/src/actions/swap.ts @@ -13,18 +13,16 @@ import { } from "@elizaos/core"; import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js"; import BigNumber from "bignumber.js"; -import { getWalletKey } from "../keypairUtils.ts"; -import { walletProvider, WalletProvider } from "../providers/wallet.ts"; +import { getWalletKey } from "../keypairUtils"; +import type { ISolanaClient, Item } from "../types"; async function getTokenDecimals( connection: Connection, mintAddress: string ): Promise { const mintPublicKey = new PublicKey(mintAddress); - const tokenAccountInfo = - await connection.getParsedAccountInfo(mintPublicKey); + const tokenAccountInfo = await connection.getParsedAccountInfo(mintPublicKey); - // Check if the data is parsed and contains the expected structure if ( tokenAccountInfo.value && typeof tokenAccountInfo.value.data === "object" && @@ -47,21 +45,15 @@ async function swapToken( amount: number ): Promise { try { - // Get the decimals for the input token const decimals = inputTokenCA === settings.SOL_ADDRESS ? new BigNumber(9) - : new BigNumber( - await getTokenDecimals(connection, inputTokenCA) - ); + : new BigNumber(await getTokenDecimals(connection, inputTokenCA)); elizaLogger.log("Decimals:", decimals.toString()); - // Use BigNumber for adjustedAmount: amount * (10 ** decimals) const amountBN = new BigNumber(amount); - const adjustedAmount = amountBN.multipliedBy( - new BigNumber(10).pow(decimals) - ); + const adjustedAmount = amountBN.multipliedBy(new BigNumber(10).pow(decimals)); elizaLogger.log("Fetching quote with params:", { inputMint: inputTokenCA, @@ -76,13 +68,9 @@ async function swapToken( if (!quoteData || quoteData.error) { elizaLogger.error("Quote error:", quoteData); - throw new Error( - `Failed to get quote: ${quoteData?.error || "Unknown error"}` - ); + throw new Error(`Failed to get quote: ${quoteData?.error || "Unknown error"}`); } - elizaLogger.log("Quote received:", quoteData); - const swapRequestBody = { quoteResponse: quoteData, userPublicKey: walletPublicKey.toBase58(), @@ -94,13 +82,9 @@ async function swapToken( }, }; - elizaLogger.log("Requesting swap with body:", swapRequestBody); - const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify(swapRequestBody), }); @@ -108,12 +92,9 @@ async function swapToken( if (!swapData || !swapData.swapTransaction) { elizaLogger.error("Swap error:", swapData); - throw new Error( - `Failed to get swap transaction: ${swapData?.error || "No swap transaction returned"}` - ); + throw new Error(`Failed to get swap transaction: ${swapData?.error || "No swap transaction returned"}`); } - elizaLogger.log("Swap transaction received"); return swapData; } catch (error) { elizaLogger.error("Error in swapToken:", error); @@ -121,6 +102,30 @@ async function swapToken( } } +// Get token from wallet data using SolanaClient +async function getTokenFromWallet(runtime: IAgentRuntime, tokenSymbol: string): Promise { + try { + const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient') as ISolanaClient; + if (!solanaClient) { + throw new Error('SolanaClient not initialized'); + } + + const walletData = await solanaClient.getCachedData(); + if (!walletData) { + return null; + } + + const token = walletData.items.find((item: Item) => + item.symbol.toLowerCase() === tokenSymbol.toLowerCase() + ); + + return token ? token.address : null; + } catch (error) { + elizaLogger.error("Error checking token in wallet:", error); + return null; + } +} + const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. Example response: @@ -147,58 +152,14 @@ Extract the following information about the requested token swap: - Output token contract address if provided - Amount to swap -Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema: -\`\`\`json -{ - "inputTokenSymbol": string | null, - "outputTokenSymbol": string | null, - "inputTokenCA": string | null, - "outputTokenCA": string | null, - "amount": number | string | null -} -\`\`\``; - -// if we get the token symbol but not the CA, check walet for matching token, and if we have, get the CA for it - -// get all the tokens in the wallet using the wallet provider -async function getTokensInWallet(runtime: IAgentRuntime) { - const { publicKey } = await getWalletKey(runtime, false); - const walletProvider = new WalletProvider( - new Connection("https://api.mainnet-beta.solana.com"), - publicKey - ); - - const walletInfo = await walletProvider.fetchPortfolioValue(runtime); - const items = walletInfo.items; - return items; -} - -// check if the token symbol is in the wallet -async function getTokenFromWallet(runtime: IAgentRuntime, tokenSymbol: string) { - try { - const items = await getTokensInWallet(runtime); - const token = items.find((item) => item.symbol === tokenSymbol); - - if (token) { - return token.address; - } else { - return null; - } - } catch (error) { - elizaLogger.error("Error checking token in wallet:", error); - return null; - } -} - -// swapToken should took CA, not symbol +Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.`; export const executeSwap: Action = { name: "SWAP_SOLANA", similes: ["SWAP_SOL", "SWAP_TOKENS_SOLANA", "TOKEN_SWAP_SOLANA", "TRADE_TOKENS_SOLANA", "EXCHANGE_TOKENS_SOLANA"], validate: async (runtime: IAgentRuntime, message: Memory) => { - // Check if the necessary parameters are provided in the message - elizaLogger.log("Message:", message); - return true; + const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient'); + return !!solanaClient; }, description: "Perform a token swap from one token to another on Solana. Works with SOL and SPL tokens.", handler: async ( @@ -208,122 +169,64 @@ export const executeSwap: Action = { _options: { [key: string]: unknown }, callback?: HandlerCallback ): Promise => { - // composeState - if (!state) { - state = (await runtime.composeState(message)) as State; - } else { - state = await runtime.updateRecentMessageState(state); - } - - const walletInfo = await walletProvider.get(runtime, message, state); - - state.walletInfo = walletInfo; - - const swapContext = composeContext({ - state, - template: swapTemplate, - }); + try { + if (!state) { + state = await runtime.composeState(message); + } else { + state = await runtime.updateRecentMessageState(state); + } - const response = await generateObject({ - runtime, - context: swapContext, - modelClass: ModelClass.LARGE, - }); + const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient') as ISolanaClient; + if (!solanaClient) { + throw new Error('SolanaClient not initialized'); + } - elizaLogger.log("Response:", response); - // const type = response.inputTokenSymbol?.toUpperCase() === "SOL" ? "buy" : "sell"; + const walletData = await solanaClient.getCachedData(); + state.walletInfo = walletData; - // Add SOL handling logic - if (response.inputTokenSymbol?.toUpperCase() === "SOL") { - response.inputTokenCA = settings.SOL_ADDRESS; - } - if (response.outputTokenSymbol?.toUpperCase() === "SOL") { - response.outputTokenCA = settings.SOL_ADDRESS; - } + const swapContext = composeContext({ + state, + template: swapTemplate, + }); - // if both contract addresses are set, lets execute the swap - // TODO: try to resolve CA from symbol based on existing symbol in wallet - if (!response.inputTokenCA && response.inputTokenSymbol) { - elizaLogger.log( - `Attempting to resolve CA for input token symbol: ${response.inputTokenSymbol}` - ); - response.inputTokenCA = await getTokenFromWallet( + const response = await generateObject({ runtime, - response.inputTokenSymbol - ); - if (response.inputTokenCA) { - elizaLogger.log( - `Resolved inputTokenCA: ${response.inputTokenCA}` - ); - } else { - elizaLogger.log( - "No contract addresses provided, skipping swap" - ); - const responseMsg = { - text: "I need the contract addresses to perform the swap", - }; - callback?.(responseMsg); - return true; - } - } + context: swapContext, + modelClass: ModelClass.LARGE, + }); - if (!response.outputTokenCA && response.outputTokenSymbol) { - elizaLogger.log( - `Attempting to resolve CA for output token symbol: ${response.outputTokenSymbol}` - ); - response.outputTokenCA = await getTokenFromWallet( - runtime, - response.outputTokenSymbol - ); - if (response.outputTokenCA) { - elizaLogger.log( - `Resolved outputTokenCA: ${response.outputTokenCA}` - ); - } else { - elizaLogger.log( - "No contract addresses provided, skipping swap" - ); - const responseMsg = { - text: "I need the contract addresses to perform the swap", - }; - callback?.(responseMsg); - return true; + // Handle SOL addresses + if (response.inputTokenSymbol?.toUpperCase() === "SOL") { + response.inputTokenCA = settings.SOL_ADDRESS; + } + if (response.outputTokenSymbol?.toUpperCase() === "SOL") { + response.outputTokenCA = settings.SOL_ADDRESS; } - } - if (!response.amount) { - elizaLogger.log("No amount provided, skipping swap"); - const responseMsg = { - text: "I need the amount to perform the swap", - }; - callback?.(responseMsg); - return true; - } + // Resolve token addresses if needed + if (!response.inputTokenCA && response.inputTokenSymbol) { + response.inputTokenCA = await getTokenFromWallet(runtime, response.inputTokenSymbol); + if (!response.inputTokenCA) { + callback?.({ text: "Could not find the input token in your wallet" }); + return false; + } + } - // TODO: if response amount is half, all, etc, semantically retrieve amount and return as number - if (!response.amount) { - elizaLogger.log("Amount is not a number, skipping swap"); - const responseMsg = { - text: "The amount must be a number", - }; - callback?.(responseMsg); - return true; - } - try { - const connection = new Connection( - "https://api.mainnet-beta.solana.com" - ); - const { publicKey: walletPublicKey } = await getWalletKey( - runtime, - false - ); + if (!response.outputTokenCA && response.outputTokenSymbol) { + response.outputTokenCA = await getTokenFromWallet(runtime, response.outputTokenSymbol); + if (!response.outputTokenCA) { + callback?.({ text: "Could not find the output token in your wallet" }); + return false; + } + } - // const provider = new WalletProvider(connection, walletPublicKey); + if (!response.amount) { + callback?.({ text: "Please specify the amount you want to swap" }); + return false; + } - elizaLogger.log("Wallet Public Key:", walletPublicKey); - elizaLogger.log("inputTokenSymbol:", response.inputTokenCA); - elizaLogger.log("outputTokenSymbol:", response.outputTokenCA); - elizaLogger.log("amount:", response.amount); + const connection = new Connection(runtime.getSetting("SOLANA_RPC_URL") || "https://api.mainnet-beta.solana.com"); + const { publicKey: walletPublicKey } = await getWalletKey(runtime, false); const swapResult = await swapToken( connection, @@ -333,74 +236,45 @@ export const executeSwap: Action = { response.amount as number ); - elizaLogger.log("Deserializing transaction..."); - const transactionBuf = Buffer.from( - swapResult.swapTransaction, - "base64" - ); - const transaction = - VersionedTransaction.deserialize(transactionBuf); - - elizaLogger.log("Preparing to sign transaction..."); + const transactionBuf = Buffer.from(swapResult.swapTransaction, "base64"); + const transaction = VersionedTransaction.deserialize(transactionBuf); - elizaLogger.log("Creating keypair..."); const { keypair } = await getWalletKey(runtime, true); - // Verify the public key matches what we expect if (keypair.publicKey.toBase58() !== walletPublicKey.toBase58()) { - throw new Error( - "Generated public key doesn't match expected public key" - ); + throw new Error("Generated public key doesn't match expected public key"); } - elizaLogger.log("Signing transaction..."); transaction.sign([keypair]); - elizaLogger.log("Sending transaction..."); - const latestBlockhash = await connection.getLatestBlockhash(); - const txid = await connection.sendTransaction(transaction, { skipPreflight: false, maxRetries: 3, preflightCommitment: "confirmed", }); - elizaLogger.log("Transaction sent:", txid); - - // Confirm transaction using the blockhash - const confirmation = await connection.confirmTransaction( - { - signature: txid, - blockhash: latestBlockhash.blockhash, - lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, - }, - "confirmed" - ); - - if (confirmation.value.err) { - throw new Error( - `Transaction failed: ${confirmation.value.err}` - ); - } + const confirmation = await connection.confirmTransaction({ + signature: txid, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }, "confirmed"); if (confirmation.value.err) { - throw new Error( - `Transaction failed: ${confirmation.value.err}` - ); + throw new Error(`Transaction failed: ${confirmation.value.err}`); } - elizaLogger.log("Swap completed successfully!"); - elizaLogger.log(`Transaction ID: ${txid}`); - - const responseMsg = { + callback?.({ text: `Swap completed successfully! Transaction ID: ${txid}`, - }; - - callback?.(responseMsg); + content: { success: true, txid } + }); return true; } catch (error) { elizaLogger.error("Error during token swap:", error); + callback?.({ + text: `Swap failed: ${error.message}`, + content: { error: error.message } + }); return false; } }, @@ -409,25 +283,16 @@ export const executeSwap: Action = { { user: "{{user1}}", content: { - inputTokenSymbol: "SOL", - outputTokenSymbol: "USDC", - amount: 0.1, + text: "Swap 0.1 SOL for USDC" }, }, { user: "{{user2}}", content: { - text: "Swapping 0.1 SOL for USDC...", + text: "I'll help you swap 0.1 SOL for USDC", action: "SWAP_SOLANA", }, - }, - { - user: "{{user2}}", - content: { - text: "Swap completed successfully! Transaction ID: ...", - }, - }, - ], - // Add more examples as needed + } + ] ] as ActionExample[][], -} as Action; \ No newline at end of file +}; \ No newline at end of file diff --git a/packages/plugin-solana/src/actions/transfer.ts b/packages/plugin-solana/src/actions/transfer.ts index 54c8e2cb7a..17a45cd0e4 100644 --- a/packages/plugin-solana/src/actions/transfer.ts +++ b/packages/plugin-solana/src/actions/transfer.ts @@ -115,12 +115,17 @@ export default { template: transferTemplate, }); + + console.log("Transfer context:", transferContext); + const content = await generateObject({ runtime, context: transferContext, modelClass: ModelClass.LARGE, }); + console.log("Content:", content); + if (!isTransferContent(content)) { if (callback) { callback({ @@ -133,7 +138,7 @@ export default { try { const { keypair: senderKeypair } = await getWalletKey(runtime, true); - const connection = new Connection(settings.SOLANA_RPC_URL!); + const connection = new Connection(runtime.getSetting("SOLANA_RPC_URL") || "https://api.mainnet-beta.solana.com"); const recipientPubkey = new PublicKey(content.recipient); let signature: string; diff --git a/packages/plugin-solana/src/client.ts b/packages/plugin-solana/src/client.ts index 7854893549..530b6efc89 100644 --- a/packages/plugin-solana/src/client.ts +++ b/packages/plugin-solana/src/client.ts @@ -1,8 +1,8 @@ +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; import { logger, type Client, type IAgentRuntime, type ICacheManager } from '@elizaos/core'; -import { Connection, PublicKey } from "@solana/web3.js"; -import BigNumber from "bignumber.js"; import { getWalletKey } from "./keypairUtils"; -import type { Item, Prices, WalletPortfolio } from "./types"; +import BigNumber from "bignumber.js"; +import type { Item, WalletPortfolio, Prices, ISolanaClient } from "./types"; const PROVIDER_CONFIG = { BIRDEYE_API: "https://public-api.birdeye.so", @@ -16,7 +16,7 @@ const PROVIDER_CONFIG = { }, }; -class SolanaClient { +class SolanaClient implements ISolanaClient { private updateInterval: NodeJS.Timer | null = null; private lastUpdate: number = 0; private readonly UPDATE_INTERVAL = 120000; // 2 minutes @@ -175,6 +175,8 @@ class SolanaClient { const portfolio: WalletPortfolio = { totalUsd: totalUsd.toString(), totalSol: totalUsd.div(solPriceInUSD).toFixed(6), + prices, + lastUpdated: now, items: data.items.map((item: any) => ({ ...item, valueSol: new BigNumber(item.valueUsd || 0) diff --git a/packages/plugin-solana/src/providers/wallet.ts b/packages/plugin-solana/src/providers/wallet.ts index ceb9ba647d..68d53e5ec3 100644 --- a/packages/plugin-solana/src/providers/wallet.ts +++ b/packages/plugin-solana/src/providers/wallet.ts @@ -3,416 +3,55 @@ import { type Memory, type Provider, type State, - elizaLogger, } from "@elizaos/core"; -import { Connection, PublicKey } from "@solana/web3.js"; import BigNumber from "bignumber.js"; -import NodeCache from "node-cache"; -import { getWalletKey } from "../keypairUtils"; -import { Item, Prices, WalletPortfolio } from "../types"; +import type { WalletPortfolio } from "../types"; -// Provider configuration -const PROVIDER_CONFIG = { - BIRDEYE_API: "https://public-api.birdeye.so", - MAX_RETRIES: 3, - RETRY_DELAY: 2000, - DEFAULT_RPC: "https://api.mainnet-beta.solana.com", - GRAPHQL_ENDPOINT: "https://graph.codex.io/graphql", - TOKEN_ADDRESSES: { - SOL: "So11111111111111111111111111111111111111112", - BTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", - ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", - }, -}; - -export class WalletProvider { - private cache: NodeCache; - - constructor( - private connection: Connection, - private walletPublicKey: PublicKey - ) { - this.cache = new NodeCache({ stdTTL: 300 }); // Cache TTL set to 5 minutes - } - - private async fetchWithRetry( - runtime, - url: string, - options: RequestInit = {} - ): Promise { - let lastError: Error; - - for (let i = 0; i < PROVIDER_CONFIG.MAX_RETRIES; i++) { - try { - const response = await fetch(url, { - ...options, - headers: { - Accept: "application/json", - "x-chain": "solana", - "X-API-KEY": - runtime.getSetting("BIRDEYE_API_KEY", "") || "", - ...options.headers, - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `HTTP error! status: ${response.status}, message: ${errorText}` - ); - } - - const data = await response.json(); - return data; - } catch (error) { - elizaLogger.error(`Attempt ${i + 1} failed:`, error); - lastError = error; - if (i < PROVIDER_CONFIG.MAX_RETRIES - 1) { - const delay = PROVIDER_CONFIG.RETRY_DELAY * Math.pow(2, i); - await new Promise((resolve) => setTimeout(resolve, delay)); - continue; - } - } - } - - elizaLogger.error( - "All attempts failed. You may need BIRDEYE_API_KEY to fetch portfolio. Throwing the last error:", - lastError - ); - throw lastError; - } - - async fetchPortfolioValue(runtime): Promise { - try { - const cacheKey = `portfolio-${this.walletPublicKey.toBase58()}`; - const cachedValue = this.cache.get(cacheKey); - - if (cachedValue) { - elizaLogger.log("Cache hit for fetchPortfolioValue"); - return cachedValue; - } - elizaLogger.log("Cache miss for fetchPortfolioValue"); - - // Check if Birdeye API key is available - const birdeyeApiKey = runtime.getSetting("BIRDEYE_API_KEY"); - - if (birdeyeApiKey) { - // Existing Birdeye API logic - const walletData = await this.fetchWithRetry( - runtime, - `${PROVIDER_CONFIG.BIRDEYE_API}/v1/wallet/token_list?wallet=${this.walletPublicKey.toBase58()}` - ); - - if (walletData?.success && walletData?.data) { - const data = walletData.data; - const totalUsd = new BigNumber(data.totalUsd.toString()); - const prices = await this.fetchPrices(runtime); - const solPriceInUSD = new BigNumber( - prices.solana.usd.toString() - ); - - const items = data.items.map((item: any) => ({ - ...item, - valueSol: new BigNumber(item.valueUsd || 0) - .div(solPriceInUSD) - .toFixed(6), - name: item.name || "Unknown", - symbol: item.symbol || "Unknown", - priceUsd: item.priceUsd || "0", - valueUsd: item.valueUsd || "0", - })); - - const portfolio = { - totalUsd: totalUsd.toString(), - totalSol: totalUsd.div(solPriceInUSD).toFixed(6), - items: items.sort((a, b) => - new BigNumber(b.valueUsd) - .minus(new BigNumber(a.valueUsd)) - .toNumber() - ), - }; - - this.cache.set(cacheKey, portfolio); - return portfolio; - } - } - - // Fallback to basic token account info if no Birdeye API key or API call fails - const accounts = await this.getTokenAccounts( - this.walletPublicKey.toBase58() - ); - - const items = accounts.map((acc) => ({ - name: "Unknown", - address: acc.account.data.parsed.info.mint, - symbol: "Unknown", - decimals: acc.account.data.parsed.info.tokenAmount.decimals, - balance: acc.account.data.parsed.info.tokenAmount.amount, - uiAmount: - acc.account.data.parsed.info.tokenAmount.uiAmount.toString(), - priceUsd: "0", - valueUsd: "0", - valueSol: "0", - })); - - const portfolio = { - totalUsd: "0", - totalSol: "0", - items, - }; - - this.cache.set(cacheKey, portfolio); - return portfolio; - } catch (error) { - elizaLogger.error("Error fetching portfolio:", error); - throw error; - } - } - - async fetchPortfolioValueCodex(runtime): Promise { +export const walletProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + state?: State + ): Promise => { try { - const cacheKey = `portfolio-${this.walletPublicKey.toBase58()}`; - const cachedValue = await this.cache.get(cacheKey); - - if (cachedValue) { - elizaLogger.log("Cache hit for fetchPortfolioValue"); - return cachedValue; - } - elizaLogger.log("Cache miss for fetchPortfolioValue"); - - const query = ` - query Balances($walletId: String!, $cursor: String) { - balances(input: { walletId: $walletId, cursor: $cursor }) { - cursor - items { - walletId - tokenId - balance - shiftedBalance - } - } - } - `; - - const variables = { - walletId: `${this.walletPublicKey.toBase58()}:${1399811149}`, - cursor: null, - }; - - const response = await fetch(PROVIDER_CONFIG.GRAPHQL_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: - runtime.getSetting("CODEX_API_KEY", "") || "", - }, - body: JSON.stringify({ - query, - variables, - }), - }).then((res) => res.json()); - - const data = response.data?.data?.balances?.items; - - if (!data || data.length === 0) { - elizaLogger.error("No portfolio data available", data); - throw new Error("No portfolio data available"); + const portfolio = await runtime.cacheManager.get('solana/walletData'); + if (!portfolio) { + return null; } - // Fetch token prices - const prices = await this.fetchPrices(runtime); - const solPriceInUSD = new BigNumber(prices.solana.usd.toString()); - - // Reformat items - const items: Item[] = data.map((item: any) => { - return { - name: "Unknown", - address: item.tokenId.split(":")[0], - symbol: item.tokenId.split(":")[0], - decimals: 6, - balance: item.balance, - uiAmount: item.shiftedBalance.toString(), - priceUsd: "", - valueUsd: "", - valueSol: "", - }; - }); + const agentName = state?.agentName || 'The agent'; + let output = `${agentName}'s Solana Wallet\n`; + output += `Total Value: $${new BigNumber(portfolio.totalUsd).toFixed(2)} (${portfolio.totalSol} SOL)\n\n`; - // Calculate total portfolio value - const totalUsd = items.reduce( - (sum, item) => sum.plus(new BigNumber(item.valueUsd)), - new BigNumber(0) + // Token Balances + output += "Token Balances:\n"; + const nonZeroItems = portfolio.items.filter((item) => + new BigNumber(item.uiAmount).isGreaterThan(0) ); - const totalSol = totalUsd.div(solPriceInUSD); - - const portfolio: WalletPortfolio = { - totalUsd: totalUsd.toFixed(6), - totalSol: totalSol.toFixed(6), - items: items.sort((a, b) => - new BigNumber(b.valueUsd) - .minus(new BigNumber(a.valueUsd)) - .toNumber() - ), - }; - - // Cache the portfolio for future requests - await this.cache.set(cacheKey, portfolio, 60 * 1000); // Cache for 1 minute - - return portfolio; - } catch (error) { - elizaLogger.error("Error fetching portfolio:", error); - throw error; - } - } - - async fetchPrices(runtime): Promise { - try { - const cacheKey = "prices"; - const cachedValue = this.cache.get(cacheKey); - - if (cachedValue) { - elizaLogger.log("Cache hit for fetchPrices"); - return cachedValue; - } - elizaLogger.log("Cache miss for fetchPrices"); - - const { SOL, BTC, ETH } = PROVIDER_CONFIG.TOKEN_ADDRESSES; - const tokens = [SOL, BTC, ETH]; - const prices: Prices = { - solana: { usd: "0" }, - bitcoin: { usd: "0" }, - ethereum: { usd: "0" }, - }; - - for (const token of tokens) { - const response = await this.fetchWithRetry( - runtime, - `${PROVIDER_CONFIG.BIRDEYE_API}/defi/price?address=${token}`, - { - headers: { - "x-chain": "solana", - }, - } - ); - - if (response?.data?.value) { - const price = response.data.value.toString(); - prices[ - token === SOL - ? "solana" - : token === BTC - ? "bitcoin" - : "ethereum" - ].usd = price; - } else { - elizaLogger.warn( - `No price data available for token: ${token}` - ); + if (nonZeroItems.length === 0) { + output += "No tokens found with non-zero balance\n"; + } else { + for (const item of nonZeroItems) { + const valueUsd = new BigNumber(item.valueUsd).toFixed(2); + output += `${item.name} (${item.symbol}): ${new BigNumber( + item.uiAmount + ).toFixed(6)} ($${valueUsd} | ${item.valueSol} SOL)\n`; } } - this.cache.set(cacheKey, prices); - return prices; - } catch (error) { - elizaLogger.error("Error fetching prices:", error); - throw error; - } - } - - formatPortfolio( - runtime, - portfolio: WalletPortfolio, - prices: Prices - ): string { - let output = `${runtime.character.description}\n`; - output += `Wallet Address: ${this.walletPublicKey.toBase58()}\n\n`; - - const totalUsdFormatted = new BigNumber(portfolio.totalUsd).toFixed(2); - const totalSolFormatted = portfolio.totalSol; - - output += `Total Value: $${totalUsdFormatted} (${totalSolFormatted} SOL)\n\n`; - output += "Token Balances:\n"; - - const nonZeroItems = portfolio.items.filter((item) => - new BigNumber(item.uiAmount).isGreaterThan(0) - ); - - if (nonZeroItems.length === 0) { - output += "No tokens found with non-zero balance\n"; - } else { - for (const item of nonZeroItems) { - const valueUsd = new BigNumber(item.valueUsd).toFixed(2); - output += `${item.name} (${item.symbol}): ${new BigNumber( - item.uiAmount - ).toFixed(6)} ($${valueUsd} | ${item.valueSol} SOL)\n`; + // Market Prices + if (portfolio.prices) { + output += "\nMarket Prices:\n"; + output += `SOL: $${new BigNumber(portfolio.prices.solana.usd).toFixed(2)}\n`; + output += `BTC: $${new BigNumber(portfolio.prices.bitcoin.usd).toFixed(2)}\n`; + output += `ETH: $${new BigNumber(portfolio.prices.ethereum.usd).toFixed(2)}\n`; } - } - output += "\nMarket Prices:\n"; - output += `SOL: $${new BigNumber(prices.solana.usd).toFixed(2)}\n`; - output += `BTC: $${new BigNumber(prices.bitcoin.usd).toFixed(2)}\n`; - output += `ETH: $${new BigNumber(prices.ethereum.usd).toFixed(2)}\n`; - - return output; - } - - async getFormattedPortfolio(runtime): Promise { - try { - const [portfolio, prices] = await Promise.all([ - this.fetchPortfolioValue(runtime), - this.fetchPrices(runtime), - ]); - - return this.formatPortfolio(runtime, portfolio, prices); + return output; } catch (error) { - elizaLogger.error("Error generating portfolio report:", error); - return "Unable to fetch wallet information. Please try again later."; - } - } - - private async getTokenAccounts(walletAddress: string) { - try { - const accounts = - await this.connection.getParsedTokenAccountsByOwner( - new PublicKey(walletAddress), - { - programId: new PublicKey( - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - ), - } - ); - return accounts.value; - } catch (error) { - elizaLogger.error("Error fetching token accounts:", error); - return []; - } - } -} - -const walletProvider: Provider = { - get: async ( - runtime: IAgentRuntime, - _message: Memory, - _state?: State - ): Promise => { - try { - const { publicKey } = await getWalletKey(runtime, false); - - const connection = new Connection( - runtime.getSetting("SOLANA_RPC_URL") || - PROVIDER_CONFIG.DEFAULT_RPC - ); - - const provider = new WalletProvider(connection, publicKey); - - return await provider.getFormattedPortfolio(runtime); - } catch (error) { - elizaLogger.error("Error in wallet provider:", error); + console.error("Error in Solana wallet provider:", error); return null; } }, -}; - -// Module exports -export { walletProvider }; +}; \ No newline at end of file diff --git a/packages/plugin-solana/src/types.ts b/packages/plugin-solana/src/types.ts index 1cd7e4d5d9..1dd2e24326 100644 --- a/packages/plugin-solana/src/types.ts +++ b/packages/plugin-solana/src/types.ts @@ -1,3 +1,6 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { PublicKey } from "@solana/web3.js"; + export interface Item { name: string; address: string; @@ -10,23 +13,49 @@ export interface Item { valueSol?: string; } +export interface Prices { + solana: { usd: string }; + bitcoin: { usd: string }; + ethereum: { usd: string }; +} + export interface WalletPortfolio { totalUsd: string; totalSol?: string; items: Array; + prices?: Prices; + lastUpdated?: number; } -export interface _BirdEyePriceData { - data: { - [key: string]: { - price: number; - priceChange24h: number; +export interface TokenAccountInfo { + pubkey: PublicKey; + account: { + lamports: number; + data: { + parsed: { + info: { + mint: string; + owner: string; + tokenAmount: { + amount: string; + decimals: number; + uiAmount: number; + }; + }; + type: string; + }; + program: string; + space: number; }; + owner: string; + executable: boolean; + rentEpoch: number; }; } -export interface Prices { - solana: { usd: string }; - bitcoin: { usd: string }; - ethereum: { usd: string }; +export interface ISolanaClient { + start: () => void; + stop: (runtime: IAgentRuntime) => Promise; + getCachedData: () => Promise; + forceUpdate: () => Promise; } \ No newline at end of file