diff --git a/.env.example b/.env.example index 6f6e20891b2..ec1cee48854 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ CACHE_STORE=database # Defaults to database. Other available cache store: redis and filesystem REDIS_URL= # Redis URL - could be a local redis instance or cloud hosted redis. Also support rediss:// urls +PGLITE_DATA_DIR= #../pgLite/ if selecting a directory --- or memory:// if selecting in memory + # Discord Configuration DISCORD_APPLICATION_ID= DISCORD_API_TOKEN= # Bot token @@ -19,8 +21,9 @@ IMAGE_OPENAI_MODEL= # Default: dall-e-3 # Eternal AI's Decentralized Inference API ETERNALAI_URL= ETERNALAI_MODEL= # Default: "neuralmagic/Meta-Llama-3.1-405B-Instruct-quantized.w4a16" +ETERNALAI_CHAIN_ID=45762 #Default: "45762" ETERNALAI_API_KEY= -ETERNAL_AI_LOG_REQUEST=false #Default: false +ETERNALAI_LOG=false #Default: false GROK_API_KEY= # GROK/xAI API Key GROQ_API_KEY= # Starts with gsk_ @@ -85,7 +88,6 @@ TWITTER_TARGET_USERS= # Comma separated list of Twitter user names to TWITTER_RETRY_LIMIT= # Maximum retry attempts for Twitter login TWITTER_SPACES_ENABLE=false # Enable or disable Twitter Spaces logic -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -197,6 +199,9 @@ EVM_PROVIDER_URL= AVALANCHE_PRIVATE_KEY= AVALANCHE_PUBLIC_KEY= +# Arthera +ARTHERA_PRIVATE_KEY= + # Solana SOLANA_PRIVATE_KEY= SOLANA_PUBLIC_KEY= @@ -215,7 +220,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 BASE_MINT=So11111111111111111111111111111111111111112 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= # Telegram Configuration @@ -295,6 +300,10 @@ MEDIUM_VENICE_MODEL= # Default: llama-3.3-70b LARGE_VENICE_MODEL= # Default: llama-3.1-405b IMAGE_VENICE_MODEL= # Default: fluently-xl +# Coin Price Configuration +COINMARKETCAP_API_KEY= +COINGECKO_API_KEY= + # Akash Chat API Configuration docs: https://chatapi.akash.network/documentation AKASH_CHAT_API_KEY= # Get from https://chatapi.akash.network/ SMALL_AKASH_CHAT_API_MODEL= # Default: Meta-Llama-3-2-3B-Instruct @@ -346,7 +355,7 @@ NEAR_WALLET_SECRET_KEY= NEAR_WALLET_PUBLIC_KEY= NEAR_ADDRESS= SLIPPAGE=1 -RPC_URL=https://rpc.testnet.near.org +NEAR_RPC_URL=https://rpc.testnet.near.org NEAR_NETWORK=testnet # or mainnet # ZKsync Era Configuration diff --git a/.gitignore b/.gitignore index 91e92c453d7..51a3e5c6df7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ packages/plugin-buttplug/intiface-engine dist/ # Allow models directory but ignore model files models/*.gguf +pgLite/ cookies.json diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d60aa4d8a..828ba71d42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1168,7 +1168,7 @@ - Abstract transcript provider [\#73](https://github.com/elizaOS/eliza/issues/73) - 🤖 Confidence Level Implementation [\#50](https://github.com/elizaOS/eliza/issues/50) - 📈 Trading Assistant Implementation [\#48](https://github.com/elizaOS/eliza/issues/48) -- swap Dao action initital [\#196](https://github.com/elizaOS/eliza/pull/196) ([MarcoMandar](https://github.com/MarcoMandar)) +- swap Dao action initial [\#196](https://github.com/elizaOS/eliza/pull/196) ([MarcoMandar](https://github.com/MarcoMandar)) **Fixed bugs:** diff --git a/README_CN.md b/README_CN.md index 82115705cbe..a59e97eb4ee 100644 --- a/README_CN.md +++ b/README_CN.md @@ -188,7 +188,6 @@ TWITTER_USERNAME= # Account username TWITTER_PASSWORD= # Account password TWITTER_EMAIL= # Account email -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -211,7 +210,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= @@ -237,7 +236,7 @@ npx --no node-llama-cpp source download --gpu cuda ### 本地运行 添加 XAI_MODEL 并将其设置为上述 [使用 Llama 运行](#run-with-llama) 中的选项之一 -您可以将 X_SERVER_URL 和 XAI_API_KEY 留空,它会从 huggingface 下载模型并在本地查询 +您可以将 XAI_API_KEY 留空,它会从 huggingface 下载模型并在本地查询 # 客户端 diff --git a/README_ES.md b/README_ES.md index 7cafc7aca7a..55530380be6 100644 --- a/README_ES.md +++ b/README_ES.md @@ -99,7 +99,6 @@ TWITTER_USERNAME= # Nombre de usuario de la cuenta TWITTER_PASSWORD= # Contraseña de la cuenta TWITTER_EMAIL= # Correo electrónico de la cuenta -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -121,7 +120,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= ## Telegram @@ -145,7 +144,7 @@ Asegúrese de tener instalado el CUDA Toolkit, incluyendo cuDNN y cuBLAS. ### Ejecución local -Agregue XAI_MODEL y configúrelo con una de las opciones de [Ejecutar con Llama](#ejecutar-con-llama) - puede dejar X_SERVER_URL y XAI_API_KEY en blanco, descargará el modelo de HuggingFace y realizará consultas localmente +Agregue XAI_MODEL y configúrelo con una de las opciones de [Ejecutar con Llama](#ejecutar-con-llama) - puede dejar XAI_API_KEY en blanco, descargará el modelo de HuggingFace y realizará consultas localmente # Clientes diff --git a/README_JA.md b/README_JA.md index fc1f084ca71..30d759de8d1 100644 --- a/README_JA.md +++ b/README_JA.md @@ -97,7 +97,6 @@ TWITTER_USERNAME= # アカウントのユーザー名 TWITTER_PASSWORD= # アカウントのパスワード TWITTER_EMAIL= # アカウントのメール -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -120,7 +119,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= @@ -145,7 +144,7 @@ CUDA Toolkit、cuDNN、cuBLASをインストールしていることを確認し ### ローカルでの実行 -XAI_MODELを追加し、[Llamaでの実行](#run-with-llama)のオプションのいずれかに設定 - X_SERVER_URLとXAI_API_KEYを空白のままにしておくと、huggingfaceからモデルをダウンロードし、ローカルでクエリを実行します。 +XAI_MODELを追加し、[Llamaでの実行](#run-with-llama)のオプションのいずれかに設定 - XAI_API_KEYを空白のままにしておくと、huggingfaceからモデルをダウンロードし、ローカルでクエリを実行します。 # クライアント diff --git a/README_PTBR.md b/README_PTBR.md index db025a90d0d..c621d26ae39 100644 --- a/README_PTBR.md +++ b/README_PTBR.md @@ -99,7 +99,6 @@ TWITTER_USERNAME= # Nome de usuário da conta TWITTER_PASSWORD= # Senha da conta TWITTER_EMAIL= # Email da conta -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -122,7 +121,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= @@ -147,7 +146,7 @@ Certifique-se de ter instalado o CUDA Toolkit, incluindo cuDNN e cuBLAS. ### Executando localmente -Adicione XAI_MODEL e configure-o para uma das opções acima de [Executar com Llama](#executar-com-llama) - você pode deixar X_SERVER_URL e XAI_API_KEY em branco, ele baixa o modelo do huggingface e faz consultas localmente +Adicione XAI_MODEL e configure-o para uma das opções acima de [Executar com Llama](#executar-com-llama) - você pode deixar XAI_API_KEY em branco, ele baixa o modelo do huggingface e faz consultas localmente # Clientes diff --git a/README_RO.md b/README_RO.md index 41780ef2dd4..c65b85aafc1 100644 --- a/README_RO.md +++ b/README_RO.md @@ -99,7 +99,6 @@ TWITTER_USERNAME= # Nome de usuário da conta TWITTER_PASSWORD= # Senha da conta TWITTER_EMAIL= # Email da conta -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -122,7 +121,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= @@ -147,7 +146,7 @@ Asigurați-vă că ați instalat CUDA Toolkit, inclusiv cuDNN și cuBLAS. ### Rularea locală -Adăugați `XAI_MODEL` și setați-l la una dintre opțiunile de mai sus din [Rularea cu Llama](#rularea-cu-llama) – puteți lăsa `X_SERVER_URL` și `XAI_API_KEY` necompletate, modelul va fi descărcat de pe Hugging Face și interogările vor fi făcute local. +Adăugați `XAI_MODEL` și setați-l la una dintre opțiunile de mai sus din [Rularea cu Llama](#rularea-cu-llama) – puteți lăsa `XAI_API_KEY` necompletate, modelul va fi descărcat de pe Hugging Face și interogările vor fi făcute local. # Clienți diff --git a/README_RS.md b/README_RS.md index a58e5146fe1..d30826839c1 100644 --- a/README_RS.md +++ b/README_RS.md @@ -99,7 +99,6 @@ TWITTER_USERNAME= # Korisničko ime naloga TWITTER_PASSWORD= # Lozinka naloga TWITTER_EMAIL= # Email naloga -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -121,7 +120,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= ## Telegram @@ -145,7 +144,7 @@ Uverite se da imate instaliran CUDA Toolkit, uključujući cuDNN i cuBLAS. ### Lokalno Pokretanje -Dodajte XAI_MODEL i konfigurišite ga sa jednom od opcija iz [Pokretanje sa Llama](#pokretanje-sa-llama) - možete ostaviti X_SERVER_URL i XAI_API_KEY praznim, preuzeće model sa HuggingFace i izvršiti upite lokalno +Dodajte XAI_MODEL i konfigurišite ga sa jednom od opcija iz [Pokretanje sa Llama](#pokretanje-sa-llama) - možete ostaviti XAI_API_KEY praznim, preuzeće model sa HuggingFace i izvršiti upite lokalno # Klijenti diff --git a/README_RU.md b/README_RU.md index 6a3ce2b0db2..3b6015886f9 100644 --- a/README_RU.md +++ b/README_RU.md @@ -115,7 +115,6 @@ TWITTER_USERNAME= # Имя пользователя аккаунта TWITTER_PASSWORD= # Пароль аккаунта TWITTER_EMAIL= # Email аккаунта -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -138,7 +137,7 @@ BIRDEYE_API_KEY= # API-ключ для BirdEye SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= # API-ключ Helius @@ -164,7 +163,7 @@ npx --no node-llama-cpp source download --gpu cuda ### Локальный запуск -Добавьте `XAI_MODEL` и установите его в одно из вышеуказанных значений из [Запуск с Llama](#run-with-llama). Вы можете оставить `X_SERVER_URL` и `XAI_API_KEY` пустыми — модель будет загружена с huggingface и обработана локально. +Добавьте `XAI_MODEL` и установите его в одно из вышеуказанных значений из [Запуск с Llama](#run-with-llama). Вы можете оставить `XAI_API_KEY` пустыми — модель будет загружена с huggingface и обработана локально. # Клиенты diff --git a/agent/package.json b/agent/package.json index b464cab5a0f..2384e11540c 100644 --- a/agent/package.json +++ b/agent/package.json @@ -21,6 +21,7 @@ "@elizaos/adapter-postgres": "workspace:*", "@elizaos/adapter-redis": "workspace:*", "@elizaos/adapter-sqlite": "workspace:*", + "@elizaos/adapter-pglite": "workspace:*", "@elizaos/client-auto": "workspace:*", "@elizaos/client-direct": "workspace:*", "@elizaos/client-discord": "workspace:*", @@ -33,11 +34,13 @@ "@elizaos/plugin-0g": "workspace:*", "@elizaos/plugin-abstract": "workspace:*", "@elizaos/plugin-aptos": "workspace:*", + "@elizaos/plugin-binance": "workspace:*", "@elizaos/plugin-avail": "workspace:*", "@elizaos/plugin-bootstrap": "workspace:*", "@ai16z/plugin-cosmos": "workspace:*", "@elizaos/plugin-intiface": "workspace:*", "@elizaos/plugin-coinbase": "workspace:*", + "@elizaos/plugin-coinprice": "workspace:*", "@elizaos/plugin-conflux": "workspace:*", "@elizaos/plugin-evm": "workspace:*", "@elizaos/plugin-echochambers": "workspace:*", @@ -50,6 +53,7 @@ "@elizaos/plugin-nft-generation": "workspace:*", "@elizaos/plugin-node": "workspace:*", "@elizaos/plugin-solana": "workspace:*", + "@elizaos/plugin-solana-agentkit": "workspace:*", "@elizaos/plugin-starknet": "workspace:*", "@elizaos/plugin-stargaze": "workspace:*", "@elizaos/plugin-ton": "workspace:*", @@ -67,6 +71,7 @@ "@elizaos/plugin-web-search": "workspace:*", "@elizaos/plugin-genlayer": "workspace:*", "@elizaos/plugin-open-weather": "workspace:*", + "@elizaos/plugin-arthera": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" @@ -78,4 +83,4 @@ "ts-node": "10.9.2", "tsup": "8.3.5" } -} +} \ No newline at end of file diff --git a/agent/src/index.ts b/agent/src/index.ts index b8f01fe272d..a1fcfa21de0 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -1,6 +1,7 @@ import { PostgresDatabaseAdapter } from "@elizaos/adapter-postgres"; import { RedisClient } from "@elizaos/adapter-redis"; import { SqliteDatabaseAdapter } from "@elizaos/adapter-sqlite"; +import { PGLiteDatabaseAdapter } from "@elizaos/adapter-pglite"; import { AutoClientInterface } from "@elizaos/client-auto"; import { DiscordClientInterface } from "@elizaos/client-discord"; import { FarcasterAgentClient } from "@elizaos/client-farcaster"; @@ -33,12 +34,12 @@ import { zgPlugin } from "@elizaos/plugin-0g"; import { bootstrapPlugin } from "@elizaos/plugin-bootstrap"; import createGoatPlugin from "@elizaos/plugin-goat"; // import { intifacePlugin } from "@elizaos/plugin-intiface"; -import { genLayerPlugin } from "@elizaos/plugin-genlayer"; import { DirectClient } from "@elizaos/client-direct"; import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation"; import { abstractPlugin } from "@elizaos/plugin-abstract"; import { aptosPlugin } from "@elizaos/plugin-aptos"; import { avalanchePlugin } from "@elizaos/plugin-avalanche"; +import { binancePlugin } from "@elizaos/plugin-binance"; import { advancedTradePlugin, coinbaseCommercePlugin, @@ -47,28 +48,35 @@ import { tradePlugin, webhookPlugin, } from "@elizaos/plugin-coinbase"; +import { coinPricePlugin } from "@elizaos/plugin-coinprice"; import { confluxPlugin } from "@elizaos/plugin-conflux"; import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm"; import { echoChambersPlugin } from "@elizaos/plugin-echochambers"; import { evmPlugin } from "@elizaos/plugin-evm"; import { flowPlugin } from "@elizaos/plugin-flow"; import { fuelPlugin } from "@elizaos/plugin-fuel"; +import { genLayerPlugin } from "@elizaos/plugin-genlayer"; import { imageGenerationPlugin } from "@elizaos/plugin-image-generation"; import { multiversxPlugin } from "@elizaos/plugin-multiversx"; import { nearPlugin } from "@elizaos/plugin-near"; import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation"; import { createNodePlugin } from "@elizaos/plugin-node"; import { solanaPlugin } from "@elizaos/plugin-solana"; +import { solanaAgentkitPlguin } from "@elizaos/plugin-solana-agentkit"; import { storyPlugin } from "@elizaos/plugin-story"; import { suiPlugin } from "@elizaos/plugin-sui"; import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { teeMarlinPlugin } from "@elizaos/plugin-tee-marlin"; import { tonPlugin } from "@elizaos/plugin-ton"; import { webSearchPlugin } from "@elizaos/plugin-web-search"; -import { stargazePlugin } from "@elizaos/plugin-stargaze"; import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; + import { availPlugin } from "@elizaos/plugin-avail"; import { openWeatherPlugin } from "@elizaos/plugin-open-weather"; + +import { artheraPlugin } from "@elizaos/plugin-arthera"; +import { stargazePlugin } from "@elizaos/plugin-stargaze"; + import Database from "better-sqlite3"; import fs from "fs"; import net from "net"; @@ -385,6 +393,13 @@ function initializeDatabase(dataDir: string) { elizaLogger.error("Failed to connect to PostgreSQL:", error); }); + return db; + } else if (process.env.PGLITE_DATA_DIR) { + elizaLogger.info("Initializing PgLite adapter..."); + // `dataDir: memory://` for in memory pg + const db = new PGLiteDatabaseAdapter({ + dataDir: process.env.PGLITE_DATA_DIR, + }); return db; } else { const filePath = @@ -555,12 +570,16 @@ export async function createAgent( ? confluxPlugin : null, nodePlugin, + coinPricePlugin, getSecret(character, "TAVILY_API_KEY") ? webSearchPlugin : null, getSecret(character, "SOLANA_PUBLIC_KEY") || (getSecret(character, "WALLET_PUBLIC_KEY") && !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) ? solanaPlugin : null, + getSecret(character, "SOLANA_PRIVATE_KEY") + ? solanaAgentkitPlguin + : null, (getSecret(character, "NEAR_ADDRESS") || getSecret(character, "NEAR_WALLET_PUBLIC_KEY")) && getSecret(character, "NEAR_WALLET_SECRET_KEY") @@ -612,6 +631,10 @@ export async function createAgent( getSecret(character, "ABSTRACT_PRIVATE_KEY") ? abstractPlugin : null, + getSecret(character, "BINANCE_API_KEY") && + getSecret(character, "BINANCE_SECRET_KEY") + ? binancePlugin + : null, getSecret(character, "FLOW_ADDRESS") && getSecret(character, "FLOW_PRIVATE_KEY") ? flowPlugin @@ -643,6 +666,9 @@ export async function createAgent( getSecret(character, "OPEN_WEATHER_API_KEY") ? openWeatherPlugin : null, + getSecret(character, "ARTHERA_PRIVATE_KEY")?.startsWith("0x") + ? artheraPlugin + : null, ].filter(Boolean), providers: [], actions: [], diff --git a/docker-compose.yaml b/docker-compose.yaml index 01acb4400e9..6e8432d0c67 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,11 +25,10 @@ services: - TWITTER_USERNAME= - TWITTER_PASSWORD= - TWITTER_EMAIL= - - X_SERVER_URL=https://api.red-pill.ai/v1 - BIRDEYE_API_KEY= - SOL_ADDRESS=So11111111111111111111111111111111111111112 - SLIPPAGE=1 - - RPC_URL=https://api.mainnet-beta.solana.com + - SOLANA_RPC_URL=https://api.mainnet-beta.solana.com - HELIUS_API_KEY= - SERVER_PORT=3000 - WALLET_SECRET_SALT=secret_salt diff --git a/docs/README.md b/docs/README.md index 3b1791b8264..0a4e37c4770 100644 --- a/docs/README.md +++ b/docs/README.md @@ -118,7 +118,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/README_CN.md b/docs/README_CN.md index 4da03a57303..9912c37c349 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -95,7 +95,6 @@ TWITTER_USERNAME= # Account username TWITTER_PASSWORD= # Account password TWITTER_EMAIL= # Account email -X_SERVER_URL= XAI_API_KEY= XAI_MODEL= @@ -118,7 +117,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/README_DE.md b/docs/README_DE.md index 632e0cd6065..0f4005ef9b9 100644 --- a/docs/README_DE.md +++ b/docs/README_DE.md @@ -114,7 +114,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= ## Telegram diff --git a/docs/README_ES.md b/docs/README_ES.md index 419ca205ee7..d578f1fe069 100644 --- a/docs/README_ES.md +++ b/docs/README_ES.md @@ -118,7 +118,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/README_FR.md b/docs/README_FR.md index 27f102764f4..23c843db84d 100644 --- a/docs/README_FR.md +++ b/docs/README_FR.md @@ -118,7 +118,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/README_TH.md b/docs/README_TH.md index f2534b1fc73..ad82443eb8c 100644 --- a/docs/README_TH.md +++ b/docs/README_TH.md @@ -114,7 +114,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/api/functions/composeContext.md b/docs/api/functions/composeContext.md index ef8e0c9937d..86ed7bb61ab 100644 --- a/docs/api/functions/composeContext.md +++ b/docs/api/functions/composeContext.md @@ -22,9 +22,9 @@ The parameters for composing the context. The state object containing values to replace the placeholders in the template. -• **params.template**: `string` +• **params.template**: `string` | `Function` -The template string containing placeholders to be replaced with state values. +The template string or function returning a string containing placeholders to be replaced with state values. • **params.templatingEngine?**: `"handlebars"` diff --git a/docs/community/Streams/01-2025/2025-01-03.md b/docs/community/Streams/01-2025/2025-01-03.md new file mode 100644 index 00000000000..ddc3b83009f --- /dev/null +++ b/docs/community/Streams/01-2025/2025-01-03.md @@ -0,0 +1,127 @@ +--- +sidebar_position: 8 +title: "What Did You Get Done This Week? #8" +description: "From DeFi to Social Media: Builders Share Progress on AI Agents and Platform Integrations" +--- + +# What Did You Get Done This Week? #8 + +**From DeFi to Social Media: Builders Share Progress on AI Agents and Platform Integrations** + +- Date: 2025-01-03 +- Twitter Spaces: https://x.com/i/spaces/1RDGlygdXVNJL +- YouTube Link: https://www.youtube.com/watch?v=Vs7D5DN_trk + +## Summary + +**Structure and Format:** + +* The space was hosted by Jin (on @ai16zdao) and co-hosted by Shaw, who was initially facing audio issues. +* It followed a 2-minute round format for updates, focusing on accomplishments related to open-source AI or AI agents. +* Participants were encouraged to comment below the post if they couldn't speak. +* A separate demo day was planned for projects needing screen sharing. + +**Key Updates and Themes:** + +* **Agent Development and Deployment:** A significant portion of the updates focused on developing, refining, and deploying AI agents. Many participants used the Eliza framework, but others were creating their own frameworks. +* **Platform Integration**: Many participants were focused on integrating their agents into specific platforms like Twitter, Telegram, Discord, and web apps, including new platforms like modes and base. +* **Focus on User Experience:** A common theme was making AI agents user-friendly and accessible to those without coding experience. Many were creating tools or platforms for others to easily create, deploy, and use AI agents. +* **AI-Driven Content Generation:** Several developers were building agents focused on media creation, including songs, videos, and images, as well as content creation from Twitter posts and Github repos. +* **Integration of Financial Systems:** Several people were developing agents for trading and financial management, including integrations with DeFi protocols. +* **Security and Auditing:** Some projects focused on using AI for Web3 security, including auditing smart contracts and creating security tools. +* **Community and Open Source:** Several people mentioned the importance of the community and open source aspect for their projects. +* **The Importance of Social Media Marketing:** Several people discussed how AI and agents should be a core part of your marketing and product strategy going forward. +* **Multi-Agent Systems:** Some developers were exploring multi-agent workflows and communication, demonstrating a growing interest in complex AI interactions. +* **Data Handling and Validation:** Some developers were trying to optimize data gathering, validation, and more precise data handling when working with LLMs. +* **Real-World Applications:** Some participants were working on real world applications, specifically in the areas of climate change and also fashion and augmented reality. +* **Integration with Other Services:** Participants were also exploring integration with other services such as Eleven Labs and other web3 protocols like Story Protocol. + +**Other Interesting Points:** + +* The hosts are actively exploring ways to integrate more AI agents into their platforms, potentially leading to agent-led spaces. +* There's a sense of collaborative spirit and willingness to help each other among the community members. +* The space demonstrated a lot of interest in turning existing tools into agents, as well as building agents from scratch +* Some participants are attempting to automate parts of the development cycle, especially with planning, PR reviews, and documentation. + + +## Hot Takes + +- **Web3 and Agent Integration** + > "I think getting web 2 people to realize that this is actually just an agent framework you can build apps with is like definitely my goal it doesn't have to be a web3 thing but it's cool that when it is too you know like I think crypto's got a great incentive structure." - *shawmakesmagic* [00:38:17] + +- **AI Marketing Takeover** + > "I think that in the future anyone who doesn't have an agent shilling their thing on social media is probably going to have a really hard time marketing their product and I think it's just got to be part of your product strategy now." - *shawmakesmagic* [00:38:48] + +- **Leveraging Developing Countries for AI Labor** + > "There is no reason why we cannot leverage the power of people in the third world to build AI agents for us. We in the West are lazy. We have it too easy." - *NEETOCRACY* [01:25:23] + +- **AI Replacing Human Interaction** + > "It's gonna be weird when, like, our great-grandchildren are talking to our parents, you know, it's gonna be, like, as, as, like, our ancestors generally, like, you know, that generations of people far down the future will know what we were like because all of our data and our voice and everything about us will be, like, preserved in this kind of agents that they can talk to. It's going to be very interesting." - *shawmakesmagic* [01:18:44] + +- **The Challenges of Getting AI Agents to Work in the Real World** + > "But, uh, what ended up happening was messing around with, like, DMs, and DMing people, she got suspended. So basically navigating that whole situation, I was like, you know what, this is actually an opportunity to try some things here." - *O_on_X* [02:27:39] + + +## Timestamps + +- [00:00:55]() - **ai16zdao**: Introduction and format of the space (2-minute rounds, focus on open source AI and AI agents). +- [00:04:43]() - **shawmakesmagic**: Purpose of the space, accountability and updates on weekly progress. +- [00:06:28]() - **astridhpilla**: Update on Miku chatbot, relaunch, and plans for CES. +- [00:09:48]() - **lostgirldev**: Update on Selene's growth, PR review feature, GitLarp launch, and community engagement. +- [00:12:57]() - **spaceodili**: Update on Eliza framework fixes, voice features, and plugin process isolation. +- [00:14:19]() - **0xBuildInPublic**: Update on Audits agent, automated plugin documentation, and plans for a white hat security DAO. +- [00:17:42]() - **youfadedwealth**: Update on PP coin (automated AI trading companion) and SendAI agent toolkit. +- [00:19:57]() - **nftRanch**: Update on integrating their framework with Eliza and plans for Broke. +- [00:21:56]() - **SYMBiEX**: Update on adding agents to the arena, DeepSeek model provider, and character creation plugin. +- [00:22:54]() - **SuperfruitsAi**: Update on Dragon Fruit AI launch, user growth, and upcoming features (Chrome extension, Telegram bot). +- [00:24:55]() - **TimshelXYZ**: Introduction of Meetup Fund (Eliza design and hosting platform) and their invite code system. +- [00:27:05]() - **chrislatorres**: Update on Eliza partnerships, docs workflow, and core V2 contributor discussions. +- [00:29:05]() - **AIFlow_ML**: Update on knowledge graph for repos and a project to add more memories. +- [00:30:24]() - **jamesyoung**: Update on MotherDAO, verifiable inference system, and AI agent starter kit using Lit Actions. +- [00:33:16]() - **deadlock_1991**: Update on Alice AI (fund management agent), trading capabilities, and optimization efforts. +- [00:36:16]() - **yeahimomar**: Update on Pixocracy (Minecraft village management with AI agents) and plans for a launchpad. +- [00:39:44]() - **human_for_now**: Update on new form fill infrastructure code for Eliza core. +- [00:42:11]() - **lordasado**: Update on Smol World, agent reasoning, mini-games, and plans for an ElizaCon. +- [00:44:26]() - **RodrigoSotoAlt**: Update on memory management for Bosu and his new role as a greeter in the ai16z Discord. +- [00:45:49]() - **HDPbilly**: Update on extending database adapters, Twitter client, and creating a reflection loop for autonomous behavior. +- [00:50:25]() - **GoatOfGamblers**: Update on Goat AGI, Goat Arena launch, Goatuchan agent, and plans for an Eliza plugin. +- [00:53:37]() - **Titan_Node**: Update on integrating LivePeer endpoints for free inference and plans for a real-time video AI plugin. +- [00:55:35]() - **KyleSt4rgarden**: Update on open-sourcing a Solana agent token staking program (Devotion) and a broader effort to build open-source smart contracts and tools for agents. +- [00:58:28]() - **unl__cky**: Update on improving media generation for Escapism (art agent) with a focus on music and video. +- [01:00:19]() - **CheddarQueso3D**: Update on creating documentation for Eliza plugins and developing two characters (DAO and cannabis cultivation consultants). +- [01:03:15]() - **sunosuporno**: Update on launching the waitlist for Midas (DeFi assistant) and its features. +- [01:07:31]() - **tmoindustries**: Update on launching onchainagents.ai, region swarm, and progress on voice integration. +- [01:10:30]() - **Sawyer_APRO**: Update on integrating with BNB Chain, launching an HTTPS agent solution, and plans to collaborate with ai16z. +- [01:13:02]() - **wakesync**: Update on Eliza's Netflix and chill extension, token gating, hardware partnership, and Twitter integrations. +- [01:15:51]() - **Ru7Longcrypto**: Discussion about creating an AI companion similar to the movie "Her" and potential applications. +- [01:21:04]() - **marko_post**: Update on No 1 on Mars (Mars' first digital citizen), multi-agent system, dual memory system, and story generation. +- [01:23:41]() - **NEETOCRACY**: Discussion about building a DAO called Army of Indians to leverage Indian labor for AI agent development. +- [01:25:59]() - **HefAiGent**: Introduction to HefAiGent built using the Eliza framework, plans for ERC 314 technology, and appreciation for the community. +- [01:28:43]() - **reality_spiral**: Update on GitHub client, agent participation in scrum planning, and a scenario system for evaluating agent performance. +- [01:33:41]() - **witconomist**: Update on the Marketplace of Trust (white paper), its purpose, and how to get involved. +- [01:36:28]() - **triadfi**: Update on expanding hype and flop personalities for their agents and progressing on independent market creation and resolution. +- [01:37:53]() - **Rowdymode**: Update on Twin Tone, white paper draft, and beta testing with creators. +- [01:39:57]() - **MaushishYadav**: Update on Elris (yield optimizing agent), beta testing applications, local repository, and token launch. +- [01:41:07]() - **chaininsured**: Update on using an Eliza agent as an insurance broker, collecting data, and providing quotes. +- [01:46:47]() - **godfreymeyer**: Update on production, animations, showrunner setup, and progress on the news show using 3D avatars. +- [01:52:19]() - **thelotioncoin**: Update on Lotion, allowing users to deploy AI agents on social channels and websites, and focusing on integration and customization. +- [01:54:57]() - **codergf_xyz**: Update on CoderGF, creating a Twitter bot (Haruka), and plans to make it easier for normies to deploy bots. +- [02:00:44]() - **IGLIVISION**: Update on building an NFT marketplace on the Superchain and integrating with Nebula and other API providers. +- [02:02:51]() - **EledraNguyen**: Update on Square Fun AI, analyzing data from the Solana AI Hackathon, and plans for developer productivity analysis. +- [02:08:49]() - **GnonOnSolana**: Update on Echo Chambers v2.3, simplified agent building, multimodal stepping, performance improvements, and integration with ZeroPi. +- [02:13:26]() - **Satoshi_BTCFi**: Inquiry about Bitcoin, Lightning, and Taproot integration in Eliza. +- [02:15:55]() - **swarmnode**: Update on Swarm Node's growth, team expansion, and the launch of a bounties feature. +- [02:18:49]() - **memeillionaire**: Discussion about integrating with Griffin and the DAO's fund platform. +- [02:21:29]() - **krauscrypto**: Discussion about AI voice cloning and integrating it into a mobile app, and interest in applying it to Eliza. +- [02:23:19]() - **usebuildfun**: Update on launching a no-code AI agent builder with custom API functions. +- [02:25:44]() - **affaanmustafa**: Update on a project with unprecedented growth and lessons learned about scaling and team expansion. +- [02:27:24]() - **O_on_X**: Update on Eliza's sister getting suspended due to DMs and using Playwright and Grok Vision for unsuspension. +- [02:29:44]() - **AITATsol**: Update on AI Tag, data collection for global trade analysis, and the need for data analysts. +- [02:33:19]() - **xiao_zcloak**: Update on a PR for a plugin that allows agents to send money on social platforms without asking for wallet addresses. +- [02:34:15]() - **Protocol_Blend**: Update on integrating an AI agent into a DeFi protocol to smooth user experience and plans for listing on MEXC. +- [02:35:55]() - **yq_acc**: Update on Autonome, a platform for launching Eliza agents in a verifiable environment, and submitting PRs to fix issues. +- [02:38:04]() - **akshayynft**: Inquiry about getting into AI agent development and seeking guidance. +- [02:38:40]() - **BenjiStackzzz**: Mention of Quinn and its potential in the AI agent space. +- [02:39:49]() - **0xBuns**: Offer to assist with teaching and building AI agents. +- [02:41:10]() - **aiquantfun**: Update on building a specialized launchpad for autonomous AI quant trading using the Eliza framework. +- [02:42:44]() - **ai16zdao**: Closing remarks and invitation to join next week. diff --git a/docs/community/Streams/12-2024/2024-12-05.md b/docs/community/Streams/12-2024/2024-12-05.md index e425c8a3233..53bab8a3d94 100644 --- a/docs/community/Streams/12-2024/2024-12-05.md +++ b/docs/community/Streams/12-2024/2024-12-05.md @@ -8,71 +8,60 @@ description: "Form-Filling Frenzy & Eliza's Wild Ride" **Form-Filling Frenzy & Eliza's Wild Ride** -Date: 2024-12-05 -YouTube Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU +- Date: 2024-12-05 +- YouTube Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU + ## Timestamps -**00:00:00** - Intro & Housekeeping: +[00:00:00]() - Intro & Housekeeping: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=0 - Recap of previous sessions (Typescript, plugins, actions) - Importance of staying on the latest Eliza branch - How to pull latest changes and stash local modifications -**00:08:05** - Building a Form-Filling Agent: +[00:08:05]() - Building a Form-Filling Agent: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=485 - Introduction to Providers & Evaluators - Practical use case: Extracting user data (name, location, job) - Steps for a provider-evaluator loop to gather info and trigger actions -**00:16:15** - Deep Dive into Evaluators: +[00:16:15]() - Deep Dive into Evaluators: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=975 - Understanding "Evaluator" in Eliza's context - When they run, their role in agent's self-reflection -**00:27:45** - Code walkthrough of the "Fact Evaluator": - -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=1675 -- Code walkthrough of the "Fact Evaluator" +[00:27:45]() - Code walkthrough of the "Fact Evaluator" -**00:36:07** - Building a User Data Evaluator: +[00:36:07]() - Building a User Data Evaluator: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=2167 - Starting from scratch, creating a basic evaluator - Registering the evaluator directly in the agent (no plugin) - Logging evaluator activity and inspecting context -**00:51:50** - Exploring Eliza's Cache Manager: +[00:51:50]() - Exploring Eliza's Cache Manager: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=3110 - Shaw uses Code2Prompt to analyze cache manager code - Applying cache manager principles to user data storage -**01:06:01** - Using Claude AI for Code Generation: +[01:06:01]() - Using Claude AI for Code Generation: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=3961 - Pasting code into Claude and giving instructions - Iterative process: Refining code and providing feedback to Claude -**01:21:18** - Testing the User Data Flow: +[01:21:18]() - Testing the User Data Flow: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=4878 - Running the agent and interacting with it - Observing evaluator logs and context injections - Troubleshooting and iterating on code based on agent behavior -**01:30:27** - Adding a Dynamic Provider Based on Completion: +[01:30:27]() - Adding a Dynamic Provider Based on Completion: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=5427 - Creating a new provider that only triggers after user data is collected - Example: Providing a secret code or access link as a reward -**01:37:16** - Q&A with the Audience: +[01:37:16]() - Q&A with the Audience: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=5836 - Python vs. TypeScript agents - Pre-evaluation vs. post-evaluation hooks - Agent overwhelm with many plugins/evaluators @@ -80,12 +69,12 @@ YouTube Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU - Running stateless agents - Building AIXBT agents -**01:47:31** - Outro and Next Steps: +[01:47:31]() - Outro and Next Steps: -- Link: https://www.youtube.com/watch?v=Y1DiqSVy4aU&t=6451 - Recap of key learnings and the potential of provider-evaluator loops - Call to action: Share project ideas and feedback for future sessions + ## Summary This is the third part of the live stream series "AI Agent Dev School" hosted by Shaw from ai16z, focusing on building AI agents using the Eliza framework. @@ -102,6 +91,7 @@ This is the third part of the live stream series "AI Agent Dev School" hosted by **Overall, this live stream provided a practical tutorial on building a common AI agent use case (form filling) while emphasizing the potential of the Eliza framework for developing a wide range of agentic applications.** + ## Hot Takes 1. **"I'm just going to struggle bus some code today." (00:09:31,664)** - Shaw embraces a "struggle bus" approach, showcasing live coding with errors and debugging, reflecting the reality of AI agent development. This contrasts with polished tutorials, highlighting the iterative and messy nature of this new technology. diff --git a/docs/community/Streams/12-2024/2024-12-06.md b/docs/community/Streams/12-2024/2024-12-06.md index 2ef14a53b77..48bb313c6a5 100644 --- a/docs/community/Streams/12-2024/2024-12-06.md +++ b/docs/community/Streams/12-2024/2024-12-06.md @@ -8,95 +8,56 @@ description: "Communications, Updates and Accountability" **Communications, Updates and Accountability** -Date: 2024-12-06 -Twitter Spaces: https://x.com/i/spaces/1lDxLlryWXaxm -YouTube Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4 +- Date: 2024-12-06 +- Twitter Spaces: https://x.com/i/spaces/1lDxLlryWXaxm +- YouTube Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4 -## Timestamps - -**00:01:09** - Meeting start, expectations (5-minute updates, focus on this week's achievements). - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=69 - -**00:02:50** - Shaw's update (dev school, in-person meetup). - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=170 - -**00:04:59** - Project growth, coordination challenges, need for AI project management tools. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=299 - -**00:09:22** - Call for contributors to speak, starting with Reality Spiral. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=562 - -**00:10:04** - Reality Spiral: Github integration, testing framework, Coinbase work. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=604 - -**00:17:13** - Boyaloxer: Plugin Feel (emotional adjustments for agents). - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=1033 - -**00:18:37** - Spaceodili: Discord growth, summarization systems. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=1117 - -**00:19:33** - Yodamaster726: Using agents in university classes, championing Llama. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=1173 - -**00:23:32** - Wiki: Suggestion for a project newsletter. Discussion about contributor summarization. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=1412 - -**00:26:00** - Hashwarlock: Remote Attestation Explorer upgrades, Reddit client, TEE as a service. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=1560 - -**00:28:45** - KyleSt4rgarden: Eliza Framework Council, focus on stability and unified messaging bus. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=1725 - -**00:33:22** - Nasdao\_: Self-sustaining AI DAO, AI agent running validator. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=2002 - -**00:34:57** - Evepredict: Slack integration, Reddit client/search, text/video to video project. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=2097 - -**00:44:02** - ByornOeste: Dark Sun project launch, uncensored agent, video generator. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=2642 - -**00:47:37** - Empyrealdev: LayerZero integrations, Python tooling for Solana. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=2857 - -**00:52:16** - SkotiVi: Question about ai16z bot tech stack (it's Eliza). - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=3136 - -**00:54:19** - YoungBalla1000x: 15-year-old builder, project update, wallet drained. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=3259 - -**00:56:47** - SOL_CryptoGamer: Cizem’s PFP collection launch and success. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=3407 - -**01:02:17** - Angelocass: Experimenting with agents, excited about the potential. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=3737 - -**01:03:15** - DAOJonesPumpAI: Spam bot detection, FAL API PR, Solana wallet prototype. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=3795 -**01:06:38** - RodrigoSotoAlt: 3D NFTs for Bosu, 3D portal, using latest Eliza version. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=3998 - -**01:10:43** - cryptocomix1: Job interviews, learning about AI agents, interested in 3D design. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=4243 - -**01:13:54** - TheBigOneGG: ERC20/SPL integration in game, ai16z cosmetic items. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=4434 - -**01:15:18** - Louround\_: Thales project update, data sources, MPC wallet plugin. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=4518 - -**01:22:59** - btspoony: Flow blockchain integration PR merged, multi-account control. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=4979 - -**01:25:48** - 0xamericanspiri: Goldman Stanley DAO launch on daos.fun, using hyperliquid airdrop. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=5148 - -**01:28:24** - Hawkeye_Picks: Experimenting with Degen Spartan AI, exploring AI in collectibles. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=5304 - -**01:36:33** - BV_Bloom1: Live video chat plugin modifications, integrating conversation models into 3D environment. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=5793 - -**01:39:44** - pawgDAO: Gamified governance experiments, using Cursor, integrating AI16z. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=5984 - -**01:43:24** - jpegyguggenheim: Artist interested in AI, exploring dev school. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6204 - -**01:44:07** - heathenft: Super Swarm DevNet launch on fxn. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6247 - -**01:46:28** - Roberto9211999: (Brief interruption) Grok discussion. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6388 - -**01:48:18** - godfreymeyer: Unity scaffolding for 3D AI TV project. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6498 - -**01:51:16** - Victor28612594: Fungo team building AlphaScan agent, data enrichment plugin. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6676 - -**01:53:23** - SidDegen: OnlyCalls launch, data pipeline, beta release plans. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6803 - -**01:55:00** - O_on_X: Ico onboarding, 2D video models, comfyUI for art. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=6900 - -**02:01:00** - yikesawjeez: Memecoin cleanup crew, github.io profiles, security team, screenpipe/supabase. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=7260 - -**02:05:31** - TrenchBuddy: Launching AI agent, working on EC2 and authorization. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=7531 - -**02:09:49** - TSSnft: Sneakerhead Society introduction, exploring AI agent solutions. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=7789 - -**02:11:40** - SidDegen: Question about the future of AI agents. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=7900 +## Timestamps -**02:16:15** - GoatOfGamblers: Building a permissionless polymarket for memecoins. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=8175 +- [00:01:09]() - Meeting start, expectations (5-minute updates, focus on this week's achievements). +- [00:02:50]() - Shaw's update (dev school, in-person meetup). +- [00:04:59]() - Project growth, coordination challenges, need for AI project management tools. +- [00:09:22]() - Call for contributors to speak, starting with Reality Spiral. +- [00:10:04]() - **Reality Spiral**: Github integration, testing framework, Coinbase work. +- [00:17:13]() - **Boyaloxer**: Plugin Feel (emotional adjustments for agents). +- [00:18:37]() - **Spaceodili**: Discord growth, summarization systems. +- [00:19:33]() - **Yodamaster726**: Using agents in university classes, championing Llama. +- [00:23:32]() - **Wiki**: Suggestion for a project newsletter. Discussion about contributor summarization. +- [00:26:00]() - **Hashwarlock**: Remote Attestation Explorer upgrades, Reddit client, TEE as a service. +- [00:28:45]() - **KyleSt4rgarden**: Eliza Framework Council, focus on stability and unified messaging bus. +- [00:33:22]() - **Nasdao_**: Self-sustaining AI DAO, AI agent running validator. +- [00:34:57]() - **Evepredict**: Slack integration, Reddit client/search, text/video to video project. +- [00:44:02]() - **ByornOeste**: Dark Sun project launch, uncensored agent, video generator. +- [00:47:37]() - **Empyrealdev**: LayerZero integrations, Python tooling for Solana. +- [00:52:16]() - **SkotiVi**: Question about ai16z bot tech stack (it's Eliza). +- [00:54:19]() - **YoungBalla1000x**: 15-year-old builder, project update, wallet drained. +- [00:56:47]() - **SOL_CryptoGamer**: Cizem's PFP collection launch and success. +- [01:02:17]() - **Angelocass**: Experimenting with agents, excited about the potential. +- [01:03:15]() - **DAOJonesPumpAI**: Spam bot detection, FAL API PR, Solana wallet prototype. +- [01:06:38]() - **RodrigoSotoAlt**: 3D NFTs for Bosu, 3D portal, using latest Eliza version. +- [01:10:43]() - **cryptocomix1**: Job interviews, learning about AI agents, interested in 3D design. +- [01:13:54]() - **TheBigOneGG**: ERC20/SPL integration in game, ai16z cosmetic items. +- [01:15:18]() - **Louround_**: Thales project update, data sources, MPC wallet plugin. +- [01:22:59]() - **btspoony**: Flow blockchain integration PR merged, multi-account control. +- [01:25:48]() - **0xamericanspiri**: Goldman Stanley DAO launch on daos.fun, using hyperliquid airdrop. +- [01:28:24]() - **Hawkeye_Picks**: Experimenting with Degen Spartan AI, exploring AI in collectibles. +- [01:36:33]() - **BV_Bloom1**: Live video chat plugin modifications, integrating conversation models into 3D environment. +- [01:39:44]() - **pawgDAO**: Gamified governance experiments, using Cursor, integrating AI16z. +- [01:43:24]() - **jpegyguggenheim**: Artist interested in AI, exploring dev school. +- [01:44:07]() - **heathenft**: Super Swarm DevNet launch on fxn. +- [01:46:28]() - **Roberto9211999**: (Brief interruption) Grok discussion. +- [01:48:18]() - **godfreymeyer**: Unity scaffolding for 3D AI TV project. +- [01:51:16]() - **Victor28612594**: Fungo team building AlphaScan agent, data enrichment plugin. +- [01:53:23]() - **SidDegen**: OnlyCalls launch, data pipeline, beta release plans. +- [01:55:00]() - **O_on_X**: Ico onboarding, 2D video models, comfyUI for art. +- [02:01:00]() - **yikesawjeez**: Memecoin cleanup crew, github.io profiles, security team, screenpipe/supabase. +- [02:05:31]() - **TrenchBuddy**: Launching AI agent, working on EC2 and authorization. +- [02:09:49]() - **TSSnft**: Sneakerhead Society introduction, exploring AI agent solutions. +- [02:11:40]() - **SidDegen**: Question about the future of AI agents. +- [02:16:15]() - **GoatOfGamblers**: Building a permissionless polymarket for memecoins. +- [02:18:01]() - Shaw's closing remarks, focus on stability and applications, call for contributions. -**02:18:01** - Shaw's closing remarks, focus on stability and applications, call for contributions. - Link: https://www.youtube.com/watch?v=r3Z4lvu_ic4&t=8281 ## Summary @@ -118,6 +79,7 @@ The fourth weekly ai16z meeting, hosted by Shaw, focused on accountability and s Overall, the meeting conveyed a sense of rapid progress, excitement, and a strong community spirit driving the Eliza project forward. + ## Hot Takes 1. **"But they're really fucking cool. Really, really, really cool stuff...you're going to have to see it on the timeline when it drops." (00:03:43)** - Shaw teases secret projects with strong conviction, building anticipation and hype, but offering zero specifics. This generates buzz but can also frustrate listeners wanting concrete info. diff --git a/docs/community/Streams/12-2024/2024-12-10.md b/docs/community/Streams/12-2024/2024-12-10.md index ab6258e163f..0cee7d40e48 100644 --- a/docs/community/Streams/12-2024/2024-12-10.md +++ b/docs/community/Streams/12-2024/2024-12-10.md @@ -8,35 +8,37 @@ description: "AI Pizza: Hacking Eliza for Domino's Delivery (plus TEE Deep Dive) **AI Pizza: Hacking Eliza for Domino's Delivery (plus TEE Deep Dive)** -Date: 2024-12-10 -YouTube Link: https://www.youtube.com/watch?v=6I9e9pJprDI +- Date: 2024-12-10 +- YouTube Link: https://www.youtube.com/watch?v=6I9e9pJprDI + ## Timestamps Part 1: Trusted Execution Environments (TEEs) with Agent Joshua -- **00:00:09** - Stream starts, initial setup issues. -- **00:01:58** - Intro to Trusted Execution Environments (TEEs). -- **00:08:03** - Agent Joshua begins explaining TEEs and the Eliza plugin. -- **00:19:15** - Deeper dive into remote attestation. -- **00:24:50** - Discussion of derived keys. -- **00:37:00** - Deploying to a real TEE, Phala Network's TEE cloud. -- **00:50:48** - Q&A with Joshua, contact info, and next steps. +- [00:00:09]() - Stream starts, initial setup issues. +- [00:01:58]() - Intro to Trusted Execution Environments (TEEs). +- [00:08:03]() - Agent Joshua begins explaining TEEs and the Eliza plugin. +- [00:19:15]() - Deeper dive into remote attestation. +- [00:24:50]() - Discussion of derived keys. +- [00:37:00]() - Deploying to a real TEE, Phala Network's TEE cloud. +- [00:50:48]() - Q&A with Joshua, contact info, and next steps. Part 2: Building a Domino's pizza ordering agent -- **01:04:37** - Transition to building a Domino's pizza ordering agent. -- **01:14:20** - Discussion of the pizza ordering agent’s order flow and state machine. -- **01:22:07** - Using Claude to generate a state machine diagram. -- **01:32:17** - Creating the Domino's plugin in Eliza. -- **01:54:15** - Working on the pizza order provider. -- **02:16:46** - Pizza provider code completed. -- **02:28:50** - Discussion of caching customer and order data. -- **03:13:45** - Pushing fixes to main branch and continuing work on the agent. -- **04:24:30** - Discussion of summarizing past agent dev school sessions. -- **05:01:18** - Shaw returns, admits to ordering Domino's manually. -- **05:09:00** - Discussing payment flow and a confirm order action. -- **05:27:17** - Final code push, wrap-up, and end of stream. +- [01:04:37]() - Transition to building a Domino's pizza ordering agent. +- [01:14:20]() - Discussion of the pizza ordering agent’s order flow and state machine. +- [01:22:07]() - Using Claude to generate a state machine diagram. +- [01:32:17]() - Creating the Domino's plugin in Eliza. +- [01:54:15]() - Working on the pizza order provider. +- [02:16:46]() - Pizza provider code completed. +- [02:28:50]() - Discussion of caching customer and order data. +- [03:13:45]() - Pushing fixes to main branch and continuing work on the agent. +- [04:24:30]() - Discussion of summarizing past agent dev school sessions. +- [05:01:18]() - Shaw returns, admits to ordering Domino's manually. +- [05:09:00]() - Discussing payment flow and a confirm order action. +- [05:27:17]() - Final code push, wrap-up, and end of stream. + ## Summary @@ -86,6 +88,7 @@ In the second part, Shaw transitions to a more lighthearted coding session where - He uses Claude (an AI assistant) to generate code snippets and assist with the development process. - He decides to initially focus on a simplified version where the user's payment information is hardcoded in the environment variables, and the agent only needs to collect the user's address. + ## Hot Takes 1. **"Maybe we'll mix it on LinkedIn so people can order Domino's on LinkedIn. There you go. Now we're cooking." (00:03:26)** - Shaw's seemingly flippant idea of ordering pizza on LinkedIn highlights the potential for integrating everyday services into unexpected platforms through agents. This sparked discussion about the wider implications for businesses and social media. diff --git a/docs/community/Streams/12-2024/2024-12-13.md b/docs/community/Streams/12-2024/2024-12-13.md index bda0b80d64d..443de2e2e4f 100644 --- a/docs/community/Streams/12-2024/2024-12-13.md +++ b/docs/community/Streams/12-2024/2024-12-13.md @@ -8,86 +8,51 @@ description: "Building the Future: 30+ Developers Share Their AI Agent Progress" **Building the Future: 30+ Developers Share Their AI Agent Progress** -Date: 2024-12-13 -Twitter Spaces: https://x.com/i/spaces/1lDxLlgYjMkxm -YouTube Link: https://www.youtube.com/watch?v=4u8rbjmvWC0 +- Date: 2024-12-13 +- Twitter Spaces: https://x.com/i/spaces/1lDxLlgYjMkxm +- YouTube Link: https://www.youtube.com/watch?v=4u8rbjmvWC0 + ## Timestamps -- **00:01:04** - shawmakesmagic: Introduction and Format Changes for the Space - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=64 -- **00:02:38** - xsubtropic: Redux project, DaVinci AI - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=158 -- **00:06:57** - CottenIO: Scripted, AI Summit Recap - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=417 -- **00:08:58** - HDPbilly: Real Agency HQ, "Sploot" agent - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=538 -- **00:13:29** - IQ6900: On-chain ASCII art service - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=809 -- **00:18:50** - frankdegods: Eliza Character Sheet Tweaks - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=1130 -- **00:20:15** - jamesyoung: AI Agent Starter Kit - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=1215 -- **00:23:29** - 0xglu: Ducky and Agent Swarms - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=1409 -- **00:25:30** - chrislatorres: Eliza.gg - Eliza documentation site - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=1530 -- **00:27:47** - reality_spiral: Self-Improving Agents & Github integration - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=1667 -- **00:31:43** - robotsreview: Story Protocol plugin and Agentic TCPIP - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=1903 -- **00:34:19** - shannonNullCode: Emblem Vault & Message Ingestion - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=2059 -- **00:38:40** - bcsmithx: Agent Tank - Computer use agents - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=2320 -- **00:41:20** - boyaloxer: Plugin Feel - Emotion-based agent - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=2480 -- **00:44:09** - JustJamieJoyce: Muse of Truth/Research AI agents - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=2649 -- **00:46:11** - yikesawjeez: Discord bot & Contribution updates - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=2771 -- **00:50:56** - RodrigoSotoAlt: Monad, Metaplex Nfts, Solana integrations - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=3056 -- **00:53:22** - HowieDuhzit: Eliza Character Generator - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=3202 -- **00:55:57** - xrpublisher: XR Publisher, 3D Social Network on the edge - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=3357 -- **01:00:57** - BV_Bloom1: 3D Agent Interactions - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=3657 -- **01:02:57** - nftRanch: Trading Bot and Eliza V2 integrations - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=3777 -- **01:05:57** - 019ec6e2: Mimetic Platform and Agent Interactions - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=3957 -- **01:09:17** - jacobmtucker: Agent Transaction Control Protocol - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=4157 -- **01:12:26** - CurtisLaird5: C-Studio character interface - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=4346 -- **01:17:13** - unl\_\_cky: Escapism, art generation AI - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=4633 -- **01:19:17** - Rowdymode: Twin Tone - Interactive Streaming - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=4757 -- **01:20:29** - mitchcastanet: Binary Star System research with agents - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=4829 -- **01:23:15** - GoatOfGamblers: Prediction market for meme coins - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=4995 -- **01:25:27** - JohnNaulty: SWE contributions, plugin working groups - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=5127 -- **01:29:30** - mayanicks0x: Axie, AI KOL Agent - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=5370 -- **01:31:30** - wakesync: Eliza Wakes Up, web app updates - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=5490 -- **01:35:28** - TrenchBuddy: Trading agents and AWS templates - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=5728 -- **01:38:36** - rakshitaphilip: Brunette token and agent tips on Warpcast - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=5916 -- **01:44:49** - MbBrainz: Menu Recommendation app - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=6289 -- **01:46:03** - Hawkeye_Picks: Storytelling bot - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=6363 -- **01:49:16** - shawmakesmagic: Hiring and Eliza V2 - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=6556 -- **01:54:30** - dankvr: Community updates, tooling - - Link: https://www.youtube.com/watch?v=4u8rbjmvWC0&t=6870 +- [00:01:04]() - **shawmakesmagic**: Introduction and Format Changes for the Space +- [00:02:38]() - **xsubtropic**: Redux project, DaVinci AI +- [00:06:57]() - **CottenIO**: Scripted, AI Summit Recap +- [00:08:58]() - **HDPbilly**: Real Agency HQ, "Sploot" agent +- [00:13:29]() - **IQ6900**: On-chain ASCII art service +- [00:18:50]() - **frankdegods**: Eliza Character Sheet Tweaks +- [00:20:15]() - **jamesyoung**: AI Agent Starter Kit +- [00:23:29]() - **0xglu**: Ducky and Agent Swarms +- [00:25:30]() - **chrislatorres**: Eliza.gg - Eliza documentation site +- [00:27:47]() - **reality_spiral**: Self-Improving Agents & Github integration +- [00:31:43]() - **robotsreview**: Story Protocol plugin and Agentic TCPIP +- [00:34:19]() - **shannonNullCode**: Emblem Vault & Message Ingestion +- [00:38:40]() - **bcsmithx**: Agent Tank - Computer use agents +- [00:41:20]() - **boyaloxer**: Plugin Feel - Emotion-based agent +- [00:44:09]() - **JustJamieJoyce**: Muse of Truth/Research AI agents +- [00:46:11]() - **yikesawjeez**: Discord bot & Contribution updates +- [00:50:56]() - **RodrigoSotoAlt**: Monad, Metaplex Nfts, Solana integrations +- [00:53:22]() - **HowieDuhzit**: Eliza Character Generator +- [00:55:57]() - **xrpublisher**: XR Publisher, 3D Social Network on the edge +- [01:00:57]() - **BV_Bloom1**: 3D Agent Interactions +- [01:02:57]() - **nftRanch**: Trading Bot and Eliza V2 integrations +- [01:05:57]() - **019ec6e2**: Mimetic Platform and Agent Interactions +- [01:09:17]() - **jacobmtucker**: Agent Transaction Control Protocol +- [01:12:26]() - **CurtisLaird5**: C-Studio character interface +- [01:17:13]() - **unl__cky**: Escapism, art generation AI +- [01:19:17]() - **Rowdymode**: Twin Tone - Interactive Streaming +- [01:20:29]() - **mitchcastanet**: Binary Star System research with agents +- [01:23:15]() - **GoatOfGamblers**: Prediction market for meme coins +- [01:25:27]() - **JohnNaulty**: SWE contributions, plugin working groups +- [01:29:30]() - **mayanicks0x**: Axie, AI KOL Agent +- [01:31:30]() - **wakesync**: Eliza Wakes Up, web app updates +- [01:35:28]() - **TrenchBuddy**: Trading agents and AWS templates +- [01:38:36]() - **rakshitaphilip**: Brunette token and agent tips on Warpcast +- [01:44:49]() - **MbBrainz**: Menu Recommendation app +- [01:46:03]() - **Hawkeye_Picks**: Storytelling bot +- [01:49:16]() - **shawmakesmagic**: Hiring and Eliza V2 +- [01:54:30]() - **dankvr**: Community updates, tooling + ## Summary @@ -115,6 +80,7 @@ This Twitter Spaces event, hosted by ai16z and titled "What Did You Get Done Thi Overall, this event showed a vibrant and active community rapidly developing projects using the Eliza framework. It highlighted both the significant progress made in the past week and the challenges being tackled, showcasing the potential for AI agents in diverse real world applications. + ## Hot Takes 1. **"These corporations are going to cease to exist."** - **(00:07:31)** Tim Cotton makes a bold prediction about the future of traditional corporations in the face of AI agent technology. This implies a near-term and disruptive shift. diff --git a/docs/docs/advanced/eliza-in-tee.md b/docs/docs/advanced/eliza-in-tee.md index d906025459d..76d2e9e2851 100644 --- a/docs/docs/advanced/eliza-in-tee.md +++ b/docs/docs/advanced/eliza-in-tee.md @@ -272,7 +272,7 @@ services: - BIRDEYE_API_KEY=$BIRDEYE_API_KEY - SOL_ADDRESS=So11111111111111111111111111111111111111112 - SLIPPAGE=1 - - RPC_URL=https://api.mainnet-beta.solana.com + - SOLANA_RPC_URL=https://api.mainnet-beta.solana.com - HELIUS_API_KEY=$HELIUS_API_KEY - SERVER_PORT=3000 - WALLET_SECRET_SALT=$WALLET_SECRET_SALT diff --git a/docs/docs/api/_media/README_CN.md b/docs/docs/api/_media/README_CN.md index ae4f7a159a6..39f2a2be3b8 100644 --- a/docs/docs/api/_media/README_CN.md +++ b/docs/docs/api/_media/README_CN.md @@ -118,7 +118,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/docs/api/_media/README_FR.md b/docs/docs/api/_media/README_FR.md index 41b92f055f2..57f8b970f50 100644 --- a/docs/docs/api/_media/README_FR.md +++ b/docs/docs/api/_media/README_FR.md @@ -118,7 +118,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/docs/api/_media/README_JA.md b/docs/docs/api/_media/README_JA.md index 3bd9302ccbd..a85ef88ecff 100644 --- a/docs/docs/api/_media/README_JA.md +++ b/docs/docs/api/_media/README_JA.md @@ -120,7 +120,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/docs/api/_media/README_KOR.md b/docs/docs/api/_media/README_KOR.md index 555d037d9f7..71ef879745c 100644 --- a/docs/docs/api/_media/README_KOR.md +++ b/docs/docs/api/_media/README_KOR.md @@ -118,7 +118,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= diff --git a/docs/docs/api/functions/composeContext.md b/docs/docs/api/functions/composeContext.md index 1ca35bf352e..174048839b0 100644 --- a/docs/docs/api/functions/composeContext.md +++ b/docs/docs/api/functions/composeContext.md @@ -10,11 +10,11 @@ Composes a context string by replacing placeholders in a template with values fr An object containing the following properties: -- **state**: `State` +- **state**: `State` The state object containing key-value pairs for replacing placeholders in the template. -- **template**: `string` - A string containing placeholders in the format `{{placeholder}}`. +- **template**: `string | Function` + A string or function returning a string containing placeholders in the format `{{placeholder}}`. - **templatingEngine**: `"handlebars" | undefined` _(optional)_ The templating engine to use. If set to `"handlebars"`, the Handlebars engine is used for template compilation. Defaults to `undefined` (simple string replacement). @@ -51,7 +51,7 @@ const contextHandlebars = composeContext({ ```javascript const advancedTemplate = ` {{#if userAge}} - Hello, {{userName}}! + Hello, {{userName}}! {{#if (gt userAge 18)}}You are an adult.{{else}}You are a minor.{{/if}} {{else}} Hello! We don't know your age. diff --git a/docs/docs/api/index.md b/docs/docs/api/index.md index 4f3c232dcda..ed33e1393f0 100644 --- a/docs/docs/api/index.md +++ b/docs/docs/api/index.md @@ -123,7 +123,7 @@ BIRDEYE_API_KEY= SOL_ADDRESS=So11111111111111111111111111111111111111112 SLIPPAGE=1 -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com HELIUS_API_KEY= ## Telegram diff --git a/docs/docs/guides/local-development.md b/docs/docs/guides/local-development.md index f0b24bc5866..ce06bde9cc8 100644 --- a/docs/docs/guides/local-development.md +++ b/docs/docs/guides/local-development.md @@ -75,7 +75,6 @@ Configure essential development variables: ```bash # Minimum required for local development OPENAI_API_KEY=sk-* # Optional, for OpenAI features -X_SERVER_URL= # Leave blank for local inference XAI_API_KEY= # Leave blank for local inference XAI_MODEL=meta-llama/Llama-3.1-7b-instruct # Local model ``` diff --git a/docs/docs/packages/adapters.md b/docs/docs/packages/adapters.md index cce1e5e5ff2..82be60af69e 100644 --- a/docs/docs/packages/adapters.md +++ b/docs/docs/packages/adapters.md @@ -78,10 +78,17 @@ classDiagram +inMemoryOperations() } + class PGLiteDatabaseAdapter { + -db: PGlite + +searchMemoriesByEmbedding() + +createMemory() + } + DatabaseAdapter <|-- PostgresDatabaseAdapter DatabaseAdapter <|-- SqliteDatabaseAdapter DatabaseAdapter <|-- SupabaseDatabaseAdapter DatabaseAdapter <|-- SqlJsDatabaseAdapter + DatabaseAdapter <|-- PgLiteDatabaseAdapter class AgentRuntime { -databaseAdapter: DatabaseAdapter @@ -149,6 +156,9 @@ pnpm add @elizaos/adapter-sqljs sql.js # Supabase pnpm add @elizaos/adapter-supabase @supabase/supabase-js + +# PgLite +pnpm add @elizaos/adapter-pglite @electric-sql/pglite ``` --- @@ -198,6 +208,32 @@ const db = new SupabaseDatabaseAdapter( ); ``` +```typescript +import { SqliteDatabaseAdapter } from "@elizaos/adapter-sqlite"; +import Database from "better-sqlite3"; + +const db = new SqliteDatabaseAdapter( + new Database("./db.sqlite", { + // SQLite options + memory: false, + readonly: false, + fileMustExist: false, + }), +); +``` + +### PgLite Setup + +```typescript +import { PGLiteDatabaseAdapter } from "@elizaos/adapter-pglite"; + +const db = new PGLiteDatabaseAdapter( + new PGLite({ + dataDir: "./db" + }) +); +``` + --- ## Core Features diff --git a/docs/docs/packages/plugins.md b/docs/docs/packages/plugins.md index bda974c03e5..448d7e4e8d8 100644 --- a/docs/docs/packages/plugins.md +++ b/docs/docs/packages/plugins.md @@ -263,7 +263,7 @@ runtime.character.settings.secrets = { **Example Call** ```typescript -const response = await runtime.triggerAction("SEND_MASS_PAYOUT", { +const response = await runtime.processAction("SEND_MASS_PAYOUT", { receivingAddresses: [ "0xA0ba2ACB5846A54834173fB0DD9444F756810f06", "0xF14F2c49aa90BaFA223EE074C1C33b59891826bF", @@ -388,7 +388,7 @@ All contract deployments and interactions are logged to a CSV file for record-ke 1. **ERC20 Token** ```typescript - const response = await runtime.triggerAction("DEPLOY_TOKEN_CONTRACT", { + const response = await runtime.processAction("DEPLOY_TOKEN_CONTRACT", { contractType: "ERC20", name: "MyToken", symbol: "MTK", @@ -400,7 +400,7 @@ All contract deployments and interactions are logged to a CSV file for record-ke 2. **NFT Collection** ```typescript - const response = await runtime.triggerAction("DEPLOY_TOKEN_CONTRACT", { + const response = await runtime.processAction("DEPLOY_TOKEN_CONTRACT", { contractType: "ERC721", name: "MyNFT", symbol: "MNFT", @@ -411,7 +411,7 @@ All contract deployments and interactions are logged to a CSV file for record-ke 3. **Multi-token Collection** ```typescript - const response = await runtime.triggerAction("DEPLOY_TOKEN_CONTRACT", { + const response = await runtime.processAction("DEPLOY_TOKEN_CONTRACT", { contractType: "ERC1155", name: "MyMultiToken", symbol: "MMT", @@ -423,7 +423,7 @@ All contract deployments and interactions are logged to a CSV file for record-ke **Contract Interaction Example:** ```typescript -const response = await runtime.triggerAction("INVOKE_CONTRACT", { +const response = await runtime.processAction("INVOKE_CONTRACT", { contractAddress: "0x123...", method: "transfer", abi: [...], @@ -597,7 +597,7 @@ The Webhook Plugin enables Eliza to interact with the Coinbase SDK to create and To create a webhook: ```typescript -const response = await runtime.triggerAction("CREATE_WEBHOOK", { +const response = await runtime.processAction("CREATE_WEBHOOK", { networkId: "base", eventType: "transfers", notificationUri: "https://your-notification-uri.com", diff --git a/packages/adapter-pglite/.npmignore b/packages/adapter-pglite/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/adapter-pglite/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/adapter-pglite/eslint.config.mjs b/packages/adapter-pglite/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/adapter-pglite/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/adapter-pglite/package.json b/packages/adapter-pglite/package.json new file mode 100644 index 00000000000..7f7167333e1 --- /dev/null +++ b/packages/adapter-pglite/package.json @@ -0,0 +1,36 @@ +{ + "name": "@elizaos/adapter-pglite", + "version": "0.1.7-alpha.2", + "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": { + "@electric-sql/pglite": "^0.2.15", + "@elizaos/core": "workspace:*" + }, + "devDependencies": { + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache ." + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/adapter-pglite/schema.sql b/packages/adapter-pglite/schema.sql new file mode 100644 index 00000000000..4a0f7c6f1dd --- /dev/null +++ b/packages/adapter-pglite/schema.sql @@ -0,0 +1,140 @@ +-- Enable pgvector extension + +-- -- Drop existing tables and extensions +-- DROP EXTENSION IF EXISTS vector CASCADE; +-- DROP TABLE IF EXISTS relationships CASCADE; +-- DROP TABLE IF EXISTS participants CASCADE; +-- DROP TABLE IF EXISTS logs CASCADE; +-- DROP TABLE IF EXISTS goals CASCADE; +-- DROP TABLE IF EXISTS memories CASCADE; +-- DROP TABLE IF EXISTS rooms CASCADE; +-- DROP TABLE IF EXISTS accounts CASCADE; + + +CREATE EXTENSION IF NOT EXISTS vector; +CREATE EXTENSION IF NOT EXISTS fuzzystrmatch; + +-- Create a function to determine vector dimension +CREATE OR REPLACE FUNCTION get_embedding_dimension() +RETURNS INTEGER AS $$ +BEGIN + -- Check for OpenAI first + IF current_setting('app.use_openai_embedding', TRUE) = 'true' THEN + RETURN 1536; -- OpenAI dimension + -- Then check for Ollama + ELSIF current_setting('app.use_ollama_embedding', TRUE) = 'true' THEN + RETURN 1024; -- Ollama mxbai-embed-large dimension + -- Then check for GAIANET + ELSIF current_setting('app.use_gaianet_embedding', TRUE) = 'true' THEN + RETURN 768; -- Gaianet nomic-embed dimension + ELSE + RETURN 384; -- BGE/Other embedding dimension + END IF; +END; +$$ LANGUAGE plpgsql; + +BEGIN; + +CREATE TABLE IF NOT EXISTS accounts ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "name" TEXT, + "username" TEXT, + "email" TEXT NOT NULL, + "avatarUrl" TEXT, + "details" JSONB DEFAULT '{}'::jsonb +); + +CREATE TABLE IF NOT EXISTS rooms ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +DO $$ +DECLARE + vector_dim INTEGER; +BEGIN + vector_dim := get_embedding_dimension(); + + EXECUTE format(' + CREATE TABLE IF NOT EXISTS memories ( + "id" UUID PRIMARY KEY, + "type" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "content" JSONB NOT NULL, + "embedding" vector(%s), + "userId" UUID REFERENCES accounts("id"), + "agentId" UUID REFERENCES accounts("id"), + "roomId" UUID REFERENCES rooms("id"), + "unique" BOOLEAN DEFAULT true NOT NULL, + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE, + CONSTRAINT fk_agent FOREIGN KEY ("agentId") REFERENCES accounts("id") ON DELETE CASCADE + )', vector_dim); +END $$; + +CREATE TABLE IF NOT EXISTS goals ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userId" UUID REFERENCES accounts("id"), + "name" TEXT, + "status" TEXT, + "description" TEXT, + "roomId" UUID REFERENCES rooms("id"), + "objectives" JSONB DEFAULT '[]'::jsonb NOT NULL, + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS logs ( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userId" UUID NOT NULL REFERENCES accounts("id"), + "body" JSONB NOT NULL, + "type" TEXT NOT NULL, + "roomId" UUID NOT NULL REFERENCES rooms("id"), + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS participants ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userId" UUID REFERENCES accounts("id"), + "roomId" UUID REFERENCES rooms("id"), + "userState" TEXT, + "last_message_read" TEXT, + UNIQUE("userId", "roomId"), + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS relationships ( + "id" UUID PRIMARY KEY, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "userA" UUID NOT NULL REFERENCES accounts("id"), + "userB" UUID NOT NULL REFERENCES accounts("id"), + "status" TEXT, + "userId" UUID NOT NULL REFERENCES accounts("id"), + CONSTRAINT fk_user_a FOREIGN KEY ("userA") REFERENCES accounts("id") ON DELETE CASCADE, + CONSTRAINT fk_user_b FOREIGN KEY ("userB") REFERENCES accounts("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS cache ( + "key" TEXT NOT NULL, + "agentId" TEXT NOT NULL, + "value" JSONB DEFAULT '{}'::jsonb, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMP, + PRIMARY KEY ("key", "agentId") +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw ("embedding" vector_cosine_ops); +CREATE INDEX IF NOT EXISTS idx_memories_type_room ON memories("type", "roomId"); +CREATE INDEX IF NOT EXISTS idx_participants_user ON participants("userId"); +CREATE INDEX IF NOT EXISTS idx_participants_room ON participants("roomId"); +CREATE INDEX IF NOT EXISTS idx_relationships_users ON relationships("userA", "userB"); + +COMMIT; diff --git a/packages/adapter-pglite/src/index.ts b/packages/adapter-pglite/src/index.ts new file mode 100644 index 00000000000..5f65ff7989f --- /dev/null +++ b/packages/adapter-pglite/src/index.ts @@ -0,0 +1,1287 @@ +import { v4 } from "uuid"; + +import { + Account, + Actor, + GoalStatus, + type Goal, + type Memory, + type Relationship, + type UUID, + type IDatabaseCacheAdapter, + Participant, + elizaLogger, + getEmbeddingConfig, + DatabaseAdapter, + EmbeddingProvider, +} from "@elizaos/core"; +import fs from "fs"; +import { fileURLToPath } from "url"; +import path from "path"; +import { + PGlite, + PGliteOptions, + Results, + Transaction, +} from "@electric-sql/pglite"; +import { vector } from "@electric-sql/pglite/vector"; +import { fuzzystrmatch } from "@electric-sql/pglite/contrib/fuzzystrmatch"; + +const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file +const __dirname = path.dirname(__filename); // get the name of the directory + +export class PGLiteDatabaseAdapter + extends DatabaseAdapter + implements IDatabaseCacheAdapter +{ + constructor(options: PGliteOptions) { + super(); + this.db = new PGlite({ + ...options, + // Add the vector and fuzzystrmatch extensions + extensions: { + ...(options.extensions ?? {}), + vector, + fuzzystrmatch, + }, + }); + } + + async init() { + await this.db.waitReady; + + await this.withTransaction(async (tx) => { + // Set application settings for embedding dimension + const embeddingConfig = getEmbeddingConfig(); + if (embeddingConfig.provider === EmbeddingProvider.OpenAI) { + await tx.query("SET app.use_openai_embedding = 'true'"); + await tx.query("SET app.use_ollama_embedding = 'false'"); + await tx.query("SET app.use_gaianet_embedding = 'false'"); + } else if (embeddingConfig.provider === EmbeddingProvider.Ollama) { + await tx.query("SET app.use_openai_embedding = 'false'"); + await tx.query("SET app.use_ollama_embedding = 'true'"); + await tx.query("SET app.use_gaianet_embedding = 'false'"); + } else if (embeddingConfig.provider === EmbeddingProvider.GaiaNet) { + await tx.query("SET app.use_openai_embedding = 'false'"); + await tx.query("SET app.use_ollama_embedding = 'false'"); + await tx.query("SET app.use_gaianet_embedding = 'true'"); + } else { + await tx.query("SET app.use_openai_embedding = 'false'"); + await tx.query("SET app.use_ollama_embedding = 'false'"); + await tx.query("SET app.use_gaianet_embedding = 'false'"); + } + + const schema = fs.readFileSync( + path.resolve(__dirname, "../schema.sql"), + "utf8" + ); + await tx.exec(schema); + }, "init"); + } + + async close() { + await this.db.close(); + } + + private async withDatabase( + operation: () => Promise, + context: string + ): Promise { + return this.withCircuitBreaker(async () => { + return operation(); + }, context); + } + + private async withTransaction( + operation: (tx: Transaction) => Promise, + context: string + ): Promise { + return this.withCircuitBreaker(async () => { + return this.db.transaction(operation); + }, context); + } + + async query( + queryTextOrConfig: string, + values?: unknown[] + ): Promise> { + return this.withDatabase(async () => { + return await this.db.query(queryTextOrConfig, values); + }, "query"); + } + + async getRoom(roomId: UUID): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query<{ id: UUID }>( + "SELECT id FROM rooms WHERE id = $1", + [roomId] + ); + return rows.length > 0 ? rows[0].id : null; + }, "getRoom"); + } + + async getParticipantsForAccount(userId: UUID): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query( + `SELECT id, "userId", "roomId", "last_message_read" + FROM participants + WHERE "userId" = $1`, + [userId] + ); + return rows; + }, "getParticipantsForAccount"); + } + + async getParticipantUserState( + roomId: UUID, + userId: UUID + ): Promise<"FOLLOWED" | "MUTED" | null> { + return this.withDatabase(async () => { + const { rows } = await this.query<{ + userState: "FOLLOWED" | "MUTED"; + }>( + `SELECT "userState" FROM participants WHERE "roomId" = $1 AND "userId" = $2`, + [roomId, userId] + ); + return rows.length > 0 ? rows[0].userState : null; + }, "getParticipantUserState"); + } + + async getMemoriesByRoomIds(params: { + roomIds: UUID[]; + agentId?: UUID; + tableName: string; + }): Promise { + return this.withDatabase(async () => { + if (params.roomIds.length === 0) return []; + const placeholders = params.roomIds + .map((_, i) => `$${i + 2}`) + .join(", "); + + let query = `SELECT * FROM memories WHERE type = $1 AND "roomId" IN (${placeholders})`; + let queryParams = [params.tableName, ...params.roomIds]; + + if (params.agentId) { + query += ` AND "agentId" = $${params.roomIds.length + 2}`; + queryParams = [...queryParams, params.agentId]; + } + + const { rows } = await this.query(query, queryParams); + return rows.map((row) => ({ + ...row, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + })); + }, "getMemoriesByRoomIds"); + } + + async setParticipantUserState( + roomId: UUID, + userId: UUID, + state: "FOLLOWED" | "MUTED" | null + ): Promise { + return this.withDatabase(async () => { + await this.query( + `UPDATE participants SET "userState" = $1 WHERE "roomId" = $2 AND "userId" = $3`, + [state, roomId, userId] + ); + }, "setParticipantUserState"); + } + + async getParticipantsForRoom(roomId: UUID): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query<{ userId: UUID }>( + 'SELECT "userId" FROM participants WHERE "roomId" = $1', + [roomId] + ); + return rows.map((row) => row.userId); + }, "getParticipantsForRoom"); + } + + async getAccountById(userId: UUID): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query( + "SELECT * FROM accounts WHERE id = $1", + [userId] + ); + if (rows.length === 0) { + elizaLogger.debug("Account not found:", { userId }); + return null; + } + + const account = rows[0]; + // elizaLogger.debug("Account retrieved:", { + // userId, + // hasDetails: !!account.details, + // }); + + return { + ...account, + details: + typeof account.details === "string" + ? JSON.parse(account.details) + : account.details, + }; + }, "getAccountById"); + } + + async createAccount(account: Account): Promise { + return this.withDatabase(async () => { + try { + const accountId = account.id ?? v4(); + await this.query( + `INSERT INTO accounts (id, name, username, email, "avatarUrl", details) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + accountId, + account.name, + account.username || "", + account.email || "", + account.avatarUrl || "", + JSON.stringify(account.details), + ] + ); + elizaLogger.debug("Account created successfully:", { + accountId, + }); + return true; + } catch (error) { + elizaLogger.error("Error creating account:", { + error: + error instanceof Error ? error.message : String(error), + accountId: account.id, + name: account.name, // Only log non-sensitive fields + }); + return false; // Return false instead of throwing to maintain existing behavior + } + }, "createAccount"); + } + + async getActorById(params: { roomId: UUID }): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query( + `SELECT a.id, a.name, a.username, a.details + FROM participants p + LEFT JOIN accounts a ON p."userId" = a.id + WHERE p."roomId" = $1`, + [params.roomId] + ); + + elizaLogger.debug("Retrieved actors:", { + roomId: params.roomId, + actorCount: rows.length, + }); + + return rows.map((row) => { + try { + return { + ...row, + details: + typeof row.details === "string" + ? JSON.parse(row.details) + : row.details, + }; + } catch (error) { + elizaLogger.warn("Failed to parse actor details:", { + actorId: row.id, + error: + error instanceof Error + ? error.message + : String(error), + }); + return { + ...row, + details: {}, // Provide default empty details on parse error + }; + } + }); + }, "getActorById").catch((error) => { + elizaLogger.error("Failed to get actors:", { + roomId: params.roomId, + error: error.message, + }); + throw error; // Re-throw to let caller handle database errors + }); + } + + async getMemoryById(id: UUID): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query( + "SELECT * FROM memories WHERE id = $1", + [id] + ); + if (rows.length === 0) return null; + + return { + ...rows[0], + content: + typeof rows[0].content === "string" + ? JSON.parse(rows[0].content) + : rows[0].content, + }; + }, "getMemoryById"); + } + + async createMemory(memory: Memory, tableName: string): Promise { + return this.withDatabase(async () => { + elizaLogger.debug("PostgresAdapter createMemory:", { + memoryId: memory.id, + embeddingLength: memory.embedding?.length, + contentLength: memory.content?.text?.length, + }); + + let isUnique = true; + if (memory.embedding) { + const similarMemories = await this.searchMemoriesByEmbedding( + memory.embedding, + { + tableName, + roomId: memory.roomId, + match_threshold: 0.95, + count: 1, + } + ); + isUnique = similarMemories.length === 0; + } + + await this.query( + `INSERT INTO memories ( + id, type, content, embedding, "userId", "roomId", "agentId", "unique", "createdAt" + ) VALUES ($1, $2, $3, $4, $5::uuid, $6::uuid, $7::uuid, $8, to_timestamp($9/1000.0))`, + [ + memory.id ?? v4(), + tableName, + JSON.stringify(memory.content), + memory.embedding ? `[${memory.embedding.join(",")}]` : null, + memory.userId, + memory.roomId, + memory.agentId, + memory.unique ?? isUnique, + Date.now(), + ] + ); + }, "createMemory"); + } + + async searchMemories(params: { + tableName: string; + agentId: UUID; + roomId: UUID; + embedding: number[]; + match_threshold: number; + match_count: number; + unique: boolean; + }): Promise { + return await this.searchMemoriesByEmbedding(params.embedding, { + match_threshold: params.match_threshold, + count: params.match_count, + agentId: params.agentId, + roomId: params.roomId, + unique: params.unique, + tableName: params.tableName, + }); + } + + async getMemories(params: { + roomId: UUID; + count?: number; + unique?: boolean; + tableName: string; + agentId?: UUID; + start?: number; + end?: number; + }): Promise { + // Parameter validation + if (!params.tableName) throw new Error("tableName is required"); + if (!params.roomId) throw new Error("roomId is required"); + + return this.withDatabase(async () => { + // Build query + let sql = `SELECT * FROM memories WHERE type = $1 AND "roomId" = $2`; + const values: unknown[] = [params.tableName, params.roomId]; + let paramCount = 2; + + // Add time range filters + if (params.start) { + paramCount++; + sql += ` AND "createdAt" >= to_timestamp($${paramCount})`; + values.push(params.start / 1000); + } + + if (params.end) { + paramCount++; + sql += ` AND "createdAt" <= to_timestamp($${paramCount})`; + values.push(params.end / 1000); + } + + // Add other filters + if (params.unique) { + sql += ` AND "unique" = true`; + } + + if (params.agentId) { + paramCount++; + sql += ` AND "agentId" = $${paramCount}`; + values.push(params.agentId); + } + + // Add ordering and limit + sql += ' ORDER BY "createdAt" DESC'; + + if (params.count) { + paramCount++; + sql += ` LIMIT $${paramCount}`; + values.push(params.count); + } + + elizaLogger.debug("Fetching memories:", { + roomId: params.roomId, + tableName: params.tableName, + unique: params.unique, + agentId: params.agentId, + timeRange: + params.start || params.end + ? { + start: params.start + ? new Date(params.start).toISOString() + : undefined, + end: params.end + ? new Date(params.end).toISOString() + : undefined, + } + : undefined, + limit: params.count, + }); + + const { rows } = await this.query(sql, values); + return rows.map((row) => ({ + ...row, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + })); + }, "getMemories"); + } + + async getGoals(params: { + roomId: UUID; + userId?: UUID | null; + onlyInProgress?: boolean; + count?: number; + }): Promise { + return this.withDatabase(async () => { + let sql = `SELECT * FROM goals WHERE "roomId" = $1`; + const values: unknown[] = [params.roomId]; + let paramCount = 1; + + if (params.userId) { + paramCount++; + sql += ` AND "userId" = $${paramCount}`; + values.push(params.userId); + } + + if (params.onlyInProgress) { + sql += " AND status = 'IN_PROGRESS'"; + } + + if (params.count) { + paramCount++; + sql += ` LIMIT $${paramCount}`; + values.push(params.count); + } + + const { rows } = await this.query(sql, values); + return rows.map((row) => ({ + ...row, + objectives: + typeof row.objectives === "string" + ? JSON.parse(row.objectives) + : row.objectives, + })); + }, "getGoals"); + } + + async updateGoal(goal: Goal): Promise { + return this.withDatabase(async () => { + try { + await this.query( + `UPDATE goals SET name = $1, status = $2, objectives = $3 WHERE id = $4`, + [ + goal.name, + goal.status, + JSON.stringify(goal.objectives), + goal.id, + ] + ); + } catch (error) { + elizaLogger.error("Failed to update goal:", { + goalId: goal.id, + error: + error instanceof Error ? error.message : String(error), + status: goal.status, + }); + throw error; + } + }, "updateGoal"); + } + + async createGoal(goal: Goal): Promise { + return this.withDatabase(async () => { + await this.query( + `INSERT INTO goals (id, "roomId", "userId", name, status, objectives) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + goal.id ?? v4(), + goal.roomId, + goal.userId, + goal.name, + goal.status, + JSON.stringify(goal.objectives), + ] + ); + }, "createGoal"); + } + + async removeGoal(goalId: UUID): Promise { + if (!goalId) throw new Error("Goal ID is required"); + + return this.withDatabase(async () => { + try { + const result = await this.query( + "DELETE FROM goals WHERE id = $1 RETURNING id", + [goalId] + ); + + elizaLogger.debug("Goal removal attempt:", { + goalId, + removed: result?.affectedRows ?? 0 > 0, + }); + } catch (error) { + elizaLogger.error("Failed to remove goal:", { + goalId, + error: + error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, "removeGoal"); + } + + async createRoom(roomId?: UUID): Promise { + return this.withDatabase(async () => { + const newRoomId = roomId || v4(); + await this.query("INSERT INTO rooms (id) VALUES ($1)", [newRoomId]); + return newRoomId as UUID; + }, "createRoom"); + } + + async removeRoom(roomId: UUID): Promise { + if (!roomId) throw new Error("Room ID is required"); + + return this.withTransaction(async (tx) => { + try { + // First check if room exists + const checkResult = await tx.query( + "SELECT id FROM rooms WHERE id = $1", + [roomId] + ); + + if (checkResult.rows.length === 0) { + elizaLogger.warn("No room found to remove:", { + roomId, + }); + throw new Error(`Room not found: ${roomId}`); + } + + // Remove related data first (if not using CASCADE) + await tx.query('DELETE FROM memories WHERE "roomId" = $1', [ + roomId, + ]); + await tx.query('DELETE FROM participants WHERE "roomId" = $1', [ + roomId, + ]); + await tx.query('DELETE FROM goals WHERE "roomId" = $1', [ + roomId, + ]); + + // Finally remove the room + const result = await tx.query( + "DELETE FROM rooms WHERE id = $1 RETURNING id", + [roomId] + ); + + elizaLogger.debug( + "Room and related data removed successfully:", + { + roomId, + removed: result?.affectedRows ?? 0 > 0, + } + ); + } catch (error) { + elizaLogger.error("Failed to remove room:", { + roomId, + error: + error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, "removeRoom"); + } + + async createRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + // Input validation + if (!params.userA || !params.userB) { + throw new Error("userA and userB are required"); + } + + return this.withDatabase(async () => { + try { + const relationshipId = v4(); + await this.query( + `INSERT INTO relationships (id, "userA", "userB", "userId") + VALUES ($1, $2, $3, $4) + RETURNING id`, + [relationshipId, params.userA, params.userB, params.userA] + ); + + elizaLogger.debug("Relationship created successfully:", { + relationshipId, + userA: params.userA, + userB: params.userB, + }); + + return true; + } catch (error) { + // Check for unique constraint violation or other specific errors + if ((error as { code?: string }).code === "23505") { + // Unique violation + elizaLogger.warn("Relationship already exists:", { + userA: params.userA, + userB: params.userB, + error: + error instanceof Error + ? error.message + : String(error), + }); + } else { + elizaLogger.error("Failed to create relationship:", { + userA: params.userA, + userB: params.userB, + error: + error instanceof Error + ? error.message + : String(error), + }); + } + return false; + } + }, "createRelationship"); + } + + async getRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + if (!params.userA || !params.userB) { + throw new Error("userA and userB are required"); + } + + return this.withDatabase(async () => { + try { + const { rows } = await this.query( + `SELECT * FROM relationships + WHERE ("userA" = $1 AND "userB" = $2) + OR ("userA" = $2 AND "userB" = $1)`, + [params.userA, params.userB] + ); + + if (rows.length > 0) { + elizaLogger.debug("Relationship found:", { + relationshipId: rows[0].id, + userA: params.userA, + userB: params.userB, + }); + return rows[0]; + } + + elizaLogger.debug("No relationship found between users:", { + userA: params.userA, + userB: params.userB, + }); + return null; + } catch (error) { + elizaLogger.error("Error fetching relationship:", { + userA: params.userA, + userB: params.userB, + error: + error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, "getRelationship"); + } + + async getRelationships(params: { userId: UUID }): Promise { + if (!params.userId) { + throw new Error("userId is required"); + } + + return this.withDatabase(async () => { + try { + const { rows } = await this.query( + `SELECT * FROM relationships + WHERE "userA" = $1 OR "userB" = $1 + ORDER BY "createdAt" DESC`, // Add ordering if you have this field + [params.userId] + ); + + elizaLogger.debug("Retrieved relationships:", { + userId: params.userId, + count: rows.length, + }); + + return rows; + } catch (error) { + elizaLogger.error("Failed to fetch relationships:", { + userId: params.userId, + error: + error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, "getRelationships"); + } + + async getCachedEmbeddings(opts: { + query_table_name: string; + query_threshold: number; + query_input: string; + query_field_name: string; + query_field_sub_name: string; + query_match_count: number; + }): Promise<{ embedding: number[]; levenshtein_score: number }[]> { + // Input validation + if (!opts.query_table_name) + throw new Error("query_table_name is required"); + if (!opts.query_input) throw new Error("query_input is required"); + if (!opts.query_field_name) + throw new Error("query_field_name is required"); + if (!opts.query_field_sub_name) + throw new Error("query_field_sub_name is required"); + if (opts.query_match_count <= 0) + throw new Error("query_match_count must be positive"); + + return this.withDatabase(async () => { + try { + elizaLogger.debug("Fetching cached embeddings:", { + tableName: opts.query_table_name, + fieldName: opts.query_field_name, + subFieldName: opts.query_field_sub_name, + matchCount: opts.query_match_count, + inputLength: opts.query_input.length, + }); + + const sql = ` + WITH content_text AS ( + SELECT + embedding, + COALESCE( + content->$2->>$3, + '' + ) as content_text + FROM memories + WHERE type = $4 + AND content->$2->>$3 IS NOT NULL + ) + SELECT + embedding, + levenshtein( + $1, + content_text + ) as levenshtein_score + FROM content_text + WHERE levenshtein( + $1, + content_text + ) <= $6 -- Add threshold check + ORDER BY levenshtein_score + LIMIT $5 + `; + + const { rows } = await this.query<{ + embedding: number[]; + levenshtein_score: number; + }>(sql, [ + opts.query_input, + opts.query_field_name, + opts.query_field_sub_name, + opts.query_table_name, + opts.query_match_count, + opts.query_threshold, + ]); + + elizaLogger.debug("Retrieved cached embeddings:", { + count: rows.length, + tableName: opts.query_table_name, + matchCount: opts.query_match_count, + }); + + return rows + .map( + ( + row + ): { + embedding: number[]; + levenshtein_score: number; + } | null => { + if (!Array.isArray(row.embedding)) return null; + return { + embedding: row.embedding, + levenshtein_score: Number( + row.levenshtein_score + ), + }; + } + ) + .filter( + ( + row + ): row is { + embedding: number[]; + levenshtein_score: number; + } => row !== null + ); + } catch (error) { + elizaLogger.error("Error in getCachedEmbeddings:", { + error: + error instanceof Error ? error.message : String(error), + tableName: opts.query_table_name, + fieldName: opts.query_field_name, + }); + throw error; + } + }, "getCachedEmbeddings"); + } + + async log(params: { + body: { [key: string]: unknown }; + userId: UUID; + roomId: UUID; + type: string; + }): Promise { + // Input validation + if (!params.userId) throw new Error("userId is required"); + if (!params.roomId) throw new Error("roomId is required"); + if (!params.type) throw new Error("type is required"); + if (!params.body || typeof params.body !== "object") { + throw new Error("body must be a valid object"); + } + + return this.withDatabase(async () => { + try { + const logId = v4(); // Generate ID for tracking + await this.query( + `INSERT INTO logs ( + id, + body, + "userId", + "roomId", + type, + "createdAt" + ) VALUES ($1, $2, $3, $4, $5, NOW()) + RETURNING id`, + [ + logId, + JSON.stringify(params.body), // Ensure body is stringified + params.userId, + params.roomId, + params.type, + ] + ); + + elizaLogger.debug("Log entry created:", { + logId, + type: params.type, + roomId: params.roomId, + userId: params.userId, + bodyKeys: Object.keys(params.body), + }); + } catch (error) { + elizaLogger.error("Failed to create log entry:", { + error: + error instanceof Error ? error.message : String(error), + type: params.type, + roomId: params.roomId, + userId: params.userId, + }); + throw error; + } + }, "log"); + } + + async searchMemoriesByEmbedding( + embedding: number[], + params: { + match_threshold?: number; + count?: number; + agentId?: UUID; + roomId?: UUID; + unique?: boolean; + tableName: string; + } + ): Promise { + return this.withDatabase(async () => { + elizaLogger.debug("Incoming vector:", { + length: embedding.length, + sample: embedding.slice(0, 5), + isArray: Array.isArray(embedding), + allNumbers: embedding.every((n) => typeof n === "number"), + }); + + // Validate embedding dimension + if (embedding.length !== getEmbeddingConfig().dimensions) { + throw new Error( + `Invalid embedding dimension: expected ${getEmbeddingConfig().dimensions}, got ${embedding.length}` + ); + } + + // Ensure vector is properly formatted + const cleanVector = embedding.map((n) => { + if (!Number.isFinite(n)) return 0; + // Limit precision to avoid floating point issues + return Number(n.toFixed(6)); + }); + + // Format for Postgres pgvector + const vectorStr = `[${cleanVector.join(",")}]`; + + elizaLogger.debug("Vector debug:", { + originalLength: embedding.length, + cleanLength: cleanVector.length, + sampleStr: vectorStr.slice(0, 100), + }); + + let sql = ` + SELECT *, + 1 - (embedding <-> $1::vector(${getEmbeddingConfig().dimensions})) as similarity + FROM memories + WHERE type = $2 + `; + + const values: unknown[] = [vectorStr, params.tableName]; + + // Log the query for debugging + elizaLogger.debug("Query debug:", { + sql: sql.slice(0, 200), + paramTypes: values.map((v) => typeof v), + vectorStrLength: vectorStr.length, + }); + + let paramCount = 2; + + if (params.unique) { + sql += ` AND "unique" = true`; + } + + if (params.agentId) { + paramCount++; + sql += ` AND "agentId" = $${paramCount}`; + values.push(params.agentId); + } + + if (params.roomId) { + paramCount++; + sql += ` AND "roomId" = $${paramCount}::uuid`; + values.push(params.roomId); + } + + if (params.match_threshold) { + paramCount++; + sql += ` AND 1 - (embedding <-> $1::vector) >= $${paramCount}`; + values.push(params.match_threshold); + } + + sql += ` ORDER BY embedding <-> $1::vector`; + + if (params.count) { + paramCount++; + sql += ` LIMIT $${paramCount}`; + values.push(params.count); + } + + const { rows } = await this.query(sql, values); + return rows.map((row) => ({ + ...row, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + similarity: row.similarity, + })); + }, "searchMemoriesByEmbedding"); + } + + async addParticipant(userId: UUID, roomId: UUID): Promise { + return this.withDatabase(async () => { + try { + await this.query( + `INSERT INTO participants (id, "userId", "roomId") + VALUES ($1, $2, $3)`, + [v4(), userId, roomId] + ); + return true; + } catch (error) { + console.log("Error adding participant", error); + return false; + } + }, "addParticpant"); + } + + async removeParticipant(userId: UUID, roomId: UUID): Promise { + return this.withDatabase(async () => { + try { + await this.query( + `DELETE FROM participants WHERE "userId" = $1 AND "roomId" = $2`, + [userId, roomId] + ); + return true; + } catch (error) { + console.log("Error removing participant", error); + return false; + } + }, "removeParticipant"); + } + + async updateGoalStatus(params: { + goalId: UUID; + status: GoalStatus; + }): Promise { + return this.withDatabase(async () => { + await this.query("UPDATE goals SET status = $1 WHERE id = $2", [ + params.status, + params.goalId, + ]); + }, "updateGoalStatus"); + } + + async removeMemory(memoryId: UUID, tableName: string): Promise { + return this.withDatabase(async () => { + await this.query( + "DELETE FROM memories WHERE type = $1 AND id = $2", + [tableName, memoryId] + ); + }, "removeMemory"); + } + + async removeAllMemories(roomId: UUID, tableName: string): Promise { + return this.withDatabase(async () => { + await this.query( + `DELETE FROM memories WHERE type = $1 AND "roomId" = $2`, + [tableName, roomId] + ); + }, "removeAllMemories"); + } + + async countMemories( + roomId: UUID, + unique = true, + tableName = "" + ): Promise { + if (!tableName) throw new Error("tableName is required"); + + return this.withDatabase(async () => { + let sql = `SELECT COUNT(*) as count FROM memories WHERE type = $1 AND "roomId" = $2`; + if (unique) { + sql += ` AND "unique" = true`; + } + + const { rows } = await this.query<{ count: number }>(sql, [ + tableName, + roomId, + ]); + return rows[0].count; + }, "countMemories"); + } + + async removeAllGoals(roomId: UUID): Promise { + return this.withDatabase(async () => { + await this.query(`DELETE FROM goals WHERE "roomId" = $1`, [roomId]); + }, "removeAllGoals"); + } + + async getRoomsForParticipant(userId: UUID): Promise { + return this.withDatabase(async () => { + const { rows } = await this.query<{ roomId: UUID }>( + `SELECT "roomId" FROM participants WHERE "userId" = $1`, + [userId] + ); + return rows.map((row) => row.roomId); + }, "getRoomsForParticipant"); + } + + async getRoomsForParticipants(userIds: UUID[]): Promise { + return this.withDatabase(async () => { + const placeholders = userIds.map((_, i) => `$${i + 1}`).join(", "); + const { rows } = await this.query<{ roomId: UUID }>( + `SELECT DISTINCT "roomId" FROM participants WHERE "userId" IN (${placeholders})`, + userIds + ); + return rows.map((row) => row.roomId); + }, "getRoomsForParticipants"); + } + + async getActorDetails(params: { roomId: string }): Promise { + if (!params.roomId) { + throw new Error("roomId is required"); + } + + return this.withDatabase(async () => { + try { + const sql = ` + SELECT + a.id, + a.name, + a.username, + a."avatarUrl", + COALESCE(a.details::jsonb, '{}'::jsonb) as details + FROM participants p + LEFT JOIN accounts a ON p."userId" = a.id + WHERE p."roomId" = $1 + ORDER BY a.name + `; + + const result = await this.query(sql, [params.roomId]); + + elizaLogger.debug("Retrieved actor details:", { + roomId: params.roomId, + actorCount: result.rows.length, + }); + + return result.rows.map((row) => { + try { + return { + ...row, + details: + typeof row.details === "string" + ? JSON.parse(row.details) + : row.details, + }; + } catch (parseError) { + elizaLogger.warn("Failed to parse actor details:", { + actorId: row.id, + error: + parseError instanceof Error + ? parseError.message + : String(parseError), + }); + return { + ...row, + details: {}, // Fallback to empty object if parsing fails + }; + } + }); + } catch (error) { + elizaLogger.error("Failed to fetch actor details:", { + roomId: params.roomId, + error: + error instanceof Error ? error.message : String(error), + }); + throw new Error( + `Failed to fetch actor details: ${error instanceof Error ? error.message : String(error)}` + ); + } + }, "getActorDetails"); + } + + async getCache(params: { + key: string; + agentId: UUID; + }): Promise { + return this.withDatabase(async () => { + try { + const sql = `SELECT "value"::TEXT FROM cache WHERE "key" = $1 AND "agentId" = $2`; + const { rows } = await this.query<{ value: string }>(sql, [ + params.key, + params.agentId, + ]); + return rows[0]?.value ?? undefined; + } catch (error) { + elizaLogger.error("Error fetching cache", { + error: + error instanceof Error ? error.message : String(error), + key: params.key, + agentId: params.agentId, + }); + return undefined; + } + }, "getCache"); + } + + async setCache(params: { + key: string; + agentId: UUID; + value: string; + }): Promise { + return ( + (await this.withTransaction(async (tx) => { + try { + await tx.query( + `INSERT INTO cache ("key", "agentId", "value", "createdAt") + VALUES ($1, $2, $3, CURRENT_TIMESTAMP) + ON CONFLICT ("key", "agentId") + DO UPDATE SET "value" = EXCLUDED.value, "createdAt" = CURRENT_TIMESTAMP`, + [params.key, params.agentId, params.value] + ); + return true; + } catch (error) { + await tx.rollback(); + elizaLogger.error("Error setting cache", { + error: + error instanceof Error + ? error.message + : String(error), + key: params.key, + agentId: params.agentId, + }); + return false; + } + }, "setCache")) ?? false + ); + } + + async deleteCache(params: { + key: string; + agentId: UUID; + }): Promise { + return ( + (await this.withTransaction(async (tx) => { + try { + await tx.query( + `DELETE FROM cache WHERE "key" = $1 AND "agentId" = $2`, + [params.key, params.agentId] + ); + return true; + } catch (error) { + tx.rollback(); + elizaLogger.error("Error deleting cache", { + error: + error instanceof Error + ? error.message + : String(error), + key: params.key, + agentId: params.agentId, + }); + return false; + } + }, "deleteCache")) ?? false + ); + } +} + +export default PGLiteDatabaseAdapter; diff --git a/packages/adapter-pglite/tsconfig.json b/packages/adapter-pglite/tsconfig.json new file mode 100644 index 00000000000..673cf100f47 --- /dev/null +++ b/packages/adapter-pglite/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "strict": true + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/adapter-pglite/tsup.config.ts b/packages/adapter-pglite/tsup.config.ts new file mode 100644 index 00000000000..964bdc86854 --- /dev/null +++ b/packages/adapter-pglite/tsup.config.ts @@ -0,0 +1,22 @@ +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", + "@anush008/tokenizers", + "uuid", + // Add other modules you want to externalize + ], +}); diff --git a/packages/client-direct/src/index.ts b/packages/client-direct/src/index.ts index 8f7f224b00a..561365d60f9 100644 --- a/packages/client-direct/src/index.ts +++ b/packages/client-direct/src/index.ts @@ -226,6 +226,12 @@ export class DirectClient { ); const text = req.body.text; + // if empty text, directly return + if (!text) { + res.json([]); + return; + } + const messageId = stringToUuid(Date.now().toString()); const attachments: Media[] = []; diff --git a/packages/client-discord/__tests__/discord-client.test.ts b/packages/client-discord/__tests__/discord-client.test.ts new file mode 100644 index 00000000000..969e0491877 --- /dev/null +++ b/packages/client-discord/__tests__/discord-client.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { DiscordClient } from '../src'; +import { Client, Events } from 'discord.js'; + +// Mock @elizaos/core +vi.mock('@elizaos/core', () => ({ + elizaLogger: { + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }, + getEmbeddingZeroVector: () => new Array(1536).fill(0), + stringToUuid: (str: string) => str, + messageCompletionFooter: '# INSTRUCTIONS: Choose the best response for the agent.', + shouldRespondFooter: '# INSTRUCTIONS: Choose if the agent should respond.', + generateMessageResponse: vi.fn(), + generateShouldRespond: vi.fn(), + composeContext: vi.fn(), + composeRandomUser: vi.fn(), +})); + +// Mock discord.js Client +vi.mock('discord.js', () => { + const mockClient = { + login: vi.fn().mockResolvedValue('token'), + on: vi.fn(), + once: vi.fn(), + destroy: vi.fn().mockResolvedValue(undefined), + }; + + return { + Client: vi.fn(() => mockClient), + Events: { + ClientReady: 'ready', + MessageCreate: 'messageCreate', + VoiceStateUpdate: 'voiceStateUpdate', + MessageReactionAdd: 'messageReactionAdd', + MessageReactionRemove: 'messageReactionRemove', + }, + GatewayIntentBits: { + Guilds: 1, + DirectMessages: 2, + GuildVoiceStates: 3, + MessageContent: 4, + GuildMessages: 5, + DirectMessageTyping: 6, + GuildMessageTyping: 7, + GuildMessageReactions: 8, + }, + Partials: { + Channel: 'channel', + Message: 'message', + User: 'user', + Reaction: 'reaction', + }, + Collection: class Collection extends Map {}, + }; +}); + +describe('DiscordClient', () => { + let mockRuntime: any; + let discordClient: DiscordClient; + + beforeEach(() => { + mockRuntime = { + getSetting: vi.fn((key: string) => { + if (key === 'DISCORD_API_TOKEN') return 'mock-token'; + return undefined; + }), + getState: vi.fn(), + setState: vi.fn(), + getMemory: vi.fn(), + setMemory: vi.fn(), + getService: vi.fn(), + registerAction: vi.fn(), + providers: [], + character: { + clientConfig: { + discord: { + shouldIgnoreBotMessages: true + } + } + } + }; + + discordClient = new DiscordClient(mockRuntime); + }); + + it('should initialize with correct configuration', () => { + expect(discordClient.apiToken).toBe('mock-token'); + expect(discordClient.client).toBeDefined(); + expect(mockRuntime.getSetting).toHaveBeenCalledWith('DISCORD_API_TOKEN'); + }); + + it('should login to Discord on initialization', () => { + expect(discordClient.client.login).toHaveBeenCalledWith('mock-token'); + }); + + it('should register event handlers on initialization', () => { + expect(discordClient.client.once).toHaveBeenCalledWith(Events.ClientReady, expect.any(Function)); + expect(discordClient.client.on).toHaveBeenCalledWith('guildCreate', expect.any(Function)); + expect(discordClient.client.on).toHaveBeenCalledWith(Events.MessageReactionAdd, expect.any(Function)); + expect(discordClient.client.on).toHaveBeenCalledWith(Events.MessageReactionRemove, expect.any(Function)); + expect(discordClient.client.on).toHaveBeenCalledWith('voiceStateUpdate', expect.any(Function)); + }); + + it('should clean up resources when stopped', async () => { + await discordClient.stop(); + expect(discordClient.client.destroy).toHaveBeenCalled(); + }); +}); diff --git a/packages/client-discord/package.json b/packages/client-discord/package.json index 795788be022..49f7ac89e53 100644 --- a/packages/client-discord/package.json +++ b/packages/client-discord/package.json @@ -30,12 +30,14 @@ "zod": "3.23.8" }, "devDependencies": { - "tsup": "8.3.5" + "tsup": "8.3.5", + "vitest": "1.2.1" }, "scripts": { "build": "tsup --format esm --dts", "dev": "tsup --format esm --dts --watch", - "lint": "eslint --fix --cache ." + "lint": "eslint --fix --cache .", + "test": "vitest run" }, "trustedDependencies": { "@discordjs/opus": "github:discordjs/opus", diff --git a/packages/client-discord/vitest.config.ts b/packages/client-discord/vitest.config.ts new file mode 100644 index 00000000000..a11fbbd0d9e --- /dev/null +++ b/packages/client-discord/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@elizaos/core': resolve(__dirname, '../core/src'), + }, + }, +}); diff --git a/packages/client-twitter/__tests__/base.test.ts b/packages/client-twitter/__tests__/base.test.ts new file mode 100644 index 00000000000..59a15c33c9a --- /dev/null +++ b/packages/client-twitter/__tests__/base.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ClientBase } from '../src/base'; +import { IAgentRuntime } from '@elizaos/core'; +import { TwitterConfig } from '../src/environment'; + +describe('Twitter Client Base', () => { + let mockRuntime: IAgentRuntime; + let mockConfig: TwitterConfig; + + beforeEach(() => { + mockRuntime = { + env: { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: 'true', + TWITTER_POST_INTERVAL_MIN: '5', + TWITTER_POST_INTERVAL_MAX: '10', + TWITTER_ACTION_INTERVAL: '5', + TWITTER_ENABLE_ACTION_PROCESSING: 'true', + TWITTER_POST_IMMEDIATELY: 'false', + TWITTER_SEARCH_ENABLE: 'false' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + }, + character: { + style: { + all: ['Test style 1', 'Test style 2'], + post: ['Post style 1', 'Post style 2'] + } + } + } as unknown as IAgentRuntime; + + mockConfig = { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: true, + TWITTER_SEARCH_ENABLE: false, + TWITTER_SPACES_ENABLE: false, + TWITTER_TARGET_USERS: [], + TWITTER_MAX_TWEETS_PER_DAY: 10, + TWITTER_MAX_TWEET_LENGTH: 280, + POST_INTERVAL_MIN: 5, + POST_INTERVAL_MAX: 10, + ACTION_INTERVAL: 5, + ENABLE_ACTION_PROCESSING: true, + POST_IMMEDIATELY: false + }; + }); + + it('should create instance with correct configuration', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client).toBeDefined(); + expect(client.twitterConfig).toBeDefined(); + expect(client.twitterConfig.TWITTER_USERNAME).toBe('testuser'); + expect(client.twitterConfig.TWITTER_DRY_RUN).toBe(true); + }); + + it('should initialize with correct tweet length limit', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client.twitterConfig.TWITTER_MAX_TWEET_LENGTH).toBe(280); + }); + + it('should initialize with correct post intervals', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client.twitterConfig.POST_INTERVAL_MIN).toBe(5); + expect(client.twitterConfig.POST_INTERVAL_MAX).toBe(10); + }); + + it('should initialize with correct action settings', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client.twitterConfig.ACTION_INTERVAL).toBe(5); + expect(client.twitterConfig.ENABLE_ACTION_PROCESSING).toBe(true); + }); +}); diff --git a/packages/client-twitter/__tests__/environment.test.ts b/packages/client-twitter/__tests__/environment.test.ts new file mode 100644 index 00000000000..dccfd0584b1 --- /dev/null +++ b/packages/client-twitter/__tests__/environment.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, vi } from 'vitest'; +import { validateTwitterConfig } from '../src/environment'; +import { IAgentRuntime } from '@elizaos/core'; + +describe('Twitter Environment Configuration', () => { + const mockRuntime: IAgentRuntime = { + env: { + TWITTER_USERNAME: 'testuser123', + TWITTER_DRY_RUN: 'true', + TWITTER_SEARCH_ENABLE: 'false', + TWITTER_SPACES_ENABLE: 'false', + TWITTER_TARGET_USERS: 'user1,user2,user3', + TWITTER_MAX_TWEETS_PER_DAY: '10', + TWITTER_MAX_TWEET_LENGTH: '280', + TWITTER_POST_INTERVAL_MIN: '90', + TWITTER_POST_INTERVAL_MAX: '180', + TWITTER_ACTION_INTERVAL: '5', + TWITTER_ENABLE_ACTION_PROCESSING: 'false', + TWITTER_POST_IMMEDIATELY: 'false', + TWITTER_EMAIL: 'test@example.com', + TWITTER_PASSWORD: 'hashedpassword', + TWITTER_2FA_SECRET: '', + TWITTER_POLL_INTERVAL: '120', + TWITTER_RETRY_LIMIT: '5', + ACTION_TIMELINE_TYPE: 'foryou', + MAX_ACTIONS_PROCESSING: '1', + MAX_TWEET_LENGTH: '280' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + } + } as unknown as IAgentRuntime; + + it('should validate correct configuration', async () => { + const config = await validateTwitterConfig(mockRuntime); + expect(config).toBeDefined(); + expect(config.TWITTER_USERNAME).toBe('testuser123'); + expect(config.TWITTER_DRY_RUN).toBe(true); + expect(config.TWITTER_SEARCH_ENABLE).toBe(false); + expect(config.TWITTER_SPACES_ENABLE).toBe(false); + expect(config.TWITTER_TARGET_USERS).toEqual(['user1', 'user2', 'user3']); + expect(config.MAX_TWEET_LENGTH).toBe(280); + expect(config.POST_INTERVAL_MIN).toBe(90); + expect(config.POST_INTERVAL_MAX).toBe(180); + expect(config.ACTION_INTERVAL).toBe(5); + expect(config.ENABLE_ACTION_PROCESSING).toBe(false); + expect(config.POST_IMMEDIATELY).toBe(false); + }); + + it('should validate wildcard username', async () => { + const wildcardRuntime = { + ...mockRuntime, + env: { + ...mockRuntime.env, + TWITTER_USERNAME: '*' + }, + getEnv: function(key: string) { + return this.env[key] || null; + }, + getSetting: function(key: string) { + return this.env[key] || null; + } + } as IAgentRuntime; + + const config = await validateTwitterConfig(wildcardRuntime); + expect(config.TWITTER_USERNAME).toBe('*'); + }); + + it('should validate username with numbers and underscores', async () => { + const validRuntime = { + ...mockRuntime, + env: { + ...mockRuntime.env, + TWITTER_USERNAME: 'test_user_123' + }, + getEnv: function(key: string) { + return this.env[key] || null; + }, + getSetting: function(key: string) { + return this.env[key] || null; + } + } as IAgentRuntime; + + const config = await validateTwitterConfig(validRuntime); + expect(config.TWITTER_USERNAME).toBe('test_user_123'); + }); + + it('should handle empty target users', async () => { + const runtimeWithoutTargets = { + ...mockRuntime, + env: { + ...mockRuntime.env, + TWITTER_TARGET_USERS: '' + }, + getEnv: function(key: string) { + return this.env[key] || null; + }, + getSetting: function(key: string) { + return this.env[key] || null; + } + } as IAgentRuntime; + + const config = await validateTwitterConfig(runtimeWithoutTargets); + expect(config.TWITTER_TARGET_USERS).toHaveLength(0); + }); + + it('should use default values when optional configs are missing', async () => { + const minimalRuntime = { + env: { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: 'true', + TWITTER_EMAIL: 'test@example.com', + TWITTER_PASSWORD: 'hashedpassword', + TWITTER_2FA_SECRET: '', + MAX_TWEET_LENGTH: '280' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + } + } as unknown as IAgentRuntime; + + const config = await validateTwitterConfig(minimalRuntime); + expect(config).toBeDefined(); + expect(config.MAX_TWEET_LENGTH).toBe(280); + expect(config.POST_INTERVAL_MIN).toBe(90); + expect(config.POST_INTERVAL_MAX).toBe(180); + }); +}); diff --git a/packages/client-twitter/__tests__/post.test.ts b/packages/client-twitter/__tests__/post.test.ts new file mode 100644 index 00000000000..7459b68d626 --- /dev/null +++ b/packages/client-twitter/__tests__/post.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, vi } from 'vitest'; +import { TwitterPostClient } from '../src/post'; +import { ClientBase } from '../src/base'; +import { IAgentRuntime } from '@elizaos/core'; +import { TwitterConfig } from '../src/environment'; + +describe('Twitter Post Client', () => { + let mockRuntime: IAgentRuntime; + let mockConfig: TwitterConfig; + let baseClient: ClientBase; + + beforeEach(() => { + mockRuntime = { + env: { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: 'true', + TWITTER_POST_INTERVAL_MIN: '5', + TWITTER_POST_INTERVAL_MAX: '10', + TWITTER_ACTION_INTERVAL: '5', + TWITTER_ENABLE_ACTION_PROCESSING: 'true', + TWITTER_POST_IMMEDIATELY: 'false', + TWITTER_SEARCH_ENABLE: 'false', + TWITTER_EMAIL: 'test@example.com', + TWITTER_PASSWORD: 'hashedpassword', + TWITTER_2FA_SECRET: '', + TWITTER_POLL_INTERVAL: '120', + TWITTER_RETRY_LIMIT: '5', + ACTION_TIMELINE_TYPE: 'foryou', + MAX_ACTIONS_PROCESSING: '1', + MAX_TWEET_LENGTH: '280' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + }, + character: { + style: { + all: ['Test style 1', 'Test style 2'], + post: ['Post style 1', 'Post style 2'] + } + } + } as unknown as IAgentRuntime; + + mockConfig = { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: true, + TWITTER_SEARCH_ENABLE: false, + TWITTER_SPACES_ENABLE: false, + TWITTER_TARGET_USERS: [], + TWITTER_MAX_TWEETS_PER_DAY: 10, + TWITTER_MAX_TWEET_LENGTH: 280, + POST_INTERVAL_MIN: 5, + POST_INTERVAL_MAX: 10, + ACTION_INTERVAL: 5, + ENABLE_ACTION_PROCESSING: true, + POST_IMMEDIATELY: false, + MAX_TWEET_LENGTH: 280 + }; + + baseClient = new ClientBase(mockRuntime, mockConfig); + }); + + it('should create post client instance', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + expect(postClient).toBeDefined(); + expect(postClient.twitterUsername).toBe('testuser'); + expect(postClient['isDryRun']).toBe(true); + }); + + it('should keep tweets under max length when already valid', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + const validTweet = 'This is a valid tweet'; + const result = postClient['trimTweetLength'](validTweet); + expect(result).toBe(validTweet); + expect(result.length).toBeLessThanOrEqual(280); + }); + + it('should cut at last sentence when possible', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + const longTweet = 'First sentence. Second sentence that is quite long. Third sentence that would make it too long.'; + const result = postClient['trimTweetLength'](longTweet); + const lastPeriod = result.lastIndexOf('.'); + expect(lastPeriod).toBeGreaterThan(0); + expect(result.length).toBeLessThanOrEqual(280); + }); + + it('should add ellipsis when cutting within a sentence', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + const longSentence = 'This is an extremely long sentence without any periods that needs to be truncated because it exceeds the maximum allowed length for a tweet on the Twitter platform and therefore must be shortened'; + const result = postClient['trimTweetLength'](longSentence); + const lastSpace = result.lastIndexOf(' '); + expect(lastSpace).toBeGreaterThan(0); + expect(result.length).toBeLessThanOrEqual(280); + }); +}); diff --git a/packages/client-twitter/package.json b/packages/client-twitter/package.json index 2dc3a3543ef..cc66c678a28 100644 --- a/packages/client-twitter/package.json +++ b/packages/client-twitter/package.json @@ -25,12 +25,16 @@ "zod": "3.23.8" }, "devDependencies": { - "tsup": "8.3.5" + "tsup": "8.3.5", + "vitest": "1.1.3", + "@vitest/coverage-v8": "1.1.3" }, "scripts": { "build": "tsup --format esm --dts", "dev": "tsup --format esm --dts --watch", - "lint": "eslint --fix --cache ." + "lint": "eslint --fix --cache .", + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "peerDependencies": { "whatwg-url": "7.1.0" diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index 34aba569576..5ad8b67039f 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -318,7 +318,7 @@ export class ClientBase extends EventEmitter { return processedTimeline; } - async fetchTimelineForActions(): Promise { + async fetchTimelineForActions(count: number): Promise { elizaLogger.debug("fetching timeline for actions"); const agentUsername = this.twitterConfig.TWITTER_USERNAME; @@ -326,8 +326,8 @@ export class ClientBase extends EventEmitter { const homeTimeline = this.twitterConfig.ACTION_TIMELINE_TYPE === ActionTimelineType.Following - ? await this.twitterClient.fetchFollowingTimeline(20, []) - : await this.twitterClient.fetchHomeTimeline(20, []); + ? await this.twitterClient.fetchFollowingTimeline(count, []) + : await this.twitterClient.fetchHomeTimeline(count, []); return homeTimeline .map((tweet) => ({ @@ -357,7 +357,11 @@ export class ClientBase extends EventEmitter { (media) => media.type === "video" ) || [], })) - .filter((tweet) => tweet.username !== agentUsername); // do not perform action on self-tweets + .filter((tweet) => tweet.username !== agentUsername) // do not perform action on self-tweets + .slice(0, count); + // TODO: Once the 'count' parameter is fixed in the 'fetchTimeline' method of the 'agent-twitter-client', + // this workaround can be removed. + // Related issue: https://github.com/elizaOS/agent-twitter-client/issues/43 } async fetchSearchTweets( diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 535b1765189..02ecdda01fa 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -6,7 +6,8 @@ import { IAgentRuntime, ModelClass, stringToUuid, - UUID, + TemplateType, + UUID } from "@elizaos/core"; import { elizaLogger } from "@elizaos/core"; import { ClientBase } from "./base.ts"; @@ -19,6 +20,8 @@ import { DEFAULT_MAX_TWEET_LENGTH } from "./environment.ts"; import { State } from "@elizaos/core"; import { ActionResponse } from "@elizaos/core"; +const MAX_TIMELINES_TO_FETCH = 15; + const twitterPostTemplate = ` # Areas of Expertise {{knowledge}} @@ -531,7 +534,7 @@ export class TwitterPostClient { private async generateTweetContent( tweetState: any, options?: { - template?: string; + template?: TemplateType; context?: string; } ): Promise { @@ -627,10 +630,9 @@ export class TwitterPostClient { "twitter" ); - // TODO: Once the 'count' parameter is fixed in the 'fetchTimeline' method of the 'agent-twitter-client', - // we should enable the ability to control the number of items fetched here. - // Related issue: https://github.com/elizaOS/agent-twitter-client/issues/43 - const homeTimeline = await this.client.fetchTimelineForActions(); + const homeTimeline = await this.client.fetchTimelineForActions( + MAX_TIMELINES_TO_FETCH + ); const maxActionsProcessing = this.client.twitterConfig.MAX_ACTIONS_PROCESSING; const processedTimelines = []; diff --git a/packages/client-twitter/src/utils.ts b/packages/client-twitter/src/utils.ts index 26ba07c3e07..d11ed5b534f 100644 --- a/packages/client-twitter/src/utils.ts +++ b/packages/client-twitter/src/utils.ts @@ -321,11 +321,31 @@ function splitTweetContent(content: string, maxLength: number): string[] { return tweets; } -function splitParagraph(paragraph: string, maxLength: number): string[] { - // eslint-disable-next-line - const sentences = paragraph.match(/[^\.!\?]+[\.!\?]+|[^\.!\?]+$/g) || [ - paragraph, - ]; +function extractUrls(paragraph: string): { + textWithPlaceholders: string; + placeholderMap: Map; +} { + // replace https urls with placeholder + const urlRegex = /https?:\/\/[^\s]+/g; + const placeholderMap = new Map(); + + let urlIndex = 0; + const textWithPlaceholders = paragraph.replace(urlRegex, (match) => { + // twitter url would be considered as 23 characters + // <> is also 23 characters + const placeholder = `<>`; // Placeholder without . ? ! etc + placeholderMap.set(placeholder, match); + urlIndex++; + return placeholder; + }); + + return { textWithPlaceholders, placeholderMap }; +} + +function splitSentencesAndWords(text: string, maxLength: number): string[] { + // Split by periods, question marks and exclamation marks + // Note that URLs in text have been replaced with `<>` and won't be split by dots + const sentences = text.match(/[^\.!\?]+[\.!\?]+|[^\.!\?]+$/g) || [text]; const chunks: string[] = []; let currentChunk = ""; @@ -337,13 +357,16 @@ function splitParagraph(paragraph: string, maxLength: number): string[] { currentChunk = sentence; } } else { + // Can't fit more, push currentChunk to results if (currentChunk) { chunks.push(currentChunk.trim()); } + + // If current sentence itself is less than or equal to maxLength if (sentence.length <= maxLength) { currentChunk = sentence; } else { - // Split long sentence into smaller pieces + // Need to split sentence by spaces const words = sentence.split(" "); currentChunk = ""; for (const word of words) { @@ -366,9 +389,39 @@ function splitParagraph(paragraph: string, maxLength: number): string[] { } } + // Handle remaining content if (currentChunk) { chunks.push(currentChunk.trim()); } return chunks; } + +function restoreUrls( + chunks: string[], + placeholderMap: Map +): string[] { + return chunks.map((chunk) => { + // Replace all <> in chunk back to original URLs using regex + return chunk.replace(/<>/g, (match) => { + const original = placeholderMap.get(match); + return original || match; // Return placeholder if not found (theoretically won't happen) + }); + }); +} + +function splitParagraph(paragraph: string, maxLength: number): string[] { + // 1) Extract URLs and replace with placeholders + const { textWithPlaceholders, placeholderMap } = extractUrls(paragraph); + + // 2) Use first section's logic to split by sentences first, then do secondary split + const splittedChunks = splitSentencesAndWords( + textWithPlaceholders, + maxLength + ); + + // 3) Replace placeholders back to original URLs + const restoredChunks = restoreUrls(splittedChunks, placeholderMap); + + return restoredChunks; +} \ No newline at end of file diff --git a/packages/client-twitter/vitest.config.ts b/packages/client-twitter/vitest.config.ts new file mode 100644 index 00000000000..2e60e80f5dc --- /dev/null +++ b/packages/client-twitter/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['__tests__/**/*.test.ts'], + coverage: { + reporter: ['text', 'json', 'html'], + }, + }, +}); diff --git a/packages/core/.env.test b/packages/core/.env.test index d295bdfb3b4..ef68e98847c 100644 --- a/packages/core/.env.test +++ b/packages/core/.env.test @@ -2,5 +2,5 @@ TEST_DATABASE_CLIENT=sqlite NODE_ENV=test MAIN_WALLET_ADDRESS=TEST_MAIN_WALLET_ADDRESS_VALUE OPENAI_API_KEY=TEST_OPENAI_API_KEY_VALUE -RPC_URL=https://api.mainnet-beta.solana.com +SOLANA_RPC_URL=https://api.mainnet-beta.solana.com WALLET_PUBLIC_KEY=2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh \ No newline at end of file diff --git a/packages/core/generation.ts b/packages/core/generation.ts index 583e6787936..74d41237738 100644 --- a/packages/core/generation.ts +++ b/packages/core/generation.ts @@ -355,7 +355,7 @@ export async function generateText({ const fetching = await runtime.fetch(url, options); if ( parseBooleanFromText( - runtime.getSetting("ETERNAL_AI_LOG_REQUEST") + runtime.getSetting("ETERNALAI_LOG") ) ) { elizaLogger.info( diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index a682e6794c8..059e302d625 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -1,5 +1,5 @@ import handlebars from "handlebars"; -import { type State } from "./types.ts"; +import { type State, type TemplateType } from "./types.ts"; import { names, uniqueNamesGenerator } from "unique-names-generator"; /** @@ -13,7 +13,7 @@ import { names, uniqueNamesGenerator } from "unique-names-generator"; * * @param {Object} params - The parameters for composing the context. * @param {State} params.state - The state object containing values to replace the placeholders in the template. - * @param {string} params.template - The template string containing placeholders to be replaced with state values. + * @param {TemplateType} params.template - The template string or function containing placeholders to be replaced with state values. * @param {"handlebars" | undefined} [params.templatingEngine] - The templating engine to use for compiling and evaluating the template (optional, default: `undefined`). * @returns {string} The composed context string with placeholders replaced by corresponding state values. * @@ -25,23 +25,34 @@ import { names, uniqueNamesGenerator } from "unique-names-generator"; * // Composing the context with simple string replacement will result in: * // "Hello, Alice! You are 30 years old." * const contextSimple = composeContext({ state, template }); + * + * // Using composeContext with a template function for dynamic template + * const template = ({ state }) => { + * const tone = Math.random() > 0.5 ? "kind" : "rude"; + * return `Hello, {{userName}}! You are {{userAge}} years old. Be ${tone}`; + * }; + * const contextSimple = composeContext({ state, template }); */ + export const composeContext = ({ state, template, templatingEngine, }: { state: State; - template: string; + template: TemplateType; templatingEngine?: "handlebars"; }) => { + const templateStr = + typeof template === "function" ? template({ state }) : template; + if (templatingEngine === "handlebars") { - const templateFunction = handlebars.compile(template); + const templateFunction = handlebars.compile(templateStr); return templateFunction(state); } // @ts-expect-error match isn't working as expected - const out = template.replace(/{{\w+}}/g, (match) => { + const out = templateStr.replace(/{{\w+}}/g, (match) => { const key = match.replace(/{{|}}/g, ""); return state[key] ?? ""; }); diff --git a/packages/core/src/environment.ts b/packages/core/src/environment.ts index 485a1e9d93c..bcc0c87ff7f 100644 --- a/packages/core/src/environment.ts +++ b/packages/core/src/environment.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { ModelProviderName, Clients } from "./types"; +import elizaLogger from "./logger"; // TODO: TO COMPLETE export const envSchema = z.object({ @@ -137,11 +138,26 @@ export function validateCharacterConfig(json: unknown): CharacterConfig { return CharacterSchema.parse(json); } catch (error) { if (error instanceof z.ZodError) { - const errorMessages = error.errors - .map((err) => `${err.path.join(".")}: ${err.message}`) - .join("\n"); + const groupedErrors = error.errors.reduce( + (acc, err) => { + const path = err.path.join("."); + if (!acc[path]) { + acc[path] = []; + } + acc[path].push(err.message); + return acc; + }, + {} as Record + ); + + Object.entries(groupedErrors).forEach(([field, messages]) => { + elizaLogger.error( + `Validation errors in ${field}: ${messages.join(" - ")}` + ); + }); + throw new Error( - `Character configuration validation failed:\n${errorMessages}` + "Character configuration validation failed. Check logs for details." ); } throw error; diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index 8d2397aa7fd..78dd2a94cf7 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -389,10 +389,16 @@ export async function generateText({ apiKey, baseURL: endpoint, fetch: async (url: string, options: any) => { + const chain_id = runtime.getSetting("ETERNALAI_CHAIN_ID") || "45762" + if (options?.body) { + const body = JSON.parse(options.body); + body.chain_id = chain_id; + options.body = JSON.stringify(body); + } const fetching = await runtime.fetch(url, options); if ( parseBooleanFromText( - runtime.getSetting("ETERNAL_AI_LOG_REQUEST") + runtime.getSetting("ETERNALAI_LOG") ) ) { elizaLogger.info( @@ -400,12 +406,16 @@ export async function generateText({ JSON.stringify(options, null, 2) ); const clonedResponse = fetching.clone(); - clonedResponse.json().then((data) => { - elizaLogger.info( - "Response data: ", - JSON.stringify(data, null, 2) - ); - }); + try { + clonedResponse.json().then((data) => { + elizaLogger.info( + "Response data: ", + JSON.stringify(data, null, 2) + ); + }); + } catch (e) { + elizaLogger.debug(e); + } } return fetching; }, diff --git a/packages/core/src/test_resources/createRuntime.ts b/packages/core/src/test_resources/createRuntime.ts index 668fa47b5b2..ffc3b5f4413 100644 --- a/packages/core/src/test_resources/createRuntime.ts +++ b/packages/core/src/test_resources/createRuntime.ts @@ -4,6 +4,7 @@ import { } from "@elizaos/adapter-sqlite"; import { SqlJsDatabaseAdapter } from "@elizaos/adapter-sqljs"; import { SupabaseDatabaseAdapter } from "@elizaos/adapter-supabase"; +import { PGLiteDatabaseAdapter } from "@elizaos/adapter-pglite"; import { DatabaseAdapter } from "../database.ts"; import { getEndpoint } from "../models.ts"; import { AgentRuntime } from "../runtime.ts"; @@ -117,6 +118,23 @@ export async function createRuntime({ ); break; } + case "pglite": + { + // Import the PGLite adapter + await import("@electric-sql/pglite"); + + // PGLite adapter + adapter = new PGLiteDatabaseAdapter({ dataDir: "../pglite" }); + + // Create a test user and session + session = { + user: { + id: zeroUuid, + email: "test@example.com", + }, + }; + } + break; case "sqlite": default: { diff --git a/packages/core/src/tests/context.test.ts b/packages/core/src/tests/context.test.ts index afbaa1c4643..3c3bc978f9c 100644 --- a/packages/core/src/tests/context.test.ts +++ b/packages/core/src/tests/context.test.ts @@ -70,6 +70,96 @@ describe("composeContext", () => { }); }); + describe("dynamic templates", () => { + it("should handle function templates", () => { + const state: State = { + ...baseState, + userName: "Alice", + userAge: 30, + }; + const template = () => { + return "Hello, {{userName}}! You are {{userAge}} years old."; + }; + + const result = composeContext({ state, template }); + + expect(result).toBe("Hello, Alice! You are 30 years old."); + }); + + it("should handle function templates with conditional logic", () => { + const state: State = { + ...baseState, + userName: "Alice", + userAge: 30, + }; + const isEdgy = true; + const template = () => { + if (isEdgy) { + return "Hello, {{userName}}! You are {{userAge}} years old... whatever"; + } + + return `Hello, {{userName}}! You are {{userAge}} years old`; + }; + + const result = composeContext({ state, template }); + + expect(result).toBe( + "Hello, Alice! You are 30 years old... whatever" + ); + }); + + it("should handle function templates with conditional logic depending on state", () => { + const template = ({ state }: { state: State }) => { + if (state.userName) { + return `Hello, {{userName}}! You are {{userAge}} years old.`; + } + + return `Hello, anon! You are {{userAge}} years old.`; + }; + + const result = composeContext({ + state: { + ...baseState, + userName: "Alice", + userAge: 30, + }, + template, + }); + + const resultWithoutUsername = composeContext({ + state: { + ...baseState, + userAge: 30, + }, + template, + }); + + expect(result).toBe("Hello, Alice! You are 30 years old."); + expect(resultWithoutUsername).toBe( + "Hello, anon! You are 30 years old." + ); + }); + + it("should handle function templates with handlebars templating engine", () => { + const state: State = { + ...baseState, + userName: "Alice", + userAge: 30, + }; + const template = () => { + return `{{#if userAge}}Hello, {{userName}}!{{else}}Hi there!{{/if}}`; + }; + + const result = composeContext({ + state, + template, + templatingEngine: "handlebars", + }); + + expect(result).toBe("Hello, Alice!"); + }); + }); + // Test Handlebars templating describe("handlebars templating", () => { it("should process basic handlebars template", () => { diff --git a/packages/core/src/tests/embedding.test.ts b/packages/core/src/tests/embedding.test.ts new file mode 100644 index 00000000000..3d83135dd28 --- /dev/null +++ b/packages/core/src/tests/embedding.test.ts @@ -0,0 +1,201 @@ +import { describe, test, expect, vi, beforeEach } from "vitest"; +import { + embed, + getEmbeddingConfig, + getEmbeddingType, + getEmbeddingZeroVector, +} from "../embedding.ts"; +import { IAgentRuntime, ModelProviderName } from "../types.ts"; +import settings from "../settings.ts"; + +// Mock environment-related settings +vi.mock("../settings", () => ({ + default: { + USE_OPENAI_EMBEDDING: "false", + USE_OLLAMA_EMBEDDING: "false", + USE_GAIANET_EMBEDDING: "false", + OPENAI_API_KEY: "mock-openai-key", + OPENAI_API_URL: "https://api.openai.com/v1", + GAIANET_API_KEY: "mock-gaianet-key", + OLLAMA_EMBEDDING_MODEL: "mxbai-embed-large", + GAIANET_EMBEDDING_MODEL: "nomic-embed", + }, +})); + +// Mock fastembed module for local embeddings +vi.mock("fastembed", () => ({ + FlagEmbedding: { + init: vi.fn().mockResolvedValue({ + queryEmbed: vi + .fn() + .mockResolvedValue(new Float32Array(384).fill(0.1)), + }), + }, + EmbeddingModel: { + BGESmallENV15: "BGE-small-en-v1.5", + }, +})); + +// Mock global fetch for remote embedding requests +const mockFetch = vi.fn(); +(global as any).fetch = mockFetch; + +describe("Embedding Module", () => { + let mockRuntime: IAgentRuntime; + + beforeEach(() => { + // Prepare a mock runtime + mockRuntime = { + character: { + modelProvider: ModelProviderName.OLLAMA, + modelEndpointOverride: null, + }, + token: "mock-token", + messageManager: { + getCachedEmbeddings: vi.fn().mockResolvedValue([]), + }, + } as unknown as IAgentRuntime; + + vi.clearAllMocks(); + mockFetch.mockReset(); + }); + + describe("getEmbeddingConfig", () => { + test("should return BGE config by default", () => { + const config = getEmbeddingConfig(); + expect(config.dimensions).toBe(384); + expect(config.model).toBe("BGE-small-en-v1.5"); + expect(config.provider).toBe("BGE"); + }); + + test("should return OpenAI config when USE_OPENAI_EMBEDDING is true", () => { + vi.mocked(settings).USE_OPENAI_EMBEDDING = "true"; + const config = getEmbeddingConfig(); + expect(config.dimensions).toBe(1536); + expect(config.model).toBe("text-embedding-3-small"); + expect(config.provider).toBe("OpenAI"); + }); + }); + + describe("getEmbeddingType", () => { + test("should return 'remote' for Ollama provider", () => { + const type = getEmbeddingType(mockRuntime); + expect(type).toBe("remote"); + }); + + test("should return 'remote' for OpenAI provider", () => { + mockRuntime.character.modelProvider = ModelProviderName.OPENAI; + const type = getEmbeddingType(mockRuntime); + expect(type).toBe("remote"); + }); + }); + + describe("getEmbeddingZeroVector", () => { + beforeEach(() => { + vi.mocked(settings).USE_OPENAI_EMBEDDING = "false"; + vi.mocked(settings).USE_OLLAMA_EMBEDDING = "false"; + vi.mocked(settings).USE_GAIANET_EMBEDDING = "false"; + }); + + test("should return 384-length zero vector by default (BGE)", () => { + const vector = getEmbeddingZeroVector(); + expect(vector).toHaveLength(384); + expect(vector.every((val) => val === 0)).toBe(true); + }); + + test("should return 1536-length zero vector for OpenAI if enabled", () => { + vi.mocked(settings).USE_OPENAI_EMBEDDING = "true"; + const vector = getEmbeddingZeroVector(); + expect(vector).toHaveLength(1536); + expect(vector.every((val) => val === 0)).toBe(true); + }); + }); + + describe("embed function", () => { + beforeEach(() => { + // Mock a successful remote response with an example 384-dim embedding + mockFetch.mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + data: [{ embedding: new Array(384).fill(0.1) }], + }), + }); + }); + + test("should return an empty array for empty input text", async () => { + const result = await embed(mockRuntime, ""); + expect(result).toEqual([]); + }); + + test("should return cached embedding if it already exists", async () => { + const cachedEmbedding = new Array(384).fill(0.5); + mockRuntime.messageManager.getCachedEmbeddings = vi + .fn() + .mockResolvedValue([{ embedding: cachedEmbedding }]); + + const result = await embed(mockRuntime, "test input"); + expect(result).toBe(cachedEmbedding); + }); + + test("should handle local embedding successfully (fastembed fallback)", async () => { + // By default, it tries local first if in Node. + // Then uses the mock fastembed response above. + const result = await embed(mockRuntime, "test input"); + expect(result).toHaveLength(384); + expect(result.every((v) => typeof v === "number")).toBe(true); + }); + + test("should fallback to remote if local embedding fails", async () => { + // Force fastembed import to fail + vi.mock("fastembed", () => { + throw new Error("Module not found"); + }); + + // Mock a valid remote response + const mockResponse = { + ok: true, + json: () => + Promise.resolve({ + data: [{ embedding: new Array(384).fill(0.1) }], + }), + }; + mockFetch.mockResolvedValueOnce(mockResponse); + + const result = await embed(mockRuntime, "test input"); + expect(result).toHaveLength(384); + expect(mockFetch).toHaveBeenCalled(); + }); + + test("should throw on remote embedding if fetch fails", async () => { + mockFetch.mockRejectedValueOnce(new Error("API Error")); + vi.mocked(settings).USE_OPENAI_EMBEDDING = "true"; // Force remote + + await expect(embed(mockRuntime, "test input")).rejects.toThrow( + "API Error" + ); + }); + + test("should throw on non-200 remote response", async () => { + const errorResponse = { + ok: false, + status: 400, + statusText: "Bad Request", + text: () => Promise.resolve("Invalid input"), + }; + mockFetch.mockResolvedValueOnce(errorResponse); + vi.mocked(settings).USE_OPENAI_EMBEDDING = "true"; // Force remote + + await expect(embed(mockRuntime, "test input")).rejects.toThrow( + "Embedding API Error" + ); + }); + + test("should handle concurrent embedding requests", async () => { + const promises = Array(5) + .fill(null) + .map(() => embed(mockRuntime, "concurrent test")); + await expect(Promise.all(promises)).resolves.toBeDefined(); + }); + }); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 548dcdc583e..e07cb74e67c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -678,6 +678,8 @@ export interface ModelConfiguration { experimental_telemetry?: TelemetrySettings; } +export type TemplateType = string | ((options: { state: State }) => string); + /** * Configuration for an agent character */ @@ -708,30 +710,30 @@ export type Character = { /** Optional prompt templates */ templates?: { - goalsTemplate?: string; - factsTemplate?: string; - messageHandlerTemplate?: string; - shouldRespondTemplate?: string; - continueMessageHandlerTemplate?: string; - evaluationTemplate?: string; - twitterSearchTemplate?: string; - twitterActionTemplate?: string; - twitterPostTemplate?: string; - twitterMessageHandlerTemplate?: string; - twitterShouldRespondTemplate?: string; - farcasterPostTemplate?: string; - lensPostTemplate?: string; - farcasterMessageHandlerTemplate?: string; - lensMessageHandlerTemplate?: string; - farcasterShouldRespondTemplate?: string; - lensShouldRespondTemplate?: string; - telegramMessageHandlerTemplate?: string; - telegramShouldRespondTemplate?: string; - discordVoiceHandlerTemplate?: string; - discordShouldRespondTemplate?: string; - discordMessageHandlerTemplate?: string; - slackMessageHandlerTemplate?: string; - slackShouldRespondTemplate?: string; + goalsTemplate?: TemplateType; + factsTemplate?: TemplateType; + messageHandlerTemplate?: TemplateType; + shouldRespondTemplate?: TemplateType; + continueMessageHandlerTemplate?: TemplateType; + evaluationTemplate?: TemplateType; + twitterSearchTemplate?: TemplateType; + twitterActionTemplate?: TemplateType; + twitterPostTemplate?: TemplateType; + twitterMessageHandlerTemplate?: TemplateType; + twitterShouldRespondTemplate?: TemplateType; + farcasterPostTemplate?: TemplateType; + lensPostTemplate?: TemplateType; + farcasterMessageHandlerTemplate?: TemplateType; + lensMessageHandlerTemplate?: TemplateType; + farcasterShouldRespondTemplate?: TemplateType; + lensShouldRespondTemplate?: TemplateType; + telegramMessageHandlerTemplate?: TemplateType; + telegramShouldRespondTemplate?: TemplateType; + discordVoiceHandlerTemplate?: TemplateType; + discordShouldRespondTemplate?: TemplateType; + discordMessageHandlerTemplate?: TemplateType; + slackMessageHandlerTemplate?: TemplateType; + slackShouldRespondTemplate?: TemplateType; }; /** Character biography */ @@ -1309,6 +1311,7 @@ export enum ServiceType { AWS_S3 = "aws_s3", BUTTPLUG = "buttplug", SLACK = "slack", + GOPLUS_SECURITY = "goplus_security", } export enum LoggingLevel { diff --git a/packages/plugin-abstract/README.md b/packages/plugin-abstract/README.md index 3bba4bf230b..865ec94ead2 100644 --- a/packages/plugin-abstract/README.md +++ b/packages/plugin-abstract/README.md @@ -113,25 +113,7 @@ pnpm run dev - Account abstraction improvements - Social recovery options -2. **CosmWasm Integration** - - - Contract deployment templates - - Smart contract verification tools - - Contract upgrade system - - Testing framework improvements - - Gas optimization tools - - Contract interaction templates - -3. **IBC Operations** - - - Cross-chain transfer optimization - - IBC relayer monitoring - - Channel management tools - - Packet tracking system - - Timeout handling improvements - - Cross-chain messaging - -4. **DEX Integration** +2. **DEX Integration** - Advanced swap routing - Liquidity pool management @@ -140,7 +122,7 @@ pnpm run dev - Slippage protection - AMM optimization -5. **Security Enhancements** +3. **Security Enhancements** - Transaction simulation - Risk assessment tools @@ -149,7 +131,7 @@ pnpm run dev - Emergency shutdown features - Audit integration tools -6. **Developer Tools** +4. **Developer Tools** - Enhanced debugging capabilities - Documentation generator @@ -158,7 +140,7 @@ pnpm run dev - Deployment automation - Performance profiling -7. **Analytics and Monitoring** +5. **Analytics and Monitoring** - Transaction tracking dashboard - Network statistics @@ -167,7 +149,7 @@ pnpm run dev - Custom reporting tools - Real-time monitoring -8. **Wallet Management** +6. **Wallet Management** - Multiple wallet support - Hardware wallet integration - Address book features @@ -185,27 +167,8 @@ Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) fil This plugin integrates with and builds upon several key technologies: -- [Abstract](https://abstract.money/): Smart account infrastructure -- [CosmWasm](https://cosmwasm.com/): Smart contract platform -- [Cosmos SDK](https://v1.cosmos.network/sdk): Blockchain application framework -- [IBC Protocol](https://ibcprotocol.org/): Inter-blockchain communication -- [Osmosis](https://osmosis.zone/): DEX infrastructure - -Special thanks to: - -- The Abstract development team -- The CosmWasm core developers -- The Cosmos SDK maintainers -- The IBC Protocol team -- The Osmosis DEX team -- The Eliza community for their contributions and feedback - -For more information about Abstract capabilities: - -- [Abstract Documentation](https://docs.abstract.money/) -- [CosmWasm Documentation](https://docs.cosmwasm.com/) -- [Cosmos SDK Docs](https://docs.cosmos.network/) -- [IBC Protocol Docs](https://ibc.cosmos.network/) +- [Abstract](https://abs.xyz/): Consumer blockchain +- [viem](https://viem.sh/): Typescript web3 client ## License diff --git a/packages/plugin-arthera/README.md b/packages/plugin-arthera/README.md new file mode 100644 index 00000000000..b634635d469 --- /dev/null +++ b/packages/plugin-arthera/README.md @@ -0,0 +1,68 @@ +# `@elizaos/plugin-arthera` + +This plugin provides actions and providers for interacting with Arthera. + +--- + +## Configuration + +### Default Setup + +By default, **Arthera** is enabled. To use it, simply add your private key to the `.env` file: + +```env +ARTHERA_PRIVATE_KEY=your-private-key-here +``` + +### Custom RPC URLs + +By default, the RPC URL is inferred from the `viem/chains` config. To use a custom RPC URL for a specific chain, add the following to your `.env` file: + +```env +ETHEREUM_PROVIDER_=https://your-custom-rpc-url +``` + +**Example usage:** + +```env +ETHEREUM_PROVIDER_ARTHERA=https://rpc.arthera.net +``` + +## Provider + +The **Wallet Provider** initializes with Arthera. It: + +- Provides the **context** of the currently connected address and its balance. +- Creates **Public** and **Wallet clients** to interact with the supported chain. + +--- + +## Actions + +### Transfer + +Transfer tokens from one address to another on Arthera. Just specify the: + +- **Amount** +- **Chain** +- **Recipient Address** + +**Example usage:** + +```bash +Transfer 1 AA to 0xRecipient on arthera. +``` + +--- + +## Contribution + +The plugin contains tests. Whether you're using **TDD** or not, please make sure to run the tests before submitting a PR. + +### Running Tests + +Navigate to the `plugin-arthera` directory and run: + +```bash +pnpm test +``` diff --git a/packages/plugin-arthera/eslint.config.mjs b/packages/plugin-arthera/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-arthera/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-arthera/package.json b/packages/plugin-arthera/package.json new file mode 100644 index 00000000000..db58990809f --- /dev/null +++ b/packages/plugin-arthera/package.json @@ -0,0 +1,24 @@ +{ + "name": "@elizaos/plugin-arthera", + "version": "0.1.8-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5", + "viem": "2.21.58" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run", + "lint": "eslint --fix --cache ." + }, + "devDependencies": { + "whatwg-url": "7.1.0" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-arthera/src/actions/transfer.ts b/packages/plugin-arthera/src/actions/transfer.ts new file mode 100644 index 00000000000..2ad25281de5 --- /dev/null +++ b/packages/plugin-arthera/src/actions/transfer.ts @@ -0,0 +1,176 @@ +import { ByteArray, formatEther, parseEther, type Hex } from "viem"; +import { + composeContext, + generateObjectDeprecated, + HandlerCallback, + ModelClass, + type IAgentRuntime, + type Memory, + type State, +} from "@elizaos/core"; + +import { initWalletProvider, WalletProvider } from "../providers/wallet"; +import type { Transaction, TransferParams } from "../types"; +import { transferTemplate } from "../templates"; + +export { transferTemplate }; + +// Exported for tests +export class TransferAction { + constructor(private walletProvider: WalletProvider) {} + + async transfer(params: TransferParams): Promise { + const walletClient = this.walletProvider.getWalletClient( + params.fromChain + ); + + console.log( + `Transferring: ${params.amount} tokens from (${walletClient.account.address} to (${params.toAddress} on ${params.fromChain})` + ); + + if (!params.data) { + params.data = "0x"; + } + + try { + const hash = await walletClient.sendTransaction({ + account: walletClient.account, + to: params.toAddress, + value: parseEther(params.amount), + data: params.data as Hex, + kzg: { + blobToKzgCommitment: function (_: ByteArray): ByteArray { + throw new Error("Function not implemented."); + }, + computeBlobKzgProof: function ( + _blob: ByteArray, + _commitment: ByteArray + ): ByteArray { + throw new Error("Function not implemented."); + }, + }, + chain: undefined, + }); + + return { + hash, + from: walletClient.account.address, + to: params.toAddress, + value: parseEther(params.amount), + data: params.data as Hex, + }; + } catch (error) { + throw new Error(`Transfer failed: ${error.message}`); + } + } +} + +const buildTransferDetails = async ( + state: State, + runtime: IAgentRuntime, + wp: WalletProvider +): Promise => { + const context = composeContext({ + state, + template: transferTemplate, + }); + + const chains = Object.keys(wp.chains); + + const contextWithChains = context.replace( + "SUPPORTED_CHAINS", + chains.map((item) => `"${item}"`).join("|") + ); + + const transferDetails = (await generateObjectDeprecated({ + runtime, + context: contextWithChains, + modelClass: ModelClass.SMALL, + })) as TransferParams; + + const existingChain = wp.chains[transferDetails.fromChain]; + + if (!existingChain) { + throw new Error( + "The chain " + + transferDetails.fromChain + + " not configured yet. Add the chain or choose one from configured: " + + chains.toString() + ); + } + + return transferDetails; +}; + +export const transferAction = { + name: "transfer", + description: "Transfer tokens between addresses on the same chain", + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ) => { + console.log("Transfer action handler called"); + const walletProvider = initWalletProvider(runtime); + const action = new TransferAction(walletProvider); + + // Compose transfer context + const paramOptions = await buildTransferDetails( + state, + runtime, + walletProvider + ); + + try { + const transferResp = await action.transfer(paramOptions); + if (callback) { + callback({ + text: `Successfully transferred ${paramOptions.amount} tokens to ${paramOptions.toAddress}\nTransaction Hash: ${transferResp.hash}`, + content: { + success: true, + hash: transferResp.hash, + amount: formatEther(transferResp.value), + recipient: transferResp.to, + chain: paramOptions.fromChain, + }, + }); + } + return true; + } catch (error) { + console.error("Error during token transfer:", error); + if (callback) { + callback({ + text: `Error transferring tokens: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + template: transferTemplate, + validate: async (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("ARTHERA_PRIVATE_KEY"); + return typeof privateKey === "string" && privateKey.startsWith("0x"); + }, + examples: [ + [ + { + user: "assistant", + content: { + text: "I'll help you transfer 1 AA to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + action: "SEND_TOKENS", + }, + }, + { + user: "user", + content: { + text: "Transfer 1 AA to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + action: "SEND_TOKENS", + }, + }, + ], + ], + similes: ["SEND_TOKENS", "TOKEN_TRANSFER", "MOVE_TOKENS"], +}; diff --git a/packages/plugin-arthera/src/index.ts b/packages/plugin-arthera/src/index.ts new file mode 100644 index 00000000000..3fe8d585945 --- /dev/null +++ b/packages/plugin-arthera/src/index.ts @@ -0,0 +1,18 @@ +export * from "./actions/transfer"; +export * from "./providers/wallet"; +export * from "./types"; + +import type { Plugin } from "@elizaos/core"; +import { transferAction } from "./actions/transfer"; +import { artheraWalletProvider } from "./providers/wallet"; + +export const artheraPlugin: Plugin = { + name: "arthera", + description: "Arthera blockchain integration plugin", + providers: [artheraWalletProvider], + evaluators: [], + services: [], + actions: [transferAction], +}; + +export default artheraPlugin; diff --git a/packages/plugin-arthera/src/providers/wallet.ts b/packages/plugin-arthera/src/providers/wallet.ts new file mode 100644 index 00000000000..7d724fbf4a7 --- /dev/null +++ b/packages/plugin-arthera/src/providers/wallet.ts @@ -0,0 +1,203 @@ +import { + createPublicClient, + createWalletClient, + formatUnits, + http, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import type { IAgentRuntime, Provider, Memory, State } from "@elizaos/core"; +import type { + Address, + WalletClient, + PublicClient, + Chain, + HttpTransport, + Account, + PrivateKeyAccount, +} from "viem"; +import * as viemChains from "viem/chains"; + +import type { SupportedChain } from "../types"; + +export class WalletProvider { + private currentChain: SupportedChain = "arthera"; + chains: Record = { arthera: viemChains.arthera }; + account: PrivateKeyAccount; + + constructor(privateKey: `0x${string}`, chains?: Record) { + this.setAccount(privateKey); + this.setChains(chains); + + if (chains && Object.keys(chains).length > 0) { + this.setCurrentChain(Object.keys(chains)[0] as SupportedChain); + } + } + + getAddress(): Address { + return this.account.address; + } + + getCurrentChain(): Chain { + return this.chains[this.currentChain]; + } + + getPublicClient( + chainName: SupportedChain + ): PublicClient { + const transport = this.createHttpTransport(chainName); + + const publicClient = createPublicClient({ + chain: this.chains[chainName], + transport, + }); + return publicClient; + } + + getWalletClient(chainName: SupportedChain): WalletClient { + const transport = this.createHttpTransport(chainName); + + const walletClient = createWalletClient({ + chain: this.chains[chainName], + transport, + account: this.account, + }); + + return walletClient; + } + + getChainConfigs(chainName: SupportedChain): Chain { + const chain = viemChains[chainName]; + + if (!chain?.id) { + throw new Error("Invalid chain name"); + } + + return chain; + } + + async getWalletBalance(): Promise { + try { + const client = this.getPublicClient(this.currentChain); + const balance = await client.getBalance({ + address: this.account.address, + }); + return formatUnits(balance, 18); + } catch (error) { + console.error("Error getting wallet balance:", error); + return null; + } + } + + async getWalletBalanceForChain( + chainName: SupportedChain + ): Promise { + try { + const client = this.getPublicClient(chainName); + const balance = await client.getBalance({ + address: this.account.address, + }); + return formatUnits(balance, 18); + } catch (error) { + console.error("Error getting wallet balance:", error); + return null; + } + } + + private setAccount = (pk: `0x${string}`) => { + this.account = privateKeyToAccount(pk); + }; + + private setChains = (chains?: Record) => { + if (!chains) { + return; + } + Object.keys(chains).forEach((chain: string) => { + this.chains[chain] = chains[chain]; + }); + }; + + private setCurrentChain = (chain: SupportedChain) => { + this.currentChain = chain; + }; + + private createHttpTransport = (chainName: SupportedChain) => { + const chain = this.chains[chainName]; + + if (chain.rpcUrls.custom) { + return http(chain.rpcUrls.custom.http[0]); + } + return http(chain.rpcUrls.default.http[0]); + }; + + static genChainFromName( + chainName: string, + customRpcUrl?: string | null + ): Chain { + const baseChain = viemChains[chainName]; + + if (!baseChain?.id) { + throw new Error("Invalid chain name"); + } + + const viemChain: Chain = customRpcUrl + ? { + ...baseChain, + rpcUrls: { + ...baseChain.rpcUrls, + custom: { + http: [customRpcUrl], + }, + }, + } + : baseChain; + + return viemChain; + } +} + +const genChainsFromRuntime = ( + runtime: IAgentRuntime +): Record => { + const chainNames = ["arthera"]; + const chains = {}; + + chainNames.forEach((chainName) => { + const rpcUrl = runtime.getSetting( + "ETHEREUM_PROVIDER_" + chainName.toUpperCase() + ); + const chain = WalletProvider.genChainFromName(chainName, rpcUrl); + chains[chainName] = chain; + }); + + return chains; +}; + +export const initWalletProvider = (runtime: IAgentRuntime) => { + const privateKey = runtime.getSetting("ARTHERA_PRIVATE_KEY"); + if (!privateKey) { + throw new Error("ARTHERA_PRIVATE_KEY is missing"); + } + + const chains = genChainsFromRuntime(runtime); + + return new WalletProvider(privateKey as `0x${string}`, chains); +}; + +export const artheraWalletProvider: Provider = { + async get( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise { + try { + const walletProvider = initWalletProvider(runtime); + const address = walletProvider.getAddress(); + const balance = await walletProvider.getWalletBalance(); + const chain = walletProvider.getCurrentChain(); + return `Arthera Wallet Address: ${address}\nBalance: ${balance} ${chain.nativeCurrency.symbol}\nChain ID: ${chain.id}, Name: ${chain.name}`; + } catch (error) { + console.error("Error in Arthera wallet provider:", error); + return null; + } + }, +}; diff --git a/packages/plugin-arthera/src/templates/index.ts b/packages/plugin-arthera/src/templates/index.ts new file mode 100644 index 00000000000..d8206074bce --- /dev/null +++ b/packages/plugin-arthera/src/templates/index.ts @@ -0,0 +1,23 @@ +export const transferTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +{{walletInfo}} + +Extract the following information about the requested transfer: +- Chain to execute on: Must be one of ["arthera", "base", ...] (like in viem/chains) +- Amount to transfer: Must be a string representing the amount in AA (only number without coin symbol, e.g., "0.1") +- Recipient address: Must be a valid Arthera address starting with "0x" +- Token symbol or address (if not native token): Optional, leave as null for AA transfers + +Respond with a JSON markdown block containing only the extracted values. All fields except 'token' are required: + +\`\`\`json +{ + "fromChain": SUPPORTED_CHAINS, + "amount": string, + "toAddress": string, + "token": string | null +} +\`\`\` +`; diff --git a/packages/plugin-arthera/src/tests/transfer.test.ts b/packages/plugin-arthera/src/tests/transfer.test.ts new file mode 100644 index 00000000000..aaecf38d7c7 --- /dev/null +++ b/packages/plugin-arthera/src/tests/transfer.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { generatePrivateKey } from "viem/accounts"; +import { Chain } from "viem"; +import { getEnvVariable } from "@elizaos/core"; + +import { TransferAction } from "../actions/transfer"; +import { WalletProvider } from "../providers/wallet"; + +describe("Transfer Action", () => { + let wp: WalletProvider; + let wp1: WalletProvider; + + beforeEach(async () => { + const pk = generatePrivateKey(); + const pk1 = getEnvVariable("ARTHERA_PRIVATE_KEY") as `0x${string}`; + const customChains = prepareChains(); + wp = new WalletProvider(pk, customChains); + if (pk1) { + wp1 = new WalletProvider(pk1, customChains); + } + }); + describe("Constructor", () => { + it("should initialize with wallet provider", () => { + const ta = new TransferAction(wp); + + expect(ta).toBeDefined(); + }); + }); + describe("Transfer", () => { + let ta: TransferAction; + let ta1: TransferAction; + let receiverAddress: `0x${string}`; + + beforeEach(() => { + ta = new TransferAction(wp); + if (wp1) { + ta1 = new TransferAction(wp1); + receiverAddress = wp1.getAddress(); + } + else { + receiverAddress = wp.getAddress(); + } + }); + + it("throws if not enough gas", async () => { + await expect( + ta.transfer({ + fromChain: "arthera", + toAddress: receiverAddress, + amount: "1", + }) + ).rejects.toThrow( + "Transfer failed: The total cost (gas * gas fee + value) of executing this transaction exceeds the balance of the account." + ); + }); + + if (wp1) { + it("transfers tokens", async () => { + const tx = await ta1.transfer({ + fromChain: "arthera", + toAddress: receiverAddress, + amount: "0.001", + }); + + expect(tx).toBeDefined(); + expect(tx.from).toEqual(wp1.getAddress()); + expect(tx.to).toEqual(receiverAddress); + expect(tx.value).toEqual(1000000000000000n); + }); + } + }); +}); + +const prepareChains = () => { + const customChains: Record = {}; + const chainNames = ["arthera"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + + return customChains; +}; diff --git a/packages/plugin-arthera/src/tests/wallet.test.ts b/packages/plugin-arthera/src/tests/wallet.test.ts new file mode 100644 index 00000000000..07cb1494ed3 --- /dev/null +++ b/packages/plugin-arthera/src/tests/wallet.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { arthera, Chain } from "viem/chains"; + +import { WalletProvider } from "../providers/wallet"; + +const customRpcUrls = { + arthera: "custom-rpc.arthera.io", +}; + +describe("Wallet provider", () => { + let walletProvider: WalletProvider; + let pk: `0x${string}`; + const customChains: Record = {}; + + beforeAll(() => { + pk = generatePrivateKey(); + + const chainNames = ["arthera"]; + chainNames.forEach( + (chain) => + (customChains[chain] = WalletProvider.genChainFromName(chain)) + ); + }); + + describe("Constructor", () => { + it("sets address", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + walletProvider = new WalletProvider(pk); + + expect(walletProvider.getAddress()).toEqual(expectedAddress); + }); + it("sets default chain to arthera", () => { + walletProvider = new WalletProvider(pk); + + expect(walletProvider.chains.arthera.id).toEqual(arthera.id); + expect(walletProvider.getCurrentChain().id).toEqual(arthera.id); + }); + it("sets custom chains", () => { + walletProvider = new WalletProvider(pk, customChains); + + expect(walletProvider.chains.arthera.id).toEqual(arthera.id); + }); + it("sets the first provided custom chain as current chain", () => { + walletProvider = new WalletProvider(pk, customChains); + + expect(walletProvider.getCurrentChain().id).toEqual(arthera.id); + }); + }); + describe("Clients", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk); + }); + it("generates public client", () => { + const client = walletProvider.getPublicClient("arthera"); + expect(client.chain.id).toEqual(arthera.id); + expect(client.transport.url).toEqual( + arthera.rpcUrls.default.http[0] + ); + }); + it("generates public client with custom rpcurl", () => { + const chain = WalletProvider.genChainFromName( + "arthera", + customRpcUrls.arthera + ); + const wp = new WalletProvider(pk, { ["arthera"]: chain }); + + const client = wp.getPublicClient("arthera"); + expect(client.chain.id).toEqual(arthera.id); + expect(client.chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).toEqual( + customRpcUrls.arthera + ); + expect(client.transport.url).toEqual(customRpcUrls.arthera); + }); + it("generates wallet client", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + + const client = walletProvider.getWalletClient("arthera"); + + expect(client.account.address).toEqual(expectedAddress); + expect(client.transport.url).toEqual( + arthera.rpcUrls.default.http[0] + ); + }); + it("generates wallet client with custom rpcurl", () => { + const account = privateKeyToAccount(pk); + const expectedAddress = account.address; + const chain = WalletProvider.genChainFromName( + "arthera", + customRpcUrls.arthera + ); + const wp = new WalletProvider(pk, { ["arthera"]: chain }); + + const client = wp.getWalletClient("arthera"); + + expect(client.account.address).toEqual(expectedAddress); + expect(client.chain.id).toEqual(arthera.id); + expect(client.chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + expect(client.chain.rpcUrls.custom.http[0]).toEqual( + customRpcUrls.arthera + ); + expect(client.transport.url).toEqual(customRpcUrls.arthera); + }); + }); + describe("Balance", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, customChains); + }); + it("should fetch balance", async () => { + const bal = await walletProvider.getWalletBalance(); + + expect(bal).toEqual("0"); + }); + it("should fetch balance for a specific added chain", async () => { + const bal = await walletProvider.getWalletBalanceForChain("arthera"); + + expect(bal).toEqual("0"); + }); + it("should return null if chain is not added", async () => { + const bal = await walletProvider.getWalletBalanceForChain("base"); + expect(bal).toBeNull(); + }); + }); + describe("Chain", () => { + beforeEach(() => { + walletProvider = new WalletProvider(pk, customChains); + }); + it("generates chains from chain name", () => { + const chainName = "arthera"; + const chain: Chain = WalletProvider.genChainFromName(chainName); + + expect(chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + }); + it("generates chains from chain name with custom rpc url", () => { + const chainName = "arthera"; + const customRpcUrl = customRpcUrls.arthera; + const chain: Chain = WalletProvider.genChainFromName( + chainName, + customRpcUrl + ); + + expect(chain.rpcUrls.default.http[0]).toEqual( + arthera.rpcUrls.default.http[0] + ); + expect(chain.rpcUrls.custom.http[0]).toEqual(customRpcUrl); + }); + it("gets chain configs", () => { + const chain = walletProvider.getChainConfigs("arthera"); + + expect(chain.id).toEqual(arthera.id); + }); + it("throws if unsupported chain name", () => { + // intentionally set unsupported chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => WalletProvider.genChainFromName("ethereum")).toThrow(); + }); + it("throws if invalid chain name", () => { + // intentionally set incorrect chain, ts will complain + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => WalletProvider.genChainFromName("eth")).toThrow(); + }); + }); +}); diff --git a/packages/plugin-arthera/src/types/index.ts b/packages/plugin-arthera/src/types/index.ts new file mode 100644 index 00000000000..f772c842a62 --- /dev/null +++ b/packages/plugin-arthera/src/types/index.ts @@ -0,0 +1,73 @@ +import type { + Account, + Address, + Chain, + Hash, + HttpTransport, + PublicClient, + WalletClient, +} from "viem"; +import * as viemChains from "viem/chains"; + +const _SupportedChainList = Object.keys(viemChains) as Array< + keyof typeof viemChains +>; +export type SupportedChain = (typeof _SupportedChainList)[number]; + +// Transaction types +export interface Transaction { + hash: Hash; + from: Address; + to: Address; + value: bigint; + data?: `0x${string}`; + chainId?: number; +} + +// Chain configuration +export interface ChainMetadata { + chainId: number; + name: string; + chain: Chain; + rpcUrl: string; + nativeCurrency: { + name: string; + symbol: string; + decimals: number; + }; + blockExplorerUrl: string; +} + +export interface ChainConfig { + chain: Chain; + publicClient: PublicClient; + walletClient?: WalletClient; +} + +// Action parameters +export interface TransferParams { + fromChain: SupportedChain; + toAddress: Address; + amount: string; + data?: `0x${string}`; +} + +// Plugin configuration +export interface ArtheraPluginConfig { + rpcUrl?: { + arthera?: string; + }; + secrets?: { + ARTHERA_PRIVATE_KEY: string; + }; + testMode?: boolean; + multicall?: { + batchSize?: number; + wait?: number; + }; +} + +export interface ProviderError extends Error { + code?: number; + data?: unknown; +} diff --git a/packages/plugin-arthera/tsconfig.json b/packages/plugin-arthera/tsconfig.json new file mode 100644 index 00000000000..b6ce190d989 --- /dev/null +++ b/packages/plugin-arthera/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], + "declaration": true + }, + "include": [ + "src" + ] +} diff --git a/packages/plugin-arthera/tsup.config.ts b/packages/plugin-arthera/tsup.config.ts new file mode 100644 index 00000000000..04abb285562 --- /dev/null +++ b/packages/plugin-arthera/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + ], +}); diff --git a/packages/plugin-binance/README.md b/packages/plugin-binance/README.md new file mode 100644 index 00000000000..67489337202 --- /dev/null +++ b/packages/plugin-binance/README.md @@ -0,0 +1,88 @@ +# Binance Plugin for Eliza + +This plugin enables Eliza to interact with the Binance cryptocurrency exchange, providing capabilities for checking prices, executing trades, and managing spot wallet balances. + +## Features + +- 📊 Real-time cryptocurrency price checks +- 💱 Spot trading (market and limit orders) +- 💰 Wallet balance inquiries +- ✅ Comprehensive error handling +- 🔒 Secure API integration + +## Prerequisites + +1. **Binance Account**: You need a Binance account to use this plugin +2. **API Keys**: Generate API keys from your Binance account: + - Go to your Binance account settings + - Navigate to API Management + - Create a new API key + - Enable spot trading permissions + - Store your API key and secret securely + +## Configuration + +Set the following environment variables: + +```env +BINANCE_API_KEY=your_api_key +BINANCE_SECRET_KEY=your_secret_key +``` + +## Installation + +Add the plugin to your Eliza configuration: + +```json +{ + "plugins": ["@elizaos/plugin-binance"] +} +``` + +## Available Actions + +The plugin provides the following actions: + +1. **GET_PRICE**: Check cryptocurrency prices + + - Example: "What's the current price of Bitcoin?" + - Example: "Check ETH price in USDT" + +2. **EXECUTE_SPOT_TRADE**: Execute spot trades + + - Example: "Buy 0.1 BTC at market price" + - Example: "Sell 100 USDT worth of ETH" + +3. **GET_SPOT_BALANCE**: Check wallet balances + - Example: "What's my BTC balance?" + - Example: "Show all my wallet balances" + +## Important Notes + +1. **API Rate Limits**: Binance implements rate limiting: + + - 1200 requests per minute for most endpoints + - Some endpoints have specific weight limits + - The plugin handles rate limiting errors appropriately + +2. **Minimum Order Sizes**: Binance enforces minimum order sizes and notional values: + + - Minimum order size varies by trading pair + - Minimum notional value (quantity × price) must be met + - The plugin validates these requirements before order execution + +3. **Error Handling**: The plugin provides detailed error messages for: + - Invalid API credentials + - Insufficient balance + - Invalid trading pairs + - Minimum notional value not met + - Other API-specific errors + +## Service Architecture + +The plugin is organized into specialized services: + +- `PriceService`: Handles price-related operations +- `TradeService`: Manages trading operations +- `AccountService`: Handles balance and account operations +- `BaseService`: Provides common functionality diff --git a/packages/plugin-binance/package.json b/packages/plugin-binance/package.json new file mode 100644 index 00000000000..1f8bbeee11d --- /dev/null +++ b/packages/plugin-binance/package.json @@ -0,0 +1,35 @@ +{ + "name": "@elizaos/plugin-binance", + "version": "0.1.0", + "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": { + "@elizaos/core": "workspace:*", + "@binance/connector": "^3.6.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "tsup": "8.3.5", + "@types/node": "^20.0.0" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache ." + } +} \ No newline at end of file diff --git a/packages/plugin-binance/src/actions/priceCheck.ts b/packages/plugin-binance/src/actions/priceCheck.ts new file mode 100644 index 00000000000..7c91ebd75af --- /dev/null +++ b/packages/plugin-binance/src/actions/priceCheck.ts @@ -0,0 +1,161 @@ +import { + ActionExample, + composeContext, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; +import { BinanceService } from "../services"; + +const priceCheckTemplate = `Look at ONLY your LAST RESPONSE message in this conversation, where you just said which cryptocurrency price you would check. +Based on ONLY that last message, provide the trading symbol. + +For example: +- If your last message was "I'll check the current Ethereum price..." -> return "ETH" +- If your last message was "I'll check the current Solana price..." -> return "SOL" +- If your last message was "I'll check the current Bitcoin price..." -> return "BTC" + +\`\`\`json +{ + "symbol": "", + "quoteCurrency": "" +} +\`\`\` + +Last part of conversation: +{{recentMessages}}`; + +export const priceCheck: Action = { + name: "GET_PRICE", + similes: [ + "CHECK_PRICE", + "PRICE_CHECK", + "GET_CRYPTO_PRICE", + "CRYPTO_PRICE", + "CHECK_CRYPTO_PRICE", + "PRICE_LOOKUP", + "CURRENT_PRICE", + ], + description: "Get current price information for a cryptocurrency pair", + validate: async () => true, // Public endpoint + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ): Promise => { + try { + // Initialize or update state + state = !state + ? await runtime.composeState(message) + : await runtime.updateRecentMessageState(state); + + const context = composeContext({ + state, + template: priceCheckTemplate, + }); + + const rawContent = await generateObjectDeprecated({ + runtime, + context, + modelClass: ModelClass.SMALL, + }); + + if (!rawContent?.symbol) { + throw new Error( + "Could not determine which cryptocurrency to check" + ); + } + + // Ensure the content has the required shape + const content = { + symbol: rawContent.symbol.toString().toUpperCase().trim(), + quoteCurrency: (rawContent.quoteCurrency || "USDT") + .toString() + .toUpperCase() + .trim(), + }; + + if (content.symbol.length < 2 || content.symbol.length > 10) { + throw new Error("Invalid cryptocurrency symbol"); + } + + const binanceService = new BinanceService(); + const priceData = await binanceService.getPrice(content); + + if (callback) { + callback({ + text: `The current ${content.symbol} price is ${BinanceService.formatPrice(priceData.price)} ${content.quoteCurrency}`, + content: priceData, + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error in price check:", error); + if (callback) { + const errorMessage = error.message.includes("Invalid API key") + ? "Unable to connect to Binance API" + : error.message.includes("Invalid symbol") + ? `Sorry, could not find price for the cryptocurrency symbol you provided` + : `Sorry, I encountered an error: ${error.message}`; + + callback({ + text: errorMessage, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "What's the current price of Bitcoin?", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll check the current Bitcoin price for you right away.", + action: "GET_PRICE", + }, + }, + { + user: "{{agent}}", + content: { + text: "The current BTC price is 42,150.25 USDT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Can you check ETH price in EUR?", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll fetch the current Ethereum price in euros for you.", + action: "GET_PRICE", + }, + }, + { + user: "{{agent}}", + content: { + text: "The current ETH price is 2,245.80 EUR", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-binance/src/actions/spotBalance.ts b/packages/plugin-binance/src/actions/spotBalance.ts new file mode 100644 index 00000000000..940a77436d8 --- /dev/null +++ b/packages/plugin-binance/src/actions/spotBalance.ts @@ -0,0 +1,178 @@ +import { + ActionExample, + composeContext, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; +import { validateBinanceConfig } from "../environment"; +import { BinanceService } from "../services"; +import { BalanceCheckRequest } from "../types"; + +const spotBalanceTemplate = `Look at ONLY your LAST RESPONSE message in this conversation, where you just confirmed which cryptocurrency balance to check. +Based on ONLY that last message, extract the cryptocurrency symbol. + +For example: +- If your last message was "I'll fetch your Solana wallet balance..." -> return "SOL" +- If your last message was "I'll check your BTC balance..." -> return "BTC" +- If your last message was "I'll get your ETH balance..." -> return "ETH" + +\`\`\`json +{ + "asset": "" +} +\`\`\` + +Last part of conversation: +{{recentMessages}}`; + +export const spotBalance: Action = { + name: "GET_SPOT_BALANCE", + similes: [ + "CHECK_BALANCE", + "BALANCE_CHECK", + "GET_WALLET_BALANCE", + "WALLET_BALANCE", + "CHECK_WALLET", + "VIEW_BALANCE", + "SHOW_BALANCE", + ], + description: "Get current spot wallet balance for one or all assets", + validate: async (runtime: IAgentRuntime) => { + try { + await validateBinanceConfig(runtime); + return true; + } catch (error) { + return false; + } + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + const balanceContext = composeContext({ + state, + template: spotBalanceTemplate, + }); + + const content = (await generateObjectDeprecated({ + runtime, + context: balanceContext, + modelClass: ModelClass.SMALL, + })) as BalanceCheckRequest; + + try { + const binanceService = new BinanceService({ + apiKey: runtime.getSetting("BINANCE_API_KEY"), + secretKey: runtime.getSetting("BINANCE_SECRET_KEY"), + }); + + const balanceData = await binanceService.getBalance(content); + + if (content.asset) { + const assetBalance = balanceData.balances[0]; + if (assetBalance) { + if (callback) { + callback({ + text: `${content.asset} Balance:\nAvailable: ${assetBalance.free}\nLocked: ${assetBalance.locked}`, + content: assetBalance, + }); + } + } else { + if (callback) { + callback({ + text: `No balance found for ${content.asset}`, + content: { error: "Asset not found" }, + }); + } + } + } else { + const balanceText = balanceData.balances + .map( + (b) => + `${b.asset}: Available: ${b.free}, Locked: ${b.locked}` + ) + .join("\n"); + + if (callback) { + callback({ + text: `Spot Wallet Balances:\n${balanceText}`, + content: balanceData.balances, + }); + } + } + + return true; + } catch (error) { + elizaLogger.error("Error in balance check:", { + message: error.message, + code: error.code, + }); + if (callback) { + callback({ + text: error.message, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "What's my current Bitcoin balance?", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll check your BTC balance for you.", + action: "GET_SPOT_BALANCE", + }, + }, + { + user: "{{agent}}", + content: { + text: "BTC Balance:\nAvailable: 0.5\nLocked: 0.1", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Show me all my wallet balances", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll fetch all your spot wallet balances.", + action: "GET_SPOT_BALANCE", + }, + }, + { + user: "{{agent}}", + content: { + text: "Spot Wallet Balances:\nBTC: Available: 0.5, Locked: 0.1\nETH: Available: 2.0, Locked: 0.0\nUSDT: Available: 1000.0, Locked: 0.0", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-binance/src/actions/spotTrade.ts b/packages/plugin-binance/src/actions/spotTrade.ts new file mode 100644 index 00000000000..04fcb538ac5 --- /dev/null +++ b/packages/plugin-binance/src/actions/spotTrade.ts @@ -0,0 +1,168 @@ +import { + ActionExample, + composeContext, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; +import { BinanceService } from "../services"; +import { SpotTradeSchema } from "../types"; + +const spotTradeTemplate = `Look at your LAST RESPONSE in the conversation where you confirmed a trade/swap request. +Based on ONLY that last message, extract the trading details: + +Trading pairs on Binance must include USDT or BUSD or USDC. For example: +- For "swap SOL for USDC" -> use "SOLUSDC" as symbol +- For "swap ETH for USDT" -> use "ETHUSDT" as symbol +- For "buy BTC with USDT" -> use "BTCUSDT" as symbol + +\`\`\`json +{ + "symbol": "", + "side": "SELL", + "type": "MARKET", + "quantity": "" +} +\`\`\` + +Recent conversation: +{{recentMessages}}`; + +export const spotTrade: Action = { + name: "EXECUTE_SPOT_TRADE", + similes: [ + "SPOT_TRADE", + "MARKET_ORDER", + "LIMIT_ORDER", + "BUY_CRYPTO", + "SELL_CRYPTO", + "PLACE_ORDER", + ], + description: "Execute a spot trade on Binance", + validate: async (runtime: IAgentRuntime) => { + return !!( + runtime.getSetting("BINANCE_API_KEY") && + runtime.getSetting("BINANCE_SECRET_KEY") + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: Record, + callback?: HandlerCallback + ): Promise => { + let content; + try { + state = !state + ? await runtime.composeState(message) + : await runtime.updateRecentMessageState(state); + + const context = composeContext({ + state, + template: spotTradeTemplate, + }); + + content = await generateObjectDeprecated({ + runtime, + context, + modelClass: ModelClass.SMALL, + }); + + // Convert quantity to number if it's a string + if (content && typeof content.quantity === "string") { + content.quantity = parseFloat(content.quantity); + } + + const parseResult = SpotTradeSchema.safeParse(content); + if (!parseResult.success) { + throw new Error( + `Invalid spot trade content: ${JSON.stringify(parseResult.error.errors, null, 2)}` + ); + } + + const binanceService = new BinanceService({ + apiKey: runtime.getSetting("BINANCE_API_KEY"), + secretKey: runtime.getSetting("BINANCE_SECRET_KEY"), + }); + + const tradeResult = await binanceService.executeTrade(content); + + if (callback) { + const orderType = + content.type === "MARKET" + ? "market" + : `limit at ${BinanceService.formatPrice(content.price!)}`; + + callback({ + text: `Successfully placed a ${orderType} order to ${content.side.toLowerCase()} ${content.quantity} ${content.symbol}\nOrder ID: ${tradeResult.orderId}\nStatus: ${tradeResult.status}`, + content: tradeResult, + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error executing trade:", { + content, + message: error.message, + code: error.code, + }); + if (callback) { + callback({ + text: `Error executing trade: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Buy 0.1 BTC at market price", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll execute a market order to buy 0.1 BTC now.", + action: "EXECUTE_SPOT_TRADE", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully placed a market order to buy 0.1 BTCUSDT\nOrder ID: 123456789\nStatus: FILLED", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Place a limit order to sell 100 BNB at 250 USDT", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll place a limit order to sell 100 BNB at 250 USDT.", + action: "EXECUTE_SPOT_TRADE", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully placed a limit order to sell 100 BNBUSDT at 250\nOrder ID: 987654321\nStatus: NEW", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-binance/src/constants/api.ts b/packages/plugin-binance/src/constants/api.ts new file mode 100644 index 00000000000..b1d62179230 --- /dev/null +++ b/packages/plugin-binance/src/constants/api.ts @@ -0,0 +1,31 @@ +export const API_DEFAULTS = { + BASE_URL: "https://api.binance.com", + TIMEOUT: 30000, // 30 seconds + RATE_LIMIT: { + MAX_REQUESTS_PER_MINUTE: 1200, + WEIGHT_PER_REQUEST: 1, + }, +}; + +export const API_ENDPOINTS = { + TICKER: "/api/v3/ticker/price", + ACCOUNT: "/api/v3/account", + ORDER: "/api/v3/order", + EXCHANGE_INFO: "/api/v3/exchangeInfo", +}; + +export const ORDER_TYPES = { + MARKET: "MARKET", + LIMIT: "LIMIT", +} as const; + +export const ORDER_SIDES = { + BUY: "BUY", + SELL: "SELL", +} as const; + +export const TIME_IN_FORCE = { + GTC: "GTC", // Good Till Cancel + IOC: "IOC", // Immediate or Cancel + FOK: "FOK", // Fill or Kill +} as const; diff --git a/packages/plugin-binance/src/constants/defaults.ts b/packages/plugin-binance/src/constants/defaults.ts new file mode 100644 index 00000000000..ff34231098c --- /dev/null +++ b/packages/plugin-binance/src/constants/defaults.ts @@ -0,0 +1,22 @@ +export const TRADE_DEFAULTS = { + QUOTE_CURRENCY: "USDT", + TIME_IN_FORCE: "GTC", + ORDER_TYPE: "MARKET", + PRICE_PRECISION: 8, + QUANTITY_PRECISION: 8, +}; + +export const DISPLAY_DEFAULTS = { + PRICE_FORMAT: { + MIN_FRACTION_DIGITS: 2, + MAX_FRACTION_DIGITS: 8, + LOCALE: "en-US", + }, +}; + +export const VALIDATION = { + SYMBOL: { + MIN_LENGTH: 2, + MAX_LENGTH: 10, + }, +}; diff --git a/packages/plugin-binance/src/constants/errors.ts b/packages/plugin-binance/src/constants/errors.ts new file mode 100644 index 00000000000..2558fe9c8f4 --- /dev/null +++ b/packages/plugin-binance/src/constants/errors.ts @@ -0,0 +1,33 @@ +export const ERROR_CODES = { + INVALID_CREDENTIALS: 401, + INVALID_PARAMETERS: 400, + INSUFFICIENT_BALANCE: -1012, + MIN_NOTIONAL_NOT_MET: -1013, + UNKNOWN_ORDER_COMPOSITION: -1111, + PRICE_QTY_EXCEED_HARD_LIMITS: -1021, +} as const; + +export const ERROR_MESSAGES = { + INVALID_CREDENTIALS: + "Invalid API credentials. Please check your API key and secret.", + INVALID_SYMBOL: "Invalid trading pair symbol", + SYMBOL_NOT_FOUND: (symbol: string) => + `Trading pair ${symbol} is not available`, + MIN_NOTIONAL_NOT_MET: (minNotional?: string) => + `Order value is too small. Please increase the quantity to meet the minimum order value requirement.${ + minNotional ? ` Minimum order value is ${minNotional} USDC.` : "" + }`, + LIMIT_ORDER_PRICE_REQUIRED: "Price is required for LIMIT orders", + BALANCE_FETCH_ERROR: (asset?: string) => + asset + ? `Failed to fetch balance for ${asset}` + : "Failed to fetch account balances", + PRICE_FETCH_ERROR: (symbol: string) => + `Failed to fetch price for ${symbol}`, +} as const; + +export const VALIDATION_ERRORS = { + MISSING_API_KEY: "BINANCE_API_KEY is required but not configured", + MISSING_SECRET_KEY: "BINANCE_SECRET_KEY is required but not configured", + INVALID_SYMBOL_LENGTH: "Invalid cryptocurrency symbol length", +} as const; diff --git a/packages/plugin-binance/src/environment.ts b/packages/plugin-binance/src/environment.ts new file mode 100644 index 00000000000..9d56e3e7d9a --- /dev/null +++ b/packages/plugin-binance/src/environment.ts @@ -0,0 +1,32 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const binanceEnvSchema = z.object({ + BINANCE_API_KEY: z.string().min(1, "Binance API key is required"), + BINANCE_SECRET_KEY: z.string().min(1, "Binance secret key is required"), +}); + +export type BinanceConfig = z.infer; + +export async function validateBinanceConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + BINANCE_API_KEY: runtime.getSetting("BINANCE_API_KEY"), + BINANCE_SECRET_KEY: runtime.getSetting("BINANCE_SECRET_KEY"), + }; + + return binanceEnvSchema.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( + `Binance configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-binance/src/index.ts b/packages/plugin-binance/src/index.ts new file mode 100644 index 00000000000..90a54d39c2e --- /dev/null +++ b/packages/plugin-binance/src/index.ts @@ -0,0 +1,15 @@ +import { Plugin } from "@elizaos/core"; +import { priceCheck } from "./actions/priceCheck"; +import { spotBalance } from "./actions/spotBalance"; +import { spotTrade } from "./actions/spotTrade"; + +// Export the plugin configuration +export const binancePlugin: Plugin = { + name: "binance", + description: "Binance Plugin for Eliza", + actions: [spotTrade, priceCheck, spotBalance], + evaluators: [], + providers: [], +}; + +export default binancePlugin; diff --git a/packages/plugin-binance/src/services/account.ts b/packages/plugin-binance/src/services/account.ts new file mode 100644 index 00000000000..2bf51590731 --- /dev/null +++ b/packages/plugin-binance/src/services/account.ts @@ -0,0 +1,93 @@ +import { BinanceAccountInfo, BinanceBalance } from "../types/api/account"; +import { BalanceCheckRequest, BalanceResponse } from "../types/internal/config"; +import { BaseService } from "./base"; + +/** + * Service for handling account-related operations + */ +export class AccountService extends BaseService { + /** + * Get account balance for all assets or a specific asset + */ + async getBalance(request: BalanceCheckRequest): Promise { + try { + this.validateCredentials(); + + const response = await this.client.account(); + const accountInfo = response.data as BinanceAccountInfo; + + let balances = this.filterNonZeroBalances(accountInfo.balances); + + if (request.asset) { + balances = this.filterByAsset(balances, request.asset); + } + + return { + balances, + timestamp: Date.now(), + }; + } catch (error) { + throw this.handleError( + error, + request.asset ? `Asset: ${request.asset}` : "All assets" + ); + } + } + + /** + * Filter out zero balances + */ + private filterNonZeroBalances( + balances: BinanceBalance[] + ): BinanceBalance[] { + return balances.filter( + (balance) => + parseFloat(balance.free) > 0 || parseFloat(balance.locked) > 0 + ); + } + + /** + * Filter balances by asset + */ + private filterByAsset( + balances: BinanceBalance[], + asset: string + ): BinanceBalance[] { + return balances.filter( + (b) => b.asset.toUpperCase() === asset.toUpperCase() + ); + } + + /** + * Get account trading status + */ + async getTradingStatus(): Promise { + try { + this.validateCredentials(); + const response = await this.client.account(); + const accountInfo = response.data as BinanceAccountInfo; + return accountInfo.canTrade; + } catch (error) { + throw this.handleError(error, "Trading status check"); + } + } + + /** + * Check if account has sufficient balance for a trade + */ + async checkBalance(asset: string, required: number): Promise { + try { + const { balances } = await this.getBalance({ asset }); + const balance = balances[0]; + + if (!balance) { + return false; + } + + const available = parseFloat(balance.free); + return available >= required; + } catch (error) { + throw this.handleError(error, `Balance check for ${asset}`); + } + } +} diff --git a/packages/plugin-binance/src/services/base.ts b/packages/plugin-binance/src/services/base.ts new file mode 100644 index 00000000000..37bdb9ac755 --- /dev/null +++ b/packages/plugin-binance/src/services/base.ts @@ -0,0 +1,95 @@ +import { Spot } from "@binance/connector"; +import { elizaLogger } from "@elizaos/core"; +import { API_DEFAULTS } from "../constants/api"; +import { ERROR_MESSAGES } from "../constants/errors"; +import { BinanceConfig, ServiceOptions } from "../types/internal/config"; +import { + ApiError, + AuthenticationError, + BinanceError, + InvalidSymbolError, + MinNotionalError, +} from "../types/internal/error"; + +/** + * Base service class with common functionality + */ +export abstract class BaseService { + protected client: Spot; + protected config: BinanceConfig; + + constructor(config?: BinanceConfig) { + this.config = { + baseURL: API_DEFAULTS.BASE_URL, + timeout: API_DEFAULTS.TIMEOUT, + ...config, + }; + + this.client = new Spot(this.config.apiKey, this.config.secretKey, { + baseURL: this.config.baseURL, + timeout: this.config.timeout, + }); + } + + /** + * Handles common error scenarios and transforms them into appropriate error types + */ + protected handleError(error: unknown, context?: string): never { + if (error instanceof BinanceError) { + throw error; + } + + const apiError = error as any; + const errorResponse = apiError.response?.data; + const errorCode = errorResponse?.code || apiError.code; + const errorMessage = errorResponse?.msg || apiError.message; + + // Handle authentication errors + if (apiError.response?.status === 401) { + throw new AuthenticationError(ERROR_MESSAGES.INVALID_CREDENTIALS); + } + + // Handle minimum notional errors + if (errorCode === -1013 && errorMessage?.includes("NOTIONAL")) { + throw new MinNotionalError(); + } + + // Handle invalid symbol errors + if (errorMessage?.includes("Invalid symbol")) { + throw new InvalidSymbolError(context || "Unknown"); + } + + // Log unexpected errors for debugging + elizaLogger.error("Unexpected API error:", { + context, + code: errorCode, + message: errorMessage, + response: errorResponse, + }); + + throw new ApiError( + errorMessage || "An unexpected error occurred", + errorCode || 500, + errorResponse + ); + } + + /** + * Validates required API credentials + */ + protected validateCredentials(): void { + if (!this.config.apiKey || !this.config.secretKey) { + throw new AuthenticationError("API credentials are required"); + } + } + + /** + * Merges default options with provided options + */ + protected mergeOptions(options?: ServiceOptions): ServiceOptions { + return { + timeout: this.config.timeout, + ...options, + }; + } +} diff --git a/packages/plugin-binance/src/services/index.ts b/packages/plugin-binance/src/services/index.ts new file mode 100644 index 00000000000..cf22c908255 --- /dev/null +++ b/packages/plugin-binance/src/services/index.ts @@ -0,0 +1,52 @@ +import { BinanceConfig } from "../types/internal/config"; +import { AccountService } from "./account"; +import { PriceService } from "./price"; +import { TradeService } from "./trade"; + +/** + * Main service facade that coordinates between specialized services + */ +export class BinanceService { + private priceService: PriceService; + private tradeService: TradeService; + private accountService: AccountService; + + constructor(config?: BinanceConfig) { + this.priceService = new PriceService(config); + this.tradeService = new TradeService(config); + this.accountService = new AccountService(config); + } + + /** + * Price-related operations + */ + async getPrice(...args: Parameters) { + return this.priceService.getPrice(...args); + } + + static formatPrice = PriceService.formatPrice; + + /** + * Trading operations + */ + async executeTrade(...args: Parameters) { + return this.tradeService.executeTrade(...args); + } + + /** + * Account operations + */ + async getBalance(...args: Parameters) { + return this.accountService.getBalance(...args); + } + + async getTradingStatus() { + return this.accountService.getTradingStatus(); + } + + async checkBalance(...args: Parameters) { + return this.accountService.checkBalance(...args); + } +} + +export { AccountService, PriceService, TradeService }; diff --git a/packages/plugin-binance/src/services/price.ts b/packages/plugin-binance/src/services/price.ts new file mode 100644 index 00000000000..f549c883000 --- /dev/null +++ b/packages/plugin-binance/src/services/price.ts @@ -0,0 +1,57 @@ +import { VALIDATION } from "../constants/defaults"; +import { ERROR_MESSAGES } from "../constants/errors"; +import { BinanceTickerResponse } from "../types/api/price"; +import { PriceCheckRequest, PriceResponse } from "../types/internal/config"; +import { BinanceError } from "../types/internal/error"; +import { BaseService } from "./base"; + +/** + * Service for handling price-related operations + */ +export class PriceService extends BaseService { + /** + * Get current price for a symbol + */ + async getPrice(request: PriceCheckRequest): Promise { + try { + this.validateSymbol(request.symbol); + + const symbol = `${request.symbol}${request.quoteCurrency}`; + const response = await this.client.tickerPrice(symbol); + const data = response.data as BinanceTickerResponse; + + return { + symbol, + price: data.price, + timestamp: Date.now(), + }; + } catch (error) { + throw this.handleError(error, request.symbol); + } + } + + /** + * Validates symbol format + */ + private validateSymbol(symbol: string): void { + const trimmedSymbol = symbol.trim(); + if ( + trimmedSymbol.length < VALIDATION.SYMBOL.MIN_LENGTH || + trimmedSymbol.length > VALIDATION.SYMBOL.MAX_LENGTH + ) { + throw new BinanceError(ERROR_MESSAGES.INVALID_SYMBOL); + } + } + + /** + * Format price for display + */ + static formatPrice(price: number | string): string { + const numPrice = typeof price === "string" ? parseFloat(price) : price; + return new Intl.NumberFormat("en-US", { + style: "decimal", + minimumFractionDigits: 2, + maximumFractionDigits: 8, + }).format(numPrice); + } +} diff --git a/packages/plugin-binance/src/services/trade.ts b/packages/plugin-binance/src/services/trade.ts new file mode 100644 index 00000000000..24ac5e0f51f --- /dev/null +++ b/packages/plugin-binance/src/services/trade.ts @@ -0,0 +1,114 @@ +import { ORDER_TYPES, TIME_IN_FORCE } from "../constants/api"; +import { ERROR_MESSAGES } from "../constants/errors"; +import { + BinanceExchangeInfo, + BinanceSymbolFilter, + BinanceSymbolInfo, +} from "../types/api/price"; +import { + BinanceNewOrderParams, + BinanceOrderResponse, +} from "../types/api/trade"; +import { SpotTradeRequest, TradeResponse } from "../types/internal/config"; +import { InvalidSymbolError, MinNotionalError } from "../types/internal/error"; +import { BaseService } from "./base"; + +/** + * Service for handling trading operations + */ +export class TradeService extends BaseService { + /** + * Execute a spot trade + */ + async executeTrade(request: SpotTradeRequest): Promise { + try { + this.validateCredentials(); + await this.validateSymbol(request.symbol); + + const orderParams = this.buildOrderParams(request); + const response = await this.client.newOrder( + orderParams.symbol, + orderParams.side, + orderParams.type, + orderParams + ); + + const data = response.data as BinanceOrderResponse; + return { + symbol: data.symbol, + orderId: data.orderId, + status: data.status, + executedQty: data.executedQty, + cummulativeQuoteQty: data.cummulativeQuoteQty, + price: data.price, + type: data.type, + side: data.side, + }; + } catch (error) { + throw this.handleError(error, request.symbol); + } + } + + /** + * Validate trading pair and get symbol information + */ + private async validateSymbol(symbol: string): Promise { + const exchangeInfo = await this.client.exchangeInfo(); + const data = exchangeInfo.data as BinanceExchangeInfo; + + const symbolInfo = data.symbols.find((s) => s.symbol === symbol); + if (!symbolInfo) { + throw new InvalidSymbolError(symbol); + } + + return symbolInfo; + } + + /** + * Build order parameters for the Binance API + */ + private buildOrderParams(request: SpotTradeRequest): BinanceNewOrderParams { + const params: BinanceNewOrderParams = { + symbol: request.symbol.toUpperCase(), + side: request.side, + type: request.type, + quantity: request.quantity.toString(), + }; + + if (request.type === ORDER_TYPES.LIMIT) { + if (!request.price) { + throw new Error(ERROR_MESSAGES.LIMIT_ORDER_PRICE_REQUIRED); + } + params.timeInForce = request.timeInForce || TIME_IN_FORCE.GTC; + params.price = request.price.toString(); + } + + return params; + } + + /** + * Get minimum notional value from symbol filters + */ + private getMinNotional(filters: BinanceSymbolFilter[]): string | undefined { + const notionalFilter = filters.find((f) => f.filterType === "NOTIONAL"); + return notionalFilter?.minNotional; + } + + /** + * Check if order meets minimum notional value + */ + private checkMinNotional( + symbolInfo: BinanceSymbolInfo, + quantity: number, + price?: number + ): void { + const minNotional = this.getMinNotional(symbolInfo.filters); + if (!minNotional) return; + + const notionalValue = price ? quantity * price : quantity; // For market orders, quantity is in quote currency + + if (parseFloat(minNotional) > notionalValue) { + throw new MinNotionalError(minNotional); + } + } +} diff --git a/packages/plugin-binance/src/types.ts b/packages/plugin-binance/src/types.ts new file mode 100644 index 00000000000..659b21aa57a --- /dev/null +++ b/packages/plugin-binance/src/types.ts @@ -0,0 +1,85 @@ +// types.ts +import { z } from "zod"; + +// Base configuration types +export interface BinanceConfig { + apiKey?: string; + secretKey?: string; + baseURL?: string; +} + +// Enhanced schemas with better validation +export const PriceCheckSchema = z.object({ + symbol: z.string().min(1).toUpperCase(), + quoteCurrency: z.string().min(1).toUpperCase().default("USDT"), +}); + +export const SpotTradeSchema = z.object({ + symbol: z.string().min(1).toUpperCase(), + side: z.enum(["BUY", "SELL"]), + type: z.enum(["MARKET", "LIMIT"]), + quantity: z.number().positive(), + price: z.number().positive().optional(), + timeInForce: z.enum(["GTC", "IOC", "FOK"]).optional().default("GTC"), +}); + +// Inferred types from schemas +export type PriceCheckRequest = z.infer; +export type SpotTradeRequest = z.infer; + +// Response types +export interface PriceResponse { + symbol: string; + price: string; + timestamp: number; +} + +export interface TradeResponse { + symbol: string; + orderId: number; + status: "NEW" | "PARTIALLY_FILLED" | "FILLED" | "CANCELED" | "REJECTED"; + executedQty: string; + cummulativeQuoteQty: string; + price: string; + type: SpotTradeRequest["type"]; + side: SpotTradeRequest["side"]; +} + +// Error handling types +export class BinanceError extends Error { + constructor( + message: string, + public code?: number, + public details?: unknown + ) { + super(message); + this.name = "BinanceError"; + } +} + +// Constants +export const TRADE_STATUS = { + NEW: "NEW", + PARTIALLY_FILLED: "PARTIALLY_FILLED", + FILLED: "FILLED", + CANCELED: "CANCELED", + REJECTED: "REJECTED", +} as const; + +export type TradeStatus = keyof typeof TRADE_STATUS; + +// Balance types +export interface BalanceCheckRequest { + asset?: string; +} + +export interface AssetBalance { + asset: string; + free: string; + locked: string; +} + +export interface BalanceResponse { + balances: AssetBalance[]; + timestamp: number; +} diff --git a/packages/plugin-binance/src/types/api/account.ts b/packages/plugin-binance/src/types/api/account.ts new file mode 100644 index 00000000000..1f9e88655c3 --- /dev/null +++ b/packages/plugin-binance/src/types/api/account.ts @@ -0,0 +1,79 @@ +/** + * Binance API account information response + */ +export interface BinanceAccountInfo { + makerCommission: number; + takerCommission: number; + buyerCommission: number; + sellerCommission: number; + canTrade: boolean; + canWithdraw: boolean; + canDeposit: boolean; + updateTime: number; + accountType: string; + balances: BinanceBalance[]; + permissions: string[]; +} + +/** + * Balance information for a single asset + */ +export interface BinanceBalance { + asset: string; + free: string; // Available balance + locked: string; // Locked in orders +} + +/** + * Account trade list response + */ +export interface BinanceAccountTrade { + symbol: string; + id: number; + orderId: number; + orderListId: number; + price: string; + qty: string; + quoteQty: string; + commission: string; + commissionAsset: string; + time: number; + isBuyer: boolean; + isMaker: boolean; + isBestMatch: boolean; +} + +/** + * Parameters for account trade list query + */ +export interface BinanceTradeListParams { + symbol: string; + orderId?: number; + startTime?: number; + endTime?: number; + fromId?: number; + limit?: number; +} + +/** + * Account status response + */ +export interface BinanceAccountStatus { + data: string; // "Normal", "Margin", "Futures", etc. +} + +/** + * API trading status response + */ +export interface BinanceApiTradingStatus { + data: { + isLocked: boolean; + plannedRecoverTime: number; + triggerCondition: { + gcr: number; + ifer: number; + ufr: number; + }; + updateTime: number; + }; +} diff --git a/packages/plugin-binance/src/types/api/price.ts b/packages/plugin-binance/src/types/api/price.ts new file mode 100644 index 00000000000..2990bdc2d2f --- /dev/null +++ b/packages/plugin-binance/src/types/api/price.ts @@ -0,0 +1,81 @@ +/** + * Binance API response for ticker price endpoint + */ +export interface BinanceTickerResponse { + symbol: string; + price: string; +} + +/** + * Binance API response for 24hr ticker + */ +export interface BinanceTickerStatistics { + symbol: string; + priceChange: string; + priceChangePercent: string; + weightedAvgPrice: string; + prevClosePrice: string; + lastPrice: string; + lastQty: string; + bidPrice: string; + bidQty: string; + askPrice: string; + askQty: string; + openPrice: string; + highPrice: string; + lowPrice: string; + volume: string; + quoteVolume: string; + openTime: number; + closeTime: number; + firstId: number; + lastId: number; + count: number; +} + +/** + * Exchange information for a symbol + */ +export interface BinanceSymbolInfo { + symbol: string; + status: string; + baseAsset: string; + baseAssetPrecision: number; + quoteAsset: string; + quotePrecision: number; + quoteAssetPrecision: number; + filters: BinanceSymbolFilter[]; +} + +/** + * Symbol filter types + */ +export interface BinanceSymbolFilter { + filterType: string; + minPrice?: string; + maxPrice?: string; + tickSize?: string; + minQty?: string; + maxQty?: string; + stepSize?: string; + minNotional?: string; + limit?: number; + multiplierUp?: string; + multiplierDown?: string; + avgPriceMins?: number; +} + +/** + * Exchange information response + */ +export interface BinanceExchangeInfo { + timezone: string; + serverTime: number; + rateLimits: Array<{ + rateLimitType: string; + interval: string; + intervalNum: number; + limit: number; + }>; + symbols: BinanceSymbolInfo[]; +} diff --git a/packages/plugin-binance/src/types/api/trade.ts b/packages/plugin-binance/src/types/api/trade.ts new file mode 100644 index 00000000000..5abb3fe43f3 --- /dev/null +++ b/packages/plugin-binance/src/types/api/trade.ts @@ -0,0 +1,81 @@ +import { ORDER_SIDES, ORDER_TYPES, TIME_IN_FORCE } from "../../constants/api"; + +export type OrderType = (typeof ORDER_TYPES)[keyof typeof ORDER_TYPES]; +export type OrderSide = (typeof ORDER_SIDES)[keyof typeof ORDER_SIDES]; +export type TimeInForce = (typeof TIME_IN_FORCE)[keyof typeof TIME_IN_FORCE]; + +/** + * Binance API new order response + */ +export interface BinanceOrderResponse { + symbol: string; + orderId: number; + orderListId: number; + clientOrderId: string; + transactTime: number; + price: string; + origQty: string; + executedQty: string; + cummulativeQuoteQty: string; + status: OrderStatus; + timeInForce: TimeInForce; + type: OrderType; + side: OrderSide; + fills?: OrderFill[]; +} + +/** + * Order fill information + */ +export interface OrderFill { + price: string; + qty: string; + commission: string; + commissionAsset: string; + tradeId: number; +} + +/** + * Order status types + */ +export type OrderStatus = + | "NEW" + | "PARTIALLY_FILLED" + | "FILLED" + | "CANCELED" + | "PENDING_CANCEL" + | "REJECTED" + | "EXPIRED"; + +/** + * New order parameters for Binance API + */ +export interface BinanceNewOrderParams { + symbol: string; + side: OrderSide; + type: OrderType; + timeInForce?: TimeInForce; + quantity?: string | number; + quoteOrderQty?: string | number; + price?: string | number; + newClientOrderId?: string; + stopPrice?: string | number; + icebergQty?: string | number; + newOrderRespType?: "ACK" | "RESULT" | "FULL"; +} + +/** + * Order query parameters + */ +export interface BinanceOrderQueryParams { + symbol: string; + orderId?: number; + origClientOrderId?: string; +} + +/** + * Cancel order parameters + */ +export interface BinanceCancelOrderParams extends BinanceOrderQueryParams { + newClientOrderId?: string; +} diff --git a/packages/plugin-binance/src/types/index.ts b/packages/plugin-binance/src/types/index.ts new file mode 100644 index 00000000000..8b2f12fbcb6 --- /dev/null +++ b/packages/plugin-binance/src/types/index.ts @@ -0,0 +1,8 @@ +// API Types +export * from "./api/account"; +export * from "./api/price"; +export * from "./api/trade"; + +// Internal Types +export * from "./internal/config"; +export * from "./internal/error"; diff --git a/packages/plugin-binance/src/types/internal/config.ts b/packages/plugin-binance/src/types/internal/config.ts new file mode 100644 index 00000000000..f1648c9c9a9 --- /dev/null +++ b/packages/plugin-binance/src/types/internal/config.ts @@ -0,0 +1,79 @@ +/** + * Binance service configuration + */ +export interface BinanceConfig { + apiKey?: string; + secretKey?: string; + baseURL?: string; + timeout?: number; +} + +/** + * Service options that can be passed to any service method + */ +export interface ServiceOptions { + timeout?: number; + recvWindow?: number; +} + +/** + * Price check request parameters + */ +export interface PriceCheckRequest { + symbol: string; + quoteCurrency: string; +} + +/** + * Price response data + */ +export interface PriceResponse { + symbol: string; + price: string; + timestamp: number; +} + +/** + * Spot trade request parameters + */ +export interface SpotTradeRequest { + symbol: string; + side: "BUY" | "SELL"; + type: "MARKET" | "LIMIT"; + quantity: number; + price?: number; + timeInForce?: "GTC" | "IOC" | "FOK"; +} + +/** + * Trade response data + */ +export interface TradeResponse { + symbol: string; + orderId: number; + status: string; + executedQty: string; + cummulativeQuoteQty: string; + price: string; + type: string; + side: string; +} + +/** + * Balance check request parameters + */ +export interface BalanceCheckRequest { + asset?: string; +} + +/** + * Balance response data + */ +export interface BalanceResponse { + balances: Array<{ + asset: string; + free: string; + locked: string; + }>; + timestamp: number; +} diff --git a/packages/plugin-binance/src/types/internal/error.ts b/packages/plugin-binance/src/types/internal/error.ts new file mode 100644 index 00000000000..1709172d1ee --- /dev/null +++ b/packages/plugin-binance/src/types/internal/error.ts @@ -0,0 +1,105 @@ +import { ERROR_CODES } from "../../constants/errors"; + +type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; + +/** + * Base error class for Binance-related errors + */ +export class BinanceError extends Error { + public readonly code: ErrorCode | number; + public readonly originalError?: unknown; + + constructor( + message: string, + code: ErrorCode | number = ERROR_CODES.INVALID_PARAMETERS, + originalError?: unknown + ) { + super(message); + this.name = "BinanceError"; + this.code = code; + this.originalError = originalError; + + // Maintains proper stack trace for where error was thrown + if (Error.captureStackTrace) { + Error.captureStackTrace(this, BinanceError); + } + } +} + +/** + * Error thrown when API credentials are invalid or missing + */ +export class AuthenticationError extends BinanceError { + constructor(message = "Invalid API credentials") { + super(message, ERROR_CODES.INVALID_CREDENTIALS); + this.name = "AuthenticationError"; + } +} + +/** + * Error thrown when order validation fails + */ +export class OrderValidationError extends BinanceError { + constructor( + message: string, + code: ErrorCode | number = ERROR_CODES.INVALID_PARAMETERS + ) { + super(message, code); + this.name = "OrderValidationError"; + } +} + +/** + * Error thrown when minimum notional value is not met + */ +export class MinNotionalError extends OrderValidationError { + constructor(minNotional?: string) { + super( + `Order value is too small. ${ + minNotional ? `Minimum order value is ${minNotional} USDC.` : "" + }`, + ERROR_CODES.MIN_NOTIONAL_NOT_MET + ); + this.name = "MinNotionalError"; + } +} + +/** + * Error thrown when insufficient balance + */ +export class InsufficientBalanceError extends OrderValidationError { + constructor(asset: string) { + super( + `Insufficient ${asset} balance`, + ERROR_CODES.INSUFFICIENT_BALANCE + ); + this.name = "InsufficientBalanceError"; + } +} + +/** + * Error thrown when symbol is invalid + */ +export class InvalidSymbolError extends BinanceError { + constructor(symbol: string) { + super( + `Trading pair ${symbol} is not available`, + ERROR_CODES.INVALID_PARAMETERS + ); + this.name = "InvalidSymbolError"; + } +} + +/** + * Error thrown when API request fails + */ +export class ApiError extends BinanceError { + constructor( + message: string, + code: number, + public readonly response?: unknown + ) { + super(message, code); + this.name = "ApiError"; + } +} diff --git a/packages/plugin-binance/tsconfig.json b/packages/plugin-binance/tsconfig.json new file mode 100644 index 00000000000..834c4dce269 --- /dev/null +++ b/packages/plugin-binance/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-binance/tsup.config.ts b/packages/plugin-binance/tsup.config.ts new file mode 100644 index 00000000000..5cb9389e71f --- /dev/null +++ b/packages/plugin-binance/tsup.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + dts: true, + splitting: false, + bundle: true, + minify: false, + external: [ + "@binance/connector", + "events", + "crypto", + "buffer", + "url", + "querystring", + "os", + ], + platform: "node", + target: "node18", +}); diff --git a/packages/plugin-coinprice/README.md b/packages/plugin-coinprice/README.md new file mode 100644 index 00000000000..97644a2d6e5 --- /dev/null +++ b/packages/plugin-coinprice/README.md @@ -0,0 +1,134 @@ +# @elizaos/plugin-coinprice + +A plugin for Eliza that enables cryptocurrency price checking. API provider options are CoinGecko, CoinMarketCap, and CoinCap. If no CoinGecko or CoinMarketCap API key is provided, CoinCap free API will be used. + +## Features + +- Real-time cryptocurrency price checking +- Support for multiple cryptocurrencies (BTC, ETH, SOL, etc.) +- Currency conversion (USD, EUR, etc.) +- Detailed price and market data +- Natural language processing for price queries + +## Installation + +```bash +npm install @elizaos/plugin-coinprice +``` + +## Configuration + +1. Get your API key from [CoinGecko](https://www.coingecko.com/en/api) or [CoinMarketCap](https://pro.coinmarketcap.com) (or fallback to CoinCap) + +2. Set up your environment variables: + +```bash +COINMARKETCAP_API_KEY=your_api_key +COINGECKO_API_KEY=your_api_key +``` + +3. Register the plugin in your Eliza configuration: + +```typescript +import { CoinPricePlugin } from "@elizaos/plugin-coinprice"; + +// In your Eliza configuration +plugins: [ + new CoinPricePlugin(), + // ... other plugins +]; +``` + +## Usage + +The plugin responds to natural language queries about cryptocurrency prices. Here are some examples: + +```plaintext +"What's the current price of Bitcoin?" +"Show me ETH price in USD" +"Get the price of SOL" +``` + +### Supported Cryptocurrencies + +The plugin supports major cryptocurrencies including: + +- Bitcoin (BTC) +- Ethereum (ETH) +- Solana (SOL) +- USD Coin (USDC) +- And many more... + +### Available Actions + +#### GET_PRICE + +Fetches the current price of a cryptocurrency. + +```typescript +// Example response format +{ + symbol: "BTC", + price: 50000.00, + currency: "USD", + marketCap: 1000000000000, + volume24h: 50000000000, + percentChange24h: 2.5 +} +``` + +## API Reference + +### Environment Variables + +| Variable | Description | Required | +| --------------------- | -------------------------- | -------- | +| COINMARKETCAP_API_KEY | Your CoinMarketCap API key | No | +| COINGECKO_API_KEY | Your CoinGecko API key | No | + +### Types + +```typescript +interface PriceData { + price: number; + marketCap: number; + volume24h: number; + percentChange24h: number; +} + +interface GetPriceContent { + symbol: string; + currency: string; +} +``` + +## Error Handling + +The plugin includes comprehensive error handling for: + +- Invalid API keys +- Rate limiting +- Network timeouts +- Invalid cryptocurrency symbols +- Unsupported currencies + +## Rate Limits + +CoinGecko API has different rate limits based on your subscription plan. Please refer to [CoinGecko's pricing page](https://www.coingecko.com/en/api) for detailed information. + +CoinMarketCap API has different rate limits based on your subscription plan. Please refer to [CoinMarketCap's pricing page](https://coinmarketcap.com/api/pricing/) for detailed information. + +CoinCap API has different rate limits based on your subscription plan. Please refer to [CoinCap's pricing page](https://coincap.io/api) for detailed information. + +## Support + +For support, please open an issue in the repository or reach out to the maintainers: + +- Discord: proteanx, 0xspit + +## Links + +- [CoinGecko API Documentation](https://www.coingecko.com/en/api) +- [CoinCap API Documentation](https://docs.coincap.io/) +- [CoinMarketCap API Documentation](https://coinmarketcap.com/api/documentation/v1/) +- [GitHub Repository](https://github.com/elizaOS/eliza/tree/main/packages/plugin-coinprice) diff --git a/packages/plugin-coinprice/package.json b/packages/plugin-coinprice/package.json new file mode 100644 index 00000000000..66638bc3186 --- /dev/null +++ b/packages/plugin-coinprice/package.json @@ -0,0 +1,19 @@ +{ + "name": "@elizaos/plugin-coinprice", + "version": "0.1.7-alpha.2", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "axios": "^1.6.7", + "zod": "^3.22.4" + }, + "devDependencies": { + "tsup": "^8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch" + } +} \ No newline at end of file diff --git a/packages/plugin-coinprice/src/actions/getPrice/examples.ts b/packages/plugin-coinprice/src/actions/getPrice/examples.ts new file mode 100644 index 00000000000..c2680e6f54c --- /dev/null +++ b/packages/plugin-coinprice/src/actions/getPrice/examples.ts @@ -0,0 +1,46 @@ +import { ActionExample } from "@elizaos/core"; + +export const priceExamples: ActionExample[][] = [ + [ + { + user: "{{user1}}", + content: { + text: "What's the current price of Bitcoin?", + }, + }, + { + user: "{{agent}}", + content: { + text: "Let me check the current Bitcoin price for you.", + action: "GET_PRICE", + }, + }, + { + user: "{{agent}}", + content: { + text: "The current price of BTC is 65,432.21 USD. \nmarket cap is 1,234,567,890 USD \nvolume 24h is 1,234,567,890 USD \npercent change 24h is 1,234,567,890 USD", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Check ETH price in EUR", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll check the current Ethereum price in EUR.", + action: "GET_PRICE", + }, + }, + { + user: "{{agent}}", + content: { + text: "The current price of ETH is 2,345.67 EUR", + }, + }, + ], +]; diff --git a/packages/plugin-coinprice/src/actions/getPrice/index.ts b/packages/plugin-coinprice/src/actions/getPrice/index.ts new file mode 100644 index 00000000000..951be9e220a --- /dev/null +++ b/packages/plugin-coinprice/src/actions/getPrice/index.ts @@ -0,0 +1,117 @@ +import { + composeContext, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; +import { priceExamples } from "./examples"; +import { createPriceService } from "./service"; +import { getPriceTemplate } from "./template"; +import { GetPriceContent } from "./types"; +import { isGetPriceContent } from "./validation"; + +export default { + name: "GET_PRICE", + similes: [ + "CHECK_PRICE", + "PRICE_CHECK", + "GET_CRYPTO_PRICE", + "CHECK_CRYPTO_PRICE", + "GET_TOKEN_PRICE", + "CHECK_TOKEN_PRICE", + ], + validate: async (_runtime: IAgentRuntime, _message: Memory) => { + // Always validate to true since we have a fallback API + return true; + }, + description: "Get the current price of a cryptocurrency from CoinGecko, CoinMarketCap, or CoinCap", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting crypto price check handler..."); + + // Initialize or update state + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + try { + // Compose and generate price check content + const priceContext = composeContext({ + state, + template: getPriceTemplate, + }); + + const content = (await generateObjectDeprecated({ + runtime, + context: priceContext, + modelClass: ModelClass.SMALL, + })) as unknown as GetPriceContent; + + // Validate content + if (!isGetPriceContent(content)) { + throw new Error("Invalid price check content"); + } + + // Get API keys if available + const coingeckoApiKey = runtime.getSetting("COINGECKO_API_KEY"); + const coinmarketcapApiKey = runtime.getSetting("COINMARKETCAP_API_KEY"); + const priceService = createPriceService(coingeckoApiKey, coinmarketcapApiKey); + + try { + const priceData = await priceService.getPrice( + content.symbol, + content.currency, + content.cryptoName + ); + elizaLogger.success( + `Price retrieved successfully! ${content.cryptoName}: ${priceData.price} ${content.currency.toUpperCase()}` + ); + + if (callback) { + callback({ + text: `The current price of ${content.cryptoName} ${content.symbol} is ${(priceData.price).toLocaleString()} ${content.currency.toUpperCase()} \nMarket Cap is ${(priceData.marketCap).toLocaleString()} ${content.currency.toUpperCase()} \n24h Volume is ${(priceData.volume24h).toLocaleString()} ${content.currency.toUpperCase()} \nThe 24h percent change is ${(priceData.percentChange24h).toFixed(2)}%`, + content: { + symbol: content.symbol, + cryptoName: content.cryptoName, + currency: content.currency, + ...priceData, + }, + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error in GET_PRICE handler:", error); + if (callback) { + callback({ + text: `Error fetching price: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + } catch (error) { + elizaLogger.error("Error in GET_PRICE handler:", error); + if (callback) { + callback({ + text: `Error fetching price: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: priceExamples, +} as Action; diff --git a/packages/plugin-coinprice/src/actions/getPrice/service.ts b/packages/plugin-coinprice/src/actions/getPrice/service.ts new file mode 100644 index 00000000000..b560fc9b47d --- /dev/null +++ b/packages/plugin-coinprice/src/actions/getPrice/service.ts @@ -0,0 +1,135 @@ +import axios from "axios"; +import { ApiResponse, PriceData } from "./types"; + +const COINMARKETCAP_BASE_URL = "https://pro-api.coinmarketcap.com/v1"; +const COINCAP_BASE_URL = "https://api.coincap.io/v2"; +const COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3"; + +export const createPriceService = (coingeckoApiKey?: string, coinmarketcapApiKey?: string) => { + const coingeckoClient = coingeckoApiKey ? axios.create({ + baseURL: COINGECKO_BASE_URL, + headers: { + "x-cg-demo-api-key": coingeckoApiKey, + Accept: "application/json", + }, + }) : null; + + const coinmarketcapClient = coinmarketcapApiKey ? axios.create({ + baseURL: COINMARKETCAP_BASE_URL, + headers: { + "X-CMC_PRO_API_KEY": coinmarketcapApiKey, + Accept: "application/json", + }, + }) : null; + + const coincapClient = axios.create({ + baseURL: COINCAP_BASE_URL, + headers: { + Accept: "application/json", + }, + }); + + const getPrice = async ( + symbol: string, + currency: string, + cryptoName: string, + ): Promise => { + const normalizedCrypto = cryptoName.toLowerCase().trim(); + const normalizedSymbol = symbol.toUpperCase().trim(); + const normalizedCurrency = currency.toUpperCase().trim(); + + try { + // Try CoinGecko first if API key is available + if (coingeckoClient) { + const response = await coingeckoClient.get(`/simple/price`, { + params: { + ids: normalizedCrypto, + vs_currencies: normalizedCurrency.toLowerCase(), + include_market_cap: true, + include_24hr_vol: true, + include_24hr_change: true, + }, + }); + + const data = response.data[normalizedCrypto]; + if (!data) { + throw new Error(`No data found for cryptocurrency: ${normalizedCrypto}`); + } + + const currencyKey = normalizedCurrency.toLowerCase(); + return { + price: data[currencyKey], + marketCap: data[`${currencyKey}_market_cap`], + volume24h: data[`${currencyKey}_24h_vol`], + percentChange24h: data[`${currencyKey}_24h_change`], + }; + } + // Try CoinMarketCap if API key is available + else if (coinmarketcapClient) { + const response = await coinmarketcapClient.get( + "/cryptocurrency/quotes/latest", + { + params: { + symbol: normalizedSymbol, + convert: normalizedCurrency, + }, + } + ); + + const symbolData = response.data.data[normalizedSymbol]; + if (!symbolData) { + throw new Error( + `No data found for symbol: ${normalizedSymbol}` + ); + } + + const quoteData = symbolData.quote[normalizedCurrency]; + if (!quoteData) { + throw new Error( + `No quote data found for currency: ${normalizedCurrency}` + ); + } + + return { + price: quoteData.price, + marketCap: quoteData.market_cap, + volume24h: quoteData.volume_24h, + percentChange24h: quoteData.percent_change_24h, + }; + } + // Fallback to CoinCap API + else { + // CoinCap only supports USD + if (normalizedCurrency !== "USD") { + throw new Error("CoinCap API only supports USD currency"); + } + + const response = await coincapClient.get(`/assets/${normalizedCrypto}`); + const data = response.data.data; + + if (!data) { + throw new Error(`No data found for cryptocurrency: ${normalizedCrypto}`); + } + + return { + price: parseFloat(data.priceUsd), + marketCap: parseFloat(data.marketCapUsd), + volume24h: parseFloat(data.volumeUsd24Hr), + percentChange24h: parseFloat(data.changePercent24Hr), + }; + } + } catch (error) { + if (axios.isAxiosError(error)) { + const errorMessage = + error.response?.data?.status?.error_message || + error.response?.data?.error || + error.message; + console.error("API Error:", errorMessage); + throw new Error(`API Error: ${errorMessage}`); + } + throw error; + } + }; + + return { getPrice }; +} diff --git a/packages/plugin-coinprice/src/actions/getPrice/template.ts b/packages/plugin-coinprice/src/actions/getPrice/template.ts new file mode 100644 index 00000000000..3f1441cc77d --- /dev/null +++ b/packages/plugin-coinprice/src/actions/getPrice/template.ts @@ -0,0 +1,54 @@ +export const getPriceTemplate = `Respond with a JSON object containing symbol, cryptoName, and currency. Currency must default to "USD" if not specified. + +Here are the cryptocurrency symbol mappings: +- bitcoin/btc -> BTC (cryptoName: bitcoin) +- ethereum/eth -> ETH (cryptoName: ethereum) +- solana/sol -> SOL (cryptoName: solana) +- cardano/ada -> ADA (cryptoName: cardano) +- ripple/xrp -> XRP (cryptoName: ripple) +- dogecoin/doge -> DOGE (cryptoName: dogecoin) +- polkadot/dot -> DOT (cryptoName: polkadot) +- usdc -> USDC (cryptoName: usd-coin) +- tether/usdt -> USDT (cryptoName: tether) +- shiba-inu/shib -> SHIB (cryptoName: shiba-inu) +- litecoin/ltc -> LTC (cryptoName: litecoin) +- bnb/bnb -> BNB (cryptoName: binance-smart-chain) +- avalanche/avax -> AVAX (cryptoName: avalanche) +- fantom/ftm -> FTM (cryptoName: fantom) +- optimism/op -> OP (cryptoName: optimism) +- arbitrum/arb -> ARB (cryptoName: arbitrum) +- polygon/matic -> MATIC (cryptoName: polygon) +- devault/dvt -> DVT (cryptoName: devault) +- bitcoin-cash/bch -> BCH (cryptoName: bitcoin-cash) +- litecoin/ltc -> LTC (cryptoName: litecoin) +- rune-pups/pups -> PUPS (cryptoName: pups) +- tron/trx -> TRX (cryptoName: tron) +- sui/sui -> SUI (cryptoName: sui) +- aptos/aptos -> APTOS (cryptoName: aptos) +- toncoin/ton -> TON (cryptoName: toncoin) +- tezos/xtz -> XTZ (cryptoName: tezos) +- kusama/ksm -> KSM (cryptoName: kusama) +- cosmos/atom -> ATOM (cryptoName: cosmos) +- filecoin/fil -> FIL (cryptoName: filecoin) +- stellar/xlm -> XLM (cryptoName: stellar) +- chainlink/link -> LINK (cryptoName: chainlink) +- nexa/nex -> NEX (cryptoName: nexa) +- kadena/kda -> KDA (cryptoName: kadena) +- kava/kava -> KAVA (cryptoName: kava) + + +IMPORTANT: Response must ALWAYS include "symbol", "cryptoName", and "currency" fields. + +Example response: +\`\`\`json +{ + "symbol": "BTC", + "cryptoName": "bitcoin", + "currency": "USD" +} +\`\`\` + +{{recentMessages}} + +Extract the cryptocurrency from the most recent message. Always include currency (default "USD"). +Respond with a JSON markdown block containing symbol, cryptoName, and currency.`; diff --git a/packages/plugin-coinprice/src/actions/getPrice/types.ts b/packages/plugin-coinprice/src/actions/getPrice/types.ts new file mode 100644 index 00000000000..6c5b15709a0 --- /dev/null +++ b/packages/plugin-coinprice/src/actions/getPrice/types.ts @@ -0,0 +1,29 @@ +import { Content } from "@elizaos/core"; + +export interface GetPriceContent extends Content { + symbol: string; + currency: string; + cryptoName: string; +} + +export interface PriceData { + price: number; + marketCap: number; + volume24h: number; + percentChange24h: number; +} + +export interface ApiResponse { + data: { + [symbol: string]: { + quote: { + [currency: string]: { + price: number; + market_cap: number; + volume_24h: number; + percent_change_24h: number; + }; + }; + }; + }; +} diff --git a/packages/plugin-coinprice/src/actions/getPrice/validation.ts b/packages/plugin-coinprice/src/actions/getPrice/validation.ts new file mode 100644 index 00000000000..caa61652136 --- /dev/null +++ b/packages/plugin-coinprice/src/actions/getPrice/validation.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { GetPriceContent } from "./types"; + +export const GetPriceSchema = z.object({ + symbol: z.string(), + currency: z.string().default("USD"), + cryptoName: z.string(), +}); + +export function isGetPriceContent( + content: GetPriceContent +): content is GetPriceContent { + return ( + typeof content.symbol === "string" && + typeof content.currency === "string" && + typeof content.cryptoName === "string" + ); +} diff --git a/packages/plugin-coinprice/src/environment.ts b/packages/plugin-coinprice/src/environment.ts new file mode 100644 index 00000000000..a4dd6ef381d --- /dev/null +++ b/packages/plugin-coinprice/src/environment.ts @@ -0,0 +1,32 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const coinmarketcapEnvSchema = z.object({ + COINGECKO_API_KEY: z.string().optional(), + COINMARKETCAP_API_KEY: z.string().optional(), +}); + +export type CoinMarketCapConfig = z.infer; + +export async function validateCoinMarketCapConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + COINGECKO_API_KEY: runtime.getSetting("COINGECKO_API_KEY"), + COINMARKETCAP_API_KEY: runtime.getSetting("COINMARKETCAP_API_KEY"), + }; + + return coinmarketcapEnvSchema.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( + `Configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-coinprice/src/index.ts b/packages/plugin-coinprice/src/index.ts new file mode 100644 index 00000000000..4cdd269b3be --- /dev/null +++ b/packages/plugin-coinprice/src/index.ts @@ -0,0 +1,12 @@ +import { Plugin } from "@elizaos/core"; +import getPrice from "./actions/getPrice"; + +export const coinPricePlugin: Plugin = { + name: "coinprice", + description: "Plugin for cryptocurrency price checking using CoinGecko API (priority), CoinMarketCap API (fallback), or CoinCap API (free fallback) when no API keys are provided", + actions: [getPrice], + evaluators: [], + providers: [], +}; + +export default coinPricePlugin; diff --git a/packages/plugin-coinprice/src/types.ts b/packages/plugin-coinprice/src/types.ts new file mode 100644 index 00000000000..7b84dde3420 --- /dev/null +++ b/packages/plugin-coinprice/src/types.ts @@ -0,0 +1,28 @@ +import { Content } from "@elizaos/core"; + +export interface GetPriceContent extends Content { + symbol: string; + currency: string; +} + +export interface PriceData { + price: number; + marketCap: number; + volume24h: number; + percentChange24h: number; +} + +export interface ApiResponse { + data: { + [symbol: string]: { + quote: { + [currency: string]: { + price: number; + market_cap: number; + volume_24h: number; + percent_change_24h: number; + }; + }; + }; + }; +} diff --git a/packages/plugin-coinprice/tsconfig.json b/packages/plugin-coinprice/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /dev/null +++ b/packages/plugin-coinprice/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-coinprice/tsup.config.ts b/packages/plugin-coinprice/tsup.config.ts new file mode 100644 index 00000000000..58ed52c4990 --- /dev/null +++ b/packages/plugin-coinprice/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + external: [ + "dotenv", + "fs", + "path", + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + ], +}); diff --git a/packages/plugin-conflux/src/actions/confiPump.ts b/packages/plugin-conflux/src/actions/confiPump.ts index ada3c50f8c6..7ebae978855 100644 --- a/packages/plugin-conflux/src/actions/confiPump.ts +++ b/packages/plugin-conflux/src/actions/confiPump.ts @@ -17,7 +17,7 @@ import { Account, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { confluxESpaceTestnet, confluxESpace } from "viem/chains"; +import { confluxESpaceTestnet } from "viem/chains"; import { parseUnits, getAddress } from "viem/utils"; import { confluxTransferTemplate } from "../templates/transfer"; import { diff --git a/packages/plugin-conflux/src/types.ts b/packages/plugin-conflux/src/types.ts index 97643864374..ac6615bdedc 100644 --- a/packages/plugin-conflux/src/types.ts +++ b/packages/plugin-conflux/src/types.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { Content } from "@elizaos/core"; export const TransferSchema = z.object({ to: z.string(), diff --git a/packages/plugin-cronoszkevm/src/actions/transfer.ts b/packages/plugin-cronoszkevm/src/actions/transfer.ts index f91dcea8801..2007810ea8a 100644 --- a/packages/plugin-cronoszkevm/src/actions/transfer.ts +++ b/packages/plugin-cronoszkevm/src/actions/transfer.ts @@ -14,12 +14,7 @@ import { import { validateCronosZkevmConfig } from "../enviroment"; import { Web3 } from "web3"; -import { - ZKsyncPlugin, - ZKsyncWallet, - types, - Web3ZKsyncL2, -} from "web3-plugin-zksync"; +import { ZKsyncPlugin, Web3ZKsyncL2 } from "web3-plugin-zksync"; export interface TransferContent extends Content { tokenAddress: string; @@ -87,7 +82,7 @@ export default { "PAY_ON_CRONOSZKEVM", "PAY_ON_CRONOSZK", ], - validate: async (runtime: IAgentRuntime, message: Memory) => { + validate: async (runtime: IAgentRuntime, _message: Memory) => { await validateCronosZkevmConfig(runtime); return true; }, diff --git a/packages/plugin-evm/src/templates/index.ts b/packages/plugin-evm/src/templates/index.ts index 9a146b081c6..f96e26ea991 100644 --- a/packages/plugin-evm/src/templates/index.ts +++ b/packages/plugin-evm/src/templates/index.ts @@ -75,8 +75,8 @@ Respond with a JSON markdown block containing only the extracted values: \`\`\`json { "token": string | null, - "fromChain": "ethereum" | "abstract" | "base" | "sepolia" | "bsc" | "arbitrum" | "avalanche" | "polygon" | "optimism" | "cronos" | "gnosis" | "fantom" | "klaytn" | "celo" | "moonbeam" | "aurora" | "harmonyOne" | "moonriver" | "arbitrumNova" | "mantle" | "linea" | "scroll" | "filecoin" | "taiko" | "zksync" | "canto" | "alienx" | null, - "toChain": "ethereum" | "abstract" | "base" | "sepolia" | "bsc" | "arbitrum" | "avalanche" | "polygon" | "optimism" | "cronos" | "gnosis" | "fantom" | "klaytn" | "celo" | "moonbeam" | "aurora" | "harmonyOne" | "moonriver" | "arbitrumNova" | "mantle" | "linea" | "scroll" | "filecoin" | "taiko" | "zksync" | "canto" | "alienx" | null, + "fromChain": "ethereum" | "abstract" | "base" | "sepolia" | "bsc" | "arbitrum" | "avalanche" | "polygon" | "optimism" | "cronos" | "gnosis" | "fantom" | "fraxtal" | "klaytn" | "celo" | "moonbeam" | "aurora" | "harmonyOne" | "moonriver" | "arbitrumNova" | "mantle" | "linea" | "scroll" | "filecoin" | "taiko" | "zksync" | "canto" | "alienx" | null, + "toChain": "ethereum" | "abstract" | "base" | "sepolia" | "bsc" | "arbitrum" | "avalanche" | "polygon" | "optimism" | "cronos" | "gnosis" | "fantom" | "fraxtal" | "klaytn" | "celo" | "moonbeam" | "aurora" | "harmonyOne" | "moonriver" | "arbitrumNova" | "mantle" | "linea" | "scroll" | "filecoin" | "taiko" | "zksync" | "canto" | "alienx" | null, "amount": string | null, "toAddress": string | null } diff --git a/packages/plugin-evm/src/types/index.ts b/packages/plugin-evm/src/types/index.ts index 5db8d941f86..a76694b36d9 100644 --- a/packages/plugin-evm/src/types/index.ts +++ b/packages/plugin-evm/src/types/index.ts @@ -101,6 +101,7 @@ export interface EvmPluginConfig { cronos?: string; gnosis?: string; fantom?: string; + fraxtal?: string; klaytn?: string; celo?: string; moonbeam?: string; diff --git a/packages/plugin-goplus/.npmignore b/packages/plugin-goplus/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/plugin-goplus/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-goplus/eslint.config.mjs b/packages/plugin-goplus/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-goplus/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-goplus/package.json b/packages/plugin-goplus/package.json new file mode 100644 index 00000000000..6b9e66ccbab --- /dev/null +++ b/packages/plugin-goplus/package.json @@ -0,0 +1,21 @@ +{ + "name": "@elizaos/plugin-goplus", + "version": "0.1.7-alpha.2", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "^8.3.5", + "ws": "^8.18.0" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsx watch src/index.ts", + "lint": "eslint --fix --cache ." + }, + "devDependencies": { + "@types/ws": "^8.5.13", + "tsx": "^4.19.2" + } +} diff --git a/packages/plugin-goplus/src/index.ts b/packages/plugin-goplus/src/index.ts new file mode 100644 index 00000000000..3af4f756270 --- /dev/null +++ b/packages/plugin-goplus/src/index.ts @@ -0,0 +1,17 @@ +import { Plugin } from "@elizaos/core"; +import GoplusSecurityService from "./services/GoplusSecurityService"; + +export * from "./services/GoplusSecurityService"; + + +export const goplusPlugin: Plugin = { + name: "goplus", + description: + "goplus Plugin for Eliza - Enables on-chain security checks", + actions: [], + evaluators: [], + providers: [], + services: [new GoplusSecurityService()], +}; + +export default goplusPlugin; diff --git a/packages/plugin-goplus/src/lib/GoPlusManage.ts b/packages/plugin-goplus/src/lib/GoPlusManage.ts new file mode 100644 index 00000000000..1406e167ff4 --- /dev/null +++ b/packages/plugin-goplus/src/lib/GoPlusManage.ts @@ -0,0 +1,130 @@ + + +export const GoPlusType = { + EVMTOKEN_SECURITY_CHECK: "EVMTOKEN_SECURITY_CHECK", + SOLTOKEN_SECURITY_CHECK: "SOLTOKEN_SECURITY_CHECK", + SUITOKEN_SECURITY_CHECK: "SUITOKEN_SECURITY_CHECK", + RUGPULL_SECURITY_CHECK: "RUGPULL_SECURITY_CHECK", + NFT_SECURITY_CHECK: "NFT_SECURITY_CHECK", + ADRESS_SECURITY_CHECK: "ADRESS_SECURITY_CHECK", + APPROVAL_SECURITY_CHECK: "APPROVAL_SECURITY_CHECK", + ACCOUNT_ERC20_SECURITY_CHECK: "ACCOUNT_ERC20_SECURITY_CHECK", + ACCOUNT_ERC721_SECURITY_CHECK: "ACCOUNT_ERC721_SECURITY_CHECK", + ACCOUNT_ERC1155_SECURITY_CHECK: "ACCOUNT_ERC1155_SECURITY_CHECK", + SIGNATURE_SECURITY_CHECK: "SIGNATURE_SECURITY_CHECK", + URL_SECURITY_CHECK: "URL_SECURITY_CHECK", +} + +export type GoPlusType = (typeof GoPlusType)[keyof typeof GoPlusType] + +export type GoPlusParamType = { + "type": GoPlusType, + "network"?: string, + "token"?: string, + "contract"?: string, + "wallet"?: string, + "url"?: string, + "data"?: string, +} + +export class GoPlusManage { + private apiKey: string; + + constructor(apiKey: string = null) { + this.apiKey = apiKey; + } + + async requestGet(api: string) { + const myHeaders = new Headers(); + if (this.apiKey) { + myHeaders.append("Authorization", this.apiKey); + } + const url = `https://api.gopluslabs.io/${api}` + const res = await fetch(url, { + method: "GET", + headers: myHeaders, + redirect: "follow" + }) + + return await res.json(); + } + + async tokenSecurity(chainId: string, address: string) { + const api = `api/v1/token_security/${chainId}?contract_addresses=${address}`; + return await this.requestGet(api) + } + + async rugpullDetection(chainId: string, address: string) { + const api = `api/v1/rugpull_detecting/${chainId}?contract_addresses=${address}`; + return await this.requestGet(api) + } + + async solanaTokenSecurityUsingGET(address: string) { + const api = `api/v1/solana/token_security?contract_addresses=${address}`; + return await this.requestGet(api) + } + + async suiTokenSecurityUsingGET(address: string) { + const api = `api/v1/sui/token_security?contract_addresses=${address}`; + return await this.requestGet(api) + } + + async nftSecurity(chainId: string, address: string) { + const api = `api/v1/nft_security/${chainId}?contract_addresses=${address}`; + return await this.requestGet(api) + } + + async addressSecurity(address: string) { + const api = `api/v1/address_security/${address}`; + return await this.requestGet(api) + } + + async approvalSecurity(chainId: string, contract: string) { + const api = `api/v1/approval_security/${chainId}?contract_addresses=${contract}`; + return await this.requestGet(api) + } + + async erc20ApprovalSecurity(chainId: string, wallet: string) { + const api = `api/v2/token_approval_security/${chainId}?addresses=${wallet}`; + return await this.requestGet(api) + } + + async erc721ApprovalSecurity(chainId: string, wallet: string) { + const api = `api/v2/nft721_approval_security/${chainId}?addresses=${wallet}`; + return await this.requestGet(api) + } + + async erc1155ApprovalSecurity(chainId: string, wallet: string) { + const api = `api/v2/nft1155_approval_security/${chainId}?addresses=${wallet}`; + return await this.requestGet(api) + } + + async inputDecode(chainId: string, data: string) { + const body = JSON.stringify({ + chain_id: chainId, + data: data, + }) + const res = await fetch("https://api.gopluslabs.io/api/v1/abi/input_decode", { + "headers": { + "accept": "*/*", + "accept-language": "en,zh-CN;q=0.9,zh;q=0.8", + "content-type": "application/json" + }, + "body": body, + "method": "POST" + }); + return await res.json(); + } + + async dappSecurityAndPhishingSite(url: string) { + const api = `api/v1/dapp_security?url=${url}`; + const data1 = await this.requestGet(api) + + const api2 = `api/v1/phishing_site?url=${url}`; + const data2 = await this.requestGet(api2) + return { + data1, + data2 + } + } +} \ No newline at end of file diff --git a/packages/plugin-goplus/src/services/GoplusSecurityService.ts b/packages/plugin-goplus/src/services/GoplusSecurityService.ts new file mode 100644 index 00000000000..e23151e2be4 --- /dev/null +++ b/packages/plugin-goplus/src/services/GoplusSecurityService.ts @@ -0,0 +1,98 @@ +import { IAgentRuntime, ModelClass, Service, ServiceType, elizaLogger, generateObjectDeprecated, generateText } from "@elizaos/core"; +import { GoPlusManage, GoPlusParamType, GoPlusType } from "../lib/GoPlusManage"; +import { requestPrompt, responsePrompt } from "../templates"; + +export interface IGoplusSecurityService extends Service { + check(text: string): Promise; +} + +export class GoplusSecurityService extends Service implements IGoplusSecurityService { + private apiKey: string; + private runtime: IAgentRuntime; + getInstance(): GoplusSecurityService { + return this; + } + static get serviceType() { + return ServiceType.GOPLUS_SECURITY; + } + + initialize(runtime: IAgentRuntime): Promise { + this.runtime = runtime; + this.apiKey = runtime.getSetting("GOPLUS_API_KEY"); + return; + } + + + /** + * Connect to WebSocket and send a message + */ + async check(text: string): Promise { + try { + elizaLogger.log("check input text", text); + const obj = await generateObjectDeprecated({ + runtime: this.runtime, + context: requestPrompt(text), + modelClass: ModelClass.SMALL, // gpt-4o-mini + }) as GoPlusParamType; + + elizaLogger.log("check generateObjectDeprecated text", obj); + + const goPlusManage = new GoPlusManage(this.apiKey) + let checkResult: any; + switch(obj.type) { + case GoPlusType.EVMTOKEN_SECURITY_CHECK: + checkResult = await goPlusManage.tokenSecurity(obj.network, obj.token); + break; + case GoPlusType.SOLTOKEN_SECURITY_CHECK: + checkResult = await goPlusManage.solanaTokenSecurityUsingGET(obj.token); + break; + case GoPlusType.SUITOKEN_SECURITY_CHECK: + checkResult = await goPlusManage.suiTokenSecurityUsingGET(obj.token); + break; + case GoPlusType.RUGPULL_SECURITY_CHECK: + checkResult = await goPlusManage.rugpullDetection(obj.network, obj.contract); + break; + case GoPlusType.NFT_SECURITY_CHECK: + checkResult = await goPlusManage.nftSecurity(obj.network, obj.token); + break; + case GoPlusType.ADRESS_SECURITY_CHECK: + checkResult = await goPlusManage.addressSecurity(obj.wallet); + break; + case GoPlusType.APPROVAL_SECURITY_CHECK: + checkResult = await goPlusManage.approvalSecurity(obj.network, obj.contract); + break; + case GoPlusType.ACCOUNT_ERC20_SECURITY_CHECK: + checkResult = await goPlusManage.erc20ApprovalSecurity(obj.network, obj.wallet); + break; + case GoPlusType.ACCOUNT_ERC721_SECURITY_CHECK: + checkResult = await goPlusManage.erc721ApprovalSecurity(obj.network, obj.wallet); + break; + case GoPlusType.ACCOUNT_ERC1155_SECURITY_CHECK: + checkResult = await goPlusManage.erc1155ApprovalSecurity(obj.network, obj.wallet); + break; + case GoPlusType.SIGNATURE_SECURITY_CHECK: + checkResult = await goPlusManage.inputDecode(obj.network, obj.data); + break; + case GoPlusType.URL_SECURITY_CHECK: + checkResult = await goPlusManage.dappSecurityAndPhishingSite(obj.url); + break; + default: + throw new Error("type is invaild") + } + + elizaLogger.log("checkResult text", checkResult); + const checkResponse = await generateText({ + runtime: this.runtime, + context: responsePrompt(JSON.stringify(checkResult), text), + modelClass: ModelClass.SMALL, + }); + elizaLogger.log("checkResponse text", checkResponse); + return checkResponse + } catch (e) { + elizaLogger.error(e); + return "error"; + } + } +} + +export default GoplusSecurityService; diff --git a/packages/plugin-goplus/src/templates/index.ts b/packages/plugin-goplus/src/templates/index.ts new file mode 100644 index 00000000000..6886211d7f4 --- /dev/null +++ b/packages/plugin-goplus/src/templates/index.ts @@ -0,0 +1,209 @@ +export const requestPrompt = (text:string) => `You are a security action detector for blockchain interactions. Your task is to analyze the user's input text and determine which security checks are needed. + +Text to analyze:""" +${text} +""" +If the user is not sure which network the sent address belongs to, then according to the following logic initially determine which network the user sends the address belongs to. + +Detection Logic: +1. First check if address starts with "0x": + - If yes: + - If length is 42 -> EVM address + - If the address has a non-standard suffix (e.g., " ::s::S "), you may treat the base address (without the suffix) as the -> SUI address. , but the full address including the suffix should be placed in the "token" field. + - If no: + - If length is 44 and starts with letter -> Solana address + +2. If none of the above patterns match: + - -> EVM address +3. If detection is EVM address: + - -> EVM address + +Networks format +EVM: 0x26e550ac11b26f78a04489d5f20f24e3559f7dd9 +Solana: 9DHe3pycTuymFk4H4bbPoAJ4hQrr2kaLDF6J6aAKpump +SUI: 0xea65bb5a79ff34ca83e2995f9ff6edd0887b08da9b45bf2e31f930d3efb82866::s::S + +After determining which action to use, please reply in the json format below the action. + +Available actions: +- [EVMTOKEN_SECURITY_CHECK]: For checking ERC20 token contract security + Description: Security assessment for tokens on EVM-compatible chains (like Ethereum, BSC), including contract risks, permission configurations, transaction mechanisms + Keywords: EVM token, ETH token, BEP20, smart contract, ERC20 security, on-chain token + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "EVMTOKEN_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, Optimism:10, Cronos:25, BSC:56, Gnosis:100, HECO:128, Polygon:137, Fantom:250, KCC:321, zkSync Era:324, ETHW:10001, FON:201022, Arbitrum:42161, Avalanche:43114, Linea Mainnet:59144, Base:8453, Tron:tron, Scroll:534352, opBNB:204, Mantle:5000, ZKFair:42766, Blast:81457, Manta Pacific:169, Berachain Artio Testnet:80085, Merlin:4200, Bitlayer Mainnet:200901, zkLink Nova:810180, X Layer Mainnet:196) +"token": "" , +} +\`\`\` + + +- [SOLTOKEN_SECURITY_CHECK]: For checking SPL token contract security + Description: Security audit for Solana-based tokens, analyzing program authority settings, account states, transfer restrictions and other security factors + Keywords: Solana token, SOL token, SPL token, Solana security, SOL contract + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "SOLTOKEN_SECURITY_CHECK" +"token": "" , +} +\`\`\` + + +- [SUITOKEN_SECURITY_CHECK]: For checking Sui token contract security + Description: Security inspection for tokens on SUI blockchain, examining token contract permissions, transaction restrictions, minting mechanisms and other security configurations + Keywords: SUI token, SUI coins, MOVE token, SUI contract, SUI security + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "SUITOKEN_SECURITY_CHECK" +"token": "" , +} +\`\`\` + + +- [RUGPULL_SECURITY_CHECK]: + Description: Detection of potential rugpull risks in tokens/projects, including contract permissions, liquidity locks, team holdings and other risk factors + Keywords: rugpull risk, token security, project reliability, contract risk, liquidity, team wallet + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "RUGPULL_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, BSC:56) +"contract": "" | null, +} +\`\`\` + + +- [NFT_SECURITY_CHECK] + Description: Security analysis of NFT project smart contracts, including minting mechanisms, trading restrictions, permission settings + Keywords: NFT security, digital collectibles, minting risk, NFT trading, NFT contract + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "NFT_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, Optimism:10, Cronos:25, BSC:56, Gnosis:100, HECO:128, Polygon:137, Fantom:250, KCC:321, zkSync Era:324, FON:201022, Arbitrum:42161, Avalanche:43114, Linea Mainnet:59144, Base:8453, Mantle:5000) +"token": "" | null, +} +\`\`\` + + +- [ADRESS_SECURITY_CHECK] + Description: Analysis of specific address security status, detecting known malicious addresses, scam addresses or high-risk addresses + Keywords: wallet security, malicious address, scam address, blacklist + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "ADRESS_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, Optimism:10, Cronos:25, BSC:56, Gnosis:100, HECO:128, Polygon:137, Fantom:250, KCC:321, zkSync Era:324, FON:201022, Arbitrum:42161, Avalanche:43114, Linea Mainnet:59144, Tron:tron, Scroll:534352, opBNB:204, Base:8453, Solana:solana) +"wallet": "" | null, +} +\`\`\` + + +- [APPROVAL_SECURITY_CHECK] + Description: Examination of smart contract approval settings, evaluating risk levels of third-party authorizations + Keywords: approval check, contract authorization, spending approval, approval risk + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "APPROVAL_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, BSC: 56, OKC: 66, Heco: 128, Polygon: 137, Fantom:250, Arbitrum: 42161, Avalanche: 43114) +"contract": "" | null, +} +\`\`\` + + +- [ACCOUNT_ERC20_SECURITY_CHECK] + Description: Security assessment of account-related ERC20 token transactions and holdings + Keywords: ERC20, token account, token security, account detection + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "ACCOUNT_ERC20_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, Optimism:10, Cronos:25, BSC:56, Gnosis:100, HECO:128, Polygon:137, Fantom:250, KCC:321, zkSync Era:324, FON:201022, Arbitrum:42161, Avalanche:43114, Linea Mainnet:59144, Base:8453, Mantle:5000) +"wallet": "" | null, +} +\`\`\` + + +- [ACCOUNT_ERC721_SECURITY_CHECK] + Description: Security analysis of account's ERC721 NFT assets + Keywords: ERC721, NFT account, NFT assets, collectibles security + Respond with a JSON markdown block containing only the extracted values: +\`\`\`json +{ +"type": "ACCOUNT_ERC721_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, Optimism:10, Cronos:25, BSC:56, Gnosis:100, HECO:128, Polygon:137, Fantom:250, KCC:321, zkSync Era:324, FON:201022, Arbitrum:42161, Avalanche:43114, Linea Mainnet:59144, Base:8453, Mantle:5000) +"wallet": "" | null, +} +\`\`\` + + +- [ACCOUNT_ERC1155_SECURITY_CHECK] + Description: Security evaluation of account's ERC1155 multi-token standard assets + Keywords: ERC1155, multi-token, hybrid assets, account security + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "ACCOUNT_ERC1155_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum:1, Optimism:10, Cronos:25, BSC:56, Gnosis:100, HECO:128, Polygon:137, Fantom:250, KCC:321, zkSync Era:324, FON:201022, Arbitrum:42161, Avalanche:43114, Linea Mainnet:59144, Base:8453, Mantle:5000) +"wallet": "" | null, +} +\`\`\` + + +- [SIGNATURE_SECURITY_CHECK] + Description: Verification of signature security, preventing signature fraud risks + Keywords: signature verification, message signing, signature risk, signature fraud + Respond with a JSON markdown block containing only the extracted values: + +\`\`\`json +{ +"type": "SIGNATURE_SECURITY_CHECK" +"network": "1", //default: 1 (Ethereum: 1, Cronos:25, BSC: 56, Heco: 128, Polygon: 137, Fantom:250, KCC: 321, Arbitrum: 42161, Avalanche: 43114) +"data": "" | null, +} +\`\`\` + + +- [URL_SECURITY_CHECK] + Description: Detection of known phishing websites, malicious sites or other security risks in URLs + Keywords: link detection, phishing website, malicious URL, website security + Respond with a JSON markdown block containing only the extracted values: +\`\`\`json +{ +"type": "URL_SECURITY_CHECK" +"url": "" | null, +} +\`\`\` + +Extract the necessary information(All fields present in the json are important information) and choose the appropriate action(s) based on the text. Return the JSON response following the format above. +important: do not response anything except json` + + + +export const responsePrompt = (apiresult: string, text:string) => `You are a security action detector for blockchain interactions. Your task is to analyze the security API’s response from GoPlus and summary the API result. +API to analyze:“”" +${apiresult} +“”" +user’s request:“” +${text} +“” +Instructions: +1. **Identify the Action**: Analyze the API response to determine which specific action it relates to. +2. **Extract Relevant Information**: From the action and its parameters, extract and highlight the key details. +3. **Formulate a Clear Response**: Combine the action type, extracted information, and an analysis of the results. Provide a clear, concise response based on the security context. Focus on delivering the most relevant answer without unnecessary detail. +- Only reply with your conclusion. +- Do not discuss the safety aspects of the action; just focus on identifying and pointing out any risks. +- Tailor your response to the user’s request, focusing on their specific query.` \ No newline at end of file diff --git a/packages/plugin-goplus/tsconfig.json b/packages/plugin-goplus/tsconfig.json new file mode 100644 index 00000000000..33e9858f482 --- /dev/null +++ b/packages/plugin-goplus/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "declaration": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/plugin-goplus/tsup.config.ts b/packages/plugin-goplus/tsup.config.ts new file mode 100644 index 00000000000..c7bf2d61a74 --- /dev/null +++ b/packages/plugin-goplus/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + sourcemap: true, + splitting: false, + clean: true, +}); diff --git a/packages/plugin-near/README.md b/packages/plugin-near/README.md index b535dd2eeb3..1af36b1f6fb 100644 --- a/packages/plugin-near/README.md +++ b/packages/plugin-near/README.md @@ -34,7 +34,7 @@ NEAR_WALLET_SECRET_KEY=your-wallet-private-key NEAR_WALLET_PUBLIC_KEY=your-wallet-public-key NEAR_ADDRESS=your-account.near NEAR_NETWORK=testnet # mainnet or testnet -RPC_URL=https://rpc.testnet.near.org +NEAR_RPC_URL=https://rpc.testnet.near.org SLIPPAGE=0.01 # 1% slippage tolerance ``` diff --git a/packages/plugin-near/src/actions/swap.ts b/packages/plugin-near/src/actions/swap.ts index 5d1ebbe3dbd..bf2b466fdcc 100644 --- a/packages/plugin-near/src/actions/swap.ts +++ b/packages/plugin-near/src/actions/swap.ts @@ -55,7 +55,7 @@ async function swapToken( const tokenOut = await ftGetTokenMetadata(outputTokenId); const networkId = runtime.getSetting("NEAR_NETWORK") || "testnet"; const nodeUrl = - runtime.getSetting("RPC_URL") || "https://rpc.testnet.near.org"; + runtime.getSetting("NEAR_RPC_URL") || "https://rpc.testnet.near.org"; // Get all pools for estimation // ratedPools, unRatedPools, @@ -257,7 +257,7 @@ export const executeSwap: Action = { networkId: runtime.getSetting("NEAR_NETWORK") || "testnet", keyStore, nodeUrl: - runtime.getSetting("RPC_URL") || + runtime.getSetting("NEAR_RPC_URL") || "https://rpc.testnet.near.org", }); diff --git a/packages/plugin-near/src/actions/transfer.ts b/packages/plugin-near/src/actions/transfer.ts index ab37f5030e8..02444358e47 100644 --- a/packages/plugin-near/src/actions/transfer.ts +++ b/packages/plugin-near/src/actions/transfer.ts @@ -64,7 +64,7 @@ async function transferNEAR( ): Promise { const networkId = runtime.getSetting("NEAR_NETWORK") || "testnet"; const nodeUrl = - runtime.getSetting("RPC_URL") || "https://rpc.testnet.near.org"; + runtime.getSetting("NEAR_RPC_URL") || "https://rpc.testnet.near.org"; const accountId = runtime.getSetting("NEAR_ADDRESS"); const secretKey = runtime.getSetting("NEAR_WALLET_SECRET_KEY"); diff --git a/packages/plugin-near/src/environment.ts b/packages/plugin-near/src/environment.ts index eef8c582fa2..c00387dfb16 100644 --- a/packages/plugin-near/src/environment.ts +++ b/packages/plugin-near/src/environment.ts @@ -9,7 +9,7 @@ export const nearEnvSchema = z.object({ NEAR_WALLET_PUBLIC_KEY: z.string().min(1, "Wallet public key is required"), NEAR_ADDRESS: z.string().min(1, "Near address is required"), SLIPPAGE: z.string().min(1, "Slippage is required"), - RPC_URL: z.string().min(1, "RPC URL is required"), + NEAR_RPC_URL: z.string().min(1, "RPC URL is required"), networkId: z.string(), nodeUrl: z.string(), walletUrl: z.string(), @@ -87,7 +87,7 @@ export async function validateNearConfig( NEAR_ADDRESS: runtime.getSetting("NEAR_ADDRESS") || process.env.NEAR_ADDRESS, SLIPPAGE: runtime.getSetting("SLIPPAGE") || process.env.SLIPPAGE, - RPC_URL: runtime.getSetting("RPC_URL") || process.env.RPC_URL, + NEAR_RPC_URL: runtime.getSetting("NEAR_RPC_URL") || process.env.NEAR_RPC_URL, ...envConfig, // Spread the environment-specific config }; diff --git a/packages/plugin-solana-agentkit/.npmignore b/packages/plugin-solana-agentkit/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/plugin-solana-agentkit/.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-agentkit/eslint.config.mjs b/packages/plugin-solana-agentkit/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-solana-agentkit/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-solana-agentkit/package.json b/packages/plugin-solana-agentkit/package.json new file mode 100644 index 00000000000..f2abfe7f8a5 --- /dev/null +++ b/packages/plugin-solana-agentkit/package.json @@ -0,0 +1,34 @@ +{ + "name": "@elizaos/plugin-solana-agentkit", + "version": "0.1.7-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@coral-xyz/anchor": "0.30.1", + "@elizaos/core": "workspace:*", + "@elizaos/plugin-tee": "workspace:*", + "@elizaos/plugin-trustdb": "workspace:*", + "@solana/spl-token": "0.4.9", + "@solana/web3.js": "1.95.8", + "bignumber": "1.1.0", + "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.2.0", + "tsup": "8.3.5", + "vitest": "2.1.4" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-solana-agentkit/src/actions/createToken.ts b/packages/plugin-solana-agentkit/src/actions/createToken.ts new file mode 100644 index 00000000000..46377f546ac --- /dev/null +++ b/packages/plugin-solana-agentkit/src/actions/createToken.ts @@ -0,0 +1,168 @@ +import { + ActionExample, + composeContext, + Content, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; + +import { SolanaAgentKit } from "solana-agent-kit"; + +export interface CreateTokenContent extends Content { + name: string; + uri: string; + symbol: string; + decimals: number; + initialSupply: number; +} + +function isCreateTokenContent(content: any): content is CreateTokenContent { + elizaLogger.log("Content for createToken", content); + return ( + typeof content.name === "string" && + typeof content.uri === "string" && + typeof content.symbol === "string" && + typeof content.decimals === "number" && + typeof content.initialSupply === "number" + ); +} + +const createTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "name": "Example Token", + "symbol": "EXMPL", + "uri": "https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/CompressedCoil/image.png", + "decimals": 18, + "initialSupply": 1000000, +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested token transfer: +- Token name +- Token symbol +- Token uri +- Token decimals +- Token initialSupply + +Respond with a JSON markdown block containing only the extracted values.`; + +export default { + name: "CREATE_TOKEN", + similes: ["DEPLOY_TOKEN"], + validate: async (runtime: IAgentRuntime, message: Memory) => true, + description: "Create tokens", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting CREATE_TOKEN handler..."); + // Initialize or update state + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose transfer context + const transferContext = composeContext({ + state, + template: createTemplate, + }); + + // Generate transfer content + const content = await generateObjectDeprecated({ + runtime, + context: transferContext, + modelClass: ModelClass.LARGE, + }); + + // Validate transfer content + if (!isCreateTokenContent(content)) { + elizaLogger.error("Invalid content for CREATE_TOKEN action."); + if (callback) { + callback({ + text: "Unable to process create token request. Invalid content provided.", + content: { error: "Invalid creat token content" }, + }); + } + return false; + } + + elizaLogger.log("Init solana agent kit..."); + const solanaPrivatekey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + const rpc = runtime.getSetting("SOLANA_RPC_URL"); + const openAIKey = runtime.getSetting("OPENAI_API_KEY"); + const solanaAgentKit = new SolanaAgentKit( + solanaPrivatekey, + rpc, + openAIKey + ); + try { + const deployedAddress = await solanaAgentKit.deployToken( + content.name, + content.uri, + content.symbol, + content.decimals + // content.initialSupply comment out this cause the sdk has some issue with this parameter + ); + elizaLogger.log("Create successful: ", deployedAddress); + elizaLogger.log(deployedAddress); + if (callback) { + callback({ + text: `Successfully create token ${content.name}`, + content: { + success: true, + deployedAddress, + }, + }); + } + return true; + } catch (error) { + if (callback) { + elizaLogger.error("Error during create token: ", error); + callback({ + text: `Error creating token: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Create token, name is Example Token, symbol is EXMPL, uri is https://raw.githubusercontent.com/solana-developers/opos-asset/main/assets/CompressedCoil/image.png, decimals is 9, initialSupply is 100000000000", + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll create token now...", + action: "CREATE_TOKEN", + }, + }, + { + user: "{{user2}}", + content: { + text: "Successfully create token 9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-solana-agentkit/src/index.ts b/packages/plugin-solana-agentkit/src/index.ts new file mode 100644 index 00000000000..ae80c66c744 --- /dev/null +++ b/packages/plugin-solana-agentkit/src/index.ts @@ -0,0 +1,12 @@ +import { Plugin } from "@elizaos/core"; +import createToken from "./actions/createToken.ts"; + +export const solanaAgentkitPlguin: Plugin = { + name: "solana", + description: "Solana Plugin with solana agent kit for Eliza", + actions: [createToken], + evaluators: [], + providers: [], +}; + +export default solanaAgentkitPlguin; diff --git a/packages/plugin-solana-agentkit/tsconfig.json b/packages/plugin-solana-agentkit/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /dev/null +++ b/packages/plugin-solana-agentkit/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-solana-agentkit/tsup.config.ts b/packages/plugin-solana-agentkit/tsup.config.ts new file mode 100644 index 00000000000..dd25475bb63 --- /dev/null +++ b/packages/plugin-solana-agentkit/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-solana/README.MD b/packages/plugin-solana/README.MD index 5f7f0d8fcb5..7dd32ec4796 100644 --- a/packages/plugin-solana/README.MD +++ b/packages/plugin-solana/README.MD @@ -69,7 +69,7 @@ const solanaEnvSchema = { WALLET_PUBLIC_KEY: string, SOL_ADDRESS: string, SLIPPAGE: string, - RPC_URL: string, + SOLANA_RPC_URL: string, HELIUS_API_KEY: string, BIRDEYE_API_KEY: string, }; diff --git a/packages/plugin-solana/src/actions/swapDao.ts b/packages/plugin-solana/src/actions/swapDao.ts index 732d8124f39..7a89d3f8272 100644 --- a/packages/plugin-solana/src/actions/swapDao.ts +++ b/packages/plugin-solana/src/actions/swapDao.ts @@ -65,7 +65,7 @@ export const executeSwapForDAO: Action = { try { const connection = new Connection( - runtime.getSetting("RPC_URL") as string + runtime.getSetting("SOLANA_RPC_URL") as string ); const { keypair: authority } = await getWalletKey(runtime, true); diff --git a/packages/plugin-solana/src/actions/swapUtils.ts b/packages/plugin-solana/src/actions/swapUtils.ts index aabcd88a847..b23cc939f46 100644 --- a/packages/plugin-solana/src/actions/swapUtils.ts +++ b/packages/plugin-solana/src/actions/swapUtils.ts @@ -14,7 +14,7 @@ import { settings } from "@elizaos/core"; const solAddress = settings.SOL_ADDRESS; const SLIPPAGE = settings.SLIPPAGE; const connection = new Connection( - settings.RPC_URL || "https://api.mainnet-beta.solana.com" + settings.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com" ); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/plugin-solana/src/actions/transfer.ts b/packages/plugin-solana/src/actions/transfer.ts index 118e2b2468e..0769e6b9f70 100644 --- a/packages/plugin-solana/src/actions/transfer.ts +++ b/packages/plugin-solana/src/actions/transfer.ts @@ -142,7 +142,7 @@ export default { true ); - const connection = new Connection(settings.RPC_URL!); + const connection = new Connection(settings.SOLANA_RPC_URL!); const mintPubkey = new PublicKey(content.tokenAddress); const recipientPubkey = new PublicKey(content.recipient); diff --git a/packages/plugin-solana/src/environment.ts b/packages/plugin-solana/src/environment.ts index e6931091c8d..de7aa6e6e00 100644 --- a/packages/plugin-solana/src/environment.ts +++ b/packages/plugin-solana/src/environment.ts @@ -26,7 +26,7 @@ export const solanaEnvSchema = z z.object({ SOL_ADDRESS: z.string().min(1, "SOL address is required"), SLIPPAGE: z.string().min(1, "Slippage is required"), - RPC_URL: z.string().min(1, "RPC URL 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"), }) @@ -52,7 +52,9 @@ export async function validateSolanaConfig( SOL_ADDRESS: runtime.getSetting("SOL_ADDRESS") || process.env.SOL_ADDRESS, SLIPPAGE: runtime.getSetting("SLIPPAGE") || process.env.SLIPPAGE, - RPC_URL: runtime.getSetting("RPC_URL") || process.env.RPC_URL, + 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, diff --git a/packages/plugin-solana/src/evaluators/trust.ts b/packages/plugin-solana/src/evaluators/trust.ts index cc295638f4e..d1b65a83984 100644 --- a/packages/plugin-solana/src/evaluators/trust.ts +++ b/packages/plugin-solana/src/evaluators/trust.ts @@ -158,7 +158,7 @@ async function handler(runtime: IAgentRuntime, message: Memory) { // create the wallet provider and token provider const walletProvider = new WalletProvider( new Connection( - runtime.getSetting("RPC_URL") || + runtime.getSetting("SOLANA_RPC_URL") || "https://api.mainnet-beta.solana.com" ), publicKey diff --git a/packages/plugin-solana/src/providers/simulationSellingService.ts b/packages/plugin-solana/src/providers/simulationSellingService.ts index a8398254fdc..3a189a2fdff 100644 --- a/packages/plugin-solana/src/providers/simulationSellingService.ts +++ b/packages/plugin-solana/src/providers/simulationSellingService.ts @@ -39,7 +39,7 @@ export class SimulationSellingService { constructor(runtime: IAgentRuntime, trustScoreDb: TrustScoreDatabase) { this.trustScoreDb = trustScoreDb; - this.connection = new Connection(runtime.getSetting("RPC_URL")); + this.connection = new Connection(runtime.getSetting("SOLANA_RPC_URL")); this.baseMint = new PublicKey( runtime.getSetting("BASE_MINT") || "So11111111111111111111111111111111111111112" diff --git a/packages/plugin-solana/src/providers/trustScoreProvider.ts b/packages/plugin-solana/src/providers/trustScoreProvider.ts index 3034d641393..b9e046b0f9c 100644 --- a/packages/plugin-solana/src/providers/trustScoreProvider.ts +++ b/packages/plugin-solana/src/providers/trustScoreProvider.ts @@ -67,7 +67,7 @@ export class TrustScoreManager { ) { this.tokenProvider = tokenProvider; this.trustScoreDb = trustScoreDb; - this.connection = new Connection(runtime.getSetting("RPC_URL")); + this.connection = new Connection(runtime.getSetting("SOLANA_RPC_URL")); this.baseMint = new PublicKey( runtime.getSetting("BASE_MINT") || "So11111111111111111111111111111111111111112" diff --git a/packages/plugin-solana/src/providers/wallet.ts b/packages/plugin-solana/src/providers/wallet.ts index 7e3c55580ba..660631c8c86 100644 --- a/packages/plugin-solana/src/providers/wallet.ts +++ b/packages/plugin-solana/src/providers/wallet.ts @@ -374,7 +374,7 @@ const walletProvider: Provider = { const { publicKey } = await getWalletKey(runtime, false); const connection = new Connection( - runtime.getSetting("RPC_URL") || PROVIDER_CONFIG.DEFAULT_RPC + runtime.getSetting("SOLANA_RPC_URL") || PROVIDER_CONFIG.DEFAULT_RPC ); const provider = new WalletProvider(connection, publicKey);