Prerequisites
Before you begin, ensure you have:- A Web3-enabled wallet (e.g., MetaMask installed in browser)
- Access to testnet tokens on Base Sepolia and Polygon Amoy
- Basic understanding of EIP-712 signatures
- Node.js 18+ and your preferred package manager
Installation
npm
Copy
npm install viem
yarn
Copy
yarn add viem
pnpm
Copy
pnpm add viem
Configuration
Set up your chain and token configurations:Copy
import {
createPublicClient,
createWalletClient,
custom,
http,
parseUnits,
formatUnits,
type Address,
type Hash
} from 'viem';
import { baseSepolia, polygonAmoy } from 'viem/chains';
// Chain configurations
export const CHAINS = {
'base-sepolia': {
name: 'Base Sepolia',
chainId: 84532,
chain: baseSepolia,
rpcUrl: 'https://sepolia.base.org',
explorerUrl: 'https://sepolia.basescan.org'
},
'polygon-amoy': {
name: 'Polygon Amoy',
chainId: 80002,
chain: polygonAmoy,
rpcUrl: 'https://rpc-amoy.polygon.technology',
explorerUrl: 'https://amoy.polygonscan.com'
}
} as const;
export type ChainKey = keyof typeof CHAINS;
// Token addresses by chain
export const TOKENS = {
'base-sepolia': {
USDC: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' as Address,
USDT: '0x66F5018cdb5d6Eb0a3d1AC57F8b77243eb0010fF' as Address,
DAI: '0xD68450706D96997899223dEd69B61a16068735f7' as Address
},
'polygon-amoy': {
USDC: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' as Address,
USDT: '0x86f6D9931BE92Ad61572e80c0F5FAe45950D4cA1' as Address,
DAI: '0xD0a00D630D8A01c3E53A8a210Ae3141BbF1A799b' as Address
}
} as const;
export type TokenSymbol = 'USDC' | 'USDT' | 'DAI';
// Lidian contract addresses by chain
export const LIDIAN_CONTRACTS: Record<ChainKey, Address> = {
'base-sepolia': '0xc2c00E85ab6bcf865986DE84053a6169bCBfa70F',
'polygon-amoy': '0x1234567890123456789012345678901234567890'
};
// Token decimals
export const TOKEN_DECIMALS: Record<TokenSymbol, number> = {
USDC: 6,
USDT: 6,
DAI: 18
};
// API configuration
export const API_BASE_URL = 'https://api.lidian.finance';
// ERC20 ABI for allowance and approve
export const ERC20_ABI = [
{
type: 'function',
name: 'allowance',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' }
],
outputs: [{ name: '', type: 'uint256' }]
},
{
type: 'function',
name: 'approve',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ name: '', type: 'bool' }]
}
] as const;
Setup Clients
Create viem clients for interacting with the blockchain:Copy
/**
* Create a public client for reading blockchain state
*/
function createPublicClientForChain(chainKey: ChainKey) {
return createPublicClient({
chain: CHAINS[chainKey].chain,
transport: http(CHAINS[chainKey].rpcUrl)
});
}
/**
* Create a wallet client for signing transactions
* Assumes browser environment with window.ethereum
*/
function createWalletClientForChain(chainKey: ChainKey) {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('No ethereum provider found');
}
return createWalletClient({
chain: CHAINS[chainKey].chain,
transport: custom(window.ethereum)
});
}
/**
* Get the connected wallet address
*/
async function getWalletAddress(): Promise<Address> {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('No ethereum provider found');
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
}) as Address[];
if (!accounts || accounts.length === 0) {
throw new Error('No accounts found');
}
return accounts[0];
}
Step 1: Request a Quote
Create a quote request to get competitive rates from liquidity providers:Copy
interface QuoteRequestParams {
fromChain: ChainKey;
toChain: ChainKey;
fromToken: TokenSymbol;
toToken: TokenSymbol;
amount: string; // Human-readable amount (e.g., "100.5")
userAddress: Address;
}
/**
* Request a quote from liquidity providers
* @returns Quote request ID for polling
*/
async function requestQuote(params: QuoteRequestParams): Promise<string> {
const { fromChain, toChain, fromToken, toToken, amount, userAddress } = params;
console.log('Creating quote request...');
console.log(` From: ${amount} ${fromToken} on ${CHAINS[fromChain].name}`);
console.log(` To: ${toToken} on ${CHAINS[toChain].name}`);
// Convert amount to proper decimals
const decimals = TOKEN_DECIMALS[fromToken];
const inputAmount = parseUnits(amount, decimals).toString();
// Get token addresses
const inputToken = TOKENS[fromChain][fromToken];
const outputToken = TOKENS[toChain][toToken];
// Create quote request
const response = await fetch(`${API_BASE_URL}/api/v1/quote-requests`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include', // Use JWT from SIWE authentication
body: JSON.stringify({
offerer: userAddress,
recipient: userAddress,
inputToken,
outputToken,
inputAmount,
inputChain: fromChain,
outputChain: toChain
})
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Request failed' }));
throw new Error(error.message || 'Failed to create quote request');
}
const data = await response.json();
const quoteRequestId = data?.data?.quoteId;
if (!quoteRequestId) {
throw new Error('No quote request ID received');
}
console.log(`✓ Quote request created: ${quoteRequestId}`);
return quoteRequestId;
}
Step 2: Poll for Quotes
After requesting a quote, poll the API to retrieve competitive quotes from liquidity providers:Copy
interface Quote {
id: string;
orderId: string;
inputAmount: string;
outputAmount: string;
inputToken: string;
outputToken: string;
inputChain: string;
outputChain: string;
liquidityProviderId: string;
typedData: {
domain: {
name: string;
version: string;
chainId: number;
verifyingContract: Address;
};
types: {
Order: Array<{ name: string; type: string }>;
};
primaryType: string;
message: Record<string, any>;
};
createdAt: string;
}
/**
* Poll for quotes from liquidity providers
* @param quoteRequestId - The quote request ID to poll for
* @param maxAttempts - Maximum number of polling attempts (default: 15)
* @param intervalMs - Interval between polls in milliseconds (default: 2000)
* @returns Array of quotes sorted by best output amount
*/
async function pollForQuotes(
quoteRequestId: string,
maxAttempts: number = 15,
intervalMs: number = 2000
): Promise<Quote[]> {
console.log('Polling for quotes...');
let attempts = 0;
while (attempts < maxAttempts) {
try {
const response = await fetch(
`${API_BASE_URL}/api/v1/quotes?quoteRequestId=${quoteRequestId}`,
{ credentials: 'include' }
);
if (response.ok) {
const data = await response.json();
const quotes: Quote[] = data?.data?.quotes || [];
if (quotes.length > 0) {
// Sort by best output amount (highest first)
const sortedQuotes = quotes.sort((a, b) =>
Number(BigInt(b.outputAmount) - BigInt(a.outputAmount))
);
console.log(`✓ Received ${quotes.length} quote(s)`);
return sortedQuotes;
}
}
} catch (error) {
console.error('Quote polling error:', error);
}
attempts++;
if (attempts < maxAttempts) {
console.log(` Waiting for quotes (${attempts}/${maxAttempts})...`);
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}
throw new Error('No quotes received within timeout period');
}
Step 3: Select the Best Quote
Copy
/**
* Select the best quote from available quotes
* @param quotes - Array of quotes (should be pre-sorted by best output)
* @param outputToken - Output token symbol for display
* @returns Best quote
*/
function selectBestQuote(quotes: Quote[], outputToken: TokenSymbol): Quote {
if (quotes.length === 0) {
throw new Error('No quotes available');
}
// Quotes are already sorted by best output, so take the first
const bestQuote = quotes[0];
// Display quote information
const decimals = TOKEN_DECIMALS[outputToken];
const outputAmount = formatUnits(BigInt(bestQuote.outputAmount), decimals);
console.log('Best Quote:');
console.log(` Quote ID: ${bestQuote.id}`);
console.log(` Order ID: ${bestQuote.orderId}`);
console.log(` Output Amount: ${outputAmount} ${outputToken}`);
console.log(` LP: ${bestQuote.liquidityProviderId}`);
return bestQuote;
}
Step 4: Check and Approve Token Allowance
Before executing the swap, ensure the Lidian contract has sufficient allowance to spend your tokens:Copy
/**
* Check token allowance and approve if necessary
* @param quote - The selected quote
* @param fromChain - Source chain key
* @param fromToken - Source token symbol
* @param userAddress - User's wallet address
*/
async function checkAndApproveToken(
quote: Quote,
fromChain: ChainKey,
fromToken: TokenSymbol,
userAddress: Address
): Promise<void> {
const tokenAddress = TOKENS[fromChain][fromToken];
const spender = LIDIAN_CONTRACTS[fromChain];
const inputAmount = BigInt(quote.inputAmount);
// Create clients
const publicClient = createPublicClientForChain(fromChain);
const walletClient = createWalletClientForChain(fromChain);
console.log('Checking token allowance...');
// Check current allowance
const allowance = await publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'allowance',
args: [userAddress, spender]
}) as bigint;
console.log(` Current allowance: ${allowance.toString()}`);
console.log(` Required amount: ${inputAmount.toString()}`);
// Approve if insufficient allowance
if (allowance < inputAmount) {
console.log('Approving token spend...');
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [spender, inputAmount],
account: userAddress
});
console.log(` Approval tx: ${hash}`);
console.log(' Waiting for confirmation...');
// Wait for transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status === 'success') {
console.log('✓ Token approval confirmed');
} else {
throw new Error('Token approval failed');
}
} else {
console.log('✓ Sufficient allowance already exists');
}
}
Step 5: Sign the Order (EIP-712)
Sign the order using EIP-712 typed data:Copy
/**
* Sign the order using EIP-712 typed data
* @param quote - The quote containing typed data to sign
* @param userAddress - User's wallet address
* @returns Signature string
*/
async function signOrder(quote: Quote, userAddress: Address): Promise<Hash> {
console.log('Requesting signature...');
const { domain, types, message } = quote.typedData;
if (!domain || !types || !message) {
throw new Error('Quote missing typed data for signing');
}
// Create wallet client
const walletClient = createWalletClientForChain(quote.inputChain as ChainKey);
// Sign the typed data
const signature = await walletClient.signTypedData({
account: userAddress,
domain,
types,
primaryType: 'Order',
message
});
console.log('✓ Order signed successfully');
return signature;
}
Step 6: Submit the Swap
Submit the signed order to execute the swap:Copy
/**
* Submit the swap to the Lidian API
* @param quote - The accepted quote
* @param signature - The EIP-712 signature
* @returns Order ID for monitoring
*/
async function submitSwap(quote: Quote, signature: Hash): Promise<string> {
console.log('Submitting swap...');
const response = await fetch(`${API_BASE_URL}/api/v1/swaps`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
orderId: quote.orderId,
quoteId: quote.id,
signature
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Swap submission failed: ${errorText}`);
}
const data = await response.json();
const orderId = data?.data?.orderId || quote.orderId;
console.log('✓ Swap submitted successfully');
console.log(` Order ID: ${orderId}`);
return orderId;
}
Step 7: Monitor Order Status
Poll the order status to track execution:Copy
type OrderStatus =
| 'created' // Order created, awaiting deposit
| 'received' // Source chain deposit confirmed
| 'fulfilled' // Destination tokens sent
| 'completed' // Settlement complete
| 'failed' // Order failed
| 'cancelled'; // Order cancelled
interface OrderDetails {
orderId: string;
status: OrderStatus;
quoteId: string;
signature: string;
moneyMoverAccountId: string;
liquidityProviderAccountId: string;
details: {
srcChainTxHash?: string;
dstChainTxHash?: string;
settlementTxHash?: string;
orderHash?: string;
srcChain?: string;
dstChain?: string;
};
createdAt: string;
updatedAt: string;
completedAt?: string;
failureReason?: string;
}
/**
* Monitor order status until completion or failure
* @param orderId - The order ID to monitor
* @param maxAttempts - Maximum polling attempts (default: 150 = 5 minutes)
* @param intervalMs - Polling interval in ms (default: 2000)
* @param onStatusChange - Optional callback for status updates
* @returns Final order details
*/
async function monitorOrderStatus(
orderId: string,
maxAttempts: number = 150,
intervalMs: number = 2000,
onStatusChange?: (status: OrderStatus, details: OrderDetails) => void
): Promise<OrderDetails> {
console.log('Monitoring order status...');
let attempts = 0;
let lastStatus: OrderStatus | null = null;
while (attempts < maxAttempts) {
try {
const response = await fetch(
`${API_BASE_URL}/api/v1/orders/${orderId}`,
{ credentials: 'include' }
);
if (response.ok) {
const data = await response.json();
const order: OrderDetails = data?.data;
if (order?.status) {
// Notify on status change
if (order.status !== lastStatus) {
console.log(` Status: ${order.status}`);
if (order.details?.srcChainTxHash) {
console.log(` Source tx: ${order.details.srcChainTxHash}`);
}
if (order.details?.dstChainTxHash) {
console.log(` Destination tx: ${order.details.dstChainTxHash}`);
}
onStatusChange?.(order.status, order);
lastStatus = order.status;
}
// Check for terminal states
if (['completed', 'settled', 'fulfilled'].includes(order.status)) {
console.log('✓ Swap completed successfully!');
if (order.details?.settlementTxHash) {
console.log(` Settlement tx: ${order.details.settlementTxHash}`);
}
return order;
}
if (['failed', 'cancelled'].includes(order.status)) {
const reason = order.failureReason || 'Unknown error';
throw new Error(`Order ${order.status}: ${reason}`);
}
}
} else if (response.status === 404) {
// Order not yet visible, continue polling
if (attempts % 5 === 0) {
console.log(' Order not yet visible, continuing to poll...');
}
}
} catch (error) {
if (error instanceof Error && error.message.includes('Order')) {
throw error; // Rethrow order-specific errors
}
console.error(' Order status check error:', error);
}
attempts++;
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error('Order monitoring timed out');
}
Complete End-to-End Script
Here’s how to put it all together in a single executable script:Copy
/**
* Execute a complete cross-chain swap
*/
async function executeSwap(
fromChain: ChainKey,
toChain: ChainKey,
fromToken: TokenSymbol,
toToken: TokenSymbol,
amount: string
): Promise<OrderDetails> {
console.log('=== Starting Lidian Cross-Chain Swap ===\n');
try {
// Get user's wallet address
const userAddress = await getWalletAddress();
console.log(`Wallet: ${userAddress}\n`);
// Step 1: Request quote
console.log('Step 1: Requesting Quote');
const quoteRequestId = await requestQuote({
fromChain,
toChain,
fromToken,
toToken,
amount,
userAddress
});
console.log();
// Step 2: Poll for quotes
console.log('Step 2: Waiting for Quotes');
const quotes = await pollForQuotes(quoteRequestId);
console.log();
// Step 3: Select best quote
console.log('Step 3: Selecting Best Quote');
const bestQuote = selectBestQuote(quotes, toToken);
console.log();
// Step 4: Check and approve token
console.log('Step 4: Checking Token Allowance');
await checkAndApproveToken(bestQuote, fromChain, fromToken, userAddress);
console.log();
// Step 5: Sign the order
console.log('Step 5: Signing Order');
const signature = await signOrder(bestQuote, userAddress);
console.log();
// Step 6: Submit swap
console.log('Step 6: Submitting Swap');
const orderId = await submitSwap(bestQuote, signature);
console.log();
// Step 7: Monitor status
console.log('Step 7: Monitoring Order Status');
const finalOrder = await monitorOrderStatus(orderId, 150, 2000, (status, details) => {
// Optional: Add custom status change handlers
if (status === 'received') {
console.log(' → Source chain deposit confirmed');
} else if (status === 'fulfilled') {
console.log(' → Destination tokens delivered');
}
});
console.log('\n=== Swap Complete ===');
return finalOrder;
} catch (error) {
console.error('\n❌ Swap Failed:');
console.error(error instanceof Error ? error.message : 'Unknown error');
throw error;
}
}
// Example usage
async function main() {
try {
const result = await executeSwap(
'base-sepolia', // From chain
'polygon-amoy', // To chain
'USDC', // From token
'USDT', // To token
'100' // Amount
);
console.log('\nFinal Order:', result);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
// Run if this is the main module
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
Usage Examples
Basic Swap
Copy
// Simple cross-chain swap
await executeSwap(
'base-sepolia',
'polygon-amoy',
'USDC',
'USDT',
'100'
);
Same-Chain Swap
Copy
// Swap between tokens on the same chain
await executeSwap(
'base-sepolia',
'base-sepolia',
'USDC',
'USDT',
'50.5'
);
With Custom Status Handling
Copy
const orderId = await submitSwap(bestQuote, signature);
await monitorOrderStatus(orderId, 150, 2000, (status, details) => {
// Custom handling for each status
switch (status) {
case 'received':
console.log('Deposit confirmed on source chain');
notifyUser('Your tokens have been deposited');
break;
case 'fulfilled':
console.log('Tokens delivered on destination chain');
notifyUser('Your swap is complete!');
break;
}
});
Order Lifecycle
Understanding the order status flow:Copy
created → received → fulfilled → completed
↓ ↓ ↓
failed failed failed/cancelled
- created - Order created in system, awaiting source chain deposit
- received - Source chain deposit transaction confirmed
- fulfilled - Destination chain tokens delivered to recipient
- completed - Settlement finalized on-chain
- failed - Order execution failed at any stage
- cancelled - Order was cancelled by user or system
Helper Utilities
Display Quote Comparison
Copy
function displayQuotes(quotes: Quote[], outputToken: TokenSymbol): void {
const decimals = TOKEN_DECIMALS[outputToken];
console.log('Available Quotes:');
quotes.forEach((quote, index) => {
const output = formatUnits(BigInt(quote.outputAmount), decimals);
const rank = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : ' ';
console.log(`${rank} ${index + 1}. ${output} ${outputToken} - LP ${quote.liquidityProviderId}`);
});
}
Calculate Exchange Rate
Copy
function calculateRate(
quote: Quote,
inputAmount: string,
fromToken: TokenSymbol,
toToken: TokenSymbol
): string {
const inputDecimals = TOKEN_DECIMALS[fromToken];
const outputDecimals = TOKEN_DECIMALS[toToken];
const inputNum = parseFloat(formatUnits(BigInt(quote.inputAmount), inputDecimals));
const outputNum = parseFloat(formatUnits(BigInt(quote.outputAmount), outputDecimals));
const rate = outputNum / inputNum;
return `1 ${fromToken} = ${rate.toFixed(6)} ${toToken}`;
}
Estimate Fees
Copy
function estimateFees(inputAmount: string, fromToken: TokenSymbol): string {
const decimals = TOKEN_DECIMALS[fromToken];
const amount = parseUnits(inputAmount, decimals);
// Protocol fee: 0.75% (75 basis points)
const fee = (amount * 75n) / 10000n;
return formatUnits(fee, decimals);
}
Error Handling
Implement comprehensive error handling:Copy
async function executeSwapWithRetry(
params: Parameters<typeof executeSwap>,
maxRetries: number = 3
): Promise<OrderDetails> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await executeSwap(...params);
} catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
// Don't retry on certain errors
if (
lastError.message.includes('insufficient') ||
lastError.message.includes('rejected') ||
lastError.message.includes('denied')
) {
throw lastError;
}
if (attempt < maxRetries) {
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
}
}
}
throw lastError || new Error('Max retries exceeded');
}
Best Practices
1. Always Validate Inputs
Copy
function validateSwapParams(
fromChain: ChainKey,
toChain: ChainKey,
fromToken: TokenSymbol,
toToken: TokenSymbol,
amount: string
): void {
// Validate chains
if (!CHAINS[fromChain] || !CHAINS[toChain]) {
throw new Error('Invalid chain');
}
// Validate tokens exist on chains
if (!TOKENS[fromChain][fromToken]) {
throw new Error(`${fromToken} not available on ${fromChain}`);
}
if (!TOKENS[toChain][toToken]) {
throw new Error(`${toToken} not available on ${toChain}`);
}
// Validate amount
const numAmount = parseFloat(amount);
if (isNaN(numAmount) || numAmount <= 0) {
throw new Error('Invalid amount');
}
}
2. Check User Balance
Copy
async function checkBalance(
chain: ChainKey,
token: TokenSymbol,
userAddress: Address,
requiredAmount: string
): Promise<boolean> {
const publicClient = createPublicClientForChain(chain);
const tokenAddress = TOKENS[chain][token];
const decimals = TOKEN_DECIMALS[token];
const balance = await publicClient.readContract({
address: tokenAddress,
abi: [{
type: 'function',
name: 'balanceOf',
stateMutability: 'view',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }]
}],
functionName: 'balanceOf',
args: [userAddress]
}) as bigint;
const required = parseUnits(requiredAmount, decimals);
return balance >= required;
}
3. Handle Network Switching
Copy
async function ensureCorrectNetwork(chainKey: ChainKey): Promise<void> {
if (typeof window === 'undefined' || !window.ethereum) {
throw new Error('No ethereum provider found');
}
const targetChainId = CHAINS[chainKey].chainId;
const currentChainId = await window.ethereum.request({
method: 'eth_chainId'
}) as string;
if (parseInt(currentChainId, 16) !== targetChainId) {
console.log(`Switching to ${CHAINS[chainKey].name}...`);
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
});
console.log('✓ Network switched');
} catch (error: any) {
// Chain not added to wallet
if (error.code === 4902) {
throw new Error(`Please add ${CHAINS[chainKey].name} to your wallet`);
}
throw error;
}
}
}
Troubleshooting
No Quotes Received
Copy
// Add more detailed logging
async function pollForQuotesVerbose(quoteRequestId: string): Promise<Quote[]> {
console.log('Polling with verbose logging...');
for (let i = 0; i < 15; i++) {
const response = await fetch(
`${API_BASE_URL}/api/v1/quotes?quoteRequestId=${quoteRequestId}`,
{ credentials: 'include' }
);
console.log(`Attempt ${i + 1}:`);
console.log(` Status: ${response.status}`);
if (response.ok) {
const data = await response.json();
console.log(` Quotes: ${data?.data?.quotes?.length || 0}`);
if (data?.data?.quotes?.length > 0) {
return data.data.quotes;
}
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('No quotes received');
}
Transaction Failures
Copy
// Add gas estimation
async function approveWithGasEstimate(
tokenAddress: Address,
spender: Address,
amount: bigint,
userAddress: Address,
chainKey: ChainKey
): Promise<Hash> {
const publicClient = createPublicClientForChain(chainKey);
const walletClient = createWalletClientForChain(chainKey);
// Estimate gas
const gasEstimate = await publicClient.estimateContractGas({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [spender, amount],
account: userAddress
});
console.log(`Estimated gas: ${gasEstimate}`);
// Add 20% buffer
const gasLimit = (gasEstimate * 120n) / 100n;
return walletClient.writeContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [spender, amount],
account: userAddress,
gas: gasLimit
});
}
Next Steps
- Explore the API Reference for detailed endpoint documentation
- Review supported chains and tokens
- Learn about EIP-712 order signing
- Check out WebSocket integration for real-time updates
Support
- Documentation: docs.lidian.finance
- Explorer: explorer.lidian.finance
- API Status: api.lidian.finance/healthz

