UNPKG

edwin-sdk

Version:

SDK for integrating AI agents with DeFi protocols

189 lines (158 loc) 7.4 kB
import { SupportedChain } from '../../core/types'; import { PublicKey, VersionedTransaction } from '@solana/web3.js'; import { SolanaWalletClient } from '../../core/wallets/solana_wallet'; import { SwapParameters } from './parameters'; import { InsufficientBalanceError } from '../../errors'; import { QuoteResponse } from '@jup-ag/api'; import edwinLogger from '../../utils/logger'; export interface JupiterQuoteParameters { inputMint: string; outputMint: string; amount: string | number; slippageBps?: number; } export class JupiterService { supportedChains: SupportedChain[] = ['solana']; TOKEN_LIST_URL = 'https://tokens.jup.ag/tokens?tags=verified'; JUPITER_QUOTE_API_URL = 'https://quote-api.jup.ag/v6'; private wallet: SolanaWalletClient; constructor(wallet: SolanaWalletClient) { this.wallet = wallet; } async swap(params: SwapParameters): Promise<string> { const { inputMint, outputMint, amount } = params; if (!inputMint || !outputMint || !amount) { throw new Error('Invalid swap params. Need: inputMint, outputMint, amount'); } const balance = await this.wallet.getBalance(inputMint); if (balance < Number(amount)) { throw new InsufficientBalanceError(Number(amount), balance, inputMint); } // 1. Get quote from Jupiter // Get token decimals and adjust amount const connection = this.wallet.getConnection(); const mintInfo = await connection.getParsedAccountInfo(new PublicKey(inputMint)); if (!mintInfo.value || !('parsed' in mintInfo.value.data)) { throw new Error('Could not fetch mint info'); } const decimals = mintInfo.value.data.parsed.info.decimals; const adjustedAmount = (Number(amount) * Math.pow(10, decimals)).toString(); // Cast adjusted amount to integer to avoid scientific notation const adjustedAmountInt = BigInt(Math.floor(Number(adjustedAmount))).toString(); const quoteParams = { inputMint, outputMint, amount: adjustedAmountInt }; const quote = await this.getQuote(quoteParams); // 2. Get swap transaction const { swapTransaction } = await this.getSerializedTransaction(quote, this.wallet.getAddress()); // 3. Deserialize the transaction const swapTransactionBuf = Buffer.from(swapTransaction, 'base64'); const transaction = VersionedTransaction.deserialize(swapTransactionBuf); // 4. Sign the transaction await this.wallet.signTransaction(transaction); // 5. Send the transaction const signature = await this.wallet.sendTransaction(connection, transaction, []); // Return the transaction signature return signature; } async getQuote(params: JupiterQuoteParameters): Promise<QuoteResponse> { const { inputMint, outputMint, amount, slippageBps = 50 } = params; const queryParams = new URLSearchParams({ inputMint, outputMint, amount: amount.toString(), slippageBps: slippageBps.toString(), }); try { const response = await fetch(`${this.JUPITER_QUOTE_API_URL}/quote?${queryParams}`); if (!response.ok) { const errorText = await response.text(); edwinLogger.error('Jupiter API error response:', errorText); throw new Error(`Jupiter API error: ${response.statusText}`); } const quote = await response.json(); return quote; } catch (error) { edwinLogger.error('Error getting quote from Jupiter:', error); throw new Error(`Jupiter API error: ${error}`); } } async getSerializedTransaction(quote: QuoteResponse, walletAddress: string): Promise<{ swapTransaction: string }> { try { const response = await fetch(`${this.JUPITER_QUOTE_API_URL}/swap`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ quoteResponse: quote, userPublicKey: walletAddress, dynamicComputeUnitLimit: true, prioritizationFeeLamports: 'auto', }), }); if (!response.ok) { const errorText = await response.text(); edwinLogger.error('Jupiter swap API error response:', errorText); throw new Error(`Jupiter swap API error: ${response.statusText}`); } const swapResult = await response.json(); if (!swapResult.swapTransaction) { throw new Error('No swap transaction returned from Jupiter API'); } return { swapTransaction: swapResult.swapTransaction }; } catch (error) { edwinLogger.error('Error getting swap transaction from Jupiter:', error); throw new Error(`Jupiter API error: ${error}`); } } /** * Get swap details from transaction hash * @param txHash The transaction hash/signature * @param outputMint The output token mint address * @returns Amount of output tokens received */ async getSwapDetailsFromTransaction(txHash: string, outputMint: string): Promise<number> { try { const connection = this.wallet.getConnection(); // Wait for confirmation if needed try { await connection.confirmTransaction(txHash); } catch (error) { edwinLogger.debug(`Transaction already confirmed or error waiting: ${error}`); // Continue even if we can't confirm again (might already be confirmed) } // Get the token balance change return await this.wallet.getTransactionTokenBalanceChange(txHash, outputMint); } catch (error) { edwinLogger.error('Error getting swap details from transaction:', error); throw new Error(`Failed to get swap details: ${error}`); } } /** * Gets the token mint address for a given ticker symbol * @param ticker The token ticker/symbol to lookup (case-sensitive) * @returns The mint address or null if not found */ async getTokenAddressFromTicker(ticker: string): Promise<string | null> { try { const response = await fetch(this.TOKEN_LIST_URL); if (!response.ok) { throw new Error(`Failed to fetch token list: ${response.statusText}`); } interface JupiterToken { symbol: string; address: string; } const tokens = (await response.json()) as JupiterToken[]; const hit = tokens.find(t => t.symbol.toUpperCase() === ticker.toUpperCase()); if (!hit) { throw new Error( `Token ${ticker} not found in Jupiter's verified token list. This token may exist but is not yet verified. Please find and verify the contract address manually.` ); } return hit.address; } catch (error) { console.error('Error fetching token address:', error); throw error; } } }