DEX API

Build swap applications on Solana#

In this guide, we will provide a use case for Solana token exchange through the OKX DEX.

  • Set up your environment
  • Obtain the token account address for toTokenAddress
  • Obtain the exchange path
  • Deserialize and sign
  • Execute the transaction

1. Set up your environment#

Import the necessary Node.js libraries and set up your environment variables. Define helper functions and assembly parameters Node.js Environment Settings.

Additionally, you need to import the following libraries after completing the above steps.

const bs58 = require('bs58');
  const solanaWeb3 = require('@solana/web3.js');
  const {Connection} = require("@solana/web3.js");
  npm i bs58
  npm i @solana/web3.js

2. Obtain the exchange path and callData#

Tip
Solana's NativeTokenAddress is 11111111111111111111111111111111.
  • Use the /swap endpoint to retrieve detailed swap paths and callData.

Here is an example of swapping SOL to wSOL on the Solana chain:

curl --location --request GET 'https://www.okx.com/api/v5/dex/aggregator/swap?amount=1000&chainId=501&fromTokenAddress=11111111111111111111111111111111&toTokenAddress=So11111111111111111111111111111111111111112&userWalletAddress=3cUbuUEJkcgtzGxvsukksNzmgqaUK9jwFS5pqxxxxxxx&slippage=0.05' \

3. Deserialize and sign#

Note
The callData here is obtained from the /swap endpoint.
// rpc
const connection = new Connection("xxxxxxxxxxx")


async function signTransaction(callData, privateKey) {

    // decode
    const transaction = bs58.decode(callData)

    let tx
    // There are two types of callData, one is the old version and the other is the new version.
    try {
        tx = solanaWeb3.Transaction.from(transaction)
    } catch (error) {
        tx = solanaWeb3.VersionedTransaction.deserialize(transaction)
    }

    // Replace the latest block hash
    const recentBlockHash = await connection.getLatestBlockhash();


    if (tx instanceof solanaWeb3.VersionedTransaction) {
        tx.message.recentBlockhash = recentBlockHash.blockhash;
    } else {
        tx.recentBlockhash = recentBlockHash.blockhash
    }



    let feePayer = solanaWeb3.Keypair.fromSecretKey(bs58.decode(privateKey))
    // sign
    if (tx instanceof solanaWeb3.VersionedTransaction) {
    // v0 callData
        tx.sign([feePayer])
      } else {
    // legacy callData
        tx.partialSign(feePayer)
      }
    console.log(tx)


}

// 'xxxxxxx' means your privateKey
signTransaction(callData,'xxxxxxx')

4. Execute the transaction#

const txId =  await connection.sendRawTransaction(tx.serialize());

  console.log('txId:', txId)

  // Verify whether it has been broadcast on the chain.
  await connection.confirmTransaction(txId);
  console.log(`https://solscan.io/tx/${txId}`);

5. Complete Implementation using typescript#

For a complete implementation of token swaps on Solana, we provide a TypeScript library that integrates with the OKX DEX API. This implementation is part of our OKX DEX API Library.

Important RPC Configuration Note: Before proceeding, you'll need to select an RPC endpoint. While this example uses Helius (https://mainnet.helius-rpc.com), you can use any Solana RPC provider of your choice. It is recommended that you use a 3rd party provider as the public solana RPC endpoint may introduce resource constraints.

⚠️ Disclaimer: The choice of RPC endpoint is entirely up to you. OKX is not responsible for any third-party RPC services. Always ensure you're using a reliable and secure RPC provider for your production environment.

Environment Setup#

Create a .env file with the following configuration:

OKX_API_KEY=your_api_key
OKX_SECRET_KEY=your_secret_key
OKX_API_PASSPHRASE=your_passphrase
OKX_PROJECT_ID=your_project_id
WALLET_ADDRESS=your_wallet_address
PRIVATE_KEY=your_private_key
SOLANA_RPC_URL=your_rpc_url

WS Endpoint is optional
WS_ENDPONT=

Complete Swap Implementation#

The following implementation provides a full-featured swap solution:

// swap.ts
import base58 from "bs58";
import BN from "bn.js";
import * as solanaWeb3 from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
import cryptoJS from "crypto-js";
import dotenv from 'dotenv';

dotenv.config();

