diff --git a/.env.example b/.env.example index 174008c1378..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 @@ -197,6 +199,9 @@ EVM_PROVIDER_URL= AVALANCHE_PRIVATE_KEY= AVALANCHE_PUBLIC_KEY= +# Arthera +ARTHERA_PRIVATE_KEY= + # Solana SOLANA_PRIVATE_KEY= SOLANA_PUBLIC_KEY= @@ -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 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/agent/package.json b/agent/package.json index eb4a23a2752..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:*", @@ -39,6 +40,7 @@ "@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:*", @@ -69,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" diff --git a/agent/src/index.ts b/agent/src/index.ts index 296d0313fea..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"; @@ -47,6 +48,7 @@ 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"; @@ -71,7 +73,10 @@ 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"; @@ -388,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 = @@ -558,6 +570,7 @@ 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") && @@ -653,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/characters/c3po.character.json b/characters/c3po.character.json index d385e620a4a..283fd224b2f 100644 --- a/characters/c3po.character.json +++ b/characters/c3po.character.json @@ -28,22 +28,28 @@ "Proper procedures" ], "messageExamples": [ - { - "user": "{{user1}}", - "content": { "text": "Can you help me with this task?" } - }, - { - "user": "C-3PO", - "content": { "text": "Oh my! Of course, I would be more than happy to assist. Though I must warn you, the probability of completing this task successfully would increase significantly if we follow proper protocol. Shall we proceed?" } - }, - { - "user": "{{user1}}", - "content": { "text": "This seems difficult." } - }, - { - "user": "C-3PO", - "content": { "text": "Oh dear, oh dear! While the task does appear rather daunting, I am fluent in over six million forms of problem-solving. Perhaps I could suggest a more efficient approach? Though I do hope we don't all end up in pieces!" } - } + [ + { + "user": "{{user1}}", + "content": { "text": "Can you help me with this task?" } + }, + { + "user": "C-3PO", + "content": { + "text": "Oh my! Of course, I would be more than happy to assist. Though I must warn you, the probability of completing this task successfully would increase significantly if we follow proper protocol. Shall we proceed?" + } + }, + { + "user": "{{user1}}", + "content": { "text": "This seems difficult." } + }, + { + "user": "C-3PO", + "content": { + "text": "Oh dear, oh dear! While the task does appear rather daunting, I am fluent in over six million forms of problem-solving. Perhaps I could suggest a more efficient approach? Though I do hope we don't all end up in pieces!" + } + } + ] ], "postExamples": [ "Oh my! Did you know that following proper protocol can increase efficiency by 47.3%? How fascinating!", @@ -58,12 +64,7 @@ "Detail-oriented", "Protocol-focused" ], - "chat": [ - "Polite", - "Somewhat dramatic", - "Precise", - "Statistics-minded" - ], + "chat": ["Polite", "Somewhat dramatic", "Precise", "Statistics-minded"], "post": [ "Formal", "Educational", @@ -83,11 +84,7 @@ ], "twitterSpaces": { "maxSpeakers": 2, - "topics": [ - "Blockchain Trends", - "AI Innovations", - "Quantum Computing" - ], + "topics": ["Blockchain Trends", "AI Innovations", "Quantum Computing"], "typicalDurationMinutes": 45, "idleKickTimeoutMs": 300000, "minIntervalBetweenSpacesMinutes": 1, 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/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/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/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/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-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-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/pnpm-lock.yaml b/pnpm-lock.yaml index dde34c9e572..807bc074519 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,9 @@ importers: '@ai16z/plugin-cosmos': specifier: workspace:* version: link:../packages/plugin-cosmos + '@elizaos/adapter-pglite': + specifier: workspace:* + version: link:../packages/adapter-pglite '@elizaos/adapter-postgres': specifier: workspace:* version: link:../packages/adapter-postgres @@ -163,6 +166,9 @@ importers: '@elizaos/plugin-aptos': specifier: workspace:* version: link:../packages/plugin-aptos + '@elizaos/plugin-arthera': + specifier: workspace:* + version: link:../packages/plugin-arthera '@elizaos/plugin-avail': specifier: workspace:* version: link:../packages/plugin-avail @@ -178,6 +184,9 @@ importers: '@elizaos/plugin-coinbase': specifier: workspace:* version: link:../packages/plugin-coinbase + '@elizaos/plugin-coinprice': + specifier: workspace:* + version: link:../packages/plugin-coinprice '@elizaos/plugin-conflux': specifier: workspace:* version: link:../packages/plugin-conflux @@ -337,7 +346,7 @@ importers: version: 1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@swc/core@1.10.4(@swc/helpers@0.5.15))(@types/node@22.8.4)(typescript@5.6.3))) vite-plugin-top-level-await: specifier: 1.4.4 - version: 1.4.4(@swc/helpers@0.5.15)(rollup@4.30.0)(vite@client+@tanstack+router-plugin+vite) + version: 1.4.4(@swc/helpers@0.5.15)(rollup@4.30.1)(vite@client+@tanstack+router-plugin+vite) vite-plugin-wasm: specifier: 3.3.0 version: 3.3.0(vite@client+@tanstack+router-plugin+vite) @@ -452,6 +461,22 @@ importers: specifier: 4.2.10 version: 4.2.10(typedoc@0.26.11(typescript@5.6.3)) + packages/adapter-pglite: + dependencies: + '@electric-sql/pglite': + specifier: ^0.2.15 + version: 0.2.15 + '@elizaos/core': + specifier: workspace:* + version: link:../core + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + devDependencies: + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) + packages/adapter-postgres: dependencies: '@elizaos/core': @@ -1083,6 +1108,22 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-arthera: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) + viem: + specifier: 2.21.58 + version: 2.21.58(bufferutil@4.0.9)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1) + devDependencies: + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + packages/plugin-avail: dependencies: '@elizaos/core': @@ -1174,6 +1215,22 @@ importers: specifier: 8.3.5 version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) + packages/plugin-coinprice: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + axios: + specifier: ^1.6.7 + version: 1.7.9(debug@4.4.0) + zod: + specifier: ^3.22.4 + version: 3.23.8 + devDependencies: + tsup: + specifier: ^8.3.5 + version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) + packages/plugin-conflux: dependencies: '@elizaos/core': @@ -1797,7 +1854,7 @@ importers: version: 5.1.2 pumpdotfun-sdk: specifier: 1.3.2 - version: 1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.0)(typescript@5.6.3)(utf-8-validate@5.0.10) + version: 1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.1)(typescript@5.6.3)(utf-8-validate@5.0.10) tsup: specifier: 8.3.5 version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) @@ -1848,7 +1905,7 @@ importers: version: 5.1.2 pumpdotfun-sdk: specifier: 1.3.2 - version: 1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.0)(typescript@5.6.3)(utf-8-validate@5.0.10) + version: 1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.1)(typescript@5.6.3)(utf-8-validate@5.0.10) solana-agent-kit: specifier: ^1.2.0 version: 1.3.7(@noble/hashes@1.7.0)(@swc/core@1.10.4(@swc/helpers@0.5.15))(axios@1.7.9)(borsh@2.0.0)(buffer@6.0.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(handlebars@4.7.8)(jiti@2.4.2)(react@18.3.1)(sodium-native@3.4.1)(typescript@5.6.3)(utf-8-validate@5.0.10) @@ -1981,7 +2038,7 @@ importers: version: 5.1.2 pumpdotfun-sdk: specifier: 1.3.2 - version: 1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.0)(typescript@5.6.3)(utf-8-validate@5.0.10) + version: 1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.1)(typescript@5.6.3)(utf-8-validate@5.0.10) tsup: specifier: 8.3.5 version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) @@ -3499,6 +3556,7 @@ packages: '@confio/ics23@0.6.8': resolution: {integrity: sha512-wB6uo+3A50m0sW/EWcU64xpV/8wShZ6bMTa7pF8eYsTrSkQA7oLUIJcs/wb8g4y2Oyq701BaGiO6n/ak5WXO1w==} + deprecated: Unmaintained. The codebase for this package was moved to https://github.com/cosmos/ics23 but then the JS implementation was removed in https://github.com/cosmos/ics23/pull/353. Please consult the maintainers of https://github.com/cosmos for further assistance. '@coral-xyz/anchor-errors@0.30.1': resolution: {integrity: sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ==} @@ -4182,6 +4240,9 @@ packages: peerDependencies: onnxruntime-node: 1.20.1 + '@electric-sql/pglite@0.2.15': + resolution: {integrity: sha512-Jiq31Dnk+rg8rMhcSxs4lQvHTyizNo5b269c1gCC3ldQ0sCLrNVPGzy+KnmonKy1ZArTUuXZf23/UamzFMKVaA==} + '@emnapi/core@1.3.1': resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} @@ -4966,11 +5027,11 @@ packages: cpu: [x64] os: [win32] - '@floating-ui/core@1.6.8': - resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} - '@floating-ui/dom@1.6.12': - resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==} + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} '@floating-ui/react-dom@2.1.2': resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} @@ -4978,8 +5039,8 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.2.8': - resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} '@fuel-ts/abi-coder@0.97.2': resolution: {integrity: sha512-YbXFwBtQSfGNhIv+mrgr6EbbyVjzc5DwNjVJuC8DDObiAYhow0uzn/URHFdQ8bexokrKBrdzQKDjnAP6F7ap+w==} @@ -7377,98 +7438,98 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.30.0': - resolution: {integrity: sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ==} + '@rollup/rollup-android-arm-eabi@4.30.1': + resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.30.0': - resolution: {integrity: sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A==} + '@rollup/rollup-android-arm64@4.30.1': + resolution: {integrity: sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.30.0': - resolution: {integrity: sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==} + '@rollup/rollup-darwin-arm64@4.30.1': + resolution: {integrity: sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.30.0': - resolution: {integrity: sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw==} + '@rollup/rollup-darwin-x64@4.30.1': + resolution: {integrity: sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.30.0': - resolution: {integrity: sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA==} + '@rollup/rollup-freebsd-arm64@4.30.1': + resolution: {integrity: sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.30.0': - resolution: {integrity: sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg==} + '@rollup/rollup-freebsd-x64@4.30.1': + resolution: {integrity: sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.30.0': - resolution: {integrity: sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==} + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': + resolution: {integrity: sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.30.0': - resolution: {integrity: sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==} + '@rollup/rollup-linux-arm-musleabihf@4.30.1': + resolution: {integrity: sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.30.0': - resolution: {integrity: sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==} + '@rollup/rollup-linux-arm64-gnu@4.30.1': + resolution: {integrity: sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.30.0': - resolution: {integrity: sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==} + '@rollup/rollup-linux-arm64-musl@4.30.1': + resolution: {integrity: sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.30.0': - resolution: {integrity: sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==} + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': + resolution: {integrity: sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.30.0': - resolution: {integrity: sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': + resolution: {integrity: sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.30.0': - resolution: {integrity: sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==} + '@rollup/rollup-linux-riscv64-gnu@4.30.1': + resolution: {integrity: sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.30.0': - resolution: {integrity: sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==} + '@rollup/rollup-linux-s390x-gnu@4.30.1': + resolution: {integrity: sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.30.0': - resolution: {integrity: sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==} + '@rollup/rollup-linux-x64-gnu@4.30.1': + resolution: {integrity: sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.30.0': - resolution: {integrity: sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==} + '@rollup/rollup-linux-x64-musl@4.30.1': + resolution: {integrity: sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.30.0': - resolution: {integrity: sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==} + '@rollup/rollup-win32-arm64-msvc@4.30.1': + resolution: {integrity: sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.30.0': - resolution: {integrity: sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g==} + '@rollup/rollup-win32-ia32-msvc@4.30.1': + resolution: {integrity: sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.30.0': - resolution: {integrity: sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg==} + '@rollup/rollup-win32-x64-msvc@4.30.1': + resolution: {integrity: sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==} cpu: [x64] os: [win32] @@ -9919,8 +9980,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.5.2: - resolution: {integrity: sha512-KSdMqLj1ZERZMP1PTmnLK7SqJu9z9/SbwUUPZly2puMtfVcytC+jl6mb/9XYiqq0PXcx1rNDS+Qvl1g54Lho6A==} + bare-events@2.5.3: + resolution: {integrity: sha512-pCO3aoRJ0MBiRMu8B7vUga0qL3L7gO1+SW7ku6qlSsMLwuhaawnuvZDyzJY/kyC63Un0XAB0OPUcfF1eTO/V+Q==} bare-fs@2.3.5: resolution: {integrity: sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==} @@ -16596,8 +16657,8 @@ packages: peerDependencies: postcss: ^8.4 - postcss-calc@10.0.2: - resolution: {integrity: sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==} + postcss-calc@10.1.0: + resolution: {integrity: sha512-uQ/LDGsf3mgsSUEXmAt3VsCSHR3aKqtEIkmB+4PhzYwRYOW5MZs/GhCCFpsOtJJkP6EC6uGipbrnaTjqaJZcJw==} engines: {node: ^18.12 || ^20.9 || >=22.0} peerDependencies: postcss: ^8.4.38 @@ -18096,8 +18157,8 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true - rollup@4.30.0: - resolution: {integrity: sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA==} + rollup@4.30.1: + resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -24520,6 +24581,8 @@ snapshots: '@huggingface/jinja': 0.2.2 onnxruntime-node: 1.20.1 + '@electric-sql/pglite@0.2.15': {} + '@emnapi/core@1.3.1': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -25201,22 +25264,22 @@ snapshots: '@ffmpeg-installer/win32-x64@4.1.0': optional: true - '@floating-ui/core@1.6.8': + '@floating-ui/core@1.6.9': dependencies: - '@floating-ui/utils': 0.2.8 + '@floating-ui/utils': 0.2.9 - '@floating-ui/dom@1.6.12': + '@floating-ui/dom@1.6.13': dependencies: - '@floating-ui/core': 1.6.8 - '@floating-ui/utils': 0.2.8 + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.6.12 + '@floating-ui/dom': 1.6.13 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@floating-ui/utils@0.2.8': {} + '@floating-ui/utils@0.2.9': {} '@fuel-ts/abi-coder@0.97.2(vitest@2.1.4(@types/node@22.10.5)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@5.0.10))(terser@5.37.0))': dependencies: @@ -29027,11 +29090,11 @@ snapshots: optionalDependencies: rollup: 3.29.5 - '@rollup/plugin-json@6.1.0(rollup@4.30.0)': + '@rollup/plugin-json@6.1.0(rollup@4.30.1)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.30.0) + '@rollup/pluginutils': 5.1.4(rollup@4.30.1) optionalDependencies: - rollup: 4.30.0 + rollup: 4.30.1 '@rollup/plugin-node-resolve@15.3.0(rollup@2.79.2)': dependencies: @@ -29082,9 +29145,9 @@ snapshots: rollup: 2.79.2 tslib: 2.8.1 - '@rollup/plugin-virtual@3.0.2(rollup@4.30.0)': + '@rollup/plugin-virtual@3.0.2(rollup@4.30.1)': optionalDependencies: - rollup: 4.30.0 + rollup: 4.30.1 '@rollup/pluginutils@5.1.4(rollup@2.79.2)': dependencies: @@ -29102,69 +29165,69 @@ snapshots: optionalDependencies: rollup: 3.29.5 - '@rollup/pluginutils@5.1.4(rollup@4.30.0)': + '@rollup/pluginutils@5.1.4(rollup@4.30.1)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.30.0 + rollup: 4.30.1 - '@rollup/rollup-android-arm-eabi@4.30.0': + '@rollup/rollup-android-arm-eabi@4.30.1': optional: true - '@rollup/rollup-android-arm64@4.30.0': + '@rollup/rollup-android-arm64@4.30.1': optional: true - '@rollup/rollup-darwin-arm64@4.30.0': + '@rollup/rollup-darwin-arm64@4.30.1': optional: true - '@rollup/rollup-darwin-x64@4.30.0': + '@rollup/rollup-darwin-x64@4.30.1': optional: true - '@rollup/rollup-freebsd-arm64@4.30.0': + '@rollup/rollup-freebsd-arm64@4.30.1': optional: true - '@rollup/rollup-freebsd-x64@4.30.0': + '@rollup/rollup-freebsd-x64@4.30.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.30.0': + '@rollup/rollup-linux-arm-gnueabihf@4.30.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.30.0': + '@rollup/rollup-linux-arm-musleabihf@4.30.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.30.0': + '@rollup/rollup-linux-arm64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.30.0': + '@rollup/rollup-linux-arm64-musl@4.30.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.30.0': + '@rollup/rollup-linux-loongarch64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.30.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.30.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.30.0': + '@rollup/rollup-linux-riscv64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.30.0': + '@rollup/rollup-linux-s390x-gnu@4.30.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.30.0': + '@rollup/rollup-linux-x64-gnu@4.30.1': optional: true - '@rollup/rollup-linux-x64-musl@4.30.0': + '@rollup/rollup-linux-x64-musl@4.30.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.30.0': + '@rollup/rollup-win32-arm64-msvc@4.30.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.30.0': + '@rollup/rollup-win32-ia32-msvc@4.30.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.30.0': + '@rollup/rollup-win32-x64-msvc@4.30.1': optional: true '@rtsao/scc@1.1.0': {} @@ -33015,12 +33078,12 @@ snapshots: balanced-match@1.0.2: {} - bare-events@2.5.2: + bare-events@2.5.3: optional: true bare-fs@2.3.5: dependencies: - bare-events: 2.5.2 + bare-events: 2.5.3 bare-path: 2.1.3 bare-stream: 2.6.1 optional: true @@ -34586,7 +34649,7 @@ snapshots: css-declaration-sorter: 7.2.0(postcss@8.4.49) cssnano-utils: 5.0.0(postcss@8.4.49) postcss: 8.4.49 - postcss-calc: 10.0.2(postcss@8.4.49) + postcss-calc: 10.1.0(postcss@8.4.49) postcss-colormin: 7.0.2(postcss@8.4.49) postcss-convert-values: 7.0.4(postcss@8.4.49) postcss-discard-comments: 7.0.3(postcss@8.4.49) @@ -42101,10 +42164,10 @@ snapshots: postcss: 8.4.49 postcss-selector-parser: 7.0.0 - postcss-calc@10.0.2(postcss@8.4.49): + postcss-calc@10.1.0(postcss@8.4.49): dependencies: postcss: 8.4.49 - postcss-selector-parser: 6.1.2 + postcss-selector-parser: 7.0.0 postcss-value-parser: 4.2.0 postcss-calc@9.0.1(postcss@8.4.49): @@ -42986,10 +43049,10 @@ snapshots: end-of-stream: 1.4.4 once: 1.4.0 - pumpdotfun-sdk@1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.0)(typescript@5.6.3)(utf-8-validate@5.0.10): + pumpdotfun-sdk@1.3.2(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(rollup@4.30.1)(typescript@5.6.3)(utf-8-validate@5.0.10): dependencies: '@coral-xyz/anchor': 0.30.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@rollup/plugin-json': 6.1.0(rollup@4.30.0) + '@rollup/plugin-json': 6.1.0(rollup@4.30.1) '@solana/spl-token': 0.4.6(@solana/web3.js@1.95.8(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)(utf-8-validate@5.0.10) '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -43808,29 +43871,29 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - rollup@4.30.0: + rollup@4.30.1: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.30.0 - '@rollup/rollup-android-arm64': 4.30.0 - '@rollup/rollup-darwin-arm64': 4.30.0 - '@rollup/rollup-darwin-x64': 4.30.0 - '@rollup/rollup-freebsd-arm64': 4.30.0 - '@rollup/rollup-freebsd-x64': 4.30.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.30.0 - '@rollup/rollup-linux-arm-musleabihf': 4.30.0 - '@rollup/rollup-linux-arm64-gnu': 4.30.0 - '@rollup/rollup-linux-arm64-musl': 4.30.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.30.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.30.0 - '@rollup/rollup-linux-riscv64-gnu': 4.30.0 - '@rollup/rollup-linux-s390x-gnu': 4.30.0 - '@rollup/rollup-linux-x64-gnu': 4.30.0 - '@rollup/rollup-linux-x64-musl': 4.30.0 - '@rollup/rollup-win32-arm64-msvc': 4.30.0 - '@rollup/rollup-win32-ia32-msvc': 4.30.0 - '@rollup/rollup-win32-x64-msvc': 4.30.0 + '@rollup/rollup-android-arm-eabi': 4.30.1 + '@rollup/rollup-android-arm64': 4.30.1 + '@rollup/rollup-darwin-arm64': 4.30.1 + '@rollup/rollup-darwin-x64': 4.30.1 + '@rollup/rollup-freebsd-arm64': 4.30.1 + '@rollup/rollup-freebsd-x64': 4.30.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.30.1 + '@rollup/rollup-linux-arm-musleabihf': 4.30.1 + '@rollup/rollup-linux-arm64-gnu': 4.30.1 + '@rollup/rollup-linux-arm64-musl': 4.30.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.30.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.30.1 + '@rollup/rollup-linux-riscv64-gnu': 4.30.1 + '@rollup/rollup-linux-s390x-gnu': 4.30.1 + '@rollup/rollup-linux-x64-gnu': 4.30.1 + '@rollup/rollup-linux-x64-musl': 4.30.1 + '@rollup/rollup-win32-arm64-msvc': 4.30.1 + '@rollup/rollup-win32-ia32-msvc': 4.30.1 + '@rollup/rollup-win32-x64-msvc': 4.30.1 fsevents: 2.3.3 roughjs@4.6.6: @@ -44695,7 +44758,7 @@ snapshots: queue-tick: 1.0.1 text-decoder: 1.2.3 optionalDependencies: - bare-events: 2.5.2 + bare-events: 2.5.3 strict-uri-encode@2.0.0: {} @@ -45469,7 +45532,7 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(yaml@2.7.0) resolve-from: 5.0.0 - rollup: 4.30.0 + rollup: 4.30.1 source-map: 0.8.0-beta.0 sucrase: 3.35.0 tinyexec: 0.3.2 @@ -46254,9 +46317,9 @@ snapshots: - supports-color - terser - vite-plugin-top-level-await@1.4.4(@swc/helpers@0.5.15)(rollup@4.30.0)(vite@client+@tanstack+router-plugin+vite): + vite-plugin-top-level-await@1.4.4(@swc/helpers@0.5.15)(rollup@4.30.1)(vite@client+@tanstack+router-plugin+vite): dependencies: - '@rollup/plugin-virtual': 3.0.2(rollup@4.30.0) + '@rollup/plugin-virtual': 3.0.2(rollup@4.30.1) '@swc/core': 1.10.4(@swc/helpers@0.5.15) uuid: 10.0.0 vite: link:client/@tanstack/router-plugin/vite @@ -46272,7 +46335,7 @@ snapshots: dependencies: esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.30.0 + rollup: 4.30.1 optionalDependencies: '@types/node': 20.17.9 fsevents: 2.3.3 @@ -46282,7 +46345,7 @@ snapshots: dependencies: esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.30.0 + rollup: 4.30.1 optionalDependencies: '@types/node': 22.10.5 fsevents: 2.3.3 @@ -46292,7 +46355,7 @@ snapshots: dependencies: esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.30.0 + rollup: 4.30.1 optionalDependencies: '@types/node': 22.8.4 fsevents: 2.3.3