Skip to content

Commit

Permalink
fix: staking data fixes (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
rackstar authored Dec 3, 2024
2 parents 0f80381 + 4f21e6d commit 194f5a0
Show file tree
Hide file tree
Showing 15 changed files with 1,537 additions and 749 deletions.
339 changes: 203 additions & 136 deletions src/lib/capacityEngine.js
Original file line number Diff line number Diff line change
@@ -1,186 +1,253 @@
const { ethers, BigNumber } = require('ethers');

const { MAX_COVER_PERIOD, SECONDS_PER_DAY } = require('./constants');
const { MAX_COVER_PERIOD } = require('./constants');
const {
bnMax,
calculateTrancheId,
calculateFirstUsableTrancheIndex,
calculateProductDataForTranche,
} = require('./helpers');
const { selectProduct, selectProductPools } = require('../store/selectors');
const { selectProduct, selectProductPools, selectProductsInPool } = require('../store/selectors');

const { WeiPerEther, Zero } = ethers.constants;

const BASIS_POINTS = 10000;

/**
* Calculates the utilization rate of the capacity.
* Calculates the index of the first usable tranche for the maximum cover period.
* This is used to determine the maximum price a user would get when buying cover.
*
* @param {BigNumber} capacityAvailableNXM - The amount of capacity available in NXM.
* @param {BigNumber} capacityUsedNXM - The amount of capacity used in NXM.
* @returns {BigNumber} The utilization rate as a BigNumber, expressed in basis points (0-10,000).
* Returns undefined if capacity in NXM is missing.
* @param {BigNumber} now - The current timestamp in seconds.
* @param {BigNumber} gracePeriod - The product's grace period in seconds.
* @returns {number} The index difference between the first usable tranche for max period and the first active tranche.
*/
function getUtilizationRate(capacityAvailableNXM, capacityUsedNXM) {
if (!capacityAvailableNXM || !capacityUsedNXM) {
return undefined;
}
function calculateFirstUsableTrancheIndexForMaxPeriod(now, gracePeriod) {
const firstActiveTrancheId = calculateTrancheId(now);
const firstUsableTrancheIdForMaxPeriod = calculateTrancheId(now.add(MAX_COVER_PERIOD).add(gracePeriod));
return firstUsableTrancheIdForMaxPeriod - firstActiveTrancheId;
}

/**
* Calculates the pool-level utilization rate across all products in the pool.
*
* @param {Array<Object>} products - Array of product capacity data for the pool
* @returns {BigNumber} The pool-level utilization rate as a BigNumber, expressed in basis points (0-10,000)
*/
function calculatePoolUtilizationRate(products) {
let totalCapacityAvailableNXM = Zero;
let totalCapacityUsedNXM = Zero;

products.forEach(product => {
totalCapacityAvailableNXM = totalCapacityAvailableNXM.add(
product.availableCapacity.find(c => c.assetId === 255)?.amount || Zero,
);
totalCapacityUsedNXM = totalCapacityUsedNXM.add(product.usedCapacity);
});

const totalCapacity = capacityAvailableNXM.add(capacityUsedNXM);
const totalCapacity = totalCapacityAvailableNXM.add(totalCapacityUsedNXM);
if (totalCapacity.isZero()) {
return BigNumber.from(0);
}

return capacityUsedNXM.mul(BASIS_POINTS).div(totalCapacity);
return totalCapacityUsedNXM.mul(BASIS_POINTS).div(totalCapacity);
}

/**
* Retrieves all product IDs that are associated with a specific pool.
*
* @param {Object} store - The Redux store containing application state.
* @param {number|string} poolId - The ID of the pool to filter products by.
* @returns {Array<string>} An array of product IDs associated with the specified pool.
* Helper function to calculate capacity for a single product.
*/
function getProductsInPool(store, poolId) {
const { products } = store.getState();
return Object.keys(products).filter(productId => {
const productPools = selectProductPools(store, productId, poolId);
return productPools?.length > 0;
});
function calculateProductCapacity(
store,
productId,
{ poolId = null, period, now, assets, assetRates, withPools = true },
) {
const product = selectProduct(store, productId);
if (!product) {
return null;
}

const firstUsableTrancheIndex = calculateFirstUsableTrancheIndex(now, product.gracePeriod, period);
const firstUsableTrancheForMaxPeriodIndex = calculateFirstUsableTrancheIndexForMaxPeriod(now, product.gracePeriod);

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

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

const { capacityAvailableNXM, totalPremium } = trancheData;
const maxTrancheAnnualPrice = capacityAvailableNXM.isZero()
? Zero
: WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);
maxAnnualPrice = bnMax(maxAnnualPrice, maxTrancheAnnualPrice);
}
}

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,
minAnnualPrice: minPrice,
maxAnnualPrice,
};

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

return capacityData;
}

/* API SERVICES */

