diff --git a/.env.example b/.env.example index ebb55391f9e..9ad022e3075 100644 --- a/.env.example +++ b/.env.example @@ -306,6 +306,10 @@ STARKNET_ADDRESS= STARKNET_PRIVATE_KEY= STARKNET_RPC_URL= +# Lens Network Configuration +LENS_ADDRESS= +LENS_PRIVATE_KEY= + # Coinbase COINBASE_COMMERCE_KEY= # From Coinbase developer portal COINBASE_API_KEY= # From Coinbase developer portal diff --git a/agent/package.json b/agent/package.json index 4e550537c72..c8fa68b8235 100644 --- a/agent/package.json +++ b/agent/package.json @@ -50,6 +50,7 @@ "@elizaos/plugin-gitbook": "workspace:*", "@elizaos/plugin-story": "workspace:*", "@elizaos/plugin-goat": "workspace:*", + "@elizaos/plugin-lensNetwork": "workspace:*", "@elizaos/plugin-icp": "workspace:*", "@elizaos/plugin-image-generation": "workspace:*", "@elizaos/plugin-movement": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index 9329c2c7fc2..bce55ffcb54 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -63,6 +63,7 @@ import { flowPlugin } from "@elizaos/plugin-flow"; import { fuelPlugin } from "@elizaos/plugin-fuel"; import { genLayerPlugin } from "@elizaos/plugin-genlayer"; import { imageGenerationPlugin } from "@elizaos/plugin-image-generation"; +import { lensPlugin } from "@elizaos/plugin-lensNetwork"; import { multiversxPlugin } from "@elizaos/plugin-multiversx"; import { nearPlugin } from "@elizaos/plugin-near"; import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation"; @@ -717,6 +718,10 @@ export async function createAgent( getSecret(character, "FLOW_PRIVATE_KEY") ? flowPlugin : null, + getSecret(character, "LENS_ADDRESS") && + getSecret(character, "LENS_PRIVATE_KEY") + ? lensPlugin + : null, getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null, getSecret(character, "MVX_PRIVATE_KEY") ? multiversxPlugin : null, getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null, diff --git a/packages/plugin-lensNetwork/README.md b/packages/plugin-lensNetwork/README.md new file mode 100644 index 00000000000..3bf8e2e48e6 --- /dev/null +++ b/packages/plugin-lensNetwork/README.md @@ -0,0 +1,99 @@ +# @elizaos/plugin-abstract + +A plugin for interacting with the Abstract blockchain network within the ElizaOS ecosystem. + +## Description +The Abstract plugin enables seamless token transfers on the Abstract testnet. It provides functionality to transfer both native ETH and ERC20 tokens using secure wallet operations. + +## Installation + +```bash +pnpm install @elizaos/plugin-lensNetwork +``` + +## Configuration + +The plugin requires the following environment variables to be set: +```typescript +LENS_ADDRESS= +LENS_PRIVATE_KEY= +``` + +## Usage + +### Basic Integration + +```typescript +import { lensPlugin } from '@elizaos/plugin-lensNetwork'; +``` + +### Transfer Examples + +```typescript +// The plugin responds to natural language commands like: + +"Send 1 Grass to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62" + +``` + +## API Reference + +### Actions + +#### SEND_TOKEN + +Transfers tokens from the agent's wallet to another address. + +**Aliases:** +- TRANSFER_TOKEN_ON_LENS +- TRANSFER_TOKENS_ON_LENS +- SEND_TOKENS_ON_LENS +- SEND_ETH_ON_LENS +- PAY_ON_LENS +- MOVE_TOKENS_ON_LENS +- MOVE_ETH_ON_LENS + +## Common Issues & Troubleshooting + +1. **Transaction Failures** + - Verify wallet has sufficient balance + - Check recipient address format + - Ensure private key is correctly set + - Verify network connectivity + +2. **Configuration Issues** + - Verify all required environment variables are set + - Ensure private key format is correct + - Check wallet address format + +## Security Best Practices + +1. **Private Key Management** + - Store private key securely using environment variables + - Never commit private keys to version control + - Use separate wallets for development and production + - Monitor wallet activity regularly + +## Development Guide + +### Setting Up Development Environment + +1. Clone the repository +2. Install dependencies: + +```bash +pnpm install +``` + +3. Build the plugin: + +```bash +pnpm run build +``` + +4. Run the plugin: + +```bash +pnpm run dev +``` + diff --git a/packages/plugin-lensNetwork/package.json b/packages/plugin-lensNetwork/package.json new file mode 100644 index 00000000000..d3388c872e2 --- /dev/null +++ b/packages/plugin-lensNetwork/package.json @@ -0,0 +1,37 @@ +{ + "name": "@elizaos/plugin-lensNetwork", + "version": "0.1.7", + "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:*", + "tsup": "^8.3.5", + "web3": "^4.15.0", + "@lens-network/sdk": "^0.0.0-canary-20241203140504", + + "dotenv": "^16.0.3", + "ethers": "^6.0.0", + "zksync-ethers": "^6.0.0" + }, + "scripts": { + "build": "tsup --format esm --dts" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-lensNetwork/src/actions/transfer.ts b/packages/plugin-lensNetwork/src/actions/transfer.ts new file mode 100644 index 00000000000..84bb54309c5 --- /dev/null +++ b/packages/plugin-lensNetwork/src/actions/transfer.ts @@ -0,0 +1,292 @@ +import { + ActionExample, + Content, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, + elizaLogger, + composeContext, + generateObject, +} from "@elizaos/core"; +import { validateLensConfig } from "../environment"; +import { getDefaultProvider, Network, Wallet } from "@lens-network/sdk/ethers"; +import { ethers, formatEther } from "ethers"; + +import { + Address, + createWalletClient, + erc20Abi, + http, + parseEther, + isAddress, +} from "viem"; + +import { z } from "zod"; + +const TransferSchema = z.object({ + tokenAddress: z.string(), + recipient: z.string(), + amount: z.string(), +}); + +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.recipient === "string" && + (typeof content.amount === "string" || + typeof content.amount === "number"); + if (!validTypes) { + return false; + } + + // Validate addresses + const validAddresses = + + 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: +- ETH/eth: 0x000000000000000000000000000000000000800A + + +Example response: +\`\`\`json +{ + + "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.`; + +const ETH_ADDRESS = "0x000000000000000000000000000000000000800A"; + +export async function setupProviders() { + // Initialize providers for both L2 (Lens) and L1 (Ethereum) + const lensProvider = getDefaultProvider(Network.Testnet); + const ethProvider = ethers.getDefaultProvider("sepolia"); + + return { lensProvider, ethProvider }; +} + +export async function setupWallet( + lensProvider: any, + ethProvider: any, + key: any +) { + // Create wallet instance with both L2 and L1 providers + const wallet = new Wallet(key, lensProvider, ethProvider); + + return wallet; +} + +export async function transferTokens( + wallet: any, + recipientAddress: string, + amount: string +) { + try { + // Validate recipient address + if (!isAddress(recipientAddress)) { + throw new Error("Invalid recipient address"); + } + + // Create transaction object + const tx = { + to: recipientAddress, + value: parseEther(amount), + }; + + // Send transaction + console.log( + `Initiating transfer of ${amount} tokens to ${recipientAddress}...` + ); + const transaction = await wallet.sendTransaction(tx); + + // Wait for transaction confirmation + console.log(`Transaction hash: ${transaction.hash}`); + const receipt = await transaction.wait(); + + console.log("Transfer completed successfully!"); + console.log("Transaction receipt:", receipt); + + return transaction.hash; + } catch (error) { + console.error("Error transferring tokens:", error); + throw error; + } +} + +export default { + name: "SEND_TOKEN", + similes: [ + "TRANSFER_TOKEN_ON_LENS", + "TRANSFER_TOKENS_ON_LENS", + "SEND_TOKENS_ON_LENS", + "SEND_GRASS_ON_LENS", + "PAY_ON_LENS", + "MOVE_TOKENS_ON_LENS", + "MOVE_GRASS_ON_LENS", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + await validateLensConfig(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 LENS 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, + schema: TransferSchema, + }) + ).object as unknown as TransferContent; + + // 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("LENS_PRIVATE_KEY")!; + const { lensProvider, ethProvider } = await setupProviders(); + const wallet = await setupWallet( + lensProvider, + ethProvider, + PRIVATE_KEY + ); + const amount = content.amount.toString(); + + let hash; + + hash = await transferTokens( + wallet, + content.recipient as Address, + amount + ); + + elizaLogger.success( + "Transfer completed successfully! Transaction hash: " + hash + ); + if (callback) { + callback({ + text: + "Transfer completed successfully! Transaction hash: " + + hash, + 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 1 Grass to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll send 1 Grass to that address now.", + action: "SEND_TOKEN", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully sent 1 Grass to 0xCCa8009f5e09F8C5dB63cb0031052F9CB635Af62\nTransaction: 0x4fed598033f0added272c3ddefd4d83a521634a738474400b27378db462a76ec", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Please send 0.1 GRASS to 0xbD8679cf79137042214fA4239b02F4022208EE82", + }, + }, + { + user: "{{agent}}", + content: { + text: "Of course. Sending 0.1 Grass to that address now.", + action: "SEND_TOKEN", + }, + }, + { + user: "{{agent}}", + content: { + text: "Successfully sent 0.1 Grass to 0xbD8679cf79137042214fA4239b02F4022208EE82\nTransaction: 0x0b9f23e69ea91ba98926744472717960cc7018d35bc3165bdba6ae41670da0f0", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-lensNetwork/src/environment.ts b/packages/plugin-lensNetwork/src/environment.ts new file mode 100644 index 00000000000..823fb3b8925 --- /dev/null +++ b/packages/plugin-lensNetwork/src/environment.ts @@ -0,0 +1,32 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const lensEnvSchema = z.object({ + LENS_ADDRESS: z.string().min(1, "LENS address is required"), + LENS_PRIVATE_KEY: z.string().min(1, "LENS private key is required"), +}); + +export type LensConfig = z.infer; + +export async function validateLensConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + LENS_ADDRESS: runtime.getSetting("LENS_ADDRESS"), + LENS_PRIVATE_KEY: runtime.getSetting("LENS_PRIVATE_KEY"), + }; + + return lensEnvSchema.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( + `Lens configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-lensNetwork/src/index.ts b/packages/plugin-lensNetwork/src/index.ts new file mode 100644 index 00000000000..e3406599e68 --- /dev/null +++ b/packages/plugin-lensNetwork/src/index.ts @@ -0,0 +1,14 @@ +import { Plugin } from "@elizaos/core"; + +import transfer from "./actions/transfer.ts"; + + +export const LensPlugin: Plugin = { + name: "Lens", + description: "Lens Plugin for Eliza", + actions: [transfer], + evaluators: [], + providers: [], +}; + +export default LensPlugin; diff --git a/packages/plugin-lensNetwork/tsconfig.json b/packages/plugin-lensNetwork/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /dev/null +++ b/packages/plugin-lensNetwork/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-lensNetwork/tsup.config.ts b/packages/plugin-lensNetwork/tsup.config.ts new file mode 100644 index 00000000000..e42bf4efeae --- /dev/null +++ b/packages/plugin-lensNetwork/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 + ], +});