import { Connection, GetProgramAccountsResponse, PublicKey } from "@solana/web3.js"; import { AuctionData, AuctionTimestamps, ClaimBountyData, DepositData, FormattedRebalanceIntent, MintData, PriceUpdatesData, REBALANCE_ACTION_STRINGS, REBALANCE_TYPE_STRINGS, RebalanceAction, RebalanceIntent, RebalanceIntentLayout, RebalanceType, RedeemData, TokenAuction, UIRebalanceIntent } from "../../layouts/intents/rebalanceIntent"; import { GetProgramAccountsFilter } from "@solana/web3.js"; import { BASKETS_V3_PROGRAM_ID, HUNDRED_PERCENT_BPS, MAX_SUPPORTED_TOKENS_PER_BASKET, MAX_TRANSFER_TOKENS } from "../../constants"; import { Fraction, fractionAdd, fractionDiv, fractionLt, fractionLte, fractionMul, fractionRoundDown, fractionRoundUp, fractionSub, fractionToDecimal } from "../../layouts/fraction"; import { getMultipleAccountsInfoBatched } from "../../txUtils"; import BN from "bn.js"; import { Basket, FormattedBasket } from "../../layouts/basket"; import { OraclePriceOnChain } from "../../layouts/oracle"; import { fetchBasket } from "../basket"; export function computeRebalanceIntentBountyAmount( rebalance_type: RebalanceType, num_tokens: number, bounty_bond: number, bounty_per_task: number, bounty_per_price_update_task_max: number, ): number { let num_tasks = 0; num_tasks += 1; // FinishPriceUpdates; num_tasks += 1; // CancelRebalance; if (rebalance_type == RebalanceType.Deposit) { num_tasks += 1; // MintBasket; } if (rebalance_type == RebalanceType.Deposit || rebalance_type == RebalanceType.Withdraw) { num_tasks += num_tokens; // TokenSettlement; } else { num_tasks += 1; // AuctionCreation; } let claim_bounty_tasks = ( num_tasks + MAX_TRANSFER_TOKENS - 1 ) / MAX_TRANSFER_TOKENS; let price_update_tasks = num_tokens; let tasks = num_tasks + claim_bounty_tasks; let bounty_total = bounty_per_price_update_task_max * price_update_tasks + bounty_per_task * tasks + bounty_bond; return bounty_total; } export function formatRebalanceIntent(rebalanceIntent: RebalanceIntent, basket?: FormattedBasket): UIRebalanceIntent { let numTokens = MAX_SUPPORTED_TOKENS_PER_BASKET; while (numTokens > 0 && rebalanceIntent.tokens[numTokens - 1].mint.equals(PublicKey.default)) numTokens--; rebalanceIntent.tokens = rebalanceIntent.tokens.slice(0, numTokens); rebalanceIntent.priceUpdateTasks = rebalanceIntent.priceUpdateTasks.slice(0, numTokens); rebalanceIntent.tokenSettlementTasks = rebalanceIntent.tokenSettlementTasks.slice(0, numTokens); let priceUpdateTasks = []; for (let i = 0; i < numTokens; i++) { priceUpdateTasks.push({ completed_by: rebalanceIntent.priceUpdateTasks[i].completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.priceUpdateTasks[i].completedBounty.toString()), completed_time: parseInt(rebalanceIntent.priceUpdateTasks[i].completedTime.toString()), }); } let tokenSettlementTasks = []; for (let i = 0; i < numTokens; i++) { tokenSettlementTasks.push({ completed_by: rebalanceIntent.tokenSettlementTasks[i].completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.tokenSettlementTasks[i].completedBounty.toString()), completed_time: parseInt(rebalanceIntent.tokenSettlementTasks[i].completedTime.toString()), }); } let placeholderTask = { completed_by: rebalanceIntent.placeholderTask.completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.placeholderTask.completedBounty.toString()), completed_time: parseInt(rebalanceIntent.placeholderTask.completedTime.toString()), }; let finishPriceUpdateTask = { completed_by: rebalanceIntent.finishPriceUpdateTask.completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.finishPriceUpdateTask.completedBounty.toString()), completed_time: parseInt(rebalanceIntent.finishPriceUpdateTask.completedTime.toString()), }; let auctionCreationTask = { completed_by: rebalanceIntent.auctionCreationTask.completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.auctionCreationTask.completedBounty.toString()), completed_time: parseInt(rebalanceIntent.auctionCreationTask.completedTime.toString()), }; let mintBasketTask = { completed_by: rebalanceIntent.mintBasketTask.completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.mintBasketTask.completedBounty.toString()), completed_time: parseInt(rebalanceIntent.mintBasketTask.completedTime.toString()), }; let cancelRebalanceTask = { completed_by: rebalanceIntent.cancelRebalanceTask.completedBy.toBase58(), completed_bounty: parseInt(rebalanceIntent.cancelRebalanceTask.completedBounty.toString()), completed_time: parseInt(rebalanceIntent.cancelRebalanceTask.completedTime.toString()), }; let tasks = [ placeholderTask, finishPriceUpdateTask, auctionCreationTask, mintBasketTask, cancelRebalanceTask, ...tokenSettlementTasks, ...priceUpdateTasks, ] let depositData: DepositData = { tokens: rebalanceIntent.tokens.map(token => ({ mint: token.mint.toBase58(), amount: parseInt(token.amount.toString()), })), } let priceUpdatesData: PriceUpdatesData = { tokens: rebalanceIntent.priceUpdateTasks.map((task, index) => ({ mint: rebalanceIntent.tokens[index].mint.toBase58(), updated: parseInt(task.completedTime.toString()) > 0 ? true : false, price: fractionToDecimal(rebalanceIntent.tokens[index].price.price).toNumber(), conf: fractionToDecimal(rebalanceIntent.tokens[index].price.conf).toNumber(), expiration: parseInt(task.completedTime.toString()) + 60, update_time: parseInt(task.completedTime.toString()), })), } let auctionData: AuctionData = { auction_stages: rebalanceIntent.auctions.map(auction => ({ start_time: parseInt(auction.startTime.toString()), end_time: parseInt(auction.endTime.toString()), })), tokens: rebalanceIntent.tokens.map(token => ({ mint: token.mint.toBase58(), amount: parseInt(token.amount.toString()), target_amount: parseInt(token.targetAmount.toString()), price: fractionToDecimal(token.price.price).toNumber(), conf: fractionToDecimal(token.price.conf).toNumber(), })), } let mintData: MintData = { mintAmount: 0, mintValue: 0, fees: { host: 0, creator: 0, managers: 0, symmetry: 0, basket: 0, }, tokens: auctionData.tokens.map(token => ({ mint: token.mint, contribution_amount: 0, remaining_amount: 0, })), } if (basket) { let minRatio = 1; for (let i = 0; i < basket.composition.length; i++) { if (basket.composition[i].amount == 0) continue; let ratio = auctionData.tokens[i].amount / basket.composition[i].amount; minRatio = Math.min(minRatio, ratio); } let mintValue = 0; let remainingValue = 0; let mintAmount = Math.floor(basket.supply_outstanding * minRatio); for (let i = 0; i < basket.composition.length; i++) { let contributionAmount = Math.floor(basket.composition[i].amount * minRatio); contributionAmount = Math.min(contributionAmount, auctionData.tokens[i].amount); mintData.tokens[i].contribution_amount = contributionAmount; mintData.tokens[i].remaining_amount = auctionData.tokens[i].amount - contributionAmount; mintValue += contributionAmount * auctionData.tokens[i].price; remainingValue += (auctionData.tokens[i].amount - contributionAmount) * auctionData.tokens[i].price; } mintData.mintValue = mintValue; mintData.mintAmount = mintAmount; mintData.fees.host = Math.floor(mintValue * rebalanceIntent.basketFeeSettings.hostDepositFeeBps / HUNDRED_PERCENT_BPS); mintData.fees.creator = Math.floor(mintValue * rebalanceIntent.basketFeeSettings.creatorDepositFeeBps / HUNDRED_PERCENT_BPS); mintData.fees.managers = Math.floor(mintValue * rebalanceIntent.basketFeeSettings.managersDepositFeeBps / HUNDRED_PERCENT_BPS); mintData.fees.basket = Math.floor(mintValue * rebalanceIntent.basketFeeSettings.basketDepositFeeBps / HUNDRED_PERCENT_BPS); mintData.fees.symmetry = Math.floor(mintValue * 10 / HUNDRED_PERCENT_BPS); mintData.mintAmount -= mintData.fees.host + mintData.fees.creator + mintData.fees.managers + mintData.fees.symmetry + mintData.fees.basket; } let redeemData: RedeemData = { tokens: rebalanceIntent.tokens.map(token => ({ mint: token.mint.toBase58(), amount: parseInt(token.amount.toString()), })).filter(token => token.amount > 0), } let bountyData: ClaimBountyData = { bounty_mint: rebalanceIntent.bounty.bountyMint.toBase58(), keepers: [], unused_bounty: { pubkey: rebalanceIntent.bounty.bountyDepositor.toBase58(), bounty_amount: parseInt(rebalanceIntent.bounty.bountyLeft.toString()), }, rent: { pubkey: rebalanceIntent.rentPayer.toBase58(), sol_amount: rebalanceIntent.solBalance ?? 0, }, } for (let i = 0; i < tasks.length; i++) { if (tasks[i].completed_bounty > 0) { let indexOf = bountyData.keepers.findIndex(keeper => keeper.pubkey == tasks[i].completed_by); if (indexOf == -1) { bountyData.keepers.push({ pubkey: tasks[i].completed_by, bounty_amount: tasks[i].completed_bounty, }); bountyData.unused_bounty.bounty_amount -= tasks[i].completed_bounty; } else { bountyData.keepers[indexOf].bounty_amount += tasks[i].completed_bounty; } } } let formatted: FormattedRebalanceIntent = { pubkey: rebalanceIntent.ownAddress!.toBase58(), sol_balance: rebalanceIntent.solBalance ?? 0, basket: rebalanceIntent.basket.toBase58(), owner: rebalanceIntent.owner.toBase58(), rent_payer: rebalanceIntent.rentPayer.toBase58(), rebalance_type: REBALANCE_TYPE_STRINGS.get(rebalanceIntent.rebalanceType) ?? "basket_custom", current_action: REBALANCE_ACTION_STRINGS.get(rebalanceIntent.currentAction) ?? "not_active", withdraw_params_burn_amount: parseInt(rebalanceIntent.withdrawParamsBurnAmount.toString()), withdraw_params_amount_wo_fees: parseInt(rebalanceIntent.withdrawParamsAmountWoFees.toString()), withdraw_params_keep_tokens_bitmask: rebalanceIntent.withdrawParamsKeepTokensBitmask, withdraw_params_keep_all_tokens: rebalanceIntent.withdrawParamsKeepAllTokens == 1 ? true : false, basket_fee_settings: { host_deposit_fee_bps: rebalanceIntent.basketFeeSettings.hostDepositFeeBps, host_withdraw_fee_bps: rebalanceIntent.basketFeeSettings.hostWithdrawFeeBps, host_management_fee_bps: rebalanceIntent.basketFeeSettings.hostManagementFeeBps, host_performance_fee_bps: rebalanceIntent.basketFeeSettings.hostPerformanceFeeBps, creator_deposit_fee_bps: rebalanceIntent.basketFeeSettings.creatorDepositFeeBps, creator_withdraw_fee_bps: rebalanceIntent.basketFeeSettings.creatorWithdrawFeeBps, creator_management_fee_bps: rebalanceIntent.basketFeeSettings.creatorManagementFeeBps, creator_performance_fee_bps: rebalanceIntent.basketFeeSettings.creatorPerformanceFeeBps, managers_deposit_fee_bps: rebalanceIntent.basketFeeSettings.managersDepositFeeBps, managers_withdraw_fee_bps: rebalanceIntent.basketFeeSettings.managersWithdrawFeeBps, managers_management_fee_bps: rebalanceIntent.basketFeeSettings.managersManagementFeeBps, managers_performance_fee_bps: rebalanceIntent.basketFeeSettings.managersPerformanceFeeBps, basket_deposit_fee_bps: rebalanceIntent.basketFeeSettings.basketDepositFeeBps, basket_withdraw_fee_bps: rebalanceIntent.basketFeeSettings.basketWithdrawFeeBps, modification_delay: parseInt(rebalanceIntent.basketFeeSettings.modificationDelay.toString()), updated_at: 0, }, execution_start_time: parseInt(rebalanceIntent.executionStartTime.toString()), rebalance_threshold_slippage_bps: rebalanceIntent.rebalanceThresholdSlippageBps, per_trade_rebalance_threshold_slippage_bps: rebalanceIntent.perTradeRebalanceThresholdSlippageBps, initial_tvl: fractionToDecimal(rebalanceIntent.initialTvl).toNumber(), auction_update_timestamp: parseInt(rebalanceIntent.auctionUpdateTimestamp.toString()), auctions: rebalanceIntent.auctions.map(auction => ({ start_time: parseInt(auction.startTime.toString()), end_time: parseInt(auction.endTime.toString()), })), tokens: rebalanceIntent.tokens.map(token => ({ mint: token.mint.toBase58(), amount: parseInt(token.amount.toString()), target_amount: parseInt(token.targetAmount.toString()), price: { price: fractionToDecimal(token.price.price).toNumber(), conf: fractionToDecimal(token.price.conf).toNumber(), update_time: parseInt(token.price.updateTime.toString()), }, keep_token: token.keepToken == 1 ? true : false, })), last_action_timestamp: parseInt(rebalanceIntent.lastActionTimestamp.toString()), bounty: { bounty_depositor: rebalanceIntent.bounty.bountyDepositor.toBase58(), bounty_mint: rebalanceIntent.bounty.bountyMint.toBase58(), bounty_per_price_update_task: { min_bounty: parseInt(rebalanceIntent.bounty.bountyPerPriceUpdateTask.minBounty.toString()), max_bounty: parseInt(rebalanceIntent.bounty.bountyPerPriceUpdateTask.maxBounty.toString()), min_bounty_until: parseInt(rebalanceIntent.bounty.bountyPerPriceUpdateTask.minBountyUntil.toString()), max_bounty_after: parseInt(rebalanceIntent.bounty.bountyPerPriceUpdateTask.maxBountyAfter.toString()), }, bounty_per_task: { min_bounty: parseInt(rebalanceIntent.bounty.bountyPerTask.minBounty.toString()), max_bounty: parseInt(rebalanceIntent.bounty.bountyPerTask.maxBounty.toString()), min_bounty_until: parseInt(rebalanceIntent.bounty.bountyPerTask.minBountyUntil.toString()), max_bounty_after: parseInt(rebalanceIntent.bounty.bountyPerTask.maxBountyAfter.toString()), }, bounty_total: parseInt(rebalanceIntent.bounty.bountyTotal.toString()), bounty_left: parseInt(rebalanceIntent.bounty.bountyLeft.toString()), }, bounty_adjustment_amount: parseInt(rebalanceIntent.bountyAdjustmentAmount.toString()), placeholder_task: placeholderTask, price_update_tasks: priceUpdateTasks, finish_price_update_task: finishPriceUpdateTask, auction_creation_task: auctionCreationTask, mint_basket_task: mintBasketTask, cancel_rebalance_task: cancelRebalanceTask, token_settlement_tasks: tokenSettlementTasks, }; let uiRebalanceIntent: UIRebalanceIntent = { rebalance_type: formatted.mint_basket_task.completed_time > 0 ? "deposit" : formatted.rebalance_type, formatted_data: formatted, chain_data: rebalanceIntent, deposit_data: null, price_updates_data: null, auction_data: null, mint_data: null, redeem_data: null, claim_bounty_data: null, } if (formatted.current_action == "deposit_tokens") { uiRebalanceIntent.deposit_data = depositData; } if (formatted.current_action == "update_prices") { uiRebalanceIntent.price_updates_data = priceUpdatesData; } if (formatted.current_action == "auction") { let now = Math.floor(Date.now() / 1000); if (now <= auctionData.auction_stages[2].end_time) { uiRebalanceIntent.auction_data = auctionData; } else { if (formatted.rebalance_type == "deposit") { uiRebalanceIntent.mint_data = mintData; } else if (formatted.rebalance_type == "withdraw" && redeemData.tokens.length > 0) { uiRebalanceIntent.redeem_data = redeemData; } else { uiRebalanceIntent.claim_bounty_data = bountyData; } } } return uiRebalanceIntent; } export async function fetchRebalanceIntent( connection: Connection, rebalanceIntentAddress: PublicKey, ): Promise { const rebalanceIntentAi = await connection.getAccountInfo(rebalanceIntentAddress); if (!rebalanceIntentAi) throw new Error("Rebalance intent not found"); let rebalanceIntent: RebalanceIntent = RebalanceIntentLayout.decode(rebalanceIntentAi.data.slice(8)); rebalanceIntent.ownAddress = rebalanceIntentAddress; rebalanceIntent.solBalance = rebalanceIntentAi.lamports ?? 0; let basket = (await fetchBasket(connection, rebalanceIntent.basket)).formatted; return formatRebalanceIntent(rebalanceIntent, basket); } export async function fetchRebalanceIntentsMultiple( connection: Connection, rebalanceIntentAddresses: PublicKey[], ): Promise> { let multipleAccountsInfo = await getMultipleAccountsInfoBatched(connection, rebalanceIntentAddresses); let rebalanceIntents: RebalanceIntent[] = rebalanceIntentAddresses.map(address => { let ai = multipleAccountsInfo.get(address.toBase58()); if (!ai) return null; return { ...RebalanceIntentLayout.decode(ai.data.slice(8)), ownAddress: address, solBalance: ai.lamports ?? 0 } }).filter(rebalanceIntent => rebalanceIntent !== null); let formattedRebalanceIntents: UIRebalanceIntent[] = rebalanceIntents.map(rebalanceIntent => formatRebalanceIntent(rebalanceIntent)); let rebalanceIntentsMap: Map = new Map(); for (let rebalanceIntent of formattedRebalanceIntents) rebalanceIntentsMap.set(rebalanceIntent.formatted_data.pubkey, rebalanceIntent); return rebalanceIntentsMap; } export interface RebalanceIntentFilter { type: "basket" | "owner"; pubkey: string; } export async function fetchRebalanceIntents( connection: Connection, filter?: RebalanceIntentFilter, ): Promise { let accountFilters: GetProgramAccountsFilter[] = [ { dataSize: 8 + RebalanceIntentLayout.getSpan() }, ]; if (filter?.type === "basket") { accountFilters.push({ memcmp: { offset: 8, bytes: filter.pubkey } }); } else if (filter?.type === "owner") { accountFilters.push({ memcmp: { offset: 8 + 32, bytes: filter.pubkey } }); } const accounts: GetProgramAccountsResponse = await connection .getProgramAccounts( BASKETS_V3_PROGRAM_ID, { commitment: "confirmed", filters: accountFilters, encoding: 'base64' } ); let basket: FormattedBasket | undefined; if (filter && filter.type === "basket") { basket = (await fetchBasket(connection, new PublicKey(filter.pubkey))).formatted; } let rebalanceIntents: UIRebalanceIntent[] = accounts.map(account => { let rebalanceIntent: RebalanceIntent = RebalanceIntentLayout.decode(account.account.data.slice(8)); rebalanceIntent.ownAddress = account.pubkey; rebalanceIntent.solBalance = account.account.lamports ?? 0; return formatRebalanceIntent(rebalanceIntent, basket); }); return rebalanceIntents; } class RebalanceIntentRustClass { self: RebalanceIntent; constructor(rebalanceIntent: RebalanceIntent) { this.self = rebalanceIntent; } findTokenIndex(mint: PublicKey): number | undefined { for (let i = 0; i < this.self.priceUpdateTasks.length; i++) if (this.self.tokens[i].mint.equals(mint)) return i; return undefined; } getSelfTvl(): Fraction { let selfTvl = { high: new BN(0), low: new BN(0) }; for (let i = 0; i < this.self.priceUpdateTasks.length; i++) { if (this.self.priceUpdateTasks[i].completedTime.isZero()) { continue; } const tokenAuction = this.self.tokens[i]; const tokenPrice = tokenAuction.price; // contains .price (mid) if (!tokenPrice || !tokenAuction.amount) continue; // price * amount const tokenValue = fractionMul(tokenPrice.price, { high: new BN(tokenAuction.amount), low: new BN(0) }); selfTvl = fractionAdd(selfTvl, tokenValue); } return selfTvl; } getBasketTvlAndWeightSum(basket: Basket): { basketTvl: Fraction, weightSum: number } { let basketTvl = { high: new BN(0), low: new BN(0) }; let weightSum = 0; for (let i = 0; i < basket.numTokens; i++) { const basketToken = basket.composition[i]; weightSum += basketToken.weight; if (basketToken.amount.isZero()) { continue; } const tokenIndex = this.findTokenIndex(basketToken.mint); if (tokenIndex === undefined) throw new Error("TokenNotFound"); if (this.self.priceUpdateTasks[tokenIndex].completedTime.isZero()) throw new Error("PriceUpdateNotCompleted"); const tokenPrice = this.self.tokens[tokenIndex].price; // price * amount const tokenValue = fractionMul(tokenPrice.price, { high: new BN(basketToken.amount), low: new BN(0) }); basketTvl = fractionAdd(basketTvl, tokenValue); } return { basketTvl, weightSum }; } private getPriceChange(price: OraclePriceOnChain, timeSinceAuctionStart: BN, auctionDuration: BN): Fraction { // price_change = conf * 2 * time_since_auction_start / auction_duration if (auctionDuration.isZero()) { throw new Error("AuctionNotLive"); } // conf * 2 const confTimes2 = fractionMul(price.conf, { high: new BN(2), low: new BN(0) }); // conf * 2 * time_since_auction_start const confTimes2TimesTime = fractionMul(confTimes2, { high: new BN(timeSinceAuctionStart), low: new BN(0) }); // (conf * 2 * time_since_auction_start) / auction_duration const auctionDurationFraction = { high: new BN(auctionDuration), low: new BN(0) }; return fractionDiv(confTimes2TimesTime, auctionDurationFraction); } private getSellPrice(price: OraclePriceOnChain, timeSinceAuctionStart: BN, auctionDuration: BN): Fraction { // sell_price = high() - get_price_change() // high() = price + conf const high = fractionAdd(price.price, price.conf); const priceChange = this.getPriceChange(price, timeSinceAuctionStart, auctionDuration); return fractionSub(high, priceChange); } private getBuyPrice(price: OraclePriceOnChain, timeSinceAuctionStart: BN, auctionDuration: BN): Fraction { // buy_price = low() + get_price_change() // low() = price - conf const low = fractionSub(price.price, price.conf); const priceChange = this.getPriceChange(price, timeSinceAuctionStart, auctionDuration); return fractionAdd(low, priceChange); } private getExchangeRate( outToken: TokenAuction, inToken: TokenAuction, timeSinceAuctionStart: BN, auctionDuration: BN, ): { amountToSell: BN, amountToBuy: BN, exchangeValue: Fraction } { const outAmount = outToken.amount; const outTargetAmount = outToken.targetAmount; const inAmount = inToken.amount; const inTargetAmount = inToken.targetAmount; if (outAmount.lte(outTargetAmount)) throw new Error("InvalidSwap"); if (inAmount.gte(inTargetAmount)) throw new Error("InvalidSwap"); const maxAmountToSell: BN = outAmount.sub(outTargetAmount); const maxAmountToBuy: BN = inTargetAmount.sub(inAmount); const sellRate: Fraction = this.getSellPrice(outToken.price, timeSinceAuctionStart, auctionDuration); const buyRate: Fraction = this.getBuyPrice(inToken.price, timeSinceAuctionStart, auctionDuration); // sellValue = sellRate * maxAmountToSell const sellValue: Fraction = fractionMul(sellRate, { high: new BN(maxAmountToSell), low: new BN(0) }); // buyValue = buyRate * maxAmountToBuy const buyValue: Fraction = fractionMul(buyRate, { high: new BN(maxAmountToBuy), low: new BN(0) }); let sellSideLimits: boolean; if (fractionLte(sellValue, buyValue)) { sellSideLimits = true; } else if (!outTargetAmount.isZero()) { sellSideLimits = false; } else { // adjustedSellValue = sellValue * (HUNDRED_PERCENT_BPS - 100) / HUNDRED_PERCENT_BPS const adjustmentFactor = fractionDiv( { high: new BN(HUNDRED_PERCENT_BPS - 100), low: new BN(0) }, { high: new BN(HUNDRED_PERCENT_BPS), low: new BN(0) } ); const adjustedSellValue = fractionMul(sellValue, adjustmentFactor); sellSideLimits = fractionLte(adjustedSellValue, buyValue); } if (sellSideLimits) { // amountToBuy = sellValue / buyRate (rounded up) const amountToBuy = fractionRoundUp(fractionDiv(sellValue, buyRate)); return { amountToSell: maxAmountToSell, amountToBuy: amountToBuy, exchangeValue: sellValue, }; } else { // amountToSell = buyValue / sellRate (rounded down) const amountToSell = fractionRoundDown(fractionDiv(buyValue, sellRate)); return { amountToSell: amountToSell, amountToBuy: maxAmountToBuy, exchangeValue: buyValue, }; } } getSwapAmounts( inTokenIndex: number, outTokenIndex: number, timeSinceAuctionStart: BN, auctionDuration: BN, ): { amountToSell: BN, amountToBuy: BN, exchangeValue: Fraction } { if (inTokenIndex === outTokenIndex) throw new Error("InvalidSwap"); if (this.self.priceUpdateTasks[inTokenIndex].completedTime.isZero()) throw new Error("PriceUpdateNotCompleted"); if (this.self.priceUpdateTasks[outTokenIndex].completedTime.isZero()) throw new Error("PriceUpdateNotCompleted"); const inToken = this.self.tokens[inTokenIndex]; const outToken = this.self.tokens[outTokenIndex]; const exchangeRate = this.getExchangeRate( outToken, inToken, timeSinceAuctionStart, auctionDuration, ); let basketBuys = exchangeRate.amountToBuy; let basketSells = exchangeRate.amountToSell; // inValue = inTokenPrice * basketBuys const inValue = fractionMul(inToken.price.price, { high: new BN(basketBuys), low: new BN(0) }); // outValue = outTokenPrice * basketSells const outValue = fractionMul(outToken.price.price, { high: new BN(basketSells), low: new BN(0) }); // slippageFactor = (HUNDRED_PERCENT_BPS - bps) / HUNDRED_PERCENT_BPS const slippageFactor = fractionDiv( { high: new BN(HUNDRED_PERCENT_BPS - this.self.perTradeRebalanceThresholdSlippageBps), low: new BN(0) }, { high: new BN(HUNDRED_PERCENT_BPS), low: new BN(0) } ); // Check: inValue < outValue * slippageFactor const outValueWithSlippage = fractionMul(outValue, slippageFactor); if (fractionLt(inValue, outValueWithSlippage)) { throw new Error("RebalanceSlippageExceeded"); } const selfTvl = this.getSelfTvl(); // minSelfTvl = initialTvl * (HUNDRED_PERCENT_BPS - rebalanceThresholdSlippageBps) / HUNDRED_PERCENT_BPS const tvlSlippageFactor = fractionDiv( { high: new BN(HUNDRED_PERCENT_BPS - this.self.rebalanceThresholdSlippageBps), low: new BN(0) }, { high: new BN(HUNDRED_PERCENT_BPS), low: new BN(0) } ); const minSelfTvl = fractionMul(this.self.initialTvl, tvlSlippageFactor); if (fractionLt(selfTvl, minSelfTvl)) { throw new Error("RebalanceSlippageExceeded"); } return exchangeRate; } updateTargetAmounts(basket: Basket): void { this.self.auctionUpdateTimestamp = new BN(Math.floor(Date.now() / 1000)); const selfTvl = this.getSelfTvl(); const { basketTvl, weightSum } = this.getBasketTvlAndWeightSum(basket); for (let tokenIndex = 0; tokenIndex < this.self.priceUpdateTasks.length; tokenIndex++) { if (this.self.tokens[tokenIndex].mint.equals(PublicKey.default)) { continue; } if (this.self.priceUpdateTasks[tokenIndex].completedTime.isZero()) { continue; } const auctionToken = this.self.tokens[tokenIndex]; const auctionTokenPrice = auctionToken.price; const tokenPrice = auctionTokenPrice.price; if (this.self.rebalanceType === RebalanceType.Withdraw) { this.self.tokens[tokenIndex].targetAmount = new BN(0); if (auctionToken.keepToken === 1) { // targetAmount = selfTvl / tokenPrice (rounded down) * 2 const targetAmount = fractionRoundDown(fractionDiv(selfTvl, tokenPrice)); this.self.tokens[tokenIndex].targetAmount = targetAmount.mul(new BN(2)); } continue; } // Find token index in basket composition let tokenIndexInBasket: number | undefined = undefined; for (let i = 0; i < basket.numTokens; i++) { if (basket.composition[i].mint.equals(this.self.tokens[tokenIndex].mint)) { tokenIndexInBasket = i; break; } } if (tokenIndexInBasket === undefined) { this.self.tokens[tokenIndex].targetAmount = new BN(0); continue; } const basketToken = basket.composition[tokenIndexInBasket]; if (this.self.rebalanceType === RebalanceType.Deposit && basket.supplyOutstanding.gt(new BN(0))) { // targetAmount = (basketToken.amount * selfTvl) / basketTvl (rounded down) const basketTokenAmountFraction = { high: new BN(basketToken.amount), low: new BN(0) }; const numerator = fractionMul(basketTokenAmountFraction, selfTvl); const targetAmount = fractionRoundDown(fractionDiv(numerator, basketTvl)); this.self.tokens[tokenIndex].targetAmount = targetAmount; } if (this.self.rebalanceType === RebalanceType.Deposit && basket.supplyOutstanding.isZero()) { // targetValue = selfTvl * weight / weightSum // targetAmount = targetValue / tokenPrice (rounded down) const targetValue = fractionDiv( fractionMul(selfTvl, { high: new BN(basketToken.weight), low: new BN(0) }), { high: new BN(weightSum), low: new BN(0) } ); const targetAmount = fractionRoundDown(fractionDiv(targetValue, tokenPrice)); this.self.tokens[tokenIndex].targetAmount = targetAmount; } if (this.self.rebalanceType === RebalanceType.Basket || this.self.rebalanceType === RebalanceType.BasketCustom) { // targetValue = basketTvl * weight / weightSum // targetAmount = targetValue / tokenPrice (rounded down) const targetValue = fractionDiv( fractionMul(basketTvl, { high: new BN(basketToken.weight), low: new BN(0) }), { high: new BN(weightSum), low: new BN(0) } ); let targetAmount = fractionRoundDown(fractionDiv(targetValue, tokenPrice)); if (this.self.bounty.bountyMint.equals(auctionToken.mint)) { targetAmount = targetAmount.add(this.self.bountyAdjustmentAmount); } this.self.tokens[tokenIndex].targetAmount = targetAmount; } } } } /** * Returns all valid swap pairs available during the rebalance auction phase. * * **Swap direction (basket perspective):** * - `inMint` / `inAmount`: what the basket is supposed to **receive**. * - `outMint` / `outAmount`: what the basket wants to **swap** (give away). * So each pair describes an **outMint → inMint** swap. When generating swap * transactions, use this direction: swap `outMint` → `inMint` (sell outAmount of * outMint, receive inAmount of inMint). * * Only runs when the rebalance is in Auction action and the current time falls * within one of the three auction windows. For the active auction, target amounts * are refreshed if they haven't been updated since the auction started. * * Iterates over completed price-update tokens to compute exchange rates via * getSwapAmounts (using time-since-start and auction duration). Pairs with * positive in/out amounts are collected and sorted by value descending * (highest value first). * * @param rebalanceIntent - The rebalance intent state * @param basket - The basket (used to update target amounts when needed) * @returns Array of swap pairs, or empty array when not in auction or outside auction windows */ export function getSwapPairs( rebalanceIntent: RebalanceIntent, basket: Basket, ): { inMint: string; outMint: string; inAmount: number; outAmount: number; value: number; }[] { let rebalanceIntentRustClass = new RebalanceIntentRustClass(rebalanceIntent); if (rebalanceIntentRustClass.self.currentAction !== RebalanceAction.Auction) return []; const timestamp = new BN(Math.floor(Date.now() / 1000)); let currentAuction; if (rebalanceIntentRustClass.self.auctions[0].startTime.lte(timestamp) && timestamp.lt(rebalanceIntentRustClass.self.auctions[0].endTime)) currentAuction = rebalanceIntentRustClass.self.auctions[0]; else if (rebalanceIntentRustClass.self.auctions[1].startTime.lte(timestamp) && timestamp.lt(rebalanceIntentRustClass.self.auctions[1].endTime)) currentAuction = rebalanceIntentRustClass.self.auctions[1]; else if (rebalanceIntentRustClass.self.auctions[2].startTime.lte(timestamp) && timestamp.lt(rebalanceIntentRustClass.self.auctions[2].endTime)) currentAuction = rebalanceIntentRustClass.self.auctions[2]; else return []; let start_time = currentAuction.startTime; if (rebalanceIntentRustClass.self.auctionUpdateTimestamp.lt(start_time)) rebalanceIntentRustClass.updateTargetAmounts(basket); let timeSinceAuctionStart = timestamp.sub(currentAuction.startTime); let auctionDuration = currentAuction.endTime.sub(currentAuction.startTime); let allPairs: { inMint: string; outMint: string; inAmount: number; outAmount: number; value: number; }[] = []; for (let inTokenIndex = 0; inTokenIndex < rebalanceIntentRustClass.self.priceUpdateTasks.length; inTokenIndex++) { if (rebalanceIntentRustClass.self.priceUpdateTasks[inTokenIndex].completedTime.isZero()) continue; for (let outTokenIndex = 0; outTokenIndex < rebalanceIntentRustClass.self.priceUpdateTasks.length; outTokenIndex++) { if (inTokenIndex === outTokenIndex) continue; if (rebalanceIntentRustClass.self.priceUpdateTasks[outTokenIndex].completedTime.isZero()) continue; try { let exchangeRate = rebalanceIntentRustClass.getSwapAmounts( inTokenIndex, outTokenIndex, timeSinceAuctionStart, auctionDuration, ); if (parseInt(exchangeRate.amountToBuy.toString()) > 0 && parseInt(exchangeRate.amountToSell.toString()) > 0) { allPairs.push({ inMint: rebalanceIntentRustClass.self.tokens[inTokenIndex].mint.toBase58(), outMint: rebalanceIntentRustClass.self.tokens[outTokenIndex].mint.toBase58(), inAmount: parseInt(exchangeRate.amountToBuy.toString()), outAmount: parseInt(exchangeRate.amountToSell.toString()), value: fractionToDecimal(exchangeRate.exchangeValue).toNumber(), }); } } catch {} } } allPairs.sort((a, b) => b.value - a.value); return allPairs; }