Skip to content

Commit

Permalink
Merge pull request #330 from Uniswap/add-filler-info
Browse files Browse the repository at this point in the history
feat: add fillerName and endpoint to quote response logs
  • Loading branch information
ConjunctiveNormalForm authored May 30, 2024
2 parents deca03f + b893633 commit 2de18fb
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 26 deletions.
46 changes: 41 additions & 5 deletions lib/entities/QuoteResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export interface QuoteResponseData
quoteId: string;
}

export interface QuoteMetadata {
endpoint: string;
fillerName: string;
}

type ValidationError = {
message: string | undefined;
value: { [key: string]: any };
Expand All @@ -25,11 +30,26 @@ interface ValidatedResponse {
validationError?: ValidationError;
}

interface FromRfqArgs {
request: QuoteRequestData;
data: RfqResponse;
type: TradeType;
metadata: QuoteMetadata;
}

interface FromRequestArgs {
request: QuoteRequestData;
amountQuoted: BigNumber;
metadata: QuoteMetadata;
filler?: string;
}

// data class for QuoteRequest helpers and conversions
export class QuoteResponse implements QuoteResponseData {
public createdAt: string;

public static fromRequest(request: QuoteRequestData, amountQuoted: BigNumber, filler?: string): QuoteResponse {
public static fromRequest(args: FromRequestArgs): QuoteResponse {
const { request, amountQuoted, metadata, filler } = args;
return new QuoteResponse(
{
chainId: request.tokenInChainId, // TODO: update schema
Expand All @@ -42,11 +62,13 @@ export class QuoteResponse implements QuoteResponseData {
filler: filler,
quoteId: request.quoteId ?? uuidv4(),
},
request.type
request.type,
metadata
);
}

public static fromRFQ(request: QuoteRequestData, data: RfqResponse, type: TradeType): ValidatedResponse {
public static fromRFQ(args: FromRfqArgs): ValidatedResponse {
const { request, data, type, metadata } = args;
let validationError: ValidationError | undefined;

const responseValidation = RfqResponseJoi.validate(data, {
Expand Down Expand Up @@ -87,13 +109,19 @@ export class QuoteResponse implements QuoteResponseData {
amountIn,
amountOut,
},
type
type,
metadata
),
...(validationError && { validationError }),
};
}

constructor(private data: QuoteResponseData, public type: TradeType, public createdAtMs = currentTimestampInMs()) {
constructor(
private data: QuoteResponseData,
public type: TradeType,
public metadata: QuoteMetadata,
public createdAtMs = currentTimestampInMs()
) {
this.createdAt = timestampInMstoSeconds(parseInt(this.createdAtMs));
}

Expand Down Expand Up @@ -163,4 +191,12 @@ export class QuoteResponse implements QuoteResponseData {
public get filler(): string | undefined {
return this.data.filler;
}

public get endpoint(): string {
return this.metadata.endpoint;
}

public get fillerName(): string {
return this.metadata.fillerName;
}
}
2 changes: 1 addition & 1 deletion lib/handlers/quote/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export async function getBestQuote(
return responses.reduce((bestQuote: QuoteResponse | null, quote: QuoteResponse) => {
log.info({
eventType: eventType,
body: { ...quote.toLog(), offerer: quote.swapper },
body: { ...quote.toLog(), offerer: quote.swapper, endpoint: quote.endpoint, fillerName: quote.fillerName },
});

if (
Expand Down
6 changes: 5 additions & 1 deletion lib/quoters/MockQuoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { Quoter, QuoterType } from '.';
import { QuoteRequest, QuoteResponse } from '../entities';

export const MOCK_FILLER_ADDRESS = '0x0000000000000000000000000000000000000001';
const METADATA = {
endpoint: 'https://uniswap.org',
fillerName: 'uniswap',
};

// mock quoter which simply returns a quote at a preconfigured exchange rate
export class MockQuoter implements Quoter {
Expand All @@ -21,7 +25,7 @@ export class MockQuoter implements Quoter {
this.log.info(
`MockQuoter: request ${request.requestId}: ${request.amount.toString()} -> ${amountQuoted.toString()}`
);
return [QuoteResponse.fromRequest(request, amountQuoted, MOCK_FILLER_ADDRESS)];
return [QuoteResponse.fromRequest({ request, amountQuoted, metadata: METADATA, filler: MOCK_FILLER_ADDRESS })];
}

public type(): QuoterType {
Expand Down
27 changes: 24 additions & 3 deletions lib/quoters/WebhookQuoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AnalyticsEventType,
Metric,
metricContext,
QuoteMetadata,
QuoteRequest,
QuoteResponse,
WebhookResponseType,
Expand Down Expand Up @@ -133,7 +134,17 @@ export class WebhookQuoter implements Quoter {
latencyMs: Date.now() - before,
};

const { response, validationError } = QuoteResponse.fromRFQ(request, hookResponse.data, request.type);
const metadata: QuoteMetadata = {
endpoint: endpoint,
fillerName: config.name,
};

const { response, validationError } = QuoteResponse.fromRFQ({
request,
data: hookResponse.data,
type: request.type,
metadata,
});

// RFQ provider explicitly elected not to quote
if (isNonQuote(request, hookResponse, response)) {
Expand Down Expand Up @@ -227,15 +238,25 @@ export class WebhookQuoter implements Quoter {
}
//if valid quote, log the opposing side as well
const opposingRequest = request.toOpposingRequest();
const opposingResponse = QuoteResponse.fromRFQ(opposingRequest, opposite.data, opposingRequest.type);
const opposingResponse = QuoteResponse.fromRFQ({
request: opposingRequest,
data: opposite.data,
type: opposingRequest.type,
metadata,
});
if (
opposingResponse &&
!isNonQuote(opposingRequest, opposite, opposingResponse.response) &&
!opposingResponse.validationError
) {
this.log.info({
eventType: 'QuoteResponse',
body: { ...opposingResponse.response.toLog(), offerer: opposingResponse.response.swapper },
body: {
...opposingResponse.response.toLog(),
offerer: opposingResponse.response.swapper,
endpoint: endpoint,
fillerName: config.name,
},
});
}

Expand Down
58 changes: 43 additions & 15 deletions test/entities/QuoteResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ const TOKEN_IN = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984';
const TOKEN_OUT = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const CHAIN_ID = 1;
const fixedTime = 4206969;
const WEBHOOK_URL = 'https://uniswap.org';
const METADATA = {
endpoint: WEBHOOK_URL,
fillerName: 'uniswap',
};

jest.spyOn(Date, 'now').mockImplementation(() => fixedTime);

describe('QuoteRequest', () => {
Expand All @@ -29,7 +35,8 @@ describe('QuoteRequest', () => {
tokenIn: TOKEN_IN,
tokenOut: TOKEN_OUT,
},
TradeType.EXACT_INPUT
TradeType.EXACT_INPUT,
METADATA
);
const quoteRequest = {
tokenInChainId: CHAIN_ID,
Expand All @@ -45,7 +52,11 @@ describe('QuoteRequest', () => {
};

it('fromRequest', async () => {
const response = QuoteResponse.fromRequest(quoteRequest, parseEther('1'));
const response = QuoteResponse.fromRequest({
request: quoteRequest,
amountQuoted: parseEther('1'),
metadata: METADATA,
});
expect(response.createdAt).toBe(quoteResponse.createdAt);
expect(response.amountIn).toEqual(quoteResponse.amountIn);
expect(response.amountOut).toEqual(quoteResponse.amountOut);
Expand All @@ -57,9 +68,9 @@ describe('QuoteRequest', () => {

describe('fromRFQ', () => {
it('fromRFQ with valid response', async () => {
const response = QuoteResponse.fromRFQ(
quoteRequest,
{
const response = QuoteResponse.fromRFQ({
request: quoteRequest,
data: {
chainId: CHAIN_ID,
requestId: REQUEST_ID,
tokenIn: TOKEN_IN,
Expand All @@ -68,16 +79,17 @@ describe('QuoteRequest', () => {
amountOut: parseEther('1').toString(),
quoteId: QUOTE_ID,
},
TradeType.EXACT_INPUT
);
type: TradeType.EXACT_INPUT,
metadata: METADATA,
});
expect(response.response).toEqual(quoteResponse);
expect(response.validationError).toBe(undefined);
});

it('fromRFQ with valid response - allow checksumed', async () => {
const response = QuoteResponse.fromRFQ(
quoteRequest,
{
const response = QuoteResponse.fromRFQ({
request: quoteRequest,
data: {
chainId: CHAIN_ID,
requestId: REQUEST_ID,
tokenIn: TOKEN_IN.toLowerCase(),
Expand All @@ -86,8 +98,9 @@ describe('QuoteRequest', () => {
amountOut: parseEther('1').toString(),
quoteId: QUOTE_ID,
},
TradeType.EXACT_INPUT
);
type: TradeType.EXACT_INPUT,
metadata: METADATA,
});
expect(response.validationError).toBe(undefined);
});

Expand All @@ -101,7 +114,12 @@ describe('QuoteRequest', () => {
amountOut: parseEther('1').toString(),
quoteId: QUOTE_ID,
};
const response = QuoteResponse.fromRFQ(quoteRequest, invalidResponse, TradeType.EXACT_INPUT);
const response = QuoteResponse.fromRFQ({
request: quoteRequest,
data: invalidResponse,
type: TradeType.EXACT_INPUT,
metadata: METADATA,
});
// ensure we overwrite amount with the request amount, dont just accept what the quoter returned
expect(response.response.amountIn).toEqual(quoteRequest.amount);
expect(response.validationError?.message).toBe('"amountIn" must be a string');
Expand All @@ -118,7 +136,12 @@ describe('QuoteRequest', () => {
amountOut: parseEther('1').toString(),
quoteId: QUOTE_ID,
};
const response = QuoteResponse.fromRFQ(quoteRequest, invalidResponse, TradeType.EXACT_INPUT);
const response = QuoteResponse.fromRFQ({
request: quoteRequest,
data: invalidResponse,
type: TradeType.EXACT_INPUT,
metadata: METADATA,
});
expect(response.response.tokenIn).toEqual('0x0000000000000000000000000000000000000000');
expect(response.validationError?.message).toBe(
'RFQ response token mismatch: request tokenIn: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 tokenOut: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 response tokenIn: 0x0000000000000000000000000000000000000000 tokenOut: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
Expand All @@ -136,7 +159,12 @@ describe('QuoteRequest', () => {
amountOut: parseEther('1').toString(),
quoteId: QUOTE_ID,
};
const response = QuoteResponse.fromRFQ(quoteRequest, invalidResponse, TradeType.EXACT_INPUT);
const response = QuoteResponse.fromRFQ({
request: quoteRequest,
data: invalidResponse,
type: TradeType.EXACT_INPUT,
metadata: METADATA,
});
expect(response.response.tokenOut).toEqual('0x0000000000000000000000000000000000000000');
expect(response.validationError?.message).toBe(
'RFQ response token mismatch: request tokenIn: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 tokenOut: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 response tokenIn: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 tokenOut: 0x0000000000000000000000000000000000000000'
Expand Down
3 changes: 2 additions & 1 deletion test/handlers/hard-quote/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ describe('Quote handler', () => {
},
data
),
type
type,
{ fillerName: 'mock', endpoint: 'mock' }
);
};

Expand Down
45 changes: 45 additions & 0 deletions test/providers/quoters/WebhookQuoter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,47 @@ describe('WebhookQuoter tests', () => {

expect(response.length).toEqual(1);
expect(response[0].toResponseJSON()).toEqual({ ...quote, quoteId: expect.any(String) });
expect(response[0].fillerName).toEqual('uniswap');
expect(response[0].endpoint).toEqual(WEBHOOK_URL);
});

it('adds filler metadata to response', 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,
},
});
})
.mockImplementationOnce((_endpoint, _req, _options) => {
return Promise.resolve({
data: quote,
});
})
.mockImplementationOnce((_endpoint, _req, _options) => {
return Promise.resolve({
data: {
...quote,
tokenIn: request.tokenOut,
tokenOut: request.tokenIn,
},
});
});
const response = await webhookQuoter.quote(request);
expect(response.length).toEqual(2);
console.log(JSON.stringify(response));
expect(['uniswap', 'searcher']).toContain(response[0].fillerName);
expect(['uniswap', 'searcher']).toContain(response[1].fillerName);
expect([WEBHOOK_URL, WEBHOOK_URL_SEARCHER]).toContain(response[0].endpoint);
expect([WEBHOOK_URL, WEBHOOK_URL_SEARCHER]).toContain(response[1].endpoint);
});

it('updates filler addresses', async () => {
Expand Down Expand Up @@ -623,6 +664,10 @@ describe('WebhookQuoter tests', () => {
amountOut: BigNumber.from(quote.amountOut),
amountIn: BigNumber.from(request.amount),
},
metadata: {
endpoint: WEBHOOK_URL,
fillerName: 'uniswap',
},
type: 0,
},
webhookUrl: WEBHOOK_URL,
Expand Down

0 comments on commit 2de18fb

Please sign in to comment.