Guides
EVM → Solana swap
Bridge USDC on Base into SOL on Solana end to end — quote, approve, send with viem, and track the order.
This guide moves 100 USDC on Base into SOL on Solana. Relay, deBridge and LI.FI all serve EVM → Solana; LayerHop picks the best one.
1. Quote#
import { LayerHop, type Quote } from "@layerhop/sdk";
const lh = new LayerHop({ baseUrl: process.env.LAYERHOP_API_URL });
const res = await lh.quote({
srcChain: "base",
srcToken: "USDC",
amount: "100000000", // 100 USDC, 6 decimals
dstChain: "solana",
dstToken: "SOL",
sender: evmAddress, // the connected EVM wallet
recipient: solanaAddress, // a Solana address — not an EVM one
slippageBps: 50,
});
const quote: Quote = res.best;Show the user quote.dstAmount and quote.dstAmountMin in SOL (9 decimals), quote.estimatedSeconds, quote.provider and quote.fees.ourFeeBps. Other routes are in res.quotes.
Warning
recipientmust be a valid address on the destination chain. Validate it before quoting; a wrong-family address will be rejected by providers or, worse, deliver to an address the user does not control.
2. Build#
const tx = await lh.buildTx(quote.quoteId);
if (tx.vm !== "evm") throw new Error("expected an EVM transaction for an EVM source");The source is EVM, so the transaction is an EvmTx on tx.chainId (8453). Selling USDC means it usually carries an approval.
3. Approve and send with viem#
import { createPublicClient, createWalletClient, custom, erc20Abi, http, type Address, type Hex } from "viem";
import { base } from "viem/chains";
const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum!) });
const pub = createPublicClient({ chain: base, transport: http() });
const [account] = await wallet.requestAddresses();
if ((await wallet.getChainId()) !== tx.chainId) await wallet.switchChain({ id: tx.chainId });
if (tx.approval) {
const spender = tx.approval.spender as Address;
const token = tx.approval.token as Address;
const need = BigInt(tx.approval.amount);
const have = await pub.readContract({ address: token, abi: erc20Abi, functionName: "allowance", args: [account!, spender] });
if (have < need) {
const h = await wallet.writeContract({ account: account!, address: token, abi: erc20Abi, functionName: "approve", args: [spender, need] });
await pub.waitForTransactionReceipt({ hash: h });
}
}
const srcTxHash = await wallet.sendTransaction({
account: account!,
to: tx.to as Address,
data: tx.data as Hex,
value: BigInt(tx.value), // non-zero for some providers (e.g. deBridge's fixed fee) or native-coin input
});
await pub.waitForTransactionReceipt({ hash: srcTxHash });4. Track#
for await (const s of lh.watch(quote.provider, srcTxHash)) {
if (s.state === "filled") console.log("Delivered:", `https://solscan.io/tx/${s.dstTxHash}`);
if (s.state === "refunded") console.log("Refunded to the sender on Base");
}Cross-chain providers are tracked by the source transaction hash. Expect unknown or pending first; Relay and deBridge typically fill in seconds.
Handling failures#
| Step | Error | Do this |
|---|---|---|
| Quote | NO_ROUTE |
Retry shortly, or change amount/token. |
| Build | QUOTE_EXPIRED, QUOTE_NOT_FOUND |
Re-quote, show the new price, ask the user to confirm again. |
| Approve/send | Wallet rejection | Nothing was spent; let the user retry. |
| Track | PROVIDER_*, RATE_LIMITED |
Keep polling with backoff; the order is on chain regardless. |
More in Error handling & retries.