133 lines
4 KiB
TypeScript
133 lines
4 KiB
TypeScript
import {
|
|
TAX_RATE_OPTIONS,
|
|
calculateSnatchShortfall,
|
|
decodePositionId,
|
|
getSnatchList,
|
|
isPositionDelinquent,
|
|
minimumTaxRate,
|
|
selectSnatchPositions,
|
|
type SnatchablePosition,
|
|
} from "../helpers";
|
|
import { uint256ToBytesLittleEndian } from "../subgraph";
|
|
import type { Position } from "../__generated__/graphql";
|
|
import { PositionStatus } from "../__generated__/graphql";
|
|
|
|
describe("helpers", () => {
|
|
test("calculateSnatchShortfall returns zero when within cap", () => {
|
|
const outstanding = 100n;
|
|
const desired = 50n;
|
|
const total = 1000n;
|
|
|
|
const result = calculateSnatchShortfall(outstanding, desired, total, 2n, 10n);
|
|
expect(result).toBe(0n);
|
|
});
|
|
|
|
test("calculateSnatchShortfall returns positive remainder when exceeding cap", () => {
|
|
const outstanding = 200n;
|
|
const desired = 200n;
|
|
const total = 1000n;
|
|
|
|
const result = calculateSnatchShortfall(outstanding, desired, total, 2n, 10n);
|
|
expect(result).toBe(200n);
|
|
});
|
|
|
|
test("minimumTaxRate finds the lowest tax", () => {
|
|
const rates = [{ taxRate: 0.12 }, { taxRate: 0.05 }, { taxRate: 0.08 }];
|
|
expect(minimumTaxRate(rates, 1)).toBeCloseTo(0.05);
|
|
expect(minimumTaxRate([], 0.42)).toBe(0.42);
|
|
});
|
|
|
|
test("selectSnatchPositions chooses cheapest positions first", () => {
|
|
const candidates: SnatchablePosition[] = [
|
|
{ id: 1n, stakeShares: 30n, taxRate: 0.05, taxRateIndex: 5 },
|
|
{ id: 2n, stakeShares: 40n, taxRate: 0.03, taxRateIndex: 3 },
|
|
{ id: 3n, stakeShares: 50n, taxRate: 0.07, taxRateIndex: 7 },
|
|
];
|
|
|
|
const result = selectSnatchPositions(candidates, {
|
|
shortfallShares: 60n,
|
|
maxTaxRate: 0.08,
|
|
});
|
|
|
|
expect(result.selected.map((item) => item.id)).toEqual([2n, 1n]);
|
|
expect(result.remainingShortfall).toBe(0n);
|
|
expect(result.coveredShares).toBe(60n);
|
|
expect(result.maxSelectedTaxRateIndex).toBe(5);
|
|
});
|
|
|
|
test("selectSnatchPositions keeps track of remaining shortfall", () => {
|
|
const candidates: SnatchablePosition[] = [
|
|
{ id: 1n, stakeShares: 10n, taxRate: 0.01 },
|
|
{ id: 2n, stakeShares: 10n, taxRate: 0.02 },
|
|
];
|
|
|
|
const result = selectSnatchPositions(candidates, {
|
|
shortfallShares: 50n,
|
|
maxTaxRate: 0.05,
|
|
});
|
|
|
|
expect(result.remainingShortfall).toBe(30n);
|
|
});
|
|
|
|
test("isPositionDelinquent respects tax rate windows", () => {
|
|
const now = 1_000_000;
|
|
const taxRate = 0.5; // 50%
|
|
const windowSeconds = (365 * 24 * 60 * 60) / taxRate;
|
|
|
|
expect(
|
|
isPositionDelinquent(now - windowSeconds + 1, taxRate, now)
|
|
).toBe(false);
|
|
expect(
|
|
isPositionDelinquent(now - windowSeconds - 10, taxRate, now)
|
|
).toBe(true);
|
|
expect(isPositionDelinquent(now, 0, now)).toBe(false);
|
|
});
|
|
|
|
test("decodePositionId works across representations", () => {
|
|
const id = 12345n;
|
|
const hex = `0x${id.toString(16)}`;
|
|
const bytes = uint256ToBytesLittleEndian(id);
|
|
|
|
expect(decodePositionId(id)).toBe(id);
|
|
expect(decodePositionId(hex)).toBe(id);
|
|
expect(decodePositionId(bytes)).toBe(id);
|
|
});
|
|
|
|
test("getSnatchList converts subgraph positions", () => {
|
|
const stakeTotalSupply = 1_000_000n * 10n ** 18n;
|
|
|
|
const positions: Position[] = [
|
|
{
|
|
__typename: "Position",
|
|
id: uint256ToBytesLittleEndian(1n),
|
|
owner: "0xowner1",
|
|
share: 0.0001,
|
|
creationTime: 0,
|
|
lastTaxTime: 0,
|
|
taxRate: 0.02,
|
|
status: PositionStatus.Active,
|
|
},
|
|
{
|
|
__typename: "Position",
|
|
id: uint256ToBytesLittleEndian(2n),
|
|
owner: "0xowner2",
|
|
share: 0.0002,
|
|
creationTime: 0,
|
|
lastTaxTime: 0,
|
|
taxRate: 0.01,
|
|
status: PositionStatus.Active,
|
|
},
|
|
];
|
|
|
|
const result = getSnatchList(positions, 10n ** 18n, 0.05, stakeTotalSupply);
|
|
|
|
expect(result).toEqual([2n]);
|
|
});
|
|
|
|
test("tax rate options exported for consumers", () => {
|
|
expect(TAX_RATE_OPTIONS.length).toBeGreaterThan(0);
|
|
expect(TAX_RATE_OPTIONS[0]).toEqual(
|
|
expect.objectContaining({ index: 0, year: 1, decimal: 0.01 })
|
|
);
|
|
});
|
|
});
|