Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new field capacityPerPool to capacity pool API #139

Open
wants to merge 13 commits into
base: feat/staking-data-pool-detailed-capacity
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ best available combination of pools for the premium.
- **URL**: `/v2/capacity/{productId}`
- **Method**: `GET`
- **OpenAPI**: [v2/api/docs/#/Capacity/get_v2_capacity__productId_](https://api.nexusmutual.io/v2/api/docs/#/Capacity/get_v2_capacity__productId_)
- **Description**: Returns the current capacity for a specific product for a period of 30 days if no period query param is specified.
- **Description**: Returns the current capacity for a specific product for a period of 30 days if no period query param is specified. Additionally, if the query parameter `withPools=true` is provided, the response will include the `capacityPerPool` field with detailed capacity information per pool.

### Capacity Route for all products in a pool
- **URL**: `/v2/capacity/pools/{poolId}`
Expand Down
256 changes: 153 additions & 103 deletions src/lib/capacityEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { ethers, BigNumber } = require('ethers');
const { NXM_PER_ALLOCATION_UNIT, MAX_COVER_PERIOD } = require('./constants');
const { bnMax, bnMin, calculateTrancheId } = require('./helpers');
const { calculateBasePrice, calculatePremiumPerYear, calculateFixedPricePremiumPerYear } = require('./quoteEngine');
const { selectAsset, selectProduct, selectProductPools } = require('../store/selectors');
const { selectProduct, selectProductPools } = require('../store/selectors');

const { WeiPerEther, Zero } = ethers.constants;

Expand Down Expand Up @@ -31,78 +31,116 @@ function getUtilizationRate(capacityAvailableNXM, capacityUsedNXM) {
return capacityUsedNXM.mul(BASIS_POINTS).div(totalCapacity);
}

/**
* Calculates available capacity for a pool.
*
* @param {Array<BigNumber>} trancheCapacities - Array of capacity BigNumbers.
* @param {Array<BigNumber>} allocations - Array of allocation BigNumbers.
* @param {number} firstUsableTrancheIndex - Index of the first usable tranche.
* @returns {BigNumber} The available capacity as a BigNumber.
*/
function calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex) {
const unused = trancheCapacities.reduce((available, capacity, index) => {
const allocationDifference = capacity.sub(allocations[index]);
const allocationToAdd =
index < firstUsableTrancheIndex
? bnMin(allocationDifference, Zero) // only carry over the negative
: allocationDifference;
return available.add(allocationToAdd);
}, Zero);
return bnMax(unused, Zero);
}

/**
* Calculates capacity and pricing data for a specific tranche of product pools.
*
* @param {Array<Object>} productPools - Array of product pool objects.
* @param {number} firstUsableTrancheIndex - Index of the first usable tranche.
* @param {boolean} useFixedPrice - Flag indicating whether to use fixed pricing.
* @param {BigNumber} now - Current timestamp in seconds.
* @returns {Object} An object containing capacity used, capacity available, minimum price, and total premium.
* @param {Object} assets - Object containing asset information.
* @param {Object} assetRates - Object containing asset rates.
* @returns {Object} An object containing aggregated data and capacity per pool.
*/
function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, useFixedPrice, now) {
return productPools.reduce(
(accumulated, pool) => {
const { capacityUsedNXM, capacityAvailableNXM, minPrice, totalPremium } = accumulated;
const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime } = pool;

// calculating the capacity in allocation points
const used = allocations.reduce((total, allocation) => total.add(allocation), Zero);
const total = trancheCapacities.reduce((total, capacity) => total.add(capacity), Zero);

const unused = trancheCapacities.reduce((available, capacity, index) => {
const allocationDifference = capacity.sub(allocations[index]);
return index < firstUsableTrancheIndex
? available.add(bnMin(allocationDifference, Zero)) // only carry over the negative
: available.add(allocationDifference);
}, Zero);

const availableCapacity = bnMax(unused, Zero);

// convert to nxm
const totalInNXM = total.mul(NXM_PER_ALLOCATION_UNIT);
const usedInNxm = used.mul(NXM_PER_ALLOCATION_UNIT);
const availableInNXM = availableCapacity.mul(NXM_PER_ALLOCATION_UNIT);

if (availableCapacity.isZero()) {
// only add up the used capacity and return the same values for the rest
return {
capacityUsedNXM: usedInNxm.add(capacityUsedNXM),
capacityAvailableNXM,
minPrice,
totalPremium,
};
}
function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, useFixedPrice, now, assets, assetRates) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove assets and assetRates from this function, as I can see we are not using availableCapacityInAssets for anything and we are recalculating it again later in the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the availableCapacityInAssets in this function is under capacityPerPool field, meaning the value is segregated by StakingPool.

on the other hand the 2nd capacityInAssets (on capacityEngine function) is the value across all StakingPools.

const aggregatedData = {
capacityUsedNXM: Zero,
capacityAvailableNXM: Zero,
minPrice: Zero,
totalPremium: Zero,
};

const capacityPerPool = productPools.map(pool => {
Comment on lines +65 to +73
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main change here is adding capacityPerPool to the result along with the previous aggregatedData return value

const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime, poolId } = pool;

const basePrice = useFixedPrice
? targetPrice
: calculateBasePrice(targetPrice, bumpedPrice, bumpedPriceUpdateTime, now);
// calculating the capacity in allocation points
const used = allocations.reduce((total, allocation) => total.add(allocation), Zero);
const total = trancheCapacities.reduce((total, capacity) => total.add(capacity), Zero);

// the minimum price depends on the surge
// so we buy the smallest possible unit of capacity
// and calculate the premium per year
const unitPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice)
: calculatePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice, usedInNxm, totalInNXM);
const availableCapacity = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex);

