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

Add balance action in evm plugin #1

Merged
merged 2 commits into from
Jan 6, 2025
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
149 changes: 149 additions & 0 deletions packages/plugin-evm/src/actions/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { composeContext, generateObjectDeprecated, ModelClass, type IAgentRuntime, type Memory, type State } from "@elizaos/core";

import { initWalletProvider } from "../providers/wallet";
import { SupportedChain, AddressParams } from "../types";
import { getAddressTemplate } from "../templates";
import { formatEther } from "viem";

export { getAddressTemplate }

const buildAddressDetails = async (
state: State,
runtime: IAgentRuntime,
): Promise<AddressParams> => {
const context = composeContext({
state,
template: getAddressTemplate,
});

const addressDetails = (await generateObjectDeprecated({
runtime,
context: context,
modelClass: ModelClass.SMALL,
})) as AddressParams;

return addressDetails;
}

export const getBalanceAction = {
name: "getBalance",
description: "Get the balance of a provided wallet address",
handler: async (
runtime: IAgentRuntime,
_message: Memory,
state: State,
_options: any,
callback?: (response: any) => void
) => {
const walletProvider = initWalletProvider(runtime);
const currentChain = walletProvider.getCurrentChain().name.toLowerCase() as SupportedChain;
const { address } = await buildAddressDetails(state, runtime);

try {
const client = walletProvider.getPublicClient(currentChain);
const balance = await client.getBalance({ address });
const formattedBalance = formatEther(balance);

if (callback) {
callback({
text: `Balance for address ${address}: ${formattedBalance}`,
content: { balance: formattedBalance },
});
}
return true;
} catch (error) {
console.error("Error getting balance:", error);
if (callback) {
callback({
text: `Error getting balance: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
template: getAddressTemplate,
validate: async (runtime: IAgentRuntime) => {
const privateKey = runtime.getSetting("EVM_PRIVATE_KEY");
return typeof privateKey === "string" && privateKey.startsWith("0x");
},
examples: [
[
{
user: "assistant",
content: {
text: "I'll check the balance for 0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
action: "GET_BALANCE",
},
},
{
user: "user",
content: {
text: "What's the balance of 0x742d35Cc6634C0532925a3b844Bc454e4438f44e?",
action: "GET_BALANCE",
},
},
],
],
similes: ["CHECK_BALANCE", "BALANCE_INQUIRY", "ACCOUNT_BALANCE"],
};

export const getBlockAction = {
name: "getBlockNumber",
description: "Get the current block number",
handler: async (
runtime: IAgentRuntime,
_message: Memory,
state: State,
_options: any,
callback?: (response: any) => void
) => {
const walletProvider = initWalletProvider(runtime);
const currentChain = walletProvider.getCurrentChain().name.toLowerCase() as SupportedChain;

try {
const client = walletProvider.getPublicClient(currentChain);
const blockNumber = Number(await client.getBlockNumber());

if (callback) {
callback({
text: `Current block number: ${blockNumber}`,
content: { blockNumber },
});
}
return true;
} catch (error) {
console.error("Error getting block number:", error);
if (callback) {
callback({
text: `Error getting block number: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
validate: async (runtime: IAgentRuntime) => {
const privateKey = runtime.getSetting("EVM_PRIVATE_KEY");
return typeof privateKey === "string" && privateKey.startsWith("0x");
},
examples: [
[
{
user: "assistant",
content: {
text: "I'll fetch the latest block number",
action: "GET_BLOCK",
},
},
{
user: "user",
content: {
text: "What's the current block number?",
action: "GET_BLOCK",
},
},
],
],
similes: ["BLOCK_NUMBER", "FETCH_BLOCK", "BLOCK_INFO"],
};
141 changes: 25 additions & 116 deletions packages/plugin-evm/src/actions/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ByteArray, formatEther, parseEther, type Hex } from "viem";
import {
composeContext,
generateObjectDeprecated,
Expand All @@ -9,100 +8,28 @@ import {
type State,
} from "@elizaos/core";

import { initWalletProvider, WalletProvider } from "../providers/wallet";
import type { Transaction, TransferParams } from "../types";
import { transferTemplate } from "../templates";
import { TransferParams } from "../types";

export { transferTemplate };

// Exported for tests
export class TransferAction {
constructor(private walletProvider: WalletProvider) {}

async transfer(params: TransferParams): Promise<Transaction> {
console.log(
`Transferring: ${params.amount} tokens to (${params.toAddress} on ${params.fromChain})`
);

if (!params.data) {
params.data = "0x";
}

this.walletProvider.switchChain(params.fromChain);

const walletClient = this.walletProvider.getWalletClient(
params.fromChain
);

try {
const hash = await walletClient.sendTransaction({
account: walletClient.account,
to: params.toAddress,
value: parseEther(params.amount),
data: params.data as Hex,
kzg: {
blobToKzgCommitment: function (_: ByteArray): ByteArray {
throw new Error("Function not implemented.");
},
computeBlobKzgProof: function (
_blob: ByteArray,
_commitment: ByteArray
): ByteArray {
throw new Error("Function not implemented.");
},
},
chain: undefined,
});

return {
hash,
from: walletClient.account.address,
to: params.toAddress,
value: parseEther(params.amount),
data: params.data as Hex,
};
} catch (error) {
throw new Error(`Transfer failed: ${error.message}`);
}
}
}

const buildTransferDetails = async (
state: State,
runtime: IAgentRuntime,
wp: WalletProvider
): Promise<TransferParams> => {
const context = composeContext({
state,
template: transferTemplate,
});

const chains = Object.keys(wp.chains);

const contextWithChains = context.replace(
"SUPPORTED_CHAINS",
chains.map((item) => `"${item}"`).join("|")
);

const transferDetails = (await generateObjectDeprecated({
runtime,
context: contextWithChains,
context: context,
modelClass: ModelClass.SMALL,
})) as TransferParams;

const existingChain = wp.chains[transferDetails.fromChain];

if (!existingChain) {
throw new Error(
"The chain " +
transferDetails.fromChain +
" not configured yet. Add the chain or choose one from configured: " +
chains.toString()
);
}

return transferDetails;
};
}

export const transferAction = {
name: "transfer",
Expand All @@ -114,48 +41,30 @@ export const transferAction = {
_options: any,
callback?: HandlerCallback
) => {
console.log("Transfer action handler called");
const walletProvider = initWalletProvider(runtime);
const action = new TransferAction(walletProvider);

// Compose transfer context
const paramOptions = await buildTransferDetails(
state,
runtime,
walletProvider
);

try {
const transferResp = await action.transfer(paramOptions);
if (callback) {
callback({
text: `Successfully transferred ${paramOptions.amount} tokens to ${paramOptions.toAddress}\nTransaction Hash: ${transferResp.hash}`,
content: {
success: true,
hash: transferResp.hash,
amount: formatEther(transferResp.value),
recipient: transferResp.to,
chain: paramOptions.fromChain,
},
});
}
return true;
} catch (error) {
console.error("Error during token transfer:", error);
if (callback) {
callback({
text: `Error transferring tokens: ${error.message}`,
content: { error: error.message },
});
const transferDetails = await buildTransferDetails(state, runtime);

try {
if (callback) {
callback({
text: `You are about to transfer ${transferDetails.amount} to ${transferDetails.toAddress}`,
content: transferDetails,
action: "SEND_TOKENS"
});
}
return true;
} catch (error) {
console.error("Error during token transfer:", error);
if (callback) {
callback({
text: `Error transferring tokens: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
return false;
}
},
},
template: transferTemplate,
validate: async (runtime: IAgentRuntime) => {
const privateKey = runtime.getSetting("EVM_PRIVATE_KEY");
return typeof privateKey === "string" && privateKey.startsWith("0x");
},
validate: async (runtime: IAgentRuntime) => true,
examples: [
[
{
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-evm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { bridgeAction } from "./actions/bridge";
import { swapAction } from "./actions/swap";
import { transferAction } from "./actions/transfer";
import { evmWalletProvider } from "./providers/wallet";
import { getBalanceAction, getBlockAction } from "./actions/common";

export const evmPlugin: Plugin = {
name: "evm",
description: "EVM blockchain integration plugin",
providers: [evmWalletProvider],
evaluators: [],
services: [],
actions: [transferAction, bridgeAction, swapAction],
actions: [transferAction, bridgeAction, swapAction, getBalanceAction, getBlockAction],
};

export default evmPlugin;
26 changes: 18 additions & 8 deletions packages/plugin-evm/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
export const transferTemplate = `Given the recent messages and wallet information below:
export const transferTemplate = `Given the recent messages below:

{{recentMessages}}

{{walletInfo}}

Extract the following information about the requested transfer:
- Chain to execute on: Must be one of ["ethereum", "base", ...] (like in viem/chains)
- Amount to transfer: Must be a string representing the amount in ETH (only number without coin symbol, e.g., "0.1")
- Recipient address: Must be a valid Ethereum address starting with "0x"
- Token symbol or address (if not native token): Optional, leave as null for ETH transfers

Respond with a JSON markdown block containing only the extracted values. All fields except 'token' are required:
Respond with a JSON markdown block containing only the extracted values. All fields are required:

\`\`\`json
{
"fromChain": SUPPORTED_CHAINS,
"amount": string,
"toAddress": string,
"token": string | null
}
\`\`\`
`;
Expand Down Expand Up @@ -72,3 +66,19 @@ Respond with a JSON markdown block containing only the extracted values. Use nul
}
\`\`\`
`;

export const getAddressTemplate = `Given the recent messages below:

{{recentMessages}}

Extract the following information about the requested address:
- Wallet address

Respond with a JSON markdown block containing only the extracted values:

\`\`\`json
{
"address": string
}
\`\`\`
`;
Loading