Skip to content

Commit

Permalink
Merge pull request #270 from Uniswap/add-hard-quote
Browse files Browse the repository at this point in the history
add hard quote
  • Loading branch information
marktoda authored Feb 23, 2024
2 parents 8ea8735 + 3b1797c commit fa4ed34
Show file tree
Hide file tree
Showing 12 changed files with 1,197 additions and 2 deletions.
113 changes: 113 additions & 0 deletions lib/entities/HardQuoteRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { TradeType } from '@uniswap/sdk-core';
import { UnsignedV2DutchOrder } from '@uniswap/uniswapx-sdk';
import { BigNumber, utils } from 'ethers';

import { HardQuoteRequestBody } from '../handlers/hard-quote';
import { QuoteRequestDataJSON } from '.';

export class HardQuoteRequest {
public order: UnsignedV2DutchOrder;

public static fromHardRequestBody(_body: HardQuoteRequestBody): HardQuoteRequest {
// TODO: parse hard request into the same V2 request object format
throw new Error('Method not implemented.');
}

constructor(private data: HardQuoteRequestBody) {
this.order = UnsignedV2DutchOrder.parse(data.encodedInnerOrder, data.tokenInChainId);
}

public toCleanJSON(): QuoteRequestDataJSON {
return {
tokenInChainId: this.tokenInChainId,
tokenOutChainId: this.tokenOutChainId,
swapper: utils.getAddress(this.swapper),
requestId: this.requestId,
tokenIn: this.tokenIn,
tokenOut: this.tokenOut,
amount: this.amount.toString(),
type: TradeType[this.type],
numOutputs: this.numOutputs,
...(this.quoteId && { quoteId: this.quoteId }),
};
}

// return an opposing quote request,
// i.e. quoting the other side of the trade
public toOpposingCleanJSON(): QuoteRequestDataJSON {
const type = this.type === TradeType.EXACT_INPUT ? TradeType.EXACT_OUTPUT : TradeType.EXACT_INPUT;
return {
tokenInChainId: this.tokenInChainId,
tokenOutChainId: this.tokenOutChainId,
requestId: this.requestId,
swapper: utils.getAddress(this.swapper),
// switch tokenIn/tokenOut
tokenIn: utils.getAddress(this.tokenOut),
tokenOut: utils.getAddress(this.tokenIn),
amount: this.amount.toString(),
// switch tradeType
type: TradeType[type],
numOutputs: this.numOutputs,
...(this.quoteId && { quoteId: this.quoteId }),
};
}

public get requestId(): string {
return this.data.requestId;
}

public get tokenInChainId(): number {
return this.data.tokenInChainId;
}

public get tokenOutChainId(): number {
return this.data.tokenInChainId;
}

public get swapper(): string {
return this.order.info.swapper;
}

public get tokenIn(): string {
return utils.getAddress(this.order.info.baseInput.token);
}

public get tokenOut(): string {
return utils.getAddress(this.order.info.baseOutputs[0].token);
}

public get amount(): BigNumber {
if (this.type === TradeType.EXACT_INPUT) {
return this.order.info.baseInput.startAmount;
} else {
const amount = BigNumber.from(0);
for (const output of this.order.info.baseOutputs) {
amount.add(output.startAmount);
}

return amount;
}
}

public get type(): TradeType {
return this.order.info.baseInput.startAmount.eq(this.order.info.baseInput.endAmount)
? TradeType.EXACT_INPUT
: TradeType.EXACT_OUTPUT;
}

public get numOutputs(): number {
return this.order.info.baseOutputs.length;
}

public get cosigner(): string {
return this.order.info.cosigner;
}

public get quoteId(): string | undefined {
return this.data.quoteId;
}

public set quoteId(quoteId: string | undefined) {
this.data.quoteId = quoteId;
}
}
1 change: 1 addition & 0 deletions lib/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './analytics-events';
export * from './aws-metrics-logger';
export * from './HardQuoteRequest';
export * from './QuoteRequest';
export * from './QuoteResponse';
62 changes: 62 additions & 0 deletions lib/handlers/hard-quote/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { MetricLoggerUnit } from '@uniswap/smart-order-router';
import Joi from 'joi';

