Merge pull request 'Refactor kraiken-lib helpers into focused modules with updated tests' (#32) from feature/split-kraiken-helpers into master

Reviewed-on: https://codeberg.org/johba/harb/pulls/32
This commit is contained in:
johba 2025-10-01 14:49:30 +02:00
commit 8947ec11ca
16 changed files with 384 additions and 322 deletions

View file

@ -28,5 +28,47 @@
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.4.3"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.js"
},
"./helpers": {
"types": "./dist/helpers.d.ts",
"require": "./dist/helpers.js",
"import": "./dist/helpers.js"
},
"./ids": {
"types": "./dist/ids.d.ts",
"require": "./dist/ids.js",
"import": "./dist/ids.js"
},
"./taxRates": {
"types": "./dist/taxRates.d.ts",
"require": "./dist/taxRates.js",
"import": "./dist/taxRates.js"
},
"./snatch": {
"types": "./dist/snatch.d.ts",
"require": "./dist/snatch.js",
"import": "./dist/snatch.js"
},
"./staking": {
"types": "./dist/staking.d.ts",
"require": "./dist/staking.js",
"import": "./dist/staking.js"
},
"./subgraph": {
"types": "./dist/subgraph.d.ts",
"require": "./dist/subgraph.js",
"import": "./dist/subgraph.js"
},
"./abis": {
"types": "./dist/abis.d.ts",
"require": "./dist/abis.js",
"import": "./dist/abis.js"
}
}
}

View file

