Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding plugin for ZKsync Era #906

Merged
merged 1 commit into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,7 @@ WHATSAPP_API_VERSION=v17.0 # WhatsApp API version (default: v17.0)
# ICP
INTERNET_COMPUTER_PRIVATE_KEY=
INTERNET_COMPUTER_ADDRESS=

# ZKsync Era Configuration
ZKSYNC_ADDRESS=
ZKSYNC_PRIVATE_KEY=
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@ai16z/plugin-icp": "workspace:*",
"@ai16z/plugin-tee": "workspace:*",
"@ai16z/plugin-coinbase": "workspace:*",
"@ai16z/plugin-zksync-era": "workspace:*",
"readline": "1.3.0",
"ws": "8.18.0",
"@ai16z/plugin-evm": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { imageGenerationPlugin } from "@ai16z/plugin-image-generation";
import { evmPlugin } from "@ai16z/plugin-evm";
import { createNodePlugin } from "@ai16z/plugin-node";
import { solanaPlugin } from "@ai16z/plugin-solana";
import { zksyncEraPlugin } from "@ai16z/plugin-zksync-era";
import { teePlugin } from "@ai16z/plugin-tee";
import Database from "better-sqlite3";
import fs from "fs";
Expand Down Expand Up @@ -392,6 +393,7 @@ export function createAgent(
: []),
getSecret(character, "WALLET_SECRET_SALT") ? teePlugin : null,
getSecret(character, "ALCHEMY_API_KEY") ? goatPlugin : null,
getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null,
].filter(Boolean),
providers: [],
actions: [],
Expand Down
20 changes: 20 additions & 0 deletions packages/plugin-zksync-era/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@ai16z/plugin-zksync-era",
"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"
}
}
232 changes: 232 additions & 0 deletions packages/plugin-zksync-era/src/actions/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import {
ActionExample,
Content,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
type Action,
elizaLogger,
composeContext,
generateObject
} from "@ai16z/eliza";
import { validateZKsyncConfig } 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:
- ZK/zk: 0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E
- ETH/eth: 0x000000000000000000000000000000000000800A
- USDC/usdc: 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4

Example response:
\`\`\`json
{
"tokenAddress": "0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E",
"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_ZKSYNC",
"TRANSFER_TOKEN_ON_ERA",
"TRANSFER_TOKENS_ON_ZKSYNC",
"TRANSFER_TOKENS_ON_ERA",
"SEND_TOKENS_ON_ZKSYNC",
"SEND_TOKENS_ON_ERA",
"SEND_ETH_ON_ZKSYNC",
"SEND_ETH_ON_ERA",
"PAY_ON_ZKSYNC",
"PAY_ON_ERA",
],
validate: async (runtime: IAgentRuntime, message: Memory) => {
await validateZKsyncConfig(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<boolean> => {
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("ZKSYNC_PRIVATE_KEY")!;
const PUBLIC_KEY = runtime.getSetting("ZKSYNC_ADDRESS")!;

const web3: Web3 = new Web3(/* optional L1 provider */);

web3.registerPlugin(
new ZKsyncPlugin(
Web3ZKsyncL2.initWithDefaultProvider(types.Network.Mainnet),
),
);

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 ZK tokens to 0xbD8679cf79137042214fA4239b02F4022208EE82",
},
},
{
user: "{{agent}}",
content: {
text: "Of course. Sending 100 ZK to that address now.",
action: "SEND_TOKEN",
},
},
{
user: "{{agent}}",
content: {
text: "Successfully sent 100 ZK to 0xbD8679cf79137042214fA4239b02F4022208EE82\nTransaction: 0x0b9f23e69ea91ba98926744472717960cc7018d35bc3165bdba6ae41670da0f0",
},
},
]
] as ActionExample[][],
} as Action;

36 changes: 36 additions & 0 deletions packages/plugin-zksync-era/src/enviroment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IAgentRuntime } from "@ai16z/eliza";
import { z } from "zod";

export const zksyncEnvSchema = z.object({
ZKSYNC_ADDRESS: z.string().min(1, "ZKsync address is required"),
ZKSYNC_PRIVATE_KEY: z.string().min(1, "ZKsync private key is required"),
});

export type ZKsyncConfig = z.infer<typeof zksyncEnvSchema>;

export async function validateZKsyncConfig(
runtime: IAgentRuntime
): Promise<ZKsyncConfig> {
try {
const config = {
ZKSYNC_ADDRESS:
runtime.getSetting("ZKSYNC_ADDRESS") ||
process.env.ZKSYNC_ADDRESS,
ZKSYNC_PRIVATE_KEY:
runtime.getSetting("ZKSYNC_PRIVATE_KEY") ||
process.env.ZKSYNC_PRIVATE_KEY
};

return zksyncEnvSchema.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(
`ZKsync configuration validation failed:\n${errorMessages}`
);
}
throw error;
}
}
13 changes: 13 additions & 0 deletions packages/plugin-zksync-era/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Plugin } from "@ai16z/eliza";

import transfer from "./actions/transfer";

export const zksyncEraPlugin: Plugin = {
name: "zksync-era",
description: "ZKsync Era Plugin for Eliza",
actions: [transfer],
evaluators: [],
providers: [],
};

export default zksyncEraPlugin;
8 changes: 8 additions & 0 deletions packages/plugin-zksync-era/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../core/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}
20 changes: 20 additions & 0 deletions packages/plugin-zksync-era/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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
],
});
Loading