Skip to content

Commit 923ea75

Browse files
committed
Merge remote-tracking branch 'upstream/swap-functionality'
Signed-off-by: MarcoMandar <malicemandar@gmail.com>
2 parents e4af25f + 57e2bb0 commit 923ea75

File tree

12 files changed

+366
-100
lines changed

12 files changed

+366
-100
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ XAI_MODEL=
102102
# For asking Claude stuff
103103
ANTHROPIC_API_KEY=
104104
105-
WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY
105+
WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY
106106
WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY
107107
108108
BIRDEYE_API_KEY=

core/.env.example

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ XAI_MODEL=
3030
# For asking Claude stuff
3131
ANTHROPIC_API_KEY=
3232

33-
WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY
33+
WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY
3434
WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY
3535

3636
BIRDEYE_API_KEY=

core/src/actions/swap.ts

+248-46
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,130 @@
1-
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
1+
import { Connection, Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js";
22
import fetch from "cross-fetch";
33
import {
44
ActionExample,
55
IAgentRuntime,
66
Memory,
77
type Action,
8+
State,
9+
ModelClass,
10+
HandlerCallback
811
} from "../core/types.ts";
12+
import { walletProvider } from "../providers/wallet.ts";
13+
import { composeContext } from "../core/context.ts";
14+
import { generateObject, generateObjectArray } from "../core/generation.ts";
15+
import { getTokenDecimals } from "./swapUtils.ts";
16+
import settings from "../core/settings.ts";
17+
import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes/index.js";
918

1019
async function swapToken(
1120
connection: Connection,
1221
walletPublicKey: PublicKey,
13-
inputTokenSymbol: string,
14-
outputTokenSymbol: string,
22+
inputTokenCA: string,
23+
outputTokenCA: string,
1524
amount: number
1625
): Promise<any> {
17-
const quoteResponse = await fetch(
18-
`https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenSymbol}&outputMint=${outputTokenSymbol}&amount=${amount * 10 ** 6}&slippageBps=50`
19-
);
20-
const quoteData = await quoteResponse.json();
21-
22-
const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", {
23-
method: "POST",
24-
headers: {
25-
"Content-Type": "application/json",
26-
},
27-
body: JSON.stringify({
28-
quoteResponse: quoteData.data,
26+
try {
27+
// Get the decimals for the input token
28+
const decimals = inputTokenCA === settings.SOL_ADDRESS ? 9 :
29+
await getTokenDecimals(connection, inputTokenCA);
30+
31+
console.log("Decimals:", decimals);
32+
33+
const adjustedAmount = amount * (10 ** decimals);
34+
35+
console.log("Fetching quote with params:", {
36+
inputMint: inputTokenCA,
37+
outputMint: outputTokenCA,
38+
amount: adjustedAmount
39+
});
40+
41+
const quoteResponse = await fetch(
42+
`https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenCA}&outputMint=${outputTokenCA}&amount=${adjustedAmount}&slippageBps=50`
43+
);
44+
const quoteData = await quoteResponse.json();
45+
46+
if (!quoteData || quoteData.error) {
47+
console.error("Quote error:", quoteData);
48+
throw new Error(`Failed to get quote: ${quoteData?.error || 'Unknown error'}`);
49+
}
50+
51+
console.log("Quote received:", quoteData);
52+
53+
const swapRequestBody = {
54+
quoteResponse: quoteData,
2955
userPublicKey: walletPublicKey.toString(),
3056
wrapAndUnwrapSol: true,
31-
}),
32-
});
57+
computeUnitPriceMicroLamports: 1000,
58+
dynamicComputeUnitLimit: true
59+
};
60+
61+
console.log("Requesting swap with body:", swapRequestBody);
62+
63+
const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", {
64+
method: "POST",
65+
headers: {
66+
"Content-Type": "application/json",
67+
},
68+
body: JSON.stringify(swapRequestBody)
69+
});
70+
71+
const swapData = await swapResponse.json();
72+
73+
if (!swapData || !swapData.swapTransaction) {
74+
console.error("Swap error:", swapData);
75+
throw new Error(`Failed to get swap transaction: ${swapData?.error || 'No swap transaction returned'}`);
76+
}
77+
78+
console.log("Swap transaction received");
79+
return swapData;
3380

