SPB Git

Repository Public

market-symbols.test.ts 21.5 KB 214 lines typescript

1import { describe, expect, it } from 'vitest';2import {3  ASSET_TRACKS, cashLegs, getNetwork, hasMarketData, hasSignerPrice, KYBER_ENABLED, MANDATE_NETWORKS, MARKET_LISTINGS, marketSymbolOf, MAX_AGENT_SYMBOLS, MandateSchema, priceSymbolOf,4  resolveAgentAssets, tradableSymbols, validateMandate, type AssetDef, type Mandate, type NetworkDef,5} from '../src/index.ts';67/** A FIXED registry for one network: only the named tokens of today's registry, plus injected ones. The expectations built on it8 *  do not move when the real registry grows (which is how these tests broke once: they pinned "what Base does not hold yet"). */9const fixed = (id: string, keep: string[], extra: AssetDef[] = []): NetworkDef => { const n = getNetwork(id), tokens = keep.map((s) => { const t = n.tokens.find((x) => x.symbol === s); if (!t) throw new Error(`${s} is not on ${id}`); return t; }); return { ...n, tokens: [...tokens, ...extra] }; };10const tok = (symbol: string, k: number, name = symbol, decimals = 18): AssetDef => ({ id: `0x${k.toString(16).padStart(40, '0')}`, symbol, name, decimals });11const sym = (n: NetworkDef, s: string) => tradableSymbols(n).find((x) => x.symbol === s);1213describe('static table of listings', () => {14  it('holds only tickers listed against USD on at least two exchanges, with well-formed pair names', () => {15    expect(Object.keys(MARKET_LISTINGS).length).toBeGreaterThan(100);16    for (const [s, l] of Object.entries(MARKET_LISTINGS)) {17      expect(s, s).toMatch(/^[A-Z0-9]{2,10}$/);18      expect([l.coinbase, l.bitstamp, l.kraken].filter(Boolean).length, s).toBeGreaterThanOrEqual(2);19      if (l.coinbase) expect(l.coinbase).toBe(`${s}-USD`);20      if (l.bitstamp) expect(l.bitstamp).toBe(`${s.toLowerCase()}usd`);21      if (l.kraken) expect(l.kraken).toMatch(/^[A-Z0-9]{4,14}$/);22    }23    expect(MARKET_LISTINGS.BTC).toEqual({ coinbase: 'BTC-USD', bitstamp: 'btcusd', kraken: 'XBTUSD' }); // Kraken's own names24    expect(MARKET_LISTINGS.DOGE!.kraken).toBe('XDGUSD');25    expect(hasMarketData('ETH') && hasSignerPrice('ETH')).toBe(true);26    expect(hasMarketData('EUR')).toBe(true); expect(hasSignerPrice('EUR')).toBe(false); // Bitstamp + Kraken: data, but no signer price27    for (const s of ['NOPE', 'constructor', '__proto__', 'toString', '']) expect(hasMarketData(s), s).toBe(false);28  });29  it('every reviewed wrapper tracks a symbol that has market data', () => {30    for (const [s, t] of Object.entries(ASSET_TRACKS)) { expect(hasMarketData(t.tracks), s).toBe(true); expect(t.issuer.length, s).toBeGreaterThan(5); }31    for (const yieldBearing of ['wstETH', 'cbETH', 'rETH', 'weETH', 'sUSDe', 'stETH']) expect(ASSET_TRACKS[yieldBearing], yieldBearing).toBeUndefined(); // not the same economic asset32  });33});3435describe('registry asset → market symbol', () => {36  it('maps natives and tokens to themselves, wrappers to what they track, cash legs to nothing', () => {37    expect(marketSymbolOf({ id: 'native', symbol: 'ETH' })).toBe('ETH');38    expect(marketSymbolOf(tok('LINK', 1))).toBe('LINK');39    for (const [w, s] of [['WETH', 'ETH'], ['WBTC', 'BTC'], ['cbBTC', 'BTC'], ['tBTC', 'BTC'], ['BTCB', 'BTC'], ['cbXRP', 'XRP'], ['EURC', 'EUR']] as const) expect(marketSymbolOf(tok(w, 2)), w).toBe(s);40    expect(marketSymbolOf(tok('USDC', 3))).toBeNull(); expect(marketSymbolOf(tok('USDT', 4))).toBeNull();41    expect(marketSymbolOf(tok('wstETH', 5))).toBe('wstETH'); // its own symbol (no market data), never ETH42    expect(() => marketSymbolOf({ id: 'native', symbol: 'WETH' })).toThrow(/ambiguous/);43    expect(priceSymbolOf(tok('cbBTC', 6))).toEqual({ symbol: 'BTC', cash: false }); // the signer prices a wrapper as its underlying44    expect(priceSymbolOf(tok('USDC', 7))).toEqual({ symbol: 'USDC', cash: true });45  });4647  it("today's registry: what each execution-enabled network can trade", () => {48    const eth = getNetwork('ethereum'), base = getNetwork('base');49    expect(sym(eth, 'ETH')!.representations.map((r) => [r.assetSymbol, r.kind])).toEqual([['ETH', 'native'], ['WETH', 'wrapped']]);50    expect(sym(eth, 'ETH')!.defaultId).toBe('native'); // rule 1: the native coin51    // bitcoin on Ethereum: every reviewed wrapper the registry holds there, in reviewed rank order; WBTC (rank 1) is the default52    const btc = sym(eth, 'BTC')!;53    expect(btc.representations.map((r) => r.assetSymbol)).toEqual(eth.tokens.filter((t) => ASSET_TRACKS[t.symbol]?.tracks === 'BTC').sort((x, y) => ASSET_TRACKS[x.symbol]!.rank - ASSET_TRACKS[y.symbol]!.rank).map((t) => t.symbol));54    expect(btc.representations.map((r) => r.assetSymbol)).toEqual(expect.arrayContaining(['WBTC', 'cbBTC', 'tBTC'])); // registry v555    expect(btc.representations[0]).toMatchObject({ assetSymbol: 'WBTC', kind: 'wrapped', issuer: expect.stringMatching(/BitGo/) });56    expect(btc.defaultId).toBe(eth.tokens.find((t) => t.symbol === 'WBTC')!.id);57    expect(btc.representations.find((r) => r.assetSymbol === 'cbBTC')).toMatchObject({ decimals: 8, issuer: expect.stringMatching(/Coinbase/) });58    expect(btc.representations.find((r) => r.assetSymbol === 'tBTC')).toMatchObject({ decimals: 18, issuer: expect.stringMatching(/Threshold/) }); // its own decimals, not WBTC's59    expect(sym(getNetwork('bsc'), 'BTC')!.representations.map((r) => r.assetSymbol)).toEqual(['WBTC', 'BTCB']);60    expect(sym(base, 'LINK')!).toMatchObject({ live: true, defaultId: base.tokens.find((t) => t.symbol === 'LINK')!.id });61    expect(sym(base, 'EUR')!).toMatchObject({ live: false, sources: ['bitstamp', 'kraken'] }); // EURC, paper only62    expect(sym(base, 'EUR')!.representations[0]).toMatchObject({ assetSymbol: 'EURC', kind: 'pegged', issuer: expect.stringMatching(/Circle/) });63    for (const id of Object.keys(KYBER_ENABLED)) { const n = getNetwork(id), t = tradableSymbols(n);64      expect(t.some((x) => x.symbol === n.native.symbol), id).toBe(true); // every native coin of an execution network has market data65      for (const x of t) { const at = `${id}:${x.symbol}`;66        expect(x.sources.length, at).toBeGreaterThanOrEqual(2); expect(['USDC', 'USDT']).not.toContain(x.symbol);67        // the default rule, restated: the native coin; else the token that is the symbol; else the lowest reviewed rank; a tie at the winning level → none68        const reps = x.representations, nat = reps.find((r) => r.kind === 'native'), own = reps.filter((r) => r.kind === 'token'), low = Math.min(...reps.map((r) => r.rank)), wrap = reps.filter((r) => r.rank === low);69        expect(x.defaultId, at).toBe(nat ? 'native' : own.length ? (own.length === 1 ? own[0]!.id : null) : wrap.length === 1 ? wrap[0]!.id : null);70        expect(x.defaultId, `${at}: today's registry never needs an explicit choice`).not.toBeNull();71        for (const r of reps) { expect(r.decimals, at).toBe([n.native, ...n.tokens].find((a) => a.id === r.id)!.decimals); // the registry's decimals for THAT network72          if (r.kind === 'wrapped' || r.kind === 'pegged') expect(r.issuer, `${at}:${r.assetSymbol}`).toBeTruthy(); else expect(r.assetSymbol, at).toBe(x.symbol); } } }73    expect(cashLegs(getNetwork('avalanche')).map((c) => c.symbol)).toEqual(['USDC', 'USDT']);74    // BNB Smart Chain since registry v5: Binance-Peg USDC and USDT, the only cash legs in 18 decimals75    expect(cashLegs(getNetwork('bsc')).map((c) => [c.symbol, c.decimals, /Binance-Peg/.test(c.name)])).toEqual([['USDC', 18, true], ['USDT', 18, true]]);76    for (const id of Object.keys(KYBER_ENABLED)) for (const c of cashLegs(getNetwork(id))) expect(c, id).toMatchObject({ id: getNetwork(id).tokens.find((t) => t.symbol === c.symbol)!.id, decimals: getNetwork(id).tokens.find((t) => t.symbol === c.symbol)!.decimals });77  });7879  it('spells tickers exactly as the registry does, and never takes a bridged or re-issued variant for the asset itself', () => {80    const all = Object.keys(KYBER_ENABLED).flatMap((id) => getNetwork(id).tokens.map((t) => ({ id, t })));81    const tickers = new Set(all.map((x) => x.t.symbol));82    // a table key that differs from a registry ticker only by case (CBBTC for cbBTC) would silently never match; BTCB (Binance) and BTC.b (Avalanche Bridge) are two assets83    const fold = (x: string) => x.toLowerCase();84    for (const k of Object.keys(ASSET_TRACKS)) for (const s of tickers) if (fold(s) === fold(k)) expect(s, `ASSET_TRACKS.${k} against the registry ticker ${s}`).toBe(k);85    for (const k of ['cbBTC', 'tBTC', 'BTCB', 'WBTC', 'WETH', 'cbXRP', 'cbDOGE', 'cbADA', 'cbLTC', 'WBNB', 'WAVAX', 'WPOL', 'WHYPE', 'EURC']) { expect(tickers.has(k), k).toBe(true); expect(ASSET_TRACKS[k], k).toBeDefined(); }86    // bridged dollars are other assets from other issuers: never a cash leg, never priced as USDC / USDT, not tradable87    for (const s of ['USDC.e', 'USDT0']) { expect(tickers.has(s), s).toBe(true);88      expect(marketSymbolOf(tok(s, 9)), s).toBe(s); expect(priceSymbolOf(tok(s, 9)), s).toEqual({ symbol: s, cash: false }); expect(hasMarketData(s), s).toBe(false); expect(ASSET_TRACKS[s], s).toBeUndefined(); }89    for (const { id, t } of all.filter((x) => /^USD[CT]/.test(x.t.symbol) && !['USDC', 'USDT'].includes(x.t.symbol))) {90      expect(cashLegs(getNetwork(id)).some((c) => c.id === t.id), `${id}:${t.symbol}`).toBe(false);91      expect(tradableSymbols(getNetwork(id)).some((x) => x.representations.some((r) => r.id === t.id)), `${id}:${t.symbol}`).toBe(false);92    }93    const onlyBridged: NetworkDef = { ...getNetwork('arbitrum'), tokens: getNetwork('arbitrum').tokens.filter((t) => !['USDC', 'USDT'].includes(t.symbol)) }; // USDC.e and USDT0 remain94    expect(onlyBridged.tokens.map((t) => t.symbol)).toEqual(expect.arrayContaining(['USDC.e', 'USDT0']));95    expect(cashLegs(onlyBridged)).toEqual([]); expect(() => resolveAgentAssets(onlyBridged, ['ETH'])).toThrow(/no cash leg/);96    // bridge tickers with no reviewed entry (no issuer named in the table) are their own symbol: no USD listing, so not tradable —97    // and they do not ride on the underlying's price either98    for (const s of ['BTC.b', 'WETH.e', 'LINK.e']) { expect(tickers.has(s), s).toBe(true); expect(ASSET_TRACKS[s], s).toBeUndefined();99      expect(marketSymbolOf(tok(s, 9)), s).toBe(s); expect(hasMarketData(s), s).toBe(false); }100    const avax = getNetwork('avalanche');101    expect(sym(avax, 'BTC')!.representations.map((r) => r.assetSymbol)).not.toContain('BTC.b'); expect(sym(avax, 'ETH')).toBeUndefined(); expect(sym(avax, 'LINK')).toBeUndefined();102    // in general: a registry ticker that is not a plain exchange ticker is tradable only through a reviewed entry naming its issuer103    for (const { id, t } of all) { const rep = tradableSymbols(getNetwork(id)).flatMap((x) => x.representations).find((r) => r.id === t.id);104      if (rep && !/^[A-Z0-9]{2,10}$/.test(t.symbol)) { expect(ASSET_TRACKS[t.symbol], `${id}:${t.symbol}`).toBeDefined(); expect(rep.issuer, `${id}:${t.symbol}`).toBeTruthy(); } }105  });106107  it('picks up assets the registry does not hold yet, with no code change', () => {108    const small = fixed('base', ['USDC', 'LINK']); // a fixed small registry: what Base held before these assets were added109    expect(sym(small, 'BTC')).toBeUndefined(); expect(sym(small, 'XRP')).toBeUndefined(); expect(sym(small, 'UNI')).toBeUndefined();110    const n = fixed('base', ['USDC', 'LINK'], [tok('cbBTC', 0xb1, 'Coinbase Wrapped BTC', 8), tok('tBTC', 0xb2, 'Threshold BTC'), tok('WBTC', 0xb3, 'Wrapped Bitcoin', 8), tok('cbXRP', 0xb4, 'Coinbase Wrapped XRP', 6), tok('UNI', 0xb5, 'Uniswap'),111      tok('WETH', 0xb6, 'Wrapped Ether'), tok('wstETH', 0xb7, 'Wrapped staked ETH'), tok('NEWCOIN', 0xb8), tok('BTC.b', 0xb9, 'Bitcoin (BTC.b)', 8), tok('USDC.e', 0xba, 'Bridged USDC', 6)]);112    expect(sym(n, 'BTC')!.representations.map((r) => r.assetSymbol)).toEqual(['WBTC', 'cbBTC', 'tBTC']); // reviewed rank order; BTC.b has no reviewed entry113    expect(sym(n, 'BTC')!.defaultId).toBe(tok('WBTC', 0xb3).id);114    expect(sym(n, 'XRP')!.representations[0]).toMatchObject({ assetSymbol: 'cbXRP', name: 'Coinbase Wrapped XRP', issuer: expect.stringMatching(/Coinbase/) });115    expect(sym(n, 'UNI')!.representations[0]!.kind).toBe('token');116    expect(sym(n, 'ETH')!.representations.map((r) => r.assetSymbol)).toEqual(['ETH', 'WETH']);117    expect(sym(n, 'wstETH')).toBeUndefined(); expect(sym(n, 'NEWCOIN')).toBeUndefined(); // no USD listing on two exchanges: no market data118    expect(tradableSymbols(n).map((t) => t.symbol)).toEqual(['BTC', 'ETH', 'LINK', 'UNI', 'XRP']); // nothing else: neither BTC.b nor USDC.e119    const r = resolveAgentAssets(n, ['BTC', 'XRP', 'UNI', 'ETH', 'LINK']);120    expect(r.assets.map((a) => [a.symbol, a.assetSymbol])).toEqual([['BTC', 'WBTC'], ['XRP', 'cbXRP'], ['UNI', 'UNI'], ['ETH', 'ETH'], ['LINK', 'LINK']]);121    expect(resolveAgentAssets(n, ['BTC', 'ETH'], { assets: { BTC: tok('cbBTC', 0xb1).id, ETH: tok('WETH', 0xb6).id } }).assets.map((a) => [a.assetSymbol, a.decimals, a.kind])).toEqual([['cbBTC', 8, 'wrapped'], ['WETH', 18, 'wrapped']]);122    // without WBTC the default falls to the next rank123    expect(sym(fixed('base', ['USDC'], [tok('cbBTC', 0xb1), tok('tBTC', 0xb2)]), 'BTC')!.defaultId).toBe(tok('cbBTC', 0xb1).id);124    expect(sym(fixed('base', ['USDC'], [tok('BTCB', 0xb1), tok('tBTC', 0xb2)]), 'BTC')!.defaultId).toBe(tok('tBTC', 0xb2).id);125    // a token that IS the symbol comes before any wrapper of it, whatever the wrapper's rank (rule 2)126    const own = fixed('polygon', ['USDC'], [tok('WETH', 0xb6), tok('ETH', 0xbb)]);127    expect(sym(own, 'ETH')!.representations.map((x) => [x.assetSymbol, x.kind])).toEqual([['ETH', 'token'], ['WETH', 'wrapped']]); expect(sym(own, 'ETH')!.defaultId).toBe(tok('ETH', 0xbb).id);128    // and the real registry of today resolves the same way with what it holds now129    const base = getNetwork('base');130    expect(resolveAgentAssets(base, ['BTC', 'XRP']).assets.map((a) => [a.symbol, a.assetSymbol, a.decimals])).toEqual([['BTC', 'WBTC', 8], ['XRP', 'cbXRP', 6]]);131    expect(resolveAgentAssets(base, ['BTC'], { assets: { BTC: base.tokens.find((t) => t.symbol === 'tBTC')!.id } }).assets[0]).toMatchObject({ assetSymbol: 'tBTC', decimals: 18 });132  });133134  it('refuses every ambiguous or wrong choice', () => {135    const base = getNetwork('base'), bare = fixed('base', ['USDC', 'LINK']);136    // two registry tokens with the same ticker at the winning level: no default, the owner must choose137    const twin = fixed('base', ['USDC', 'LINK'], [tok('cbBTC', 0xc1), tok('cbBTC', 0xc2)]);138    expect(sym(twin, 'BTC')!.defaultId).toBeNull();139    expect(() => resolveAgentAssets(twin, ['BTC'])).toThrow(/several representations.*choose one/);140    expect(resolveAgentAssets(twin, ['BTC'], { assets: { BTC: tok('cbBTC', 0xc2).id } }).assets[0]!.id).toBe(tok('cbBTC', 0xc2).id);141    // the same twins below a single better-ranked wrapper: the tie is not at the winning level, the default stands142    const under = fixed('base', ['USDC', 'LINK'], [tok('cbBTC', 0xc1), tok('cbBTC', 0xc2), tok('WBTC', 0xc3, 'Wrapped Bitcoin', 8)]);143    expect(sym(under, 'BTC')!.defaultId).toBe(tok('WBTC', 0xc3).id);144    // two tokens that both ARE the symbol: no default either145    const two = fixed('base', ['USDC'], [tok('UNI', 0xc4), tok('UNI', 0xc5)]);146    expect(sym(two, 'UNI')!.defaultId).toBeNull(); expect(() => resolveAgentAssets(two, ['UNI'])).toThrow(/choose one/);147    const link = base.tokens.find((t) => t.symbol === 'LINK')!.id;148    expect(() => resolveAgentAssets(base, ['ETH'], { assets: { ETH: link } })).toThrow(/does not track/); // LINK is not ETH149    expect(() => resolveAgentAssets(base, ['ETH'], { assets: { LINK: link } })).toThrow(/not in the universe/);150    expect(() => resolveAgentAssets(bare, ['BTC'])).toThrow(/no asset of the wallet registry tracks BTC on Base/);151    // on the real registry: a symbol the exchanges list but that no asset of the network tracks152    const absent = Object.keys(MARKET_LISTINGS).filter((s) => hasMarketData(s) && !['USDC', 'USDT'].includes(s) && !sym(base, s));153    expect(absent.length).toBeGreaterThan(0);154    for (const s of absent.slice(0, 5)) expect(() => resolveAgentAssets(base, [s]), s).toThrow(new RegExp(`no asset of the wallet registry tracks ${s} on Base`));155    expect(() => resolveAgentAssets(base, ['NOPE'])).toThrow(/no market data/);156    expect(() => resolveAgentAssets(base, ['ETH', 'ETH'])).toThrow(/duplicate/);157    expect(() => resolveAgentAssets(base, ['USDC'])).toThrow(/cash leg/);158    expect(() => resolveAgentAssets(base, [])).toThrow(/between 1 and 12/);159    expect(() => resolveAgentAssets(getNetwork('solana'), ['SOL'])).toThrow(/do not trade/);160    const many = getNetwork('ethereum'), all = tradableSymbols(many).map((t) => t.symbol);161    expect(all.length).toBeGreaterThan(MAX_AGENT_SYMBOLS);162    expect(resolveAgentAssets(many, all.slice(0, 12)).assets).toHaveLength(12);163    expect(() => resolveAgentAssets(many, all.slice(0, 13))).toThrow(/between 1 and 12/);164  });165166  it('cash leg: USDC by default, USDT only by explicit choice, nothing else', () => {167    const avax = getNetwork('avalanche');168    expect(resolveAgentAssets(avax, ['AVAX']).cash).toMatchObject({ symbol: 'USDC', kind: 'cash' });169    expect(resolveAgentAssets(avax, ['AVAX'], { cash: 'USDT' }).cash).toMatchObject({ symbol: 'USDT', decimals: 6 });170    // every execution-enabled network: USDC when the registry holds it there, in the registry's decimals for that network; USDT never by default171    for (const id of MANDATE_NETWORKS) { const n = getNetwork(id), legs = cashLegs(n), usdc = n.tokens.find((t) => t.symbol === 'USDC'), usdt = n.tokens.find((t) => t.symbol === 'USDT');172      if (usdc) expect(resolveAgentAssets(n, [n.native.symbol]).cash, id).toMatchObject({ symbol: 'USDC', id: usdc.id, decimals: usdc.decimals, name: usdc.name });173      else expect(() => resolveAgentAssets(n, [n.native.symbol]), id).toThrow(legs.length ? /choose USDT as the cash leg/ : /no cash leg/);174      if (usdt) expect(resolveAgentAssets(n, [n.native.symbol], { cash: 'USDT' }).cash, id).toMatchObject({ symbol: 'USDT', id: usdt.id, decimals: usdt.decimals });175      else expect(() => resolveAgentAssets(n, [n.native.symbol], { cash: 'USDT' }), id).toThrow(new RegExp(`USDT is not in the wallet registry on ${n.name}`)); }176    // BNB Smart Chain: Binance-Peg dollars in 18 decimals (everywhere else a cash leg has 6)177    expect(resolveAgentAssets(getNetwork('bsc'), ['BNB'])).toMatchObject({ cash: { symbol: 'USDC', decimals: 18, name: 'Binance-Peg USD Coin' }, assets: [{ symbol: 'BNB', id: 'native' }] });178    expect(resolveAgentAssets(getNetwork('bsc'), ['BNB'], { cash: 'USDT' }).cash).toMatchObject({ symbol: 'USDT', decimals: 18 });179    // fixed registries: a network holding USDC only, one holding USDT only, one holding neither180    const usdcOnly = fixed('base', ['USDC', 'LINK']);181    expect(() => resolveAgentAssets(usdcOnly, ['ETH'], { cash: 'USDT' })).toThrow(/USDT is not in the wallet registry on Base; choose USDC/);182    expect(() => resolveAgentAssets(getNetwork('base'), ['ETH'], { cash: 'USDS' })).toThrow(/cannot be a cash leg/);183    expect(() => resolveAgentAssets(getNetwork('base'), ['ETH'], { cash: 'USDC.e' })).toThrow(/cannot be a cash leg/);184    expect(() => resolveAgentAssets(fixed('bsc', []), ['BNB'])).toThrow(/no cash leg/);185    const bsc = fixed('bsc', [], [tok('USDT', 0xd1, 'Tether USD')]); // where USDC does not exist: the owner chooses USDT186    expect(() => resolveAgentAssets(bsc, ['BNB'])).toThrow(/choose USDT as the cash leg/);187    expect(resolveAgentAssets(bsc, ['BNB'], { cash: 'USDT' })).toMatchObject({ cash: { symbol: 'USDT', decimals: 18 }, assets: [{ symbol: 'BNB', id: 'native' }] });188    // two tokens answering to "USDC" on one network: neither is taken189    expect(cashLegs(fixed('base', ['USDC'], [tok('USDC', 0xd2, 'Another USDC', 6)]))).toEqual([]);190  });191});192193describe('mandates on every execution-enabled network', () => {194  const now = 1_800_000_000;195  const mandate = (network: string, o: Partial<Mandate> = {}): Mandate => { const n = getNetwork(network), cash = cashLegs(n)[0];196    const assets = ['native', ...(cash ? [cash.id] : [n.tokens[0]!.id])];197    return MandateSchema.parse({ v: 1, mandateId: '7a1c1f0e-8f5f-4b1e-9c56-000000000001', keyId: 'spw-0123456789abcdef', vaultIndex: 3, vault: '0x' + '11'.repeat(20), network, provider: 'kyberswap', assets,198      maxOrder: Object.fromEntries(assets.map((a) => [a, '1000'])), maxDaily: Object.fromEntries(assets.map((a) => [a, '5000'])), maxOrdersPerDay: 4, maxSlippageBps: 150, createdAt: now, expiresAt: now + 86400, strategySha256: 'ab'.repeat(32), label: 'x', ...o }); };199  it('are exactly the networks whose router was reviewed', () => {200    expect([...MANDATE_NETWORKS]).toEqual(['base', 'ethereum', 'arbitrum', 'polygon', 'optimism', 'bsc', 'avalanche', 'hyperevm']);201    expect([...MANDATE_NETWORKS]).toEqual(Object.keys(KYBER_ENABLED));202    for (const id of MANDATE_NETWORKS) expect(() => validateMandate(mandate(id), now), id).not.toThrow();203  });204  it('refuse any other network and any asset that is not of the mandate network', () => {205    for (const id of ['solana', 'bitcoin', 'tron']) expect(() => validateMandate({ ...mandate('base'), network: id }, now), id).toThrow();206    expect(() => validateMandate({ ...mandate('base'), network: 'nowhere' }, now)).toThrow(/unknown network/);207    const baseUsdc = cashLegs(getNetwork('base'))[0]!.id;208    for (const id of MANDATE_NETWORKS.filter((x) => x !== 'base')) // Base's USDC address is not an asset of any other network209      expect(() => validateMandate({ ...mandate(id), assets: ['native', baseUsdc], maxOrder: { native: '1' }, maxDaily: { native: '1' } }, now), id).toThrow(/not allowlisted/);210    expect(() => validateMandate({ ...mandate('arbitrum'), expiresAt: now + 91 * 86400 }, now)).toThrow(/90 days/); // limits unchanged211    expect(MandateSchema.safeParse({ ...mandate('base'), assets: Array.from({ length: 13 }, (_, k) => `a${k}`) }).success).toBe(false); // still 12 assets at most212  });213});214
Files main
Menu

Theme