@kamino-finance/kliquidity-sdk
Version:
Typescript SDK for interacting with the Kamino Liquidity (kliquidity) protocol
295 lines (263 loc) • 10.5 kB
text/typescript
import { Address, Instruction, isSome, Option, TransactionSigner } from '@solana/kit';
import { readU128LE, writeU128LE } from './bytes';
import Decimal from 'decimal.js';
import {
DriftDirection,
RebalanceAutodriftStep,
RebalanceType,
StakingRateSource,
StrategyConfigOption,
} from '../@codegen/kliquidity/types';
import { getUpdateStrategyConfigInstruction } from '../@codegen/kliquidity/instructions';
import { RebalanceFieldInfo, RebalanceFieldsDict } from './types';
import { fromBN, toBN } from './raydiumBridge';
import { PoolPriceReferenceType, TwapPriceReferenceType } from './priceReferenceTypes';
import { U64_MAX } from '../constants/numericalValues';
import { RaydiumSqrtPriceMath as SqrtPriceMath } from './raydiumSdkCompat';
import { DEFAULT_PUBLIC_KEY } from '../constants/pubkeys';
import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { sqrtPriceToPrice as orcaSqrtPriceToPrice } from '@orca-so/whirlpools-core';
export const DollarBasedMintingMethod = new Decimal(0);
export const ProportionalMintingMethod = new Decimal(1);
export const RebalanceParamOffset = new Decimal(256);
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const Dex = ['ORCA', 'RAYDIUM', 'METEORA'] as const;
export type Dex = (typeof Dex)[number];
export const ReferencePriceType = [PoolPriceReferenceType, TwapPriceReferenceType] as const;
export type ReferencePriceType = (typeof ReferencePriceType)[number];
export function dexToNumber(dex: Dex): number {
for (let i = 0; i < Dex.length; i++) {
if (Dex[i] === dex) {
return i;
}
}
throw new Error(`Unknown DEX ${dex}`);
}
export function numberToDex(num: number): Dex {
const dex = Dex[num];
if (!dex) {
throw new Error(`Unknown DEX ${num}`);
}
return dex;
}
export function numberToReferencePriceType(num: number): ReferencePriceType {
const referencePriceType = ReferencePriceType[num];
if (!referencePriceType) {
throw new Error(`Strategy has invalid reference price type set: ${num}`);
}
return referencePriceType;
}
export function getStrategyConfigValue(value: Decimal): number[] {
const buf = new Uint8Array(128);
const dv = new DataView(buf.buffer);
dv.setBigUint64(0, BigInt(value.toString()), true);
return [...buf];
}
export function buildStrategyRebalanceParams(
params: Array<Decimal>,
rebalance_type: RebalanceType,
tokenADecimals?: number,
tokenBDecimals?: number
): number[] {
const buf = new Uint8Array(128);
const dv = new DataView(buf.buffer);
if (rebalance_type === RebalanceType.Manual) {
// Manual has no params
} else if (rebalance_type === RebalanceType.PricePercentage) {
dv.setUint16(0, params[0].toNumber(), true);
dv.setUint16(2, params[1].toNumber(), true);
} else if (rebalance_type === RebalanceType.PricePercentageWithReset) {
dv.setUint16(0, params[0].toNumber(), true);
dv.setUint16(2, params[1].toNumber(), true);
dv.setUint16(4, params[2].toNumber(), true);
dv.setUint16(6, params[3].toNumber(), true);
} else if (rebalance_type === RebalanceType.Drift) {
dv.setInt32(0, params[0].toNumber(), true);
dv.setInt32(4, params[1].toNumber(), true);
dv.setInt32(8, params[2].toNumber(), true);
dv.setBigUint64(12, BigInt(params[3].toString()), true);
dv.setUint8(20, params[4].toNumber());
} else if (rebalance_type === RebalanceType.TakeProfit) {
// TODO: fix this for meteora
const lowerPrice = SqrtPriceMath.priceToSqrtPriceX64(params[0], tokenADecimals!, tokenBDecimals!);
const upperPrice = SqrtPriceMath.priceToSqrtPriceX64(params[1], tokenADecimals!, tokenBDecimals!);
writeU128LE(dv, 0, fromBN(lowerPrice));
writeU128LE(dv, 16, fromBN(upperPrice));
dv.setUint8(32, params[2].toNumber());
} else if (rebalance_type === RebalanceType.PeriodicRebalance) {
dv.setBigUint64(0, BigInt(params[0].toString()), true);
dv.setUint16(8, params[1].toNumber(), true);
dv.setUint16(10, params[2].toNumber(), true);
} else if (rebalance_type === RebalanceType.Expander) {
dv.setUint16(0, params[0].toNumber(), true);
dv.setUint16(2, params[1].toNumber(), true);
dv.setUint16(4, params[2].toNumber(), true);
dv.setUint16(6, params[3].toNumber(), true);
dv.setUint16(8, params[4].toNumber(), true);
dv.setUint16(10, params[5].toNumber(), true);
dv.setUint8(12, params[6].toNumber());
} else if (rebalance_type === RebalanceType.Autodrift) {
dv.setUint32(0, params[0].toNumber(), true);
dv.setInt32(4, params[1].toNumber(), true);
dv.setInt32(8, params[2].toNumber(), true);
dv.setUint16(12, params[3].toNumber(), true);
dv.setUint8(14, params[4].toNumber());
dv.setUint8(15, params[5].toNumber());
dv.setUint8(16, params[6].toNumber());
} else {
throw 'Rebalance type not valid ' + rebalance_type;
}
return [...buf];
}
export function doesStrategyHaveResetRange(rebalanceTypeNumber: number): boolean {
const rebalanceType = numberToRebalanceType(rebalanceTypeNumber);
return rebalanceType === RebalanceType.PricePercentageWithReset || rebalanceType === RebalanceType.Expander;
}
export function numberToDriftDirection(value: number): DriftDirection {
if (value == 0) {
return DriftDirection.Increasing;
} else if (value == 1) {
return DriftDirection.Decreasing;
} else {
throw new Error(`Invalid drift direction ${value.toString()}`);
}
}
export function numberToStakingRateSource(value: number): StakingRateSource {
if (value == 0) {
return StakingRateSource.Constant;
} else if (value == 1) {
return StakingRateSource.Scope;
} else {
throw new Error(`Invalid staking rate source ${value.toString()}`);
}
}
export function numberToAutodriftStep(value: number): RebalanceAutodriftStep {
if (value == 0) {
return RebalanceAutodriftStep.Uninitialized;
} else if (value == 1) {
return RebalanceAutodriftStep.Autodrifting;
} else {
throw new Error(`Invalid autodrift step ${value.toString()}`);
}
}
export function numberToRebalanceType(rebalance_type: number): RebalanceType {
if (rebalance_type == 0) {
return RebalanceType.Manual;
} else if (rebalance_type == 1) {
return RebalanceType.PricePercentage;
} else if (rebalance_type == 2) {
return RebalanceType.PricePercentageWithReset;
} else if (rebalance_type == 3) {
return RebalanceType.Drift;
} else if (rebalance_type == 4) {
return RebalanceType.TakeProfit;
} else if (rebalance_type == 5) {
return RebalanceType.PeriodicRebalance;
} else if (rebalance_type == 6) {
return RebalanceType.Expander;
} else if (rebalance_type == 7) {
return RebalanceType.Autodrift;
} else {
throw new Error(`Invalid rebalance type ${rebalance_type.toString()}`);
}
}
export async function getUpdateStrategyConfigIx(
signer: TransactionSigner,
globalConfig: Address,
strategy: Address,
mode: StrategyConfigOption,
amount: Decimal,
programId: Address,
newAccount: Address = DEFAULT_PUBLIC_KEY
): Promise<Instruction> {
return getUpdateStrategyConfigInstruction(
{
adminAuthority: signer,
newAccount,
globalConfig,
strategy,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
mode,
value: new Uint8Array(getStrategyConfigValue(amount)),
},
{ programAddress: programId }
);
}
export function collToLamportsDecimal(amount: Decimal, decimals: number): Decimal {
const factor = new Decimal(10).pow(decimals);
return amount.mul(factor);
}
export function lamportsToNumberDecimal(amount: Decimal.Value, decimals: number): Decimal {
const factor = new Decimal(10).pow(decimals);
return new Decimal(amount).div(factor);
}
export function readBigUint128LE(buf: Uint8Array, offset: number): bigint {
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
return readU128LE(dv, offset);
}
export function readPriceOption(buf: Uint8Array, offset: number): [number, Decimal] {
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
if (dv.getUint8(offset) == 0) {
return [offset + 1, new Decimal(0)];
}
const value = dv.getBigUint64(offset + 1, true);
const exp = dv.getBigUint64(offset + 9, true);
return [offset + 17, new Decimal(value.toString()).div(new Decimal(10).pow(exp.toString()))];
}
export function rebalanceFieldsDictToInfo(rebalanceFields: RebalanceFieldsDict): RebalanceFieldInfo[] {
const rebalanceFieldsInfo: RebalanceFieldInfo[] = [];
for (const key in rebalanceFields) {
const value = rebalanceFields[key];
rebalanceFieldsInfo.push({
label: key,
type: 'number',
value: value,
enabled: false,
});
}
return rebalanceFieldsInfo;
}
export function isVaultInitialized(vault: Address, decimals: bigint): boolean {
return vault !== DEFAULT_PUBLIC_KEY && decimals > 0n;
}
export function sqrtPriceToPrice(sqrtPrice: bigint, dexNo: number, decimalsA: number, decimalsB: number): Decimal {
const dex = numberToDex(dexNo);
if (dex == 'ORCA') {
return new Decimal(orcaSqrtPriceToPrice(sqrtPrice, decimalsA, decimalsB));
}
if (dex == 'RAYDIUM') {
return SqrtPriceMath.sqrtPriceX64ToPrice(toBN(sqrtPrice), decimalsA, decimalsB);
}
if (dex == 'METEORA') {
const price = new Decimal(sqrtPrice.toString());
return price.div(new Decimal(U64_MAX));
}
throw new Error(`Got invalid dex number ${dex}`);
}
// Zero is not a valid TWAP component as that indicates the SOL price
export function stripTwapZeros(chain: number[]): number[] {
return chain.filter((component) => component > 0);
}
export function percentageToBPS(pct: number): number {
return pct * 100;
}
export function keyOrDefault(key: Address, defaultKey: Address): Address {
if (key === DEFAULT_PUBLIC_KEY) {
return defaultKey;
}
return key;
}
// Extract value from Option if Some, otherwise return null
export function optionGetValue<T>(option: Option<T>): T | null {
return isSome(option) ? (option as any).value : null;
}
// Extract value from Option if Some, otherwise return undefined
export function optionGetValueOrUndefined<T>(option: Option<T>): T | undefined {
return isSome(option) ? (option as any).value : undefined;
}
// Extract value from Option if Some, otherwise return provided default
export function optionGetValueOrDefault<T>(option: Option<T>, defaultValue: T): T {
return isSome(option) ? (option as any).value : defaultValue;
}