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

feat: add plugin-sui #934

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 @@ -275,3 +275,7 @@ AWS_S3_UPLOAD_PATH=

# Deepgram
DEEPGRAM_API_KEY=

# Sui
SUI_PRIVATE_KEY= # Sui Mnemonic Seed Phrase (`sui keytool generate ed25519`)
SUI_NETWORK= # must be one of mainnet, testnet, devnet, localnet
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@ai16z/plugin-node": "workspace:*",
"@ai16z/plugin-solana": "workspace:*",
"@ai16z/plugin-starknet": "workspace:*",
"@ai16z/plugin-sui": "workspace:*",
"@ai16z/plugin-tee": "workspace:*",
"readline": "1.3.0",
"ws": "8.18.0",
Expand Down
2 changes: 2 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { solanaPlugin } from "@ai16z/plugin-solana";
import { teePlugin, TEEMode } from "@ai16z/plugin-tee";
import { aptosPlugin, TransferAptosToken } from "@ai16z/plugin-aptos";
import { flowPlugin } from "@ai16z/plugin-flow";
import { suiPlugin } from "@ai16z/plugin-sui";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
Expand Down Expand Up @@ -491,6 +492,7 @@ export async function createAgent(
? flowPlugin
: null,
getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null,
getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null,
].filter(Boolean),
providers: [],
actions: [],
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-sui/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
3 changes: 3 additions & 0 deletions packages/plugin-sui/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [...eslintGlobalConfig];
26 changes: 26 additions & 0 deletions packages/plugin-sui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@ai16z/plugin-sui",
"version": "0.1.5-alpha.5",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@ai16z/eliza": "workspace:*",
"@ai16z/plugin-trustdb": "workspace:*",
"@mysten/sui": "^1.16.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",
"lint": "eslint . --fix",
"test": "vitest run"
},
"peerDependencies": {
"form-data": "4.0.1",
"whatwg-url": "7.1.0"
}
}
214 changes: 214 additions & 0 deletions packages/plugin-sui/src/actions/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { elizaLogger } from "@ai16z/eliza";
import {
ActionExample,
Content,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
type Action,
} from "@ai16z/eliza";
import { composeContext } from "@ai16z/eliza";
import { generateObjectV2 } from "@ai16z/eliza";
import { z } from "zod";

import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { SUI_DECIMALS } from "@mysten/sui/utils";
import { Transaction } from "@mysten/sui/transactions";

import { walletProvider } from "../providers/wallet";

type SuiNetwork = "mainnet" | "testnet" | "devnet" | "localnet";

export interface TransferContent extends Content {
recipient: string;
amount: string | number;
}

function isTransferContent(content: Content): 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": "0xaa000b3651bd1e57554ebd7308ca70df7c8c0e8e09d67123cc15c8a8a79342b3",
"amount": "1"
}
\`\`\`

{{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_SUI",
"PAY",
],
validate: async (runtime: IAgentRuntime, message: Memory) => {
console.log("Validating sui 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 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...");

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 transferSchema = z.object({
recipient: z.string(),
amount: z.union([z.string(), z.number()]),
});

// Compose transfer context
const transferContext = composeContext({
state,
template: transferTemplate,
});

// Generate transfer content with the schema
const content = await generateObjectV2({
runtime,
context: transferContext,
schema: transferSchema,
modelClass: ModelClass.SMALL,
});

const transferContent = content.object as TransferContent;

// Validate transfer content
if (!isTransferContent(transferContent)) {
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("SUI_PRIVATE_KEY");
const suiAccount = Ed25519Keypair.deriveKeypair(privateKey);
const network = runtime.getSetting("SUI_NETWORK");
const suiClient = new SuiClient({
url: getFullnodeUrl(network as SuiNetwork),
});

const adjustedAmount = BigInt(
Number(transferContent.amount) * Math.pow(10, SUI_DECIMALS)
);
console.log(
`Transferring: ${transferContent.amount} tokens (${adjustedAmount} base units)`
);
const tx = new Transaction();
const [coin] = tx.splitCoins(tx.gas, [adjustedAmount]);
tx.transferObjects([coin], transferContent.recipient);
const executedTransaction =
await suiClient.signAndExecuteTransaction({
signer: suiAccount,
transaction: tx,
});

console.log("Transfer successful:", executedTransaction.digest);

if (callback) {
callback({
text: `Successfully transferred ${transferContent.amount} SUI to ${transferContent.recipient}, Transaction: ${executedTransaction.digest}`,
content: {
success: true,
hash: executedTransaction.digest,
amount: transferContent.amount,
recipient: transferContent.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 1 SUI tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0",
},
},
{
user: "{{user2}}",
content: {
text: "I'll send 1 SUI tokens now...",
action: "SEND_TOKEN",
},
},
{
user: "{{user2}}",
content: {
text: "Successfully sent 1 SUI tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0, Transaction: 0x39a8c432d9bdad993a33cc1faf2e9b58fb7dd940c0425f1d6db3997e4b4b05c0",
},
},
],
] as ActionExample[][],
} as Action;
36 changes: 36 additions & 0 deletions packages/plugin-sui/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 suiEnvSchema = z.object({
SUI_PRIVATE_KEY: z.string().min(1, "Sui private key is required"),
SUI_NETWORK: z.enum(["mainnet", "testnet"]),
});

export type SuiConfig = z.infer<typeof suiEnvSchema>;

export async function validateSuiConfig(
runtime: IAgentRuntime
): Promise<SuiConfig> {
try {
const config = {
SUI_PRIVATE_KEY:
runtime.getSetting("SUI_PRIVATE_KEY") ||
process.env.SUI_PRIVATE_KEY,
SUI_NETWORK:
runtime.getSetting("SUI_NETWORK") ||
process.env.SUI_NETWORK,
};

return suiEnvSchema.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(
`Sui configuration validation failed:\n${errorMessages}`
);
}
throw error;
}
}
15 changes: 15 additions & 0 deletions packages/plugin-sui/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Plugin } from "@ai16z/eliza";
import transferToken from "./actions/transfer.ts";
import { WalletProvider, walletProvider } from "./providers/wallet.ts";

export { WalletProvider, transferToken as TransferSuiToken };

export const suiPlugin: Plugin = {
name: "sui",
description: "Sui Plugin for Eliza",
actions: [transferToken],
evaluators: [],
providers: [walletProvider],
};

export default suiPlugin;
Loading
Loading