34-
return await swapResponse.json();
81+
} catch (error) {
82+
console.error("Error in swapToken:", error);
83+
throw error;
84+
}
3585
}
3686

37-
async function promptConfirmation(): Promise<boolean> {
38-
// Implement your own confirmation logic here
39-
// This is just a placeholder example
40-
const confirmSwap = window.confirm("Confirm the token swap?");
41-
return confirmSwap;
87+
88+
const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
89+
90+
Example response:
91+
\`\`\`json
92+
{
93+
"inputTokenSymbol": "SOL",
94+
"outputTokenSymbol": "USDC",
95+
"inputTokenCA": "So11111111111111111111111111111111111111112",
96+
"outputTokenCA": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
97+
"amount": 1.5
98+
}
99+
\`\`\`
100+
101+
{{recentMessages}}
102+
103+
Given the recent messages and wallet information below:
104+
105+
{{walletInfo}}
106+
107+
Extract the following information about the requested token swap:
108+
- Input token symbol (the token being sold)
109+
- Output token symbol (the token being bought)
110+
- Input token contract address if provided
111+
- Output token contract address if provided
112+
- Amount to swap
113+
114+
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:
115+
\`\`\`json
116+
{
117+
"inputTokenSymbol": string | null,
118+
"outputTokenSymbol": string | null,
119+
"inputTokenCA": string | null,
120+
"outputTokenCA": string | null,
121+
"amount": number | string | null
42122
}
123+
\`\`\``;
124+
125+
// if we get the token symbol but not the CA, check walet for matching token, and if we have, get the CA for it
126+
127+
// swapToken should took CA, not symbol
43128

44129
export const executeSwap: Action = {
45130
name: "EXECUTE_SWAP",
@@ -52,10 +137,73 @@ export const executeSwap: Action = {
52137
description: "Perform a token swap.",
53138
handler: async (
54139
runtime: IAgentRuntime,
55-
message: Memory
140+
message: Memory,
141+
state: State,
142+
_options: { [key: string]: unknown },
143+
callback?: HandlerCallback
56144
): Promise<boolean> => {
57-
const { inputTokenSymbol, outputTokenSymbol, amount } = message.content;
145+
146+
// composeState
147+
if (!state) {
148+
state = (await runtime.composeState(message)) as State;
149+
} else {
150+
state = await runtime.updateRecentMessageState(state);
151+
}
152+
153+
const walletInfo = await walletProvider.get(runtime, message, state);
154+
155+
state.walletInfo = walletInfo;
156+
157+
const swapContext = composeContext({
158+
state,
159+
template: swapTemplate,
160+
});
58161

162+
const response = await generateObject({
163+
runtime,
164+
context: swapContext,
165+
modelClass: ModelClass.LARGE,
166+
});
167+
168+
console.log("Response:", response);
169+
170+
// Add SOL handling logic
171+
if (response.inputTokenSymbol?.toUpperCase() === 'SOL') {
172+
response.inputTokenCA = settings.SOL_ADDRESS;
173+
}
174+
if (response.outputTokenSymbol?.toUpperCase() === 'SOL') {
175+
response.outputTokenCA = settings.SOL_ADDRESS;
176+
}
177+
178+
// if both contract addresses are set, lets execute the swap
179+
// TODO: try to resolve CA from symbol based on existing symbol in wallet
180+
if (!response.inputTokenCA || !response.outputTokenCA) {
181+
console.log("No contract addresses provided, skipping swap");
182+
const responseMsg = {
183+
text: "I need the contract addresses to perform the swap",
184+
};
185+
callback?.(responseMsg);
186+
return true;
187+
}
188+
189+
if (!response.amount) {
190+
console.log("No amount provided, skipping swap");
191+
const responseMsg = {
192+
text: "I need the amount to perform the swap",
193+
};
194+
callback?.(responseMsg);
195+
return true;
196+
}
197+
198+
// TODO: if response amount is half, all, etc, semantically retrieve amount and return as number
199+
if (!response.amount) {
200+
console.log("Amount is not a number, skipping swap");
201+
const responseMsg = {
202+
text: "The amount must be a number",
203+
};
204+
callback?.(responseMsg);
205+
return true;
206+
}
59207
try {
60208
const connection = new Connection(
61209
"https://api.mainnet-beta.solana.com"
@@ -64,40 +212,94 @@ export const executeSwap: Action = {
64212
runtime.getSetting("WALLET_PUBLIC_KEY")
65213
);
66214

215+
console.log("Wallet Public Key:", walletPublicKey);
216+
console.log("inputTokenSymbol:", response.inputTokenCA);
217+
console.log("outputTokenSymbol:", response.outputTokenCA);
218+
console.log("amount:", response.amount);
219+
67220
const swapResult = await swapToken(
68221
connection,
69222
walletPublicKey,
70-
inputTokenSymbol as string,
71-
outputTokenSymbol as string,
72-
amount as number
223+
response.inputTokenCA as string,
224+
response.outputTokenCA as string,
225+
response.amount as number
73226
);
74227

75-
console.log("Swap Quote:");
76-
console.log(swapResult.quote);
228+
console.log("Deserializing transaction...");
229+
const transactionBuf = Buffer.from(swapResult.swapTransaction, "base64");
230+
const transaction = VersionedTransaction.deserialize(transactionBuf);
231+
232+
console.log("Preparing to sign transaction...");
233+
const privateKeyString = runtime.getSetting("WALLET_PRIVATE_KEY");
234+
235+
// Handle different private key formats
236+
let secretKey: Uint8Array;
237+
try {
238+
// First try to decode as base58
239+
secretKey = bs58.decode(privateKeyString);
240+
} catch (e) {
241+
try {
242+
// If that fails, try base64
243+
secretKey = Uint8Array.from(Buffer.from(privateKeyString, 'base64'));
244+
} catch (e2) {
245+
throw new Error('Invalid private key format');
246+
}
247+
}
77248

78-
const confirmSwap = await promptConfirmation();
79-
if (!confirmSwap) {
80-
console.log("Swap canceled by user");
81-
return false;
249+
// Verify the key length
250+
if (secretKey.length !== 64) {
251+
console.error("Invalid key length:", secretKey.length);
252+
throw new Error(`Invalid private key length: ${secretKey.length}. Expected 64 bytes.`);
82253
}
83254

84-
const transaction = Transaction.from(
85-
Buffer.from(swapResult.swapTransaction, "base64")
86-
);
87-
const privateKey = runtime.getSetting("WALLET_PRIVATE_KEY");
88-
const keypair = Keypair.fromSecretKey(
89-
Uint8Array.from(Buffer.from(privateKey, "base64"))
90-
);
91-
transaction.sign(keypair);
255+
console.log("Creating keypair...");
256+
const keypair = Keypair.fromSecretKey(secretKey);
257+
258+
// Verify the public key matches what we expect
259+
const expectedPublicKey = runtime.getSetting("WALLET_PUBLIC_KEY");
260+
if (keypair.publicKey.toBase58() !== expectedPublicKey) {
261+
throw new Error("Generated public key doesn't match expected public key");
262+
}
92263

93-
const txid = await connection.sendRawTransaction(
94-
transaction.serialize()
95-
);
96-
await connection.confirmTransaction(txid);
264+
console.log("Signing transaction...");
265+
transaction.sign([keypair]);
266+
267+
console.log("Sending transaction...");
268+
269+
const latestBlockhash = await connection.getLatestBlockhash();
270+
271+
const txid = await connection.sendTransaction(transaction, {
272+
skipPreflight: false,
273+
maxRetries: 3,
274+
preflightCommitment: 'confirmed'
275+
});
276+
277+
console.log("Transaction sent:", txid);
278+
279+
// Confirm transaction using the blockhash
280+
const confirmation = await connection.confirmTransaction({
281+
signature: txid,
282+
blockhash: latestBlockhash.blockhash,
283+
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
284+
}, 'confirmed');
285+
286+
if (confirmation.value.err) {
287+
throw new Error(`Transaction failed: ${confirmation.value.err}`);
288+
}
289+
290+
if (confirmation.value.err) {
291+
throw new Error(`Transaction failed: ${confirmation.value.err}`);
292+
}
97293

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

297+
const responseMsg = {
298+
text: `Swap completed successfully! Transaction ID: ${txid}`,
299+
};
300+
301+
callback?.(responseMsg);
302+
101303
return true;
102304
} catch (error) {
103305
console.error("Error during token swap:", error);

0 commit comments

Comments
 (0)