import Decimal from 'decimal.js'; import { createHash } from 'crypto'; import { AccountInfo, Connection, GetProgramAccountsFilter, GetProgramAccountsResponse, PublicKey } from '@solana/web3.js'; import { BASKETS_V3_PROGRAM_ID, MAX_MANAGERS_PER_BASKET, PYTHNET_CUSTODY_PRICE_USDC_ACCOUNT, PYTHNET_CUSTODY_PRICE_WSOL_ACCOUNT } from '../constants'; import { Basket, BASKET_TYPES_STRINGS, BasketLayout, FormattedAccumulatedFees, FormattedAsset, FormattedLookupTables } from '../layouts/basket'; import { ORACLE_TYPES_STRINGS, OracleType, PYTH_USDC_ORACLE_SETTINGS, PYTH_WSOL_ORACLE_SETTINGS, } from '../layouts/oracle'; import { getMultipleAccountsInfoBatched, getMultipleAddressLookupTableAccounts } from '../txUtils'; import { PriceAggregator } from './oracles/oracle'; import { PythOracle } from './oracles/pythOracle'; import { getNextTickArrayStartIndex, getPdaTickArrayAddress, getTickArrayStartIndexByTick, PoolState } from './oracles/raydiumClmmOracle'; import { fractionToDecimal } from '../layouts/fraction'; import { FormattedBasket, FormattedCreatorSettings, FormattedManagersSettings, FormattedFeeSettings, FormattedScheduleSettings, FormattedAutomationSettings, FormattedLpSettings, FormattedMetadataSettings, FormattedDepositsSettings, FormattedForceRebalanceSettings, FormattedCustomRebalanceSettings, FormattedAddTokenSettings, FormattedUpdateWeightsSettings, FormattedMakeDirectSwapSettings, } from '../layouts/basket'; // export async function computeTokenMintsHash(mints: string[]): Promise { // let hashBuffer = new Uint8Array(32) // for (const mint of mints) { // const mintBytes = new PublicKey(mint).toBytes() // const combined = new Uint8Array(hashBuffer.length + mintBytes.length) // combined.set(hashBuffer, 0) // combined.set(mintBytes, hashBuffer.length) // const hashArrayBuffer = await crypto.subtle.digest("SHA-256", combined) // hashBuffer = new Uint8Array(hashArrayBuffer) // } // return Buffer.from(hashBuffer) // } export function computeTokenMintsHash(mints: string[]): number[] { let hashBuffer = Buffer.alloc(32); for (const mint of mints) { hashBuffer = createHash('sha256').update(Buffer.concat([hashBuffer, new PublicKey(mint).toBytes()])).digest(); } return Array.from(hashBuffer); } export function addFieldsToBasket(basket: Basket): Basket { let metadataSettings = basket.settings.metadata; let metadata = { symbol: Buffer.from(metadataSettings.symbol.slice(0, metadataSettings.symbolLength)).toString(), name: Buffer.from(metadataSettings.name.slice(0, metadataSettings.nameLength)).toString(), uri: Buffer.from(metadataSettings.uri.slice(0, metadataSettings.uriLength)).toString() } basket.composition = basket.composition.slice(0, basket.numTokens); for (let i = 0; i < basket.numTokens; i++) { let agg = basket.composition[i].oracleAggregator; basket.composition[i].oracleAggregator.oracles = agg.oracles.slice(0, agg.numOracles); for (let j = 0; j < agg.numOracles; j++) { let lutIds = agg.oracles[j].accountsToLoadLutIds; let lutIndices = agg.oracles[j].accountsToLoadLutIndices; basket.composition[i].oracleAggregator.oracles[j].accountsToLoadLutIds = lutIds.slice(0, agg.oracles[j].oracleSettings.numRequiredAccounts); basket.composition[i].oracleAggregator.oracles[j].accountsToLoadLutIndices = lutIndices.slice(0, agg.oracles[j].oracleSettings.numRequiredAccounts); } } let totalManagers = MAX_MANAGERS_PER_BASKET; while (totalManagers > 0 && basket.settings.managers.managers[totalManagers - 1].equals(PublicKey.default)) { totalManagers--; } basket.settings.managers.managers = basket.settings.managers.managers.slice(0, totalManagers); basket.settings.managers.managersWeightBps = basket.settings.managers.managersWeightBps.slice(0, totalManagers); basket = {...basket, metadata: metadata}; let creator_settings: FormattedCreatorSettings = { creator: basket.settings.creator.toBase58(), } let manager_settings: FormattedManagersSettings = { managers: basket.settings.managers.managers.map((manager, i) => ({ pubkey: manager.toBase58(), fee_split_weight_bps: basket.settings.managers.managersWeightBps[i], authorities: { managers: (((2 ** i) & basket.settings.managersAuthorityBitmask) != 0), fees: (((2 ** i) & basket.settings.feesAuthorityBitmask) != 0), schedule: (((2 ** i) & basket.settings.scheduleAuthorityBitmask) != 0), automation: (((2 ** i) & basket.settings.automationAuthorityBitmask) != 0), lp: (((2 ** i) & basket.settings.lpAuthorityBitmask) != 0), metadata: (((2 ** i) & basket.settings.metadataAuthorityBitmask) != 0), deposits: (((2 ** i) & basket.settings.depositsAuthorityBitmask) != 0), force_rebalance: (((2 ** i) & basket.settings.forceRebalanceAuthorityBitmask) != 0), custom_rebalance: (((2 ** i) & basket.settings.customRebalanceAuthorityBitmask) != 0), add_token: (((2 ** i) & basket.settings.addTokenIntentAuthorityBitmask) != 0), update_weights: (((2 ** i) & basket.settings.updateWeightsIntentAuthorityBitmask) != 0), make_direct_swap: (((2 ** i) & basket.settings.makeDirectSwapIntentAuthorityBitmask) != 0), }, })), modification_delay: parseInt(basket.settings.managers.modificationDelay.toString()), updated_at: parseInt(basket.settings.managersLastUpdateTimestamp.toString()), } let fee_settings: FormattedFeeSettings = { host_deposit_fee_bps: basket.settings.fees.hostDepositFeeBps, host_withdraw_fee_bps: basket.settings.fees.hostWithdrawFeeBps, host_management_fee_bps: basket.settings.fees.hostManagementFeeBps, host_performance_fee_bps: basket.settings.fees.hostPerformanceFeeBps, creator_deposit_fee_bps: basket.settings.fees.creatorDepositFeeBps, creator_withdraw_fee_bps: basket.settings.fees.creatorWithdrawFeeBps, creator_management_fee_bps: basket.settings.fees.creatorManagementFeeBps, creator_performance_fee_bps: basket.settings.fees.creatorPerformanceFeeBps, managers_deposit_fee_bps: basket.settings.fees.managersDepositFeeBps, managers_withdraw_fee_bps: basket.settings.fees.managersWithdrawFeeBps, managers_management_fee_bps: basket.settings.fees.managersManagementFeeBps, managers_performance_fee_bps: basket.settings.fees.managersPerformanceFeeBps, basket_deposit_fee_bps: basket.settings.fees.basketDepositFeeBps, basket_withdraw_fee_bps: basket.settings.fees.basketWithdrawFeeBps, modification_delay: parseInt(basket.settings.fees.modificationDelay.toString()), updated_at: parseInt(basket.settings.feesLastUpdateTimestamp.toString()), } let schedule_settings: FormattedScheduleSettings = { cycle_start_time: parseInt(basket.settings.schedule.cycleStartTime.toString()), cycle_duration: parseInt(basket.settings.schedule.cycleDuration.toString()), deposits_start: parseInt(basket.settings.schedule.depositsStart.toString()), deposits_end: parseInt(basket.settings.schedule.depositsEnd.toString()), automation_start: parseInt(basket.settings.schedule.automationStart.toString()), automation_end: parseInt(basket.settings.schedule.automationEnd.toString()), management_start: parseInt(basket.settings.schedule.managementStart.toString()), management_end: parseInt(basket.settings.schedule.managementEnd.toString()), modification_delay: parseInt(basket.settings.schedule.modificationDelay.toString()), updated_at: parseInt(basket.settings.scheduleLastUpdateTimestamp.toString()), } let automation_settings: FormattedAutomationSettings = { enabled: basket.settings.automation.allowAutomation == 1 ? true : false, rebalance_slippage_threshold_bps: basket.settings.automation.rebalanceSlippageThresholdBps, per_trade_rebalance_slippage_threshold_bps: basket.settings.automation.perTradeRebalanceSlippageThresholdBps, rebalance_activation_threshold_abs_bps: basket.settings.automation.rebalanceActivationThresholdAbsBps, rebalance_activation_threshold_rel_bps: basket.settings.automation.rebalanceActivationThresholdRelBps, rebalance_activation_cooldown: parseInt(basket.settings.automation.rebalanceActivationCooldown.toString()), modification_delay: parseInt(basket.settings.automation.modificationDelay.toString()), updated_at: parseInt(basket.settings.automationLastUpdateTimestamp.toString()), } let lp_settings: FormattedLpSettings = { enabled: basket.settings.lp.allowLp == 1 ? true : false, lp_threshold_bps: basket.settings.lp.lpThresholdBps, modification_delay: parseInt(basket.settings.lp.modificationDelay.toString()), updated_at: parseInt(basket.settings.lpLastUpdateTimestamp.toString()), } let metadata_settings: FormattedMetadataSettings = { symbol: metadata.symbol, name: metadata.name, uri: metadata.uri, modification_delay: parseInt(basket.settings.metadata.modificationDelay.toString()), updated_at: parseInt(basket.settings.metadataLastUpdateTimestamp.toString()), } let deposits_settings: FormattedDepositsSettings = { enabled: basket.settings.depositsAreAllowed == 1 ? true : false, } let force_rebalance_settings: FormattedForceRebalanceSettings = { enabled: basket.settings.forceRebalanceIsAllowed == 1 ? true : false, modification_delay: parseInt(basket.settings.forceRebalanceModificationDelay.toString()), updated_at: parseInt(basket.settings.forceRebalanceLastUpdateTimestamp.toString()), } let custom_rebalance_settings: FormattedCustomRebalanceSettings = { enabled: basket.settings.customRebalanceIsAllowed == 1 ? true : false, modification_delay: parseInt(basket.settings.customRebalanceModificationDelay.toString()), updated_at: parseInt(basket.settings.customRebalanceLastUpdateTimestamp.toString()), } let add_token_settings: FormattedAddTokenSettings = { modification_delay: parseInt(basket.settings.addTokenDelay.toString()), updated_at: parseInt(basket.settings.addTokenLastUpdateTimestamp.toString()), } let update_weights_settings: FormattedUpdateWeightsSettings = { modification_delay: parseInt(basket.settings.updateWeightsDelay.toString()), updated_at: parseInt(basket.settings.updateWeightsLastUpdateTimestamp.toString()), } let make_direct_swap_settings: FormattedMakeDirectSwapSettings = { modification_delay: parseInt(basket.settings.makeDirectSwapDelay.toString()), updated_at: parseInt(basket.settings.makeDirectSwapLastUpdateTimestamp.toString()), } let accumulated_fees: FormattedAccumulatedFees = { symmetry_fees: parseInt(basket.accumulatedFees.symmetryFees.toString()), creator_fees: parseInt(basket.accumulatedFees.creatorFees.toString()), host_fees: parseInt(basket.accumulatedFees.hostFees.toString()), managers_fees: parseInt(basket.accumulatedFees.managersFees.toString()), }; let lookup_tables: FormattedLookupTables = { active: [ { pubkey: basket.lookupTables.active[0].toBase58(), contents: basket.lutPubkeys?.[0]?.state.addresses.map(address => address.toBase58()) || [], }, { pubkey: basket.lookupTables.active[1].toBase58(), contents: basket.lutPubkeys?.[1]?.state.addresses.map(address => address.toBase58()) || [], }, ], temp: [ { pubkey: basket.lookupTables.temp[0].toBase58(), contents: basket.lutPubkeys?.[2]?.state.addresses.map(address => address.toBase58()) || [], }, { pubkey: basket.lookupTables.temp[1].toBase58(), contents: basket.lutPubkeys?.[3]?.state.addresses.map(address => address.toBase58()) || [], }, ], }; let composition: FormattedAsset[] = basket.composition.map(asset => ({ mint: asset.mint.toBase58(), amount: parseInt(asset.amount.toString()), weight: asset.weight, active: asset.active == 1 ? true : false, oracle_aggregator: { num_oracles: asset.oracleAggregator.numOracles, min_oracles_thresh: asset.oracleAggregator.minOraclesThresh, oracles: asset.oracleAggregator.oracles.map(oracle => ({ oracle_settings: { oracle_type: ORACLE_TYPES_STRINGS.get(oracle.oracleSettings.oracleType) ?? "example", account_lut_id: oracle.accountsToLoadLutIds[0], account_lut_index: oracle.accountsToLoadLutIndices[0], account: lookup_tables.active[oracle.accountsToLoadLutIds[0]].contents[oracle.accountsToLoadLutIndices[0]], num_required_accounts: oracle.oracleSettings.numRequiredAccounts, weight: oracle.oracleSettings.weight, is_required: oracle.oracleSettings.isRequired == 1 ? true : false, conf_thresh_bps: oracle.oracleSettings.confThreshBps, volatility_thresh_bps: oracle.oracleSettings.volatilityThreshBps, max_slippage_bps: oracle.oracleSettings.maxSlippageBps, min_liquidity: parseInt(oracle.oracleSettings.minLiquidity.toString()), staleness_thresh: parseInt(oracle.oracleSettings.stalenessThresh.toString()), staleness_conf_rate_bps: oracle.oracleSettings.stalenessConfRateBps, token_decimals: oracle.oracleSettings.tokenDecimals, twap_seconds_ago: parseInt(oracle.oracleSettings.twapSecondsAgo.toString()), twap_secondary_seconds_ago: parseInt(oracle.oracleSettings.twapSecondarySecondsAgo.toString()), quote: oracle.oracleSettings.quote == 0 ? "usdc" : "wsol", side: oracle.oracleSettings.side == 0 ? "base" : "quote", }, accounts_to_load_lut_ids: oracle.accountsToLoadLutIds.map(id => id), accounts_to_load_lut_indices: oracle.accountsToLoadLutIndices.map(index => index), })), min_conf_bps: asset.oracleAggregator.minConfBps, conf_thresh_bps: asset.oracleAggregator.confThreshBps, conf_multiplier: fractionToDecimal(asset.oracleAggregator.confMultiplier).toNumber(), }, })); let formatted: FormattedBasket = { pubkey: basket.ownAddress.toBase58(), name: basket.metadata?.name || "", symbol: basket.metadata?.symbol || "", uri: basket.metadata?.uri || "", version: basket.version, own_address: basket.ownAddress.toBase58(), mint: basket.mint.toBase58(), supply_outstanding: parseInt(basket.supplyOutstanding.toString()), creator: basket.settings.creator.toBase58(), host: basket.settings.host.toBase58(), basket_type: BASKET_TYPES_STRINGS.get(basket.settings.basketType) ?? "private", bounty_mint: basket.settings.bountyMint.toBase58(), bounty_balance: parseInt(basket.settings.bountyBalance.toString()), start_price: fractionToDecimal(basket.settings.startPrice).toNumber(), high_watermark: fractionToDecimal(basket.settings.highWaterMark).toNumber(), active_rebalance: parseInt(basket.settings.activeRebalance.toString()), active_withdraws: parseInt(basket.settings.activeWithdraws.toString()), active_managements: parseInt(basket.settings.activeManagements.toString()), last_automation_execution_timestamp: parseInt(basket.settings.lastAutomationExecutionTimestamp.toString()), creator_settings: creator_settings, manager_settings: manager_settings, fee_settings: fee_settings, schedule_settings: schedule_settings, automation_settings: automation_settings, lp_settings: lp_settings, metadata_settings: metadata_settings, deposits_settings: deposits_settings, force_rebalance_settings: force_rebalance_settings, custom_rebalance_settings: custom_rebalance_settings, add_token_settings: add_token_settings, update_weights_settings: update_weights_settings, make_direct_swap_settings: make_direct_swap_settings, accumulated_fees: accumulated_fees, lookup_tables: lookup_tables, composition: composition, } basket = {...basket, formatted: formatted}; return basket; } export async function addLutsToBaskets( connection: Connection, baskets: Basket[], ): Promise { let allLutPubkeys: PublicKey[][] = []; for (let basket of baskets) allLutPubkeys.push([...basket.lookupTables.active, ...basket.lookupTables.temp]); let allInfos = (await getMultipleAddressLookupTableAccounts(connection, [allLutPubkeys]))[0]; for (let i = 0; i < baskets.length; i++) { baskets[i].lutPubkeys = allInfos[i]; } return baskets; } export async function fetchBasket( connection: Connection, basketAddress: PublicKey, ): Promise { const basketAi = await connection.getAccountInfo(basketAddress); if (!basketAi) throw new Error("Basket not found"); let basket = BasketLayout.decode(basketAi.data.slice(8)); basket = (await addLutsToBaskets(connection, [basket]))[0]; basket = addFieldsToBasket(basket); return basket; } export async function fetchBasketsMultiple( connection: Connection, basketAddresses: PublicKey[], ): Promise> { let multipleAccountsInfo = await getMultipleAccountsInfoBatched(connection, basketAddresses); let baskets: Basket[] = basketAddresses.map(address => { let ai = multipleAccountsInfo.get(address.toBase58()); if (!ai) return null; return BasketLayout.decode(ai.data.slice(8)) }).filter(basket => basket !== null); baskets = await addLutsToBaskets(connection, baskets); baskets = baskets.map(basket => addFieldsToBasket(basket)); let basketsMap: Map = new Map(); for (let basket of baskets) { basketsMap.set(basket.ownAddress.toBase58(), basket); } return basketsMap; } export interface BasketFilter { type: "host" | "creator" | "manager"; pubkey: string; } export async function fetchBaskets( connection: Connection, filter?: BasketFilter, ): Promise { // Managers array begins at this offset in Basket account data. const MANAGERS_ARRAY_OFFSET = 8 + 1 + 32 + 32 + 8 + 32 + 32 + 1 + 32 + 8 + 16 + 16 + 8 + 8 + 8 + 8; const MANAGER_PUBKEY_SIZE = 32; if (filter?.type === "manager") { try { const managerPubkey = new PublicKey(filter.pubkey); const managerBytes = Buffer.from(managerPubkey.toBytes()); const managerSliceLength = MAX_MANAGERS_PER_BASKET * MANAGER_PUBKEY_SIZE; // Request only manager bytes for each basket (single GPA), then filter locally. const managerSlices = await connection.getProgramAccounts(BASKETS_V3_PROGRAM_ID, { commitment: "confirmed", filters: [{ dataSize: 8 + BasketLayout.getSpan() }], dataSlice: { offset: MANAGERS_ARRAY_OFFSET, length: managerSliceLength, }, encoding: 'base64', }); const matchedPubkeys: PublicKey[] = []; for (const basketAccount of managerSlices) { const managersData = basketAccount.account.data; for (let i = 0; i < MAX_MANAGERS_PER_BASKET; i++) { const start = i * MANAGER_PUBKEY_SIZE; const end = start + MANAGER_PUBKEY_SIZE; if (managersData.subarray(start, end).equals(managerBytes)) { matchedPubkeys.push(basketAccount.pubkey); break; } } } if (matchedPubkeys.length === 0) { return []; } const matchedAccountsInfo = await getMultipleAccountsInfoBatched(connection, matchedPubkeys); const accounts: AccountInfo[] = []; for (const pubkey of matchedPubkeys) { const accountInfo = matchedAccountsInfo.get(pubkey.toBase58()); if (accountInfo) { accounts.push(accountInfo); } } let baskets: Basket[] = accounts.map(account => BasketLayout.decode(account.data.slice(8))); baskets = await addLutsToBaskets(connection, baskets); baskets = baskets.map(basket => addFieldsToBasket(basket)); return baskets; } catch (error) { // Fallback for RPC providers that might not support/serve this path reliably. } } let filters: GetProgramAccountsFilter[] = [{ dataSize: 8 + BasketLayout.getSpan() }]; if (filter?.type === "creator" || filter?.type === "host") { filters.push({ memcmp: { offset: filter?.type === "creator" ? 8 + 1 + 32 + 32 + 8 : 8 + 1 + 32 + 32 + 8 + 32, bytes: filter.pubkey } }); } let accounts = await connection.getProgramAccounts(BASKETS_V3_PROGRAM_ID, { commitment: "confirmed", filters: filters, encoding: 'base64' }); let baskets: Basket[] = accounts.map(account => BasketLayout.decode(account.account.data.slice(8))); baskets = await addLutsToBaskets(connection, baskets); baskets = baskets.map(basket => addFieldsToBasket(basket)); return baskets; } export async function fetchBasketOracleAccountInfos( connection: Connection, basket: Basket, ): Promise | null>> { let allKeys: PublicKey[] = []; for (let i = 0; i < basket.numTokens; i++) { let agg = basket.composition[i].oracleAggregator; for (let j = 0; j < agg.numOracles; j++) { const oracleData = agg.oracles[j]; for (let k = 0; k < oracleData.oracleSettings.numRequiredAccounts; k++) { const lutId = oracleData.accountsToLoadLutIds[k]; const lutIdx = oracleData.accountsToLoadLutIndices[k]; if (lutId === 0 && lutIdx === 0) continue; if (!basket.lutPubkeys) continue; allKeys.push(basket.lutPubkeys[lutId].state.addresses[lutIdx]); } } } allKeys.push(PYTHNET_CUSTODY_PRICE_WSOL_ACCOUNT); allKeys.push(PYTHNET_CUSTODY_PRICE_USDC_ACCOUNT); // load known accounts const baseInfos = await getMultipleAccountsInfoBatched(connection, allKeys); // derive and bind tick array PDAs for Raydium CLMM, then batch load them const derivedTickArrayKeys: PublicKey[] = []; for (let i = 0; i < basket.numTokens; i++) { const agg = basket.composition[i].oracleAggregator; for (let j = 0; j < agg.numOracles; j++) { const oracleData = agg.oracles[j]; if (oracleData.oracleSettings.oracleType !== OracleType.RaydiumClmm) continue; const lutId = oracleData.accountsToLoadLutIds[0]; const lutIdx = oracleData.accountsToLoadLutIndices[0]; if (!basket.lutPubkeys) continue; const poolPk = basket.lutPubkeys[lutId].state.addresses[lutIdx]; const poolAi = baseInfos.get(poolPk.toBase58()); if (!poolAi) continue; const [poolState] = PoolState.decode(poolAi.data, 8); const currStart = getTickArrayStartIndexByTick(poolState.tickCurrent, poolState.tickSpacing); const prevStart = getNextTickArrayStartIndex(currStart, poolState.tickSpacing, true); const nextStart = getNextTickArrayStartIndex(currStart, poolState.tickSpacing, false); [prevStart, currStart, nextStart].forEach(start => derivedTickArrayKeys.push(getPdaTickArrayAddress(poolPk, start)) ); } } let derivedInfos = new Map | null>(); derivedInfos = await getMultipleAccountsInfoBatched(connection, derivedTickArrayKeys); derivedInfos.forEach((v, k) => baseInfos.set(k, v)); return baseInfos; } export async function loadBasketPrice( basket: Basket, connection: Connection, ): Promise { let oracleAccountInfos = await fetchBasketOracleAccountInfos(connection, basket); let solPrice = PythOracle.fetch( PYTH_WSOL_ORACLE_SETTINGS, //@ts-ignore [oracleAccountInfos.get(PYTHNET_CUSTODY_PRICE_WSOL_ACCOUNT.toBase58())] ); let usdcPrice = PythOracle.fetch( PYTH_USDC_ORACLE_SETTINGS, //@ts-ignore [oracleAccountInfos.get(PYTHNET_CUSTODY_PRICE_USDC_ACCOUNT.toBase58())] ); let totalValue = new Decimal(0); for (let i = 0; i < basket.numTokens; i++) { const oraclePrice = PriceAggregator.fetch( basket.composition[i].oracleAggregator, //@ts-ignore [basket.lutPubkeys[0], basket.lutPubkeys[1]], oracleAccountInfos, solPrice, usdcPrice ); let amount = new Decimal(basket.composition[i].amount.toString()); const value = oraclePrice.price.mul(amount); basket.composition[i].price = oraclePrice; basket.composition[i].value = value; totalValue = totalValue.add(value); } basket.tvl = totalValue; let supplyOutstanding = new Decimal(basket.supplyOutstanding.toString()); if (supplyOutstanding.gt(new Decimal(0))) basket.price = totalValue.div(supplyOutstanding); return basket; }