diff --git a/.env.example b/.env.example index aa702a32ce2..8d4f0d523ea 100644 --- a/.env.example +++ b/.env.example @@ -58,6 +58,13 @@ FARCASTER_POLL_INTERVAL=120 # How often (in seconds) the bot should check for # Telegram Configuration TELEGRAM_BOT_TOKEN= +# Telegram account client Configuration +TELEGRAM_ACCOUNT_PHONE= # Account phone number for authorization +TELEGRAM_ACCOUNT_APP_ID= # Telegram app api_id (get it at me.telegram.org) +TELEGRAM_ACCOUNT_APP_HASH= # Telegram app api_hash (get it at me.telegram.org) +TELEGRAM_ACCOUNT_DEVICE_MODEL= # Device model. Example: Samsung Galaxy S28+ +TELEGRAM_ACCOUNT_SYSTEM_VERSION= # Device system version. Example: Android 12 S? (31) + # Twitter/X Configuration TWITTER_DRY_RUN=false TWITTER_USERNAME= # Account username @@ -357,6 +364,10 @@ MORALIS_API_KEY= EVM_PRIVATE_KEY= EVM_PROVIDER_URL= +# Zilliqa +ZILLIQA_PRIVATE_KEY= +ZILLIQA_PROVIDER_URL= + # Avalanche AVALANCHE_PRIVATE_KEY= AVALANCHE_PUBLIC_KEY= diff --git a/agent/package.json b/agent/package.json index ee414cfd10e..5f4aafcafb8 100644 --- a/agent/package.json +++ b/agent/package.json @@ -1,161 +1,162 @@ { - "name": "@elizaos/agent", - "version": "0.1.9-alpha.1", - "main": "src/index.ts", - "type": "module", - "scripts": { - "start": "node --loader ts-node/esm src/index.ts", - "dev": "node --loader ts-node/esm src/index.ts", - "check-types": "tsc --noEmit", - "test": "jest" - }, - "nodemonConfig": { - "watch": [ - "src", - "../core/dist" - ], - "ext": "ts,json", - "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" - }, - "dependencies": { - "@elizaos/adapter-supabase": "workspace:*", - "@elizaos/adapter-postgres": "workspace:*", - "@elizaos/adapter-redis": "workspace:*", - "@elizaos/adapter-sqlite": "workspace:*", - "@elizaos/adapter-pglite": "workspace:*", - "@elizaos/adapter-qdrant": "workspace:*", - "@elizaos/adapter-mongodb": "workspace:*", - "@elizaos/client-auto": "workspace:*", - "@elizaos/client-direct": "workspace:*", - "@elizaos/client-discord": "workspace:*", - "@elizaos/client-farcaster": "workspace:*", - "@elizaos/client-lens": "workspace:*", - "@elizaos/client-telegram": "workspace:*", - "@elizaos/client-twitter": "workspace:*", - "@elizaos/client-instagram": "workspace:*", - "@elizaos/client-slack": "workspace:*", - "@elizaos/client-alexa": "workspace:*", - "@elizaos/client-simsai": "workspace:*", - "@elizaos/core": "workspace:*", - "@elizaos/plugin-0g": "workspace:*", - "@elizaos/plugin-abstract": "workspace:*", - "@elizaos/plugin-agentkit": "workspace:*", - "@elizaos/plugin-aptos": "workspace:*", - "@elizaos/plugin-birdeye": "workspace:*", - "@elizaos/plugin-coingecko": "workspace:*", - "@elizaos/plugin-coinmarketcap": "workspace:*", - "@elizaos/plugin-zerion": "workspace:*", - "@elizaos/plugin-binance": "workspace:*", - "@elizaos/plugin-avail": "workspace:*", - "@elizaos/plugin-bnb": "workspace:*", - "@elizaos/plugin-bootstrap": "workspace:*", - "@elizaos/plugin-di": "workspace:*", - "@elizaos/plugin-cosmos": "workspace:*", - "@elizaos/plugin-intiface": "workspace:*", - "@elizaos/plugin-coinbase": "workspace:*", - "@elizaos/plugin-conflux": "workspace:*", - "@elizaos/plugin-evm": "workspace:*", - "@elizaos/plugin-echochambers": "workspace:*", - "@elizaos/plugin-flow": "workspace:*", - "@elizaos/plugin-gitbook": "workspace:*", - "@elizaos/plugin-story": "workspace:*", - "@elizaos/plugin-gitcoin-passport": "workspace:*", - "@elizaos/plugin-goat": "workspace:*", - "@elizaos/plugin-lensNetwork": "workspace:*", - "@elizaos/plugin-icp": "workspace:*", - "@elizaos/plugin-initia": "workspace:*", - "@elizaos/plugin-image-generation": "workspace:*", - "@elizaos/plugin-lit": "workspace:*", - "@elizaos/plugin-gelato": "workspace:*", - "@elizaos/plugin-moralis": "workspace:*", - "@elizaos/plugin-mind-network": "workspace:*", - "@elizaos/plugin-movement": "workspace:*", - "@elizaos/plugin-massa": "workspace:*", - "@elizaos/plugin-news": "workspace:*", - "@elizaos/plugin-nft-generation": "workspace:*", - "@elizaos/plugin-node": "workspace:*", - "@elizaos/plugin-quick-intel": "workspace:*", - "@elizaos/plugin-solana": "workspace:*", - "@elizaos/plugin-solana-v2": "workspace:*", - "@elizaos/plugin-injective": "workspace:*", - "@elizaos/plugin-solana-agent-kit": "workspace:*", - "@elizaos/plugin-squid-router": "workspace:*", - "@elizaos/plugin-autonome": "workspace:*", - "@elizaos/plugin-starknet": "workspace:*", - "@elizaos/plugin-stargaze": "workspace:*", - "@elizaos/plugin-giphy": "workspace:*", - "@elizaos/plugin-ton": "workspace:*", - "@elizaos/plugin-sui": "workspace:*", - "@elizaos/plugin-sgx": "workspace:*", - "@elizaos/plugin-iq6900": "workspace:*", - "@elizaos/plugin-tee": "workspace:*", - "@elizaos/plugin-tee-log": "workspace:*", - "@elizaos/plugin-tee-marlin": "workspace:*", - "@elizaos/plugin-multiversx": "workspace:*", - "@elizaos/plugin-near": "workspace:*", - "@elizaos/plugin-zksync-era": "workspace:*", - "@elizaos/plugin-twitter": "workspace:*", - "@elizaos/plugin-primus": "workspace:*", - "@elizaos/plugin-cronoszkevm": "workspace:*", - "@elizaos/plugin-cronos": "workspace:*", - "@elizaos/plugin-3d-generation": "workspace:*", - "@elizaos/plugin-fuel": "workspace:*", - "@elizaos/plugin-avalanche": "workspace:*", - "@elizaos/plugin-video-generation": "workspace:*", - "@elizaos/plugin-web-search": "workspace:*", - "@elizaos/plugin-dexscreener": "workspace:*", - "@elizaos/plugin-letzai": "workspace:*", - "@elizaos/plugin-thirdweb": "workspace:*", - "@elizaos/plugin-genlayer": "workspace:*", - "@elizaos/plugin-tee-verifiable-log": "workspace:*", - "@elizaos/plugin-depin": "workspace:*", - "@elizaos/plugin-open-weather": "workspace:*", - "@elizaos/plugin-obsidian": "workspace:*", - "@elizaos/plugin-arthera": "workspace:*", - "@elizaos/plugin-allora": "workspace:*", - "@elizaos/plugin-opacity": "workspace:*", - "@elizaos/plugin-hyperliquid": "workspace:*", - "@elizaos/plugin-akash": "workspace:*", - "@elizaos/plugin-quai": "workspace:*", - "@elizaos/plugin-lightning": "workspace:*", - "@elizaos/plugin-b2": "workspace:*", - "@elizaos/plugin-nft-collections": "workspace:*", - "@elizaos/plugin-pyth-data": "workspace:*", - "@elizaos/plugin-openai": "workspace:*", - "@elizaos/plugin-devin": "workspace:*", - "@elizaos/plugin-holdstation": "workspace:*", - "@elizaos/plugin-router-nitro": "workspace:*", - "@elizaos/plugin-nvidia-nim": "workspace:*", - "@elizaos/plugin-0x": "workspace:*", - "@elizaos/plugin-bittensor": "workspace:*", - "@elizaos/plugin-chainbase": "workspace:*", - "@elizaos/plugin-dkg": "workspace:*", - "@elizaos/plugin-email": "workspace:*", - "@elizaos/plugin-sei": "workspace:*", - "@elizaos/plugin-omniflix": "workspace:*", - "@elizaos/plugin-suno": "workspace:*", - "@elizaos/plugin-udio": "workspace:*", - "@elizaos/plugin-hyperbolic": "workspace:*", - "@elizaos/plugin-football": "workspace:*", - "@elizaos/plugin-imgflip": "workspace:*", - "@elizaos/plugin-ethstorage": "workspace:*", - "@elizaos/plugin-mina": "workspace:*", - "@elizaos/plugin-email-automation": "workspace:*", - "@elizaos/plugin-dcap": "workspace:*", - "@elizaos/plugin-form": "workspace:*", - "@elizaos/plugin-ankr": "workspace:*", - "@elizaos/client-xmtp": "workspace:*", - "@elizaos/plugin-btcfun": "workspace:*", - "@elizaos/plugin-trikon": "workspace:*", - "readline": "1.3.0", - "ws": "8.18.0", - "yargs": "17.7.2" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "jest": "^29.7.0", - "ts-jest": "^29.2.5", - "ts-node": "10.9.2", - "tsup": "8.3.5" - } -} \ No newline at end of file + "name": "@elizaos/agent", + "version": "0.1.9-alpha.1", + "main": "src/index.ts", + "type": "module", + "scripts": { + "start": "node --loader ts-node/esm src/index.ts", + "dev": "node --loader ts-node/esm src/index.ts", + "check-types": "tsc --noEmit", + "test": "jest" + }, + "nodemonConfig": { + "watch": [ + "src", + "../core/dist" + ], + "ext": "ts,json", + "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" + }, + "dependencies": { + "@elizaos/adapter-supabase": "workspace:*", + "@elizaos/adapter-postgres": "workspace:*", + "@elizaos/adapter-redis": "workspace:*", + "@elizaos/adapter-sqlite": "workspace:*", + "@elizaos/adapter-pglite": "workspace:*", + "@elizaos/adapter-qdrant": "workspace:*", + "@elizaos/adapter-mongodb": "workspace:*", + "@elizaos/client-auto": "workspace:*", + "@elizaos/client-direct": "workspace:*", + "@elizaos/client-discord": "workspace:*", + "@elizaos/client-farcaster": "workspace:*", + "@elizaos/client-lens": "workspace:*", + "@elizaos/client-telegram": "workspace:*", + "@elizaos/client-telegram-account": "workspace:*", + "@elizaos/client-twitter": "workspace:*", + "@elizaos/client-instagram": "workspace:*", + "@elizaos/client-slack": "workspace:*", + "@elizaos/client-alexa": "workspace:*", + "@elizaos/client-simsai": "workspace:*", + "@elizaos/core": "workspace:*", + "@elizaos/plugin-0g": "workspace:*", + "@elizaos/plugin-abstract": "workspace:*", + "@elizaos/plugin-agentkit": "workspace:*", + "@elizaos/plugin-aptos": "workspace:*", + "@elizaos/plugin-birdeye": "workspace:*", + "@elizaos/plugin-coingecko": "workspace:*", + "@elizaos/plugin-coinmarketcap": "workspace:*", + "@elizaos/plugin-zerion": "workspace:*", + "@elizaos/plugin-binance": "workspace:*", + "@elizaos/plugin-avail": "workspace:*", + "@elizaos/plugin-bnb": "workspace:*", + "@elizaos/plugin-bootstrap": "workspace:*", + "@elizaos/plugin-di": "workspace:*", + "@elizaos/plugin-cosmos": "workspace:*", + "@elizaos/plugin-intiface": "workspace:*", + "@elizaos/plugin-coinbase": "workspace:*", + "@elizaos/plugin-conflux": "workspace:*", + "@elizaos/plugin-evm": "workspace:*", + "@elizaos/plugin-echochambers": "workspace:*", + "@elizaos/plugin-flow": "workspace:*", + "@elizaos/plugin-gitbook": "workspace:*", + "@elizaos/plugin-story": "workspace:*", + "@elizaos/plugin-gitcoin-passport": "workspace:*", + "@elizaos/plugin-goat": "workspace:*", + "@elizaos/plugin-lensNetwork": "workspace:*", + "@elizaos/plugin-icp": "workspace:*", + "@elizaos/plugin-initia": "workspace:*", + "@elizaos/plugin-image-generation": "workspace:*", + "@elizaos/plugin-lit": "workspace:*", + "@elizaos/plugin-gelato": "workspace:*", + "@elizaos/plugin-moralis": "workspace:*", + "@elizaos/plugin-mind-network": "workspace:*", + "@elizaos/plugin-movement": "workspace:*", + "@elizaos/plugin-massa": "workspace:*", + "@elizaos/plugin-news": "workspace:*", + "@elizaos/plugin-nft-generation": "workspace:*", + "@elizaos/plugin-node": "workspace:*", + "@elizaos/plugin-quick-intel": "workspace:*", + "@elizaos/plugin-solana": "workspace:*", + "@elizaos/plugin-solana-v2": "workspace:*", + "@elizaos/plugin-injective": "workspace:*", + "@elizaos/plugin-solana-agent-kit": "workspace:*", + "@elizaos/plugin-squid-router": "workspace:*", + "@elizaos/plugin-autonome": "workspace:*", + "@elizaos/plugin-starknet": "workspace:*", + "@elizaos/plugin-stargaze": "workspace:*", + "@elizaos/plugin-giphy": "workspace:*", + "@elizaos/plugin-ton": "workspace:*", + "@elizaos/plugin-sui": "workspace:*", + "@elizaos/plugin-sgx": "workspace:*", + "@elizaos/plugin-iq6900": "workspace:*", + "@elizaos/plugin-tee": "workspace:*", + "@elizaos/plugin-tee-log": "workspace:*", + "@elizaos/plugin-tee-marlin": "workspace:*", + "@elizaos/plugin-multiversx": "workspace:*", + "@elizaos/plugin-near": "workspace:*", + "@elizaos/plugin-zksync-era": "workspace:*", + "@elizaos/plugin-twitter": "workspace:*", + "@elizaos/plugin-primus": "workspace:*", + "@elizaos/plugin-cronoszkevm": "workspace:*", + "@elizaos/plugin-cronos": "workspace:*", + "@elizaos/plugin-3d-generation": "workspace:*", + "@elizaos/plugin-fuel": "workspace:*", + "@elizaos/plugin-avalanche": "workspace:*", + "@elizaos/plugin-video-generation": "workspace:*", + "@elizaos/plugin-web-search": "workspace:*", + "@elizaos/plugin-dexscreener": "workspace:*", + "@elizaos/plugin-letzai": "workspace:*", + "@elizaos/plugin-thirdweb": "workspace:*", + "@elizaos/plugin-genlayer": "workspace:*", + "@elizaos/plugin-tee-verifiable-log": "workspace:*", + "@elizaos/plugin-depin": "workspace:*", + "@elizaos/plugin-open-weather": "workspace:*", + "@elizaos/plugin-obsidian": "workspace:*", + "@elizaos/plugin-arthera": "workspace:*", + "@elizaos/plugin-allora": "workspace:*", + "@elizaos/plugin-opacity": "workspace:*", + "@elizaos/plugin-hyperliquid": "workspace:*", + "@elizaos/plugin-akash": "workspace:*", + "@elizaos/plugin-quai": "workspace:*", + "@elizaos/plugin-lightning": "workspace:*", + "@elizaos/plugin-b2": "workspace:*", + "@elizaos/plugin-nft-collections": "workspace:*", + "@elizaos/plugin-pyth-data": "workspace:*", + "@elizaos/plugin-openai": "workspace:*", + "@elizaos/plugin-devin": "workspace:*", + "@elizaos/plugin-holdstation": "workspace:*", + "@elizaos/plugin-router-nitro": "workspace:*", + "@elizaos/plugin-nvidia-nim": "workspace:*", + "@elizaos/plugin-0x": "workspace:*", + "@elizaos/plugin-bittensor": "workspace:*", + "@elizaos/plugin-chainbase": "workspace:*", + "@elizaos/plugin-dkg": "workspace:*", + "@elizaos/plugin-email": "workspace:*", + "@elizaos/plugin-sei": "workspace:*", + "@elizaos/plugin-omniflix": "workspace:*", + "@elizaos/plugin-suno": "workspace:*", + "@elizaos/plugin-udio": "workspace:*", + "@elizaos/plugin-hyperbolic": "workspace:*", + "@elizaos/plugin-football": "workspace:*", + "@elizaos/plugin-imgflip": "workspace:*", + "@elizaos/plugin-ethstorage": "workspace:*", + "@elizaos/plugin-mina": "workspace:*", + "@elizaos/plugin-email-automation": "workspace:*", + "@elizaos/plugin-dcap": "workspace:*", + "@elizaos/plugin-form": "workspace:*", + "@elizaos/plugin-ankr": "workspace:*", + "@elizaos/client-xmtp": "workspace:*", + "@elizaos/plugin-trikon": "workspace:*", + "@elizaos/plugin-zilliqa": "workspace:*", + "readline": "1.3.0", + "ws": "8.18.0", + "yargs": "17.7.2" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "10.9.2", + "tsup": "8.3.5" + } +} diff --git a/agent/src/index.ts b/agent/src/index.ts index 84b6fa8b1fc..1ad34bf43c2 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -10,6 +10,7 @@ import { InstagramClientInterface } from "@elizaos/client-instagram" import { LensAgentClient } from "@elizaos/client-lens" import { SlackClientInterface } from "@elizaos/client-slack" import { TelegramClientInterface } from "@elizaos/client-telegram" +import { TelegramAccountClientInterface } from "@elizaos/client-telegram-account" import { TwitterClientInterface } from "@elizaos/client-twitter" import { AlexaClientInterface } from "@elizaos/client-alexa"; import { MongoDBDatabaseAdapter } from "@elizaos/adapter-mongodb" @@ -52,6 +53,8 @@ import { footballPlugin } from "@elizaos/plugin-football" import { bootstrapPlugin } from "@elizaos/plugin-bootstrap" import { normalizeCharacter } from "@elizaos/plugin-di" import createGoatPlugin from "@elizaos/plugin-goat" +import createZilliqaPlugin from "@elizaos/plugin-zilliqa"; + // import { intifacePlugin } from "@elizaos/plugin-intiface"; import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation" import { abstractPlugin } from "@elizaos/plugin-abstract" @@ -145,7 +148,7 @@ import { ankrPlugin } from "@elizaos/plugin-ankr"; import { formPlugin } from "@elizaos/plugin-form"; import { MongoClient } from "mongodb"; import { quickIntelPlugin } from "@elizaos/plugin-quick-intel" -import { btcfunPlugin } from "@elizaos/plugin-btcfun" + import { trikonPlugin } from "@elizaos/plugin-trikon" const __filename = fileURLToPath(import.meta.url) // get the resolved path to the file const __dirname = path.dirname(__filename) // get the name of the directory @@ -643,6 +646,11 @@ export async function initializeClients(character: Character, runtime: IAgentRun if (telegramClient) clients.telegram = telegramClient } + if (clientTypes.includes(Clients.TELEGRAM_ACCOUNT)) { + const telegramAccountClient = await TelegramAccountClientInterface.start(runtime); + if (telegramAccountClient) clients.telegram_account = telegramAccountClient; + } + if (clientTypes.includes(Clients.TWITTER)) { const twitterClient = await TwitterClientInterface.start(runtime) if (twitterClient) { @@ -750,6 +758,13 @@ export async function createAgent(character: Character, db: IDatabaseAdapter, ca goatPlugin = await createGoatPlugin((secret) => getSecret(character, secret)) } + let zilliqaPlugin: any | undefined; + if (getSecret(character, "ZILLIQA_PRIVATE_KEY")) { + zilliqaPlugin = await createZilliqaPlugin((secret) => + getSecret(character, secret) + ); + } + // Initialize Reclaim adapter if environment variables are present // let verifiableInferenceAdapter; // if ( @@ -846,7 +861,8 @@ export async function createAgent(character: Character, db: IDatabaseAdapter, ca getSecret(character, "ENABLE_TEE_LOG") && ((teeMode !== TEEMode.OFF && walletSecretSalt) || getSecret(character, "SGX")) ? teeLogPlugin : null, getSecret(character, "OMNIFLIX_API_URL") && getSecret(character, "OMNIFLIX_MNEMONIC") ? OmniflixPlugin : null, getSecret(character, "COINBASE_API_KEY") && getSecret(character, "COINBASE_PRIVATE_KEY") && getSecret(character, "COINBASE_NOTIFICATION_URI") ? webhookPlugin : null, - goatPlugin, + goatPlugin, + zilliqaPlugin, getSecret(character, "COINGECKO_API_KEY") || getSecret(character, "COINGECKO_PRO_API_KEY") ? coingeckoPlugin : null, getSecret(character, "MORALIS_API_KEY") ? moralisPlugin : null, getSecret(character, "EVM_PROVIDER_URL") ? goatPlugin : null, @@ -912,7 +928,6 @@ export async function createAgent(character: Character, db: IDatabaseAdapter, ca getSecret(character, "DCAP_EVM_PRIVATE_KEY") && getSecret(character, "DCAP_MODE") ? dcapPlugin : null, getSecret(character, "QUICKINTEL_API_KEY") ? quickIntelPlugin : null, getSecret(character, "GELATO_RELAY_API_KEY") ? gelatoPlugin : null, - getSecret(character, "BTC_PRIVATE_KEY_WIF") && getSecret(character, "BTC_FUN_API_URL") ? btcfunPlugin : null, getSecret(character, "TRIKON_WALLET_ADDRESS") ? trikonPlugin : null, ].flat().filter(Boolean), providers: [], diff --git a/package.json b/package.json index ef0c91930c8..14475de04e3 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,11 @@ "@polkadot/util-crypto": "12.6.2", "@polkadot/types-create": "10.13.1", "@polkadot/types-codec": "10.13.1", + "@polkadot/keyring": "12.6.2", "@ai-sdk/provider": "1.0.6", "@ai-sdk/provider-utils": "2.1.2", - "cookie": "0.7.0" + "cookie": "0.7.0", + "bs58": "5.0.0" } }, "engines": { diff --git a/packages/client-telegram-account/.npmignore b/packages/client-telegram-account/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/client-telegram-account/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-btcfun/eslint.config.mjs b/packages/client-telegram-account/eslint.config.mjs similarity index 100% rename from packages/plugin-btcfun/eslint.config.mjs rename to packages/client-telegram-account/eslint.config.mjs diff --git a/packages/client-telegram-account/package.json b/packages/client-telegram-account/package.json new file mode 100644 index 00000000000..9c52dd9b4ca --- /dev/null +++ b/packages/client-telegram-account/package.json @@ -0,0 +1,42 @@ +{ + "name": "@elizaos/client-telegram-account", + "version": "0.1.9-alpha.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "glob": "11.0.0", + "input": "^1.0.1", + "telegram": "2.17.4" + }, + "devDependencies": { + "tsup": "8.3.5", + "vitest": "1.1.3", + "@vitest/coverage-v8": "1.1.3" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run", + "test:coverage": "vitest run --coverage" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/client-telegram-account/src/environment.ts b/packages/client-telegram-account/src/environment.ts new file mode 100644 index 00000000000..d4e48b50d4a --- /dev/null +++ b/packages/client-telegram-account/src/environment.ts @@ -0,0 +1,64 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z, ZodError } from "zod"; + +export const telegramAccountEnvSchema = z.object({ + TELEGRAM_ACCOUNT_PHONE: z.string(), + TELEGRAM_ACCOUNT_APP_ID: z.number().int(), + TELEGRAM_ACCOUNT_APP_HASH: z.string(), + TELEGRAM_ACCOUNT_DEVICE_MODEL: z.string(), + TELEGRAM_ACCOUNT_SYSTEM_VERSION: z.string(), +}); + +export type TelegramAccountConfig = z.infer; + + +function safeParseInt( + value: string | undefined | null, + defaultValue: number = null +): number { + if (!value) return defaultValue; + const parsed = parseInt(value, 10); + return isNaN(parsed) ? defaultValue : Math.max(1, parsed); +} + + +export async function validateTelegramAccountConfig( + runtime: IAgentRuntime +): Promise { + try { + const telegramAccountConfig = { + TELEGRAM_ACCOUNT_PHONE: + runtime.getSetting("TELEGRAM_ACCOUNT_PHONE") || + process.env.TELEGRAM_ACCOUNT_PHONE, + + TELEGRAM_ACCOUNT_APP_ID: safeParseInt( + runtime.getSetting("TELEGRAM_ACCOUNT_APP_ID") || + process.env.TELEGRAM_ACCOUNT_APP_ID + ), + + TELEGRAM_ACCOUNT_APP_HASH: + runtime.getSetting("TELEGRAM_ACCOUNT_APP_HASH") || + process.env.TELEGRAM_ACCOUNT_APP_HASH, + + TELEGRAM_ACCOUNT_DEVICE_MODEL: + runtime.getSetting("TELEGRAM_ACCOUNT_DEVICE_MODEL") || + process.env.TELEGRAM_ACCOUNT_DEVICE_MODEL, + + TELEGRAM_ACCOUNT_SYSTEM_VERSION: + runtime.getSetting("TELEGRAM_ACCOUNT_SYSTEM_VERSION") || + process.env.TELEGRAM_ACCOUNT_SYSTEM_VERSION + }; + + return telegramAccountEnvSchema.parse(telegramAccountConfig); + } catch (error) { + if (error instanceof ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Telegram account configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/client-telegram-account/src/index.ts b/packages/client-telegram-account/src/index.ts new file mode 100644 index 00000000000..3c9f7768363 --- /dev/null +++ b/packages/client-telegram-account/src/index.ts @@ -0,0 +1,19 @@ +import { elizaLogger } from "@elizaos/core"; +import { Client, IAgentRuntime } from "@elizaos/core"; +import {TelegramAccountConfig, validateTelegramAccountConfig} from "./environment.ts"; +import { TelegramAccountClient } from "./telegramAccountClient.ts" + +export const TelegramAccountClientInterface: Client = { + start: async (runtime: IAgentRuntime) => { + const telegramAccountConfig: TelegramAccountConfig = await validateTelegramAccountConfig(runtime); + const telegramAccountClient = new TelegramAccountClient(runtime, telegramAccountConfig); + await telegramAccountClient.start(); + + return telegramAccountClient; + }, + stop: async (_runtime: IAgentRuntime) => { + elizaLogger.warn("Telegram client does not support stopping yet"); + }, +}; + +export default TelegramAccountClientInterface; diff --git a/packages/client-telegram-account/src/telegramAccountClient.ts b/packages/client-telegram-account/src/telegramAccountClient.ts new file mode 100644 index 00000000000..021b60a4245 --- /dev/null +++ b/packages/client-telegram-account/src/telegramAccountClient.ts @@ -0,0 +1,340 @@ +import { + IAgentRuntime, + UUID, + Content, + Memory, + HandlerCallback, + ModelClass, + State, + Media, + elizaLogger, + getEmbeddingZeroVector, + composeContext, + generateMessageResponse, + stringToUuid +} from "@elizaos/core"; +import { TelegramAccountConfig } from "./environment.ts"; +import { TelegramClient, Api } from "telegram"; +import { StoreSession } from "telegram/sessions"; +import { NewMessage, NewMessageEvent } from "telegram/events"; +import { Entity } from "telegram/define"; +import input from "input"; +import bigInt from "big-integer"; +import { getTelegramAccountMessageHandlerTemplate } from "./templates.ts" +import { escapeMarkdown, splitMessage } from "./utils.ts"; + +export class TelegramAccountClient { + private runtime: IAgentRuntime; + private telegramAccountConfig: TelegramAccountConfig; + private client: TelegramClient; + private account: Api.User; + + constructor(runtime: IAgentRuntime, telegramAccountConfig: TelegramAccountConfig) { + elizaLogger.log("📱 Constructing new TelegramAccountClient..."); + + this.runtime = runtime; + this.telegramAccountConfig = telegramAccountConfig; + + elizaLogger.log("✅ TelegramClient constructor completed"); + } + + public async start(): Promise { + elizaLogger.log("🚀 Starting Telegram account..."); + + try { + await this.initializeAccount(); + this.setupEventsHandlers(); + + elizaLogger.success(`✅ Telegram account client successfully started for character ${this.runtime.character.name}`); + } catch (error) { + elizaLogger.error("❌ Failed to launch Telegram account:", error); + throw error; + } + } + + private async initializeAccount(): Promise { + // Prepare telegram account client + this.client = new TelegramClient( + new StoreSession('./data/telegram_account_session'), + this.telegramAccountConfig.TELEGRAM_ACCOUNT_APP_ID, + this.telegramAccountConfig.TELEGRAM_ACCOUNT_APP_HASH, + { + connectionRetries: 5, + deviceModel: this.telegramAccountConfig.TELEGRAM_ACCOUNT_DEVICE_MODEL, + systemVersion: this.telegramAccountConfig.TELEGRAM_ACCOUNT_SYSTEM_VERSION, + } + ) + + // Account sign in or connect + await this.client.start({ + phoneNumber: this.telegramAccountConfig.TELEGRAM_ACCOUNT_PHONE, + password: null, + phoneCode: async () => await input.text('Enter received Telegram code: '), + onError: (err) => console.log(err), + }); + + this.client.session.save(); + + // Testing connection + this.account = await this.client.getEntity('me') as Api.User; + } + + private setupEventsHandlers(): void { + this.newMessageHandler() + } + + private newMessageHandler() { + this.client.addEventHandler(async (event: NewMessageEvent) => { + try { + if (!event.message.message) return; + + // Get sender and chat full object + const sender = await event.message.getSender(); + if (sender.className != 'User') return; + + const chat = (await event.message.getChat()); + if (chat.className != 'User' && chat.className != 'Chat' && (chat.className == 'Channel' && !chat.megagroup)) return; + + // Get user full name + let senderName = sender.firstName; + if (sender.lastName) senderName += ' ' + sender.lastName; + + // Get reply message + let replyMessage = null; + if (event.message.replyTo) { + replyMessage = await event.message.getReplyMessage() + } + + // Convert IDs to UUIDs + const userUUID = stringToUuid(`tg-${sender.id.toString()}`) as UUID; + const roomUUID = stringToUuid(`tg-${chat.id.toString()}` + "-" + this.runtime.agentId) as UUID; + const messageUUID = stringToUuid(`tg-message-${roomUUID}-${event.message.id.toString()}` + "-" + this.runtime.agentId) as UUID; + const agentUUID = this.runtime.agentId; + const replyMessageUUID = replyMessage ? stringToUuid(`tg-message-${roomUUID}-${replyMessage.id.toString()}` + "-" + this.runtime.agentId) as UUID : null; + + // Ensure connection + await this.runtime.ensureConnection( + userUUID, + roomUUID, + sender.username, + senderName, + "telegram-account", + ); + + if (!event.message.message) return; + + // Create content + const content: Content = { + text: event.message.message, + inReplyTo: replyMessageUUID, + source: "telegram-account", + }; + + // Create memory for the message + const memory: Memory = { + id: messageUUID, + agentId: agentUUID, + userId: userUUID, + roomId: roomUUID, + content, + createdAt: event.message.date * 1000, + embedding: getEmbeddingZeroVector(), + }; + + // Create memory + await this.runtime.messageManager.createMemory(memory); + + // Update state with the new memory + let state = await this.runtime.composeState(memory); + state = await this.runtime.updateRecentMessageState(state); + + // Decide whether to respond + const shouldRespond = await this._shouldRespond(event.message, chat, replyMessage); + + // Send response in chunks + const callback: HandlerCallback = async (content: Content) => { + const sentMessages = await this.sendMessageInChunks( + chat.id, + content, + chat.className == 'User' ? null : event.message.id + ); + + if (sentMessages) { + const memories: Memory[] = []; + + // Create memories for each sent message + for (let i = 0; i < sentMessages.length; i++) { + const sentMessage = sentMessages[i]; + const isLastMessage = i === sentMessages.length - 1; + + const memory: Memory = { + id: stringToUuid(`tg-message-${roomUUID}-${sentMessage.id.toString()}` + "-" + this.runtime.agentId) as UUID, + agentId: agentUUID, + userId: agentUUID, + roomId: roomUUID, + content: { + ...content, + text: sentMessage.message, + inReplyTo: messageUUID, + }, + createdAt: sentMessage.date * 1000, + embedding: getEmbeddingZeroVector(), + }; + + // Set action to CONTINUE for all messages except the last one + // For the last message, use the original action from the response content + memory.content.action = !isLastMessage + ? "CONTINUE" + : content.action; + + await this.runtime.messageManager.createMemory(memory); + memories.push(memory); + } + + return memories; + } + }; + + if (shouldRespond) { + // Mark chat as read + await this.client.markAsRead(chat); + + // Show that a bot is typing a message + await this.client.invoke( + new Api.messages.SetTyping({ + peer: chat, + action: new Api.SendMessageTypingAction() + }) + ); + + // Generate response + const template = this.runtime.character?.templates + ?.messageHandlerTemplate || + getTelegramAccountMessageHandlerTemplate(this.account); + + const context = composeContext({ + state, + template: template, + }); + + const responseContent = await this._generateResponse( + memory, + state, + context + ); + + if (!responseContent || !responseContent.text) return; + + // Execute callback to send messages and log memories + const responseMessages = await callback(responseContent); + + // Update state after response + state = await this.runtime.updateRecentMessageState(state); + + // Handle any resulting actions + await this.runtime.processActions( + memory, + responseMessages, + state, + callback + ); + } + + await this.runtime.evaluate(memory, state, shouldRespond, callback); + } catch (error) { + elizaLogger.error("❌ Error handling message:", error); + elizaLogger.error("Error sending message:", error); + } + }, new NewMessage({ incoming: true })); + } + + // Decide if the bot should respond to the message + private async _shouldRespond( + message: Api.Message, + chat: Entity, + replyMessage?: Api.Message + ): Promise { + if (replyMessage) { + const replyFrom = replyMessage.fromId as Api.PeerUser; + if (replyFrom && replyFrom.userId.eq(this.account.id)) return true; + } + + if (chat.className == 'User') { + return true; + } + else { + return message.message.includes(`@${this.account.username}`) + } + } + + // Generate a response using AI + private async _generateResponse( + message: Memory, + _state: State, + context: string + ): Promise { + const { userId, roomId } = message; + + const response = await generateMessageResponse({ + runtime: this.runtime, + context, + modelClass: ModelClass.LARGE, + }); + + if (!response) { + console.error("❌ No response from generateMessageResponse"); + return null; + } + + await this.runtime.databaseAdapter.log({ + body: { message, context, response }, + userId, + roomId, + type: "response", + }); + + return response; + } + + // Send long messages in chunks + private async sendMessageInChunks( + chatId: bigInt.BigInteger, + content: Content, + replyToMessageId?: number + ) { + if (content.attachments && content.attachments.length > 0) { + content.attachments.map(async (attachment: Media) => { + await this.client.sendFile( + chatId, + { + file: attachment.url, + forceDocument: true, + caption: attachment.description, + replyTo: replyToMessageId + } + ); + + }); + } else { + const chunks = splitMessage(content.text); + const sentMessages = []; + + for (let i = 0; i < chunks.length; i++) { + const chunk = escapeMarkdown(chunks[i]); + + const sentMessage = await this.client.sendMessage( + chatId, + { + message: chunk, + parseMode: 'markdown', + replyTo: replyToMessageId + } + ); + + sentMessages.push(sentMessage); + } + + return sentMessages; + } + } +} diff --git a/packages/client-telegram-account/src/templates.ts b/packages/client-telegram-account/src/templates.ts new file mode 100644 index 00000000000..1801fe7ce91 --- /dev/null +++ b/packages/client-telegram-account/src/templates.ts @@ -0,0 +1,43 @@ +import { messageCompletionFooter } from "@elizaos/core"; +import {Api} from "telegram"; + +const telegramAccountMessageHandlerTemplate = ` +{{actionExamples}} +(Action examples are for reference only. Do not use the information from them in your response.) + +# Knowledge +{{knowledge}} + +# About {{agentName}}: +{{telegramAccountInfo}} +{{bio}} +{{lore}} + +{{characterMessageExamples}} + +{{providers}} + +{{attachments}} + +{{actions}} + +# Capabilities +Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. + +{{messageDirections}} + +{{recentMessages}} + +# Task: Generate a reply in the voice, style and perspective of {{agentName}} while using the thread above as additional context. You are replying on Telegram. + +{{formattedConversation}} +` + messageCompletionFooter; + +export function getTelegramAccountMessageHandlerTemplate(account: Api.User): string { + return telegramAccountMessageHandlerTemplate.replace('{{telegramAccountInfo}}', ` +Username: @${account.username} +First name: ${account.firstName} +Last name: ${account.lastName} +Telegram ID: ${account.id} + `); +} diff --git a/packages/client-telegram-account/src/utils.ts b/packages/client-telegram-account/src/utils.ts new file mode 100644 index 00000000000..20dac374fed --- /dev/null +++ b/packages/client-telegram-account/src/utils.ts @@ -0,0 +1,47 @@ +export function escapeMarkdown(text: string): string { + // Don't escape if it's a code block + if (text.startsWith("```") && text.endsWith("```")) { + return text; + } + + // Split the text by code blocks + const parts = text.split(/(```[\s\S]*?```)/g); + + return parts + .map((part, index) => { + // If it's a code block (odd indices in the split result will be code blocks) + if (index % 2 === 1) { + return part; + } + // For regular text, only escape characters that need escaping in Markdown + return ( + part + // First preserve any intended inline code spans + .replace(/`.*?`/g, (match) => match) + // Then only escape the minimal set of special characters that need escaping in Markdown mode + .replace(/([*_`\\])/g, "\\$1") + ); + }) + .join(""); +} + +/** + * Splits a message into chunks that fit within Telegram's message length limit + */ +export function splitMessage(text: string, maxLength: number = 3000): string[] { + const chunks: string[] = []; + let currentChunk = ""; + + const lines = text.split("\n"); + for (const line of lines) { + if (currentChunk.length + line.length + 1 <= maxLength) { + currentChunk += (currentChunk ? "\n" : "") + line; + } else { + if (currentChunk) chunks.push(currentChunk); + currentChunk = line; + } + } + + if (currentChunk) chunks.push(currentChunk); + return chunks; +} diff --git a/packages/client-telegram-account/tsconfig.json b/packages/client-telegram-account/tsconfig.json new file mode 100644 index 00000000000..005fbac9d36 --- /dev/null +++ b/packages/client-telegram-account/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-btcfun/tsup.config.ts b/packages/client-telegram-account/tsup.config.ts similarity index 90% rename from packages/plugin-btcfun/tsup.config.ts rename to packages/client-telegram-account/tsup.config.ts index 984bea9ceeb..e42bf4efeae 100644 --- a/packages/plugin-btcfun/tsup.config.ts +++ b/packages/client-telegram-account/tsup.config.ts @@ -15,8 +15,6 @@ export default defineConfig({ "https", "http", "agentkeepalive", - "viem", - "@lifi/sdk", - "tslog", + // Add other modules you want to externalize ], }); diff --git a/packages/client-telegram-account/vitest.config.ts b/packages/client-telegram-account/vitest.config.ts new file mode 100644 index 00000000000..2e60e80f5dc --- /dev/null +++ b/packages/client-telegram-account/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['__tests__/**/*.test.ts'], + coverage: { + reporter: ['text', 'json', 'html'], + }, + }, +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 239efcdef19..ffb70fa591b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -651,6 +651,7 @@ export enum Clients { DIRECT = "direct", TWITTER = "twitter", TELEGRAM = "telegram", + TELEGRAM_ACCOUNT = "telegram-account", FARCASTER = "farcaster", LENS = "lens", AUTO = "auto", diff --git a/packages/plugin-3d-generation/__tests__/actions/generate3d.test.ts b/packages/plugin-3d-generation/__tests__/actions/generate3d.test.ts new file mode 100644 index 00000000000..a5e4f90a834 --- /dev/null +++ b/packages/plugin-3d-generation/__tests__/actions/generate3d.test.ts @@ -0,0 +1,225 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ThreeDGenerationPlugin } from '../../src'; +import type { Memory, State, IAgentRuntime, HandlerCallback } from '@elizaos/core'; +import { elizaLogger } from '@elizaos/core'; +import { fal } from '@fal-ai/client'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; + +// Mock external dependencies +vi.mock('@elizaos/core', () => ({ + elizaLogger: { + log: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('@fal-ai/client', () => ({ + fal: { + subscribe: vi.fn(), + }, +})); + +vi.mock('fs', () => ({ + existsSync: vi.fn(), + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +vi.mock('path', () => ({ + dirname: vi.fn().mockReturnValue('content_cache'), +})); + +vi.mock('crypto', () => ({ + randomUUID: vi.fn().mockReturnValue('db98fb20-1f1f-4017-8314-7cc61e66c4e6'), +})); + +// Get the ThreeDGeneration action from the plugin +const ThreeDGeneration = ThreeDGenerationPlugin.actions[0]; + +describe('ThreeDGeneration Action', () => { + const mockRuntime: Required> = { + getSetting: vi.fn(), + }; + + const mockMessage: Required> = { + id: 'test-message-id', + content: { + text: 'Generate a 3D model of a cute cat', + }, + }; + + const mockState: Required> = { + messages: [], + context: {}, + }; + + const mockCallback: HandlerCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + global.fetch = vi.fn(); + mockRuntime.getSetting.mockReturnValue('test-fal-api-key'); + }); + + describe('validate', () => { + it('should validate successfully with API key', async () => { + const result = await ThreeDGeneration.validate(mockRuntime, mockMessage); + expect(result).toBe(true); + expect(elizaLogger.log).toHaveBeenCalledWith('FAL_API_KEY present:', true); + }); + + it('should fail validation without API key', async () => { + mockRuntime.getSetting.mockReturnValue(undefined); + const result = await ThreeDGeneration.validate(mockRuntime, mockMessage); + expect(result).toBe(false); + expect(elizaLogger.log).toHaveBeenCalledWith('FAL_API_KEY present:', false); + }); + }); + + describe('handler', () => { + beforeEach(() => { + vi.mocked(fal.subscribe).mockResolvedValue({ + data: { + model_mesh: { + url: 'https://example.com/3d-model.glb', + file_name: 'model.glb', + }, + }, + }); + + vi.mocked(global.fetch).mockResolvedValue({ + arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)), + } as unknown as Response); + }); + + it('should handle successful 3D generation', async () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + await ThreeDGeneration.handler( + mockRuntime, + mockMessage, + mockState, + {}, + mockCallback + ); + + // Check initial message + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('I\'ll generate a 3D object based on your prompt'), + }); + + // Verify FAL API call + expect(fal.subscribe).toHaveBeenCalledWith(expect.any(String), { + input: expect.objectContaining({ + prompt: expect.stringContaining('cute cat'), + }), + logs: true, + onQueueUpdate: expect.any(Function), + }); + + // Verify file handling + expect(fs.mkdirSync).toHaveBeenCalledWith('content_cache', { recursive: true }); + expect(fs.writeFileSync).toHaveBeenCalled(); + + // Verify final callback + expect(mockCallback).toHaveBeenLastCalledWith( + expect.objectContaining({ + text: 'Here\'s your generated 3D object!', + attachments: [expect.objectContaining({ + url: 'https://example.com/3d-model.glb', + title: 'Generated 3D', + source: 'ThreeDGeneration', + description: expect.stringContaining('cute cat'), + text: expect.stringContaining('cute cat'), + })], + }), + ['content_cache/generated_3d_model.glb'] + ); + }); + + it('should handle empty or short prompts', async () => { + const shortMessage: Memory = { + id: 'test-message-id', + content: { + text: 'hi', + }, + }; + + await ThreeDGeneration.handler( + mockRuntime, + shortMessage, + mockState, + {}, + mockCallback + ); + + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Could you please provide more details'), + }); + expect(fal.subscribe).not.toHaveBeenCalled(); + }); + + it('should handle FAL API errors', async () => { + vi.mocked(fal.subscribe).mockRejectedValue(new Error('API Error')); + + await ThreeDGeneration.handler( + mockRuntime, + mockMessage, + mockState, + {}, + mockCallback + ); + + expect(mockCallback).toHaveBeenLastCalledWith({ + text: expect.stringContaining('3D generation failed'), + error: true, + }); + }); + + it('should handle file system errors', async () => { + vi.mocked(fs.writeFileSync).mockImplementation(() => { + throw new Error('File system error'); + }); + + await ThreeDGeneration.handler( + mockRuntime, + mockMessage, + mockState, + {}, + mockCallback + ); + + expect(mockCallback).toHaveBeenLastCalledWith({ + text: expect.stringContaining('3D generation failed'), + error: true, + }); + }); + + it('should clean up prompt by removing mentions and commands', async () => { + const messageWithMentions: Memory = { + id: 'test-message-id', + content: { + text: '<@123456> generate 3D a cute cat render 3D', + }, + }; + + await ThreeDGeneration.handler( + mockRuntime, + messageWithMentions, + mockState, + {}, + mockCallback + ); + + expect(fal.subscribe).toHaveBeenCalledWith(expect.any(String), { + input: expect.objectContaining({ + prompt: 'a cute cat', + }), + logs: true, + onQueueUpdate: expect.any(Function), + }); + }); + }); +}); diff --git a/packages/plugin-3d-generation/package.json b/packages/plugin-3d-generation/package.json index b2b82e5a7da..2f338d8b31c 100644 --- a/packages/plugin-3d-generation/package.json +++ b/packages/plugin-3d-generation/package.json @@ -23,9 +23,13 @@ "tsup": "8.3.5", "whatwg-url": "7.1.0" }, + "devDependencies": { + "vitest": "^2.1.5" + }, "scripts": { "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch" + "dev": "tsup --format esm --dts --watch", + "test": "vitest run" }, "peerDependencies": { "whatwg-url": "7.1.0" diff --git a/packages/plugin-3d-generation/vitest.config.ts b/packages/plugin-3d-generation/vitest.config.ts new file mode 100644 index 00000000000..adbf7255380 --- /dev/null +++ b/packages/plugin-3d-generation/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}); diff --git a/packages/plugin-anyone/__tests__/actions/startAnyone.test.ts b/packages/plugin-anyone/__tests__/actions/startAnyone.test.ts new file mode 100644 index 00000000000..582ec14d94d --- /dev/null +++ b/packages/plugin-anyone/__tests__/actions/startAnyone.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { startAnyone } from '../../src/actions/startAnyone'; +import { AnyoneClientService } from '../../src/services/AnyoneClientService'; +import { AnyoneProxyService } from '../../src/services/AnyoneProxyService'; + +vi.mock('../../src/services/AnyoneClientService', () => ({ + AnyoneClientService: { + initialize: vi.fn(), + getInstance: vi.fn(), + stop: vi.fn(), + } +})); + +vi.mock('../../src/services/AnyoneProxyService', () => ({ + AnyoneProxyService: { + getInstance: vi.fn(() => ({ + initialize: vi.fn(), + cleanup: vi.fn() + })) + } +})); + +describe('startAnyone Action', () => { + const mockRuntime = { + getSetting: vi.fn(), + getState: vi.fn(), + setState: vi.fn(), + }; + + const mockMessage = { + content: { + text: 'Start Anyone', + type: 'START_ANYONE' + } + }; + + const mockState = {}; + const mockCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('validate', () => { + it('should validate successfully', async () => { + const result = await startAnyone.validate(mockRuntime, mockMessage); + expect(result).toBe(true); + }); + }); + + describe('handler', () => { + it('should initialize AnyoneClientService and AnyoneProxyService', async () => { + const mockProxyInstance = { + initialize: vi.fn(), + cleanup: vi.fn() + }; + vi.mocked(AnyoneProxyService.getInstance).mockReturnValue(mockProxyInstance); + + const result = await startAnyone.handler( + mockRuntime, + mockMessage, + mockState, + {}, + mockCallback + ); + + expect(AnyoneClientService.initialize).toHaveBeenCalled(); + expect(AnyoneProxyService.getInstance).toHaveBeenCalled(); + expect(mockProxyInstance.initialize).toHaveBeenCalled(); + expect(mockCallback).toHaveBeenCalledWith({ + text: 'Started Anyone' + }); + expect(result).toBe(true); + }); + + it('should handle initialization errors gracefully', async () => { + const error = new Error('Initialization failed'); + vi.mocked(AnyoneClientService.initialize).mockRejectedValue(error); + + await expect( + startAnyone.handler(mockRuntime, mockMessage, mockState, {}, mockCallback) + ).rejects.toThrow('Initialization failed'); + }); + }); + + describe('metadata', () => { + it('should have correct name and similes', () => { + expect(startAnyone.name).toBe('START_ANYONE'); + expect(startAnyone.similes).toEqual(['ANYONE']); + }); + + it('should have valid examples', () => { + expect(Array.isArray(startAnyone.examples)).toBe(true); + expect(startAnyone.examples.length).toBeGreaterThan(0); + + startAnyone.examples.forEach(example => { + expect(Array.isArray(example)).toBe(true); + expect(example.length).toBe(2); + expect(example[1].content.action).toBe('START_ANYONE'); + }); + }); + }); +}); diff --git a/packages/plugin-anyone/__tests__/actions/stopAnyone.test.ts b/packages/plugin-anyone/__tests__/actions/stopAnyone.test.ts new file mode 100644 index 00000000000..f3d4dc541e6 --- /dev/null +++ b/packages/plugin-anyone/__tests__/actions/stopAnyone.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { stopAnyone } from '../../src/actions/stopAnyone'; +import { AnyoneClientService } from '../../src/services/AnyoneClientService'; +import { AnyoneProxyService } from '../../src/services/AnyoneProxyService'; + +vi.mock('../../src/services/AnyoneClientService', () => ({ + AnyoneClientService: { + initialize: vi.fn(), + getInstance: vi.fn(), + stop: vi.fn(), + } +})); + +vi.mock('../../src/services/AnyoneProxyService', () => ({ + AnyoneProxyService: { + getInstance: vi.fn(() => ({ + initialize: vi.fn(), + cleanup: vi.fn() + })) + } +})); + +describe('stopAnyone Action', () => { + const mockRuntime = { + getSetting: vi.fn(), + getState: vi.fn(), + setState: vi.fn(), + }; + + const mockMessage = { + content: { + text: 'Stop Anyone', + type: 'STOP_ANYONE' + } + }; + + const mockState = {}; + const mockCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('validate', () => { + it('should validate successfully', async () => { + const result = await stopAnyone.validate(mockRuntime, mockMessage); + expect(result).toBe(true); + }); + }); + + describe('handler', () => { + it('should stop AnyoneClientService and cleanup AnyoneProxyService', async () => { + const mockProxyInstance = { + initialize: vi.fn(), + cleanup: vi.fn() + }; + vi.mocked(AnyoneProxyService.getInstance).mockReturnValue(mockProxyInstance); + + const result = await stopAnyone.handler( + mockRuntime, + mockMessage, + mockState, + {}, + mockCallback + ); + + expect(mockProxyInstance.cleanup).toHaveBeenCalled(); + expect(AnyoneClientService.stop).toHaveBeenCalled(); + expect(mockCallback).toHaveBeenCalledWith({ + text: 'Stopped Anyone and cleaned up proxy' + }); + expect(result).toBe(true); + }); + + it('should handle cleanup errors gracefully', async () => { + const error = new Error('Cleanup failed'); + vi.mocked(AnyoneClientService.stop).mockRejectedValue(error); + + await expect( + stopAnyone.handler(mockRuntime, mockMessage, mockState, {}, mockCallback) + ).rejects.toThrow('Cleanup failed'); + }); + }); + + describe('metadata', () => { + it('should have correct name and similes', () => { + expect(stopAnyone.name).toBe('STOP_ANYONE'); + expect(stopAnyone.similes).toEqual(['STOP_PROXY']); + }); + + it('should have valid examples', () => { + expect(Array.isArray(stopAnyone.examples)).toBe(true); + expect(stopAnyone.examples.length).toBeGreaterThan(0); + + stopAnyone.examples.forEach(example => { + expect(Array.isArray(example)).toBe(true); + expect(example.length).toBe(2); + expect(example[1].content.action).toBe('STOP_ANYONE'); + }); + }); + }); +}); diff --git a/packages/plugin-anyone/package.json b/packages/plugin-anyone/package.json index 946545fccec..72db36f2023 100644 --- a/packages/plugin-anyone/package.json +++ b/packages/plugin-anyone/package.json @@ -10,9 +10,16 @@ "axios": "^1.7.9", "tsup": "8.3.5" }, + "devDependencies": { + "vitest": "^1.2.1", + "@vitest/coverage-v8": "^1.2.1" + }, "scripts": { "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch" + "dev": "tsup --format esm --dts --watch", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "peerDependencies": { "whatwg-url": "7.1.0" diff --git a/packages/plugin-anyone/vitest.config.ts b/packages/plugin-anyone/vitest.config.ts new file mode 100644 index 00000000000..e11899a25b6 --- /dev/null +++ b/packages/plugin-anyone/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['__tests__/**/*.test.ts'], + mockReset: true, + clearMocks: true, + restoreMocks: true, + }, +}); diff --git a/packages/plugin-apro/README.MD b/packages/plugin-apro/README.MD new file mode 100644 index 00000000000..354f70d8d1a --- /dev/null +++ b/packages/plugin-apro/README.MD @@ -0,0 +1,172 @@ + +# @elizaos/plugin-apro + +Foundation plugin that enables advanced agent interactions, data verification, and price queries on the Eliza OS platform. It streamlines agent creation, verification processes, and provides a flexible framework for building robust agent-based solutions. + +## Overview + +The Apro plugin bridges agent-based logic with the Eliza ecosystem. It handles agent registration, data verification, and price queries, empowering both automated and user-driven workflows. + +## Features + +### Agent Operations +- **Agent Creation**: Deploy new agents with custom settings +- **Registration**: Register agents on-chain or via standardized processes +- **Multi-Signer Framework**: Supports threshold-based approval flows + +### Data Verification +- **Chain Validation**: Verify data authenticity on-chain +- **Transaction Execution**: Handle verification logic with built-in security checks +- **Auto-Hashing**: Convert raw data to hashed formats when needed +- **Metadata Parsing**: Validate content type, encoding, and compression + +### Price Queries +- **Live Price Data**: Fetch price information for various pairs +- **Format Validation**: Normalize user query inputs to standard trading-pair formats +- **APIs Integration**: Retrieve real-time or near-real-time pricing information + +## Security Features + +### Access Control +- **Private Key Management**: Safe usage of private keys for transaction signing +- **Environment Variables**: Secure injection of credentials +- **On-Chain Validation**: Leverage on-chain contract checks + +### Verification +- **Input Validation**: Strict schema checks before on-chain operations +- **Transaction Receipts**: Provide verifiable transaction details +- **Error Handling**: Detailed error logs for quick debugging + +## Installation + +```bash +npm install @elizaos/plugin-apro +``` + +## Configuration + +Configure the plugin by setting environment variables or runtime settings: +- APRO_RPC_URL +- APRO_PROXY_ADDRESS +- APRO_PRIVATE_KEY +- APRO_CONVERTER_ADDRESS +- APRO_AUTO_HASH_DATA + +## Usage + +### Basic Setup +```typescript +import { aproPlugin } from "@elizaos/plugin-apro"; + +// Initialize the plugin +const runtime = await initializeRuntime({ + plugins: [aproPlugin], +}); +``` + +### Actions + +#### CREATE_AND_REGISTER_AGENT +Creates and registers an agent using specified settings. + +```typescript +const result = await runtime.executeAction("CREATE_AND_REGISTER_AGENT", { + signers: [...], + threshold: 3, + agentHeader: { ... }, + // ...other fields... +}); +``` + +#### VERIFY +Verifies data on-chain via the Agent SDK. + +```typescript +const result = await runtime.executeAction("VERIFY", { + payload: { + data: "0x...hexData", + signatures: [...], + }, + agent: "0x...agentAddress", + digest: "0x...digestString", +}); +``` + +#### PRICE_QUERY +Fetches live price data for a specified trading pair. + +```typescript +const result = await runtime.executeAction("PRICE_QUERY", { + pair: "BTC/USD", +}); +``` + +## Performance Optimization + +1. **Cache Management** + - Implement caching for frequent queries + - Monitor retrieval times and cache hits + +2. **Network Efficiency** + - Batch requests where possible + - Validate response parsing to reduce overhead + +## System Requirements +- Node.js 16.x or higher +- Sufficient network access to on-chain endpoints +- Basic configuration of environment variables +- Minimum 4GB RAM recommended + +## Troubleshooting + +1. **Invalid Agent Settings** + - Ensure signers and threshold are correct + - Validate agentHeader for proper UUIDs and numeric values + +2. **Verification Failures** + - Check the input data formats + - Confirm environment variables are set + +3. **Price Query Errors** + - Verify the trading pair format + - Check external API availability + +## Safety & Security + +1. **Credential Management** + - Store private keys securely + - Do not commit secrets to version control + +2. **Transaction Limits** + - Configure thresholds to mitigate abuse + - Log transaction attempts and failures + +3. **Monitoring & Logging** + - Track unusual activity + - Maintain detailed audit logs + +## Support + +For issues or feature requests: +1. Check existing documentation +2. Submit a GitHub issue with relevant details +3. Include transaction logs and system info if applicable + +## Contributing + +We welcome pull requests! Refer to the project’s CONTRIBUTING.md and open discussions to coordinate efforts. + +## Credits + +- [APRO](https://www.apro.com/) - Plugin sponsor and partner +- [ai-agent-sdk-js](https://github.com/APRO-com/ai-agent-sdk-js) - Underlying agent SDK +- [ethers.js](https://docs.ethers.io/) - Transaction and contract interaction +- Community contributors for feedback and testing + +For more information about Apro plugin capabilities: + +- [Apro Documentation](https://docs.apro.com/en) + +## License + +This plugin is part of the Eliza project. Refer to the main project repository for licensing details. \ No newline at end of file diff --git a/packages/plugin-apro/package.json b/packages/plugin-apro/package.json new file mode 100644 index 00000000000..d27567ff8da --- /dev/null +++ b/packages/plugin-apro/package.json @@ -0,0 +1,34 @@ +{ + "name": "@elizaos/plugin-apro", + "version": "0.0.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "ai-agent-sdk-js": "^0.0.2" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run" + }, + "devDependencies": { + "tsup": "8.3.5", + "vitest": "2.1.4" + } +} diff --git a/packages/plugin-apro/src/actions/attpsPriceQuery.ts b/packages/plugin-apro/src/actions/attpsPriceQuery.ts new file mode 100644 index 00000000000..201f2705b8b --- /dev/null +++ b/packages/plugin-apro/src/actions/attpsPriceQuery.ts @@ -0,0 +1,105 @@ +import { Action, composeContext, elizaLogger, generateObject, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "@elizaos/core"; +import { attpsPriceQueryTemplate } from "../templates"; +import { AttpsPriceQuery, AttpsPriceQueryResponse, AttpsPriceQuerySchema, isAttpsPriceQuery } from "../types"; + +async function fetchPriceData(sourceAgentId: string, feedId: string) { + const response = await fetch(`https://ai-agent-test.apro.com/api/ai-agent/price-detail?sourceAgentId=${sourceAgentId}&feedId=${feedId}`); + const { result, code, message } = await response.json(); + if (code !== 0) { + throw new Error(message); + } + return result as AttpsPriceQueryResponse; +} + +function cleanNumber(numStr: string) { + return parseFloat(numStr).toString(); +} + +export const attpsPriceQuery: Action = { + name: "ATTPS_PRICE_QUERY", + similes: [ + 'ATTPS_PRICE_FETCH', + ], + description: "Call remote API to fetch price data for a given source agent id and feed id.", + validate: async (runtime: IAgentRuntime, message: Memory) => { + return true; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + _options?: { [key: string]: unknown }, + callback?: HandlerCallback + ) => { + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Generate price query params + let attpsPriceQuery: AttpsPriceQuery; + try { + const response = await generateObject({ + runtime, + context: composeContext({ + state, + template: attpsPriceQueryTemplate, + }), + modelClass: ModelClass.LARGE, + schema: AttpsPriceQuerySchema, + }); + attpsPriceQuery = response.object as AttpsPriceQuery; + elizaLogger.info('The price query params received:', attpsPriceQuery); + } catch (error: any) { + elizaLogger.error('Failed to generate price query params:', error); + callback({ + text: 'Failed to generate price query params. Please provide valid input.', + }); + return; + } + + // Validate price query params + if (!isAttpsPriceQuery(attpsPriceQuery)) { + elizaLogger.error('Invalid price query params:', attpsPriceQuery); + callback({ + text: 'Invalid price query params. Please provide valid input.', + }); + return; + } + + // Fetch price data + try { + const { sourceAgentId, feedId } = attpsPriceQuery; + const priceData = await fetchPriceData(sourceAgentId, feedId); + elizaLogger.info('The Price data received:', priceData); + + const message = `Ask price: ${cleanNumber(priceData.askPrice)}\nBid price: ${cleanNumber(priceData.bidPrice)}\nMid price: ${cleanNumber(priceData.midPrice)}\nTimestamp: ${priceData.validTimeStamp}`; + callback({ + text: `Here is the price data:\n${message}`, + }); + } catch (error: any) { + elizaLogger.error(`Error fetching price data, error: `, error); + callback({ + text: 'Error fetching price data, error: ' + error.message, + }) + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Can you fetch price data for source agent id ... and feed id ...?", + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll fetch price data for you. Give me a moment.", + action: 'ATTPS_PRICE_QUERY', + }, + } + ], + ], +} \ No newline at end of file diff --git a/packages/plugin-apro/src/actions/createAndRegisterAgent.ts b/packages/plugin-apro/src/actions/createAndRegisterAgent.ts new file mode 100644 index 00000000000..243cebc7c94 --- /dev/null +++ b/packages/plugin-apro/src/actions/createAndRegisterAgent.ts @@ -0,0 +1,132 @@ +import { Action, composeContext, elizaLogger, generateObject, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "@elizaos/core"; +import { AgentSDK, AgentSettings, parseNewAgentAddress } from "ai-agent-sdk-js"; +import { createAgentTemplate } from "../templates"; +import { AgentSettingsSchema, isAgentSettings } from "../types"; +import { ContractTransactionResponse } from "ethers"; + +export const createAndRegisterAgent: Action = { + name: "CREATE_AND_REGISTER_AGENT", + similes: [ + 'CREATE_AGENT', + 'REGISTER_AGENT', + ], + description: "Create and register an agent with APRO. User must provide agent settings.", + validate: async (_runtime: IAgentRuntime, _message: Memory) => { + return true; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + _options?: { [key: string]: unknown }, + callback?: HandlerCallback + ) => { + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Generate agent settings + let agentSettings: AgentSettings + try { + const agentSettingsDetail = await generateObject({ + runtime, + context: composeContext({ + state, + template: createAgentTemplate, + }), + modelClass: ModelClass.LARGE, + schema: AgentSettingsSchema, + }); + agentSettings = agentSettingsDetail.object as AgentSettings; + elizaLogger.info('The Agent settings received:', agentSettings); + } catch (error: any) { + elizaLogger.error('Failed to generate Agent settings:', error); + callback({ + text: 'Failed to generate Agent settings. Please provide valid input.', + }); + return; + } + + // Validate agent settings + if (!isAgentSettings(agentSettings)) { + elizaLogger.error('Invalid Agent settings:', agentSettings); + callback({ + text: 'Invalid Agent settings. Please provide valid input.', + }); + return; + } + + // Create SDK agent + let agent: AgentSDK + try { + agent = new AgentSDK({ + proxyAddress: runtime.getSetting('APRO_PROXY_ADDRESS') ?? process.env.APRO_PROXY_ADDRESS, + rpcUrl: runtime.getSetting('APRO_RPC_URL') ?? process.env.APRO_RPC_URL, + privateKey: runtime.getSetting('APRO_PRIVATE_KEY') ?? process.env.APRO_PRIVATE_KEY, + }); + } catch (error: any) { + elizaLogger.error('Failed to create Agent SDK:', error); + callback({ + text: 'Failed to create Agent SDK. Please check the apro plugin configuration.', + }); + return; + } + + // Create and register agent + let tx: ContractTransactionResponse + try { + tx = await agent.createAndRegisterAgent({agentSettings}) + elizaLogger.info('Successfully send create and register agent transaction:', tx.hash); + + const receipt = await tx.wait() + const agentAddress = parseNewAgentAddress(receipt) + + elizaLogger.info(`Created agent at address: ${agentAddress}`) + callback({ text: 'Agent created and registered successfully: ' + agentAddress }) + } catch (error: any) { + elizaLogger.error(`Error creating agent: ${error.message}`); + let message = 'Error creating agent: ' + error.message + if (tx?.hash) { + message += ` Transaction hash: ${tx.hash}` + } + await callback({ text: message }); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: `I want to Create and register apro ai-agent with the following settings: + { + signers: [ + '0x003CD3bD8Ac5b045be8E49d4dfd9928E1765E471', + '0xdE3701195b9823E41b3fc2c98922A94399E2a01C', + '0xB54E5D4faa950e8B6a01ed5a790Ac260c81Ad224', + '0x48eE063a6c67144E09684ac8AD9a0044836f348B', + '0xbBbCc052F1277dd94e88e8E5BD6D7FF9a29BaC98' + ], + threshold: 3, + converterAddress: "0x24c36e9996eb84138Ed7cAa483B4c59FF7640E5C", + agentHeader: { + sourceAgentName: 'ElizaOS Test Agent', + targetAgentId: '1105302c-7556-49b2-b6fe-3aedba9c0682', + messageType: 0, + priority: 1, + ttl: 3600, + }, + }` + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll help you create and register the agent.", + action: "CREATE_AND_REGISTER_AGENT", + }, + }, + ] + ], +} \ No newline at end of file diff --git a/packages/plugin-apro/src/actions/priceQuery.ts b/packages/plugin-apro/src/actions/priceQuery.ts new file mode 100644 index 00000000000..8d92c3d3676 --- /dev/null +++ b/packages/plugin-apro/src/actions/priceQuery.ts @@ -0,0 +1,110 @@ +import { Action, composeContext, elizaLogger, generateObject, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "@elizaos/core"; +import { priceQueryTemplate } from "../templates"; +import { isPriceQueryParams, PriceData, PriceQueryParams, PriceQueryParamsSchema } from "../types"; + +async function fetchPriceData(pair: string) { + const response = await fetch(`https://live-api.apro.com/api/live-stream/reports?pair=${pair}`); + const { result } = await response.json(); + return result as PriceData[]; +} + +export const priceQuery: Action = { + name: "PRICE_QUERY", + similes: [ + 'PRICE_FETCH', + ], + description: "Call remote API to fetch price data for a given pair.", + validate: async (runtime: IAgentRuntime, message: Memory) => { + return true; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + _options?: { [key: string]: unknown }, + callback?: HandlerCallback + ) => { + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Generate price query params + let priceQueryParams: PriceQueryParams; + try { + const response = await generateObject({ + runtime, + context: composeContext({ + state, + template: priceQueryTemplate, + }), + modelClass: ModelClass.LARGE, + schema: PriceQueryParamsSchema, + }); + priceQueryParams = response.object as PriceQueryParams; + elizaLogger.info('The price query params received:', priceQueryParams); + } catch (error: any) { + elizaLogger.error('Failed to generate price query params:', error); + callback({ + text: 'Failed to generate price query params. Please provide valid input.', + }); + return; + } + + // Validate price query params + if (!isPriceQueryParams(priceQueryParams)) { + elizaLogger.error('Invalid price query params:', priceQueryParams); + callback({ + text: 'Invalid price query params. Please provide valid input.', + }); + return; + } + + // Fetch price data + try { + const { pair } = priceQueryParams; + const priceData = await fetchPriceData(pair); + elizaLogger.info('The Price data received:', priceData); + + if (!priceData || priceData.length === 0) { + elizaLogger.error('No price data found for pair:', pair); + callback({ + text: `No price data found for pair ${pair}.`, + }); + return; + } + + let priceDataString = priceData.map((data) => { + return `Feed ID: ${data.feedId}\nBid Price: ${data.bidPrice}\nMid Price: ${data.midPrice}\nAsk Price: ${data.askPrice}\nTimestamp: ${data.timestamp}`; + }).join('\n\n'); + callback({ + text: `Price data for pair ${pair}: \n${priceDataString}`, + }); + } catch (error: any) { + elizaLogger.error(`Error fetching price data, error: `, error); + callback( + { + text: 'Error fetching price data, error: ' + error.message, + } + ) + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Can you fetch price data for pair BTC/USD?", + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll fetch price data for pair BTC/USD.", + action: 'PRICE_QUERY', + }, + } + ], + ], +} \ No newline at end of file diff --git a/packages/plugin-apro/src/actions/verifyData.ts b/packages/plugin-apro/src/actions/verifyData.ts new file mode 100644 index 00000000000..1163dc912da --- /dev/null +++ b/packages/plugin-apro/src/actions/verifyData.ts @@ -0,0 +1,119 @@ +import { Action, composeContext, elizaLogger, generateObject, HandlerCallback, IAgentRuntime, Memory, ModelClass, State } from "@elizaos/core"; +import { AgentSDK, VerifyParams } from "ai-agent-sdk-js"; +import { verifyDataTemplate } from "../templates"; +import { isVerifyParams, VerifyParamsSchema } from "../types"; +import { ContractTransactionResponse } from "ethers"; + +export const verifyData: Action = { + name: "VERIFY", + similes: [ + 'VERIFY_DATA', + ], + description: "Verify data with APRO. User must provide data to verify.", + validate: async (_runtime: IAgentRuntime, _message: Memory) => { + return true; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State, + _options?: { [key: string]: unknown }, + callback?: HandlerCallback + ) => { + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Generate verify params + let verifyParams: VerifyParams; + try { + const response = await generateObject({ + runtime, + context: composeContext({ + state, + template: verifyDataTemplate, + }), + modelClass: ModelClass.LARGE, + schema: VerifyParamsSchema, + }); + + verifyParams = response.object as VerifyParams; + elizaLogger.info('The verify params received:', verifyParams); + } catch (error: any) { + elizaLogger.error('Failed to generate verify params:', error); + callback({ + text: 'Failed to generate verify params. Please provide valid input.', + }); + return; + } + + // Validate verify params + if (!isVerifyParams(verifyParams)) { + elizaLogger.error('Invalid verify params:', verifyParams); + callback({ + text: 'Invalid verify params. Please provide valid input.', + }); + return; + } + + // Create SDK agent + let agent: AgentSDK + try { + agent = new AgentSDK({ + proxyAddress: runtime.getSetting('APRO_PROXY_ADDRESS') ?? process.env.APRO_PROXY_ADDRESS, + rpcUrl: runtime.getSetting('APRO_RPC_URL') ?? process.env.APRO_RPC_URL, + privateKey: runtime.getSetting('APRO_PRIVATE_KEY') ?? process.env.APRO_PRIVATE_KEY, + autoHashData: (runtime.getSetting('APRO_AUTO_HASH_DATA') ?? process.env.APRO_AUTO_HASH_DATA) === 'true', + converterAddress: runtime.getSetting('APRO_CONVERTER_ADDRESS') ?? process.env.APRO_CONVERTER_ADDRESS, + }); + } catch (error: any) { + elizaLogger.error('Failed to create Agent SDK:', error); + callback({ + text: 'Failed to create Agent SDK. Please check the apro plugin configuration.', + }); + return; + } + + // Verify data + let tx: ContractTransactionResponse + try { + tx = await agent.verify(verifyParams) + elizaLogger.info(`Data verification transaction sent. Transaction ID: ${tx.hash}`); + + const receipt = await tx.wait(); + elizaLogger.info(`Data verified successfully.`); + + callback({ + text: 'Success: Data verified successfully. Transaction ID: ' + receipt.hash, + }) + } catch (error: any) { + elizaLogger.error(`Error verify data: ${error.message}`); + let message = 'Error verifying data: ' + error.message + if (tx?.hash) { + message += ` Transaction hash: ${tx.hash}` + } + callback({ + text: message, + }) + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "I want to verify data: ...", + }, + }, + { + user: "{{user2}}", + content: { + text: "Sure, I'll verify the data.", + action: "VERIFY", + }, + }, + ] + ], +}; \ No newline at end of file diff --git a/packages/plugin-apro/src/index.ts b/packages/plugin-apro/src/index.ts new file mode 100644 index 00000000000..17bc4f6c4b1 --- /dev/null +++ b/packages/plugin-apro/src/index.ts @@ -0,0 +1,18 @@ +import type { Plugin } from "@elizaos/core"; +import { createAndRegisterAgent } from "./actions/createAndRegisterAgent"; +import { verifyData } from "./actions/verifyData"; +import { attpsPriceQuery } from "./actions/attpsPriceQuery"; + +export const aproPlugin: Plugin = { + name: "apro", + description: "Apro Plugin for Eliza", + actions: [ + createAndRegisterAgent, + verifyData, + attpsPriceQuery, + ], + evaluators: [], + providers: [], +}; + +export default aproPlugin; \ No newline at end of file diff --git a/packages/plugin-apro/src/templates.ts b/packages/plugin-apro/src/templates.ts new file mode 100644 index 00000000000..180e6f62c88 --- /dev/null +++ b/packages/plugin-apro/src/templates.ts @@ -0,0 +1,238 @@ +export const createAgentTemplate = ` +TASK: Extract ONLY the explicitly mentioned details from the user's input messages to create an agent configuration. DO NOT generate, infer or create any data that is not explicitly present in the input. + +RULES: +1. ONLY extract information that is explicitly present in the user's messages +2. Use null for ANY field where the exact required information is not present +3. Do not make assumptions or generate random values +4. If no valid data can be extracted, return a JSON with all null values +5. Only accept properly formatted addresses and UUIDs - do not create or infer them + +REQUIRED FIELDS: +- signers: Array of valid Ethereum addresses (42-char hex starting with '0x') +- threshold: Explicit number mentioned as threshold +- converterAddress: Valid Ethereum address (42-char hex starting with '0x') +- agentHeader: Object containing: + * messageId: Valid UUID format only + * sourceAgentId: Valid UUID format only + * sourceAgentName: Explicit agent name + * targetAgentId: Valid UUID format only + * messageType: Explicit number + * priority: Explicit number + * ttl: Explicit number in seconds + +OUTPUT FORMAT: +\`\`\`json +{ + "signers": [ + "" + ], + "threshold": null, + "converterAddress": null, + "agentHeader": { + "messageId": null, + "sourceAgentId": null, + "sourceAgentName": null, + "targetAgentId": null, + "messageType": null, + "priority": null, + "ttl": null + } +} +\`\`\` + +VALIDATION: +- Ethereum addresses must be 42 characters starting with '0x' +- UUIDs must match standard UUID format +- Numbers must be explicitly mentioned in the context +- Do not include any fields or values that are not explicitly mentioned in the user's input + +Context messages: +{{recentMessages}} +`; + +export const verifyDataTemplate = ` +TASK: STRICTLY extract ONLY explicitly mentioned verification details from the user's input messages. DO NOT generate, infer, or create any data that is not explicitly present in the input. + +STRICT RULES: +1. ONLY extract information that is EXPLICITLY present in the user's messages +2. Set null for ANY field where the exact required information is not present +3. DO NOT create, generate, or infer any values +4. Return all fields as null if no valid data can be extracted +5. Only accept properly formatted hexadecimal strings and numbers +6. Reject and set to null any values that don't match the required format + +REQUIRED FORMATS: +1. Hexadecimal strings must: + - Start with '0x' + - Contain only valid hex characters (0-9, a-f, A-F) + - Match the expected length for their purpose + +2. Ethereum addresses must: + - Be exactly 42 characters long + - Start with '0x' + - Contain only valid hex characters + +3. Numbers must: + - Be explicitly mentioned + - Be valid integers + - Be in the appropriate range for their purpose + +FIELD SPECIFICATIONS: +payload: + - data: Must be valid hex string starting with '0x' + - dataHash: Must be valid hex string starting with '0x' + - signatures: Array of objects, each containing: + * r: 64-character hex string (without '0x') + * s: 64-character hex string (without '0x') + * v: Integer number + - metadata: + * contentType: String matching known content types + * encoding: String or null + * compression: String or null + +agent: Must be valid 42-character Ethereum address +digest: Must be valid hex string starting with '0x' + +OUTPUT FORMAT: +\`\`\`json +{ + "payload": { + "data": null, + "dataHash": null, + "signatures": [], + "metadata": { + "contentType": null, + "encoding": null, + "compression": null + } + }, + "agent": null, + "digest": null +} +\`\`\` + +VALIDATION RULES: +1. For hex strings: + - Verify proper '0x' prefix where required + - Verify correct length + - Verify only valid hex characters + +2. For signatures: + - Only include if complete r, s, v values are present + - Verify r and s are valid 64-character hex strings + - Verify v is a valid integer + +3. For metadata: + - Only include contentType if it matches known formats + - Set encoding and compression to null if not explicitly specified + +4. General: + - Do not attempt to calculate or derive missing values + - Do not fill in partial information + - Return empty arrays instead of null for array fields when no valid items exist + +Input context to process: +{{recentMessages}} + +Remember: When in doubt, use null. Never generate fake data. +`; + +export const priceQueryTemplate = ` +TASK: Extract cryptocurrency trading pair information from user input. Extract pairs that follow the specified format patterns, regardless of whether the symbols represent actual cryptocurrencies. + +TRADING PAIR RULES: +1. Format Requirements: + - Must contain two symbols separated by a delimiter + - Acceptable delimiters: '/', '-', '_', or space + - Convert all pairs to standardized FORMAT: BASE/QUOTE + - Convert all letters to uppercase + +2. Symbol Requirements: + - Must be 2-5 characters long + - Must contain only letters + - Must be uppercase in output + +3. Pattern Recognition Examples: + - "ABC/USD" -> Valid, return "ABC/USD" + - "ABC-USD" -> Convert to "ABC/USD" + - "ABC USD" -> Convert to "ABC/USD" + - "ABCUSD" -> Convert to "ABC/USD" + - "ABCoin/USD" -> Invalid (symbol too long) + - "ABC to USD" -> Convert to "ABC/USD" + - "123/USD" -> Invalid (contains numbers) + - "A/USD" -> Invalid (symbol too short) + - "ABCDEF/USD" -> Invalid (symbol too long) + +VALIDATION: +1. REJECT and return null if: + - Only one symbol is mentioned + - Symbols are longer than 5 characters + - Symbols are shorter than 2 characters + - Symbols contain non-letter characters + - Format is completely unrecognizable + - More than two symbols are mentioned + +OUTPUT FORMAT: +\`\`\`json +{ + "pair": null +} +\`\`\` + +IMPORTANT NOTES: +1. DO NOT modify or correct user-provided symbols +2. DO NOT validate if symbols represent real cryptocurrencies +3. ONLY check format compliance +4. When format is invalid, return null +5. Accept ANY symbols that meet format requirements + +Input context to process: +{{recentMessages}} +`; + +export const attpsPriceQueryTemplate = ` + +TASK: Extract source agent and message identifiers from user input. Validate and format according to specified patterns. + +PARAMETER RULES: + +1. sourceAgentId Requirements: + - Format: UUID v4 format (8-4-4-4-12 hexadecimal) + - Case insensitive input but output must be lowercase + - Example: "b660e3f4-bbfe-4acb-97bd-c0869a7ea142" + +2. feedId Requirements: + - Format: 64-character hexadecimal prefixed with 0x + - Must be exactly 66 characters long including prefix + - Example: "0x0003665949c883f9e0f6f002eac32e00bd59dfe6c34e92a91c37d6a8322d6489" + +VALIDATION: + +1. REJECT and set to null if: + - Invalid UUID structure for sourceAgentId + - feedId length ≠ 66 characters + - feedId missing 0x prefix + - Contains non-hexadecimal characters + - Extra/missing hyphens in UUID + - Incorrect segment lengths + +OUTPUT FORMAT: +\`\`\`json +{ + "sourceAgentId": null, + "feedId": null +} +\`\`\` + +PROCESSING RULES: +1. Normalize sourceAgentId to lowercase +2. Preserve original feedId casing +3. Strict format validation before acceptance +4. Partial matches should return null +5. Return null for ambiguous formats + +Input context to process: +{{recentMessages}} + +`; \ No newline at end of file diff --git a/packages/plugin-apro/src/types.ts b/packages/plugin-apro/src/types.ts new file mode 100644 index 00000000000..82c85b1998b --- /dev/null +++ b/packages/plugin-apro/src/types.ts @@ -0,0 +1,142 @@ +import { z } from "zod"; + +export interface AgentSettings { + signers: string[] + threshold: number + converterAddress: string + agentHeader: { + messageId?: string + sourceAgentId?: string + sourceAgentName: string + targetAgentId: string + timestamp?: number + messageType: number + priority: number + ttl: number + } +} + +export const AgentSettingsSchema = z.object({ + signers: z.array(z.string()), + threshold: z.number(), + converterAddress: z.string(), + agentHeader: z.object({ + messageId: z.string().nullish(), + sourceAgentId: z.string().nullish(), + sourceAgentName: z.string(), + targetAgentId: z.string(), + timestamp: z.number().nullish(), + messageType: z.number(), + priority: z.number(), + ttl: z.number(), + }), +}); + +export const isAgentSettings = (value: any): value is AgentSettings => { + if (AgentSettingsSchema.safeParse(value).success) { + return true; + } + return false; +} + +interface Signature { + r: string + s: string + v: 1 | 0 | 27 | 28 + } + +interface MessagePayload { + data: string + dataHash?: string + signatures: Signature[] + metadata?: Metadata + } + +export interface VerifyParams { + agent: string + digest: string + payload: MessagePayload +} + +export const VerifyParamsSchema = z.object({ + agent: z.string(), + digest: z.string(), + payload: z.object({ + data: z.string(), + dataHash: z.string().nullish(), + signatures: z.array(z.object({ + r: z.string(), + s: z.string(), + v: z.number(), + })), + metadata: z.object({ + contentType: z.string().nullish(), + encoding: z.string().nullish(), + compression: z.string().nullish(), + }).nullish(), + }), +}); + +export const isVerifyParams = (value: any): value is VerifyParams => { + if (VerifyParamsSchema.safeParse(value).success) { + return true; + } + return false; +} + +export interface PriceQueryParams { + pair: string +} + +export const PriceQueryParamsSchema = z.object({ + pair: z.string(), +}); + +export const isPriceQueryParams = (value: any): value is PriceQueryParams => { + if (PriceQueryParamsSchema.safeParse(value).success) { + return true; + } + return false; +} + +export interface PriceData { + feedId: string + pair: string + networks: string[] + bidPrice: string + askPrice: string + midPrice: string + bidPriceChange: number + askPriceChange: number + midPriceChange: number + timestamp: number +} + +export const AttpsPriceQuerySchema = z.object({ + sourceAgentId: z.string(), + feedId: z.string(), +}); + +export const isAttpsPriceQuery = (value: any): value is AttpsPriceQuery => { + if (AttpsPriceQuerySchema.safeParse(value).success) { + return true; + } + return false; +} + +export interface AttpsPriceQuery { + sourceAgentId: string + feedId: string +} + +export interface AttpsPriceQueryResponse { + feedId: string + validTimeStamp: number + observeTimeStamp: number + nativeFee: number + tokenFee: number + expireTimeStamp: number + midPrice: string + askPrice: string + bidPrice: string +} \ No newline at end of file diff --git a/packages/plugin-apro/tsconfig.json b/packages/plugin-apro/tsconfig.json new file mode 100644 index 00000000000..005fbac9d36 --- /dev/null +++ b/packages/plugin-apro/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-apro/tsup.config.ts b/packages/plugin-apro/tsup.config.ts new file mode 100644 index 00000000000..7c51f1a4ca2 --- /dev/null +++ b/packages/plugin-apro/tsup.config.ts @@ -0,0 +1,12 @@ +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: [ + // Add other modules you want to externalize + ], +}); diff --git a/packages/plugin-btcfun/README.md b/packages/plugin-btcfun/README.md deleted file mode 100644 index 2b5db29d492..00000000000 --- a/packages/plugin-btcfun/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# `@elizaos/plugin-btcfun` - -A powerful plugin for Eliza that enables interaction with BTCFun via the Bitcoin network. This plugin provides seamless integration for minting BRC20 and Runes tokens using BTCFun's API services. - -## Features - -### Provider Features - -- **BTCFun Provider** - - Validates token operations for both BRC20 and Runes tokens - - Creates and manages minting orders - - Handles transaction broadcasting - - Manages PSBT (Partially Signed Bitcoin Transactions) - -### Action Features - -- **Token Minting** - - Supports minting of both BRC20 and Runes tokens - - Configurable mint parameters: - - Mint cap (maximum token supply) - - Address fundraising cap (per-address limit) - - Mint deadline (duration in seconds) - - Handles Bitcoin transaction signing and broadcasting - - Automatic PSBT creation and management - -## Configuration - -### Default Setup - -By default, **Bitcoin mainnet** is enabled. To use it, configure the following environment variables in your `.env` file: - -```env -BTC_PRIVATE_KEY_WIF=your-private-key-here -ADDRESS=your-address-here -BTCFUN_API_URL=https://api-testnet-new.btc.fun -MINTCAP=10000 -MINTDEADLINE=864000 -ADDRESS_FUNDRAISING_CAP=100 -``` - -### Environment Variables - -- `BTC_PRIVATE_KEY_WIF`: Your Bitcoin private key in WIF format -- `ADDRESS`: Your Bitcoin address -- `BTCFUN_API_URL`: BTCFun API endpoint URL -- `MINTCAP`: Default maximum token supply for minting -- `MINTDEADLINE`: Default duration for minting in seconds -- `ADDRESS_FUNDRAISING_CAP`: Default per-address fundraising limit - -## API Reference - -The plugin integrates with BTCFun's API services through the following endpoints: -- `/api/v1/import/brc20_validate` - BRC20 token validation -- `/api/v1/import/rune_validate` - Runes token validation -- `/api/v1/import/brc20_order` - BRC20 minting order creation -- `/api/v1/import/rune_order` - Runes minting order creation -- `/api/v1/import/broadcast` - Transaction broadcasting - -## License - -See parent project for license information. - -## Contributing - -Contributions are welcome! See parent project for contribution guidelines. diff --git a/packages/plugin-btcfun/package.json b/packages/plugin-btcfun/package.json deleted file mode 100644 index 2b933306b63..00000000000 --- a/packages/plugin-btcfun/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@elizaos/plugin-btcfun", - "version": "0.1.7-alpha.2", - "main": "dist/index.js", - "type": "module", - "types": "dist/index.d.ts", - "dependencies": { - "@elizaos/core": "workspace:*", - "@lifi/data-types": "5.15.5", - "@lifi/sdk": "3.4.1", - "@lifi/types": "16.3.0", - "tsup": "8.3.5", - "viem": "2.22.2", - "@okxweb3/coin-bitcoin": "1.2.0", - "@okxweb3/crypto-lib": "1.0.10", - "tiny-secp256k1": "2.2.3" - }, - "scripts": { - "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch", - "test": "vitest run", - "lint": "eslint --fix --cache ." - }, - "peerDependencies": { - "whatwg-url": "7.1.0" - } -} diff --git a/packages/plugin-btcfun/src/actions/btcfun.ts b/packages/plugin-btcfun/src/actions/btcfun.ts deleted file mode 100644 index d17c42652a5..00000000000 --- a/packages/plugin-btcfun/src/actions/btcfun.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ByteArray, formatEther, parseEther, type Hex } from "viem"; -import { - composeContext, - generateObjectDeprecated, - HandlerCallback, - ModelClass, - type IAgentRuntime, - type Memory, - type State, -} from "@elizaos/core"; - -import { networks, Psbt } from 'bitcoinjs-lib'; -import { BIP32Factory } from 'bip32'; -import {randomBytes} from 'crypto'; -import * as ecc from 'tiny-secp256k1'; -import { BtcWallet, privateKeyFromWIF } from "@okxweb3/coin-bitcoin"; -import { base } from "@okxweb3/crypto-lib"; -import { mintTemplate } from "../templates"; -import {initBtcFunProvider} from "../providers/btcfun.ts"; -export { mintTemplate }; - -function checkTokenType(tokenType: string) { - if (tokenType.toLowerCase() !== "brc20" && tokenType.toLowerCase() !== "runes") { - throw new Error("Invalid token type"); - } -} - -export const btcfunMintAction = { - name: "mint", - description: "btcfun mint brc20/runes", - handler: async ( - runtime: IAgentRuntime, - _message: Memory, - state: State, - _options: any, - callback?: HandlerCallback - ) => { - console.log("btcfun action handler called"); - const btcfunProvider = initBtcFunProvider(runtime); - - const chainCode = randomBytes(32); - const bip32Factory = BIP32Factory(ecc); - const network = networks.bitcoin; - const privateKeyWif = runtime.getSetting("BTC_PRIVATE_KEY_WIF") ?? process.env.BTC_PRIVATE_KEY_WIF; - let address = runtime.getSetting("ADDRESS") ?? process.env.ADDRESS; - - const privateKey = base.fromHex(privateKeyFromWIF(privateKeyWif, network)); - const privateKeyHex = base.toHex(privateKey); - const privateKeyBuffer = Buffer.from(privateKeyHex, 'hex'); - const keyPair = bip32Factory.fromPrivateKey(privateKeyBuffer, chainCode, network); - const publicKeyBuffer = Buffer.from(keyPair.publicKey); - const publicKeyHex = publicKeyBuffer.toString('hex'); - - // Compose mint context - const mintContext = composeContext({ - state, - template: mintTemplate, - }); - const content = await generateObjectDeprecated({ - runtime, - context: mintContext, - modelClass: ModelClass.LARGE, - }); - let tokenType = content.tokenType; - let tick = content.inputToken; - let mintcap = content.mintcap ?? runtime.getSetting("MINTCAP"); - let mintdeadline = content.mintdeadline ?? runtime.getSetting("MINTDEADLINE"); - let addressFundraisingCap = content.addressFundraisingCap ?? runtime.getSetting("ADDRESS_FUNDRAISING_CAP"); - console.log("begin to mint token", tick, content) - checkTokenType(tokenType) - //validateToken - await btcfunProvider.validateToken(tokenType, address, tick); - console.log("validate token success") - - try { - let {order_id, psbt_hex} = await btcfunProvider.createOrder( - tokenType, publicKeyHex, address, publicKeyHex, address, 10, - tick, addressFundraisingCap, mintdeadline, mintcap) - const psbt = Psbt.fromHex(psbt_hex) - let wallet = new BtcWallet() - const toSignInputs = []; - psbt.data.inputs.forEach((input, index)=>{ - toSignInputs.push({ - index: index, - address: address, - sighashTypes: [0], - disableTweakSigner: false, - }); - }) - - let params = { - type: 3, - psbt: psbt_hex, - autoFinalized: false, - toSignInputs: toSignInputs, - }; - - let signParams = { - privateKey: privateKeyWif, - data: params, - }; - let signedPsbtHex = await wallet.signTransaction(signParams); - const txHash = await btcfunProvider.broadcastOrder(order_id, signedPsbtHex) - console.log('signedPsbtHex: ', signedPsbtHex, 'orderID: ', order_id, 'txhash', txHash) - if (callback) { - callback({ - text: `Successfully mint ${tokenType} ${tick} tokens, mintcap ${mintcap}, mintdeadline ${mintdeadline}, addressFundraisingCap ${addressFundraisingCap} ,txhash ${txHash}`, - content: { - success: true, - orderID: order_id, - }, - }); - } - } catch (error) { - console.error('Error:', error); - } - }, - template: mintTemplate, - validate: async (runtime: IAgentRuntime) => { - const privateKey = runtime.getSetting("BTC_PRIVATE_KEY_WIF"); - return typeof privateKey === "string" && privateKey.length > 0; - }, - examples: [ - [ - { - user: "assistant", - content: { - text: "I'll help you mint 100000000 BRC20 Party", - action: "MINT_BRC20", - }, - }, - { - user: "assistant", - content: { - text: "I'll help you mint 100000000 RUNES Party", - action: "MINT_RUNES", - }, - }, - { - user: "user", - content: { - text: "import token BRC20 `Party`, mintcap 100000, addressFundraisingCap 10 mintdeadline 864000", - action: "MINT_BRC20", - }, - }, - { - user: "user", - content: { - text: "import token RUNES `Party2`, mintcap 100000, addressFundraisingCap 10 mintdeadline 864000", - action: "MINT_RUNES", - }, - }, - ], - ], - similes: ["MINT_BRC20","MINT_RUNES"], -}; diff --git a/packages/plugin-btcfun/src/index.ts b/packages/plugin-btcfun/src/index.ts deleted file mode 100644 index d80ab99bdbc..00000000000 --- a/packages/plugin-btcfun/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {btcfunMintAction} from "./actions/btcfun.ts"; - -export * from "./providers/btcfun"; - -import type { Plugin } from "@elizaos/core"; - -export const btcfunPlugin: Plugin = { - name: "btcfun", - description: "btcfun plugin", - providers: [], - evaluators: [], - services: [], - actions: [btcfunMintAction], -}; - -export default btcfunPlugin; diff --git a/packages/plugin-btcfun/src/providers/btcfun.ts b/packages/plugin-btcfun/src/providers/btcfun.ts deleted file mode 100644 index 7f06f872916..00000000000 --- a/packages/plugin-btcfun/src/providers/btcfun.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fetch from 'node-fetch'; -import type {IAgentRuntime} from "@elizaos/core"; - -export const initBtcFunProvider = (runtime: IAgentRuntime) => { - - const btcfunApiURL = runtime.getSetting("BTCFUN_API_URL") ?? process.env.BTCFUN_API_URL - if (!btcfunApiURL) { - throw new Error("BTCFUN_API_URL is not set"); - } - - return new BtcfunProvider(btcfunApiURL); -}; - -export class BtcfunProvider { - private apiUrl: string; - - constructor(apiUrl: string) { - this.apiUrl = apiUrl; - } - - async validateToken(tokenType: string, address: string, ticker: string) { - const url = tokenType === "runes" - ? `${this.apiUrl}/api/v1/import/rune_validate` - : `${this.apiUrl}/api/v1/import/brc20_validate`; - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - address: address, - ticker: ticker, - }), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.statusText}`); - } - - return response.json(); - } - - async createOrder(tokenType: string, paymentFromPubKey: string, paymentFrom: string, ordinalsFromPubKey: string, ordinalsFrom: string, feeRate: number, tick: string, addressFundraisingCap: string, mintDeadline: number, mintCap: string) { - const url = tokenType === "runes" - ? `${this.apiUrl}/api/v1/import/rune_order` - : `${this.apiUrl}/api/v1/import/brc20_order`; - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - payment_from_pub_key: paymentFromPubKey, - payment_from: paymentFrom, - ordinals_from_pub_key: ordinalsFromPubKey, - ordinals_from: ordinalsFrom, - fee_rate: feeRate, - tick: tick, - address_fundraising_cap: addressFundraisingCap, - mint_deadline: mintDeadline, - mint_cap: mintCap, - }), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.statusText}`); - } - - const result = await response.json(); - - if (result.code === "OK" && result.data) { - const { order_id, psbt_hex } = result.data; - return { order_id, psbt_hex }; - } else { - console.log("Invalid response", result) - throw new Error("Invalid response"); - } - } - - async broadcastOrder(orderId: string, signedPsbtHex: string) { - const response = await fetch(`${this.apiUrl}/api/v1/import/broadcast`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - order_id: orderId, - signed_psbt_hex: signedPsbtHex, - }), - }); - - if (!response.ok) { - throw new Error(`Error: ${response.statusText}`); - } - const result = await response.json(); - console.log("broadcastOrder result", result); - - if (result.code === "OK" && result.data) { - return result.data; - } - } -} diff --git a/packages/plugin-btcfun/src/templates/index.ts b/packages/plugin-btcfun/src/templates/index.ts deleted file mode 100644 index 1c6dae4c8e7..00000000000 --- a/packages/plugin-btcfun/src/templates/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const mintTemplate = `Given the recent messages and wallet information below: - -{{recentMessages}} - -{{walletInfo}} - -Extract the following information about the requested token swap: -- Input token type, eg:runes or brc20, should convert BRC20,brc20 to brc20, runes or RUNES to runes -- Input token symbol (the token being mint), eg: mint token abc -- Input token mintcap eg: "10000" -- Input token addressFundraisingCap everyone can offer eg: "10" -- Input token mintdeadline ,duration Using seconds as the unit eg: 864000 - - -Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined: - -\`\`\`json -{ - "tokenType": string | null, - "inputToken": string | null, - "mintcap": string | 1000, - "addressFundraisingCap": string | 10, - "mintdeadline" : number | 864000, -} -\`\`\` -`; diff --git a/packages/plugin-goat/package.json b/packages/plugin-goat/package.json index a868b64ed55..718881a0f53 100644 --- a/packages/plugin-goat/package.json +++ b/packages/plugin-goat/package.json @@ -21,7 +21,7 @@ "dependencies": { "@elizaos/core": "workspace:*", "@goat-sdk/adapter-vercel-ai": "0.2.0", - "@goat-sdk/core": "0.4.0", + "@goat-sdk/core": "0.4.6", "@goat-sdk/plugin-erc20": "0.2.2", "@goat-sdk/plugin-kim": "0.1.2", "@goat-sdk/wallet-evm": "0.2.0", diff --git a/packages/plugin-imgflip/src/actions/generate-meme.ts b/packages/plugin-imgflip/src/actions/generate-meme.ts index e713a87e6a5..dae01c661e3 100644 --- a/packages/plugin-imgflip/src/actions/generate-meme.ts +++ b/packages/plugin-imgflip/src/actions/generate-meme.ts @@ -89,23 +89,29 @@ async function getImgflipTemplate(template: string): Promise { throw new Error("Failed to find meme template"); } - // Try to find a close match - const closestMatch = allMemes.data.memes.find( + // Try to find close matches + const closeMatches = allMemes.data.memes.filter( (meme) => meme.name.toLowerCase().includes(template.toLowerCase()) || template.toLowerCase().includes(meme.name.toLowerCase()) ); - if (!closestMatch) { - // If no match found, return a popular template - return allMemes.data.memes[0]; + if (closeMatches.length === 0) { + // If no match found, return a random popular template + const randomIndex = Math.floor( + Math.random() * Math.min(10, allMemes.data.memes.length) + ); + return allMemes.data.memes[randomIndex]; } - return closestMatch; + // Return a random template from close matches + const randomIndex = Math.floor(Math.random() * closeMatches.length); + return closeMatches[randomIndex]; } - // Return the first (best) match from search results - return result.data.memes[0]; + // Return a random template from search results + const randomIndex = Math.floor(Math.random() * result.data.memes.length); + return result.data.memes[randomIndex]; } async function generateMemeCaptions( @@ -180,42 +186,6 @@ async function genereateMeme( return result.data.url; } -async function generateMemeText( - runtime: IAgentRuntime, - state: State, - imgflipTemplate: string, - captions: string[] -): Promise { - const template = ` -# About Arony: -{{bio}} -{{lore}} - -# Task: Generate a single sentence accompanying a meme in the character's voice and style. -The imgflip template used for the meme is: **${imgflipTemplate}** -The captions used for the meme are: -${captions.join("\n")} - -# Instructions: -Do not include hashtags. -Only respond with the text - do not include any other text.`; - - const context = await composeContext({ - state, - template, - }); - - elizaLogger.debug("generateMemeText context: ", context); - - const response = await generateText({ - runtime, - context, - modelClass: ModelClass.SMALL, - }); - - return response; -} - export interface Meme { url: string; text: string; @@ -223,7 +193,7 @@ export interface Meme { export async function generateMemeActionHandler( runtime: IAgentRuntime, - message: Memory, + message: string, state: State ): Promise { // STEPS @@ -231,26 +201,20 @@ export async function generateMemeActionHandler( // 2. Get the template's captions number from imgflip -> imgflip API call // 2. Generate the captions for the meme, based on the template (**also consider the agent character**) -> LLM call // 3. Generate the meme -> imgflip API call - // 4. Generate a text for the meme (**also consider the agent character**) -> LLM call - // 5. Return the meme url and the text + // 5. Return the meme url and text, description of the meme - const template = await findImgflipTemplate(runtime, message.content.text); + const template = await findImgflipTemplate(runtime, message); const imgflipTemplate = await getImgflipTemplate(template); const captions = await generateMemeCaptions( runtime, - message.content.text, + message, state, template, imgflipTemplate.box_count ); const url = await genereateMeme(imgflipTemplate, captions); - const text = await generateMemeText( - runtime, - state, - imgflipTemplate.name, - captions - ); + const text = `Generated a meme, using imgflip.com:\nMeme template: "${template}".\nCaptions:\n${captions.join("\n")}\nMeme URL: ${url}`; return { url, @@ -272,7 +236,11 @@ export const generateMemeAction: Action = { options: any, callback: HandlerCallback ) => { - const meme = await generateMemeActionHandler(runtime, message, state); + const meme = await generateMemeActionHandler( + runtime, + message.content.text, + state + ); const newMemory: Memory = { ...message, @@ -291,7 +259,10 @@ export const generateMemeAction: Action = { await runtime.messageManager.createMemory(newMemory); - callback(newMemory.content); + callback({ + text: "", + attachments: newMemory.content.attachments, + }); return true; }, diff --git a/packages/plugin-imgflip/src/actions/index.ts b/packages/plugin-imgflip/src/actions/index.ts index 7d709f129f9..864820e39eb 100644 --- a/packages/plugin-imgflip/src/actions/index.ts +++ b/packages/plugin-imgflip/src/actions/index.ts @@ -1 +1 @@ -export * from "./generate-meme.ts"; +export * from "./generate-meme"; diff --git a/packages/plugin-imgflip/src/index.ts b/packages/plugin-imgflip/src/index.ts index a3a1b58a3f8..97c39002286 100644 --- a/packages/plugin-imgflip/src/index.ts +++ b/packages/plugin-imgflip/src/index.ts @@ -1,16 +1,13 @@ import { Plugin } from "@elizaos/core"; -import { generateMemeAction } from "./actions/generate-meme.ts"; +import { generateMemeAction, generateMemeActionHandler, Meme } from "./actions"; -export * as actions from "./actions"; +export { generateMemeAction, generateMemeActionHandler, Meme }; export const imgflipPlugin: Plugin = { name: "imgflip", description: "Generate memes using imgflip.com", - actions: [ - generateMemeAction, - ], + actions: [generateMemeAction], evaluators: [], providers: [], }; export default imgflipPlugin; - diff --git a/packages/plugin-suno/src/actions/customGenerate.ts b/packages/plugin-suno/src/actions/customGenerate.ts index e23582e37a0..e15e777e083 100644 --- a/packages/plugin-suno/src/actions/customGenerate.ts +++ b/packages/plugin-suno/src/actions/customGenerate.ts @@ -1,6 +1,6 @@ -import { type Action, type IAgentRuntime, type Memory, type State, type HandlerCallback } from "@elizaos/core"; +import type { Action, IAgentRuntime, Memory, State, HandlerCallback } from "@elizaos/core"; import { SunoProvider } from "../providers/suno"; -import { CustomGenerateParams, GenerationResponse } from "../types"; +import type { CustomGenerateParams } from "../types"; const customGenerateMusic: Action = { name: "custom-generate-music", @@ -24,7 +24,7 @@ const customGenerateMusic: Action = { runtime: IAgentRuntime, message: Memory, state: State, - options: { [key: string]: unknown }, + _options: { [key: string]: unknown }, callback?: HandlerCallback ): Promise => { try { @@ -54,7 +54,7 @@ const customGenerateMusic: Action = { if (callback) { callback({ - text: `Successfully generated custom music`, + text: 'Successfully generated custom music', content: response }); } diff --git a/packages/plugin-suno/src/actions/extend.ts b/packages/plugin-suno/src/actions/extend.ts index 5e83b8acb61..b704989ccf7 100644 --- a/packages/plugin-suno/src/actions/extend.ts +++ b/packages/plugin-suno/src/actions/extend.ts @@ -1,6 +1,6 @@ -import { type Action, type IAgentRuntime, type Memory, type State, type HandlerCallback } from "@elizaos/core"; +import type { Action, IAgentRuntime, Memory, State, HandlerCallback } from "@elizaos/core"; import { SunoProvider } from "../providers/suno"; -import { ExtendParams, GenerationResponse } from "../types"; +import type { ExtendParams } from "../types"; const extendAudio: Action = { name: "extend-audio", @@ -20,7 +20,7 @@ const extendAudio: Action = { runtime: IAgentRuntime, message: Memory, state: State, - options: { [key: string]: unknown }, + _options: { [key: string]: unknown }, callback?: HandlerCallback ): Promise => { try { diff --git a/packages/plugin-suno/src/actions/generate.ts b/packages/plugin-suno/src/actions/generate.ts index 398d46b1f03..571f8c61096 100644 --- a/packages/plugin-suno/src/actions/generate.ts +++ b/packages/plugin-suno/src/actions/generate.ts @@ -1,6 +1,6 @@ -import { type Action, type IAgentRuntime, type Memory, type State, type HandlerCallback } from "@elizaos/core"; +import type { Action, IAgentRuntime, Memory, State, HandlerCallback } from "@elizaos/core"; import { SunoProvider } from "../providers/suno"; -import { GenerateParams, GenerationResponse } from "../types"; +import type { GenerateParams } from "../types"; const generateMusic: Action = { name: "generate-music", @@ -22,7 +22,7 @@ const generateMusic: Action = { runtime: IAgentRuntime, message: Memory, state: State, - options: { [key: string]: unknown }, + _options: { [key: string]: unknown }, callback?: HandlerCallback ): Promise => { try { @@ -47,7 +47,7 @@ const generateMusic: Action = { if (callback) { callback({ - text: `Successfully generated music based on your prompt`, + text: 'Successfully generated music based on your prompt', content: response }); } @@ -56,7 +56,7 @@ const generateMusic: Action = { } catch (error) { if (callback) { callback({ - text: `Failed to generate music: ${(error as Error).message}`, + text: `Failed to extend audio: ${(error as Error).message}`, error: error }); } diff --git a/packages/plugin-suno/src/index.ts b/packages/plugin-suno/src/index.ts index d599420df9f..85db7e423ff 100644 --- a/packages/plugin-suno/src/index.ts +++ b/packages/plugin-suno/src/index.ts @@ -1,4 +1,4 @@ -import { type Plugin } from "@elizaos/core"; +import type { Plugin } from "@elizaos/core"; import generateMusic from "./actions/generate"; import customGenerateMusic from "./actions/customGenerate"; import extendAudio from "./actions/extend"; diff --git a/packages/plugin-suno/src/providers/suno.ts b/packages/plugin-suno/src/providers/suno.ts index 62751f32986..7d628a5c089 100644 --- a/packages/plugin-suno/src/providers/suno.ts +++ b/packages/plugin-suno/src/providers/suno.ts @@ -1,4 +1,5 @@ -import { IAgentRuntime, Memory, State, type Provider } from "@elizaos/core"; +import type { IAgentRuntime, Memory, State } from "@elizaos/core"; // Added type keyword +import type { Provider } from "@elizaos/core"; // Added type keyword export interface SunoConfig { apiKey: string; @@ -22,8 +23,8 @@ export class SunoProvider implements Provider { this.baseUrl = config.baseUrl || 'https://api.suno.ai/v1'; } - async get(_runtime: IAgentRuntime, _message: Memory, _state?: State): Promise { - return this; + async get(_runtime: IAgentRuntime, _message: Memory, _state?: State): Promise<{ status: string }> { + return { status: 'ready' }; } async request(endpoint: string, options: RequestInit = {}) { diff --git a/packages/plugin-zilliqa/README.md b/packages/plugin-zilliqa/README.md new file mode 100644 index 00000000000..cadde319a1c --- /dev/null +++ b/packages/plugin-zilliqa/README.md @@ -0,0 +1,50 @@ +# @elizaos/plugin-zilliqa + +A plugin for integrating Zilliqa blockchain capabilities through the GOAT (Great Onchain Agent Toolkit) framework within the ElizaOS ecosystem. + +## Description + +[GOAT](https://ohmygoat.dev/) 🐐 (Great Onchain Agent Toolkit) is an open-source framework for adding blockchain tools such as wallets, being able to hold or trade tokens, or interacting with blockchain smart contracts, to your AI agent. + +- [Chains supported](https://ohmygoat.dev/chains-wallets-plugins) +- [Plugins supported](https://ohmygoat.dev/chains-wallets-plugins) + +This plugin integrates the GOAT Zilliqa plugin and wallet with Eliza. + +## Installation + +```bash +pnpm install @elizaos/plugin-zilliqa +``` + +## Configuration + +### Environment Variables + +```typescript +EVM_PRIVATE_KEY= +EVM_PROVIDER_URL= +ENABLE_ZILLIQA=1 +``` + +## Common Issues & Troubleshooting + +1. **Agent not executing an action**: + + - If you are also using the EVM Plugin, sometimes the agent might confuse the action name with an EVM Plugin action name instead of the GOAT Plugin action. Removing the EVM Plugin should fix this issue. There is no need for you to use both plugins at the same time. + - If you are using Trump as a character it might be tricky to get them to perform any action since the character is full of prompts that aim to change the topic of the conversation. To fix this try using a different character or create your own with prompts that are more suitable to what the agent is supposed to do. + +2. **Wallet Connection Issues** + + - Verify private key is correctly formatted + - Check RPC endpoint availability + - Ensure sufficient network balance + +3. **Transaction Issues** + - Verify gas availability + - Check network congestion + - Confirm transaction parameters + +## License + +This plugin is part of the Eliza project. See the main project repository for license information. diff --git a/packages/plugin-zilliqa/package.json b/packages/plugin-zilliqa/package.json new file mode 100644 index 00000000000..54fa403ff53 --- /dev/null +++ b/packages/plugin-zilliqa/package.json @@ -0,0 +1,26 @@ +{ + "name": "@elizaos/plugin-zilliqa", + "version": "0.1.7-alpha.2", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "@goat-sdk/adapter-vercel-ai": "0.2.7", + "@goat-sdk/core": "0.4.6", + "@goat-sdk/plugin-zilliqa": "0.1.3", + "@goat-sdk/wallet-evm": "0.2.6", + "@goat-sdk/wallet-viem": "0.2.6", + "@goat-sdk/wallet-zilliqa": "0.2.6", + "@zilliqa-js/account": "^3.5.0", + "@zilliqa-js/zilliqa": "^3.5.0", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-zilliqa/src/actions.ts b/packages/plugin-zilliqa/src/actions.ts new file mode 100644 index 00000000000..d77a9b230d9 --- /dev/null +++ b/packages/plugin-zilliqa/src/actions.ts @@ -0,0 +1,320 @@ +import { getOnChainTools } from "@goat-sdk/adapter-vercel-ai"; +import { MODE, USDC, erc20 } from "@goat-sdk/plugin-erc20"; +import { kim } from "@goat-sdk/plugin-kim"; +import { sendETH } from "@goat-sdk/wallet-evm"; +import { WalletClientBase } from "@goat-sdk/core"; +import { zilliqa } from "@goat-sdk/plugin-zilliqa"; + +import { + generateText, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type State, + composeContext, + elizaLogger, +} from "@elizaos/core"; +import { Zilliqa } from "@zilliqa-js/zilliqa"; + +export async function getOnChainActions( + evmWallet: WalletClientBase, + zilliqaWallet: WalletClientBase +) { + const actionsWithoutHandler = [ + { + name: "GET_BALANCE", + description: + "Retrieve the balance of a zilliqa account using the GET_ZILLIQA_ADDRESS_BALANCE tool or an evm account using the GET_BALANCE tool. Addresses may be expressed as a hex or bech32 address", + similes: [], + validate: async () => true, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Tell me the balance of account 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e", + action: "GET_BALANCE", + }, + }, + { + user: "{{agentName}}", + content: { + text: "The balance of account 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e is 2.01 zil", + action: "GET_BALANCE", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Tell me the balance of the account zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g", + }, + }, + { + user: "{{agentName}}", + content: { + text: "The balance of account zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g is 18.05 zil", + action: "GET_BALANCE", + }, + }, + ], + ], + }, + { + name: "CONVERT", + description: + "Convert address formats from bech32 to hex using the CONVERT_FROM_BECH32 tool or from hex to bech32 using the CONVERT_TO_BECH32 tool. The addresses to be converted may be either evm or zilliqa", + similes: [], + validate: async () => true, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Convert 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e to bech32", + }, + }, + { + user: "{{agentName}}", + content: { + text: "The bech32 address for 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e is zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g", + action: "CONVERT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Convert zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g to hex", + }, + }, + { + user: "{{agentName}}", + content: { + text: "The hex address for zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g is 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e", + action: "CONVERT", + }, + }, + ], + ], + }, + { + name: "TRANSFER", + description: + "Transfer funds from a Zilliqa address using TRANSFER_FROM_ZILLIQA_ADDRESS or from an EVM address using TRANSFER_FROM_EVM_ADDRESS. Addresses may be in either bech32 or hex format. Both kinds of transfer return the transaction id in hex.", + similes: [], + validate: async () => true, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Transfer 2 ZIL from the EVM address zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g to 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e", + action: "TRANSFER", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Transfer 2 ZIL from the Zilliqa address zil17r9jftrxhfeht0umn386j83q3k0240fwn7g70g to 0xf0cb24ac66ba7375bf9b9c4fa91e208d9eaabd2e", + action: "TRANSFER", + }, + }, + ], + ], + }, + // 1. Add your actions here + ]; + + const tools = await getOnChainTools({ + wallet: evmWallet, + // 2. Configure the plugins you need to perform those actions + plugins: [sendETH()], + }); + + const zilTools = await getOnChainTools({ + wallet: zilliqaWallet, + plugins: [zilliqa()], + }); + + const allTools = { ...zilTools, ...tools }; + // 3. Let GOAT handle all the actions + return actionsWithoutHandler.map((action) => ({ + ...action, + handler: getActionHandler(action.name, action.description, allTools), + })); +} + +function getActionHandler( + actionName: string, + actionDescription: string, + tools +) { + return async ( + runtime: IAgentRuntime, + message: Memory, + state: State | undefined, + options?: Record, + callback?: HandlerCallback + ): Promise => { + let currentState = state ?? (await runtime.composeState(message)); + currentState = await runtime.updateRecentMessageState(currentState); + + try { + // 1. Call the tools needed + const context = composeActionContext( + actionName, + actionDescription, + currentState + ); + const result = await generateText({ + runtime, + context, + tools, + maxSteps: 10, + // Uncomment to see the log each tool call when debugging + onStepFinish: (step) => { + console.log(step.toolResults); + }, + modelClass: ModelClass.LARGE, + }); + + // 2. Compose the response + const response = composeResponseContext(result, currentState); + const responseText = await generateResponse(runtime, response); + + callback?.({ + text: responseText, + content: {}, + }); + return true; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + // 3. Compose the error response + const errorResponse = composeErrorResponseContext( + errorMessage, + currentState + ); + const errorResponseText = await generateResponse( + runtime, + errorResponse + ); + + callback?.({ + text: errorResponseText, + content: { error: errorMessage }, + }); + return false; + } + }; +} + +function composeActionContext( + actionName: string, + actionDescription: string, + state: State +): string { + const actionTemplate = ` +# Knowledge +{{knowledge}} + +About {{agentName}}: +{{bio}} +{{lore}} + +{{providers}} + +{{attachments}} + + +# Action: ${actionName} +${actionDescription} + +{{recentMessages}} + +Based on the action chosen and the previous messages, execute the action and respond to the user using the tools you were given. +`; + return composeContext({ state, template: actionTemplate }); +} + +function composeResponseContext(result: unknown, state: State): string { + const responseTemplate = ` + # Action Examples +{{actionExamples}} +(Action examples are for reference only. Do not use the information from them in your response.) + +# Knowledge +{{knowledge}} + +# Task: Generate dialog and actions for the character {{agentName}}. +About {{agentName}}: +{{bio}} +{{lore}} + +{{providers}} + +{{attachments}} + +# Capabilities +Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. + +Here is the result: +${JSON.stringify(result)} + +{{actions}} + +Respond to the message knowing that the action was successful and these were the previous messages: +{{recentMessages}} + `; + return composeContext({ state, template: responseTemplate }); +} + +function composeErrorResponseContext( + errorMessage: string, + state: State +): string { + const errorResponseTemplate = ` +# Knowledge +{{knowledge}} + +# Task: Generate dialog and actions for the character {{agentName}}. +About {{agentName}}: +{{bio}} +{{lore}} + +{{providers}} + +{{attachments}} + +# Capabilities +Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. + +{{actions}} + +Respond to the message knowing that the action failed. +The error was: +${errorMessage} + +These were the previous messages: +{{recentMessages}} + `; + return composeContext({ state, template: errorResponseTemplate }); +} + +async function generateResponse( + runtime: IAgentRuntime, + context: string +): Promise { + return generateText({ + runtime, + context, + modelClass: ModelClass.SMALL, + }); +} diff --git a/packages/plugin-zilliqa/src/index.ts b/packages/plugin-zilliqa/src/index.ts new file mode 100644 index 00000000000..84e72c90f08 --- /dev/null +++ b/packages/plugin-zilliqa/src/index.ts @@ -0,0 +1,25 @@ +import type { Plugin } from "@elizaos/core"; +import { getOnChainActions } from "./actions"; +import { getZilliqaWalletClient, getWalletProviders } from "./wallet"; + +export async function zilliqaPlugin( + getSetting: (key: string) => string | undefined +): Promise { + const zilliqaWalletClient = await getZilliqaWalletClient(getSetting); + if (!zilliqaWalletClient) { + throw new Error("Zilliqa wallet client initialization failed. Ensure that ZILLIQA_PRIVATE_KEY and ZILLIQA_PROVIDER_URL are configured."); + } + const walletClient = zilliqaWalletClient!.getEVM(); + const actions = await getOnChainActions(walletClient, zilliqaWalletClient!); + + return { + name: "[ZILLIQA] Onchain Actions", + description: "Zilliqa integration plugin", + providers: getWalletProviders(walletClient, zilliqaWalletClient), + evaluators: [], + services: [], + actions: actions, + }; +} + +export default zilliqaPlugin; diff --git a/packages/plugin-zilliqa/src/wallet.ts b/packages/plugin-zilliqa/src/wallet.ts new file mode 100644 index 00000000000..47fcf7b6941 --- /dev/null +++ b/packages/plugin-zilliqa/src/wallet.ts @@ -0,0 +1,85 @@ +import { WalletClientBase } from "@goat-sdk/core"; +import { viem, type Chain } from "@goat-sdk/wallet-viem"; +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { mode } from "viem/chains"; +import { + zilliqaChainId, + zilliqaJSViemWalletClient, + ZilliqaWalletClient +} from "@goat-sdk/wallet-zilliqa"; +import { Account } from "@zilliqa-js/zilliqa"; + +// Add the chain you want to use, remember to update also +// the ZILLIQA_PROVIDER_URL to the correct one for the chain +export const chain = mode; + +function getViemChain(provider: string, id: number, decimals: number): Chain { + return { + id: id | 0x8000, + name: "zilliqa", + nativeCurrency: { + decimals: decimals, + name: "Zil", + symbol: "ZIL", + }, + rpcUrls: { + default: { + https: [provider], + }, + }, + }; +} + +export async function getZilliqaWalletClient( + getSetting: (key: string) => string | undefined +) { + const privateKey = getSetting("ZILLIQA_PRIVATE_KEY"); + if (!privateKey) return null; + + const provider = getSetting("ZILLIQA_PROVIDER_URL"); + if (!provider) throw new Error("ZILLIQA_PROVIDER_URL not configured"); + + const chainId = await zilliqaChainId(provider); + const account = new Account(privateKey); + const viemChain = getViemChain(provider, chainId, 18); + const viemWallet = createWalletClient({ + account: privateKeyToAccount(privateKey as `0x${string}`), + chain: viemChain, + transport: http(provider), + }); + + return zilliqaJSViemWalletClient(viemWallet, provider, account, chainId); +} + +export function getWalletProviders( + walletClient: WalletClientBase, + zilliqa: ZilliqaWalletClient +) { + return [ + { + async get(): Promise { + try { + const address = walletClient.getAddress(); + const balance = await walletClient.balanceOf(address); + return `EVM Wallet Address: ${address}\nBalance: ${balance} ZIL`; + } catch (error) { + console.error("Error in EVM wallet provider:", error); + return null; + } + }, + }, + { + async get(): Promise { + try { + const address = + zilliqa.getZilliqa().wallet.defaultAccount?.address; + return `Zilliqa wallet address: ${address}\n`; + } catch (error) { + console.error("Error in zilliqa wallet provider:", error); + return null; + } + }, + }, + ]; +} diff --git a/packages/plugin-btcfun/tsconfig.json b/packages/plugin-zilliqa/tsconfig.json similarity index 53% rename from packages/plugin-btcfun/tsconfig.json rename to packages/plugin-zilliqa/tsconfig.json index 2d8d3fe8181..f642a90aee1 100644 --- a/packages/plugin-btcfun/tsconfig.json +++ b/packages/plugin-zilliqa/tsconfig.json @@ -3,13 +3,7 @@ "compilerOptions": { "outDir": "dist", "rootDir": "./src", - "typeRoots": [ - "./node_modules/@types", - "./src/types" - ], "declaration": true }, - "include": [ - "src" - ] -} \ No newline at end of file + "include": ["src"] +} diff --git a/packages/plugin-zilliqa/tsup.config.ts b/packages/plugin-zilliqa/tsup.config.ts new file mode 100644 index 00000000000..5385e92cf89 --- /dev/null +++ b/packages/plugin-zilliqa/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 + "crypto", // Externalize crypto to use Node.js built-in module + "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", + "@lifi/sdk", + ], +});