import { HardQuoteRequest, Metric } from '../../entities';
import { timestampInMstoSeconds } from '../../util/time';
import { APIGLambdaHandler } from '../base';
import { APIHandleRequestParams, ErrorResponse, Response } from '../base/api-handler';
import { ContainerInjected, RequestInjected } from './injector';
import { HardQuoteRequestBody, HardQuoteRequestBodyJoi } from './schema';

export class QuoteHandler extends APIGLambdaHandler<
ContainerInjected,
RequestInjected,
HardQuoteRequestBody,
void,
null
> {
public async handleRequest(
params: APIHandleRequestParams<ContainerInjected, RequestInjected, HardQuoteRequestBody, void>
): Promise<ErrorResponse | Response<null>> {
const {
requestInjected: { log, metric },
requestBody,
} = params;
const start = Date.now();

metric.putMetric(Metric.QUOTE_REQUESTED, 1, MetricLoggerUnit.Count);

const request = HardQuoteRequest.fromHardRequestBody(requestBody);

// TODO: finalize on v2 metrics logging
log.info({
eventType: 'HardQuoteRequest',
body: {
requestId: request.requestId,
tokenInChainId: request.tokenInChainId,
tokenOutChainId: request.tokenInChainId,
encoded: requestBody.encodedInnerOrder,
sig: requestBody.innerSig,
createdAt: timestampInMstoSeconds(start),
createdAtMs: start.toString(),
},
});

return {
statusCode: 200,
body: null,
};
}

protected requestBodySchema(): Joi.ObjectSchema | null {
return HardQuoteRequestBodyJoi;
}

protected requestQueryParamsSchema(): Joi.ObjectSchema | null {
return null;
}

protected responseBodySchema(): Joi.ObjectSchema | null {
return null;
}
}
3 changes: 3 additions & 0 deletions lib/handlers/hard-quote/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { QuoteHandler as HardQuoteHandler } from './handler';
export { QuoteInjector as HardQuoteInjector } from './injector';
export * from './schema';
101 changes: 101 additions & 0 deletions lib/handlers/hard-quote/injector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { IMetric, setGlobalLogger, setGlobalMetric } from '@uniswap/smart-order-router';
import { MetricsLogger } from 'aws-embedded-metrics';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { default as bunyan, default as Logger } from 'bunyan';

import {
BETA_S3_KEY,
FADE_RATE_BUCKET,
FADE_RATE_S3_KEY,
PRODUCTION_S3_KEY,
WEBHOOK_CONFIG_BUCKET,
} from '../../constants';
import { AWSMetricsLogger, UniswapXParamServiceMetricDimension } from '../../entities/aws-metrics-logger';
import { S3WebhookConfigurationProvider } from '../../providers';
import { FirehoseLogger } from '../../providers/analytics';
import { S3CircuitBreakerConfigurationProvider } from '../../providers/circuit-breaker/s3';
import { MockFillerComplianceConfigurationProvider } from '../../providers/compliance';
import { Quoter, WebhookQuoter } from '../../quoters';
import { STAGE } from '../../util/stage';
import { ApiInjector, ApiRInj } from '../base/api-handler';
import { HardQuoteRequestBody } from './schema';

export interface ContainerInjected {
quoters: Quoter[];
firehose: FirehoseLogger;
}

export interface RequestInjected extends ApiRInj {
metric: IMetric;
}

