Skip to content

Commit

Permalink
Merge pull request #302 from Uniswap/v2-config
Browse files Browse the repository at this point in the history
feat(WebhookConfig): add SupportedProtocol config
  • Loading branch information
ConjunctiveNormalForm authored Apr 5, 2024
2 parents 9bcbc2a + 888fae3 commit edbe5fa
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 18 deletions.
10 changes: 9 additions & 1 deletion lib/handlers/hard-quote/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Joi from 'joi';
import { v4 as uuidv4 } from 'uuid';

import { HardQuoteRequest, HardQuoteResponse, Metric, QuoteResponse } from '../../entities';
import { ProtocolVersion } from '../../providers';
import { NoQuotesAvailable, OrderPostError, UnknownOrderCosignerError } from '../../util/errors';
import { timestampInMstoSeconds } from '../../util/time';
import { APIGLambdaHandler } from '../base';
Expand Down Expand Up @@ -70,7 +71,14 @@ export class QuoteHandler extends APIGLambdaHandler<
},
});

const bestQuote = await getBestQuote(quoters, request.toQuoteRequest(), log, metric, 'HardResponse');
const bestQuote = await getBestQuote(
quoters,
request.toQuoteRequest(),
log,
metric,
ProtocolVersion.V2,
'HardResponse'
);
if (!bestQuote && !requestBody.allowNoQuote) {
if (!requestBody.allowNoQuote) {
throw new NoQuotesAvailable();
Expand Down
6 changes: 5 additions & 1 deletion lib/handlers/quote/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Logger from 'bunyan';
import Joi from 'joi';

import { Metric, QuoteRequest, QuoteResponse } from '../../entities';
import { ProtocolVersion } from '../../providers';
import { Quoter } from '../../quoters';
import { NoQuotesAvailable } from '../../util/errors';
import { timestampInMstoSeconds } from '../../util/time';
Expand Down Expand Up @@ -86,9 +87,12 @@ export async function getBestQuote(
quoteRequest: QuoteRequest,
log: Logger,
metric: IMetric,
protocolVersion: ProtocolVersion = ProtocolVersion.V1,
eventType: EventType = 'QuoteResponse'
): Promise<QuoteResponse | null> {
const responses: QuoteResponse[] = (await Promise.all(quoters.map((q) => q.quote(quoteRequest)))).flat();
const responses: QuoteResponse[] = (
await Promise.all(quoters.map((q) => q.quote(quoteRequest, protocolVersion)))
).flat();
switch (responses.length) {
case 0:
metric.putMetric(Metric.RFQ_COUNT_0, 1, MetricLoggerUnit.Count);
Expand Down
6 changes: 6 additions & 0 deletions lib/providers/webhook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ type WebhookOverrides = {
timeout: number;
};

export enum ProtocolVersion {
V1 = 'v1',
V2 = 'v2',
}

export interface WebhookConfiguration {
name: string;
hash: string;
Expand All @@ -15,6 +20,7 @@ export interface WebhookConfiguration {
// if null, send for all chains
chainIds?: number[];
addresses?: string[];
supportedVersions?: ProtocolVersion[];
}

export interface WebhookConfigurationProvider {
Expand Down
7 changes: 6 additions & 1 deletion lib/providers/webhook/mock.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { WebhookConfiguration, WebhookConfigurationProvider } from '.';
import { ProtocolVersion, WebhookConfiguration, WebhookConfigurationProvider } from '.';

export class MockWebhookConfigurationProvider implements WebhookConfigurationProvider {
constructor(private endpoints: WebhookConfiguration[]) {}

async getEndpoints(): Promise<WebhookConfiguration[]> {
return this.endpoints;
}

async getFillerSupportedProtocols(endpoint: string): Promise<ProtocolVersion[]> {
const config = this.endpoints.find((e) => e.endpoint === endpoint);
return config?.supportedVersions ?? [ProtocolVersion.V1];
}
}
20 changes: 19 additions & 1 deletion lib/providers/webhook/s3.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { default as Logger } from 'bunyan';

import { ProtocolVersion, WebhookConfiguration, WebhookConfigurationProvider } from '.';
import { checkDefined } from '../../preconditions/preconditions';
import { WebhookConfiguration, WebhookConfigurationProvider } from '.';

export type FillerAddressesMap = Map<string, Set<string>>;

Expand Down Expand Up @@ -62,4 +62,22 @@ export class S3WebhookConfigurationProvider implements WebhookConfigurationProvi
this.endpoints = JSON.parse(await s3Body.transformToString()) as WebhookConfiguration[];
this.log.info({ endpoints: this.endpoints }, `Fetched ${this.endpoints.length} endpoints from S3`);
}

/*
* Returns the supported protocol versions for the filler at the given endpoint.
* @param endpoint - The endpoint to check the supported protocol versions for.
* @returns List of endpoint's supported protocols; defaults to v1 only
*
*/
async getFillerSupportedProtocols(endpoint: string): Promise<ProtocolVersion[]> {
if (
this.endpoints.length === 0 ||
Date.now() - this.lastUpdatedEndpointsTimestamp > S3WebhookConfigurationProvider.UPDATE_ENDPOINTS_PERIOD_MS
) {
await this.fetchEndpoints();
this.lastUpdatedEndpointsTimestamp = Date.now();
}
const config = this.endpoints.find((e) => e.endpoint === endpoint);
return config?.supportedVersions ?? [ProtocolVersion.V1];
}
}
2 changes: 1 addition & 1 deletion lib/quoters/MockQuoter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Logger from 'bunyan';
import { BigNumber } from 'ethers';

import { QuoteRequest, QuoteResponse } from '../entities';
import { Quoter, QuoterType } from '.';
import { QuoteRequest, QuoteResponse } from '../entities';

export const MOCK_FILLER_ADDRESS = '0x0000000000000000000000000000000000000001';

Expand Down
31 changes: 23 additions & 8 deletions lib/quoters/WebhookQuoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
QuoteResponse,
WebhookResponseType,
} from '../entities';
import { WebhookConfiguration, WebhookConfigurationProvider } from '../providers';
import { ProtocolVersion, WebhookConfiguration, WebhookConfigurationProvider } from '../providers';
import { FirehoseLogger } from '../providers/analytics';
import { CircuitBreakerConfigurationProvider } from '../providers/circuit-breaker';
import { FillerComplianceConfigurationProvider } from '../providers/compliance';
Expand All @@ -41,14 +41,14 @@ export class WebhookQuoter implements Quoter {
this.ALLOW_LIST = _allow_list;
}

public async quote(request: QuoteRequest): Promise<QuoteResponse[]> {
const endpoints = await this.getEligibleEndpoints();
public async quote(request: QuoteRequest, version: ProtocolVersion = ProtocolVersion.V1): Promise<QuoteResponse[]> {
let endpoints = await this.getEligibleEndpoints();
const endpointToAddrsMap = await this.complianceProvider.getEndpointToExcludedAddrsMap();
endpoints.filter((e) => {
return (
endpointToAddrsMap.get(e.endpoint) === undefined || !endpointToAddrsMap.get(e.endpoint)?.has(request.swapper)
);
});
endpoints = endpoints.filter(
(e) =>
passFillerCompliance(e, endpointToAddrsMap, request.swapper) &&
getEndpointSupportedProtocols(e).includes(version)
);

this.log.info({ endpoints }, `Fetching quotes from ${endpoints.length} endpoints`);
const quotes = await Promise.all(endpoints.map((e) => this.fetchQuote(e, request)));
Expand Down Expand Up @@ -312,3 +312,18 @@ function isNonQuote(request: QuoteRequest, hookResponse: AxiosResponse, parsedRe

return false;
}

export function getEndpointSupportedProtocols(e: WebhookConfiguration) {
if (!e.supportedVersions || e.supportedVersions.length == 0) {
return [ProtocolVersion.V1];
}
return e.supportedVersions;
}

export function passFillerCompliance(
e: WebhookConfiguration,
endpointToAddrsMap: Map<string, Set<string>>,
swapper: string
) {
return endpointToAddrsMap.get(e.endpoint) === undefined || !endpointToAddrsMap.get(e.endpoint)?.has(swapper);
}
3 changes: 2 additions & 1 deletion lib/quoters/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { QuoteRequest, QuoteResponse } from '../entities';
import { ProtocolVersion } from '../providers';

export enum QuoterType {
TEST = 'TEST',
Expand All @@ -7,7 +8,7 @@ export enum QuoterType {
}

export interface Quoter {
quote(request: QuoteRequest): Promise<QuoteResponse[]>;
quote(request: QuoteRequest, version: ProtocolVersion): Promise<QuoteResponse[]>;
type(): QuoterType;
}

Expand Down
116 changes: 112 additions & 4 deletions test/providers/quoters/WebhookQuoter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import axios from 'axios';
import { BigNumber, ethers } from 'ethers';

import { AnalyticsEventType, QuoteRequest, WebhookResponseType } from '../../../lib/entities';
import { MockWebhookConfigurationProvider } from '../../../lib/providers';
import { MockWebhookConfigurationProvider, ProtocolVersion } from '../../../lib/providers';
import { FirehoseLogger } from '../../../lib/providers/analytics';
import { MockCircuitBreakerConfigurationProvider } from '../../../lib/providers/circuit-breaker/mock';
import { MockFillerComplianceConfigurationProvider } from '../../../lib/providers/compliance';
Expand Down Expand Up @@ -156,9 +156,117 @@ describe('WebhookQuoter tests', () => {
{ quoteId: expect.any(String), ...request.toCleanJSON() },
{ headers: {}, timeout: 500 }
);
expect(mockedAxios.post).not.toBeCalledWith(WEBHOOK_URL_ONEINCH, request.toCleanJSON(), {
headers: {},
timeout: 500,
expect(mockedAxios.post).not.toBeCalledWith(
WEBHOOK_URL_ONEINCH,
{
quoteId: expect.any(String),
...request.toCleanJSON(),
},
{
headers: {},
timeout: 500,
}
);
});

describe('Supported protocols tests', () => {
const webhookProvider = new MockWebhookConfigurationProvider([
{ name: 'uniswap', endpoint: WEBHOOK_URL, headers: {}, hash: '0xuni', supportedVersions: [ProtocolVersion.V2] },
{ name: '1inch', endpoint: WEBHOOK_URL_ONEINCH, headers: {}, hash: '0x1inch' },
{
name: 'searcher',
endpoint: WEBHOOK_URL_SEARCHER,
headers: {},
hash: '0xsearcher',
supportedVersions: [ProtocolVersion.V1, ProtocolVersion.V2],
},
]);
const circuitBreakerProvider = new MockCircuitBreakerConfigurationProvider([
{ hash: '0xuni', fadeRate: 0.1, enabled: true },
{ hash: '0x1inch', fadeRate: 0.1, enabled: true },
{ hash: '0xsearcher', fadeRate: 0.1, enabled: true },
]);
const webhookQuoter = new WebhookQuoter(
logger,
mockFirehoseLogger,
webhookProvider,
circuitBreakerProvider,
emptyMockComplianceProvider
);
it('v1 quote request only sent to fillers supporting v1', async () => {
mockedAxios.post
.mockImplementationOnce((_endpoint, _req, _options) => {
return Promise.resolve({
data: quote,
});
})
.mockImplementationOnce((_endpoint, _req, _options) => {
return Promise.resolve({
data: {
...quote,
tokenIn: request.tokenOut,
tokenOut: request.tokenIn,
},
});
});

await webhookQuoter.quote(request, ProtocolVersion.V1);
expect(mockedAxios.post).toBeCalledWith(
WEBHOOK_URL_ONEINCH,
{ quoteId: expect.any(String), ...request.toCleanJSON() },
{ headers: {}, timeout: 500 }
);
expect(mockedAxios.post).toBeCalledWith(
WEBHOOK_URL_SEARCHER,
{ quoteId: expect.any(String), ...request.toCleanJSON() },
{ headers: {}, timeout: 500 }
);
expect(mockedAxios.post).not.toBeCalledWith(WEBHOOK_URL, request.toCleanJSON(), {
headers: {},
timeout: 500,
});
});

it('v2 quote request only sent to fillers supporting v2', async () => {
mockedAxios.post
.mockImplementationOnce((_endpoint, _req, _options) => {
return Promise.resolve({
data: quote,
});
})
.mockImplementationOnce((_endpoint, _req, _options) => {
return Promise.resolve({
data: {
...quote,
tokenIn: request.tokenOut,
tokenOut: request.tokenIn,
},
});
});

await webhookQuoter.quote(request, ProtocolVersion.V2);
expect(mockedAxios.post).toBeCalledWith(
WEBHOOK_URL,
{ quoteId: expect.any(String), ...request.toCleanJSON() },
{ headers: {}, timeout: 500 }
);
expect(mockedAxios.post).toBeCalledWith(
WEBHOOK_URL_SEARCHER,
{ quoteId: expect.any(String), ...request.toCleanJSON() },
{
headers: {},
timeout: 500,
}
);
// empty config defaults to v1 only
expect(mockedAxios.post).not.toBeCalledWith(
WEBHOOK_URL_ONEINCH,
{ quoteId: expect.any(String), ...request.toCleanJSON() },
{
headers: {},
timeout: 500,
}
);
});
});

Expand Down

0 comments on commit edbe5fa

Please sign in to comment.