Overview
Solana Swap Execution - Alpha ModeSwap execution on Solana is currently in alpha mode. The feature is functional but may have limitations. EVM chains are fully stable and production-ready. Please report any Solana-related issues to the Mobula team.
This guide walks you through the complete process of executing a token swap using Mobula’s Swap API. You’ll learn how to:
- Get an optimized swap quote
- Sign the transaction with your wallet
- Broadcast the transaction to the blockchain
- Monitor the transaction status
The Mobula Swap API provides optimal routing across multiple DEXs and supports both EVM chains (Ethereum, BSC, Base, etc.) and Solana.
Prerequisites
- Node.js 18+ installed
- A Mobula API key (get one at admin.mobula.io)
- Basic knowledge of TypeScript/JavaScript
- A wallet with funds on the blockchain you want to swap on
Installation
First, install the required dependencies:
npm install @solana/web3.js @solana/wallet-adapter-base ethers
Complete Swap Flow
Step 1: Get a Swap Quote
The first step is to request a swap quote. The API will return the estimated output amount and a serialized transaction ready to be signed.
interface SwapQuoteParams {
chainId: string;
tokenIn: string;
tokenOut: string;
amount: string;
walletAddress: string;
slippage?: string;
onlyProtocols?: string;
excludedProtocols?: string;
}
interface SwapQuoteResponse {
data: {
transaction?: {
serialized: string;
type: 'legacy' | 'versioned';
};
estimatedAmountOut?: string;
estimatedSlippage?: number;
requestId: string;
};
error?: string;
}
async function getSwapQuote(params: SwapQuoteParams): Promise<SwapQuoteResponse> {
const queryParams = new URLSearchParams({
chainId: params.chainId,
tokenIn: params.tokenIn,
tokenOut: params.tokenOut,
amount: params.amount,
walletAddress: params.walletAddress,
slippage: params.slippage || '1',
});
if (params.onlyProtocols) {
queryParams.append('onlyProtocols', params.onlyProtocols);
}
if (params.excludedProtocols) {
queryParams.append('excludedProtocols', params.excludedProtocols);
}
const response = await fetch(
`https://api.mobula.io/api/2/swap/quoting?${queryParams}`,
{
headers: {
'Authorization': `Bearer ${process.env.MOBULA_API_KEY}`,
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
Step 2: Sign the Transaction
Once you have the quote, you need to sign the transaction with your wallet. The process differs between Solana and EVM chains.
Solana Transaction Signing
import { VersionedTransaction, Transaction } from '@solana/web3.js';
import type { WalletAdapter } from '@solana/wallet-adapter-base';
async function signSolanaTransaction(
serializedTransaction: string,
transactionType: 'legacy' | 'versioned',
wallet: WalletAdapter
): Promise<string> {
// Decode the base64 transaction
const transactionBuffer = Buffer.from(serializedTransaction, 'base64');
if (transactionType === 'versioned') {
// Handle versioned transaction (most common for Solana)
const transaction = VersionedTransaction.deserialize(transactionBuffer);
// Sign the transaction with the wallet
const signedTx = await wallet.signTransaction(transaction);
// Serialize the signed transaction back to base64
return Buffer.from(signedTx.serialize()).toString('base64');
} else {
// Handle legacy transaction
const transaction = Transaction.from(transactionBuffer);
// Sign the transaction
const signedTx = await wallet.signTransaction(transaction);
// Serialize and encode
return Buffer.from(
signedTx.serialize({ requireAllSignatures: false })
).toString('base64');
}
}
EVM Transaction Signing
import { ethers } from 'ethers';
async function signEvmTransaction(
serializedTransaction: string,
wallet: ethers.Wallet
): Promise<string> {
// Decode the transaction
const transactionBuffer = Buffer.from(serializedTransaction, 'base64');
// Parse the transaction
const transaction = ethers.utils.parseTransaction(transactionBuffer);
// Sign the transaction
const signedTx = await wallet.signTransaction({
to: transaction.to,
data: transaction.data,
value: transaction.value,
gasLimit: transaction.gasLimit,
gasPrice: transaction.gasPrice,
nonce: transaction.nonce,
chainId: transaction.chainId,
});
// Return as base64
return Buffer.from(ethers.utils.arrayify(signedTx)).toString('base64');
}
Step 3: Send the Signed Transaction
After signing, broadcast the transaction to the blockchain.
interface SwapSendParams {
chainId: string;
signedTransaction: string;
}
interface SwapSendResponse {
data: {
success: boolean;
transactionHash?: string;
requestId: string;
};
error?: string;
}
async function sendSwapTransaction(params: SwapSendParams): Promise<SwapSendResponse> {
const response = await fetch('https://api.mobula.io/api/2/swap/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.MOBULA_API_KEY}`,
},
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
Step 4: Monitor Transaction Status
After broadcasting, monitor the transaction on the blockchain.
import { Connection, PublicKey } from '@solana/web3.js';
import { ethers } from 'ethers';
// For Solana
async function waitForSolanaConfirmation(
signature: string,
connection: Connection,
maxAttempts: number = 30
): Promise<boolean> {
for (let i = 0; i < maxAttempts; i++) {
const status = await connection.getSignatureStatus(signature);
if (status.value?.confirmationStatus === 'confirmed' ||
status.value?.confirmationStatus === 'finalized') {
console.log(`✅ Transaction confirmed: ${signature}`);
return true;
}
if (status.value?.err) {
console.error(`❌ Transaction failed:`, status.value.err);
return false;
}
// Wait 2 seconds before checking again
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.warn(`⚠️ Transaction not confirmed after ${maxAttempts} attempts`);
return false;
}
// For EVM
async function waitForEvmConfirmation(
txHash: string,
provider: ethers.providers.Provider,
confirmations: number = 1
): Promise<boolean> {
try {
const receipt = await provider.waitForTransaction(txHash, confirmations);
if (receipt.status === 1) {
console.log(`✅ Transaction confirmed: ${txHash}`);
return true;
} else {
console.error(`❌ Transaction failed: ${txHash}`);
return false;
}
} catch (error) {
console.error(`❌ Error waiting for transaction:`, error);
return false;
}
}
Complete Examples
Example 1: Swap SOL to USDC on Solana
import { Connection, PublicKey } from '@solana/web3.js';
import { WalletAdapter } from '@solana/wallet-adapter-base';
async function swapSolToUsdc(
wallet: WalletAdapter,
amountInSol: number,
slippagePercent: number = 1
) {
const connection = new Connection('https://api.mainnet-beta.solana.com');
console.log(`🔄 Starting swap: ${amountInSol} SOL → USDC`);
try {
// Step 1: Get quote
console.log('📊 Fetching quote...');
const quote = await getSwapQuote({
chainId: 'solana',
tokenIn: 'So11111111111111111111111111111111111111112', // SOL
tokenOut: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
amount: (amountInSol * 1e9).toString(), // Convert to lamports
walletAddress: wallet.publicKey!.toString(),
slippage: slippagePercent.toString(),
});
if (quote.error || !quote.data.transaction) {
throw new Error(quote.error || 'No transaction returned');
}
console.log(`💰 Estimated output: ${quote.data.estimatedAmountOut} USDC`);
console.log(`📉 Estimated slippage: ${quote.data.estimatedSlippage}%`);
// Step 2: Sign transaction
console.log('✍️ Signing transaction...');
const signedTx = await signSolanaTransaction(
quote.data.transaction.serialized,
quote.data.transaction.type,
wallet
);
// Step 3: Send transaction
console.log('📤 Broadcasting transaction...');
const result = await sendSwapTransaction({
chainId: 'solana',
signedTransaction: signedTx,
});
if (!result.data.success || !result.data.transactionHash) {
throw new Error(result.error || 'Transaction failed');
}
console.log(`🔗 Transaction hash: ${result.data.transactionHash}`);
// Step 4: Wait for confirmation
console.log('⏳ Waiting for confirmation...');
const confirmed = await waitForSolanaConfirmation(
result.data.transactionHash,
connection
);
if (confirmed) {
console.log('🎉 Swap completed successfully!');
return result.data.transactionHash;
} else {
throw new Error('Transaction not confirmed');
}
} catch (error) {
console.error('❌ Swap failed:', error);
throw error;
}
}
// Usage
const amountToSwap = 0.1; // 0.1 SOL
const maxSlippage = 1; // 1%
swapSolToUsdc(wallet, amountToSwap, maxSlippage)
.then(txHash => console.log(`Success! TX: ${txHash}`))
.catch(error => console.error('Failed:', error));
Example 2: Swap ETH to USDC on Ethereum
import { ethers } from 'ethers';
async function swapEthToUsdc(
wallet: ethers.Wallet,
amountInEth: number,
slippagePercent: number = 1
) {
const provider = wallet.provider;
console.log(`🔄 Starting swap: ${amountInEth} ETH → USDC`);
try {
// Step 1: Get quote
console.log('📊 Fetching quote...');
const quote = await getSwapQuote({
chainId: 'ethereum',
tokenIn: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', // ETH
tokenOut: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
amount: ethers.utils.parseEther(amountInEth.toString()).toString(),
walletAddress: wallet.address,
slippage: slippagePercent.toString(),
});
if (quote.error || !quote.data.transaction) {
throw new Error(quote.error || 'No transaction returned');
}
const estimatedUsdc = parseFloat(quote.data.estimatedAmountOut || '0') / 1e6;
console.log(`💰 Estimated output: ${estimatedUsdc.toFixed(2)} USDC`);
console.log(`📉 Estimated slippage: ${quote.data.estimatedSlippage}%`);
// Step 2: Sign transaction
console.log('✍️ Signing transaction...');
const signedTx = await signEvmTransaction(
quote.data.transaction.serialized,
wallet
);
// Step 3: Send transaction
console.log('📤 Broadcasting transaction...');
const result = await sendSwapTransaction({
chainId: 'ethereum',
signedTransaction: signedTx,
});
if (!result.data.success || !result.data.transactionHash) {
throw new Error(result.error || 'Transaction failed');
}
console.log(`🔗 Transaction hash: ${result.data.transactionHash}`);
// Step 4: Wait for confirmation
console.log('⏳ Waiting for confirmation...');
const confirmed = await waitForEvmConfirmation(
result.data.transactionHash,
provider!,
1
);
if (confirmed) {
console.log('🎉 Swap completed successfully!');
return result.data.transactionHash;
} else {
throw new Error('Transaction not confirmed');
}
} catch (error) {
console.error('❌ Swap failed:', error);
throw error;
}
}
// Usage with ethers wallet
const provider = new ethers.providers.JsonRpcProvider('https://eth.llamarpc.com');
const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);
const amountToSwap = 0.1; // 0.1 ETH
const maxSlippage = 1; // 1%
swapEthToUsdc(wallet, amountToSwap, maxSlippage)
.then(txHash => console.log(`Success! TX: ${txHash}`))
.catch(error => console.error('Failed:', error));
Example 3: Advanced Swap with Protocol Restrictions
async function advancedSwap(
wallet: WalletAdapter,
tokenIn: string,
tokenOut: string,
amount: string,
options: {
slippage?: number;
onlyProtocols?: string[];
excludedProtocols?: string[];
} = {}
) {
try {
// Get quote with advanced options
const quote = await getSwapQuote({
chainId: 'solana',
tokenIn,
tokenOut,
amount,
walletAddress: wallet.publicKey!.toString(),
slippage: options.slippage?.toString() || '1',
onlyProtocols: options.onlyProtocols?.join(','),
excludedProtocols: options.excludedProtocols?.join(','),
});
if (quote.error) {
throw new Error(quote.error);
}
console.log('Quote received:', {
estimatedOut: quote.data.estimatedAmountOut,
slippage: quote.data.estimatedSlippage,
requestId: quote.data.requestId,
});
// ... continue with signing and sending
} catch (error) {
console.error('Swap error:', error);
throw error;
}
}
// Usage: Only use Raydium and Orca
advancedSwap(
wallet,
'So11111111111111111111111111111111111111112', // SOL
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
'100000000', // 0.1 SOL
{
slippage: 2,
onlyProtocols: ['raydium-amm', 'orca-whirlpool'],
}
);
// Usage: Exclude specific protocol
advancedSwap(
wallet,
'So11111111111111111111111111111111111111112',
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'100000000',
{
excludedProtocols: ['0xSomeFactoryAddress'],
}
);
Error Handling Best Practices
enum SwapErrorType {
QUOTE_FAILED = 'QUOTE_FAILED',
SIGNING_FAILED = 'SIGNING_FAILED',
BROADCAST_FAILED = 'BROADCAST_FAILED',
CONFIRMATION_TIMEOUT = 'CONFIRMATION_TIMEOUT',
INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
SLIPPAGE_EXCEEDED = 'SLIPPAGE_EXCEEDED',
}
class SwapError extends Error {
constructor(
public type: SwapErrorType,
message: string,
public originalError?: unknown
) {
super(message);
this.name = 'SwapError';
}
}
async function executeSwapWithErrorHandling(
params: SwapQuoteParams,
wallet: WalletAdapter
) {
try {
// Get quote
const quote = await getSwapQuote(params);
if (quote.error) {
if (quote.error.includes('slippage')) {
throw new SwapError(
SwapErrorType.SLIPPAGE_EXCEEDED,
'Slippage tolerance exceeded. Try increasing slippage or reducing amount.',
quote.error
);
}
if (quote.error.includes('liquidity')) {
throw new SwapError(
SwapErrorType.QUOTE_FAILED,
'Insufficient liquidity for this swap.',
quote.error
);
}
throw new SwapError(
SwapErrorType.QUOTE_FAILED,
`Failed to get quote: ${quote.error}`,
quote.error
);
}
if (!quote.data.transaction) {
throw new SwapError(
SwapErrorType.QUOTE_FAILED,
'No transaction returned from quote'
);
}
// Sign transaction
let signedTx: string;
try {
signedTx = await signSolanaTransaction(
quote.data.transaction.serialized,
quote.data.transaction.type,
wallet
);
} catch (error) {
throw new SwapError(
SwapErrorType.SIGNING_FAILED,
'Failed to sign transaction. User may have rejected.',
error
);
}
// Send transaction
const result = await sendSwapTransaction({
chainId: params.chainId,
signedTransaction: signedTx,
});
if (!result.data.success) {
throw new SwapError(
SwapErrorType.BROADCAST_FAILED,
result.error || 'Failed to broadcast transaction',
result.error
);
}
return {
success: true,
transactionHash: result.data.transactionHash,
estimatedAmountOut: quote.data.estimatedAmountOut,
};
} catch (error) {
if (error instanceof SwapError) {
// Handle specific swap errors
switch (error.type) {
case SwapErrorType.SLIPPAGE_EXCEEDED:
console.error('💥 Slippage too high. Consider:');
console.error(' - Increasing slippage tolerance');
console.error(' - Reducing swap amount');
console.error(' - Trying again in a few moments');
break;
case SwapErrorType.SIGNING_FAILED:
console.error('✍️ Transaction signing failed');
console.error(' - User may have rejected the transaction');
console.error(' - Check wallet connection');
break;
case SwapErrorType.BROADCAST_FAILED:
console.error('📡 Failed to broadcast transaction');
console.error(' - Check network connection');
console.error(' - Verify wallet has sufficient balance for fees');
break;
default:
console.error('❌ Swap error:', error.message);
}
throw error;
}
// Unknown error
console.error('❌ Unexpected error:', error);
throw new SwapError(
SwapErrorType.QUOTE_FAILED,
'An unexpected error occurred',
error
);
}
}
Important Notes
Token Addresses
- Native tokens: Use
0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for ETH, BNB, MATIC, etc.
- Solana native: Use
So11111111111111111111111111111111111111112 for SOL
- Token amounts: Always use the smallest unit (wei for EVM, lamports for Solana)
Amount Conversion
// EVM chains (18 decimals for most tokens)
const ethAmount = '1.5'; // 1.5 ETH
const amountInWei = ethers.utils.parseEther(ethAmount).toString();
// For tokens with different decimals
const usdcAmount = '100'; // 100 USDC
const amountInSmallestUnit = (parseFloat(usdcAmount) * 1e6).toString(); // USDC has 6 decimals
// Solana (9 decimals for most tokens)
const solAmount = 0.5; // 0.5 SOL
const amountInLamports = (solAmount * 1e9).toString();
Protocol Filtering
-
onlyProtocols: Restricts routing to specific tradable protocols. Only valid tradable pool types will be considered.
onlyProtocols: 'uniswap-v3,sushiswap-v2'
-
excludedProtocols: Excludes specific factory addresses from routing.
excludedProtocols: '0xfactoryAddress1,0xfactoryAddress2'
Slippage Recommendations
- Stablecoins: 0.1% - 0.5%
- Major tokens: 0.5% - 1%
- Low liquidity tokens: 2% - 5%
- Memecoins: 5% - 10%
Higher slippage increases success rate but may result in worse execution prices.
Troubleshooting
Common Issues
-
“Slippage too high”
- Increase slippage tolerance
- Reduce swap amount
- Wait for better market conditions
-
“No route found”
- Check token addresses are correct
- Verify tokens have liquidity on the chain
- Try without protocol restrictions
-
“Transaction simulation failed”
- Insufficient balance (including gas fees)
- Token approval may be needed (for non-native tokens)
- Slippage tolerance too low
-
Transaction not confirming
- Network congestion (especially on Ethereum)
- Gas price too low (EVM chains)
- Check blockchain explorer with transaction hash
Next Steps
Support
Need help? Join our community: