Repository Public
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