diff --git a/packages/core/src/prices/__tests__/priceVaultBuy.test.ts b/packages/core/src/prices/__tests__/priceVaultBuy.test.ts index 0fee314..ef299f1 100644 --- a/packages/core/src/prices/__tests__/priceVaultBuy.test.ts +++ b/packages/core/src/prices/__tests__/priceVaultBuy.test.ts @@ -1,6 +1,7 @@ import { formatEther, parseEther } from 'viem'; import { makePriceVaultBuy } from '../priceVaultBuy'; import { WeiPerEther, Zero } from '@nftx/constants'; +import { InsufficientLiquidityError } from '@nftx/errors'; type PriceVaultBuy = ReturnType; let priceVaultBuy: PriceVaultBuy; @@ -11,6 +12,7 @@ let quoteVaultBuy: jest.Mock; let tokenIds: `${number}`[]; let vault: Parameters[0]['vault']; let holdings: Parameters[0]['holdings']; +let bypassLiquidityCheck: boolean; beforeEach(() => { fetchAmmQuote = jest.fn(({ buyAmount }: { buyAmount: bigint }) => ({ @@ -65,6 +67,7 @@ beforeEach(() => { quantity: 1n, }, ]; + bypassLiquidityCheck = false; priceVaultBuy = makePriceVaultBuy({ fetchAmmQuote, quoteVaultBuy }); run = () => @@ -74,6 +77,7 @@ beforeEach(() => { tokenIds, vault, provider: null as any, + bypassLiquidityCheck, }); }); @@ -129,6 +133,35 @@ describe('when there are more than 5 token ids', () => { expect(Number(formatEther(result.price))).toBeLessThan(20); }); }); + + describe('when there is no price', () => { + beforeEach(() => { + fetchAmmQuote.mockResolvedValue({ price: 0n }); + }); + + it('throw an insufficient liquidity error', async () => { + const promise = run(); + + await expect(promise).rejects.toThrow(InsufficientLiquidityError); + }); + + describe('when bypassLiquidityCheck is true', () => { + beforeEach(() => { + bypassLiquidityCheck = true; + }); + + it('returns a zero value price', async () => { + const promise = run(); + + await expect(promise).resolves.toBeTruthy(); + + const result = await promise; + + expect(`${result.price}`).toBe('150000000000000000'); + expect(`${result.vTokenPrice}`).toBe('0'); + }); + }); + }); }); describe('when there are 5 or less token ids', () => { @@ -168,6 +201,51 @@ describe('when there are 5 or less token ids', () => { expect(Number(formatEther(result.price))).toBeLessThan(7.1); }); }); + + describe('when there is no price stored on the vault', () => { + beforeEach(() => { + vault.prices = undefined as any; + }); + + it('returns a rough price estimate', async () => { + const result = await run(); + + expect(`${result.price}`).toBe('2050000000000000000'); + expect(fetchAmmQuote).toBeCalled(); + }); + }); + + describe('when there is no price', () => { + beforeEach(() => { + vault.prices[1].redeem.price = Zero; + vault.prices[1].redeem.vTokenPrice = Zero; + vault.prices[1].redeem.feePrice = Zero; + vault.prices[1].redeem.premiumPrice = Zero; + }); + + it('throws an insufficient liquidity error', async () => { + const promise = run(); + + await expect(promise).rejects.toThrow(InsufficientLiquidityError); + }); + + describe('when bypassLiquidityCheck is true', () => { + beforeEach(() => { + bypassLiquidityCheck = true; + }); + + it('returns a zero value price', async () => { + const promise = run(); + + await expect(promise).resolves.toBeTruthy(); + + const result = await promise; + + expect(`${result.price}`).toBe('0'); + expect(`${result.vTokenPrice}`).toBe('0'); + }); + }); + }); }); describe('when vault is an 1155', () => { diff --git a/packages/core/src/prices/priceVaultBuy.ts b/packages/core/src/prices/priceVaultBuy.ts index 12347a1..7e7b20f 100644 --- a/packages/core/src/prices/priceVaultBuy.ts +++ b/packages/core/src/prices/priceVaultBuy.ts @@ -23,7 +23,6 @@ const checkLiquidity =

(price: P) => { if (!price.vTokenPrice) { throw new InsufficientLiquidityError(); } - return price; }; const getIndexedPrice = ({ @@ -32,19 +31,21 @@ const getIndexedPrice = ({ vault, now, network, + bypassLiquidityCheck, }: { vault: Pick; tokenIds: TokenIds; holdings: Pick[]; now: number; network: number; + bypassLiquidityCheck: boolean | undefined; }) => { // We store the prices for buying up to 5 NFTs // so we can save ourselves from having to make additional calls/calculations // We just pull out the stored price, and add a premium if necessary const totalTokenIds = getTotalTokenIds(tokenIds); const { vTokenToEth } = vault; - const price = vault.prices?.[totalTokenIds - 1]?.redeem; + let price = vault.prices?.[totalTokenIds - 1]?.redeem; if (!price) { return; } @@ -56,11 +57,17 @@ const getIndexedPrice = ({ network, }); - return { + price = { ...price, premiumPrice, price: price.price + premiumPrice, }; + + if (!bypassLiquidityCheck) { + checkLiquidity(price); + } + + return price; }; const getRoughPrice = async ({ @@ -68,6 +75,7 @@ const getRoughPrice = async ({ network, tokenIds, vault, + bypassLiquidityCheck, fetchAmmQuote, now, }: { @@ -75,6 +83,7 @@ const getRoughPrice = async ({ holdings: Pick[]; vault: Pick; network: number; + bypassLiquidityCheck: boolean | undefined; fetchAmmQuote: FetchAmmQuote; now: number; }) => { @@ -94,6 +103,7 @@ const getRoughPrice = async ({ buyToken: vault.id, buyAmount, sellToken: 'ETH', + throwOnError: !bypassLiquidityCheck, }); const feePrice = calculateTotalFeePrice( vault.fees.redeemFee, @@ -124,6 +134,10 @@ const getRoughPrice = async ({ routeString, }; + if (!bypassLiquidityCheck) { + checkLiquidity(result); + } + return result; }; @@ -142,6 +156,7 @@ export const makePriceVaultBuy = bypassIndexedPrice, holdings: allHoldings, provider, + bypassLiquidityCheck, }: { network: number; tokenIds: TokenIds; @@ -152,6 +167,7 @@ export const makePriceVaultBuy = bypassIndexedPrice?: boolean; holdings: Pick[]; provider: Provider; + bypassLiquidityCheck?: boolean; }): Promise => { const now = Math.floor(Date.now() / 1000); const totalTokenIds = getTotalTokenIds(tokenIds); @@ -180,7 +196,8 @@ export const makePriceVaultBuy = userAddress: '0x', vault, network, - }).then(checkLiquidity); + bypassLiquidityCheck, + }); } } } @@ -192,9 +209,10 @@ export const makePriceVaultBuy = vault, now, network, + bypassLiquidityCheck, }); if (result) { - return checkLiquidity(result); + return result; } } @@ -205,7 +223,8 @@ export const makePriceVaultBuy = vault, fetchAmmQuote: fetchAmmQuote, now, - }).then(checkLiquidity); + bypassLiquidityCheck, + }); }; const priceVaultBuy = makePriceVaultBuy({ diff --git a/packages/core/src/prices/priceVaultSell.ts b/packages/core/src/prices/priceVaultSell.ts index 18c4aba..a131e43 100644 --- a/packages/core/src/prices/priceVaultSell.ts +++ b/packages/core/src/prices/priceVaultSell.ts @@ -19,19 +19,24 @@ const checkLiquidity = (price: MarketplacePrice) => { if (price.price < Zero) { throw new MintFeeExceedsValueError(); } - return price; }; const getIndexedPrice = ({ tokenIds, vault, + bypassLiquidityCheck, }: { tokenIds: TokenIds; vault: Pick; + bypassLiquidityCheck: boolean | undefined; }) => { const totalTokenIds = getTotalTokenIds(tokenIds); const price = vault.prices?.[totalTokenIds - 1]?.mint; // We don't need to worry about premium pricing on sells + // Check the price has enough liquidity + if (price && !bypassLiquidityCheck) { + checkLiquidity(price); + } return price; }; @@ -39,11 +44,13 @@ const getRoughPrice = async ({ network, tokenIds, vault, + bypassLiquidityCheck, fetchAmmQuote, }: { network: number; tokenIds: TokenIds; vault: Pick; + bypassLiquidityCheck: boolean | undefined; fetchAmmQuote: FetchAmmQuote; }) => { const totalTokenIds = getTotalTokenIds(tokenIds); @@ -59,6 +66,7 @@ const getRoughPrice = async ({ sellAmount, buyToken: 'ETH', network, + throwOnError: !bypassLiquidityCheck, }); const feePrice = calculateTotalFeePrice( vault.fees.mintFee, @@ -81,6 +89,11 @@ const getRoughPrice = async ({ routeString, }; + // Check the price has enough liquidity + if (!bypassLiquidityCheck) { + checkLiquidity(result); + } + return result; }; @@ -91,11 +104,13 @@ export const makePriceVaultSell = network, tokenIds, vault, + bypassLiquidityCheck, }: { network: number; tokenIds: TokenIds; vault: Pick; bypassIndexedPrice?: boolean; + bypassLiquidityCheck?: boolean; }) => { const totalTokenIds = getTotalTokenIds(tokenIds); @@ -104,9 +119,9 @@ export const makePriceVaultSell = }); if (bypassIndexedPrice !== true && totalTokenIds <= 5) { - const result = getIndexedPrice({ tokenIds, vault }); + const result = getIndexedPrice({ tokenIds, vault, bypassLiquidityCheck }); if (result) { - return checkLiquidity(result); + return result; } } @@ -115,7 +130,8 @@ export const makePriceVaultSell = tokenIds, vault, fetchAmmQuote, - }).then(checkLiquidity); + bypassLiquidityCheck, + }); }; export default makePriceVaultSell({ fetchAmmQuote }); diff --git a/packages/core/src/prices/priceVaultSwap.ts b/packages/core/src/prices/priceVaultSwap.ts index 9a3fa49..e8945ce 100644 --- a/packages/core/src/prices/priceVaultSwap.ts +++ b/packages/core/src/prices/priceVaultSwap.ts @@ -103,6 +103,7 @@ export const makePriceVaultSwap = holdings: allHoldings, vault, bypassIndexedPrice, + bypassLiquidityCheck, }: { network: number; provider: Provider; @@ -114,6 +115,7 @@ export const makePriceVaultSwap = buyTokenIds: TokenIds; holdings: Pick[]; bypassIndexedPrice?: boolean; + bypassLiquidityCheck?: boolean; }) => { const now = Math.floor(Date.now() / 1000); const totalIn = getTotalTokenIds(sellTokenIds); @@ -153,6 +155,7 @@ export const makePriceVaultSwap = sellTokenIds, userAddress: '0x', vault, + bypassLiquidityCheck, }); } } diff --git a/packages/core/src/prices/quoteVaultBuy.ts b/packages/core/src/prices/quoteVaultBuy.ts index bfe6eb4..b51b1df 100644 --- a/packages/core/src/prices/quoteVaultBuy.ts +++ b/packages/core/src/prices/quoteVaultBuy.ts @@ -105,6 +105,7 @@ export const makeQuoteVaultBuy = vault, holdings: allHoldings, slippagePercentage, + bypassLiquidityCheck, }: { vault: Pick; tokenIds: TokenIds; @@ -113,6 +114,7 @@ export const makeQuoteVaultBuy = provider: Provider; holdings: Pick[]; slippagePercentage?: number; + bypassLiquidityCheck?: boolean; }) => { const totalTokenIds = getTotalTokenIds(tokenIds); const buyAmount = parseEther(`${totalTokenIds}`); @@ -143,6 +145,7 @@ export const makeQuoteVaultBuy = sellToken: 'WETH', userAddress: getChainConstant(MARKETPLACE_ZAP, network), slippagePercentage, + throwOnError: !bypassLiquidityCheck, }); const items = await Promise.all( diff --git a/packages/core/src/prices/quoteVaultMint.ts b/packages/core/src/prices/quoteVaultMint.ts index 9be857e..b49ead5 100644 --- a/packages/core/src/prices/quoteVaultMint.ts +++ b/packages/core/src/prices/quoteVaultMint.ts @@ -23,6 +23,7 @@ const quoteVaultMint = async ({ userAddress, vault, slippagePercentage, + bypassLiquidityCheck, }: { network: number; tokenIds: TokenIds; @@ -30,6 +31,7 @@ const quoteVaultMint = async ({ vault: Pick; provider: Provider; slippagePercentage?: number; + bypassLiquidityCheck?: boolean; }) => { const { feePrice, items, premiumPrice, price, vTokenPrice } = await quoteVaultSell({ @@ -39,6 +41,7 @@ const quoteVaultMint = async ({ userAddress, vault, slippagePercentage, + bypassLiquidityCheck, }); const tokenIdsIn = getUniqueTokenIds(tokenIds); diff --git a/packages/core/src/prices/quoteVaultRedeem.ts b/packages/core/src/prices/quoteVaultRedeem.ts index 20c27f1..4826d02 100644 --- a/packages/core/src/prices/quoteVaultRedeem.ts +++ b/packages/core/src/prices/quoteVaultRedeem.ts @@ -28,6 +28,7 @@ const quoteVaultRedeem = async ({ userAddress, vault, slippagePercentage, + bypassLiquidityCheck, }: { network: number; provider: Provider; @@ -36,6 +37,7 @@ const quoteVaultRedeem = async ({ tokenIds: TokenIds; holdings: VaultHolding[]; slippagePercentage?: number; + bypassLiquidityCheck?: boolean; }) => { const standard = vault.is1155 ? 'ERC1155' : 'ERC721'; const totalTokenIds = getTotalTokenIds(tokenIds); @@ -62,6 +64,7 @@ const quoteVaultRedeem = async ({ vault, network, slippagePercentage, + bypassLiquidityCheck, }); const value = increaseByPercentage( diff --git a/packages/core/src/prices/quoteVaultSell.ts b/packages/core/src/prices/quoteVaultSell.ts index 2081ab9..2fb699d 100644 --- a/packages/core/src/prices/quoteVaultSell.ts +++ b/packages/core/src/prices/quoteVaultSell.ts @@ -39,6 +39,7 @@ export const makeQuoteVaultSell = userAddress, vault, slippagePercentage, + bypassLiquidityCheck, }: { vault: Pick; network: number; @@ -46,6 +47,7 @@ export const makeQuoteVaultSell = userAddress: Address; provider: Provider; slippagePercentage?: number; + bypassLiquidityCheck?: boolean; }) => { const totalTokenIds = getTotalTokenIds(tokenIds); const sellAmount = parseEther(`${totalTokenIds}`); @@ -74,6 +76,7 @@ export const makeQuoteVaultSell = network, userAddress: getChainConstant(MARKETPLACE_ZAP, network), slippagePercentage, + throwOnError: !bypassLiquidityCheck, }); const vTokenPricePerItem = (vTokenPrice * WeiPerEther) / sellAmount; diff --git a/packages/core/src/prices/quoteVaultSwap.ts b/packages/core/src/prices/quoteVaultSwap.ts index 4e8b7e6..6b34322 100644 --- a/packages/core/src/prices/quoteVaultSwap.ts +++ b/packages/core/src/prices/quoteVaultSwap.ts @@ -53,6 +53,7 @@ export const makeQuoteVaultSwap = buyTokenIds: TokenIds; holdings: Pick[]; slippagePercentage?: number; + bypassLiquidityCheck?: boolean; }) => { const tokenIdsIn = getUniqueTokenIds(sellTokenIds); const amountsIn = getTokenIdAmounts(sellTokenIds); diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 27d8ccd..05b6f9a 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -21,3 +21,19 @@ export const mapObj = >( ): any => { return reduceObj(obj, (acc, key, value) => [...acc, fn(key, value)]); }; + +/** + * wraps a promise and returns a tuple of [error, result] + * @param p Promise + * @returns [error, result] + */ +export const t = (p: Promise): Promise<[any, T]> => { + return p.then( + (result) => { + return [undefined, result] as [any, T]; + }, + (err) => { + return [err, undefined] as [any, T]; + } + ); +};