-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #297 from o-on-x/main
Added Transfer / Send Token Action
- Loading branch information
Showing
2 changed files
with
259 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters