DEX API

Build swap applications on Sui#

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
  • Process and sign transaction
  • Execute the transaction

1. Set up your environment#

For convenience, you can clone the OKX DEX API Library and install the necessary dependencies:

git clone https://github.com/okx/dex-api-library.git
cd dex-api-library
npm install

Additionally, you need to create a .env file with the following configuration: You can obtain your OKX API credentials from: https://www.okx.com/web3/build/dev-portal

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_sui_wallet_address
PRIVATE_KEY=your_sui_wallet_private_key

This demo uses the hexWithoutFlag format of your SUI privatekey

You can follow the steps below to obtain it:

  1. Export and save your SUI wallet's private key
  2. Download and install the SUI CLI
  3. Use the following command to convert your SUI wallet's private key to the hexWithoutFlag format
sui keytool convert <your_sui_private_key>
  1. Use the output value as PRIVATE_KEY in your .env file

2. Obtain token information and swap quote#

Use the /dex/aggregator/quote endpoint to retrieve token information and the /dex/aggregator/swap endpoint for swap data.

Here's an example of swapping SUI to USDC:

function getHeaders(timestamp: string, method: string, requestPath: string, queryString = "") {
    if (!apiKey || !secretKey || !apiPassphrase || !projectId) {
        throw new Error("Missing required environment variables");
    }
    const timestamp = new Date().toISOString();
    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/swap";
    const params = {
        chainId: SUI_CHAIN_ID,
        fromTokenAddress,
        toTokenAddress,
        amount: "1000000", 
        slippage: "0.5",
        userWalletAddress: normalizedWalletAddress,
    };

    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 Data = data.data[0]
    return Data
    }

3. Process and sign transaction#

Note
The transaction data here is obtained from the /swap endpoint.
async function executeSwap(txData: string, privateKey: string) {
    // Create transaction block
    const txBlock = Transaction.from(txData);
    txBlock.setSender(normalizedWalletAddress);

    // Set gas parameters
    const referenceGasPrice = await client.getReferenceGasPrice();
    txBlock.setGasPrice(BigInt(referenceGasPrice));
    txBlock.setGasBudget(BigInt(CONFIG.DEFAULT_GAS_BUDGET));

    // Build and sign transaction
    const builtTx = await txBlock.build({ client });
    const txBytes = Buffer.from(builtTx).toString('base64');

    const signedTx = await wallet.signTransaction({
        privateKey,
        data: {
            type: 'raw',
            data: txBytes
        }
    });

    return signedTx;
}

4. Execute the transaction#

const result = await client.executeTransactionBlock({
    transactionBlock: builtTx,
    signature: [signedTx.signature],
    options: {
        showEffects: true,
        showEvents: true,
    }
});

// Verify the transaction
const confirmation = await client.waitForTransaction({
    digest: result.digest,
    options: {
        showEffects: true,
        showEvents: true,
    }
});

console.log("Transaction ID:", result.digest);
console.log("Explorer URL:", `https://suiscan.xyz/mainnet/tx/${result.digest}`);

5. Complete Implementation using typescript#

The following implementation provides a full-featured swap solution:

// swap.ts
import { SuiWallet } from "@okxweb3/coin-sui";
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
import { Transaction } from '@mysten/sui/transactions';
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;


// Constants
const SUI_CHAIN_ID = "784";
const DEFAULT_GAS_BUDGET = 50000000;
const MAX_RETRIES = 3;

// Initialize clients
const wallet = new SuiWallet();
const client = new SuiClient({
    url: getFullnodeUrl('mainnet')
});

// Normalize wallet address
const normalizedWalletAddress = normalizeSuiAddress(userAddress);

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: SUI_CHAIN_ID,
        fromTokenAddress,
        toTokenAddress,
        amount: "1000000", 
        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 (BigInt(Math.floor(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 0x2::sui::SUI 0xdba...::usdc::USDC");
            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: SUI_CHAIN_ID,
            amount: rawAmount,
            fromTokenAddress,
            toTokenAddress,
            slippage: "0.5",
            userWalletAddress: normalizedWalletAddress,
        };

        // 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 {
                // Create transaction block
                const txBlock = Transaction.from(swapData.tx.data);
                txBlock.setSender(normalizedWalletAddress);

                // Set gas parameters
                const referenceGasPrice = await client.getReferenceGasPrice();
                txBlock.setGasPrice(BigInt(referenceGasPrice));
                txBlock.setGasBudget(BigInt(DEFAULT_GAS_BUDGET));

                // Build and sign transaction
                const builtTx = await txBlock.build({ client });
                const txBytes = Buffer.from(builtTx).toString('base64');

                const signedTx = await wallet.signTransaction({
                    privateKey: userPrivateKey,
                    data: {
                        type: 'raw',
                        data: txBytes
                    }
                });

                if (!signedTx?.signature) {
                    throw new Error("Failed to sign transaction");
                }

                // Execute transaction
                const result = await client.executeTransactionBlock({
                    transactionBlock: builtTx,
                    signature: [signedTx.signature],
                    options: {
                        showEffects: true,
                        showEvents: true,
                    }
                });

                // Wait for confirmation
                const confirmation = await client.waitForTransaction({
                    digest: result.digest,
                    options: {
                        showEffects: true,
                        showEvents: true,
                    }
                });

                console.log("\nSwap completed successfully!");
                console.log("Transaction ID:", result.digest);
                console.log("Explorer URL:", `https://suiscan.xyz/mainnet/tx/${result.digest}`);

                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 1.5 SUI to USDC
npx ts-node swap.ts 1.5 0x2::sui::SUI 0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC

This will return a response similar to the following:

Getting token information...
From: SUI (9 decimals)
To: USDC (6 decimals)
Amount in SUI base units: 1500000000

Swap Quote:
Input: 1.5 SUI ($2.73)
Output: 2.73 USDC ($2.73)

Executing swap transaction...
Signing transaction...
Executing transaction...

Swap completed successfully!
Transaction ID: 5LncQyzK7YmcodcsQMYwnjYBAYBkKJAaS1XR2RLiCVyPyA5nwHjUNuSQos4VGk4CJm5spRPngdnv8cQYjYYwCAVu
Explorer URL: https://suiscan.xyz/mainnet/tx/5LncQyzK7Ym