LayerHopdocs

Guides

EVM → Solana swap

Bridge USDC on Base into SOL on Solana end to end — quote, approve, send with viem, and track the order.

Open in ClaudeOpen in ChatGPT

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#

TypeScript
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

recipient must 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#

TypeScript
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#

TypeScript
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#

TypeScript
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.