Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Orca whirlpools #122

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"testFriktion": "mocha --require ts-node/register --timeout 10000000 ./test/friktion.ts",
"testNftFinance": "mocha --require ts-node/register --timeout 10000000 ./test/nftFinance.ts",
"testLido": "mocha --require ts-node/register --timeout 10000000 ./test/lido.ts",
"testMarinade": "mocha --require ts-node/register --timeout 10000000 ./test/marinade.ts"
"testMarinade": "mocha --require ts-node/register --timeout 10000000 ./test/marinade.ts",
"testWhirlpools": "mocha --require ts-node/register --timeout 10000000 ./test/whirlpools.ts"
},
"keywords": [],
"author": "",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * as nftFinance from "./nftFinance";
export * as friktion from "./friktion";
export * as lido from "./lido";
export * as marinade from "./marinade";
export * as whirlpools from "./whirlpools";
26 changes: 26 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ export interface PageConfig {
pageIndex: number;
}

export interface ICLPoolInfo {
poolId: PublicKey;
tokenAMint: PublicKey;
tokenBMint: PublicKey;
}

export interface ICLPositionInfo {
positionId: PublicKey;
mint: PublicKey;
userKey: PublicKey;
}

export enum PoolDirection {
Obverse,
Reverse,
Expand Down Expand Up @@ -221,3 +233,17 @@ export interface IInstanceVault {
parseWithdrawer?(data: Buffer, withdrawerId: PublicKey): IWithdrawerInfo;
getAllWithdrawers?(connection: Connection, userKey: PublicKey): Promise<IWithdrawerInfo[]>;
}

export interface IInstanceCLPool {
getAllPools(connection: Connection, page?: PageConfig): Promise<ICLPoolInfo[]>;
getAllPoolWrappers(connection: Connection, page?: PageConfig): Promise<ICLPoolWrapper[]>;
getPool(connection: Connection, poolId: PublicKey): Promise<ICLPoolInfo>;
getPoolWrapper(connection: Connection, poolId: PublicKey): Promise<ICLPoolWrapper>;
parsePool(data: Buffer, poolId: PublicKey): ICLPoolInfo;
getPosition(connection: Connection, positionId: PublicKey): Promise<ICLPositionInfo>;
getAllPositions(connection: Connection, userKey: PublicKey): Promise<ICLPositionInfo[]>;
parsePosition(data: Buffer, positionId: PublicKey): ICLPositionInfo;
}
export interface ICLPoolWrapper {
poolInfo: ICLPoolInfo;
}
5 changes: 5 additions & 0 deletions src/whirlpools/ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { PublicKey } from "@solana/web3.js";

export const ORCA_WHIRLPOOLS_PROGRAM_ID = new PublicKey("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc");

export const ORCA_WHIRLPOOLS_CONFIG = new PublicKey("2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ");
95 changes: 95 additions & 0 deletions src/whirlpools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export * from "./ids";
export * from "./infos";
export * from "./layouts";
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { ICLPoolInfo, ICLPositionInfo } from "../types";

export interface PoolInfo extends ICLPoolInfo {
poolId: PublicKey;
whirlpoolBump: number;
config: WhirlpoolConfig;
tickSpacing: number;
tickSpacingSeed0: number;
tickSpacingSeed1: number;
feeRate: number;
protocolFeeRate: number;
liquidity: BN;
sqrtPrice: BN;
tickCurrentIndex: number;
protocolFeeOwedA: BN;
protocolFeeOwedB: BN;
tokenMintA: PublicKey;
tokenVaultA: PublicKey;
feeGrowthGlobalA: BN;
tokenMintB: PublicKey;
tokenVaultB: PublicKey;
feeGrowthGlobalB: BN;
rewardLastUpdatedTimestamp: BN;
reward0: RewardInfo;
reward1: RewardInfo;
reward2: RewardInfo;
tickArray: Tick[];
}

export interface PositionInfo extends ICLPositionInfo {
positionId: PublicKey;
poolId: PublicKey;
positionMint: PublicKey;
liquidity: BN;
tickLowerIndex: number;
tickUpperIndex: number;
feeGrowthCheckpointA: BN;
feeOwedA: BN;
feeGrowthCheckpointB: BN;
feeOwedB: BN;
rewardInfo0: PositionReward;
rewardInfo1: PositionReward;
rewardInfo2: PositionReward;
}

export interface WhirlpoolConfig {
configId: PublicKey;
feeAuthority: PublicKey;
collectProtocolFeesAuthority: PublicKey;
rewardEmissionsSuperAuthority: PublicKey;
defaultProtocolFeeRate: number;
}

export interface FeeConfig {
configId: PublicKey;
defaultProtocolFeeRate: PublicKey;
tickSpacing: number;
defaultFeeRate: number;
}

export interface Tick {
tickId: PublicKey;
index: number;
initialized: boolean;
liquidityGross: BN;
liquidityNet: BN;
feeGrowthOutsideA: BN;
feeGrowthOutsideB: BN;
rewardGrowthsOutside0: BN;
rewardGrowthsOutside1: BN;
rewardGrowthsOutside2: BN;
}
export interface RewardInfo {
mint: PublicKey;
vault: PublicKey;
authority: PublicKey;
emissionsPerSecondX64: BN;
growthGlobalX64: BN;
}

export interface PositionReward {
growthInsideCheckpoint: BN;
amountOwed: BN;
}

export interface TickArray {
startTickIndex: number;
ticks: Tick[];
whirlpool: PublicKey;
}
150 changes: 150 additions & 0 deletions src/whirlpools/infos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
Connection,
MemcmpFilter,
GetProgramAccountsConfig,
DataSizeFilter,
PublicKey,
GetProgramAccountsFilter,
} from "@solana/web3.js";
import BN from "bn.js";
import { ICLPoolWrapper, ICLPositionInfo, IInstanceCLPool, PageConfig } from "../types";
import { CONFIG_LAYOUT, POSITION_LAYOUT, TICK_ARRAY_LAYOUT, WHIRLPOOL_LAYOUT } from "./layouts";
import { ORCA_WHIRLPOOLS_CONFIG, ORCA_WHIRLPOOLS_PROGRAM_ID } from "./ids";
import * as types from ".";
import { AccountLayout, MintLayout, TOKEN_PROGRAM_ID } from "@solana/spl-token-v2";

