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-ton #1039

Merged
merged 2 commits 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 @@ -270,3 +270,7 @@ AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_S3_BUCKET=
AWS_S3_UPLOAD_PATH=

# Ton
TON_PRIVATE_KEY= # Ton Mnemonic Seed Phrase Join With Empty String
TON_RPC_URL= # ton rpc
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-ton": "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 @@ -43,6 +43,7 @@ import { createNodePlugin } from "@ai16z/plugin-node";
import { solanaPlugin } from "@ai16z/plugin-solana";
import { teePlugin, TEEMode } from "@ai16z/plugin-tee";
import { aptosPlugin, TransferAptosToken } from "@ai16z/plugin-aptos";
import { tonPlugin } from "@ai16z/plugin-ton";
import { flowPlugin } from "@ai16z/plugin-flow";
import Database from "better-sqlite3";
import fs from "fs";
Expand Down Expand Up @@ -478,6 +479,7 @@ export async function createAgent(
? flowPlugin
: null,
getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null,
getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null,
].filter(Boolean),
providers: [],
actions: [],
Expand Down
25 changes: 25 additions & 0 deletions packages/plugin-ton/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@ai16z/plugin-ton",
"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:*",
"bignumber": "1.1.0",
"bignumber.js": "9.1.2",
"node-cache": "5.1.2",
"@ton/ton": "15.1.0",
"@ton/crypto": "3.3.0",
"tsup": "8.3.5"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run"
},
"peerDependencies": {
"whatwg-url": "7.1.0"
}
}
220 changes: 220 additions & 0 deletions packages/plugin-ton/src/actions/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
elizaLogger,
composeContext,
Content,
HandlerCallback,
ModelClass,
generateObjectV2,
type IAgentRuntime,
type Memory,
type State,
} from "@ai16z/eliza";
import { z } from "zod";

import { initWalletProvider, WalletProvider, nativeWalletProvider } from "../providers/wallet";
import { internal } from "@ton/ton";

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": "EQCGScrZe1xbyWqWDvdI6mzP-GAcAWFv6ZXuaJOuSqemxku4",
"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 class TransferAction {
constructor(private walletProvider: WalletProvider) {}

async transfer(params: TransferContent): Promise<string> {
console.log(
`Transferring: ${params.amount} tokens to (${params.recipient})`
);

const walletClient = this.walletProvider.getWalletClient();
const contract = walletClient.open(this.walletProvider.wallet);

try {
// Create a transfer
const seqno: number = await contract.getSeqno();
const transfer = await contract.createTransfer({
seqno,
secretKey: this.walletProvider.keypair.secretKey,
messages: [internal({
value: params.amount.toString(),
to: params.recipient,
body: 'eliza ton wallet plugin',
})]
});

await contract.send(transfer);

// await this.waitForTransaction(seqno, contract);

return transfer.hash().toString('hex');

} catch (error) {
throw new Error(`Transfer failed: ${error.message}`);
}
}
}

const buildTransferDetails = async (
runtime: IAgentRuntime,
message: Memory,
state: State,
): Promise<TransferContent> => {
const walletInfo = await nativeWalletProvider.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;

return transferContent;
};

export default {
name: "SEND_TOKEN",
similes: [
"SEND_TOKENS",
"TOKEN_TRANSFER",
"MOVE_TOKENS",
"SEND_TON"
],
description: "Transfer tokens from the agent's wallet to another",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
options: any,
callback?: HandlerCallback
) => {
elizaLogger.log("Starting SEND_TOKEN handler...");

const transferDetails = await buildTransferDetails(
runtime,
message,
state,
);

// Validate transfer content
if (!isTransferContent(transferDetails)) {
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 walletProvider = await initWalletProvider(runtime);
const action = new TransferAction(walletProvider);
const hash = await action.transfer(transferDetails);

if (callback) {
callback({
text: `Successfully transferred ${transferDetails.amount} TON to ${transferDetails.recipient}, Transaction: ${hash}`,
content: {
success: true,
hash: hash,
amount: transferDetails.amount,
recipient: transferDetails.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;
}
},
template: transferTemplate,
validate: async (runtime: IAgentRuntime) => {
//console.log("Validating TON transfer from user:", message.userId);
return true;
},
examples: [
[
{
user: "{{user1}}",
content: {
text: "Send 1 TON tokens to EQCGScrZe1xbyWqWDvdI6mzP-GAcAWFv6ZXuaJOuSqemxku4",
action: "SEND_TOKENS",
},
},
{
user: "{{user2}}",
content: {
text: "I'll send 1 TON tokens now...",
action: "SEND_TOKENS",
},
},
{
user: "{{user2}}",
content: {
text: "Successfully sent 1 TON tokens to EQCGScrZe1xbyWqWDvdI6mzP-GAcAWFv6ZXuaJOuSqemxku4, Transaction: c8ee4a2c1bd070005e6cd31b32270aa461c69b927c3f4c28b293c80786f78b43",
},
},
],
],
};
36 changes: 36 additions & 0 deletions packages/plugin-ton/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 envSchema = z.object({
TON_PRIVATE_KEY: z.string().min(1, "Ton private key is required"),
TON_RPC_URL: z.string(),
});

export type EnvConfig = z.infer<typeof envSchema>;

export async function validateEnvConfig(
runtime: IAgentRuntime
): Promise<EnvConfig> {
try {
const config = {
TON_PRIVATE_KEY:
runtime.getSetting("TON_PRIVATE_KEY") ||
process.env.TON_PRIVATE_KEY,
TON_RPC_URL:
runtime.getSetting("TON_RPC_URL") ||
process.env.TON_RPC_URL,
};

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

export { WalletProvider, transferAction as TransferTonToken };

export const tonPlugin: Plugin = {
name: "ton",
description: "Ton Plugin for Eliza",
actions: [transferAction],
evaluators: [],
providers: [nativeWalletProvider],
};

export default tonPlugin;
Loading
Loading