Repository Public
1// Live READ-ONLY check of the LI.FI swap adapter: real routes from https://li.quest/v1/advanced/routes for small amounts2// (about 30 USD each), with latency, compared with CoinGecko spot prices. Price discovery only: the endpoint returns no3// transaction data, and nothing is built, signed or sent. The addresses are public ones, used as sender/recipient labels.4// Keyless LI.FI allows 75 route requests per two hours per IP: this script spends 8 of them.5// Run: npx tsx scripts/smoke-swap-lifi.mts (LIFI_API_KEY is used when set)6import { NETWORKS, formatUnits } from '@spw/core';7import { checkQuote, httpClient, resolveAsset, type AssetRef, type SwapQuote, type SwapQuoteRequest } from '@spw/swap';8import { createLifiProvider } from '../packages/swap/src/providers/lifi.ts';910const ADDR: Record<string, string> = {11 evm: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', sol: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', tron: 'TV6MuMXfmLbBqPZvBHdwFsDnQeVfnmiuSi',12 btc: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', sui: '0x7d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b58e',13};14const GECKO: Record<string, string> = { ETH: 'ethereum', SOL: 'solana', BTC: 'bitcoin', USDC: 'usd-coin', USDT: 'tether', TRX: 'tron', SUI: 'sui', LINK: 'chainlink', JUP: 'jupiter-exchange-solana', HYPE: 'hyperliquid' };15const tok = (network: string, symbol: string): AssetRef => ({ network, asset: NETWORKS[network]!.tokens.find((t) => t.symbol === symbol)!.id });16const nat = (network: string): AssetRef => ({ network, asset: 'native' });17const USD = 30;18/** [label, from, to, lowest acceptable (guaranteed output value / input value) for the best route] */19const CASES: [string, AssetRef, AssetRef, number][] = [20 ['USDC Base -> USDC Solana (EVM -> SVM)', tok('base', 'USDC'), tok('solana', 'USDC'), 0.97],21 ['ETH Ethereum -> SOL Solana', nat('ethereum'), nat('solana'), 0.96],22 ['SOL Solana -> USDC Arbitrum (SVM -> EVM)', nat('solana'), tok('arbitrum', 'USDC'), 0.96],23 ['ETH Arbitrum -> USDC Arbitrum (same chain)', nat('arbitrum'), tok('arbitrum', 'USDC'), 0.97],24 ['BTC Bitcoin -> ETH Ethereum (UTXO -> EVM)', nat('bitcoin'), nat('ethereum'), 0.95],25 ['USDT TRON -> USDT Ethereum (TVM -> EVM)', tok('tron', 'USDT'), tok('ethereum', 'USDT'), 0.95],26 ['LINK Ethereum -> JUP Solana (swap + bridge)', tok('ethereum', 'LINK'), tok('solana', 'JUP'), 0.95],27 ['SUI Sui -> USDC Base (MVM -> EVM)', nat('sui'), tok('base', 'USDC'), 0.95],28];2930const sym = (a: AssetRef) => resolveAsset(a).def.symbol, dec = (a: AssetRef) => resolveAsset(a).def.decimals;31const fmt = (a: AssetRef, v: bigint) => `${formatUnits(v, dec(a))} ${sym(a)}`;32const prices = await (await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${Object.values(GECKO).join(',')}&vs_currencies=usd`)).json() as Record<string, { usd: number }>;33const px = (a: AssetRef): number => { const p = prices[GECKO[sym(a)]!]?.usd; if (!(p > 0)) throw new Error(`no CoinGecko price for ${sym(a)}`); return p; };34const usd = (a: AssetRef, v: bigint) => (Number(v) / 10 ** dec(a)) * px(a);35console.log('market reference (CoinGecko, USD):', Object.entries(GECKO).map(([s, id]) => `${s} ${prices[id]?.usd}`).join(' '));3637const lifi = createLifiProvider(process.env), http = httpClient('lifi');38console.log('provider', lifi.id, JSON.stringify(lifi.capabilities()));39let failed = 0;40const lat: number[] = [];41for (const [label, from, to, floor] of CASES) {42 const famOf = (a: AssetRef) => ADDR[NETWORKS[a.network]!.family] ?? ADDR.evm!;43 // about 30 USD of the input asset, rounded to 4 significant digits of base units (display amount only; the quote itself is integer end to end)44 const units = (USD / px(from)) * 10 ** dec(from), mag = 10 ** Math.max(0, Math.floor(Math.log10(units)) - 3);45 const req: SwapQuoteRequest = { from, to, amountIn: BigInt(Math.round(units / mag)) * BigInt(mag), sender: famOf(from), recipient: famOf(to), slippageBps: 50 };46 const t0 = performance.now();47 let quotes: SwapQuote[];48 try { quotes = await lifi.quote(req, { signal: AbortSignal.timeout(15_000), http, now: Date.now }); }49 catch (e) { failed++; console.log(`\n${label}\n ERR ${(e as { code?: string }).code ?? ''} ${(e as Error).message}`); continue; }50 const ms = Math.round(performance.now() - t0); lat.push(ms);51 const inUsd = usd(from, req.amountIn);52 console.log(`\n${label}: ${fmt(from, req.amountIn)} (~${inUsd.toFixed(2)} USD), slippage 0.5 % — ${quotes.length} route(s) in ${ms} ms`);53 if (quotes.length === 0) { failed++; console.log(' FAIL no route'); continue; }54 for (const q of quotes) {55 const invalid = checkQuote(q, req, Date.now()); if (invalid) { failed++; console.log(' FAIL invalid quote:', invalid); }56 const onTop = q.fees.filter((f) => !f.includedInOutput).reduce((s, f) => s + usd(f.asset, f.amount), 0), inside = q.fees.filter((f) => f.includedInOutput).reduce((s, f) => s + usd(f.asset, f.amount), 0);57 console.log(` ${q.steps.map((s) => `${s.kind}[${s.via.join(' + ')}] ${sym(s.from)}@${s.from.network}->${sym(s.to)}@${s.to.network}`).join(' , ')}`);58 console.log(` out ${fmt(to, q.amountOut)} min ${fmt(to, q.minAmountOut)} = ${(usd(to, q.amountOut) / inUsd * 100).toFixed(2)} % / ${(usd(to, q.minAmountOut) / inUsd * 100).toFixed(2)} % of input value | ${q.estimatedSeconds} s | fees inside output ~${inside.toFixed(3)} USD, on top (gas) ~${onTop.toFixed(3)} USD | ${q.fees.length} fee item(s) | atomic ${q.atomic} | ${q.executability.class}`);59 }60 // sanity band on the best route: the guaranteed output must be worth 95–103 % (bridges) of the input at market prices61 const best = quotes.reduce((a, b) => (b.minAmountOut > a.minAmountOut ? b : a)), ratio = usd(to, best.minAmountOut) / inUsd, top = usd(to, best.amountOut) / inUsd;62 const ok = ratio >= floor && top <= 1.03;63 if (!ok) failed++;64 console.log(` ${ok ? 'OK ' : 'FAIL'} best guaranteed output = ${(ratio * 100).toFixed(2)} % of market value (band ${(floor * 100).toFixed(0)}–103 %), implied rate 1 ${sym(from)} = ${(Number(best.amountOut) / 10 ** dec(to) / (Number(req.amountIn) / 10 ** dec(from))).toPrecision(6)} ${sym(to)} vs market ${(px(from) / px(to)).toPrecision(6)}`);65}66lat.sort((a, b) => a - b);67console.log(`\nlatency ms: min ${lat[0]} median ${lat[Math.floor(lat.length / 2)]} max ${lat[lat.length - 1]} (n=${lat.length})`);68console.log(failed === 0 ? 'SMOKE OK' : `SMOKE FAILED (${failed})`);69process.exit(failed === 0 ? 0 : 1);70