Refactor integrator ID and add Prometheus metrics
This commit is contained in:
@@ -50,7 +50,7 @@ const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
|
||||
samplerGasLimit: 500e6,
|
||||
ethGasStationUrl: ETH_GAS_STATION_API_URL,
|
||||
rfqt: {
|
||||
takerApiKeyWhitelist: [],
|
||||
integratorsWhitelist: [],
|
||||
makerAssetOfferings: {},
|
||||
txOriginBlacklist: new Set(),
|
||||
},
|
||||
|
||||
@@ -75,6 +75,7 @@ export class SwapQuoter {
|
||||
private readonly _marketOperationUtils: MarketOperationUtils;
|
||||
private readonly _rfqtOptions?: SwapQuoterRfqOpts;
|
||||
private readonly _quoteRequestorHttpClient: AxiosInstance;
|
||||
private readonly _integratorIdsSet: Set<string>;
|
||||
|
||||
/**
|
||||
* Instantiates a new SwapQuoter instance
|
||||
@@ -164,6 +165,9 @@ export class SwapQuoter {
|
||||
httpsAgent: new HttpsAgent({ keepAlive: true, timeout: KEEP_ALIVE_TTL }),
|
||||
...(rfqt ? rfqt.axiosInstanceOpts : {}),
|
||||
});
|
||||
|
||||
const integratorIds = this._rfqtOptions?.integratorsWhitelist.map(integrator => integrator.integratorId) || [];
|
||||
this._integratorIdsSet = new Set(integratorIds);
|
||||
}
|
||||
|
||||
public async getBatchMarketBuySwapQuoteAsync(
|
||||
@@ -414,12 +418,11 @@ export class SwapQuoter {
|
||||
return isOpenOrder && !willOrderExpire && isFeeTypeAllowed;
|
||||
}; // tslint:disable-line:semicolon
|
||||
|
||||
private _isApiKeyWhitelisted(apiKey: string | undefined): boolean {
|
||||
private _isIntegrationKeyWhitelisted(apiKey: string | undefined): boolean {
|
||||
if (!apiKey) {
|
||||
return false;
|
||||
}
|
||||
const whitelistedApiKeys = this._rfqtOptions ? this._rfqtOptions.takerApiKeyWhitelist : [];
|
||||
return whitelistedApiKeys.includes(apiKey);
|
||||
return this._integratorIdsSet.has(apiKey);
|
||||
}
|
||||
|
||||
private _isTxOriginBlacklisted(txOrigin: string | undefined): boolean {
|
||||
@@ -438,7 +441,7 @@ export class SwapQuoter {
|
||||
return rfqt;
|
||||
}
|
||||
// tslint:disable-next-line: boolean-naming
|
||||
const { apiKey, nativeExclusivelyRFQ, intentOnFilling, txOrigin } = rfqt;
|
||||
const { integrator, nativeExclusivelyRFQ, intentOnFilling, txOrigin } = rfqt;
|
||||
// If RFQ-T is enabled and `nativeExclusivelyRFQ` is set, then `ERC20BridgeSource.Native` should
|
||||
// never be excluded.
|
||||
if (nativeExclusivelyRFQ === true && !sourceFilters.isAllowed(ERC20BridgeSource.Native)) {
|
||||
@@ -446,11 +449,11 @@ export class SwapQuoter {
|
||||
}
|
||||
|
||||
// If an API key was provided, but the key is not whitelisted, raise a warning and disable RFQ
|
||||
if (!this._isApiKeyWhitelisted(apiKey)) {
|
||||
if (!this._isIntegrationKeyWhitelisted(integrator.integratorId)) {
|
||||
if (this._rfqtOptions && this._rfqtOptions.warningLogger) {
|
||||
this._rfqtOptions.warningLogger(
|
||||
{
|
||||
apiKey,
|
||||
...integrator,
|
||||
},
|
||||
'Attempt at using an RFQ API key that is not whitelisted. Disabling RFQ for the request lifetime.',
|
||||
);
|
||||
@@ -474,7 +477,7 @@ export class SwapQuoter {
|
||||
// Otherwise check other RFQ options
|
||||
if (
|
||||
intentOnFilling && // The requestor is asking for a firm quote
|
||||
this._isApiKeyWhitelisted(apiKey) && // A valid API key was provided
|
||||
this._isIntegrationKeyWhitelisted(integrator.integratorId) && // A valid API key was provided
|
||||
sourceFilters.isAllowed(ERC20BridgeSource.Native) // Native liquidity is not excluded
|
||||
) {
|
||||
if (!txOrigin || txOrigin === constants.NULL_ADDRESS) {
|
||||
|
||||
@@ -243,7 +243,7 @@ export interface RfqmRequestOptions extends RfqRequestOpts {
|
||||
export interface RfqRequestOpts {
|
||||
takerAddress: string;
|
||||
txOrigin: string;
|
||||
apiKey: string;
|
||||
integrator: Integrator
|
||||
apiKeyWhitelist?: string[];
|
||||
intentOnFilling: boolean;
|
||||
isIndicative?: boolean;
|
||||
@@ -294,8 +294,13 @@ export interface RfqFirmQuoteValidator {
|
||||
getRfqtTakerFillableAmountsAsync(quotes: RfqOrder[]): Promise<BigNumber[]>;
|
||||
}
|
||||
|
||||
export interface Integrator {
|
||||
integratorId: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface SwapQuoterRfqOpts {
|
||||
takerApiKeyWhitelist: string[];
|
||||
integratorsWhitelist: Integrator[]
|
||||
makerAssetOfferings: RfqMakerAssetOfferings;
|
||||
txOriginBlacklist: Set<string>;
|
||||
altRfqCreds?: {
|
||||
|
||||
@@ -61,6 +61,20 @@ export interface MetricsProxy {
|
||||
* @param expirationTimeSeconds the expiration time in seconds
|
||||
*/
|
||||
incrementFillRatioWarningCounter(isLastLook: boolean, maker: string): void;
|
||||
|
||||
|
||||
logRfqMakerInteraction(interaction: {
|
||||
isLastLook: boolean,
|
||||
integrator: {
|
||||
integratorId: string;
|
||||
label: string;
|
||||
};
|
||||
url: string;
|
||||
quoteType: 'firm' | 'indicative';
|
||||
statusCode: number;
|
||||
latencyMs: number;
|
||||
included: boolean;
|
||||
}): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,18 +486,28 @@ export class QuoteRequestor {
|
||||
try {
|
||||
if (typedMakerUrl.pairType === RfqPairType.Standard) {
|
||||
const response = await this._quoteRequestorHttpClient.get(`${typedMakerUrl.url}/${quotePath}`, {
|
||||
headers: { '0x-api-key': options.apiKey },
|
||||
headers: { '0x-api-key': options.integrator.integratorId },
|
||||
params: requestParams,
|
||||
timeout: timeoutMs,
|
||||
cancelToken: cancelTokenSource.token,
|
||||
});
|
||||
const latencyMs = Date.now() - timeBeforeAwait;
|
||||
const {isLastLook, integrator} = options;
|
||||
this._metrics?.logRfqMakerInteraction({
|
||||
isLastLook: isLastLook || false,
|
||||
url: typedMakerUrl.url,
|
||||
quoteType,
|
||||
statusCode: response.status,
|
||||
latencyMs,
|
||||
included: true,
|
||||
integrator,
|
||||
});
|
||||
this._infoLogger({
|
||||
rfqtMakerInteraction: {
|
||||
...partialLogEntry,
|
||||
response: {
|
||||
included: true,
|
||||
apiKey: options.apiKey,
|
||||
apiKey: options.integrator.integratorId,
|
||||
takerAddress: requestParams.takerAddress,
|
||||
txOrigin: requestParams.txOrigin,
|
||||
statusCode: response.status,
|
||||
@@ -501,7 +525,7 @@ export class QuoteRequestor {
|
||||
typedMakerUrl.url,
|
||||
this._altRfqCreds.altRfqApiKey,
|
||||
this._altRfqCreds.altRfqProfile,
|
||||
options.apiKey,
|
||||
options.integrator.integratorId,
|
||||
quoteType === 'firm' ? AltQuoteModel.Firm : AltQuoteModel.Indicative,
|
||||
makerToken,
|
||||
takerToken,
|
||||
@@ -519,7 +543,7 @@ export class QuoteRequestor {
|
||||
...partialLogEntry,
|
||||
response: {
|
||||
included: true,
|
||||
apiKey: options.apiKey,
|
||||
apiKey: options.integrator.integratorId,
|
||||
takerAddress: requestParams.takerAddress,
|
||||
txOrigin: requestParams.txOrigin,
|
||||
statusCode: quote.status,
|
||||
@@ -538,7 +562,7 @@ export class QuoteRequestor {
|
||||
...partialLogEntry,
|
||||
response: {
|
||||
included: false,
|
||||
apiKey: options.apiKey,
|
||||
apiKey: options.integrator.integratorId,
|
||||
takerAddress: requestParams.takerAddress,
|
||||
txOrigin: requestParams.txOrigin,
|
||||
statusCode: err.response ? err.response.status : undefined,
|
||||
@@ -549,7 +573,7 @@ export class QuoteRequestor {
|
||||
rfqMakerBlacklist.logTimeoutOrLackThereof(typedMakerUrl.url, latencyMs >= timeoutMs);
|
||||
this._warningLogger(
|
||||
convertIfAxiosError(err),
|
||||
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${typedMakerUrl.url} for API key ${options.apiKey} for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`,
|
||||
`Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${typedMakerUrl.url} for API key ${options.integrator.integratorId} (${options.integrator.label}) for taker address ${options.takerAddress} and tx origin ${options.txOrigin}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as _ from 'lodash';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
|
||||
import { MarketOperation, QuoteRequestor, RfqRequestOpts, SignedNativeOrder } from '../src';
|
||||
import { NativeOrderWithFillableAmounts } from '../src/types';
|
||||
import { Integrator, NativeOrderWithFillableAmounts } from '../src/types';
|
||||
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
||||
import {
|
||||
BUY_SOURCE_FILTER_BY_CHAIN_ID,
|
||||
@@ -62,6 +62,10 @@ const SELL_SOURCES = SELL_SOURCE_FILTER_BY_CHAIN_ID[ChainId.Mainnet].sources;
|
||||
const TOKEN_ADJACENCY_GRAPH: TokenAdjacencyGraph = { default: [] };
|
||||
|
||||
const SIGNATURE = { v: 1, r: NULL_BYTES, s: NULL_BYTES, signatureType: SignatureType.EthSign };
|
||||
const FOO_INTEGRATOR: Integrator = {
|
||||
integratorId: 'foo',
|
||||
label: 'foo',
|
||||
};
|
||||
|
||||
/**
|
||||
* gets the orders required for a market sell operation by (potentially) merging native orders with
|
||||
@@ -745,7 +749,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
feeSchedule,
|
||||
rfqt: {
|
||||
isIndicative: false,
|
||||
apiKey: 'foo',
|
||||
integrator: FOO_INTEGRATOR,
|
||||
takerAddress: randomAddress(),
|
||||
txOrigin: randomAddress(),
|
||||
intentOnFilling: true,
|
||||
@@ -790,7 +794,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
...DEFAULT_OPTS,
|
||||
rfqt: {
|
||||
isIndicative: false,
|
||||
apiKey: 'foo',
|
||||
integrator: FOO_INTEGRATOR,
|
||||
takerAddress: randomAddress(),
|
||||
intentOnFilling: true,
|
||||
txOrigin: randomAddress(),
|
||||
@@ -837,7 +841,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
...DEFAULT_OPTS,
|
||||
rfqt: {
|
||||
isIndicative: true,
|
||||
apiKey: 'foo',
|
||||
integrator: FOO_INTEGRATOR,
|
||||
takerAddress: randomAddress(),
|
||||
txOrigin: randomAddress(),
|
||||
intentOnFilling: true,
|
||||
@@ -896,7 +900,10 @@ describe('MarketOperationUtils tests', () => {
|
||||
...DEFAULT_OPTS,
|
||||
rfqt: {
|
||||
isIndicative: false,
|
||||
apiKey: 'foo',
|
||||
integrator: {
|
||||
integratorId: 'foo',
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress: randomAddress(),
|
||||
intentOnFilling: true,
|
||||
txOrigin: randomAddress(),
|
||||
@@ -954,7 +961,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
...DEFAULT_OPTS,
|
||||
rfqt: {
|
||||
isIndicative: false,
|
||||
apiKey: 'foo',
|
||||
integrator: FOO_INTEGRATOR,
|
||||
takerAddress: randomAddress(),
|
||||
txOrigin: randomAddress(),
|
||||
intentOnFilling: true,
|
||||
|
||||
@@ -240,7 +240,10 @@ describe('QuoteRequestor', async () => {
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
@@ -435,7 +438,10 @@ describe('QuoteRequestor', async () => {
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
@@ -551,7 +557,10 @@ describe('QuoteRequestor', async () => {
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
@@ -675,7 +684,10 @@ describe('QuoteRequestor', async () => {
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
@@ -762,7 +774,10 @@ describe('QuoteRequestor', async () => {
|
||||
MarketOperation.Sell,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
@@ -823,7 +838,10 @@ describe('QuoteRequestor', async () => {
|
||||
MarketOperation.Buy,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin: takerAddress,
|
||||
intentOnFilling: true,
|
||||
@@ -1088,7 +1106,10 @@ describe('QuoteRequestor', async () => {
|
||||
altScenario.requestedOperation,
|
||||
undefined,
|
||||
{
|
||||
apiKey,
|
||||
integrator: {
|
||||
integratorId: apiKey,
|
||||
label: 'foo',
|
||||
},
|
||||
takerAddress,
|
||||
txOrigin,
|
||||
intentOnFilling: true,
|
||||
|
||||
Reference in New Issue
Block a user