Web3 MCP Server

import { Connection, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js'; import { Keypair } from '@solana/web3.js'; import fetch from 'cross-fetch'; import bs58 from 'bs58'; const JUPITER_API_BASE = 'https://quote-api.jup.ag/v6'; // Common token addresses const COMMON_TOKENS = { SOL: 'So11111111111111111111111111111111111111112', USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', ORCA: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', } as const; interface TokenMetadata { symbol: string; decimals: number; address: string; name: string; logoURI?: string; } // Types for Jupiter API responses interface JupiterQuoteResponse { inputMint: string; inAmount: string; outputMint: string; outAmount: string; otherAmountThreshold: string; swapMode: string; slippageBps: number; platformFee: { amount?: string; feeBps?: number; } | null; priceImpactPct: string; routePlan: Array<{ swapInfo: { ammKey: string; label?: string; inputMint: string; outputMint: string; inAmount: string; outAmount: string; feeAmount: string; feeMint: string; }; percent: number; }>; contextSlot?: number; timeTaken?: number; } interface JupiterSwapResponse { swapTransaction: string; lastValidBlockHeight: number; prioritizationFeeLamports: number; computeUnitLimit: number; } // Cache for token metadata const tokenMetadataCache = new Map<string, TokenMetadata>(); // Initialize token list async function initializeTokenList() { if (tokenMetadataCache.size > 0) return; try { const response = await fetch('https://token.jup.ag/strict'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); for (const token of data) { tokenMetadataCache.set(token.address, { symbol: token.symbol, decimals: token.decimals, address: token.address, name: token.name, logoURI: token.logoURI }); } // Ensure we have the common tokens ensureCommonTokens(); } catch (error) { console.error('Failed to fetch token list:', error); // Fallback to common tokens ensureCommonTokens(); } } // Ensure we have common tokens in cache function ensureCommonTokens() { // SOL if (!tokenMetadataCache.has(COMMON_TOKENS.SOL)) { tokenMetadataCache.set(COMMON_TOKENS.SOL, { symbol: 'SOL', decimals: 9, address: COMMON_TOKENS.SOL, name: 'Solana' }); } // USDC if (!tokenMetadataCache.has(COMMON_TOKENS.USDC)) { tokenMetadataCache.set(COMMON_TOKENS.USDC, { symbol: 'USDC', decimals: 6, address: COMMON_TOKENS.USDC, name: 'USD Coin' }); } // Add other common tokens as needed } // Get token metadata async function getTokenMetadata(mint: string): Promise<TokenMetadata> { await initializeTokenList(); const tokenInfo = tokenMetadataCache.get(mint); if (!tokenInfo) { throw new Error(`Token metadata not found for ${mint}`); } return tokenInfo; } // Format amount based on decimals function formatTokenAmount(amount: string, decimals: number): string { const value = parseInt(amount) / Math.pow(10, decimals); return value.toFixed(decimals).replace(/\.?0+$/, ''); } // Get quote from Jupiter export async function getJupiterQuote( inputMint: string, outputMint: string, amount: string, slippageBps: number = 50 ): Promise<JupiterQuoteResponse> { // Initialize token list before getting quote await initializeTokenList(); // Clean input parameters inputMint = inputMint.toString().trim(); outputMint = outputMint.toString().trim(); amount = amount.toString().trim(); // Validate addresses try { new PublicKey(inputMint); new PublicKey(outputMint); } catch (error: any) { throw new Error(`Invalid token address: ${error?.message || 'Invalid format'}`); } const url = new URL('/quote', JUPITER_API_BASE); url.searchParams.set('inputMint', inputMint); url.searchParams.set('outputMint', outputMint); url.searchParams.set('amount', amount); url.searchParams.set('slippageBps', slippageBps.toString()); url.searchParams.set('onlyDirectRoutes', 'false'); url.searchParams.set('restrictIntermediateTokens', 'true'); console.log('Fetching quote from:', url.toString()); const response = await fetch(url.toString()); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to get Jupiter quote: ${error}`); } return response.json(); } // Build swap transaction export async function buildJupiterSwapTransaction( quoteResponse: JupiterQuoteResponse, userPublicKey: string ): Promise<string> { const url = new URL('/swap', JUPITER_API_BASE); const body = { quoteResponse, userPublicKey, dynamicComputeUnitLimit: true, dynamicSlippage: true, prioritizationFeeLamports: { priorityLevelWithMaxLamports: { maxLamports: 1000000, priorityLevel: "veryHigh" } } }; const response = await fetch(url.toString(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (!response.ok) { const error = await response.text(); throw new Error(`Failed to build swap transaction: ${error}`); } const { swapTransaction } = await response.json(); return swapTransaction; } // Execute swap transaction export async function executeJupiterSwap( connection: Connection, swapTransaction: string, privateKey: Uint8Array ): Promise<string> { try { const keypair = Keypair.fromSecretKey(privateKey); // Convert base64 transaction to Uint8Array const transactionBinary = Buffer.from(swapTransaction, 'base64'); // Deserialize and sign the transaction const transaction = VersionedTransaction.deserialize(transactionBinary); transaction.sign([keypair]); // Send the transaction with optimized parameters const signature = await connection.sendRawTransaction( transaction.serialize(), { skipPreflight: true, maxRetries: 2, preflightCommitment: 'confirmed' } ); // Wait for confirmation await connection.confirmTransaction(signature, 'confirmed'); return signature; } catch (error) { console.error('Error executing swap:', error); throw error; } } // Format quote details for display export async function formatQuoteDetails(quote: JupiterQuoteResponse): Promise<string> { const [inputToken, outputToken] = await Promise.all([ getTokenMetadata(quote.inputMint), getTokenMetadata(quote.outputMint) ]); const inputAmount = formatTokenAmount(quote.inAmount, inputToken.decimals); const outputAmount = formatTokenAmount(quote.outAmount, outputToken.decimals); const routeSteps = quote.routePlan .map(r => r.swapInfo.label || 'Unknown AMM') .join(' → '); // Calculate rate const inAmountNum = parseFloat(quote.inAmount) / Math.pow(10, inputToken.decimals); const outAmountNum = parseFloat(quote.outAmount) / Math.pow(10, outputToken.decimals); const rate = outAmountNum / inAmountNum; return `Swap Quote Details: Input: ${inputAmount} ${inputToken.symbol} Output: ${outputAmount} ${outputToken.symbol} Rate: 1 ${inputToken.symbol} = ${rate.toFixed(6)} ${outputToken.symbol} Price Impact: ${quote.priceImpactPct}% Slippage Tolerance: ${quote.slippageBps / 100}% Route: ${routeSteps}`; }