diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index 8886c5bcaa..c7dc690e32 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -47,7 +47,6 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { rfqt: { takerApiKeyWhitelist: [], makerAssetOfferings: {}, - skipBuyRequests: false, }, }; diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index fd93848447..5817743e40 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -48,7 +48,6 @@ export class SwapQuoter { private readonly _orderStateUtils: OrderStateUtils; private readonly _quoteRequestor: QuoteRequestor; private readonly _rfqtTakerApiKeyWhitelist: string[]; - private readonly _rfqtSkipBuyRequests: boolean; /** * Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. @@ -170,10 +169,6 @@ export class SwapQuoter { this.expiryBufferMs = expiryBufferMs; this.permittedOrderFeeTypes = permittedOrderFeeTypes; this._rfqtTakerApiKeyWhitelist = rfqt ? rfqt.takerApiKeyWhitelist || [] : []; - this._rfqtSkipBuyRequests = - rfqt && rfqt.skipBuyRequests !== undefined - ? rfqt.skipBuyRequests - : (r => r !== undefined && r.skipBuyRequests === true)(constants.DEFAULT_SWAP_QUOTER_OPTS.rfqt); this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._protocolFeeUtils = ProtocolFeeUtils.getInstance( @@ -561,20 +556,31 @@ export class SwapQuoter { } else { gasPrice = await this.getGasPriceEstimationOrThrowAsync(); } + + // If RFQT is enabled and `nativeExclusivelyRFQT` is set, then `ERC20BridgeSource.Native` should + // never be excluded. + if ( + opts.rfqt && + opts.rfqt.nativeExclusivelyRFQT === true && + opts.excludedSources.includes(ERC20BridgeSource.Native) + ) { + throw new Error('Native liquidity cannot be excluded if "rfqt.nativeExclusivelyRFQT" is set'); + } + // get batches of orders from different sources, awaiting sources in parallel const orderBatchPromises: Array> = []; orderBatchPromises.push( - // Don't fetch from the DB if Native has been excluded - opts.excludedSources.includes(ERC20BridgeSource.Native) + // Don't fetch Open Orderbook orders from the DB if Native has been excluded, or if `nativeExclusivelyRFQT` has been set. + opts.excludedSources.includes(ERC20BridgeSource.Native) || + (opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true) ? Promise.resolve([]) : this._getSignedOrdersAsync(makerAssetData, takerAssetData), ); if ( - opts.rfqt && - opts.rfqt.intentOnFilling && - opts.rfqt.apiKey && - this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) && - !(marketOperation === MarketOperation.Buy && this._rfqtSkipBuyRequests) + opts.rfqt && // This is an RFQT-enabled API request + opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote + !opts.excludedSources.includes(ERC20BridgeSource.Native) && // Native liquidity is not excluded + this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) // A valid API key was provided ) { if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { throw new Error('RFQ-T requests must specify a taker address'); @@ -636,8 +642,7 @@ export class SwapQuoter { opts !== undefined && opts.isIndicative !== undefined && opts.isIndicative && - this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) && - !(op === MarketOperation.Buy && this._rfqtSkipBuyRequests) + this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) ); } } diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 798e320f8c..1bf1d1e69c 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -199,12 +199,18 @@ export interface SwapQuoteOrdersBreakdown { [source: string]: BigNumber; } +/** + * nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity. + * If set to `true` and `ERC20BridgeSource.Native` is part of the `excludedSources` + * array in `SwapQuoteRequestOpts`, an Error will be raised. + */ export interface RfqtRequestOpts { takerAddress: string; apiKey: string; intentOnFilling: boolean; isIndicative?: boolean; makerEndpointMaxResponseTimeMs?: number; + nativeExclusivelyRFQT?: boolean; } /** @@ -249,7 +255,6 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { rfqt?: { takerApiKeyWhitelist: string[]; makerAssetOfferings: RfqtMakerAssetOfferings; - skipBuyRequests?: boolean; warningLogger?: LogFunction; infoLogger?: LogFunction; }; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 4b67ccaa73..597dcb0fc1 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -26,14 +26,23 @@ import { OrderDomain, } from './types'; -async function getRfqtIndicativeQuotesAsync( +/** + * Returns a indicative quotes or an empty array if RFQT is not enabled or requested + * @param makerAssetData the maker asset data + * @param takerAssetData the taker asset data + * @param marketOperation Buy or Sell + * @param assetFillAmount the amount to fill, in base units + * @param opts market request options + */ +export async function getRfqtIndicativeQuotesAsync( makerAssetData: string, takerAssetData: string, marketOperation: MarketOperation, assetFillAmount: BigNumber, opts: Partial, ): Promise { - if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) { + const hasExcludedNativeLiquidity = opts.excludedSources && opts.excludedSources.includes(ERC20BridgeSource.Native); + if (!hasExcludedNativeLiquidity && opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) { return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( makerAssetData, takerAssetData, diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 765f2ae24c..93815c0b43 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -13,14 +13,20 @@ import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils'; import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils'; import * as _ from 'lodash'; +import * as TypeMoq from 'typemoq'; -import { MarketOperation, SignedOrderWithFillableAmounts } from '../src'; -import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; +import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src'; +import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/'; import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants'; import { createFillPaths } from '../src/utils/market_operation_utils/fills'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types'; +const MAKER_TOKEN = randomAddress(); +const TAKER_TOKEN = randomAddress(); +const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); +const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); + // tslint:disable: custom-no-magic-numbers promise-function-async describe('MarketOperationUtils tests', () => { const CHAIN_ID = 1; @@ -30,11 +36,6 @@ describe('MarketOperationUtils tests', () => { const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge; const UNISWAP_V2_BRIDGE_ADDRESS = contractAddresses.uniswapV2Bridge; const CURVE_BRIDGE_ADDRESS = contractAddresses.curveBridge; - - const MAKER_TOKEN = randomAddress(); - const TAKER_TOKEN = randomAddress(); - const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); - const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); let originalSamplerOperations: any; before(() => { @@ -344,6 +345,68 @@ describe('MarketOperationUtils tests', () => { }, } as any) as DexOrderSampler; + describe('getRfqtIndicativeQuotesAsync', () => { + const partialRfqt: RfqtRequestOpts = { + apiKey: 'foo', + takerAddress: NULL_ADDRESS, + isIndicative: true, + intentOnFilling: false, + }; + + it('returns an empty array if native liquidity is excluded from the salad', async () => { + const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Strict); + const result = await getRfqtIndicativeQuotesAsync( + MAKER_ASSET_DATA, + TAKER_ASSET_DATA, + MarketOperation.Sell, + new BigNumber('100e18'), + { + rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, + excludedSources: [ERC20BridgeSource.Native], + }, + ); + expect(result.length).to.eql(0); + requestor.verify( + r => + r.requestRfqtIndicativeQuotesAsync( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + TypeMoq.Times.never(), + ); + }); + + it('calls RFQT if Native source is not excluded', async () => { + const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose); + requestor + .setup(r => + r.requestRfqtIndicativeQuotesAsync( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve([])) + .verifiable(TypeMoq.Times.once()); + await getRfqtIndicativeQuotesAsync( + MAKER_ASSET_DATA, + TAKER_ASSET_DATA, + MarketOperation.Sell, + new BigNumber('100e18'), + { + rfqt: { quoteRequestor: requestor.object, ...partialRfqt }, + excludedSources: [], + }, + ); + requestor.verifyAll(); + }); + }); + describe('MarketOperationUtils', () => { let marketOperationUtils: MarketOperationUtils;