Merge pull request #2581 from 0xProject/fix/asset-swapper/rfqt-logging

asset-swapper: Improve logging for 0x API consumption
This commit is contained in:
F. Eugene Aumson 2020-05-11 19:13:07 -04:00 committed by GitHub
commit d9e13d6b99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 97 deletions

View File

@ -41,6 +41,7 @@ export {
ForwarderExtensionContractOpts,
GetExtensionContractTypeOpts,
LiquidityForTakerMakerAssetDataPair,
LogFunction,
MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote,

View File

@ -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;
};
}

View File

@ -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<undefined | AxiosResponse<SignedOrder>> = 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<SignedOrder>(`${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<SignedOrder>( // not yet BigNumber
makerAssetData,
takerAssetData,
assetFillAmount,
marketOperation,
_opts,
'firm',
);
const responses = responsesIfDefined.filter(
(respIfDefd): respIfDefd is AxiosResponse<SignedOrder> => 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<RfqtIndicativeQuoteResponse>
> = 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<RfqtIndicativeQuoteResponse>(`${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<RfqtIndicativeQuoteResponse>( // not yet BigNumber
makerAssetData,
takerAssetData,
assetFillAmount,
marketOperation,
_opts,
'indicative',
);
const axiosResponses = axiosResponsesIfDefined.filter(
(respIfDefd): respIfDefd is AxiosResponse<RfqtIndicativeQuoteResponse> => 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<ResponseT>(
makerAssetData: string,
takerAssetData: string,
assetFillAmount: BigNumber,
marketOperation: MarketOperation,
options: RfqtRequestOpts,
quoteType: 'firm' | 'indicative',
): Promise<ResponseT[]> {
// create an array of promises for quote responses, using "undefined"
// as a placeholder for failed requests.
const responsesIfDefined: Array<undefined | AxiosResponse<ResponseT>> = 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<ResponseT>(`${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<ResponseT> => respIfDefd !== undefined,
);
return responses.map(response => response.data);
}
}