From 7a89476e95d749978a8baa7b4687b787acbfb13f Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Tue, 8 Oct 2024 14:00:00 +0300 Subject: [PATCH 01/13] feat: add capacityPerPool field --- src/lib/capacityEngine.js | 174 ++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 65 deletions(-) diff --git a/src/lib/capacityEngine.js b/src/lib/capacityEngine.js index eecc0d6e..39f940e8 100644 --- a/src/lib/capacityEngine.js +++ b/src/lib/capacityEngine.js @@ -38,71 +38,97 @@ function getUtilizationRate(capacityAvailableNXM, capacityUsedNXM) { * @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) { + const aggregatedData = { + capacityUsedNXM: Zero, + capacityAvailableNXM: Zero, + minPrice: Zero, + totalPremium: Zero, + }; + + const capacityPerPool = productPools.map(pool => { + const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime, poolId, poolName } = 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 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 poolMinPrice = WeiPerEther.mul(unitPremium).div(NXM_PER_ALLOCATION_UNIT); + const availableCapacity = bnMax(unused, Zero); - // 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); + // 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); + 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, + poolName, + availableCapacity: [], + allocatedNxm: usedInNxm.toString(), + minAnnualPrice: '0', + maxAnnualPrice: '0', }; - }, - { 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); + + aggregatedData.minPrice = aggregatedData.minPrice.eq(Zero) + ? poolMinPrice + : bnMin(aggregatedData.minPrice, poolMinPrice); + aggregatedData.totalPremium = aggregatedData.totalPremium.add(poolPremium); + + const availableCapacityInAssets = Object.keys(assets).map(assetId => ({ + assetId: Number(assetId), + amount: availableInNXM.mul(assetRates[assetId]).div(WeiPerEther).toString(), + asset: assets[assetId], + })); + + return { + poolId, + poolName, + availableCapacity: availableCapacityInAssets, + allocatedNxm: usedInNxm.toString(), + minAnnualPrice: poolMinPrice.toString(), + maxAnnualPrice: poolMaxPrice.toString(), + }; + }); + + return { aggregatedData, capacityPerPool }; } /** @@ -176,13 +202,20 @@ 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); if (product.useFixedPrice) { // Fixed Price - const productData = calculateProductDataForTranche(productPools, firstUsableTrancheIndex, true, now); - - const { capacityAvailableNXM, capacityUsedNXM, minPrice, totalPremium } = productData; + const { aggregatedData, capacityPerPool } = calculateProductDataForTranche( + productPools, + firstUsableTrancheIndex, + true, + now, + assets, + assetRates, + ); + const { capacityAvailableNXM, capacityUsedNXM, minPrice, totalPremium } = aggregatedData; const maxAnnualPrice = capacityAvailableNXM.isZero() ? Zero @@ -201,21 +234,31 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30 } = utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM), minAnnualPrice: minPrice, maxAnnualPrice, + capacityPerPool, }); } else { // Non-fixed Price - let productData = {}; + let aggregatedData = {}; let maxAnnualPrice = BigNumber.from(0); + let capacityPerPool = []; // 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 @@ -224,7 +267,7 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30 } = maxAnnualPrice = bnMax(maxAnnualPrice, maxTrancheAnnualPrice); } - const { capacityAvailableNXM, capacityUsedNXM, minPrice } = productData; + const { capacityAvailableNXM, capacityUsedNXM, minPrice } = aggregatedData; const capacityInAssets = Object.keys(assets).map(assetId => ({ assetId: Number(assetId), amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther), @@ -238,6 +281,7 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30 } = utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM), minAnnualPrice: minPrice, maxAnnualPrice, + capacityPerPool, }); } } From 53a781d783cce66ded4224ff571fe0e20c01b6e3 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:29:54 +0300 Subject: [PATCH 02/13] refactor: clean up calculateProductDataForTranche * add calculateAvailableCapacity --- src/lib/capacityEngine.js | 63 +++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/lib/capacityEngine.js b/src/lib/capacityEngine.js index 39f940e8..f8d5854e 100644 --- a/src/lib/capacityEngine.js +++ b/src/lib/capacityEngine.js @@ -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; @@ -31,6 +31,26 @@ function getUtilizationRate(capacityAvailableNXM, capacityUsedNXM) { return capacityUsedNXM.mul(BASIS_POINTS).div(totalCapacity); } +/** + * Calculates available capacity for a pool. + * + * @param {Array} trancheCapacities - Array of capacity BigNumbers. + * @param {Array} 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. * @@ -51,37 +71,29 @@ function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, u }; const capacityPerPool = productPools.map(pool => { - const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime, poolId, poolName } = pool; + const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime, poolId } = 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); + const availableCapacity = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); // convert to nxm const totalInNXM = total.mul(NXM_PER_ALLOCATION_UNIT); - const usedInNxm = used.mul(NXM_PER_ALLOCATION_UNIT); + const usedInNXM = used.mul(NXM_PER_ALLOCATION_UNIT); const availableInNXM = availableCapacity.mul(NXM_PER_ALLOCATION_UNIT); - aggregatedData.capacityUsedNXM = aggregatedData.capacityUsedNXM.add(usedInNxm); + aggregatedData.capacityUsedNXM = aggregatedData.capacityUsedNXM.add(usedInNXM); aggregatedData.capacityAvailableNXM = aggregatedData.capacityAvailableNXM.add(availableInNXM); if (availableCapacity.isZero()) { return { poolId, - poolName, availableCapacity: [], - allocatedNxm: usedInNxm.toString(), - minAnnualPrice: '0', - maxAnnualPrice: '0', + allocatedNxm: usedInNXM.toString(), + minAnnualPrice: Zero, + maxAnnualPrice: Zero, }; } @@ -94,7 +106,7 @@ function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, u // and calculate the premium per year const unitPremium = useFixedPrice ? calculateFixedPricePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice) - : calculatePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice, usedInNxm, totalInNXM); + : calculatePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice, usedInNXM, totalInNXM); const poolMinPrice = WeiPerEther.mul(unitPremium).div(NXM_PER_ALLOCATION_UNIT); @@ -103,28 +115,27 @@ function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, u // 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); + : calculatePremiumPerYear(availableInNXM, basePrice, usedInNXM, totalInNXM); const poolMaxPrice = availableInNXM.isZero() ? Zero : WeiPerEther.mul(poolPremium).div(availableInNXM); - aggregatedData.minPrice = aggregatedData.minPrice.eq(Zero) - ? poolMinPrice - : bnMin(aggregatedData.minPrice, poolMinPrice); + if (aggregatedData.minPrice.isZero() || poolMinPrice.lt(aggregatedData.minPrice)) { + aggregatedData.minPrice = poolMinPrice; + } aggregatedData.totalPremium = aggregatedData.totalPremium.add(poolPremium); const availableCapacityInAssets = Object.keys(assets).map(assetId => ({ assetId: Number(assetId), - amount: availableInNXM.mul(assetRates[assetId]).div(WeiPerEther).toString(), + amount: availableInNXM.mul(assetRates[assetId]).div(WeiPerEther), asset: assets[assetId], })); return { poolId, - poolName, availableCapacity: availableCapacityInAssets, - allocatedNxm: usedInNxm.toString(), - minAnnualPrice: poolMinPrice.toString(), - maxAnnualPrice: poolMaxPrice.toString(), + allocatedNxm: usedInNXM, + minAnnualPrice: poolMinPrice, + maxAnnualPrice: poolMaxPrice, }; }); From d805fa6a2389ee231027a777ea0170e1c445132b Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:32:23 +0300 Subject: [PATCH 03/13] feat: add capacityPerPool field if withPools=true * remove code duplication in capacityEngine --- src/lib/capacityEngine.js | 83 ++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/src/lib/capacityEngine.js b/src/lib/capacityEngine.js index f8d5854e..ca269fc1 100644 --- a/src/lib/capacityEngine.js +++ b/src/lib/capacityEngine.js @@ -187,9 +187,10 @@ function calculateTrancheInfo(time, product, period) { * @param {number|null} [options.poolId=null] - The ID of the pool to filter products by. * @param {Array} [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} 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 = []; @@ -213,46 +214,29 @@ 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 { aggregatedData, capacityPerPool } = calculateProductDataForTranche( + ({ aggregatedData, capacityPerPool } = calculateProductDataForTranche( productPools, firstUsableTrancheIndex, true, now, assets, assetRates, - ); - const { capacityAvailableNXM, capacityUsedNXM, minPrice, totalPremium } = aggregatedData; - - 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, - capacityPerPool, - }); + )); + + const { capacityAvailableNXM, totalPremium } = aggregatedData; + maxAnnualPrice = capacityAvailableNXM.isZero() ? Zero : WeiPerEther.mul(totalPremium).div(capacityAvailableNXM); } else { // Non-fixed Price - let aggregatedData = {}; - let maxAnnualPrice = BigNumber.from(0); - let capacityPerPool = []; - // 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( @@ -277,30 +261,39 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30 } = maxAnnualPrice = bnMax(maxAnnualPrice, maxTrancheAnnualPrice); } + } + + const { capacityAvailableNXM, capacityUsedNXM, minPrice } = aggregatedData; + 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, + }; - const { capacityAvailableNXM, capacityUsedNXM, minPrice } = aggregatedData; - 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, - capacityPerPool, - }); + if (withPools) { + capacityData.capacityPerPool = capacityPerPool; } + + capacities.push(capacityData); } return capacities; } module.exports = { - capacityEngine, getUtilizationRate, + calculateAvailableCapacity, + calculateProductDataForTranche, + getProductsInPool, + calculateTrancheInfo, + capacityEngine, }; From dc2ebcb64ee7542e3936fdde72a7a856cd2da981 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:33:35 +0300 Subject: [PATCH 04/13] feat: add support for capacityPerPool in formatCapacityResult --- src/routes/capacity.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/routes/capacity.js b/src/routes/capacity.js index 1f18343f..a7c0806b 100644 --- a/src/routes/capacity.js +++ b/src/routes/capacity.js @@ -1,3 +1,5 @@ +const { inspect } = require('node:util'); + const { ethers, BigNumber } = require('ethers'); const express = require('express'); @@ -18,6 +20,17 @@ const formatCapacityResult = capacity => ({ utilizationRate: capacity.utilizationRate.toNumber(), minAnnualPrice: formatUnits(capacity.minAnnualPrice), maxAnnualPrice: formatUnits(capacity.maxAnnualPrice), + capacityPerPool: capacity.capacityPerPool?.map(c => ({ + poolId: c.poolId, + availableCapacity: c.availableCapacity.map(({ assetId, amount, asset }) => ({ + assetId, + amount: amount.toString(), + asset, + })), + allocatedNxm: c.allocatedNxm.toString(), + minAnnualPrice: formatUnits(c.minAnnualPrice), + maxAnnualPrice: formatUnits(c.maxAnnualPrice), + })), }); /** From 6c219d7760fe6d0dce1c234d8135cfa4b6430025 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:34:54 +0300 Subject: [PATCH 05/13] feat: add response logging on capacity endpoints --- src/routes/capacity.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/routes/capacity.js b/src/routes/capacity.js index a7c0806b..76e152f3 100644 --- a/src/routes/capacity.js +++ b/src/routes/capacity.js @@ -66,9 +66,12 @@ router.get( try { const period = BigNumber.from(periodQuery); const store = req.app.get('store'); - const response = capacityEngine(store, { period }); + const capacities = capacityEngine(store, { period }); - res.json(response.map(capacity => formatCapacityResult(capacity))); + const response = capacities.map(capacity => formatCapacityResult(capacity)); + console.log(inspect(capacities, { depth: null })); + + res.json(response); } catch (error) { console.error(error); return res.status(500).send({ error: 'Internal Server Error', response: null }); @@ -108,6 +111,7 @@ router.get( asyncRoute(async (req, res) => { const productId = Number(req.params.productId); const periodQuery = Number(req.query.period) || 30; + const withPools = req.query.withPools === 'true'; if (!Number.isInteger(periodQuery) || periodQuery < 28 || periodQuery > 365) { return res.status(400).send({ error: 'Invalid period: must be an integer between 28 and 365', response: null }); @@ -119,13 +123,16 @@ router.get( try { const period = BigNumber.from(periodQuery); const store = req.app.get('store'); - const [capacity] = capacityEngine(store, { productIds: [productId], period }); + const [capacity] = capacityEngine(store, { productIds: [productId], period, withPools }); if (!capacity) { return res.status(400).send({ error: 'Invalid Product Id', response: null }); } - res.json(formatCapacityResult(capacity)); + const response = formatCapacityResult(capacity); + console.log(inspect(response, { depth: null })); + + res.json(response); } catch (error) { console.error(error); return res.status(500).send({ error: 'Internal Server Error', response: null }); @@ -188,13 +195,16 @@ router.get( try { const period = BigNumber.from(periodQuery); const store = req.app.get('store'); - const response = capacityEngine(store, { poolId, period }); + const capacities = capacityEngine(store, { poolId, period }); - if (response.length === 0) { + if (capacities.length === 0) { return res.status(404).send({ error: 'Pool not found', response: null }); } - res.json(response.map(capacity => formatCapacityResult(capacity))); + const response = capacities.map(capacity => formatCapacityResult(capacity)); + console.log(inspect(response, { depth: null })); + + res.json(response); } catch (error) { console.error(error); return res.status(500).send({ error: 'Internal Server Error', response: null }); @@ -265,10 +275,15 @@ router.get( const period = BigNumber.from(periodQuery); const store = req.app.get('store'); const [capacity] = capacityEngine(store, { poolId, productIds: [productId], period }); + if (!capacity) { return res.status(404).send({ error: 'Product not found in the specified pool', response: null }); } - res.json(formatCapacityResult(capacity)); + + const response = formatCapacityResult(capacity); + console.log(inspect(response, { depth: null })); + + res.json(response); } catch (error) { console.error(error); return res.status(500).send({ error: 'Internal Server Error', response: null }); From 32b10d24591448977ca62cdd798f79a3f34a96bb Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:37:09 +0300 Subject: [PATCH 06/13] test: add unit test for capacityEngine functions * capacityPerPools unit tests if withPools=true * calculateProductDataForTranche * calculateAvailableCapacity * getProductsInPool * calculateTrancheInfo --- test/unit/capacityEngine.js | 380 +++++++++++++++++++++++++++++++++++- 1 file changed, 379 insertions(+), 1 deletion(-) diff --git a/test/unit/capacityEngine.js b/test/unit/capacityEngine.js index 2e0c4c00..6f33e0d1 100644 --- a/test/unit/capacityEngine.js +++ b/test/unit/capacityEngine.js @@ -3,13 +3,23 @@ const ethers = require('ethers'); const sinon = require('sinon'); const { capacities, poolProductCapacities } = require('./responses'); -const { capacityEngine, getUtilizationRate } = require('../../src/lib/capacityEngine'); // Import the function to test +const { + capacityEngine, + getUtilizationRate, + calculateAvailableCapacity, + calculateProductDataForTranche, + getProductsInPool, + calculateTrancheInfo, +} = require('../../src/lib/capacityEngine'); +const { NXM_PER_ALLOCATION_UNIT } = require('../../src/lib/constants'); +const { calculateTrancheId, bnMax } = require('../../src/lib/helpers'); const { selectAsset } = require('../../src/store/selectors'); const mockStore = require('../mocks/store'); const { BigNumber } = ethers; const { parseEther } = ethers.utils; const { Zero } = ethers.constants; +const { assets, assetRates } = mockStore; describe('Capacity Engine tests', function () { describe('capacityEngine', function () { @@ -179,6 +189,61 @@ describe('Capacity Engine tests', function () { expect(product.productId).to.equal(Number(nonFixedPricedProductId)); expect(product.minAnnualPrice).to.not.deep.equal(product.maxAnnualPrice); }); + + it('should not include capacityPerPool when withPools is false', function () { + const productId = '0'; + const [productWithoutPools] = capacityEngine(store, { productIds: [productId], withPools: false }); + expect(productWithoutPools).to.not.have.property('capacityPerPool'); + }); + + it('should include capacityPerPool if query param withPool=true', function () { + const productId = '0'; + + const [productWithPools] = capacityEngine(store, { productIds: [productId], withPools: true }); + expect(productWithPools).to.have.property('capacityPerPool'); + + // Sum up values from capacityPerPool + const initCapacity = { usedCapacity: Zero, availableCapacity: {}, minAnnualPrice: Zero, maxAnnualPrice: Zero }; + const summedCapacity = productWithPools.capacityPerPool.reduce((acc, pool) => { + acc.usedCapacity = acc.usedCapacity.add(pool.allocatedNxm); + acc.maxAnnualPrice = bnMax(acc.maxAnnualPrice, pool.maxAnnualPrice); + + // skip poolId 3 as there is 0 available capacity + if (pool.poolId !== 3 && (acc.minAnnualPrice.isZero() || pool.minAnnualPrice.lt(acc.minAnnualPrice))) { + acc.minAnnualPrice = pool.minAnnualPrice; + } + + pool.availableCapacity.forEach(asset => { + acc.availableCapacity[asset.assetId] = (acc.availableCapacity[asset.assetId] ?? Zero).add(asset.amount); + }); + + return acc; + }, initCapacity); + + // Compare summed values with root-level (across all pools) values + expect(summedCapacity.usedCapacity.toString()).to.equal(productWithPools.usedCapacity.toString()); + expect(summedCapacity.minAnnualPrice.toString()).to.equal(productWithPools.minAnnualPrice.toString()); + expect(summedCapacity.maxAnnualPrice.toString()).to.equal(productWithPools.maxAnnualPrice.toString()); + + productWithPools.availableCapacity.forEach(asset => { + expect(summedCapacity.availableCapacity[asset.assetId].toString()).to.equal(asset.amount.toString()); + }); + }); + + it('should have root-level (across all pools) prices within range of pool prices', function () { + const productId = '0'; + const [productWithPools] = capacityEngine(store, { productIds: [productId], withPools: true }); + + const poolPrices = productWithPools.capacityPerPool + .filter(pool => pool.poolId !== 3) // skip poolId 3 as there is 0 available capacity + .map(pool => ({ + min: pool.minAnnualPrice, + max: pool.maxAnnualPrice, + })); + + expect(productWithPools.minAnnualPrice.toString()).to.equal(Math.min(...poolPrices.map(p => p.min)).toString()); + expect(productWithPools.maxAnnualPrice.toString()).to.equal(Math.max(...poolPrices.map(p => p.max)).toString()); + }); }); describe('getUtilizationRate tests', function () { @@ -220,4 +285,317 @@ describe('Capacity Engine tests', function () { expect(utilizationRate.toNumber()).to.equal(0); }); }); + + describe('calculateAvailableCapacity', function () { + it('should calculate available capacity correctly when all tranches are usable', function () { + const trancheCapacities = [BigNumber.from(100), BigNumber.from(200), BigNumber.from(300)]; + const allocations = [BigNumber.from(50), BigNumber.from(100), BigNumber.from(150)]; + const firstUsableTrancheIndex = 0; + + const result = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); + + expect(result.toString()).to.equal('300'); // (100-50) + (200-100) + (300-150) = 300 + }); + + it('should handle unusable tranches correctly', function () { + const trancheCapacities = [BigNumber.from(100), BigNumber.from(200), BigNumber.from(300)]; + const allocations = [BigNumber.from(50), BigNumber.from(100), BigNumber.from(150)]; + const firstUsableTrancheIndex = 1; + + const result = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); + + expect(result.toString()).to.equal('250'); // 0 + (200-100) + (300-150) = 250 + }); + + it('should carry over negative values on unusable tranches', function () { + const trancheCapacities = [BigNumber.from(100), BigNumber.from(200), BigNumber.from(300)]; + const allocations = [BigNumber.from(150), BigNumber.from(100), BigNumber.from(150)]; + const firstUsableTrancheIndex = 1; + + const result = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); + + expect(result.toString()).to.equal('200'); // -50 + (200-100) + (300-150) = 200 + }); + + it('should return zero when all capacity is allocated', function () { + const trancheCapacities = [BigNumber.from(100), BigNumber.from(200)]; + const allocations = [BigNumber.from(100), BigNumber.from(200)]; + const firstUsableTrancheIndex = 0; + + const result = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); + + expect(result.toString()).to.equal('0'); + }); + + it('should handle empty arrays', function () { + const trancheCapacities = []; + const allocations = []; + const firstUsableTrancheIndex = 0; + + const result = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); + + expect(result.toString()).to.equal('0'); + }); + + it('should handle case where allocations exceed capacities', function () { + const trancheCapacities = [BigNumber.from(100), BigNumber.from(200)]; + const allocations = [BigNumber.from(150), BigNumber.from(250)]; + const firstUsableTrancheIndex = 0; + + const result = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex); + + expect(result.toString()).to.equal('0'); + }); + }); + + describe('calculateProductDataForTranche', function () { + const now = BigNumber.from(1000); + + function assertAvailableCapacity(capacityPool, availableInNXM) { + expect(capacityPool.availableCapacity).to.be.an('array'); + expect(capacityPool.availableCapacity).to.have.lengthOf(Object.keys(assets).length); + + Object.keys(assets).forEach((assetId, index) => { + const expectedAmount = availableInNXM.mul(assetRates[assetId]).div(BigNumber.from(10).pow(18)); + expect(capacityPool.availableCapacity[index].assetId).to.equal(Number(assetId)); + expect(capacityPool.availableCapacity[index].amount.toString()).to.equal(expectedAmount.toString()); + expect(capacityPool.availableCapacity[index].asset).to.deep.equal(assets[assetId]); + }); + } + + it('should calculate product data correctly for fixed price', function () { + const product2Pool1 = [mockStore.poolProducts['2_1']]; // Product 2 uses fixed price + const firstUsableTrancheIndex = 0; + const [{ allocations, trancheCapacities }] = product2Pool1; + + const { aggregatedData, capacityPerPool } = calculateProductDataForTranche( + product2Pool1, + firstUsableTrancheIndex, + mockStore.products['2'].useFixedPrice, + now, + assets, + assetRates, + ); + + const [capacityPool] = capacityPerPool; + const lastIndex = allocations.length - 1; + + expect(aggregatedData.capacityUsedNXM.toString()).to.equal(allocations[lastIndex].toString()); + expect(aggregatedData.capacityAvailableNXM.toString()).to.equal( + trancheCapacities[lastIndex].sub(allocations[lastIndex]).mul(NXM_PER_ALLOCATION_UNIT).toString(), + ); + + expect(capacityPerPool).to.have.lengthOf(1); + expect(capacityPool.poolId).to.equal(1); + expect(capacityPool.minAnnualPrice.toString()).to.equal(capacityPool.maxAnnualPrice.toString()); + expect(capacityPool.allocatedNxm.toString()).to.equal(allocations[lastIndex].toString()); + + const availableInNXM = trancheCapacities[lastIndex].sub(allocations[lastIndex]).mul(NXM_PER_ALLOCATION_UNIT); + assertAvailableCapacity(capacityPool, availableInNXM); + }); + + it('should calculate product data correctly for non-fixed price', function () { + const product0Pool1 = [mockStore.poolProducts['0_1']]; // Product 0 doesn't use fixed price + const firstUsableTrancheIndex = 0; + const [{ allocations, trancheCapacities }] = product0Pool1; + + const { aggregatedData, capacityPerPool } = calculateProductDataForTranche( + product0Pool1, + firstUsableTrancheIndex, + mockStore.products['0'].useFixedPrice, + now, + assets, + assetRates, + ); + + const [pool1Capacity] = capacityPerPool; + const lastIndex = allocations.length - 1; + + expect(aggregatedData.capacityUsedNXM.toString()).to.equal(allocations[lastIndex].toString()); + expect(aggregatedData.capacityAvailableNXM.toString()).to.equal( + trancheCapacities[lastIndex].sub(allocations[lastIndex]).mul(NXM_PER_ALLOCATION_UNIT).toString(), + ); + expect(capacityPerPool).to.have.lengthOf(1); + expect(pool1Capacity.poolId).to.equal(1); + expect(pool1Capacity.minAnnualPrice.toString()).to.not.equal(pool1Capacity.maxAnnualPrice.toString()); + + const availableInNXM = trancheCapacities[lastIndex].sub(allocations[lastIndex]).mul(NXM_PER_ALLOCATION_UNIT); + assertAvailableCapacity(pool1Capacity, availableInNXM); + }); + + it('should handle zero available capacity', function () { + const productPools = [ + { + ...mockStore.poolProducts['0_1'], + allocations: [...Array(7).fill(Zero), BigNumber.from(9840)], + trancheCapacities: [...Array(7).fill(Zero), BigNumber.from(9840)], + }, + ]; + const firstUsableTrancheIndex = 0; + + const { aggregatedData, capacityPerPool } = calculateProductDataForTranche( + productPools, + firstUsableTrancheIndex, + mockStore.products['0'].useFixedPrice, + now, + assets, + assetRates, + ); + + const [pool1Capacity] = capacityPerPool; + + expect(aggregatedData.capacityAvailableNXM.toString()).to.equal('0'); + expect(pool1Capacity.availableCapacity).to.deep.equal([]); + expect(pool1Capacity.minAnnualPrice.toString()).to.equal('0'); + expect(pool1Capacity.maxAnnualPrice.toString()).to.equal('0'); + }); + + it('should calculate product data correctly for multiple pools of the same product', function () { + const productPools = [mockStore.poolProducts['0_1'], mockStore.poolProducts['0_2']]; + const firstUsableTrancheIndex = 0; + + const { aggregatedData, capacityPerPool } = calculateProductDataForTranche( + productPools, + firstUsableTrancheIndex, + mockStore.products['0'].useFixedPrice, + now, + assets, + assetRates, + ); + + expect(capacityPerPool).to.have.lengthOf(2); + + const [pool1Product0, pool2Product0] = productPools; + const [pool1Capacity, pool2Capacity] = capacityPerPool; + + const lastIndex1 = pool1Product0.allocations.length - 1; + const lastIndex2 = pool2Product0.allocations.length - 1; + + // Check aggregated data + expect(aggregatedData.capacityUsedNXM.toString()).to.equal( + pool1Product0.allocations[lastIndex1].add(pool2Product0.allocations[lastIndex2]).toString(), + ); + expect(aggregatedData.capacityAvailableNXM.toString()).to.equal( + pool1Product0.trancheCapacities[lastIndex1] + .sub(pool1Product0.allocations[lastIndex1]) + .add(pool2Product0.trancheCapacities[lastIndex2].sub(pool2Product0.allocations[lastIndex2])) + .mul(NXM_PER_ALLOCATION_UNIT) + .toString(), + ); + + expect(pool1Capacity.poolId).to.equal(1); + expect(pool2Capacity.poolId).to.equal(2); + + // Additional checks for each pool + capacityPerPool.forEach((poolCapacity, index) => { + expect(poolCapacity.minAnnualPrice.toString()).to.not.equal(poolCapacity.maxAnnualPrice.toString()); + expect(poolCapacity.availableCapacity.length).to.not.equal(0); + + const { allocations, trancheCapacities } = productPools[index]; + const lastIndex = allocations.length - 1; + const availableInNXM = trancheCapacities[lastIndex].sub(allocations[lastIndex]).mul(NXM_PER_ALLOCATION_UNIT); + assertAvailableCapacity(poolCapacity, availableInNXM); + }); + }); + }); + + describe('getProductsInPool', function () { + let mockStore; + + beforeEach(function () { + mockStore = { + getState: sinon.stub(), + }; + }); + + afterEach(function () { + sinon.restore(); + }); + + it('should return correct product IDs for a given pool', function () { + mockStore.getState.returns({ + products: { 1: {}, 2: {}, 3: {} }, + productPoolIds: { 1: [1, 2], 2: [2], 3: [1, 2] }, + poolProducts: { '1_2': { poolId: 2 }, '2_2': { poolId: 2 }, '3_2': { poolId: 2 } }, + }); + + const poolId = 2; + const result = getProductsInPool(mockStore, poolId); + + expect(result).to.deep.equal(['1', '2', '3']); + }); + + it('should return an empty array for a pool with no products', function () { + mockStore.getState.returns({ + products: { 1: {}, 2: {}, 3: {} }, + productPoolIds: { 1: [1], 2: [1], 3: [1] }, + poolProducts: { '1_1': { poolId: 1 }, '2_1': { poolId: 1 }, '3_1': { poolId: 1 } }, + }); + + const poolId = 2; + const result = getProductsInPool(mockStore, poolId); + + expect(result).to.deep.equal([]); + }); + + it('should handle undefined productPools', function () { + mockStore.getState.returns({ + products: { 1: {}, 2: {}, 3: {} }, + productPoolIds: {}, + poolProducts: {}, + }); + + const poolId = 2; + const result = getProductsInPool(mockStore, poolId); + + expect(result).to.deep.equal([]); + }); + }); + + describe('calculateTrancheInfo', function () { + const SECONDS_PER_DAY = BigNumber.from(24 * 3600); + const MAX_COVER_PERIOD = BigNumber.from(365 * 24 * 3600); + + it('should calculate tranche indices correctly', function () { + const time = BigNumber.from(1000); + const product = { gracePeriod: BigNumber.from(86400) }; // 1 day grace period + const period = 30; // 30 days coverage period + + const result = calculateTrancheInfo(time, product, period); + + const expectedFirstUsableTrancheIndex = + calculateTrancheId(time.add(SECONDS_PER_DAY.mul(period)).add(product.gracePeriod)) - calculateTrancheId(time); + const expectedFirstUsableTrancheForMaxPeriodIndex = + calculateTrancheId(time.add(MAX_COVER_PERIOD).add(product.gracePeriod)) - calculateTrancheId(time); + + expect(result.firstUsableTrancheIndex).to.equal(expectedFirstUsableTrancheIndex); + expect(result.firstUsableTrancheForMaxPeriodIndex).to.equal(expectedFirstUsableTrancheForMaxPeriodIndex); + }); + + it('should handle maximum period', function () { + const time = BigNumber.from(1000); + const product = { gracePeriod: BigNumber.from(86400) }; + const period = 365; // Maximum period + + const result = calculateTrancheInfo(time, product, period); + + expect(result.firstUsableTrancheIndex).to.equal(result.firstUsableTrancheForMaxPeriodIndex); + }); + + it('should handle very large grace period', function () { + const time = BigNumber.from(1000); + const product = { gracePeriod: BigNumber.from(365 * 24 * 3600) }; // 1 year grace period + const period = 30; + + const result = calculateTrancheInfo(time, product, period); + + const expectedFirstUsableTrancheIndex = + calculateTrancheId(time.add(SECONDS_PER_DAY.mul(period)).add(product.gracePeriod)) - calculateTrancheId(time); + const expectedFirstUsableTrancheForMaxPeriodIndex = + calculateTrancheId(time.add(MAX_COVER_PERIOD).add(product.gracePeriod)) - calculateTrancheId(time); + + expect(result.firstUsableTrancheIndex).to.equal(expectedFirstUsableTrancheIndex); + expect(result.firstUsableTrancheForMaxPeriodIndex).to.equal(expectedFirstUsableTrancheForMaxPeriodIndex); + }); + }); }); From 484eabf56e115bd2e856347cbf294756807586d5 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:38:56 +0300 Subject: [PATCH 07/13] test: update /capacity/:poolId route unit tests * capacityPerPool field unit test --- test/unit/responses.js | 102 +++++++++++++++++++++++++++++++++++ test/unit/routes/capacity.js | 16 +++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/test/unit/responses.js b/test/unit/responses.js index 21e5ab65..66be4b5d 100644 --- a/test/unit/responses.js +++ b/test/unit/responses.js @@ -181,6 +181,107 @@ const capacities = [ }, ]; +const productCapacityPerPools = { + // productId 0 + 0: [ + { + allocatedNxm: '0', + availableCapacity: [ + { + amount: '1011555965965397760', + asset: { + decimals: 18, + id: 0, + symbol: 'ETH', + }, + assetId: 0, + }, + { + amount: '2826484798959880487553', + asset: { + decimals: 18, + id: 1, + symbol: 'DAI', + }, + assetId: 1, + }, + { + amount: '2826484797', + asset: { + decimals: 6, + id: 6, + symbol: 'USDC', + }, + assetId: 6, + }, + { + amount: '98400000000000000000', + asset: { + decimals: 18, + id: 255, + symbol: 'NXM', + }, + assetId: 255, + }, + ], + maxAnnualPrice: '0.03', + minAnnualPrice: '0.02', + poolId: 1, + }, + { + allocatedNxm: '0', + availableCapacity: [ + { + amount: '3750158703091230720', + asset: { + decimals: 18, + id: 0, + symbol: 'ETH', + }, + assetId: 0, + }, + { + amount: '10478675352241508148979', + asset: { + decimals: 18, + id: 1, + symbol: 'DAI', + }, + assetId: 1, + }, + { + amount: '10478675347', + asset: { + decimals: 6, + id: 6, + symbol: 'USDC', + }, + assetId: 6, + }, + { + amount: '364800000000000000000', + asset: { + decimals: 18, + id: 255, + symbol: 'NXM', + }, + assetId: 255, + }, + ], + maxAnnualPrice: '0.03', + minAnnualPrice: '0.02', + poolId: 2, + }, + { + allocatedNxm: '364800000000000000000', + availableCapacity: [], + maxAnnualPrice: '0.0', + minAnnualPrice: '0.0', + poolId: 3, + }, + ], +}; + // capacities response for product by pool const poolProductCapacities = { // poolId 2 @@ -381,5 +482,6 @@ module.exports = { assets, capacities, poolProductCapacities, + productCapacityPerPools, getQuote, }; diff --git a/test/unit/routes/capacity.js b/test/unit/routes/capacity.js index 87c4a636..1ad49568 100644 --- a/test/unit/routes/capacity.js +++ b/test/unit/routes/capacity.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const supertest = require('supertest'); const initApp = require('../../mocks/server'); -const { capacities, poolProductCapacities } = require('../responses'); +const { capacities, poolProductCapacities, productCapacityPerPools } = require('../responses'); describe('Capacity Routes', () => { let server; @@ -39,9 +39,21 @@ describe('Capacity Routes', () => { const productId = 0; const url = `/v2/capacity/${productId}`; const { body: response } = await server.get(url).expect('Content-Type', /json/).expect(200); - expect(response).to.be.deep.equal(capacities[0]); + expect(response).to.be.deep.equal(capacities[productId]); }); + it('should have capacityPerPool if queryParam withPools=true', async function () { + const productId = 0; + const url = `/v2/capacity/${productId}?withPools=true`; + const { body: response } = await server.get(url).expect('Content-Type', /json/).expect(200); + + const expectedCapacity = capacities[productId]; + expectedCapacity.capacityPerPool = productCapacityPerPools[productId]; + + expect(response).to.be.deep.equal(expectedCapacity); + }); + // should get return capacityPerPool if queryParam withPool=true + it('should return 400 Invalid Product Id for non-existent productId', async function () { const nonExistentProductId = 999; const url = `/v2/capacity/${nonExistentProductId}`; From 35c99425ad17e55088655e6bb648935a03ee891d Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 16:56:14 +0300 Subject: [PATCH 08/13] docs: add capacityPerPool to openapi docs --- src/routes/capacity.js | 78 +++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/src/routes/capacity.js b/src/routes/capacity.js index 76e152f3..857b85b2 100644 --- a/src/routes/capacity.js +++ b/src/routes/capacity.js @@ -295,6 +295,53 @@ router.get( * @openapi * components: * schemas: + * AssetInfo: + * type: object + * description: An object containing asset info + * properties: + * id: + * type: integer + * description: The id of the asset + * symbol: + * type: string + * description: The symbol of the asset + * decimals: + * type: integer + * description: The decimals of the asset + * example: 18 + * AvailableCapacity: + * type: object + * properties: + * assetId: + * type: integer + * description: The asset id + * amount: + * type: string + * format: integer + * description: The capacity amount expressed in the asset + * asset: + * $ref: '#/components/schemas/AssetInfo' + * PoolCapacity: + * type: object + * properties: + * poolId: + * type: integer + * description: The pool id + * availableCapacity: + * type: array + * description: The maximum available capacity for the pool's product. + * items: + * $ref: '#/components/schemas/AvailableCapacity' + * allocatedNxm: + * type: string + * format: integer + * description: The used capacity amount for active covers on the pool. + * minAnnualPrice: + * type: string + * description: The minimal annual price is a percentage value between 0-1. + * maxAnnualPrice: + * type: string + * description: The maximal annual price is a percentage value between 0-1. * CapacityResult: * type: object * properties: @@ -305,29 +352,7 @@ router.get( * type: array * description: The maximum available capacity for the product. * items: - * type: object - * properties: - * assetId: - * type: integer - * description: The asset id - * amount: - * type: string - * format: integer - * description: The capacity amount expressed in the asset - * asset: - * type: object - * description: An object containing asset info - * properties: - * id: - * type: integer - * description: The id of the asset - * symbol: - * type: string - * description: The symbol of the asset - * decimals: - * type: integer - * description: The decimals of the asset - * example: 18 + * $ref: '#/components/schemas/AvailableCapacity' * allocatedNxm: * type: string * format: integer @@ -342,6 +367,13 @@ router.get( * maxAnnualPrice: * type: string * description: The maximal annual price is a percentage value between 0-1. + * capacityPerPool: + * type: array + * description: >- + * The capacity per pool. This field is only included when the query parameter `withPools=true`. + * If `withPools` is false or not provided, this field will be undefined and not present in the response. + * items: + * $ref: '#/components/schemas/PoolCapacity' */ module.exports = router; From faea433bf1ac50723a1cf66b5619a168535f6e66 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Fri, 11 Oct 2024 17:09:55 +0300 Subject: [PATCH 09/13] docs: update README docs for /v2/capacity/:productId --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c2cb961..ca3f1354 100644 --- a/README.md +++ b/README.md @@ -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}` From 6cef32a9c5757f10be34dfb6577c6763062e7c4f Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Tue, 19 Nov 2024 11:47:07 +0200 Subject: [PATCH 10/13] test: fix unit tests --- test/unit/responses.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/unit/responses.js b/test/unit/responses.js index 66be4b5d..8b75eee4 100644 --- a/test/unit/responses.js +++ b/test/unit/responses.js @@ -214,6 +214,15 @@ const productCapacityPerPools = { }, assetId: 6, }, + { + amount: '8095761', + asset: { + decimals: 8, + id: 7, + symbol: 'cbBTC', + }, + assetId: 7, + }, { amount: '98400000000000000000', asset: { @@ -258,6 +267,15 @@ const productCapacityPerPools = { }, assetId: 6, }, + { + amount: '30013555', + asset: { + decimals: 8, + id: 7, + symbol: 'cbBTC', + }, + assetId: 7, + }, { amount: '364800000000000000000', asset: { From dfb93047f2a834465c24710d5f8d394791bbb8da Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Tue, 19 Nov 2024 12:36:49 +0200 Subject: [PATCH 11/13] docs: fix swagger docs for capacity endpoints --- src/routes/capacity.js | 140 +++++++++++++++++++++++++++++------------ 1 file changed, 101 insertions(+), 39 deletions(-) diff --git a/src/routes/capacity.js b/src/routes/capacity.js index 857b85b2..11c4f365 100644 --- a/src/routes/capacity.js +++ b/src/routes/capacity.js @@ -93,13 +93,80 @@ router.get( * schema: * type: integer * description: The product id + * - in: query + * name: withPools + * required: false + * schema: + * type: boolean + * default: false + * description: When true, includes `capacityPerPool` field in the response * responses: * 200: - * description: Returns capacity data for a product + * description: Returns capacity data for a product. If withPools=true, includes capacityPerPool data. * content: * application/json: * schema: - * $ref: '#/components/schemas/CapacityResult' + * oneOf: + * - $ref: '#/components/schemas/CapacityResult' + * - $ref: '#/components/schemas/CapacityResultWithPools' + * examples: + * withoutPools: + * summary: Response when withPools=false + * value: + * productId: 1 + * availableCapacity: [ + * { + * assetId: 1, + * amount: "1000000000000000000", + * asset: { + * id: 1, + * symbol: "ETH", + * decimals: 18 + * } + * } + * ] + * allocatedNxm: "500000000000000000" + * utilizationRate: 5000 + * minAnnualPrice: "0.025" + * maxAnnualPrice: "0.1" + * withPools: + * summary: Response when withPools=true + * value: + * productId: 1 + * availableCapacity: [ + * { + * assetId: 1, + * amount: "1000000000000000000", + * asset: { + * id: 1, + * symbol: "ETH", + * decimals: 18 + * } + * } + * ] + * allocatedNxm: "500000000000000000" + * utilizationRate: 5000 + * minAnnualPrice: "0.025" + * maxAnnualPrice: "0.1" + * capacityPerPool: [ + * { + * poolId: 1, + * availableCapacity: [ + * { + * assetId: 1, + * amount: "500000000000000000", + * asset: { + * id: 1, + * symbol: "ETH", + * decimals: 18 + * } + * } + * ], + * allocatedNxm: "250000000000000000", + * minAnnualPrice: "0.025", + * maxAnnualPrice: "0.1" + * } + * ] * 400: * description: Invalid productId or period * 500: @@ -321,59 +388,54 @@ router.get( * description: The capacity amount expressed in the asset * asset: * $ref: '#/components/schemas/AssetInfo' - * PoolCapacity: + * BaseCapacityFields: * type: object * properties: - * poolId: - * type: integer - * description: The pool id * availableCapacity: * type: array - * description: The maximum available capacity for the pool's product. + * description: The maximum available capacity. * items: * $ref: '#/components/schemas/AvailableCapacity' * allocatedNxm: * type: string * format: integer - * description: The used capacity amount for active covers on the pool. + * description: The used capacity amount for active covers. * minAnnualPrice: * type: string * description: The minimal annual price is a percentage value between 0-1. * maxAnnualPrice: * type: string * description: The maximal annual price is a percentage value between 0-1. + * PoolCapacity: + * allOf: + * - $ref: '#/components/schemas/BaseCapacityFields' + * - type: object + * properties: + * poolId: + * type: integer + * description: The pool id * CapacityResult: - * type: object - * properties: - * productId: - * type: integer - * description: The product id - * availableCapacity: - * type: array - * description: The maximum available capacity for the product. - * items: - * $ref: '#/components/schemas/AvailableCapacity' - * allocatedNxm: - * type: string - * format: integer - * description: The used capacity amount for active covers on the product. - * utilizationRate: - * type: number - * format: integer - * description: The percentage of used capacity to total capacity, expressed as basis points (0-10,000). - * minAnnualPrice: - * type: string - * description: The minimal annual price is a percentage value between 0-1. - * maxAnnualPrice: - * type: string - * description: The maximal annual price is a percentage value between 0-1. - * capacityPerPool: - * type: array - * description: >- - * The capacity per pool. This field is only included when the query parameter `withPools=true`. - * If `withPools` is false or not provided, this field will be undefined and not present in the response. - * items: - * $ref: '#/components/schemas/PoolCapacity' + * allOf: + * - $ref: '#/components/schemas/BaseCapacityFields' + * - type: object + * properties: + * productId: + * type: integer + * description: The product id + * utilizationRate: + * type: number + * format: integer + * description: The percentage of used capacity to total capacity, expressed as basis points (0-10,000). + * CapacityResultWithPools: + * allOf: + * - $ref: '#/components/schemas/CapacityResult' + * - type: object + * properties: + * capacityPerPool: + * type: array + * description: The capacity per pool. + * items: + * $ref: '#/components/schemas/PoolCapacity' */ module.exports = router; From cd433271971a95ca8974e6c3f2cc489bea5c4882 Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Tue, 19 Nov 2024 13:04:34 +0200 Subject: [PATCH 12/13] test: clean up unit tests --- test/unit/routes/capacity.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/routes/capacity.js b/test/unit/routes/capacity.js index 1ad49568..598e2cc8 100644 --- a/test/unit/routes/capacity.js +++ b/test/unit/routes/capacity.js @@ -35,14 +35,14 @@ describe('Capacity Routes', () => { }); describe('GET /capacity/:productId', () => { - it('should get all capacities for one product', async function () { + it('should get all capacities for the specified productId', async function () { const productId = 0; const url = `/v2/capacity/${productId}`; const { body: response } = await server.get(url).expect('Content-Type', /json/).expect(200); expect(response).to.be.deep.equal(capacities[productId]); }); - it('should have capacityPerPool if queryParam withPools=true', async function () { + it('should have capacityPerPool field if queryParam withPools=true', async function () { const productId = 0; const url = `/v2/capacity/${productId}?withPools=true`; const { body: response } = await server.get(url).expect('Content-Type', /json/).expect(200); @@ -52,7 +52,6 @@ describe('Capacity Routes', () => { expect(response).to.be.deep.equal(expectedCapacity); }); - // should get return capacityPerPool if queryParam withPool=true it('should return 400 Invalid Product Id for non-existent productId', async function () { const nonExistentProductId = 999; From 92c29d411d405a5c58dd52cc0a1dba30c479cf8f Mon Sep 17 00:00:00 2001 From: Rocky Murdoch Date: Tue, 19 Nov 2024 16:53:16 +0200 Subject: [PATCH 13/13] docs: add code comments --- src/lib/capacityEngine.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/capacityEngine.js b/src/lib/capacityEngine.js index ca269fc1..a923ff99 100644 --- a/src/lib/capacityEngine.js +++ b/src/lib/capacityEngine.js @@ -124,6 +124,7 @@ function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, u } 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), @@ -264,6 +265,7 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30, wi } 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),