Merge pull request 'more logic to lib' (#5) from feature-lib into master
Reviewed-on: https://codeberg.org/johba/harb/pulls/5
This commit is contained in:
commit
5e6e21878c
25 changed files with 5838 additions and 3464 deletions
|
|
@ -10,24 +10,21 @@ yarn test
|
|||
## Import
|
||||
|
||||
```
|
||||
yarn add harb-lib
|
||||
yarn add kraiken-lib
|
||||
```
|
||||
|
||||
then
|
||||
|
||||
```
|
||||
import {bytesToUint256LittleEndian, uint256ToBytesLittleEndian} from 'harb-lib'
|
||||
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from "kraiken-lib";
|
||||
|
||||
uint256ToBytesLittleEndian(3n);
|
||||
```
|
||||
|
||||
## get Snatch List
|
||||
|
||||
|
||||
```
|
||||
import { getSnatchList } from "../helpers";
|
||||
|
||||
getSnatchList(<list of positions, <amount of stake>, <new tax rate>);
|
||||
import { getSnatchList } from "kraiken-lib";
|
||||
|
||||
const positionIds = getSnatchList(positions, neededShares, maxTaxRateDecimal, stakeTotalSupply);
|
||||
```
|
||||
|
||||
|
|
|
|||
132
kraiken-lib/package-lock.json
generated
132
kraiken-lib/package-lock.json
generated
|
|
@ -9,7 +9,6 @@
|
|||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.10",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-tag": "^2.12.6"
|
||||
},
|
||||
|
|
@ -3097,134 +3096,6 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz",
|
||||
"integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"apollo-utilities": "^1.3.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.9.3",
|
||||
"zen-observable-ts": "^0.8.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.11.3 || ^0.12.3 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link-http": {
|
||||
"version": "1.5.17",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz",
|
||||
"integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-http-common": "^0.2.16",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link-http-common": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz",
|
||||
"integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"apollo-link": "^1.2.14",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link-http-common/node_modules/ts-invariant": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||
"integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link-http-common/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/apollo-link-http/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/apollo-link/node_modules/ts-invariant": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||
"integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-link/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/apollo-link/node_modules/zen-observable-ts": {
|
||||
"version": "0.8.21",
|
||||
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz",
|
||||
"integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3",
|
||||
"zen-observable": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-utilities": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz",
|
||||
"integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@wry/equality": "^0.1.2",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"ts-invariant": "^0.4.0",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-utilities/node_modules/@wry/equality": {
|
||||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
|
||||
"integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-utilities/node_modules/ts-invariant": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz",
|
||||
"integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/apollo-utilities/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
|
|
@ -4450,6 +4321,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-querystring": {
|
||||
|
|
@ -4826,7 +4698,7 @@
|
|||
"version": "5.16.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz",
|
||||
"integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"website"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.10",
|
||||
"apollo-link-http": "^1.5.17",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-tag": "^2.12.6"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,36 +1,253 @@
|
|||
import { bytesToUint256LittleEndian } from "./subgraph";
|
||||
import { Position, PositionStatus } from './__generated__/graphql';
|
||||
import type { Position } from "./__generated__/graphql";
|
||||
|
||||
const STAKE_TOTOAL_SUPPLY: bigint = 1000000000000000000n * 10000000n;
|
||||
export interface SnatchablePosition {
|
||||
id: bigint;
|
||||
stakeShares: bigint;
|
||||
taxRate: number;
|
||||
taxRateIndex?: number;
|
||||
owner?: string | null;
|
||||
}
|
||||
|
||||
const sortAndFilterPositions = (positions) => {
|
||||
return positions
|
||||
.filter(position => position.status === PositionStatus.Active)
|
||||
.sort((a, b) => parseFloat(a.taxRate) - parseFloat(b.taxRate));
|
||||
};
|
||||
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[], // all active positions
|
||||
needed: bigint, // amount of stake requested by user
|
||||
taxRate: number
|
||||
): bigint[] {
|
||||
positions: Position[],
|
||||
needed: bigint,
|
||||
taxRate: number,
|
||||
stakeTotalSupply: bigint
|
||||
): bigint[] {
|
||||
if (stakeTotalSupply <= 0n) {
|
||||
throw new Error("stakeTotalSupply must be greater than zero");
|
||||
}
|
||||
|
||||
const sortedActivePositions = sortAndFilterPositions(positions);
|
||||
const candidates: SnatchablePosition[] = positions.map((position) => {
|
||||
const shareRatio = Number(position.share);
|
||||
const scaledShares = BigInt(
|
||||
Math.round(shareRatio * Number(SHARE_SCALE))
|
||||
);
|
||||
|
||||
const rv: bigint[] = [];
|
||||
let i = 0;
|
||||
while (needed > 0 && i < positions.length) {
|
||||
const available = (STAKE_TOTOAL_SUPPLY * BigInt(Math.round(sortedActivePositions[i].share * 10000000))) / BigInt(10000000);
|
||||
if (sortedActivePositions[i].taxRate < taxRate) {
|
||||
rv.push(bytesToUint256LittleEndian(sortedActivePositions[i].id));
|
||||
needed -= available;
|
||||
}
|
||||
i++;
|
||||
// code block to be executed
|
||||
}
|
||||
|
||||
if (needed > 0)
|
||||
throw new Error('Not enough capacity');
|
||||
return {
|
||||
id: toBigIntId(position.id),
|
||||
owner: position.owner,
|
||||
stakeShares: (stakeTotalSupply * scaledShares) / SHARE_SCALE,
|
||||
taxRate: Number(position.taxRate),
|
||||
};
|
||||
});
|
||||
|
||||
return rv;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,24 @@
|
|||
export {bytesToUint256LittleEndian, uint256ToBytesLittleEndian} from './subgraph'
|
||||
export {
|
||||
bytesToUint256LittleEndian,
|
||||
uint256ToBytesLittleEndian,
|
||||
} from "./subgraph";
|
||||
|
||||
export {getSnatchList} from './helpers'
|
||||
// Backward compatible aliases
|
||||
export {
|
||||
bytesToUint256LittleEndian as bytesToUint256,
|
||||
uint256ToBytesLittleEndian as uint256ToBytes,
|
||||
} from "./subgraph";
|
||||
|
||||
export {
|
||||
TAX_RATE_OPTIONS,
|
||||
calculateSnatchShortfall,
|
||||
minimumTaxRate,
|
||||
selectSnatchPositions,
|
||||
type SnatchablePosition,
|
||||
type SnatchSelectionOptions,
|
||||
type SnatchSelectionResult,
|
||||
type TaxRateOption,
|
||||
decodePositionId,
|
||||
getSnatchList,
|
||||
isPositionDelinquent,
|
||||
} from "./helpers";
|
||||
|
|
|
|||
|
|
@ -1,49 +1,133 @@
|
|||
import {
|
||||
TAX_RATE_OPTIONS,
|
||||
calculateSnatchShortfall,
|
||||
decodePositionId,
|
||||
getSnatchList,
|
||||
isPositionDelinquent,
|
||||
minimumTaxRate,
|
||||
selectSnatchPositions,
|
||||
type SnatchablePosition,
|
||||
} from "../helpers";
|
||||
import { uint256ToBytesLittleEndian } from "../subgraph";
|
||||
import { getSnatchList } from "../helpers";
|
||||
import { Position, PositionStatus } from '../__generated__/graphql';
|
||||
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 mockPos1: Position[] = [
|
||||
{
|
||||
__typename: 'Position',
|
||||
id: uint256ToBytesLittleEndian(3n),
|
||||
owner: '0x8db6b632d743aef641146dc943acb64957155388',
|
||||
share: 0.000001,
|
||||
creationTime: 1610000000,
|
||||
taxRate: 0.05,
|
||||
status: PositionStatus.Active, // Enum usage
|
||||
},
|
||||
{
|
||||
__typename: 'Position',
|
||||
id: uint256ToBytesLittleEndian(4n),
|
||||
owner: '0x8db6b632d743aef641146dc943acb64957155388',
|
||||
share: 0.000001,
|
||||
creationTime: 1610000000,
|
||||
taxRate: 0.01,
|
||||
status: PositionStatus.Active, // Enum usage
|
||||
},
|
||||
{
|
||||
__typename: 'Position',
|
||||
id: uint256ToBytesLittleEndian(5n),
|
||||
owner: '0x8db6b632d743aef641146dc943acb64957155388',
|
||||
share: 0.000001,
|
||||
creationTime: 1610000000,
|
||||
taxRate: 0.02,
|
||||
status: PositionStatus.Active, // Enum usage
|
||||
}
|
||||
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 },
|
||||
];
|
||||
|
||||
describe('BigInt Conversion Functions', () => {
|
||||
test('get a list of positions to snatch', async () => {
|
||||
// snatch 20 of 10000000 stake
|
||||
expect(getSnatchList(mockPos1, 20000000000000000000n, 0.03)).toEqual([4n, 5n]);
|
||||
// snatch 10 of 10000000 stake
|
||||
expect(getSnatchList(mockPos1, 10000000000000000000n, 0.02)).toEqual([4n]);
|
||||
// snatch 10 of 10000000 stake
|
||||
expect(getSnatchList(mockPos1, 30000000000000000000n, 0.06)).toEqual([4n, 5n, 3n]);
|
||||
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('test error', async () => {
|
||||
expect(() => {getSnatchList(mockPos1, 20000000000000000000n, 0.02)}).toThrow("Not enough capacity");
|
||||
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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz"
|
||||
integrity sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==
|
||||
|
||||
"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.14.0", "@babel/core@^7.22.9", "@babel/core@^7.23.9":
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.14.0", "@babel/core@^7.22.9", "@babel/core@^7.23.9", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8":
|
||||
version "7.24.3"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz"
|
||||
integrity sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==
|
||||
|
|
@ -1234,7 +1234,7 @@
|
|||
slash "^3.0.0"
|
||||
write-file-atomic "^4.0.2"
|
||||
|
||||
"@jest/types@^29.6.3":
|
||||
"@jest/types@^29.0.0", "@jest/types@^29.6.3":
|
||||
version "29.6.3"
|
||||
resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz"
|
||||
integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==
|
||||
|
|
@ -1432,7 +1432,7 @@
|
|||
resolved "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.36.tgz"
|
||||
integrity sha512-b7bq23s4fgBB76n34m2b3RBf6M369B0Z9uRR8aHTMd8kZISRkmDEpPD8hhpYvDFzr3bJCPES96cm3Q6qRNDbQw==
|
||||
|
||||
"@types/node@*":
|
||||
"@types/node@*", "@types/node@>=13":
|
||||
version "20.12.2"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz"
|
||||
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
|
||||
|
|
@ -1528,13 +1528,6 @@
|
|||
dependencies:
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@wry/equality@^0.1.2":
|
||||
version "0.1.11"
|
||||
resolved "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz"
|
||||
integrity sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@wry/equality@^0.5.6":
|
||||
version "0.5.7"
|
||||
resolved "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz"
|
||||
|
|
@ -1610,44 +1603,6 @@ anymatch@^3.0.3:
|
|||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
apollo-link-http-common@^0.2.16:
|
||||
version "0.2.16"
|
||||
resolved "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz"
|
||||
integrity sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==
|
||||
dependencies:
|
||||
apollo-link "^1.2.14"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-link-http@^1.5.17:
|
||||
version "1.5.17"
|
||||
resolved "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz"
|
||||
integrity sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==
|
||||
dependencies:
|
||||
apollo-link "^1.2.14"
|
||||
apollo-link-http-common "^0.2.16"
|
||||
tslib "^1.9.3"
|
||||
|
||||
apollo-link@^1.2.14:
|
||||
version "1.2.14"
|
||||
resolved "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz"
|
||||
integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==
|
||||
dependencies:
|
||||
apollo-utilities "^1.3.0"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.9.3"
|
||||
zen-observable-ts "^0.8.21"
|
||||
|
||||
apollo-utilities@^1.3.0:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz"
|
||||
integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==
|
||||
dependencies:
|
||||
"@wry/equality" "^0.1.2"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
ts-invariant "^0.4.0"
|
||||
tslib "^1.10.0"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz"
|
||||
|
|
@ -1689,7 +1644,7 @@ auto-bind@~4.0.0:
|
|||
resolved "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz"
|
||||
integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==
|
||||
|
||||
babel-jest@^29.7.0:
|
||||
babel-jest@^29.0.0, babel-jest@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz"
|
||||
integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==
|
||||
|
|
@ -1821,7 +1776,7 @@ braces@^3.0.2:
|
|||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
browserslist@^4.22.2:
|
||||
browserslist@^4.22.2, "browserslist@>= 4.21.0":
|
||||
version "4.23.0"
|
||||
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz"
|
||||
integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==
|
||||
|
|
@ -2354,7 +2309,7 @@ fast-glob@^3.2.9:
|
|||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x:
|
||||
fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
|
@ -2553,12 +2508,12 @@ graphql-tag@^2.11.0, graphql-tag@^2.12.6:
|
|||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
graphql-ws@^5.14.0:
|
||||
graphql-ws@^5.14.0, graphql-ws@^5.5.5:
|
||||
version "5.16.0"
|
||||
resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz"
|
||||
integrity sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==
|
||||
|
||||
graphql@^16.8.1:
|
||||
graphql@*, "graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql@^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "graphql@^15.0.0 || ^16.0.0", graphql@^16.8.1, "graphql@>=0.11 <=16", "graphql@14 - 16":
|
||||
version "16.8.1"
|
||||
resolved "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
|
@ -3099,7 +3054,7 @@ jest-resolve-dependencies@^29.7.0:
|
|||
jest-regex-util "^29.6.3"
|
||||
jest-snapshot "^29.7.0"
|
||||
|
||||
jest-resolve@^29.7.0:
|
||||
jest-resolve@*, jest-resolve@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz"
|
||||
integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==
|
||||
|
|
@ -3243,7 +3198,7 @@ jest-worker@^29.7.0:
|
|||
merge-stream "^2.0.0"
|
||||
supports-color "^8.0.0"
|
||||
|
||||
jest@^29.7.0:
|
||||
jest@^29.0.0, jest@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz"
|
||||
integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==
|
||||
|
|
@ -4258,13 +4213,6 @@ ts-invariant@^0.10.3:
|
|||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
ts-invariant@^0.4.0:
|
||||
version "0.4.4"
|
||||
resolved "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz"
|
||||
integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
|
||||
ts-jest@^29.1.2:
|
||||
version "29.1.2"
|
||||
resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz"
|
||||
|
|
@ -4284,11 +4232,6 @@ ts-log@^2.2.3:
|
|||
resolved "https://registry.npmjs.org/ts-log/-/ts-log-2.2.5.tgz"
|
||||
integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==
|
||||
|
||||
tslib@^1.10.0, tslib@^1.9.3:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.1, tslib@^2.6.2, tslib@~2.6.0:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
||||
|
|
@ -4304,7 +4247,7 @@ type-fest@^0.21.3:
|
|||
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz"
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
typescript@^5.4.3:
|
||||
typescript@^5.4.3, "typescript@>=4.3 <6", typescript@>=4.9.5:
|
||||
version "5.4.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz"
|
||||
integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==
|
||||
|
|
@ -4468,7 +4411,7 @@ write-file-atomic@^4.0.2:
|
|||
imurmurhash "^0.1.4"
|
||||
signal-exit "^3.0.7"
|
||||
|
||||
ws@^8.12.0, ws@^8.13.0, ws@^8.15.0:
|
||||
ws@*, ws@^8.12.0, ws@^8.13.0, ws@^8.15.0:
|
||||
version "8.16.0"
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz"
|
||||
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
|
||||
|
|
@ -4551,14 +4494,6 @@ yocto-queue@^0.1.0:
|
|||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zen-observable-ts@^0.8.21:
|
||||
version "0.8.21"
|
||||
resolved "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz"
|
||||
integrity sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==
|
||||
dependencies:
|
||||
tslib "^1.9.3"
|
||||
zen-observable "^0.8.0"
|
||||
|
||||
zen-observable-ts@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz"
|
||||
|
|
@ -4566,7 +4501,7 @@ zen-observable-ts@^1.2.5:
|
|||
dependencies:
|
||||
zen-observable "0.8.15"
|
||||
|
||||
zen-observable@^0.8.0, zen-observable@0.8.15:
|
||||
zen-observable@0.8.15:
|
||||
version "0.8.15"
|
||||
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
|
||||
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
||||
|
|
|
|||
640
landing/package-lock.json
generated
640
landing/package-lock.json
generated
File diff suppressed because it is too large
Load diff
8
ponder/.env.local
Normal file
8
ponder/.env.local
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Auto-generated by local_env.sh
|
||||
PONDER_NETWORK=local
|
||||
KRAIKEN_ADDRESS=0x56186c1e64ca8043def78d06aff222212ea5df71
|
||||
STAKE_ADDRESS=0x056e4a859558a3975761abd7385506bc4d8a8e60
|
||||
START_BLOCK=31435585
|
||||
# Use PostgreSQL connection
|
||||
DATABASE_URL=postgresql://ponder:ponder_local@localhost/ponder_local
|
||||
DATABASE_SCHEMA=ponder_local_31435585
|
||||
891
ponder/package-lock.json
generated
891
ponder/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,15 +10,20 @@
|
|||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.5.0",
|
||||
"ponder": "^0.13.1",
|
||||
"viem": "^2.21.0",
|
||||
"hono": "^4.5.0"
|
||||
"viem": "^2.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"esbuild": "^0.25.10",
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "^0.25.10",
|
||||
"vite": "^7.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
ponder/ponder-env.d.ts
vendored
11
ponder/ponder-env.d.ts
vendored
|
|
@ -9,6 +9,17 @@ declare module "ponder:schema" {
|
|||
export * from "./ponder.schema.ts";
|
||||
}
|
||||
|
||||
declare module "ponder:registry" {
|
||||
const ponder: any;
|
||||
export { ponder };
|
||||
}
|
||||
|
||||
declare module "ponder:api" {
|
||||
export const db: any;
|
||||
const api: any;
|
||||
export default api;
|
||||
}
|
||||
|
||||
// This file enables type checking and editor autocomplete for this Ponder project.
|
||||
// After upgrading, you may find that changes have been made to this file.
|
||||
// If this happens, please commit the changes. Do not manually edit this file.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createConfig } from "ponder";
|
||||
import type { Abi } from "viem";
|
||||
import KraikenAbi from "./abis/Kraiken.json";
|
||||
import StakeAbi from "./abis/Stake.json";
|
||||
|
||||
|
|
@ -9,9 +10,9 @@ const networks = {
|
|||
chainId: 31337,
|
||||
rpc: "http://127.0.0.1:8545",
|
||||
contracts: {
|
||||
kraiken: "0x56186c1E64cA8043dEF78d06AfF222212eA5df71", // DeployLocal 2025-09-23
|
||||
stake: "0x056E4a859558A3975761ABd7385506BC4D8A8E60", // DeployLocal 2025-09-23
|
||||
startBlock: 31425917, // DeployLocal broadcast block
|
||||
kraiken: process.env.KRAIKEN_ADDRESS || "0x56186c1E64cA8043dEF78d06AfF222212eA5df71",
|
||||
stake: process.env.STAKE_ADDRESS || "0x056E4a859558A3975761ABd7385506BC4D8A8E60",
|
||||
startBlock: parseInt(process.env.START_BLOCK || "31425917"),
|
||||
},
|
||||
},
|
||||
// Base Sepolia testnet
|
||||
|
|
@ -45,6 +46,11 @@ if (!selectedNetwork) {
|
|||
}
|
||||
|
||||
export default createConfig({
|
||||
// Use PostgreSQL if DATABASE_URL is set, otherwise use PGlite
|
||||
database: process.env.DATABASE_URL ? {
|
||||
kind: "postgres" as const,
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
} : undefined,
|
||||
chains: {
|
||||
[NETWORK]: {
|
||||
id: selectedNetwork.chainId,
|
||||
|
|
@ -53,13 +59,13 @@ export default createConfig({
|
|||
},
|
||||
contracts: {
|
||||
Kraiken: {
|
||||
abi: KraikenAbi.abi,
|
||||
abi: KraikenAbi.abi as Abi,
|
||||
chain: NETWORK,
|
||||
address: selectedNetwork.contracts.kraiken as `0x${string}`,
|
||||
startBlock: selectedNetwork.contracts.startBlock,
|
||||
},
|
||||
Stake: {
|
||||
abi: StakeAbi.abi,
|
||||
abi: StakeAbi.abi as Abi,
|
||||
chain: NETWORK,
|
||||
address: selectedNetwork.contracts.stake as `0x${string}`,
|
||||
startBlock: selectedNetwork.contracts.startBlock,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
|
||||
|
||||
ponder.on("Kraiken:Transfer", async ({ event, context }) => {
|
||||
ponder.on("Kraiken:Transfer", async ({ event, context }: any) => {
|
||||
const { from, to, value } = event.args;
|
||||
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
|
@ -44,7 +44,7 @@ ponder.on("Kraiken:Transfer", async ({ event, context }) => {
|
|||
await updateHourlyData(context, event.block.timestamp);
|
||||
});
|
||||
|
||||
ponder.on("StatsBlock:block", async ({ event, context }) => {
|
||||
ponder.on("StatsBlock:block", async ({ event, context }: any) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
await updateHourlyData(context, event.block.timestamp);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function toShareRatio(share: bigint, stakeTotalSupply: bigint): number {
|
|||
return Number(share) / Number(stakeTotalSupply);
|
||||
}
|
||||
|
||||
ponder.on("Stake:PositionCreated", async ({ event, context }) => {
|
||||
ponder.on("Stake:PositionCreated", async ({ event, context }: any) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const stakeTotalSupply = await getStakeTotalSupply(context);
|
||||
|
|
@ -53,7 +53,7 @@ ponder.on("Stake:PositionCreated", async ({ event, context }) => {
|
|||
await refreshOutstandingStake(context);
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionRemoved", async ({ event, context }) => {
|
||||
ponder.on("Stake:PositionRemoved", async ({ event, context }: any) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const positionId = event.args.positionId.toString();
|
||||
|
|
@ -75,7 +75,7 @@ ponder.on("Stake:PositionRemoved", async ({ event, context }) => {
|
|||
await updateHourlyData(context, event.block.timestamp);
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionShrunk", async ({ event, context }) => {
|
||||
ponder.on("Stake:PositionShrunk", async ({ event, context }: any) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const positionId = event.args.positionId.toString();
|
||||
|
|
@ -97,7 +97,7 @@ ponder.on("Stake:PositionShrunk", async ({ event, context }) => {
|
|||
await updateHourlyData(context, event.block.timestamp);
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionTaxPaid", async ({ event, context }) => {
|
||||
ponder.on("Stake:PositionTaxPaid", async ({ event, context }: any) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
await updateHourlyData(context, event.block.timestamp);
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ ponder.on("Stake:PositionTaxPaid", async ({ event, context }) => {
|
|||
await updateHourlyData(context, event.block.timestamp);
|
||||
});
|
||||
|
||||
ponder.on("Stake:PositionRateHiked", async ({ event, context }) => {
|
||||
ponder.on("Stake:PositionRateHiked", async ({ event, context }: any) => {
|
||||
const positionId = event.args.positionId.toString();
|
||||
await context.db.update(positions, { id: positionId }).set({
|
||||
taxRate: TAX_RATES[Number(event.args.newTaxRate)] || 0,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@
|
|||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
"types": ["node"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"ponder/virtual": ["./node_modules/ponder/src/types.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "ponder.config.ts", "ponder.schema.ts"],
|
||||
"include": ["src/**/*", "ponder.config.ts", "ponder.schema.ts", "ponder-env.d.ts"],
|
||||
"exclude": ["node_modules", ".ponder"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ LOG_DIR="$STATE_DIR/logs"
|
|||
|
||||
ANVIL_PID_FILE="$STATE_DIR/anvil.pid"
|
||||
PONDER_PID_FILE="$STATE_DIR/ponder.pid"
|
||||
LANDING_PID_FILE="$STATE_DIR/landing.pid"
|
||||
WEBAPP_PID_FILE="$STATE_DIR/webapp.pid"
|
||||
|
||||
ANVIL_LOG="$LOG_DIR/anvil.log"
|
||||
PONDER_LOG="$LOG_DIR/ponder.log"
|
||||
LANDING_LOG="$LOG_DIR/landing.log"
|
||||
WEBAPP_LOG="$LOG_DIR/webapp.log"
|
||||
SETUP_LOG="$LOG_DIR/setup.log"
|
||||
|
||||
FORK_URL=${FORK_URL:-"https://sepolia.base.org"}
|
||||
|
|
@ -37,7 +37,7 @@ SKIP_CLEANUP=false
|
|||
COMMAND="start"
|
||||
|
||||
cleanup() {
|
||||
stop_process "Frontend" "$LANDING_PID_FILE"
|
||||
stop_process "Frontend" "$WEBAPP_PID_FILE"
|
||||
stop_process "Ponder" "$PONDER_PID_FILE"
|
||||
stop_process "Anvil" "$ANVIL_PID_FILE"
|
||||
|
||||
|
|
@ -109,6 +109,22 @@ start_directories() {
|
|||
mkdir -p "$LOG_DIR"
|
||||
}
|
||||
|
||||
ensure_dependencies() {
|
||||
if [[ ! -d "$ROOT_DIR/ponder/node_modules" ]]; then
|
||||
log "Installing ponder dependencies"
|
||||
pushd "$ROOT_DIR/ponder" >/dev/null
|
||||
npm install >>"$SETUP_LOG" 2>&1
|
||||
popd >/dev/null
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ROOT_DIR/web-app/node_modules" ]]; then
|
||||
log "Installing web-app dependencies"
|
||||
pushd "$ROOT_DIR/web-app" >/dev/null
|
||||
npm install >>"$SETUP_LOG" 2>&1
|
||||
popd >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
start_anvil() {
|
||||
if [[ -f "$ANVIL_PID_FILE" ]]; then
|
||||
log "Anvil already running (pid $(cat "$ANVIL_PID_FILE"))"
|
||||
|
|
@ -133,15 +149,17 @@ deploy_protocol() {
|
|||
}
|
||||
|
||||
extract_addresses() {
|
||||
local run_file="$ROOT_DIR/onchain/broadcast/DeployLocal.sol/84532/run-latest.json"
|
||||
if [[ ! -f "$run_file" ]]; then
|
||||
log "Deployment artifact not found: $run_file"
|
||||
local run_file
|
||||
run_file="$(ls -t "$ROOT_DIR/onchain/broadcast/DeployLocal.sol"/*/run-latest.json 2>/dev/null | head -n1)"
|
||||
if [[ -z "$run_file" || ! -f "$run_file" ]]; then
|
||||
log "Deployment artifact not found under onchain/broadcast/DeployLocal.sol"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LIQUIDITY_MANAGER="$(jq -r '.transactions[] | select(.contractName=="LiquidityManager") | .contractAddress' "$run_file" | head -n1)"
|
||||
KRAIKEN="$(jq -r '.transactions[] | select(.contractName=="Kraiken") | .contractAddress' "$run_file" | head -n1)"
|
||||
STAKE="$(jq -r '.transactions[] | select(.contractName=="Stake") | .contractAddress' "$run_file" | head -n1)"
|
||||
log "Using deployment artifact: ${run_file#$ROOT_DIR/}"
|
||||
|
||||
if [[ -z "$LIQUIDITY_MANAGER" || "$LIQUIDITY_MANAGER" == "null" ]]; then
|
||||
log "Failed to extract LiquidityManager address"
|
||||
|
|
@ -151,6 +169,23 @@ extract_addresses() {
|
|||
echo "LIQUIDITY_MANAGER=$LIQUIDITY_MANAGER" >"$STATE_DIR/contracts.env"
|
||||
echo "KRAIKEN=$KRAIKEN" >>"$STATE_DIR/contracts.env"
|
||||
echo "STAKE=$STAKE" >>"$STATE_DIR/contracts.env"
|
||||
|
||||
# Get deployment block number
|
||||
local deploy_block
|
||||
deploy_block="$(jq -r '.receipts[0].blockNumber' "$run_file" | xargs printf "%d")"
|
||||
|
||||
# Create .env.local for Ponder with deployed addresses
|
||||
cat > "$ROOT_DIR/ponder/.env.local" <<EOF
|
||||
# Auto-generated by local_env.sh
|
||||
PONDER_NETWORK=local
|
||||
KRAIKEN_ADDRESS=$KRAIKEN
|
||||
STAKE_ADDRESS=$STAKE
|
||||
START_BLOCK=$deploy_block
|
||||
# Use PostgreSQL connection
|
||||
DATABASE_URL=postgresql://ponder:ponder_local@localhost/ponder_local
|
||||
DATABASE_SCHEMA=ponder_local_${deploy_block}
|
||||
EOF
|
||||
log "Created Ponder .env.local with deployed addresses and block $deploy_block"
|
||||
}
|
||||
|
||||
source_addresses() {
|
||||
|
|
@ -212,16 +247,16 @@ start_ponder() {
|
|||
}
|
||||
|
||||
start_frontend() {
|
||||
if [[ -f "$LANDING_PID_FILE" ]]; then
|
||||
log "Frontend already running (pid $(cat "$LANDING_PID_FILE"))"
|
||||
if [[ -f "$WEBAPP_PID_FILE" ]]; then
|
||||
log "Frontend already running (pid $(cat "$WEBAPP_PID_FILE"))"
|
||||
return
|
||||
fi
|
||||
|
||||
log "Starting frontend (Vite dev server)"
|
||||
pushd "$ROOT_DIR/landing" >/dev/null
|
||||
npm run dev -- --host 0.0.0.0 --port 5173 >"$LANDING_LOG" 2>&1 &
|
||||
pushd "$ROOT_DIR/web-app" >/dev/null
|
||||
npm run dev -- --host 0.0.0.0 --port 5173 >"$WEBAPP_LOG" 2>&1 &
|
||||
popd >/dev/null
|
||||
echo $! >"$LANDING_PID_FILE"
|
||||
echo $! >"$WEBAPP_PID_FILE"
|
||||
|
||||
wait_for_http "$FRONTEND_URL"
|
||||
}
|
||||
|
|
@ -229,12 +264,13 @@ start_frontend() {
|
|||
start_environment() {
|
||||
ensure_tools
|
||||
start_directories
|
||||
ensure_dependencies
|
||||
start_anvil
|
||||
deploy_protocol
|
||||
extract_addresses
|
||||
bootstrap_liquidity_manager
|
||||
prepare_application_state
|
||||
start_ponder
|
||||
start_ponder # Re-enabled with PostgreSQL
|
||||
start_frontend
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +286,7 @@ start_and_wait() {
|
|||
log " Frontend: $FRONTEND_URL"
|
||||
log "Press Ctrl+C to shut everything down"
|
||||
|
||||
wait "$(cat "$ANVIL_PID_FILE")" "$(cat "$PONDER_PID_FILE")" "$(cat "$LANDING_PID_FILE")"
|
||||
wait "$(cat "$ANVIL_PID_FILE")" "$(cat "$PONDER_PID_FILE")" "$(cat "$WEBAPP_PID_FILE")"
|
||||
}
|
||||
|
||||
stop_environment() {
|
||||
|
|
@ -306,7 +342,7 @@ status_environment() {
|
|||
|
||||
service_status "Anvil" "$ANVIL_PID_FILE" "$ANVIL_LOG"
|
||||
service_status "Ponder" "$PONDER_PID_FILE" "$PONDER_LOG"
|
||||
service_status "Frontend" "$LANDING_PID_FILE" "$LANDING_LOG"
|
||||
service_status "Frontend" "$WEBAPP_PID_FILE" "$WEBAPP_LOG"
|
||||
|
||||
if [[ -f "$STATE_DIR/contracts.env" ]]; then
|
||||
printf '\nContracts:\n'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ require('dotenv').config();
|
|||
const { ethers } = require('ethers');
|
||||
const express = require('express');
|
||||
const { execute } = require('./.graphclient');
|
||||
const { bytesToUint256 } = require('kraiken-lib');
|
||||
const { decodePositionId, isPositionDelinquent } = require('kraiken-lib');
|
||||
|
||||
const myQuery = `
|
||||
query GetPositions {
|
||||
|
|
@ -56,17 +56,10 @@ async function canCallFunction() {
|
|||
}
|
||||
|
||||
async function checkPosition(position) {
|
||||
let taxRate = parseFloat(position.taxRate);
|
||||
let lastTaxTime = position.lastTaxTime;
|
||||
let passed = (Date.now() / 1000) - lastTaxTime;
|
||||
if (passed > 365 * 24 * 60 * 60 / taxRate ) {
|
||||
const hexString = position.id;
|
||||
const cleanHexString = hexString.startsWith('0x') ? hexString.substring(2) : hexString;
|
||||
const bytes = new Uint8Array(Math.ceil(cleanHexString.length / 2));
|
||||
for (let i = 0, j = 0; i < cleanHexString.length; i += 2, j++) {
|
||||
bytes[j] = parseInt(cleanHexString.slice(i, i + 2), 16);
|
||||
}
|
||||
let positionId = bytesToUint256(bytes);
|
||||
const taxRate = Number(position.taxRate);
|
||||
const lastTaxTime = Number(position.lastTaxTime);
|
||||
if (isPositionDelinquent(lastTaxTime, taxRate)) {
|
||||
const positionId = decodePositionId(position.id);
|
||||
console.log(`Calling payTax on ${positionId}`);
|
||||
const tx = await stakeContract.payTax(positionId);
|
||||
await tx.wait();
|
||||
|
|
@ -163,4 +156,3 @@ app.get('/status', async (req, res) => {
|
|||
app.listen(PORT, () => {
|
||||
console.log(`HTTP server running on port ${PORT}`);
|
||||
});
|
||||
|
||||
|
|
|
|||
6655
web-app/package-lock.json
generated
6655
web-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,12 +14,12 @@
|
|||
"dependencies": {
|
||||
"@tanstack/vue-query": "^5.64.2",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@wagmi/vue": "^0.1.11",
|
||||
"@wagmi/vue": "^0.2.8",
|
||||
"axios": "^1.7.9",
|
||||
"chart.js": "^4.4.7",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"element-plus": "^2.9.3",
|
||||
"harb-lib": "^0.2.0",
|
||||
"kraiken-lib": "file:../kraiken-lib",
|
||||
"sass": "^1.83.4",
|
||||
"viem": "^2.22.13",
|
||||
"vitest": "^3.0.4",
|
||||
|
|
|
|||
|
|
@ -140,9 +140,13 @@ import { formatBigIntDivision, InsertCommaNumber, formatBigNumber, bigInt2Number
|
|||
// import { bytesToUint256, uint256ToBytes } from "harb-lib";
|
||||
// import { getSnatchList } from "harb-lib/dist/";
|
||||
import { formatUnits } from "viem";
|
||||
import axios from "axios";
|
||||
import { useAccount } from "@wagmi/vue";
|
||||
import { loadActivePositions, usePositions, type Position } from "@/composables/usePositions";
|
||||
import {
|
||||
calculateSnatchShortfall,
|
||||
selectSnatchPositions,
|
||||
minimumTaxRate,
|
||||
type SnatchablePosition,
|
||||
} from "kraiken-lib";
|
||||
|
||||
import { useStake } from "@/composables/useStake";
|
||||
import { useClaim } from "@/composables/useClaim";
|
||||
|
|
@ -178,6 +182,7 @@ const floorTax = ref(1);
|
|||
const minStake = ref(0n);
|
||||
const stakeSlots = ref();
|
||||
const supplyFreeze = ref<number>(0);
|
||||
const shortfallShares = ref<bigint>(0n);
|
||||
let debounceTimer: ReturnType<typeof setTimeout>;
|
||||
watchEffect(() => {
|
||||
console.log("supplyFreeze");
|
||||
|
|
@ -199,16 +204,7 @@ watchEffect(() => {
|
|||
}, 500); // Verzögerung von 500ms
|
||||
});
|
||||
|
||||
const openPositionsAvailable = computed(() => {
|
||||
if (
|
||||
bigInt2Number(statCollection.outstandingStake, 18) + bigInt2Number(stake.stakingAmountShares, 18) <=
|
||||
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const openPositionsAvailable = computed(() => shortfallShares.value <= 0n);
|
||||
|
||||
watchEffect(() => {
|
||||
console.log("stakeSlots");
|
||||
|
|
@ -228,10 +224,8 @@ const tokenIssuance = computed(() => {
|
|||
});
|
||||
|
||||
function getMinFloorTax() {
|
||||
const taxRate = activePositions.value.reduce((min, item) => {
|
||||
return item.taxRate < min ? item.taxRate : min;
|
||||
}, 1);
|
||||
return taxRate * 100;
|
||||
const minRate = minimumTaxRate(activePositions.value, 0);
|
||||
return Math.round(minRate * 100);
|
||||
}
|
||||
|
||||
async function stakeSnatch() {
|
||||
|
|
@ -333,114 +327,124 @@ function setMaxAmount() {
|
|||
|
||||
stake.stakingAmountNumber = maxStakeAmount.value;
|
||||
}
|
||||
const snatchAblePositions = ref<Array<any>>([]);
|
||||
const snatchAblePositions = ref<Position[]>([]);
|
||||
let selectionRun = 0;
|
||||
|
||||
watchEffect(async () => {
|
||||
console.log("watchEffect - snatchAblePositions");
|
||||
|
||||
let outStandingStake = statCollection.outstandingStake;
|
||||
let positions = activePositions.value;
|
||||
let selectedTaxRate = taxRate.value;
|
||||
|
||||
// Prüfe, ob bereits offene Positionen verfügbar sind, dann kein Snatching erforderlich
|
||||
if (openPositionsAvailable.value) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("bigInt2Number(outStandingStake, 18)", bigInt2Number(outStandingStake, 18));
|
||||
console.log("bigInt2Number(stake.stakingAmountShares, 18)", bigInt2Number(stake.stakingAmountShares, 18));
|
||||
console.log(
|
||||
"(bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2)",
|
||||
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2
|
||||
);
|
||||
|
||||
// **1. Berechnung der Differenz**
|
||||
const difference =
|
||||
bigInt2Number(outStandingStake, 18) +
|
||||
bigInt2Number(stake.stakingAmountShares, 18) -
|
||||
bigInt2Number(statCollection.stakeTotalSupply, 18) * 0.2;
|
||||
|
||||
console.log("difference", difference);
|
||||
|
||||
// **2. Potentiell snatchbare Positionen holen**
|
||||
const snatchAble = positions.filter((obj: Position) => {
|
||||
if (demo) {
|
||||
return obj.taxRatePercentage < selectedTaxRate;
|
||||
}
|
||||
return obj.taxRatePercentage < selectedTaxRate && !obj.iAmOwner;
|
||||
// return obj.taxRatePercentage < selectedTaxRate;
|
||||
watchEffect((onCleanup) => {
|
||||
const runId = ++selectionRun;
|
||||
let cancelled = false;
|
||||
onCleanup(() => {
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
console.log("snatchAblePositions", snatchAble);
|
||||
const compute = async () => {
|
||||
if (statCollection.stakeTotalSupply === 0n) {
|
||||
shortfallShares.value = 0n;
|
||||
if (!cancelled && runId === selectionRun) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (snatchAble.length === 0) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
return;
|
||||
}
|
||||
const stakingShares = stake.stakingAmountShares ?? 0n;
|
||||
const shortfall = calculateSnatchShortfall(
|
||||
statCollection.outstandingStake,
|
||||
stakingShares,
|
||||
statCollection.stakeTotalSupply,
|
||||
2n,
|
||||
10n
|
||||
);
|
||||
|
||||
// 3. Positionen nach taxRate sortieren (aufsteigend)
|
||||
const sortedPositions = [...snatchAble].sort((a, b) => a.taxRatePercentage - b.taxRatePercentage);
|
||||
shortfallShares.value = shortfall;
|
||||
|
||||
// 4. Die 50 % der niedrigsten Positionen auswählen
|
||||
const halfIndex = Math.floor(sortedPositions.length / 2);
|
||||
let candidatePositions = sortedPositions.slice(0, halfIndex);
|
||||
if (shortfall <= 0n) {
|
||||
if (!cancelled && runId === selectionRun) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Falls zu wenig vorhanden, nimm gleich alle
|
||||
if (candidatePositions.length === 0) {
|
||||
candidatePositions = sortedPositions;
|
||||
}
|
||||
const maxTaxRateDecimal = (taxRate.value ?? 0) / 100;
|
||||
const includeOwned = demo;
|
||||
const recipient = wallet.account.address ?? null;
|
||||
|
||||
// 5. Zufällige Reihenfolge nur innerhalb der günstigeren Hälfte
|
||||
const shuffled = [...candidatePositions].sort(() => Math.random() - 0.5);
|
||||
const eligiblePositions = activePositions.value.filter((position: Position) => {
|
||||
if (position.taxRate >= maxTaxRateDecimal) {
|
||||
return false;
|
||||
}
|
||||
if (!includeOwned && position.iAmOwner) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
let selectedPositions: Position[] = [];
|
||||
let accumulatedStake = 0;
|
||||
if (eligiblePositions.length === 0) {
|
||||
if (!cancelled && runId === selectionRun) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const minStakeShares = await assetsToShares(minStake.value);
|
||||
const minStakeShareNumber = bigInt2Number(minStakeShares, 18);
|
||||
const candidates: SnatchablePosition[] = [];
|
||||
|
||||
// Erstversuch: mit der günstigen Hälfte
|
||||
for (const position of shuffled) {
|
||||
const harbDepositShares = await assetsToShares(position.harbDeposit);
|
||||
const harbDepositNumber = bigInt2Number(harbDepositShares, 18);
|
||||
for (const position of eligiblePositions) {
|
||||
try {
|
||||
const shares = await assetsToShares(position.harbDeposit);
|
||||
candidates.push({
|
||||
id: position.positionId,
|
||||
owner: position.owner,
|
||||
stakeShares: shares,
|
||||
taxRate: position.taxRate,
|
||||
taxRateIndex: position.taxRateIndex,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Unable to compute stake shares for position", position.id, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (accumulatedStake <= difference) {
|
||||
selectedPositions.push(position);
|
||||
accumulatedStake += harbDepositNumber;
|
||||
}
|
||||
const selection = selectSnatchPositions(candidates, {
|
||||
shortfallShares: shortfall,
|
||||
maxTaxRate: maxTaxRateDecimal,
|
||||
includeOwned,
|
||||
recipientAddress: recipient,
|
||||
});
|
||||
|
||||
if (accumulatedStake >= difference) break;
|
||||
}
|
||||
if (cancelled || runId !== selectionRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: restliche Positionen – sortiert von günstig nach teuer
|
||||
if (accumulatedStake < difference && candidatePositions.length < sortedPositions.length) {
|
||||
const remainingPositions = sortedPositions.slice(halfIndex); // bereits sortiert!
|
||||
if (selection.remainingShortfall > 0n) {
|
||||
snatchAblePositions.value = [];
|
||||
floorTax.value = getMinFloorTax();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const position of remainingPositions) {
|
||||
const harbDepositShares = await assetsToShares(position.harbDeposit);
|
||||
const harbDepositNumber = bigInt2Number(harbDepositShares, 18);
|
||||
const positionById = new Map<bigint, Position>();
|
||||
for (const position of activePositions.value) {
|
||||
positionById.set(position.positionId, position);
|
||||
}
|
||||
|
||||
if (!selectedPositions.includes(position)) {
|
||||
selectedPositions.push(position);
|
||||
accumulatedStake += harbDepositNumber;
|
||||
}
|
||||
const selectedPositions = selection.selected
|
||||
.map((candidate) => positionById.get(candidate.id))
|
||||
.filter((value): value is Position => Boolean(value));
|
||||
|
||||
if (accumulatedStake >= difference) break;
|
||||
}
|
||||
}
|
||||
console.log("selectedPositions", selectedPositions);
|
||||
snatchAblePositions.value = selectedPositions;
|
||||
snatchAblePositions.value = selectedPositions;
|
||||
|
||||
// **Berechnung von `floorTax.value`**
|
||||
if (selectedPositions.length > 0) {
|
||||
const taxRatePosition = Math.max(...selectedPositions.map((p) => p.taxRateIndex)) +1;
|
||||
floorTax.value = adjustTaxRate.taxRates[taxRatePosition].year;
|
||||
} else {
|
||||
floorTax.value = getMinFloorTax();
|
||||
}
|
||||
if (selection.maxSelectedTaxRateIndex !== undefined) {
|
||||
const nextIndex = selection.maxSelectedTaxRateIndex + 1;
|
||||
const option =
|
||||
adjustTaxRate.taxRates[nextIndex] ??
|
||||
adjustTaxRate.taxRates[selection.maxSelectedTaxRateIndex];
|
||||
floorTax.value = option ? option.year : getMinFloorTax();
|
||||
} else {
|
||||
floorTax.value = getMinFloorTax();
|
||||
}
|
||||
};
|
||||
|
||||
compute();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +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";
|
||||
|
||||
const contractToast = useContractToast();
|
||||
|
||||
|
|
@ -13,38 +14,11 @@ enum State {
|
|||
Action = "Action",
|
||||
}
|
||||
|
||||
export const taxRates = [
|
||||
{ index: 0,year: 1, daily: 0.00274 },
|
||||
{ index: 1, year: 3, daily: 0.00822 },
|
||||
{ index: 2, year: 5, daily: 0.0137 },
|
||||
{ index: 3, year: 8, daily: 0.02192 },
|
||||
{ index: 4, year: 12, daily: 0.03288 },
|
||||
{ index: 5, year: 18, daily: 0.04932 },
|
||||
{ index: 6, year: 24, daily: 0.06575 },
|
||||
{ index: 7, year: 30, daily: 0.08219 },
|
||||
{ index: 8, year: 40, daily: 0.10959 },
|
||||
{ index: 9, year: 50, daily: 0.13699 },
|
||||
{ index: 10, year: 60, daily: 0.16438 },
|
||||
{ index: 11, year: 80, daily: 0.21918 },
|
||||
{ index: 12, year: 100, daily: 0.27397 },
|
||||
{ index: 13, year: 130, daily: 0.35616 },
|
||||
{ index: 14, year: 180, daily: 0.49315 },
|
||||
{ index: 15, year: 250, daily: 0.68493 },
|
||||
{ index: 16, year: 320, daily: 0.87671 },
|
||||
{ index: 17, year: 420, daily: 1.15068 },
|
||||
{ index: 18, year: 540, daily: 1.47945 },
|
||||
{ index: 19, year: 700, daily: 1.91781 },
|
||||
{ index: 20, year: 920, daily: 2.52055 },
|
||||
{ index: 21, year: 1200, daily: 3.28767 },
|
||||
{ index: 22, year: 1600, daily: 4.38356 },
|
||||
{ index: 23, year: 2000, daily: 5.47945 },
|
||||
{ index: 24, year: 2600, daily: 7.12329 },
|
||||
{ index: 25, year: 3400, daily: 9.31507 },
|
||||
{ index: 26, year: 4400, daily: 12.05479 },
|
||||
{ index: 27, year: 5700, daily: 15.61644 },
|
||||
{ index: 28, year: 7500, daily: 20.54795 },
|
||||
{ index: 29, year: 9700, daily: 26.57534 },
|
||||
];
|
||||
export const taxRates = TAX_RATE_OPTIONS.map(({ index, year, daily }) => ({
|
||||
index,
|
||||
year,
|
||||
daily,
|
||||
}));
|
||||
|
||||
export function useAdjustTaxRate() {
|
||||
const loading = ref();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { getAccount, watchContractEvent, watchChainId, watchAccount } from "@wag
|
|||
import type { WatchChainIdReturnType, WatchAccountReturnType, GetAccountReturnType} from "@wagmi/core";
|
||||
|
||||
import { HarbContract } from "@/contracts/harb";
|
||||
import { bytesToUint256, uint256ToBytes } from "harb-lib";
|
||||
import { bytesToUint256, uint256ToBytes } from "kraiken-lib";
|
||||
import { bigInt2Number } from "@/utils/helper";
|
||||
import { useChain } from "@/composables/useChain";
|
||||
import { taxRates } from "@/composables/useAdjustTaxRates";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"kraiken-lib": ["../kraiken-lib/src"],
|
||||
"kraiken-lib/*": ["../kraiken-lib/src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export default defineConfig({
|
|||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'kraiken-lib': fileURLToPath(new URL('../kraiken-lib/src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue