Skip to content

Commit 9bcd489

Browse files
committed
add plugin-sui
- add walletProvider for Sui - add transferSui action
1 parent 5a3d348 commit 9bcd489

14 files changed

+862
-0
lines changed

.env.example

+4
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,7 @@ AWS_SECRET_ACCESS_KEY=
264264
AWS_REGION=
265265
AWS_S3_BUCKET=
266266
AWS_S3_UPLOAD_PATH=
267+
268+
# Sui
269+
SUI_PRIVATE_KEY= # Sui Mnemonic Seed Phrase (`sui keytool generate ed25519`)
270+
SUI_NETWORK= # must be one of mainnet, testnet, devnet, localnet

agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@ai16z/plugin-node": "workspace:*",
4141
"@ai16z/plugin-solana": "workspace:*",
4242
"@ai16z/plugin-starknet": "workspace:*",
43+
"@ai16z/plugin-sui": "workspace:*",
4344
"@ai16z/plugin-tee": "workspace:*",
4445
"readline": "1.3.0",
4546
"ws": "8.18.0",

agent/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { solanaPlugin } from "@ai16z/plugin-solana";
4444
import { teePlugin, TEEMode } from "@ai16z/plugin-tee";
4545
import { aptosPlugin, TransferAptosToken } from "@ai16z/plugin-aptos";
4646
import { flowPlugin } from "@ai16z/plugin-flow";
47+
import { suiPlugin } from "@ai16z/plugin-sui";
4748
import Database from "better-sqlite3";
4849
import fs from "fs";
4950
import path from "path";
@@ -473,6 +474,7 @@ export async function createAgent(
473474
? flowPlugin
474475
: null,
475476
getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null,
477+
getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null,
476478
].filter(Boolean),
477479
providers: [],
478480
actions: [],