/**
* Calculates the index of the first usable tranche for the maximum cover period.
* This is used to determine the maximum price a user would get when buying cover.
* Gets capacity data for all products.
* GET /capacity
*
* @param {BigNumber} now - The current timestamp in seconds.
* @param {BigNumber} gracePeriod - The product's grace period in seconds.
* @returns {number} The index difference between the first usable tranche for max period and the first active tranche.
* @param {Object} store - The Redux store containing application state.
* @param {number} period - The coverage period in seconds.
* @returns {Array<Object>} Array of product capacity data.
*/
function calculateFirstUsableTrancheForMaxPeriodIndex(now, gracePeriod) {
const firstActiveTrancheId = calculateTrancheId(now);
const firstUsableTrancheForMaxPeriodId = calculateTrancheId(now.add(MAX_COVER_PERIOD).add(gracePeriod));
return firstUsableTrancheForMaxPeriodId - firstActiveTrancheId;
function getAllProductCapacities(store, period) {
const { assets, assetRates, products } = store.getState();
const now = BigNumber.from(Date.now()).div(1000);

return Object.keys(products)
.map(productId => calculateProductCapacity(store, productId, { period, now, assets, assetRates, withPools: false }))
.filter(Boolean); // remove any nulls (i.e. productId did not match any products)
}

/**
* Calculates the capacity and pricing information for products and pools.
* Gets capacity data for a single product across all pools.
* GET /capacity/:productId
*
* @param {Object} store - The Redux store containing application state.
* @param {Object} [options={}] - Optional parameters for capacity calculation.
* @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.periodSeconds=30*SECONDS_PER_DAY] - The coverage period in seconds
* @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.
* @param {string|number} productId - The product ID.
* @param {number} period - The coverage period in seconds.
* @returns {Object|null} Product capacity data or null if product not found.
*/
function capacityEngine(
store,
{ poolId = null, productIds = [], periodSeconds = SECONDS_PER_DAY.mul(30), withPools = false } = {},
) {
const { assets, assetRates, products } = store.getState();
function getProductCapacity(store, productId, period) {
const { assets, assetRates } = store.getState();
const now = BigNumber.from(Date.now()).div(1000);
const capacities = [];

let productIdsToProcess;
if (productIds.length > 0) {
productIdsToProcess = [...productIds];
} else if (poolId !== null) {
// If only poolId is provided, get all products in that pool
productIdsToProcess = getProductsInPool(store, poolId);
} else {
// If neither productIds nor poolId is provided, process all products
productIdsToProcess = Object.keys(products);
}

for (const productId of productIdsToProcess) {
const product = selectProduct(store, productId);

if (!product) {
continue;
}

const firstUsableTrancheIndex = calculateFirstUsableTrancheIndex(now, product.gracePeriod, periodSeconds);
const firstUsableTrancheForMaxPeriodIndex = calculateFirstUsableTrancheForMaxPeriodIndex(now, product.gracePeriod);
return calculateProductCapacity(store, productId, {
period,
now,
assets,
assetRates,
});
}

// Use productPools from poolId if available; otherwise, select all pools for productId
const productPools = selectProductPools(store, productId, poolId);
/**
* Gets capacity data for a pool, including all its products.
* GET /capacity/pools/:poolId
*
* @param {Object} store - The Redux store containing application state.
* @param {string|number} poolId - The pool ID.
* @param {number} period - The coverage period in seconds.
* @returns {Object|null} Pool capacity data or null if pool not found.
*/
function getPoolCapacity(store, poolId, period) {
const { assets, assetRates } = store.getState();
const now = BigNumber.from(Date.now()).div(1000);
const productIds = selectProductsInPool(store, poolId);

let aggregatedData = {};
let capacityPerPool = [];
let maxAnnualPrice = Zero;
if (productIds.length === 0) {
return null;
}

if (product.useFixedPrice) {
// Fixed Price
({ aggregatedData, capacityPerPool } = calculateProductDataForTranche(
productPools,
firstUsableTrancheIndex,
true,
const productsCapacity = productIds
.map(productId =>
calculateProductCapacity(store, productId, {
poolId,
period,
now,
assets,
assetRates,
));

const { capacityAvailableNXM, totalPremium } = aggregatedData;
maxAnnualPrice = capacityAvailableNXM.isZero() ? Zero : WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);
} else {
// Non-fixed Price
// use the first 6 tranches (over 1 year) for calculating the max annual price
for (let i = 0; i <= firstUsableTrancheForMaxPeriodIndex; i++) {
const { aggregatedData: trancheData, capacityPerPool: trancheCapacityPerPool } = calculateProductDataForTranche(
productPools,
i,
false,
now,
assets,
assetRates,
);

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

const { capacityAvailableNXM, totalPremium } = trancheData;

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

maxAnnualPrice = bnMax(maxAnnualPrice, maxTrancheAnnualPrice);
}
}

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;
}
withPools: false,
}),
)
.filter(Boolean); // remove any nulls (i.e. productId did not match any products)

return {
poolId: Number(poolId),
utilizationRate: calculatePoolUtilizationRate(productsCapacity),
productsCapacity,
};
}

capacities.push(capacityData);
}
/**
* Gets capacity data for a specific product in a specific pool.
* GET /capacity/pools/:poolId/products/:productId
*
* @param {Object} store - The Redux store containing application state.
* @param {string|number} poolId - The pool ID.
* @param {string|number} productId - The product ID.
* @param {number} period - The coverage period in seconds.
* @returns {Object|null} Product capacity data for the specific pool or null if not found.
*/
function getProductCapacityInPool(store, poolId, productId, period) {
const { assets, assetRates } = store.getState();
const now = BigNumber.from(Math.floor(Date.now() / 1000));

const poolProductCapacity = calculateProductCapacity(store, productId, {
poolId,
period,
now,
assets,
assetRates,
withPools: false,
});

return capacities;
return poolProductCapacity;
}

module.exports = {
getUtilizationRate,
calculateFirstUsableTrancheForMaxPeriodIndex,
getProductsInPool,
capacityEngine,
getAllProductCapacities,
getProductCapacity,
getPoolCapacity,
getProductCapacityInPool,
// Keep these exports for testing purposes
calculateProductCapacity,
calculatePoolUtilizationRate,
calculateFirstUsableTrancheIndexForMaxPeriod,
};
Loading

0 comments on commit 194f5a0

Please sign in to comment.