export class QuoteInjector extends ApiInjector<ContainerInjected, RequestInjected, HardQuoteRequestBody, void> {
public async buildContainerInjected(): Promise<ContainerInjected> {
const log: Logger = bunyan.createLogger({
name: this.injectorName,
serializers: bunyan.stdSerializers,
level: bunyan.INFO,
});

const stage = process.env['stage'];
const s3Key = stage === STAGE.BETA ? BETA_S3_KEY : PRODUCTION_S3_KEY;

const circuitBreakerProvider = new S3CircuitBreakerConfigurationProvider(
log,
`${FADE_RATE_BUCKET}-${stage}-1`,
FADE_RATE_S3_KEY
);

const webhookProvider = new S3WebhookConfigurationProvider(log, `${WEBHOOK_CONFIG_BUCKET}-${stage}-1`, s3Key);
await webhookProvider.fetchEndpoints();

// TODO: decide if we should handle filler compliance differently
//const complianceKey = stage === STAGE.BETA ? BETA_COMPLIANCE_S3_KEY : PROD_COMPLIANCE_S3_KEY;
//const fillerComplianceProvider = new S3FillerComplianceConfigurationProvider(
// log,
// `${COMPLIANCE_CONFIG_BUCKET}-${stage}-1`,
// complianceKey
//);
const fillerComplianceProvider = new MockFillerComplianceConfigurationProvider([]);

const firehose = new FirehoseLogger(log, process.env.ANALYTICS_STREAM_ARN!);

const quoters: Quoter[] = [
new WebhookQuoter(log, firehose, webhookProvider, circuitBreakerProvider, fillerComplianceProvider),
];
return {
quoters: quoters,
firehose: firehose,
};
}

public async getRequestInjected(
_containerInjected: ContainerInjected,
requestBody: HardQuoteRequestBody,
_requestQueryParams: void,
_event: APIGatewayProxyEvent,
context: Context,
log: Logger,
metricsLogger: MetricsLogger
): Promise<RequestInjected> {
const requestId = context.awsRequestId;

log = log.child({
serializers: bunyan.stdSerializers,
requestBody,
requestId,
});
setGlobalLogger(log);

metricsLogger.setNamespace('Uniswap');
metricsLogger.setDimensions(UniswapXParamServiceMetricDimension);
const metric = new AWSMetricsLogger(metricsLogger);
setGlobalMetric(metric);

return {
log,
metric,
requestId,
};
}
}
22 changes: 22 additions & 0 deletions lib/handlers/hard-quote/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Joi from 'joi';

import { FieldValidator } from '../../util/validator';

/* Hard quote request from user */
export const HardQuoteRequestBodyJoi = Joi.object({
requestId: FieldValidator.requestId.required(),
quoteId: FieldValidator.uuid.optional(),
encodedInnerOrder: Joi.string().required(),
innerSig: FieldValidator.rawSignature.required(),
tokenInChainId: FieldValidator.chainId.required(),
tokenOutChainId: Joi.number().integer().valid(Joi.ref('tokenInChainId')).required(),
});

export type HardQuoteRequestBody = {
requestId: string;
quoteId?: string;
encodedInnerOrder: string;
innerSig: string;
tokenInChainId: number;
tokenOutChainId: number;
};
9 changes: 9 additions & 0 deletions lib/util/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ export class FieldValidator {
public static readonly tradeType = Joi.string().valid('EXACT_INPUT', 'EXACT_OUTPUT');

public static readonly uuid = Joi.string().guid({ version: 'uuidv4' });

// A Raw Signature is a common Signature format where the r, s and v
// are concatenated into a 65 byte(130 nibble) DataHexString
public static readonly rawSignature = Joi.string().custom((value: string, helpers: CustomHelpers<string>) => {
if (!ethers.utils.isHexString(value, 65) && !ethers.utils.isHexString(value, 64)) {
return helpers.message({ custom: 'Signature in wrong format' });
}
return value;
});
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@uniswap/sdk-core": "^3.1.0",
"@uniswap/smart-order-router": "^3.3.0",
"@uniswap/token-lists": "^1.0.0-beta.31",
"@uniswap/uniswapx-sdk": "1.5.0-alpha.7",
"@uniswap/v3-sdk": "^3.9.0",
"aws-cdk-lib": "2.85.0",
"aws-embedded-metrics": "^4.1.0",
Expand Down
Loading

0 comments on commit fa4ed34

Please sign in to comment.