@ -1,253 +1,4 @@
import { bytesToUint256LittleEndian } from "./subgraph";
import type { Position } from "./__generated__/graphql";
export interface SnatchablePosition {
id: bigint;
stakeShares: bigint;
taxRate: number;
taxRateIndex?: number;
owner?: string | null;
}
export interface SnatchSelectionOptions {
shortfallShares: bigint;
maxTaxRate: number;
includeOwned?: boolean;
recipientAddress?: string | null;
}
export interface SnatchSelectionResult {
selected: SnatchablePosition[];
coveredShares: bigint;
remainingShortfall: bigint;
maxSelectedTaxRate?: number;
maxSelectedTaxRateIndex?: number;
}
export interface TaxRateOption {
index: number;
year: number;
daily: number;
decimal: number;
}
export const TAX_RATE_OPTIONS: TaxRateOption[] = [
{ index: 0, year: 1, daily: 0.00274, decimal: 0.01 },
{ index: 1, year: 3, daily: 0.00822, decimal: 0.03 },
{ index: 2, year: 5, daily: 0.0137, decimal: 0.05 },
{ index: 3, year: 8, daily: 0.02192, decimal: 0.08 },
{ index: 4, year: 12, daily: 0.03288, decimal: 0.12 },
{ index: 5, year: 18, daily: 0.04932, decimal: 0.18 },
{ index: 6, year: 24, daily: 0.06575, decimal: 0.24 },
{ index: 7, year: 30, daily: 0.08219, decimal: 0.3 },
{ index: 8, year: 40, daily: 0.10959, decimal: 0.4 },
{ index: 9, year: 50, daily: 0.13699, decimal: 0.5 },
{ index: 10, year: 60, daily: 0.16438, decimal: 0.6 },
{ index: 11, year: 80, daily: 0.21918, decimal: 0.8 },
{ index: 12, year: 100, daily: 0.27397, decimal: 1.0 },
{ index: 13, year: 130, daily: 0.35616, decimal: 1.3 },
{ index: 14, year: 180, daily: 0.49315, decimal: 1.8 },
{ index: 15, year: 250, daily: 0.68493, decimal: 2.5 },
{ index: 16, year: 320, daily: 0.87671, decimal: 3.2 },
{ index: 17, year: 420, daily: 1.15068, decimal: 4.2 },
{ index: 18, year: 540, daily: 1.47945, decimal: 5.4 },
{ index: 19, year: 700, daily: 1.91781, decimal: 7.0 },
{ index: 20, year: 920, daily: 2.52055, decimal: 9.2 },
{ index: 21, year: 1200, daily: 3.28767, decimal: 12.0 },
{ index: 22, year: 1600, daily: 4.38356, decimal: 16.0 },
{ index: 23, year: 2000, daily: 5.47945, decimal: 20.0 },
{ index: 24, year: 2600, daily: 7.12329, decimal: 26.0 },
{ index: 25, year: 3400, daily: 9.31507, decimal: 34.0 },
{ index: 26, year: 4400, daily: 12.05479, decimal: 44.0 },
{ index: 27, year: 5700, daily: 15.61644, decimal: 57.0 },
{ index: 28, year: 7500, daily: 20.54795, decimal: 75.0 },
{ index: 29, year: 9700, daily: 26.57534, decimal: 97.0 },
];
const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
function normaliseAddress(value?: string | null): string {
return (value ?? "").toLowerCase();
}
export function calculateSnatchShortfall(
outstandingStake: bigint,
desiredStakeShares: bigint,
stakeTotalSupply: bigint,
capNumerator: bigint = 2n,
capDenominator: bigint = 10n
): bigint {
if (capDenominator === 0n) {
throw new Error("capDenominator must be greater than zero");
}
const cap = (stakeTotalSupply * capNumerator) / capDenominator;
const required = outstandingStake + desiredStakeShares;
const delta = required - cap;
return delta > 0n ? delta : 0n;
}
export function minimumTaxRate<T extends { taxRate: number }>(
positions: T[],
fallback: number = 0
): number {
if (!positions.length) return fallback;
return positions.reduce(
(min, position) => (position.taxRate < min ? position.taxRate : min),
Number.POSITIVE_INFINITY
);
}
export function selectSnatchPositions(
candidates: SnatchablePosition[],
options: SnatchSelectionOptions
): SnatchSelectionResult {
const {
shortfallShares,
maxTaxRate,
includeOwned = false,
recipientAddress = null,
} = options;
if (shortfallShares <= 0n) {
return {
selected: [],
coveredShares: 0n,
remainingShortfall: 0n,
};
}
const recipientNormalised = normaliseAddress(recipientAddress);
const filtered = candidates.filter((candidate) => {
if (candidate.taxRate >= maxTaxRate) {
return false;
}
if (!includeOwned && candidate.owner) {
return normaliseAddress(candidate.owner) !== recipientNormalised;
}
return true;
});
const sorted = filtered.slice().sort((a, b) => {
if (a.taxRate === b.taxRate) {
return Number(a.stakeShares - b.stakeShares);
}
return a.taxRate - b.taxRate;
});
const selection: SnatchablePosition[] = [];
let remaining = shortfallShares;
let covered = 0n;
let maxSelectedTaxRate: number | undefined;
let maxSelectedTaxRateIndex: number | undefined;
for (const candidate of sorted) {
if (remaining <= 0n) break;
if (candidate.stakeShares <= 0n) continue;
selection.push(candidate);
remaining -= candidate.stakeShares;
if (remaining < 0n) {
covered = shortfallShares;
remaining = 0n;
} else {
covered = shortfallShares - remaining;
}
if (
maxSelectedTaxRate === undefined ||
candidate.taxRate > maxSelectedTaxRate
) {
maxSelectedTaxRate = candidate.taxRate;
if (typeof candidate.taxRateIndex === "number") {
maxSelectedTaxRateIndex = candidate.taxRateIndex;
}
} else if (
candidate.taxRate === maxSelectedTaxRate &&
typeof candidate.taxRateIndex === "number" &&
candidate.taxRateIndex > (maxSelectedTaxRateIndex ?? -1)
) {
maxSelectedTaxRateIndex = candidate.taxRateIndex;
}
}
return {
selected: selection,
coveredShares: covered,
remainingShortfall: remaining > 0n ? remaining : 0n,
maxSelectedTaxRate,
maxSelectedTaxRateIndex,
};
}
export function toBigIntId(id: string | Uint8Array | number | bigint): bigint {
if (typeof id === "bigint") return id;
if (typeof id === "number") return BigInt(id);
if (typeof id === "string") {
const trimmed = id.startsWith("0x") ? id : `0x${id}`;
return BigInt(trimmed);
}
if (id instanceof Uint8Array) {
return bytesToUint256LittleEndian(id);
}
throw new Error("Unsupported position id type");
}
export function decodePositionId(id: string | Uint8Array | number | bigint): bigint {
return toBigIntId(id);
}
export function isPositionDelinquent(
lastTaxTimestamp: number,
taxRate: number,
referenceTimestamp: number = Math.floor(Date.now() / 1000),
secondsInYear: number = SECONDS_IN_YEAR
): boolean {
const rate = Number(taxRate);
if (rate <= 0) return false;
const allowance = secondsInYear / rate;
return referenceTimestamp - lastTaxTimestamp > allowance;
}
const SHARE_SCALE = 1_000_000n;
export function getSnatchList(
positions: Position[],
needed: bigint,
taxRate: number,
stakeTotalSupply: bigint
): bigint[] {
if (stakeTotalSupply <= 0n) {
throw new Error("stakeTotalSupply must be greater than zero");
}
const candidates: SnatchablePosition[] = positions.map((position) => {
const shareRatio = Number(position.share);
const scaledShares = BigInt(
Math.round(shareRatio * Number(SHARE_SCALE))
);
return {
id: toBigIntId(position.id),
owner: position.owner,
stakeShares: (stakeTotalSupply * scaledShares) / SHARE_SCALE,
taxRate: Number(position.taxRate),
};
});
const result = selectSnatchPositions(candidates, {
shortfallShares: needed,
maxTaxRate: taxRate,
includeOwned: true,
});
if (result.remainingShortfall > 0n) {
throw new Error("Not enough capacity");
}
return result.selected.map((candidate) => candidate.id);
}
export * from "./staking";
export * from "./snatch";
export * from "./ids";
export * from "./taxRates";

