From 7ec4899d630cfc0e3141b7231d6b669ce1b52d04 Mon Sep 17 00:00:00 2001 From: Jack Ellis Date: Tue, 30 Apr 2024 16:05:17 +0100 Subject: [PATCH] feat(@nftx/trade): add fulfill function this function takes any quote from fetchQuote and fulfills it, regardless of whether it's a sell/buy, a mint/redeem, or an ERC20 quote --- packages/api/src/prices/fetchEthPrice.ts | 1 + packages/api/src/prices/fetchPrice.ts | 1 + packages/api/src/prices/fetchQuote.ts | 5 ++- packages/api/src/prices/fetchSpotPrice.ts | 9 +++-- packages/api/src/prices/fetchSpread.ts | 1 + packages/trade/src/price/ammQuoteToPrice.ts | 36 ++++++++++--------- packages/trade/src/trade/fulfill.ts | 40 +++++++++++++++++++++ packages/trade/src/trade/index.ts | 1 + packages/types/src/price.ts | 2 +- 9 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 packages/trade/src/trade/fulfill.ts diff --git a/packages/api/src/prices/fetchEthPrice.ts b/packages/api/src/prices/fetchEthPrice.ts index 3a39042b..e685205b 100644 --- a/packages/api/src/prices/fetchEthPrice.ts +++ b/packages/api/src/prices/fetchEthPrice.ts @@ -1,5 +1,6 @@ import fetchSpotPrice from './fetchSpotPrice'; +/** Returns the current price of ETH in USDC terms */ const fetchEthPrice = async ({ network }: { network?: number }) => { const { price } = await fetchSpotPrice({ network, diff --git a/packages/api/src/prices/fetchPrice.ts b/packages/api/src/prices/fetchPrice.ts index 1f8c5af7..a34abd09 100644 --- a/packages/api/src/prices/fetchPrice.ts +++ b/packages/api/src/prices/fetchPrice.ts @@ -8,6 +8,7 @@ import fetchQuote, { SwapArgs, } from './fetchQuote'; +/** Returns an off-chain price for a transaction */ function fetchPrice(args: BuyArgs): Promise; function fetchPrice(args: SellArgs): Promise; function fetchPrice(args: SwapArgs): Promise; diff --git a/packages/api/src/prices/fetchQuote.ts b/packages/api/src/prices/fetchQuote.ts index 6d773dd8..0845a5ff 100644 --- a/packages/api/src/prices/fetchQuote.ts +++ b/packages/api/src/prices/fetchQuote.ts @@ -59,6 +59,7 @@ type QuoteArgs = { permit2?: Permit2Quote; }; +/** Returns an on-chain quote for a transaction. The response object can be passed into @nftx/trade's fulfill method to execute the quote */ function fetchQuote(args: BuyArgs & PriceArgs): Promise; function fetchQuote(args: BuyArgs & QuoteArgs): Promise; function fetchQuote(args: SellArgs & PriceArgs): Promise; @@ -122,10 +123,12 @@ function fetchQuote(args: any) { ? MarketplacePrice : MarketplaceQuote; + const method = quoteType === 'price' ? 'GET' : 'POST'; + return queryApi({ url, query, - method: quoteType === 'price' ? 'GET' : 'POST', + method, }); } diff --git a/packages/api/src/prices/fetchSpotPrice.ts b/packages/api/src/prices/fetchSpotPrice.ts index 6793ec12..dc850bcb 100644 --- a/packages/api/src/prices/fetchSpotPrice.ts +++ b/packages/api/src/prices/fetchSpotPrice.ts @@ -1,7 +1,9 @@ import type { QuoteToken } from '@nftx/types'; import fetchPrice from './fetchPrice'; import { WeiPerEther } from '@nftx/constants'; +import { UnknownError } from '@nftx/errors'; +/** Returns the spot price for buying a token */ const fetchSpotPrice = async ({ tokenAddress, amount = WeiPerEther, @@ -13,8 +15,10 @@ const fetchSpotPrice = async ({ quoteToken?: QuoteToken; amount?: bigint; }) => { + // We start by attempting to quote 1 whole token, but if it fails (e.g. due to insufficient liquidity), + // we can try to quote a smaller fraction of the token until we find a price that works. let fraction = WeiPerEther; - let error: any = new Error('Failed to fetch spot price'); + let error: any = new UnknownError('Failed to fetch spot price'); do { try { @@ -26,7 +30,8 @@ const fetchSpotPrice = async ({ buyAmount: fraction, }); - quote.price = (quote.price * amount) / fraction; + // Extrapolate the price to determine the spot price for 1 whole token, then scale it by the desired amount. + quote.price = quote.vTokenPrice = (quote.price * amount) / fraction; return quote; } catch (e) { diff --git a/packages/api/src/prices/fetchSpread.ts b/packages/api/src/prices/fetchSpread.ts index 3b955f5e..a380d806 100644 --- a/packages/api/src/prices/fetchSpread.ts +++ b/packages/api/src/prices/fetchSpread.ts @@ -2,6 +2,7 @@ import { WeiPerEther, Zero } from '@nftx/constants'; import { QuoteToken } from '@nftx/types'; import fetchPrice from './fetchPrice'; +/** Returns the difference between a buy and sell of a single token */ const fetchSpread = async ({ tokenAddress, network, diff --git a/packages/trade/src/price/ammQuoteToPrice.ts b/packages/trade/src/price/ammQuoteToPrice.ts index 6095adae..83b1c63c 100644 --- a/packages/trade/src/price/ammQuoteToPrice.ts +++ b/packages/trade/src/price/ammQuoteToPrice.ts @@ -63,22 +63,24 @@ const ammQuoteToPrice = (quote: NftxQuote) => { ); } - const methodParameters: MarketplaceQuote['methodParameters'] | undefined = - quote.methodParameters - ? { - amountsIn: [], - amountsOut: [], - premiumLimit: '', - standard: 'ERC20', - tokenIdsIn: [], - tokenIdsOut: [], - vaultAddress: '0x', - vaultId: '', - executeCalldata: quote.methodParameters.calldata, - to: quote.methodParameters.to, - value: quote.methodParameters.value, - } - : undefined; + let methodParameters = + undefined as any as MarketplaceQuote['methodParameters']; + + if (quote.methodParameters) { + methodParameters = { + amountsIn: [], + amountsOut: [], + premiumLimit: '', + standard: 'ERC20', + tokenIdsIn: [], + tokenIdsOut: [], + vaultAddress: '0x', + vaultId: '', + executeCalldata: quote.methodParameters.calldata, + to: quote.methodParameters.to, + value: quote.methodParameters.value, + }; + } const price: MarketplaceQuote = { approveContracts, @@ -91,7 +93,7 @@ const ammQuoteToPrice = (quote: NftxQuote) => { type: 'erc20', vTokenPrice: BigInt(quote.quote), routeString: quote.routeString, - methodParameters: methodParameters as MarketplaceQuote['methodParameters'], + methodParameters: methodParameters, }; return price; diff --git a/packages/trade/src/trade/fulfill.ts b/packages/trade/src/trade/fulfill.ts new file mode 100644 index 00000000..02048b71 --- /dev/null +++ b/packages/trade/src/trade/fulfill.ts @@ -0,0 +1,40 @@ +import { MarketplaceQuote, Provider, Signer } from '@nftx/types'; +import tradeErc20 from './tradeErc20'; +import swap from './swap'; +import redeem from './redeem'; +import mint from './mint'; +import sell from './sell'; +import buy from './buy'; +import { UnknownError } from '@nftx/errors'; + +/** Fulfills any quote returned by @nftx/api's fetchQuote method */ +const fulfill = ({ + network, + quote, + signer, + provider, +}: { + quote: Pick; + network: number; + signer: Signer; + provider: Provider; +}) => { + switch (quote.type) { + case 'buy': + return buy({ provider, quote, signer, network }); + case 'sell': + return sell({ provider, quote, signer, network }); + case 'mint': + return mint({ provider, quote, signer }); + case 'redeem': + return redeem({ quote, provider, signer }); + case 'swap': + return swap({ quote, provider, signer, network }); + case 'erc20': + return tradeErc20({ provider, quote, signer, network }); + default: + throw new UnknownError(`Unknown quote type: ${quote.type}`); + } +}; + +export default fulfill; diff --git a/packages/trade/src/trade/index.ts b/packages/trade/src/trade/index.ts index 633d5bbb..80ec6ef2 100644 --- a/packages/trade/src/trade/index.ts +++ b/packages/trade/src/trade/index.ts @@ -4,3 +4,4 @@ export { default as redeem } from './redeem'; export { default as sell } from './sell'; export { default as swap } from './swap'; export { default as tradeErc20 } from './tradeErc20'; +export { default as fulfill } from './fulfill'; diff --git a/packages/types/src/price.ts b/packages/types/src/price.ts index 04f0ba8a..964f3b50 100644 --- a/packages/types/src/price.ts +++ b/packages/types/src/price.ts @@ -31,7 +31,7 @@ export type ApproveContract = { standard?: 'ERC721' | 'ERC1155' | 'ERC20'; }; -/** A price object for buying/selling/swapping an NFT through the marketplace zap or trading an ERC20 through our AMM */ +/** A price object for buying/selling/swapping an NFT through the marketplace zap or trading an ERC20 through NFTX's AMM */ export type MarketplacePrice = { type: 'buy' | 'sell' | 'swap' | 'mint' | 'redeem' | 'erc20'; /** The total price in ETH */