src/transport/address-budget.test.ts
v0.3.0 · 11.9 KB
import { describe, expect, it } from 'vitest';
import {
ADDRESS_BASE_REQUESTS,
ADDRESS_BUDGET_MIN_POLL_MS,
ADDRESS_BUDGET_POLL_MS,
ADDRESS_EXHAUSTED_INTERVAL_MS,
REQUEST_PRICE_USD,
addressCancelCap,
baseAddressRequestCap,
budgetMode,
computeAddressBudget,
getAddressBudget,
hoursToExhaustion,
parseUserRateLimit,
reserveRequestCostUsd,
surplusAlreadyInCap,
} from './address-budget.js';
import type { InfoRequest, InfoRequester } from './types.js';
const ADDR_MIXED = '0x00000000000000000000000000000000000000Cd'; // privacy-allow: placeholder address
const ADDR_LOWER = '0x00000000000000000000000000000000000000cd'; // privacy-allow: placeholder address
function stubInfo(response: unknown) {
const calls: InfoRequest[] = [];
const info: InfoRequester = async <T>(body: InfoRequest) => {
calls.push(body);
return response as T;
};
return { info, calls };
}
describe('constants', () => {
it('match the documented address limit', () => {
expect(ADDRESS_BASE_REQUESTS).toBe(10_000);
expect(REQUEST_PRICE_USD).toBe(0.0005);
expect(ADDRESS_EXHAUSTED_INTERVAL_MS).toBe(10_000);
expect(ADDRESS_BUDGET_POLL_MS).toBe(60_000);
expect(ADDRESS_BUDGET_MIN_POLL_MS).toBe(5_000);
});
});
describe('baseAddressRequestCap', () => {
it('is 10 000 + 1 per whole $1 of volume (live: $0 -> 10 000, $10 -> 10 010)', () => {
expect(baseAddressRequestCap('0')).toBe(10_000);
expect(baseAddressRequestCap('0.0')).toBe(10_000);
expect(baseAddressRequestCap('10')).toBe(10_010);
expect(baseAddressRequestCap('10.999999')).toBe(10_010);
expect(baseAddressRequestCap(10.5)).toBe(10_010);
expect(baseAddressRequestCap('123456789.29')).toBe(123_466_789);
});
it('does not use float rounding on long decimals', () => {
expect(baseAddressRequestCap('0.29')).toBe(10_000);
expect(baseAddressRequestCap('99999999999.999999999999')).toBe(100_000_009_999);
});
it('rejects non-decimal input', () => {
expect(() => baseAddressRequestCap('-1')).toThrow(TypeError);
expect(() => baseAddressRequestCap('abc')).toThrow(TypeError);
expect(() => baseAddressRequestCap(Number.NaN)).toThrow(TypeError);
});
});
describe('addressCancelCap', () => {
it('is min(limit + 100 000, limit * 2)', () => {
expect(addressCancelCap(10_000)).toBe(20_000);
expect(addressCancelCap(100_000)).toBe(200_000);
expect(addressCancelCap(250_000)).toBe(350_000);
});
});
describe('budgetMode', () => {
it('freeze < 10%, economy < 30%, otherwise normal', () => {
expect(budgetMode(9.99)).toBe('freeze');
expect(budgetMode(10)).toBe('economy');
expect(budgetMode(29.99)).toBe('economy');
expect(budgetMode(30)).toBe('normal');
expect(budgetMode(100)).toBe('normal');
expect(budgetMode(50, 60, 80)).toBe('freeze');
});
it('fails closed on NaN', () => {
expect(budgetMode(Number.NaN)).toBe('freeze');
});
});
describe('computeAddressBudget', () => {
it('derives remaining from cap, used and local placements', () => {
const b = computeAddressBudget({ cumVlm: '10.0', nRequestsUsed: 77, nRequestsCap: 10_010, nRequestsSurplus: 0 });
expect(b.remaining).toBe(9_933);
expect(b.mode).toBe('normal');
expect(b.exhausted).toBe(false);
const after = computeAddressBudget({ cumVlm: '10.0', nRequestsUsed: 77, nRequestsCap: 10_010 }, { placesSinceRead: 33 });
expect(after.remaining).toBe(9_900);
});
it('adds the bought surplus by default and can skip it to avoid double counting', () => {
const rl = { cumVlm: '0', nRequestsUsed: 9_000, nRequestsCap: 10_000, nRequestsSurplus: 2_000 };
expect(computeAddressBudget(rl).effectiveCap).toBe(12_000);
expect(computeAddressBudget(rl).remaining).toBe(3_000);
expect(computeAddressBudget(rl, { countSurplus: false }).remaining).toBe(1_000);
expect(computeAddressBudget({ ...rl, nRequestsSurplus: -5 }).surplus).toBe(0);
});
it('reports economy, freeze and exhaustion', () => {
expect(computeAddressBudget({ cumVlm: '0', nRequestsUsed: 7_500, nRequestsCap: 10_000 }).mode).toBe('economy');
expect(computeAddressBudget({ cumVlm: '0', nRequestsUsed: 9_500, nRequestsCap: 10_000 }).mode).toBe('freeze');
const dead = computeAddressBudget({ cumVlm: '0', nRequestsUsed: 12_000, nRequestsCap: 10_000 });
expect(dead.remaining).toBe(0);
expect(dead.exhausted).toBe(true);
expect(dead.mode).toBe('freeze');
const zeroCap = computeAddressBudget({ cumVlm: '0', nRequestsUsed: 0, nRequestsCap: 0 });
expect(zeroCap.remainingPct).toBe(0);
expect(zeroCap.exhausted).toBe(true);
});
});
describe('parseUserRateLimit', () => {
it('accepts the documented shape, with or without surplus', () => {
expect(parseUserRateLimit({ cumVlm: '12.5', nRequestsUsed: 1, nRequestsCap: 10_012, nRequestsSurplus: 0 })).toEqual({
cumVlm: '12.5',
nRequestsUsed: 1,
nRequestsCap: 10_012,
nRequestsSurplus: 0,
});
expect(parseUserRateLimit({ cumVlm: '0', nRequestsUsed: 0, nRequestsCap: 10_000 })).toEqual({
cumVlm: '0',
nRequestsUsed: 0,
nRequestsCap: 10_000,
});
});
it('throws on malformed replies instead of inventing zeros', () => {
expect(() => parseUserRateLimit(null)).toThrow(TypeError);
expect(() => parseUserRateLimit([])).toThrow(TypeError);
expect(() => parseUserRateLimit({ cumVlm: '0', nRequestsUsed: '5', nRequestsCap: 10_000 })).toThrow(TypeError);
expect(() => parseUserRateLimit({ cumVlm: '0', nRequestsUsed: 5 })).toThrow(TypeError);
expect(() => parseUserRateLimit({ cumVlm: 'NaN', nRequestsUsed: 5, nRequestsCap: 10_000 })).toThrow(TypeError);
expect(() => parseUserRateLimit({ cumVlm: '0', nRequestsUsed: 5, nRequestsCap: 10_000, nRequestsSurplus: 'x' })).toThrow(TypeError);
});
});
describe('getAddressBudget', () => {
it('reads userRateLimit with a lower-cased user and derives the budget', async () => {
const { info, calls } = stubInfo({ cumVlm: '10.0', nRequestsUsed: 4, nRequestsCap: 10_010, nRequestsSurplus: 0 });
const b = await getAddressBudget(info, ADDR_MIXED, { placesSinceRead: 6 });
expect(calls).toEqual([{ type: 'userRateLimit', user: ADDR_LOWER }]);
expect(b.user).toBe(ADDR_LOWER);
expect(b.remaining).toBe(10_000);
expect(b.cap).toBe(10_010);
expect(b.raw.cumVlm).toBe('10.0');
});
it('validates the address before spending weight', async () => {
const { info, calls } = stubInfo({});
await expect(getAddressBudget(info, '0xYOUR_ADDRESS')).rejects.toThrow(TypeError);
await expect(getAddressBudget(info, '0x1234')).rejects.toThrow(TypeError);
expect(calls).toHaveLength(0);
});
it('propagates transport errors and rejects malformed responses', async () => {
const failing: InfoRequester = async () => {
throw new Error('HTTP 429');
};
await expect(getAddressBudget(failing, ADDR_LOWER)).rejects.toThrow('429');
await expect(getAddressBudget(stubInfo({ nope: true }).info, ADDR_LOWER)).rejects.toThrow(/malformed/);
});
it('forwards the abort signal', async () => {
let seen: AbortSignal | undefined;
const info: InfoRequester = async <T>(_b: InfoRequest, opts?: { signal?: AbortSignal }) => {
seen = opts?.signal;
return { cumVlm: '0', nRequestsUsed: 0, nRequestsCap: 10_000 } as T;
};
const ctl = new AbortController();
await getAddressBudget(info, ADDR_LOWER, { signal: ctl.signal });
expect(seen).toBe(ctl.signal);
});
});
describe('hoursToExhaustion', () => {
it('uses the net drain: placements minus $ volume per hour', () => {
expect(hoursToExhaustion(1_000, 500, 100)).toBe(2.5);
expect(hoursToExhaustion(0, 500, 100)).toBe(0);
});
it('is null when volume pays for the placements or remaining is unknown', () => {
expect(hoursToExhaustion(1_000, 100, 100)).toBeNull();
expect(hoursToExhaustion(1_000, 50, 100)).toBeNull();
expect(hoursToExhaustion(Number.POSITIVE_INFINITY, 500, 0)).toBeNull();
expect(hoursToExhaustion(1_000, Number.NaN, 0)).toBeNull();
});
});
describe('reserveRequestCostUsd', () => {
it('prices requests at 0.0005 USDC (2 000 = $1, 10 000 = $5)', () => {
expect(reserveRequestCostUsd(2_000)).toBe(1);
expect(reserveRequestCostUsd(10_000)).toBe(5);
expect(reserveRequestCostUsd(100)).toBe(0.05);
expect(reserveRequestCostUsd(1)).toBe(0.0005);
expect(reserveRequestCostUsd(0)).toBe(0);
expect(reserveRequestCostUsd(-10)).toBe(0);
});
});
describe('surplusAlreadyInCap (experimental)', () => {
const before = { cumVlm: '0', nRequestsUsed: 100, nRequestsCap: 10_000, nRequestsSurplus: 0 };
it('detects a cap that grew by the chunk while surplus also grew', () => {
const after = { ...before, nRequestsCap: 12_000, nRequestsSurplus: 2_000 };
expect(surplusAlreadyInCap(before, after, 2_000)).toBe(true);
const nearly = { ...before, nRequestsCap: 11_850, nRequestsSurplus: 2_000 };
expect(surplusAlreadyInCap(before, nearly, 2_000)).toBe(true);
});
it('is false when only one of the fields moved or chunk is invalid', () => {
expect(surplusAlreadyInCap(before, { ...before, nRequestsSurplus: 2_000 }, 2_000)).toBe(false);
expect(surplusAlreadyInCap(before, { ...before, nRequestsCap: 12_000 }, 2_000)).toBe(false);
expect(surplusAlreadyInCap(before, { ...before, nRequestsCap: 12_000, nRequestsSurplus: 2_000 }, 0)).toBe(false);
});
});
describe('address budget edge cases', () => {
it('floors numeric cumVlm without float rounding up (10.9999999 must not become 10 011)', () => {
expect(baseAddressRequestCap(10.9999999)).toBe(10_010);
expect(baseAddressRequestCap(0.1 + 0.2)).toBe(10_000);
expect(baseAddressRequestCap(0.29 * 100)).toBe(10_028); // 28.999999999999996 -> 28
expect(baseAddressRequestCap(0)).toBe(10_000);
expect(() => baseAddressRequestCap(-0.5)).toThrow(TypeError);
expect(() => baseAddressRequestCap(Number.POSITIVE_INFINITY)).toThrow(TypeError);
});
it('clamps absurd volumes to a safe integer', () => {
expect(baseAddressRequestCap('123456789012345678901234.5')).toBe(Number.MAX_SAFE_INTEGER);
expect(baseAddressRequestCap(1e300)).toBe(Number.MAX_SAFE_INTEGER);
});
it('local placements beyond the remainder clamp to zero and freeze', () => {
const b = computeAddressBudget({ cumVlm: '0', nRequestsUsed: 9_990, nRequestsCap: 10_000 }, { placesSinceRead: 50 });
expect(b.remaining).toBe(0);
expect(b.exhausted).toBe(true);
expect(b.mode).toBe('freeze');
expect(computeAddressBudget({ cumVlm: '0', nRequestsUsed: 0, nRequestsCap: 10_000 }, { placesSinceRead: -5 }).remaining).toBe(10_000);
});
it('mode thresholds are strict: exactly 10% is economy, exactly 30% is normal', () => {
expect(computeAddressBudget({ cumVlm: '0', nRequestsUsed: 9_000, nRequestsCap: 10_000 }).mode).toBe('economy');
expect(computeAddressBudget({ cumVlm: '0', nRequestsUsed: 7_000, nRequestsCap: 10_000 }).mode).toBe('normal');
});
it('accepts a null surplus and rejects negative counters', () => {
expect(parseUserRateLimit({ cumVlm: '1.5', nRequestsUsed: 2, nRequestsCap: 10_001, nRequestsSurplus: null })).toEqual({
cumVlm: '1.5',
nRequestsUsed: 2,
nRequestsCap: 10_001,
});
expect(() => parseUserRateLimit({ cumVlm: '1', nRequestsUsed: -1, nRequestsCap: 10_001 })).toThrow(TypeError);
expect(() => parseUserRateLimit({ cumVlm: '-1', nRequestsUsed: 1, nRequestsCap: 10_001 })).toThrow(TypeError);
});
it('hoursToExhaustion treats negative remaining as zero and zero net as never', () => {
expect(hoursToExhaustion(-10, 100, 0)).toBe(0);
expect(hoursToExhaustion(1000, 100, 100)).toBeNull();
expect(hoursToExhaustion(Number.NaN, 100, 0)).toBeNull();
});
it('surplus check at the 0.9 x chunk boundary', () => {
const before = { cumVlm: '0', nRequestsUsed: 0, nRequestsCap: 10_000, nRequestsSurplus: 0 };
expect(surplusAlreadyInCap(before, { ...before, nRequestsCap: 11_800, nRequestsSurplus: 2_000 }, 2_000)).toBe(true);
expect(surplusAlreadyInCap(before, { ...before, nRequestsCap: 11_799, nRequestsSurplus: 2_000 }, 2_000)).toBe(false);
});
});