packages/plugin-sui/.npmignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*
2+
3+
!dist/**
4+
!package.json
5+
!readme.md
6+
!tsup.config.ts

packages/plugin-sui/eslint.config.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import eslintGlobalConfig from "../../eslint.config.mjs";
2+
3+
export default [...eslintGlobalConfig];

packages/plugin-sui/package.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@ai16z/plugin-sui",
3+
"version": "0.1.5-alpha.5",
4+
"main": "dist/index.js",
5+
"type": "module",
6+
"types": "dist/index.d.ts",
7+
"dependencies": {
8+
"@ai16z/eliza": "workspace:*",
9+
"@ai16z/plugin-trustdb": "workspace:*",
10+
"@mysten/sui": "^1.16.0",
11+
"bignumber": "1.1.0",
12+
"bignumber.js": "9.1.2",
13+
"node-cache": "5.1.2",
14+
"tsup": "8.3.5",
15+
"vitest": "2.1.4"
16+
},
17+
"scripts": {
18+
"build": "tsup --format esm --dts",
19+
"lint": "eslint . --fix",
20+
"test": "vitest run"
21+
},
22+
"peerDependencies": {
23+
"form-data": "4.0.1",
24+
"whatwg-url": "7.1.0"
25+
}
26+
}
+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { elizaLogger } from "@ai16z/eliza";
2+
import {
3+
ActionExample,
4+
Content,
5+
HandlerCallback,
6+
IAgentRuntime,
7+
Memory,
8+
ModelClass,
9+
State,
10+
type Action,
11+
} from "@ai16z/eliza";
12+
import { composeContext } from "@ai16z/eliza";
13+
import { generateObjectV2 } from "@ai16z/eliza";
14+
import { z } from "zod";
15+
16+
import { SuiClient, getFullnodeUrl } from "@mysten/sui/client";
17+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
18+
import { SUI_DECIMALS } from "@mysten/sui/utils";
19+
import { Transaction } from "@mysten/sui/transactions";
20+
21+
import { walletProvider } from "../providers/wallet";
22+
23+
type SuiNetwork = "mainnet" | "testnet" | "devnet" | "localnet";
24+
25+
export interface TransferContent extends Content {
26+
recipient: string;
27+
amount: string | number;
28+
}
29+
30+
function isTransferContent(content: Content): content is TransferContent {
31+
console.log("Content for transfer", content);
32+
return (
33+
typeof content.recipient === "string" &&
34+
(typeof content.amount === "string" ||
35+
typeof content.amount === "number")
36+
);
37+
}
38+
39+
const transferTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
40+
41+
Example response:
42+
\`\`\`json
43+
{
44+
"recipient": "0xaa000b3651bd1e57554ebd7308ca70df7c8c0e8e09d67123cc15c8a8a79342b3",
45+
"amount": "1"
46+
}
47+
\`\`\`
48+
49+
{{recentMessages}}
50+
51+
Given the recent messages, extract the following information about the requested token transfer:
52+
- Recipient wallet address
53+
- Amount to transfer
54+
55+
Respond with a JSON markdown block containing only the extracted values.`;
56+
57+
export default {
58+
name: "SEND_TOKEN",
59+
similes: [
60+
"TRANSFER_TOKEN",
61+
"TRANSFER_TOKENS",
62+
"SEND_TOKENS",
63+
"SEND_SUI",
64+
"PAY",
65+
],
66+
validate: async (runtime: IAgentRuntime, message: Memory) => {
67+
console.log("Validating sui transfer from user:", message.userId);
68+
//add custom validate logic here
69+
/*
70+
const adminIds = runtime.getSetting("ADMIN_USER_IDS")?.split(",") || [];
71+
//console.log("Admin IDs from settings:", adminIds);
72+
73+
const isAdmin = adminIds.includes(message.userId);
74+
75+
if (isAdmin) {
76+
//console.log(`Authorized transfer from user: ${message.userId}`);
77+
return true;
78+
}
79+
else
80+
{
81+
//console.log(`Unauthorized transfer attempt from user: ${message.userId}`);
82+
return false;
83+
}
84+
*/
85+
return true;
86+
},
87+
description: "Transfer tokens from the agent's wallet to another address",
88+
handler: async (
89+
runtime: IAgentRuntime,
90+
message: Memory,
91+
state: State,
92+
_options: { [key: string]: unknown },
93+
callback?: HandlerCallback
94+
): Promise<boolean> => {
95+
elizaLogger.log("Starting SEND_TOKEN handler...");
96+
97+
const walletInfo = await walletProvider.get(runtime, message, state);
98+
state.walletInfo = walletInfo;
99+
100+
// Initialize or update state
101+
if (!state) {
102+
state = (await runtime.composeState(message)) as State;
103+
} else {
104+
state = await runtime.updateRecentMessageState(state);
105+
}
106+
107+
// Define the schema for the expected output
108+
const transferSchema = z.object({
109+
recipient: z.string(),
110+
amount: z.union([z.string(), z.number()]),
111+
});
112+
113+
// Compose transfer context
114+
const transferContext = composeContext({
115+
state,
116+
template: transferTemplate,
117+
});
118+
119+
// Generate transfer content with the schema
120+
const content = await generateObjectV2({
121+
runtime,
122+
context: transferContext,
123+
schema: transferSchema,
124+
modelClass: ModelClass.SMALL,
125+
});
126+
127+
const transferContent = content.object as TransferContent;
128+
129+
// Validate transfer content
130+
if (!isTransferContent(transferContent)) {
131+
console.error("Invalid content for TRANSFER_TOKEN action.");
132+
if (callback) {
133+
callback({
134+
text: "Unable to process transfer request. Invalid content provided.",
135+
content: { error: "Invalid transfer content" },
136+
});
137+
}
138+
return false;
139+
}
140+
141+
try {
142+
const privateKey = runtime.getSetting("SUI_PRIVATE_KEY");
143+
const suiAccount = Ed25519Keypair.deriveKeypair(privateKey);
144+
const network = runtime.getSetting("SUI_NETWORK");
145+
const suiClient = new SuiClient({
146+
url: getFullnodeUrl(network as SuiNetwork),
147+
});
148+
149+
const adjustedAmount = BigInt(
150+
Number(transferContent.amount) * Math.pow(10, SUI_DECIMALS)
151+
);
152+
console.log(
153+
`Transferring: ${transferContent.amount} tokens (${adjustedAmount} base units)`
154+
);
155+
const tx = new Transaction();
156+
const [coin] = tx.splitCoins(tx.gas, [adjustedAmount]);
157+
tx.transferObjects([coin], transferContent.recipient);
158+
const executedTransaction =
159+
await suiClient.signAndExecuteTransaction({
160+
signer: suiAccount,
161+
transaction: tx,
162+
});
163+
164+
console.log("Transfer successful:", executedTransaction.digest);
165+
166+
if (callback) {
167+
callback({
168+
text: `Successfully transferred ${transferContent.amount} SUI to ${transferContent.recipient}, Transaction: ${executedTransaction.digest}`,
169+
content: {
170+
success: true,
171+
hash: executedTransaction.digest,
172+
amount: transferContent.amount,
173+
recipient: transferContent.recipient,
174+
},
175+
});
176+
}
177+
178+
return true;
179+
} catch (error) {
180+
console.error("Error during token transfer:", error);
181+
if (callback) {
182+
callback({
183+
text: `Error transferring tokens: ${error.message}`,
184+
content: { error: error.message },
185+
});
186+
}
187+
return false;
188+
}
189+
},
190+
191+
examples: [
192+
[
193+
{
194+
user: "{{user1}}",
195+
content: {
196+
text: "Send 1 SUI tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0",
197+
},
198+
},
199+
{
200+
user: "{{user2}}",
201+
content: {
202+
text: "I'll send 1 SUI tokens now...",
203+
action: "SEND_TOKEN",
204+
},
205+
},
206+
{
207+
user: "{{user2}}",
208+
content: {
209+
text: "Successfully sent 1 SUI tokens to 0x4f2e63be8e7fe287836e29cde6f3d5cbc96eefd0c0e3f3747668faa2ae7324b0, Transaction: 0x39a8c432d9bdad993a33cc1faf2e9b58fb7dd940c0425f1d6db3997e4b4b05c0",
210+
},
211+
},
212+
],
213+
] as ActionExample[][],
214+
} as Action;