20
kraiken-lib/src/ids.ts Normal file
View file

@ -0,0 +1,20 @@
import { bytesToUint256LittleEndian } from "./subgraph";
export function toBigIntId(id: string | Uint8Array | number | bigint): bigint {
if (typeof id === "bigint") return id;
if (typeof id === "number") return BigInt(id);
if (typeof id === "string") {
const trimmed = id.startsWith("0x") ? id : `0x${id}`;
return BigInt(trimmed);
}
if (id instanceof Uint8Array) {
return bytesToUint256LittleEndian(id);
}
throw new Error("Unsupported position id type");
}
export function decodePositionId(
id: string | Uint8Array | number | bigint
): bigint {
return toBigIntId(id);
}

View file

@ -9,18 +9,22 @@ export {
uint256ToBytesLittleEndian as uint256ToBytes,
} from "./subgraph";
export { TAX_RATE_OPTIONS, type TaxRateOption } from "./taxRates";
export {
TAX_RATE_OPTIONS,
calculateSnatchShortfall,
isPositionDelinquent,
} from "./staking";
export {
minimumTaxRate,
selectSnatchPositions,
getSnatchList,
type SnatchablePosition,
type SnatchSelectionOptions,
type SnatchSelectionResult,
type TaxRateOption,
decodePositionId,
getSnatchList,
isPositionDelinquent,
} from "./helpers";
} from "./snatch";
export { decodePositionId } from "./ids";
export { KraikenAbi, StakeAbi, ABIS } from "./abis";

164
kraiken-lib/src/snatch.ts Normal file
View file

