Repository Public
1import { describe, expect, it } from 'vitest';2import { NETWORKS } from '@spw/core';3import {4 assessRisk, checkQuote, DEFAULT_POLICY, LiquidityGraph, ProviderError, ProviderRuntime, QuoteAggregator, rank, SwapRouter, valueQuote,5 type AssetRef, type HttpClient, type SwapProvider, type SwapQuote, type SwapQuoteRequest, type Valuer,6} from '../src/index.ts';78const USDC_BASE: AssetRef = { network: 'base', asset: NETWORKS.base.tokens.find((t) => t.symbol === 'USDC')!.id };9const ETH_BASE: AssetRef = { network: 'base', asset: 'native' };10const SOL: AssetRef = { network: 'solana', asset: 'native' };11const USDC_SOL: AssetRef = { network: 'solana', asset: NETWORKS.solana.tokens.find((t) => t.symbol === 'USDC')!.id };12const BTC: AssetRef = { network: 'bitcoin', asset: 'native' };13const noHttp: HttpClient = { getJson: async () => { throw new Error('no network in tests'); }, postJson: async () => { throw new Error('no network in tests'); } };14const PRICE: Record<string, number> = { ETH: 2000, USDC: 1, SOL: 80, BTC: 70000 };15const value: Valuer = (a, amt) => { const n = NETWORKS[a.network]!, d = a.asset === 'native' ? n.native : n.tokens.find((t) => t.id.toLowerCase() === a.asset.toLowerCase())!; const p = PRICE[d.symbol]; return p === undefined ? null : (Number(amt) / 10 ** d.decimals) * p; };16const req = (from: AssetRef, to: AssetRef, amountIn: bigint): SwapQuoteRequest => ({ from, to, amountIn, sender: 'SENDER', recipient: 'RECIPIENT', slippageBps: 50 });1718let qn = 0;19function quoteOf(provider: string, r: SwapQuoteRequest, out: bigint, o: Partial<SwapQuote> = {}): SwapQuote {20 return { id: `${provider}-${++qn}`, provider, request: r, steps: [{ kind: r.from.network === r.to.network ? 'swap' : 'bridge', provider, from: r.from, to: r.to, via: ['pool'], contracts: [] }],21 amountOut: out, minAmountOut: (out * 995n) / 1000n, fees: [], estimatedSeconds: 20, priceImpactBps: 5, expiresAt: Date.now() + 30_000, firmness: 'indicative',22 executability: { class: 'opaque', reason: 'test' }, refundable: true, atomic: true, ...o };23}24/** A scripted provider: `rate(from,to)` gives output per unit input (in base units ratio) or null for "no route". */25function fake(id: string, opts: { pairs?: (a: AssetRef, b: AssetRef) => boolean; out: (r: SwapQuoteRequest) => bigint | null; delayMs?: number; fail?: () => boolean; patch?: Partial<SwapQuote>; needsKey?: boolean }): SwapProvider & { calls: number } {26 const p = { id, name: id, calls: 0,27 capabilities: () => ({ networks: ['base', 'solana', 'bitcoin'], crossChain: true, needsKey: !!opts.needsKey, configured: !opts.needsKey, kinds: ['swap' as const, 'bridge' as const], docs: '' }),28 supportsPair: (a: AssetRef, b: AssetRef) => (opts.pairs ? opts.pairs(a, b) : true),29 async quote(r: SwapQuoteRequest, ctx: { signal: AbortSignal }) {30 p.calls += 1;31 if (opts.delayMs) await new Promise<void>((res, rej) => { const t = setTimeout(res, opts.delayMs); ctx.signal.addEventListener('abort', () => { clearTimeout(t); rej(new Error('aborted')); }); });32 if (opts.fail?.()) throw new ProviderError(id, 'http', 'HTTP 500');33 const out = opts.out(r);34 return out === null ? [] : [quoteOf(id, r, out, opts.patch)];35 } };36 return p;37}38const setup = (providers: SwapProvider[], policy: Partial<typeof DEFAULT_POLICY> = {}) => {39 policy = { ...DEFAULT_POLICY, ratePerSec: 1000, burst: 1000, ...policy };40 const rts = providers.map((p) => new ProviderRuntime(p, policy as typeof DEFAULT_POLICY, noHttp));41 const agg = new QuoteAggregator(rts), graph = new LiquidityGraph(providers);42 return { rts, agg, graph, router: new SwapRouter(agg, graph, value) };43};44// 1 ETH = 2000 USDC, at a provider-specific discount in basis points45const ethToUsdc = (bps: number) => (r: SwapQuoteRequest) => (r.amountIn * 2000n * (10_000n - BigInt(bps))) / 10_000n / 10n ** 12n;46const sameChain = (a: AssetRef, b: AssetRef) => a.network === b.network;4748describe('provider runtime', () => {49 it('enforces the per-provider deadline and reports a timeout', async () => {50 const rt = new ProviderRuntime(fake('slow', { out: () => 1n, delayMs: 300 }), { ...DEFAULT_POLICY, timeoutMs: 40 }, noHttp);51 await expect(rt.quote(req(ETH_BASE, USDC_BASE, 10n ** 18n))).rejects.toMatchObject({ code: 'timeout' });52 expect(rt.stats().timeouts).toBe(1);53 });54 it('opens the circuit after repeated failures, then lets one probe through after the cooldown', async () => {55 let t = 1_000_000, failing = true;56 const p = fake('flaky', { out: ethToUsdc(0), fail: () => failing });57 const rt = new ProviderRuntime(p, { ...DEFAULT_POLICY, breakerFailures: 3, breakerCooldownMs: 10_000, ratePerSec: 1000, burst: 1000 }, noHttp, () => t);58 for (let k = 0; k < 3; k++) await expect(rt.quote(req(ETH_BASE, USDC_BASE, 1n))).rejects.toMatchObject({ code: 'http' });59 expect(rt.stats().state).toBe('open');60 await expect(rt.quote(req(ETH_BASE, USDC_BASE, 1n))).rejects.toMatchObject({ code: 'unavailable' });61 expect(p.calls).toBe(3); // the open circuit did not call the provider62 t += 10_001; expect(rt.stats().state).toBe('half_open');63 await expect(rt.quote(req(ETH_BASE, USDC_BASE, 1n))).rejects.toBeTruthy(); // probe fails: open again64 expect(rt.stats().state).toBe('open');65 t += 10_001; failing = false;66 expect((await rt.quote(req(ETH_BASE, USDC_BASE, 10n ** 18n))).length).toBe(1);67 expect(rt.stats().state).toBe('closed');68 });69 it('rate-limits locally and refills over time', async () => {70 let t = 0;71 const rt = new ProviderRuntime(fake('rl', { out: ethToUsdc(0) }), { ...DEFAULT_POLICY, ratePerSec: 1, burst: 2 }, noHttp, () => t);72 await rt.quote(req(ETH_BASE, USDC_BASE, 1n)); await rt.quote(req(ETH_BASE, USDC_BASE, 1n));73 await expect(rt.quote(req(ETH_BASE, USDC_BASE, 1n))).rejects.toMatchObject({ code: 'rate_limited' });74 t += 1000; await rt.quote(req(ETH_BASE, USDC_BASE, 1n));75 });76 it('a caller that cancels does not count against the provider', async () => {77 const rt = new ProviderRuntime(fake('slow', { out: () => 1n, delayMs: 200 }), DEFAULT_POLICY, noHttp);78 const ac = new AbortController(); setTimeout(() => ac.abort(), 20);79 await expect(rt.quote(req(ETH_BASE, USDC_BASE, 1n), ac.signal)).rejects.toMatchObject({ code: 'timeout' });80 expect(rt.stats().failures).toBe(0);81 });82});8384describe('quote validation (no provider answer is trusted)', () => {85 const r = req(ETH_BASE, USDC_BASE, 10n ** 18n), now = Date.now();86 const good = quoteOf('p', r, 2000_000000n);87 it('accepts a coherent quote', () => expect(checkQuote(good, r, now)).toBeNull());88 it.each([89 ['another pair', { ...good, request: { ...r, to: SOL } }], ['another amount', { ...good, request: { ...r, amountIn: 5n } }],90 ['another recipient', { ...good, request: { ...r, recipient: 'ATTACKER' } }], ['zero output', { ...good, amountOut: 0n, minAmountOut: 0n }],91 ['minimum above expected', { ...good, minAmountOut: good.amountOut + 1n }], ['expired', { ...good, expiresAt: now - 1 }],92 ['float amount', { ...good, amountOut: 1.5 as unknown as bigint }], ['negative fee', { ...good, fees: [{ kind: 'network', asset: ETH_BASE, amount: -1n, includedInOutput: false, estimated: true }] }],93 ['steps end elsewhere', { ...good, steps: [{ ...good.steps[0]!, to: SOL }] }], ['no steps', { ...good, steps: [] }],94 ['broken chain of steps', { ...good, steps: [{ ...good.steps[0]!, to: SOL }, { ...good.steps[0]!, from: BTC }] }],95 ] as [string, SwapQuote][])('rejects: %s', (_n, q) => expect(checkQuote(q, r, now)).not.toBeNull());96});9798describe('aggregator', () => {99 it('queries providers concurrently, streams fastest first, and isolates failures', async () => {100 const { agg } = setup([fake('a', { out: ethToUsdc(10), delayMs: 60 }), fake('b', { out: ethToUsdc(30), delayMs: 5 }), fake('down', { out: () => 1n, fail: () => true }),101 fake('none', { out: () => null }), fake('keyed', { out: () => 1n, needsKey: true }), fake('other-chain', { out: () => 1n, pairs: () => false })]);102 const t0 = Date.now(); const seen: string[] = [];103 for await (const e of agg.stream(req(ETH_BASE, USDC_BASE, 10n ** 18n))) seen.push(e.type === 'quote' ? `q:${e.quote.provider}` : e.type === 'provider' ? `${e.status}:${e.provider}` : 'done');104 expect(Date.now() - t0).toBeLessThan(200); // not 60 + 5 + … sequentially105 expect(seen.indexOf('q:b')).toBeLessThan(seen.indexOf('q:a'));106 expect(seen).toContain('error:down'); expect(seen).toContain('no_route:none'); expect(seen).toContain('skipped:keyed');107 expect(seen.some((s) => s.includes('other-chain'))).toBe(false);108 expect(seen[seen.length - 1]).toBe('done');109 });110 it('drops a provider answer that is for another recipient', async () => {111 const { agg } = setup([fake('evil', { out: ethToUsdc(0), patch: { request: { ...req(ETH_BASE, USDC_BASE, 10n ** 18n), recipient: 'ATTACKER' } } })]);112 const { quotes, events } = await agg.collect(req(ETH_BASE, USDC_BASE, 10n ** 18n));113 expect(quotes.length).toBe(0);114 expect(events.some((e) => e.type === 'provider' && e.code === 'invalid_quote')).toBe(true);115 });116 it('respects the global deadline', async () => {117 const { agg } = setup([fake('slow', { out: () => 1n, delayMs: 2000 }), fake('fast', { out: ethToUsdc(0), delayMs: 5 })], { timeoutMs: 5000 });118 const t0 = Date.now();119 const { quotes } = await agg.collect(req(ETH_BASE, USDC_BASE, 10n ** 18n), { deadlineMs: 120 });120 expect(Date.now() - t0).toBeLessThan(400);121 expect(quotes.map((q) => q.provider)).toEqual(['fast']);122 });123});124125describe('risk engine', () => {126 const r = req(ETH_BASE, USDC_BASE, 10n ** 18n);127 const flags = (q: SwapQuote, peers: SwapQuote[] = []) => assessRisk(q, valueQuote(q, value), peers).flags.map((f) => f.code);128 it('flags a quote that is better than the market as high risk', () => {129 const a = assessRisk(quoteOf('p', r, 2200_000000n), valueQuote(quoteOf('p', r, 2200_000000n), value));130 expect(a.level).toBe('high'); expect(a.flags.map((f) => f.code)).toContain('better_than_market');131 });132 it('flags bad execution, price impact, opaque and cross-chain properties', () => {133 expect(flags(quoteOf('p', r, 1700_000000n))).toContain('far_below_market');134 expect(flags(quoteOf('p', r, 1990_000000n, { priceImpactBps: 350 }))).toContain('high_price_impact');135 expect(flags(quoteOf('p', r, 1990_000000n))).toContain('opaque_transaction');136 const x = req(USDC_BASE, USDC_SOL, 100_000000n);137 expect(flags(quoteOf('p', x, 99_900000n, { refundable: false }))).toEqual(expect.arrayContaining(['cross_chain', 'no_refund_path']));138 });139 it('detects an outlier against peers and unpriced fees', () => {140 const peers = [quoteOf('a', r, 1995_000000n), quoteOf('b', r, 1996_000000n)], out = quoteOf('c', r, 2110_000000n);141 expect(flags(out, [...peers, out])).toContain('outlier_vs_peers');142 const f = quoteOf('d', r, 1995_000000n, { fees: [{ kind: 'network', asset: { network: 'hyperevm', asset: 'native' }, amount: 5n, includedInOutput: false, estimated: true }] });143 expect(flags(f)).toContain('fee_unpriced');144 });145 it('subtracts only the fees that are not already in the output', () => {146 const q = quoteOf('p', r, 2000_000000n, { fees: [{ kind: 'network', asset: ETH_BASE, amount: 10n ** 15n, includedInOutput: false, estimated: true }, { kind: 'protocol', asset: USDC_BASE, amount: 5_000000n, includedInOutput: true, estimated: false }] });147 const v = valueQuote(q, value);148 expect(v.extraFeesUsd).toBeCloseTo(2, 6);149 expect(v.netUsd).toBeCloseTo(1990 - 2, 6);150 });151});152153describe('router', () => {154 it('ranks on guaranteed net output, not on the headline number', async () => {155 const gas = (eth: bigint) => [{ kind: 'network' as const, asset: ETH_BASE, amount: eth, includedInOutput: false, estimated: true }];156 const { router } = setup([fake('headline', { out: ethToUsdc(0), pairs: sameChain, patch: { fees: gas(5n * 10n ** 15n) } }), fake('net', { out: ethToUsdc(20), pairs: sameChain, patch: { fees: gas(10n ** 14n) } })]);157 const s = await router.search(req(ETH_BASE, USDC_BASE, 10n ** 18n));158 expect(s.routes.length).toBe(2);159 expect(s.routes.find((x) => x.id === s.best.best_price)!.providers).toEqual(['net']); // 4 USD better output loses to 9.8 USD more gas160 });161 it('fastest never gives up more than 2 % of the best net output; balanced is explicit', async () => {162 const { router } = setup([fake('cheap-slow', { out: ethToUsdc(0), pairs: sameChain, patch: { estimatedSeconds: 600 } }), fake('fast-ok', { out: ethToUsdc(100), pairs: sameChain, patch: { estimatedSeconds: 15 } }),163 fake('fast-bad', { out: ethToUsdc(260), pairs: sameChain, patch: { estimatedSeconds: 2 } })]);164 const s = await router.search(req(ETH_BASE, USDC_BASE, 10n ** 18n));165 const by = (m: 'best_price' | 'fastest' | 'balanced') => s.routes.find((x) => x.id === s.best[m])!.providers[0];166 expect(by('best_price')).toBe('cheap-slow'); expect(by('fastest')).toBe('fast-ok'); expect(['cheap-slow', 'fast-ok']).toContain(by('balanced'));167 expect(s.routes.filter((x) => x.pareto).map((x) => x.providers[0]).sort()).toEqual(['cheap-slow', 'fast-bad', 'fast-ok']);168 });169 it('never selects a high-risk route while a lower-risk one exists, in any mode', async () => {170 const { router } = setup([fake('honest', { out: ethToUsdc(30), pairs: sameChain }), fake('too-good', { out: (r) => ethToUsdc(0)(r) * 115n / 100n, pairs: sameChain, patch: { estimatedSeconds: 1 } })]);171 const s = await router.search(req(ETH_BASE, USDC_BASE, 10n ** 18n));172 expect(s.routes.find((x) => x.providers[0] === 'too-good')!.risk.level).toBe('high');173 for (const id of Object.values(s.best)) expect(s.routes.find((x) => x.id === id)!.providers).toEqual(['honest']);174 });175 it('builds a route through an intermediate asset when no direct route exists, spending only what leg 1 guarantees', async () => {176 const dex = fake('dex', { pairs: sameChain, out: (r) => (r.from.asset === 'native' ? ethToUsdc(10)(r) : null) });177 const bridge = fake('bridge', { pairs: (a, b) => a.network !== b.network && a.asset !== 'native' && b.asset !== 'native', out: (r) => r.amountIn - 100_000n, patch: { estimatedSeconds: 900 } });178 const { router } = setup([dex, bridge]);179 const s = await router.search(req(ETH_BASE, USDC_SOL, 10n ** 18n));180 expect(s.reachable).toBe(true); expect(s.routes.length).toBe(2); // one per stablecoin hub on Base: USDC, and bridged USDT (registry v5)181 const r = s.routes.find((x) => x.path[1]!.asset === USDC_BASE.asset)!;182 expect(r.providers).toEqual(['dex', 'bridge']);183 expect(r.path.map((p) => `${p.network}:${p.asset === 'native' ? 'native' : 'token'}`)).toEqual(['base:native', 'base:token', 'solana:token']);184 expect(r.legs[1]!.request.amountIn).toBe(r.legs[0]!.minAmountOut);185 expect(r.legs[0]!.request.recipient).toBe('SENDER'); // the intermediate asset stays in the wallet186 expect(r.estimatedSeconds).toBe(920);187 expect(r.risk.flags.map((f) => f.code)).toContain('multi_step');188 });189 it('hides a two-leg route that does not clearly beat the direct one', async () => {190 const direct = fake('direct', { pairs: (a, b) => a.network !== b.network, out: (r) => (r.from.asset === 'native' && r.to.asset !== 'native' ? ethToUsdc(5)(r) : r.from.asset !== 'native' ? r.amountIn - 1000n : null) });191 const dex = fake('dex', { pairs: sameChain, out: (r) => (r.from.asset === 'native' ? ethToUsdc(5)(r) : null) });192 const s = await setup([direct, dex]).router.search(req(ETH_BASE, USDC_SOL, 10n ** 18n));193 expect(s.routes.every((r) => r.legs.length === 1)).toBe(true);194 });195 it('says so when nothing connects the pair, and when providers cover it but none answers', async () => {196 const none = await setup([fake('dex', { pairs: sameChain, out: ethToUsdc(0) })]).router.search(req(BTC, SOL, 100_000n));197 expect(none.reachable).toBe(false); expect(none.routes).toEqual([]); expect(none.best).toEqual({});198 const dry = await setup([fake('x', { out: () => null })]).router.search(req(BTC, SOL, 100_000n));199 expect(dry.reachable).toBe(true); expect(dry.routes).toEqual([]);200 });201 it('rank() is stable on an empty set and with unpriced assets falls back to raw minimum output', () => {202 expect(rank([])).toEqual({});203 });204 it('handles thousands of concurrent route searches', async () => {205 const { router, rts } = setup([fake('a', { out: ethToUsdc(10), pairs: sameChain }), fake('b', { out: ethToUsdc(12), pairs: sameChain }), fake('c', { out: ethToUsdc(8), pairs: sameChain, fail: () => Math.random() < 0.2 })],206 { breakerFailures: 1_000_000, ratePerSec: 1e9, burst: 1e9 });207 const t0 = Date.now();208 const res = await Promise.all(Array.from({ length: 3000 }, (_, k) => router.search(req(ETH_BASE, USDC_BASE, BigInt(k + 1) * 10n ** 15n))));209 const ms = Date.now() - t0;210 expect(res.every((s) => s.routes.length >= 2)).toBe(true);211 expect(res.every((s) => s.routes.find((r) => r.id === s.best.best_price)!.minAmountOut >= s.routes[s.routes.length - 1]!.minAmountOut)).toBe(true);212 expect(rts.reduce((n, r) => n + r.stats().calls, 0)).toBe(27000); // per search and provider: the direct pair, plus both legs through the other stablecoin hub on Base (USDT, registry v5)213 expect(ms).toBeLessThan(8000);214 console.log(`3000 concurrent searches over 3 providers: ${ms} ms (${(ms / 3000).toFixed(2)} ms each)`);215 });216});217