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,
});
}