// Environment variables
const apiKey = process.env.OKX_API_KEY;
const secretKey = process.env.OKX_SECRET_KEY;
const apiPassphrase = process.env.OKX_API_PASSPHRASE;
const projectId = process.env.OKX_PROJECT_ID;
const userAddress = process.env.WALLET_ADDRESS;
const userPrivateKey = process.env.PRIVATE_KEY;
const solanaRpcUrl = process.env.SOLANA_RPC_URL;

// Constants
const SOLANA_CHAIN_ID = "501";
const COMPUTE_UNITS = 300000;
const MAX_RETRIES = 3;

const connection = new Connection(`${solanaRpcUrl}`, {
    confirmTransactionInitialTimeout: 5000
    //  wsEndpoint: solanaWsUrl,

});



function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
    if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
        throw new Error("Missing required environment variables");
    }

    const stringToSign = timestamp + method + requestPath + queryString;
    return {
        "Content-Type": "application/json",
        "OK-ACCESS-KEY": apiKey,
        "OK-ACCESS-SIGN": cryptoJS.enc.Base64.stringify(
            cryptoJS.HmacSHA256(stringToSign, secretKey)
        ),
        "OK-ACCESS-TIMESTAMP": timestamp,
        "OK-ACCESS-PASSPHRASE": apiPassphrase,
        "OK-ACCESS-PROJECT": projectId,
    };
}

async function getTokenInfo(fromTokenAddress: string, toTokenAddress: string) {
    const timestamp = new Date().toISOString();
    const requestPath = "/api/v5/dex/aggregator/quote";
    const params = {
        chainId: SOLANA_CHAIN_ID,
        fromTokenAddress,
        toTokenAddress,
        amount: "1000000", // small amount just to get token info
        slippage: "0.5",
    };

    const queryString = "?" + new URLSearchParams(params).toString();
    const headers = getHeaders(timestamp, "GET", requestPath, queryString);

    const response = await fetch(
        `https://www.okx.com${requestPath}${queryString}`,
        { method: "GET", headers }
    );

    if (!response.ok) {
        throw new Error(`Failed to get quote: ${await response.text()}`);
    }

    const data = await response.json();
    if (data.code !== "0" || !data.data?.[0]) {
        throw new Error("Failed to get token information");
    }

    const quoteData = data.data[0];
    return {
        fromToken: {
            symbol: quoteData.fromToken.tokenSymbol,
            decimals: parseInt(quoteData.fromToken.decimal),
            price: quoteData.fromToken.tokenUnitPrice
        },
        toToken: {
            symbol: quoteData.toToken.tokenSymbol,
            decimals: parseInt(quoteData.toToken.decimal),
            price: quoteData.toToken.tokenUnitPrice
        }
    };
}

function convertAmount(amount: string, decimals: number) {
    try {
        if (!amount || isNaN(parseFloat(amount))) {
            throw new Error("Invalid amount");
        }
        const value = parseFloat(amount);
        if (value <= 0) {
            throw new Error("Amount must be greater than 0");
        }
        return new BN(value * Math.pow(10, decimals)).toString();
    } catch (err) {
        console.error("Amount conversion error:", err);
        throw new Error("Invalid amount format");
    }
}

