Repository Public
1// Cosmos Hub adapter against two scripted REST sources (no network): agreement rules, refusals, fee and gas, tracking and broadcast outcomes.2import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';3import { decodeTxRaw } from '@cosmjs/proto-signing';4import { cosmosTxid, NETWORKS } from '@spw/core';5import type { Adapter } from '../src/chains/rpc.ts';67const n = NETWORKS.cosmos;8const [A, B] = n.rpc as [string, string];9const FROM = 'cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4', TO = 'cosmos1wnacchusmacys78rxcr640gjpz000f87gnz6zf', MODULE = 'cosmos13pxn9n3qw79e03844rdadagmg0nshmwf7qvuye';10const NEW = 'cosmos1uqdsdwwuujcrk95ur6wf6kwhjpajkmjmcjy3ed';11const TIP = 33_821_300;12const hashOf = (h: number) => Buffer.from(String(h).padStart(32, '0')).toString('base64');13const RAW = Buffer.from('signed transaction bytes').toString('base64'), TXID = cosmosTxid(RAW);1415interface Req { url: string; base: string; path: string; query: URLSearchParams; height: number | undefined; body: any }16type Reply = { status?: number; body: unknown; height?: number | null } | 'down';17type Handler = (r: Req) => Reply;18const notFound = (message = 'not found'): Reply => ({ status: 404, body: { code: 5, message, details: [] } });19const txError = (message: string): Reply => ({ status: 500, body: { code: 2, message, details: [] } });20const base = (addr: string, accountNumber: string, sequence: string) => ({ account: { '@type': '/cosmos.auth.v1beta1.BaseAccount', address: addr, pub_key: null, account_number: accountNumber, sequence } });2122/** A healthy pair of sources; individual tests override routes per source. Routes are matched by path prefix. */23function world(over: Record<string, Handler> = {}, perUrl: Record<string, Record<string, Handler | 'down'>> = {}) {24 const state = { tip: { [A]: TIP, [B]: TIP } as Record<string, number>, now: Date.now(),25 accounts: { [FROM]: base(FROM, '1425847', '7'), [TO]: base(TO, '99', '0'),26 [MODULE]: { account: { '@type': '/cosmos.auth.v1beta1.ModuleAccount', base_account: { address: MODULE, account_number: '2844531', sequence: '0' }, name: 'feemarket-fee-collector' } } } as Record<string, unknown>,27 balances: { [FROM]: '30000000' } as Record<string, string> };28 const block = (r: Req, h: number): Reply => ({ body: { block_id: { hash: hashOf(h) }, block: { header: { height: String(h), chain_id: 'cosmoshub-4', time: new Date(state.now).toISOString() } } }, height: null });29 const routes: Record<string, Handler> = {30 '/cosmos/base/tendermint/v1beta1/node_info': () => ({ body: { default_node_info: { network: 'cosmoshub-4' } }, height: null }),31 '/cosmos/base/tendermint/v1beta1/blocks/latest': (r) => block(r, state.tip[r.base]!),32 '/cosmos/base/tendermint/v1beta1/blocks/': (r) => block(r, Number(r.path.split('/').pop())),33 '/cosmos/auth/v1beta1/accounts/': (r) => { const a = state.accounts[r.path.split('/').pop()!]; return a ? { body: a } : notFound('account not found'); },34 '/cosmos/bank/v1beta1/spendable_balances/': (r) => ({ body: { balance: { denom: 'uatom', amount: state.balances[r.path.split('/')[5]!] ?? '0' } } }),35 '/feemarket/v1/gas_price/uatom': () => ({ body: { price: { denom: 'uatom', amount: '0.005000000000000000' } } }),36 '/cosmos/base/node/v1beta1/config': (r) => ({ body: { minimum_gas_price: r.base === A ? '0.005000000000000000uatom' : '0.0025uatom,0.1ibc/ABC' } }),37 '/cosmos/tx/v1beta1/simulate': () => ({ body: { gas_info: { gas_wanted: '75000000', gas_used: '86054' } } }),38 '/cosmos/tx/v1beta1/txs/': () => notFound('tx not found'),39 '/cosmos/tx/v1beta1/txs': (r) => (r.body ? { body: { tx_response: { code: 0, txhash: TXID, raw_log: '' } } } : { body: { txs: [], tx_responses: [], total: '0' } }),40 ...over,41 };42 const find = (table: Record<string, Handler | 'down'>, path: string) => Object.keys(table).filter((k) => (k.endsWith('/') ? path.startsWith(k) : path === k)).sort((x, y) => y.length - x.length)[0];43 const calls: Req[] = [];44 vi.stubGlobal('fetch', async (url: string, init: RequestInit = {}) => {45 const u = new URL(url), baseUrl = [A, B].find((s) => url.startsWith(s))!;46 const hdr = (init.headers ?? {}) as Record<string, string>;47 const req: Req = { url, base: baseUrl, path: u.pathname, query: u.searchParams, height: hdr['x-cosmos-block-height'] ? Number(hdr['x-cosmos-block-height']) : undefined, body: init.body ? JSON.parse(String(init.body)) : undefined };48 calls.push(req);49 const mine = perUrl[baseUrl] ?? {}, k = find(mine, u.pathname);50 const h = k !== undefined ? mine[k]! : routes[find(routes, u.pathname)!];51 if (h === 'down' || !h) throw new TypeError('fetch failed');52 const r = h(req);53 if (r === 'down') throw new TypeError('fetch failed');54 const height = r.height === undefined ? (req.height ?? state.tip[baseUrl]!) : r.height;55 return new Response(JSON.stringify(r.body), { status: r.status ?? 200, headers: height === null || (r.status ?? 200) !== 200 ? {} : { 'x-cosmos-block-height': String(height) } });56 });57 return { state, calls };58}5960let make: () => Adapter;61beforeAll(async () => {62 Object.assign(process.env, { SPW_ORIGIN: 'https://wallet.test', SPW_RP_ID: 'wallet.test', DATABASE_URL: 'postgres://unused', SPW_SIGNER_SECRET: 'x'.repeat(48), SPW_BROADCAST: 'off', SPW_WORKERS: 'off' });63 const { cosmosAdapter } = await import('../src/chains/cosmos.ts');64 make = () => cosmosAdapter(n);65});66afterEach(() => vi.unstubAllGlobals());67const req = { asset: 'native', from: FROM, to: TO, amount: 2_000_000n };68const code = async (p: Promise<unknown>) => p.then(() => 'ok', (e: { code?: string; message: string }) => e.code ?? `error:${e.message}`);69const ACCOUNTS = '/cosmos/auth/v1beta1/accounts/', BLOCKS = '/cosmos/base/tendermint/v1beta1/blocks/', LATEST = BLOCKS + 'latest', SIM = '/cosmos/tx/v1beta1/simulate', TXS = '/cosmos/tx/v1beta1/txs';70const X = { account_number: '1425847', sequence: '7', gas_limit: '200000', fee_amount: '1500', timeout_height: String(TIP + 30) };7172describe('cosmos adapter: health and identity', () => {73 it('is ok only when both sources are up, report the chain id and agree on a recent block', async () => {74 world();75 expect(await make().health()).toMatchObject({ ok: true, readable: true, identityVerified: true, height: TIP });76 world({}, { [B]: { [LATEST]: 'down' } });77 expect(await make().health()).toMatchObject({ ok: false, readable: true, identityVerified: false });78 world({}, { [B]: { '/cosmos/base/tendermint/v1beta1/node_info': () => ({ body: { default_node_info: { network: 'theta-testnet-001' } } }) } });79 expect(await make().health()).toMatchObject({ ok: false, identityVerified: false });80 world({}, { [B]: { [BLOCKS]: (r) => ({ body: { block_id: { hash: hashOf(1) }, block: { header: { height: r.path.split('/').pop() === 'latest' ? String(TIP) : r.path.split('/').pop(), chain_id: 'cosmoshub-4', time: new Date().toISOString() } } } }) } });81 expect(await make().health()).toMatchObject({ ok: false, readable: true, identityVerified: false }); // same chain id, another block at that height (fork / impostor)82 world({}, { [A]: { [LATEST]: (r) => ({ body: { block_id: { hash: hashOf(TIP) }, block: { header: { height: String(TIP), chain_id: 'cosmoshub-4', time: new Date(Date.now() - 600_000).toISOString() } } } }) } });83 expect(await make().health()).toMatchObject({ ok: false, readable: true }); // a stalled node is not a source84 world({}, { [A]: { [LATEST]: 'down' }, [B]: { [LATEST]: 'down' } });85 expect(await make().health()).toMatchObject({ ok: false, readable: false });86 });87 it('prepare refuses when the identity cannot be verified on both sources', async () => {88 world({}, { [B]: { '/cosmos/base/tendermint/v1beta1/node_info': () => ({ body: { default_node_info: { network: 'cosmoshub-3' } } }) } });89 expect(await code(make().prepare(req))).toBe('rpc_degraded');90 });91});9293describe('cosmos adapter: prepare', () => {94 it('returns account number, sequence, gas, fee and timeout = reference block + 30, all in the intent section', async () => {95 const w = world();96 const p = await make().prepare({ ...req, memo: '104588310' });97 // fee = ceil(200 000 gas × 0.005 uatom × 1.5)98 expect(p).toEqual({ section: { x: X }, maxFee: 1500n, feeEstimate: 1500n, warnings: [], ttlSec: 90 });99 // every state read that feeds the signature was made on both sources, pinned to the reference block100 for (const path of [ACCOUNTS + FROM, ACCOUNTS + TO, `/cosmos/bank/v1beta1/spendable_balances/${FROM}/by_denom`, '/feemarket/v1/gas_price/uatom'])101 expect(w.calls.filter((c) => c.path === path && c.height === TIP).map((c) => c.base).sort(), path).toEqual([A, B].sort());102 expect(w.calls.filter((c) => c.path === BLOCKS + TIP).length).toBe(2);103 // the dry run is this exact transfer, without key and signature, on both sources; nothing is broadcast104 const sims = w.calls.filter((c) => c.path === SIM);105 expect(sims.map((c) => c.base).sort()).toEqual([A, B].sort());106 const d = decodeTxRaw(Uint8Array.from(Buffer.from(sims[0]!.body.tx_bytes, 'base64')));107 expect(d.body).toMatchObject({ memo: '104588310', timeoutHeight: BigInt(TIP + 30) });108 expect(d.authInfo.fee).toMatchObject({ amount: [{ denom: 'uatom', amount: '1500' }], gasLimit: 200000n });109 expect(d.authInfo.signerInfos[0]).toMatchObject({ publicKey: undefined, sequence: 7n });110 expect(d.signatures.map((s) => s.length)).toEqual([0]);111 expect(w.calls.some((c) => c.path === TXS && c.body)).toBe(false);112 });113 it('uses the lower tip as the reference block, the highest gas of the two simulations and the highest required price', async () => {114 const w = world({}, { [B]: { [SIM]: () => ({ body: { gas_info: { gas_used: '130001' } } }) } });115 w.state.tip[B] = TIP - 2;116 const p = await make().prepare(req);117 expect(p.section).toEqual({ x: { ...X, gas_limit: '260002', fee_amount: '1951', timeout_height: String(TIP - 2 + 30) } }); // ceil(260002 × 0.0075)118 expect(p.maxFee).toBe(1951n);119 // a node whose own mempool floor is above the consensus price: its floor is what gets a transaction accepted there120 world({}, { [B]: { '/cosmos/base/node/v1beta1/config': () => ({ body: { minimum_gas_price: '0.02uatom' } }) } });121 expect((await make().prepare(req)).maxFee).toBe(6000n);122 world({ '/feemarket/v1/gas_price/uatom': () => ({ body: { price: { denom: 'uatom', amount: '0.2' } } }) });123 expect(await code(make().prepare(req))).toBe('fee_too_high'); // 60 000 uatom > ceiling: refuse rather than underpay or overpay124 world({ [SIM]: () => ({ body: { gas_info: { gas_used: '600000' } } }) });125 expect(await code(make().prepare(req))).toBe('simulation_failed'); // not what a plain send costs126 });127 it('fails closed when a source is down or the sources disagree on anything that feeds the signature', async () => {128 for (const m of [LATEST, BLOCKS, ACCOUNTS, '/cosmos/bank/v1beta1/spendable_balances/', '/feemarket/v1/gas_price/uatom', '/cosmos/base/node/v1beta1/config', SIM, '/cosmos/base/tendermint/v1beta1/node_info']) {129 world({}, { [B]: { [m]: 'down' } });130 expect(await code(make().prepare(req)), m).toBe('rpc_degraded');131 }132 const w = world(); w.state.tip[A] = TIP + 9;133 expect(await code(make().prepare(req))).toBe('rpc_disagreement'); // too far apart134 world({}, { [B]: { [BLOCKS]: (r) => ({ body: { block_id: { hash: hashOf(r.path.endsWith('latest') ? TIP : 1) }, block: { header: { height: r.path.endsWith('latest') ? String(TIP) : r.path.split('/').pop(), chain_id: 'cosmoshub-4', time: new Date().toISOString() } } } }) } });135 expect(await code(make().prepare(req))).toBe('rpc_disagreement'); // different block at the reference height (fork)136 for (const lie of [base(FROM, '1425847', '8'), base(FROM, '1425848', '7')]) {137 world({}, { [B]: { [ACCOUNTS + FROM]: () => ({ body: lie }) } });138 expect(await code(make().prepare(req)), JSON.stringify(lie)).toBe('rpc_disagreement');139 }140 world({}, { [B]: { [ACCOUNTS + TO]: () => notFound() } });141 expect(await code(make().prepare(req))).toBe('rpc_disagreement'); // destination exists on one source only142 world({}, { [B]: { '/cosmos/bank/v1beta1/spendable_balances/': () => ({ body: { balance: { denom: 'uatom', amount: '30000001' } } }) } });143 expect(await code(make().prepare(req))).toBe('rpc_disagreement');144 world({}, { [B]: { '/feemarket/v1/gas_price/uatom': () => ({ body: { price: { denom: 'uatom', amount: '0.005000000000000001' } } }) } });145 expect(await code(make().prepare(req))).toBe('rpc_disagreement');146 world({}, { [B]: { [ACCOUNTS]: (r) => ({ body: base(FROM, '1425847', '7'), height: TIP - 3 }) } });147 expect(await code(make().prepare(req))).toBe('rpc_degraded'); // answered from another height than the one asked148 world({}, { [B]: { [LATEST]: () => ({ body: { block_id: { hash: hashOf(TIP) }, block: { header: { height: String(TIP), chain_id: 'cosmoshub-5', time: new Date().toISOString() } } } }) } });149 expect(await code(make().prepare(req))).toBe('rpc_degraded'); // a source on another chain150 for (const bad of [{ balance: { denom: 'uatom', amount: '-5' } }, { balance: { denom: 'ibc/ABC', amount: '5' } }, {}]) {151 world({ '/cosmos/bank/v1beta1/spendable_balances/': () => ({ body: bad }) });152 expect(await code(make().prepare(req))).toBe('rpc_degraded');153 }154 world({ '/feemarket/v1/gas_price/uatom': () => ({ body: { price: { denom: 'uatom', amount: '0' } } }), '/cosmos/base/node/v1beta1/config': () => ({ body: { minimum_gas_price: '' } }) });155 expect(await code(make().prepare(req))).toBe('rpc_degraded'); // a zero price is not believable156 world({ [SIM]: () => ({ status: 502, body: 'bad gateway' }) });157 expect(await code(make().prepare(req))).toBe('rpc_degraded');158 });159 it('refuses what the chain would refuse, before anything is signed', async () => {160 world();161 expect(await code(make().prepare({ ...req, asset: 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2' }))).toBe('unsupported_asset');162 expect(await code(make().prepare({ ...req, amount: 0n }))).toBe('bad_amount');163 expect(await code(make().prepare({ ...req, memo: ' 104588310' }))).toBe('bad_memo');164 expect(await code(make().prepare({ ...req, memo: '' }))).toBe('bad_memo');165 expect(await code(make().prepare({ ...req, to: FROM }))).toBe('bad_address');166 expect(await code(make().prepare({ ...req, to: 'osmo19rl4cm2hmr8afy4kldpxz3fka4jguq0a5m7df8' }))).toBe('bad_address');167 expect(await code(make().prepare({ ...req, to: MODULE }))).toBe('bad_destination');168 expect(await code(make().prepare({ ...req, from: NEW }))).toBe('insufficient_funds'); // account not on chain yet169 expect(await code(make().prepare({ ...req, from: MODULE, to: TO }))).toBe('unsupported_account');170 expect(await code(make().prepare({ ...req, to: NEW }))).toBe('ok'); // the transfer creates the destination account171 expect(await code(make().prepare({ ...req, amount: 30_000_000n - 1500n }))).toBe('ok'); // exactly the balance172 expect(await code(make().prepare({ ...req, amount: 30_000_000n - 1499n }))).toBe('insufficient_funds');173 world({ [SIM]: () => txError('account sequence mismatch, expected 8, got 7: incorrect account sequence [cosmos/cosmos-sdk@v0.53.8/x/auth/ante/sigverify.go:370] with gas used: \'13750\'') });174 expect(await code(make().prepare(req))).toBe('pending_tx');175 world({}, { [B]: { [SIM]: () => txError('failed to execute message; message index: 0: spendable balance 3uatom is smaller than 2000000uatom: insufficient funds') } });176 expect(await code(make().prepare(req))).toBe('simulation_failed'); // one source's verdict is enough to refuse177 });178});179180describe('cosmos adapter: tracking', () => {181 const tracked = (x: Record<string, string> = X) => make().track({ txid: TXID, intent: { from: FROM, x }, blockRef: null, broadcastAt: 0 });182 const found = (c: number, extra: object = {}): Handler => () => ({ body: { tx_response: { txhash: TXID, height: String(TIP + 3), code: c, raw_log: c ? 'out of gas' : '', ...extra } } });183 it('a transaction in a block with code 0 is final; any other code is a failure', async () => {184 world({ [TXS + '/']: found(0) });185 expect(await tracked()).toEqual({ state: 'finalized', confirmations: 1, blockRef: String(TIP + 3) });186 world({}, { [A]: { [TXS + '/']: 'down' }, [B]: { [TXS + '/']: found(0) } });187 expect(await tracked()).toMatchObject({ state: 'finalized' });188 world({ [TXS + '/']: found(11, { codespace: 'sdk' }) });189 expect(await tracked()).toMatchObject({ state: 'failed', blockRef: String(TIP + 3), error: expect.stringMatching(/code 11 sdk: out of gas.*only the network fee/) });190 world({ [TXS + '/']: () => ({ body: { tx_response: { txhash: 'CD'.repeat(32), height: '5', code: 0 } } }) });191 expect(await tracked()).toMatchObject({ state: 'broadcast' }); // an answer about another transaction proves nothing192 });193 it('declares it dead only past the timeout height, on both sources, with the sequence still unused', async () => {194 let w = world();195 expect(await tracked()).toEqual({ state: 'broadcast', confirmations: 0, rebroadcast: true }); // window still open196 w = world(); w.state.tip[A] = TIP + 31; w.state.tip[B] = TIP + 30;197 expect(await tracked()).toMatchObject({ state: 'broadcast' }); // one source has not passed the timeout yet198 w = world(); w.state.tip[A] = TIP + 31; w.state.tip[B] = TIP + 31;199 expect(await tracked()).toEqual({ state: 'failed', confirmations: 0, error: 'expired before inclusion (timeout height passed); no funds moved' });200 w = world({}, { [B]: { [TXS + '/']: 'down' } }); w.state.tip[A] = TIP + 99; w.state.tip[B] = TIP + 99;201 expect(await tracked()).toMatchObject({ state: 'broadcast' }); // one source cannot say: never "failed" on half the evidence202 w = world({}, { [B]: { [LATEST]: 'down' } }); w.state.tip[A] = TIP + 99;203 expect(await tracked()).toMatchObject({ state: 'broadcast' });204 // sequence consumed although neither index knows the hash: not provably ours → wait, then "replaced", never "failed"205 w = world(); w.state.tip[A] = TIP + 35; w.state.tip[B] = TIP + 35; w.state.accounts[FROM] = base(FROM, '1425847', '8');206 expect(await tracked()).toEqual({ state: 'broadcast', confirmations: 0 });207 w = world(); w.state.tip[A] = TIP + 51; w.state.tip[B] = TIP + 51; w.state.accounts[FROM] = base(FROM, '1425847', '8');208 expect(await tracked()).toEqual({ state: 'replaced', confirmations: 0, error: 'the sequence was consumed by another transaction' });209 w = world({}, { [B]: { [ACCOUNTS]: 'down' } }); w.state.tip[A] = TIP + 99; w.state.tip[B] = TIP + 99;210 expect(await tracked()).toEqual({ state: 'broadcast', confirmations: 0 });211 });212});213214describe('cosmos adapter: broadcast, balances, deposits', () => {215 it('broadcasts to every source; success on one is enough, "already known" and "already in a block" are success', async () => {216 let w = world();217 await make().broadcast(RAW);218 expect(w.calls.filter((c) => c.path === TXS && c.body).map((c) => [c.base, c.body.tx_bytes, c.body.mode])).toEqual([[A, RAW, 'BROADCAST_MODE_SYNC'], [B, RAW, 'BROADCAST_MODE_SYNC']]);219 world({}, { [A]: { [TXS]: 'down' } });220 await make().broadcast(RAW);221 world({ [TXS]: () => ({ body: { tx_response: { code: 19, codespace: 'sdk', raw_log: 'tx already in mempool' } } }) });222 await make().broadcast(RAW);223 w = world({ [TXS]: () => ({ body: { tx_response: { code: 32, codespace: 'sdk', raw_log: 'account sequence mismatch, expected 8, got 7' } } }) });224 await expect(make().broadcast(RAW)).rejects.toThrow(/code 32 \(sdk\): account sequence mismatch/);225 world({ [TXS]: () => ({ body: { tx_response: { code: 32, codespace: 'sdk', raw_log: 'account sequence mismatch' } } }), [TXS + '/']: () => ({ body: { tx_response: { txhash: TXID, height: '5', code: 0 } } }) });226 await make().broadcast(RAW); // rejected because this very transaction is already in a block227 world({ [TXS]: () => ({ body: { tx_response: { code: 0, txhash: 'EF'.repeat(32) } } }) });228 await expect(make().broadcast(RAW)).rejects.toThrow(); // a source acknowledging another hash did not take ours229 world({ [TXS]: () => ({ body: { tx_response: { code: 13, codespace: 'sdk', raw_log: 'insufficient fee' } } }) });230 await expect(make().broadcast(RAW)).rejects.toThrow(/insufficient fee/);231 await expect(make().broadcast('not base64!')).rejects.toThrow(/invalid transaction payload/);232 });233 it('reads the spendable native balance with failover; an account not on chain reads as zero', async () => {234 world({}, { [A]: { '/cosmos/bank/v1beta1/spendable_balances/': 'down' } });235 expect(await make().balances(FROM)).toEqual({ native: 30_000_000n });236 expect(await make().balances(NEW)).toEqual({ native: 0n });237 await expect(make().balances('cosmos1xyz')).rejects.toThrow(/invalid address/);238 });239 it('lists incoming native transfers from committed, successful transactions only', async () => {240 const ev = (recipient: string, sender: string, amount: string) => ({ type: 'transfer', attributes: [{ key: 'recipient', value: recipient }, { key: 'sender', value: sender }, { key: 'amount', value: amount }] });241 const tx = (height: number, hash: string, c: number, events: unknown[]) => ({ height: String(height), txhash: hash, code: c, timestamp: '2026-11-11T00:21:53Z', events });242 const list = [243 tx(TIP - 1, 'A1'.repeat(32), 0, [ev(MODULE, TO, '700uatom'), ev(FROM, TO, '11952053uatom')]), // fee payment, then the send244 tx(TIP - 2, 'A2'.repeat(32), 0, [ev(FROM, TO, '5ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2,42uatom'), ev(FROM, MODULE, '8uatom')]), // mixed coins; two credits in one transaction245 tx(TIP - 3, 'A3'.repeat(32), 5, [ev(FROM, TO, '999uatom')]), // failed transaction246 tx(TIP - 4, 'A4'.repeat(32), 0, [ev(FROM, FROM, '1uatom'), ev(TO, FROM, '77uatom'), ev(FROM, TO, '9xuatom'), ev(FROM, TO, '0uatom')]), // self, outgoing, another denom, zero247 tx(TIP - 500, 'A5'.repeat(32), 0, [ev(FROM, TO, '3uatom')]),248 tx(TIP + 7, 'A6'.repeat(32), 0, [ev(FROM, TO, '4uatom')]), // "above the tip": not believable249 ];250 const w = world({ [TXS]: (r) => ({ body: { tx_responses: r.query.get('page') === '1' ? list : [], total: String(list.length) } }) });251 const first = await make().deposits(FROM, null);252 expect(first.cursor).toBe(TIP);253 expect(first.finalizedHeight).toBe(TIP);254 expect(first.items).toEqual([255 { txid: 'A1'.repeat(32), idx: 1, asset: 'native', amount: 11952053n, from: TO, block: TIP - 1, time: 1794356513 },256 { txid: 'A2'.repeat(32), idx: 0, asset: 'native', amount: 42n, from: TO, block: TIP - 2, time: 1794356513 },257 { txid: 'A2'.repeat(32), idx: 1, asset: 'native', amount: 8n, from: MODULE, block: TIP - 2, time: 1794356513 },258 { txid: 'A5'.repeat(32), idx: 0, asset: 'native', amount: 3n, from: TO, block: TIP - 500, time: 1794356513 },259 ]);260 const q = w.calls.find((c) => c.path === TXS)!.query;261 expect([q.get('query'), q.get('order_by'), q.get('page'), q.get('limit')]).toEqual([`transfer.recipient='${FROM}'`, 'ORDER_BY_DESC', '1', '50']);262 expect((await make().deposits(FROM, TIP - 2)).items.map((d) => d.txid)).toEqual(['A1'.repeat(32)]); // only what is above the cursor263 expect(await make().deposits(FROM, TIP)).toEqual({ items: [], cursor: TIP, finalizedHeight: TIP });264 });265});266