Skip to content

Commit

Permalink
feat(@nftx/core): bypass liquidity check for vault buy/sell/swap
Browse files Browse the repository at this point in the history
This commit adds a bypassLiquidityCheck parameter to the price and quote methods.
This allows us to get a quote for a trade without checking if there is enough liquidity,
and means we can still calculate premiums and vault fees for a trade.
  • Loading branch information
jackmellis committed Jun 4, 2024
1 parent 5bab97a commit a739fe6
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 10 deletions.
78 changes: 78 additions & 0 deletions packages/core/src/prices/__tests__/priceVaultBuy.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof makePriceVaultBuy>;
let priceVaultBuy: PriceVaultBuy;
Expand All @@ -11,6 +12,7 @@ let quoteVaultBuy: jest.Mock;
let tokenIds: `${number}`[];
let vault: Parameters<PriceVaultBuy>[0]['vault'];
let holdings: Parameters<PriceVaultBuy>[0]['holdings'];
let bypassLiquidityCheck: boolean;

beforeEach(() => {
fetchAmmQuote = jest.fn(({ buyAmount }: { buyAmount: bigint }) => ({
Expand Down Expand Up @@ -65,6 +67,7 @@ beforeEach(() => {
quantity: 1n,
},
];
bypassLiquidityCheck = false;

priceVaultBuy = makePriceVaultBuy({ fetchAmmQuote, quoteVaultBuy });
run = () =>
Expand All @@ -74,6 +77,7 @@ beforeEach(() => {
tokenIds,
vault,
provider: null as any,
bypassLiquidityCheck,
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
31 changes: 25 additions & 6 deletions packages/core/src/prices/priceVaultBuy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const checkLiquidity = <P extends { vTokenPrice: bigint }>(price: P) => {
if (!price.vTokenPrice) {
throw new InsufficientLiquidityError();
}
return price;
};

const getIndexedPrice = ({
Expand All @@ -32,19 +31,21 @@ const getIndexedPrice = ({
vault,
now,
network,
bypassLiquidityCheck,
}: {
vault: Pick<Vault, 'vTokenToEth' | 'prices'>;
tokenIds: TokenIds;
holdings: Pick<VaultHolding, 'dateAdded' | 'tokenId'>[];
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;
}
Expand All @@ -56,25 +57,33 @@ const getIndexedPrice = ({
network,
});

return {
price = {
...price,
premiumPrice,
price: price.price + premiumPrice,
};

if (!bypassLiquidityCheck) {
checkLiquidity(price);
}

return price;
};

const getRoughPrice = async ({
holdings,
network,
tokenIds,
vault,
bypassLiquidityCheck,
fetchAmmQuote,
now,
}: {
tokenIds: TokenIds;
holdings: Pick<VaultHolding, 'tokenId' | 'dateAdded'>[];
vault: Pick<Vault, 'vTokenToEth' | 'fees' | 'id'>;
network: number;
bypassLiquidityCheck: boolean | undefined;
fetchAmmQuote: FetchAmmQuote;
now: number;
}) => {
Expand All @@ -94,6 +103,7 @@ const getRoughPrice = async ({
buyToken: vault.id,
buyAmount,
sellToken: 'ETH',
throwOnError: !bypassLiquidityCheck,
});
const feePrice = calculateTotalFeePrice(
vault.fees.redeemFee,
Expand Down Expand Up @@ -124,6 +134,10 @@ const getRoughPrice = async ({
routeString,
};

if (!bypassLiquidityCheck) {
checkLiquidity(result);
}

return result;
};

Expand All @@ -142,6 +156,7 @@ export const makePriceVaultBuy =
bypassIndexedPrice,
holdings: allHoldings,
provider,
bypassLiquidityCheck,
}: {
network: number;
tokenIds: TokenIds;
Expand All @@ -152,6 +167,7 @@ export const makePriceVaultBuy =
bypassIndexedPrice?: boolean;
holdings: Pick<VaultHolding, 'tokenId' | 'dateAdded' | 'quantity'>[];
provider: Provider;
bypassLiquidityCheck?: boolean;
}): Promise<MarketplacePrice> => {
const now = Math.floor(Date.now() / 1000);
const totalTokenIds = getTotalTokenIds(tokenIds);
Expand Down Expand Up @@ -180,7 +196,8 @@ export const makePriceVaultBuy =
userAddress: '0x',
vault,
network,
}).then(checkLiquidity);
bypassLiquidityCheck,
});
}
}
}
Expand All @@ -192,9 +209,10 @@ export const makePriceVaultBuy =
vault,
now,
network,
bypassLiquidityCheck,
});
if (result) {
return checkLiquidity(result);
return result;
}
}

Expand All @@ -205,7 +223,8 @@ export const makePriceVaultBuy =
vault,
fetchAmmQuote: fetchAmmQuote,
now,
}).then(checkLiquidity);
bypassLiquidityCheck,
});
};