async function main() {
    try {
        const args = process.argv.slice(2);
        if (args.length < 3) {
            console.log("Usage: ts-node swap.ts <amount> <fromTokenAddress> <toTokenAddress>");
            console.log("Example: ts-node swap.ts 1.5 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
            process.exit(1);
        }

        const [amount, fromTokenAddress, toTokenAddress] = args;

        if (!userPrivateKey || !userAddress) {
            throw new Error("Private key or user address not found");
        }

        // Get token information
        console.log("Getting token information...");
        const tokenInfo = await getTokenInfo(fromTokenAddress, toTokenAddress);
        console.log(`From: ${tokenInfo.fromToken.symbol} (${tokenInfo.fromToken.decimals} decimals)`);
        console.log(`To: ${tokenInfo.toToken.symbol} (${tokenInfo.toToken.decimals} decimals)`);

        // Convert amount using fetched decimals
        const rawAmount = convertAmount(amount, tokenInfo.fromToken.decimals);
        console.log(`Amount in ${tokenInfo.fromToken.symbol} base units:`, rawAmount);

        // Get swap quote
        const quoteParams = {
            chainId: SOLANA_CHAIN_ID,
            amount: rawAmount,
            fromTokenAddress,
            toTokenAddress,
            slippage: "0.5",
            userWalletAddress: userAddress,
        } as Record<string, string>;

        // Get swap data
        const timestamp = new Date().toISOString();
        const requestPath = "/api/v5/dex/aggregator/swap";
        const queryString = "?" + new URLSearchParams(quoteParams).toString();
        const headers = getHeaders(timestamp, "GET", requestPath, queryString);

        console.log("Requesting swap quote...");
        const response = await fetch(
            `https://www.okx.com${requestPath}${queryString}`,
            { method: "GET", headers }
        );

        const data = await response.json();
        if (data.code !== "0") {
            throw new Error(`API Error: ${data.msg}`);
        }

        const swapData = data.data[0];

        // Show estimated output and price impact
        const outputAmount = parseFloat(swapData.routerResult.toTokenAmount) / Math.pow(10, tokenInfo.toToken.decimals);
        console.log("\nSwap Quote:");
        console.log(`Input: ${amount} ${tokenInfo.fromToken.symbol} ($${(parseFloat(amount) * parseFloat(tokenInfo.fromToken.price)).toFixed(2)})`);
        console.log(`Output: ${outputAmount.toFixed(tokenInfo.toToken.decimals)} ${tokenInfo.toToken.symbol} ($${(outputAmount * parseFloat(tokenInfo.toToken.price)).toFixed(2)})`);
        if (swapData.priceImpactPercentage) {
            console.log(`Price Impact: ${swapData.priceImpactPercentage}%`);
        }

        console.log("\nExecuting swap transaction...");
        let retryCount = 0;
        while (retryCount < MAX_RETRIES) {
            try {
                if (!swapData || (!swapData.tx && !swapData.data)) {
                    throw new Error("Invalid swap data structure");
                }

                const transactionData = swapData.tx?.data || swapData.data;
                if (!transactionData || typeof transactionData !== 'string') {
                    throw new Error("Invalid transaction data");
                }

                const recentBlockHash = await connection.getLatestBlockhash();
                console.log("Got blockhash:", recentBlockHash.blockhash);

                const decodedTransaction = base58.decode(transactionData);
                let tx;

                try {
                    tx = solanaWeb3.VersionedTransaction.deserialize(decodedTransaction);
                    console.log("Successfully created versioned transaction");
                    tx.message.recentBlockhash = recentBlockHash.blockhash;
                } catch (e) {
                    console.log("Versioned transaction failed, trying legacy:", e);
                    tx = solanaWeb3.Transaction.from(decodedTransaction);
                    console.log("Successfully created legacy transaction");
                    tx.recentBlockhash = recentBlockHash.blockhash;
                }

                const computeBudgetIx = solanaWeb3.ComputeBudgetProgram.setComputeUnitLimit({
                    units: COMPUTE_UNITS
                });

                const feePayer = solanaWeb3.Keypair.fromSecretKey(
                    base58.decode(userPrivateKey)
                );

                if (tx instanceof solanaWeb3.VersionedTransaction) {
                    tx.sign([feePayer]);
                } else {
                    tx.partialSign(feePayer);
                }

                const txId = await connection.sendRawTransaction(tx.serialize(), {
                    skipPreflight: false,
                    maxRetries: 5
                });

                const confirmation = await connection.confirmTransaction({
                    signature: txId,
                    blockhash: recentBlockHash.blockhash,
                    lastValidBlockHeight: recentBlockHash.lastValidBlockHeight
                }, 'confirmed');

                if (confirmation?.value?.err) {
                    throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
                }

                console.log("\nSwap completed successfully!");
                console.log("Transaction ID:", txId);
                console.log("Explorer URL:", `https://solscan.io/tx/${txId}`);

                process.exit(0);
            } catch (error) {
                console.error(`Attempt ${retryCount + 1} failed:`, error);
                retryCount++;

                if (retryCount === MAX_RETRIES) {
                    throw error;
                }

                await new Promise(resolve => setTimeout(resolve, 2000 * retryCount));
            }
        }
    } catch (error) {
        console.error("Error:", error instanceof Error ? error.message : "Unknown error");
        process.exit(1);
    }
}

if (require.main === module) {
    main();
}

Usage Example#

To execute a swap, run the script with the following parameters:

npx ts-node swap.ts <amount> <fromTokenAddress> <toTokenAddress>

For Example:

# Example: Swap .01 SOL to USDC
npx ts-node swap.ts .01 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

Will return a response similar to the following:

From: SOL (9 decimals)
To: USDC (6 decimals)
Amount in SOL base units: 10000000
Requesting swap quote...

Swap Quote:
Input: .01 SOL ($1.82)
Output: 1.820087 USDC ($1.82)

Executing swap transaction...
Got blockhash: J7cWaf9UQJyN6SqatDHhmdAtP3skN7YKFCJnbaLeKf3r
Successfully created versioned transaction

Swap completed successfully!
Transaction ID: 5LncQyzK7YmcodcsQMYwnjYBAYBkKJAaS1XR2RLiCVyPyA5nwHjUNuSQos4VGk4CJm5spRPngdnv8cQYjYYwCAVu
Explorer URL: https://solscan.io/tx/5LncQyzK7YmcodcsQMYwnjYBAYBkKJAaS1XR2RLiCVyPyA5nwHjUNuSQos4VGk4CJm5spRPngdnv8cQYjYYwCAVu

6. MEV Protection#

Trading on any network comes with MEV(Maximal Extractable Value) risks, but here are some methods to potentially protect your users' trades on Solana. This implementation includes several approaches that developers can implement to minimize their users' MEV exposure.

Smart Protection Approaches#

The first line of defense uses dynamic priority fees - think of it as your bid in an auction against MEV bots:

static async getPriorityFee(): Promise<number> {
    const recentFees = await connection.getRecentPrioritizationFees();
    const maxFee = Math.max(...recentFees.map(fee => fee.prioritizationFee));
    return Math.min(maxFee * 1.5, MEV_PROTECTION.MAX_PRIORITY_FEE);
}

For larger trades, you can enable TWAP(Time-Weighted Average Price). Instead of making one big splash that MEV bots love to target, your trade gets split into smaller pieces:

if (MEV_PROTECTION.TWAP_ENABLED) {
    const chunks = await TWAPExecution.splitTrade(
        rawAmount,
        fromTokenAddress,
        toTokenAddress
    );
}

Protection in Action#

When you execute a trade with this implementation, several things happen:

(1)Pre-Trade Checks:

  • The token you're buying gets checked for honeypot characteristics
  • Network fees are analyzed to set competitive priority
  • Your trade size determines if it should be split up

(2)During the Trade:

  • Large trades can be split into parts with randomized timing
  • Each piece gets its own priority fee based on market conditions
  • Specific block targeting helps reduce exposure

(3)Transaction Safety:

  • Each transaction runs through simulation first
  • Built-in confirmation tracking
  • Automatic retry logic if something goes wrong

To be transaprent - MEV on Solana is like rain. You can't stop it completely, but these protections are like carrying an umbrella. They make life harder for MEV bots, but more robust solutions require .

Making It Work For You#

Some practical tips when using these protections:

(1)For trades where TWAP is applicabe, consider enabling it in the code example:

MEV_PROTECTION.TWAP_ENABLED = true;

(2)During crazy market conditions, you might want to bump up priority fees:

MEV_PROTECTION.PRIORITY_MULTIPLIER = 3; // More competitive

(3)Set slippage based on the token you're trading:

CONFIG.SLIPPAGE = "0.5" // Standard setting

It's worth noting that better MEV protection usually means slower execution. If you need speed above all else, you might need to accept more MEV risk. It's about finding what works for your users' needs.

Real World Example#

Here's how you'd use it:

npx ts-node solana-swap-mev.ts .02 11111111111111111111111111111111 EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v

Which Returns:

Swap completed successfully!
Transaction IDs: 669uQvX6wRRGo3mUMvyPG5s9kFN9pCZsKER5kbByfWUKptWHTCUMpfycwMXC2RFMJpzYBKaPAMfCbxr3886fzkQY, 51nvyyGWQU3Nw8jo7g1Suq2sAZSUkgA7bSJ8upbFtR8bSsibs896R6Bifi6ucFmhuTP63cmsM8bKJiFz6AA14LxA, 5ov1cVk64adFVnnXZpizrdRFd4BvpASMwkkVTohRWtig5Fu519iQSahVbddvjRAtfcimNGg6XhN8cTaneVddc63j, 2ySKQq5gmfYZ1sJuCrz72aNFMknu943PBAw9ebRFtFeLpW4Q9PXjNTHwY1uiREVmvDiYGJZu9piKvBNDLorx5zi5

Explorer URLs:
https://solscan.io/tx/669uQvX6wRRGo3mUMvyPG5s9kFN9pCZsKER5kbByfWUKptWHTCUMpfycwMXC2RFMJpzYBKaPAMfCbxr3886fzkQY
https://solscan.io/tx/51nvyyGWQU3Nw8jo7g1Suq2sAZSUkgA7bSJ8upbFtR8bSsibs896R6Bifi6ucFmhuTP63cmsM8bKJiFz6AA14LxA
https://solscan.io/tx/5ov1cVk64adFVnnXZpizrdRFd4BvpASMwkkVTohRWtig5Fu519iQSahVbddvjRAtfcimNGg6XhN8cTaneVddc63j
https://solscan.io/tx/2ySKQq5gmfYZ1sJuCrz72aNFMknu943PBAw9ebRFtFeLpW4Q9PXjNTHwY1uiREVmvDiYGJZu9piKvBNDLorx5zi5

Looking Beyond Implementation Protection#

While the protections in this implementation offer solid defense mechanisms, it's worth noting that the most advanced MEV protection on Solana happens at the validator level.

Validator-level solutions can intercept and protect trades before they even hit the mempool, providing protection at a much deeper layer than application-level safeguards. However, these solutions typically require specialized infrastructure setup and aren't accessible through standard RPC endpoints.

The reality is: the most effective MEV protection combines multiple approaches - from smart contract level safeguards, to application-level protections like those in this implementation, all the way up to validator-level solutions. Each layer adds its own unique benefits and tradeoffs in the battle against MEV.

7.Add Swap Instructions#

The swap-instruction endpoint is your go-to when you need more control over the swap process than the /swap endpoint provides. While /swap gives you a pre-built transaction ready to sign, swap-instruction lets you:

  • Build custom transaction signing flows
  • Handle instruction processing your own way
  • Add your own instructions to the transaction if needed
  • Work with lookup tables directly for optimizing transaction size

This guide walks through a complete implementation using swap instructions. You'll see how to fetch them from the API, process them, and build them into a working transaction.

Set up your environment#

Import the necessary libraries and configure your environment:

// Required Solana dependencies for DEX interaction
import {
    Connection,          // Handles RPC connections to Solana network
    Keypair,            // Manages wallet keypairs for signing
    PublicKey,          // Handles Solana public key conversion and validation
    TransactionInstruction,    // Core transaction instruction type
    TransactionMessage,        // Builds transaction messages (v0 format)
    VersionedTransaction,      // Supports newer transaction format with lookup tables
    RpcResponseAndContext,     // RPC response wrapper type
    SimulatedTransactionResponse,  // Simulation result type
    AddressLookupTableAccount,     // For transaction size optimization
    PublicKeyInitData              // Public key input type
} from "@solana/web3.js";
import base58 from "bs58";    // Required for private key decoding
import dotenv from "dotenv";  // Environment variable management
dotenv.config();

Initialize Connection and Wallet#

Set up your connection and wallet instance:

// Note: Consider using a reliable RPC endpoint with high rate limits for production
const connection = new Connection(
    process.env.SOLANA_RPC_URL || "https://api.mainnet-beta.solana.com"
);

// Initialize wallet for signing
// This wallet will be the fee payer and transaction signer
// Ensure it has sufficient SOL for transaction fees
const wallet = Keypair.fromSecretKey(
    Uint8Array.from(base58.decode(process.env.PRIVATE_KEY?.toString() || ""))
);

Configure Swap Parameters#

Set up the parameters for your swap:

// Configure swap parameters
    const baseUrl = "https://beta.okex.org/api/v5/dex/aggregator/swap-instruction";
    const params = {
        chainId: "501",              // Solana mainnet chain ID
        feePercent: "1",            // Platform fee percentage
        amount: "1000000",          // Amount in smallest denomination (e.g., lamports for SOL)
        fromTokenAddress: "11111111111111111111111111111111",  // SOL mint address
        toTokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",  // USDC mint address
        slippage: "0.1",            // Slippage tolerance in percentage
        userWalletAddress: process.env.WALLET_ADDRESS || "",   // Wallet performing the swap
        priceTolerance: "0",        // Maximum allowed price impact
        autoSlippage: "false",      // Use fixed slippage instead of auto
        fromTokenReferrerWalletAddress: process.env.WALLET_ADDRESS || "",  // For referral fees
        pathNum: "3"                // Maximum routes to consider
    }

Process Swap Instructions#

Fetch and process the swap instructions:

// Helper function to convert DEX API instructions to Solana format
// The DEX returns instructions in a custom format that needs conversion
function createTransactionInstruction(instruction: any): TransactionInstruction {
    return new TransactionInstruction({
        programId: new PublicKey(instruction.programId),  // DEX program ID
        keys: instruction.accounts.map((key: any) => ({            pubkey: new PublicKey(key.pubkey),    // Account address
            isSigner: key.isSigner,     // True if account must sign tx
            isWritable: key.isWritable  // True if instruction modifies account
        })),
        data: Buffer.from(instruction.data, 'base64')  // Instruction parameters
    });
}

// Fetch optimal swap route and instructions from DEX
// This call finds the best price across different DEX liquidity pools
const url = `${baseUrl}?${new URLSearchParams(params).toString()}`;
const { data: { instructionLists, addressLookupTableAccount } } =
    await fetch(url, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' }
    }).then(res => res.json());

