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:
commit
8947ec11ca
16 changed files with 384 additions and 322 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
20
kraiken-lib/src/ids.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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
164
kraiken-lib/src/snatch.ts
Normal 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);
|
||||
}
|
||||
31
kraiken-lib/src/staking.ts
Normal file
31
kraiken-lib/src/staking.ts
Normal 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;
|
||||
}
|
||||
39
kraiken-lib/src/taxRates.ts
Normal file
39
kraiken-lib/src/taxRates.ts
Normal 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 },
|
||||
];
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { describe, expect, test } from "@jest/globals";
|
||||
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from "../subgraph";
|
||||
import { Position, PositionStatus } from '../__generated__/graphql';
|
||||
|
||||
|
|
|
|||
15
kraiken-lib/src/tests/ids.test.ts
Normal file
15
kraiken-lib/src/tests/ids.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
32
kraiken-lib/src/tests/staking.test.ts
Normal file
32
kraiken-lib/src/tests/staking.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
11
kraiken-lib/src/tests/taxRates.test.ts
Normal file
11
kraiken-lib/src/tests/taxRates.test.ts
Normal 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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue