UNPKG

@symmetry-hq/baskets-v2-sdk

Version:

Symmetry Baskets V2 SDK

365 lines (320 loc) 13.2 kB
import { PublicKey } from "@solana/web3.js"; import { BN, Program } from "@coral-xyz/anchor"; import { BasketsProgram } from "../idl/types"; import { loadOraclePrice, OraclePrice, OracleType } from "../utils/oracle"; import { MANAGERS_PER_BASKET, PYTHNET_CUSTODY_PRICE_USDC_ACCOUNT, TOTAL_WEIGHT } from "../utils/constants"; import { PYTHNET_CUSTODY_PRICE_SOL_ACCOUNT } from "../utils/constants"; import { USDC_DECIMALS } from "../utils/constants"; import { getAccountInfos } from "../utils/programAccounts"; import { WSOL_DECIMALS } from "../utils/constants"; export const BASKETS_STATE_SIZE = 28189; export interface BasketState { version: number; ownAddress: PublicKey; basketType: number; basketPda: PublicKey; mint: PublicKey; supplyOutstanding: BN; lastPrice: BN; startingPrice: BN; highestPrice: BN; creator: PublicKey; creatorDepositFeeBps: number; creatorManagementFeeBps: number; creatorPerformanceFeeBps: number; host: PublicKey; hostDepositFeeBps: number; hostManagementFeeBps: number; hostPerformanceFeeBps: number; managers: PublicKey[]; managersWeightBps: number[]; managersAuthority: number[]; managersDepositFeeBps: number; managersPerformanceFeeBps: number; managersManagementFeeBps: number; basketDepositFeeBps: number; basketWithdrawFeeBps: number; rebalanceIntervalSeconds: BN; rebalanceThresholdBps: number; rebalanceSlippageBps: number; lpThresholdBps: number; allowAutomation: number; allowLp: number; lamportsForAutomation: BN; symbolLength: number; symbol: number[]; nameLength: number; name: number[]; uriLength: number; uri: number[]; metadataAccount: PublicKey; lookupTable1: PublicKey; lookupTable2: PublicKey; otherLookupTable1: PublicKey; otherLookupTable2: PublicKey; writeVersion: BN; numTokens: number; compositionMints: PublicKey[]; compositionDecimals: number[]; compositionOracleType: number[]; compositionOracle1: PublicKey[]; compositionOracle2: PublicKey[]; compositionTargetWeights: number[]; compositionAmounts: BN[]; tokenPrices: BN[]; tokenPriceUpdateTimestamps: BN[]; lastRebalanceTimestamp: BN[]; extraData: PublicKey[]; } export interface ParsedBasketState { version: number; ownAddress: string; basketType: number; basketPda: string; mint: string; supplyOutstanding: number; lastPrice: number; startingPrice: number; highestPrice: number; creator: string; creatorDepositFeeBps: number; creatorManagementFeeBps: number; creatorPerformanceFeeBps: number; host: string; hostDepositFeeBps: number; hostManagementFeeBps: number; hostPerformanceFeeBps: number; managers: string[]; managersWeightBps: number[]; managersAuthority: number[]; managersDepositFeeBps: number; managersPerformanceFeeBps: number; managersManagementFeeBps: number; basketDepositFeeBps: number; basketWithdrawFeeBps: number; rebalanceIntervalSeconds: number; rebalanceThresholdBps: number; rebalanceSlippageBps: number; lpThresholdBps: number; allowAutomation: number; allowLp: number; lamportsForAutomation: number; metadataAccount: string; lookupTable1: string; lookupTable2: string; otherLookupTable1: string; otherLookupTable2: string; writeVersion: number; numTokens: number; compositionMints: string[]; compositionDecimals: number[]; compositionOracleType: number[]; compositionOracle1: string[]; compositionOracle2: string[]; compositionTargetWeights: number[]; compositionAmounts: number[]; tokenPrices: number[]; tokenPriceUpdateTimestamps: number[]; lastRebalanceTimestamp: number[]; metadata: any; tvl: any; tokenValues: any; } export async function fetchBasketState( program: Program<BasketsProgram>, basket: PublicKey ): Promise<BasketState> { return await program.account.basketV200.fetch(basket); } export function parseBasketState( basketState: BasketState ): ParsedBasketState { const managers = []; for (let i = 0; i < MANAGERS_PER_BASKET; i++) { if (basketState.managers[i].equals(PublicKey.default)) { break; } managers.push(basketState.managers[i]); } return { version: basketState.version, ownAddress: basketState.ownAddress.toBase58(), basketType: basketState.basketType, basketPda: basketState.basketPda.toBase58(), mint: basketState.mint.toBase58(), supplyOutstanding: parseInt(basketState.supplyOutstanding.toString()), lastPrice: parseInt(basketState.lastPrice.toString()), startingPrice: parseInt(basketState.startingPrice.toString()), highestPrice: parseInt(basketState.highestPrice.toString()), creator: basketState.creator.toBase58(), creatorDepositFeeBps: basketState.creatorDepositFeeBps, creatorManagementFeeBps: basketState.creatorManagementFeeBps, creatorPerformanceFeeBps: basketState.creatorPerformanceFeeBps, host: basketState.host.toBase58(), hostDepositFeeBps: basketState.hostDepositFeeBps, hostManagementFeeBps: basketState.hostManagementFeeBps, hostPerformanceFeeBps: basketState.hostPerformanceFeeBps, managers: basketState.managers.slice(0, managers.length).map((manager) => manager.toBase58()), managersWeightBps: basketState.managersWeightBps.slice(0, managers.length), managersAuthority: basketState.managersAuthority.slice(0, managers.length), managersDepositFeeBps: basketState.managersDepositFeeBps, managersPerformanceFeeBps: basketState.managersPerformanceFeeBps, managersManagementFeeBps: basketState.managersManagementFeeBps, basketDepositFeeBps: basketState.basketDepositFeeBps, basketWithdrawFeeBps: basketState.basketWithdrawFeeBps, rebalanceIntervalSeconds: parseInt(basketState.rebalanceIntervalSeconds.toString()), rebalanceThresholdBps: basketState.rebalanceThresholdBps, rebalanceSlippageBps: basketState.rebalanceSlippageBps, lpThresholdBps: basketState.lpThresholdBps, allowAutomation: basketState.allowAutomation, allowLp: basketState.allowLp, lamportsForAutomation: parseInt(basketState.lamportsForAutomation.toString()), // symbolLength: basketState.symbolLength, // symbol: basketState.symbol, // nameLength: basketState.nameLength, // name: basketState.name, // uriLength: basketState.uriLength, // uri: basketState.uri, metadataAccount: basketState.metadataAccount.toBase58(), lookupTable1: basketState.lookupTable1.toBase58(), lookupTable2: basketState.lookupTable2.toBase58(), otherLookupTable1: basketState.otherLookupTable1.toBase58(), otherLookupTable2: basketState.otherLookupTable2.toBase58(), writeVersion: parseInt(basketState.writeVersion.toString()), numTokens: basketState.numTokens, compositionMints: basketState.compositionMints.slice(0, basketState.numTokens).map((mint) => mint.toBase58()), compositionDecimals: basketState.compositionDecimals.slice(0, basketState.numTokens), compositionOracleType: basketState.compositionOracleType.slice(0, basketState.numTokens), compositionOracle1: basketState.compositionOracle1.slice(0, basketState.numTokens).map((oracle) => oracle.toBase58()), compositionOracle2: basketState.compositionOracle2.slice(0, basketState.numTokens).map((oracle) => oracle.toBase58()), compositionTargetWeights: basketState.compositionTargetWeights.slice(0, basketState.numTokens), compositionAmounts: basketState.compositionAmounts.slice(0, basketState.numTokens).map(x => parseInt(x.toString())), tokenPrices: basketState.tokenPrices.slice(0, basketState.numTokens).map(x => parseInt(x.toString())), tokenPriceUpdateTimestamps: basketState.tokenPriceUpdateTimestamps.slice(0, basketState.numTokens).map(x => parseInt(x.toString())), lastRebalanceTimestamp: basketState.lastRebalanceTimestamp.slice(0, basketState.numTokens).map(x => parseInt(x.toString())), metadata: null, tvl: null, tokenValues: null, }; } export async function getBasketTokenPrices( program: Program<BasketsProgram>, basketState: BasketState, ): Promise<OraclePrice[]> { // Collect oracle accounts to fetch const oracleAccounts: PublicKey[] = [ PYTHNET_CUSTODY_PRICE_USDC_ACCOUNT, PYTHNET_CUSTODY_PRICE_SOL_ACCOUNT, ]; for (let i = 0; i < basketState.numTokens; i++) { oracleAccounts.push(basketState.compositionOracle1[i]); const oracle2 = basketState.compositionOracle2[i]; if (!oracle2.equals(PublicKey.default)) { oracleAccounts.push(oracle2); } } const oracleAccountInfos = await getAccountInfos(program.provider.connection, oracleAccounts); const usdcPrice = loadOraclePrice(USDC_DECIMALS, OracleType.Pyth, oracleAccountInfos[0], null, 0, 0, 0).avgPrice; const solPrice = loadOraclePrice(WSOL_DECIMALS, OracleType.Pyth, oracleAccountInfos[1], null, 0, 0, 0).avgPrice; // Calculate prices for each token const oraclePrices: OraclePrice[] = []; let currentIndex = 2; for (let i = 0; i < basketState.numTokens; i++) { const oracleAccount1 = oracleAccountInfos[currentIndex]; let oracleAccount2 = oracleAccountInfos[currentIndex]; currentIndex++; if (!basketState.compositionOracle2[i].equals(PublicKey.default)) { oracleAccount2 = oracleAccountInfos[currentIndex]; currentIndex++; } const oraclePrice = loadOraclePrice( basketState.compositionDecimals[i], basketState.compositionOracleType[i], oracleAccount1, oracleAccount2, solPrice, usdcPrice, 0, ); oraclePrices.push(oraclePrice); } return oraclePrices; } export interface RebalanceInfo { token: PublicKey, tokenDecimals: number, tokenPrice: number, index: number, currentAmount: number, currentWeight: number, currentValue: number, targetWeight: number, targetValue: number, valueDiff: number, maxSpendAmount: number, } export function computeRebalanceInfos( params: { basketState: BasketState, oraclePrices: OraclePrice[], } ): { tvl: number, tokenValues: any[], rebalanceInfos: RebalanceInfo[], } { let basketValue = 0; const values: number[] = []; for (let i = 0; i < params.basketState.numTokens; i++) { const tokenPrice = params.oraclePrices[i].avgPrice; const currentAmount = parseInt(params.basketState.compositionAmounts[i].toString()); const decimals = params.basketState.compositionDecimals[i]; const currentValue = tokenPrice * currentAmount / (10 ** decimals); basketValue += currentValue; values.push(currentValue); }; const tokenValues: any[] = []; const rebalanceInfos: RebalanceInfo[] = []; for (let i = 0; i < params.basketState.numTokens; i++) { const tokenPrice = params.oraclePrices[i].avgPrice; const currentAmount = parseInt(params.basketState.compositionAmounts[i].toString()); const decimals = params.basketState.compositionDecimals[i]; const currentValue = tokenPrice * currentAmount / (10 ** decimals); const currentWeight = Math.floor(currentValue * TOTAL_WEIGHT / basketValue); const targetWeight = params.basketState.compositionTargetWeights[i]; const targetValue = basketValue * targetWeight / TOTAL_WEIGHT; let maxSpendAmount = 0; if (currentWeight > targetWeight) { maxSpendAmount = Math.floor(currentAmount * (currentWeight - targetWeight) / currentWeight); } rebalanceInfos.push({ token: params.basketState.compositionMints[i], tokenDecimals: decimals, tokenPrice: tokenPrice, index: i, currentAmount: currentAmount, currentWeight: currentWeight, currentValue: currentValue, targetWeight: targetWeight, targetValue: targetValue, valueDiff: targetValue - currentValue, maxSpendAmount: maxSpendAmount, }); tokenValues.push([Number(values[i].toFixed(6)), (currentWeight/100).toFixed(2) + "% -> " + (targetWeight/100).toFixed(2)+ "%"]); } return { tvl: basketValue, tokenValues: tokenValues, rebalanceInfos: rebalanceInfos, }; } export async function getBasketTvl( program: Program<BasketsProgram>, basketState: BasketState, ) { const oraclePrices = await getBasketTokenPrices(program, basketState); return computeRebalanceInfos({ basketState: basketState, oraclePrices: oraclePrices, }); }