@ -0,0 +1,164 @@
import type { Position } from "./__generated__/graphql";
import { toBigIntId } from "./ids";
export interface SnatchablePosition {
id: bigint;
stakeShares: bigint;
taxRate: number;
taxRateIndex?: number;
owner?: string | null;
}
export interface SnatchSelectionOptions {
shortfallShares: bigint;
maxTaxRate: number;
includeOwned?: boolean;
recipientAddress?: string | null;
}
export interface SnatchSelectionResult {
selected: SnatchablePosition[];
coveredShares: bigint;
remainingShortfall: bigint;
maxSelectedTaxRate?: number;
maxSelectedTaxRateIndex?: number;
}
const SHARE_SCALE = 1_000_000n;
function normaliseAddress(value?: string | null): string {
return (value ?? "").toLowerCase();
}
export function minimumTaxRate<T extends { taxRate: number }>(
positions: T[],
fallback: number = 0
): number {
if (!positions.length) return fallback;
return positions.reduce(
(min, position) => (position.taxRate < min ? position.taxRate : min),
Number.POSITIVE_INFINITY
);
}
export function selectSnatchPositions(
candidates: SnatchablePosition[],
options: SnatchSelectionOptions
): SnatchSelectionResult {
const {
shortfallShares,
maxTaxRate,
includeOwned = false,
recipientAddress = null,
} = options;
if (shortfallShares <= 0n) {
return {
selected: [],
coveredShares: 0n,
remainingShortfall: 0n,
};
}
const recipientNormalised = normaliseAddress(recipientAddress);
const filtered = candidates.filter((candidate) => {
if (candidate.taxRate >= maxTaxRate) {
return false;
}
if (!includeOwned && candidate.owner) {
return normaliseAddress(candidate.owner) !== recipientNormalised;
}
return true;
});
const sorted = filtered.slice().sort((a, b) => {
if (a.taxRate === b.taxRate) {
return Number(a.stakeShares - b.stakeShares);
}
return a.taxRate - b.taxRate;
});
const selection: SnatchablePosition[] = [];
let remaining = shortfallShares;
let covered = 0n;
let maxSelectedTaxRate: number | undefined;
let maxSelectedTaxRateIndex: number | undefined;
for (const candidate of sorted) {
if (remaining <= 0n) break;
if (candidate.stakeShares <= 0n) continue;
selection.push(candidate);
remaining -= candidate.stakeShares;
if (remaining < 0n) {
covered = shortfallShares;
remaining = 0n;
} else {
covered = shortfallShares - remaining;
}
if (
maxSelectedTaxRate === undefined ||
candidate.taxRate > maxSelectedTaxRate
) {
maxSelectedTaxRate = candidate.taxRate;
if (typeof candidate.taxRateIndex === "number") {
maxSelectedTaxRateIndex = candidate.taxRateIndex;
}
} else if (
candidate.taxRate === maxSelectedTaxRate &&
typeof candidate.taxRateIndex === "number" &&
candidate.taxRateIndex > (maxSelectedTaxRateIndex ?? -1)
) {
maxSelectedTaxRateIndex = candidate.taxRateIndex;
}
}
return {
selected: selection,
coveredShares: covered,
remainingShortfall: remaining > 0n ? remaining : 0n,
maxSelectedTaxRate,
maxSelectedTaxRateIndex,
};
}
export function getSnatchList(
positions: Position[],
needed: bigint,
taxRate: number,
stakeTotalSupply: bigint
): bigint[] {
if (stakeTotalSupply <= 0n) {
throw new Error("stakeTotalSupply must be greater than zero");
}
const candidates: SnatchablePosition[] = positions.map((position) => {
const shareRatio = Number(position.share);
const scaledShares = BigInt(
Math.round(shareRatio * Number(SHARE_SCALE))
);
return {
id: toBigIntId(position.id),
owner: position.owner,
stakeShares: (stakeTotalSupply * scaledShares) / SHARE_SCALE,
taxRate: Number(position.taxRate),
};
});
const result = selectSnatchPositions(candidates, {
shortfallShares: needed,
maxTaxRate: taxRate,
includeOwned: true,
});
if (result.remainingShortfall > 0n) {
throw new Error("Not enough capacity");
}
return result.selected.map((candidate) => candidate.id);
}

View file

