jupiter dex aggregator integration

This commit is contained in:
Alex Cheng
2025-02-11 13:21:54 -06:00
parent 20b44f3b6a
commit bf0060f2eb
4 changed files with 462 additions and 217 deletions

View File

@@ -5,13 +5,12 @@ A Model-Context-Protocol server for interacting with multiple blockchains includ
## Features
Solana Operations:
- Get current slot number
- Check SOL account balances
- Get detailed account information
- Display keypair information
- Transfer SOL between accounts (using private key from .env)
- Transfer SOL between accounts
- View SPL token balances
- Get detailed SPL token account information
- Get your wallet address from private key
- Swap tokens using Jupiter (Best price routing across all Solana DEXs)
Ethereum & EVM Chain Operations:
- Check native token balances across multiple networks
@@ -82,17 +81,18 @@ npm run build
Ask Claude:
### Solana Operations
- "What's the latest slot on Solana?"
- "What's my Solana address?" - Shows your address derived from private key in .env
- "What's the balance of 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3?"
- "Transfer 0.001 SOL to Cg6cVS4tjkxHthm3K9BHhmvqF7kSz8GnXqqYXnHBzGXd"
- "Show me my SPL token balances for address 62QXuWZ3WT6ws1ZFxJobVDVXn6bEsiYpLo5yG612U6u3"
- "Show me my SPL token balances"
- "Swap 0.1 SOL to USDC" (Uses Jupiter for best price routing)
### EVM Operations
- "What's the ETH balance of 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a?"
- "Check the USDC balance for 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a on Ethereum"
- "Send 0.001 ETH to 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a"
- "What's the ETH balance of 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933?"
- "Check the USDC balance for 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Ethereum"
- "Send 0.1 ETH to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933"
- "What's the current gas price on Arbitrum?"
- "Send 100 USDC to 0x6b96f801ae7f7112e659181a84d2e22ebd0bc72a on Polygon"
- "Send 100 USDC to 0x556437c4d22ceaeeebf82006b85bdcc0ae67d933 on Polygon"
## Security Notes
@@ -114,7 +114,7 @@ For EVM operations, you can specify the network by name (ethereum, base, arbitru
To modify or extend the tool:
1. Source code is in the `src` directory
2. Chain-specific code is in `src/chains`
2. Chain-specific code in `src/chains`
3. Run `npm run build` after making changes
4. Use TypeScript for all new code
@@ -124,4 +124,4 @@ Contributions are welcome! Please submit pull requests with any improvements or
## License
Apache-2.0 License
ISC License

View File

@@ -46,12 +46,15 @@
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@jup-ag/api": "^6.0.12",
"@modelcontextprotocol/sdk": "^1.4.1",
"@solana/spl-token": "^0.4.1",
"@solana/web3.js": "^1.87.6",
"bs58": "^5.0.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.4.5",
"ethers": "^6.11.1",
"fetch-retry": "^5.0.6",
"zod": "^3.24.1"
},
"devDependencies": {

277
src/chains/jupiter.ts Normal file
View File

@@ -0,0 +1,277 @@
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}`;
}

View File

@@ -17,6 +17,7 @@ import {
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import bs58 from 'bs58';
import { getJupiterQuote, buildJupiterSwapTransaction, executeJupiterSwap, formatQuoteDetails } from './jupiter.js';
const SOLANA_RPC = process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com";
@@ -49,6 +50,13 @@ async function getTokenAccounts(walletAddress: string) {
}
}
interface SwapParams {
inputMint: string;
outputMint: string;
amount: string;
slippageBps?: number;
}
export function registerSolanaTools(server: McpServer) {
// Debug: Check environment variables
console.error('Debug - ENV vars available:', Object.keys(process.env));
@@ -58,17 +66,35 @@ export function registerSolanaTools(server: McpServer) {
}
server.tool(
"getSlot",
"Get the current slot",
"getMyAddress",
"Get your Solana public address from private key in .env",
{},
async () => {
try {
const slot = await solanaConnection.getSlot();
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);
const publicKey = keypair.publicKey.toString();
// Get SOL balance
const balance = await solanaConnection.getBalance(keypair.publicKey);
const solBalance = balance / LAMPORTS_PER_SOL;
// Get token balances
const tokenAccounts = await getTokenAccounts(publicKey);
const tokenBalances = tokenAccounts
.filter(account => account.amount > 0)
.map(account => `${account.amount} (Mint: ${account.mint.toString()})`)
.join('\n');
return {
content: [
{
type: "text",
text: `Current slot: ${slot}`,
text: `Your Solana Address: ${publicKey}\nSOL Balance: ${solBalance} SOL\n\nToken Balances:\n${tokenBalances}`,
},
],
};
@@ -78,7 +104,7 @@ export function registerSolanaTools(server: McpServer) {
content: [
{
type: "text",
text: `Failed to retrieve current slot: ${error.message}`,
text: `Failed to get address: ${error.message}`,
},
],
};
@@ -120,54 +146,6 @@ export function registerSolanaTools(server: McpServer) {
}
);
server.tool(
"getKeypairInfo",
"Get information about a keypair from its secret key",
{
secretKey: z.string().describe("Base58 encoded secret key or array of bytes"),
},
async ({ secretKey }) => {
try {
let keypair: Keypair;
try {
const decoded = Uint8Array.from(secretKey.split(',').map(num => parseInt(num.trim())));
keypair = Keypair.fromSecretKey(decoded);
} catch {
keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(secretKey)));
}
const publicKey = keypair.publicKey;
const balance = await solanaConnection.getBalance(publicKey);
const accountInfo = await solanaConnection.getAccountInfo(publicKey);
return {
content: [
{
type: "text",
text: `Keypair Information:
Public Key: ${publicKey.toBase58()}
Balance: ${balance / LAMPORTS_PER_SOL} SOL
Account Program Owner: ${accountInfo?.owner?.toBase58() || 'N/A'}
Account Size: ${accountInfo?.data.length || 0} bytes
Is Executable: ${accountInfo?.executable || false}
Rent Epoch: ${accountInfo?.rentEpoch || 0}`,
},
],
};
} catch (err) {
const error = err as Error;
return {
content: [
{
type: "text",
text: `Failed to retrieve keypair information: ${error.message}`,
},
],
};
}
}
);
server.tool(
"getAccountInfo",
"Get detailed account information for a Solana address",
@@ -231,120 +209,6 @@ Data (${encoding}): ${formattedData}`,
}
);
server.tool(
"transfer",
"Transfer SOL from your keypair (using private key from .env) to another address",
{
toAddress: z.string().describe("Destination wallet address"),
amount: z.number().positive().describe("Amount of SOL to send"),
},
async ({ toAddress, amount }) => {
try {
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
let fromKeypair: Keypair;
try {
// Try to decode the private key from base58
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
fromKeypair = Keypair.fromSecretKey(new Uint8Array(privateKeyBytes));
// Verify the keypair is valid
if (!fromKeypair.publicKey) {
throw new Error('Invalid keypair generated');
}
} catch (error) {
console.error('Error decoding private key:', error);
throw new Error('Failed to create keypair from private key. Please ensure the private key is a valid base58-encoded string.');
}
// Convert SOL to lamports
const lamports = amount * LAMPORTS_PER_SOL;
// Validate destination address
let destinationPubkey: PublicKey;
try {
destinationPubkey = new PublicKey(toAddress);
} catch (error) {
throw new Error('Invalid destination address');
}
// Build transaction
const transaction = new Transaction();
// Get recent blockhash
const { blockhash } = await solanaConnection.getLatestBlockhash('finalized');
transaction.recentBlockhash = blockhash;
transaction.feePayer = fromKeypair.publicKey;
// Add transfer instruction
transaction.add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: destinationPubkey,
lamports,
})
);
// Sign and send transaction
try {
const signature = await sendAndConfirmTransaction(
solanaConnection,
transaction,
[fromKeypair],
{ commitment: 'confirmed' }
);
// Verify the transaction was successful
const confirmedTransaction = await solanaConnection.getTransaction(signature, {
commitment: 'confirmed',
});
if (!confirmedTransaction?.meta?.err) {
return {
content: [
{
type: "text",
text: `Transfer successful!
From: ${fromKeypair.publicKey.toBase58()}
To: ${toAddress}
Amount: ${amount} SOL
Transaction signature: ${signature}
Explorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} else {
throw new Error('Transaction failed on-chain');
}
} catch (err) {
console.error('Transaction error:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
return {
content: [
{
type: "text",
text: `Failed to transfer SOL: ${errorMessage}. Please check your wallet has sufficient SOL for the transfer and transaction fees.`,
},
],
};
}
} catch (err) {
console.error('Setup error:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
return {
content: [
{
type: "text",
text: `Failed to setup transfer: ${errorMessage}`,
},
],
};
}
}
);
server.tool(
"getSplTokenBalances",
"Get SPL token balances for a Solana address",
@@ -394,52 +258,153 @@ Explorer URL: https://explorer.solana.com/tx/${signature}`,
);
server.tool(
"getSplTokenInfo",
"Get detailed information about a specific SPL token account",
"getSwapQuote",
"Get best swap quote from Jupiter DEX aggregator",
{
tokenMint: z.string().describe("Token mint address"),
ownerAddress: z.string().describe("Token account owner address"),
inputMint: z.string().describe("Input token mint address"),
outputMint: z.string().describe("Output token mint address"),
amount: z.string().describe("Amount of input tokens (in smallest denomination)"),
slippageBps: z.number().optional().describe("Slippage tolerance in basis points (optional, default 50 = 0.5%)"),
},
async ({ tokenMint, ownerAddress }) => {
async ({ inputMint, outputMint, amount, slippageBps }: SwapParams) => {
try {
const mint = new PublicKey(tokenMint);
const owner = new PublicKey(ownerAddress);
// Validate input parameters
inputMint = inputMint.trim();
outputMint = outputMint.trim();
amount = amount.toString().trim();
const tokenAccounts = await solanaConnection.getParsedTokenAccountsByOwner(
owner,
{
mint: mint,
}
);
const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps);
const formattedDetails = await formatQuoteDetails(quote);
if (tokenAccounts.value.length === 0) {
return {
content: [
{
type: "text",
text: `No token account found for mint ${tokenMint} owned by ${ownerAddress}`,
text: formattedDetails,
},
],
quote, // Store quote for subsequent swap
};
} catch (err) {
console.error('Error getting swap quote:', err);
return {
content: [
{
type: "text",
text: `Failed to get swap quote: ${err instanceof Error ? err.message : 'Unknown error'}`,
},
],
};
}
}
);
const accountInfo = tokenAccounts.value[0].account;
const parsedInfo = accountInfo.data.parsed.info;
server.tool(
"executeSwap",
"Execute a token swap using Jupiter DEX aggregator (using private key from .env)",
{
inputMint: z.string().describe("Input token mint address"),
outputMint: z.string().describe("Output token mint address"),
amount: z.string().describe("Amount of input tokens (in smallest denomination)"),
slippageBps: z.number().optional().describe("Slippage tolerance in basis points (optional, default 50 = 0.5%)"),
},
async ({ inputMint, outputMint, amount, slippageBps }: SwapParams) => {
try {
// Validate input parameters
inputMint = inputMint.trim();
outputMint = outputMint.trim();
amount = amount.toString().trim();
// Check for private key
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
// Decode private key
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
const keypair = Keypair.fromSecretKey(privateKeyBytes);
// Get quote
const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps);
const formattedQuote = await formatQuoteDetails(quote);
// Build transaction
const swapTransaction = await buildJupiterSwapTransaction(
quote,
keypair.publicKey.toString()
);
// Execute swap
const signature = await executeJupiterSwap(
solanaConnection,
swapTransaction,
privateKeyBytes
);
return {
content: [
{
type: "text",
text: `Token Account Information:
Token Account: ${tokenAccounts.value[0].pubkey.toString()}
Mint: ${parsedInfo.mint}
Owner: ${parsedInfo.owner}
Balance: ${parsedInfo.tokenAmount.uiAmount}
Decimals: ${parsedInfo.tokenAmount.decimals}
State: ${parsedInfo.state}
Is Native: ${parsedInfo.isNative}
Delegation: ${parsedInfo.delegate || 'None'}
Close Authority: ${parsedInfo.closeAuthority || 'None'}`,
text: `${formattedQuote}\n\nSwap executed successfully!\nTransaction signature: ${signature}\nExplorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
} catch (err) {
console.error('Error executing swap:', err);
return {
content: [
{
type: "text",
text: `Failed to execute swap: ${err instanceof Error ? err.message : 'Unknown error'}`,
},
],
};
}
}
);
server.tool(
"transfer",
"Transfer SOL from your keypair (using private key from .env) to another address",
{
toAddress: z.string().describe("Destination wallet address"),
amount: z.number().positive().describe("Amount of SOL to send"),
},
async ({ toAddress, amount }) => {
try {
if (!process.env.SOLANA_PRIVATE_KEY) {
throw new Error('SOLANA_PRIVATE_KEY not found in environment variables');
}
const privateKeyBytes = bs58.decode(process.env.SOLANA_PRIVATE_KEY);
const fromKeypair = Keypair.fromSecretKey(privateKeyBytes);
const lamports = amount * LAMPORTS_PER_SOL;
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: fromKeypair.publicKey,
toPubkey: new PublicKey(toAddress),
lamports,
})
);
const signature = await sendAndConfirmTransaction(
solanaConnection,
transaction,
[fromKeypair]
);
return {
content: [
{
type: "text",
text: `Transfer successful!
From: ${fromKeypair.publicKey.toBase58()}
To: ${toAddress}
Amount: ${amount} SOL
Transaction signature: ${signature}
Explorer URL: https://explorer.solana.com/tx/${signature}`,
},
],
};
@@ -449,7 +414,7 @@ Close Authority: ${parsedInfo.closeAuthority || 'None'}`,
content: [
{
type: "text",
text: `Failed to retrieve token information: ${error.message}`,
text: `Failed to transfer SOL: ${error.message}`,
},
],
};