Skip to content

Commit

Permalink
Merge branch 'main' into add-kms-signer
Browse files Browse the repository at this point in the history
  • Loading branch information
marktoda committed Feb 29, 2024
2 parents 76b5da3 + 4020388 commit 01f686d
Show file tree
Hide file tree
Showing 19 changed files with 942 additions and 501 deletions.
164 changes: 164 additions & 0 deletions :
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { TradeType } from '@uniswap/sdk-core';
import { MetricLoggerUnit } from '@uniswap/smart-order-router';
import { CosignedV2DutchOrder, CosignerData } from '@uniswap/uniswapx-sdk';
import { BigNumber, ethers } from 'ethers';
import Joi from 'joi';

import { HardQuoteRequest, HardQuoteResponse, Metric, QuoteResponse } from '../../entities';
import { NoQuotesAvailable, OrderPostError, UnknownOrderCosignerError } from '../../util/errors';
import { timestampInMstoSeconds } from '../../util/time';
import { APIGLambdaHandler } from '../base';
import { APIHandleRequestParams, ErrorResponse, Response } from '../base/api-handler';
import { getBestQuote } from '../quote/handler';
import { ContainerInjected, RequestInjected } from './injector';
import {
HardQuoteRequestBody,
HardQuoteRequestBodyJoi,
HardQuoteResponseData,
HardQuoteResponseDataJoi,
} from './schema';

const DEFAULT_EXCLUSIVITY_OVERRIDE_BPS = 100; // non-exclusive fillers must override price by this much

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

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

const request = HardQuoteRequest.fromHardRequestBody(requestBody);

// we dont have access to the cosigner key, throw
if (request.order.info.cosigner !== cosignerAddress) {
log.error({ cosigner: request.order.info.cosigner }, 'Unknown cosigner');
throw new UnknownOrderCosignerError();
}

// 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(),
},
});

const bestQuote = await getBestQuote(quoters, request.toQuoteRequest(), log, metric);
if (!bestQuote) {
metric.putMetric(Metric.HARD_QUOTE_404, 1, MetricLoggerUnit.Count);
throw new NoQuotesAvailable();
}

log.info({ bestQuote: bestQuote }, 'bestQuote');

// TODO: use server key to cosign instead of local wallet
const cosignerData = getCosignerData(request, bestQuote);
const cosignature = await cosigner.signDigest(request.order.cosignatureHash(cosignerData));
const cosignedOrder = CosignedV2DutchOrder.fromUnsignedOrder(
request.order,
cosignerData,
cosignature
);

try {
await orderServiceProvider.postOrder(cosignedOrder, request.innerSig, request.quoteId);
} catch (e) {
metric.putMetric(Metric.HARD_QUOTE_400, 1, MetricLoggerUnit.Count);
throw new OrderPostError();
}

metric.putMetric(Metric.HARD_QUOTE_200, 1, MetricLoggerUnit.Count);
metric.putMetric(Metric.HARD_QUOTE_LATENCY, Date.now() - start, MetricLoggerUnit.Milliseconds);
const response = new HardQuoteResponse(request, cosignedOrder);

return {
statusCode: 200,
body: response.toResponseJSON(),
};
}

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

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

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

export function getCosignerData(request: HardQuoteRequest, quote: QuoteResponse): CosignerData {
const decayStartTime = getDecayStartTime(request.tokenInChainId);
// default to open order with the original prices
let filler = ethers.constants.AddressZero;
let inputAmount = BigNumber.from(0);
const outputAmounts = request.order.info.baseOutputs.map(() => BigNumber.from(0));

// if the quote is better, then increase amounts by the difference
if (request.type === TradeType.EXACT_INPUT) {
if (quote.amountOut.gt(request.totalOutputAmountStart)) {
const increase = quote.amountOut.sub(request.totalOutputAmountStart);
// give all the increase to the first (swapper) output
outputAmounts[0] = request.order.info.baseOutputs[0].startAmount.add(increase);
if (quote.filler) {
filler = quote.filler;
}
}
} else {
if (quote.amountIn.lt(request.totalInputAmountStart)) {
inputAmount = quote.amountIn;
if (quote.filler) {
filler = quote.filler;
}
}
}

return {
decayStartTime: decayStartTime,
decayEndTime: getDecayEndTime(request.tokenInChainId, decayStartTime),
exclusiveFiller: filler,
exclusivityOverrideBps: DEFAULT_EXCLUSIVITY_OVERRIDE_BPS,
inputAmount: inputAmount,
outputAmounts: outputAmounts,
};
}

function getDecayStartTime(chainId: number): number {
const nowTimestamp = Math.floor(Date.now() / 1000);
switch (chainId) {
case 1:
return nowTimestamp + 24; // 2 blocks
default:
return nowTimestamp + 10; // 10 seconds
}
}

function getDecayEndTime(chainId: number, startTime: number): number {
switch (chainId) {
case 1:
return startTime + 60; // 5 blocks
default:
return startTime + 30; // 30 seconds
}
}
6 changes: 6 additions & 0 deletions bin/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export class APIPipeline extends Stack {
synth: synthStep,
});

const urlSecrets = sm.Secret.fromSecretAttributes(this, 'urlSecrets', {
secretCompleteArn: 'arn:aws:secretsmanager:us-east-2:644039819003:secret:gouda-service-api-xCINOs',
});

