added harb-lib

This commit is contained in:
JulesCrown 2024-04-03 21:43:12 +02:00
parent ea1b007283
commit 5a8c80e838
15 changed files with 4876 additions and 48 deletions

2
harb-lib/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
dist
node_modules

22
harb-lib/README.md Normal file
View file

@ -0,0 +1,22 @@
## Test
```
yarn test
```
## Import
```
yarn add harb-lib
```
then
```
import {bytesToUint256LittleEndian, uint256ToBytesLittleEndian} from 'harb-lib'
uint256ToBytesLittleEndian(3n);
```

8
harb-lib/codegen.yml Normal file
View file

@ -0,0 +1,8 @@
overwrite: true
schema: "../subgraph/harb/schema.graphql" # Or an endpoint if your schema is remote
documents: "../subgraph/harb/src/**/*.graphql"
generates:
src/__generated__/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"

4
harb-lib/jest.config.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

32
harb-lib/package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "harb-lib",
"version": "0.1.0",
"description": "helper functions and snatch selection",
"main": "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",
"apollo-link-http": "^1.5.17",
"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",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.4.3"
}
}

52
harb-lib/src/__generated__/graphql.ts generated Normal file
View file

@ -0,0 +1,52 @@
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: 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 */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
BigDecimal: { input: any; output: any; }
BigInt: { input: any; output: any; }
Bytes: { input: any; output: any; }
};
export type Position = {
__typename?: 'Position';
creationTime: Scalars['Int']['output'];
id: Scalars['Bytes']['output'];
lastTaxTime?: Maybe<Scalars['Int']['output']>;
owner: Scalars['Bytes']['output'];
share: Scalars['BigDecimal']['output'];
status: PositionStatus;
taxRate: Scalars['BigDecimal']['output'];
};
export enum PositionStatus {
Active = 'Active',
Closed = 'Closed'
}
export type Query = {
__typename?: 'Query';
positions?: Maybe<Array<Maybe<Position>>>;
stats?: Maybe<Array<Maybe<Stats>>>;
};
export type Stats = {
__typename?: 'Stats';
activeSupply: Scalars['BigInt']['output'];
id: Scalars['Bytes']['output'];
outstandingSupply: Scalars['BigInt']['output'];
};
export type GetPositionsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetPositionsQuery = { __typename?: 'Query', positions?: Array<{ __typename?: 'Position', id: any, owner: any, share: any, creationTime: number, lastTaxTime?: number | null, taxRate: any, status: PositionStatus } | null> | null };

1
harb-lib/src/index.ts Normal file
View file

@ -0,0 +1 @@
export {bytesToUint256LittleEndian, uint256ToBytesLittleEndian} from './subgraph'

32
harb-lib/src/subgraph.ts Normal file
View file

@ -0,0 +1,32 @@
export function bytesToUint256LittleEndian(bytes: Uint8Array): bigint {
// console.log(hexString);
// 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 value: bigint = 0n;
for (let i = bytes.length - 1; i >= 0; i--) {
value = (value << 8n) | BigInt(bytes[i]);
}
return value;
}
export function uint256ToBytesLittleEndian(value: bigint): Uint8Array {
const bytes = new Uint8Array(4);
for (let i = 0; i < 4; i++) {
bytes[i] = Number((value >> (8n * BigInt(i))) & 0xFFn);
}
return bytes;
// let hexString = '0x';
// for (let i = 0; i < bytes.length; i++) {
// // Convert each byte to a hexadecimal string and pad with zero if needed
// hexString += bytes[i].toString(16).padStart(2, '0');
// }
// return hexString;
}

View file

@ -0,0 +1,29 @@
import { bytesToUint256LittleEndian, uint256ToBytesLittleEndian } from "../subgraph"; // Adjust the import path to where your functions are defined
import { Position, PositionStatus } from '../__generated__/graphql';
describe('BigInt Conversion Functions', () => {
test('converts uint256 to bytes and back (little endian)', async () => {
const mockPos: Position = {
__typename: 'Position',
id: uint256ToBytesLittleEndian(3n),
owner: '0x8db6b632d743aef641146dc943acb64957155388',
share: '0.000001',
creationTime: 1610000000,
taxRate: '0.01',
status: PositionStatus.Active, // Enum usage
};
let hexString = '0x';
for (let i = 0; i < mockPos.id.length; i++) {
// Convert each byte to a hexadecimal string and pad with zero if needed
hexString += mockPos.id[i].toString(16).padStart(2, '0');
}
expect(hexString).toEqual('0x03000000');
// return hexString;
expect(bytesToUint256LittleEndian(mockPos.id)).toEqual(3n);
});
});

11
harb-lib/tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"declaration": true,
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}

