Prerequisites
- A Crossmint wallet with Base set as the default chain
- A production API Key with the scope:
wallets:transactions.create(create in the Production Console) - USDC on Base mainnet in the wallet
- ETH on Base for gas fees (not required if gas sponsorship is enabled)
Bridge Tokens
High level steps to bridging tokens with LI.FI and Crossmint:- Request a bridge quote from the LI.FI API
- Approve the LI.FI router to spend the source token
- Execute the bridge transaction using the quote calldata
- Poll the LI.FI status endpoint until the bridge completes
Cross-chain bridges operate on mainnet only. The wallet must hold sufficient tokens on the source chain and ETH for gas fees.
- React
- Node.js
- React Native
- Swift
- REST
Report incorrect code
Copy
Ask AI
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-ui";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
const LIFI_API = "https://li.quest/v1";
const FROM_CHAIN = "8453"; // Base
const TO_CHAIN = "137"; // Polygon
const FROM_TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const TO_TOKEN = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // USDC on Polygon
const USDC_DECIMALS = 6;
export function BridgeComponent() {
const { wallet } = useWallet();
async function bridge(amount: string) {
if(!wallet) return;
const evmWallet = EVMWallet.from(wallet);
// 1. Get a bridge quote from the LI.FI API
const quoteRes = await fetch(
`${LIFI_API}/quote?` +
new URLSearchParams({
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
fromToken: FROM_TOKEN,
toToken: TO_TOKEN,
fromAmount: parseUnits(amount, USDC_DECIMALS).toString(),
fromAddress: wallet.address,
integrator: "crossmint",
})
);
if (!quoteRes.ok) {
throw new Error(`Quote request failed: ${quoteRes.status}`);
}
const quote = await quoteRes.json();
// 2. Approve the LI.FI router to spend USDC
if (quote.estimate.approvalAddress) {
await evmWallet.sendTransaction({
to: FROM_TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
quote.estimate.approvalAddress,
parseUnits(amount, USDC_DECIMALS),
],
}),
});
}
// 3. Execute the bridge transaction
const tx = await evmWallet.sendTransaction({
to: quote.transactionRequest.to,
data: quote.transactionRequest.data,
value: quote.transactionRequest.value
? BigInt(quote.transactionRequest.value)
: 0n,
});
// 4. Poll the LI.FI status endpoint until completion
let status = "PENDING";
while (status === "PENDING" || status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`${LIFI_API}/status?` +
new URLSearchParams({
txHash: tx.hash,
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
})
);
const s = await statusRes.json();
status = s.status;
}
}
return (
<button onClick={() => bridge("5")}>Bridge Tokens</button>
);
}
Report incorrect code
Copy
Ask AI
import { CrossmintWallets, createCrossmint, EVMWallet } from "@crossmint/wallets-sdk";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
const crossmint = createCrossmint({
apiKey: <YOUR_SERVER_API_KEY>,
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const wallet = await crossmintWallets.getWallet(
"email:user@example.com:evm",
{ chain: "base", signer: { type: "email" } }
);
if(!wallet) throw new Error("Wallet not found");
const evmWallet = EVMWallet.from(wallet);
const LIFI_API = "https://li.quest/v1";
const FROM_CHAIN = "8453"; // Base
const TO_CHAIN = "137"; // Polygon
const FROM_TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const TO_TOKEN = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // USDC on Polygon
const USDC_DECIMALS = 6;
const amount = "5";
// 1. Get a bridge quote from the LI.FI API
const quoteRes = await fetch(
`${LIFI_API}/quote?` +
new URLSearchParams({
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
fromToken: FROM_TOKEN,
toToken: TO_TOKEN,
fromAmount: parseUnits(amount, USDC_DECIMALS).toString(),
fromAddress: wallet.address,
integrator: "crossmint",
})
);
if (!quoteRes.ok) {
throw new Error(`Quote request failed: ${quoteRes.status}`);
}
const quote = await quoteRes.json();
// 2. Approve the LI.FI router to spend USDC
if (quote.estimate.approvalAddress) {
await evmWallet.sendTransaction({
to: FROM_TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
quote.estimate.approvalAddress,
parseUnits(amount, USDC_DECIMALS),
],
}),
});
}
// 3. Execute the bridge transaction
const tx = await evmWallet.sendTransaction({
to: quote.transactionRequest.to,
data: quote.transactionRequest.data,
value: quote.transactionRequest.value
? BigInt(quote.transactionRequest.value)
: 0n,
});
// 4. Poll the LI.FI status endpoint until completion
let status = "PENDING";
while (status === "PENDING" || status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`${LIFI_API}/status?` +
new URLSearchParams({
txHash: tx.hash,
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
})
);
const s = await statusRes.json();
status = s.status;
}
console.log("Bridge complete!");
Report incorrect code
Copy
Ask AI
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-native-ui";
import { View, Button } from "react-native";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
const LIFI_API = "https://li.quest/v1";
const FROM_CHAIN = "8453"; // Base
const TO_CHAIN = "137"; // Polygon
const FROM_TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const TO_TOKEN = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // USDC on Polygon
const USDC_DECIMALS = 6;
export function BridgeComponent() {
const { wallet } = useWallet();
async function bridge(amount: string) {
if(!wallet) return;
const evmWallet = EVMWallet.from(wallet);
// 1. Get a bridge quote from the LI.FI API
const quoteRes = await fetch(
`${LIFI_API}/quote?` +
new URLSearchParams({
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
fromToken: FROM_TOKEN,
toToken: TO_TOKEN,
fromAmount: parseUnits(amount, USDC_DECIMALS).toString(),
fromAddress: wallet.address,
integrator: "crossmint",
})
);
if (!quoteRes.ok) {
throw new Error(`Quote request failed: ${quoteRes.status}`);
}
const quote = await quoteRes.json();
// 2. Approve the LI.FI router to spend USDC
if (quote.estimate.approvalAddress) {
await evmWallet.sendTransaction({
to: FROM_TOKEN,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "approve",
args: [
quote.estimate.approvalAddress,
parseUnits(amount, USDC_DECIMALS),
],
}),
});
}
// 3. Execute the bridge transaction
const tx = await evmWallet.sendTransaction({
to: quote.transactionRequest.to,
data: quote.transactionRequest.data,
value: quote.transactionRequest.value
? BigInt(quote.transactionRequest.value)
: 0n,
});
// 4. Poll the LI.FI status endpoint until completion
let status = "PENDING";
while (status === "PENDING" || status === "NOT_FOUND") {
await new Promise((r) => setTimeout(r, 5000));
const statusRes = await fetch(
`${LIFI_API}/status?` +
new URLSearchParams({
txHash: tx.hash,
fromChain: FROM_CHAIN,
toChain: TO_CHAIN,
})
);
const s = await statusRes.json();
status = s.status;
}
}
return (
<View>
<Button title="Bridge Tokens" onPress={() => bridge("5")} />
</View>
);
}
Report incorrect code
Copy
Ask AI
import CrossmintClient
import Wallet
import Foundation
let sdk = CrossmintSDK.shared
let wallet = try await sdk.crossmintWallets
.getOrCreateWallet(
chain: .base,
signer: .email(email: "user@example.com")
)
let evmWallet = try EVMWallet.from(wallet: wallet)
let fromChain = "8453"
let toChain = "137"
let fromToken = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
let toToken = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"
let amount = "5000000" // 5 USDC (6 decimals)
// 1. Get a bridge quote from the LI.FI API
var quoteURL = URLComponents(string: "https://li.quest/v1/quote")!
quoteURL.queryItems = [
URLQueryItem(name: "fromChain", value: fromChain),
URLQueryItem(name: "toChain", value: toChain),
URLQueryItem(name: "fromToken", value: fromToken),
URLQueryItem(name: "toToken", value: toToken),
URLQueryItem(name: "fromAmount", value: amount),
URLQueryItem(name: "fromAddress", value: wallet.address),
URLQueryItem(name: "integrator", value: "crossmint"),
]
let (quoteData, quoteResponse) = try await URLSession.shared.data(from: quoteURL.url!)
guard let httpResponse = quoteResponse as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let quote = try JSONSerialization.jsonObject(with: quoteData) as! [String: Any]
let estimate = quote["estimate"] as! [String: Any]
let txRequest = quote["transactionRequest"] as! [String: Any]
// 2. Approve the LI.FI router to spend USDC
// Encodes ERC-20 approve(address,uint256) — selector 0x095ea7b3
// Note: UInt64 supports up to ~18.4e18. For tokens with 18 decimals
// and large amounts, use a big-integer hex conversion instead.
func leftPad(_ str: String, toLength len: Int) -> String {
String(repeating: "0", count: max(0, len - str.count)) + str
}
if let approvalAddress = estimate["approvalAddress"] as? String {
let addr = leftPad(String(approvalAddress.dropFirst(2)), toLength: 64)
let amt = leftPad(String(UInt64(amount)!, radix: 16), toLength: 64)
let approveData = "0x095ea7b3" + addr + amt
let _ = try await evmWallet.sendTransaction(
to: fromToken,
value: "0",
data: approveData
)
}
// 3. Execute the bridge transaction
let result = try await evmWallet.sendTransaction(
to: txRequest["to"] as! String,
value: (txRequest["value"] as? String) ?? "0",
data: txRequest["data"] as! String
)
// 4. Poll the LI.FI status endpoint until completion
var bridgeStatus = "PENDING"
while bridgeStatus == "PENDING" || bridgeStatus == "NOT_FOUND" {
try await Task.sleep(nanoseconds: 5_000_000_000)
var statusURL = URLComponents(string: "https://li.quest/v1/status")!
statusURL.queryItems = [
URLQueryItem(name: "txHash", value: result.hash),
URLQueryItem(name: "fromChain", value: fromChain),
URLQueryItem(name: "toChain", value: toChain),
]
let (statusData, _) = try await URLSession.shared.data(from: statusURL.url!)
let statusJSON = try JSONSerialization.jsonObject(with: statusData) as! [String: Any]
bridgeStatus = statusJSON["status"] as! String
}
Transactions must be approved by one of the wallet’s signers.
The SDK handles this automatically, but with the REST API you must approve the transaction to complete it.
Get a bridge quote from the LI.FI API
Call the LI.FI API The response includes
/quote endpoint with the source and destination chain, token addresses, and amount.Report incorrect code
Copy
Ask AI
curl --request GET \
--url 'https://li.quest/v1/quote?fromChain=8453&toChain=137&fromToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&toToken=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359&fromAmount=5000000&fromAddress=<wallet-address>&integrator=crossmint' \
--header 'Content-Type: application/json'
estimate.approvalAddress and transactionRequest with the bridge calldata.Send the approve and bridge transactions
Use the quote response to send two transactions via the Crossmint REST API:
- Approve — call the USDC contract with ERC-20
approvecalldata targetingestimate.approvalAddress - Bridge — send
transactionRequest.to,transactionRequest.data, andtransactionRequest.value
Poll for bridge status
Call the LI.FI The bridge is complete when
/status endpoint every five seconds until the bridge completes.Report incorrect code
Copy
Ask AI
curl --request GET \
--url 'https://li.quest/v1/status?txHash=<bridge-tx-hash>&fromChain=8453&toChain=137' \
--header 'Content-Type: application/json'
status is DONE. If FAILED, check the transaction on the block explorer for details.Customizing the Bridge
You can bridge any token pair that LI.FI supports by changing the chain and token constants. You can also perform cross-chain swaps — changing both the asset and the chain in a single operation:Report incorrect code
Copy
Ask AI
const FROM_CHAIN = "1"; // Ethereum
const TO_CHAIN = "42161"; // Arbitrum
const FROM_TOKEN =
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC on Ethereum
const TO_TOKEN =
"0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // USDC on Arbitrum
Troubleshooting
Transaction fails with insufficient balance
Transaction fails with insufficient balance
Verify the wallet holds enough USDC on Base and ETH for gas fees.
Use the Check Balances guide to confirm token balances before bridging.
ERC-20 approval transaction is rejected
ERC-20 approval transaction is rejected
Ensure the approval amount is greater than or equal to the bridge amount.
If a previous approval exists for a smaller amount, send a new approval transaction with the correct value.
Verify the API key has the
wallets:transactions.create scope.Bridge status stays PENDING for a long time
Bridge status stays PENDING for a long time
Cross-chain bridges can take several minutes depending on the route and chain congestion.
The LI.FI
/status endpoint returns PENDING until both the source and destination chain transactions confirm.
If the status does not change after 10 minutes, check the source transaction on a block explorer to verify it was included.
