@symmetry-hq/baskets-v2-sdk
Version:
Symmetry Baskets V2 SDK
365 lines (320 loc) • 13.2 kB
text/typescript
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,
});
}