4565
harb-lib/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -212,46 +212,7 @@ contract BaseLineLP {
});
}
// call this function when price has moved up 15%
function shift() external {
// Fetch the current tick from the Uniswap V3 pool
(uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0();
// TODO: check slippage with oracle
// ## check price moved up
{
// Check if current tick is within the specified range
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
// center tick can be calculated positive and negative numbers the same
int24 centerTick = anchorTickLower + ((anchorTickUpper - anchorTickLower) / 2);
int24 amplitudeTick = anchorTickLower + (anchorTickUpper - anchorTickLower) * 3 / 20;
// Determine the correct comparison direction based on token0isWeth
bool isUp = token0isWeth ? currentTick > centerTick : currentTick < centerTick;
bool isEnough = token0isWeth ? currentTick > amplitudeTick : currentTick < amplitudeTick;
// Check Conditions
require(isUp, "call slide(), not shift()");
require(isEnough, "amplitude not reached, come back later");
}
// ## scrape positions
uint256 ethInAnchor;
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
if (i == uint256(Stage.ANCHOR)) {
ethInAnchor = token0isWeth ? amount0 : amount1;
}
}
// TODO: handle fees
// ## set new positions
// reduce Anchor by 10% of new ETH. It will be moved into Floor
uint256 initialEthInAnchor = ethIn(Stage.ANCHOR);
ethInAnchor -= (ethInAnchor - initialEthInAnchor) * 10 / LIQUIDITY_RATIO_DIVISOR;
function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal {
// ### set Anchor position
uint128 anchorLiquidity;
{
@ -261,11 +222,11 @@ contract BaseLineLP {
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, ethInAnchor
sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, ethInAnchor
sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor
);
}
_mint(tickLower, tickUpper, anchorLiquidity);
@ -322,16 +283,103 @@ contract BaseLineLP {
}
_mint(tickLower, tickUpper, liquidity);
}
}
// call this function when price has moved up 15%
function shift() external {
require(positions[Stage.ANCHOR].liquidity > 0, "Not initialized");
// Fetch the current tick from the Uniswap V3 pool
(uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0();
// TODO: check slippage with oracle
// ## check price moved up
{
// Check if current tick is within the specified range
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
// center tick can be calculated positive and negative numbers the same
int24 centerTick = anchorTickLower + ((anchorTickUpper - anchorTickLower) / 2);
int24 amplitudeTick = anchorTickLower + (anchorTickUpper - anchorTickLower) * 3 / 20;
// Determine the correct comparison direction based on token0isWeth
bool isUp = token0isWeth ? currentTick > centerTick : currentTick < centerTick;
bool isEnough = token0isWeth ? currentTick > amplitudeTick : currentTick < amplitudeTick;
// Check Conditions
require(isUp, "call slide(), not shift()");
require(isEnough, "amplitude not reached, come back later!");
}
// ## scrape positions
uint256 ethInAnchor;
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
if (i == uint256(Stage.ANCHOR)) {
ethInAnchor = token0isWeth ? amount0 : amount1;
}
}
// TODO: handle fees
// ## set new positions
// reduce Anchor by 10% of new ETH. It will be moved into Floor
uint256 initialEthInAnchor = ethIn(Stage.ANCHOR);
ethInAnchor -= (ethInAnchor - initialEthInAnchor) * 10 / LIQUIDITY_RATIO_DIVISOR;
// cap anchor size at 10 % of total ETH
uint256 ethBalance = address(this).balance;
ethInAnchor = (ethInAnchor > ethBalance / 10) ? ethBalance / 10 : ethInAnchor;
_set(sqrtPriceX96, currentTick, ethInAnchor);
}
function slide() external {
// check price moved down
// check price moved down enough
// scrape positions
// get outstanding upply and capacity
// set new positions
// burn harb
// Fetch the current tick from the Uniswap V3 pool
(uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0();
// TODO: check slippage with oracle
// ## check price moved down
if (positions[Stage.ANCHOR].liquidity > 0) {
// Check if current tick is within the specified range
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
// center tick can be calculated positive and negative numbers the same
int24 centerTick = anchorTickLower + ((anchorTickUpper - anchorTickLower) / 2);
int24 amplitudeTick = anchorTickLower + (anchorTickUpper - anchorTickLower) * 3 / 20;
// Determine the correct comparison direction based on token0isWeth
bool isDown = token0isWeth ? currentTick < centerTick : currentTick > centerTick;
bool isEnough = token0isWeth ? currentTick < amplitudeTick : currentTick > amplitudeTick;
// Check Conditions
require(isDown, "call shift(), not slide()");
require(isEnough, "amplitude not reached, diamond hands!");
}
// ## scrape positions
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
if (position.liquidity > 0) {
pool.burn(position.tickLower, position.tickUpper, position.liquidity);
// TODO: handle fees
}
}
uint256 ethBalance = address(this).balance;
if (ethBalance == 0) {
// TODO: set only discovery
return;
}
uint256 ethInAnchor = ethIn(Stage.ANCHOR);
uint256 ethInFloor = ethIn(Stage.FLOOR);
// use previous ration of Floor to Anchor
uint256 ethInNewAnchor = ethBalance * ethInAnchor / (ethInAnchor + ethInFloor);
// but cap anchor size at 10 % of total ETH
ethInNewAnchor = (ethInNewAnchor > ethBalance / 10) ? ethBalance / 10 : ethInNewAnchor;
_set(sqrtPriceX96, currentTick, ethInNewAnchor);
}
}

View file

@ -88,6 +88,7 @@ contract Stake is IStake {
if (sharesWanted < minStake) {
revert SharesTooLow(receiver, assets, sharesWanted, minStake);
}
// TODO: check that position size is multiple of minStake
// run through all suggested positions to snatch
for (uint256 i = 0; i < positionsToSnatch.length; i++) {
@ -104,6 +105,8 @@ contract Stake is IStake {
// TODO: what if someone calls payTax and exitPosition in the same transaction?
_payTax(pos, 0);
_exitPosition(positionsToSnatch[i], pos);
// TODO: exit positions partially, if needed
// TODO: avoid greeving where more positions are freed than needed.
}
// try to make a new position in the free space and hope it is big enough

View file

@ -1,3 +1,6 @@
scalar Bytes
scalar BigInt
scalar BigDecimal
type Stats @entity {
id: Bytes!
@ -21,3 +24,8 @@ type Position @entity {
taxRate: BigDecimal!
status: PositionStatus!
}
type Query {
stats: [Stats]
positions: [Position]
}

View file

@ -0,0 +1,11 @@
query GetPositions {
positions {
id
owner
share
creationTime
lastTaxTime
taxRate
status
}
}