Demo Code
Front-end code example
import React, { useState, useEffect } from 'react';
import {
supportedChains,
switchToSupportedChain,
getCurrentChainId,
isChainSupported
} from '../utils/chainUtils';
import './Pages.css';
import axios from 'axios';
import { BigNumber } from 'bignumber.js';
import { ethers, Interface } from 'ethers';
import { SignatureTransfer } from "@uniswap/permit2-sdk";
import { Permit2Abi } from '../utils/Permit2';
import { OpenSwapPermitAbi } from '../utils/OpenSwapPermitAbi';
/**
* Swap Component - Handles token swapping functionality
* Allows users to exchange tokens using OpenOcean API
*/
const Swap = () => {
const baseUrl = 'https://open-api.openocean.finance/v4'
// const baseUrl = 'https://openapi-test.openocean.finance/v4'
// Supported chains for gasless swaps
const gaslessChain = ['arbitrum', 'bsc', 'sonic', 'base', 'sei', 'eth', 'hyperevm', 'avax', 'uni']
// Token state for input and output tokens
const [inToken, setInToken] = useState({
address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
symbol: 'USDC',
decimals: 6
});
const [outToken, setOutToken] = useState({
address: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
symbol: 'USDT',
decimals: 6
});
// Ethereum provider and transaction state
const [provider, setProvider] = useState(null);
const [fromAmount, setFromAmount] = useState('0.1');
const [toAmount, setToAmount] = useState('');
const [slippage, setSlippage] = useState(0.5);
const [isLoading, setIsLoading] = useState(false);
const [gasPrice, setGasPrice] = useState(10000);
// Quote data from API
const [quote, setQuote] = useState({
inAmount: 0,
outAmount: 0,
inAmountDecimals: 0,
outAmountDecimals: 0,
inTokenAddress: '',
outTokenAddress: '',
gasPrice: 0
});
// Wallet connection state
const [isWalletConnected, setIsWalletConnected] = useState(false);
const [walletAccount, setWalletAccount] = useState('');
const [currentChainId, setCurrentChainId] = useState(null);
const [tokens, setTokens] = useState([]);
// Chain configuration
const chain = {
chainId: 8453,
chainName: 'base',
chainCode: 'base' // Add chainCode for gasless chain validation
}
// Initialize component on mount
useEffect(() => {
checkWalletConnection();
getTokens();
getQuote();
}, []);
/**
* Check if wallet is already connected on component mount
*/
const checkWalletConnection = async () => {
if (typeof window.ethereum !== 'undefined') {
try {
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (accounts.length > 0) {
await connectWallet();
}
} catch (error) {
console.error('Error checking wallet connection:', error);
}
}
};
/**
* Connect to MetaMask wallet and handle chain switching
*/
const connectWallet = async () => {
setIsLoading(true);
try {
if (typeof window.ethereum === 'undefined') {
alert('Please install MetaMask!');
return;
}
await window.ethereum.request({
method: 'eth_requestAccounts'
});
// Get current chain ID
const chainId = await getCurrentChainId();
setCurrentChainId(chainId);
// Check if current chain is supported
if (chainId != chain.chainId) {
const shouldSwitch = window.confirm(
`Current chain (${chainId}) is not supported. Would you like to switch to Base?`
);
if (shouldSwitch) {
const switched = await switchToSupportedChain(chain.chainId);
if (!switched) {
alert('Failed to switch to supported chain. Please switch manually.');
setIsLoading(false);
return;
}
// Update chain ID after switch
const newChainId = await getCurrentChainId();
setCurrentChainId(newChainId);
} else {
setIsLoading(false);
return;
}
}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const feeData = await provider.getFeeData();
const gasPrice = feeData.gasPrice;
setGasPrice(Number(gasPrice));
setProvider(provider);
setWalletAccount(address);
setIsWalletConnected(true);
console.log('Wallet connected:', address, 'on chain:', chainId);
// Listen for account changes
window.ethereum.on('accountsChanged', (newAccounts) => {
if (newAccounts.length > 0) {
connectWallet();
} else {
disconnectWallet();
}
});
// Listen for chain changes
window.ethereum.on('chainChanged', (chainId) => {
const newChainId = parseInt(chainId, 16);
setCurrentChainId(newChainId);
if (!isChainSupported(newChainId)) {
alert(`Chain ${newChainId} is not supported. Please switch to a supported chain.`);
setIsWalletConnected(false);
}
});
} catch (error) {
console.error('Error connecting wallet:', error);
alert('Failed to connect wallet: ' + error.message);
} finally {
setIsLoading(false);
}
};
/**
* Disconnect wallet and reset state
*/
const disconnectWallet = () => {
setWalletAccount('');
setIsWalletConnected(false);
setCurrentChainId(null);
console.log('Wallet disconnected');
};
/**
* Check if token is native (ETH/WETH)
*/
const isNativeToken = (tokenAddress, chainId) => {
return tokenAddress === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
}
/**
* Execute token swap transaction
* Handles token approval and swap execution
*/
const handleSwap = async () => {
if (!isWalletConnected) {
alert('Please connect your wallet first!');
return;
}
if (!getIsGasLessChain(chain.chainCode)) {
alert('This chain is not supported for gasless swaps');
return;
}
try {
setIsLoading(true);
const fromAmountDecimals = fromAmount * 10 ** inToken.decimals;
let params = {
inTokenAddress: inToken.address,
outTokenAddress: outToken.address,
amountDecimals: fromAmountDecimals,
slippage: slippage * 100,
gasPrice: gasPrice,
account: walletAccount,
}
// Get swap quote from OpenOcean API
let url = `${baseUrl}/gasless/${chain.chainId}/quote?${Object.entries(params).map(([key, value]) => `${key}=${value}`).join('&')}`
let res = await axios.get(url);
// Destructure API response data, rename inToken to avoid conflict with component state
const { inAmount, data, to, fees, flags, hash } = res.data.data;
let swapParams = {
from: walletAccount,
to,
data,
gasPrice: gasPrice
};
const permit2Address = await getPermit2ContractAddress(chain.chainCode);
// Handle token approval for non-native tokens
if (!isNativeToken(inToken.address, chain.chainId)) {
// let approveAmount = fromAmountDecimals;
let approveAmount = BigNumber('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF').toFixed(0);
await checkTokenApprove(inToken.address, permit2Address, fromAmountDecimals, gasPrice, approveAmount);
} else {
swapParams.value = inAmount;
}
const permitSign = await setPermit2Signer(inAmount, inToken.address, permit2Address);
if (!permitSign) {
throw new Error('User rejected the request.');
}
const { permit, nonce, deadline } = permitSign || {};
let gasLessData = {
from: walletAccount,
to: to,
data: data,
amountDecimals: params.amountDecimals,
feeAmount1: fees[0] ? (+fees[0].inFeeAmount * (10 ** fees[0].decimals)) : 0,
feeAmount2: fees[1] ? (+fees[1].inFeeAmount * (10 ** fees[1].decimals)) : 0,
flag: flags,
gasPriceDecimals: gasPrice,
deadline: deadline,
inToken: inToken.address,
outToken: outToken.address,
nonce: Number(nonce),
permit: permit,
// hash: hash,
// usd_valuation: Number((Number(params.amountDecimals) * Number(inToken.usd)).toFixed(4))
}
let resGasless = await axios.post(`${baseUrl}/gasless/${chain.chainId}/swap`, gasLessData)
if (!resGasless.data.orderHash) throw new Error(resGasless.msg || resGasless.err || 'Transaction error')
let hashSwap = await getGasHashTimeout(resGasless.data.orderHash, 0)
if (hashSwap) {
console.log('Transaction successful:', hashSwap);
alert('Swap completed successfully!');
setFromAmount('');
setToAmount('');
}
} catch (error) {
console.error('Error executing swap:', error);
alert('Failed to execute swap: ' + error.message);
} finally {
setIsLoading(false);
}
};
/**
* Poll for transaction hash with timeout
* @param {string} orderHash - Order hash from gasless swap
* @param {number} i - Retry counter
* @returns {string|null} Transaction hash or null if timeout
*/
const getGasHashTimeout = async (orderHash, i) => {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
let res = await axios.get(`${baseUrl}/gasless/${chain.chainId}/order?orderHash=${orderHash}`)
if (!res.data.data) throw new Error(res.data.msg || 'No data received')
if (res.data.data.err) throw new Error(res.data.data.err)
if (res.data.data.hash) return res.data.data.hash
if (i > 30) return null // Timeout after 60 seconds (30 * 2s)
return getGasHashTimeout(orderHash, i + 1)
} catch (error) {
console.error('Error polling transaction hash:', error);
if (i > 30) return null
return getGasHashTimeout(orderHash, i + 1)
}
}
/**
* Check if current chain supports gasless swaps
*/
const getIsGasLessChain = (chainCode) => {
return gaslessChain.indexOf(chainCode) !== -1
}
/**
* Get Permit2 contract address for the specified chain
*/
const getPermit2ContractAddress = async (chainCode) => {
const contract = {
"eth": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
"arbitrum": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
"base": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
}[chainCode] || "0x000000000022D473030F116dDEE9F6B43aC78BA3"
return contract
}
/**
* Create and sign Permit2 signature for gasless swap
* @param {string} amount - Amount to permit
* @param {string} token - Token address
* @param {string} permit2Address - Permit2 contract address
* @returns {Object|null} Permit data or null if failed
*/
const setPermit2Signer = async (amount, token, permit2Address) => {
try {
// OpenOcean gasless spender address
let spender = '0xB1DD8E9ebbF5F150B75642D1653dF0dacd0bfF47'
let deadline = Math.floor(Date.now() / 1000) + 60 * 30; // 30 minutes from now
let nonce = await getPermit2Nonce(spender);
const permitSingle = {
permitted: {
token: token,
amount: amount,
},
spender,
nonce,
deadline: deadline,
};
const { domain, types, values } = SignatureTransfer.getPermitData(
permitSingle,
permit2Address,
chain.chainId
);
let signer = await provider.getSigner();
const signature = await signer.signTypedData(domain, types, values);
const permitTransferfromData = [
[[token, amount], nonce, deadline],
[spender, amount],
walletAccount,
signature,
];
const PERMIT2_INTERFACE = new Interface(Permit2Abi);
const data = PERMIT2_INTERFACE.encodeFunctionData(
"0x30f28b7a", // permitTransferFrom function selector
permitTransferfromData
);
return { permit: data, nonce, deadline, spender };
} catch (e) {
console.error('Error in setPermit2Signer:', e);
return null;
}
};
/**
* Get next nonce for Permit2 signature
* @param {string} spender - Spender address
* @returns {number} Next nonce value
*/
const getPermit2Nonce = async (spender) => {
try {
const tokenContract = new ethers.Contract(spender, OpenSwapPermitAbi, provider);
const nonce = await tokenContract.permit2NextNonce(walletAccount)
return nonce
} catch (error) {
console.error('Error getting Permit2 nonce:', error);
throw error;
}
};
/**
* Handle input amount changes and trigger quote update
*/
const handleFromAmountChange = (value) => {
if (Number(value) <= 0) {
return;
}
setFromAmount(value);
setToAmount('');
if (fromAmount) {
getQuote(value);
}
};
/**
* Switch input and output tokens
*/
const handleTokenSwitch = () => {
setInToken(outToken);
setOutToken(inToken);
// setFromAmount(toAmount);
setToAmount('');
getQuote();
};
/**
* Format wallet address for display
*/
const formatAddress = (address) => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
/**
* Fetch available tokens from OpenOcean API
*/
const getTokens = async () => {
let url = `${baseUrl}/${chain.chainId}/tokenList`
const { data } = await axios.get(url);
console.log(data.data);
setTokens(data.data);
};
/**
* Get swap quote from OpenOcean API
*/
const getQuote = async (value) => {
if (!fromAmount) return;
setIsLoading(true);
try {
let url = `${baseUrl}/gasless/${chain.chainId}/quote?inTokenAddress=${inToken.address}&outTokenAddress=${outToken.address}&amountDecimals=${(value || fromAmount) * 10 ** inToken.decimals}&slippage=${slippage * 100}&gasPrice=${gasPrice}`
const { data } = await axios.get(url);
console.log(data);
setQuote(data.data);
setToAmount(Number(data.data.outAmount) / (10 ** outToken.decimals));
} catch (error) {
console.error('Error getting quote:', error);
setToAmount('');
} finally {
setIsLoading(false);
}
}
/**
* Change selected token (input or output)
*/
const changeToken = (token, type) => {
if (type === 'from') {
setInToken(token);
} else {
setOutToken(token);
}
setToAmount('');
getQuote();
}
/**
* Check and handle token approval for swap contract
*/
const checkTokenApprove = async (
tokenAddress,
contractAddress,
amount,
gasPrice,
approveAmount
) => {
// Get current allowance
const allowance = await getTokenAllowance(tokenAddress, contractAddress);
if (BigNumber(allowance).comparedTo(amount) < 0) {
return await approveToken(
tokenAddress,
contractAddress,
approveAmount || amount,
gasPrice
);
}
return true;
};
/**
* Get token allowance for a specific contract
*/
const getTokenAllowance = async (tokenAddress, contractAddress) => {
try {
// ERC20 ABI for allowance function
const erc20Abi = [
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"type": "function"
}
];
const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, provider);
const allowance = await tokenContract.allowance(walletAccount, contractAddress);
return allowance
} catch (error) {
console.error('Error checking token approval:', error);
return 0;
}
};
/**
* Approve token spending for swap contract
*/
const approveToken = async (tokenAddress, contractAddress, maxAmount, gasPrice) => {
try {
// ERC20 ABI for approve function
const erc20Abi = [
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
}
];
const signer = await provider.getSigner();
const tokenContract = new ethers.Contract(tokenAddress, erc20Abi, signer);
const tx = await tokenContract.approve(contractAddress, maxAmount, {
gasPrice: gasPrice
});
const receipt = await tx.wait(); // Wait for transaction confirmation
console.log('Approval successful:', receipt);
alert('Token approved successfully!');
return true;
} catch (error) {
console.error('Error approving token:', error);
alert('Failed to approve token: ' + error.message);
return false;
}
};
return (
<div className="page">
<h1>Swap</h1>
<div className="page-content">
<p>This is the Swap page for token exchange functionality.</p>
{/* Wallet Connection Section */}
<div className="wallet-section">
{!isWalletConnected ? (
<div className="wallet-notice">
<p>⚠️ Please connect your MetaMask wallet to use Swap functionality</p>
<button
className="connect-button"
onClick={connectWallet}
disabled={isLoading}
>
{isLoading ? 'Connecting...' : 'Connect MetaMask'}
</button>
</div>
) : (
<div className="wallet-status">
<p>✅ Connected: {formatAddress(walletAccount)}</p>
<p>🌐 Chain: {supportedChains[currentChainId]?.name || `Chain ${currentChainId}`}</p>
<button
className="disconnect-button"
onClick={disconnectWallet}
>
Disconnect
</button>
</div>
)}
</div>
{/* Swap Form */}
<div className="swap-form">
<div>
<span>
Trading Pair <i style={{ fontSize: '0.8rem', color: 'red' }}>{chain.chainName}</i>
</span>
<span>{inToken.symbol}/{outToken.symbol}</span>
</div>
{/* Input Token Section */}
<div className="form-group">
<label>From</label>
<div className="token-input-group">
<input
type="text"
placeholder="Enter token amount"
value={fromAmount}
onChange={(e) => handleFromAmountChange(e.target.value)}
disabled={!isWalletConnected}
/>
<select
value={inToken.address}
onChange={(e) => changeToken(tokens.find(token => token.address === e.target.value), 'from')}
disabled={!isWalletConnected}
>
{tokens.map((token) => (
<option key={token.address} value={token.address}>{token.symbol}</option>
))}
</select>
</div>
</div>
{/* Token Switch Arrow */}
<div className="swap-arrow" onClick={handleTokenSwitch}>
</div>
{/* Output Token Section */}
<div className="form-group">
<label>To</label>
<div className="token-input-group">
<input
type="text"
placeholder="Output amount"
value={toAmount}
readOnly
disabled
style={{ background: 'rgba(0,0,0,0.1)', cursor: 'not-allowed' }}
/>
<select
value={outToken.address}
onChange={(e) => changeToken(tokens.find(token => token.address === e.target.value), 'to')}
disabled={!isWalletConnected}
>
{tokens.map((token) => (
<option key={token.address} value={token.address}>{token.symbol}</option>
))}
</select>
</div>
</div>
{/* Slippage Settings */}
<div className="form-group" style={{ marginTop: '20px' }}>
<label>Slippage Tolerance (%)</label>
<input
type="number"
placeholder="0.5"
value={slippage}
onChange={(e) => setSlippage(parseFloat(e.target.value) || 0.5)}
min="0.1"
max="50"
step="0.1"
disabled={!isWalletConnected}
/>
</div>
{/* Swap Details Display */}
{fromAmount && toAmount && (
<div className="swap-details">
<h3>Swap Details</h3>
<div className="detail-row">
<span>Exchange Rate:</span>
<span>1 {inToken.symbol} = {(toAmount / Number(fromAmount)).toFixed(6)} {outToken.symbol}</span>
</div>
<div className="detail-row">
<span>You will receive:</span>
<span>{toAmount} {outToken.symbol}</span>
</div>
<div className="detail-row">
<span>Network:</span>
<span>{supportedChains[currentChainId]?.name || `Chain ${currentChainId}`}</span>
</div>
</div>
)}
{/* Swap Button */}
<button
className="swap-button"
onClick={handleSwap}
disabled={!isWalletConnected || !fromAmount || isLoading}
>
{isLoading ? 'Processing...' :
!isWalletConnected ? 'Connect Wallet to Swap' :
!fromAmount ? 'Enter Amount to Swap' : 'Swap'}
</button>
</div>
</div>
</div>
);
};
export default Swap;
Last updated