From 957e3eb93cbf492a6c277a5f89afb4f76cff543f Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 14 Apr 2020 17:19:02 -0400 Subject: [PATCH 01/18] asset-swapper: Restructure RFQ-T request options There's one subtlety here: apiKey has been moved to be within the rfqt namespace, after talking to Fabio and discovering that he only needs to re-use the API key in 0x API, not in asset-swapper. --- packages/asset-swapper/src/constants.ts | 6 ++-- packages/asset-swapper/src/index.ts | 2 +- packages/asset-swapper/src/swap_quoter.ts | 8 ++--- packages/asset-swapper/src/types.ts | 17 +++++------ .../src/utils/quote_requestor.ts | 30 +++++++++++++------ .../test/quote_requestor_test.ts | 23 +++++++------- 6 files changed, 50 insertions(+), 36 deletions(-) diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index cadd6d03f2..2f335809f7 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -5,7 +5,7 @@ import { ForwarderExtensionContractOpts, OrderPrunerOpts, OrderPrunerPermittedFeeTypes, - RfqtFirmQuoteRequestOpts, + RfqtRequestOpts, SwapQuoteExecutionOpts, SwapQuoteGetOutputOpts, SwapQuoteRequestOpts, @@ -66,7 +66,7 @@ const DEFAULT_SWAP_QUOTE_REQUEST_OPTS: SwapQuoteRequestOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, }; -const DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS: RfqtFirmQuoteRequestOpts = { +const DEFAULT_RFQT_REQUEST_OPTS: Partial = { makerEndpointMaxResponseTimeMs: 1000, }; @@ -86,7 +86,7 @@ export const constants = { DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, DEFAULT_SWAP_QUOTE_REQUEST_OPTS, DEFAULT_PER_PAGE, - DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS, + DEFAULT_RFQT_REQUEST_OPTS, NULL_ERC20_ASSET_DATA, PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 9ea63d5b63..8c6ff86073 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -45,7 +45,7 @@ export { MarketOperation, MarketSellSwapQuote, MockedRfqtFirmQuoteResponse, - RfqtFirmQuoteRequestOpts, + RfqtRequestOpts, SwapQuote, SwapQuoteConsumerBase, SwapQuoteConsumerOpts, diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 25e913b822..2a151d88b2 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -24,6 +24,7 @@ import { calculateLiquidity } from './utils/calculate_liquidity'; import { MarketOperationUtils } from './utils/market_operation_utils'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; import { DexOrderSampler } from './utils/market_operation_utils/sampler'; +import { GetMarketOrdersOpts } from './utils/market_operation_utils/types'; import { orderPrunerUtils } from './utils/order_prune_utils'; import { OrderStateUtils } from './utils/order_state_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; @@ -533,8 +534,8 @@ export class SwapQuoter { if ( opts.rfqt && opts.rfqt.intentOnFilling && - opts.apiKey && - this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) + opts.rfqt.apiKey && + this._rfqtTakerApiKeyWhitelist.includes(opts.rfqt.apiKey) ) { if (!opts.rfqt.takerAddress || opts.rfqt.takerAddress === constants.NULL_ADDRESS) { throw new Error('RFQ-T requests must specify a taker address'); @@ -545,8 +546,7 @@ export class SwapQuoter { takerAssetData, assetFillAmount, marketOperation, - opts.apiKey, - opts.rfqt.takerAddress, + opts.rfqt, ), ); } diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index a93e8ede40..d59f3f41c2 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -188,16 +188,19 @@ export interface SwapQuoteOrdersBreakdown { [source: string]: BigNumber; } +export interface RfqtRequestOpts { + takerAddress: string; + apiKey: string; + intentOnFilling?: boolean; + makerEndpointMaxResponseTimeMs?: number; +} + /** * gasPrice: gas price to determine protocolFee amount, default to ethGasStation fast amount */ export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts { gasPrice?: BigNumber; - apiKey?: string; - rfqt?: { - takerAddress: string; - intentOnFilling: boolean; - }; + rfqt?: RfqtRequestOpts; } /** @@ -274,10 +277,6 @@ export enum OrderPrunerPermittedFeeTypes { TakerDenominatedTakerFee = 'TAKER_DENOMINATED_TAKER_FEE', } -export interface RfqtFirmQuoteRequestOpts { - makerEndpointMaxResponseTimeMs?: number; -} - /** * Represents a mocked RFQT maker responses. */ diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index db73ab0038..41ec08ad13 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -6,7 +6,7 @@ import Axios, { AxiosResponse } from 'axios'; import * as _ from 'lodash'; import { constants } from '../constants'; -import { MarketOperation, RfqtFirmQuoteRequestOpts } from '../types'; +import { MarketOperation, RfqtRequestOpts } from '../types'; /** * Request quotes from RFQ-T providers @@ -27,6 +27,17 @@ function getTokenAddressOrThrow(assetData: string): string { throw new Error(`Decoded asset data (${JSON.stringify(decodedAssetData)}) does not contain a token address`); } +function assertTakerAddressOrThrow(takerAddress: string | undefined): void { + if ( + takerAddress === undefined || + takerAddress === '' || + takerAddress === '0x' || + takerAddress === constants.NULL_ADDRESS + ) { + throw new Error('RFQ-T requires the presence of a taker address'); + } +} + export class QuoteRequestor { private readonly _rfqtMakerEndpoints: string[]; private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); @@ -40,11 +51,10 @@ export class QuoteRequestor { takerAssetData: string, assetFillAmount: BigNumber, marketOperation: MarketOperation, - takerApiKey: string, - takerAddress: string, - options?: Partial, + options?: Partial, ): Promise { - const { makerEndpointMaxResponseTimeMs } = _.merge({}, constants.DEFAULT_RFQT_FIRM_QUOTE_REQUEST_OPTS, options); + const _opts = _.merge({}, constants.DEFAULT_RFQT_REQUEST_OPTS, options); + assertTakerAddressOrThrow(_opts.takerAddress); const buyToken = getTokenAddressOrThrow(makerAssetData); const sellToken = getTokenAddressOrThrow(takerAssetData); @@ -55,20 +65,22 @@ export class QuoteRequestor { this._rfqtMakerEndpoints.map(async rfqtMakerEndpoint => { try { return await Axios.get(`${rfqtMakerEndpoint}/quote`, { - headers: { '0x-api-key': takerApiKey }, + headers: { '0x-api-key': _opts.apiKey }, params: { sellToken, buyToken, buyAmount: marketOperation === MarketOperation.Buy ? assetFillAmount.toString() : undefined, sellAmount: marketOperation === MarketOperation.Sell ? assetFillAmount.toString() : undefined, - takerAddress, + takerAddress: _opts.takerAddress, }, - timeout: makerEndpointMaxResponseTimeMs, + timeout: _opts.makerEndpointMaxResponseTimeMs, }); } catch (err) { logUtils.warn( - `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${takerApiKey} for taker address ${takerAddress}`, + `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ + _opts.apiKey + } for taker address ${_opts.takerAddress}`, ); logUtils.warn(err); return undefined; diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts index 1062ae4a03..34f2d63662 100644 --- a/packages/asset-swapper/test/quote_requestor_test.ts +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -23,7 +23,7 @@ describe('QuoteRequestor', async () => { describe('requestRfqtFirmQuotesAsync', async () => { it('should return successful RFQT requests', async () => { const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; - const takerApiKey = 'my-ko0l-api-key'; + const apiKey = 'my-ko0l-api-key'; // Set up RFQT responses // tslint:disable-next-line:array-type @@ -43,7 +43,7 @@ describe('QuoteRequestor', async () => { }); mockedRequests.push({ endpoint: 'https://1337.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: successfulOrder1, responseCode: StatusCodes.Success, @@ -51,7 +51,7 @@ describe('QuoteRequestor', async () => { // Test out a bad response code, ensure it doesnt cause throw mockedRequests.push({ endpoint: 'https://420.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: { error: 'bad request' }, responseCode: StatusCodes.InternalError, @@ -59,7 +59,7 @@ describe('QuoteRequestor', async () => { // Test out a successful response code but an invalid order mockedRequests.push({ endpoint: 'https://421.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: { makerAssetData: '123' }, responseCode: StatusCodes.Success, @@ -71,7 +71,7 @@ describe('QuoteRequestor', async () => { }); mockedRequests.push({ endpoint: 'https://422.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: wrongMakerAssetDataOrder, responseCode: StatusCodes.Success, @@ -83,7 +83,7 @@ describe('QuoteRequestor', async () => { }); mockedRequests.push({ endpoint: 'https://423.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: wrongTakerAssetDataOrder, responseCode: StatusCodes.Success, @@ -97,7 +97,7 @@ describe('QuoteRequestor', async () => { delete unsignedOrder.signature; mockedRequests.push({ endpoint: 'https://424.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: unsignedOrder, responseCode: StatusCodes.Success, @@ -107,7 +107,7 @@ describe('QuoteRequestor', async () => { const successfulOrder2 = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData }); mockedRequests.push({ endpoint: 'https://37.0.0.1', - requestApiKey: takerApiKey, + requestApiKey: apiKey, requestParams: expectedParams, responseData: successfulOrder2, responseCode: StatusCodes.Success, @@ -128,8 +128,11 @@ describe('QuoteRequestor', async () => { takerAssetData, new BigNumber(10000), MarketOperation.Sell, - takerApiKey, - takerAddress, + { + apiKey, + takerAddress, + intentOnFilling: true, + }, ); expect(resp.sort()).to.eql([successfulOrder1, successfulOrder2].sort()); }); From 80c8712b2ad2715488cdfe78e322d28701c1f6a9 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 14 Apr 2020 20:44:14 -0400 Subject: [PATCH 02/18] asset-swapper: Rm unused field in SwapQuoterOpts --- packages/asset-swapper/src/swap_quoter.ts | 3 +-- packages/asset-swapper/src/types.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 2a151d88b2..a5b3f6f111 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -170,8 +170,7 @@ export class SwapQuoter { this._devUtilsContract = new DevUtilsContract(this._contractAddresses.devUtils, provider); this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); - this._quoteRequestor = - options.quoteRequestor || new QuoteRequestor(options.rfqt ? options.rfqt.makerEndpoints || [] : []); + this._quoteRequestor = new QuoteRequestor(options.rfqt ? options.rfqt.makerEndpoints || [] : []); const sampler = new DexOrderSampler( new IERC20BridgeSamplerContract(this._contractAddresses.erc20BridgeSampler, this.provider, { gas: samplerGasLimit, diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index d59f3f41c2..dfc864a9a0 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -3,7 +3,6 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { GetMarketOrdersOpts } from './utils/market_operation_utils/types'; -import { QuoteRequestor } from './utils/quote_requestor'; /** * expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m). @@ -226,7 +225,6 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { takerApiKeyWhitelist: string[]; makerEndpoints: string[]; }; - quoteRequestor?: QuoteRequestor; } /** From 33fdfdc8c074a0f8b0979258465ef53f46307a80 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 14 Apr 2020 22:08:23 -0400 Subject: [PATCH 03/18] asset-swapper: Extract method inferQueryParams For use in upcoming implementation of indicative quotes. --- .../src/utils/quote_requestor.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 41ec08ad13..8b09e14286 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -38,6 +38,29 @@ function assertTakerAddressOrThrow(takerAddress: string | undefined): void { } } +function inferQueryParams( + marketOperation: MarketOperation, + makerAssetData: string, + takerAssetData: string, + assetFillAmount: BigNumber, +): { buyToken: string; sellToken: string; buyAmount: string | undefined; sellAmount: string | undefined } { + if (marketOperation === MarketOperation.Buy) { + return { + buyToken: getTokenAddressOrThrow(makerAssetData), + sellToken: getTokenAddressOrThrow(takerAssetData), + buyAmount: assetFillAmount.toString(), + sellAmount: undefined, + }; + } else { + return { + buyToken: getTokenAddressOrThrow(makerAssetData), + sellToken: getTokenAddressOrThrow(takerAssetData), + sellAmount: assetFillAmount.toString(), + buyAmount: undefined, + }; + } +} + export class QuoteRequestor { private readonly _rfqtMakerEndpoints: string[]; private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); @@ -56,9 +79,6 @@ export class QuoteRequestor { const _opts = _.merge({}, constants.DEFAULT_RFQT_REQUEST_OPTS, options); assertTakerAddressOrThrow(_opts.takerAddress); - const buyToken = getTokenAddressOrThrow(makerAssetData); - const sellToken = getTokenAddressOrThrow(takerAssetData); - // create an array of promises for quote responses, using "undefined" // as a placeholder for failed requests. const responsesIfDefined: Array> = await Promise.all( @@ -67,12 +87,8 @@ export class QuoteRequestor { return await Axios.get(`${rfqtMakerEndpoint}/quote`, { headers: { '0x-api-key': _opts.apiKey }, params: { - sellToken, - buyToken, - buyAmount: marketOperation === MarketOperation.Buy ? assetFillAmount.toString() : undefined, - sellAmount: - marketOperation === MarketOperation.Sell ? assetFillAmount.toString() : undefined, takerAddress: _opts.takerAddress, + ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), }, timeout: _opts.makerEndpointMaxResponseTimeMs, }); From d6d4d29257c4cb0810c280f8edc33939f0e6092e Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 15 Apr 2020 18:19:36 -0400 Subject: [PATCH 04/18] asset-swapper: RFQ-T indicative quotes These changes have been exercised via mocha tests in the 0x-api repo. Not sure why I had to add GetMarketOrdersRfqtOpts to the package exports. `yarn test:generate_docs:circleci` said: $ node ./packages/monorepo-scripts/lib/doc_generate.js --package @0x/asset-swapper GENERATE_DOCS: Generating Typedoc JSON for @0x/asset-swapper... GENERATE_DOCS: Generating Typedoc Markdown for @0x/asset-swapper... GENERATE_DOCS: Modifying Markdown To Exclude Unexported Items... Error: @0x/asset-swapper package needs to export: GetMarketOrdersRfqtOpts From it's index.ts. If any are from external dependencies, then add them to the EXTERNAL_TYPE_MAP. at DocGenerateUtils._lookForMissingReferenceExportsThrowIfExists (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:288:19) at DocGenerateUtils. (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:255:34) at step (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:32:23) at Object.next (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:13:53) at fulfilled (/root/repo/packages/monorepo-scripts/lib/utils/doc_generate_utils.js:4:58) at at process._tickCallback (internal/process/next_tick.js:189:7) --- packages/asset-swapper/src/index.ts | 3 +- packages/asset-swapper/src/swap_quoter.ts | 17 +++- packages/asset-swapper/src/types.ts | 13 +++ .../src/utils/market_operation_utils/index.ts | 59 +++++++++---- .../utils/market_operation_utils/orders.ts | 30 +++++++ .../src/utils/market_operation_utils/types.ts | 13 ++- .../src/utils/quote_requestor.ts | 84 +++++++++++++++++++ .../asset-swapper/src/utils/rfqt_mocker.ts | 21 +++++ .../test/quote_requestor_test.ts | 2 +- 9 files changed, 222 insertions(+), 20 deletions(-) diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 8c6ff86073..148ea65924 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -64,8 +64,9 @@ export { CollapsedFill, NativeCollapsedFill, OptimizedMarketOrder, + GetMarketOrdersRfqtOpts, } from './utils/market_operation_utils/types'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; -export { QuoteRequestor } from './utils/quote_requestor'; +export { QuoteRequestor, RfqtIndicativeQuoteResponse } from './utils/quote_requestor'; export { rfqtMocker } from './utils/rfqt_mocker'; diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index a5b3f6f111..e2c392a637 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -9,6 +9,7 @@ import * as _ from 'lodash'; import { constants } from './constants'; import { + CalculateSwapQuoteOpts, LiquidityForTakerMakerAssetDataPair, MarketBuySwapQuote, MarketOperation, @@ -24,7 +25,6 @@ import { calculateLiquidity } from './utils/calculate_liquidity'; import { MarketOperationUtils } from './utils/market_operation_utils'; import { createDummyOrderForSampler } from './utils/market_operation_utils/orders'; import { DexOrderSampler } from './utils/market_operation_utils/sampler'; -import { GetMarketOrdersOpts } from './utils/market_operation_utils/types'; import { orderPrunerUtils } from './utils/order_prune_utils'; import { OrderStateUtils } from './utils/order_state_utils'; import { ProtocolFeeUtils } from './utils/protocol_fee_utils'; @@ -565,19 +565,30 @@ export class SwapQuoter { let swapQuote: SwapQuote; + const calcOpts: CalculateSwapQuoteOpts = opts; + if ( + // we should request indicative quotes: + calcOpts.rfqt && + !calcOpts.rfqt.intentOnFilling && + calcOpts.rfqt.apiKey && + this._rfqtTakerApiKeyWhitelist.includes(calcOpts.rfqt.apiKey) + ) { + calcOpts.rfqt.quoteRequestor = this._quoteRequestor; + } + if (marketOperation === MarketOperation.Buy) { swapQuote = await this._swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( orders, assetFillAmount, gasPrice, - opts, + calcOpts, ); } else { swapQuote = await this._swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( orders, assetFillAmount, gasPrice, - opts, + calcOpts, ); } diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index dfc864a9a0..27d695d7e9 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -287,3 +287,16 @@ export interface MockedRfqtFirmQuoteResponse { responseData: any; responseCode: number; } + +/** + * Represents a mocked RFQT maker responses. + */ +export interface MockedRfqtIndicativeQuoteResponse { + endpoint: string; + requestApiKey: string; + requestParams: { + [key: string]: string | undefined; + }; + responseData: any; + responseCode: number; +} 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 371fcce13f..98aa08d73c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types'; import { BigNumber, NULL_ADDRESS } from '@0x/utils'; import { MarketOperation } from '../../types'; +import { RfqtIndicativeQuoteResponse } from '../quote_requestor'; import { difference } from '../utils'; import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants'; @@ -13,7 +14,12 @@ import { getPathAdjustedSlippage, getPathSize, } from './fills'; -import { createOrdersFromPath, createSignedOrdersWithFillableAmounts, getNativeOrderTokens } from './orders'; +import { + createOrdersFromPath, + createSignedOrdersFromRfqtIndicativeQuotes, + createSignedOrdersWithFillableAmounts, + getNativeOrderTokens, +} from './orders'; import { findOptimalPath } from './path_optimizer'; import { DexOrderSampler, getSampleAmounts } from './sampler'; import { @@ -57,12 +63,7 @@ export class MarketOperationUtils { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); // Call the sampler contract. - const [ - orderFillableAmounts, - liquidityProviderAddress, - ethToMakerAssetRate, - dexQuotes, - ] = await this._sampler.executeAsync( + const samplerPromise = this._sampler.executeAsync( // Get native order fillable amounts. DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders), // Get the custom liquidity provider from registry. @@ -92,10 +93,25 @@ export class MarketOperationUtils { this._liquidityProviderRegistry, ), ); + const rfqtPromise = + _opts !== undefined && _opts.rfqt !== undefined && _opts.rfqt.quoteRequestor !== undefined + ? _opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( + nativeOrders[0].makerAssetData, + nativeOrders[0].takerAssetData, + takerAmount, + MarketOperation.Sell, + _opts.rfqt, + ) + : Promise.resolve([]); + const [ + [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes], + rfqtIndicativeQuotes, + ] = await Promise.all([samplerPromise, rfqtPromise]); return this._generateOptimizedOrders({ orderFillableAmounts, nativeOrders, dexQuotes, + rfqtIndicativeQuotes, liquidityProviderAddress, inputToken: takerToken, outputToken: makerToken, @@ -130,12 +146,7 @@ export class MarketOperationUtils { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]); // Call the sampler contract. - const [ - orderFillableAmounts, - liquidityProviderAddress, - ethToTakerAssetRate, - dexQuotes, - ] = await this._sampler.executeAsync( + const samplerPromise = this._sampler.executeAsync( // Get native order fillable amounts. DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders), // Get the custom liquidity provider from registry. @@ -165,11 +176,26 @@ export class MarketOperationUtils { this._liquidityProviderRegistry, ), ); + const rfqtPromise = + opts !== undefined && _opts.rfqt !== undefined && _opts.rfqt.quoteRequestor !== undefined + ? _opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( + nativeOrders[0].makerAssetData, + nativeOrders[0].takerAssetData, + makerAmount, + MarketOperation.Buy, + _opts.rfqt, + ) + : []; + const [ + [orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes], + rfqtIndicativeQuotes, + ] = await Promise.all([samplerPromise, rfqtPromise]); return this._generateOptimizedOrders({ orderFillableAmounts, nativeOrders, dexQuotes, + rfqtIndicativeQuotes, liquidityProviderAddress, inputToken: makerToken, outputToken: takerToken, @@ -246,6 +272,7 @@ export class MarketOperationUtils { orderFillableAmounts, nativeOrders, dexQuotes, + rfqtIndicativeQuotes: [], inputToken: makerToken, outputToken: takerToken, side: MarketOperation.Buy, @@ -274,6 +301,7 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[]; orderFillableAmounts: BigNumber[]; dexQuotes: DexSample[][]; + rfqtIndicativeQuotes: RfqtIndicativeQuoteResponse[]; runLimit?: number; ethToOutputRate?: BigNumber; bridgeSlippage?: number; @@ -290,7 +318,10 @@ export class MarketOperationUtils { const paths = createFillPaths({ side, // Augment native orders with their fillable amounts. - orders: createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts), + orders: [ + ...createSignedOrdersWithFillableAmounts(side, opts.nativeOrders, opts.orderFillableAmounts), + ...createSignedOrdersFromRfqtIndicativeQuotes(opts.rfqtIndicativeQuotes), + ], dexQuotes: opts.dexQuotes, targetInput: inputAmount, ethToOutputRate: opts.ethToOutputRate, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 018055f5fc..854f3f9642 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -4,6 +4,7 @@ import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; +import { RfqtIndicativeQuoteResponse } from '../quote_requestor'; import { DEFAULT_CURVE_OPTS, @@ -358,3 +359,32 @@ function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder { ...(fill as NativeCollapsedFill).nativeOrder, }; } + +export function createSignedOrdersFromRfqtIndicativeQuotes( + quotes: RfqtIndicativeQuoteResponse[], +): SignedOrderWithFillableAmounts[] { + return quotes.map(quote => { + return { + fillableMakerAssetAmount: quote.makerAssetAmount, + fillableTakerAssetAmount: quote.takerAssetAmount, + makerAssetAmount: quote.makerAssetAmount, + takerAssetAmount: quote.takerAssetAmount, + makerAssetData: quote.makerAssetData, + takerAssetData: quote.takerAssetData, + takerAddress: NULL_ADDRESS, + makerAddress: NULL_ADDRESS, + senderAddress: NULL_ADDRESS, + feeRecipientAddress: NULL_ADDRESS, + salt: ZERO_AMOUNT, // generatePseudoRandomSalt(), + expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS), + makerFeeAssetData: NULL_BYTES, + takerFeeAssetData: NULL_BYTES, + makerFee: ZERO_AMOUNT, + takerFee: ZERO_AMOUNT, + fillableTakerFeeAmount: ZERO_AMOUNT, + signature: WALLET_SIGNATURE, + chainId: 0, // HACK !!!!!!!!! how can we get at this from this context? + exchangeAddress: NULL_ADDRESS, // HACK !!!!!!!!! how can we get at this from this context? + }; + }); +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index b2d372a6e3..baecb18ca0 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -1,7 +1,8 @@ import { IERC20BridgeSamplerContract } from '@0x/contract-wrappers'; import { BigNumber } from '@0x/utils'; -import { SignedOrderWithFillableAmounts } from '../../types'; +import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; +import { QuoteRequestor, RfqtIndicativeQuoteResponse } from '../../utils/quote_requestor'; /** * Order domain keys: chainId and exchange @@ -34,6 +35,7 @@ export enum ERC20BridgeSource { CurveUsdcDaiUsdtTusd = 'Curve_USDC_DAI_USDT_TUSD', LiquidityProvider = 'LiquidityProvider', CurveUsdcDaiUsdtBusd = 'Curve_USDC_DAI_USDT_BUSD', + Rfqt = 'Rfqt', } // Internal `fillData` field for `Fill` objects. @@ -44,6 +46,10 @@ export interface NativeFillData extends FillData { order: SignedOrderWithFillableAmounts; } +export interface RfqtFillData extends FillData { + quote: RfqtIndicativeQuoteResponse; +} + /** * Represents an individual DEX sample from the sampler contract. */ @@ -130,6 +136,10 @@ export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts { fills: CollapsedFill[]; } +export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts { + quoteRequestor?: QuoteRequestor; +} + /** * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. */ @@ -183,6 +193,7 @@ export interface GetMarketOrdersOpts { * sources. Defaults to `true`. */ allowFallback: boolean; + rfqt?: GetMarketOrdersRfqtOpts; /** * Whether to combine contiguous bridge orders into a single DexForwarderBridge * order. Defaults to `true`. diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 8b09e14286..60ce20248e 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -12,6 +12,13 @@ import { MarketOperation, RfqtRequestOpts } from '../types'; * Request quotes from RFQ-T providers */ +export interface RfqtIndicativeQuoteResponse { + makerAssetData: string; + makerAssetAmount: BigNumber; + takerAssetData: string; + takerAssetAmount: BigNumber; +} + function getTokenAddressOrThrow(assetData: string): string { const decodedAssetData = assetDataUtils.decodeAssetDataOrThrow(assetData); if (decodedAssetData.hasOwnProperty('tokenAddress')) { @@ -141,4 +148,81 @@ export class QuoteRequestor { return orders; } + + public async requestRfqtIndicativeQuotesAsync( + makerAssetData: string, + takerAssetData: string, + assetFillAmount: BigNumber, + marketOperation: MarketOperation, + options: RfqtRequestOpts, + ): Promise { + const _opts = _.merge({}, constants.DEFAULT_RFQT_REQUEST_OPTS, options); + assertTakerAddressOrThrow(_opts.takerAddress); + + const axiosResponsesIfDefined: Array< + undefined | AxiosResponse + > = await Promise.all( + this._rfqtMakerEndpoints.map(async rfqtMakerEndpoint => { + try { + return await Axios.get(`${rfqtMakerEndpoint}/price`, { + headers: { '0x-api-key': options.apiKey }, + params: { + takerAddress: options.takerAddress, + ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), + }, + timeout: options.makerEndpointMaxResponseTimeMs, + }); + } catch (err) { + logUtils.warn( + `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ + options.apiKey + } for taker address ${options.takerAddress}`, + ); + logUtils.warn(err); + return undefined; + } + }), + ); + + const axiosResponses = axiosResponsesIfDefined.filter( + (respIfDefd): respIfDefd is AxiosResponse => respIfDefd !== undefined, + ); + + const responsesWithStringInts = axiosResponses.map(response => response.data); // not yet BigNumber + + const validResponsesWithStringInts = responsesWithStringInts.filter(response => { + if (this._isValidRfqtIndicativeQuoteResponse(response)) { + return true; + } + logUtils.warn(`Invalid RFQ-T indicative quote received, filtering out: ${JSON.stringify(response)}`); + return false; + }); + + const responses = validResponsesWithStringInts.map(response => { + return { + ...response, + makerAssetAmount: new BigNumber(response.makerAssetAmount), + takerAssetAmount: new BigNumber(response.takerAssetAmount), + }; + }); + + return responses; + } + + private _isValidRfqtIndicativeQuoteResponse(response: RfqtIndicativeQuoteResponse): boolean { + const hasValidMakerAssetAmount = this._schemaValidator.isValid( + response.makerAssetAmount, + schemas.wholeNumberSchema, + ); + const hasValidTakerAssetAmount = this._schemaValidator.isValid( + response.takerAssetAmount, + schemas.wholeNumberSchema, + ); + const hasValidMakerAssetData = this._schemaValidator.isValid(response.makerAssetData, schemas.hexSchema); + const hasValidTakerAssetData = this._schemaValidator.isValid(response.takerAssetData, schemas.hexSchema); + if (hasValidMakerAssetAmount && hasValidTakerAssetAmount && hasValidMakerAssetData && hasValidTakerAssetData) { + return true; + } + return false; + } } diff --git a/packages/asset-swapper/src/utils/rfqt_mocker.ts b/packages/asset-swapper/src/utils/rfqt_mocker.ts index dfb8426f67..77e16e8925 100644 --- a/packages/asset-swapper/src/utils/rfqt_mocker.ts +++ b/packages/asset-swapper/src/utils/rfqt_mocker.ts @@ -28,6 +28,27 @@ export const rfqtMocker = { .replyOnce(responseCode, responseData); } + await performFn(); + } finally { + // Ensure we always restore axios afterwards + mockedAxios.restore(); + } + }, + withMockedRfqtIndicativeQuotes: async ( + mockedResponses: MockedRfqtFirmQuoteResponse[], + performFn: () => Promise, + ) => { + const mockedAxios = new AxiosMockAdapter(axios); + try { + // Mock out RFQT responses + for (const mockedResponse of mockedResponses) { + const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; + const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; + mockedAxios + .onGet(`${endpoint}/price`, { params: requestParams }, requestHeaders) + .replyOnce(responseCode, responseData); + } + await performFn(); } finally { // Ensure we always restore axios afterwards diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts index 34f2d63662..6f626917a8 100644 --- a/packages/asset-swapper/test/quote_requestor_test.ts +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -20,7 +20,7 @@ describe('QuoteRequestor', async () => { const makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken); const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken); - describe('requestRfqtFirmQuotesAsync', async () => { + describe('requestRfqtFirmQuotesAsync for firm quotes', async () => { it('should return successful RFQT requests', async () => { const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; const apiKey = 'my-ko0l-api-key'; From 45bc967f301eb67dafcaede2f1086a59229ec1f9 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Fri, 17 Apr 2020 01:42:10 -0400 Subject: [PATCH 05/18] asset-swapper: Differentiate RFQT warning messages --- packages/asset-swapper/src/utils/quote_requestor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 60ce20248e..3dbba34bd8 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -101,7 +101,7 @@ export class QuoteRequestor { }); } catch (err) { logUtils.warn( - `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ + `Failed to get RFQ-T firm quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ _opts.apiKey } for taker address ${_opts.takerAddress}`, ); @@ -174,7 +174,7 @@ export class QuoteRequestor { }); } catch (err) { logUtils.warn( - `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ + `Failed to get RFQ-T indicative quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ options.apiKey } for taker address ${options.takerAddress}`, ); From 2456adcb68102023796e6216fa39f3474b6bfaa6 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 20:14:18 -0400 Subject: [PATCH 06/18] asset-swapper: validate RFQT indicative quotes Validate that the responses returned from maker endpoints both conform to expected JSON schema data types and also have the expected asset data, per the taker's request. --- .../src/utils/quote_requestor.ts | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 3dbba34bd8..63b6770b1a 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -68,6 +68,17 @@ function inferQueryParams( } } +function hasExpectedAssetData( + expectedMakerAssetData: string, + expectedTakerAssetData: string, + makerAssetDataInQuestion: string, + takerAssetDataInQuestion: string, +): boolean { + const hasExpectedMakerAssetData = makerAssetDataInQuestion.toLowerCase() === expectedMakerAssetData.toLowerCase(); + const hasExpectedTakerAssetData = takerAssetDataInQuestion.toLowerCase() === expectedTakerAssetData.toLowerCase(); + return hasExpectedMakerAssetData && hasExpectedTakerAssetData; +} + export class QuoteRequestor { private readonly _rfqtMakerEndpoints: string[]; private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); @@ -124,9 +135,14 @@ export class QuoteRequestor { return false; } - const hasExpectedMakerAssetData = order.makerAssetData.toLowerCase() === makerAssetData.toLowerCase(); - const hasExpectedTakerAssetData = order.takerAssetData.toLowerCase() === takerAssetData.toLowerCase(); - if (!hasExpectedMakerAssetData || !hasExpectedTakerAssetData) { + if ( + !hasExpectedAssetData( + makerAssetData, + takerAssetData, + order.makerAssetData.toLowerCase(), + order.takerAssetData.toLowerCase(), + ) + ) { logUtils.warn(`Unexpected asset data in RFQ-T order, filtering out: ${JSON.stringify(order)}`); return false; } @@ -191,11 +207,19 @@ export class QuoteRequestor { const responsesWithStringInts = axiosResponses.map(response => response.data); // not yet BigNumber const validResponsesWithStringInts = responsesWithStringInts.filter(response => { - if (this._isValidRfqtIndicativeQuoteResponse(response)) { - return true; + if (!this._isValidRfqtIndicativeQuoteResponse(response)) { + logUtils.warn(`Invalid RFQ-T indicative quote received, filtering out: ${JSON.stringify(response)}`); + return false; + } + if ( + !hasExpectedAssetData(makerAssetData, takerAssetData, response.makerAssetData, response.takerAssetData) + ) { + logUtils.warn( + `Unexpected asset data in RFQ-T indicative quote, filtering out: ${JSON.stringify(response)}`, + ); + return false; } - logUtils.warn(`Invalid RFQ-T indicative quote received, filtering out: ${JSON.stringify(response)}`); - return false; + return true; }); const responses = validResponsesWithStringInts.map(response => { @@ -210,16 +234,18 @@ export class QuoteRequestor { } private _isValidRfqtIndicativeQuoteResponse(response: RfqtIndicativeQuoteResponse): boolean { - const hasValidMakerAssetAmount = this._schemaValidator.isValid( - response.makerAssetAmount, - schemas.wholeNumberSchema, - ); - const hasValidTakerAssetAmount = this._schemaValidator.isValid( - response.takerAssetAmount, - schemas.wholeNumberSchema, - ); - const hasValidMakerAssetData = this._schemaValidator.isValid(response.makerAssetData, schemas.hexSchema); - const hasValidTakerAssetData = this._schemaValidator.isValid(response.takerAssetData, schemas.hexSchema); + const hasValidMakerAssetAmount = + response.makerAssetAmount !== undefined && + this._schemaValidator.isValid(response.makerAssetAmount, schemas.wholeNumberSchema); + const hasValidTakerAssetAmount = + response.takerAssetAmount !== undefined && + this._schemaValidator.isValid(response.takerAssetAmount, schemas.wholeNumberSchema); + const hasValidMakerAssetData = + response.makerAssetData !== undefined && + this._schemaValidator.isValid(response.makerAssetData, schemas.hexSchema); + const hasValidTakerAssetData = + response.takerAssetData !== undefined && + this._schemaValidator.isValid(response.takerAssetData, schemas.hexSchema); if (hasValidMakerAssetAmount && hasValidTakerAssetAmount && hasValidMakerAssetData && hasValidTakerAssetData) { return true; } From 464c918134f88e4c8e72add5a083ef21539ddadc Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 20:15:47 -0400 Subject: [PATCH 07/18] asset-swapper: test RFQT indicative quote handling --- .../test/quote_requestor_test.ts | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts index 6f626917a8..137ba5af34 100644 --- a/packages/asset-swapper/test/quote_requestor_test.ts +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -5,7 +5,7 @@ import { BigNumber } from '@0x/utils'; import * as chai from 'chai'; import 'mocha'; -import { MarketOperation, MockedRfqtFirmQuoteResponse } from '../src/types'; +import { MarketOperation, MockedRfqtFirmQuoteResponse, MockedRfqtIndicativeQuoteResponse } from '../src/types'; import { QuoteRequestor } from '../src/utils/quote_requestor'; import { rfqtMocker } from '../src/utils/rfqt_mocker'; @@ -138,4 +138,99 @@ describe('QuoteRequestor', async () => { }); }); }); + describe('requestRfqtIndicativeQuotesAsync for Indicative quotes', async () => { + it('should return successful RFQT requests', async () => { + const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; + const apiKey = 'my-ko0l-api-key'; + + // Set up RFQT responses + // tslint:disable-next-line:array-type + const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; + const expectedParams = { + sellToken: takerToken, + buyToken: makerToken, + sellAmount: '10000', + buyAmount: undefined, + takerAddress, + }; + // Successful response + const successfulQuote1 = { + makerAssetData, + takerAssetData, + makerAssetAmount: new BigNumber(expectedParams.sellAmount), + takerAssetAmount: new BigNumber(expectedParams.sellAmount), + }; + mockedRequests.push({ + endpoint: 'https://1337.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: successfulQuote1, + responseCode: StatusCodes.Success, + }); + // Test out a bad response code, ensure it doesnt cause throw + mockedRequests.push({ + endpoint: 'https://420.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: { error: 'bad request' }, + responseCode: StatusCodes.InternalError, + }); + // Test out a successful response code but an invalid order + mockedRequests.push({ + endpoint: 'https://421.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: { makerAssetData: '123' }, + responseCode: StatusCodes.Success, + }); + // A successful response code and valid response data, but for wrong maker asset data + mockedRequests.push({ + endpoint: 'https://422.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: { ...successfulQuote1, makerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) }, + responseCode: StatusCodes.Success, + }); + // A successful response code and valid response data, but for wrong taker asset data + mockedRequests.push({ + endpoint: 'https://423.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: { ...successfulQuote1, takerAssetData: assetDataUtils.encodeERC20AssetData(otherToken1) }, + responseCode: StatusCodes.Success, + }); + // Another Successful response + mockedRequests.push({ + endpoint: 'https://37.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: successfulQuote1, + responseCode: StatusCodes.Success, + }); + + return rfqtMocker.withMockedRfqtIndicativeQuotes(mockedRequests, async () => { + const qr = new QuoteRequestor([ + 'https://1337.0.0.1', + 'https://420.0.0.1', + 'https://421.0.0.1', + 'https://422.0.0.1', + 'https://423.0.0.1', + 'https://424.0.0.1', + 'https://37.0.0.1', + ]); + const resp = await qr.requestRfqtIndicativeQuotesAsync( + makerAssetData, + takerAssetData, + new BigNumber(10000), + MarketOperation.Sell, + { + apiKey, + takerAddress, + intentOnFilling: true, + }, + ); + expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort()); + }); + }); + }); }); From 4cc9ceabd21edecc28a430973d90f7ad96c0a2a0 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 20:16:55 -0400 Subject: [PATCH 08/18] asset-swapper: Rm unused ERC20BridgeSource.Rfqt Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r410517580 --- packages/asset-swapper/src/utils/market_operation_utils/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index baecb18ca0..57fd9dfcc8 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -35,7 +35,6 @@ export enum ERC20BridgeSource { CurveUsdcDaiUsdtTusd = 'Curve_USDC_DAI_USDT_TUSD', LiquidityProvider = 'LiquidityProvider', CurveUsdcDaiUsdtBusd = 'Curve_USDC_DAI_USDT_BUSD', - Rfqt = 'Rfqt', } // Internal `fillData` field for `Fill` objects. From 5effc6ec90e5a90402e0efb01cefd0b6a6a8ef1b Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 21:47:11 -0400 Subject: [PATCH 09/18] asset-swapper: extract method Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r411514718 --- .../src/utils/market_operation_utils/index.ts | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) 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 98aa08d73c..a2007a2e99 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -32,6 +32,26 @@ import { OrderDomain, } from './types'; +async function getRfqtIndicativeQuotesAsync( + makerAssetData: string, + takerAssetData: string, + marketOperation: MarketOperation, + assetFillAmount: BigNumber, + opts: Partial, +): Promise { + if (opts.rfqt && opts.rfqt.quoteRequestor) { + return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( + makerAssetData, + takerAssetData, + assetFillAmount, + marketOperation, + opts.rfqt, + ); + } else { + return Promise.resolve([]); + } +} + export class MarketOperationUtils { private readonly _wethAddress: string; @@ -93,16 +113,13 @@ export class MarketOperationUtils { this._liquidityProviderRegistry, ), ); - const rfqtPromise = - _opts !== undefined && _opts.rfqt !== undefined && _opts.rfqt.quoteRequestor !== undefined - ? _opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( - nativeOrders[0].makerAssetData, - nativeOrders[0].takerAssetData, - takerAmount, - MarketOperation.Sell, - _opts.rfqt, - ) - : Promise.resolve([]); + const rfqtPromise = getRfqtIndicativeQuotesAsync( + makerToken, + takerToken, + MarketOperation.Sell, + takerAmount, + _opts, + ); const [ [orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, dexQuotes], rfqtIndicativeQuotes, @@ -176,16 +193,13 @@ export class MarketOperationUtils { this._liquidityProviderRegistry, ), ); - const rfqtPromise = - opts !== undefined && _opts.rfqt !== undefined && _opts.rfqt.quoteRequestor !== undefined - ? _opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( - nativeOrders[0].makerAssetData, - nativeOrders[0].takerAssetData, - makerAmount, - MarketOperation.Buy, - _opts.rfqt, - ) - : []; + const rfqtPromise = getRfqtIndicativeQuotesAsync( + makerToken, + takerToken, + MarketOperation.Buy, + makerAmount, + _opts, + ); const [ [orderFillableAmounts, liquidityProviderAddress, ethToTakerAssetRate, dexQuotes], rfqtIndicativeQuotes, From ad7868ebe10b902c04f34cd2cfdac64b72dfb8bc Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 22:13:19 -0400 Subject: [PATCH 10/18] asset-s: clean up order faking for indicative RFQT Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r411545804 --- .../src/utils/market_operation_utils/orders.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 854f3f9642..320bee4fb2 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -375,16 +375,16 @@ export function createSignedOrdersFromRfqtIndicativeQuotes( makerAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS, feeRecipientAddress: NULL_ADDRESS, - salt: ZERO_AMOUNT, // generatePseudoRandomSalt(), - expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS), + salt: ZERO_AMOUNT, + expirationTimeSeconds: ZERO_AMOUNT, makerFeeAssetData: NULL_BYTES, takerFeeAssetData: NULL_BYTES, makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, fillableTakerFeeAmount: ZERO_AMOUNT, signature: WALLET_SIGNATURE, - chainId: 0, // HACK !!!!!!!!! how can we get at this from this context? - exchangeAddress: NULL_ADDRESS, // HACK !!!!!!!!! how can we get at this from this context? + chainId: 0, + exchangeAddress: NULL_ADDRESS, }; }); } From 8875f924b0079ca955aa917f5430fd78860b00f4 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 22:23:14 -0400 Subject: [PATCH 11/18] asset-s: test requestRfqtIndicativeQuotesAsync --- .../test/quote_requestor_test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts index 137ba5af34..d2c9ef7df9 100644 --- a/packages/asset-swapper/test/quote_requestor_test.ts +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -232,5 +232,50 @@ describe('QuoteRequestor', async () => { expect(resp.sort()).to.eql([successfulQuote1, successfulQuote1].sort()); }); }); + it('should return successful RFQT indicative quote requests', async () => { + const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; + const apiKey = 'my-ko0l-api-key'; + + // Set up RFQT responses + // tslint:disable-next-line:array-type + const mockedRequests: MockedRfqtIndicativeQuoteResponse[] = []; + const expectedParams = { + sellToken: takerToken, + buyToken: makerToken, + buyAmount: '10000', + sellAmount: undefined, + takerAddress, + }; + // Successful response + const successfulQuote1 = { + makerAssetData, + takerAssetData, + makerAssetAmount: new BigNumber(expectedParams.buyAmount), + takerAssetAmount: new BigNumber(expectedParams.buyAmount), + }; + mockedRequests.push({ + endpoint: 'https://1337.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: successfulQuote1, + responseCode: StatusCodes.Success, + }); + + return rfqtMocker.withMockedRfqtIndicativeQuotes(mockedRequests, async () => { + const qr = new QuoteRequestor(['https://1337.0.0.1']); + const resp = await qr.requestRfqtIndicativeQuotesAsync( + makerAssetData, + takerAssetData, + new BigNumber(10000), + MarketOperation.Buy, + { + apiKey, + takerAddress, + intentOnFilling: true, + }, + ); + expect(resp.sort()).to.eql([successfulQuote1].sort()); + }); + }); }); }); From 245b6da5774a9fe1c72b33e0c410b332bb5b4212 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 22:26:17 -0400 Subject: [PATCH 12/18] asset-s: Require RfqtRequestOpts.intentOnFilling Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r411083497 --- packages/asset-swapper/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 27d695d7e9..e83e9de5ff 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -190,7 +190,7 @@ export interface SwapQuoteOrdersBreakdown { export interface RfqtRequestOpts { takerAddress: string; apiKey: string; - intentOnFilling?: boolean; + intentOnFilling: boolean; makerEndpointMaxResponseTimeMs?: number; } From ba2ac6a7b5b362dd0b62179aa44921c4ad7ada75 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 22:44:50 -0400 Subject: [PATCH 13/18] asset-s: Clarify indicative quote enablement Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r411498729 --- packages/asset-swapper/src/swap_quoter.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index e2c392a637..861ba428bf 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -566,13 +566,8 @@ export class SwapQuoter { let swapQuote: SwapQuote; const calcOpts: CalculateSwapQuoteOpts = opts; - if ( - // we should request indicative quotes: - calcOpts.rfqt && - !calcOpts.rfqt.intentOnFilling && - calcOpts.rfqt.apiKey && - this._rfqtTakerApiKeyWhitelist.includes(calcOpts.rfqt.apiKey) - ) { + + if (calcOpts.rfqt !== undefined && this._shouldEnableIndicativeRfqt(calcOpts.rfqt)) { calcOpts.rfqt.quoteRequestor = this._quoteRequestor; } @@ -594,5 +589,8 @@ export class SwapQuoter { return swapQuote; } + private _shouldEnableIndicativeRfqt(opts: CalculateSwapQuoteOpts['rfqt']): boolean { + return opts !== undefined && !opts.intentOnFilling && this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey); + } } // tslint:disable-next-line: max-file-line-count From 83289bc801013e1b7d2bc0b6f5696d886be90784 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Mon, 20 Apr 2020 22:50:11 -0400 Subject: [PATCH 14/18] asset-s: consider falsey takerAddress as absent Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r411543876 --- packages/asset-swapper/src/utils/quote_requestor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 63b6770b1a..7ee754e2e4 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -39,6 +39,7 @@ function assertTakerAddressOrThrow(takerAddress: string | undefined): void { takerAddress === undefined || takerAddress === '' || takerAddress === '0x' || + !takerAddress || takerAddress === constants.NULL_ADDRESS ) { throw new Error('RFQ-T requires the presence of a taker address'); From 2c97208e7455656866189a68cd0ff2cab3c64e74 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 21 Apr 2020 00:45:26 -0400 Subject: [PATCH 15/18] asset-s: simplify type with `?`, not `|undefined` Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2555#discussion_r411081841 --- packages/asset-swapper/src/utils/quote_requestor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 7ee754e2e4..0846dbb63c 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -51,7 +51,7 @@ function inferQueryParams( makerAssetData: string, takerAssetData: string, assetFillAmount: BigNumber, -): { buyToken: string; sellToken: string; buyAmount: string | undefined; sellAmount: string | undefined } { +): { buyToken: string; sellToken: string; buyAmount?: string; sellAmount?: string } { if (marketOperation === MarketOperation.Buy) { return { buyToken: getTokenAddressOrThrow(makerAssetData), From e8ff5da2097c5f97ea465c83b03abcf3f84dbf08 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 21 Apr 2020 01:49:28 -0400 Subject: [PATCH 16/18] asset-swapper: update CHANGELOG --- packages/asset-swapper/CHANGELOG.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index eb5086f07f..a3f04f5ea4 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -45,6 +45,10 @@ { "note": "Add support for RFQ-T, querying maker-hosted endpoints for quotes to be submitted by the taker", "pr": 2541 + }, + { + "note": "Add support for indicative (non-committal) quotes via RFQ-T", + "pr": 2555 } ] }, From 11622c586aa36ca952bdd0180747c7215a1f6972 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 22 Apr 2020 10:00:09 -0400 Subject: [PATCH 17/18] asset-s: Add RfqtRequestOpts.isIndicative --- packages/asset-swapper/src/swap_quoter.ts | 7 ++++++- packages/asset-swapper/src/types.ts | 1 + .../src/utils/market_operation_utils/index.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 861ba428bf..df3c9dd94c 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -590,7 +590,12 @@ export class SwapQuoter { return swapQuote; } private _shouldEnableIndicativeRfqt(opts: CalculateSwapQuoteOpts['rfqt']): boolean { - return opts !== undefined && !opts.intentOnFilling && this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey); + return ( + opts !== undefined && + opts.isIndicative !== undefined && + opts.isIndicative && + this._rfqtTakerApiKeyWhitelist.includes(opts.apiKey) + ); } } // tslint:disable-next-line: max-file-line-count diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index e83e9de5ff..599ca9255f 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -191,6 +191,7 @@ export interface RfqtRequestOpts { takerAddress: string; apiKey: string; intentOnFilling: boolean; + isIndicative?: boolean; makerEndpointMaxResponseTimeMs?: number; } 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 a2007a2e99..9756e27205 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -39,7 +39,7 @@ async function getRfqtIndicativeQuotesAsync( assetFillAmount: BigNumber, opts: Partial, ): Promise { - if (opts.rfqt && opts.rfqt.quoteRequestor) { + if (opts.rfqt && opts.rfqt.isIndicative === true && opts.rfqt.quoteRequestor) { return opts.rfqt.quoteRequestor.requestRfqtIndicativeQuotesAsync( makerAssetData, takerAssetData, From 153533f1d5cae5025a4a1e478ce12e226bedcc5c Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Tue, 21 Apr 2020 02:34:54 -0400 Subject: [PATCH 18/18] Fix bug in prior revision: wrong asset data I tried to get fancy back in 5effc6ec90e5a90402e0efb01cefd0b6a6a8ef1b. I changed something more than the single refactor targetted by the commit, and it broke things! This reverts part of that commit, restoring clean runs of 0x API tests. --- .../src/utils/market_operation_utils/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 9756e27205..6ae2affa10 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -114,8 +114,8 @@ export class MarketOperationUtils { ), ); const rfqtPromise = getRfqtIndicativeQuotesAsync( - makerToken, - takerToken, + nativeOrders[0].makerAssetData, + nativeOrders[0].takerAssetData, MarketOperation.Sell, takerAmount, _opts, @@ -194,8 +194,8 @@ export class MarketOperationUtils { ), ); const rfqtPromise = getRfqtIndicativeQuotesAsync( - makerToken, - takerToken, + nativeOrders[0].makerAssetData, + nativeOrders[0].takerAssetData, MarketOperation.Buy, makerAmount, _opts,