const poolMinPrice = WeiPerEther.mul(unitPremium).div(NXM_PER_ALLOCATION_UNIT);
// convert to nxm
const totalInNXM = total.mul(NXM_PER_ALLOCATION_UNIT);
const usedInNXM = used.mul(NXM_PER_ALLOCATION_UNIT);
const availableInNXM = availableCapacity.mul(NXM_PER_ALLOCATION_UNIT);

// the maximum price a user would get can only be determined if the entire available
// capacity is bought because the routing will always pick the cheapest
// so we're summing up the premium for all pools and then calculate the average at the end
const poolPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(availableInNXM, basePrice)
: calculatePremiumPerYear(availableInNXM, basePrice, usedInNxm, totalInNXM);
aggregatedData.capacityUsedNXM = aggregatedData.capacityUsedNXM.add(usedInNXM);
aggregatedData.capacityAvailableNXM = aggregatedData.capacityAvailableNXM.add(availableInNXM);

if (availableCapacity.isZero()) {
return {
capacityUsedNXM: usedInNxm.add(capacityUsedNXM),
capacityAvailableNXM: availableInNXM.add(capacityAvailableNXM),
minPrice: minPrice.eq(Zero) ? poolMinPrice : bnMin(minPrice, poolMinPrice),
totalPremium: totalPremium.add(poolPremium),
poolId,
availableCapacity: [],
allocatedNxm: usedInNXM.toString(),
minAnnualPrice: Zero,
maxAnnualPrice: Zero,
};
},
{ capacityUsedNXM: Zero, capacityAvailableNXM: Zero, minPrice: Zero, totalPremium: Zero },
);
}

const basePrice = useFixedPrice
? targetPrice
: calculateBasePrice(targetPrice, bumpedPrice, bumpedPriceUpdateTime, now);

// the minimum price depends on the surge
// so we buy the smallest possible unit of capacity
// and calculate the premium per year
const unitPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice)
: calculatePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice, usedInNXM, totalInNXM);

const poolMinPrice = WeiPerEther.mul(unitPremium).div(NXM_PER_ALLOCATION_UNIT);

// the maximum price a user would get can only be determined if the entire available
// capacity is bought because the routing will always pick the cheapest
// so we're summing up the premium for all pools and then calculate the average at the end
const poolPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(availableInNXM, basePrice)
: calculatePremiumPerYear(availableInNXM, basePrice, usedInNXM, totalInNXM);

const poolMaxPrice = availableInNXM.isZero() ? Zero : WeiPerEther.mul(poolPremium).div(availableInNXM);

if (aggregatedData.minPrice.isZero() || poolMinPrice.lt(aggregatedData.minPrice)) {
aggregatedData.minPrice = poolMinPrice;
}
aggregatedData.totalPremium = aggregatedData.totalPremium.add(poolPremium);

// The available capacity of a product for a particular pool
const availableCapacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: availableInNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: assets[assetId],
}));

return {
poolId,
availableCapacity: availableCapacityInAssets,
allocatedNxm: usedInNXM,
minAnnualPrice: poolMinPrice,
maxAnnualPrice: poolMaxPrice,
};
});

