Merge pull request #2706 from 0xProject/feat/asset-swapper/punish-latent-rfqt-makers
asset-swapper: Punish latent RFQT makers
This commit is contained in:
commit
32d11d1ba5
@ -20,6 +20,8 @@ const NULL_ERC20_ASSET_DATA = '0xf47261b0000000000000000000000000000000000000000
|
|||||||
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
|
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||||
const MAINNET_CHAIN_ID = 1;
|
const MAINNET_CHAIN_ID = 1;
|
||||||
const ONE_SECOND_MS = 1000;
|
const ONE_SECOND_MS = 1000;
|
||||||
|
const ONE_MINUTE_SECS = 60;
|
||||||
|
const ONE_MINUTE_MS = ONE_SECOND_MS * ONE_MINUTE_SECS;
|
||||||
const DEFAULT_PER_PAGE = 1000;
|
const DEFAULT_PER_PAGE = 1000;
|
||||||
const ZERO_AMOUNT = new BigNumber(0);
|
const ZERO_AMOUNT = new BigNumber(0);
|
||||||
|
|
||||||
@ -97,6 +99,7 @@ export const constants = {
|
|||||||
ETHER_TOKEN_DECIMALS: 18,
|
ETHER_TOKEN_DECIMALS: 18,
|
||||||
ONE_AMOUNT: new BigNumber(1),
|
ONE_AMOUNT: new BigNumber(1),
|
||||||
ONE_SECOND_MS,
|
ONE_SECOND_MS,
|
||||||
|
ONE_MINUTE_MS,
|
||||||
DEFAULT_SWAP_QUOTER_OPTS,
|
DEFAULT_SWAP_QUOTER_OPTS,
|
||||||
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS,
|
||||||
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS,
|
||||||
|
@ -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 ${
|
||||||
|
33
packages/asset-swapper/src/utils/rfq_maker_blacklist.ts
Normal file
33
packages/asset-swapper/src/utils/rfq_maker_blacklist.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Tracks a maker's history of timely responses, and manages whether a given
|
||||||
|
* maker should be avoided for being too latent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { constants } from '../constants';
|
||||||
|
|
||||||
|
export class RfqMakerBlacklist {
|
||||||
|
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 * constants.ONE_MINUTE_MS;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._makerTimeoutStreakLength[makerUrl] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public isMakerBlacklisted(makerUrl: string): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now > this._makerBlacklistedUntilDate[makerUrl]) {
|
||||||
|
delete this._makerBlacklistedUntilDate[makerUrl];
|
||||||
|
}
|
||||||
|
return this._makerBlacklistedUntilDate[makerUrl] > now;
|
||||||
|
}
|
||||||
|
}
|
40
packages/asset-swapper/test/rfq_maker_blacklist_test.ts
Normal file
40
packages/asset-swapper/test/rfq_maker_blacklist_test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import * as chai from 'chai';
|
||||||
|
import 'mocha';
|
||||||
|
|
||||||
|
import { constants } from '../src/constants';
|
||||||
|
import { RfqMakerBlacklist } from '../src/utils/rfq_maker_blacklist';
|
||||||
|
|
||||||
|
import { chaiSetup } from './utils/chai_setup';
|
||||||
|
|
||||||
|
chaiSetup.configure();
|
||||||
|
const expect = chai.expect;
|
||||||
|
|
||||||
|
describe('RfqMakerBlacklist', () => {
|
||||||
|
it('does blacklist', async () => {
|
||||||
|
const blacklistDurationMinutes = 1;
|
||||||
|
const timeoutStreakThreshold = 3;
|
||||||
|
const blacklist = new RfqMakerBlacklist(blacklistDurationMinutes, timeoutStreakThreshold);
|
||||||
|
blacklist.logTimeoutOrLackThereof('makerA', true);
|
||||||
|
blacklist.logTimeoutOrLackThereof('makerA', true);
|
||||||
|
expect(blacklist.isMakerBlacklisted('makerA')).to.be.false();
|
||||||
|
blacklist.logTimeoutOrLackThereof('makerA', true);
|
||||||
|
const sleepTimeMs = 10;
|
||||||
|
await new Promise<void>(r => {
|
||||||
|
setTimeout(r, sleepTimeMs);
|
||||||
|
});
|
||||||
|
expect(blacklist.isMakerBlacklisted('makerA')).to.be.true();
|
||||||
|
});
|
||||||
|
it('does unblacklist', async () => {
|
||||||
|
const blacklistDurationMinutes = 0.1;
|
||||||
|
const timeoutStreakThreshold = 3;
|
||||||
|
const blacklist = new RfqMakerBlacklist(blacklistDurationMinutes, timeoutStreakThreshold);
|
||||||
|
blacklist.logTimeoutOrLackThereof('makerA', true);
|
||||||
|
blacklist.logTimeoutOrLackThereof('makerA', true);
|
||||||
|
blacklist.logTimeoutOrLackThereof('makerA', true);
|
||||||
|
expect(blacklist.isMakerBlacklisted('makerA')).to.be.true();
|
||||||
|
await new Promise<void>(r => {
|
||||||
|
setTimeout(r, blacklistDurationMinutes * constants.ONE_MINUTE_MS);
|
||||||
|
});
|
||||||
|
expect(blacklist.isMakerBlacklisted('makerA')).to.be.false();
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user