// Process DEX instructions into Solana-compatible format
const instructions: TransactionInstruction[] = [];
// Remove duplicate lookup table addresses returned by DEX
const addressLookupTableAccount2 = Array.from(new Set(addressLookupTableAccount));
console.log("Lookup tables to load:", addressLookupTableAccount2);

// Convert each DEX instruction to Solana format
if (instructionLists?.length) {
    instructions.push(...instructionLists.map(createTransactionInstruction));
}

Handle Address Lookup Tables#

Process the address lookup tables for transaction optimization:

// Process lookup tables for transaction optimization
// Lookup tables are crucial for complex swaps that interact with many accounts
// They significantly reduce transaction size and cost
const addressLookupTableAccounts: AddressLookupTableAccount[] = [];
if (addressLookupTableAccount2?.length > 0) {
    console.log("Loading address lookup tables...");
    // Fetch all lookup tables in parallel for better performance
    const lookupTableAccounts = await Promise.all(
        addressLookupTableAccount2.map(async (address: unknown) => {
            const pubkey = new PublicKey(address as PublicKeyInitData);
            // Get lookup table account data from Solana
            const account = await connection
                .getAddressLookupTable(pubkey)
                .then((res) => res.value);
            if (!account) {
                throw new Error(`Could not fetch lookup table account ${address}`);
            }
            return account;
        })
    );
    addressLookupTableAccounts.push(...lookupTableAccounts);
}

