From df83abafa9e739aa7ec3105d7b31e106cdae1eac Mon Sep 17 00:00:00 2001 From: Mark Toda Date: Wed, 17 Apr 2024 12:07:04 -0400 Subject: [PATCH] feat: make IQuoteRequest interface Have HardQuoteRequest implement interface instead of squanch into a QuoteRequest. should make it easier to vary the logic for each in the future --- lib/entities/HardQuoteRequest.ts | 58 ++++++++++++++++++------ lib/entities/QuoteRequest.ts | 20 +++++--- lib/handlers/hard-quote/handler.ts | 2 +- lib/handlers/hard-quote/index.ts | 2 +- lib/handlers/quote/handler.ts | 4 +- lib/handlers/quote/injector.ts | 2 +- lib/providers/circuit-breaker/s3.ts | 2 +- lib/providers/compliance/s3.ts | 2 +- lib/providers/webhook/s3.ts | 2 +- lib/quoters/MockQuoter.ts | 2 +- lib/quoters/WebhookQuoter.ts | 10 ++-- lib/quoters/index.ts | 4 +- test/handlers/hard-quote/handler.test.ts | 2 +- 13 files changed, 76 insertions(+), 36 deletions(-) diff --git a/lib/entities/HardQuoteRequest.ts b/lib/entities/HardQuoteRequest.ts index 28f53661..bc1b30e3 100644 --- a/lib/entities/HardQuoteRequest.ts +++ b/lib/entities/HardQuoteRequest.ts @@ -1,13 +1,14 @@ import { TradeType } from '@uniswap/sdk-core'; -import { UnsignedV2DutchOrder } from '@uniswap/uniswapx-sdk'; +import { UnsignedV2DutchOrder, V2DutchOrderBuilder } from '@uniswap/uniswapx-sdk'; import { BigNumber, ethers, utils } from 'ethers'; import { v4 as uuidv4 } from 'uuid'; -import { QuoteRequest, QuoteRequestDataJSON } from '.'; import { HardQuoteRequestBody } from '../handlers/hard-quote'; import { ProtocolVersion } from '../providers'; +import { ExternalRFQDataJSON, IQuoteRequest } from '.'; -export class HardQuoteRequest { +export class HardQuoteRequest implements IQuoteRequest { + public protocol = ProtocolVersion.V2; public order: UnsignedV2DutchOrder; private data: HardQuoteRequestBody; @@ -23,12 +24,12 @@ export class HardQuoteRequest { this.order = UnsignedV2DutchOrder.parse(_data.encodedInnerOrder, _data.tokenInChainId); } - public toCleanJSON(): QuoteRequestDataJSON { + public toJSON(): ExternalRFQDataJSON { return { tokenInChainId: this.tokenInChainId, tokenOutChainId: this.tokenOutChainId, - swapper: ethers.constants.AddressZero, requestId: this.requestId, + swapper: this.swapper, tokenIn: this.tokenIn, tokenOut: this.tokenOut, amount: this.amount.toString(), @@ -39,9 +40,16 @@ export class HardQuoteRequest { }; } + public toCleanJSON(): ExternalRFQDataJSON { + return { + ...this.toJSON(), + swapper: ethers.constants.AddressZero, + }; + } + // return an opposing quote request, // i.e. quoting the other side of the trade - public toOpposingCleanJSON(): QuoteRequestDataJSON { + public toOpposingCleanJSON(): ExternalRFQDataJSON { const type = this.type === TradeType.EXACT_INPUT ? TradeType.EXACT_OUTPUT : TradeType.EXACT_INPUT; return { ...this.toCleanJSON(), @@ -54,13 +62,24 @@ export class HardQuoteRequest { }; } - // transforms into a quote request that can be used to query quoters - public toQuoteRequest(): QuoteRequest { - return new QuoteRequest({ - ...this.toCleanJSON(), - swapper: this.swapper, - amount: this.amount, - type: this.type, + public toOpposingRequest(): IQuoteRequest { + const oppositeOrder = V2DutchOrderBuilder.fromOrder(this.order) + .input({ + startAmount: this.totalOutputAmountStart, + endAmount: this.totalOutputAmountEnd, + token: this.tokenOut, + }) + .output({ + startAmount: this.totalInputAmountStart, + endAmount: this.totalInputAmountEnd, + token: this.tokenIn, + recipient: this.swapper, + }) + .buildPartial(); + + return new HardQuoteRequest({ + ...this.data, + encodedInnerOrder: oppositeOrder.serialize(), }); } @@ -97,10 +116,23 @@ export class HardQuoteRequest { return amount; } + public get totalOutputAmountEnd(): BigNumber { + let amount = BigNumber.from(0); + for (const output of this.order.info.outputs) { + amount = amount.add(output.endAmount); + } + + return amount; + } + public get totalInputAmountStart(): BigNumber { return this.order.info.input.startAmount; } + public get totalInputAmountEnd(): BigNumber { + return this.order.info.input.endAmount; + } + public get amount(): BigNumber { if (this.type === TradeType.EXACT_INPUT) { return this.totalInputAmountStart; diff --git a/lib/entities/QuoteRequest.ts b/lib/entities/QuoteRequest.ts index 37e9881d..3f8c9ad1 100644 --- a/lib/entities/QuoteRequest.ts +++ b/lib/entities/QuoteRequest.ts @@ -19,13 +19,21 @@ export interface QuoteRequestData { quoteId?: string; } -export interface QuoteRequestDataJSON extends Omit { +// the data sent to external RFQ providers +export interface ExternalRFQDataJSON extends Omit { amount: string; type: string; } +export interface IQuoteRequest extends QuoteRequestData { + toJSON(): ExternalRFQDataJSON; + toCleanJSON(): ExternalRFQDataJSON; + toOpposingCleanJSON(): ExternalRFQDataJSON; + toOpposingRequest(): IQuoteRequest; +} + // data class for QuoteRequest helpers and conversions -export class QuoteRequest { +export class QuoteRequest implements IQuoteRequest { public static fromRequestBody(body: PostQuoteRequestBody): QuoteRequest { return new QuoteRequest({ tokenInChainId: body.tokenInChainId, @@ -43,7 +51,7 @@ export class QuoteRequest { constructor(private data: QuoteRequestData) {} - public toJSON(): QuoteRequestDataJSON { + public toJSON(): ExternalRFQDataJSON { return { tokenInChainId: this.tokenInChainId, tokenOutChainId: this.tokenOutChainId, @@ -59,7 +67,7 @@ export class QuoteRequest { }; } - public toCleanJSON(): QuoteRequestDataJSON { + public toCleanJSON(): ExternalRFQDataJSON { return { tokenInChainId: this.tokenInChainId, tokenOutChainId: this.tokenOutChainId, @@ -77,7 +85,7 @@ export class QuoteRequest { // return an opposing quote request, // i.e. quoting the other side of the trade - public toOpposingCleanJSON(): QuoteRequestDataJSON { + public toOpposingCleanJSON(): ExternalRFQDataJSON { const type = this.type === TradeType.EXACT_INPUT ? TradeType.EXACT_OUTPUT : TradeType.EXACT_INPUT; return { tokenInChainId: this.tokenOutChainId, @@ -96,7 +104,7 @@ export class QuoteRequest { }; } - public toOpposingRequest(): QuoteRequest { + public toOpposingRequest(): IQuoteRequest { const opposingJSON = this.toOpposingCleanJSON(); return new QuoteRequest({ ...opposingJSON, diff --git a/lib/handlers/hard-quote/handler.ts b/lib/handlers/hard-quote/handler.ts index 9a51c36e..a68ae8ba 100644 --- a/lib/handlers/hard-quote/handler.ts +++ b/lib/handlers/hard-quote/handler.ts @@ -70,7 +70,7 @@ export class QuoteHandler extends APIGLambdaHandler< }, }); - const bestQuote = await getBestQuote(quoters, request.toQuoteRequest(), log, metric, 'HardResponse'); + const bestQuote = await getBestQuote(quoters, request, log, metric, 'HardResponse'); if (!bestQuote && !requestBody.allowNoQuote) { if (!requestBody.allowNoQuote) { throw new NoQuotesAvailable(); diff --git a/lib/handlers/hard-quote/index.ts b/lib/handlers/hard-quote/index.ts index 73612078..a60e1b13 100644 --- a/lib/handlers/hard-quote/index.ts +++ b/lib/handlers/hard-quote/index.ts @@ -1,3 +1,3 @@ export { QuoteHandler as HardQuoteHandler } from './handler'; -export { RequestInjected, ContainerInjected, QuoteInjector as HardQuoteInjector } from './injector'; +export { ContainerInjected, QuoteInjector as HardQuoteInjector, RequestInjected } from './injector'; export * from './schema'; diff --git a/lib/handlers/quote/handler.ts b/lib/handlers/quote/handler.ts index d1233d9a..7d12a972 100644 --- a/lib/handlers/quote/handler.ts +++ b/lib/handlers/quote/handler.ts @@ -3,7 +3,7 @@ import { IMetric, MetricLoggerUnit } from '@uniswap/smart-order-router'; import Logger from 'bunyan'; import Joi from 'joi'; -import { Metric, QuoteRequest, QuoteResponse } from '../../entities'; +import { Metric, IQuoteRequest, QuoteRequest, QuoteResponse } from '../../entities'; import { Quoter } from '../../quoters'; import { NoQuotesAvailable } from '../../util/errors'; import { timestampInMstoSeconds } from '../../util/time'; @@ -83,7 +83,7 @@ export class QuoteHandler extends APIGLambdaHandler< // fetch quotes from all quoters and return the best one export async function getBestQuote( quoters: Quoter[], - quoteRequest: QuoteRequest, + quoteRequest: IQuoteRequest, log: Logger, metric: IMetric, eventType: EventType = 'QuoteResponse' diff --git a/lib/handlers/quote/injector.ts b/lib/handlers/quote/injector.ts index ccb9ed99..adeb5669 100644 --- a/lib/handlers/quote/injector.ts +++ b/lib/handlers/quote/injector.ts @@ -12,8 +12,8 @@ import { FADE_RATE_BUCKET, FADE_RATE_S3_KEY, INTEGRATION_S3_KEY, - PRODUCTION_S3_KEY, PROD_COMPLIANCE_S3_KEY, + PRODUCTION_S3_KEY, WEBHOOK_CONFIG_BUCKET, } from '../../constants'; import { diff --git a/lib/providers/circuit-breaker/s3.ts b/lib/providers/circuit-breaker/s3.ts index 9c133ed0..1f4ed381 100644 --- a/lib/providers/circuit-breaker/s3.ts +++ b/lib/providers/circuit-breaker/s3.ts @@ -3,9 +3,9 @@ import { NodeHttpHandler } from '@smithy/node-http-handler'; import { MetricsLogger, Unit } from 'aws-embedded-metrics'; import Logger from 'bunyan'; -import { CircuitBreakerConfiguration, CircuitBreakerConfigurationProvider } from '.'; import { Metric } from '../../entities'; import { checkDefined } from '../../preconditions/preconditions'; +import { CircuitBreakerConfiguration, CircuitBreakerConfigurationProvider } from '.'; export class S3CircuitBreakerConfigurationProvider implements CircuitBreakerConfigurationProvider { private log: Logger; diff --git a/lib/providers/compliance/s3.ts b/lib/providers/compliance/s3.ts index 98a868c5..994f319d 100644 --- a/lib/providers/compliance/s3.ts +++ b/lib/providers/compliance/s3.ts @@ -1,8 +1,8 @@ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { default as Logger } from 'bunyan'; -import { FillerComplianceConfiguration, FillerComplianceConfigurationProvider } from '.'; import { checkDefined } from '../../preconditions/preconditions'; +import { FillerComplianceConfiguration, FillerComplianceConfigurationProvider } from '.'; export class S3FillerComplianceConfigurationProvider implements FillerComplianceConfigurationProvider { private log: Logger; diff --git a/lib/providers/webhook/s3.ts b/lib/providers/webhook/s3.ts index 053be138..030513a9 100644 --- a/lib/providers/webhook/s3.ts +++ b/lib/providers/webhook/s3.ts @@ -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 { ProtocolVersion, WebhookConfiguration, WebhookConfigurationProvider } from '.'; export type FillerAddressesMap = Map>; diff --git a/lib/quoters/MockQuoter.ts b/lib/quoters/MockQuoter.ts index 82d9ec4b..e5c91752 100644 --- a/lib/quoters/MockQuoter.ts +++ b/lib/quoters/MockQuoter.ts @@ -1,8 +1,8 @@ import Logger from 'bunyan'; import { BigNumber } from 'ethers'; -import { Quoter, QuoterType } from '.'; import { QuoteRequest, QuoteResponse } from '../entities'; +import { Quoter, QuoterType } from '.'; export const MOCK_FILLER_ADDRESS = '0x0000000000000000000000000000000000000001'; diff --git a/lib/quoters/WebhookQuoter.ts b/lib/quoters/WebhookQuoter.ts index 728663b4..468afe2e 100644 --- a/lib/quoters/WebhookQuoter.ts +++ b/lib/quoters/WebhookQuoter.ts @@ -4,13 +4,12 @@ import axios, { AxiosError, AxiosResponse } from 'axios'; import Logger from 'bunyan'; import { v4 as uuidv4 } from 'uuid'; -import { Quoter, QuoterType } from '.'; import { AnalyticsEvent, AnalyticsEventType, + IQuoteRequest, Metric, metricContext, - QuoteRequest, QuoteResponse, WebhookResponseType, } from '../entities'; @@ -20,6 +19,7 @@ import { CircuitBreakerConfigurationProvider } from '../providers/circuit-breake import { FillerComplianceConfigurationProvider } from '../providers/compliance'; import { FillerAddressRepository } from '../repositories/filler-address-repository'; import { timestampInMstoISOString } from '../util/time'; +import { Quoter, QuoterType } from '.'; // TODO: shorten, maybe take from env config const WEBHOOK_TIMEOUT_MS = 500; @@ -43,7 +43,7 @@ export class WebhookQuoter implements Quoter { this.ALLOW_LIST = _allow_list; } - public async quote(request: QuoteRequest): Promise { + public async quote(request: IQuoteRequest): Promise { let endpoints = await this.getEligibleEndpoints(); const endpointToAddrsMap = await this.complianceProvider.getEndpointToExcludedAddrsMap(); endpoints = endpoints.filter( @@ -92,7 +92,7 @@ export class WebhookQuoter implements Quoter { } } - private async fetchQuote(config: WebhookConfiguration, request: QuoteRequest): Promise { + private async fetchQuote(config: WebhookConfiguration, request: IQuoteRequest): Promise { const { name, endpoint, headers } = config; if (config.chainIds !== undefined && !config.chainIds.includes(request.tokenInChainId)) { this.log.debug( @@ -313,7 +313,7 @@ export class WebhookQuoter implements Quoter { // valid non-quote responses: // - 404 // - 0 amount quote -function isNonQuote(request: QuoteRequest, hookResponse: AxiosResponse, parsedResponse: QuoteResponse): boolean { +function isNonQuote(request: IQuoteRequest, hookResponse: AxiosResponse, parsedResponse: QuoteResponse): boolean { if (hookResponse.status === 404) { return true; } diff --git a/lib/quoters/index.ts b/lib/quoters/index.ts index c534828b..3c4e1600 100644 --- a/lib/quoters/index.ts +++ b/lib/quoters/index.ts @@ -1,4 +1,4 @@ -import { QuoteRequest, QuoteResponse } from '../entities'; +import { IQuoteRequest, QuoteResponse } from '../entities'; export enum QuoterType { TEST = 'TEST', @@ -7,7 +7,7 @@ export enum QuoterType { } export interface Quoter { - quote(request: QuoteRequest): Promise; + quote(request: IQuoteRequest): Promise; type(): QuoterType; } diff --git a/test/handlers/hard-quote/handler.test.ts b/test/handlers/hard-quote/handler.test.ts index c5b0bf95..3d1b1a94 100644 --- a/test/handlers/hard-quote/handler.test.ts +++ b/test/handlers/hard-quote/handler.test.ts @@ -18,7 +18,7 @@ import { } from '../../../lib/handlers/hard-quote'; import { getCosignerData } from '../../../lib/handlers/hard-quote/handler'; import { MockOrderServiceProvider } from '../../../lib/providers'; -import { MockQuoter, MOCK_FILLER_ADDRESS, Quoter } from '../../../lib/quoters'; +import { MOCK_FILLER_ADDRESS, MockQuoter, Quoter } from '../../../lib/quoters'; jest.mock('axios');