const rfqWebhookConfig = sm.Secret.fromSecretAttributes(this, 'RfqConfig', {
secretCompleteArn: 'arn:aws:secretsmanager:us-east-2:644039819003:secret:rfq-webhook-config-sy04bH',
});
Expand All @@ -117,6 +121,7 @@ export class APIPipeline extends Stack {
stage: STAGE.BETA,
envVars: {
RFQ_WEBHOOK_CONFIG: rfqWebhookConfig.secretValue.toString(),
ORDER_SERVICE_URL: urlSecrets.secretValueFromJson('GOUDA_SERVICE_BETA').toString(),
FILL_LOG_SENDER_ACCOUNT: '321377678687',
ORDER_LOG_SENDER_ACCOUNT: '321377678687',
URA_ACCOUNT: '665191769009',
Expand All @@ -136,6 +141,7 @@ export class APIPipeline extends Stack {
chatbotSNSArn: 'arn:aws:sns:us-east-2:644039819003:SlackChatbotTopic',
envVars: {
RFQ_WEBHOOK_CONFIG: rfqWebhookConfig.secretValue.toString(),
ORDER_SERVICE_URL: urlSecrets.secretValueFromJson('GOUDA_SERVICE_PROD').toString(),
FILL_LOG_SENDER_ACCOUNT: '316116520258',
ORDER_LOG_SENDER_ACCOUNT: '316116520258',
URA_ACCOUNT: '652077092967',
Expand Down
54 changes: 35 additions & 19 deletions lib/entities/HardQuoteRequest.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { TradeType } from '@uniswap/sdk-core';
import { UnsignedV2DutchOrder } from '@uniswap/uniswapx-sdk';
import { BigNumber, utils } from 'ethers';
import { BigNumber, ethers, utils } from 'ethers';

import { HardQuoteRequestBody } from '../handlers/hard-quote';
import { QuoteRequestDataJSON } from '.';
import { QuoteRequest, 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.');
public static fromHardRequestBody(body: HardQuoteRequestBody): HardQuoteRequest {
return new HardQuoteRequest(body);
}

constructor(private data: HardQuoteRequestBody) {
Expand All @@ -21,7 +20,7 @@ export class HardQuoteRequest {
return {
tokenInChainId: this.tokenInChainId,
tokenOutChainId: this.tokenOutChainId,
swapper: utils.getAddress(this.swapper),
swapper: ethers.constants.AddressZero,
requestId: this.requestId,
tokenIn: this.tokenIn,
tokenOut: this.tokenOut,
Expand All @@ -37,21 +36,26 @@ export class HardQuoteRequest {
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),
...this.toCleanJSON(),
// 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 }),
};
}

// 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 get requestId(): string {
return this.data.requestId;
}
Expand All @@ -76,16 +80,24 @@ export class HardQuoteRequest {
return utils.getAddress(this.order.info.baseOutputs[0].token);
}

public get totalOutputAmountStart(): BigNumber {
let amount = BigNumber.from(0);
for (const output of this.order.info.baseOutputs) {
amount = amount.add(output.startAmount);
}

return amount;
}

public get totalInputAmountStart(): BigNumber {
return this.order.info.baseInput.startAmount;
}

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

return amount;
return this.totalOutputAmountStart;
}
}

Expand All @@ -103,6 +115,10 @@ export class HardQuoteRequest {
return this.order.info.cosigner;
}

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

public get quoteId(): string | undefined {
return this.data.quoteId;
}
Expand Down
96 changes: 96 additions & 0 deletions lib/entities/HardQuoteResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { CosignedV2DutchOrder } from '@uniswap/uniswapx-sdk';
import { BigNumber } from 'ethers';
import { v4 as uuidv4 } from 'uuid';

import { HardQuoteResponseData } from '../handlers/hard-quote/schema';
import { currentTimestampInMs, timestampInMstoSeconds } from '../util/time';
import { HardQuoteRequest } from '.';

// data class for hard quote response helpers and conversions
export class HardQuoteResponse {
public createdAt: string;

constructor(
public request: HardQuoteRequest,
public order: CosignedV2DutchOrder,
public createdAtMs = currentTimestampInMs()
) {
this.createdAt = timestampInMstoSeconds(parseInt(this.createdAtMs));
}

public toResponseJSON(): HardQuoteResponseData {
return {
requestId: this.request.requestId,
quoteId: this.request.quoteId,
chainId: this.request.tokenInChainId,
filler: this.order.info.cosignerData.exclusiveFiller,
encodedOrder: this.order.serialize(),
orderHash: this.order.hash(),
};
}

public toLog() {
return {
quoteId: this.quoteId,
requestId: this.requestId,
tokenInChainId: this.chainId,
tokenOutChainId: this.chainId,
tokenIn: this.tokenIn,
amountIn: this.amountIn.toString(),
tokenOut: this.tokenOut,
amountOut: this.amountOut.toString(),
swapper: this.swapper,
filler: this.filler,
orderHash: this.order.hash(),
createdAt: this.createdAt,
createdAtMs: this.createdAtMs,
};
}

public get quoteId(): string {
return this.request.quoteId ?? uuidv4();
}

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

public get chainId(): number {
return this.order.chainId;
}

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

public get tokenIn(): string {
return this.request.tokenIn;
}

public get amountOut(): BigNumber {
const resolved = this.order.resolve({
timestamp: this.order.info.cosignerData.decayStartTime,
});
let amount = BigNumber.from(0);
for (const output of resolved.outputs) {
amount = amount.add(output.amount);
}

return amount;
}

public get amountIn(): BigNumber {
const resolved = this.order.resolve({
timestamp: this.order.info.cosignerData.decayStartTime,
});
return resolved.input.amount;
}

public get tokenOut(): string {
return this.request.tokenOut;
}

public get filler(): string | undefined {
return this.order.info.cosignerData.exclusiveFiller;
}
}
Loading

0 comments on commit 01f686d

Please sign in to comment.