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

Swap functionality #197

Merged
merged 5 commits into from
Nov 5, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ XAI_MODEL=
# For asking Claude stuff
ANTHROPIC_API_KEY=

WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY
WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY
WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY

BIRDEYE_API_KEY=
Expand Down
2 changes: 1 addition & 1 deletion core/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ XAI_MODEL=
# For asking Claude stuff
ANTHROPIC_API_KEY=

WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY
WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY
WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY

BIRDEYE_API_KEY=
Expand Down
294 changes: 248 additions & 46 deletions core/src/actions/swap.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,130 @@
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import { Connection, Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js";
import fetch from "cross-fetch";
import {
ActionExample,
IAgentRuntime,
Memory,
type Action,
State,
ModelClass,
HandlerCallback
} from "../core/types.ts";
import { walletProvider } from "../providers/wallet.ts";
import { composeContext } from "../core/context.ts";
import { generateObject, generateObjectArray } from "../core/generation.ts";
import { getTokenDecimals } from "./swapUtils.ts";
import settings from "../core/settings.ts";
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes/index.js";

async function swapToken(
connection: Connection,
walletPublicKey: PublicKey,
inputTokenSymbol: string,
outputTokenSymbol: string,
inputTokenCA: string,
outputTokenCA: string,
amount: number
): Promise<any> {
const quoteResponse = await fetch(
`https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenSymbol}&outputMint=${outputTokenSymbol}&amount=${amount * 10 ** 6}&slippageBps=50`
);
const quoteData = await quoteResponse.json();

const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
quoteResponse: quoteData.data,
try {
// Get the decimals for the input token
const decimals = inputTokenCA === settings.SOL_ADDRESS ? 9 :
await getTokenDecimals(connection, inputTokenCA);

console.log("Decimals:", decimals);

const adjustedAmount = amount * (10 ** decimals);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use BN to avoid issues here


console.log("Fetching quote with params:", {
inputMint: inputTokenCA,
outputMint: outputTokenCA,
amount: adjustedAmount
});

const quoteResponse = await fetch(
`https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenCA}&outputMint=${outputTokenCA}&amount=${adjustedAmount}&slippageBps=50`
);
const quoteData = await quoteResponse.json();

if (!quoteData || quoteData.error) {
console.error("Quote error:", quoteData);
throw new Error(`Failed to get quote: ${quoteData?.error || 'Unknown error'}`);
}

console.log("Quote received:", quoteData);

const swapRequestBody = {
quoteResponse: quoteData,
userPublicKey: walletPublicKey.toString(),
wrapAndUnwrapSol: true,
}),
});
computeUnitPriceMicroLamports: 1000,
dynamicComputeUnitLimit: true
};

console.log("Requesting swap with body:", swapRequestBody);

const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(swapRequestBody)
});

const swapData = await swapResponse.json();

if (!swapData || !swapData.swapTransaction) {
console.error("Swap error:", swapData);
throw new Error(`Failed to get swap transaction: ${swapData?.error || 'No swap transaction returned'}`);
}

console.log("Swap transaction received");
return swapData;

return await swapResponse.json();
} catch (error) {
console.error("Error in swapToken:", error);
throw error;
}
}

