1
- import { Connection , Keypair , PublicKey , Transaction } from "@solana/web3.js" ;
1
+ import { Connection , Keypair , PublicKey , Transaction , VersionedTransaction } from "@solana/web3.js" ;
2
2
import fetch from "cross-fetch" ;
3
3
import {
4
4
ActionExample ,
5
5
IAgentRuntime ,
6
6
Memory ,
7
7
type Action ,
8
+ State ,
9
+ ModelClass ,
10
+ HandlerCallback
8
11
} 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" ;
9
18
10
19
async function swapToken (
11
20
connection : Connection ,
12
21
walletPublicKey : PublicKey ,
13
- inputTokenSymbol : string ,
14
- outputTokenSymbol : string ,
22
+ inputTokenCA : string ,
23
+ outputTokenCA : string ,
15
24
amount : number
16
25
) : 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 ,
29
55
userPublicKey : walletPublicKey . toString ( ) ,
30
56
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 ;
33
80
34
- return await swapResponse . json ( ) ;
81
+ } catch ( error ) {
82
+ console . error ( "Error in swapToken:" , error ) ;
83
+ throw error ;
84
+ }
35
85
}
36
86
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
42
122
}
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
43
128
44
129
export const executeSwap : Action = {
45
130
name : "EXECUTE_SWAP" ,
@@ -52,10 +137,73 @@ export const executeSwap: Action = {
52
137
description : "Perform a token swap." ,
53
138
handler : async (
54
139
runtime : IAgentRuntime ,
55
- message : Memory
140
+ message : Memory ,
141
+ state : State ,
142
+ _options : { [ key : string ] : unknown } ,
143
+ callback ?: HandlerCallback
56
144
) : 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
+ } ) ;
58
161
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
+ }
59
207
try {
60
208
const connection = new Connection (
61
209
"https://api.mainnet-beta.solana.com"
@@ -64,40 +212,94 @@ export const executeSwap: Action = {
64
212
runtime . getSetting ( "WALLET_PUBLIC_KEY" )
65
213
) ;
66
214
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
+
67
220
const swapResult = await swapToken (
68
221
connection ,
69
222
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
73
226
) ;
74
227
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
+ }
77
248
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.` ) ;
82
253
}
83
254
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
+ }
92
263
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
+ }
97
293
98
294
console . log ( "Swap completed successfully!" ) ;
99
295
console . log ( `Transaction ID: ${ txid } ` ) ;
100
296
297
+ const responseMsg = {
298
+ text : `Swap completed successfully! Transaction ID: ${ txid } ` ,
299
+ } ;
300
+
301
+ callback ?.( responseMsg ) ;
302
+
101
303
return true ;
102
304
} catch ( error ) {
103
305
console . error ( "Error during token swap:" , error ) ;
0 commit comments