let infos: IInstanceCLPool;

infos = class InstanceWhirlpools {
static async getAllPools(connection: Connection, page?: PageConfig): Promise<types.PoolInfo[]> {
const wpConfig = await getConfig(connection);
let filters: GetProgramAccountsFilter[] = [{ memcmp: { offset: 8, bytes: wpConfig.configId.toString() } }];

const config: GetProgramAccountsConfig = { filters: filters };
let whirlpoolInfos = await connection.getProgramAccounts(ORCA_WHIRLPOOLS_PROGRAM_ID, config);
let ticksMaps = await getAllTickMap(connection);
return whirlpoolInfos
.filter((info) => info.account.data.length > 600)
.map((whirlpoolInfo) => {
const poolInfo = this.parsePool(whirlpoolInfo.account.data, whirlpoolInfo.pubkey);
return {
...poolInfo,
config: wpConfig,
tickArray: ticksMaps.has(poolInfo.poolId.toString()) ? ticksMaps.get(poolInfo.poolId.toString())! : [],
};
});
}
static async getPool(connection: Connection, poolId: PublicKey): Promise<types.PoolInfo> {
const wpConfig = await getConfig(connection);
let ticksMaps = await getAllTickMap(connection);
const poolInfoRaw = await connection.getAccountInfo(poolId);
let poolInfo = this.parsePool(poolInfoRaw!.data, poolId);

return {
...poolInfo,
config: wpConfig,
tickArray: ticksMaps.has(poolInfo.poolId.toString()) ? ticksMaps.get(poolInfo.poolId.toString())! : [],
};
}
static async getPoolWrapper(connection: Connection, poolId: PublicKey): Promise<PoolInfoWrapper> {
const pool = await this.getPool(connection, poolId);
return new PoolInfoWrapper(pool);
}

static parsePool(data: Buffer, infoPubkey: PublicKey): types.PoolInfo {
if (data.length < WHIRLPOOL_LAYOUT.span) throw new Error("Invalid pool data");
let parsed = WHIRLPOOL_LAYOUT.decode(data) as types.PoolInfo;
return { ...parsed, poolId: infoPubkey, tokenAMint: parsed.tokenMintA, tokenBMint: parsed.tokenMintB };
}
static async getAllPoolWrappers(connection: Connection, page?: PageConfig): Promise<PoolInfoWrapper[]> {
return (await this.getAllPools(connection, page)).map((pool) => new PoolInfoWrapper(pool));
}
static async getPosition(connection: Connection, positionId: PublicKey): Promise<ICLPositionInfo> {
const positionInfoRaw = await connection.getAccountInfo(positionId);

return this.parsePosition(positionInfoRaw!.data, positionId);
}
static async getAllPositions(connection: Connection, userKey: PublicKey): Promise<ICLPositionInfo[]> {
let allUserTokenMintSet = new Map<string, BN>();
(await connection.getTokenAccountsByOwner(userKey, { programId: TOKEN_PROGRAM_ID })).value.forEach((info) => {
let rawAccount = AccountLayout.decode(info.account.data);
if (rawAccount.amount.toString() == "0") return false;
allUserTokenMintSet.set(rawAccount.mint.toString(), new BN(rawAccount.amount.toString()));
});
const sizeFilter: DataSizeFilter = {
dataSize: POSITION_LAYOUT.span,
};
let filters: GetProgramAccountsFilter[] = [sizeFilter];
let allPositionRaw = await connection.getProgramAccounts(ORCA_WHIRLPOOLS_PROGRAM_ID, { filters: filters });
return allPositionRaw
.map((info) => {
return this.parsePosition(info.account.data, info.pubkey);
})
.filter((info) => {
return allUserTokenMintSet.has(info.mint.toString());
});
}
static parsePosition(data: Buffer, positionId: PublicKey): ICLPositionInfo {
if (data.length < POSITION_LAYOUT.span) throw new Error("Invalid position data");
return { ...POSITION_LAYOUT.decode(data), positionId: positionId } as ICLPositionInfo;
}
};
export class PoolInfoWrapper implements ICLPoolWrapper {
constructor(public poolInfo: types.PoolInfo) {}
getPositionTokenAmount(position: types.PositionInfo) {
let liquidity = position.liquidity;
let currentTick = this.poolInfo.tickCurrentIndex;
let ticks = this.poolInfo.tickArray.filter(
(tick) => tick.index >= position.tickLowerIndex && tick.index <= position.tickUpperIndex
);
}
}

