src/history/funding.test.ts
v0.3.0 · 3.9 KB
import { describe, expect, it } from 'vitest';
import type { InfoRequest, InfoRequester } from '../transport/types.js';
import {
fetchFundingHistory,
fetchUserFunding,
fundingPayment,
sumFundingUsdc,
type FundingRateRecord,
type UserFundingEvent,
} from './funding.js';
const USER = '0x0000000000000000000000000000000000000000'; // privacy-allow: placeholder address
const H = 3_600_000;
function ev(time: number, coin: string, usdc: string): UserFundingEvent {
return { time, hash: '0x00', delta: { type: 'funding', coin, usdc, szi: '1', fundingRate: '0.0000125', nSamples: null } };
}
function server<T extends { time: number }>(rows: T[], cap: number) {
const calls: InfoRequest[] = [];
const info = (async (b: InfoRequest) => {
calls.push(b);
const end = b.endTime as number | undefined;
return rows.filter((r) => r.time >= (b.startTime as number) && (end === undefined || r.time <= end)).slice(0, cap);
}) as InfoRequester;
return { info, calls };
}
describe('fetchUserFunding', () => {
it('pages forward and keeps all coins paid at the same hourly timestamp', async () => {
// 30 hours x 40 coins, cap 500: pages cut inside one hourly timestamp.
const rows: UserFundingEvent[] = [];
for (let h = 0; h < 30; h += 1) for (let c = 0; c < 40; c += 1) rows.push(ev(h * H, `C${c}`, '-0.01'));
const { info, calls } = server(rows, 500);
const res = await fetchUserFunding(info, USER, { startTime: 0, endTime: 30 * H, pageDelayMs: 0 });
expect(res.items).toHaveLength(1200);
expect(res.complete).toBe(true);
expect(calls[0]).toEqual({ type: 'userFunding', user: USER, startTime: 0, endTime: 30 * H });
expect(sumFundingUsdc(res.items)).toBe('-12');
expect(sumFundingUsdc(res.items, 'C0')).toBe('-0.3');
});
it('with a known pageLimit a short page ends the loop without an extra request', async () => {
const rows = [ev(0, 'BTC', '1'), ev(H, 'BTC', '1')];
const { info, calls } = server(rows, 500);
await fetchUserFunding(info, USER, { startTime: 0, pageLimit: 500, pageDelayMs: 0 });
expect(calls).toHaveLength(1);
});
it('validates the window and propagates errors', async () => {
const { info } = server<UserFundingEvent>([], 10);
await expect(fetchUserFunding(info, USER, { startTime: 5, endTime: 1 })).rejects.toThrow(RangeError);
const failing = (async () => {
throw new Error('HTTP 429');
}) as InfoRequester;
await expect(fetchUserFunding(failing, USER, { startTime: 0 })).rejects.toThrow('HTTP 429');
});
});
describe('fetchFundingHistory', () => {
it('pages a coin rate history and sorts it', async () => {
const rows: FundingRateRecord[] = Array.from({ length: 1200 }, (_, i) => ({
coin: 'ETH',
fundingRate: '0.00001',
premium: '0',
time: i * H,
}));
const { info, calls } = server(rows, 500);
const res = await fetchFundingHistory(info, { coin: 'ETH', startTime: 0, pageDelayMs: 0, maxPages: 10 });
expect(res.items).toHaveLength(1200);
expect(res.items[1199]!.time).toBe(1199 * H);
expect(calls[0]).toEqual({ type: 'fundingHistory', coin: 'ETH', startTime: 0 });
expect(res.complete).toBe(true);
});
it('stops at maxPages as incomplete', async () => {
const rows: FundingRateRecord[] = Array.from({ length: 100 }, (_, i) => ({ coin: 'ETH', fundingRate: '0', premium: '0', time: i }));
const { info } = server(rows, 10);
const res = await fetchFundingHistory(info, { coin: 'ETH', startTime: 0, pageDelayMs: 0, maxPages: 2 });
expect(res.complete).toBe(false);
expect(res.pages).toBe(2);
});
});
describe('fundingPayment', () => {
it('positive rate: a long pays, a short receives', () => {
expect(fundingPayment('2', '100', '0.0001')).toBe('-0.02');
expect(fundingPayment('-2', '100', '0.0001')).toBe('0.02');
expect(fundingPayment('0', '100', '0.0001')).toBe('0');
expect(() => fundingPayment('x', '1', '1')).toThrow(/szi/);
});
});