@ -0,0 +1,31 @@
const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
export function calculateSnatchShortfall(
outstandingStake: bigint,
desiredStakeShares: bigint,
stakeTotalSupply: bigint,
capNumerator: bigint = 2n,
capDenominator: bigint = 10n
): bigint {
if (capDenominator === 0n) {
throw new Error("capDenominator must be greater than zero");
}
const cap = (stakeTotalSupply * capNumerator) / capDenominator;
const required = outstandingStake + desiredStakeShares;
const delta = required - cap;
return delta > 0n ? delta : 0n;
}
export function isPositionDelinquent(
lastTaxTimestamp: number,
taxRate: number,
referenceTimestamp: number = Math.floor(Date.now() / 1000),
secondsInYear: number = SECONDS_IN_YEAR
): boolean {
const rate = Number(taxRate);
if (rate <= 0) return false;
const allowance = secondsInYear / rate;
return referenceTimestamp - lastTaxTimestamp > allowance;
}

View file

@ -0,0 +1,39 @@
export interface TaxRateOption {
index: number;
year: number;
daily: number;
decimal: number;
}
export const TAX_RATE_OPTIONS: TaxRateOption[] = [
{ index: 0, year: 1, daily: 0.00274, decimal: 0.01 },
{ index: 1, year: 3, daily: 0.00822, decimal: 0.03 },
{ index: 2, year: 5, daily: 0.0137, decimal: 0.05 },
{ index: 3, year: 8, daily: 0.02192, decimal: 0.08 },
{ index: 4, year: 12, daily: 0.03288, decimal: 0.12 },
{ index: 5, year: 18, daily: 0.04932, decimal: 0.18 },
{ index: 6, year: 24, daily: 0.06575, decimal: 0.24 },
{ index: 7, year: 30, daily: 0.08219, decimal: 0.3 },
{ index: 8, year: 40, daily: 0.10959, decimal: 0.4 },
{ index: 9, year: 50, daily: 0.13699, decimal: 0.5 },
{ index: 10, year: 60, daily: 0.16438, decimal: 0.6 },
{ index: 11, year: 80, daily: 0.21918, decimal: 0.8 },
{ index: 12, year: 100, daily: 0.27397, decimal: 1.0 },
{ index: 13, year: 130, daily: 0.35616, decimal: 1.3 },
{ index: 14, year: 180, daily: 0.49315, decimal: 1.8 },
{ index: 15, year: 250, daily: 0.68493, decimal: 2.5 },
{ index: 16, year: 320, daily: 0.87671, decimal: 3.2 },
{ index: 17, year: 420, daily: 1.15068, decimal: 4.2 },
{ index: 18, year: 540, daily: 1.47945, decimal: 5.4 },
{ index: 19, year: 700, daily: 1.91781, decimal: 7.0 },
{ index: 20, year: 920, daily: 2.52055, decimal: 9.2 },
{ index: 21, year: 1200, daily: 3.28767, decimal: 12.0 },
{ index: 22, year: 1600, daily: 4.38356, decimal: 16.0 },
{ index: 23, year: 2000, daily: 5.47945, decimal: 20.0 },
{ index: 24, year: 2600, daily: 7.12329, decimal: 26.0 },
{ index: 25, year: 3400, daily: 9.31507, decimal: 34.0 },
{ index: 26, year: 4400, daily: 12.05479, decimal: 44.0 },
{ index: 27, year: 5700, daily: 15.61644, decimal: 57.0 },
{ index: 28, year: 7500, daily: 20.54795, decimal: 75.0 },
{ index: 29, year: 9700, daily: 26.57534, decimal: 97.0 },
];

View file

@ -1,3 +1,4 @@
import { describe, expect, test } from "@jest/globals";
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from "../subgraph";
import { Position, PositionStatus } from '../__generated__/graphql';

View file

@ -0,0 +1,15 @@
import { describe, expect, test } from "@jest/globals";
import { decodePositionId } from "../ids";
import { uint256ToBytesLittleEndian } from "../subgraph";
describe("ids", () => {
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);
});
});