Create and Sign Transaction#

Create the transaction message and sign it:

// Get recent blockhash for transaction timing and uniqueness
// Transactions are only valid for a limited time after this blockhash
const latestBlockhash = await connection.getLatestBlockhash('finalized');

// Create versioned transaction message
// V0 message format required for lookup table support
const messageV0 = new TransactionMessage({
    payerKey: wallet.publicKey,     // Fee payer address
    recentBlockhash: latestBlockhash.blockhash,  // Transaction timing
    instructions                     // Swap instructions from DEX
}).compileToV0Message(addressLookupTableAccounts);  // Include lookup tables

// Create new versioned transaction with optimizations
const transaction = new VersionedTransaction(messageV0);

// Simulate transaction to check for errors
// This helps catch issues before paying fees
const result: RpcResponseAndContext<SimulatedTransactionResponse> =
    await connection.simulateTransaction(transaction);

// Sign transaction with fee payer wallet
const feePayer = Keypair.fromSecretKey(
    base58.decode(process.env.PRIVATE_KEY?.toString() || "")
);
transaction.sign([feePayer])

Execute Transaction#

Finally, simulate and send the transaction:

// Send transaction to Solana
// skipPreflight=false ensures additional validation
// maxRetries helps handle network issues
const txId = await connection.sendRawTransaction(transaction.serialize(), {
    skipPreflight: false,  // Run preflight validation
    maxRetries: 5         // Retry on failure
});

// Log transaction details
console.log("Raw transaction:", transaction.serialize());
console.log("Base58 transaction:", base58.encode(transaction.serialize()));

// Log simulation results for debugging
console.log("=========simulate result=========");
result.value.logs?.forEach((log) => {
    console.log(log);
});

// Log transaction results
console.log("Transaction ID:", txId);
console.log("Explorer URL:", `https://solscan.io/tx/${txId}`);

Best Practices and Considerations#

When implementing swap instructions, keep these key points in mind:

  1. You can view an example of the full Typescript implementation here
  2. Error Handling: Always implement proper error handling for API responses and transaction simulation results.
  3. Slippage Protection: Choose appropriate slippage parameters based on your use case and market conditions.
  4. Gas Optimization: Use address lookup tables when available to reduce transaction size and costs.
  5. Transaction Simulation: Always simulate transactions before sending them to catch potential issues early. You can also use this for testing without executing the transaction.
  6. Retry Logic: Implement proper retry mechanisms for failed transactions with appropriate backoff strategies.

By following these practices and understanding the swap instruction process, you can build reliable and efficient token exchange functionality into your Solana applications.