const priceVaultBuy = makePriceVaultBuy({
Expand Down
24 changes: 20 additions & 4 deletions packages/core/src/prices/priceVaultSell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,38 @@ const checkLiquidity = (price: MarketplacePrice) => {
if (price.price < Zero) {
throw new MintFeeExceedsValueError();
}
return price;
};

const getIndexedPrice = ({
tokenIds,
vault,
bypassLiquidityCheck,
}: {
tokenIds: TokenIds;
vault: Pick<Vault, 'prices'>;
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;
};

const getRoughPrice = async ({
network,
tokenIds,
vault,
bypassLiquidityCheck,
fetchAmmQuote,
}: {
network: number;
tokenIds: TokenIds;
vault: Pick<Vault, 'vTokenToEth' | 'id' | 'fees'>;
bypassLiquidityCheck: boolean | undefined;
fetchAmmQuote: FetchAmmQuote;
}) => {
const totalTokenIds = getTotalTokenIds(tokenIds);
Expand All @@ -59,6 +66,7 @@ const getRoughPrice = async ({
sellAmount,
buyToken: 'ETH',
network,
throwOnError: !bypassLiquidityCheck,
});
const feePrice = calculateTotalFeePrice(
vault.fees.mintFee,
Expand All @@ -81,6 +89,11 @@ const getRoughPrice = async ({
routeString,
};

// Check the price has enough liquidity
if (!bypassLiquidityCheck) {
checkLiquidity(result);
}

return result;
};

Expand All @@ -91,11 +104,13 @@ export const makePriceVaultSell =
network,
tokenIds,
vault,
bypassLiquidityCheck,
}: {
network: number;
tokenIds: TokenIds;
vault: Pick<Vault, 'id' | 'prices' | 'vTokenToEth' | 'fees'>;
bypassIndexedPrice?: boolean;
bypassLiquidityCheck?: boolean;
}) => {
const totalTokenIds = getTotalTokenIds(tokenIds);

Expand All @@ -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;
}
}

Expand All @@ -115,7 +130,8 @@ export const makePriceVaultSell =
tokenIds,
vault,
fetchAmmQuote,
}).then(checkLiquidity);
bypassLiquidityCheck,
});
};

export default makePriceVaultSell({ fetchAmmQuote });
3 changes: 3 additions & 0 deletions packages/core/src/prices/priceVaultSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const makePriceVaultSwap =
holdings: allHoldings,
vault,
bypassIndexedPrice,
bypassLiquidityCheck,
}: {
network: number;
provider: Provider;
Expand All @@ -114,6 +115,7 @@ export const makePriceVaultSwap =
buyTokenIds: TokenIds;
holdings: Pick<VaultHolding, 'dateAdded' | 'tokenId' | 'quantity'>[];
bypassIndexedPrice?: boolean;
bypassLiquidityCheck?: boolean;
}) => {
const now = Math.floor(Date.now() / 1000);
const totalIn = getTotalTokenIds(sellTokenIds);
Expand Down Expand Up @@ -153,6 +155,7 @@ export const makePriceVaultSwap =
sellTokenIds,
userAddress: '0x',
vault,
bypassLiquidityCheck,
});
}
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/prices/quoteVaultBuy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const makeQuoteVaultBuy =
vault,
holdings: allHoldings,
slippagePercentage,
bypassLiquidityCheck,
}: {
vault: Pick<Vault, 'fees' | 'id' | 'vaultId' | 'is1155'>;
tokenIds: TokenIds;
Expand All @@ -113,6 +114,7 @@ export const makeQuoteVaultBuy =
provider: Provider;
holdings: Pick<VaultHolding, 'dateAdded' | 'tokenId'>[];
slippagePercentage?: number;
bypassLiquidityCheck?: boolean;
}) => {
const totalTokenIds = getTotalTokenIds(tokenIds);
const buyAmount = parseEther(`${totalTokenIds}`);
Expand Down Expand Up @@ -143,6 +145,7 @@ export const makeQuoteVaultBuy =
sellToken: 'WETH',
userAddress: getChainConstant(MARKETPLACE_ZAP, network),
slippagePercentage,
throwOnError: !bypassLiquidityCheck,
});

const items = await Promise.all(
Expand Down
Loading

0 comments on commit a739fe6

Please sign in to comment.