View file

@ -1,36 +1,15 @@
import { describe, expect, test } from "@jest/globals";
import {
TAX_RATE_OPTIONS,
calculateSnatchShortfall,
decodePositionId,
getSnatchList,
isPositionDelinquent,
minimumTaxRate,
selectSnatchPositions,
type SnatchablePosition,
} from "../helpers";
} from "../snatch";
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);
});
describe("snatch", () => {
test("minimumTaxRate finds the lowest tax", () => {
const rates = [{ taxRate: 0.12 }, { taxRate: 0.05 }, { taxRate: 0.08 }];
expect(minimumTaxRate(rates, 1)).toBeCloseTo(0.05);
@ -69,30 +48,6 @@ describe("helpers", () => {
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;
@ -123,11 +78,4 @@ describe("helpers", () => {
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 })
);
});
});

View file

@ -0,0 +1,32 @@
import { describe, expect, test } from "@jest/globals";
import { calculateSnatchShortfall, isPositionDelinquent } from "../staking";
describe("staking", () => {
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("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);
});
});

View file

@ -0,0 +1,11 @@
import { describe, expect, test } from "@jest/globals";
import { TAX_RATE_OPTIONS } from "../taxRates";
describe("taxRates", () => {
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 })
);
});
});

View file

@ -6,7 +6,8 @@ const dotenvPath = process.env.TXN_BOT_ENV_FILE
require('dotenv').config({ path: dotenvPath });
const { ethers } = require('ethers');
const express = require('express');
const { decodePositionId, isPositionDelinquent } = require('kraiken-lib');
const { decodePositionId } = require('kraiken-lib/ids');
const { isPositionDelinquent } = require('kraiken-lib/staking');
const ACTIVE_POSITIONS_QUERY = `
query ActivePositions {

View file

@ -28,10 +28,13 @@ vi.mock('../useAdjustTaxRates', () => ({
useAdjustTaxRate: vi.fn()
}))
vi.mock('kraiken-lib', () => ({
vi.mock('kraiken-lib/staking', () => ({
calculateSnatchShortfall: vi.fn((outstandingStake, stakingShares, stakeTotalSupply) => {
return stakingShares > outstandingStake ? 0n : outstandingStake - stakingShares
}),
})
}))
vi.mock('kraiken-lib/snatch', () => ({
selectSnatchPositions: vi.fn((candidates, options) => {
if (candidates.length === 0) {
return { selected: [], remainingShortfall: options.shortfallShares }
@ -271,4 +274,4 @@ describe('useSnatchSelection', () => {
// Position has taxRateIndex: 1, so nextIndex = 2, taxRates[2] = { year: 3 }
expect(floorTax.value).toBe(3)
})
})
})

View file

@ -4,7 +4,7 @@ import { waitForTransactionReceipt } from "@wagmi/core";
import { config } from "@/wagmi";
import { compactNumber, formatBigIntDivision } from "@/utils/helper";
import { useContractToast } from "./useContractToast";
import { TAX_RATE_OPTIONS } from "kraiken-lib";
import { TAX_RATE_OPTIONS } from "kraiken-lib/taxRates";
const contractToast = useContractToast();

View file

@ -4,12 +4,12 @@ import { useStake } from './useStake'
import { useWallet } from './useWallet'
import { useStatCollection } from './useStatCollection'
import { useAdjustTaxRate } from './useAdjustTaxRates'
import { calculateSnatchShortfall } from 'kraiken-lib/staking'
import {
calculateSnatchShortfall,
selectSnatchPositions,
minimumTaxRate,
type SnatchablePosition,
} from 'kraiken-lib'
} from 'kraiken-lib/snatch'
/**
* Converts Kraiken token assets to shares using the same formula as Stake.sol:
@ -177,4 +177,4 @@ export function useSnatchSelection(demo = false) {
floorTax,
openPositionsAvailable,
}
}
}