From 451be6b1c1506bc376b99e0b75e85d9ded1753cd Mon Sep 17 00:00:00 2001 From: rahat chowdhury Date: Tue, 31 Dec 2024 15:57:38 -0500 Subject: [PATCH 1/5] add support for movement network --- packages/plugin-aptos/src/constants.ts | 10 +++++++++- packages/plugin-aptos/src/enviroment.ts | 2 +- packages/plugin-aptos/src/providers/wallet.ts | 10 ++++++++-- packages/plugin-aptos/src/utils.ts | 11 +++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/plugin-aptos/src/utils.ts diff --git a/packages/plugin-aptos/src/constants.ts b/packages/plugin-aptos/src/constants.ts index 92df2614da..93a32ffcc7 100644 --- a/packages/plugin-aptos/src/constants.ts +++ b/packages/plugin-aptos/src/constants.ts @@ -1 +1,9 @@ -export const APT_DECIMALS = 8; \ No newline at end of file +export const APT_DECIMALS = 8; +export const MOVEMENT_NETWORK = { + MAINNET: { + fullnode: 'https://mainnet.movementnetwork.xyz/v1', + }, + TESTNET: { + fullnode: 'https://aptos.testnet.bardock.movementlabs.xyz/v1', + }, +}; \ No newline at end of file diff --git a/packages/plugin-aptos/src/enviroment.ts b/packages/plugin-aptos/src/enviroment.ts index 59719eaab0..2d7b2e3c14 100644 --- a/packages/plugin-aptos/src/enviroment.ts +++ b/packages/plugin-aptos/src/enviroment.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const aptosEnvSchema = z.object({ APTOS_PRIVATE_KEY: z.string().min(1, "Aptos private key is required"), - APTOS_NETWORK: z.enum(["mainnet", "testnet"]), + APTOS_NETWORK: z.enum(["mainnet", "testnet", "movement_mainnet", "movement_testnet"]), }); export type AptosConfig = z.infer; diff --git a/packages/plugin-aptos/src/providers/wallet.ts b/packages/plugin-aptos/src/providers/wallet.ts index fbb209c3ac..ff948837bb 100644 --- a/packages/plugin-aptos/src/providers/wallet.ts +++ b/packages/plugin-aptos/src/providers/wallet.ts @@ -17,7 +17,8 @@ import { import BigNumber from "bignumber.js"; import NodeCache from "node-cache"; import * as path from "path"; -import { APT_DECIMALS } from "../constants"; +import { APT_DECIMALS, MOVEMENT_NETWORK } from "../constants"; +import { isMovementNetwork, getMovementNetworkType } from "../utils"; // Provider configuration const PROVIDER_CONFIG = { @@ -237,7 +238,12 @@ const walletProvider: Provider = { try { const aptosClient = new Aptos( new AptosConfig({ - network, + network: isMovementNetwork(network) + ? { + network: Network.CUSTOM, + fullnode: MOVEMENT_NETWORK[getMovementNetworkType(network)].fullnode + } + : { network } }) ); const provider = new WalletProvider( diff --git a/packages/plugin-aptos/src/utils.ts b/packages/plugin-aptos/src/utils.ts new file mode 100644 index 0000000000..1ec60a0d8f --- /dev/null +++ b/packages/plugin-aptos/src/utils.ts @@ -0,0 +1,11 @@ +export function isMovementNetwork(network: string): boolean { + return network.startsWith('movement_'); +} + +export function getMovementNetworkType(network: string): 'MAINNET' | 'TESTNET' { + return network === 'movement_mainnet' ? 'MAINNET' : 'TESTNET'; +} + +export function getTokenSymbol(network: string): string { + return network.startsWith('movement_') ? 'MOVE' : 'APT'; +} \ No newline at end of file From cda69e2f32a4762205d38296efc5c500d43d968e Mon Sep 17 00:00:00 2001 From: rahat chowdhury Date: Wed, 1 Jan 2025 17:34:44 -0500 Subject: [PATCH 2/5] seperate movement functionality into its own plugin --- packages/plugin-aptos/package.json | 13 +- packages/plugin-aptos/src/constants.ts | 10 +- packages/plugin-aptos/src/enviroment.ts | 2 +- packages/plugin-aptos/src/providers/wallet.ts | 10 +- packages/plugin-aptos/tsup.config.ts | 32 ++- packages/plugin-movement/.npmignore | 6 + packages/plugin-movement/eslint.config.mjs | 3 + packages/plugin-movement/package.json | 30 ++ .../plugin-movement/src/actions/transfer.ts | 224 +++++++++++++++ packages/plugin-movement/src/constants.ts | 16 ++ packages/plugin-movement/src/enviroment.ts | 36 +++ packages/plugin-movement/src/environment.ts | 39 +++ packages/plugin-movement/src/index.ts | 15 + .../plugin-movement/src/providers/wallet.ts | 266 ++++++++++++++++++ .../plugin-movement/src/tests/wallet.test.ts | 104 +++++++ packages/plugin-movement/src/utils.ts | 11 + packages/plugin-movement/tsconfig.json | 10 + packages/plugin-movement/tsup.config.ts | 31 ++ 18 files changed, 820 insertions(+), 38 deletions(-) create mode 100644 packages/plugin-movement/.npmignore create mode 100644 packages/plugin-movement/eslint.config.mjs create mode 100644 packages/plugin-movement/package.json create mode 100644 packages/plugin-movement/src/actions/transfer.ts create mode 100644 packages/plugin-movement/src/constants.ts create mode 100644 packages/plugin-movement/src/enviroment.ts create mode 100644 packages/plugin-movement/src/environment.ts create mode 100644 packages/plugin-movement/src/index.ts create mode 100644 packages/plugin-movement/src/providers/wallet.ts create mode 100644 packages/plugin-movement/src/tests/wallet.test.ts create mode 100644 packages/plugin-movement/src/utils.ts create mode 100644 packages/plugin-movement/tsconfig.json create mode 100644 packages/plugin-movement/tsup.config.ts diff --git a/packages/plugin-aptos/package.json b/packages/plugin-aptos/package.json index bc1badd89b..55a11ffc80 100644 --- a/packages/plugin-aptos/package.json +++ b/packages/plugin-aptos/package.json @@ -9,14 +9,17 @@ "@aptos-labs/ts-sdk": "^1.26.0", "bignumber": "1.1.0", "bignumber.js": "9.1.2", - "node-cache": "5.1.2", + "node-cache": "5.1.2" + }, + "devDependencies": { "tsup": "8.3.5", - "vitest": "2.1.4" + "vitest": "2.1.4", + "typescript": "^5.0.0" }, "scripts": { - "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch", - "lint": "eslint --fix --cache .", + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint --fix --cache .", "test": "vitest run" }, "peerDependencies": { diff --git a/packages/plugin-aptos/src/constants.ts b/packages/plugin-aptos/src/constants.ts index 93a32ffcc7..92df2614da 100644 --- a/packages/plugin-aptos/src/constants.ts +++ b/packages/plugin-aptos/src/constants.ts @@ -1,9 +1 @@ -export const APT_DECIMALS = 8; -export const MOVEMENT_NETWORK = { - MAINNET: { - fullnode: 'https://mainnet.movementnetwork.xyz/v1', - }, - TESTNET: { - fullnode: 'https://aptos.testnet.bardock.movementlabs.xyz/v1', - }, -}; \ No newline at end of file +export const APT_DECIMALS = 8; \ No newline at end of file diff --git a/packages/plugin-aptos/src/enviroment.ts b/packages/plugin-aptos/src/enviroment.ts index 2d7b2e3c14..59719eaab0 100644 --- a/packages/plugin-aptos/src/enviroment.ts +++ b/packages/plugin-aptos/src/enviroment.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const aptosEnvSchema = z.object({ APTOS_PRIVATE_KEY: z.string().min(1, "Aptos private key is required"), - APTOS_NETWORK: z.enum(["mainnet", "testnet", "movement_mainnet", "movement_testnet"]), + APTOS_NETWORK: z.enum(["mainnet", "testnet"]), }); export type AptosConfig = z.infer; diff --git a/packages/plugin-aptos/src/providers/wallet.ts b/packages/plugin-aptos/src/providers/wallet.ts index ff948837bb..fbb209c3ac 100644 --- a/packages/plugin-aptos/src/providers/wallet.ts +++ b/packages/plugin-aptos/src/providers/wallet.ts @@ -17,8 +17,7 @@ import { import BigNumber from "bignumber.js"; import NodeCache from "node-cache"; import * as path from "path"; -import { APT_DECIMALS, MOVEMENT_NETWORK } from "../constants"; -import { isMovementNetwork, getMovementNetworkType } from "../utils"; +import { APT_DECIMALS } from "../constants"; // Provider configuration const PROVIDER_CONFIG = { @@ -238,12 +237,7 @@ const walletProvider: Provider = { try { const aptosClient = new Aptos( new AptosConfig({ - network: isMovementNetwork(network) - ? { - network: Network.CUSTOM, - fullnode: MOVEMENT_NETWORK[getMovementNetworkType(network)].fullnode - } - : { network } + network, }) ); const provider = new WalletProvider( diff --git a/packages/plugin-aptos/tsup.config.ts b/packages/plugin-aptos/tsup.config.ts index dd25475bb6..682efa51ea 100644 --- a/packages/plugin-aptos/tsup.config.ts +++ b/packages/plugin-aptos/tsup.config.ts @@ -5,25 +5,27 @@ export default defineConfig({ outDir: "dist", sourcemap: true, clean: true, - format: ["esm"], // Ensure you're targeting CommonJS + format: ["esm"], + dts: true, + minify: false, + splitting: false, 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", + "@elizaos/core", + "@aptos-labs/ts-sdk", + "bignumber", + "bignumber.js", + "node-cache", + "dotenv", + "fs", + "path", "https", "http", - "agentkeepalive", - "safe-buffer", - "base-x", - "bs58", - "borsh", - "@solana/buffer-layout", "stream", "buffer", - "querystring", - "amqplib", - // Add other modules you want to externalize + "querystring" ], + noExternal: [], + esbuildOptions(options) { + options.platform = 'node' + } }); diff --git a/packages/plugin-movement/.npmignore b/packages/plugin-movement/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/plugin-movement/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-movement/eslint.config.mjs b/packages/plugin-movement/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-movement/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-movement/package.json b/packages/plugin-movement/package.json new file mode 100644 index 0000000000..2396c2c80b --- /dev/null +++ b/packages/plugin-movement/package.json @@ -0,0 +1,30 @@ +{ + "name": "@elizaos/plugin-movement", + "version": "0.1.0", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "description": "Movement Network Plugin for Eliza", + "dependencies": { + "@elizaos/core": "workspace:*", + "@aptos-labs/ts-sdk": "^1.26.0", + "bignumber": "1.1.0", + "bignumber.js": "9.1.2", + "node-cache": "5.1.2" + }, + "devDependencies": { + "tsup": "8.3.5", + "vitest": "2.1.4", + "typescript": "^5.0.0" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-movement/src/actions/transfer.ts b/packages/plugin-movement/src/actions/transfer.ts new file mode 100644 index 0000000000..626a751a8e --- /dev/null +++ b/packages/plugin-movement/src/actions/transfer.ts @@ -0,0 +1,224 @@ +import { elizaLogger } from "@elizaos/core"; +import { + ActionExample, + Content, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; +import { composeContext } from "@elizaos/core"; +import { generateObjectDeprecated } from "@elizaos/core"; +import { + Account, + Aptos, + AptosConfig, + Ed25519PrivateKey, + Network, + PrivateKey, + PrivateKeyVariants, +} from "@aptos-labs/ts-sdk"; +import { walletProvider } from "../providers/wallet"; + +export interface TransferContent extends Content { + recipient: string; + amount: string | number; +} + +function isTransferContent(content: any): content is TransferContent { + console.log("Content for transfer", content); + return ( + typeof content.recipient === "string" && + (typeof content.amount === "string" || + typeof content.amount === "number") + ); +} + +const transferTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "recipient": "0x2badda48c062e861ef17a96a806c451fd296a49f45b272dee17f85b0e32663fd", + "amount": "1000" +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested token transfer: +- 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", + "TRANSFER_TOKENS", + "SEND_TOKENS", + "SEND_APT", + "PAY", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + console.log("Validating apt transfer from user:", message.userId); + //add custom validate logic here + /* + const adminIds = runtime.getSetting("ADMIN_USER_IDS")?.split(",") || []; + //console.log("Admin IDs from settings:", adminIds); + + const isAdmin = adminIds.includes(message.userId); + + if (isAdmin) { + //console.log(`Authorized transfer from user: ${message.userId}`); + return true; + } + else + { + //console.log(`Unauthorized transfer attempt from user: ${message.userId}`); + return false; + } + */ + return false; + }, + 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..."); + + const walletInfo = await walletProvider.get(runtime, message, state); + state.walletInfo = walletInfo; + + // 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 generateObjectDeprecated({ + 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 privateKey = runtime.getSetting("APTOS_PRIVATE_KEY"); + const aptosAccount = Account.fromPrivateKey({ + privateKey: new Ed25519PrivateKey( + PrivateKey.formatPrivateKey( + privateKey, + PrivateKeyVariants.Ed25519 + ) + ), + }); + const network = runtime.getSetting("APTOS_NETWORK") as Network; + const aptosClient = new Aptos( + new AptosConfig({ + network, + }) + ); + + const APT_DECIMALS = 8; + const adjustedAmount = BigInt( + Number(content.amount) * Math.pow(10, APT_DECIMALS) + ); + console.log( + `Transferring: ${content.amount} tokens (${adjustedAmount} base units)` + ); + + const tx = await aptosClient.transaction.build.simple({ + sender: aptosAccount.accountAddress.toStringLong(), + data: { + function: "0x1::aptos_account::transfer", + typeArguments: [], + functionArguments: [content.recipient, adjustedAmount], + }, + }); + const committedTransaction = + await aptosClient.signAndSubmitTransaction({ + signer: aptosAccount, + transaction: tx, + }); + const executedTransaction = await aptosClient.waitForTransaction({ + transactionHash: committedTransaction.hash, + }); + + console.log("Transfer successful:", executedTransaction.hash); + + if (callback) { + callback({ + text: `Successfully transferred ${content.amount} APT to ${content.recipient}, Transaction: ${executedTransaction.hash}`, + content: { + success: true, + hash: executedTransaction.hash, + amount: content.amount, + recipient: content.recipient, + }, + }); + } + + 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; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Send 69 APT tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0", + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll send 69 APT tokens now...", + action: "SEND_TOKEN", + }, + }, + { + user: "{{user2}}", + content: { + text: "Successfully sent 69 APT tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0, Transaction: 0x39a8c432d9bdad993a33cc1faf2e9b58fb7dd940c0425f1d6db3997e4b4b05c0", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-movement/src/constants.ts b/packages/plugin-movement/src/constants.ts new file mode 100644 index 0000000000..eec5ddec5f --- /dev/null +++ b/packages/plugin-movement/src/constants.ts @@ -0,0 +1,16 @@ +export const MOV_DECIMALS = 8; + +export const MOVEMENT_NETWORKS = { + mainnet: { + fullnode: 'https://fullnode.mainnet.mov.network/v1', + chainId: '1', + name: 'Movement Mainnet' + }, + bardock: { + fullnode: 'https://fullnode.testnet.mov.network/v1', + chainId: '2', + name: 'Movement Bardock Testnet' + } +} as const; + +export const DEFAULT_NETWORK = 'bardock'; \ No newline at end of file diff --git a/packages/plugin-movement/src/enviroment.ts b/packages/plugin-movement/src/enviroment.ts new file mode 100644 index 0000000000..2d7b2e3c14 --- /dev/null +++ b/packages/plugin-movement/src/enviroment.ts @@ -0,0 +1,36 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const aptosEnvSchema = z.object({ + APTOS_PRIVATE_KEY: z.string().min(1, "Aptos private key is required"), + APTOS_NETWORK: z.enum(["mainnet", "testnet", "movement_mainnet", "movement_testnet"]), +}); + +export type AptosConfig = z.infer; + +export async function validateAptosConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + APTOS_PRIVATE_KEY: + runtime.getSetting("APTOS_PRIVATE_KEY") || + process.env.APTOS_PRIVATE_KEY, + APTOS_NETWORK: + runtime.getSetting("APTOS_NETWORK") || + process.env.APTOS_NETWORK, + }; + + return aptosEnvSchema.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( + `Aptos configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-movement/src/environment.ts b/packages/plugin-movement/src/environment.ts new file mode 100644 index 0000000000..a990396252 --- /dev/null +++ b/packages/plugin-movement/src/environment.ts @@ -0,0 +1,39 @@ +import { IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const movementEnvSchema = z.object({ + MOVEMENT_PRIVATE_KEY: z.string().min(1, "Movement private key is required"), + MOVEMENT_NETWORK: z.enum(["mainnet", "bardock"]).default("bardock"), +}); + +export type MovementConfig = z.infer; + +export async function validateMovementConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + MOVEMENT_PRIVATE_KEY: + runtime.getSetting("MOVEMENT_PRIVATE_KEY") || + runtime.getSetting("APTOS_PRIVATE_KEY") || // Fallback for compatibility + process.env.MOVEMENT_PRIVATE_KEY, + MOVEMENT_NETWORK: + runtime.getSetting("MOVEMENT_NETWORK") || + runtime.getSetting("APTOS_NETWORK")?.replace("movement_", "") || // Handle movement_bardock -> bardock + process.env.MOVEMENT_NETWORK || + "bardock", + }; + + return movementEnvSchema.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( + `Movement configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} \ No newline at end of file diff --git a/packages/plugin-movement/src/index.ts b/packages/plugin-movement/src/index.ts new file mode 100644 index 0000000000..d8d54ba243 --- /dev/null +++ b/packages/plugin-movement/src/index.ts @@ -0,0 +1,15 @@ +import { Plugin } from "@elizaos/core"; +import transferToken from "./actions/transfer"; +import { WalletProvider, walletProvider } from "./providers/wallet"; + +export { WalletProvider, transferToken as TransferMovementToken }; + +export const movementPlugin: Plugin = { + name: "movement", + description: "Movement Network Plugin for Eliza", + actions: [transferToken], + evaluators: [], + providers: [walletProvider], +}; + +export default movementPlugin; diff --git a/packages/plugin-movement/src/providers/wallet.ts b/packages/plugin-movement/src/providers/wallet.ts new file mode 100644 index 0000000000..3e4f8f6a16 --- /dev/null +++ b/packages/plugin-movement/src/providers/wallet.ts @@ -0,0 +1,266 @@ +import { + IAgentRuntime, + ICacheManager, + Memory, + Provider, + State, +} from "@elizaos/core"; +import { + Account, + Aptos, + AptosConfig, + Ed25519PrivateKey, + Network, + PrivateKey, + PrivateKeyVariants, +} from "@aptos-labs/ts-sdk"; +import BigNumber from "bignumber.js"; +import NodeCache from "node-cache"; +import * as path from "path"; +import { APT_DECIMALS, MOVEMENT_NETWORK } from "../constants"; +import { isMovementNetwork, getMovementNetworkType } from "../utils"; + +// Provider configuration +const PROVIDER_CONFIG = { + MAX_RETRIES: 3, + RETRY_DELAY: 2000, +}; + +interface WalletPortfolio { + totalUsd: string; + totalApt: string; +} + +interface Prices { + apt: { usd: string }; +} + +export class WalletProvider { + private cache: NodeCache; + private cacheKey: string = "aptos/wallet"; + + constructor( + private aptosClient: Aptos, + private address: string, + private cacheManager: ICacheManager + ) { + this.cache = new NodeCache({ stdTTL: 300 }); // Cache TTL set to 5 minutes + } + + private async readFromCache(key: string): Promise { + const cached = await this.cacheManager.get( + path.join(this.cacheKey, key) + ); + return cached; + } + + private async writeToCache(key: string, data: T): Promise { + await this.cacheManager.set(path.join(this.cacheKey, key), data, { + expires: Date.now() + 5 * 60 * 1000, + }); + } + + private async getCachedData(key: string): Promise { + // Check in-memory cache first + const cachedData = this.cache.get(key); + if (cachedData) { + return cachedData; + } + + // Check file-based cache + const fileCachedData = await this.readFromCache(key); + if (fileCachedData) { + // Populate in-memory cache + this.cache.set(key, fileCachedData); + return fileCachedData; + } + + return null; + } + + private async setCachedData(cacheKey: string, data: T): Promise { + // Set in-memory cache + this.cache.set(cacheKey, data); + + // Write to file-based cache + await this.writeToCache(cacheKey, data); + } + + private async fetchPricesWithRetry() { + let lastError: Error; + + for (let i = 0; i < PROVIDER_CONFIG.MAX_RETRIES; i++) { + try { + const cellanaAptUsdcPoolAddr = + "0x234f0be57d6acfb2f0f19c17053617311a8d03c9ce358bdf9cd5c460e4a02b7c"; + const response = await fetch( + `https://api.dexscreener.com/latest/dex/pairs/aptos/${cellanaAptUsdcPoolAddr}` + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `HTTP error! status: ${response.status}, message: ${errorText}` + ); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error(`Attempt ${i + 1} failed:`, error); + lastError = error; + if (i < PROVIDER_CONFIG.MAX_RETRIES - 1) { + const delay = PROVIDER_CONFIG.RETRY_DELAY * Math.pow(2, i); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + } + } + + console.error( + "All attempts failed. Throwing the last error:", + lastError + ); + throw lastError; + } + + async fetchPortfolioValue(): Promise { + try { + const cacheKey = `portfolio-${this.address}`; + const cachedValue = + await this.getCachedData(cacheKey); + + if (cachedValue) { + console.log("Cache hit for fetchPortfolioValue", cachedValue); + return cachedValue; + } + console.log("Cache miss for fetchPortfolioValue"); + + const prices = await this.fetchPrices().catch((error) => { + console.error("Error fetching APT price:", error); + throw error; + }); + const aptAmountOnChain = await this.aptosClient + .getAccountAPTAmount({ + accountAddress: this.address, + }) + .catch((error) => { + console.error("Error fetching APT amount:", error); + throw error; + }); + + const aptAmount = new BigNumber(aptAmountOnChain).div( + new BigNumber(10).pow(APT_DECIMALS) + ); + const totalUsd = new BigNumber(aptAmount).times(prices.apt.usd); + + const portfolio = { + totalUsd: totalUsd.toString(), + totalApt: aptAmount.toString(), + }; + this.setCachedData(cacheKey, portfolio); + console.log("Fetched portfolio:", portfolio); + return portfolio; + } catch (error) { + console.error("Error fetching portfolio:", error); + throw error; + } + } + + async fetchPrices(): Promise { + try { + const cacheKey = "prices"; + const cachedValue = await this.getCachedData(cacheKey); + + if (cachedValue) { + console.log("Cache hit for fetchPrices"); + return cachedValue; + } + console.log("Cache miss for fetchPrices"); + + const aptPriceData = await this.fetchPricesWithRetry().catch( + (error) => { + console.error("Error fetching APT price:", error); + throw error; + } + ); + const prices: Prices = { + apt: { usd: aptPriceData.pair.priceUsd }, + }; + this.setCachedData(cacheKey, prices); + return prices; + } catch (error) { + console.error("Error fetching prices:", error); + throw error; + } + } + + formatPortfolio(runtime, portfolio: WalletPortfolio): string { + let output = `${runtime.character.name}\n`; + output += `Wallet Address: ${this.address}\n`; + + const totalUsdFormatted = new BigNumber(portfolio.totalUsd).toFixed(2); + const totalAptFormatted = new BigNumber(portfolio.totalApt).toFixed(4); + + output += `Total Value: $${totalUsdFormatted} (${totalAptFormatted} APT)\n`; + + return output; + } + + async getFormattedPortfolio(runtime): Promise { + try { + const portfolio = await this.fetchPortfolioValue(); + return this.formatPortfolio(runtime, portfolio); + } catch (error) { + console.error("Error generating portfolio report:", error); + return "Unable to fetch wallet information. Please try again later."; + } + } +} + +const walletProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise => { + const privateKey = runtime.getSetting("APTOS_PRIVATE_KEY"); + const aptosAccount = Account.fromPrivateKey({ + privateKey: new Ed25519PrivateKey( + PrivateKey.formatPrivateKey( + privateKey, + PrivateKeyVariants.Ed25519 + ) + ), + }); + const network = runtime.getSetting("APTOS_NETWORK") as Network; + + try { + console.log("Network:", network); + const aptosClient = new Aptos( + new AptosConfig( + isMovementNetwork(network) + ? { + network: Network.CUSTOM, + fullnode: MOVEMENT_NETWORK[getMovementNetworkType(network)].fullnode + } + : { + network + } + ) + ); + const provider = new WalletProvider( + aptosClient, + aptosAccount.accountAddress.toStringLong(), + runtime.cacheManager + ); + return await provider.getFormattedPortfolio(runtime); + } catch (error) { + console.error("Error in wallet provider:", error); + return null; + } + }, +}; + +// Module exports +export { walletProvider }; diff --git a/packages/plugin-movement/src/tests/wallet.test.ts b/packages/plugin-movement/src/tests/wallet.test.ts new file mode 100644 index 0000000000..f7d2829413 --- /dev/null +++ b/packages/plugin-movement/src/tests/wallet.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import { WalletProvider } from "../providers/wallet.ts"; +import { + Account, + Aptos, + AptosConfig, + Ed25519PrivateKey, + Network, + PrivateKey, + PrivateKeyVariants, +} from "@aptos-labs/ts-sdk"; +import { defaultCharacter } from "@elizaos/core"; +import BigNumber from "bignumber.js"; +import { APT_DECIMALS } from "../constants.ts"; + +// Mock NodeCache +vi.mock("node-cache", () => { + return { + default: vi.fn().mockImplementation(() => ({ + set: vi.fn(), + get: vi.fn().mockReturnValue(null), + })), + }; +}); + +// Mock path module +vi.mock("path", async () => { + const actual = await vi.importActual("path"); + return { + ...actual, + join: vi.fn().mockImplementation((...args) => args.join("/")), + }; +}); + +// Mock the ICacheManager +const mockCacheManager = { + get: vi.fn().mockResolvedValue(null), + set: vi.fn(), + delete: vi.fn(), +}; + +describe("WalletProvider", () => { + let walletProvider; + let mockedRuntime; + + beforeEach(() => { + vi.clearAllMocks(); + mockCacheManager.get.mockResolvedValue(null); + + const aptosClient = new Aptos( + new AptosConfig({ + network: Network.TESTNET, + }) + ); + const aptosAccount = Account.fromPrivateKey({ + privateKey: new Ed25519PrivateKey( + PrivateKey.formatPrivateKey( + // this is a testnet private key + "0x90e02bf2439492bd9be1ec5f569704accefd65ba88a89c4dcef1977e0203211e", + PrivateKeyVariants.Ed25519 + ) + ), + }); + + // Create new instance of TokenProvider with mocked dependencies + walletProvider = new WalletProvider( + aptosClient, + aptosAccount.accountAddress.toStringLong(), + mockCacheManager + ); + + mockedRuntime = { + character: defaultCharacter, + }; + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + describe("Wallet Integration", () => { + it("should check wallet address", async () => { + const result = + await walletProvider.getFormattedPortfolio(mockedRuntime); + + const prices = await walletProvider.fetchPrices(); + const aptAmountOnChain = + await walletProvider.aptosClient.getAccountAPTAmount({ + accountAddress: walletProvider.address, + }); + const aptAmount = new BigNumber(aptAmountOnChain) + .div(new BigNumber(10).pow(APT_DECIMALS)) + .toFixed(4); + const totalUsd = new BigNumber(aptAmount) + .times(prices.apt.usd) + .toFixed(2); + + expect(result).toEqual( + `Eliza\nWallet Address: ${walletProvider.address}\n` + + `Total Value: $${totalUsd} (${aptAmount} APT)\n` + ); + }); + }); +}); diff --git a/packages/plugin-movement/src/utils.ts b/packages/plugin-movement/src/utils.ts new file mode 100644 index 0000000000..1ec60a0d8f --- /dev/null +++ b/packages/plugin-movement/src/utils.ts @@ -0,0 +1,11 @@ +export function isMovementNetwork(network: string): boolean { + return network.startsWith('movement_'); +} + +export function getMovementNetworkType(network: string): 'MAINNET' | 'TESTNET' { + return network === 'movement_mainnet' ? 'MAINNET' : 'TESTNET'; +} + +export function getTokenSymbol(network: string): string { + return network.startsWith('movement_') ? 'MOVE' : 'APT'; +} \ No newline at end of file diff --git a/packages/plugin-movement/tsconfig.json b/packages/plugin-movement/tsconfig.json new file mode 100644 index 0000000000..73993deaaf --- /dev/null +++ b/packages/plugin-movement/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-movement/tsup.config.ts b/packages/plugin-movement/tsup.config.ts new file mode 100644 index 0000000000..682efa51ea --- /dev/null +++ b/packages/plugin-movement/tsup.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + dts: true, + minify: false, + splitting: false, + external: [ + "@elizaos/core", + "@aptos-labs/ts-sdk", + "bignumber", + "bignumber.js", + "node-cache", + "dotenv", + "fs", + "path", + "https", + "http", + "stream", + "buffer", + "querystring" + ], + noExternal: [], + esbuildOptions(options) { + options.platform = 'node' + } +}); From a2310186d67fa1ce0767fdf903143c270ae4cdaf Mon Sep 17 00:00:00 2001 From: rahat chowdhury Date: Wed, 1 Jan 2025 18:13:55 -0500 Subject: [PATCH 3/5] remove unnecasary changes for aptos --- packages/plugin-aptos/package.json | 51 +++++++++++++--------------- packages/plugin-aptos/src/utils.ts | 11 ------ packages/plugin-aptos/tsup.config.ts | 32 ++++++++--------- 3 files changed, 39 insertions(+), 55 deletions(-) delete mode 100644 packages/plugin-aptos/src/utils.ts diff --git a/packages/plugin-aptos/package.json b/packages/plugin-aptos/package.json index 55a11ffc80..e38009d4e4 100644 --- a/packages/plugin-aptos/package.json +++ b/packages/plugin-aptos/package.json @@ -1,29 +1,26 @@ { - "name": "@elizaos/plugin-aptos", - "version": "0.1.7-alpha.2", - "main": "dist/index.js", - "type": "module", - "types": "dist/index.d.ts", - "dependencies": { - "@elizaos/core": "workspace:*", - "@aptos-labs/ts-sdk": "^1.26.0", - "bignumber": "1.1.0", - "bignumber.js": "9.1.2", - "node-cache": "5.1.2" - }, - "devDependencies": { - "tsup": "8.3.5", - "vitest": "2.1.4", - "typescript": "^5.0.0" - }, - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "lint": "eslint --fix --cache .", - "test": "vitest run" - }, - "peerDependencies": { - "form-data": "4.0.1", - "whatwg-url": "7.1.0" + "name": "@elizaos/plugin-aptos", + "version": "0.1.7-alpha.2", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "@aptos-labs/ts-sdk": "^1.26.0", + "bignumber": "1.1.0", + "bignumber.js": "9.1.2", + "node-cache": "5.1.2", + "tsup": "8.3.5", + "vitest": "2.1.4" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } } -} diff --git a/packages/plugin-aptos/src/utils.ts b/packages/plugin-aptos/src/utils.ts deleted file mode 100644 index 1ec60a0d8f..0000000000 --- a/packages/plugin-aptos/src/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function isMovementNetwork(network: string): boolean { - return network.startsWith('movement_'); -} - -export function getMovementNetworkType(network: string): 'MAINNET' | 'TESTNET' { - return network === 'movement_mainnet' ? 'MAINNET' : 'TESTNET'; -} - -export function getTokenSymbol(network: string): string { - return network.startsWith('movement_') ? 'MOVE' : 'APT'; -} \ No newline at end of file diff --git a/packages/plugin-aptos/tsup.config.ts b/packages/plugin-aptos/tsup.config.ts index 682efa51ea..dd25475bb6 100644 --- a/packages/plugin-aptos/tsup.config.ts +++ b/packages/plugin-aptos/tsup.config.ts @@ -5,27 +5,25 @@ export default defineConfig({ outDir: "dist", sourcemap: true, clean: true, - format: ["esm"], - dts: true, - minify: false, - splitting: false, + format: ["esm"], // Ensure you're targeting CommonJS external: [ - "@elizaos/core", - "@aptos-labs/ts-sdk", - "bignumber", - "bignumber.js", - "node-cache", - "dotenv", - "fs", - "path", + "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", + "safe-buffer", + "base-x", + "bs58", + "borsh", + "@solana/buffer-layout", "stream", "buffer", - "querystring" + "querystring", + "amqplib", + // Add other modules you want to externalize ], - noExternal: [], - esbuildOptions(options) { - options.platform = 'node' - } }); From 7ba8c869cadb9ec6f10af3f29484d138c85c4832 Mon Sep 17 00:00:00 2001 From: rahat chowdhury Date: Thu, 2 Jan 2025 00:43:28 -0500 Subject: [PATCH 4/5] add support for movement network --- agent/package.json | 1 + packages/plugin-aptos/package.json | 2 +- .../plugin-movement/src/actions/transfer.ts | 195 +++++++++++------- packages/plugin-movement/src/constants.ts | 21 +- packages/plugin-movement/src/enviroment.ts | 36 ---- packages/plugin-movement/src/environment.ts | 2 - .../plugin-movement/src/providers/wallet.ts | 62 +++--- .../src/tests/transfer.test.ts | 20 ++ .../plugin-movement/src/tests/wallet.test.ts | 90 +++++--- packages/plugin-movement/src/utils.ts | 11 - 10 files changed, 242 insertions(+), 198 deletions(-) delete mode 100644 packages/plugin-movement/src/enviroment.ts create mode 100644 packages/plugin-movement/src/tests/transfer.test.ts delete mode 100644 packages/plugin-movement/src/utils.ts diff --git a/agent/package.json b/agent/package.json index a0a5192ec5..6764b92d55 100644 --- a/agent/package.json +++ b/agent/package.json @@ -44,6 +44,7 @@ "@elizaos/plugin-goat": "workspace:*", "@elizaos/plugin-icp": "workspace:*", "@elizaos/plugin-image-generation": "workspace:*", + "@elizaos/plugin-movement": "workspace:*", "@elizaos/plugin-nft-generation": "workspace:*", "@elizaos/plugin-node": "workspace:*", "@elizaos/plugin-solana": "workspace:*", diff --git a/packages/plugin-aptos/package.json b/packages/plugin-aptos/package.json index e38009d4e4..48edb40c79 100644 --- a/packages/plugin-aptos/package.json +++ b/packages/plugin-aptos/package.json @@ -23,4 +23,4 @@ "form-data": "4.0.1", "whatwg-url": "7.1.0" } - } + } \ No newline at end of file diff --git a/packages/plugin-movement/src/actions/transfer.ts b/packages/plugin-movement/src/actions/transfer.ts index 626a751a8e..43490fbcb6 100644 --- a/packages/plugin-movement/src/actions/transfer.ts +++ b/packages/plugin-movement/src/actions/transfer.ts @@ -21,6 +21,7 @@ import { PrivateKeyVariants, } from "@aptos-labs/ts-sdk"; import { walletProvider } from "../providers/wallet"; +import { MOVEMENT_NETWORK_CONFIG, MOVE_DECIMALS, MOVEMENT_EXPLORER_URL } from "../constants"; export interface TransferContent extends Content { recipient: string; @@ -28,7 +29,7 @@ export interface TransferContent extends Content { } function isTransferContent(content: any): content is TransferContent { - console.log("Content for transfer", content); + elizaLogger.debug("Validating transfer content:", content); return ( typeof content.recipient === "string" && (typeof content.amount === "string" || @@ -36,55 +37,62 @@ function isTransferContent(content: any): content is TransferContent { ); } -const transferTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. +const transferTemplate = `You are processing a token transfer request. Extract the recipient address and amount from the message. +Example request: "can you send 1 move to 0x123..." Example response: \`\`\`json { - "recipient": "0x2badda48c062e861ef17a96a806c451fd296a49f45b272dee17f85b0e32663fd", - "amount": "1000" + "recipient": "0x123...", + "amount": "1" } \`\`\` +Rules: +1. The recipient address always starts with "0x" +2. The amount is typically a number less than 100 +3. Return exact values found in the message + +Recent messages: {{recentMessages}} -Given the recent messages, extract the following information about the requested token transfer: -- Recipient wallet address -- Amount to transfer +Extract and return ONLY the following in a JSON block: +- recipient: The wallet address starting with 0x +- amount: The number of tokens to send -Respond with a JSON markdown block containing only the extracted values.`; +Return ONLY the JSON block with these two fields.`; export default { - name: "SEND_TOKEN", + name: "TRANSFER_MOVE", similes: [ + "SEND_TOKEN", "TRANSFER_TOKEN", "TRANSFER_TOKENS", "SEND_TOKENS", - "SEND_APT", + "SEND_MOVE", "PAY", ], + triggers: [ + "send move", + "send 1 move", + "transfer move", + "send token", + "transfer token", + "can you send", + "please send", + "send" + ], + shouldHandle: (message: Memory) => { + const text = message.content?.text?.toLowerCase() || ""; + return text.includes("send") && text.includes("move") && text.includes("0x"); + }, validate: async (runtime: IAgentRuntime, message: Memory) => { - console.log("Validating apt transfer from user:", message.userId); - //add custom validate logic here - /* - const adminIds = runtime.getSetting("ADMIN_USER_IDS")?.split(",") || []; - //console.log("Admin IDs from settings:", adminIds); - - const isAdmin = adminIds.includes(message.userId); - - if (isAdmin) { - //console.log(`Authorized transfer from user: ${message.userId}`); - return true; - } - else - { - //console.log(`Unauthorized transfer attempt from user: ${message.userId}`); - return false; - } - */ - return false; + elizaLogger.debug("Starting transfer validation for user:", message.userId); + elizaLogger.debug("Message text:", message.content?.text); + return true; // Let the handler do the validation }, - description: "Transfer tokens from the agent's wallet to another address", + priority: 1000, // High priority for transfer actions + description: "Transfer Move tokens from the agent's wallet to another address", handler: async ( runtime: IAgentRuntime, message: Memory, @@ -92,46 +100,22 @@ export default { _options: { [key: string]: unknown }, callback?: HandlerCallback ): Promise => { - elizaLogger.log("Starting SEND_TOKEN handler..."); - - const walletInfo = await walletProvider.get(runtime, message, state); - state.walletInfo = walletInfo; - - // 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, + elizaLogger.debug("Starting TRANSFER_MOVE handler..."); + elizaLogger.debug("Message:", { + text: message.content?.text, + userId: message.userId, + action: message.content?.action }); - // Generate transfer content - const content = await generateObjectDeprecated({ - runtime, - context: transferContext, - modelClass: ModelClass.SMALL, - }); + try { + const privateKey = runtime.getSetting("MOVEMENT_PRIVATE_KEY"); + elizaLogger.debug("Got private key:", privateKey ? "Present" : "Missing"); - // 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; - } + const network = runtime.getSetting("MOVEMENT_NETWORK"); + elizaLogger.debug("Network config:", network); + elizaLogger.debug("Available networks:", Object.keys(MOVEMENT_NETWORK_CONFIG)); - try { - const privateKey = runtime.getSetting("APTOS_PRIVATE_KEY"); - const aptosAccount = Account.fromPrivateKey({ + const movementAccount = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey( PrivateKey.formatPrivateKey( privateKey, @@ -139,23 +123,60 @@ export default { ) ), }); - const network = runtime.getSetting("APTOS_NETWORK") as Network; + elizaLogger.debug("Created Movement account:", movementAccount.accountAddress.toStringLong()); + const aptosClient = new Aptos( new AptosConfig({ - network, + network: Network.CUSTOM, + fullnode: MOVEMENT_NETWORK_CONFIG[network].fullnode }) ); + elizaLogger.debug("Created Aptos client with network:", MOVEMENT_NETWORK_CONFIG[network].fullnode); + + const walletInfo = await walletProvider.get(runtime, message, state); + state.walletInfo = walletInfo; + + // 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 generateObjectDeprecated({ + 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; + } - const APT_DECIMALS = 8; const adjustedAmount = BigInt( - Number(content.amount) * Math.pow(10, APT_DECIMALS) + Number(content.amount) * Math.pow(10, MOVE_DECIMALS) ); console.log( `Transferring: ${content.amount} tokens (${adjustedAmount} base units)` ); const tx = await aptosClient.transaction.build.simple({ - sender: aptosAccount.accountAddress.toStringLong(), + sender: movementAccount.accountAddress.toStringLong(), data: { function: "0x1::aptos_account::transfer", typeArguments: [], @@ -164,23 +185,30 @@ export default { }); const committedTransaction = await aptosClient.signAndSubmitTransaction({ - signer: aptosAccount, + signer: movementAccount, transaction: tx, }); const executedTransaction = await aptosClient.waitForTransaction({ transactionHash: committedTransaction.hash, }); - console.log("Transfer successful:", executedTransaction.hash); + const explorerUrl = `${MOVEMENT_EXPLORER_URL}/${executedTransaction.hash}?network=${MOVEMENT_NETWORK_CONFIG[network].explorerNetwork}`; + elizaLogger.debug("Transfer successful:", { + hash: executedTransaction.hash, + amount: content.amount, + recipient: content.recipient, + explorerUrl + }); if (callback) { callback({ - text: `Successfully transferred ${content.amount} APT to ${content.recipient}, Transaction: ${executedTransaction.hash}`, + text: `Successfully transferred ${content.amount} MOVE to ${content.recipient}\nTransaction: ${executedTransaction.hash}\nView on Explorer: ${explorerUrl}`, content: { success: true, hash: executedTransaction.hash, amount: content.amount, recipient: content.recipient, + explorerUrl }, }); } @@ -203,22 +231,31 @@ export default { { user: "{{user1}}", content: { - text: "Send 69 APT tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0", + text: "can you send 1 move to 0xa07ab7d3739dc793f9d538f7d7163705176ba59f7a8c994a07357a3a7d97d843", }, }, { user: "{{user2}}", content: { - text: "I'll send 69 APT tokens now...", - action: "SEND_TOKEN", + text: "I'll help you transfer 1 Move token...", + action: "TRANSFER_MOVE", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "send 1 move to 0xa07ab7d3739dc793f9d538f7d7163705176ba59f7a8c994a07357a3a7d97d843", }, }, { user: "{{user2}}", content: { - text: "Successfully sent 69 APT tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0, Transaction: 0x39a8c432d9bdad993a33cc1faf2e9b58fb7dd940c0425f1d6db3997e4b4b05c0", + text: "Processing Move token transfer...", + action: "TRANSFER_MOVE", }, }, - ], + ] ] as ActionExample[][], } as Action; diff --git a/packages/plugin-movement/src/constants.ts b/packages/plugin-movement/src/constants.ts index eec5ddec5f..8a9c84847c 100644 --- a/packages/plugin-movement/src/constants.ts +++ b/packages/plugin-movement/src/constants.ts @@ -1,16 +1,19 @@ -export const MOV_DECIMALS = 8; +export const MOVE_DECIMALS = 8; -export const MOVEMENT_NETWORKS = { +export const MOVEMENT_NETWORK_CONFIG = { mainnet: { - fullnode: 'https://fullnode.mainnet.mov.network/v1', - chainId: '1', - name: 'Movement Mainnet' + fullnode: 'https://mainnet.movementnetwork.xyz/v1', + chainId: '126', + name: 'Movement Mainnet', + explorerNetwork: 'mainnet' }, bardock: { - fullnode: 'https://fullnode.testnet.mov.network/v1', - chainId: '2', - name: 'Movement Bardock Testnet' + fullnode: 'https://aptos.testnet.bardock.movementlabs.xyz/v1', + chainId: '250', + name: 'Movement Bardock Testnet', + explorerNetwork: 'bardock+testnet' } } as const; -export const DEFAULT_NETWORK = 'bardock'; \ No newline at end of file +export const DEFAULT_NETWORK = 'bardock'; +export const MOVEMENT_EXPLORER_URL = 'https://explorer.movementnetwork.xyz/txn'; \ No newline at end of file diff --git a/packages/plugin-movement/src/enviroment.ts b/packages/plugin-movement/src/enviroment.ts deleted file mode 100644 index 2d7b2e3c14..0000000000 --- a/packages/plugin-movement/src/enviroment.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IAgentRuntime } from "@elizaos/core"; -import { z } from "zod"; - -export const aptosEnvSchema = z.object({ - APTOS_PRIVATE_KEY: z.string().min(1, "Aptos private key is required"), - APTOS_NETWORK: z.enum(["mainnet", "testnet", "movement_mainnet", "movement_testnet"]), -}); - -export type AptosConfig = z.infer; - -export async function validateAptosConfig( - runtime: IAgentRuntime -): Promise { - try { - const config = { - APTOS_PRIVATE_KEY: - runtime.getSetting("APTOS_PRIVATE_KEY") || - process.env.APTOS_PRIVATE_KEY, - APTOS_NETWORK: - runtime.getSetting("APTOS_NETWORK") || - process.env.APTOS_NETWORK, - }; - - return aptosEnvSchema.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( - `Aptos configuration validation failed:\n${errorMessages}` - ); - } - throw error; - } -} diff --git a/packages/plugin-movement/src/environment.ts b/packages/plugin-movement/src/environment.ts index a990396252..96081dbe97 100644 --- a/packages/plugin-movement/src/environment.ts +++ b/packages/plugin-movement/src/environment.ts @@ -15,11 +15,9 @@ export async function validateMovementConfig( const config = { MOVEMENT_PRIVATE_KEY: runtime.getSetting("MOVEMENT_PRIVATE_KEY") || - runtime.getSetting("APTOS_PRIVATE_KEY") || // Fallback for compatibility process.env.MOVEMENT_PRIVATE_KEY, MOVEMENT_NETWORK: runtime.getSetting("MOVEMENT_NETWORK") || - runtime.getSetting("APTOS_NETWORK")?.replace("movement_", "") || // Handle movement_bardock -> bardock process.env.MOVEMENT_NETWORK || "bardock", }; diff --git a/packages/plugin-movement/src/providers/wallet.ts b/packages/plugin-movement/src/providers/wallet.ts index 3e4f8f6a16..90b6d1ac21 100644 --- a/packages/plugin-movement/src/providers/wallet.ts +++ b/packages/plugin-movement/src/providers/wallet.ts @@ -17,8 +17,7 @@ import { import BigNumber from "bignumber.js"; import NodeCache from "node-cache"; import * as path from "path"; -import { APT_DECIMALS, MOVEMENT_NETWORK } from "../constants"; -import { isMovementNetwork, getMovementNetworkType } from "../utils"; +import { MOVE_DECIMALS, MOVEMENT_NETWORK_CONFIG } from "../constants"; // Provider configuration const PROVIDER_CONFIG = { @@ -28,16 +27,16 @@ const PROVIDER_CONFIG = { interface WalletPortfolio { totalUsd: string; - totalApt: string; + totalMove: string; } interface Prices { - apt: { usd: string }; + move: { usd: string }; } export class WalletProvider { private cache: NodeCache; - private cacheKey: string = "aptos/wallet"; + private cacheKey: string = "movement/wallet"; constructor( private aptosClient: Aptos, @@ -91,10 +90,10 @@ export class WalletProvider { for (let i = 0; i < PROVIDER_CONFIG.MAX_RETRIES; i++) { try { - const cellanaAptUsdcPoolAddr = - "0x234f0be57d6acfb2f0f19c17053617311a8d03c9ce358bdf9cd5c460e4a02b7c"; + const MoveUsdcPoolAddr = + "0xA04d13F092f68F603A193832222898B0d9f52c71"; const response = await fetch( - `https://api.dexscreener.com/latest/dex/pairs/aptos/${cellanaAptUsdcPoolAddr}` + `https://api.dexscreener.com/latest/dex/pairs/ethereum/${MoveUsdcPoolAddr}` ); if (!response.ok) { @@ -137,26 +136,26 @@ export class WalletProvider { console.log("Cache miss for fetchPortfolioValue"); const prices = await this.fetchPrices().catch((error) => { - console.error("Error fetching APT price:", error); + console.error("Error fetching Move price:", error); throw error; }); - const aptAmountOnChain = await this.aptosClient + const moveAmountOnChain = await this.aptosClient .getAccountAPTAmount({ accountAddress: this.address, }) .catch((error) => { - console.error("Error fetching APT amount:", error); + console.error("Error fetching Move amount:", error); throw error; }); - const aptAmount = new BigNumber(aptAmountOnChain).div( - new BigNumber(10).pow(APT_DECIMALS) + const moveAmount = new BigNumber(moveAmountOnChain).div( + new BigNumber(10).pow(MOVE_DECIMALS) ); - const totalUsd = new BigNumber(aptAmount).times(prices.apt.usd); + const totalUsd = new BigNumber(moveAmount).times(prices.move.usd); const portfolio = { totalUsd: totalUsd.toString(), - totalApt: aptAmount.toString(), + totalMove: moveAmount.toString(), }; this.setCachedData(cacheKey, portfolio); console.log("Fetched portfolio:", portfolio); @@ -178,14 +177,14 @@ export class WalletProvider { } console.log("Cache miss for fetchPrices"); - const aptPriceData = await this.fetchPricesWithRetry().catch( + const movePriceData = await this.fetchPricesWithRetry().catch( (error) => { - console.error("Error fetching APT price:", error); + console.error("Error fetching Move price:", error); throw error; } ); const prices: Prices = { - apt: { usd: aptPriceData.pair.priceUsd }, + move: { usd: movePriceData.pair.priceUsd }, }; this.setCachedData(cacheKey, prices); return prices; @@ -200,9 +199,9 @@ export class WalletProvider { output += `Wallet Address: ${this.address}\n`; const totalUsdFormatted = new BigNumber(portfolio.totalUsd).toFixed(2); - const totalAptFormatted = new BigNumber(portfolio.totalApt).toFixed(4); + const totalMoveFormatted = new BigNumber(portfolio.totalMove).toFixed(4); - output += `Total Value: $${totalUsdFormatted} (${totalAptFormatted} APT)\n`; + output += `Total Value: $${totalUsdFormatted} (${totalMoveFormatted} Move)\n`; return output; } @@ -224,8 +223,8 @@ const walletProvider: Provider = { _message: Memory, _state?: State ): Promise => { - const privateKey = runtime.getSetting("APTOS_PRIVATE_KEY"); - const aptosAccount = Account.fromPrivateKey({ + const privateKey = runtime.getSetting("MOVEMENT_PRIVATE_KEY"); + const movementAccount = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey( PrivateKey.formatPrivateKey( privateKey, @@ -233,25 +232,18 @@ const walletProvider: Provider = { ) ), }); - const network = runtime.getSetting("APTOS_NETWORK") as Network; + const network = runtime.getSetting("MOVEMENT_NETWORK") as Network; try { - console.log("Network:", network); const aptosClient = new Aptos( - new AptosConfig( - isMovementNetwork(network) - ? { - network: Network.CUSTOM, - fullnode: MOVEMENT_NETWORK[getMovementNetworkType(network)].fullnode - } - : { - network - } - ) + new AptosConfig({ + network: Network.CUSTOM, + fullnode: MOVEMENT_NETWORK_CONFIG[network].fullnode + }) ); const provider = new WalletProvider( aptosClient, - aptosAccount.accountAddress.toStringLong(), + movementAccount.accountAddress.toStringLong(), runtime.cacheManager ); return await provider.getFormattedPortfolio(runtime); diff --git a/packages/plugin-movement/src/tests/transfer.test.ts b/packages/plugin-movement/src/tests/transfer.test.ts new file mode 100644 index 0000000000..e98689a002 --- /dev/null +++ b/packages/plugin-movement/src/tests/transfer.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import transferAction from "../actions/transfer"; + +describe("Movement Transfer Action", () => { + describe("Action Configuration", () => { + it("should have correct action name and triggers", () => { + expect(transferAction.name).toBe("TRANSFER_MOVE"); + expect(transferAction.triggers).toContain("send move"); + expect(transferAction.priority).toBe(1000); + }); + + it("should validate transfer messages correctly", () => { + const validMessage = "send 1 move to 0x123"; + const invalidMessage = "hello world"; + + expect(transferAction.shouldHandle({ content: { text: validMessage }})).toBe(true); + expect(transferAction.shouldHandle({ content: { text: invalidMessage }})).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/packages/plugin-movement/src/tests/wallet.test.ts b/packages/plugin-movement/src/tests/wallet.test.ts index f7d2829413..833426cc2e 100644 --- a/packages/plugin-movement/src/tests/wallet.test.ts +++ b/packages/plugin-movement/src/tests/wallet.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; -import { WalletProvider } from "../providers/wallet.ts"; +import { WalletProvider } from "../providers/wallet"; import { Account, Aptos, @@ -11,7 +11,7 @@ import { } from "@aptos-labs/ts-sdk"; import { defaultCharacter } from "@elizaos/core"; import BigNumber from "bignumber.js"; -import { APT_DECIMALS } from "../constants.ts"; +import { MOVE_DECIMALS, MOVEMENT_NETWORK_CONFIG } from "../constants"; // Mock NodeCache vi.mock("node-cache", () => { @@ -49,28 +49,37 @@ describe("WalletProvider", () => { const aptosClient = new Aptos( new AptosConfig({ - network: Network.TESTNET, + network: Network.CUSTOM, + fullnode: MOVEMENT_NETWORK_CONFIG.bardock.fullnode }) ); - const aptosAccount = Account.fromPrivateKey({ + const movementAccount = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey( PrivateKey.formatPrivateKey( - // this is a testnet private key - "0x90e02bf2439492bd9be1ec5f569704accefd65ba88a89c4dcef1977e0203211e", + // this is a test private key - DO NOT USE IN PRODUCTION + "0x5b4ca82e1835dcc51e58a3dec44b857edf60a26156b00f73d74bf96f5daecfb5", PrivateKeyVariants.Ed25519 ) ), }); - // Create new instance of TokenProvider with mocked dependencies + // Create new instance of WalletProvider with Movement configuration walletProvider = new WalletProvider( aptosClient, - aptosAccount.accountAddress.toStringLong(), + movementAccount.accountAddress.toStringLong(), mockCacheManager ); mockedRuntime = { - character: defaultCharacter, + character: { + ...defaultCharacter, + settings: { + secrets: { + MOVEMENT_PRIVATE_KEY: "0x5b4ca82e1835dcc51e58a3dec44b857edf60a26156b00f73d74bf96f5daecfb5", + MOVEMENT_NETWORK: "bardock" + } + } + }, }; }); @@ -78,27 +87,58 @@ describe("WalletProvider", () => { vi.clearAllTimers(); }); - describe("Wallet Integration", () => { - it("should check wallet address", async () => { - const result = - await walletProvider.getFormattedPortfolio(mockedRuntime); + describe("Movement Wallet Integration", () => { + it("should check wallet address and balance", async () => { + const result = await walletProvider.getFormattedPortfolio(mockedRuntime); const prices = await walletProvider.fetchPrices(); - const aptAmountOnChain = - await walletProvider.aptosClient.getAccountAPTAmount({ - accountAddress: walletProvider.address, - }); - const aptAmount = new BigNumber(aptAmountOnChain) - .div(new BigNumber(10).pow(APT_DECIMALS)) + const moveAmountOnChain = await walletProvider.aptosClient.getAccountAPTAmount({ + accountAddress: walletProvider.address, + }); + const moveAmount = new BigNumber(moveAmountOnChain) + .div(new BigNumber(10).pow(MOVE_DECIMALS)) .toFixed(4); - const totalUsd = new BigNumber(aptAmount) - .times(prices.apt.usd) + const totalUsd = new BigNumber(moveAmount) + .times(prices.move.usd) .toFixed(2); - expect(result).toEqual( - `Eliza\nWallet Address: ${walletProvider.address}\n` + - `Total Value: $${totalUsd} (${aptAmount} APT)\n` - ); + expect(result).toContain(walletProvider.address); + expect(result).toContain(`$${totalUsd}`); + expect(result).toContain(`${moveAmount} Move`); + + expect(result).toContain('Total Value:'); + expect(result).toContain('Wallet Address:'); + }); + + it("should fetch Movement token prices", async () => { + const prices = await walletProvider.fetchPrices(); + expect(prices).toHaveProperty("move.usd"); + expect(["string", "number"]).toContain(typeof prices.move.usd); + }); + + it("should cache wallet info", async () => { + await walletProvider.getFormattedPortfolio(mockedRuntime); + expect(mockCacheManager.set).toHaveBeenCalled(); + }); + + it("should use cached wallet info when available", async () => { + const cachedInfo = { + totalUsd: "100.00", + totalMove: "50.0000" + }; + mockCacheManager.get.mockResolvedValueOnce(cachedInfo); + + const result = await walletProvider.getFormattedPortfolio(mockedRuntime); + expect(result).toContain(cachedInfo.totalUsd); + expect(result).toContain(cachedInfo.totalMove); + }); + + it("should handle network errors gracefully", async () => { + const mockError = new Error("Network error"); + vi.spyOn(walletProvider.aptosClient, "getAccountAPTAmount").mockRejectedValueOnce(mockError); + + const result = await walletProvider.getFormattedPortfolio(mockedRuntime); + expect(result).toBe("Unable to fetch wallet information. Please try again later."); }); }); }); diff --git a/packages/plugin-movement/src/utils.ts b/packages/plugin-movement/src/utils.ts deleted file mode 100644 index 1ec60a0d8f..0000000000 --- a/packages/plugin-movement/src/utils.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function isMovementNetwork(network: string): boolean { - return network.startsWith('movement_'); -} - -export function getMovementNetworkType(network: string): 'MAINNET' | 'TESTNET' { - return network === 'movement_mainnet' ? 'MAINNET' : 'TESTNET'; -} - -export function getTokenSymbol(network: string): string { - return network.startsWith('movement_') ? 'MOVE' : 'APT'; -} \ No newline at end of file From 74b717668512bcb28b9c656c31e9c70bfa31d201 Mon Sep 17 00:00:00 2001 From: rahat chowdhury Date: Thu, 2 Jan 2025 01:09:45 -0500 Subject: [PATCH 5/5] add readme instructions --- packages/plugin-movement/readme.md | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/plugin-movement/readme.md diff --git a/packages/plugin-movement/readme.md b/packages/plugin-movement/readme.md new file mode 100644 index 0000000000..cecd6fee59 --- /dev/null +++ b/packages/plugin-movement/readme.md @@ -0,0 +1,43 @@ +# @elizaos/plugin-movement + +Movement Network plugin for Eliza OS. This plugin enables Movement Network blockchain functionality for your Eliza agent. + +## Features + +- Send MOVE tokens +- Check wallet balances +- Support for Movement Network transactions + +## Installation + +```bash +pnpm add @elizaos/plugin-movement +``` + +## Instructions + +1. First, ensure you have a Movement Network wallet and private key. + +2. Add the Movement plugin to your character's configuration: + +```json +{ +"name": "Movement Agent", +"plugins": ["@elizaos/plugin-movement"], +"settings": { +"secrets": { +"MOVEMENT_PRIVATE_KEY": "your_private_key_here", +"MOVEMENT_NETWORK": "bardock" +} +} +} +``` + +Set up your environment variables in the `.env` file: + +```bash +MOVEMENT_PRIVATE_KEY=your_private_key_here +MOVEMENT_NETWORK=bardock +``` + +