Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/swap-functionality'
Browse files Browse the repository at this point in the history
Signed-off-by: MarcoMandar <[email protected]>
  • Loading branch information
MarcoMandar committed Nov 4, 2024
2 parents e4af25f + 57e2bb0 commit 923ea75
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 100 deletions.
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);

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

0 comments on commit 923ea75

Please sign in to comment.