diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index 2f335809f7..8bd3940f0a 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -46,7 +46,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = { samplerGasLimit: 250e6, rfqt: { takerApiKeyWhitelist: [], - makerEndpoints: [], + makerAssetOfferings: {}, }, }; diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 148ea65924..f6c4477c1d 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -45,6 +45,7 @@ export { MarketOperation, MarketSellSwapQuote, MockedRfqtFirmQuoteResponse, + RfqtMakerAssetOfferings, RfqtRequestOpts, SwapQuote, SwapQuoteConsumerBase, diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index ceb1cd58a8..82a2bd5d0f 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -171,7 +171,7 @@ export class SwapQuoter { this._protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS); this._orderStateUtils = new OrderStateUtils(this._devUtilsContract); this._quoteRequestor = new QuoteRequestor( - options.rfqt ? options.rfqt.makerEndpoints || [] : [], + options.rfqt ? options.rfqt.makerAssetOfferings || {} : {}, options.rfqt ? options.rfqt.warningLogger : undefined, options.rfqt ? options.rfqt.infoLogger : undefined, ); diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index f43a28fb8d..eb060daf54 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -208,6 +208,14 @@ export interface SwapQuoteRequestOpts extends CalculateSwapQuoteOpts { */ export interface CalculateSwapQuoteOpts extends GetMarketOrdersOpts {} +/** + * A mapping from RFQ-T quote provider URLs to the trading pairs they support. + * The value type represents an array of supported asset pairs, with each array element encoded as a 2-element array of token addresses. + */ +export interface RfqtMakerAssetOfferings { + [endpoint: string]: Array<[string, string]>; +} + /** * chainId: The ethereum chain id. Defaults to 1 (mainnet). * orderRefreshIntervalMs: The interval in ms that getBuyQuoteAsync should trigger an refresh of orders and order states. Defaults to 10000ms (10s). @@ -224,7 +232,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { liquidityProviderRegistryAddress?: string; rfqt?: { takerApiKeyWhitelist: string[]; - makerEndpoints: string[]; + makerAssetOfferings: RfqtMakerAssetOfferings; warningLogger?: (s: string) => void; infoLogger?: (s: string) => void; }; diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 756e1e4d02..5c5caac326 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, RfqtRequestOpts } from '../types'; +import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; /** * Request quotes from RFQ-T providers @@ -85,7 +85,7 @@ export class QuoteRequestor { private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); constructor( - private readonly _rfqtMakerEndpoints: string[], + private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, private readonly _warningLogger: (s: string) => void = s => logUtils.warn(s), private readonly _infoLogger: (s: string) => void = () => { return; }, ) {} @@ -104,25 +104,28 @@ export class QuoteRequestor { // as a placeholder for failed requests. const timeBeforeAwait = Date.now(); const responsesIfDefined: Array> = await Promise.all( - this._rfqtMakerEndpoints.map(async rfqtMakerEndpoint => { - try { - return await Axios.get(`${rfqtMakerEndpoint}/quote`, { - headers: { '0x-api-key': _opts.apiKey }, - params: { - takerAddress: _opts.takerAddress, - ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), - }, - timeout: _opts.makerEndpointMaxResponseTimeMs, - }); - } catch (err) { - this._warningLogger( - `Failed to get RFQ-T firm quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ - _opts.apiKey - } for taker address ${_opts.takerAddress}`, - ); - this._warningLogger(err); - return undefined; + Object.keys(this._rfqtAssetOfferings).map(async url => { + if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { + try { + return await Axios.get(`${url}/quote`, { + headers: { '0x-api-key': _opts.apiKey }, + params: { + takerAddress: _opts.takerAddress, + ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), + }, + timeout: _opts.makerEndpointMaxResponseTimeMs, + }); + } catch (err) { + this._warningLogger( + `Failed to get RFQ-T firm quote from market maker endpoint ${url} for API key ${ + _opts.apiKey + } for taker address ${_opts.takerAddress}`, + ); + this._warningLogger(err); + return undefined; + } } + return undefined; }), ); this._infoLogger(JSON.stringify({ aggregatedRfqtLatencyMs: Date.now() - timeBeforeAwait })); @@ -184,25 +187,28 @@ export class QuoteRequestor { 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) { - this._warningLogger( - `Failed to get RFQ-T indicative quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${ - options.apiKey - } for taker address ${options.takerAddress}`, - ); - this._warningLogger(err); - return undefined; + Object.keys(this._rfqtAssetOfferings).map(async url => { + if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { + try { + return await Axios.get(`${url}/price`, { + headers: { '0x-api-key': options.apiKey }, + params: { + takerAddress: options.takerAddress, + ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), + }, + timeout: options.makerEndpointMaxResponseTimeMs, + }); + } catch (err) { + this._warningLogger( + `Failed to get RFQ-T indicative quote from market maker endpoint ${url} for API key ${ + options.apiKey + } for taker address ${options.takerAddress}`, + ); + this._warningLogger(err); + return undefined; + } } + return undefined; }), ); this._infoLogger(JSON.stringify({ aggregatedRfqtLatencyMs: Date.now() - timeBeforeAwait })); @@ -269,4 +275,18 @@ export class QuoteRequestor { } return false; } + + private _makerSupportsPair(makerUrl: string, makerAssetData: string, takerAssetData: string): boolean { + const makerTokenAddress = getTokenAddressOrThrow(makerAssetData); + const takerTokenAddress = getTokenAddressOrThrow(takerAssetData); + for (const assetPair of this._rfqtAssetOfferings[makerUrl]) { + if ( + (assetPair[0] === makerTokenAddress && assetPair[1] === takerTokenAddress) || + (assetPair[0] === takerTokenAddress && assetPair[1] === makerTokenAddress) + ) { + return true; + } + } + return false; + } } diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts index 13c598322e..0d4998e41a 100644 --- a/packages/asset-swapper/test/quote_requestor_test.ts +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -109,6 +109,17 @@ describe('QuoteRequestor', async () => { responseCode: StatusCodes.Success, }); + // Shouldn't ping an RFQ-T provider when they don't support the requested asset pair. + // (see how QuoteRequestor constructor parameters below don't list + // any supported asset pairs for this maker.) + mockedRequests.push({ + endpoint: 'https://426.0.0.1', + requestApiKey: apiKey, + requestParams: expectedParams, + responseData: successfulOrder1, + responseCode: StatusCodes.Success, + }); + // Another Successful response const successfulOrder2 = testOrderFactory.generateTestSignedOrder({ makerAssetData, takerAssetData }); mockedRequests.push({ @@ -120,15 +131,16 @@ describe('QuoteRequestor', async () => { }); return rfqtMocker.withMockedRfqtFirmQuotes(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 qr = new QuoteRequestor({ + 'https://1337.0.0.1': [[makerToken, takerToken]], + 'https://420.0.0.1': [[makerToken, takerToken]], + 'https://421.0.0.1': [[makerToken, takerToken]], + 'https://422.0.0.1': [[makerToken, takerToken]], + 'https://423.0.0.1': [[makerToken, takerToken]], + 'https://424.0.0.1': [[makerToken, takerToken]], + 'https://426.0.0.1': [], + 'https://37.0.0.1': [[makerToken, takerToken]], + }); const resp = await qr.requestRfqtFirmQuotesAsync( makerAssetData, takerAssetData, @@ -216,15 +228,15 @@ describe('QuoteRequestor', async () => { }); 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 qr = new QuoteRequestor({ + 'https://1337.0.0.1': [[makerToken, takerToken]], + 'https://420.0.0.1': [[makerToken, takerToken]], + 'https://421.0.0.1': [[makerToken, takerToken]], + 'https://422.0.0.1': [[makerToken, takerToken]], + 'https://423.0.0.1': [[makerToken, takerToken]], + 'https://424.0.0.1': [[makerToken, takerToken]], + 'https://37.0.0.1': [[makerToken, takerToken]], + }); const resp = await qr.requestRfqtIndicativeQuotesAsync( makerAssetData, takerAssetData, @@ -270,7 +282,7 @@ describe('QuoteRequestor', async () => { }); return rfqtMocker.withMockedRfqtIndicativeQuotes(mockedRequests, async () => { - const qr = new QuoteRequestor(['https://1337.0.0.1']); + const qr = new QuoteRequestor({ 'https://1337.0.0.1': [[makerToken, takerToken]] }); const resp = await qr.requestRfqtIndicativeQuotesAsync( makerAssetData, takerAssetData,