asset-swapper: Punish latent RFQT makers
This commit is contained in:
@@ -11,6 +11,7 @@ import { constants } from '../constants';
|
|||||||
import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types';
|
import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types';
|
||||||
|
|
||||||
import { ONE_SECOND_MS } from './market_operation_utils/constants';
|
import { ONE_SECOND_MS } from './market_operation_utils/constants';
|
||||||
|
import { RfqMakerBlacklist } from './rfq_maker_blacklist';
|
||||||
|
|
||||||
// tslint:disable-next-line: custom-no-magic-numbers
|
// tslint:disable-next-line: custom-no-magic-numbers
|
||||||
const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS;
|
const KEEP_ALIVE_TTL = 5 * 60 * ONE_SECOND_MS;
|
||||||
@@ -20,6 +21,10 @@ export const quoteRequestorHttpClient: AxiosInstance = Axios.create({
|
|||||||
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
|
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MAKER_TIMEOUT_STREAK_LENGTH = 10;
|
||||||
|
const MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES = 10;
|
||||||
|
const rfqMakerBlacklist = new RfqMakerBlacklist(MAKER_TIMEOUT_STREAK_LENGTH, MAKER_TIMEOUT_BLACKLIST_DURATION_MINUTES);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request quotes from RFQ-T providers
|
* Request quotes from RFQ-T providers
|
||||||
*/
|
*/
|
||||||
@@ -334,7 +339,10 @@ export class QuoteRequestor {
|
|||||||
const result: Array<{ response: ResponseT; makerUri: string }> = [];
|
const result: Array<{ response: ResponseT; makerUri: string }> = [];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(this._rfqtAssetOfferings).map(async url => {
|
Object.keys(this._rfqtAssetOfferings).map(async url => {
|
||||||
if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) {
|
if (
|
||||||
|
this._makerSupportsPair(url, makerAssetData, takerAssetData) &&
|
||||||
|
!rfqMakerBlacklist.isMakerBlacklisted(url)
|
||||||
|
) {
|
||||||
const requestParamsWithBigNumbers = {
|
const requestParamsWithBigNumbers = {
|
||||||
takerAddress: options.takerAddress,
|
takerAddress: options.takerAddress,
|
||||||
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
|
...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount),
|
||||||
@@ -354,6 +362,10 @@ export class QuoteRequestor {
|
|||||||
|
|
||||||
const partialLogEntry = { url, quoteType, requestParams };
|
const partialLogEntry = { url, quoteType, requestParams };
|
||||||
const timeBeforeAwait = Date.now();
|
const timeBeforeAwait = Date.now();
|
||||||
|
const maxResponseTimeMs =
|
||||||
|
options.makerEndpointMaxResponseTimeMs === undefined
|
||||||
|
? constants.DEFAULT_RFQT_REQUEST_OPTS.makerEndpointMaxResponseTimeMs!
|
||||||
|
: options.makerEndpointMaxResponseTimeMs;
|
||||||
try {
|
try {
|
||||||
const quotePath = (() => {
|
const quotePath = (() => {
|
||||||
switch (quoteType) {
|
switch (quoteType) {
|
||||||
@@ -368,8 +380,9 @@ export class QuoteRequestor {
|
|||||||
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
|
const response = await quoteRequestorHttpClient.get<ResponseT>(`${url}/${quotePath}`, {
|
||||||
headers: { '0x-api-key': options.apiKey },
|
headers: { '0x-api-key': options.apiKey },
|
||||||
params: requestParams,
|
params: requestParams,
|
||||||
timeout: options.makerEndpointMaxResponseTimeMs,
|
timeout: maxResponseTimeMs,
|
||||||
});
|
});
|
||||||
|
const latencyMs = Date.now() - timeBeforeAwait;
|
||||||
this._infoLogger({
|
this._infoLogger({
|
||||||
rfqtMakerInteraction: {
|
rfqtMakerInteraction: {
|
||||||
...partialLogEntry,
|
...partialLogEntry,
|
||||||
@@ -378,12 +391,14 @@ export class QuoteRequestor {
|
|||||||
apiKey: options.apiKey,
|
apiKey: options.apiKey,
|
||||||
takerAddress: requestParams.takerAddress,
|
takerAddress: requestParams.takerAddress,
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
latencyMs: Date.now() - timeBeforeAwait,
|
latencyMs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs);
|
||||||
result.push({ response: response.data, makerUri: url });
|
result.push({ response: response.data, makerUri: url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const latencyMs = Date.now() - timeBeforeAwait;
|
||||||
this._infoLogger({
|
this._infoLogger({
|
||||||
rfqtMakerInteraction: {
|
rfqtMakerInteraction: {
|
||||||
...partialLogEntry,
|
...partialLogEntry,
|
||||||
@@ -392,10 +407,11 @@ export class QuoteRequestor {
|
|||||||
apiKey: options.apiKey,
|
apiKey: options.apiKey,
|
||||||
takerAddress: requestParams.takerAddress,
|
takerAddress: requestParams.takerAddress,
|
||||||
statusCode: err.response ? err.response.status : undefined,
|
statusCode: err.response ? err.response.status : undefined,
|
||||||
latencyMs: Date.now() - timeBeforeAwait,
|
latencyMs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs);
|
||||||
this._warningLogger(
|
this._warningLogger(
|
||||||
convertIfAxiosError(err),
|
convertIfAxiosError(err),
|
||||||
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${
|
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${
|
||||||
|
38
packages/asset-swapper/src/utils/rfq_maker_blacklist.ts
Normal file
38
packages/asset-swapper/src/utils/rfq_maker_blacklist.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Tracks a maker's history of timely responses, and manages whether a given
|
||||||
|
* maker should be avoided for being too latent.
|
||||||
|
*/
|
||||||
|
export class RfqMakerBlacklist {
|
||||||
|
// tslint:disable-next-line:custom-no-magic-numbers
|
||||||
|
private static readonly _msPerMinute = 1000 * 60;
|
||||||
|
private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {};
|
||||||
|
private readonly _makerBlacklistedUntilDate: { [makerUrl: string]: number } = {};
|
||||||
|
constructor(private readonly _blacklistDurationMinutes: number, private readonly _timeoutStreakThreshold: number) {}
|
||||||
|
public logTimeoutOrLackThereof(makerUrl: string, didTimeout: boolean): void {
|
||||||
|
if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) {
|
||||||
|
this._makerTimeoutStreakLength[makerUrl] = 0;
|
||||||
|
}
|
||||||
|
if (didTimeout) {
|
||||||
|
this._makerTimeoutStreakLength[makerUrl] += 1;
|
||||||
|
if (this._makerTimeoutStreakLength[makerUrl] > this._timeoutStreakThreshold) {
|
||||||
|
this._makerBlacklistedUntilDate[makerUrl] =
|
||||||
|
Date.now() + this._blacklistDurationMinutes * RfqMakerBlacklist._msPerMinute;
|
||||||
|
this._makerTimeoutStreakLength[makerUrl] = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._makerTimeoutStreakLength[makerUrl] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public isMakerBlacklisted(makerUrl: string): boolean {
|
||||||
|
if (this._makerBlacklistedUntilDate.hasOwnProperty(makerUrl)) {
|
||||||
|
if (this._makerBlacklistedUntilDate[makerUrl] > Date.now()) {
|
||||||
|
delete this._makerBlacklistedUntilDate[makerUrl];
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user