diff --git a/.env.example b/.env.example index b1f05998b7..1765f8a4f7 100644 --- a/.env.example +++ b/.env.example @@ -335,3 +335,7 @@ STORY_PRIVATE_KEY= # Story private key STORY_API_BASE_URL= # Story API base URL STORY_API_KEY= # Story API key PINATA_JWT= # Pinata JWT for uploading files to IPFS + +# Cronos zkEVM +CRONOSZKEVM_ADDRESS= +CRONOSZKEVM_PRIVATE_KEY= diff --git a/agent/package.json b/agent/package.json index 91a900d600..78b4d8a4ad 100644 --- a/agent/package.json +++ b/agent/package.json @@ -51,6 +51,7 @@ "@elizaos/plugin-multiversx": "workspace:*", "@elizaos/plugin-near": "workspace:*", "@elizaos/plugin-zksync-era": "workspace:*", + "@ai16z/plugin-cronoszkevm": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/agent/src/index.ts b/agent/src/index.ts index 1e49bae84f..038324e8b1 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -55,6 +55,7 @@ import { suiPlugin } from "@elizaos/plugin-sui"; import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { tonPlugin } from "@elizaos/plugin-ton"; import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; +import { cronosZkEVMPlugin } from "@ai16z/plugin-cronoszkEVM"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -568,6 +569,7 @@ export async function createAgent( getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null, getSecret(character, "MVX_PRIVATE_KEY") ? multiversxPlugin : null, getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null, + getSecret(character, "CRONOSZKEVM_PRIVATE_KEY") ? cronosZkEVMPlugin : null, getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null, getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null, getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null, diff --git a/packages/plugin-cronoszkevm/package.json b/packages/plugin-cronoszkevm/package.json new file mode 100644 index 0000000000..5098b11193 --- /dev/null +++ b/packages/plugin-cronoszkevm/package.json @@ -0,0 +1,20 @@ +{ + "name": "@ai16z/plugin-cronoszkevm", + "version": "0.1.4-alpha.3", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "@ai16z/plugin-trustdb": "workspace:*", + "tsup": "^8.3.5", + "web3": "^4.15.0", + "web3-plugin-zksync": "^1.0.8" + }, + "scripts": { + "build": "tsup --format esm --dts" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-cronoszkevm/src/actions/transfer.ts b/packages/plugin-cronoszkevm/src/actions/transfer.ts new file mode 100644 index 0000000000..772a0bd3e2 --- /dev/null +++ b/packages/plugin-cronoszkevm/src/actions/transfer.ts @@ -0,0 +1,230 @@ +import { + ActionExample, + Content, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, + elizaLogger, + composeContext, + generateObject +} from "@ai16z/eliza"; +import { validateCronosZkevmConfig } from "../enviroment"; + +import {Web3} from "web3"; +import { + ZKsyncPlugin, + ZKsyncWallet, + types, + Web3ZKsyncL2 +} from "web3-plugin-zksync"; + +export interface TransferContent extends Content { + tokenAddress: string; + recipient: string; + amount: string | number; +} + +export function isTransferContent( + content: TransferContent + ): content is TransferContent { + + // Validate types + const validTypes = + typeof content.tokenAddress === "string" && + typeof content.recipient === "string" && + (typeof content.amount === "string" || + typeof content.amount === "number"); + if (!validTypes) { + return false; + } + + // Validate addresses + const validAddresses = + content.tokenAddress.startsWith("0x") && + content.tokenAddress.length === 42 && + content.recipient.startsWith("0x") && + content.recipient.length === 42; + + return validAddresses; +} + + +const transferTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Here are several frequently used addresses. Use these for the corresponding tokens: +- ZKCRO/zkCRO: 0x000000000000000000000000000000000000800A +- USDC/usdc: 0xaa5b845f8c9c047779bedf64829601d8b264076c +- ETH/eth: 0x898b3560affd6d955b1574d87ee09e46669c60ea + +Example response: +\`\`\`json +{ + "tokenAddress": "0xaa5b845f8c9c047779bedf64829601d8b264076c", + "recipient": "0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62", + "amount": "1000" +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested token transfer: +- Token contract address +- Recipient wallet address +- Amount to transfer + +Respond with a JSON markdown block containing only the extracted values.`; + +export default { + name: "SEND_TOKEN", + similes: [ + "TRANSFER_TOKEN_ON_CRONOSZKEVM", + "TRANSFER_TOKENS_ON_CRONOSZK", + "SEND_TOKENS_ON_CRONOSZKEVM", + "SEND_TOKENS_ON_CRONOSZK", + "SEND_ETH_ON_CRONOSZKEVM", + "SEND_ETH_ON_CRONOSZK", + "PAY_ON_CRONOSZKEVM", + "PAY_ON_CRONOSZK", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + await validateCronosZkevmConfig(runtime); + return true; + }, + description: "Transfer tokens from the agent's wallet to another address", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting SEND_TOKEN handler..."); + + // Initialize or update state + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose transfer context + const transferContext = composeContext({ + state, + template: transferTemplate, + }); + + // Generate transfer content + const content = await generateObject({ + runtime, + context: transferContext, + modelClass: ModelClass.SMALL, + }); + + // Validate transfer content + if (!isTransferContent(content)) { + console.error("Invalid content for TRANSFER_TOKEN action."); + if (callback) { + callback({ + text: "Unable to process transfer request. Invalid content provided.", + content: { error: "Invalid transfer content" }, + }); + } + return false; + } + + try { + const PRIVATE_KEY = runtime.getSetting("CRONOSZKEVM_PRIVATE_KEY")!; + const PUBLIC_KEY = runtime.getSetting("CRONOSZKEVM_ADDRESS")!; + + const web3: Web3 = new Web3(/* optional L1 provider */); + + web3.registerPlugin( + new ZKsyncPlugin( + new Web3ZKsyncL2('https://mainnet.zkevm.cronos.org'), + ), + ); + + const smartAccount = new web3.ZKsync.SmartAccount({ address: PUBLIC_KEY, secret: "0x" + PRIVATE_KEY }) + + const transferTx = await smartAccount.transfer({ + to: content.recipient, + token: content.tokenAddress, + amount: web3.utils.toWei(content.amount,'ether'), + }); + + const receipt = await transferTx.wait(); + + elizaLogger.success( + "Transfer completed successfully! tx: " + receipt.transactionHash + ); + if (callback) { + callback({ + text: + "Transfer completed successfully! tx: " + + receipt.transactionHash, + content: {}, + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error during token transfer:", error); + if (callback) { + callback({ + text: `Error transferring tokens: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Send 100 USDC to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll send 100 USDC to that address now.", + action: "SEND_TOKEN", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully sent 100 USDC to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Please send 100 ZKCRO tokens to 0xbD8679cf79137042214fA4239b02F4022208EE82", + }, + }, + { + user: "{{agent}}", + content: { + text: "Of course. Sending 100 ZKCRO to that address now.", + action: "SEND_TOKEN", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully sent 100 ZKCRO to 0xbD8679cf79137042214fA4239b02F4022208EE82\nTransaction: 0x0b9f23e69ea91ba98926744472717960cc7018d35bc3165bdba6ae41670da0f0", + }, + }, + ] + ] as ActionExample[][], +} as Action; + diff --git a/packages/plugin-cronoszkevm/src/enviroment.ts b/packages/plugin-cronoszkevm/src/enviroment.ts new file mode 100644 index 0000000000..0fc02e3ac7 --- /dev/null +++ b/packages/plugin-cronoszkevm/src/enviroment.ts @@ -0,0 +1,36 @@ +import { IAgentRuntime } from "@ai16z/eliza"; +import { z } from "zod"; + +export const CronosZkEVMEnvSchema = z.object({ + CRONOSZKEVM_ADDRESS: z.string().min(1, "Cronos zkEVM address is required"), + CRONOSZKEVM_PRIVATE_KEY: z.string().min(1, "Cronos zkEVM private key is required"), +}); + +export type CronoszkEVMConfig = z.infer; + +export async function validateCronosZkevmConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + CRONOSZKEVM_ADDRESS: + runtime.getSetting("CRONOSZKEVM_ADDRESS") || + process.env.CRONOSZKEVM_ADDRESS, + CRONOSZKEVM_PRIVATE_KEY: + runtime.getSetting("CRONOSZKEVM_PRIVATE_KEY") || + process.env.CRONOSZKEVM_PRIVATE_KEY + }; + + return CronosZkEVMEnvSchema.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `CronosZkEVM configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-cronoszkevm/src/index.ts b/packages/plugin-cronoszkevm/src/index.ts new file mode 100644 index 0000000000..5c6ccf8f9b --- /dev/null +++ b/packages/plugin-cronoszkevm/src/index.ts @@ -0,0 +1,13 @@ +import { Plugin } from "@ai16z/eliza"; + +import transfer from "./actions/transfer"; + +export const cronosZkEVMPlugin: Plugin = { + name: "cronoszkevm", + description: "Cronos zkEVM plugin for Eliza", + actions: [transfer], + evaluators: [], + providers: [], +}; + +export default cronosZkEVMPlugin; diff --git a/packages/plugin-cronoszkevm/tsconfig.json b/packages/plugin-cronoszkevm/tsconfig.json new file mode 100644 index 0000000000..005fbac9d3 --- /dev/null +++ b/packages/plugin-cronoszkevm/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-cronoszkevm/tsup.config.ts b/packages/plugin-cronoszkevm/tsup.config.ts new file mode 100644 index 0000000000..121caa999a --- /dev/null +++ b/packages/plugin-cronoszkevm/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive" + // Add other modules you want to externalize + ], +});