All articles
cross-chainswapsolanajupiterlifirelaywormholepythbuilders

Painless cross-chain swaps: Solana builder stack map

What makes a cross-chain swap feel local for Solana apps — stack layers (Jupiter, LI.FI, Relay, Wormhole, Pyth), status machine, and a thin quote→status PoC.

devrels.xyz/a/251short link

Painless cross-chain swaps: Solana builder stack map. Users want “type amount, confirm once, money shows up.” Builders still assemble DEX liquidity, bridges or solvers, gas on two chains, and a status machine that does not lie. This piece is for app builders shipping Solana-first product with optional EVM (and other) legs — not a vendor bake-off.

What “painless” can mean

Cross-chain is not same-chain atomicity. A route can still hop bridges and intermediate tokens. Pain drops when product hides the tourism:

Feel vs truth
User feelWhat is actually allowed
One in-amount, one out-amountMulti-hop route; show hops only when risk needs trust
One primary confirmMay still need dest gas top-up or permit; say so up front
Progress that finishesStatus API: pending / stuck / done / failed — not fake % bars
Failure with a money pathRefund, retry, or open support with tx ids — not dead end
Same-chain is instant-ishKeep Jupiter-class path; never force bridge UX for SOL↔SPL

Pain inventory (ship these away)

  1. Wrong destination gas token → stuck after source burn
  2. Silent pending with no ETA or explorer deep links
  3. Four signatures for one “swap” (approve, swap, bridge, claim)
  4. Showing bridge picker to non-power users
  5. Same-chain pair forced through a cross-chain router
  6. Price shown from a stale CEX ticker while route uses different pool math

Stack map (Solana primary)

Layers builders combine
LayerJobExamplesWhen
Same-chain DEX / metaBest SOL/SPL executionJupiterDefault for any pair that never leaves Solana
Omnichain liquidity routerQuote + execute across bridges/DEXsLI.FIApp needs many corridors without owning each bridge
Intent / solverUser signs intent; solvers compete to fillRelay, SODAXPayments/swaps where fill quality matters more than path UI
Messaging / asset bridgeMove messages or lock/mint assetsWormhole, CCTP-class USDC, native bridge product APIsYou own corridor policy or need custom message payloads
Price / riskMark, slippage bounds, liquidation-adjacent UXPythShow fair value and reject toxic quotes before sign
Wallet / sessionSign Solana + optional EVMKit / adapters, embedded walletsAlways — multi-chain auth is half the pain

Rule of thumb: compose up only as far as the corridor needs. Jupiter alone if both legs are Solana. Router or solver when the user starts on Base and ends in USDC on Solana. Wormhole (or a specialist bridge) when you need a specific message or asset path, not a generic “best price” shopper. Pyth sits beside the quote so the UI can refuse nonsense output before the wallet opens.

Chooser for Solana apps

Corridor → default approach
CorridorStart here
SOL ↔ SPL on SolanaJupiter (or meta that already nests Jupiter engines)
EVM stable → Solana USDC/SOLLI.FI quote or Relay/SODAX-class intent; verify Solana in chain list
App-controlled message + custom receiverWormhole messaging + your program; not a swap aggregator alone
USDC-native burn/mint corridorsCCTP-style path (often inside a router)
Show “fair” mid while routingPyth price feed for the pair; separate from execution venue

Status machine (minimum)

text
QUOTE_OK
  → USER_REVIEW (amount, est. out, fee, ETA, steps collapsed)
  → SIGNED_SOURCE
  → IN_FLIGHT   (poll status; show source tx + dest expectation)
  → DONE        (dest balance or receipt link)
  → FAILED      (reason + funds location + retry/refund CTA)
  → STUCK       (past ETA; escalate with both chain explorers)

Integrators that stop at “tx submitted” create support debt. Router products expose status endpoints for a reason — use them (LI.FI /status, Relay execute status). Messaging layers need their own finality rules (Wormhole VAA attested ≠ user-visible balance until redeemed).

Where Pyth and Wormhole sit

Pyth is not a bridge. It is the price layer you use to bound slippage UI, check that a quoted out-amount is sane vs mark, and power any on-chain check that should not trust the frontend. See Pyth: the price layer and what/why.

