UNPKG

@symmetry-hq/baskets-sdk

Version:

Software Development Kit for interacting with Symmetry Baskets Program

353 lines (352 loc) 18.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getConfirmedTimestamp = getConfirmedTimestamp; exports.getLookupTableAccount = getLookupTableAccount; exports.isForceRebalanceNeeded = isForceRebalanceNeeded; exports.getSortedRebalanceInfo = getSortedRebalanceInfo; exports.buildRebalanceTransactions = buildRebalanceTransactions; exports.shouldProcessRebalance = shouldProcessRebalance; exports.calculateRebalanceAmounts = calculateRebalanceAmounts; exports.getJupiterSwapData = getJupiterSwapData; exports.buildRebalanceTransaction = buildRebalanceTransaction; exports.processInstructions = processInstructions; exports.processInstruction = processInstruction; exports.signAndSendTransactions = signAndSendTransactions; exports.calculateFlashRebalanceAmounts = calculateFlashRebalanceAmounts; exports.getFlashRebalanceInfo = getFlashRebalanceInfo; const web3_js_1 = require("@solana/web3.js"); const config_1 = require("./config"); const instructionsBuilder_1 = require("./instructionsBuilder"); const utils_1 = require("./utils"); /** * 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 */ function getConfirmedTimestamp(connection, basket) { return __awaiter(this, void 0, void 0, function* () { const blockTime = yield connection.getBlockTime(yield 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 */ function getLookupTableAccount(connection, lookupTableAccount) { return __awaiter(this, void 0, void 0, function* () { const result = yield 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 */ function isForceRebalanceNeeded(basket, walletPublicKey) { 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 */ function getSortedRebalanceInfo(basket, oraclePriceData, timestamp, tokenList) { 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 */ function buildRebalanceTransactions(basket, rebalanceInfos, oraclePriceData, forceRebalance, lookups, maxAllowedAccounts, walletPublicKey, connection, program, tokenList, lamports, updateOraclesTxData, softCap, hardCap, underTokens, overTokens, jupAPIkey) { return __awaiter(this, void 0, void 0, function* () { let txsToSend = 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 = yield 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 = yield 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 */ function shouldProcessRebalance(over, under, forceRebalance) { 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 */ function calculateRebalanceAmounts(over, under, oraclePriceData, tokenList, hardCap) { 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 */ function getJupiterSwapData(from, to, tokenAmount, maxAllowedAccounts, basket, oraclePriceData, walletPublicKey, tokenList, jupAPIkey) { return __awaiter(this, void 0, void 0, function* () { const data = yield (0, utils_1.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 */ function buildRebalanceTransaction(basket, from, to, tokenAmount, jupData, lookups, walletPublicKey, connection, program, tokenList, lamports) { return __awaiter(this, void 0, void 0, function* () { const { addressLookupTableAddresses, swapInstruction, setupInstructions } = jupData; const processedSetupInstructions = processInstructions(setupInstructions); const processedSwapInstruction = processInstruction(swapInstruction); const lookupTableAccounts = yield (0, utils_1.getAddressLookupTableAccounts)(connection, addressLookupTableAddresses); return { payerKey: walletPublicKey, instructions: [ yield (0, instructionsBuilder_1.buildUpdateCurrentWeightsIx)(program, basket, tokenList), ...processedSetupInstructions, yield (0, instructionsBuilder_1.buildWithdrawBeforeRebalanceIx)(program, walletPublicKey, basket, tokenList, from, to, tokenAmount), processedSwapInstruction, yield (0, instructionsBuilder_1.buildDepositAfterRebalanceIx)(program, walletPublicKey, basket, tokenList, to), web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: config_1.ADDITIONAL_UNITS }), web3_js_1.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: lamports }) ], lookupTables: [...lookupTableAccounts, ...lookups] }; }); } /** * Processes instructions by converting pubkeys to PublicKey objects. * @param instructions Instructions to process * @returns Processed instructions */ function processInstructions(instructions) { return instructions.map(processInstruction); } /** * Processes a single instruction by converting pubkeys to PublicKey objects. * @param instruction Instruction to process * @returns Processed instruction */ function processInstruction(instruction) { return { programId: new web3_js_1.PublicKey(instruction.programId), keys: instruction.accounts.map((a) => (Object.assign(Object.assign({}, a), { pubkey: new web3_js_1.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 */ function signAndSendTransactions(txsToSend, connection, wallet, confirmFirst) { return __awaiter(this, void 0, void 0, function* () { if (txsToSend.length == 0) return []; const blockhash = (yield connection.getLatestBlockhash("confirmed")).blockhash; const signedTransactions = yield (0, utils_1.signVersionedTransactions)(wallet, txsToSend.map(tx => new web3_js_1.VersionedTransaction(new web3_js_1.TransactionMessage({ payerKey: tx.payerKey, recentBlockhash: blockhash, instructions: tx.instructions, }).compileToV0Message(tx.lookupTables)))).catch((e) => { console.log("Sign V Transactions", e); return []; }); return (0, utils_1.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 */ function calculateFlashRebalanceAmounts(numTokens, timestamp, lastRebalanceTime, rebalanceInterval, currentCompToken, currentCompAmount, targetWeights, weightSum, tokenList, rebalanceThreshold, oraclePriceData) { const currentValues = []; 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: [], 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 = { 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 */ function getFlashRebalanceInfo(basket, tokenList, oraclePriceData, timestamp) { // 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) => parseInt(x.toString())), parseInt(rebalanceInterval.toString()), currentCompToken.map((x) => parseInt(x.toString())), currentCompAmount.map((x) => parseInt(x.toString())), targetWeight.map((x) => parseInt(x.toString())), parseInt(weightSum.toString()), tokenList, parseInt(rebalanceThreshold.toString()), oraclePriceData); }