diff --git a/ON_CHAIN_UPDATES.md b/ON_CHAIN_UPDATES.md deleted file mode 100644 index 7eca81dc..00000000 --- a/ON_CHAIN_UPDATES.md +++ /dev/null @@ -1,42 +0,0 @@ -# On-chain Updates - -## Introduction - -This document is used for logging the on-chain updates for products and product types. The updates are made by the Nexus Mutual team and are used to keep the SDK up-to-date with the latest on-chain data. - -## How to update this document - -To update this document, follow the steps below: - -1. Identify the section(s) you need to update based on whether the on-chain changes are at listing level or at product type level. -2. In the corresponding section(s), add a new line starting with `YYYY-MM-DD`, where `YYYY-MM-DD` is the date of the update. -3. Fill in the details of the update. e.g. `Updated Protocol Cover cover wording`, `Updated Pendle listing annex`, etc. -4. Submit a PR with the changes. - -## Product Types Updates - -- 2025-03-26: Updated `Native Syndicate Cover` cover wording. - -## Listings Updates - -- 2025-04-23: Updated the Annex for `Fasanara`. -- 2025-05-21: Updated `Fasanara` listing annex. -- 2025-10-03: Updated `Dialectic Ellipse` listing annex -- 2025-11-05: Updated `AUTOfinance` listing name and annex. -- 2025-11-05: Updated `AUTOfinance autoUSD Vault` listing name and annex. -- 2025-11-05: Updated `Blue Chip Euler v2 Vaults & Markets` listing name and annex. -- 2025-11-05: Updated `Blue Chip Morpho Vaults & Markets` listing name and annex. -- 2025-11-05: Updated `B-Tier Morpho Vaults & Markets` listing name and annex. -- 2025-11-05: Updated `B-Tier Euler v2 Vaults & Markets` listing name and annex. -- 2025-11-05: Updated `Elite Cover` listing annex. -- 2025-11-05: Updated `Essential Cover` listing annex. -- 2025-11-05: Updated `Entry Cover` listing annex. -- 2025-11-05: Updated `Brava Advanced Cover` listing annex. -- 2025-11-05: Updated `Brava Balanced Cover` listing annex. -- 2025-11-05: Updated `Yearn v3` listing annex. -- 2025-11-05: Updated `Beefy` listing annex. -- 2025-12-09: Updated `Dialectic Ellipse` listing annex. -- 2025-12-11: Updated `Defi Vault 2` listing annex. -- 2026-01-26: Updated `K&R AAA` && `K&R AA` && `K&R A` && `K&R B` && `K&R C` && `K&R D` listings min price. -- 2026-02-02: Updated `Kidnap & Ransom Cover`product type grace period -- 2026-02-05: Updated `OpenCover Morpho Base Vaults` & `Noon DeFi Deployment 1` & `Noon DeFi Deployment 2` listing annex. diff --git a/README.md b/README.md index 9032bfbb..4a464d38 100644 --- a/README.md +++ b/README.md @@ -12,23 +12,31 @@ This package only exports CommonJS modules. You can import it like this: ```js // Usage with ES6 modules -import { products, productTypes } from '@nexusmutual/sdk'; +import { NexusSDK } from '@nexusmutual/sdk'; ``` ## Nexus Mutual contract addresses and abis Source of truth for the latest mainnet addresses. Feeds into https://api.nexusmutual.io/sdk/. -## Listed products and product types metadata +## Product metadata + +Product and product type metadata is served by the Nexus Mutual API (default base URL `https://api.nexusmutual.io/v2`). +Use the `ProductAPI` class to query products and product types instead of local JSON or logo assets. +For logos, use the URL `https://api.nexusmutual.io/v2/logos/:productId` -The `products` folder contains all protocols listed on Nexus Mutual. +### Example -If you're a protocol owner and want to update any details (i.e. logo, website, etc), please submit a PR. -Logos should meet the following criteria: +```typescript +import { ProductAPI } from '@nexusmutual/sdk'; -- svg format, with 1:1 ratio -- no fixed width or height -- the image should reach the edge of the viewbox +const productApi = new ProductAPI(); + +const productTypes = await productApi.getAllProductTypes(); +const product = await productApi.getProductById(247); + +console.log(productTypes.length, product.name); +``` ## Development @@ -250,7 +258,9 @@ Note: The following product types do not require IPFS content: - sherlockBugBounty - immunefiBugBounty -For a complete list of products and product types, see [products.json](https://sdk.nexusmutual.io/data/products.json) and [product-types.json](https://sdk.nexusmutual.io/data/product-types.json). +For a complete list of products and product types, use the API endpoints: +`GET https://api.nexusmutual.io/v2/products` and `GET https://api.nexusmutual.io/v2/product-types`, +or the `ProductAPI.getAllProducts()` and `ProductAPI.getAllProductTypes()` helpers. ### Validation Errors diff --git a/package-lock.json b/package-lock.json index b1838066..1b76eb23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,8 +45,7 @@ "semver": "^7.4.0", "ts-jest": "^29.1.1", "tsup": "^7.1.0", - "typescript": "^5.1.6", - "viem": "^1.16.6" + "typescript": "^5.1.6" }, "engines": { "node": ">=11.14.0" @@ -61,12 +60,6 @@ "node": ">=0.10.0" } }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", - "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==", - "dev": true - }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -2681,30 +2674,6 @@ "integrity": "sha512-eRp7yuD8mxTPnmIyUbmjc+JjaW/fNDgjnXgaSFnbCChocMEmAINEZt/ynwvq0SeqBJnWaYxxGXKqZNHylyUVwQ==", "license": "GPL-3.0" }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dev": true, - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "dev": true, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2740,42 +2709,6 @@ "node": ">= 8" } }, - "node_modules/@scure/base": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", - "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", - "dev": true, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", - "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", - "dev": true, - "dependencies": { - "@noble/curves": "~1.2.0", - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", - "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", - "dev": true, - "dependencies": { - "@noble/hashes": "~1.3.0", - "@scure/base": "~1.1.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3206,30 +3139,6 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "node_modules/abitype": { - "version": "0.9.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.8.tgz", - "integrity": "sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.19.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -6879,21 +6788,6 @@ "node": ">=12" } }, - "node_modules/isows": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", - "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "peerDependencies": { - "ws": "*" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9789,57 +9683,6 @@ "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", "license": "MIT" }, - "node_modules/viem": { - "version": "1.21.4", - "resolved": "https://registry.npmjs.org/viem/-/viem-1.21.4.tgz", - "integrity": "sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "dependencies": { - "@adraffy/ens-normalize": "1.10.0", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@scure/bip32": "1.3.2", - "@scure/bip39": "1.2.1", - "abitype": "0.9.8", - "isows": "1.0.3", - "ws": "8.13.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/viem/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index b1a0f37e..c8d50c56 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "semver": "^7.4.0", "ts-jest": "^29.1.1", "tsup": "^7.1.0", - "typescript": "^5.1.6", - "viem": "^1.16.6" + "typescript": "^5.1.6" } } diff --git a/public/index.html b/public/index.html index 4a55ec19..33b2416e 100644 --- a/public/index.html +++ b/public/index.html @@ -24,8 +24,9 @@
- Product types 🡭
- Products 🡭 + + Product types 🡭
+ Products 🡭
diff --git a/scripts/build-logos.js b/scripts/build-logos.js deleted file mode 100644 index 8c5dfd32..00000000 --- a/scripts/build-logos.js +++ /dev/null @@ -1,100 +0,0 @@ -const { appendFile, mkdir, readdir, readFile, writeFile, copyFile } = require('fs').promises; -const path = require('path'); - -const { loadConfig, optimize } = require('svgo'); - -const { parseFilePath } = require('./utils'); - -const GENERATED_OUTPUT_DIR = path.join(__dirname, '../generated'); -const LOGOS_OUTPUT_DIR = path.join(GENERATED_OUTPUT_DIR, '/logos'); -const SRC_DIR = path.join(__dirname, '../src/logos'); - -const buildLogos = async () => { - await mkdir(LOGOS_OUTPUT_DIR, { recursive: true }); - - // Find all logos in the src/ folder - const allFilePaths = await readFiles(SRC_DIR); - const allFileNames = allFilePaths.map(p => path.basename(p)); - const svgFilePaths = allFilePaths.filter(path => path.endsWith('.svg')); - const otherFilePaths = allFilePaths.filter(path => !path.endsWith('.svg')); - - // Load `./svgo.config.js` - const config = await loadConfig(); - - // Create a Promise for each svg transformation - const transformations = svgFilePaths.map(async filePath => { - const content = await readFile(filePath); - const { filename } = parseFilePath(filePath); - - const { data } = optimize(content, config); - - return { name: filename, svg: data }; - }); - - // Execute all the transformations in parallel - const components = await Promise.all(transformations); - - // Write each component to output directory - await Promise.all( - components.map(async ({ name, svg }) => { - await writeFile(path.join(LOGOS_OUTPUT_DIR, `${name}.svg`), svg); - console.log(`Copy optimized ${name}.svg`); - }), - ); - - // Copy over non-svg files for legacy support. These files should be replaced with svg's - otherFilePaths.forEach(async filePath => { - const { filename, extension } = parseFilePath(filePath); - console.log(`Copy ${filename} for legacy support`); - await copyFile(filePath, path.join(LOGOS_OUTPUT_DIR, `${filename}.${extension}`)); - }); - - // Contains all file names including extension - const allLogoFileNames = allFileNames.map(filePath => { - const { filename, extension } = parseFilePath(filePath); - return `${filename}.${extension}`; - }); - - // Contains all file names without extension - const allLogoNames = allFileNames.map(filePath => { - const { filename } = parseFilePath(filePath); - return filename; - }); - - // Write `types.ts` file - await appendFile( - path.join(GENERATED_OUTPUT_DIR, 'types.ts'), - `\ -export const allLogoFileNames = [ - ${allLogoFileNames.map(filename => `'${filename}'`).join(',\n ')} -] as const; - -export type LogoFileName = (typeof allLogoFileNames)[number]; - -export const allLogoNames = [ - ${allLogoNames.map(name => `'${name}'`).join(',\n ')} -] as const; - -export type LogoName = (typeof allLogoNames)[number]; -`, - ); -}; - -async function readFiles(dir) { - const dirents = await readdir(dir, { withFileTypes: true }); - - const nestedPaths = await Promise.all( - dirents.map(dirent => { - const res = path.resolve(dir, dirent.name); - return dirent.isDirectory() ? readFiles(res) : res; - }), - ); - - const paths = nestedPaths.flat(); - - return paths; -} - -module.exports = { - buildLogos, -}; diff --git a/scripts/build-products.js b/scripts/build-products.js deleted file mode 100644 index b48454e9..00000000 --- a/scripts/build-products.js +++ /dev/null @@ -1,187 +0,0 @@ -const fs = require('fs'); -const { readdir } = require('fs').promises; -const path = require('path'); - -const { CoverProducts, Cover, addresses } = require('@nexusmutual/deployments'); -const ethers = require('ethers'); -const fetch = require('node-fetch'); - -const { parseProductCoverAssets, parseFilePath, getCoverAssetsSymbols, fetchEventsInBatches } = require('./utils'); -const { allPrivateProductsIds } = require(path.join(__dirname, '../src/constants/privateProducts.js')); -const productMetadata = require('../data/legacy-product-metadata.json'); - -const { PROVIDER_URL } = process.env; - -const ipfsURL = ipfsHash => `https://api.nexusmutual.io/ipfs/${ipfsHash}`; - -const fetchProductTypes = async coverProducts => { - const productTypesCount = (await coverProducts.getProductTypeCount()).toNumber(); - const ids = Array.from({ length: productTypesCount }, (_, i) => i); - - const productTypes = await Promise.all( - ids.map(async id => { - const [productType, name, { ipfsHash }] = await Promise.all([ - coverProducts.getProductType(id), - coverProducts.getProductTypeName(id), - coverProducts.getLatestProductTypeMetadata(id), - ]); - const coverWordingURL = ipfsURL(ipfsHash); - return { - id, - coverWordingURL, - name: name.trim(), - gracePeriod: productType.gracePeriod, - claimMethod: productType.claimMethod, - assessmentCooldownPeriod: productType.assessmentCooldownPeriod, - payoutRedemptionPeriod: productType.payoutRedemptionPeriod, - }; - }), - ); - - return productTypes; -}; - -const createLogoDict = async logosDir => { - const dirents = await readdir(logosDir, { withFileTypes: true }); - const filenames = dirents.map(dirent => dirent.name); - - const map = filenames.reduce((acc, filename) => { - const { id, filename: name, extension } = parseFilePath(filename); - - // Skip files that don't have an id. These files have no id in the filename and will never be - // used in a product - if (id) { - acc[Number(id)] = `${name}.${extension}`; - } - - return acc; - }, {}); - - return map; -}; - -const EVENTS_START_BLOCK = 7700000; - -const fetchProducts = async (coverContract, coverProducts, provider) => { - const eventFilter = coverProducts.filters.ProductSet(); - - const events = await fetchEventsInBatches(coverProducts, eventFilter, EVENTS_START_BLOCK, provider); - - const logos = await createLogoDict(path.join(__dirname, '../src/logos')); - - const sortedEvents = events - // sort ascending by blockNumber to get the latest ipfs hash - // (ascending rather than descending due to the reduce into productMetadata object below) - .sort((a, b) => a.blockNumber - b.blockNumber); - - await Promise.all( - sortedEvents.map(async event => { - const id = event.args.id.toNumber(); - const { ipfsHash } = await coverProducts.getLatestProductMetadata(id); - productMetadata[id] = { id, ipfsHash }; - - // Only update the timestamp if it's not already set - // This is to avoid overwriting the timestamp if the event was an update, not a creation (e.g. for ipfsMetadata) - if (!productMetadata[id].timestamp) { - const block = await provider.getBlock(event.blockNumber); - productMetadata[id].timestamp = block.timestamp; - } - }), - ); - - const productsCount = await coverProducts.getProductCount(); - const ids = Array.from(Array(productsCount.toNumber()).keys()); - const batches = []; - - while (ids.length) { - batches.push(ids.splice(0, 50)); - } - - const products = []; - const coverAssetsMap = await getCoverAssetsSymbols(provider); - const defaultMinPriceRatio = await coverContract.getDefaultMinPriceRatio(); - const defaultMinPrice = defaultMinPriceRatio.toNumber(); - - for (const batch of batches) { - const promises = batch.map(async id => { - const { productType, isDeprecated, useFixedPrice, coverAssets, minPrice } = await coverProducts.getProduct(id); - const name = await coverProducts.getProductName(id); - console.log(`Processing #${id} (${name})`); - const { ipfsHash, timestamp } = productMetadata[id]; - const metadata = ipfsHash === '' ? {} : await fetch(ipfsURL(ipfsHash)).then(res => res.json()); - - if (logos[id] === undefined) { - logos[id] = 'nexus-mutual.svg'; - } - - const isPrivate = allPrivateProductsIds.includes(id); - - let productMinPrice = minPrice || defaultMinPrice; - - if (productType === 2) { - productMinPrice = 0; - } - - return { - id, - name, - productType, - isDeprecated, - useFixedPrice, - logo: logos[id], - metadata, - coverAssets: parseProductCoverAssets(coverAssets, coverAssetsMap), - isPrivate, - timestamp, - minPrice: productMinPrice, - }; - }); - - const batchProducts = await Promise.all(promises); - products.push(...batchProducts); - } - - return products; -}; - -const buildProducts = async () => { - if (PROVIDER_URL === undefined) { - console.log('PROVIDER_URL environment variable is not defined'); - process.exit(1); - } - - const provider = new ethers.providers.JsonRpcProvider(PROVIDER_URL); - const coverProducts = new ethers.Contract(addresses.CoverProducts, CoverProducts, provider); - - console.log('Generating product types...'); - const productTypesPath = path.join(__dirname, '../generated/product-types.json'); - const productTypes = await fetchProductTypes(coverProducts); - fs.writeFileSync(productTypesPath, JSON.stringify(productTypes, null, 2)); - - // Generate ProductTypes enum - const generatedTypesPath = path.join(__dirname, '../generated/types.ts'); - const productTypeNamesCamelCased = productTypes - .map(({ name }) => name) - .map(name => name.replace(/ /g, '')) - .map(name => name.replace(/ETH/g, 'Eth')) - .map(name => name.replace(/&/g, 'And')) - .map(name => `${name[0].toLowerCase()}${name.slice(1)}`) - .map(name => (name.endsWith('Cover') ? name.slice(0, -5) : name)); - fs.appendFileSync( - generatedTypesPath, - `\nexport enum ProductTypes {\n${productTypeNamesCamelCased.map((name, i) => ` ${name} = ${i},`).join('\n')}\n}\n`, - ); - - const coverContract = new ethers.Contract(addresses.Cover, Cover, provider); - - console.log('Generating products...'); - const productsPath = path.join(__dirname, '../generated/products.json'); - const products = await fetchProducts(coverContract, coverProducts, provider); - fs.writeFileSync(productsPath, JSON.stringify(products, null, 2)); - - console.log('Done.'); -}; - -module.exports = { - buildProducts, -}; diff --git a/scripts/build.js b/scripts/build.js index e04c89ec..f250a36b 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -5,9 +5,6 @@ const path = require('node:path'); const { build } = require('tsup'); -const { buildLogos } = require('./build-logos'); -const { buildProducts } = require('./build-products'); - const main = async () => { // Clean directories const dist = path.join(__dirname, '../dist'); @@ -27,10 +24,6 @@ const main = async () => { const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')); fs.writeFileSync(sdkVersionPath, JSON.stringify({ version }, null, 2)); - // Build assets - await buildLogos(); - await buildProducts(); - await build({ entry: ['src/index.ts'], format: ['cjs', 'esm'], @@ -42,19 +35,6 @@ const main = async () => { publicDir: 'public', }); - // Copy over all processed logo files to dist - const logosOutDir = path.join(dist, 'logos'); - const logosSrcDir = path.join(__dirname, '../generated/logos'); - fs.mkdirSync(logosOutDir); - const logoDirents = await fs.promises.readdir(logosSrcDir, { withFileTypes: true }); - for (const dirent of logoDirents) { - if (dirent.isFile()) { - const source = path.join(logosSrcDir, dirent.name); - const dest = path.join(logosOutDir, dirent.name); - fs.copyFileSync(source, dest); - } - } - // Copy addresses from @nexusmutual/deployments package const addressesFile = path.join(__dirname, '../node_modules/@nexusmutual/deployments/dist/data/addresses.json'); const dataOutDir = path.join(dist, 'data'); @@ -74,12 +54,8 @@ const main = async () => { } } - // Copy products.json and product-types.json from generated to dist - const generatedDir = path.join(__dirname, '../generated'); - fs.copyFileSync(path.join(generatedDir, 'products.json'), path.join(dist, 'data/products.json')); - fs.copyFileSync(path.join(generatedDir, 'product-types.json'), path.join(dist, 'data/product-types.json')); - // Copy version.json from generated to dist + const generatedDir = path.join(__dirname, '../generated'); fs.copyFileSync(path.join(generatedDir, 'version.json'), path.join(dist, 'data/version.json')); }; diff --git a/src/constants/cover.ts b/src/constants/cover.ts index 61c848fb..9f373be9 100644 --- a/src/constants/cover.ts +++ b/src/constants/cover.ts @@ -1,7 +1,3 @@ -import { Address, zeroAddress } from 'viem'; - -import { ProductTypes } from '../../generated/types'; - export enum CoverAsset { ETH = 0, DAI = 1, @@ -24,85 +20,5 @@ export const MINIMUM_COVER_PERIOD = 28; export const MAXIMUM_COVER_PERIOD = 365; export const DEFAULT_SLIPPAGE = 10; // 0.1% -// Commission ratios -export const DEFAULT_COMMISSION_RATIO = 15_00; // 15% -export const SHERLOCK_BUG_BOUNTY_COMMISSION_RATIO = 10_00; // 10% -export const TRM_COMMISSION_RATIO = 10_00; // 10% -export const FUND_PORTFOLIO_COMMISSION_RATIO = 10_00; // 10% -export const DEFI_PASS_COMMISSION_RATIO = 10_00; // 10% -export const GENERALIZED_FUND_PORTFOLIO_COMMISSION_RATIO = 10_00; // 10% -export const CRYPTO_COVER_COMMISSION_RATIO = 10_00; // 10% -export const NEXUS_MUTUAL_COVER_COMMISSION_RATIO = 10_00; // 10% -export const LEVERAGED_LIQUIDATION_COMMISSION_RATIO = 10_00; // 10% -export const NON_EVM_PROTOCOL_COMMISSION_RATIO = 10_00; -export const KIDNAP_AND_RANSOM_COVER_COMMISSION_RATIO = 10_00; -export const NO_COMMISSION = 0; // 0% - -// Commission destinations -export const NO_COMMISSION_DESTINATION = zeroAddress as Address; export const NEXUS_MUTUAL_DAO_TREASURY_ADDRESS = '0x8e53D04644E9ab0412a8c6bd228C84da7664cFE3'; -export const IMMUNEFI_ADDRESS = '0x9c2F47079eb7Def5dd01Dd7E1138583f82376bDc' as Address; -export const SPEARBIT_CANTINA_ADDRESS = '0x3Dcb7CFbB431A11CAbb6f7F2296E2354f488Efc2' as Address; -export const VAULT_ADDRESS = '0x5f2b6e70aa6a217e9ecd1ed7d0f8f38ce9a348a2'; - -export const BUY_COVER_COMMISSION_RATIO_BY_PRODUCT_TYPE: Record = { - [ProductTypes.ethSlashing]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.liquidCollectiveEthStaking]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.stakewiseEthStaking]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.sherlockQuotaShare]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.unoReQuotaShare]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.theRetailMutual]: TRM_COMMISSION_RATIO, - [ProductTypes.singleProtocol]: NEXUS_MUTUAL_COVER_COMMISSION_RATIO, - [ProductTypes.multiProtocol]: NEXUS_MUTUAL_COVER_COMMISSION_RATIO, - [ProductTypes.custody]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.yieldToken]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.sherlockExcess]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.nativeProtocol]: NEXUS_MUTUAL_COVER_COMMISSION_RATIO, - [ProductTypes.ethSlashingUmbrella]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.openCoverTransaction]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.fundPortfolio]: FUND_PORTFOLIO_COMMISSION_RATIO, - [ProductTypes.sherlockBugBounty]: SHERLOCK_BUG_BOUNTY_COMMISSION_RATIO, - [ProductTypes.deFiPass]: DEFI_PASS_COMMISSION_RATIO, - [ProductTypes.followOn]: NO_COMMISSION, - [ProductTypes.immunefiBugBounty]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.nexusMutual]: NEXUS_MUTUAL_COVER_COMMISSION_RATIO, - [ProductTypes.generalizedFundPortfolio]: GENERALIZED_FUND_PORTFOLIO_COMMISSION_RATIO, - [ProductTypes.crypto]: CRYPTO_COVER_COMMISSION_RATIO, - [ProductTypes.nativeSyndicate]: NO_COMMISSION, - [ProductTypes.spearbitCantina]: DEFAULT_COMMISSION_RATIO, - [ProductTypes.leveragedLiquidation]: LEVERAGED_LIQUIDATION_COMMISSION_RATIO, - [ProductTypes.nonEVMProtocol]: NON_EVM_PROTOCOL_COMMISSION_RATIO, - [ProductTypes.kidnapAndRansom]: KIDNAP_AND_RANSOM_COVER_COMMISSION_RATIO, - [ProductTypes.vault]: DEFAULT_COMMISSION_RATIO, -}; - -export const BUY_COVER_COMMISSION_DESTINATION_BY_PRODUCT_TYPE: Record = { - [ProductTypes.ethSlashing]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.liquidCollectiveEthStaking]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.stakewiseEthStaking]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.sherlockQuotaShare]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.unoReQuotaShare]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.theRetailMutual]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.singleProtocol]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.multiProtocol]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.custody]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.yieldToken]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.sherlockExcess]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.nativeProtocol]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.ethSlashingUmbrella]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.openCoverTransaction]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.fundPortfolio]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.sherlockBugBounty]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.deFiPass]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.followOn]: NO_COMMISSION_DESTINATION, - [ProductTypes.immunefiBugBounty]: IMMUNEFI_ADDRESS, - [ProductTypes.nexusMutual]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.generalizedFundPortfolio]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.crypto]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.nativeSyndicate]: NO_COMMISSION_DESTINATION, - [ProductTypes.spearbitCantina]: SPEARBIT_CANTINA_ADDRESS, - [ProductTypes.leveragedLiquidation]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.nonEVMProtocol]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.kidnapAndRansom]: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, - [ProductTypes.vault]: VAULT_ADDRESS, -}; +export const NEXUS_MUTUAL_COVER_COMMISSION_RATIO = 1000; // 10% diff --git a/src/constants/index.ts b/src/constants/index.ts index 562b7175..59fbe1a7 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,5 +1,2 @@ -const { allPrivateProductsIds } = require('./privateProducts') as { allPrivateProductsIds: number[] }; - -export { allPrivateProductsIds }; export * from './cover'; export * from './products'; diff --git a/src/constants/privateProducts.js b/src/constants/privateProducts.js deleted file mode 100644 index 428ef444..00000000 --- a/src/constants/privateProducts.js +++ /dev/null @@ -1,109 +0,0 @@ -// List of all private products ids -// For new private products, add the id here - -const allPrivateProductsIds = [ - 17, // Bundle: Gelt + mStable + Aave v2 - 43, // Liquid Collective - 63, // Sherlock - 65, // Stakewise 3rd party (3 ETH/validator) - 66, // Stakewise operated (3 ETH/validator) - 82, // EtherFi - 83, // Squeeth by Opyn (Sherlock) - 84, // Rage Trade (Sherlock) - 85, // Sentiment (Sherlock) - 86, // Lyra Newport (Sherlock) - 87, // Perennial (Sherlock) - 88, // LiquiFi (Sherlock) - 89, // Lyra Avalon (Sherlock) - 90, // Buffer Finance (Sherlock) - 91, // Hook (Sherlock) - 92, // Holyheld (Sherlock) - 93, // Union Finance (Sherlock)" - 94, // OpenQ (Sherlock) - 99, // Chorus One - 100, // Kiln - 101, // Vertex (Native Protocol) - 102, // The Retail Mutual - 104, // Teller (Sherlock) - 105, // Ajna (Sherlock) - 107, // Vox Finance (UnoRe) - 108, // MahaLend (UnoRe) - 109, // SELF (UnoRe) - 110, // Scallop (UnoRe) - 111, // WeFi (UnoRe) - 112, // ZkTsunami (UnoRe) - 113, // Hats Protocol - 151, // Arcadia (Sherlock) - 159, // Liquid Collective - 163, // Pocket Universe - 164, // Request Finance - 186, // Delta Prime (UnoRe) - 195, // Dialectic Moonphase - 196, // Dialectic Chronograph - 220, // Flat Money (Sherlock) - 227, // Base DeFi Pass - 233, // Relative Finance - 239, // Everstake - 240, // Ensuro (deprecated) - 250, // Fasanara - 259, // Bittensor - 260, // DeFi Covered - 273, // L1 Advisors Cover - 274, // Figment ETH Slashing - 287, // Ensuro Yield (deprecated) - 291, // OC Cover - 292, // Usual Bug Bounty - 323, // Colend Single Protocol Cover - 334, // Noon DeFi Deployment - 337, // Mellow Finance Bug Bounty Cover - 340, // wstETH (Aave) - 341, // weETH (Aave) - 342, // rsETH (Aave) - 353, // Dialectic Ellipse - 358, // DeFi Vault 1 - 359, // DeFi Vault 2 - 360, // TermMax Markets - 361, // Vesu - 362, // Algoquant - 363, // Solstice USX Depeg - 366, // OpenCover Morpho Base Vaults - 367, // K&R AAA - 368, // K&R AA - 369, // K&R A - 370, // K&R B - 371, // K&R C - 372, // K&R D - 373, // PT-cUSD-29JAN2026 (Morpho) - 375, // Fasanara 2 - 376, // Noon DeFi Deployment 2 - 377, // IPOR TAU InfiniFi Vault - 379, // PT Ethena sUSDE 5FEB2026 (Aave) - 380, // PT Ethena USDe 5FEB2026 (Aave) - 381, // PT Ethena sUSDE 15JAN2026 (Aave) - 382, // PT Ethena USDe 15JAN2026 (Aave) - 389, // Fasanara 2 - 390, // Noon DeFi Deployment 2 - 392, // Sablier - 393, // OpenCover Smokehouse USDC - 394, // Ekubo Starknet - 395, // OpenCover Smokehouse USDT - 396, // OpenCover Sentora PYUSD - 397, // OpenCover Steakhouse H Yield I USDT - 398, // OpenCover Steakhouse Prime I USDT - 399, // OpenCover Steakhouse H Yield I AUSD - 400, // OpenCover Steakhouse H Yield I USDC - 401, // PT-stcUSD-23JUL2026 (Morpho) - 402, // PT-cUSD-23JUL2026 (Morpho) - 403, // TermMax - 405, // OpenCover YoUSD - 406, // Teller - 409, // OpenCover WETH SuperVault - 410, // OpenCover Base USDC SuperVault - 411, // OpenCover WBTC SuperVault - 412, // OpenCover Steakhouse HYI USDC Base - 413, // OpenCover Steakhouse USDC -]; - -module.exports = { - allPrivateProductsIds, -}; diff --git a/src/index.ts b/src/index.ts index f62b98c6..789ea975 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,22 +3,18 @@ import * as deployments from '@nexusmutual/deployments'; import * as constants from './constants'; import * as ipfs from './ipfs'; import { NexusSDK } from './nexus-sdk'; +import * as productApi from './product-api'; import * as quote from './quote'; import * as swap from './swap'; import * as types from './types'; -import productTypesData from '../generated/product-types.json'; -import productsData from '../generated/products.json'; -import * as generatedTypes from '../generated/types'; const nexusSdk = { ...deployments, - products: productsData, - productTypes: productTypesData, - ...generatedTypes, ...swap, ...types, ...quote, ...ipfs, + ...productApi, ...constants, NexusSDK, }; @@ -26,12 +22,7 @@ const nexusSdk = { // Re-export everything from the deployments package (e.g. `addresses` and `abis`) export * from '@nexusmutual/deployments'; -// Export product data, so it will be included in the bundle -export { default as products } from '../generated/products.json'; -export { default as productTypes } from '../generated/product-types.json'; - -// Export generated logo types -export * from '../generated/types'; +export * from './product-api'; export * from './swap'; diff --git a/src/product-api/ProductAPI.ts b/src/product-api/ProductAPI.ts new file mode 100644 index 00000000..16e21278 --- /dev/null +++ b/src/product-api/ProductAPI.ts @@ -0,0 +1,51 @@ +import { NexusSDKBase } from '../nexus-sdk-base'; +import { Product, ProductType } from '../types/product'; +import { NexusSDKConfig } from '../types/sdk'; + +export class ProductAPI extends NexusSDKBase { + /** + * Create a new ProductAPI instance + * @param config SDK configuration + */ + constructor(config: NexusSDKConfig = {}) { + super(config); + } + + /** + * Get product type details by ID + * @param productTypeId ID of the product type + * @returns Product type details + */ + public async getProductTypeById(productTypeId: number): Promise { + const productTypeEndpoint = `/product-types/${productTypeId}?withAttributes=ipfsContentType`; + return this.sendRequest(productTypeEndpoint); + } + + /** + * Get all product types + * @returns List of product types + */ + public async getAllProductTypes(): Promise { + const productTypesEndpoint = '/product-types?withAttributes=ipfsContentType'; + return this.sendRequest(productTypesEndpoint); + } + + /** + * Get product details by ID + * @param productId ID of the product + * @returns Product details + */ + public async getProductById(productId: number): Promise { + const productEndpoint = `/products/${productId}`; + return this.sendRequest(productEndpoint); + } + + /** + * Get all products + * @returns List of products + */ + public async getAllProducts(): Promise { + const productsEndpoint = '/products'; + return this.sendRequest(productsEndpoint); + } +} diff --git a/src/product-api/index.ts b/src/product-api/index.ts new file mode 100644 index 00000000..e9f560aa --- /dev/null +++ b/src/product-api/index.ts @@ -0,0 +1 @@ +export * from './ProductAPI'; diff --git a/src/quote/Quote.ts b/src/quote/Quote.ts index bb91efd1..aac94e0a 100644 --- a/src/quote/Quote.ts +++ b/src/quote/Quote.ts @@ -1,11 +1,6 @@ import { AxiosError, AxiosRequestConfig } from 'axios'; -import productTypes from '../../generated/product-types.json'; -import products from '../../generated/products.json'; -import { ProductTypes } from '../../generated/types'; import { - BUY_COVER_COMMISSION_DESTINATION_BY_PRODUCT_TYPE, - BUY_COVER_COMMISSION_RATIO_BY_PRODUCT_TYPE, COMMISSION_DENOMINATOR, CoverAsset, DEFAULT_SLIPPAGE, @@ -17,6 +12,7 @@ import { } from '../constants'; import { Ipfs } from '../ipfs'; import { NexusSDKBase } from '../nexus-sdk-base'; +import { ProductAPI } from '../product-api/ProductAPI'; import { CoverRouterProductCapacityResponse, CoverRouterQuoteResponse, @@ -27,23 +23,14 @@ import { NexusSDKConfig, QuoteParams, IPFSTypeContentTuple, - IPFS_CONTENT_TYPE_BY_PRODUCT_TYPE, } from '../types'; -type ProductDTO = Omit<(typeof products)[number], 'productType'> & { - productType: ProductTypes; -}; - -const productsMap: Record = products.reduce( - (acc, product) => ({ ...acc, [product.id]: product }), - {}, -); - /** * Class for handling quote-related functionality */ export class Quote extends NexusSDKBase { private ipfs: Ipfs; + private productAPI: ProductAPI; /** * Create a new Quote instance @@ -53,6 +40,7 @@ export class Quote extends NexusSDKBase { constructor(config: NexusSDKConfig = {}, ipfs?: Ipfs) { super(config); this.ipfs = ipfs || new Ipfs(config); + this.productAPI = new ProductAPI(config); } /** @@ -151,26 +139,29 @@ export class Quote extends NexusSDKBase { } } - const productType = productsMap[productId]?.productType; - if (productType === undefined) { + const product = await this.productAPI.getProductById(productId); + const productTypeId = product?.productType; + if (productTypeId === undefined) { return { result: undefined, error: { message: `Invalid product` }, }; } - if (IPFS_CONTENT_TYPE_BY_PRODUCT_TYPE[productType] !== undefined && !ipfsCidOrContent) { + const productType = await this.productAPI.getProductTypeById(productTypeId); + + if (productType.ipfsContentType !== undefined && !ipfsCidOrContent) { return { result: undefined, error: { message: `Missing IPFS content. \n - ${productTypes[productType]?.name} requires ${IPFS_CONTENT_TYPE_BY_PRODUCT_TYPE[productType]} content type.`, + ${productType.name} requires ${productType.ipfsContentType} content type.`, }, }; } let ipfsData = ipfsCidOrContent as string; - const contentType = IPFS_CONTENT_TYPE_BY_PRODUCT_TYPE[productType]; + const contentType = productType.ipfsContentType; // Handle uploading content to IPFS if provided as an object if (!isString && contentType !== undefined && ipfsCidOrContent) { @@ -207,12 +198,12 @@ export class Quote extends NexusSDKBase { const maxPremiumInAsset = this.calculatePremiumWithCommissionAndSlippage( BigInt(premium), - commissionRatio || BUY_COVER_COMMISSION_RATIO_BY_PRODUCT_TYPE[productType], + commissionRatio || +productType.commissionRatio, slippageValue, ); const yearlyCostPerc = this.calculatePremiumWithCommissionAndSlippage( BigInt(quote.annualPrice), - commissionRatio || BUY_COVER_COMMISSION_RATIO_BY_PRODUCT_TYPE[productType], + commissionRatio || +productType.commissionRatio, slippageValue, ); @@ -236,9 +227,8 @@ export class Quote extends NexusSDKBase { period: period * 60 * 60 * 24, // seconds maxPremiumInAsset: maxPremiumInAsset.toString(), paymentAsset: paymentAssetEnum, - commissionRatio: commissionRatio || BUY_COVER_COMMISSION_RATIO_BY_PRODUCT_TYPE[productType], - commissionDestination: - commissionDestination || BUY_COVER_COMMISSION_DESTINATION_BY_PRODUCT_TYPE[productType], + commissionRatio: commissionRatio || +productType.commissionRatio, + commissionDestination: commissionDestination || productType.commissionDestination, ipfsData, }, poolAllocationRequests: quote.poolAllocationRequests, diff --git a/src/quote/calculatePremiumWithCommissionAndSlippage.test.ts b/src/quote/calculatePremiumWithCommissionAndSlippage.test.ts index f815d2a5..b7c3517d 100644 --- a/src/quote/calculatePremiumWithCommissionAndSlippage.test.ts +++ b/src/quote/calculatePremiumWithCommissionAndSlippage.test.ts @@ -1,4 +1,4 @@ -import { formatEther, parseEther } from 'viem'; +import { formatEther, parseEther } from 'ethers/lib/utils'; import { Quote } from './index'; import { COMMISSION_DENOMINATOR } from '../constants/cover'; @@ -6,7 +6,7 @@ import { COMMISSION_DENOMINATOR } from '../constants/cover'; const BUY_COVER_COMMISSION_RATIO = 1500; describe('calculatePremiumWithCommissionAndSlippage', () => { - let priceValue = parseEther('0'); + let priceValue = parseEther('0').toBigInt(); const quoteApi = new Quote(); const priceWithCommissionFormula = (value: string) => @@ -18,11 +18,11 @@ describe('calculatePremiumWithCommissionAndSlippage', () => { ); beforeEach(() => { - priceValue = parseEther('0'); + priceValue = parseEther('0').toBigInt(); }); it('should return 0 BigInt value for 0 price value', () => { - const expectedValue = parseEther('0'); + const expectedValue = parseEther('0').toBigInt(); expect(quoteApi.calculatePremiumWithCommissionAndSlippage(priceValue, BUY_COVER_COMMISSION_RATIO)).toStrictEqual( expectedValue, @@ -31,7 +31,7 @@ describe('calculatePremiumWithCommissionAndSlippage', () => { it('should return correct BigInt values for 1 value price - integer value', () => { const value = '1'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const expectedValue = priceWithCommissionAndSlippageFormula(value); @@ -43,67 +43,67 @@ describe('calculatePremiumWithCommissionAndSlippage', () => { it('should return correct BigInt value for 0.1 value price - decimal value < 1', () => { const value = '0.1'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const expectedValue = priceWithCommissionAndSlippageFormula(value); const actualValue = quoteApi.calculatePremiumWithCommissionAndSlippage(priceValue, BUY_COVER_COMMISSION_RATIO); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); it('should return correct BigInt value for 1.23456789 value price - decimal value > 1', () => { const value = '1.23456789'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const expectedValue = priceWithCommissionAndSlippageFormula(value); const actualValue = quoteApi.calculatePremiumWithCommissionAndSlippage(priceValue, BUY_COVER_COMMISSION_RATIO); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); it('should return correct BigInt values for 1000 value price - integer value', () => { const value = '1000'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const expectedValue = priceWithCommissionAndSlippageFormula(value); const actualValue = quoteApi.calculatePremiumWithCommissionAndSlippage(priceValue, BUY_COVER_COMMISSION_RATIO); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); it('should return correct BigInt value for 0.1 value price as BigInt-like object - decimal value < 1', () => { const value = '0.1'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const expectedValue = priceWithCommissionAndSlippageFormula(value); const actualValue = quoteApi.calculatePremiumWithCommissionAndSlippage(priceValue, BUY_COVER_COMMISSION_RATIO); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); it('should return correct BigInt value for 1.23456789 value price as BigInt-like object - decimal value > 1', () => { const value = '1.23456789'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const expectedValue = priceWithCommissionAndSlippageFormula(value); const actualValue = quoteApi.calculatePremiumWithCommissionAndSlippage(priceValue, BUY_COVER_COMMISSION_RATIO); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); it('should return correct BigInt values for 1 value price and 1% slippage', () => { const value = '1'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const slippageValue = 100; const expectedValue = priceWithCommissionAndSlippageFormula(value, slippageValue); @@ -113,14 +113,14 @@ describe('calculatePremiumWithCommissionAndSlippage', () => { BUY_COVER_COMMISSION_RATIO, slippageValue, ); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); it('should return correct BigInt values for 1 value price and 0.5% slippage', () => { const value = '1'; - priceValue = parseEther(value); + priceValue = parseEther(value).toBigInt(); const slippageValue = 50; const expectedValue = priceWithCommissionAndSlippageFormula(value, slippageValue); @@ -130,7 +130,7 @@ describe('calculatePremiumWithCommissionAndSlippage', () => { BUY_COVER_COMMISSION_RATIO, slippageValue, ); - const actualValueFormatted = formatEther(actualValue); + const actualValueFormatted = formatEther(BigInt(actualValue)); expect(parseFloat(actualValueFormatted)).toBeCloseTo(expectedValue, 4); }); diff --git a/src/quote/getQuoteAndBuyCoverInputs.test.ts b/src/quote/getQuoteAndBuyCoverInputs.test.ts index 99cc3974..3fe7bbc4 100644 --- a/src/quote/getQuoteAndBuyCoverInputs.test.ts +++ b/src/quote/getQuoteAndBuyCoverInputs.test.ts @@ -1,5 +1,5 @@ +import { parseEther } from 'ethers/lib/utils'; import mockAxios from 'jest-mock-axios'; -import { parseEther } from 'viem'; import { CoverAsset, @@ -9,7 +9,6 @@ import { NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, SLIPPAGE_DENOMINATOR, TARGET_PRICE_DENOMINATOR, - NEXUS_MUTUAL_COVER_COMMISSION_RATIO, } from '../constants/cover'; import { Quote } from '../quote'; import { @@ -20,9 +19,37 @@ import { CoverValidators, DefiPassContent, } from '../types'; +import { Product, ProductType } from '../types/product'; jest.mock('axios', () => mockAxios); jest.setTimeout(10_000); +const mockProduct: Product = { + id: 1, + productType: 1, + name: 'Test Product', + minPrice: '100', + coverAssets: [CoverAsset.ETH], + initialPriceRation: '100', + capacityReductionRatio: '0', + isDeprecated: false, + useFixedPrice: false, + metadata: '', + allowedPools: [], + logo: '', +}; + +const mockProductType: ProductType = { + id: 1, + name: 'Protocol', + metadata: '', + claimMethod: 0, + gracePeriod: '30', + assessmentCooldownPeriod: '0', + payoutRedemptionPeriod: '0', + commissionRatio: '500', + commissionDestination: NEXUS_MUTUAL_DAO_TREASURY_ADDRESS, +}; + const coverRouterCapacityResponse: CoverRouterProductCapacityResponse = { productId: 150, availableCapacity: [ @@ -79,12 +106,16 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('uses DEFAULT_NEXUS_API_URL if no API URL is supplied', async () => { - mockAxios.get.mockResolvedValue({ data: {} }); const productId = 1; const amount = '100'; const period = 30; const coverAsset = CoverAsset.ETH; + mockAxios.get.mockResolvedValueOnce({ data: mockProduct }); + mockAxios.get.mockResolvedValueOnce({ data: mockProductType }); + mockAxios.get.mockResolvedValueOnce({ data: coverRouterQuoteResponse }); + mockAxios.get.mockResolvedValueOnce({ data: coverRouterCapacityResponse }); + await quoteApi.getQuoteAndBuyCoverInputs({ productId, amount, @@ -93,14 +124,11 @@ describe('getQuoteAndBuyCoverInputs', () => { buyerAddress, }); - const defaultGetQuoteUrl = DEFAULT_NEXUS_API_URL + '/quote'; - expect(mockAxios.get).toHaveBeenCalledWith(defaultGetQuoteUrl, { - params: { amount, coverAsset, period, productId, paymentAsset: coverAsset }, - }); + const defaultGetProductUrl = DEFAULT_NEXUS_API_URL + '/products/1'; + expect(mockAxios.get).toHaveBeenCalledWith(defaultGetProductUrl, {}); }); it('allows the consumer to override nexusApiUrl param', async () => { - mockAxios.get.mockResolvedValue({ data: {} }); const url = 'http://hahahahahah'; const quoteApi = new Quote({ apiUrl: url }); const productId = 1; @@ -108,6 +136,11 @@ describe('getQuoteAndBuyCoverInputs', () => { const period = 30; const coverAsset = CoverAsset.ETH; + mockAxios.get.mockResolvedValueOnce({ data: mockProduct }); + mockAxios.get.mockResolvedValueOnce({ data: mockProductType }); + mockAxios.get.mockResolvedValueOnce({ data: coverRouterQuoteResponse }); + mockAxios.get.mockResolvedValueOnce({ data: coverRouterCapacityResponse }); + await quoteApi.getQuoteAndBuyCoverInputs({ productId, amount, @@ -118,10 +151,8 @@ describe('getQuoteAndBuyCoverInputs', () => { ipfsCidOrContent: '', }); - const overrideGetQuoteUrl = url + '/quote'; - expect(mockAxios.get).toHaveBeenCalledWith(overrideGetQuoteUrl, { - params: { amount, coverAsset, period, productId, paymentAsset: coverAsset }, - }); + const overrideGetProductUrl = url + '/products/1'; + expect(mockAxios.get).toHaveBeenCalledWith(overrideGetProductUrl, {}); }); const invalidProductIds = [-1, 'a', true, {}, [], null, undefined]; @@ -217,8 +248,10 @@ describe('getQuoteAndBuyCoverInputs', () => { ); it('returns an error if ipfsData is not a valid IPFS content for the product type - ETH Slashing', async () => { - mockAxios.get.mockResolvedValueOnce({ data: coverRouterQuoteResponse }); - mockAxios.get.mockResolvedValueOnce({ data: coverRouterCapacityResponse }); + const ethSlashingProduct = { ...mockProduct, id: 82, productType: 5 }; + const ethSlashingProductType = { ...mockProductType, id: 5, ipfsContentType: 'coverValidators' }; + mockAxios.get.mockResolvedValueOnce({ data: ethSlashingProduct }); + mockAxios.get.mockResolvedValueOnce({ data: ethSlashingProductType }); const invalidContent: CoverFreeText = { version: '1.0', freeText: 'test' }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ @@ -238,6 +271,11 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an error if ipfsData content has an empty validators field for ETH Slashing product type', async () => { + const ethSlashingProduct = { ...mockProduct, id: 82, productType: 5 }; + const ethSlashingProductType = { ...mockProductType, id: 5, ipfsContentType: 'coverValidators' }; + mockAxios.get.mockResolvedValueOnce({ data: ethSlashingProduct }); + mockAxios.get.mockResolvedValueOnce({ data: ethSlashingProductType }); + const emptyValidators: CoverValidators = { version: '1.0', validators: [] }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ ...quoteParams, @@ -258,6 +296,11 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an error if ipfsData is not a valid IPFS content for the product type - UnoRe Quota Share', async () => { + const quotaShareProduct = { ...mockProduct, id: 107, productType: 6 }; + const quotaShareProductType = { ...mockProductType, id: 6, ipfsContentType: 'coverQuotaShare' }; + mockAxios.get.mockResolvedValueOnce({ data: quotaShareProduct }); + mockAxios.get.mockResolvedValueOnce({ data: quotaShareProductType }); + const invalidContent: CoverFreeText = { version: '1.0', freeText: 'test' }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ ...quoteParams, @@ -276,6 +319,11 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an error if ipfsData is not a valid IPFS content for the product type - Fund Portfolio', async () => { + const fundPortfolioProduct = { ...mockProduct, id: 195, productType: 7 }; + const fundPortfolioProductType = { ...mockProductType, id: 7, ipfsContentType: 'coverAumCoverAmountPercentage' }; + mockAxios.get.mockResolvedValueOnce({ data: fundPortfolioProduct }); + mockAxios.get.mockResolvedValueOnce({ data: fundPortfolioProductType }); + const invalidContent: CoverFreeText = { version: '1.0', freeText: 'test' }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ ...quoteParams, @@ -294,6 +342,11 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an error if ipfsData is not a valid IPFS content for the product type - Nexus Mutual Cover', async () => { + const nexusMutualCoverProduct = { ...mockProduct, id: 247, productType: 8 }; + const nexusMutualCoverProductType = { ...mockProductType, id: 8, ipfsContentType: 'coverWalletAddresses' }; + mockAxios.get.mockResolvedValueOnce({ data: nexusMutualCoverProduct }); + mockAxios.get.mockResolvedValueOnce({ data: nexusMutualCoverProductType }); + const invalidContent: CoverFreeText = { version: '1.0', freeText: 'test' }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ ...quoteParams, @@ -312,6 +365,11 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an error if ipfsData is not a valid IPFS content for Defi Pass', async () => { + const defiPassProduct = { ...mockProduct, id: 227, productType: 9 }; + const defiPassProductType = { ...mockProductType, id: 9, ipfsContentType: 'defiPassContent' }; + mockAxios.get.mockResolvedValueOnce({ data: defiPassProduct }); + mockAxios.get.mockResolvedValueOnce({ data: defiPassProductType }); + const emptyWalletsContent: DefiPassContent = { version: '1.0', wallets: [] }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ ...quoteParams, @@ -332,6 +390,11 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an error if ipfsData is not a valid IPFS content for the Defi Pass - empty address', async () => { + const defiPassProduct = { ...mockProduct, id: 227, productType: 9 }; + const defiPassProductType = { ...mockProductType, id: 9, ipfsContentType: 'defiPassContent' }; + mockAxios.get.mockResolvedValueOnce({ data: defiPassProduct }); + mockAxios.get.mockResolvedValueOnce({ data: defiPassProductType }); + const emptyWalletString: DefiPassContent = { version: '1.0', walletAddress: '' }; const { error } = await quoteApi.getQuoteAndBuyCoverInputs({ ...quoteParams, @@ -351,6 +414,9 @@ describe('getQuoteAndBuyCoverInputs', () => { it('allows the consumer to provide a valid IPFS CID', async () => { const ipfsCid = 'QmYfSDbuQLqJ2MAG3ATRjUPVFQubAhAM5oiYuuu9Kfs8RY'; + const nexusMutualCoverProduct = { ...mockProduct, id: 247, productType: 8 }; + const nexusMutualCoverProductType = { ...mockProductType, id: 8, ipfsContentType: 'coverWalletAddresses' }; + const coverRouterQuoteResponse: CoverRouterQuoteResponse = { quote: { totalCoverAmountInAsset: parseEther('1000').toString(), @@ -367,6 +433,8 @@ describe('getQuoteAndBuyCoverInputs', () => { }, capacities: [{ poolId: '147', capacity: [{ assetId: '1', amount: parseEther('1000').toString() }] }], }; + mockAxios.get.mockResolvedValueOnce({ data: nexusMutualCoverProduct }); + mockAxios.get.mockResolvedValueOnce({ data: nexusMutualCoverProductType }); mockAxios.get.mockResolvedValueOnce({ data: coverRouterQuoteResponse }); mockAxios.get.mockResolvedValueOnce({ data: coverRouterCapacityResponse }); @@ -381,6 +449,12 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('allows the consumer to provide a valid IPFS content', async () => { + const nexusMutualCoverProduct = { ...mockProduct, id: 247, productType: 8 }; + const nexusMutualCoverProductType = { ...mockProductType, id: 8, ipfsContentType: 'coverWalletAddresses' }; + + mockAxios.get.mockResolvedValueOnce({ data: nexusMutualCoverProduct }); + mockAxios.get.mockResolvedValueOnce({ data: nexusMutualCoverProductType }); + mockAxios.post.mockResolvedValueOnce({ data: { ipfsHash: 'QmYfSDbuQLqJ2MAG3ATRjUPVFQubAhAM5oiYuuu9Kfs8RY', @@ -421,6 +495,9 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('returns an object with displayInfo and buyCoverInput parameters', async () => { + mockAxios.get.mockResolvedValueOnce({ data: mockProduct }); + mockAxios.get.mockResolvedValueOnce({ data: mockProductType }); + const coverRouterQuoteResponse: CoverRouterQuoteResponse = { quote: { totalCoverAmountInAsset: parseEther('1000').toString(), @@ -450,11 +527,11 @@ describe('getQuoteAndBuyCoverInputs', () => { const { premiumInAsset, annualPrice } = coverRouterQuoteResponse.quote; const expectedMaxPremiumInAsset = quoteApi.calculatePremiumWithCommissionAndSlippage( BigInt(premiumInAsset), - NEXUS_MUTUAL_COVER_COMMISSION_RATIO, + +mockProductType.commissionRatio, ); const expectedYearlyCostPerc = quoteApi.calculatePremiumWithCommissionAndSlippage( BigInt(annualPrice), - NEXUS_MUTUAL_COVER_COMMISSION_RATIO, + +mockProductType.commissionRatio, ); expect(error).toBeUndefined(); @@ -470,15 +547,17 @@ describe('getQuoteAndBuyCoverInputs', () => { expect(result?.buyCoverInput.buyCoverParams.period).toBe(30 * 60 * 60 * 24); expect(result?.buyCoverInput.buyCoverParams.maxPremiumInAsset).toBe(expectedMaxPremiumInAsset.toString()); expect(result?.buyCoverInput.buyCoverParams.paymentAsset).toBe(CoverAsset.ETH); - expect(result?.buyCoverInput.buyCoverParams.commissionRatio).toBe(NEXUS_MUTUAL_COVER_COMMISSION_RATIO); - expect(result?.buyCoverInput.buyCoverParams.commissionDestination).toBe(NEXUS_MUTUAL_DAO_TREASURY_ADDRESS); + expect(result?.buyCoverInput.buyCoverParams.commissionRatio).toBe(+mockProductType.commissionRatio); + expect(result?.buyCoverInput.buyCoverParams.commissionDestination).toBe(mockProductType.commissionDestination); expect(result?.buyCoverInput.buyCoverParams.ipfsData).toBe('QmYfSDbuQLqJ2MAG3ATRjUPVFQubAhAM5oiYuuu9Kfs8RY'); - expect(mockAxios.get).toHaveBeenCalledTimes(2); + expect(mockAxios.get).toHaveBeenCalledTimes(4); }); it('should handle "Not enough capacity for the cover amount" error correctly - ETH', async () => { mockAxios.get.mockReset(); + mockAxios.get.mockResolvedValueOnce({ data: mockProduct }); + mockAxios.get.mockResolvedValueOnce({ data: mockProductType }); mockAxios.get.mockRejectedValueOnce({ isAxiosError: true, response: { @@ -496,6 +575,8 @@ describe('getQuoteAndBuyCoverInputs', () => { }); it('should handle "Not enough capacity for the cover amount" error correctly - DAI', async () => { + mockAxios.get.mockResolvedValueOnce({ data: mockProduct }); + mockAxios.get.mockResolvedValueOnce({ data: mockProductType }); mockAxios.get.mockRejectedValueOnce({ isAxiosError: true, response: { diff --git a/src/swap/calculateEthForExactNxm.test.ts b/src/swap/calculateEthForExactNxm.test.ts index 9e123386..5f5537c1 100644 --- a/src/swap/calculateEthForExactNxm.test.ts +++ b/src/swap/calculateEthForExactNxm.test.ts @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -21,7 +21,7 @@ describe('calculateEthForExactNxm', () => { ]; test.each(cases)('calculates nxm out for eth in correctly - %s', (_type, nxmOut, expectedEthIn) => { - const nxmOutParsed = parseEther(nxmOut.toString()); + const nxmOutParsed = parseEther(nxmOut.toString()).toBigInt(); const ethInCalculated = swapApi.calculateEthForExactNxm(nxmOutParsed, reserves); expect(ethInCalculated.toString()).toBe(expectedEthIn.toString()); }); @@ -29,13 +29,13 @@ describe('calculateEthForExactNxm', () => { // throws error for invalid nxmOut values // eslint-disable-next-line @typescript-eslint/no-explicit-any const invalidCases: Array<[string, any, string]> = [ - ['large value = nxmA reserve', parseEther('142858.457219554100789497'), 'Not enough NXM in the pool'], - ['large value over nxmA reserve', parseEther('150000'), 'Not enough NXM in the pool'], - ['larger value over nxmA reserve', parseEther('1000000'), 'Not enough NXM in the pool'], - ['zero value', parseEther('0'), 'NXM out value must be greater than 0'], - ['unit negative value', parseEther('-1'), 'NXM out value must be greater than 0'], - ['large negative value', parseEther('-1000000'), 'NXM out value must be greater than 0'], - ['small negative value', parseEther('-0.000000000000000001'), 'NXM out value must be greater than 0'], + ['large value = nxmA reserve', parseEther('142858.457219554100789497').toBigInt(), 'Not enough NXM in the pool'], + ['large value over nxmA reserve', parseEther('150000').toBigInt(), 'Not enough NXM in the pool'], + ['larger value over nxmA reserve', parseEther('1000000').toBigInt(), 'Not enough NXM in the pool'], + ['zero value', parseEther('0').toBigInt(), 'NXM out value must be greater than 0'], + ['unit negative value', parseEther('-1').toBigInt(), 'NXM out value must be greater than 0'], + ['large negative value', parseEther('-1000000').toBigInt(), 'NXM out value must be greater than 0'], + ['small negative value', parseEther('-0.000000000000000001').toBigInt(), 'NXM out value must be greater than 0'], ['null value', null, 'NXM out value must be greater than 0'], ['undefined value', undefined, 'Cannot mix BigInt and other types, use explicit conversions'], ['string value', '1', 'Cannot mix BigInt and other types, use explicit conversions'], diff --git a/src/swap/calculateExactEthForNxm.test.ts b/src/swap/calculateExactEthForNxm.test.ts index 0f301861..2827e080 100644 --- a/src/swap/calculateExactEthForNxm.test.ts +++ b/src/swap/calculateExactEthForNxm.test.ts @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -22,7 +22,7 @@ describe('calculateExactEthForNxm', () => { ]; test.each(cases)('calculates eth out for nxm in correctly - %s', (_type, nxmIn, expectedEthOut) => { - const nxmInParsed = parseEther(nxmIn.toString()); + const nxmInParsed = parseEther(nxmIn.toString()).toBigInt(); const ethOutCalculated = swapApi.calculateExactEthForNxm(nxmInParsed, reserves); expect(ethOutCalculated.toString()).toBe(expectedEthOut.toString()); }); @@ -30,10 +30,10 @@ describe('calculateExactEthForNxm', () => { // throws error for invalid nxmIn values // eslint-disable-next-line @typescript-eslint/no-explicit-any const invalidCases: Array<[string, any, string]> = [ - ['zero value', parseEther('0'), 'NXM in value must be greater than 0'], - ['unit negative value', parseEther('-1'), 'NXM in value must be greater than 0'], - ['large negative value', parseEther('-1000000'), 'NXM in value must be greater than 0'], - ['small negative value', parseEther('-0.000000000000000001'), 'NXM in value must be greater than 0'], + ['zero value', parseEther('0').toBigInt(), 'NXM in value must be greater than 0'], + ['unit negative value', parseEther('-1').toBigInt(), 'NXM in value must be greater than 0'], + ['large negative value', parseEther('-1000000').toBigInt(), 'NXM in value must be greater than 0'], + ['small negative value', parseEther('-0.000000000000000001').toBigInt(), 'NXM in value must be greater than 0'], ['null value', null, 'NXM in value must be greater than 0'], ['undefined value', undefined, 'Cannot mix BigInt and other types, use explicit conversions'], ['string value', '1', 'Cannot mix BigInt and other types, use explicit conversions'], diff --git a/src/swap/calculateExactNxmForEth.test.ts b/src/swap/calculateExactNxmForEth.test.ts index f8f6bd26..73dd6246 100644 --- a/src/swap/calculateExactNxmForEth.test.ts +++ b/src/swap/calculateExactNxmForEth.test.ts @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -22,7 +22,7 @@ describe('calculateExactNxmForEth', () => { ]; test.each(cases)('calculates nxm out for eth in correctly - %s', (_type, ethIn, expectedNxmOut) => { - const ethInParsed = parseEther(ethIn.toString()); + const ethInParsed = parseEther(ethIn.toString()).toBigInt(); const nxmOutCalculated = swapApi.calculateExactNxmForEth(ethInParsed, reserves); expect(nxmOutCalculated.toString()).toBe(expectedNxmOut.toString()); }); @@ -30,10 +30,10 @@ describe('calculateExactNxmForEth', () => { // throws error for invalid nxmIn values // eslint-disable-next-line @typescript-eslint/no-explicit-any const invalidCases: Array<[string, any, string]> = [ - ['zero value', parseEther('0'), 'ETH in value must be greater than 0'], - ['unit negative value', parseEther('-1'), 'ETH in value must be greater than 0'], - ['large negative value', parseEther('-1000000'), 'ETH in value must be greater than 0'], - ['small negative value', parseEther('-0.000000000000000001'), 'ETH in value must be greater than 0'], + ['zero value', parseEther('0').toBigInt(), 'ETH in value must be greater than 0'], + ['unit negative value', parseEther('-1').toBigInt(), 'ETH in value must be greater than 0'], + ['large negative value', parseEther('-1000000').toBigInt(), 'ETH in value must be greater than 0'], + ['small negative value', parseEther('-0.000000000000000001').toBigInt(), 'ETH in value must be greater than 0'], ['null value', null, 'ETH in value must be greater than 0'], ['undefined value', undefined, 'Cannot mix BigInt and other types, use explicit conversions'], ['string value', '1', 'Cannot mix BigInt and other types, use explicit conversions'], diff --git a/src/swap/calculateNxmForExactEth.test.ts b/src/swap/calculateNxmForExactEth.test.ts index 3c9e2acf..39e00ae0 100644 --- a/src/swap/calculateNxmForExactEth.test.ts +++ b/src/swap/calculateNxmForExactEth.test.ts @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -21,7 +21,7 @@ describe('calculateNxmForExactEth', () => { ]; test.each(cases)('calculates nxm out for eth in correctly - %s', (_type, ethOut, expectedNxmIn) => { - const ethOutParsed = parseEther(ethOut.toString()); + const ethOutParsed = parseEther(ethOut.toString()).toBigInt(); const nxmInCalculated = swapApi.calculateNxmForExactEth(ethOutParsed, reserves); expect(nxmInCalculated.toString()).toBe(expectedNxmIn.toString()); }); @@ -29,13 +29,13 @@ describe('calculateNxmForExactEth', () => { // throws error for invalid ethOut values // eslint-disable-next-line @typescript-eslint/no-explicit-any const invalidCases: Array<[string, any, string]> = [ - ['large = ethReserve', parseEther('5000'), 'Not enough ETH in the pool'], + ['large = ethReserve', parseEther('5000').toBigInt(), 'Not enough ETH in the pool'], ['large value over ethReserve', parseEther('15000'), 'Not enough ETH in the pool'], - ['larger value over ethReserve', parseEther('100000'), 'Not enough ETH in the pool'], - ['zero value', parseEther('0'), 'ETH out value must be greater than 0'], - ['unit negative value', parseEther('-1'), 'ETH out value must be greater than 0'], - ['large negative value', parseEther('-1000000'), 'ETH out value must be greater than 0'], - ['small negative value', parseEther('-0.000000000000000001'), 'ETH out value must be greater than 0'], + ['larger value over ethReserve', parseEther('100000').toBigInt(), 'Not enough ETH in the pool'], + ['zero value', parseEther('0').toBigInt(), 'ETH out value must be greater than 0'], + ['unit negative value', parseEther('-1').toBigInt(), 'ETH out value must be greater than 0'], + ['large negative value', parseEther('-1000000').toBigInt(), 'ETH out value must be greater than 0'], + ['small negative value', parseEther('-0.000000000000000001').toBigInt(), 'ETH out value must be greater than 0'], ['null value', null, 'ETH out value must be greater than 0'], ['undefined value', undefined, 'Cannot mix BigInt and other types, use explicit conversions'], ['string value', '1', 'Cannot mix BigInt and other types, use explicit conversions'], diff --git a/src/swap/calculatePriceImpactA.test.ts b/src/swap/calculatePriceImpactA.test.ts index 7c34352c..c70b9c4e 100644 --- a/src/swap/calculatePriceImpactA.test.ts +++ b/src/swap/calculatePriceImpactA.test.ts @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -22,7 +22,7 @@ describe('calculatePriceImpactB', () => { ]; test.each(cases)('calculates price impact A - %s', (_type, ethIn, expectedPriceImpact) => { - const ethInParsed = parseEther(ethIn.toString()); + const ethInParsed = parseEther(ethIn.toString()).toBigInt(); const priceImpact = swapApi.calculatePriceImpactA(ethInParsed, reserves); expect(priceImpact.toString()).toBe(expectedPriceImpact.toString()); }); @@ -30,10 +30,10 @@ describe('calculatePriceImpactB', () => { // throws error for invalid nxmIn values // eslint-disable-next-line @typescript-eslint/no-explicit-any const invalidCases: Array<[string, any, string]> = [ - ['zero value', parseEther('0'), 'ETH in value must be greater than 0'], - ['unit negative value', parseEther('-1'), 'ETH in value must be greater than 0'], - ['large negative value', parseEther('-1000000'), 'ETH in value must be greater than 0'], - ['small negative value', parseEther('-0.000000000000000001'), 'ETH in value must be greater than 0'], + ['zero value', parseEther('0').toBigInt(), 'ETH in value must be greater than 0'], + ['unit negative value', parseEther('-1').toBigInt(), 'ETH in value must be greater than 0'], + ['large negative value', parseEther('-1000000').toBigInt(), 'ETH in value must be greater than 0'], + ['small negative value', parseEther('-0.000000000000000001').toBigInt(), 'ETH in value must be greater than 0'], ['null value', null, 'ETH in value must be greater than 0'], ['undefined value', undefined, 'Cannot mix BigInt and other types, use explicit conversions'], ['string value', '1', 'Cannot mix BigInt and other types, use explicit conversions'], diff --git a/src/swap/calculatePriceImpactB.test.ts b/src/swap/calculatePriceImpactB.test.ts index 6c7b4e4a..06e2db87 100644 --- a/src/swap/calculatePriceImpactB.test.ts +++ b/src/swap/calculatePriceImpactB.test.ts @@ -1,5 +1,5 @@ import { BigNumber } from 'ethers'; -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -22,7 +22,7 @@ describe('calculatePriceImpactB', () => { ]; test.each(cases)('calculates price impact B - %s', (_type, nxmIn, expectedPriceImpact) => { - const nxmInParsed = parseEther(nxmIn.toString()); + const nxmInParsed = parseEther(nxmIn.toString()).toBigInt(); const priceImpact = swapApi.calculatePriceImpactB(nxmInParsed, reserves); expect(priceImpact.toString()).toBe(expectedPriceImpact.toString()); }); @@ -30,12 +30,12 @@ describe('calculatePriceImpactB', () => { // throws error for invalid nxmIn values // eslint-disable-next-line @typescript-eslint/no-explicit-any const invalidCases: Array<[string, any, string]> = [ - ['small value = 1e-17', parseEther('0.00000000000000001'), 'Division by zero'], - ['small value = 1e-18', parseEther('0.000000000000000001'), 'Division by zero'], - ['zero value', parseEther('0'), 'NXM in value must be greater than 0'], - ['unit negative value', parseEther('-1'), 'NXM in value must be greater than 0'], - ['large negative value', parseEther('-1000000'), 'NXM in value must be greater than 0'], - ['small negative value', parseEther('-0.000000000000000001'), 'NXM in value must be greater than 0'], + ['small value = 1e-17', parseEther('0.00000000000000001').toBigInt(), 'Division by zero'], + ['small value = 1e-18', parseEther('0.000000000000000001').toBigInt(), 'Division by zero'], + ['zero value', parseEther('0').toBigInt(), 'NXM in value must be greater than 0'], + ['unit negative value', parseEther('-1').toBigInt(), 'NXM in value must be greater than 0'], + ['large negative value', parseEther('-1000000').toBigInt(), 'NXM in value must be greater than 0'], + ['small negative value', parseEther('-0.000000000000000001').toBigInt(), 'NXM in value must be greater than 0'], ['null value', null, 'NXM in value must be greater than 0'], ['undefined value', undefined, 'Cannot mix BigInt and other types, use explicit conversions'], ['string value', '1', 'Cannot mix BigInt and other types, use explicit conversions'], diff --git a/src/swap/calculateSpotPrice.test.ts b/src/swap/calculateSpotPrice.test.ts index 738ef247..6343e2d3 100644 --- a/src/swap/calculateSpotPrice.test.ts +++ b/src/swap/calculateSpotPrice.test.ts @@ -1,4 +1,4 @@ -import { parseEther } from 'viem'; +import { parseEther } from 'ethers/lib/utils'; import { Reserves } from './reserves.type'; import { Swap } from './Swap'; @@ -6,15 +6,15 @@ import { Swap } from './Swap'; describe('calculateSpotPrice', () => { const swapApi = new Swap(); const reserves: Reserves = { - nxmA: parseEther('2'), - nxmB: parseEther('5'), - ethReserve: parseEther('10'), + nxmA: parseEther('2').toBigInt(), + nxmB: parseEther('5').toBigInt(), + ethReserve: parseEther('10').toBigInt(), budget: BigInt('0'), }; it('calculates spot price', () => { const { spotPriceA, spotPriceB } = swapApi.calculateSpotPrice(reserves); - expect(spotPriceA).toBe(parseEther('5')); - expect(spotPriceB).toBe(parseEther('2')); + expect(spotPriceA).toBe(parseEther('5').toBigInt()); + expect(spotPriceB).toBe(parseEther('2').toBigInt()); }); }); diff --git a/src/types/ipfs.ts b/src/types/ipfs.ts index 30f89826..c69b7fe1 100644 --- a/src/types/ipfs.ts +++ b/src/types/ipfs.ts @@ -1,6 +1,5 @@ import { z } from 'zod'; -import { ProductTypes } from '../../generated/types'; import { coverValidatorsSchema, coverQuotaShareSchema, @@ -95,70 +94,6 @@ export type IPFSTypeContentTuple = | [type: ContentType.file, content: File] | [type: ContentType.productAnnex, content: ProductAnnex]; -export const IPFS_CONTENT_TYPE_BY_PRODUCT_TYPE: Record = { - [ProductTypes.ethSlashing]: ContentType.coverValidators, - [ProductTypes.liquidCollectiveEthStaking]: ContentType.coverValidators, - [ProductTypes.stakewiseEthStaking]: ContentType.coverValidators, - [ProductTypes.sherlockQuotaShare]: ContentType.coverQuotaShare, - [ProductTypes.unoReQuotaShare]: ContentType.coverQuotaShare, - [ProductTypes.deFiPass]: ContentType.defiPassContent, - [ProductTypes.nexusMutual]: ContentType.coverWalletAddresses, - [ProductTypes.followOn]: ContentType.coverFreeText, - [ProductTypes.fundPortfolio]: ContentType.coverAumCoverAmountPercentage, - [ProductTypes.generalizedFundPortfolio]: ContentType.coverAumCoverAmountPercentage, - // --------------------------------------------------------- - [ProductTypes.singleProtocol]: undefined, - [ProductTypes.custody]: undefined, - [ProductTypes.yieldToken]: undefined, - [ProductTypes.sherlockExcess]: undefined, - [ProductTypes.nativeProtocol]: undefined, - [ProductTypes.theRetailMutual]: undefined, - [ProductTypes.multiProtocol]: undefined, - [ProductTypes.ethSlashingUmbrella]: undefined, - [ProductTypes.openCoverTransaction]: undefined, - [ProductTypes.sherlockBugBounty]: undefined, - [ProductTypes.immunefiBugBounty]: undefined, - [ProductTypes.crypto]: undefined, - [ProductTypes.nativeSyndicate]: undefined, - [ProductTypes.spearbitCantina]: undefined, - [ProductTypes.leveragedLiquidation]: undefined, - [ProductTypes.nonEVMProtocol]: undefined, - [ProductTypes.kidnapAndRansom]: undefined, - [ProductTypes.vault]: undefined, -}; - -export interface IPFSContentForProductType { - [ProductTypes.ethSlashing]: CoverValidators; - [ProductTypes.liquidCollectiveEthStaking]: CoverValidators; - [ProductTypes.stakewiseEthStaking]: CoverValidators; - [ProductTypes.sherlockQuotaShare]: CoverQuotaShare; - [ProductTypes.unoReQuotaShare]: CoverQuotaShare; - [ProductTypes.deFiPass]: DefiPassContent; - [ProductTypes.nexusMutual]: CoverWalletAddresses; - [ProductTypes.followOn]: CoverFreeText; - [ProductTypes.fundPortfolio]: CoverAumCoverAmountPercentage; - [ProductTypes.generalizedFundPortfolio]: CoverAumCoverAmountPercentage; - // --------------------------------------------------------- - [ProductTypes.singleProtocol]: undefined; - [ProductTypes.custody]: undefined; - [ProductTypes.yieldToken]: undefined; - [ProductTypes.sherlockExcess]: undefined; - [ProductTypes.nativeProtocol]: undefined; - [ProductTypes.theRetailMutual]: undefined; - [ProductTypes.multiProtocol]: undefined; - [ProductTypes.ethSlashingUmbrella]: undefined; - [ProductTypes.openCoverTransaction]: undefined; - [ProductTypes.sherlockBugBounty]: undefined; - [ProductTypes.immunefiBugBounty]: undefined; - [ProductTypes.crypto]: undefined; - [ProductTypes.nativeSyndicate]: undefined; - [ProductTypes.spearbitCantina]: undefined; - [ProductTypes.leveragedLiquidation]: undefined; - [ProductTypes.nonEVMProtocol]: undefined; - [ProductTypes.kidnapAndRansom]: undefined; - [ProductTypes.vault]: undefined; -} - export type IPFSUploadServiceResponse = { ipfsHash: string; }; diff --git a/src/types/product.ts b/src/types/product.ts new file mode 100644 index 00000000..492e6b7e --- /dev/null +++ b/src/types/product.ts @@ -0,0 +1,31 @@ +import { Integer } from './data'; +import { ContentType } from './ipfs'; +import { CoverAsset } from '../constants'; + +export type ProductType = { + id: Integer; + name: string; + metadata: string; + claimMethod: Integer; + gracePeriod: string; + assessmentCooldownPeriod: string; + payoutRedemptionPeriod: string; + commissionRatio: string; + commissionDestination: string; + ipfsContentType?: ContentType; // Optional field to specify required IPFS content type for the product +}; + +export type Product = { + id: Integer; + productType: ProductType['id']; + name: string; + minPrice: string; + coverAssets: CoverAsset[]; + initialPriceRation: string; + capacityReductionRatio: string; + isDeprecated: boolean; + useFixedPrice: boolean; + metadata: string; + allowedPools: number[]; + logo: string; +};