lint/lib (#49)
resolves #42 Co-authored-by: johba <johba@harb.eth> Reviewed-on: https://codeberg.org/johba/harb/pulls/49
This commit is contained in:
parent
4f7cebda56
commit
09c36f2c87
19 changed files with 2340 additions and 236 deletions
3
.husky/pre-commit
Executable file
3
.husky/pre-commit
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
cd kraiken-lib || exit 1
|
||||||
|
npx lint-staged
|
||||||
8
kraiken-lib/.prettierrc
Normal file
8
kraiken-lib/.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 140,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
29
kraiken-lib/eslint.config.js
Normal file
29
kraiken-lib/eslint.config.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
||||||
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ["src/**/*.ts"],
|
||||||
|
ignores: ["src/tests/**/*", "src/__generated__/**/*"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: { project: "./tsconfig.json" }
|
||||||
|
},
|
||||||
|
plugins: { "@typescript-eslint": tsPlugin },
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{ selector: "function", format: ["camelCase"] },
|
||||||
|
{ selector: "variable", format: ["camelCase", "UPPER_CASE"] },
|
||||||
|
{ selector: "typeLike", format: ["PascalCase"] }
|
||||||
|
],
|
||||||
|
"max-len": ["error", { code: 140, ignoreStrings: true, ignoreTemplateLiterals: true }],
|
||||||
|
"no-console": "error",
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||||
|
"prefer-const": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
1453
kraiken-lib/package-lock.json
generated
1453
kraiken-lib/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,37 +5,6 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"/dist"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"test": "jest",
|
|
||||||
"compile": "graphql-codegen",
|
|
||||||
"watch": "graphql-codegen -w"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@apollo/client": "^3.9.10",
|
|
||||||
"graphql": "^16.8.1",
|
|
||||||
"graphql-tag": "^2.12.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@graphql-codegen/cli": "^5.0.2",
|
|
||||||
"@graphql-codegen/client-preset": "^4.2.5",
|
|
||||||
"@graphql-codegen/typescript": "^4.0.6",
|
|
||||||
"@graphql-codegen/typescript-operations": "^4.2.0",
|
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/node": "^24.6.0",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"ts-jest": "^29.1.2",
|
|
||||||
"typescript": "^5.4.3"
|
|
||||||
},
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
|
@ -77,5 +46,47 @@
|
||||||
"require": "./dist/abis.js",
|
"require": "./dist/abis.js",
|
||||||
"import": "./dist/abis.js"
|
"import": "./dist/abis.js"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"/dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"compile": "graphql-codegen",
|
||||||
|
"watch": "graphql-codegen -w",
|
||||||
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
|
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
||||||
|
"format": "prettier --write 'src/**/*.ts'",
|
||||||
|
"format:check": "prettier --check 'src/**/*.ts'",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.ts": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.9.10",
|
||||||
|
"graphql": "^16.8.1",
|
||||||
|
"graphql-tag": "^2.12.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "^5.0.2",
|
||||||
|
"@graphql-codegen/client-preset": "^4.2.5",
|
||||||
|
"@graphql-codegen/typescript": "^4.0.6",
|
||||||
|
"@graphql-codegen/typescript-operations": "^4.2.0",
|
||||||
|
"@graphql-typed-document-node/core": "^3.2.0",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^24.6.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||||
|
"@typescript-eslint/parser": "^8.45.0",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"lint-staged": "^16.2.3",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
|
"typescript": "^5.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
kraiken-lib/src/__generated__/graphql.ts
generated
18
kraiken-lib/src/__generated__/graphql.ts
generated
|
|
@ -7,14 +7,14 @@ export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> =
|
||||||
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
|
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
|
||||||
/** All built-in and custom scalars, mapped to their actual values */
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
export type Scalars = {
|
export type Scalars = {
|
||||||
ID: { input: string; output: string; }
|
ID: { input: string; output: string };
|
||||||
String: { input: string; output: string; }
|
String: { input: string; output: string };
|
||||||
Boolean: { input: boolean; output: boolean; }
|
Boolean: { input: boolean; output: boolean };
|
||||||
Int: { input: number; output: number; }
|
Int: { input: number; output: number };
|
||||||
Float: { input: number; output: number; }
|
Float: { input: number; output: number };
|
||||||
BigInt: { input: string; output: string; }
|
BigInt: { input: string; output: string };
|
||||||
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
|
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
|
||||||
JSON: { input: any; output: any; }
|
JSON: { input: any; output: any };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Meta = {
|
export type Meta = {
|
||||||
|
|
@ -39,12 +39,10 @@ export type Query = {
|
||||||
statss: StatsPage;
|
statss: StatsPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryPositionsArgs = {
|
export type QueryPositionsArgs = {
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryPositionssArgs = {
|
export type QueryPositionssArgs = {
|
||||||
after?: InputMaybe<Scalars['String']['input']>;
|
after?: InputMaybe<Scalars['String']['input']>;
|
||||||
before?: InputMaybe<Scalars['String']['input']>;
|
before?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
|
@ -54,12 +52,10 @@ export type QueryPositionssArgs = {
|
||||||
where?: InputMaybe<PositionsFilter>;
|
where?: InputMaybe<PositionsFilter>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryStatsArgs = {
|
export type QueryStatsArgs = {
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryStatssArgs = {
|
export type QueryStatssArgs = {
|
||||||
after?: InputMaybe<Scalars['String']['input']>;
|
after?: InputMaybe<Scalars['String']['input']>;
|
||||||
before?: InputMaybe<Scalars['String']['input']>;
|
before?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,15 @@ import StakeForgeOutput from '../../onchain/out/Stake.sol/Stake.json' assert { t
|
||||||
/**
|
/**
|
||||||
* Kraiken ERC20 token contract ABI
|
* Kraiken ERC20 token contract ABI
|
||||||
*/
|
*/
|
||||||
export const KraikenAbi = KraikenForgeOutput.abi;
|
export const KRAIKEN_ABI = KraikenForgeOutput.abi;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stake (Harberger tax staking) contract ABI
|
* Stake (Harberger tax staking) contract ABI
|
||||||
*/
|
*/
|
||||||
export const StakeAbi = StakeForgeOutput.abi;
|
export const STAKE_ABI = StakeForgeOutput.abi;
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
export const ABIS = {
|
export const ABIS = {
|
||||||
Kraiken: KraikenAbi,
|
Kraiken: KRAIKEN_ABI,
|
||||||
Stake: StakeAbi,
|
Stake: STAKE_ABI,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export * from "./staking.js";
|
export * from './staking.js';
|
||||||
export * from "./snatch.js";
|
export * from './snatch.js';
|
||||||
export * from "./ids.js";
|
export * from './ids.js';
|
||||||
export * from "./taxRates.js";
|
export * from './taxRates.js';
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
import { bytesToUint256LittleEndian } from "./subgraph.js";
|
import { bytesToUint256LittleEndian } from './subgraph.js';
|
||||||
|
|
||||||
export function toBigIntId(id: string | Uint8Array | number | bigint): bigint {
|
export function toBigIntId(id: string | Uint8Array | number | bigint): bigint {
|
||||||
if (typeof id === "bigint") return id;
|
if (typeof id === 'bigint') return id;
|
||||||
if (typeof id === "number") return BigInt(id);
|
if (typeof id === 'number') return BigInt(id);
|
||||||
if (typeof id === "string") {
|
if (typeof id === 'string') {
|
||||||
const trimmed = id.startsWith("0x") ? id : `0x${id}`;
|
const trimmed = id.startsWith('0x') ? id : `0x${id}`;
|
||||||
return BigInt(trimmed);
|
return BigInt(trimmed);
|
||||||
}
|
}
|
||||||
if (id instanceof Uint8Array) {
|
if (id instanceof Uint8Array) {
|
||||||
return bytesToUint256LittleEndian(id);
|
return bytesToUint256LittleEndian(id);
|
||||||
}
|
}
|
||||||
throw new Error("Unsupported position id type");
|
throw new Error('Unsupported position id type');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodePositionId(
|
export function decodePositionId(id: string | Uint8Array | number | bigint): bigint {
|
||||||
id: string | Uint8Array | number | bigint
|
|
||||||
): bigint {
|
|
||||||
return toBigIntId(id);
|
return toBigIntId(id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
export {
|
export { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from './subgraph.js';
|
||||||
bytesToUint256LittleEndian,
|
|
||||||
uint256ToBytesLittleEndian,
|
|
||||||
} from "./subgraph.js";
|
|
||||||
|
|
||||||
// Backward compatible aliases
|
// Backward compatible aliases
|
||||||
export {
|
export { bytesToUint256LittleEndian as bytesToUint256, uint256ToBytesLittleEndian as uint256ToBytes } from './subgraph.js';
|
||||||
bytesToUint256LittleEndian as bytesToUint256,
|
|
||||||
uint256ToBytesLittleEndian as uint256ToBytes,
|
|
||||||
} from "./subgraph.js";
|
|
||||||
|
|
||||||
export { TAX_RATE_OPTIONS, type TaxRateOption } from "./taxRates.js";
|
export { TAX_RATE_OPTIONS, type TaxRateOption } from './taxRates.js';
|
||||||
|
|
||||||
export {
|
export { calculateSnatchShortfall, isPositionDelinquent } from './staking.js';
|
||||||
calculateSnatchShortfall,
|
|
||||||
isPositionDelinquent,
|
|
||||||
} from "./staking.js";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
minimumTaxRate,
|
minimumTaxRate,
|
||||||
|
|
@ -23,8 +14,11 @@ export {
|
||||||
type SnatchablePosition,
|
type SnatchablePosition,
|
||||||
type SnatchSelectionOptions,
|
type SnatchSelectionOptions,
|
||||||
type SnatchSelectionResult,
|
type SnatchSelectionResult,
|
||||||
} from "./snatch.js";
|
} from './snatch.js';
|
||||||
|
|
||||||
export { decodePositionId } from "./ids.js";
|
export { decodePositionId } from './ids.js';
|
||||||
|
|
||||||
export { KraikenAbi, StakeAbi, ABIS } from "./abis.js";
|
export { KRAIKEN_ABI, STAKE_ABI, ABIS } from './abis.js';
|
||||||
|
|
||||||
|
// Backward compatible aliases
|
||||||
|
export { KRAIKEN_ABI as KraikenAbi, STAKE_ABI as StakeAbi } from './abis.js';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Positions } from "./__generated__/graphql.js";
|
import type { Positions } from './__generated__/graphql.js';
|
||||||
import { toBigIntId } from "./ids.js";
|
import { toBigIntId } from './ids.js';
|
||||||
|
|
||||||
export interface SnatchablePosition {
|
export interface SnatchablePosition {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
|
|
@ -27,30 +27,16 @@ export interface SnatchSelectionResult {
|
||||||
const SHARE_SCALE = 1_000_000n;
|
const SHARE_SCALE = 1_000_000n;
|
||||||
|
|
||||||
function normaliseAddress(value?: string | null): string {
|
function normaliseAddress(value?: string | null): string {
|
||||||
return (value ?? "").toLowerCase();
|
return (value ?? '').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function minimumTaxRate<T extends { taxRate: number }>(
|
export function minimumTaxRate<T extends { taxRate: number }>(positions: T[], fallback: number = 0): number {
|
||||||
positions: T[],
|
|
||||||
fallback: number = 0
|
|
||||||
): number {
|
|
||||||
if (!positions.length) return fallback;
|
if (!positions.length) return fallback;
|
||||||
return positions.reduce(
|
return positions.reduce((min, position) => (position.taxRate < min ? position.taxRate : min), Number.POSITIVE_INFINITY);
|
||||||
(min, position) => (position.taxRate < min ? position.taxRate : min),
|
|
||||||
Number.POSITIVE_INFINITY
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectSnatchPositions(
|
export function selectSnatchPositions(candidates: SnatchablePosition[], options: SnatchSelectionOptions): SnatchSelectionResult {
|
||||||
candidates: SnatchablePosition[],
|
const { shortfallShares, maxTaxRate, includeOwned = false, recipientAddress = null } = options;
|
||||||
options: SnatchSelectionOptions
|
|
||||||
): SnatchSelectionResult {
|
|
||||||
const {
|
|
||||||
shortfallShares,
|
|
||||||
maxTaxRate,
|
|
||||||
includeOwned = false,
|
|
||||||
recipientAddress = null,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
if (shortfallShares <= 0n) {
|
if (shortfallShares <= 0n) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -62,7 +48,7 @@ export function selectSnatchPositions(
|
||||||
|
|
||||||
const recipientNormalised = normaliseAddress(recipientAddress);
|
const recipientNormalised = normaliseAddress(recipientAddress);
|
||||||
|
|
||||||
const filtered = candidates.filter((candidate) => {
|
const filtered = candidates.filter(candidate => {
|
||||||
if (candidate.taxRate >= maxTaxRate) {
|
if (candidate.taxRate >= maxTaxRate) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -100,17 +86,14 @@ export function selectSnatchPositions(
|
||||||
covered = shortfallShares - remaining;
|
covered = shortfallShares - remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (maxSelectedTaxRate === undefined || candidate.taxRate > maxSelectedTaxRate) {
|
||||||
maxSelectedTaxRate === undefined ||
|
|
||||||
candidate.taxRate > maxSelectedTaxRate
|
|
||||||
) {
|
|
||||||
maxSelectedTaxRate = candidate.taxRate;
|
maxSelectedTaxRate = candidate.taxRate;
|
||||||
if (typeof candidate.taxRateIndex === "number") {
|
if (typeof candidate.taxRateIndex === 'number') {
|
||||||
maxSelectedTaxRateIndex = candidate.taxRateIndex;
|
maxSelectedTaxRateIndex = candidate.taxRateIndex;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
candidate.taxRate === maxSelectedTaxRate &&
|
candidate.taxRate === maxSelectedTaxRate &&
|
||||||
typeof candidate.taxRateIndex === "number" &&
|
typeof candidate.taxRateIndex === 'number' &&
|
||||||
candidate.taxRateIndex > (maxSelectedTaxRateIndex ?? -1)
|
candidate.taxRateIndex > (maxSelectedTaxRateIndex ?? -1)
|
||||||
) {
|
) {
|
||||||
maxSelectedTaxRateIndex = candidate.taxRateIndex;
|
maxSelectedTaxRateIndex = candidate.taxRateIndex;
|
||||||
|
|
@ -133,14 +116,12 @@ export function getSnatchList(
|
||||||
stakeTotalSupply: bigint
|
stakeTotalSupply: bigint
|
||||||
): bigint[] {
|
): bigint[] {
|
||||||
if (stakeTotalSupply <= 0n) {
|
if (stakeTotalSupply <= 0n) {
|
||||||
throw new Error("stakeTotalSupply must be greater than zero");
|
throw new Error('stakeTotalSupply must be greater than zero');
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidates: SnatchablePosition[] = positions.map((position) => {
|
const candidates: SnatchablePosition[] = positions.map(position => {
|
||||||
const shareRatio = Number(position.share);
|
const shareRatio = Number(position.share);
|
||||||
const scaledShares = BigInt(
|
const scaledShares = BigInt(Math.round(shareRatio * Number(SHARE_SCALE)));
|
||||||
Math.round(shareRatio * Number(SHARE_SCALE))
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: toBigIntId(position.id),
|
id: toBigIntId(position.id),
|
||||||
|
|
@ -157,8 +138,8 @@ export function getSnatchList(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.remainingShortfall > 0n) {
|
if (result.remainingShortfall > 0n) {
|
||||||
throw new Error("Not enough capacity");
|
throw new Error('Not enough capacity');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.selected.map((candidate) => candidate.id);
|
return result.selected.map(candidate => candidate.id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export function calculateSnatchShortfall(
|
||||||
capDenominator: bigint = 10n
|
capDenominator: bigint = 10n
|
||||||
): bigint {
|
): bigint {
|
||||||
if (capDenominator === 0n) {
|
if (capDenominator === 0n) {
|
||||||
throw new Error("capDenominator must be greater than zero");
|
throw new Error('capDenominator must be greater than zero');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cap = (stakeTotalSupply * capNumerator) / capDenominator;
|
const cap = (stakeTotalSupply * capNumerator) / capDenominator;
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
|
|
||||||
export function bytesToUint256LittleEndian(bytes: Uint8Array): bigint {
|
export function bytesToUint256LittleEndian(bytes: Uint8Array): bigint {
|
||||||
// console.log(hexString);
|
// console.log(hexString);
|
||||||
// const cleanHexString = hexString.startsWith('0x') ? hexString.substring(2) : hexString;
|
// const cleanHexString = hexString.startsWith('0x') ? hexString.substring(2) : hexString;
|
||||||
// const bytes = new Uint8Array(Math.ceil(cleanHexString.length / 2));
|
// const bytes = new Uint8Array(Math.ceil(cleanHexString.length / 2));
|
||||||
// for (let i = 0, j = 0; i < cleanHexString.length; i += 2, j++) {
|
// for (let i = 0, j = 0; i < cleanHexString.length; i += 2, j++) {
|
||||||
// bytes[j] = parseInt(cleanHexString.slice(i, i + 2), 16);
|
// bytes[j] = parseInt(cleanHexString.slice(i, i + 2), 16);
|
||||||
// }
|
// }
|
||||||
let value: bigint = 0n;
|
let value: bigint = 0n;
|
||||||
|
|
||||||
for (let i = bytes.length - 1; i >= 0; i--) {
|
for (let i = bytes.length - 1; i >= 0; i--) {
|
||||||
value = (value << 8n) | BigInt(bytes[i]);
|
value = (value << 8n) | BigInt(bytes[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uint256ToBytesLittleEndian(value: bigint): Uint8Array {
|
export function uint256ToBytesLittleEndian(value: bigint): Uint8Array {
|
||||||
const bytes = new Uint8Array(4);
|
const bytes = new Uint8Array(4);
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
bytes[i] = Number((value >> (8n * BigInt(i))) & 0xFFn);
|
bytes[i] = Number((value >> (8n * BigInt(i))) & 0xffn);
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
// let hexString = '0x';
|
// let hexString = '0x';
|
||||||
|
|
||||||
// for (let i = 0; i < bytes.length; i++) {
|
// for (let i = 0; i < bytes.length; i++) {
|
||||||
// // Convert each byte to a hexadecimal string and pad with zero if needed
|
// // Convert each byte to a hexadecimal string and pad with zero if needed
|
||||||
// hexString += bytes[i].toString(16).padStart(2, '0');
|
// hexString += bytes[i].toString(16).padStart(2, '0');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// return hexString;
|
// return hexString;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { describe, expect, test } from "@jest/globals";
|
import { describe, expect, test } from '@jest/globals';
|
||||||
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from "../subgraph";
|
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from '../subgraph';
|
||||||
import { Position, PositionStatus } from '../__generated__/graphql';
|
import { Position, PositionStatus } from '../__generated__/graphql';
|
||||||
|
|
||||||
|
|
||||||
describe('BigInt Conversion Functions', () => {
|
describe('BigInt Conversion Functions', () => {
|
||||||
test('converts uint256 to bytes and back (little endian)', async () => {
|
test('converts uint256 to bytes and back (little endian)', async () => {
|
||||||
|
|
||||||
const mockPos: Position = {
|
const mockPos: Position = {
|
||||||
__typename: 'Position',
|
__typename: 'Position',
|
||||||
id: uint256ToBytesLittleEndian(3n),
|
id: uint256ToBytesLittleEndian(3n),
|
||||||
|
|
@ -23,8 +21,7 @@ describe('BigInt Conversion Functions', () => {
|
||||||
}
|
}
|
||||||
expect(hexString).toEqual('0x03000000');
|
expect(hexString).toEqual('0x03000000');
|
||||||
|
|
||||||
// return hexString;
|
// return hexString;
|
||||||
expect(bytesToUint256LittleEndian(mockPos.id)).toEqual(3n);
|
expect(bytesToUint256LittleEndian(mockPos.id)).toEqual(3n);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { describe, expect, test } from "@jest/globals";
|
import { describe, expect, test } from '@jest/globals';
|
||||||
import { decodePositionId } from "../ids";
|
import { decodePositionId } from '../ids';
|
||||||
import { uint256ToBytesLittleEndian } from "../subgraph";
|
import { uint256ToBytesLittleEndian } from '../subgraph';
|
||||||
|
|
||||||
describe("ids", () => {
|
describe('ids', () => {
|
||||||
test("decodePositionId works across representations", () => {
|
test('decodePositionId works across representations', () => {
|
||||||
const id = 12345n;
|
const id = 12345n;
|
||||||
const hex = `0x${id.toString(16)}`;
|
const hex = `0x${id.toString(16)}`;
|
||||||
const bytes = uint256ToBytesLittleEndian(id);
|
const bytes = uint256ToBytesLittleEndian(id);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
import { describe, expect, test } from "@jest/globals";
|
import { describe, expect, test } from '@jest/globals';
|
||||||
import {
|
import { getSnatchList, minimumTaxRate, selectSnatchPositions, type SnatchablePosition } from '../snatch';
|
||||||
getSnatchList,
|
import { uint256ToBytesLittleEndian } from '../subgraph';
|
||||||
minimumTaxRate,
|
import type { Position } from '../__generated__/graphql';
|
||||||
selectSnatchPositions,
|
import { PositionStatus } from '../__generated__/graphql';
|
||||||
type SnatchablePosition,
|
|
||||||
} from "../snatch";
|
|
||||||
import { uint256ToBytesLittleEndian } from "../subgraph";
|
|
||||||
import type { Position } from "../__generated__/graphql";
|
|
||||||
import { PositionStatus } from "../__generated__/graphql";
|
|
||||||
|
|
||||||
describe("snatch", () => {
|
describe('snatch', () => {
|
||||||
test("minimumTaxRate finds the lowest tax", () => {
|
test('minimumTaxRate finds the lowest tax', () => {
|
||||||
const rates = [{ taxRate: 0.12 }, { taxRate: 0.05 }, { taxRate: 0.08 }];
|
const rates = [{ taxRate: 0.12 }, { taxRate: 0.05 }, { taxRate: 0.08 }];
|
||||||
expect(minimumTaxRate(rates, 1)).toBeCloseTo(0.05);
|
expect(minimumTaxRate(rates, 1)).toBeCloseTo(0.05);
|
||||||
expect(minimumTaxRate([], 0.42)).toBe(0.42);
|
expect(minimumTaxRate([], 0.42)).toBe(0.42);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selectSnatchPositions chooses cheapest positions first", () => {
|
test('selectSnatchPositions chooses cheapest positions first', () => {
|
||||||
const candidates: SnatchablePosition[] = [
|
const candidates: SnatchablePosition[] = [
|
||||||
{ id: 1n, stakeShares: 30n, taxRate: 0.05, taxRateIndex: 5 },
|
{ id: 1n, stakeShares: 30n, taxRate: 0.05, taxRateIndex: 5 },
|
||||||
{ id: 2n, stakeShares: 40n, taxRate: 0.03, taxRateIndex: 3 },
|
{ id: 2n, stakeShares: 40n, taxRate: 0.03, taxRateIndex: 3 },
|
||||||
|
|
@ -28,13 +23,13 @@ describe("snatch", () => {
|
||||||
maxTaxRate: 0.08,
|
maxTaxRate: 0.08,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.selected.map((item) => item.id)).toEqual([2n, 1n]);
|
expect(result.selected.map(item => item.id)).toEqual([2n, 1n]);
|
||||||
expect(result.remainingShortfall).toBe(0n);
|
expect(result.remainingShortfall).toBe(0n);
|
||||||
expect(result.coveredShares).toBe(60n);
|
expect(result.coveredShares).toBe(60n);
|
||||||
expect(result.maxSelectedTaxRateIndex).toBe(5);
|
expect(result.maxSelectedTaxRateIndex).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selectSnatchPositions keeps track of remaining shortfall", () => {
|
test('selectSnatchPositions keeps track of remaining shortfall', () => {
|
||||||
const candidates: SnatchablePosition[] = [
|
const candidates: SnatchablePosition[] = [
|
||||||
{ id: 1n, stakeShares: 10n, taxRate: 0.01 },
|
{ id: 1n, stakeShares: 10n, taxRate: 0.01 },
|
||||||
{ id: 2n, stakeShares: 10n, taxRate: 0.02 },
|
{ id: 2n, stakeShares: 10n, taxRate: 0.02 },
|
||||||
|
|
@ -48,14 +43,14 @@ describe("snatch", () => {
|
||||||
expect(result.remainingShortfall).toBe(30n);
|
expect(result.remainingShortfall).toBe(30n);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getSnatchList converts subgraph positions", () => {
|
test('getSnatchList converts subgraph positions', () => {
|
||||||
const stakeTotalSupply = 1_000_000n * 10n ** 18n;
|
const stakeTotalSupply = 1_000_000n * 10n ** 18n;
|
||||||
|
|
||||||
const positions: Position[] = [
|
const positions: Position[] = [
|
||||||
{
|
{
|
||||||
__typename: "Position",
|
__typename: 'Position',
|
||||||
id: uint256ToBytesLittleEndian(1n),
|
id: uint256ToBytesLittleEndian(1n),
|
||||||
owner: "0xowner1",
|
owner: '0xowner1',
|
||||||
share: 0.0001,
|
share: 0.0001,
|
||||||
creationTime: 0,
|
creationTime: 0,
|
||||||
lastTaxTime: 0,
|
lastTaxTime: 0,
|
||||||
|
|
@ -63,9 +58,9 @@ describe("snatch", () => {
|
||||||
status: PositionStatus.Active,
|
status: PositionStatus.Active,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: "Position",
|
__typename: 'Position',
|
||||||
id: uint256ToBytesLittleEndian(2n),
|
id: uint256ToBytesLittleEndian(2n),
|
||||||
owner: "0xowner2",
|
owner: '0xowner2',
|
||||||
share: 0.0002,
|
share: 0.0002,
|
||||||
creationTime: 0,
|
creationTime: 0,
|
||||||
lastTaxTime: 0,
|
lastTaxTime: 0,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { describe, expect, test } from "@jest/globals";
|
import { describe, expect, test } from '@jest/globals';
|
||||||
import { calculateSnatchShortfall, isPositionDelinquent } from "../staking";
|
import { calculateSnatchShortfall, isPositionDelinquent } from '../staking';
|
||||||
|
|
||||||
describe("staking", () => {
|
describe('staking', () => {
|
||||||
test("calculateSnatchShortfall returns zero when within cap", () => {
|
test('calculateSnatchShortfall returns zero when within cap', () => {
|
||||||
const outstanding = 100n;
|
const outstanding = 100n;
|
||||||
const desired = 50n;
|
const desired = 50n;
|
||||||
const total = 1000n;
|
const total = 1000n;
|
||||||
|
|
@ -11,7 +11,7 @@ describe("staking", () => {
|
||||||
expect(result).toBe(0n);
|
expect(result).toBe(0n);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("calculateSnatchShortfall returns positive remainder when exceeding cap", () => {
|
test('calculateSnatchShortfall returns positive remainder when exceeding cap', () => {
|
||||||
const outstanding = 200n;
|
const outstanding = 200n;
|
||||||
const desired = 200n;
|
const desired = 200n;
|
||||||
const total = 1000n;
|
const total = 1000n;
|
||||||
|
|
@ -20,7 +20,7 @@ describe("staking", () => {
|
||||||
expect(result).toBe(200n);
|
expect(result).toBe(200n);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isPositionDelinquent respects tax rate windows", () => {
|
test('isPositionDelinquent respects tax rate windows', () => {
|
||||||
const now = 1_000_000;
|
const now = 1_000_000;
|
||||||
const taxRate = 0.5; // 50%
|
const taxRate = 0.5; // 50%
|
||||||
const windowSeconds = (365 * 24 * 60 * 60) / taxRate;
|
const windowSeconds = (365 * 24 * 60 * 60) / taxRate;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import { describe, expect, test } from "@jest/globals";
|
import { describe, expect, test } from '@jest/globals';
|
||||||
import { TAX_RATE_OPTIONS } from "../taxRates";
|
import { TAX_RATE_OPTIONS } from '../taxRates';
|
||||||
|
|
||||||
describe("taxRates", () => {
|
describe('taxRates', () => {
|
||||||
test("tax rate options exported for consumers", () => {
|
test('tax rate options exported for consumers', () => {
|
||||||
expect(TAX_RATE_OPTIONS.length).toBeGreaterThan(0);
|
expect(TAX_RATE_OPTIONS.length).toBeGreaterThan(0);
|
||||||
expect(TAX_RATE_OPTIONS[0]).toEqual(
|
expect(TAX_RATE_OPTIONS[0]).toEqual(expect.objectContaining({ index: 0, year: 1, decimal: 0.01 }));
|
||||||
expect.objectContaining({ index: 0, year: 1, decimal: 0.01 })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue