From 6aa1cdb6d91afabc631c1220e9144f32c6c4ca8b Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Wed, 13 Mar 2024 14:33:11 -0400 Subject: [PATCH 1/8] add success rate alarms for log processor lambdas --- bin/stacks/analytics-stack.ts | 58 ++++++++++++++++++++++++++++++++++- bin/stacks/api-stack.ts | 7 +++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/bin/stacks/analytics-stack.ts b/bin/stacks/analytics-stack.ts index 5208418..be2683b 100644 --- a/bin/stacks/analytics-stack.ts +++ b/bin/stacks/analytics-stack.ts @@ -41,6 +41,7 @@ export interface AnalyticsStackProps extends cdk.NestedStackProps { envVars: Record; analyticsStreamArn: string; stage: string; + chatbotSNSArn?: string; } /** @@ -59,7 +60,7 @@ export class AnalyticsStack extends cdk.NestedStack { constructor(scope: Construct, id: string, props: AnalyticsStackProps) { super(scope, id, props); - const { quoteLambda, analyticsStreamArn, stage } = props; + const { quoteLambda, analyticsStreamArn, stage, chatbotSNSArn } = props; /* S3 Initialization */ const rfqRequestBucket = new aws_s3.Bucket(this, 'RfqRequestBucket'); @@ -470,6 +471,61 @@ export class AnalyticsStack extends cdk.NestedStack { ], }) ); + let chatBotTopic: cdk.aws_sns.ITopic; + if (chatbotSNSArn) { + chatBotTopic = cdk.aws_sns.Topic.fromTopicArn(this, 'ChatbotTopic', chatbotSNSArn); + } + + /* log processor alarms */ + [quoteProcessorLambda, fillEventProcessorLambda, postOrderProcessorLambda, botOrderEventsProcessorLambda].forEach( + (lambda) => { + const successRateSev2Name = `${lambda.node.id}-SEV2-SuccessRate`; + const successRateSev3Name = `${lambda.node.id}-SEV3-SuccessRate`; + + const errors = lambda.metricErrors({ + period: cdk.Duration.minutes(5), + statistic: cdk.aws_cloudwatch.Stats.SUM, + label: `${lambda.node.id} Errors`, + }); + + const invocations = lambda.metricInvocations({ + period: cdk.Duration.minutes(5), + statistic: cdk.aws_cloudwatch.Stats.SUM, + label: `${lambda.node.id} Invocations`, + }); + + const successRate = new cdk.aws_cloudwatch.MathExpression({ + expression: '100 - 100 * errors / MAX([errors, invocations])', + usingMetrics: { + errors, + invocations, + }, + label: `${lambda.node.id} Success Rate`, + }); + + const successRateSev2 = new cdk.aws_cloudwatch.Alarm(this, successRateSev2Name, { + metric: successRate, + threshold: 60, + evaluationPeriods: 1, + comparisonOperator: cdk.aws_cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, + actionsEnabled: true, + }); + + const successRateSev3 = new cdk.aws_cloudwatch.Alarm(this, successRateSev3Name, { + metric: successRate, + threshold: 90, + evaluationPeriods: 1, + comparisonOperator: cdk.aws_cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, + actionsEnabled: true, + }); + + if (chatBotTopic) { + successRateSev2.addAlarmAction(new cdk.aws_cloudwatch_actions.SnsAction(chatBotTopic)); + successRateSev3.addAlarmAction(new cdk.aws_cloudwatch_actions.SnsAction(chatBotTopic)); + } + } + ); + // CDK doesn't have this implemented yet, so have to use the CloudFormation resource (lower level of abstraction) // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesisfirehose-deliverystream.html const uraRequestStream = new aws_firehose.CfnDeliveryStream(this, 'uraRequestStream', { diff --git a/bin/stacks/api-stack.ts b/bin/stacks/api-stack.ts index 955184c..651439b 100644 --- a/bin/stacks/api-stack.ts +++ b/bin/stacks/api-stack.ts @@ -10,9 +10,9 @@ import * as aws_lambda from 'aws-cdk-lib/aws-lambda'; import * as aws_lambda_nodejs from 'aws-cdk-lib/aws-lambda-nodejs'; import * as aws_logs from 'aws-cdk-lib/aws-logs'; import * as aws_waf from 'aws-cdk-lib/aws-wafv2'; -import { KmsStack } from './kms-stack' import { Construct } from 'constructs'; import * as path from 'path'; +import { KmsStack } from './kms-stack'; import { Metric } from '../../lib/entities'; import { STAGE } from '../../lib/util/stage'; @@ -156,7 +156,7 @@ export class APIStack extends cdk.Stack { }); // KMS initialization - const kmsStack = new KmsStack(this, `${SERVICE_NAME}HardQuoteCosignerKey-1`) + const kmsStack = new KmsStack(this, `${SERVICE_NAME}HardQuoteCosignerKey-1`); /* * Firehose Initialization @@ -184,7 +184,7 @@ export class APIStack extends cdk.Stack { actions: ['kms:GetPublicKey', 'kms:Sign'], effect: aws_iam.Effect.ALLOW, }) - ) + ); lambdaRole.addToPolicy( new aws_iam.PolicyStatement({ @@ -461,6 +461,7 @@ export class APIStack extends cdk.Stack { envVars: props.envVars, analyticsStreamArn: firehoseStack.analyticsStreamArn, stage, + chatbotSNSArn, }); const cronStack = new CronStack(this, 'CronStack', { From f938486dd6372f11c45c4799ca7aaacde3641561 Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Thu, 14 Mar 2024 10:45:06 -0400 Subject: [PATCH 2/8] add hard quote firehose streams --- bin/stacks/analytics-stack.ts | 120 ++++++++++++++++++++++- bin/stacks/api-stack.ts | 1 + lib/entities/HardQuoteRequest.ts | 2 +- lib/handlers/hard-quote/handler.ts | 15 ++- lib/handlers/quote/handler.ts | 7 +- test/handlers/hard-quote/handler.test.ts | 5 +- 6 files changed, 136 insertions(+), 14 deletions(-) diff --git a/bin/stacks/analytics-stack.ts b/bin/stacks/analytics-stack.ts index be2683b..62dd999 100644 --- a/bin/stacks/analytics-stack.ts +++ b/bin/stacks/analytics-stack.ts @@ -38,6 +38,7 @@ enum RS_DATA_TYPES { export interface AnalyticsStackProps extends cdk.NestedStackProps { quoteLambda: aws_lambda_nodejs.NodejsFunction; + hardQuoteLambda: aws_lambda_nodejs.NodejsFunction; envVars: Record; analyticsStreamArn: string; stage: string; @@ -60,12 +61,14 @@ export class AnalyticsStack extends cdk.NestedStack { constructor(scope: Construct, id: string, props: AnalyticsStackProps) { super(scope, id, props); - const { quoteLambda, analyticsStreamArn, stage, chatbotSNSArn } = props; + const { quoteLambda, hardQuoteLambda, analyticsStreamArn, stage, chatbotSNSArn } = props; /* S3 Initialization */ const rfqRequestBucket = new aws_s3.Bucket(this, 'RfqRequestBucket'); + const hardRequestBucket = new aws_s3.Bucket(this, 'HardRequestBucket'); const unifiedRoutingRequestBucket = new aws_s3.Bucket(this, 'UnifiedRoutingRequestBucket'); const rfqResponseBucket = new aws_s3.Bucket(this, 'RfqResponseBucket'); + const hardResponseBucket = new aws_s3.Bucket(this, 'HardResponseBucket'); const unifiedRoutingResponseBucket = new aws_s3.Bucket(this, 'UnifiedRoutingResponseBucket'); const fillBucket = new aws_s3.Bucket(this, 'FillBucket'); const ordersBucket = new aws_s3.Bucket(this, 'OrdersBucket'); @@ -76,6 +79,8 @@ export class AnalyticsStack extends cdk.NestedStack { const dsRole = aws_iam.Role.fromRoleArn(this, 'DsRole', 'arn:aws:iam::867401673276:user/bq-load-sa'); rfqRequestBucket.grantRead(dsRole); rfqResponseBucket.grantRead(dsRole); + hardRequestBucket.grantRead(dsRole); + hardResponseBucket.grantRead(dsRole); unifiedRoutingRequestBucket.grantRead(dsRole); unifiedRoutingResponseBucket.grantRead(dsRole); fillBucket.grantRead(dsRole); @@ -184,6 +189,18 @@ export class AnalyticsStack extends cdk.NestedStack { ], }); + const hardRequestTable = new aws_rs.Table(this, 'HardRequestTable', { + cluster: rsCluster, + adminUser: creds, + databaseName: RS_DATABASE_NAME, + tableName: 'HardRequests', + tableColumns: [ + { name: 'requestId', dataType: RS_DATA_TYPES.UUID, distKey: true }, + { name: 'createdAt', dataType: RS_DATA_TYPES.TIMESTAMP }, + { name: 'createdAtMs', dataType: RS_DATA_TYPES.TIMESTAMP_MS }, + ], + }); + const uraResponseTable = new aws_rs.Table(this, 'UnifiedRoutingResponseTable', { cluster: rsCluster, adminUser: creds, @@ -234,6 +251,27 @@ export class AnalyticsStack extends cdk.NestedStack { ], }); + const hardResponseTable = new aws_rs.Table(this, 'HardResponseTable', { + cluster: rsCluster, + adminUser: creds, + databaseName: RS_DATABASE_NAME, + tableName: 'HardResponses', + tableColumns: [ + { name: 'quoteId', dataType: RS_DATA_TYPES.UUID }, + { name: 'requestId', dataType: RS_DATA_TYPES.UUID, distKey: true }, + { name: 'offerer', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'tokenIn', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'tokenOut', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'amountIn', dataType: RS_DATA_TYPES.UINT256 }, + { name: 'amountOut', dataType: RS_DATA_TYPES.UINT256 }, + { name: 'tokenInChainId', dataType: RS_DATA_TYPES.INTEGER }, + { name: 'tokenOutChainId', dataType: RS_DATA_TYPES.INTEGER }, + { name: 'filler', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'createdAt', dataType: RS_DATA_TYPES.TIMESTAMP }, + { name: 'createdAtMs', dataType: RS_DATA_TYPES.TIMESTAMP_MS }, + ], + }); + const archivedOrdersTable = new aws_rs.Table(this, 'archivedOrdersTable', { cluster: rsCluster, adminUser: creds, @@ -594,6 +632,72 @@ export class AnalyticsStack extends cdk.NestedStack { }, }); + const hardRequestFirehoseStream = new aws_firehose.CfnDeliveryStream(this, 'HardRequestStream', { + redshiftDestinationConfiguration: { + clusterJdbcurl: `jdbc:redshift://${rsCluster.clusterEndpoint.hostname}:${rsCluster.clusterEndpoint.port}/${RS_DATABASE_NAME}`, + username: 'admin', + password: creds.secretValueFromJson('password').toString(), + s3Configuration: { + bucketArn: rfqRequestBucket.bucketArn, + roleArn: firehoseRole.roleArn, + compressionFormat: 'UNCOMPRESSED', + }, + roleArn: firehoseRole.roleArn, + copyCommand: { + copyOptions: "JSON 'auto ignorecase'", + dataTableName: hardRequestTable.tableName, + dataTableColumns: hardRequestTable.tableColumns.map((column) => column.name).toString(), + }, + processingConfiguration: { + enabled: true, + processors: [ + { + type: 'Lambda', + parameters: [ + { + parameterName: 'LambdaArn', + parameterValue: quoteProcessorLambda.functionArn, + }, + ], + }, + ], + }, + }, + }); + + const hardResponseFirehoseStream = new aws_firehose.CfnDeliveryStream(this, 'HardResponseStream', { + redshiftDestinationConfiguration: { + clusterJdbcurl: `jdbc:redshift://${rsCluster.clusterEndpoint.hostname}:${rsCluster.clusterEndpoint.port}/${RS_DATABASE_NAME}`, + username: 'admin', + password: creds.secretValueFromJson('password').toString(), + s3Configuration: { + bucketArn: rfqRequestBucket.bucketArn, + roleArn: firehoseRole.roleArn, + compressionFormat: 'UNCOMPRESSED', + }, + roleArn: firehoseRole.roleArn, + copyCommand: { + copyOptions: "JSON 'auto ignorecase'", + dataTableName: hardResponseTable.tableName, + dataTableColumns: hardResponseTable.tableColumns.map((column) => column.name).toString(), + }, + processingConfiguration: { + enabled: true, + processors: [ + { + type: 'Lambda', + parameters: [ + { + parameterName: 'LambdaArn', + parameterValue: quoteProcessorLambda.functionArn, + }, + ], + }, + ], + }, + }, + }); + const uraResponseStream = new aws_firehose.CfnDeliveryStream(this, 'UnifiedRoutingResponseStream', { redshiftDestinationConfiguration: { clusterJdbcurl: `jdbc:redshift://${rsCluster.clusterEndpoint.hostname}:${rsCluster.clusterEndpoint.port}/${RS_DATABASE_NAME}`, @@ -1012,6 +1116,20 @@ export class AnalyticsStack extends cdk.NestedStack { roleArn: subscriptionRole.roleArn, }); + new aws_logs.CfnSubscriptionFilter(this, 'HardRequestSub', { + destinationArn: hardRequestFirehoseStream.attrArn, + filterPattern: '{ $.eventType = "HardRequest" }', + logGroupName: hardQuoteLambda.logGroup.logGroupName, + roleArn: subscriptionRole.roleArn, + }); + + new aws_logs.CfnSubscriptionFilter(this, 'HardResponseSub', { + destinationArn: hardResponseFirehoseStream.attrArn, + filterPattern: '{ $.eventType = "HardResponse" }', + logGroupName: hardQuoteLambda.logGroup.logGroupName, + roleArn: subscriptionRole.roleArn, + }); + new CfnOutput(this, 'fillDestinationName', { value: fillDestination.attrArn, }); diff --git a/bin/stacks/api-stack.ts b/bin/stacks/api-stack.ts index 651439b..cd515da 100644 --- a/bin/stacks/api-stack.ts +++ b/bin/stacks/api-stack.ts @@ -458,6 +458,7 @@ export class APIStack extends cdk.Stack { */ const analyticsStack = new AnalyticsStack(this, 'AnalyticsStack', { quoteLambda, + hardQuoteLambda, envVars: props.envVars, analyticsStreamArn: firehoseStack.analyticsStreamArn, stage, diff --git a/lib/entities/HardQuoteRequest.ts b/lib/entities/HardQuoteRequest.ts index b08cefb..6d3cb38 100644 --- a/lib/entities/HardQuoteRequest.ts +++ b/lib/entities/HardQuoteRequest.ts @@ -2,8 +2,8 @@ import { TradeType } from '@uniswap/sdk-core'; import { UnsignedV2DutchOrder } from '@uniswap/uniswapx-sdk'; import { BigNumber, ethers, utils } from 'ethers'; -import { HardQuoteRequestBody } from '../handlers/hard-quote'; import { QuoteRequest, QuoteRequestDataJSON } from '.'; +import { HardQuoteRequestBody } from '../handlers/hard-quote'; export class HardQuoteRequest { public order: UnsignedV2DutchOrder; diff --git a/lib/handlers/hard-quote/handler.ts b/lib/handlers/hard-quote/handler.ts index c102af3..fb0d093 100644 --- a/lib/handlers/hard-quote/handler.ts +++ b/lib/handlers/hard-quote/handler.ts @@ -4,6 +4,7 @@ import { CosignedV2DutchOrder, CosignerData } from '@uniswap/uniswapx-sdk'; import { BigNumber, ethers } from 'ethers'; import Joi from 'joi'; +import { EventType } from 'aws-cdk-lib/aws-s3'; import { HardQuoteRequest, HardQuoteResponse, Metric, QuoteResponse } from '../../entities'; import { NoQuotesAvailable, OrderPostError, UnknownOrderCosignerError } from '../../util/errors'; import { timestampInMstoSeconds } from '../../util/time'; @@ -47,21 +48,19 @@ export class QuoteHandler extends APIGLambdaHandler< throw new UnknownOrderCosignerError(); } - // TODO: finalize on v2 metrics logging + // Instead of decoding the order, we rely on frontend passing in the requestId + // from indicative quote log.info({ - eventType: 'HardQuoteRequest', + eventType: 'HardRequest', body: { requestId: request.requestId, - tokenInChainId: request.tokenInChainId, - tokenOutChainId: request.tokenInChainId, - encoded: requestBody.encodedInnerOrder, - sig: requestBody.innerSig, + quoteId: request.quoteId, createdAt: timestampInMstoSeconds(start), createdAtMs: start.toString(), }, }); - const bestQuote = await getBestQuote(quoters, request.toQuoteRequest(), log, metric); + const bestQuote = await getBestQuote(quoters, request.toQuoteRequest(), log, metric, 'HardResponse'); if (!bestQuote) { metric.putMetric(Metric.HARD_QUOTE_404, 1, MetricLoggerUnit.Count); throw new NoQuotesAvailable(); @@ -75,7 +74,7 @@ export class QuoteHandler extends APIGLambdaHandler< const cosignedOrder = CosignedV2DutchOrder.fromUnsignedOrder(request.order, cosignerData, cosignature); try { - await orderServiceProvider.postOrder(cosignedOrder, request.innerSig, request.quoteId); + await orderServiceProvider.postOrder(cosignedOrder, request.innerSig, bestQuote.quoteId); } catch (e) { metric.putMetric(Metric.HARD_QUOTE_400, 1, MetricLoggerUnit.Count); throw new OrderPostError(); diff --git a/lib/handlers/quote/handler.ts b/lib/handlers/quote/handler.ts index 2186e61..d1233d9 100644 --- a/lib/handlers/quote/handler.ts +++ b/lib/handlers/quote/handler.ts @@ -12,6 +12,8 @@ import { APIHandleRequestParams, ErrorResponse, Response } from '../base/api-han import { ContainerInjected, RequestInjected } from './injector'; import { PostQuoteRequestBody, PostQuoteRequestBodyJoi, PostQuoteResponse, URAResponseJoi } from './schema'; +export type EventType = 'QuoteResponse' | 'HardResponse'; + export class QuoteHandler extends APIGLambdaHandler< ContainerInjected, RequestInjected, @@ -83,7 +85,8 @@ export async function getBestQuote( quoters: Quoter[], quoteRequest: QuoteRequest, log: Logger, - metric: IMetric + metric: IMetric, + eventType: EventType = 'QuoteResponse' ): Promise { const responses: QuoteResponse[] = (await Promise.all(quoters.map((q) => q.quote(quoteRequest)))).flat(); switch (responses.length) { @@ -107,7 +110,7 @@ export async function getBestQuote( // return the response with the highest amountOut value return responses.reduce((bestQuote: QuoteResponse | null, quote: QuoteResponse) => { log.info({ - eventType: 'QuoteResponse', + eventType: eventType, body: { ...quote.toLog(), offerer: quote.swapper }, }); diff --git a/test/handlers/hard-quote/handler.test.ts b/test/handlers/hard-quote/handler.test.ts index dbeed50..16ef67f 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 { MOCK_FILLER_ADDRESS, MockQuoter, Quoter } from '../../../lib/quoters'; +import { MockQuoter, MOCK_FILLER_ADDRESS, Quoter } from '../../../lib/quoters'; jest.mock('axios'); @@ -124,6 +124,7 @@ describe('Quote handler', () => { it('Simple request and response', async () => { const quoters = [new MockQuoter(logger, 1, 1)]; const request = await getRequest(getOrder({ cosigner: cosignerWallet.address })); + console.log(request); const response: APIGatewayProxyResult = await getQuoteHandler(quoters).handler( getEvent(request), @@ -245,7 +246,7 @@ describe('Quote handler', () => { }); }); - it.only('No quotes', async () => { + it('No quotes', async () => { const request = await getRequest(getOrder({ cosigner: cosignerWallet.address })); const response: APIGatewayProxyResult = await getQuoteHandler([]).handler( From eb19377ddc578bc04958a9adad26a1aec074eefb Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Thu, 14 Mar 2024 12:06:10 -0400 Subject: [PATCH 3/8] fix hard-quote tests --- lib/handlers/hard-quote/schema.ts | 2 ++ test/handlers/hard-quote/handler.test.ts | 12 +++++++----- test/handlers/hard-quote/schema.test.ts | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/handlers/hard-quote/schema.ts b/lib/handlers/hard-quote/schema.ts index 3453c59..fb7cda2 100644 --- a/lib/handlers/hard-quote/schema.ts +++ b/lib/handlers/hard-quote/schema.ts @@ -7,6 +7,7 @@ export const HardQuoteRequestBodyJoi = Joi.object({ requestId: FieldValidator.requestId.required(), quoteId: FieldValidator.uuid.optional(), encodedInnerOrder: Joi.string().required(), + cosigner: FieldValidator.address.required(), innerSig: FieldValidator.rawSignature.required(), tokenInChainId: FieldValidator.chainId.required(), tokenOutChainId: Joi.number().integer().valid(Joi.ref('tokenInChainId')).required(), @@ -17,6 +18,7 @@ export type HardQuoteRequestBody = { quoteId?: string; encodedInnerOrder: string; innerSig: string; + cosigner: string; tokenInChainId: number; tokenOutChainId: number; }; diff --git a/test/handlers/hard-quote/handler.test.ts b/test/handlers/hard-quote/handler.test.ts index 16ef67f..a0aaac1 100644 --- a/test/handlers/hard-quote/handler.test.ts +++ b/test/handlers/hard-quote/handler.test.ts @@ -22,6 +22,9 @@ import { MockQuoter, MOCK_FILLER_ADDRESS, Quoter } from '../../../lib/quoters'; jest.mock('axios'); +const swapperWallet = Wallet.createRandom(); +const cosignerWallet = Wallet.createRandom(); + const QUOTE_ID = 'a83f397c-8ef4-4801-a9b7-6e79155049f6'; const REQUEST_ID = 'a83f397c-8ef4-4801-a9b7-6e79155049f6'; const TOKEN_IN = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'; @@ -44,7 +47,7 @@ export const getOrder = (data: Partial): UnsignedV2Dut nonce: BigNumber.from(10), additionalValidationContract: ethers.constants.AddressZero, additionalValidationData: '0x', - cosigner: ethers.constants.AddressZero, + cosigner: cosignerWallet.address, cosignerData: undefined, baseInput: { token: TOKEN_IN, @@ -68,9 +71,6 @@ export const getOrder = (data: Partial): UnsignedV2Dut }; describe('Quote handler', () => { - const swapperWallet = Wallet.createRandom(); - const cosignerWallet = Wallet.createRandom(); - // Creating mocks for all the handler dependencies. const requestInjectedMock: Promise = new Promise( (resolve) => @@ -114,6 +114,7 @@ describe('Quote handler', () => { tokenOutChainId: CHAIN_ID, encodedInnerOrder: order.serialize(), innerSig: sig, + cosigner: cosignerWallet.address, }; }; @@ -246,7 +247,8 @@ describe('Quote handler', () => { }); }); - it('No quotes', async () => { + // TODO: remove only after posting order is unblocked + it.only('No quotes', async () => { const request = await getRequest(getOrder({ cosigner: cosignerWallet.address })); const response: APIGatewayProxyResult = await getQuoteHandler([]).handler( diff --git a/test/handlers/hard-quote/schema.test.ts b/test/handlers/hard-quote/schema.test.ts index b635237..0dd7d48 100644 --- a/test/handlers/hard-quote/schema.test.ts +++ b/test/handlers/hard-quote/schema.test.ts @@ -10,6 +10,7 @@ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; const REQUEST_ID = uuidv4(); const QUOTE_ID = uuidv4(); +const COSIGNER = '0x0000000000000000000000000000000000000000'; const validTokenIn = [USDC, WETH].reduce(lowerUpper, []); const validTokenOut = [USDC, WETH].reduce(lowerUpper, []); @@ -41,6 +42,7 @@ const validHardRequestBodyCombos = validTokenIn.flatMap((tokenIn) => tokenInChainId: 1, tokenOutChainId: 1, encodedInnerOrder: order.serialize(), + cosigner: COSIGNER, innerSig: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', }; @@ -61,6 +63,7 @@ describe('hard-quote schemas', () => { quoteId: QUOTE_ID, encodedInnerOrder: body.encodedInnerOrder, innerSig: body.innerSig, + cosigner: COSIGNER, }); } }); @@ -90,31 +93,36 @@ describe('hard-quote schemas', () => { }); it('requires tokenInChainId to be defined', () => { - const { tokenOutChainId, requestId, quoteId, encodedInnerOrder, innerSig } = validHardRequestBodyCombos[0]; + const { tokenOutChainId, requestId, quoteId, encodedInnerOrder, innerSig, cosigner } = + validHardRequestBodyCombos[0]; const validated = HardQuoteRequestBodyJoi.validate({ tokenOutChainId, requestId, quoteId, encodedInnerOrder, innerSig, + cosigner, }); expect(validated.error?.message).toEqual('"tokenInChainId" is required'); }); it('requires tokenOutChainId to be defined', () => { - const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig } = validHardRequestBodyCombos[0]; + const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig, cosigner } = + validHardRequestBodyCombos[0]; const validated = HardQuoteRequestBodyJoi.validate({ tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig, + cosigner, }); expect(validated.error?.message).toEqual('"tokenOutChainId" is required'); }); it('requires tokenOutChainId and tokenInChainId to be the same value', () => { - const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig } = validHardRequestBodyCombos[0]; + const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig, cosigner } = + validHardRequestBodyCombos[0]; const validated = HardQuoteRequestBodyJoi.validate({ tokenInChainId, tokenOutChainId: 5, @@ -122,6 +130,7 @@ describe('hard-quote schemas', () => { quoteId, encodedInnerOrder, innerSig, + cosigner, }); expect(validated.error?.message).toContain('"tokenOutChainId" must be [ref:tokenInChainId]'); }); From 702991238e257051329133291d1c66050b545d0b Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Thu, 14 Mar 2024 12:23:56 -0400 Subject: [PATCH 4/8] Revert "fix hard-quote tests" This reverts commit eb19377ddc578bc04958a9adad26a1aec074eefb. --- lib/handlers/hard-quote/schema.ts | 2 -- test/handlers/hard-quote/handler.test.ts | 12 +++++------- test/handlers/hard-quote/schema.test.ts | 15 +++------------ 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/lib/handlers/hard-quote/schema.ts b/lib/handlers/hard-quote/schema.ts index fb7cda2..3453c59 100644 --- a/lib/handlers/hard-quote/schema.ts +++ b/lib/handlers/hard-quote/schema.ts @@ -7,7 +7,6 @@ export const HardQuoteRequestBodyJoi = Joi.object({ requestId: FieldValidator.requestId.required(), quoteId: FieldValidator.uuid.optional(), encodedInnerOrder: Joi.string().required(), - cosigner: FieldValidator.address.required(), innerSig: FieldValidator.rawSignature.required(), tokenInChainId: FieldValidator.chainId.required(), tokenOutChainId: Joi.number().integer().valid(Joi.ref('tokenInChainId')).required(), @@ -18,7 +17,6 @@ export type HardQuoteRequestBody = { quoteId?: string; encodedInnerOrder: string; innerSig: string; - cosigner: string; tokenInChainId: number; tokenOutChainId: number; }; diff --git a/test/handlers/hard-quote/handler.test.ts b/test/handlers/hard-quote/handler.test.ts index a0aaac1..16ef67f 100644 --- a/test/handlers/hard-quote/handler.test.ts +++ b/test/handlers/hard-quote/handler.test.ts @@ -22,9 +22,6 @@ import { MockQuoter, MOCK_FILLER_ADDRESS, Quoter } from '../../../lib/quoters'; jest.mock('axios'); -const swapperWallet = Wallet.createRandom(); -const cosignerWallet = Wallet.createRandom(); - const QUOTE_ID = 'a83f397c-8ef4-4801-a9b7-6e79155049f6'; const REQUEST_ID = 'a83f397c-8ef4-4801-a9b7-6e79155049f6'; const TOKEN_IN = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'; @@ -47,7 +44,7 @@ export const getOrder = (data: Partial): UnsignedV2Dut nonce: BigNumber.from(10), additionalValidationContract: ethers.constants.AddressZero, additionalValidationData: '0x', - cosigner: cosignerWallet.address, + cosigner: ethers.constants.AddressZero, cosignerData: undefined, baseInput: { token: TOKEN_IN, @@ -71,6 +68,9 @@ export const getOrder = (data: Partial): UnsignedV2Dut }; describe('Quote handler', () => { + const swapperWallet = Wallet.createRandom(); + const cosignerWallet = Wallet.createRandom(); + // Creating mocks for all the handler dependencies. const requestInjectedMock: Promise = new Promise( (resolve) => @@ -114,7 +114,6 @@ describe('Quote handler', () => { tokenOutChainId: CHAIN_ID, encodedInnerOrder: order.serialize(), innerSig: sig, - cosigner: cosignerWallet.address, }; }; @@ -247,8 +246,7 @@ describe('Quote handler', () => { }); }); - // TODO: remove only after posting order is unblocked - it.only('No quotes', async () => { + it('No quotes', async () => { const request = await getRequest(getOrder({ cosigner: cosignerWallet.address })); const response: APIGatewayProxyResult = await getQuoteHandler([]).handler( diff --git a/test/handlers/hard-quote/schema.test.ts b/test/handlers/hard-quote/schema.test.ts index 0dd7d48..b635237 100644 --- a/test/handlers/hard-quote/schema.test.ts +++ b/test/handlers/hard-quote/schema.test.ts @@ -10,7 +10,6 @@ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; const REQUEST_ID = uuidv4(); const QUOTE_ID = uuidv4(); -const COSIGNER = '0x0000000000000000000000000000000000000000'; const validTokenIn = [USDC, WETH].reduce(lowerUpper, []); const validTokenOut = [USDC, WETH].reduce(lowerUpper, []); @@ -42,7 +41,6 @@ const validHardRequestBodyCombos = validTokenIn.flatMap((tokenIn) => tokenInChainId: 1, tokenOutChainId: 1, encodedInnerOrder: order.serialize(), - cosigner: COSIGNER, innerSig: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', }; @@ -63,7 +61,6 @@ describe('hard-quote schemas', () => { quoteId: QUOTE_ID, encodedInnerOrder: body.encodedInnerOrder, innerSig: body.innerSig, - cosigner: COSIGNER, }); } }); @@ -93,36 +90,31 @@ describe('hard-quote schemas', () => { }); it('requires tokenInChainId to be defined', () => { - const { tokenOutChainId, requestId, quoteId, encodedInnerOrder, innerSig, cosigner } = - validHardRequestBodyCombos[0]; + const { tokenOutChainId, requestId, quoteId, encodedInnerOrder, innerSig } = validHardRequestBodyCombos[0]; const validated = HardQuoteRequestBodyJoi.validate({ tokenOutChainId, requestId, quoteId, encodedInnerOrder, innerSig, - cosigner, }); expect(validated.error?.message).toEqual('"tokenInChainId" is required'); }); it('requires tokenOutChainId to be defined', () => { - const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig, cosigner } = - validHardRequestBodyCombos[0]; + const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig } = validHardRequestBodyCombos[0]; const validated = HardQuoteRequestBodyJoi.validate({ tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig, - cosigner, }); expect(validated.error?.message).toEqual('"tokenOutChainId" is required'); }); it('requires tokenOutChainId and tokenInChainId to be the same value', () => { - const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig, cosigner } = - validHardRequestBodyCombos[0]; + const { tokenInChainId, requestId, quoteId, encodedInnerOrder, innerSig } = validHardRequestBodyCombos[0]; const validated = HardQuoteRequestBodyJoi.validate({ tokenInChainId, tokenOutChainId: 5, @@ -130,7 +122,6 @@ describe('hard-quote schemas', () => { quoteId, encodedInnerOrder, innerSig, - cosigner, }); expect(validated.error?.message).toContain('"tokenOutChainId" must be [ref:tokenInChainId]'); }); From 8becf4dbbb8d8a6234991a146ea6d2ab0332d398 Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Thu, 14 Mar 2024 13:49:52 -0400 Subject: [PATCH 5/8] fix test --- bin/stacks/api-stack.ts | 2 +- test/handlers/hard-quote/handler.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/stacks/api-stack.ts b/bin/stacks/api-stack.ts index cd515da..07cb038 100644 --- a/bin/stacks/api-stack.ts +++ b/bin/stacks/api-stack.ts @@ -307,7 +307,7 @@ export class APIStack extends cdk.Stack { sourceMap: true, }, environment: { - VERSION: '2', + VERSION: '3', NODE_OPTIONS: '--enable-source-maps', ...props.envVars, stage, diff --git a/test/handlers/hard-quote/handler.test.ts b/test/handlers/hard-quote/handler.test.ts index 16ef67f..39266dc 100644 --- a/test/handlers/hard-quote/handler.test.ts +++ b/test/handlers/hard-quote/handler.test.ts @@ -246,7 +246,7 @@ describe('Quote handler', () => { }); }); - it('No quotes', async () => { + it.only('No quotes', async () => { const request = await getRequest(getOrder({ cosigner: cosignerWallet.address })); const response: APIGatewayProxyResult = await getQuoteHandler([]).handler( From 8c96fec4df972d79bfc29098f5585af3f9e8e9ce Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Thu, 14 Mar 2024 13:52:25 -0400 Subject: [PATCH 6/8] remove console log --- test/handlers/hard-quote/handler.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/handlers/hard-quote/handler.test.ts b/test/handlers/hard-quote/handler.test.ts index 39266dc..b8588eb 100644 --- a/test/handlers/hard-quote/handler.test.ts +++ b/test/handlers/hard-quote/handler.test.ts @@ -124,7 +124,6 @@ describe('Quote handler', () => { it('Simple request and response', async () => { const quoters = [new MockQuoter(logger, 1, 1)]; const request = await getRequest(getOrder({ cosigner: cosignerWallet.address })); - console.log(request); const response: APIGatewayProxyResult = await getQuoteHandler(quoters).handler( getEvent(request), From c52ca6dad5c3025c692978c30868d543c856d119 Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Thu, 14 Mar 2024 14:19:21 -0400 Subject: [PATCH 7/8] remove unused import --- lib/handlers/hard-quote/handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/handlers/hard-quote/handler.ts b/lib/handlers/hard-quote/handler.ts index fb0d093..7ed4725 100644 --- a/lib/handlers/hard-quote/handler.ts +++ b/lib/handlers/hard-quote/handler.ts @@ -4,7 +4,6 @@ import { CosignedV2DutchOrder, CosignerData } from '@uniswap/uniswapx-sdk'; import { BigNumber, ethers } from 'ethers'; import Joi from 'joi'; -import { EventType } from 'aws-cdk-lib/aws-s3'; import { HardQuoteRequest, HardQuoteResponse, Metric, QuoteResponse } from '../../entities'; import { NoQuotesAvailable, OrderPostError, UnknownOrderCosignerError } from '../../util/errors'; import { timestampInMstoSeconds } from '../../util/time'; From 040e42a83b0ac869f0932c873c21d812b4cc2147 Mon Sep 17 00:00:00 2001 From: ConjunctiveNormalForm Date: Mon, 18 Mar 2024 08:57:41 -0400 Subject: [PATCH 8/8] log individual fileds of hard requests --- bin/stacks/analytics-stack.ts | 10 ++++++++++ lib/handlers/hard-quote/handler.ts | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/bin/stacks/analytics-stack.ts b/bin/stacks/analytics-stack.ts index 62dd999..4b06310 100644 --- a/bin/stacks/analytics-stack.ts +++ b/bin/stacks/analytics-stack.ts @@ -196,6 +196,16 @@ export class AnalyticsStack extends cdk.NestedStack { tableName: 'HardRequests', tableColumns: [ { name: 'requestId', dataType: RS_DATA_TYPES.UUID, distKey: true }, + { name: 'quoteId', dataType: RS_DATA_TYPES.UUID }, + { name: 'offerer', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'tokenInChainId', dataType: RS_DATA_TYPES.INTEGER }, + { name: 'tokenOutChainId', dataType: RS_DATA_TYPES.INTEGER }, + { name: 'tokenIn', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'tokenOut', dataType: RS_DATA_TYPES.ADDRESS }, + { name: 'amount', dataType: RS_DATA_TYPES.UINT256 }, + { name: 'type', dataType: RS_DATA_TYPES.TRADE_TYPE }, + { name: 'numOutputs', dataType: RS_DATA_TYPES.INTEGER }, + { name: 'cosigner', dataType: RS_DATA_TYPES.ADDRESS }, { name: 'createdAt', dataType: RS_DATA_TYPES.TIMESTAMP }, { name: 'createdAtMs', dataType: RS_DATA_TYPES.TIMESTAMP_MS }, ], diff --git a/lib/handlers/hard-quote/handler.ts b/lib/handlers/hard-quote/handler.ts index 7ed4725..3ba7c99 100644 --- a/lib/handlers/hard-quote/handler.ts +++ b/lib/handlers/hard-quote/handler.ts @@ -54,6 +54,15 @@ export class QuoteHandler extends APIGLambdaHandler< body: { requestId: request.requestId, quoteId: request.quoteId, + tokenInChainId: request.tokenInChainId, + tokenOutChainId: request.tokenOutChainId, + tokenIn: request.tokenIn, + tokenOut: request.tokenOut, + offerer: request.swapper, + amount: request.amount.toString(), + type: TradeType[request.type], + numOutputs: request.numOutputs, + cosigner: request.order.info.cosigner, createdAt: timestampInMstoSeconds(start), createdAtMs: start.toString(), },