import Decimal from 'decimal.js'; import BN from 'bn.js'; import { AccountInfo, PublicKey } from '@solana/web3.js'; import { HUNDRED_PERCENT_BPS } from '../../constants'; import { OracleSettings, Quote, Side } from '../../layouts/oracle'; import { U128_MAX } from './constants'; import { OraclePrice } from './oracle'; const CLMM_PROGRAM_ID = new PublicKey("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"); const TICK_ARRAY_SEED = Buffer.from("tick_array"); const MIN_TICK = -443636; const MAX_TICK = 443636; export function i32ToBytes(num: number): Uint8Array { const arr = new ArrayBuffer(4); const view = new DataView(arr); view.setInt32(0, num, false); return new Uint8Array(arr); } export function getTickArrayStartIndexByTick(tickIndex: number, tickSpacing: number): number { const perArray = TICK_ARRAY_SIZE_USIZE * tickSpacing; return Math.floor(tickIndex / perArray) * perArray; } export function getNextTickArrayStartIndex(lastStart: number, tickSpacing: number, zeroForOne: boolean): number { return zeroForOne ? lastStart - tickSpacing * TICK_ARRAY_SIZE_USIZE : lastStart + tickSpacing * TICK_ARRAY_SIZE_USIZE; } export function getPdaTickArrayAddress(poolId: PublicKey, startIndex: number): PublicKey { return PublicKey.findProgramAddressSync( [TICK_ARRAY_SEED, poolId.toBuffer(), i32ToBytes(startIndex)], CLMM_PROGRAM_ID )[0]; } class Observation { blockTimestamp: BN; tickCumulative: BN; padding: BN[]; constructor(params: {blockTimestamp: BN, tickCumulative: BN, padding: BN[]}){ this.blockTimestamp = params.blockTimestamp; this.tickCumulative = params.tickCumulative; this.padding = params.padding; } static decode(data: Buffer, offset = 0): [Observation, number] { const blockTimestamp = new BN(data.readUInt32LE(offset)); const tickCumulative = new BN( data.subarray(offset + 4, offset + 12), "le" ).fromTwos(64); const padding: BN[] = []; let cursor = offset + 12; for (let i = 0; i < 4; i++) { padding.push(new BN(data.subarray(cursor, cursor + 8), "le")); cursor += 8; } return [ new Observation({ blockTimestamp, tickCumulative, padding }), cursor ]; } sub(other: Observation): Observation { if (!(other instanceof Observation)) { throw new TypeError("Subtraction is only supported between Observation instances"); } return new Observation({ blockTimestamp: this.blockTimestamp.sub(other.blockTimestamp), tickCumulative: this.tickCumulative.sub(other.tickCumulative), padding: this.padding }); } add(other: Observation): Observation { if (!(other instanceof Observation)) { throw new TypeError("Addition is only supported between Observation instances"); } return new Observation({ blockTimestamp: this.blockTimestamp.add(other.blockTimestamp), tickCumulative: this.tickCumulative.add(other.tickCumulative), padding: this.padding }); } getWeightedObservation(time: BN): Observation { // cumT = (self.tickCumulative * time) / self.blockTimestamp const cum = this.tickCumulative.mul(time).div(this.blockTimestamp); return new Observation({ blockTimestamp: time, tickCumulative: cum, padding: this.padding }); } adjustToTimestamp(targetTimestamp: BN, observationPrev: Observation): Observation { // delta = self.sub(prevObservation) const delta = this.sub(observationPrev); // weightedDelta = delta.getWeightedObservation(targetTimestamp - self.blockTimestamp) const timeForWeighted = targetTimestamp.sub(this.blockTimestamp); const weightedDelta = delta.getWeightedObservation(timeForWeighted); // return self.add(weightedDelta) return this.add(weightedDelta); } } class ObservationState { initialized: boolean; recentEpoch: BN; observationIndex: number; poolId: PublicKey; observations: Observation[]; padding: BN[]; constructor(params: { initialized: boolean, recentEpoch: BN, observationIndex: number, poolId: PublicKey, observations: Observation[], padding: BN[] }){ this.initialized = params.initialized; this.recentEpoch = params.recentEpoch; this.observationIndex = params.observationIndex; this.poolId = params.poolId; this.observations = params.observations; this.padding = params.padding; } static decode(data: Buffer, offset = 8): [ObservationState, number] { let cursor = offset; const initialized = data.readUInt8(cursor) !== 0; cursor += 1; const recentEpoch = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const observationIndex = data.readUInt16LE(cursor); cursor += 2; const poolId = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const observations: Observation[] = []; for (let i = 0; i < 100; i++) { let obs: Observation; [obs, cursor] = Observation.decode(data, cursor); observations.push(obs); } const padding: BN[] = []; for (let i = 0; i < 4; i++) { padding.push(new BN(data.subarray(cursor, cursor + 8), "le")); cursor += 8; } return [ new ObservationState({ initialized, recentEpoch, observationIndex, poolId, observations, padding }), cursor ]; } } class RewardInfo { rewardState: number; openTime: BN; endTime: BN; lastUpdateTime: BN; emissionsPerSecondX64: BN; rewardTotalEmissioned: BN; rewardClaimed: BN; tokenMint: PublicKey; tokenVault: PublicKey; authority: PublicKey; rewardGrowthGlobalX64: BN; constructor(params: { rewardState: number, openTime: BN, endTime: BN, lastUpdateTime: BN, emissionsPerSecondX64: BN, rewardTotalEmissioned: BN, rewardClaimed: BN, tokenMint: PublicKey, tokenVault: PublicKey, authority: PublicKey, rewardGrowthGlobalX64: BN }){ this.rewardState = params.rewardState; this.openTime = params.openTime; this.endTime = params.endTime; this.lastUpdateTime = params.lastUpdateTime; this.emissionsPerSecondX64 = params.emissionsPerSecondX64; this.rewardTotalEmissioned = params.rewardTotalEmissioned; this.rewardClaimed = params.rewardClaimed; this.tokenMint = params.tokenMint; this.tokenVault = params.tokenVault; this.authority = params.authority; this.rewardGrowthGlobalX64 = params.rewardGrowthGlobalX64; } static decode(data: Buffer, offset: number = 0): [RewardInfo, number] { let cursor = offset; const rewardState = data.readUInt8(cursor); cursor += 1; const openTime = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const endTime = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const lastUpdateTime = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const emissionsPerSecondX64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const rewardTotalEmissioned = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const rewardClaimed = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const tokenMint = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const tokenVault = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const authority = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const rewardGrowthGlobalX64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; return [ new RewardInfo({ rewardState, openTime, endTime, lastUpdateTime, emissionsPerSecondX64, rewardTotalEmissioned, rewardClaimed, tokenMint, tokenVault, authority, rewardGrowthGlobalX64 }), cursor]; } } export class PoolState { bump: number[]; ammConfig: PublicKey; owner: PublicKey; tokenMint0: PublicKey; tokenMint1: PublicKey; tokenVault0: PublicKey; tokenVault1: PublicKey; observationKey: PublicKey; mintDecimals0: number; mintDecimals1: number; tickSpacing: number; liquidity: BN; sqrtPriceX64: BN; tickCurrent: number; padding3: number; padding4: number; feeGrowthGlobal0X64: BN; feeGrowthGlobal1X64: BN; protocolFeesToken0: BN; protocolFeesToken1: BN; swapInAmountToken0: BN; swapOutAmountToken1: BN; swapInAmountToken1: BN; swapOutAmountToken0: BN; status: number; padding: number[]; rewardInfos: RewardInfo[]; tickArrayBitmap: BN[]; totalFeesToken0: BN; totalFeesClaimedToken0: BN; totalFeesToken1: BN; totalFeesClaimedToken1: BN; fundFeesToken0: BN; fundFeesToken1: BN; openTime: BN; recentEpoch: BN; padding1: BN[]; padding2: BN[]; constructor( params: { bump: number[], ammConfig: PublicKey, owner: PublicKey, tokenMint0: PublicKey, tokenMint1: PublicKey, tokenVault0: PublicKey, tokenVault1: PublicKey, observationKey: PublicKey, mintDecimals0: number, mintDecimals1: number, tickSpacing: number, liquidity: BN, sqrtPriceX64: BN, tickCurrent: number, padding3: number, padding4: number, feeGrowthGlobal0X64: BN, feeGrowthGlobal1X64: BN, protocolFeesToken0: BN, protocolFeesToken1: BN, swapInAmountToken0: BN, swapOutAmountToken1: BN, swapInAmountToken1: BN, swapOutAmountToken0: BN, status: number, padding: number[], rewardInfos: RewardInfo[], tickArrayBitmap: BN[], totalFeesToken0: BN, totalFeesClaimedToken0: BN, totalFeesToken1: BN, totalFeesClaimedToken1: BN, fundFeesToken0: BN, fundFeesToken1: BN, openTime: BN, recentEpoch: BN, padding1: BN[], padding2: BN[] } ){ this.bump = params.bump; this.ammConfig = params.ammConfig; this.owner = params.owner; this.tokenMint0 = params.tokenMint0; this.tokenMint1 = params.tokenMint1; this.tokenVault0 = params.tokenVault0; this.tokenVault1 = params.tokenVault1; this.observationKey = params.observationKey; this.mintDecimals0 = params.mintDecimals0; this.mintDecimals1 = params.mintDecimals1; this.tickSpacing = params.tickSpacing; this.liquidity = params.liquidity; this.sqrtPriceX64 = params.sqrtPriceX64; this.tickCurrent = params.tickCurrent; this.padding3 = params.padding3; this.padding4 = params.padding4; this.feeGrowthGlobal0X64 = params.feeGrowthGlobal0X64; this.feeGrowthGlobal1X64 = params.feeGrowthGlobal1X64; this.protocolFeesToken0 = params.protocolFeesToken0; this.protocolFeesToken1 = params.protocolFeesToken1; this.swapInAmountToken0 = params.swapInAmountToken0; this.swapOutAmountToken1 = params.swapOutAmountToken1; this.swapInAmountToken1 = params.swapInAmountToken1; this.swapOutAmountToken0 = params.swapOutAmountToken0; this.status = params.status; this.padding = params.padding; this.rewardInfos = params.rewardInfos; this.tickArrayBitmap = params.tickArrayBitmap; this.totalFeesToken0 = params.totalFeesToken0; this.totalFeesClaimedToken0 = params.totalFeesClaimedToken0; this.totalFeesToken1 = params.totalFeesToken1; this.totalFeesClaimedToken1 = params.totalFeesClaimedToken1; this.fundFeesToken0 = params.fundFeesToken0; this.fundFeesToken1 = params.fundFeesToken1; this.openTime = params.openTime; this.recentEpoch = params.recentEpoch; this.padding1 = params.padding1; this.padding2 = params.padding2; } static decode(data: Buffer, offset: number = 8): [PoolState, number] { let cursor = offset; const bump = [data.readUInt8(cursor)]; cursor += 1; const ammConfig = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const owner = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const tokenMint0 = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const tokenMint1 = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const tokenVault0 = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const tokenVault1 = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const observationKey = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const mintDecimals0 = data.readUInt8(cursor); cursor += 1; const mintDecimals1 = data.readUInt8(cursor); cursor += 1; const tickSpacing = data.readUInt16LE(cursor); cursor += 2; const liquidity = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const sqrtPriceX64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const tickCurrent = data.readInt32LE(cursor); cursor += 4; const padding3 = data.readUInt16LE(cursor); cursor += 2; const padding4 = data.readUInt16LE(cursor); cursor += 2; const feeGrowthGlobal0X64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const feeGrowthGlobal1X64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const protocolFeesToken0 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const protocolFeesToken1 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const swapInAmountToken0 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const swapOutAmountToken1 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const swapInAmountToken1 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const swapOutAmountToken0 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const status = data.readUInt8(cursor); cursor += 1; const padding: number[] = []; for (let i = 0; i < 7; i++) { padding.push(data.readUInt8(cursor)); cursor += 1; } // RewardInfos const rewardInfos: RewardInfo[] = []; for (let i = 0; i < 3; i++) { const decoded = RewardInfo.decode(data, cursor); rewardInfos.push(decoded[0]); cursor = decoded[1]; } // tickArrayBitmap [u64;16] const tickArrayBitmap: BN[] = []; for (let i = 0; i < 16; i++) { tickArrayBitmap.push(new BN(data.subarray(cursor, cursor + 8), "le")); cursor += 8; } const totalFeesToken0 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const totalFeesClaimedToken0 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const totalFeesToken1 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const totalFeesClaimedToken1 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const fundFeesToken0 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const fundFeesToken1 = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const openTime = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const recentEpoch = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const padding1: BN[] = []; for (let i = 0; i < 24; i++) { padding1.push(new BN(data.subarray(cursor, cursor + 8), "le")); cursor += 8; } const padding2: BN[] = []; for (let i = 0; i < 32; i++) { padding2.push(new BN(data.subarray(cursor, cursor + 8), "le")); cursor += 8; } return [ new PoolState({ bump, ammConfig, owner, tokenMint0, tokenMint1, tokenVault0, tokenVault1, observationKey, mintDecimals0, mintDecimals1, tickSpacing, liquidity, sqrtPriceX64, tickCurrent, padding3, padding4, feeGrowthGlobal0X64, feeGrowthGlobal1X64, protocolFeesToken0, protocolFeesToken1, swapInAmountToken0, swapOutAmountToken1, swapInAmountToken1, swapOutAmountToken0, status, padding, rewardInfos, tickArrayBitmap, totalFeesToken0, totalFeesClaimedToken0, totalFeesToken1, totalFeesClaimedToken1, fundFeesToken0, fundFeesToken1, openTime, recentEpoch, padding1, padding2, }), cursor, ]; } } const TICK_ARRAY_SIZE_USIZE = 60; const REWARD_NUM = 3; class TickState { tick: number; // i32 liquidityNet: BN; // i128 (signed) liquidityGross: BN; // u128 feeGrowthOutside0X64: BN; // u128 feeGrowthOutside1X64: BN; // u128 rewardGrowthsOutsideX64: BN[]; // [u128; REWARD_NUM] padding: number[]; // [u32;13] constructor(params: { tick: number, liquidityNet: BN, liquidityGross: BN, feeGrowthOutside0X64: BN, feeGrowthOutside1X64: BN, rewardGrowthsOutsideX64: BN[], padding: number[] }){ this.tick = params.tick; this.liquidityNet = params.liquidityNet; this.liquidityGross = params.liquidityGross; this.feeGrowthOutside0X64 = params.feeGrowthOutside0X64; this.feeGrowthOutside1X64 = params.feeGrowthOutside1X64; this.rewardGrowthsOutsideX64 = params.rewardGrowthsOutsideX64; this.padding = params.padding; } static decode(data: Buffer, offset: number = 0): [TickState, number] { let cursor = offset; const tick = data.readInt32LE(cursor); cursor += 4; // liquidity_net: i128 (signed) const liquidityNet = new BN(data.subarray(cursor, cursor + 16), "le").fromTwos(128); cursor += 16; // liquidity_gross: u128 const liquidityGross = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const feeGrowthOutside0X64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const feeGrowthOutside1X64 = new BN(data.subarray(cursor, cursor + 16), "le"); cursor += 16; const rewardGrowthsOutsideX64: BN[] = []; for (let i = 0; i < REWARD_NUM; i++) { rewardGrowthsOutsideX64.push(new BN(data.subarray(cursor, cursor + 16), "le")); cursor += 16; } const padding: number[] = []; for (let i = 0; i < 13; i++) { padding.push(data.readUInt32LE(cursor)); cursor += 4; } return [ new TickState({ tick, liquidityNet, liquidityGross, feeGrowthOutside0X64, feeGrowthOutside1X64, rewardGrowthsOutsideX64, padding }), cursor ]; } } class TickArrayState { poolId: PublicKey; startTickIndex: number; // i32 ticks: TickState[]; // length TICK_ARRAY_SIZE_USIZE initializedTickCount: number; // u8 recentEpoch: BN; // u64 padding: number[]; // u8[107] constructor(params: { poolId: PublicKey, startTickIndex: number, ticks: TickState[], initializedTickCount: number, recentEpoch: BN, padding: number[] }){ this.poolId = params.poolId; this.startTickIndex = params.startTickIndex; this.ticks = params.ticks; this.initializedTickCount = params.initializedTickCount; this.recentEpoch = params.recentEpoch; this.padding = params.padding; } static decode(data: Buffer, offset: number = 8): [TickArrayState, number] { let cursor = offset; const poolId = new PublicKey(data.subarray(cursor, cursor + 32)); cursor += 32; const startTickIndex = data.readInt32LE(cursor); cursor += 4; const ticks: TickState[] = []; for (let i = 0; i < TICK_ARRAY_SIZE_USIZE; i++) { const [tickState, next] = TickState.decode(data, cursor); ticks.push(tickState); cursor = next; } const initializedTickCount = data.readUInt8(cursor); cursor += 1; const recentEpoch = new BN(data.subarray(cursor, cursor + 8), "le"); cursor += 8; const padding: number[] = []; for (let i = 0; i < 107; i++) { padding.push(data.readUInt8(cursor)); cursor += 1; } return [ new TickArrayState({ poolId, startTickIndex, ticks, initializedTickCount, recentEpoch, padding }), cursor ]; } } export class RaydiumCLMMOracle { static deriveObservationKey(poolId: PublicKey): [PublicKey, number] { const seeds = [ Buffer.from("observation"), poolId.toBuffer(), ]; return PublicKey.findProgramAddressSync(seeds, CLMM_PROGRAM_ID); }; static getObservationAtIndex(observations: Observation[], index: number) { return observations[index]; } static getObservationAtTimestamp(timestamp: BN, observations: Observation[], startIndex: number): Observation { let observationCurrent = this.getObservationAtIndex(observations, startIndex); let index = startIndex; // const obsIndex = this.observationState.observationIndex; // Loop backwards until we find the observation <= timestamp while (observationCurrent.blockTimestamp.gt(timestamp)) { index = (index - 1 + 100) % 100; observationCurrent = this.getObservationAtIndex(observations, index); if (index === startIndex) { throw Error("Observations do not go back far enough in time"); break; // looped all the way around } } // Previous index for interpolation const prevIndex = (index - 1 + 100) % 100; const observationPrev = this.getObservationAtIndex(observations, prevIndex); // Adjust current observation to the target timestamp const result = observationCurrent.adjustToTimestamp(timestamp, observationPrev); return result; } static getDeltaObservations(currentTime: BN, observations: Observation[], observationIndex: number, primarySeconds: BN, secondarySeconds: BN): Observation[] { const observationCurrent = this.getObservationAtTimestamp(currentTime, observations, observationIndex); const observationPrimary = this.getObservationAtTimestamp(new BN(currentTime.sub(primarySeconds)), observations, observationIndex); const deltaPrimary = observationCurrent.sub(observationPrimary); const observationSecondary = this.getObservationAtTimestamp(new BN(currentTime.sub(secondarySeconds)), observations, observationIndex); const deltaSecondary = observationCurrent.sub(observationSecondary); return [deltaPrimary,deltaSecondary]; } static getSqrtPriceX64FromTick(tick: number): BN { if (!Number.isInteger(tick)) { throw new Error("tick must be integer"); } if (tick < MIN_TICK || tick > MAX_TICK) { throw new Error("tick must be in MIN_TICK and MAX_TICK"); } const tickAbs: number = tick < 0 ? tick * -1 : tick; let ratio: BN = (tickAbs & 0x1) != 0 ? new BN("18445821805675395072") : new BN("18446744073709551616"); if ((tickAbs & 0x2) != 0) ratio = mulRightShift(ratio, new BN("18444899583751176192")); if ((tickAbs & 0x4) != 0) ratio = mulRightShift(ratio, new BN("18443055278223355904")); if ((tickAbs & 0x8) != 0) ratio = mulRightShift(ratio, new BN("18439367220385607680")); if ((tickAbs & 0x10) != 0) ratio = mulRightShift(ratio, new BN("18431993317065453568")); if ((tickAbs & 0x20) != 0) ratio = mulRightShift(ratio, new BN("18417254355718170624")); if ((tickAbs & 0x40) != 0) ratio = mulRightShift(ratio, new BN("18387811781193609216")); if ((tickAbs & 0x80) != 0) ratio = mulRightShift(ratio, new BN("18329067761203558400")); if ((tickAbs & 0x100) != 0) ratio = mulRightShift(ratio, new BN("18212142134806163456")); if ((tickAbs & 0x200) != 0) ratio = mulRightShift(ratio, new BN("17980523815641700352")); if ((tickAbs & 0x400) != 0) ratio = mulRightShift(ratio, new BN("17526086738831433728")); if ((tickAbs & 0x800) != 0) ratio = mulRightShift(ratio, new BN("16651378430235570176")); if ((tickAbs & 0x1000) != 0) ratio = mulRightShift(ratio, new BN("15030750278694412288")); if ((tickAbs & 0x2000) != 0) ratio = mulRightShift(ratio, new BN("12247334978884435968")); if ((tickAbs & 0x4000) != 0) ratio = mulRightShift(ratio, new BN("8131365268886854656")); if ((tickAbs & 0x8000) != 0) ratio = mulRightShift(ratio, new BN("3584323654725218816")); if ((tickAbs & 0x10000) != 0) ratio = mulRightShift(ratio, new BN("696457651848324352")); if ((tickAbs & 0x20000) != 0) ratio = mulRightShift(ratio, new BN("26294789957507116")); if ((tickAbs & 0x40000) != 0) ratio = mulRightShift(ratio, new BN("37481735321082")); if (tick > 0) ratio = U128_MAX.div(ratio); return ratio; } static sqrtPriceX64ToPrice(sqrtPriceX64: BN, decimalsA: number, decimalsB: number): Decimal { return this.x64ToDecimal(sqrtPriceX64) .pow(2) .mul(Decimal.pow(10, decimalsA - decimalsB)); } static x64ToDecimal(num: BN, decimalPlaces?: number): Decimal { return new Decimal(num.toString()).div(Decimal.pow(2, 64)).toDecimalPlaces(decimalPlaces); } static getSpotPrice(tickCurrent: number, mintDecimals0: number, mintDecimals1: number): Decimal { let price = this.sqrtPriceX64ToPrice(this.getSqrtPriceX64FromTick(tickCurrent), mintDecimals0, mintDecimals1); return price; } static getTwapPrimary( observations: Observation[], observationIndex: number, primarySeconds: BN, secondarySeconds: BN, mintDecimals0: number, mintDecimals1: number, currentTime: BN): Decimal { let observation = this.getDeltaObservations(currentTime, observations, observationIndex, primarySeconds, secondarySeconds)[0]; let tick = observation.tickCumulative.div(observation.blockTimestamp).toNumber(); let twap = this.sqrtPriceX64ToPrice(this.getSqrtPriceX64FromTick(tick), mintDecimals0, mintDecimals1); return twap; } static getTwapSecondary( observations: Observation[], observationIndex: number, primarySeconds: BN, secondarySeconds: BN, mintDecimals0: number, mintDecimals1: number, currentTime: BN): Decimal { let observation = this.getDeltaObservations(currentTime, observations, observationIndex, primarySeconds, secondarySeconds)[1]; let tick = observation.tickCumulative.div(observation.blockTimestamp).toNumber(); let twap = this.sqrtPriceX64ToPrice(this.getSqrtPriceX64FromTick(tick), mintDecimals0, mintDecimals1); return twap; } static getConfidence( observations: Observation[], observationIndex: number, primarySeconds: BN, secondarySeconds: BN, tickCurrent: number, mintDecimals0: number, mintDecimals1: number, currentTime: BN ): Decimal { let primary = this.getTwapPrimary( observations, observationIndex, primarySeconds, secondarySeconds, mintDecimals0, mintDecimals1, currentTime); let secondary = this.getTwapSecondary( observations, observationIndex, primarySeconds, secondarySeconds, mintDecimals0, mintDecimals1, currentTime); let spot = this.getSpotPrice(tickCurrent, mintDecimals0, mintDecimals1); let first = primary.sub(spot).abs(); let second = secondary.sub(spot).abs(); let third = primary.sub(secondary).abs(); let conf = Decimal.max(first, second, third); return conf; } static calculateMaxPriceImpactForMinLiquidity( oracleParams: OracleSettings, poolState: PoolState, prevTickArray: TickArrayState, currentTickArray: TickArrayState, nextTickArray: TickArrayState, solPrice: OraclePrice, usdPrice: OraclePrice, ): Decimal { // local helpers / aliases const sStart = this.x64ToDecimal(poolState.sqrtPriceX64); const Lstart = new Decimal(poolState.liquidity.toString()); const spotPrice = this.sqrtPriceX64ToPrice(poolState.sqrtPriceX64, poolState.mintDecimals0, poolState.mintDecimals1); const minLiquidityUsd = new Decimal(oracleParams.minLiquidity.toString()); let amountQuote: Decimal; if (oracleParams.quote === Quote.Usdc) { amountQuote = minLiquidityUsd.mul(new Decimal(10).pow(6)).div(usdPrice.price); } else { amountQuote = minLiquidityUsd.mul(new Decimal(10).pow(9)).div(solPrice.price); } const tickSpacing = poolState.tickSpacing; // Build fast lookup structures once: // - tickMap: absoluteTick -> TickState // - arrays of initialized tick indices per region (prev/current/next) const tickMap = new Map(); const prevIdxs: number[] = []; const currIdxs: number[] = []; const nextIdxs: number[] = []; const pushTicks = (ta: TickArrayState, outArr: number[]) => { const start = ta.startTickIndex; for (let i = 0; i < ta.ticks.length; i++) { const tickVal = start + i * tickSpacing; const t = ta.ticks[i]; // store in map regardless, to speed up findTickState tickMap.set(tickVal, t); if (t.liquidityGross.gt(new BN(0))) outArr.push(tickVal); } }; pushTicks(prevTickArray, prevIdxs); pushTicks(currentTickArray, currIdxs); pushTicks(nextTickArray, nextIdxs); // findTickState map lookup const findTickState = (idx: number | null): TickState | null => { if (idx === null) return null; return tickMap.get(idx) ?? null; }; // find next initialized tick index scanning only relevant arrays: // forward === true => check current then next // forward === false => check current then prev const findNextInitializedTickIndex = (startTick: number, forward: boolean): number | null => { const lists = forward ? [currIdxs, nextIdxs] : [currIdxs, prevIdxs]; let best: number | null = null; for (const list of lists) { // for forward: find smallest tick > startTick // for backward: find largest tick < startTick if (forward) { for (let i = 0; i < list.length; i++) { const v = list[i]; if (v > startTick) { best = best === null ? v : Math.min(best, v); // since list ascending we can break early for this list break; } } if (best !== null) break; // we already found closest in current list } else { // backward: list ascending, scan from end for speed for (let i = list.length - 1; i >= 0; i--) { const v = list[i]; if (v < startTick) { best = best === null ? v : Math.max(best, v); break; } } if (best !== null) break; } } return best; }; const sFor = (tickIdx: number | null) => tickIdx === null ? null : this.x64ToDecimal(this.getSqrtPriceX64FromTick(tickIdx)); // simulate quote -> base (s increases). returns avg quote-per-base or null const simulateIncrease = (amountQ: Decimal): Decimal | null => { let remaining = amountQ; let s = sStart; let L = Lstart; let totalBaseOut = new Decimal(0); let currentTick = poolState.tickCurrent; let boundary = findNextInitializedTickIndex(currentTick, true); let steps = 0; while (remaining.gt(0) && steps++ < 500) { const sBoundary = sFor(boundary); if (!sBoundary) { // no known boundary -> consume all in one segment const sEnd = s.add(remaining.div(L)); totalBaseOut = totalBaseOut.add(L.mul(new Decimal(1).div(s).sub(new Decimal(1).div(sEnd)))); remaining = new Decimal(0); break; } const deltaQToBoundary = L.mul(sBoundary.sub(s)); if (remaining.lte(deltaQToBoundary)) { const sEnd = s.add(remaining.div(L)); totalBaseOut = totalBaseOut.add(L.mul(new Decimal(1).div(s).sub(new Decimal(1).div(sEnd)))); remaining = new Decimal(0); break; } // consume whole segment to boundary totalBaseOut = totalBaseOut.add(L.mul(new Decimal(1).div(s).sub(new Decimal(1).div(sBoundary)))); remaining = remaining.sub(deltaQToBoundary); // cross tick: safe to assert boundary non-null because sBoundary existed const bt = boundary!; const ts = findTickState(bt); if (ts) L = L.add(new Decimal(ts.liquidityNet.toString())); s = sBoundary; currentTick = bt + tickSpacing; boundary = findNextInitializedTickIndex(currentTick, true); } if (totalBaseOut.lte(0)) return null; return amountQ.div(totalBaseOut); }; // simulate base -> quote (s decreases). amountQuote is converted to approximate amountBase via spot const simulateDecrease = (amountQ: Decimal): Decimal | null => { const amountBase = amountQ.div(spotPrice); let remainingBase = amountBase; let s = sStart; let L = Lstart; let totalQuoteOut = new Decimal(0); let currentTick = poolState.tickCurrent; let boundary = findNextInitializedTickIndex(currentTick, false); let steps = 0; while (remainingBase.gt(0) && steps++ < 500) { const sBoundary = sFor(boundary); if (!sBoundary) { const denom = L.add(remainingBase.mul(s)); if (denom.eq(0)) return null; const sEnd = L.mul(s).div(denom); totalQuoteOut = totalQuoteOut.add(L.mul(s.sub(sEnd))); remainingBase = new Decimal(0); break; } const deltaAtoBoundary = L.mul(new Decimal(1).div(sBoundary).sub(new Decimal(1).div(s))); if (remainingBase.lte(deltaAtoBoundary)) { const denom = L.add(remainingBase.mul(s)); const sEnd = L.mul(s).div(denom); totalQuoteOut = totalQuoteOut.add(L.mul(s.sub(sEnd))); remainingBase = new Decimal(0); break; } // consume to boundary const denom = L.add(deltaAtoBoundary.mul(s)); const sEnd = L.mul(s).div(denom); totalQuoteOut = totalQuoteOut.add(L.mul(s.sub(sEnd))); remainingBase = remainingBase.sub(deltaAtoBoundary); // cross tick going down const bt = boundary!; const ts = findTickState(bt); if (ts) L = L.sub(new Decimal(ts.liquidityNet.toString())); s = sEnd; currentTick = bt - tickSpacing; boundary = findNextInitializedTickIndex(currentTick, false); } if (totalQuoteOut.lte(0)) return null; if (amountBase.eq(0)) return null; return totalQuoteOut.div(amountBase); }; const up = simulateIncrease(amountQuote); const down = simulateDecrease(amountQuote); const toImpact = (avg: Decimal | null) => !avg || avg.lte(0) ? new Decimal(0) : avg.sub(spotPrice).abs().div(spotPrice).mul(new Decimal(10000)); return Decimal.max(toImpact(up), toImpact(down)); } static fetch( oracleParams: OracleSettings, accountInfos: AccountInfo[], solPrice: OraclePrice, usdPrice: OraclePrice, ): OraclePrice { const poolState = accountInfos[0]; const observationState = accountInfos[1]; const prevTickArrayState = accountInfos[2]; const currentTickArrayState = accountInfos[3]; const nextTickArrayState = accountInfos[4]; const [decodedPoolState, num] = PoolState.decode(poolState.data, 8); const [decodedObservationState, _] = ObservationState.decode(observationState.data, 8); const [decodedPrevTickArrayState, __] = TickArrayState.decode(prevTickArrayState.data, 8); const [decodedCurrentTickArrayState, ___] = TickArrayState.decode(currentTickArrayState.data, 8); const [decodedNextTickArrayState, ____] = TickArrayState.decode(nextTickArrayState.data, 8); const currentTime = new BN(Math.floor(Date.now() / 1000)); let primaryPrice = RaydiumCLMMOracle.getTwapPrimary( decodedObservationState.observations, decodedObservationState.observationIndex, oracleParams.twapSecondsAgo, oracleParams.twapSecondarySecondsAgo, decodedPoolState.mintDecimals0, decodedPoolState.mintDecimals1, currentTime); let secondaryPrice = RaydiumCLMMOracle.getTwapSecondary( decodedObservationState.observations, decodedObservationState.observationIndex, oracleParams.twapSecondsAgo, oracleParams.twapSecondarySecondsAgo, decodedPoolState.mintDecimals0, decodedPoolState.mintDecimals1, currentTime); let spotPrice = RaydiumCLMMOracle.getSpotPrice( decodedPoolState.tickCurrent, decodedPoolState.mintDecimals0, decodedPoolState.mintDecimals1); // Validate primary TWAP is not zero if (primaryPrice.eq(0)) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } // === 1. Min liquidity and price impact validation (FIRST) === if (oracleParams.minLiquidity.gt(new BN(0))) { const impactBps = RaydiumCLMMOracle.calculateMaxPriceImpactForMinLiquidity( oracleParams, decodedPoolState, decodedPrevTickArrayState, decodedCurrentTickArrayState, decodedNextTickArrayState, solPrice, usdPrice ); if (oracleParams.maxSlippageBps > 0 && impactBps.gt(new Decimal(oracleParams.maxSlippageBps))) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } } // === 2. Confidence calculation === let maxPrice = Decimal.max(primaryPrice, secondaryPrice, spotPrice); let minPrice = Decimal.min(primaryPrice, secondaryPrice, spotPrice); let confidence = maxPrice.gt(minPrice) ? maxPrice.sub(minPrice) : new Decimal(0); const lastUpdateTimestamp = decodedObservationState.observations[decodedObservationState.observationIndex].blockTimestamp; // === 3. Inflate confidence by staleness === const deltaSecondsBN = currentTime.sub(lastUpdateTimestamp); const deltaSeconds = new Decimal(deltaSecondsBN.toString()); if (oracleParams.stalenessConfRateBps > 0 && deltaSeconds.gt(0)) { const stalenessRate = new Decimal(oracleParams.stalenessConfRateBps).div(new Decimal(10000)); const inflateFactor = new Decimal(1).add(deltaSeconds.mul(stalenessRate)); confidence = confidence.mul(inflateFactor); } // === 4. Validate confidence threshold === const confRatioBps = confidence.div(primaryPrice).mul(new Decimal(HUNDRED_PERCENT_BPS)); if (confRatioBps.gt(new Decimal(oracleParams.confThreshBps))) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } // === 5. Validate staleness threshold === if (oracleParams.stalenessThresh.gt(new BN(0))) { if (currentTime.sub(lastUpdateTimestamp).gt(oracleParams.stalenessThresh)) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } } // === 6. Validate volatility threshold === if (!minPrice.eq(0)) { const volRatioBps = maxPrice.sub(minPrice).div(minPrice).mul(new Decimal(HUNDRED_PERCENT_BPS)); if (volRatioBps.gt(new Decimal(oracleParams.volatilityThreshBps))) { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } } else { return new OraclePrice(new Decimal(0), new Decimal(0), 0); } // === 7. Convert to USD === if (oracleParams.quote === Quote.Usdc) { primaryPrice = primaryPrice.mul(usdPrice.price); confidence = confidence.mul(usdPrice.price); } else { primaryPrice = primaryPrice.mul(solPrice.price); confidence = confidence.mul(solPrice.price); } return new OraclePrice(primaryPrice, confidence, currentTime.toNumber()); } } function mulRightShift(val: BN, mulBy: BN): BN { return signedRightShift(val.mul(mulBy), 64, 256); } function signedRightShift(n0: BN, shiftBy: number, bitWidth: number): BN { const twoN0 = n0.toTwos(bitWidth).shrn(shiftBy); twoN0.imaskn(bitWidth - shiftBy + 1); return twoN0.fromTwos(bitWidth - shiftBy); }