return { aggregatedData, capacityPerPool };
}

/**
Expand Down Expand Up @@ -150,9 +188,10 @@ function calculateTrancheInfo(time, product, period) {
* @param {number|null} [options.poolId=null] - The ID of the pool to filter products by.
* @param {Array<number>} [options.productIds=[]] - Array of product IDs to process.
* @param {number} [options.period=30] - The coverage period in days.
* @param {boolean} [options.withPools=false] - Flag indicating whether to include capacityPerPool data field.
* @returns {Array<Object>} An array of capacity information objects for each product.
*/
function capacityEngine(store, { poolId = null, productIds = [], period = 30 } = {}) {
function capacityEngine(store, { poolId = null, productIds = [], period = 30, withPools = false } = {}) {
const { assets, assetRates, products } = store.getState();
const now = BigNumber.from(Date.now()).div(1000);
const capacities = [];
Expand All @@ -176,76 +215,87 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30 } =
}

const { firstUsableTrancheIndex, firstUsableTrancheForMaxPeriodIndex } = calculateTrancheInfo(now, product, period);

// Use productPools from poolId if available; otherwise, select all pools for productId
const productPools = selectProductPools(store, productId, poolId);

let aggregatedData = {};
let capacityPerPool = [];
let maxAnnualPrice = Zero;

if (product.useFixedPrice) {
// Fixed Price
const productData = calculateProductDataForTranche(productPools, firstUsableTrancheIndex, true, now);

const { capacityAvailableNXM, capacityUsedNXM, minPrice, totalPremium } = productData;

const maxAnnualPrice = capacityAvailableNXM.isZero()
? Zero
: WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);

const capacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: selectAsset(store, assetId),
}));

capacities.push({
productId: Number(productId),
availableCapacity: capacityInAssets,
usedCapacity: capacityUsedNXM,
utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM),
minAnnualPrice: minPrice,
maxAnnualPrice,
});
({ aggregatedData, capacityPerPool } = calculateProductDataForTranche(
productPools,
firstUsableTrancheIndex,
true,
now,
assets,
assetRates,
));

const { capacityAvailableNXM, totalPremium } = aggregatedData;
maxAnnualPrice = capacityAvailableNXM.isZero() ? Zero : WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);
} else {
// Non-fixed Price
let productData = {};
let maxAnnualPrice = BigNumber.from(0);

// use the first 6 tranches (over 1 year) for calculating the max annual price
for (let i = 0; i <= firstUsableTrancheForMaxPeriodIndex; i++) {
const productTrancheData = calculateProductDataForTranche(productPools, i, false, now);
const { aggregatedData: trancheData, capacityPerPool: trancheCapacityPerPool } = calculateProductDataForTranche(
productPools,
i,
false,
now,
assets,
assetRates,
);

if (i === firstUsableTrancheIndex) {
productData = productTrancheData;
aggregatedData = trancheData;
capacityPerPool = trancheCapacityPerPool;
}

const { capacityAvailableNXM, totalPremium } = productTrancheData;
const { capacityAvailableNXM, totalPremium } = trancheData;

const maxTrancheAnnualPrice = capacityAvailableNXM.isZero()
? Zero
: WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);

maxAnnualPrice = bnMax(maxAnnualPrice, maxTrancheAnnualPrice);
}
}

const { capacityAvailableNXM, capacityUsedNXM, minPrice } = productData;
const capacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: selectAsset(store, assetId),
}));

capacities.push({
productId: Number(productId),
availableCapacity: capacityInAssets,
usedCapacity: capacityUsedNXM,
utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM),
minAnnualPrice: minPrice,
maxAnnualPrice,
});
const { capacityAvailableNXM, capacityUsedNXM, minPrice } = aggregatedData;
// The available capacity of a product across all pools
const capacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: assets[assetId],
}));

const capacityData = {
productId: Number(productId),
availableCapacity: capacityInAssets,
usedCapacity: capacityUsedNXM,
utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM),
minAnnualPrice: minPrice,
maxAnnualPrice,
};

if (withPools) {
capacityData.capacityPerPool = capacityPerPool;
}

capacities.push(capacityData);
}

return capacities;
}

module.exports = {
capacityEngine,
getUtilizationRate,
calculateAvailableCapacity,
calculateProductDataForTranche,
getProductsInPool,
calculateTrancheInfo,
capacityEngine,
};
Loading