jupiter dex aggregator integration
This commit is contained in:
24
README.md
24
README.md
@@ -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
|
||||
@@ -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
277
src/chains/jupiter.ts
Normal 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}`;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
if (tokenAccounts.value.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No token account found for mint ${tokenMint} owned by ${ownerAddress}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const accountInfo = tokenAccounts.value[0].account;
|
||||
const parsedInfo = accountInfo.data.parsed.info;
|
||||
const quote = await getJupiterQuote(inputMint, outputMint, amount, slippageBps);
|
||||
const formattedDetails = await formatQuoteDetails(quote);
|
||||
|
||||
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: 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'}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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: `${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}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user