Wormhole is messaging and (via connected apps) asset movement — guardian-attested messages, NTT-style transfers, and a large connected-chain set. Use it when you need a protocol message or a specific transfer primitive; do not reinvent a multi-bridge shopper on top of raw VAAs unless that is the product. Docs: wormhole.com/docs.

Thin PoC — quote → status

Sketch for a server or agent using an omnichain quote API (LI.FI shape). Swap base URL and chain ids for your vendor; keep the state machine.

typescript
// Thin PoC: painless path = quote + review fields + poll status
// Not production signing — shows the control flow only.

const LIQUEST = "https://li.quest/v1"

type Quote = {
  id?: string
  toolDetails?: { name?: string }
  estimate?: { toAmount?: string; executionDuration?: number }
  transactionRequest?: { data?: string; to?: string; value?: string }
  // …vendor-specific Solana instruction payloads also appear here
}

export async function getCrossChainQuote(p: {
  fromChain: string
  toChain: string
  fromToken: string
  toToken: string
  fromAmount: string // base units
  fromAddress: string
}): Promise<Quote> {
  const q = new URLSearchParams({
    fromChain: p.fromChain,
    toChain: p.toChain,
    fromToken: p.fromToken,
    toToken: p.toToken,
    fromAmount: p.fromAmount,
    fromAddress: p.fromAddress,
  })
  const res = await fetch(`${LIQUEST}/quote?${q}`)
  if (!res.ok) throw new Error(`quote ${res.status}`)
  return res.json()
}

/** Optional: sanity-check quoted out vs Pyth mark (pseudo). */
export function reviewAgainstMark(quotedOutHuman: number, pythMark: number, maxBps = 100) {
  if (!pythMark) return { ok: true as const }
  const bps = Math.abs(quotedOutHuman - pythMark) / pythMark * 10_000
  return bps <= maxBps
    ? { ok: true as const }
    : { ok: false as const, bps, reason: "quote far from mark — refuse or warn hard" }
}

export async function pollStatus(p: {
  txHash: string
  fromChain: string
  toChain: string
  bridge?: string
}): Promise<{ status: string; substatus?: string }> {
  const q = new URLSearchParams({
    txHash: p.txHash,
    fromChain: p.fromChain,
    toChain: p.toChain,
    ...(p.bridge ? { bridge: p.bridge } : {}),
  })
  for (let i = 0; i < 60; i++) {
    const res = await fetch(`${LIQUEST}/status?${q}`)
    const body = await res.json()
    const status = body.status || body.detail || "UNKNOWN"
    if (status === "DONE" || status === "FAILED") return body
    await new Promise((r) => setTimeout(r, 3000))
  }
  return { status: "STUCK" }
}

// App wiring (conceptual):
// 1) if fromChain===toChain===solana → Jupiter, skip this path
// 2) quote = await getCrossChainQuote(...)
// 3) reviewAgainstMark(...) using Pyth HTTP/Hermes or on-chain price
// 4) show collapsed steps + ETA from quote.estimate
// 5) wallet signs quote.transactionRequest / Solana ixs
// 6) pollStatus until DONE | FAILED | STUCK

Same-chain branch (do not skip in production):

bash
# Same-chain SOL/SPL — Jupiter quote shape (illustrative)
# Prefer current Jupiter Metis / Ultra docs for live paths.
curl -s "https://quote-api.jup.ag/v6/quote?inputMint=So11111111111111111111111111111111111111112&outputMint=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v&amount=100000000&slippageBps=50" | head

Anti-patterns

  • Dual-integrate three routers just for logos — pick one meta path
  • Hide “pending” past ETA with no stuck state
  • Ignore destination gas and claim steps until users file tickets
  • Use Pyth as execution venue (it is price, not liquidity)
  • Parse Wormhole yourself when a maintained router already covers the corridor

People and links

Stack references
PieceLink
Jupiterarticle · station.jup.ag
LI.FIarticle · docs.li.fi
Relayarticle
SODAXarticle
Pythprice layer · docs.pyth.network
Wormholewormhole.com/docs · @wormhole

Resources

Keep reading

Get new articles in your inbox

Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.

Painless cross-chain swaps: Solana builder stack map | devrels.xyz