export async function getConfig(connection: Connection): Promise<types.WhirlpoolConfig> {
let configInfo = await connection.getAccountInfo(ORCA_WHIRLPOOLS_CONFIG);
if (configInfo) {
let parsed = CONFIG_LAYOUT.decode(configInfo.data);
return { ...parsed, configId: ORCA_WHIRLPOOLS_CONFIG } as types.WhirlpoolConfig;
} else {
throw new Error("Config not found");
}
}
export async function getAllTickMap(connection: Connection, whirlpoolId?: PublicKey) {
const sizeFilter: DataSizeFilter = {
dataSize: TICK_ARRAY_LAYOUT.span,
};
let filters: GetProgramAccountsFilter[] = [sizeFilter];
if (whirlpoolId) {
filters.push({ memcmp: { offset: TICK_ARRAY_LAYOUT.span - 32, bytes: whirlpoolId.toBase58() } });
}
const config: GetProgramAccountsConfig = { filters: filters };
let tickArrayInfos = await connection.getProgramAccounts(ORCA_WHIRLPOOLS_PROGRAM_ID, config);
let tickArraysMap = new Map<string, types.TickArray[]>();
let ticksMaps = new Map<string, types.Tick[]>();
for (let tickArrayInfo of tickArrayInfos) {
let tickArray = TICK_ARRAY_LAYOUT.decode(tickArrayInfo.account.data) as types.TickArray;
if (tickArraysMap.has(tickArray.whirlpool.toString())) {
let array = tickArraysMap.get(tickArray.whirlpool.toString())!;
array.push(tickArray);
} else {
tickArraysMap.set(tickArray.whirlpool.toString(), [tickArray]);
}
}
tickArraysMap.forEach((tickArrays, whirlpoolId) => {
let ticks: types.Tick[] = [];
tickArrays.sort((a, b) => a.startTickIndex - b.startTickIndex);
tickArrays.forEach((tickArray) => {
for (let i = 0; i < tickArray.ticks.length; i++) {
let tick = tickArray.ticks[i];
tick.index = i + tickArray.startTickIndex;
if (tick) {
ticks.push(tick);
}
}
});
ticksMaps.set(whirlpoolId, ticks);
});
return ticksMaps;
}

