UNPKG

@symmetry-hq/baskets-v2-sdk

Version:

Symmetry Baskets V2 SDK

195 lines (167 loc) 7.06 kB
// Core dependencies import { AccountLayout, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { AccountInfo, PublicKey } from "@solana/web3.js"; // Local imports import { MEME_SOL, RAYDIUM_CPMM, RAYDIUM_LIQUIDITY_POOL_V4, USDC_DECIMALS, USDC_MINT, WSOL_DECIMALS, WSOL_MINT } from "./constants"; import { BasketsProgram } from "../idl/types"; import { Program } from "@coral-xyz/anchor"; import { PythSponsoredFeeds } from "../state/oracle"; export enum OracleType { Pyth, RaydiumCpmm, RaydiumLiquidityPoolV4, Unknown } export class OracleTypeUtils { static toU8(type: OracleType): number { switch (type) { case OracleType.Pyth: return 0; case OracleType.RaydiumCpmm: return 1; case OracleType.RaydiumLiquidityPoolV4: return 2; case OracleType.Unknown: return 3; } } static fromU8(value: number): OracleType { switch (value) { case 0: return OracleType.Pyth; case 1: return OracleType.RaydiumCpmm; case 2: return OracleType.RaydiumLiquidityPoolV4; default: return OracleType.Unknown; } } } export function validateOracle( program: Program<BasketsProgram>, tokenMint: PublicKey, tokenMintAccountInfo: AccountInfo<Buffer>, oracleAccountInfo: AccountInfo<Buffer>, oracleType: number, oracle1: PublicKey, oracle2: PublicKey ): string { const oracleTypeObj = OracleTypeUtils.fromU8(oracleType); // Validate token mint if (!tokenMintAccountInfo.owner.equals(TOKEN_PROGRAM_ID)) return "Invalid token mint owner"; if (tokenMintAccountInfo.data.length !== 82) return "Invalid token mint account size"; if (oracleTypeObj == OracleType.Pyth) { const pythSponsoredFeeds: PythSponsoredFeeds = program.coder.accounts.decode("pythSponsoredFeeds", oracleAccountInfo.data); let index = 0; for (let i = 0; i < pythSponsoredFeeds.numTokens; i++) if (pythSponsoredFeeds.mints[i].equals(tokenMint)) { index = i; break; } if (!pythSponsoredFeeds.mints[index].equals(tokenMint)) return "Token mint not found in PythSponsoredFeeds"; if (!pythSponsoredFeeds.feeds[index].equals(oracle1)) return "Invalid pyth oracle feed"; if (pythSponsoredFeeds.isActive[index] == 0) return "Pyth oracle feed is not active"; return "OK"; } if (oracleTypeObj == OracleType.RaydiumCpmm || oracleTypeObj == OracleType.RaydiumLiquidityPoolV4) { const accountData = oracleAccountInfo.data; // Check status for V4 pools if (oracleTypeObj == OracleType.RaydiumLiquidityPoolV4) { const status = accountData[0]; if (status !== 6) return "Invalid status (Using OrderBook)"; } // Get token vault and mint offsets based on pool type const [vaultOffset0, vaultOffset1, mintOffset0, mintOffset1] = oracleTypeObj == OracleType.RaydiumCpmm ? [72, 104, 168, 200] : [336, 368, 400, 432]; // Extract token vaults and mints let tokenVault0 = new PublicKey(accountData.slice(vaultOffset0, vaultOffset0 + 32)); let tokenVault1 = new PublicKey(accountData.slice(vaultOffset1, vaultOffset1 + 32)); let tokenMint0 = new PublicKey(accountData.slice(mintOffset0, mintOffset0 + 32)); let tokenMint1 = new PublicKey(accountData.slice(mintOffset1, mintOffset1 + 32)); // Swap if needed to ensure tokenMint1 is SOL/USDC if (tokenMint0.equals(WSOL_MINT) || tokenMint0.equals(USDC_MINT) || tokenMint0.equals(MEME_SOL)) { [tokenMint0, tokenMint1] = [tokenMint1, tokenMint0]; [tokenVault0, tokenVault1] = [tokenVault1, tokenVault0]; } // Validate owner program const expectedOwner = oracleTypeObj == OracleType.RaydiumCpmm ? RAYDIUM_CPMM : RAYDIUM_LIQUIDITY_POOL_V4; if (!oracleAccountInfo.owner.equals(expectedOwner)) return "Invalid oracle account owner"; // Validate token mints and vaults if (!tokenMint0.equals(tokenMint)) return "Invalid base token mint"; if (!tokenMint1.equals(WSOL_MINT) && !tokenMint1.equals(USDC_MINT) && !tokenMint1.equals(MEME_SOL)) { return "Invalid quote token mint (Should be SOL, MEME-SOL or USDC)"; } if (!tokenVault0.equals(oracle1)) return "Invalid base token vault"; if (!tokenVault1.equals(oracle2)) return "Invalid quote token vault"; return "OK"; } return "Invalid oracle type"; } export interface OraclePrice { sellPrice: number; avgPrice: number; buyPrice: number; age: number; sellValue: number; } export function loadOraclePrice( tokenDecimals: number, oracleType: number, oracleAccount1: AccountInfo<Buffer> | null, oracleAccount2: AccountInfo<Buffer> | null, solPrice: number, usdcPrice: number, tokenAmount: number ): OraclePrice { let avgPrice: number; let confidence: number; let sellValue: number; if (!oracleAccount1) { throw new Error(); } switch (OracleTypeUtils.fromU8(oracleType)) { case OracleType.Pyth: { const accountData = oracleAccount1.data; const price = accountData.readBigInt64LE(73); const exp = accountData.readInt32LE(89); avgPrice = Number(price) * 10 ** exp; confidence = avgPrice / 100; const sellPrice = avgPrice - confidence; const powNum = 10 ** tokenDecimals; sellValue =tokenAmount * sellPrice / powNum; break; } case OracleType.RaydiumCpmm: case OracleType.RaydiumLiquidityPoolV4: { if (!oracleAccount2) { throw new Error(); } const baseData = AccountLayout.decode(oracleAccount1.data); const quoteData = AccountLayout.decode(oracleAccount2.data); const x = parseInt(baseData.amount.toString()); const y = parseInt(quoteData.amount.toString()); const quoteMint = quoteData.mint; const [quoteDecimals, quotePrice] = (quoteMint.equals(WSOL_MINT) || quoteMint.equals(MEME_SOL)) ? [WSOL_DECIMALS, solPrice] : [USDC_DECIMALS, usdcPrice]; const priceRaw = (y * quotePrice) / x; const tokenDecimalsPow = 10 ** tokenDecimals; const quoteDecimalsPow = 10 ** quoteDecimals; avgPrice = (priceRaw * tokenDecimalsPow) / quoteDecimalsPow; confidence = avgPrice / 100; const baseAmount = (tokenAmount * y) / (x + tokenAmount); const powNum = 10 ** tokenDecimals; sellValue = baseAmount * quotePrice / powNum; break; } default: throw new Error(); } return { sellPrice: avgPrice - confidence, avgPrice, buyPrice: avgPrice + confidence, age: 0, sellValue }; }