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

Added Transfer / Send Token Action #297

Merged
merged 2 commits into from
Nov 13, 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
246 changes: 246 additions & 0 deletions packages/plugin-solana/src/actions/transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { AnchorProvider } from "@coral-xyz/anchor";
import { Wallet } from "@coral-xyz/anchor";

import { getAssociatedTokenAddressSync, createTransferInstruction } from "@solana/spl-token";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes/index.js";
import settings from "@ai16z/eliza/src/settings.ts";

import {
Connection,
Keypair,
PublicKey,
TransactionMessage,
VersionedTransaction
} from "@solana/web3.js";


import {
ActionExample,
Content,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
type Action,
} from "@ai16z/eliza/src/types.ts";
import { composeContext } from "@ai16z/eliza/src/context.ts";
import { generateObject } from "@ai16z/eliza/src/generation.ts";

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

function isTransferContent(
runtime: IAgentRuntime,
content: any
): content is TransferContent {
console.log("Content for transfer", content);
return (
typeof content.tokenAddress === "string" &&
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
{
"tokenAddress": "BieefG47jAHCGZBxi2q87RDuHyGZyYC3vAzxpyu8pump",
"recipient": "9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa",
"amount": "1000"
}
\`\`\`

{{recentMessages}}

Given the recent messages, extract the following information about the requested token transfer:
- Token contract address
- 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_SOL", "PAY"],
validate: async (runtime: IAgentRuntime, message: Memory) => {
console.log("Validating 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<boolean> => {
console.log("Starting TRANSFER_TOKEN handler...");

// 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 generateObject({
runtime,
context: transferContext,
modelClass: ModelClass.SMALL,
});

// Validate transfer content
if (!isTransferContent(runtime, 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 privateKeyString = runtime.getSetting("WALLET_PRIVATE_KEY")!;
const secretKey = bs58.decode(privateKeyString);
const senderKeypair = Keypair.fromSecretKey(secretKey);

const connection = new Connection(settings.RPC_URL!);

const mintPubkey = new PublicKey(content.tokenAddress);
const recipientPubkey = new PublicKey(content.recipient);

// Get decimals (simplest way)
const mintInfo = await connection.getParsedAccountInfo(mintPubkey);
const decimals = (mintInfo.value?.data as any)?.parsed?.info?.decimals ?? 9;

// Adjust amount with decimals
const adjustedAmount = BigInt(Number(content.amount) * Math.pow(10, decimals));
console.log(`Transferring: ${content.amount} tokens (${adjustedAmount} base units)`);

// Rest of the existing working code...
const senderATA = getAssociatedTokenAddressSync(mintPubkey, senderKeypair.publicKey);
const recipientATA = getAssociatedTokenAddressSync(mintPubkey, recipientPubkey);

const instructions = [];

const recipientATAInfo = await connection.getAccountInfo(recipientATA);
if (!recipientATAInfo) {
const { createAssociatedTokenAccountInstruction } = await import("@solana/spl-token");
instructions.push(
createAssociatedTokenAccountInstruction(
senderKeypair.publicKey,
recipientATA,
recipientPubkey,
mintPubkey
)
);
}

instructions.push(
createTransferInstruction(
senderATA,
recipientATA,
senderKeypair.publicKey,
adjustedAmount
)
);



// Create and sign versioned transaction
const messageV0 = new TransactionMessage({
payerKey: senderKeypair.publicKey,
recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
instructions
}).compileToV0Message();

const transaction = new VersionedTransaction(messageV0);
transaction.sign([senderKeypair]);

// Send transaction
const signature = await connection.sendTransaction(transaction);

console.log("Transfer successful:", signature);

if (callback) {
callback({
text: `Successfully transferred ${content.amount} tokens to ${content.recipient}\nTransaction: ${signature}`,
content: {
success: true,
signature,
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 EZSIS BieefG47jAHCGZBxi2q87RDuHyGZyYC3vAzxpyu8pump to 9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa"
}
},
{
user: "{{user2}}",
content: {
text: "I'll send 69 EZSIS tokens now...",
action: "SEND_TOKEN"
}
},
{
user: "{{user2}}",
content: {
text: "Successfully sent 69 EZSIS tokens to 9jW8FPr6BSSsemWPV22UUCzSqkVdTp6HTyPqeqyuBbCa\nTransaction: 5KtPn3DXXzHkb7VAVHZGwXJQqww39ASnrf7YkyJoF2qAGEpBEEGvRHLnnTG8ZVwKqNHMqSckWVGnsQAgfH5pbxEb"
}
}
]
] as ActionExample[][]
} as Action;
22 changes: 13 additions & 9 deletions packages/plugin-solana/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Plugin } from "@ai16z/eliza/src/types.ts";
// import { executeSwap } from "./actions/swap.ts";
// import take_order from "./actions/takeOrder";
// import pumpfun from "./actions/pumpfun";
// import { executeSwapForDAO } from "./actions/swapDao";
//import { executeSwap } from "./actions/swap.ts";
//import take_order from "./actions/takeOrder";
//import pumpfun from "./actions/pumpfun.ts";
//import { executeSwapForDAO } from "./actions/swapDao";
//import transferToken from "./actions/transfer.ts";
import { walletProvider } from "./providers/wallet.ts";
import { trustScoreProvider } from "./providers/trustScoreProvider.ts";
import { trustEvaluator } from "./evaluators/trust.ts";
Expand All @@ -11,12 +12,15 @@ export const solanaPlugin: Plugin = {
name: "solana",
description: "Solana Plugin for Eliza",
actions: [
// executeSwap,
// pumpfun,
// executeSwapForDAO,
// take_order,
//executeSwap,
//pumpfun,
//transferToken,
//executeSwapForDAO,
//take_order,
],
evaluators: [
trustEvaluator
],
evaluators: [trustEvaluator],
providers: [walletProvider, trustScoreProvider],
};

Expand Down
Loading