packages/plugin-sui/src/enviroment.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { IAgentRuntime } from "@ai16z/eliza";
2+
import { z } from "zod";
3+
4+
export const suiEnvSchema = z.object({
5+
SUI_PRIVATE_KEY: z.string().min(1, "Sui private key is required"),
6+
SUI_NETWORK: z.enum(["mainnet", "testnet"]),
7+
});
8+
9+
export type SuiConfig = z.infer<typeof suiEnvSchema>;
10+
11+
export async function validateSuiConfig(
12+
runtime: IAgentRuntime
13+
): Promise<SuiConfig> {
14+
try {
15+
const config = {
16+
SUI_PRIVATE_KEY:
17+
runtime.getSetting("SUI_PRIVATE_KEY") ||
18+
process.env.SUI_PRIVATE_KEY,
19+
SUI_NETWORK:
20+
runtime.getSetting("SUI_NETWORK") ||
21+
process.env.SUI_NETWORK,
22+
};
23+
24+
return suiEnvSchema.parse(config);
25+
} catch (error) {
26+
if (error instanceof z.ZodError) {
27+
const errorMessages = error.errors
28+
.map((err) => `${err.path.join(".")}: ${err.message}`)
29+
.join("\n");
30+
throw new Error(
31+
`Sui configuration validation failed:\n${errorMessages}`
32+
);
33+
}
34+
throw error;
35+
}
36+
}

packages/plugin-sui/src/index.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Plugin } from "@ai16z/eliza";
2+
import transferToken from "./actions/transfer.ts";
3+
import { WalletProvider, walletProvider } from "./providers/wallet.ts";
4+
5+
export { WalletProvider, transferToken as TransferSuiToken };
6+
7+
export const suiPlugin: Plugin = {
8+
name: "sui",
9+
description: "Sui Plugin for Eliza",
10+
actions: [transferToken],
11+
evaluators: [],
12+
providers: [walletProvider],
13+
};
14+
15+
export default suiPlugin;

0 commit comments

Comments
 (0)