From 67125470176b9ea6e2e1b665145807cf47e27064 Mon Sep 17 00:00:00 2001 From: Vakurin Date: Sat, 11 Jan 2025 06:26:05 +0700 Subject: [PATCH] feat: enable swapping via aftermath and domain name conversion --- packages/plugin-sui/package.json | 3 + .../src/actions/convertNameToAddress.ts | 222 +++++++++++++++++ packages/plugin-sui/src/actions/swap.ts | 232 ++++++++++++++++++ packages/plugin-sui/src/index.ts | 4 +- 4 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-sui/src/actions/convertNameToAddress.ts create mode 100644 packages/plugin-sui/src/actions/swap.ts diff --git a/packages/plugin-sui/package.json b/packages/plugin-sui/package.json index 1c0cf194321..21d75029f67 100644 --- a/packages/plugin-sui/package.json +++ b/packages/plugin-sui/package.json @@ -22,6 +22,9 @@ "@elizaos/core": "workspace:*", "@elizaos/plugin-trustdb": "workspace:*", "@mysten/sui": "^1.16.0", + "@mysten/sui.js": "^0.54.1", + "@mysten/suins": "^0.4.2", + "aftermath-ts-sdk": "^1.2.45", "bignumber": "1.1.0", "bignumber.js": "9.1.2", "node-cache": "5.1.2", diff --git a/packages/plugin-sui/src/actions/convertNameToAddress.ts b/packages/plugin-sui/src/actions/convertNameToAddress.ts new file mode 100644 index 00000000000..b7089cc6c80 --- /dev/null +++ b/packages/plugin-sui/src/actions/convertNameToAddress.ts @@ -0,0 +1,222 @@ +import { + ActionExample, + Content, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + composeContext, + elizaLogger, + generateObject, + type Action, +} from "@elizaos/core"; +import { z } from "zod"; + +import { SuiClient, getFullnodeUrl } from "@mysten/sui/client"; +import { SuinsClient } from "@mysten/suins"; + +import { walletProvider } from "../providers/wallet"; + +type SuiNetwork = "mainnet" | "testnet" | "devnet" | "localnet"; + +export interface NameToAddressContent extends Content { + recipientName: string; +} + +function isNameToAddressContent( + content: Content +): content is NameToAddressContent { + console.log("Content for show address", content); + return typeof content.recipientName === "string"; +} + +const nameToAddressTemplate = `Extract the SUI domain name from the recent messages and return it in a JSON format. + +Example input: "Convert adeniyi.sui to address" or "What's the address for adeniyi.sui" +Example output: +\`\`\`json +{ + "recipientName": "adeniyi.sui" +} +\`\`\` + +{{recentMessages}} + +Extract the SUI domain name (ending in .sui) that needs to be converted to an address. +If no valid .sui domain is found, return null.`; + +export default { + name: "CONVERT_SUINS_TO_ADDRESS", + similes: [ + "CONVERT_SUI_NAME_TO_ADDRESS", + "CONVERT_DOMAIN_TO_ADDRESS", + "SHOW_ADDRESS_BY_NAME", + "SHOW_ADDRESS_BY_DOMAIN", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + console.log( + "Validating sui name to address 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 true; + }, + description: "Convert a name service domain to an sui address", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting CONVERT_SUINS_TO_ADDRESS 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); + } + + // Define the schema for the expected output + const nameToAddressSchema = z.object({ + recipientName: z.string(), + }); + + // Compose transfer context + const nameToAddressContext = composeContext({ + state, + template: nameToAddressTemplate, + }); + + // Generate transfer content with the schema + const content = await generateObject({ + runtime, + context: nameToAddressContext, + schema: nameToAddressSchema, + modelClass: ModelClass.SMALL, + }); + + const nameToAddressContent = content.object as NameToAddressContent; + + // Validate transfer content + if (!isNameToAddressContent(nameToAddressContent)) { + console.error( + "Invalid content for CONVERT_SUINS_TO_ADDRESS action." + ); + if (callback) { + callback({ + text: "Unable to process name to address request. Invalid content provided.", + content: { error: "Invalid name to address content" }, + }); + } + return false; + } + + try { + const network = runtime.getSetting("SUI_NETWORK"); + const suiClient = new SuiClient({ + url: getFullnodeUrl(network as SuiNetwork), + }); + const suinsClient = new SuinsClient({ + client: suiClient, + network: network as Exclude, + }); + + console.log( + "Getting address for name:", + nameToAddressContent.recipientName + ); + + const address = await suinsClient.getNameRecord( + nameToAddressContent.recipientName + ); + console.log("Address:", address); + + if (callback) { + callback({ + text: `Successfully convert ${nameToAddressContent.recipientName} to ${address.targetAddress}`, + content: { + success: true, + address: address.targetAddress, + }, + }); + } + + return true; + } catch (error) { + console.error("Error during name to address conversion:", error); + if (callback) { + callback({ + text: `Error during name to address conversion: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Convert adeniyi.sui to address", + }, + }, + { + user: "{{user2}}", + content: { + text: "Converting adeniyi.sui to address...", + action: "CONVERT_SUINS_TO_ADDRESS", + }, + }, + { + user: "{{user2}}", + content: { + text: "Successfully convert adeniyi.sui to 0x1eb7c57e3f2bd0fc6cb9dcffd143ea957e4d98f805c358733f76dee0667fe0b1", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Convert @adeniyi to address", + }, + }, + { + user: "{{user2}}", + content: { + text: "Convert @adeniyi to address", + action: "CONVERT_NAME_TO_ADDRESS", + }, + }, + { + user: "{{user2}}", + content: { + text: "Successfully convert @adeniyi to 0x1eb7c57e3f2bd0fc6cb9dcffd143ea957e4d98f805c358733f76dee0667fe0b1", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-sui/src/actions/swap.ts b/packages/plugin-sui/src/actions/swap.ts new file mode 100644 index 00000000000..100c785ba09 --- /dev/null +++ b/packages/plugin-sui/src/actions/swap.ts @@ -0,0 +1,232 @@ +import { + ActionExample, + Content, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + composeContext, + elizaLogger, + generateObject, + type Action, +} from "@elizaos/core"; +import { z } from "zod"; + +import { SuiClient, getFullnodeUrl } from "@mysten/sui/client"; +import { SUI_DECIMALS } from "@mysten/sui/utils"; +import { + Aftermath, + RouterCompleteTradeRoute as AftermathRouterCompleteTradeRoute, +} from "aftermath-ts-sdk"; + +import { walletProvider } from "../providers/wallet"; +import { parseAccount } from "../utils"; + +type SuiNetwork = "mainnet" | "testnet" | "devnet" | "localnet"; + +export interface SwapContent extends Content { + recipient: string; + amount: string | number; + fromCoinType: string; + toCoinType: string; +} + +function isSwapContent(content: Content): content is SwapContent { + console.log("Content for swap", content); + return ( + typeof content.recipient === "string" && + typeof content.fromCoinType === "string" && + typeof content.toCoinType === "string" && + (typeof content.amount === "string" || + typeof content.amount === "number") + ); +} + +const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "recipient": "0xaa000b3651bd1e57554ebd7308ca70df7c8c0e8e09d67123cc15c8a8a79342b3", + "amount": "1", + "fromCoinType": "0x2::sui::SUI", + "toCoinType": "0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP" +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested token swap: +- Recipient wallet address to receive swapped tokens +- Amount of tokens to swap +- Source token type to swap from +- Destination token type to swap to + +Respond with a JSON markdown block containing only the extracted values.`; + +export default { + name: "SWAP_TOKEN", + similes: ["SWAP_TOKEN", "SWAP_TOKENS", "SWAP_SUI", "SWAP"], + validate: async (runtime: IAgentRuntime, message: Memory) => { + console.log("Validating sui swap 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 true; + }, + description: "Swap 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 SWAP_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); + } + + // Define the schema for the expected output + const swapSchema = z.object({ + recipient: z.string(), + amount: z.union([z.string(), z.number()]), + fromCoinType: z.string(), + toCoinType: z.string(), + }); + + // Compose swap context + const swapContext = composeContext({ + state, + template: swapTemplate, + }); + + // Generate swap content with the schema + const content = await generateObject({ + runtime, + context: swapContext, + schema: swapSchema, + modelClass: ModelClass.SMALL, + }); + + const swapContent = content.object as SwapContent; + + // Validate swap content + if (!isSwapContent(swapContent)) { + console.error("Invalid content for SWAP_TOKEN action."); + if (callback) { + callback({ + text: "Unable to process swap request. Invalid content provided.", + content: { error: "Invalid swap content" }, + }); + } + return false; + } + + try { + const suiAccount = parseAccount(runtime); + const network = runtime.getSetting("SUI_NETWORK"); + const suiClient = new SuiClient({ + url: getFullnodeUrl(network as SuiNetwork), + }); + // TODO: make this dynamic + const router = new Aftermath("MAINNET").Router(); + + // TODO: change this to use the correct decimals for any coin + const adjustedAmount = BigInt( + Number(swapContent.amount) * Math.pow(10, SUI_DECIMALS) + ); + console.log( + `Swapping: ${swapContent.amount} ${swapContent.fromCoinType} to ${swapContent.toCoinType} (${adjustedAmount} base units)` + ); + const route = await router.getCompleteTradeRouteGivenAmountIn({ + coinInType: swapContent.fromCoinType, + coinOutType: swapContent.toCoinType, + coinInAmount: adjustedAmount, + }); + console.log("Route:", route); + const tx = await router.getTransactionForCompleteTradeRoute({ + walletAddress: swapContent.recipient, + completeRoute: route, + slippage: 0.01, // 1% max slippage + }); + console.log("Transaction:", tx); + const executedTransaction = + await suiClient.signAndExecuteTransaction({ + signer: suiAccount, + transaction: tx, + }); + + console.log("Swap successful:", executedTransaction.digest); + + if (callback) { + callback({ + text: `Successfully swapped ${swapContent.amount} ${swapContent.fromCoinType} to ${swapContent.toCoinType} to ${swapContent.recipient}, Transaction: ${executedTransaction.digest}`, + content: { + success: true, + hash: executedTransaction.digest, + amount: swapContent.amount, + recipient: swapContent.recipient, + }, + }); + } + + return true; + } catch (error) { + console.error("Error during token swap:", error); + if (callback) { + callback({ + text: `Error swapping tokens: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Swap 1 SUI(0x2::sui::SUI) to DEEP(0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP) to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0", + }, + }, + { + user: "{{user2}}", + content: { + text: "Swap 1 SUI(0x2::sui::SUI) to DEEP(0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP) to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0", + action: "SWAP_TOKEN", + }, + }, + { + user: "{{user2}}", + content: { + text: "Successfully swapped 1 SUI to DEEP to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0, Transaction: 0x39a8c432d9bdad993a33cc1faf2e9b58fb7dd940c0425f1d6db3997e4b4b05c0", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-sui/src/index.ts b/packages/plugin-sui/src/index.ts index 5f69381fda0..1626ba15403 100644 --- a/packages/plugin-sui/src/index.ts +++ b/packages/plugin-sui/src/index.ts @@ -1,5 +1,7 @@ import { Plugin } from "@elizaos/core"; import transferToken from "./actions/transfer.ts"; +import convertNameToAddress from "./actions/convertNameToAddress.ts"; +import swapToken from "./actions/swap.ts"; import { WalletProvider, walletProvider } from "./providers/wallet.ts"; export { WalletProvider, transferToken as TransferSuiToken }; @@ -7,7 +9,7 @@ export { WalletProvider, transferToken as TransferSuiToken }; export const suiPlugin: Plugin = { name: "sui", description: "Sui Plugin for Eliza", - actions: [transferToken], + actions: [transferToken, convertNameToAddress, swapToken], evaluators: [], providers: [walletProvider], };