Skip to content

Commit

Permalink
Merge pull request #297 from o-on-x/main
Browse files Browse the repository at this point in the history
Added Transfer / Send Token Action
  • Loading branch information
o-on-x authored Nov 13, 2024
2 parents 8af7170 + d5d6798 commit b84caac
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 9 deletions.
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

0 comments on commit b84caac

Please sign in to comment.