import type { Address } from "@solana/kit"; import { fetchVault, findVaultLpMintPda, VOLTR_VAULT_PROGRAM_ADDRESS, type FeeConfiguration, type Vault, } from "../generated"; import { calculateAssetsForWithdrawAmount, calculateLpForDepositAmount, calculateLpForWithdrawAmount, calculateUnrealisedLpFees, getTotalLpSupplyInclFees, } from "./math"; import type { ExtensionsRpc } from "./rpc"; /** * Total management fee in BPS — sum of manager + admin + protocol, matching * the program's `Vault::get_total_fee_configuration_management_fee`. */ export function getTotalManagementFeeBps( feeConfiguration: FeeConfiguration, ): number { return ( feeConfiguration.managerManagementFee + feeConfiguration.adminManagementFee + feeConfiguration.protocolManagementFee ); } async function vaultLpMintAddress(vault: Address): Promise
{ const [address] = await findVaultLpMintPda( { vault }, { programAddress: VOLTR_VAULT_PROGRAM_ADDRESS }, ); return address; } async function fetchTokenSupply( rpc: ExtensionsRpc, mint: Address, ): Promise<{ amount: bigint; decimals: number }> { const { value } = await rpc.getTokenSupply(mint).send(); return { amount: BigInt(value.amount), decimals: value.decimals }; } async function loadVaultAndLpSupply( rpc: ExtensionsRpc, vault: Address, ): Promise<{ data: Vault; lpSupply: bigint; lpDecimals: number }> { const account = await fetchVault(rpc, vault); const lpMint = await vaultLpMintAddress(vault); const { amount, decimals } = await fetchTokenSupply(rpc, lpMint); return { data: account.data, lpSupply: amount, lpDecimals: decimals }; } /** * Returns the on-chain "asset per LP" ratio used by mint/burn math: total * asset value divided by `total_lp_supply_incl_fees + unrealised management * fee LP`. Mirrors what the deposit/withdraw handlers see after they call * `handle_management_fee`. Returns 0 when there is no LP at all. */ export async function getCurrentAssetPerLpForVault( rpc: ExtensionsRpc, vault: Address, ): Promise { const { data, lpSupply } = await loadVaultAndLpSupply(rpc, vault); const totalLpSupplyInclFees = getTotalLpSupplyInclFees({ lpSupply, vaultAccumulatedLpAdminFees: data.feeState.accumulatedLpAdminFees, vaultAccumulatedLpManagerFees: data.feeState.accumulatedLpManagerFees, vaultAccumulatedLpProtocolFees: data.feeState.accumulatedLpProtocolFees, vaultDeadWeight: data.deadWeight, }); const unrealisedLpFees = calculateUnrealisedLpFees( totalLpSupplyInclFees, data.asset.totalValue, data.feeUpdate.lastManagementFeeUpdateTs, BigInt(getTotalManagementFeeBps(data.feeConfiguration)), ); const totalLpSupply = totalLpSupplyInclFees + unrealisedLpFees; if (totalLpSupply === 0n) return 0; return Number(data.asset.totalValue) / Number(totalLpSupply); } export type VaultLpSupplyBreakdown = { circulating: bigint; unharvestedFees: bigint; deadWeight: bigint; unrealisedFees: bigint; total: bigint; }; export async function getVaultLpSupplyBreakdown( rpc: ExtensionsRpc, vault: Address, ): Promise { const { data, lpSupply } = await loadVaultAndLpSupply(rpc, vault); const unharvestedFees = data.feeState.accumulatedLpAdminFees + data.feeState.accumulatedLpManagerFees + data.feeState.accumulatedLpProtocolFees; const deadWeight = data.deadWeight; const currentTotalLp = lpSupply + unharvestedFees + deadWeight; const unrealisedFees = calculateUnrealisedLpFees( currentTotalLp, data.asset.totalValue, data.feeUpdate.lastManagementFeeUpdateTs, BigInt(getTotalManagementFeeBps(data.feeConfiguration)), ); return { circulating: lpSupply, unharvestedFees, deadWeight, unrealisedFees, total: currentTotalLp + unrealisedFees, }; } export async function calculateAssetsForWithdraw( rpc: ExtensionsRpc, vault: Address, lpAmount: bigint, ): Promise { const { data, lpSupply } = await loadVaultAndLpSupply(rpc, vault); return calculateAssetsForWithdrawAmount({ vaultTotalValue: data.asset.totalValue, vaultLastUpdatedLockedProfit: data.lockedProfitState.lastUpdatedLockedProfit, vaultLastReport: data.lockedProfitState.lastReport, vaultLockedProfitDegradationDuration: data.vaultConfiguration.lockedProfitDegradationDuration, vaultAccumulatedLpAdminFees: data.feeState.accumulatedLpAdminFees, vaultAccumulatedLpManagerFees: data.feeState.accumulatedLpManagerFees, vaultAccumulatedLpProtocolFees: data.feeState.accumulatedLpProtocolFees, vaultDeadWeight: data.deadWeight, vaultRedemptionFeeBps: data.feeConfiguration.redemptionFee, vaultManagementFeeBps: getTotalManagementFeeBps(data.feeConfiguration), vaultLastManagementFeeUpdateTs: data.feeUpdate.lastManagementFeeUpdateTs, lpSupply, lpAmount, }); } export async function calculateLpForWithdraw( rpc: ExtensionsRpc, vault: Address, assetAmount: bigint, ): Promise { const { data, lpSupply } = await loadVaultAndLpSupply(rpc, vault); return calculateLpForWithdrawAmount({ vaultTotalValue: data.asset.totalValue, vaultLastUpdatedLockedProfit: data.lockedProfitState.lastUpdatedLockedProfit, vaultLastReport: data.lockedProfitState.lastReport, vaultLockedProfitDegradationDuration: data.vaultConfiguration.lockedProfitDegradationDuration, vaultAccumulatedLpAdminFees: data.feeState.accumulatedLpAdminFees, vaultAccumulatedLpManagerFees: data.feeState.accumulatedLpManagerFees, vaultAccumulatedLpProtocolFees: data.feeState.accumulatedLpProtocolFees, vaultDeadWeight: data.deadWeight, vaultRedemptionFeeBps: data.feeConfiguration.redemptionFee, vaultManagementFeeBps: getTotalManagementFeeBps(data.feeConfiguration), vaultLastManagementFeeUpdateTs: data.feeUpdate.lastManagementFeeUpdateTs, lpSupply, assetAmount, }); } export async function calculateLpForDeposit( rpc: ExtensionsRpc, vault: Address, assetAmount: bigint, ): Promise { const { data, lpSupply, lpDecimals } = await loadVaultAndLpSupply(rpc, vault); // Asset decimals are only required when the LP pool is empty (1:1 mint). // Avoid an extra RPC round-trip in the common case. let assetDecimals = 0; const lpSupplyInclAccumulatedFees = getTotalLpSupplyInclFees({ lpSupply, vaultAccumulatedLpAdminFees: data.feeState.accumulatedLpAdminFees, vaultAccumulatedLpManagerFees: data.feeState.accumulatedLpManagerFees, vaultAccumulatedLpProtocolFees: data.feeState.accumulatedLpProtocolFees, vaultDeadWeight: data.deadWeight, }); const unrealisedLpFees = calculateUnrealisedLpFees( lpSupplyInclAccumulatedFees, data.asset.totalValue, data.feeUpdate.lastManagementFeeUpdateTs, BigInt(getTotalManagementFeeBps(data.feeConfiguration)), ); const lpSupplyInclFees = lpSupplyInclAccumulatedFees + unrealisedLpFees; if (lpSupplyInclFees === 0n) { const { decimals } = await fetchTokenSupply(rpc, data.asset.mint); assetDecimals = decimals; } return calculateLpForDepositAmount({ vaultTotalValue: data.asset.totalValue, vaultAccumulatedLpAdminFees: data.feeState.accumulatedLpAdminFees, vaultAccumulatedLpManagerFees: data.feeState.accumulatedLpManagerFees, vaultAccumulatedLpProtocolFees: data.feeState.accumulatedLpProtocolFees, vaultDeadWeight: data.deadWeight, vaultIssuanceFeeBps: data.feeConfiguration.issuanceFee, vaultManagementFeeBps: getTotalManagementFeeBps(data.feeConfiguration), vaultLastManagementFeeUpdateTs: data.feeUpdate.lastManagementFeeUpdateTs, lpSupply, assetAmount, assetDecimals, lpDecimals, }); }