Repository Public
1// Jupiter adapter against REAL answers recorded from https://api.jup.ag/swap/v2 (scripts/record-swap-jupiter-fixtures.mts).2// No network: the real httpClient runs over a fetch stub that replays the recorded status + body.3import { readFileSync } from 'node:fs';4import { describe, expect, it } from 'vitest';5import { NETWORKS } from '@spw/core';6import { checkQuote, DEFAULT_POLICY, httpClient, ProviderError, ProviderRuntime, QuoteAggregator, type AssetRef, type ProviderContext, type SwapQuote, type SwapQuoteRequest } from '../src/index.ts';7import { createProviders } from '../src/providers/index.ts';8import { createJupiterProvider, JUPITER_API_BASE, JUPITER_KEYLESS_POLICY, JUPITER_PROGRAM, jupiterBuildSchema, WSOL_MINT, type JupiterBuild } from '../src/providers/jupiter.ts';9import { JUPITER_DEX_PROGRAMS } from '../src/providers/jupiter-dexes.ts';1011interface Fixture { recordedAt: string; url: string; status: number; body: unknown }12const fixture = (name: string): Fixture => JSON.parse(readFileSync(new URL(`./fixtures/jupiter/${name}.json`, import.meta.url), 'utf8')) as Fixture;13const clone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T;1415const TOKEN_2022 = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';16const OTHER_WALLET = '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM';17const token = (symbol: string) => NETWORKS.solana!.tokens.find((t) => t.symbol === symbol)!;18const ref = (symbol: string): AssetRef => ({ network: 'solana', asset: symbol === 'SOL' ? 'native' : token(symbol).id });19const SOL = ref('SOL'), USDC = ref('USDC'), PYUSD = ref('PYUSD'), USDG = ref('USDG');20const refOfMint = (mint: string): AssetRef => ({ network: 'solana', asset: mint === WSOL_MINT ? 'native' : mint });21const decimals = (a: AssetRef) => (a.asset === 'native' ? NETWORKS.solana!.native.decimals : NETWORKS.solana!.tokens.find((t) => t.id === a.asset)!.decimals);2223/** the request that produced a recorded answer, rebuilt from the recorded URL */24function requestOf(f: Fixture): SwapQuoteRequest {25 const q = new URL(f.url).searchParams, taker = q.get('taker')!;26 return { from: refOfMint(q.get('inputMint')!), to: refOfMint(q.get('outputMint')!), amountIn: BigInt(q.get('amount')!), sender: taker, recipient: taker, slippageBps: Number(q.get('slippageBps')) };27}28/** replay a status + body through the real HTTP client; records what the adapter asked for */29function replay(status: number, body: unknown, calls: { url: string; init?: RequestInit }[] = []) {30 const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => { calls.push({ url: String(url), init }); return new Response(typeof body === 'string' ? body : JSON.stringify(body), { status }); }) as typeof fetch;31 return { http: httpClient('jupiter', fetchImpl), calls };32}33const ctxFor = (f: Fixture, http: ProviderContext['http'], now = Date.parse(f.recordedAt) + 400): ProviderContext => ({ signal: new AbortController().signal, http, now: () => now });34const provider = createJupiterProvider();35async function quoteFixture(name: string, mutate?: (b: JupiterBuild) => unknown, req?: (r: SwapQuoteRequest) => SwapQuoteRequest): Promise<{ quotes: SwapQuote[]; f: Fixture; r: SwapQuoteRequest; calls: { url: string; init?: RequestInit }[] }> {36 const f = fixture(name); let body = clone(f.body);37 if (mutate) { const out = mutate(body as JupiterBuild); if (out !== undefined) body = out; }38 const r = req ? req(requestOf(f)) : requestOf(f), { http, calls } = replay(f.status, body);39 return { quotes: await provider.quote(r, ctxFor(f, http)), f, r, calls };40}41const SUCCESS = ['sol-usdc', 'usdc-sol', 'sol-pyusd', 'pyusd-sol', 'usdg-usdc', 'usdc-usdg', 'usdc-wbtc', 'usdc-bonk', 'wbtc-usdc-high-impact'] as const;4243describe('jupiter: capabilities and pair filter (local, no network)', () => {44 it('is registered, keyless, Solana only, same-chain only', () => {45 const all = createProviders({});46 expect(all.map((p) => p.id)).toContain('jupiter');47 const cap = provider.capabilities();48 expect(cap).toMatchObject({ networks: ['solana'], crossChain: false, needsKey: false, configured: true, kinds: ['swap'] });49 expect(cap.docs).toMatch(/^https:\/\/developers\.jup\.ag\//);50 expect(provider.name).toContain('Metis'); // labelling required by Jupiter's API licence51 expect(JUPITER_API_BASE).toBe('https://api.jup.ag/swap/v2');52 expect(JUPITER_KEYLESS_POLICY.ratePerSec).toBeLessThanOrEqual(0.5);53 });54 it('accepts SOL and every registry token on Solana, both ways, including the Token-2022 mints', () => {55 expect(token('PYUSD').program).toBe('token2022'); expect(token('USDG').program).toBe('token2022');56 for (const t of NETWORKS.solana!.tokens) { const a = { network: 'solana', asset: t.id }; expect(provider.supportsPair(SOL, a)).toBe(true); expect(provider.supportsPair(a, SOL)).toBe(true); }57 expect(provider.supportsPair(PYUSD, USDG)).toBe(true);58 });59 it('refuses other networks, cross-chain pairs, identical assets and mints outside the registry', () => {60 const usdcBase = { network: 'base', asset: NETWORKS.base!.tokens.find((t) => t.symbol === 'USDC')!.id };61 expect(provider.supportsPair(SOL, usdcBase)).toBe(false);62 expect(provider.supportsPair(usdcBase, { network: 'base', asset: 'native' })).toBe(false);63 expect(provider.supportsPair(SOL, SOL)).toBe(false);64 expect(provider.supportsPair(USDC, USDC)).toBe(false);65 expect(provider.supportsPair(SOL, { network: 'solana', asset: WSOL_MINT })).toBe(false); // wrapped SOL is not a wallet asset66 expect(provider.supportsPair(SOL, { network: 'solana', asset: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB264' })).toBe(false);67 expect(provider.supportsPair({ network: 'nowhere', asset: 'native' }, SOL)).toBe(false);68 });69});7071describe('jupiter: exact mapping of recorded answers', () => {72 it.each(SUCCESS)('%s: one HTTP round, exact amounts, guaranteed minimum, steps, validity', async (name) => {73 const { quotes, f, r, calls } = await quoteFixture(name);74 const b = f.body as JupiterBuild;75 expect(calls).toHaveLength(1);76 expect(calls[0]!.url).toBe(f.url); // the adapter asks exactly what was recorded: mints, base-unit amount, taker, slippageBps77 expect(calls[0]!.init?.method).toBe('GET');78 expect(quotes).toHaveLength(1);79 const q = quotes[0]!;80 expect(q.provider).toBe('jupiter'); expect(q.request).toBe(r);81 expect(q.amountOut).toBe(BigInt(b.outAmount));82 expect(q.minAmountOut).toBe(BigInt(b.otherAmountThreshold));83 // slippage exactly as Jupiter defines it: outAmount − floor(outAmount × slippageBps / 10 000)84 expect(q.minAmountOut).toBe(q.amountOut - (q.amountOut * BigInt(r.slippageBps)) / 10_000n);85 expect(q.estimatedSeconds).toBeNull();86 expect(q.priceImpactBps).toBeCloseTo(Number(b.priceImpactPct) * 10_000, 2);87 expect(q.atomic).toBe(true); expect(q.refundable).toBe(true); expect(q.firmness).toBe('indicative');88 expect(q.executability.class).toBe('opaque');89 expect((q.executability as { reason: string }).reason).toMatch(/cannot rebuild/);90 // validity: 30 s from the moment Jupiter fetched the blockhash (never later than 30 s from now)91 const now = Date.parse(f.recordedAt) + 400;92 expect(q.expiresAt).toBe(Math.min(now, b.blockhashWithMetadata.fetchedAt.secs_since_epoch * 1000) + 30_000);93 expect(q.expiresAt).toBeGreaterThan(now); expect(q.expiresAt - now).toBeLessThanOrEqual(30_000);94 // one atomic step naming every venue, the aggregator program and every pool of the route plan95 expect(q.steps).toHaveLength(1);96 const s = q.steps[0]!;97 expect(s).toMatchObject({ kind: 'swap', provider: 'jupiter', from: r.from, to: r.to });98 expect(s.via).toEqual([...new Set(b.routePlan.map((l) => l.swapInfo.label))]);99 expect(s.contracts[0]).toEqual({ network: 'solana', address: JUPITER_PROGRAM, role: 'router' });100 for (const l of b.routePlan) expect(s.contracts).toContainEqual({ network: 'solana', address: l.swapInfo.ammKey, role: 'vault' });101 for (const c of s.contracts) expect(c.network).toBe('solana');102 // the raw payload is the validated answer, and the quote passes the aggregator's structural check103 expect(q.raw).toEqual(jupiterBuildSchema.parse(f.body));104 expect(checkQuote(q, r, now)).toBeNull();105 expect(checkQuote(q, r, q.expiresAt)).toBe('quote already expired');106 });107108 it('pins the recorded numbers (base units, registry decimals)', async () => {109 const out = async (n: string) => { const { quotes } = await quoteFixture(n); return quotes[0]!; };110 const solUsdc = await out('sol-usdc'), usdcSol = await out('usdc-sol'), wbtc = await out('usdc-wbtc'), bonk = await out('usdc-bonk'), usdg0 = await out('usdc-usdg'), pyusdSol = await out('pyusd-sol');111 expect([solUsdc.request.amountIn, solUsdc.amountOut, solUsdc.minAmountOut]).toEqual([300_000_000n, 24_898_345n, 24_773_854n]); // 0.3 SOL -> 24.898345 USDC112 expect([usdcSol.amountOut, usdcSol.minAmountOut]).toEqual([301_259_329n, 299_753_033n]); // 25 USDC -> 0.301259329 SOL113 expect([wbtc.amountOut, wbtc.minAmountOut]).toEqual([34_088n, 33_918n]); // 25 USDC -> 0.00034088 WBTC (8 decimals)114 expect([bonk.amountOut, bonk.minAmountOut]).toEqual([885_426_761_904n, 880_999_628_095n]); // 25 USDC -> 8 854 267.61904 BONK (5 decimals)115 expect([usdg0.request.slippageBps, usdg0.amountOut, usdg0.minAmountOut]).toEqual([0, 24_999_274n, 24_999_274n]); // zero slippage: minimum = expected116 expect([pyusdSol.request.slippageBps, pyusdSol.amountOut, pyusdSol.minAmountOut]).toEqual([100, 301_284_623n, 298_271_777n]); // 1 %117 // decimals: the two directions of SOL/USDC imply the same price once scaled by the registry decimals118 const human = (q: SwapQuote) => ({ i: Number(q.request.amountIn) / 10 ** decimals(q.request.from), o: Number(q.amountOut) / 10 ** decimals(q.request.to) });119 const a = human(solUsdc), b = human(usdcSol);120 expect(a.o / a.i).toBeGreaterThan(10); expect(a.o / a.i).toBeLessThan(10_000); // USD per SOL121 expect(Math.abs(a.o / a.i / (b.i / b.o) - 1)).toBeLessThan(0.005);122 });123124 it('reports price impact in basis points, including a near-total one', async () => {125 expect((await quoteFixture('wbtc-usdc-high-impact')).quotes[0]!.priceImpactBps).toBe(9949.8); // "0.99498…" = 99.498 %126 expect((await quoteFixture('sol-usdc')).quotes[0]!.priceImpactBps).toBe(3.19); // "0.000318522…"127 expect((await quoteFixture('pyusd-sol')).quotes[0]!.priceImpactBps).toBe(0); // "0"128 });129130 it('classifies fees: only the network fee, in SOL, estimated, not included in the output', async () => {131 for (const name of SUCCESS) {132 const { quotes, f } = await quoteFixture(name), b = f.body as JupiterBuild;133 expect((b as { platformFee?: unknown }).platformFee).toBeUndefined(); // /build charges no Jupiter fee and we ask for no integrator fee134 const price = new DataView(Uint8Array.from(Buffer.from(b.computeBudgetInstructions[0]!.data, 'base64')).buffer).getBigUint64(1, true);135 expect(quotes[0]!.fees).toEqual([{ kind: 'network', asset: SOL, amount: 5_000n + (price * 1_400_000n + 999_999n) / 1_000_000n, includedInOutput: false, estimated: true }]);136 }137 // sol-usdc: 3 331 micro-lamports per compute unit -> 5 000 + ceil(3 331 × 1.4) = 9 664 lamports at most138 expect((await quoteFixture('sol-usdc')).quotes[0]!.fees[0]!.amount).toBe(9_664n);139 // no priority-fee instruction: the signature fee alone140 expect((await quoteFixture('sol-usdc', (b) => { b.computeBudgetInstructions = []; })).quotes[0]!.fees[0]!.amount).toBe(5_000n);141 });142143 it('really routes the Token-2022 mints (PYUSD, USDG): token accounts are created under the Token-2022 program', async () => {144 for (const [name, sym] of [['sol-pyusd', 'PYUSD'], ['pyusd-sol', 'PYUSD'], ['usdg-usdc', 'USDG'], ['usdc-usdg', 'USDG']] as const) {145 const { quotes, f } = await quoteFixture(name), b = f.body as JupiterBuild, mint = token(sym).id;146 expect(quotes).toHaveLength(1);147 expect(b.routePlan.some((l) => l.swapInfo.inputMint === mint || l.swapInfo.outputMint === mint)).toBe(true);148 const keys = (ix: JupiterBuild['swapInstruction']) => ix.accounts.map((a) => a.pubkey);149 expect(keys(b.swapInstruction)).toContain(mint); expect(keys(b.swapInstruction)).toContain(TOKEN_2022);150 if (requestOf(f).to.asset === mint) expect(b.setupInstructions.some((ix) => ix.programId === ATA_PROGRAM && keys(ix).includes(mint) && keys(ix).includes(TOKEN_2022))).toBe(true);151 }152 });153154 it('native SOL is the wrapped-SOL mint: wrap on the way in, unwrap (close account) on the way out', async () => {155 const wrap = fixture('sol-usdc'), unwrap = fixture('usdc-sol');156 expect(new URL(wrap.url).searchParams.get('inputMint')).toBe(WSOL_MINT);157 expect((wrap.body as JupiterBuild).setupInstructions.map((i) => i.programId)).toContain('11111111111111111111111111111111'); // SOL transfer into the wSOL account158 expect((unwrap.body as JupiterBuild).cleanupInstruction).toMatchObject({ programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', data: 'CQ==' }); // CloseAccount159 });160161 it('the DEX table is the recorded program-id-to-label answer, and names a program that the swap instruction really uses', async () => {162 const live = fixture('program-id-to-label').body as Record<string, string>;163 const fromLive: Record<string, string[]> = {};164 for (const [pid, label] of Object.entries(live)) (fromLive[label] ??= []).push(pid);165 for (const v of Object.values(fromLive)) v.sort();166 expect(JUPITER_DEX_PROGRAMS).toEqual(fromLive);167 for (const name of SUCCESS) {168 const { quotes, f } = await quoteFixture(name), b = f.body as JupiterBuild;169 const used = new Set(b.swapInstruction.accounts.map((a) => a.pubkey));170 for (const l of b.routePlan) {171 const programs = JUPITER_DEX_PROGRAMS[l.swapInfo.label]!;172 expect(programs, l.swapInfo.label).toBeDefined();173 expect(programs.some((p) => used.has(p)), `${name}: ${l.swapInfo.label}`).toBe(true);174 for (const p of programs) expect(quotes[0]!.steps[0]!.contracts).toContainEqual({ network: 'solana', address: p, role: 'program' });175 }176 }177 });178179 it('a venue missing from the table is still quoted, without a program entry', async () => {180 const { quotes } = await quoteFixture('usdc-sol', (b) => { b.routePlan[0]!.swapInfo.label = 'Brand New DEX'; });181 expect(quotes[0]!.steps[0]!.via).toEqual(['Brand New DEX']);182 expect(quotes[0]!.steps[0]!.contracts.filter((c) => c.role === 'program')).toEqual([]);183 });184185 it('keeps only validated fields in raw (unknown keys are dropped)', async () => {186 const { quotes } = await quoteFixture('usdc-sol', (b) => { (b as Record<string, unknown>).surprise = { transaction: 'AAAA' }; });187 expect(quotes[0]!.raw).not.toHaveProperty('surprise');188 });189190 it('sends the optional API key as x-api-key, and nothing without one', async () => {191 const f = fixture('usdc-sol'), a = replay(200, f.body), b = replay(200, f.body);192 await createJupiterProvider({ JUPITER_API_KEY: ' jup_test ' }).quote(requestOf(f), ctxFor(f, a.http));193 await provider.quote(requestOf(f), ctxFor(f, b.http));194 expect(new Headers(a.calls[0]!.init?.headers).get('x-api-key')).toBe('jup_test');195 expect(new Headers(b.calls[0]!.init?.headers).has('x-api-key')).toBe(false);196 });197});198199describe('jupiter: no route is an empty array', () => {200 it.each(['no-route-dust', 'no-route-liquidity'])('%s (recorded HTTP 400)', async (name) => {201 const { quotes, f, calls } = await quoteFixture(name);202 expect(f.status).toBe(400); expect(calls).toHaveLength(1); expect(quotes).toEqual([]);203 });204 it('requests it cannot serve as asked are refused locally, without calling the API', async () => {205 const f = fixture('sol-usdc'), base = requestOf(f);206 const none = async (r: SwapQuoteRequest) => { const { http, calls } = replay(200, f.body); expect(await provider.quote(r, ctxFor(f, http))).toEqual([]); expect(calls).toHaveLength(0); };207 await none({ ...base, recipient: OTHER_WALLET }); // the output of a /build swap lands in the taker's own account208 await none({ ...base, sender: 'SENDER', recipient: 'SENDER' }); // not a Solana address209 await none({ ...base, sender: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', recipient: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' });210 await none({ ...base, amountIn: 0n }); await none({ ...base, amountIn: -5n }); await none({ ...base, amountIn: 1n << 64n });211 await none({ ...base, slippageBps: 12.5 }); await none({ ...base, slippageBps: -1 }); await none({ ...base, slippageBps: 10_001 });212 await none({ ...base, to: { network: 'base', asset: 'native' } }); await none({ ...base, to: SOL });213 });214 it('through the aggregator: a quote for a recorded answer, no_route for a recorded 400', async () => {215 const run = async (name: string) => {216 const f = fixture(name), { http } = replay(f.status, f.body), now = Date.parse(f.recordedAt) + 400;217 const rt = new ProviderRuntime(provider, { ...DEFAULT_POLICY, ...JUPITER_KEYLESS_POLICY }, http, () => now);218 return new QuoteAggregator([rt], () => now).collect(requestOf(f));219 };220 const ok = await run('sol-pyusd');221 expect(ok.quotes).toHaveLength(1); expect(ok.quotes[0]!.amountOut).toBe(24_892_199n);222 const none = await run('no-route-dust');223 expect(none.quotes).toEqual([]); expect(none.events.find((e) => e.type === 'provider')).toMatchObject({ provider: 'jupiter', status: 'no_route' });224 });225});226227describe('jupiter: provider failures throw', () => {228 const failing = async (status: number, body: unknown) => { const f = fixture('sol-usdc'); return provider.quote(requestOf(f), ctxFor(f, replay(status, body).http)); };229 it('recorded 400 that is not a no-route answer', async () => {230 const f = fixture('error-invalid-taker');231 await expect(failing(f.status, f.body)).rejects.toMatchObject({ provider: 'jupiter', code: 'http', status: 400 });232 });233 it('recorded transient upstream failure wrapped in a 400 (stale AMM oracle): a failure, not "no route"', async () => {234 const f = fixture('error-oracle-stale'), { http } = replay(f.status, f.body);235 await expect(provider.quote(requestOf(f), ctxFor(f, http))).rejects.toMatchObject({ provider: 'jupiter', code: 'http', status: 400 });236 });237 it('recorded 429 from the API gateway', async () => {238 const f = fixture('error-rate-limited');239 await expect(failing(f.status, f.body)).rejects.toMatchObject({ provider: 'jupiter', code: 'rate_limited' });240 });241 it('other 400 shapes, 5xx and non-JSON bodies', async () => {242 await expect(failing(400, { error: 'Invalid amount' })).rejects.toMatchObject({ code: 'http' });243 await expect(failing(400, { success: false, error: { issues: [], name: 'ZodError' } })).rejects.toMatchObject({ code: 'http' });244 await expect(failing(400, { error: 'No routes found, try later' })).rejects.toMatchObject({ code: 'http' }); // only the exact documented strings mean "no route"245 await expect(failing(500, { error: 'No routes found' })).rejects.toMatchObject({ code: 'http' });246 await expect(failing(502, '<html>Bad gateway</html>')).rejects.toMatchObject({ code: 'bad_response' });247 await expect(failing(200, '<html>maintenance</html>')).rejects.toMatchObject({ code: 'bad_response' });248 });249 it('timeout and abort: the signal reaches fetch and the failure is a timeout', async () => {250 const f = fixture('sol-usdc'); let seen: AbortSignal | undefined;251 const hang = (async (_u: string | URL | Request, init?: RequestInit) => { seen = init?.signal ?? undefined; return new Promise<Response>((_res, rej) => init!.signal!.addEventListener('abort', () => rej(new DOMException('aborted', 'AbortError')))); }) as typeof fetch;252 // the adapter alone, aborted by its context253 const ac = new AbortController(); setTimeout(() => ac.abort(), 20);254 await expect(provider.quote(requestOf(f), { signal: ac.signal, http: httpClient('jupiter', hang), now: Date.now })).rejects.toMatchObject({ provider: 'jupiter', code: 'timeout' });255 expect(seen).toBe(ac.signal);256 // under the runtime: the per-provider deadline fires and is counted257 const rt = new ProviderRuntime(provider, { ...DEFAULT_POLICY, timeoutMs: 30 }, httpClient('jupiter', hang));258 await expect(rt.quote(requestOf(f))).rejects.toMatchObject({ code: 'timeout' });259 expect(rt.stats().timeouts).toBe(1);260 // a caller that gives up does not count against the provider261 const rt2 = new ProviderRuntime(provider, DEFAULT_POLICY, httpClient('jupiter', hang)), outer = new AbortController(); setTimeout(() => outer.abort(), 20);262 await expect(rt2.quote(requestOf(f), outer.signal)).rejects.toMatchObject({ code: 'timeout' });263 expect(rt2.stats().failures).toBe(0);264 });265});266267describe('jupiter: malformed or hostile answers are rejected (schema), never turned into a quote', () => {268 const rejects = async (name: string, mutate: (b: JupiterBuild) => unknown, fx = 'sol-usdc') => {269 const err = await quoteFixture(fx, mutate).then(() => null, (e: unknown) => e);270 expect(err, name).toBeInstanceOf(ProviderError);271 expect(err, name).toMatchObject({ provider: 'jupiter', code: 'schema' });272 };273 const any = (b: JupiterBuild) => b as unknown as Record<string, unknown>;274 const BONK = token('BONK').id, WBTC = token('WBTC').id;275 it('wrong tokens, amounts or terms', async () => {276 await rejects('output is another registry token', (b) => { b.outputMint = BONK; });277 await rejects('output is a look-alike mint', (b) => { b.outputMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1w'; });278 await rejects('input is another token', (b) => { b.inputMint = WBTC; });279 await rejects('mints swapped', (b) => { [b.inputMint, b.outputMint] = [b.outputMint, b.inputMint]; });280 await rejects('another input amount', (b) => { b.inAmount = '300000001'; });281 await rejects('another slippage', (b) => { b.slippageBps = 500; });282 await rejects('exact-out answer', (b) => { any(b).swapMode = 'ExactOut'; });283 await rejects('zero output', (b) => { b.outAmount = '0'; b.otherAmountThreshold = '0'; });284 await rejects('legacy transaction version', (b) => { any(b).transactionVersion = 1; });285 });286 it('amounts that are not unsigned integer strings', async () => {287 for (const v of ['24898345.5', '-24898345', '2.4898345e7', '0x17BE9A9', ' 24898345', '024898345', '', '1e3', 'NaN', '18446744073709551616', '9'.repeat(40)]) await rejects(`outAmount ${JSON.stringify(v)}`, (b) => { b.outAmount = v; });288 await rejects('outAmount as a JSON number', (b) => { any(b).outAmount = 24898345; });289 await rejects('outAmount as a float', (b) => { any(b).outAmount = 24898345.5; });290 await rejects('threshold as a float string', (b) => { b.otherAmountThreshold = '24773854.9'; });291 await rejects('negative threshold', (b) => { b.otherAmountThreshold = '-1'; });292 await rejects('negative input', (b) => { b.inAmount = '-300000000'; });293 await rejects('leg amount as a float', (b) => { b.routePlan[0]!.swapInfo.outAmount = '1.5'; });294 await rejects('slippage as a string', (b) => { any(b).slippageBps = '50'; });295 await rejects('price impact not a number', (b) => { b.priceImpactPct = 'low'; });296 });297 it('a guaranteed minimum outside the requested slippage', async () => {298 await rejects('minimum of 1 unit', (b) => { b.otherAmountThreshold = '1'; });299 await rejects('one unit below the exact floor', (b) => { b.otherAmountThreshold = String((BigInt(b.outAmount) * 9_950n) / 10_000n - 1n); });300 await rejects('minimum above the expected output', (b) => { b.otherAmountThreshold = String(BigInt(b.outAmount) + 1n); });301 await rejects('zero minimum', (b) => { b.otherAmountThreshold = '0'; });302 // the exact floor itself is inside the band: accepted, and taken as the provider states it303 const { quotes } = await quoteFixture('sol-usdc', (b) => { b.otherAmountThreshold = String((BigInt(b.outAmount) * 9_950n) / 10_000n); });304 expect(quotes[0]!.minAmountOut).toBe(24_773_853n);305 });306 it('missing fields and wrong shapes', async () => {307 for (const k of ['inputMint', 'outputMint', 'inAmount', 'outAmount', 'otherAmountThreshold', 'swapMode', 'slippageBps', 'priceImpactPct', 'routePlan', 'computeBudgetInstructions', 'setupInstructions', 'swapInstruction', 'cleanupInstruction', 'otherInstructions', 'addressesByLookupTableAddress', 'blockhashWithMetadata', 'transactionVersion']) await rejects(`missing ${k}`, (b) => { delete any(b)[k]; });308 await rejects('empty route plan', (b) => { b.routePlan = []; });309 await rejects('leg without pool', (b) => { delete (b.routePlan[0]!.swapInfo as Record<string, unknown>).ammKey; });310 await rejects('leg without bps', (b) => { delete (b.routePlan[0] as Record<string, unknown>).bps; });311 await rejects('no blockhash time', (b) => { delete (b.blockhashWithMetadata as Record<string, unknown>).fetchedAt; });312 await rejects('short blockhash', (b) => { b.blockhashWithMetadata.blockhash = [1, 2, 3]; });313 await rejects('pool key not base58', (b) => { b.routePlan[0]!.swapInfo.ammKey = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; });314 await rejects('null body', () => null);315 await rejects('array body', () => []);316 await rejects('error object with HTTP 200', () => ({ error: 'No routes found' }));317 await rejects('V1 quote shape (no instructions)', (b) => ({ inputMint: b.inputMint, inAmount: b.inAmount, outputMint: b.outputMint, outAmount: b.outAmount, otherAmountThreshold: b.otherAmountThreshold, swapMode: 'ExactIn', slippageBps: 50, platformFee: null, priceImpactPct: '0', routePlan: b.routePlan }));318 });319 it('a route plan that does not add up', async () => {320 await rejects('legs deliver less than quoted', (b) => { b.outAmount = String(BigInt(b.outAmount) + 1_000_000n); b.otherAmountThreshold = b.outAmount; });321 await rejects('legs take another input', (b) => { const l = b.routePlan.find((x) => x.swapInfo.inputMint === WSOL_MINT)!; l.swapInfo.inAmount = String(BigInt(l.swapInfo.inAmount) - 1n); });322 await rejects('route ends in another mint', (b) => { for (const l of b.routePlan) if (l.swapInfo.outputMint === b.outputMint) l.swapInfo.outputMint = BONK; });323 await rejects('self-swap leg', (b) => { b.routePlan[0]!.swapInfo.outputMint = b.routePlan[0]!.swapInfo.inputMint; });324 });325 it('instructions for another wallet, another program, or with extras nobody asked for', async () => {326 const SYSTEM = '11111111111111111111111111111111';327 await rejects('output for another recipient: every occurrence of the taker replaced', (b) => JSON.parse(JSON.stringify(b).replaceAll(requestOf(fixture('sol-usdc')).sender, OTHER_WALLET)));328 await rejects('a second signer', (b) => { b.setupInstructions[0]!.accounts.push({ pubkey: OTHER_WALLET, isSigner: true, isWritable: true }); });329 await rejects('swap signed by another wallet', (b) => { for (const a of b.swapInstruction.accounts) if (a.isSigner) a.pubkey = OTHER_WALLET; });330 await rejects('swap not signed by the taker', (b) => { for (const a of b.swapInstruction.accounts) a.isSigner = false; });331 await rejects('swap sent to another program', (b) => { b.swapInstruction.programId = 'JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB'; });332 await rejects('unknown program in setup', (b) => { b.setupInstructions[0]!.programId = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr'; });333 await rejects('unknown program in cleanup', (b) => { b.cleanupInstruction!.programId = OTHER_WALLET; });334 await rejects('extra instruction', (b) => { b.otherInstructions.push({ programId: SYSTEM, accounts: [{ pubkey: b.swapInstruction.accounts.find((a) => a.isSigner)!.pubkey, isSigner: true, isWritable: true }, { pubkey: OTHER_WALLET, isSigner: false, isWritable: true }], data: 'AgAAAADKmjsAAAAA' }); });335 await rejects('tip instruction', (b) => { any(b).tipInstruction = b.setupInstructions[0]; });336 await rejects('platform fee nobody asked for', (b) => { any(b).platformFee = { amount: '100000', feeBps: 40 }; });337 await rejects('compute budget instruction that is not a price', (b) => { b.computeBudgetInstructions[0]!.data = 'AkANAwA='; });338 await rejects('compute budget slot used by another program', (b) => { b.computeBudgetInstructions[0]!.programId = SYSTEM; });339 await rejects('instruction data not base64', (b) => { b.swapInstruction.data = '<script>'; });340 });341});342343// The execution dossier (docs/swap/providers/jupiter.md) states what an isolated signer could check by itself in a /build344// answer. These are those facts, pinned on every recorded answer (derivations with @solana/kit, a dev dependency).345describe('jupiter: structure of the recorded instructions (the facts the execution dossier relies on)', async () => {346 const { address, getAddressEncoder, getProgramDerivedAddress } = await import('@solana/kit');347 const TOKEN = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', SYSTEM = '11111111111111111111111111111111';348 const programOf = (mint: string) => (NETWORKS.solana!.tokens.find((t) => t.id === mint)?.program === 'token2022' ? TOKEN_2022 : TOKEN);349 const enc = getAddressEncoder();350 const ata = async (owner: string, mint: string) => (await getProgramDerivedAddress({ programAddress: address(ATA_PROGRAM), seeds: [enc.encode(address(owner)), enc.encode(address(programOf(mint))), enc.encode(address(mint))] }))[0] as string;351 const bytes = (b64: string) => Buffer.from(b64, 'base64');352353 it.each(SUCCESS)('%s', async (name) => {354 const f = fixture(name), b = f.body as JupiterBuild, q = new URL(f.url).searchParams;355 const taker = q.get('taker')!, inMint = q.get('inputMint')!, outMint = q.get('outputMint')!;356 const src = await ata(taker, inMint), dst = await ata(taker, outMint);357 // swap instruction: Anchor `route_v2` or `shared_accounts_route_v2` of the aggregator, carrying the exact input, the358 // quoted output and the slippage (the program enforces quoted_out × (1 − slippage)); no platform fee359 const data = bytes(b.swapInstruction.data), disc = data.subarray(0, 8).toString('hex');360 expect(['bb64facc31c4af14', 'd19853937cfed8e9']).toContain(disc); // sha256("global:route_v2")[..8], sha256("global:shared_accounts_route_v2")[..8]361 const shared = disc === 'd19853937cfed8e9', o = shared ? 9 : 8;362 expect(data.readBigUInt64LE(o)).toBe(BigInt(b.inAmount));363 expect(data.readBigUInt64LE(o + 8)).toBe(BigInt(b.outAmount));364 expect(data.readUInt16LE(o + 16)).toBe(b.slippageBps);365 expect(data.readUInt16LE(o + 18)).toBe(0); // platform fee bps366 expect(data.readUInt32LE(o + 22)).toBe(b.routePlan.length);367 // accounts at fixed positions: the taker (only signer), its own source and destination token accounts, both mints368 const keys = b.swapInstruction.accounts.map((a) => a.pubkey), at = shared ? { taker: 1, src: 2, dst: 5, inMint: 6, outMint: 7 } : { taker: 0, src: 1, dst: 2, inMint: 3, outMint: 4 };369 expect([keys[at.taker], keys[at.src], keys[at.dst], keys[at.inMint], keys[at.outMint]]).toEqual([taker, src, dst, inMint, outMint]);370 expect(b.swapInstruction.accounts.filter((a) => a.isSigner).map((a) => a.pubkey)).toEqual([taker]);371 // setup: only idempotent creations of the taker's OWN associated token accounts (possibly for intermediate mints),372 // and, when SOL is the input, a transfer of exactly the input amount into the taker's wrapped-SOL account + SyncNative373 for (const ix of b.setupInstructions) {374 const k = ix.accounts.map((a) => a.pubkey), d = bytes(ix.data);375 if (ix.programId === ATA_PROGRAM) { expect(d).toEqual(Buffer.from([1])); expect([k[0], k[2]]).toEqual([taker, taker]); expect([TOKEN, TOKEN_2022]).toContain(k[5]); }376 else if (ix.programId === SYSTEM) { expect(inMint).toBe(WSOL_MINT); expect(d.readUInt32LE(0)).toBe(2); expect([k[0], k[1]]).toEqual([taker, src]); expect(d.readBigUInt64LE(4)).toBe(BigInt(b.inAmount)); }377 else { expect(ix.programId).toBe(TOKEN); expect(d).toEqual(Buffer.from([17])); expect(k).toEqual([src]); }378 }379 expect(b.setupInstructions.some((ix) => ix.programId === ATA_PROGRAM && ix.accounts[1]!.pubkey === dst)).toBe(true);380 // cleanup: present exactly when SOL is on either side — CloseAccount of the taker's wrapped-SOL account, back to the taker381 expect(b.cleanupInstruction !== null).toBe(inMint === WSOL_MINT || outMint === WSOL_MINT);382 if (b.cleanupInstruction) expect(b.cleanupInstruction.accounts.map((a) => a.pubkey)).toEqual([inMint === WSOL_MINT ? src : dst, taker, taker]);383 // versioned (v0) transaction material: at least one address lookup table384 expect(Object.keys(b.addressesByLookupTableAddress ?? {}).length).toBeGreaterThan(0);385 });386});387