export { infos };
81 changes: 81 additions & 0 deletions src/whirlpools/layouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { publicKey, u8, u64, u128, u16, i32, bool, i128, str } from "@project-serum/borsh";
//@ts-ignore
import { blob, seq, struct } from "buffer-layout";

export const CONFIG_LAYOUT = struct([
blob(8, "discriminator"),
publicKey("feeAuthority"),
publicKey("collectProtocolFeesAuthority"),
publicKey("rewardEmissionsSuperAuthority"),
u16("defaultProtocolFeeRate"),
]);
export const REWARD_LAYOUT = [
publicKey("mint"),
publicKey("vault"),
publicKey("authority"),
u128("emissionsPerSecondX64"),
u128("growthGlobalX64"),
];
export const POSITION_REWARD_LAYOUT = [u128("growthInsideCheckpoint"), u64("amountOwed")];

export const FEE_LAYOUT = struct([publicKey("whirlpoolsConfig"), u16("tickSpacing"), u16("defaultFeeRate")]);

export const POSITION_LAYOUT = struct([
blob(8, "discriminator"),
publicKey("whirlpool"),
publicKey("mint"),
u128("liquidity"),
i32("tickLowerIndex"),
i32("tickUpperIndex"),
u128("feeGrowthCheckpointA"),
u64("feeOwedA"),
u128("feeGrowthCheckpointB"),
u64("feeOwedB"),
struct(POSITION_REWARD_LAYOUT, "rewardInfo0"),
struct(POSITION_REWARD_LAYOUT, "rewardInfo1"),
struct(POSITION_REWARD_LAYOUT, "rewardInfo2"),
]);

export const WHIRLPOOL_LAYOUT = struct([
blob(8, "discriminator"),
publicKey("whirlpoolsConfig"),
u8("whirlpoolBump"),
u16("tickSpacing"),
u8("tickSpacingSeed0"),
u8("tickSpacingSeed1"),
u16("feeRate"),
u16("protocolFeeRate"),
u128("liquidity"),
u128("sqrtPrice"),
i32("tickCurrentIndex"),
u64("protocolFeeOwedA"),
u64("protocolFeeOwedB"),
publicKey("tokenMintA"),
publicKey("tokenVaultA"),
u128("feeGrowthGlobalA"),
publicKey("tokenMintB"),
publicKey("tokenVaultB"),
u128("feeGrowthGlobalB"),
u64("rewardLastUpdatedTimestamp"),
struct(REWARD_LAYOUT, "reward0"),
struct(REWARD_LAYOUT, "reward1"),
struct(REWARD_LAYOUT, "reward2"),
]);

export const TICK_LAYOUT = struct([
bool("initialized"),
i128("liquidityNet"),
u128("liquidityGross"),
u128("feeGrowthOutsideA"),
u128("feeGrowthOutsideB"),
u128("rewardGrowthsOutside0"),
u128("rewardGrowthsOutside1"),
u128("rewardGrowthsOutside2"),
]);

export const TICK_ARRAY_LAYOUT = struct([
blob(8, "discriminator"),
i32("startTickIndex"),
seq(TICK_LAYOUT, 88, "ticks"),
publicKey("whirlpool"),
]);
Loading