Merge pull request #2642 from 0xProject/only-rfqt

Add the `nativeExclusivelyRFQT` argument.
This commit is contained in:
Daniel Pyrathon 2020-07-23 15:05:02 -07:00 committed by GitHub
commit 9a16f5736e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 25 deletions

View File

@ -47,7 +47,6 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
rfqt: { rfqt: {
takerApiKeyWhitelist: [], takerApiKeyWhitelist: [],
makerAssetOfferings: {}, makerAssetOfferings: {},
skipBuyRequests: false,
}, },
}; };

View File

@ -48,7 +48,6 @@ export class SwapQuoter {
private readonly _orderStateUtils: OrderStateUtils; private readonly _orderStateUtils: OrderStateUtils;
private readonly _quoteRequestor: QuoteRequestor; private readonly _quoteRequestor: QuoteRequestor;
private readonly _rfqtTakerApiKeyWhitelist: string[]; private readonly _rfqtTakerApiKeyWhitelist: string[];
private readonly _rfqtSkipBuyRequests: boolean;
/** /**
* Instantiates a new SwapQuoter instance given existing liquidity in the form of orders and feeOrders. * 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.expiryBufferMs = expiryBufferMs;
this.permittedOrderFeeTypes = permittedOrderFeeTypes; this.permittedOrderFeeTypes = permittedOrderFeeTypes;
this._rfqtTakerApiKeyWhitelist = rfqt ? rfqt.takerApiKeyWhitelist || [] : []; 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._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId);
this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider);
this._protocolFeeUtils = ProtocolFeeUtils.getInstance( this._protocolFeeUtils = ProtocolFeeUtils.getInstance(
@ -561,20 +556,31 @@ export class SwapQuoter {
} else { } else {
gasPrice = await this.getGasPriceEstimationOrThrowAsync(); 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 // get batches of orders from different sources, awaiting sources in parallel
const orderBatchPromises: Array<Promise<SignedOrder[]>> = []; const orderBatchPromises: Array<Promise<SignedOrder[]>> = [];
orderBatchPromises.push( orderBatchPromises.push(
// Don't fetch from the DB if Native has been excluded // 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.excludedSources.includes(ERC20BridgeSource.Native) ||
(opts.rfqt && opts.rfqt.nativeExclusivelyRFQT === true)
? Promise.resolve([]) ? Promise.resolve([])
: this._getSignedOrdersAsync(makerAssetData, takerAssetData), : this._getSignedOrdersAsync(makerAssetData, takerAssetData),
); );
if ( if (
opts.rfqt && opts.rfqt && // This is an RFQT-enabled API request
opts.rfqt.intentOnFilling && opts.rfqt.intentOnFilling && // The requestor is asking for a firm quote
opts.rfqt.apiKey && !opts.excludedSources.includes(ERC20BridgeSource.Native) && // Native liquidity is not excluded
this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) && this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) // A valid API key was provided
!(marketOperation === MarketOperation.Buy && this._rfqtSkipBuyRequests)
) { ) {
if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) {
throw new Error('RFQ-T requests must specify a taker address'); throw new Error('RFQ-T requests must specify a taker address');
@ -636,8 +642,7 @@ export class SwapQuoter {
opts !== undefined && opts !== undefined &&
opts.isIndicative !== undefined && opts.isIndicative !== undefined &&
opts.isIndicative && opts.isIndicative &&
this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) && this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey)
!(op === MarketOperation.Buy && this._rfqtSkipBuyRequests)
); );
} }
} }

View File

@ -199,12 +199,18 @@ export interface SwapQuoteOrdersBreakdown {
[source: string]: BigNumber; [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 { export interface RfqtRequestOpts {
takerAddress: string; takerAddress: string;
apiKey: string; apiKey: string;
intentOnFilling: boolean; intentOnFilling: boolean;
isIndicative?: boolean; isIndicative?: boolean;
makerEndpointMaxResponseTimeMs?: number; makerEndpointMaxResponseTimeMs?: number;
nativeExclusivelyRFQT?: boolean;
} }
/** /**
@ -249,7 +255,6 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
rfqt?: { rfqt?: {
takerApiKeyWhitelist: string[]; takerApiKeyWhitelist: string[];
makerAssetOfferings: RfqtMakerAssetOfferings; makerAssetOfferings: RfqtMakerAssetOfferings;
skipBuyRequests?: boolean;
warningLogger?: LogFunction; warningLogger?: LogFunction;
infoLogger?: LogFunction; infoLogger?: LogFunction;
}; };

View File

@ -26,14 +26,23 @@ import {
OrderDomain, OrderDomain,
} from './types'; } 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, makerAssetData: string,
takerAssetData: string, takerAssetData: string,
marketOperation: MarketOperation, marketOperation: MarketOperation,
assetFillAmount: BigNumber, assetFillAmount: BigNumber,
opts: Partial<GetMarketOrdersOpts>, opts: Partial<GetMarketOrdersOpts>,
): Promise<RFQTIndicativeQuote[]> { ): Promise<RFQTIndicativeQuote[]> {
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( return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync(
makerAssetData, makerAssetData,
takerAssetData, takerAssetData,

View File

@ -13,14 +13,20 @@ import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types';
import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils'; import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as TypeMoq from 'typemoq';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src'; import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; 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 { 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 { createFillPaths } from '../src/utils/market_operation_utils/fills';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types'; 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 // tslint:disable: custom-no-magic-numbers promise-function-async
describe('MarketOperationUtils tests', () => { describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1; const CHAIN_ID = 1;
@ -30,11 +36,6 @@ describe('MarketOperationUtils tests', () => {
const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge; const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge;
const UNISWAP_V2_BRIDGE_ADDRESS = contractAddresses.uniswapV2Bridge; const UNISWAP_V2_BRIDGE_ADDRESS = contractAddresses.uniswapV2Bridge;
const CURVE_BRIDGE_ADDRESS = contractAddresses.curveBridge; 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; let originalSamplerOperations: any;
before(() => { before(() => {
@ -344,6 +345,68 @@ describe('MarketOperationUtils tests', () => {
}, },
} as any) as DexOrderSampler; } 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', () => { describe('MarketOperationUtils', () => {
let marketOperationUtils: MarketOperationUtils; let marketOperationUtils: MarketOperationUtils;