// Jupiter swap quote: read-only quote with human amounts. Zero dependencies. import { pathToFileURL } from "node:url"; const API = "https://lite-api.jup.ag"; const KNOWN = { SOL: { mint: "So11111111111111111111111111111111111111112", symbol: "SOL", decimals: 9 }, USDC: { mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", symbol: "USDC", decimals: 6 }, USDT: { mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", symbol: "USDT", decimals: 6 }, }; const MINT_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; async function getJson(url) { const res = await fetch(url, { headers: { accept: "application/json" } }); const body = await res.json().catch(() => null); if (!res.ok) throw new Error(body?.error ?? `Jupiter answered ${res.status}`); return body; } export async function resolveToken(input) { const upper = String(input ?? "").toUpperCase(); if (KNOWN[upper]) return KNOWN[upper]; const known = Object.values(KNOWN).find((k) => k.mint === input); if (known) return known; if (!MINT_RE.test(String(input ?? ""))) throw new Error(`Unknown token "${input}": use SOL, USDC, USDT or a mint address`); const list = await getJson(`${API}/tokens/v2/search?query=${input}`); const t = (Array.isArray(list) ? list : []).find((x) => x?.id === input); if (!t || !Number.isInteger(t.decimals)) throw new Error(`Jupiter does not know mint ${input}`); return { mint: t.id, symbol: t.symbol ?? null, decimals: t.decimals }; } // Whole-token string -> integer base units, without floating point drift. export function toBaseUnits(amount, decimals) { const s = String(amount).trim(); if (!/^\d+(\.\d+)?$/.test(s)) throw new Error(`Invalid amount "${amount}"`); const [whole, frac = ""] = s.split("."); const units = BigInt(whole + frac.padEnd(decimals, "0").slice(0, decimals)); if (units <= 0n) throw new Error("Amount must be above zero"); return units; } export function fromBaseUnits(units, decimals) { const s = BigInt(units).toString().padStart(decimals + 1, "0"); const whole = s.slice(0, s.length - decimals); const frac = s.slice(s.length - decimals).replace(/0+$/, ""); return Number(frac ? `${whole}.${frac}` : whole); } export async function quote(from, to, amount, { slippageBps = 50 } = {}) { if (!from || !to || amount === undefined) throw new Error("Usage: quote.mjs [--slippage-bps 50]"); slippageBps = Math.max(0, Math.min(1000, Math.round(Number(slippageBps) || 0))); const a = await resolveToken(from); const b = await resolveToken(to); if (a.mint === b.mint) throw new Error("From and to are the same token"); const units = toBaseUnits(amount, a.decimals); const q = await getJson( `${API}/swap/v1/quote?inputMint=${a.mint}&outputMint=${b.mint}&amount=${units}&slippageBps=${slippageBps}`, ); const outAmount = fromBaseUnits(q.outAmount, b.decimals); const inAmount = fromBaseUnits(q.inAmount, a.decimals); return { source: "jupiter", fetchedAt: new Date().toISOString(), input: { mint: a.mint, symbol: a.symbol, amount: inAmount }, output: { mint: b.mint, symbol: b.symbol, amount: outAmount, minimum: fromBaseUnits(q.otherAmountThreshold, b.decimals) }, slippageBps, priceImpactPct: Number(q.priceImpactPct) * 100, price: inAmount > 0 ? outAmount / inAmount : null, route: (q.routePlan ?? []).map((r) => ({ dex: r?.swapInfo?.label ?? "unknown", percent: r?.percent ?? null })), contextSlot: q.contextSlot ?? null, }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const args = process.argv.slice(2); const i = args.indexOf("--slippage-bps"); const slippageBps = i >= 0 ? Number(args[i + 1]) : 50; if (i >= 0) args.splice(i, 2); quote(args[0], args[1], args[2], { slippageBps }) .then((r) => console.log(JSON.stringify(r, null, 2))) .catch((e) => { console.log(JSON.stringify({ error: String(e?.message ?? e) })); process.exit(1); }); }