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.