diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index f6c4477c1d..187f8df134 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -41,6 +41,7 @@ export { ForwarderExtensionContractOpts, GetExtensionContractTypeOpts, LiquidityForTakerMakerAssetDataPair, + LogFunction, MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote, diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index db3f993eec..f08de9d704 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -3,6 +3,7 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import { GetMarketOrdersOpts } from './utils/market_operation_utils/types'; +import { LogFunction } from './utils/quote_requestor'; /** * expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m). @@ -216,6 +217,8 @@ export interface RfqtMakerAssetOfferings { [endpoint: string]: Array<[string, string]>; } +export { LogFunction } from './utils/quote_requestor'; + /** * 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). @@ -234,8 +237,8 @@ export interface SwapQuoterOpts extends OrderPrunerOpts { takerApiKeyWhitelist: string[]; makerAssetOfferings: RfqtMakerAssetOfferings; skipBuyRequests?: boolean; - warningLogger?: (s: string) => void; - infoLogger?: (s: string) => void; + warningLogger?: LogFunction; + infoLogger?: LogFunction; }; } diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 6eedca4a0a..d1ccbc588d 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -80,13 +80,25 @@ function hasExpectedAssetData( return hasExpectedMakerAssetData && hasExpectedTakerAssetData; } +function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has AxiosError.toJSON() returning object */ { + if (error.hasOwnProperty('isAxiosError') && error.isAxiosError && error.hasOwnProperty('toJSON')) { + return error.toJSON(); + } else { + return error; + } +} + +export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; + export class QuoteRequestor { private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); constructor( private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, - private readonly _warningLogger: (a: any) => void = a => logUtils.warn(a), - private readonly _infoLogger: (a: any) => void = () => undefined, + private readonly _warningLogger: LogFunction = (obj, msg) => + logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), + private readonly _infoLogger: LogFunction = (obj, msg) => + logUtils.log(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs, ) {} @@ -100,52 +112,19 @@ export class QuoteRequestor { const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; assertTakerAddressOrThrow(_opts.takerAddress); - // create an array of promises for quote responses, using "undefined" - // as a placeholder for failed requests. - const responsesIfDefined: Array> = await Promise.all( - Object.keys(this._rfqtAssetOfferings).map(async url => { - if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { - try { - const timeBeforeAwait = Date.now(); - const response = await Axios.get(`${url}/quote`, { - headers: { '0x-api-key': _opts.apiKey }, - params: { - takerAddress: _opts.takerAddress, - ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), - }, - timeout: _opts.makerEndpointMaxResponseTimeMs, - }); - this._infoLogger({ - rfqtFirmQuoteMakerResponseTime: { - makerEndpoint: url, - responseTimeMs: Date.now() - timeBeforeAwait, - }, - }); - return response; - } 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; - }), + const ordersWithStringInts = await this._getQuotesAsync( // not yet BigNumber + makerAssetData, + takerAssetData, + assetFillAmount, + marketOperation, + _opts, + 'firm', ); - const responses = responsesIfDefined.filter( - (respIfDefd): respIfDefd is AxiosResponse => respIfDefd !== undefined, - ); - - const ordersWithStringInts = responses.map(response => response.data); // not yet BigNumber - const validatedOrdersWithStringInts = ordersWithStringInts.filter(order => { const hasValidSchema = this._schemaValidator.isValid(order, schemas.signedOrderSchema); if (!hasValidSchema) { - this._warningLogger(`Invalid RFQ-t order received, filtering out: ${JSON.stringify(order)}`); + this._warningLogger(order, 'Invalid RFQ-t order received, filtering out'); return false; } @@ -157,12 +136,12 @@ export class QuoteRequestor { order.takerAssetData.toLowerCase(), ) ) { - this._warningLogger(`Unexpected asset data in RFQ-T order, filtering out: ${JSON.stringify(order)}`); + this._warningLogger(order, 'Unexpected asset data in RFQ-T order, filtering out'); return false; } if (order.takerAddress.toLowerCase() !== _opts.takerAddress.toLowerCase()) { - this._warningLogger(`Unexpected takerAddress in RFQ-T order, filtering out: ${JSON.stringify(order)}`); + this._warningLogger(order, 'Unexpected takerAddress in RFQ-T order, filtering out'); return false; } @@ -183,7 +162,7 @@ export class QuoteRequestor { const orders = validatedOrders.filter(order => { if (orderCalculationUtils.willOrderExpire(order, this._expiryBufferMs / constants.ONE_SECOND_MS)) { - this._warningLogger(`Expiry too soon in RFQ-T order, filtering out: ${JSON.stringify(order)}`); + this._warningLogger(order, 'Expiry too soon in RFQ-T order, filtering out'); return false; } return true; @@ -202,61 +181,24 @@ export class QuoteRequestor { const _opts: RfqtRequestOpts = { ...constants.DEFAULT_RFQT_REQUEST_OPTS, ...options }; assertTakerAddressOrThrow(_opts.takerAddress); - const axiosResponsesIfDefined: Array< - undefined | AxiosResponse - > = await Promise.all( - Object.keys(this._rfqtAssetOfferings).map(async url => { - if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { - try { - const timeBeforeAwait = Date.now(); - const response = await Axios.get(`${url}/price`, { - headers: { '0x-api-key': options.apiKey }, - params: { - takerAddress: options.takerAddress, - ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), - }, - timeout: options.makerEndpointMaxResponseTimeMs, - }); - this._infoLogger({ - rfqtIndicativeQuoteMakerResponseTime: { - makerEndpoint: url, - responseTimeMs: Date.now() - timeBeforeAwait, - }, - }); - return response; - } 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; - }), + const responsesWithStringInts = await this._getQuotesAsync( // not yet BigNumber + makerAssetData, + takerAssetData, + assetFillAmount, + marketOperation, + _opts, + 'indicative', ); - 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)) { - this._warningLogger( - `Invalid RFQ-T indicative quote received, filtering out: ${JSON.stringify(response)}`, - ); + this._warningLogger(response, 'Invalid RFQ-T indicative quote received, filtering out'); return false; } if ( !hasExpectedAssetData(makerAssetData, takerAssetData, response.makerAssetData, response.takerAssetData) ) { - this._warningLogger( - `Unexpected asset data in RFQ-T indicative quote, filtering out: ${JSON.stringify(response)}`, - ); + this._warningLogger(response, 'Unexpected asset data in RFQ-T indicative quote, filtering out'); return false; } return true; @@ -273,9 +215,7 @@ export class QuoteRequestor { const responses = validResponses.filter(response => { if (this._isExpirationTooSoon(response.expirationTimeSeconds)) { - this._warningLogger( - `Expiry too soon in RFQ-T indicative quote, filtering out: ${JSON.stringify(response)}`, - ); + this._warningLogger(response, 'Expiry too soon in RFQ-T indicative quote, filtering out'); return false; } return true; @@ -331,4 +271,81 @@ export class QuoteRequestor { const currentTimeMs = new BigNumber(Date.now()); return expirationTimeMs.isLessThan(currentTimeMs.plus(this._expiryBufferMs)); } + + private async _getQuotesAsync( + makerAssetData: string, + takerAssetData: string, + assetFillAmount: BigNumber, + marketOperation: MarketOperation, + options: RfqtRequestOpts, + quoteType: 'firm' | 'indicative', + ): Promise { + // create an array of promises for quote responses, using "undefined" + // as a placeholder for failed requests. + const responsesIfDefined: Array> = await Promise.all( + Object.keys(this._rfqtAssetOfferings).map(async url => { + if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { + const requestParams = { + takerAddress: options.takerAddress, + ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), + }; + const partialLogEntry = { url, quoteType, requestParams }; + const timeBeforeAwait = Date.now(); + try { + const quotePath = (() => { + switch (quoteType) { + case 'firm': + return 'quote'; + break; + case 'indicative': + return 'price'; + break; + default: + throw new Error(`Unexpected quote type ${quoteType}`); + } + })(); + const response = await Axios.get(`${url}/${quotePath}`, { + headers: { '0x-api-key': options.apiKey }, + params: requestParams, + timeout: options.makerEndpointMaxResponseTimeMs, + }); + this._infoLogger({ + rfqtMakerInteraction: { + ...partialLogEntry, + response: { + statusCode: response.status, + latencyMs: Date.now() - timeBeforeAwait, + }, + }, + }); + return response; + } catch (err) { + this._infoLogger({ + rfqtMakerInteraction: { + ...partialLogEntry, + response: { + statusCode: err.code, + latencyMs: Date.now() - timeBeforeAwait, + }, + }, + }); + this._warningLogger( + convertIfAxiosError(err), + `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ + options.apiKey + } for taker address ${options.takerAddress}`, + ); + return undefined; + } + } + return undefined; + }), + ); + + const responses = responsesIfDefined.filter( + (respIfDefd): respIfDefd is AxiosResponse => respIfDefd !== undefined, + ); + + return responses.map(response => response.data); + } }