async function promptConfirmation(): Promise<boolean> {
// Implement your own confirmation logic here
// This is just a placeholder example
const confirmSwap = window.confirm("Confirm the token swap?");
return confirmSwap;

const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.

Example response:
\`\`\`json
{
"inputTokenSymbol": "SOL",
"outputTokenSymbol": "USDC",
"inputTokenCA": "So11111111111111111111111111111111111111112",
"outputTokenCA": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": 1.5
}
\`\`\`

{{recentMessages}}

Given the recent messages and wallet information below:

{{walletInfo}}

Extract the following information about the requested token swap:
- Input token symbol (the token being sold)
- Output token symbol (the token being bought)
- Input token contract address if provided
- Output token contract address if provided
- Amount to swap

Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema:
\`\`\`json
{
"inputTokenSymbol": string | null,
"outputTokenSymbol": string | null,
"inputTokenCA": string | null,
"outputTokenCA": string | null,
"amount": number | string | null
}
\`\`\``;

// if we get the token symbol but not the CA, check walet for matching token, and if we have, get the CA for it

// swapToken should took CA, not symbol

export const executeSwap: Action = {
name: "EXECUTE_SWAP",
Expand All @@ -52,10 +137,73 @@ export const executeSwap: Action = {
description: "Perform a token swap.",
handler: async (
runtime: IAgentRuntime,
message: Memory
message: Memory,
state: State,
_options: { [key: string]: unknown },
callback?: HandlerCallback
): Promise<boolean> => {
const { inputTokenSymbol, outputTokenSymbol, amount } = message.content;

// composeState
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

const walletInfo = await walletProvider.get(runtime, message, state);

state.walletInfo = walletInfo;

const swapContext = composeContext({
state,
template: swapTemplate,
});

const response = await generateObject({
runtime,
context: swapContext,
modelClass: ModelClass.LARGE,
});

console.log("Response:", response);

// Add SOL handling logic
if (response.inputTokenSymbol?.toUpperCase() === 'SOL') {
response.inputTokenCA = settings.SOL_ADDRESS;
}
if (response.outputTokenSymbol?.toUpperCase() === 'SOL') {
response.outputTokenCA = settings.SOL_ADDRESS;
}

// if both contract addresses are set, lets execute the swap
// TODO: try to resolve CA from symbol based on existing symbol in wallet
if (!response.inputTokenCA || !response.outputTokenCA) {
console.log("No contract addresses provided, skipping swap");
const responseMsg = {
text: "I need the contract addresses to perform the swap",
};
callback?.(responseMsg);
return true;
}

if (!response.amount) {
console.log("No amount provided, skipping swap");
const responseMsg = {
text: "I need the amount to perform the swap",
};
callback?.(responseMsg);
return true;
}

// TODO: if response amount is half, all, etc, semantically retrieve amount and return as number
if (!response.amount) {
console.log("Amount is not a number, skipping swap");
const responseMsg = {
text: "The amount must be a number",
};
callback?.(responseMsg);
return true;
}
try {
const connection = new Connection(
"https://api.mainnet-beta.solana.com"
Expand All @@ -64,40 +212,94 @@ export const executeSwap: Action = {
runtime.getSetting("WALLET_PUBLIC_KEY")
);

console.log("Wallet Public Key:", walletPublicKey);
console.log("inputTokenSymbol:", response.inputTokenCA);
console.log("outputTokenSymbol:", response.outputTokenCA);
console.log("amount:", response.amount);

const swapResult = await swapToken(
connection,
walletPublicKey,
inputTokenSymbol as string,
outputTokenSymbol as string,
amount as number
response.inputTokenCA as string,
response.outputTokenCA as string,
response.amount as number
);

console.log("Swap Quote:");
console.log(swapResult.quote);
console.log("Deserializing transaction...");
const transactionBuf = Buffer.from(swapResult.swapTransaction, "base64");
const transaction = VersionedTransaction.deserialize(transactionBuf);

console.log("Preparing to sign transaction...");
const privateKeyString = runtime.getSetting("WALLET_PRIVATE_KEY");

// Handle different private key formats
let secretKey: Uint8Array;
try {
// First try to decode as base58
secretKey = bs58.decode(privateKeyString);
} catch (e) {
try {
// If that fails, try base64
secretKey = Uint8Array.from(Buffer.from(privateKeyString, 'base64'));
} catch (e2) {
throw new Error('Invalid private key format');
}
}

const confirmSwap = await promptConfirmation();
if (!confirmSwap) {
console.log("Swap canceled by user");
return false;
// Verify the key length
if (secretKey.length !== 64) {
console.error("Invalid key length:", secretKey.length);
throw new Error(`Invalid private key length: ${secretKey.length}. Expected 64 bytes.`);
}

const transaction = Transaction.from(
Buffer.from(swapResult.swapTransaction, "base64")
);
const privateKey = runtime.getSetting("WALLET_PRIVATE_KEY");
const keypair = Keypair.fromSecretKey(
Uint8Array.from(Buffer.from(privateKey, "base64"))
);
transaction.sign(keypair);
console.log("Creating keypair...");
const keypair = Keypair.fromSecretKey(secretKey);

// Verify the public key matches what we expect
const expectedPublicKey = runtime.getSetting("WALLET_PUBLIC_KEY");
if (keypair.publicKey.toBase58() !== expectedPublicKey) {
throw new Error("Generated public key doesn't match expected public key");
}

const txid = await connection.sendRawTransaction(
transaction.serialize()
);
await connection.confirmTransaction(txid);
console.log("Signing transaction...");
transaction.sign([keypair]);

console.log("Sending transaction...");

const latestBlockhash = await connection.getLatestBlockhash();

const txid = await connection.sendTransaction(transaction, {
skipPreflight: false,
maxRetries: 3,
preflightCommitment: 'confirmed'
});

console.log("Transaction sent:", txid);

// Confirm transaction using the blockhash
const confirmation = await connection.confirmTransaction({
signature: txid,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
}, 'confirmed');

if (confirmation.value.err) {
throw new Error(`Transaction failed: ${confirmation.value.err}`);
}

if (confirmation.value.err) {
throw new Error(`Transaction failed: ${confirmation.value.err}`);
}

console.log("Swap completed successfully!");
console.log(`Transaction ID: ${txid}`);

const responseMsg = {
text: `Swap completed successfully! Transaction ID: ${txid}`,
};

callback?.(responseMsg);

return true;
} catch (error) {
console.error("Error during token swap:", error);
Expand Down
Loading