-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
1,537 additions
and
749 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.