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:
- Export and save your SUI wallet's private key
- Download and install the SUI CLI
- Use the following command to convert your SUI wallet's private key to the
hexWithoutFlag
format
sui keytool convert <your_sui_private_key>
- 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#
/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