diff --git a/.env.example b/.env.example index 7603a9f2c59..86aaa58f977 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,27 @@ +# OpenAI Configuration OPENAI_API_KEY= + +# Anthropic Configuration ANTHROPIC_API_KEY= + +# Fill these out if you want to use Discord +DISCORD_APPLICATION_ID= +DISCORD_API_TOKEN= + +# Fill these out if you want to use Telegram +TELEGRAM_BOT_TOKEN= + +# Fill these out if you want to use Twitter +TWITTER_USERNAME= +TWITTER_PASSWORD= +TWITTER_EMAIL= + +# Fill these out if you want to use EVM +EVM_PRIVATE_KEY= +EVM_CHAINS=mainnet,sepolia,base,arbitrum,polygon +EVM_PROVIDER_URL= + +# Fill these out if you want to use Solana +SOLANA_PUBLIC_KEY= +SOLANA_PRIVATE_KEY= +BIRDEYE_API_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9febd13a35d..caf0f5024db 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ node_modules .env_main concatenated-output.ts embedding-cache.json -packages/plugin-buttplug/intiface-engine node-compile-cache @@ -50,10 +49,7 @@ characters/**/*.env characters/**/*.key characters/**/private/ -packages/core/src/providers/cache -packages/core/src/providers/cache/* cache/* -packages/plugin-coinbase/src/plugins/transactions.csv tsup.config.bundled_*.mjs @@ -71,10 +67,6 @@ eliza.manifest eliza.manifest.sgx eliza.sig -packages/plugin-nvidia-nim/extra -packages/plugin-nvidia-nim/old_code -packages/plugin-nvidia-nim/docs - # Edriziai specific ignores characters/edriziai-info/secrets.json diff --git a/bun.lockb b/bun.lockb index 8b3ceb7127f..d5576174e8a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a30dbbe88eb..1f733a9b518 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:cli": "turbo run build --filter=./packages/cli && cd packages/cli && bun link", "start": "turbo run start --filter=!./packages/docs", "agent": "turbo run start --filter=@elizaos/agent", - "dev": "turbo run build --filter=./packages/core && turbo run dev --filter=!./packages/core --concurrency=20", + "dev": "turbo run build --filter=./packages/core && turbo run dev --filter=!./packages/core --filter=!./packages/docs --concurrency=20", "release": "bun run build && bun format && npx lerna publish --no-private --force-publish", "docker:build": "bash ./scripts/docker.sh build", "docker:run": "bash ./scripts/docker.sh run", diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index bfee1123210..f5ec4cc0ca9 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -307,8 +307,6 @@ export function createApiRouter( router.post("/agent/start", async (req, res) => { const { characterPath, characterJson } = req.body; - console.log("characterPath:", characterPath); - console.log("characterJson:", characterJson); try { let character: Character; if (characterJson) { @@ -340,7 +338,6 @@ export function createApiRouter( router.post("/agents/:agentId/stop", async (req, res) => { const agentId = req.params.agentId; - console.log("agentId", agentId); const agent: IAgentRuntime = agents.get(agentId); // update character diff --git a/packages/agent/src/defaultCharacter.ts b/packages/agent/src/defaultCharacter.ts index 2f3a570de06..31b51a5478d 100644 --- a/packages/agent/src/defaultCharacter.ts +++ b/packages/agent/src/defaultCharacter.ts @@ -4,13 +4,15 @@ export const defaultCharacter: Character = { name: "Eliza", username: "eliza", plugins: [ - "@elizaos/plugin-anthropic", + // "@elizaos/plugin-anthropic", "@elizaos/plugin-openai", - "@elizaos/plugin-local-ai", - "@elizaos/plugin-discord", + // "@elizaos/plugin-local-ai", + // "@elizaos/plugin-discord", "@elizaos/plugin-node", - "@elizaos/plugin-telegram", - "@elizaos/plugin-twitter", + // "@elizaos/plugin-telegram", + // "@elizaos/plugin-twitter", + "@elizaos/plugin-evm", + "@elizaos/plugin-solana", ], settings: { secrets: {}, diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 5307b322b03..efd7b8d03bb 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -348,7 +348,7 @@ async function findDatabaseAdapter(runtime: IAgentRuntime) { let adapter: Adapter | undefined; // if not found, default to sqlite if (adapters.length === 0) { - const sqliteAdapterPlugin = await import('@elizaos-plugins/sqlite'); + const sqliteAdapterPlugin = await import('@elizaos/plugin-sqlite'); const sqliteAdapterPluginDefault = sqliteAdapterPlugin.default; adapter = sqliteAdapterPluginDefault.adapters[0]; if (!adapter) { diff --git a/packages/agent/src/plugins.test.ts b/packages/agent/src/plugins.test.ts index 16618050955..3ae9a52d30f 100644 --- a/packages/agent/src/plugins.test.ts +++ b/packages/agent/src/plugins.test.ts @@ -25,7 +25,7 @@ async function findDatabaseAdapter(runtime: IAgentRuntime) { // Default to sqlite if no adapter found if (adapters.length === 0) { - const sqliteAdapter = await import('@elizaos-plugins/sqlite'); + const sqliteAdapter = await import('@elizaos/plugin-sqlite'); adapter = sqliteAdapter.default.adapters[0]; if (!adapter) { throw new Error("No database adapter found in default sqlite plugin"); diff --git a/packages/agent/src/server.ts b/packages/agent/src/server.ts index 827b9b56f8f..83cc2fa254b 100644 --- a/packages/agent/src/server.ts +++ b/packages/agent/src/server.ts @@ -1,16 +1,15 @@ import { composeContext, - logger, generateMessageResponse, generateObject, - messageCompletionFooter, + logger, ModelClass, stringToUuid, + type Character, type Content, - type Media, - type Memory, type IAgentRuntime, - type Character + type Media, + type Memory } from "@elizaos/core"; import bodyParser from "body-parser"; import cors from "cors"; @@ -20,9 +19,8 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { z } from "zod"; import { createApiRouter } from "./api.ts"; +import { hyperfiHandlerTemplate, messageHandlerTemplate, upload } from "./helper.ts"; import replyAction from "./reply.ts"; -import { messageHandlerTemplate } from "./helper.ts"; -import { upload } from "./helper.ts"; @@ -31,7 +29,7 @@ export class CharacterServer { public app: express.Application; private agents: Map; // container management private server: any; // Store server instance - public startAgent: () => Promise; // Store startAgent function + public startAgent: (character: Character) => Promise; // Store startAgent function public loadCharacterTryPath: (characterPath: string) => Promise; // Store loadCharacterTryPath function public jsonToCharacter: (filePath: string, character: string | never) => Promise; // Store jsonToCharacter function @@ -91,9 +89,10 @@ export class CharacterServer { return; } - const transcription = await runtime.useModel(ModelClass.TRANSCRIPTION, fs.createReadStream(audioFile.path)); - - res.json(transcription); + const audioBuffer = fs.readFileSync(audioFile.path); + const transcription = await runtime.useModel(ModelClass.TRANSCRIPTION, audioBuffer); + + res.json({text: transcription}); } ); diff --git a/packages/cli/src/commands/character.ts b/packages/cli/src/commands/character.ts index 81d02cf7692..e052a83047b 100644 --- a/packages/cli/src/commands/character.ts +++ b/packages/cli/src/commands/character.ts @@ -1,5 +1,5 @@ // src/commands/agent.ts -import { Database, SqliteDatabaseAdapter } from "@elizaos-plugins/sqlite"; +import { Database, SqliteDatabaseAdapter } from "@elizaos/plugin-sqlite"; import type { MessageExample, UUID } from "@elizaos/core"; import { MessageExampleSchema } from "@elizaos/core"; import { Command } from "commander"; @@ -116,10 +116,16 @@ async function collectCharacterData( const examples = response.value .split('\\n') .map(line => line.trim()) - .filter(Boolean); - formData.messageExamples = examples.length > 0 - ? examples - : [`{{user1}}: hey how are you?\n${formData.name}`]; + .filter(Boolean) + .map(line => ({ + user: line.split(':')[0].trim(), + content: { + text: line.split(':').slice(1).join(':').trim() + } + })); + formData.messageExamples = examples.length > 0 + ? [examples] + : []; break; } diff --git a/packages/cli/src/database.ts b/packages/cli/src/database.ts index 393beac7169..1627cd403a0 100644 --- a/packages/cli/src/database.ts +++ b/packages/cli/src/database.ts @@ -1,5 +1,5 @@ import Database from "better-sqlite3" -import { SqliteDatabaseAdapter } from "@elizaos-plugins/sqlite" +import { SqliteDatabaseAdapter } from "@elizaos/plugin-sqlite" // Initialize database export const adapter = new SqliteDatabaseAdapter(new Database("./eliza.db")) diff --git a/packages/cli/src/templates/database/sqlite.ts.txt b/packages/cli/src/templates/database/sqlite.ts.txt index 1ffdcf4c363..53faf20fa9b 100644 --- a/packages/cli/src/templates/database/sqlite.ts.txt +++ b/packages/cli/src/templates/database/sqlite.ts.txt @@ -1,4 +1,4 @@ -import { SqliteDatabaseAdapter } from '@elizaos-plugins/sqlite'; +import { SqliteDatabaseAdapter } from '@elizaos/plugin-sqlite'; import Database from 'better-sqlite3'; import path from 'path'; diff --git a/packages/cli/src/utils/registry/index.ts b/packages/cli/src/utils/registry/index.ts index 96e43429a69..5764dcd4861 100644 --- a/packages/cli/src/utils/registry/index.ts +++ b/packages/cli/src/utils/registry/index.ts @@ -9,7 +9,6 @@ const agent = process.env.https_proxy export async function getRegistryIndex(): Promise { try { - console.log("REGISTRY_URL", REGISTRY_URL) const response = await fetch(REGISTRY_URL, { agent }) // Get the response body as text first const text = await response.text() diff --git a/packages/cli/src/utils/templates.ts b/packages/cli/src/utils/templates.ts index 73d99701a52..2504b079342 100644 --- a/packages/cli/src/utils/templates.ts +++ b/packages/cli/src/utils/templates.ts @@ -2,7 +2,7 @@ export function createDatabaseTemplate(database: string) { if (database === "sqlite") { return `import { Database } from "better-sqlite3" - import { SqliteDatabaseAdapter } from "@elizaos-plugins/sqlite" + import { SqliteDatabaseAdapter } from "@elizaos/plugin-sqlite" // Initialize database export const db = new Database("./eliza.db") diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index fb43f779ae7..056a1369136 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -131,12 +131,19 @@ export async function generateText({ stopSequences?: string[]; customSystemPrompt?: string; }): Promise { + + logger.debug("Generating text") + logger.debug(context) + const text = await runtime.useModel(modelClass, { runtime, context, stopSequences, }); + logger.debug("Generated text") + logger.debug(text) + return text; } @@ -257,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) { @@ -266,7 +273,7 @@ export const generateObject = async ({ throw new Error(errorMessage); } - const { object } = await runtime.useModel(modelClass, { + const obj = await runtime.useModel(modelClass, { runtime, context, modelClass, @@ -274,8 +281,29 @@ export const generateObject = async ({ object: true, }); - logger.debug(`Received Object response from ${modelClass} model.`); - return object; + 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); + } + + 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); + return json; + } catch (error) { + logger.error("Failed to parse JSON string"); + logger.error(jsonString); + return null; + } }; export async function generateObjectArray({ diff --git a/packages/core/src/knowledge.ts b/packages/core/src/knowledge.ts index 8b157646fa8..9a149d7455d 100644 --- a/packages/core/src/knowledge.ts +++ b/packages/core/src/knowledge.ts @@ -8,7 +8,6 @@ async function get( runtime: AgentRuntime, message: Memory ): Promise { - console.log("get", message); // Add validation for message if (!message?.content?.text) { logger.warn("Invalid message for knowledge query:", { @@ -70,7 +69,6 @@ async function set( chunkSize = 512, bleed = 20 ) { - console.log("set", item); const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, null); await runtime.documentsManager.createMemory({ id: item.id, diff --git a/packages/core/src/logger.ts b/packages/core/src/logger.ts index 76c4b0a5547..21b2b6ddf49 100644 --- a/packages/core/src/logger.ts +++ b/packages/core/src/logger.ts @@ -29,7 +29,7 @@ const createStream = () => { }); }; -const defaultLevel = process?.env?.DEFAULT_LOG_LEVEL || "info"; +const defaultLevel = process?.env?.DEFAULT_LOG_LEVEL || process?.env?.LOG_LEVEL || "info"; const options = { level: defaultLevel, diff --git a/packages/core/src/memory.ts b/packages/core/src/memory.ts index bda44b96f7b..08ea3e13e6e 100644 --- a/packages/core/src/memory.ts +++ b/packages/core/src/memory.ts @@ -50,7 +50,6 @@ export class MemoryManager implements IMemoryManager { * @throws Error if the memory content is empty */ async addEmbeddingToMemory(memory: Memory): Promise { - console.log("addEmbeddingToMemory", memory); // Return early if embedding already exists if (memory.embedding) { return memory; @@ -174,7 +173,6 @@ export class MemoryManager implements IMemoryManager { */ async createMemory(memory: Memory, unique = false): Promise { // TODO: check memory.agentId == this.runtime.agentId - console.log("createMemory", memory); const existingMessage = await this.runtime.databaseAdapter.getMemoryById(memory.id); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 0ce9fb06259..d67adb17836 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -327,6 +327,15 @@ export class AgentRuntime implements IAgentRuntime { for(const route of plugin.routes){ this.routes.push(route); } + + for(const client of plugin.clients){ + client.start(this).then((startedClient) => { + logger.debug( + `Initializing client: ${client.name}` + ); + this.clients.push(startedClient); + }); + } } this.plugins = plugins; @@ -353,6 +362,25 @@ export class AgentRuntime implements IAgentRuntime { this.clients.push(startedClient); } } + + if (plugin.actions) { + for (const action of plugin.actions) { + this.registerAction(action); + } + } + + if (plugin.evaluators) { + for (const evaluator of plugin.evaluators) { + this.registerEvaluator(evaluator); + } + } + + if (plugin.providers) { + for (const provider of plugin.providers) { + this.registerContextProvider(provider); + } + } + if (plugin.models) { for (const [modelClass, handler] of Object.entries(plugin.models)) { this.registerModel(modelClass as ModelClass, handler as (params: any) => Promise); @@ -602,8 +630,6 @@ export class AgentRuntime implements IAgentRuntime { modelClass: ModelClass.TEXT_SMALL, }); - console.log("***** result", result); - const evaluators = parseJsonArrayFromText( result, ) as unknown as string[]; diff --git a/packages/core/src/test_resources/createRuntime.ts b/packages/core/src/test_resources/createRuntime.ts index 21beebe4d21..3f3b1bf3eb2 100644 --- a/packages/core/src/test_resources/createRuntime.ts +++ b/packages/core/src/test_resources/createRuntime.ts @@ -1,4 +1,4 @@ -import { SqliteDatabaseAdapter, loadVecExtensions } from "@elizaos-plugins/sqlite"; +import { SqliteDatabaseAdapter, loadVecExtensions } from "@elizaos/plugin-sqlite"; import type { DatabaseAdapter } from "../database.ts"; import { AgentRuntime } from "../runtime.ts"; import type { Action, Evaluator, Provider } from "../types.ts"; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b9cd5c6f0f2..e417795c36e 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -527,7 +527,7 @@ export type Media = { */ export type ClientInstance = { /** Client name */ - // name: string; + name?: string; /** Stop client connection */ stop: (runtime: IAgentRuntime) => Promise; diff --git a/packages/plugin-anthropic/src/index.ts b/packages/plugin-anthropic/src/index.ts index 9c3418566c1..7cdde4c6208 100644 --- a/packages/plugin-anthropic/src/index.ts +++ b/packages/plugin-anthropic/src/index.ts @@ -20,10 +20,12 @@ export const anthropicPlugin: Plugin = { async init(config: Record) { try { const validatedConfig = await configSchema.parseAsync(config); - // Set all validated configuration values as environment variables. - Object.entries(validatedConfig).forEach(([key, value]) => { + + // Set all environment variables at once + for (const [key, value] of Object.entries(validatedConfig)) { if (value) process.env[key] = value; - }); + } + // (Optional) If the Anthropics SDK supports API key verification, // you might add a check here. } catch (error) { diff --git a/packages/plugin-openai/src/index.ts b/packages/plugin-openai/src/index.ts index f14041f87dd..be4cf054df3 100644 --- a/packages/plugin-openai/src/index.ts +++ b/packages/plugin-openai/src/index.ts @@ -50,9 +50,9 @@ export const openaiPlugin: Plugin = { const validatedConfig = await configSchema.parseAsync(config); // Set all environment variables at once - Object.entries(validatedConfig).forEach(([key, value]) => { + for (const [key, value] of Object.entries(validatedConfig)) { if (value) process.env[key] = value; - }); + } // Verify API key const baseURL = @@ -146,6 +146,9 @@ export const openaiPlugin: Plugin = { runtime.getSetting("SMALL_MODEL") ?? "gpt-4o-mini"; + console.log("generating text") + console.log(context) + const { text: openaiResponse } = await aiGenerateText({ model: openai.languageModel(model), prompt: context, @@ -178,10 +181,6 @@ export const openaiPlugin: Plugin = { baseURL, }); - const smallModel = - runtime.getSetting("OPENAI_SMALL_MODEL") ?? - runtime.getSetting("SMALL_MODEL") ?? - "gpt-4o-mini"; const model = runtime.getSetting("OPENAI_LARGE_MODEL") ?? runtime.getSetting("LARGE_MODEL") ?? "gpt-4o"; diff --git a/packages/plugin-solana/.npmignore b/packages/plugin-solana/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/plugin-solana/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-solana/README.md b/packages/plugin-solana/README.md new file mode 100644 index 00000000000..c0c3e79d8c8 --- /dev/null +++ b/packages/plugin-solana/README.md @@ -0,0 +1,374 @@ +# @elizaos/plugin-solana + +Core Solana blockchain plugin for Eliza OS that provides essential services and actions for token operations, trading, and DeFi integrations. + +## Overview + +The Solana plugin serves as a foundational component of Eliza OS, bridging Solana blockchain capabilities with the Eliza ecosystem. It provides crucial services for token operations, trading, portfolio management, and DeFi integrations, enabling both automated and user-directed interactions with the Solana blockchain. + +## Features + +### Token Operations + +- **Token Creation**: Deploy new tokens with customizable metadata +- **Token Transfers**: Send and receive tokens securely +- **Balance Management**: Track and manage token balances +- **Portfolio Analytics**: Real-time portfolio valuation and tracking + +### Trading Operations + +- **Token Swaps**: Execute trades between tokens using Jupiter aggregator +- **Order Management**: Place and track token orders +- **Price Monitoring**: Real-time price feeds and historical data +- **Automated Trading**: Configurable trading strategies and automation + +### DeFi Integration + +- **Liquidity Analysis**: Monitor and analyze pool liquidity +- **Market Making**: Automated market making capabilities +- **Yield Optimization**: Smart routing for optimal yields +- **Risk Management**: Advanced risk scoring and monitoring + +### Trust & Security + +- **Trust Scoring**: Dynamic trust score calculation for tokens +- **Risk Assessment**: Real-time risk evaluation for trades +- **Performance Tracking**: Historical performance monitoring +- **Simulation Mode**: Test strategies without real transactions + +## Security Features + +### Access Control + +- **Wallet Management**: Secure wallet key derivation and storage +- **Permission Scoping**: Granular control over trading permissions +- **TEE Integration**: Trusted Execution Environment support +- **Key Protection**: Secure private key handling + +### Risk Management + +- **Trade Limits**: Configurable transaction limits +- **Slippage Protection**: Automatic slippage controls +- **Validation Checks**: Multi-level transaction validation +- **Simulation Support**: Pre-execution transaction simulation + +## Installation + +```bash +npm install @elizaos/plugin-solana +``` + +## Configuration + +Configure the plugin by setting the following environment variables: + +```typescript +const solanaEnvSchema = { + WALLET_SECRET_SALT: string(optional), + WALLET_SECRET_KEY: string, + WALLET_PUBLIC_KEY: string, + SOL_ADDRESS: string, + SLIPPAGE: string, + SOLANA_RPC_URL: string, + HELIUS_API_KEY: string, + BIRDEYE_API_KEY: string, +}; +``` + +## Usage + +### Basic Setup + +```typescript +import { solanaPlugin } from "@elizaos/plugin-solana"; + +// Initialize the plugin +const runtime = await initializeRuntime({ + plugins: [solanaPlugin], +}); +``` + +### Services + +#### TokenProvider + +Manages token operations and information retrieval. + +```typescript +const tokenProvider = new TokenProvider( + tokenAddress, + walletProvider, + cacheManager +); +await tokenProvider.getTokensInWallet(runtime); +``` + +#### WalletProvider + +Handles wallet operations and portfolio management. + +```typescript +const walletProvider = new WalletProvider(connection, publicKey); +await walletProvider.getFormattedPortfolio(runtime); +``` + +#### TrustScoreProvider + +Evaluates and manages trust scores for tokens and trading activities. + +```typescript +const trustScore = await runtime.getProvider("trustScore"); +``` + +## Actions + +### executeSwap + +Executes a token swap using Jupiter aggregator. + +```typescript +// Example usage +const result = await runtime.executeAction("EXECUTE_SWAP", { + inputTokenSymbol: "SOL", + outputTokenSymbol: "USDC", + amount: 0.1, +}); +``` + +### transferToken + +Transfers tokens between wallets. + +```typescript +// Example usage +const result = await runtime.executeAction("SEND_TOKEN", { + tokenAddress: "TokenAddressHere", + recipient: "RecipientAddressHere", + amount: "1000", +}); +``` + +### transferSol + +Transfers SOL between wallets. + +```typescript +// Example usage +const result = await runtime.executeAction("SEND_SOL", { + recipient: "RecipientAddressHere", + amount: "1000", +}); +``` + +### takeOrder + +Places a buy order based on conviction level. + +```typescript +// Example usage +const result = await runtime.executeAction("TAKE_ORDER", { + ticker: "SOL", + contractAddress: "ContractAddressHere", +}); +``` + +### pumpfun + +Creates and buys tokens on pump.fun. + +```typescript +// Example usage +const result = await runtime.executeAction("CREATE_AND_BUY_TOKEN", { + tokenMetadata: { + name: "TokenName", + symbol: "SYMBOL", + description: "Token description", + image_description: "Image description", + }, + buyAmountSol: 0.1, +}); +``` + +### fomo + +Creates and buys tokens on fomo.fund. + +```typescript +// Example usage +const result = await runtime.executeAction("CREATE_AND_BUY_TOKEN", { + tokenMetadata: { + name: "TokenName", + symbol: "SYMBOL", + description: "Token description", + image_description: "Image description", + }, + buyAmountSol: 0.1, + requiredLiquidity: 1000, +}); +``` + +### executeSwapForDAO + +Executes token swaps for DAO operations. + +```typescript +// Example usage +const result = await runtime.executeAction("EXECUTE_SWAP_DAO", { + inputTokenSymbol: "SOL", + outputTokenSymbol: "USDC", + amount: 0.1, +}); +``` + +## Performance Optimization + +1. **Cache Management** + + - Implement token data caching + - Configure cache TTL settings + - Monitor cache hit rates + +2. **RPC Optimization** + + - Use connection pooling + - Implement request batching + - Monitor RPC usage + +3. **Transaction Management** + - Optimize transaction bundling + - Implement retry strategies + - Monitor transaction success rates + +## System Requirements + +- Node.js 16.x or higher +- Solana CLI tools (optional) +- Minimum 4GB RAM recommended +- Stable internet connection +- Access to Solana RPC endpoint + +## Troubleshooting + +### Common Issues + +1. **Wallet Connection Failures** + +```bash +Error: Failed to connect to wallet +``` + +- Verify RPC endpoint is accessible +- Check wallet configuration settings +- Ensure proper network selection + +2. **Transaction Errors** + +```bash +Error: Transaction simulation failed +``` + +- Check account balances +- Verify transaction parameters +- Ensure proper fee configuration + +3. **Price Feed Issues** + +```bash +Error: Unable to fetch price data +``` + +- Verify API key configuration +- Check network connectivity +- Ensure price feed service status + +## Safety & Security + +### Best Practices + +1. **Environment Variables** + + - Store sensitive keys in environment variables + - Use .env.example for non-sensitive defaults + - Never commit real credentials to version control + +2. **Transaction Limits** + + - Set maximum transaction amounts + - Implement daily trading limits + - Configure per-token restrictions + +3. **Monitoring** + + - Track failed transaction attempts + - Monitor unusual trading patterns + - Log security-relevant events + +4. **Recovery** + - Implement transaction rollback mechanisms + - Maintain backup RPC endpoints + - Document recovery procedures + +## Performance Optimization + +1. **Cache Management** + + - Implement token data caching + - Configure cache TTL settings + - Monitor cache hit rates + +2. **RPC Optimization** + + - Use connection pooling + - Implement request batching + - Monitor RPC usage + +3. **Transaction Management** + - Optimize transaction bundling + - Implement retry strategies + - Monitor transaction success rates + +## Support + +For issues and feature requests, please: + +1. Check the troubleshooting guide above +2. Review existing GitHub issues +3. Submit a new issue with: + - System information + - Error logs + - Steps to reproduce + - Transaction IDs (if applicable) + +## Contributing + +Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information. + +## Credits + +This plugin integrates with and builds upon several key technologies: + +- [Solana](https://solana.com/) - The core blockchain platform +- [Solana Web3.js](https://github.com/solana-labs/solana-web3.js) - Core Solana interactions +- [SPL Token](https://spl.solana.com/) - Token program interactions +- [Jupiter](https://jup.ag/) - Token swap aggregation +- [Birdeye](https://birdeye.so/) - Price feeds and analytics +- [Helius](https://helius.xyz/) - Enhanced RPC services +- [Anchor](https://project-serum.github.io/anchor/) - Smart contract framework +- [FOMO](https://fomo.fund/) - Token creation and trading +- [Pump.fun](https://pump.fun/) - Token creation and trading + +Special thanks to: + +- The Solana ecosystem and all the open-source contributors who make these integrations possible. +- The Eliza community for their contributions and feedback. + +For more information about Solana blockchain capabilities: + +- [Solana Documentation](https://docs.solana.com/) +- [Solana Developer Portal](https://solana.com/developers) +- [Solana Network Dashboard](https://solscan.io/) +- [Solana GitHub Repository](https://github.com/solana-labs/solana) + +## License + +This plugin is part of the Eliza project. See the main project repository for license information. diff --git a/packages/plugin-solana/__tests__/actions/swap.test.ts b/packages/plugin-solana/__tests__/actions/swap.test.ts new file mode 100644 index 00000000000..f38f806c4e8 --- /dev/null +++ b/packages/plugin-solana/__tests__/actions/swap.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect, vi } from 'vitest'; + +describe('Swap Action', () => { + describe('validate', () => { + it('should handle swap message validation', async () => { + const mockMessage = { + content: 'Swap 1 SOL to USDC', + metadata: { + fromToken: 'SOL', + toToken: 'USDC', + amount: '1' + } + }; + + // Basic test to ensure message structure + expect(mockMessage.metadata).toBeDefined(); + expect(mockMessage.metadata.fromToken).toBe('SOL'); + expect(mockMessage.metadata.toToken).toBe('USDC'); + expect(mockMessage.metadata.amount).toBe('1'); + }); + }); +}); diff --git a/packages/plugin-solana/biome.json b/packages/plugin-solana/biome.json new file mode 100644 index 00000000000..818716a6219 --- /dev/null +++ b/packages/plugin-solana/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedVariables": "error" + }, + "suspicious": { + "noExplicitAny": "error" + }, + "style": { + "useConst": "error", + "useImportType": "off" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5" + } + }, + "files": { + "ignore": [ + "dist/**/*", + "extra/**/*", + "node_modules/**/*" + ] + } +} \ No newline at end of file diff --git a/packages/plugin-solana/images/banner.jpg b/packages/plugin-solana/images/banner.jpg new file mode 100644 index 00000000000..b0da69f7644 Binary files /dev/null and b/packages/plugin-solana/images/banner.jpg differ diff --git a/packages/plugin-solana/images/logo.jpg b/packages/plugin-solana/images/logo.jpg new file mode 100644 index 00000000000..11238b75fd3 Binary files /dev/null and b/packages/plugin-solana/images/logo.jpg differ diff --git a/packages/plugin-solana/package.json b/packages/plugin-solana/package.json new file mode 100644 index 00000000000..f61b659a6a9 --- /dev/null +++ b/packages/plugin-solana/package.json @@ -0,0 +1,52 @@ +{ + "name": "@elizaos/plugin-solana", + "version": "0.1.9", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@coral-xyz/anchor": "0.30.1", + "@elizaos/core": "workspace:*", + "@solana/spl-token": "0.4.9", + "@solana/web3.js": "npm:@solana/web3.js@1.95.8", + "bignumber.js": "9.1.2", + "bs58": "6.0.0", + "fomo-sdk-solana": "1.3.2", + "node-cache": "5.1.2", + "pumpdotfun-sdk": "1.3.2", + "solana-agent-kit": "^1.4.0", + "tsup": "8.3.5", + "vitest": "2.1.9" + }, + "devDependencies": { + "@biomejs/biome": "1.5.3", + "tsup": "^8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "biome check src/", + "lint:fix": "biome check --apply src/", + "format": "biome format src/", + "format:fix": "biome format --write src/", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-solana/src/actions/swap.ts b/packages/plugin-solana/src/actions/swap.ts new file mode 100644 index 00000000000..b03e320e235 --- /dev/null +++ b/packages/plugin-solana/src/actions/swap.ts @@ -0,0 +1,298 @@ +import { + type Action, + type ActionExample, + composeContext, + elizaLogger, + generateObject, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + settings, + type State, +} from "@elizaos/core"; +import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js"; +import BigNumber from "bignumber.js"; +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); + + if ( + tokenAccountInfo.value && + typeof tokenAccountInfo.value.data === "object" && + "parsed" in tokenAccountInfo.value.data + ) { + const parsedInfo = tokenAccountInfo.value.data.parsed?.info; + if (parsedInfo && typeof parsedInfo.decimals === "number") { + return parsedInfo.decimals; + } + } + + throw new Error("Unable to fetch token decimals"); +} + +async function swapToken( + connection: Connection, + walletPublicKey: PublicKey, + inputTokenCA: string, + outputTokenCA: string, + amount: number +): Promise { + try { + const decimals = + inputTokenCA === settings.SOL_ADDRESS + ? new BigNumber(9) + : new BigNumber(await getTokenDecimals(connection, inputTokenCA)); + + elizaLogger.log("Decimals:", decimals.toString()); + + const amountBN = new BigNumber(amount); + const adjustedAmount = amountBN.multipliedBy(new BigNumber(10).pow(decimals)); + + elizaLogger.log("Fetching quote with params:", { + inputMint: inputTokenCA, + outputMint: outputTokenCA, + amount: adjustedAmount, + }); + + const quoteResponse = await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenCA}&outputMint=${outputTokenCA}&amount=${adjustedAmount}&dynamicSlippage=true&maxAccounts=64` + ); + const quoteData = await quoteResponse.json(); + + if (!quoteData || quoteData.error) { + elizaLogger.error("Quote error:", quoteData); + throw new Error(`Failed to get quote: ${quoteData?.error || "Unknown error"}`); + } + + const swapRequestBody = { + quoteResponse: quoteData, + userPublicKey: walletPublicKey.toBase58(), + dynamicComputeUnitLimit: true, + dynamicSlippage: true, + priorityLevelWithMaxLamports: { + maxLamports: 4000000, + priorityLevel: "veryHigh", + }, + }; + + const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(swapRequestBody), + }); + + const swapData = await swapResponse.json(); + + if (!swapData || !swapData.swapTransaction) { + elizaLogger.error("Swap error:", swapData); + throw new Error(`Failed to get swap transaction: ${swapData?.error || "No swap transaction returned"}`); + } + + return swapData; + } catch (error) { + elizaLogger.error("Error in swapToken:", error); + throw error; + } +} + +// 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: +\`\`\`json +{ + "inputTokenSymbol": "SOL", + "outputTokenSymbol": "USDC", + "inputTokenCA": "So11111111111111111111111111111111111111112", + "outputTokenCA": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": 1.5 +} +\`\`\` + +{{recentMessages}} + +Given the recent messages and wallet information below: + +{{walletInfo}} + +Extract the following information about the requested token swap: +- Input token symbol (the token being sold) +- Output token symbol (the token being bought) +- Input token contract address if provided +- 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.`; + +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) => { + 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 ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + try { + if (!state) { + state = await runtime.composeState(message); + } else { + state = await runtime.updateRecentMessageState(state); + } + + const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient') as ISolanaClient; + if (!solanaClient) { + throw new Error('SolanaClient not initialized'); + } + + const walletData = await solanaClient.getCachedData(); + state.walletInfo = walletData; + + const swapContext = composeContext({ + state, + template: swapTemplate, + }); + + const response = await generateObject({ + runtime, + context: swapContext, + modelClass: ModelClass.LARGE, + }); + + // Handle SOL addresses + if (response.inputTokenSymbol?.toUpperCase() === "SOL") { + response.inputTokenCA = settings.SOL_ADDRESS; + } + if (response.outputTokenSymbol?.toUpperCase() === "SOL") { + response.outputTokenCA = settings.SOL_ADDRESS; + } + + // 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; + } + } + + 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; + } + } + + if (!response.amount) { + callback?.({ text: "Please specify the amount you want to swap" }); + return false; + } + + 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, + walletPublicKey, + response.inputTokenCA as string, + response.outputTokenCA as string, + response.amount as number + ); + + const transactionBuf = Buffer.from(swapResult.swapTransaction, "base64"); + const transaction = VersionedTransaction.deserialize(transactionBuf); + + const { keypair } = await getWalletKey(runtime, true); + if (keypair.publicKey.toBase58() !== walletPublicKey.toBase58()) { + throw new Error("Generated public key doesn't match expected public key"); + } + + transaction.sign([keypair]); + + const latestBlockhash = await connection.getLatestBlockhash(); + const txid = await connection.sendTransaction(transaction, { + skipPreflight: false, + maxRetries: 3, + preflightCommitment: "confirmed", + }); + + 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}`); + } + + callback?.({ + text: `Swap completed successfully! Transaction ID: ${txid}`, + 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; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Swap 0.1 SOL for USDC" + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll help you swap 0.1 SOL for USDC", + action: "SWAP_SOLANA", + }, + } + ] + ] as ActionExample[][], +}; \ No newline at end of file diff --git a/packages/plugin-solana/src/actions/transfer.ts b/packages/plugin-solana/src/actions/transfer.ts new file mode 100644 index 00000000000..02440a78779 --- /dev/null +++ b/packages/plugin-solana/src/actions/transfer.ts @@ -0,0 +1,279 @@ +import { + type Action, + type ActionExample, + composeContext, + type Content, + elizaLogger, + generateObject, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, settings, type State +} from "@elizaos/core"; +import { + createAssociatedTokenAccountInstruction, + createTransferInstruction, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { + Connection, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { getWalletKey } from "../keypairUtils"; + +interface TransferContent extends Content { + tokenAddress: string | null; // null for SOL transfers + recipient: string; + amount: string | number; +} + +function isTransferContent( + content: any +): content is TransferContent { + elizaLogger.log("Content for transfer", content); + + // Base validation + if (!content.recipient || typeof content.recipient !== "string") { + return false; + } + + // SOL transfer validation + if (content.tokenAddress === null) { + return typeof content.amount === "number"; + } + + // SPL token transfer validation + if (typeof content.tokenAddress === "string") { + return typeof content.amount === "string" || typeof content.amount === "number"; + } + + return false; +} + +const transferTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example responses: +For SPL tokens: +\`\`\`json +{ + "tokenAddress": "BieefG47jAHCGZBxi2q87RDuHyGZyYC3vAzxpyu8pump", + "recipient": "9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa", + "amount": "1000" +} +\`\`\` + +For SOL: +\`\`\`json +{ + "tokenAddress": null, + "recipient": "9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa", + "amount": 1.5 +} +\`\`\` + +{{recentMessages}} + +Extract the following information about the requested transfer: +- Token contract address (use null for SOL transfers) +- Recipient wallet address +- Amount to transfer +`; + +export default { + name: "TRANSFER_SOLANA", + similes: [ + "TRANSFER_SOL", + "SEND_TOKEN_SOLANA", "TRANSFER_TOKEN_SOLANA", "SEND_TOKENS_SOLANA", "TRANSFER_TOKENS_SOLANA", + "SEND_SOL", "SEND_TOKEN_SOL", "PAY_SOL", "PAY_TOKEN_SOL", "PAY_TOKENS_SOL", "PAY_TOKENS_SOLANA", + "PAY_SOLANA" + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + elizaLogger.log("Validating transfer from user:", message.userId); + return true; + }, + description: "Transfer SOL or SPL tokens to another address on Solana.", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting TRANSFER handler..."); + + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + const transferContext = composeContext({ + state, + template: transferTemplate, + }); + + + const content = await generateObject({ + runtime, + context: transferContext, + modelClass: ModelClass.LARGE, + }); + + console.log("Content:", content); + + if (!isTransferContent(content)) { + if (callback) { + callback({ + text: "Need a valid recipient address and amount to transfer.", + content: { error: "Invalid transfer content" }, + }); + } + return false; + } + + try { + const { keypair: senderKeypair } = await getWalletKey(runtime, true); + const connection = new Connection(runtime.getSetting("SOLANA_RPC_URL") || "https://api.mainnet-beta.solana.com"); + const recipientPubkey = new PublicKey(content.recipient); + + let signature: string; + + // Handle SOL transfer + if (content.tokenAddress === null) { + const lamports = Number(content.amount) * 1e9; + + const instruction = SystemProgram.transfer({ + fromPubkey: senderKeypair.publicKey, + toPubkey: recipientPubkey, + lamports, + }); + + const messageV0 = new TransactionMessage({ + payerKey: senderKeypair.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [instruction], + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + transaction.sign([senderKeypair]); + + signature = await connection.sendTransaction(transaction); + + if (callback) { + callback({ + text: `Sent ${content.amount} SOL. Transaction hash: ${signature}`, + content: { + success: true, + signature, + amount: content.amount, + recipient: content.recipient, + }, + }); + } + } + // Handle SPL token transfer + else { + const mintPubkey = new PublicKey(content.tokenAddress); + const mintInfo = await connection.getParsedAccountInfo(mintPubkey); + const decimals = (mintInfo.value?.data as any)?.parsed?.info?.decimals ?? 9; + const adjustedAmount = BigInt(Number(content.amount) * Math.pow(10, decimals)); + + const senderATA = getAssociatedTokenAddressSync(mintPubkey, senderKeypair.publicKey); + const recipientATA = getAssociatedTokenAddressSync(mintPubkey, recipientPubkey); + + const instructions = []; + + const recipientATAInfo = await connection.getAccountInfo(recipientATA); + if (!recipientATAInfo) { + instructions.push( + createAssociatedTokenAccountInstruction( + senderKeypair.publicKey, + recipientATA, + recipientPubkey, + mintPubkey + ) + ); + } + + instructions.push( + createTransferInstruction( + senderATA, + recipientATA, + senderKeypair.publicKey, + adjustedAmount + ) + ); + + const messageV0 = new TransactionMessage({ + payerKey: senderKeypair.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions, + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + transaction.sign([senderKeypair]); + + signature = await connection.sendTransaction(transaction); + + if (callback) { + callback({ + text: `Sent ${content.amount} tokens to ${content.recipient}\nTransaction hash: ${signature}`, + content: { + success: true, + signature, + amount: content.amount, + recipient: content.recipient, + }, + }); + } + } + + return true; + } catch (error) { + elizaLogger.error("Error during transfer:", error); + if (callback) { + callback({ + text: `Transfer failed: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Send 1.5 SOL to 9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa", + }, + }, + { + user: "{{user2}}", + content: { + text: "Sending SOL now...", + action: "TRANSFER_SOLANA", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Send 69 $DEGENAI BieefG47jAHCGZBxi2q87RDuHyGZyYC3vAzxpyu8pump to 9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa", + }, + }, + { + user: "{{user2}}", + content: { + text: "Sending the tokens now...", + action: "TRANSFER_SOLANA", + }, + }, + ], + ] as ActionExample[][], +} as Action; \ No newline at end of file diff --git a/packages/plugin-solana/src/bignumber.ts b/packages/plugin-solana/src/bignumber.ts new file mode 100644 index 00000000000..f320676a0fc --- /dev/null +++ b/packages/plugin-solana/src/bignumber.ts @@ -0,0 +1,9 @@ +import BigNumber from "bignumber.js"; + +// Re-export BigNumber constructor +export const BN = BigNumber; + +// Helper function to create new BigNumber instances +export function toBN(value: string | number | BigNumber): BigNumber { + return new BigNumber(value); +} diff --git a/packages/plugin-solana/src/client.ts b/packages/plugin-solana/src/client.ts new file mode 100644 index 00000000000..530b6efc890 --- /dev/null +++ b/packages/plugin-solana/src/client.ts @@ -0,0 +1,258 @@ +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { logger, type Client, type IAgentRuntime, type ICacheManager } from '@elizaos/core'; +import { getWalletKey } from "./keypairUtils"; +import BigNumber from "bignumber.js"; +import type { Item, WalletPortfolio, Prices, ISolanaClient } from "./types"; + +const PROVIDER_CONFIG = { + BIRDEYE_API: "https://public-api.birdeye.so", + MAX_RETRIES: 3, + RETRY_DELAY: 2000, + DEFAULT_RPC: "https://api.mainnet-beta.solana.com", + TOKEN_ADDRESSES: { + SOL: "So11111111111111111111111111111111111111112", + BTC: "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", + ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + }, +}; + +class SolanaClient implements ISolanaClient { + private updateInterval: NodeJS.Timer | null = null; + private lastUpdate: number = 0; + private readonly UPDATE_INTERVAL = 120000; // 2 minutes + private readonly CACHE_KEY = 'solana/walletData'; + private connection: Connection; + private publicKey: PublicKey; + + constructor( + private runtime: IAgentRuntime, + private cacheManager: ICacheManager, + connection: Connection, + publicKey: PublicKey + ) { + this.connection = connection; + this.publicKey = publicKey; + this.start(); + } + + start() { + logger.log('SolanaClient start'); + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + + this.updateInterval = setInterval(async () => { + logger.log('Updating wallet data'); + await this.updateWalletData(); + }, this.UPDATE_INTERVAL); + + // Initial update + this.updateWalletData().catch(console.error); + } + + public stop(runtime: IAgentRuntime) { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + + const client = runtime.clients.find(client => client.name === 'SolanaClient'); + runtime.clients = runtime.clients.filter(c => c !== client); + + return Promise.resolve(); + } + + private async fetchWithRetry( + 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": this.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}`); + } + + return await response.json(); + } catch (error) { + logger.error(`Attempt ${i + 1} failed:`, error); + lastError = error; + if (i < PROVIDER_CONFIG.MAX_RETRIES - 1) { + await new Promise(resolve => setTimeout(resolve, PROVIDER_CONFIG.RETRY_DELAY * Math.pow(2, i))); + continue; + } + } + } + + throw lastError; + } + + private async fetchPrices(): Promise { + const cacheKey = "prices"; + const cachedValue = await this.cacheManager.get(cacheKey); + + if (cachedValue) { + logger.log("Cache hit for fetchPrices"); + return cachedValue; + } + + logger.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( + `${PROVIDER_CONFIG.BIRDEYE_API}/defi/price?address=${token}` + ); + + if (response?.data?.value) { + const price = response.data.value.toString(); + prices[ + token === SOL ? "solana" : token === BTC ? "bitcoin" : "ethereum" + ].usd = price; + } + } + + await this.cacheManager.set(cacheKey, prices); + return prices; + } + + private async getTokenAccounts() { + try { + const accounts = await this.connection.getParsedTokenAccountsByOwner( + this.publicKey, + { + programId: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + } + ); + return accounts.value; + } catch (error) { + logger.error("Error fetching token accounts:", error); + return []; + } + } + + private async updateWalletData(force: boolean = false): Promise { + const now = Date.now(); + + // Don't update if less than interval has passed, unless forced + if (!force && now - this.lastUpdate < this.UPDATE_INTERVAL) { + const cached = await this.getCachedData(); + if (cached) return cached; + } + + try { + // Try Birdeye API first + const birdeyeApiKey = this.runtime.getSetting("BIRDEYE_API_KEY"); + if (birdeyeApiKey) { + const walletData = await this.fetchWithRetry( + `${PROVIDER_CONFIG.BIRDEYE_API}/v1/wallet/token_list?wallet=${this.publicKey.toBase58()}` + ); + + if (walletData?.success && walletData?.data) { + const data = walletData.data; + const totalUsd = new BigNumber(data.totalUsd.toString()); + const prices = await this.fetchPrices(); + const solPriceInUSD = new BigNumber(prices.solana.usd); + + 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) + .div(solPriceInUSD) + .toFixed(6), + name: item.name || "Unknown", + symbol: item.symbol || "Unknown", + priceUsd: item.priceUsd || "0", + valueUsd: item.valueUsd || "0", + })), + }; + + await this.cacheManager.set(this.CACHE_KEY, portfolio); + this.lastUpdate = now; + return portfolio; + } + } + + // Fallback to basic token account info + const accounts = await this.getTokenAccounts(); + const items: Item[] = 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: WalletPortfolio = { + totalUsd: "0", + totalSol: "0", + items, + }; + + await this.cacheManager.set(this.CACHE_KEY, portfolio); + this.lastUpdate = now; + return portfolio; + + } catch (error) { + logger.error("Error updating wallet data:", error); + throw error; + } + } + + public async getCachedData(): Promise { + return await this.cacheManager.get(this.CACHE_KEY); + } + + public async forceUpdate(): Promise { + return await this.updateWalletData(true); + } + + public getPublicKey(): PublicKey { + return this.publicKey; + } + + public getConnection(): Connection { + return this.connection; + } +} + +export const SolanaClientInterface: Client = { + name: 'SolanaClient', + start: async (runtime: IAgentRuntime) => { + logger.log('initSolanaClient'); + + const connection = new Connection( + runtime.getSetting("SOLANA_RPC_URL") || PROVIDER_CONFIG.DEFAULT_RPC + ); + + const { publicKey } = await getWalletKey(runtime, false); + + return new SolanaClient(runtime, runtime.cacheManager, connection, publicKey); + } +}; \ No newline at end of file diff --git a/packages/plugin-solana/src/environment.ts b/packages/plugin-solana/src/environment.ts new file mode 100644 index 00000000000..5a536ced6fb --- /dev/null +++ b/packages/plugin-solana/src/environment.ts @@ -0,0 +1,78 @@ +import type { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const solanaEnvSchema = z + .object({ + WALLET_SECRET_SALT: z.string().optional(), + }) + .and( + z.union([ + z.object({ + WALLET_SECRET_KEY: z + .string() + .min(1, "Wallet secret key is required"), + WALLET_PUBLIC_KEY: z + .string() + .min(1, "Wallet public key is required"), + }), + z.object({ + WALLET_SECRET_SALT: z + .string() + .min(1, "Wallet secret salt is required"), + }), + ]) + ) + .and( + z.object({ + SOL_ADDRESS: z.string().min(1, "SOL address is required"), + SLIPPAGE: z.string().min(1, "Slippage is required"), + SOLANA_RPC_URL: z.string().min(1, "RPC URL is required"), + HELIUS_API_KEY: z.string().min(1, "Helius API key is required"), + BIRDEYE_API_KEY: z.string().min(1, "Birdeye API key is required"), + }) + ); + +export type SolanaConfig = z.infer; + +export async function validateSolanaConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + WALLET_SECRET_SALT: + runtime.getSetting("WALLET_SECRET_SALT") || + process.env.WALLET_SECRET_SALT, + WALLET_SECRET_KEY: + runtime.getSetting("WALLET_SECRET_KEY") || + process.env.WALLET_SECRET_KEY, + WALLET_PUBLIC_KEY: + runtime.getSetting("SOLANA_PUBLIC_KEY") || + runtime.getSetting("WALLET_PUBLIC_KEY") || + process.env.WALLET_PUBLIC_KEY, + SOL_ADDRESS: + runtime.getSetting("SOL_ADDRESS") || process.env.SOL_ADDRESS, + SLIPPAGE: runtime.getSetting("SLIPPAGE") || process.env.SLIPPAGE, + SOLANA_RPC_URL: + runtime.getSetting("SOLANA_RPC_URL") || + process.env.SOLANA_RPC_URL, + HELIUS_API_KEY: + runtime.getSetting("HELIUS_API_KEY") || + process.env.HELIUS_API_KEY, + BIRDEYE_API_KEY: + runtime.getSetting("BIRDEYE_API_KEY") || + process.env.BIRDEYE_API_KEY, + }; + + return solanaEnvSchema.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Solana configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-solana/src/index.ts b/packages/plugin-solana/src/index.ts new file mode 100644 index 00000000000..a062298fad2 --- /dev/null +++ b/packages/plugin-solana/src/index.ts @@ -0,0 +1,18 @@ +import type { Plugin } from "@elizaos/core"; +import { executeSwap } from "./actions/swap.ts"; +import transferToken from "./actions/transfer.ts"; +import { walletProvider } from "./providers/wallet.ts"; +import { SolanaClientInterface } from "./client.ts"; + +export const solanaPlugin: Plugin = { + name: "solana", + description: "Solana Plugin for Eliza", + actions: [ + transferToken, + executeSwap, + ], + evaluators: [], + providers: [walletProvider], + clients: [SolanaClientInterface], +}; +export default solanaPlugin; diff --git a/packages/plugin-solana/src/keypairUtils.ts b/packages/plugin-solana/src/keypairUtils.ts new file mode 100644 index 00000000000..c4c37e5e90b --- /dev/null +++ b/packages/plugin-solana/src/keypairUtils.ts @@ -0,0 +1,59 @@ +import { Keypair, PublicKey } from "@solana/web3.js"; +import bs58 from "bs58"; +import { type IAgentRuntime, elizaLogger } from "@elizaos/core"; + +export interface KeypairResult { + keypair?: Keypair; + publicKey?: PublicKey; +} + +/** + * Gets either a keypair or public key based on TEE mode and runtime settings + * @param runtime The agent runtime + * @param requirePrivateKey Whether to return a full keypair (true) or just public key (false) + * @returns KeypairResult containing either keypair or public key + */ +export async function getWalletKey( + runtime: IAgentRuntime, + requirePrivateKey = true +): Promise { + // TEE mode is OFF + if (requirePrivateKey) { + const privateKeyString = + runtime.getSetting("SOLANA_PRIVATE_KEY") ?? + runtime.getSetting("WALLET_PRIVATE_KEY"); + + if (!privateKeyString) { + throw new Error("Private key not found in settings"); + } + + try { + // First try base58 + const secretKey = bs58.decode(privateKeyString); + return { keypair: Keypair.fromSecretKey(secretKey) }; + } catch (e) { + elizaLogger.log("Error decoding base58 private key:", e); + try { + // Then try base64 + elizaLogger.log("Try decoding base64 instead"); + const secretKey = Uint8Array.from( + Buffer.from(privateKeyString, "base64") + ); + return { keypair: Keypair.fromSecretKey(secretKey) }; + } catch (e2) { + elizaLogger.error("Error decoding private key: ", e2); + throw new Error("Invalid private key format"); + } + } + } else { + const publicKeyString = + runtime.getSetting("SOLANA_PUBLIC_KEY") ?? + runtime.getSetting("WALLET_PUBLIC_KEY"); + + if (!publicKeyString) { + throw new Error("Solana Public key not found in settings, but plugin was loaded, please set SOLANA_PUBLIC_KEY"); + } + + return { publicKey: new PublicKey(publicKeyString) }; + } +} diff --git a/packages/plugin-solana/src/providers/wallet.ts b/packages/plugin-solana/src/providers/wallet.ts new file mode 100644 index 00000000000..68d53e5ec30 --- /dev/null +++ b/packages/plugin-solana/src/providers/wallet.ts @@ -0,0 +1,57 @@ +import { + type IAgentRuntime, + type Memory, + type Provider, + type State, +} from "@elizaos/core"; +import BigNumber from "bignumber.js"; +import type { WalletPortfolio } from "../types"; + +export const walletProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + state?: State + ): Promise => { + try { + const portfolio = await runtime.cacheManager.get('solana/walletData'); + if (!portfolio) { + return null; + } + + 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`; + + // Token Balances + 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`; + } + + return output; + } catch (error) { + console.error("Error in Solana wallet provider:", error); + return null; + } + }, +}; \ No newline at end of file diff --git a/packages/plugin-solana/src/types.ts b/packages/plugin-solana/src/types.ts new file mode 100644 index 00000000000..1dd2e24326b --- /dev/null +++ b/packages/plugin-solana/src/types.ts @@ -0,0 +1,61 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { PublicKey } from "@solana/web3.js"; + +export interface Item { + name: string; + address: string; + symbol: string; + decimals: number; + balance: string; + uiAmount: string; + priceUsd: string; + valueUsd: string; + 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 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 ISolanaClient { + start: () => void; + stop: (runtime: IAgentRuntime) => Promise; + getCachedData: () => Promise; + forceUpdate: () => Promise; +} \ No newline at end of file diff --git a/packages/plugin-solana/tsconfig.json b/packages/plugin-solana/tsconfig.json new file mode 100644 index 00000000000..005fbac9d36 --- /dev/null +++ b/packages/plugin-solana/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-solana/tsup.config.ts b/packages/plugin-solana/tsup.config.ts new file mode 100644 index 00000000000..dd25475bb63 --- /dev/null +++ b/packages/plugin-solana/tsup.config.ts @@ -0,0 +1,29 @@ +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", + "safe-buffer", + "base-x", + "bs58", + "borsh", + "@solana/buffer-layout", + "stream", + "buffer", + "querystring", + "amqplib", + // Add other modules you want to externalize + ], +}); diff --git a/packages/plugin-sqlite/package.json b/packages/plugin-sqlite/package.json index 82f4e3f65ab..8a78a431127 100644 --- a/packages/plugin-sqlite/package.json +++ b/packages/plugin-sqlite/package.json @@ -1,5 +1,5 @@ { - "name": "@elizaos-plugins/sqlite", + "name": "@elizaos/plugin-sqlite", "version": "1.0.0-alpha.0", "type": "module", "main": "dist/index.js", diff --git a/packages/plugin-tee/src/providers/walletProvider.ts b/packages/plugin-tee/src/providers/walletProvider.ts deleted file mode 100644 index 24ac7943b34..00000000000 --- a/packages/plugin-tee/src/providers/walletProvider.ts +++ /dev/null @@ -1,295 +0,0 @@ -/* This is an example of how WalletProvider can use DeriveKeyProvider to generate a Solana Keypair */ -import { type IAgentRuntime, type Memory, type Provider, type State, logger } from '@elizaos/core'; -import { Connection, type Keypair, type PublicKey } from '@solana/web3.js'; -import BigNumber from 'bignumber.js'; -import NodeCache from 'node-cache'; -import { DeriveKeyProvider } from './deriveKeyProvider'; -import type { RemoteAttestationQuote } from '@elizaos/core'; -// 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', - TOKEN_ADDRESSES: { - SOL: 'So11111111111111111111111111111111111111112', - BTC: '3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh', - ETH: '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs', - }, -}; - -export interface Item { - name: string; - address: string; - symbol: string; - decimals: number; - balance: string; - uiAmount: string; - priceUsd: string; - valueUsd: string; - valueSol?: string; -} - -interface WalletPortfolio { - totalUsd: string; - totalSol?: string; - items: Array; -} - -interface _BirdEyePriceData { - data: { - [key: string]: { - price: number; - priceChange24h: number; - }; - }; -} - -interface Prices { - solana: { usd: string }; - bitcoin: { usd: string }; - ethereum: { usd: string }; -} - -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 apiKey = runtime.getSetting('BIRDEYE_API_KEY'); - const response = await fetch(url, { - ...options, - headers: { - Accept: 'application/json', - 'x-chain': 'solana', - 'X-API-KEY': apiKey || '', - ...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) { - logger.error(`Attempt ${i + 1} failed:`, error); - lastError = error; - if (i < PROVIDER_CONFIG.MAX_RETRIES - 1) { - const delay = PROVIDER_CONFIG.RETRY_DELAY * 2 ** i; - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - } - - logger.error('All attempts failed. 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) { - logger.log('Cache hit for fetchPortfolioValue'); - return cachedValue; - } - logger.log('Cache miss for fetchPortfolioValue'); - - const walletData = await this.fetchWithRetry( - runtime, - `${ - PROVIDER_CONFIG.BIRDEYE_API - }/v1/wallet/token_list?wallet=${this.walletPublicKey.toBase58()}`, - ); - - if (!walletData?.success || !walletData?.data) { - logger.error('No portfolio data available', walletData); - throw new Error('No portfolio data available'); - } - - 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 totalSol = totalUsd.div(solPriceInUSD); - const portfolio = { - totalUsd: totalUsd.toString(), - totalSol: totalSol.toFixed(6), - items: items.sort((a, b) => - new BigNumber(b.valueUsd).minus(new BigNumber(a.valueUsd)).toNumber(), - ), - }; - this.cache.set(cacheKey, portfolio); - return portfolio; - } catch (error) { - logger.error('Error fetching portfolio:', error); - throw error; - } - } - - async fetchPrices(runtime): Promise { - try { - const cacheKey = 'prices'; - const cachedValue = this.cache.get(cacheKey); - - if (cachedValue) { - logger.log('Cache hit for fetchPrices'); - return cachedValue; - } - logger.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 { - logger.warn(`No price data available for token: ${token}`); - } - } - - this.cache.set(cacheKey, prices); - return prices; - } catch (error) { - logger.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`; - } - } - - 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); - } catch (error) { - logger.error('Error generating portfolio report:', error); - return 'Unable to fetch wallet information. Please try again later.'; - } - } -} - -const walletProvider: Provider = { - get: async (runtime: IAgentRuntime, _message: Memory, _state?: State): Promise => { - const agentId = runtime.agentId; - const teeMode = runtime.getSetting('TEE_MODE'); - const deriveKeyProvider = new DeriveKeyProvider(teeMode); - try { - // Validate wallet configuration - if (!runtime.getSetting('WALLET_SECRET_SALT')) { - logger.error('Wallet secret salt is not configured in settings'); - return ''; - } - - let publicKey: PublicKey; - try { - const derivedKeyPair: { - keypair: Keypair; - attestation: RemoteAttestationQuote; - } = await deriveKeyProvider.deriveEd25519Keypair( - runtime.getSetting('WALLET_SECRET_SALT'), - 'solana', - agentId, - ); - publicKey = derivedKeyPair.keypair.publicKey; - logger.log('Wallet Public Key: ', publicKey.toBase58()); - } catch (error) { - logger.error('Error creating PublicKey:', error); - return ''; - } - - const connection = new Connection(PROVIDER_CONFIG.DEFAULT_RPC); - const provider = new WalletProvider(connection, publicKey); - - const porfolio = await provider.getFormattedPortfolio(runtime); - return porfolio; - } catch (error) { - logger.error('Error in wallet provider:', error.message); - return `Failed to fetch wallet information: ${ - error instanceof Error ? error.message : 'Unknown error' - }`; - } - }, -}; - -// Module exports -export { walletProvider };