UNPKG

@symmetry-hq/baskets-sdk

Version:

Software Development Kit for interacting with Symmetry Baskets Program

484 lines (448 loc) 19.5 kB
import { PublicKey, TransactionSignature, AddressLookupTableAccount, TransactionInstruction, ComputeBudgetProgram, VersionedTransaction, TransactionMessage, Connection } from '@solana/web3.js'; import { Basket } from './basketState'; import { BasketError, TokenSettings, ADDITIONAL_UNITS, TransactionToSend } from './config'; import { buildUpdateCurrentWeightsIx, buildWithdrawBeforeRebalanceIx, buildDepositAfterRebalanceIx } from './instructionsBuilder'; import { generateJupTxData, getAddressLookupTableAccounts, signVersionedTransactions, sendSignedTransactions, getOraclePrices, delay } from './utils'; import { BN, Program } from '@coral-xyz/anchor'; import { BasketsIDL } from './basketsIDL'; import { getAssociatedTokenAddressSync } from './splTokenHelpers'; // Updated type definitions type RebalanceInfo = { over: TokenRebalanceInfo[], under: TokenRebalanceInfo[] }; type TokenRebalanceInfo = { value: number, token: number, amount: number, iT?: boolean, rR?: boolean, uM: number }; type RebalanceAmounts = { from: number, to: number, tokenAmount: number, value: number }; type JupiterSwapData = { addressLookupTableAddresses: string[]; swapInstruction: any; setupInstructions: any[]; res: { inAmount: number }; tokenAmount: number; swapValue: number, }; /** * Fetches the current timestamp from the confirmed slot on the Solana blockchain. * This is used to determine when rebalances should occur based on basket settings. * @param connection The Solana connection * @returns The current timestamp or a default value */ export async function getConfirmedTimestamp(connection: Connection, basket: Basket): Promise<number> { const blockTime = await connection.getBlockTime(await connection.getSlot("confirmed")).catch((e) => null); return (blockTime !== null && basket.data.sellState.toNumber() == 0) ? blockTime : 2000000000; } /** * Fetches the lookup table account from the Solana blockchain. * Lookup tables are used to optimize transaction size and cost for complex operations like rebalancing. * @param connection The Solana connection * @returns The lookup table account */ export async function getLookupTableAccount(connection: Connection, lookupTableAccount: PublicKey): Promise<AddressLookupTableAccount> { const result = await connection.getAddressLookupTable(lookupTableAccount); //@ts-ignore return result.value; } /** * Determines if force rebalance is needed for actively managed baskets. * This allows basket managers to trigger rebalances regardless of other conditions. * @param basket The basket to check * @param wallet The wallet to use * @returns True if force rebalance is needed, false otherwise */ export function isForceRebalanceNeeded(basket: Basket, walletPublicKey: PublicKey): boolean { if (basket.data.sellState.toNumber() == 1 && basket.data.rebalanceSellState.toNumber() == 2) return true; return walletPublicKey.equals(basket.data.manager) && basket.data.activelyManaged.toNumber() === 1; } /** * Gets and sorts the rebalance info for a basket. * This function calculates which tokens are over or under their target weights, * considering the basket's rebalance threshold and current market prices. * @param basket The basket to rebalance * @param oraclePriceData Oracle price data for accurate token valuation * @param timestamp Current timestamp * @param tokenList The token list * @returns Sorted rebalance info */ export function getSortedRebalanceInfo(basket: Basket, oraclePriceData: number[], timestamp: number, tokenList: any): RebalanceInfo { const rebalanceInfos = getFlashRebalanceInfo(basket, tokenList, oraclePriceData, timestamp); rebalanceInfos.over.sort((a, b) => b.value - a.value); rebalanceInfos.under.sort((a, b) => b.value - a.value); return rebalanceInfos; } /** * Builds rebalance transactions for a basket. * This function creates transactions to adjust token weights to match their target weights, * considering factors like rebalance threshold, interval, and slippage settings. * @param basket The basket to rebalance * @param rebalanceInfos Information about tokens that need rebalancing * @param oraclePriceData Current oracle price data for tokens * @param forceRebalance Whether to force rebalance regardless of conditions * @param lookupTableAccount The lookup table account for the transaction * @param wallet The wallet to use for the transaction * @param connection The Solana connection * @param program The program instance * @param tokenList List of tokens in the basket * @param lamports Amount of lamports to use * @returns An array of transactions to send to the Solana blockchain */ export async function buildRebalanceTransactions( basket: Basket, rebalanceInfos: RebalanceInfo, oraclePriceData: number[], forceRebalance: boolean, lookups: AddressLookupTableAccount[], maxAllowedAccounts: number, walletPublicKey: PublicKey, connection: Connection, program: Program<BasketsIDL>, tokenList: TokenSettings[], lamports: number, updateOraclesTxData: TransactionToSend[], softCap: number, hardCap: number, underTokens: number, overTokens: number, jupAPIkey: string, ): Promise<TransactionToSend[]> { let txsToSend: TransactionToSend[] = updateOraclesTxData; let size = txsToSend.length; if (basket.data.sellState.toNumber() != 0) { overTokens = rebalanceInfos.over.length; underTokens = rebalanceInfos.under.length } // Iterate over overweighted and underweighted tokens for (const over of rebalanceInfos.over.slice(0, overTokens)) { for (const under of rebalanceInfos.under.slice(0, underTokens)) { // Check if we should process this rebalance based on thresholds and intervals if (!shouldProcessRebalance(over, under, forceRebalance)) continue; // Calculate the amounts to rebalance const { from, to, tokenAmount, value } = calculateRebalanceAmounts( over, under, oraclePriceData, tokenList, hardCap, ); if (value <= softCap && !forceRebalance && basket.data.sellState.toNumber() == 0) continue; // Get Jupiter swap data for the rebalance const jupData = await getJupiterSwapData( from, to, tokenAmount, maxAllowedAccounts, basket, oraclePriceData, walletPublicKey, tokenList, jupAPIkey ); if (!jupData) continue; if (jupData.swapValue <= softCap && !forceRebalance && basket.data.sellState.toNumber() == 0) continue; // Build the rebalance transaction const tx = await buildRebalanceTransaction( basket, from, to, jupData.tokenAmount, jupData, lookups, walletPublicKey, connection, program, tokenList, lamports, ); txsToSend.push(tx); // Update rebalance info values over.value -= jupData.swapValue; under.value -= jupData.swapValue; } } if (txsToSend.length == size) return []; return txsToSend; } /** * Determines if a rebalance should be processed. * This function checks if the rebalance is necessary based on the basket's settings and current state. * @param over Over-weighted token info * @param under Under-weighted token info * @param forceRebalance Whether to force rebalance * @returns True if rebalance should be processed, false otherwise */ export function shouldProcessRebalance(over: TokenRebalanceInfo, under: TokenRebalanceInfo, forceRebalance: boolean): boolean { if (over.value <= 0 || under.value <= 0) return false; if (!forceRebalance && (over.iT && under.iT)) return false; // Both tokens are within threshold if (!forceRebalance && (over.rR && under.rR)) return false; // Both tokens were recently rebalanced return true; } /** * Calculates rebalance amounts. * This function determines how much of each token should be swapped to bring them closer to their target weights. * @param over Over-weighted token info * @param under Under-weighted token info * @param oraclePriceData Oracle price data * @param tokenList The token list * @returns Calculated rebalance amounts */ export function calculateRebalanceAmounts( over: TokenRebalanceInfo, under: TokenRebalanceInfo, oraclePriceData: number[], tokenList: any, hardCap: number ): RebalanceAmounts { const from = over.token; const to = under.token; const maxAmountUn = Math.min(hardCap, under.value) / oraclePriceData[from] * 10 ** tokenList[from].decimals; const maxAmountOv = Math.min(hardCap, over.value) / oraclePriceData[from] * 10 ** tokenList[from].decimals; let tokenAmount = Math.min(Math.floor(Math.min(maxAmountUn, maxAmountOv) * 0.995), over.amount); if (maxAmountUn > maxAmountOv && over.uM !== 0) tokenAmount = over.uM; const value = tokenAmount * oraclePriceData[from] / 10 ** tokenList[from].decimals; return { from, to, tokenAmount, value }; } /** * Gets Jupiter swap data. * This function prepares the data needed for a token swap using Jupiter DEX aggregator. * @param from From token * @param to To token * @param tokenAmount Token amount * @param basket Basket * @param oraclePriceData Oracle price data * @param wallet The wallet to use * @param tokenList The token list * @returns Jupiter swap data or null if failed */ export async function getJupiterSwapData( from: number, to: number, tokenAmount: number, maxAllowedAccounts: number, basket: Basket, oraclePriceData: number[], walletPublicKey: PublicKey, tokenList: any, jupAPIkey: string, ): Promise<JupiterSwapData | null> { const data = await generateJupTxData( walletPublicKey, tokenList[from].tokenMint, tokenList[to].tokenMint, tokenAmount, maxAllowedAccounts, basket.data.rebalanceSlippage.toNumber(), oraclePriceData[from] / 10 ** tokenList[from].decimals, oraclePriceData[to] / 10 ** tokenList[to].decimals, jupAPIkey ).catch((e) => { console.log("---------- Error ------------"); console.log("Jup Tx Data", e.message); console.log("---------- End Error ------------"); return null; }); return data; } /** * Builds a rebalance transaction. * This function creates a transaction that will perform the actual rebalancing of tokens in the basket on the Solana blockchain. * @param basket The basket to rebalance * @param from From token * @param to To token * @param tokenAmount Token amount * @param jupData Jupiter swap data * @param lookupTableAccount Lookup table account * @param wallet The wallet to use * @param connection The Solana connection * @param program The program to use * @param tokenList The token list * @param lamports The lamports to use * @returns A transaction to send to the Solana blockchain */ export async function buildRebalanceTransaction( basket: Basket, from: number, to: number, tokenAmount: number, jupData: JupiterSwapData, lookups: AddressLookupTableAccount[], walletPublicKey: PublicKey, connection: Connection, program: Program<BasketsIDL>, tokenList: TokenSettings[], lamports: number ): Promise<TransactionToSend> { const { addressLookupTableAddresses, swapInstruction, setupInstructions } = jupData; const processedSetupInstructions = processInstructions(setupInstructions); const processedSwapInstruction = processInstruction(swapInstruction); const lookupTableAccounts = await getAddressLookupTableAccounts(connection, addressLookupTableAddresses); return { payerKey: walletPublicKey, instructions: [ await buildUpdateCurrentWeightsIx(program, basket, tokenList), ...processedSetupInstructions, await buildWithdrawBeforeRebalanceIx( program, walletPublicKey, basket, tokenList, from, to, tokenAmount ), processedSwapInstruction, await buildDepositAfterRebalanceIx( program, walletPublicKey, basket, tokenList, to ), ComputeBudgetProgram.setComputeUnitLimit({units: ADDITIONAL_UNITS}), ComputeBudgetProgram.setComputeUnitPrice({microLamports: lamports}) ], lookupTables: [...lookupTableAccounts, ...lookups] }; } /** * Processes instructions by converting pubkeys to PublicKey objects. * @param instructions Instructions to process * @returns Processed instructions */ export function processInstructions(instructions: any[]): TransactionInstruction[] { return instructions.map(processInstruction); } /** * Processes a single instruction by converting pubkeys to PublicKey objects. * @param instruction Instruction to process * @returns Processed instruction */ export function processInstruction(instruction: any): TransactionInstruction { return { programId: new PublicKey(instruction.programId), keys: instruction.accounts.map((a: any) => ({ ...a, pubkey: new PublicKey(a.pubkey) })), data: Buffer.from(instruction.data, "base64") }; } /** * Signs and sends transactions to the Solana blockchain. * @param txsToSend Transactions to send * @param connection The Solana connection * @param wallet The wallet to use * @returns An array of transaction signatures */ export async function signAndSendTransactions( txsToSend: TransactionToSend[], connection: Connection, wallet: any, confirmFirst: number, ): Promise<TransactionSignature[]> { if (txsToSend.length == 0) return []; const blockhash = (await connection.getLatestBlockhash("confirmed")).blockhash; const signedTransactions = await signVersionedTransactions( wallet, txsToSend.map(tx => new VersionedTransaction( new TransactionMessage({ payerKey: tx.payerKey, recentBlockhash: blockhash, instructions: tx.instructions, }).compileToV0Message(tx.lookupTables) )) ).catch((e) => { console.log("Sign V Transactions", e); return []; }); return sendSignedTransactions(connection, signedTransactions, confirmFirst); } /** * Calculates the flash rebalance amounts for a basket of tokens. * This function determines which tokens need to be rebalanced based on their current weights, * target weights, and the basket's rebalance threshold. * * @param numTokens - The number of tokens in the basket * @param timestamp - The current timestamp * @param lastRebalanceTime - Array of timestamps for the last rebalance of each token * @param rebalanceInterval - The interval between rebalances * @param currentCompToken - Array of current token compositions * @param currentCompAmount - Array of current token amounts * @param targetWeights - Array of target weights for each token * @param weightSum - The sum of all target weights * @param tokenList - List of token settings * @param rebalanceThreshold - The threshold for rebalancing * @param oraclePriceData - Array of current oracle prices for each token * * @returns An object containing arrays of over-weighted and under-weighted tokens */ export function calculateFlashRebalanceAmounts( numTokens: number, timestamp: number, lastRebalanceTime: number[], rebalanceInterval: number, currentCompToken: number[], currentCompAmount: number[], targetWeights: number[], weightSum: number, tokenList: TokenSettings[], rebalanceThreshold: number, oraclePriceData: number[], ): { over: TokenRebalanceInfo[] under: TokenRebalanceInfo[] } { const currentValues: number[] = []; let basketWorth = 0; // Calculate current values and total basket worth for (let i = 0; i < numTokens; i++) { const price = oraclePriceData[currentCompToken[i]]; const tokenAmount = currentCompAmount[i] / Math.pow(10, tokenList[currentCompToken[i]].decimals); const tokenValue = price * tokenAmount; currentValues.push(tokenValue); basketWorth += tokenValue; } // Return empty arrays if basket is worth nothing if (basketWorth === 0) return { over: [], under: [] }; const res: { over: TokenRebalanceInfo[], under: TokenRebalanceInfo[] } = { over: [], under: [] }; // Determine over-weighted and under-weighted tokens for (let i = 0; i < numTokens; i++) { const currentPercentage = (currentValues[i] / basketWorth) * 10000; const targetPercentage = Math.floor((targetWeights[i] / weightSum) * 10000); const recentlyRebalanced = (lastRebalanceTime[i] + rebalanceInterval > timestamp); const inThresholds = ( currentPercentage >= targetPercentage * (1 - rebalanceThreshold / 10000) && currentPercentage <= targetPercentage * (1 + rebalanceThreshold / 10000) ); const diffOnChain = Math.floor(currentPercentage) == 0 ? 0 : Math.floor( currentCompAmount[i] * Math.floor(Math.floor(currentPercentage) - targetPercentage) / Math.floor(currentPercentage) ); // Create TokenRebalanceInfo object const item: TokenRebalanceInfo = { token: currentCompToken[i], value: Math.abs((currentPercentage - targetPercentage) * basketWorth) / 10000, amount: targetPercentage == 0 ? currentCompAmount[i] : diffOnChain, rR: recentlyRebalanced, iT: inThresholds, uM: targetPercentage === 0 ? currentCompAmount[i] : 0 }; // Categorize as over-weighted or under-weighted if (currentValues[i] >= targetPercentage * basketWorth / 10000) { res.over.push(item); } else { res.under.push(item); } } return res; } /** * Retrieves flash rebalance information for a given basket. * * @param basket - The basket to analyze * @param tokenList - List of token settings * @param oraclePriceData - Array of current oracle prices for each token * @param timestamp - The current timestamp * * @returns An object containing arrays of over-weighted and under-weighted tokens */ export function getFlashRebalanceInfo( basket: Basket, tokenList: TokenSettings[], oraclePriceData: number[], timestamp: number, ): { over: TokenRebalanceInfo[] under: TokenRebalanceInfo[] } { // Extract relevant data from the basket const { currentCompToken, currentCompAmount, targetWeight, numOfTokens, rebalanceThreshold, weightSum, rebalanceInterval, lastRebalanceTime } = basket.data; // Call calculateFlashRebalanceAmounts with extracted and parsed data return calculateFlashRebalanceAmounts( parseInt(numOfTokens.toString()), timestamp, lastRebalanceTime.map((x: BN) => parseInt(x.toString())), parseInt(rebalanceInterval.toString()), currentCompToken.map((x: BN) => parseInt(x.toString())), currentCompAmount.map((x: BN) => parseInt(x.toString())), targetWeight.map((x: BN) => parseInt(x.toString())), parseInt(weightSum.toString()), tokenList, parseInt(rebalanceThreshold.toString()), oraclePriceData, ); }