├── .nvmrc ├── .dockerignore ├── lib ├── entities │ ├── index.ts │ └── RelayOrderEntity.ts ├── errors │ ├── InvalidTokenInAddress.ts │ ├── TooManyOpenOrdersError.ts │ ├── ValidationError.ts │ ├── OrderValidationFailedError.ts │ ├── UnexpectedOrderTypeError.ts │ └── NoHandlerConfiguredError.ts ├── handlers │ ├── shared │ │ ├── post.ts │ │ ├── index.ts │ │ ├── sfn.ts │ │ └── get.ts │ ├── base │ │ ├── index.ts │ │ ├── ErrorCode.ts │ │ └── sfn-handler.ts │ ├── get-orders │ │ ├── schema │ │ │ ├── GetOrderTypeQueryParamEnum.ts │ │ │ ├── Common.ts │ │ │ ├── GetPriorityOrderResponse.ts │ │ │ ├── GetRelayOrderResponse.ts │ │ │ ├── GetDutchV2OrderResponse.ts │ │ │ ├── index.ts │ │ │ ├── GetOrdersResponse.ts │ │ │ └── GetDutchV3OrderResponse.ts │ │ ├── injector.ts │ │ └── index.ts │ ├── check-order-status │ │ ├── schema.ts │ │ ├── injector.ts │ │ ├── fill-event-logger.ts │ │ └── index.ts │ ├── get-nonce │ │ ├── index.ts │ │ ├── schema │ │ │ └── index.ts │ │ ├── injector.ts │ │ └── handler.ts │ ├── get-unimind │ │ ├── index.ts │ │ ├── schema │ │ │ └── index.ts │ │ └── injector.ts │ ├── order-notification │ │ ├── index.ts │ │ ├── schema │ │ │ └── index.ts │ │ ├── types.ts │ │ └── injector.ts │ ├── get-docs │ │ ├── index.ts │ │ ├── GetDocsInjector.ts │ │ ├── GetDocsUIInjector.ts │ │ ├── GetDocsUIHandler.ts │ │ ├── swagger-ui.ts │ │ └── GetDocsHandler.ts │ ├── OrderParser.ts │ ├── OnChainValidatorMap.ts │ ├── get-limit-orders │ │ ├── injector.ts │ │ └── index.ts │ ├── post-order │ │ ├── injector.ts │ │ └── schema │ │ │ └── index.ts │ ├── post-limit-order │ │ └── injector.ts │ ├── EventWatcherMap.ts │ └── constants.ts ├── util │ ├── time.ts │ ├── stage.ts │ ├── OrderValidationResponse.ts │ ├── errors.ts │ ├── encryption.ts │ ├── address.ts │ ├── analytics-events.ts │ ├── chain.ts │ ├── metrics.ts │ ├── nonce.ts │ ├── comparison.ts │ ├── Permit2Validator.ts │ ├── constants.ts │ ├── unimind.ts │ └── order.ts ├── models │ ├── Order.ts │ ├── index.ts │ ├── UniswapXOrder.ts │ ├── DutchV1Order.ts │ ├── LimitOrder.ts │ └── RelayOrder.ts ├── Logging.ts ├── preconditions │ └── preconditions.ts ├── HttpStatusCode.ts ├── providers │ ├── base.ts │ ├── types.ts │ ├── s3-webhook-provider.ts │ └── json-webhook-provider.ts ├── RpcUrlMap.ts ├── Config.ts ├── repositories │ ├── IndexMappers │ │ └── IndexMapper.ts │ ├── base.ts │ ├── unimind-parameters-repository.ts │ ├── quote-metadata-repository.ts │ ├── util.ts │ ├── RelayOrderRepository.ts │ └── limit-orders-repository.ts ├── config │ ├── dynamodb.ts │ └── unimind-list.ts ├── Metrics.ts └── unimind │ └── batchedStrategy.ts ├── cdk.json ├── test ├── factories │ ├── SDKRelayOrderFactory.test.ts │ ├── PartialDeep.ts │ ├── SDKPriorityOrderFactory.test.ts │ ├── SDKRelayOrderFactory.ts │ ├── SDKDutchOrderV1Factory.test.ts │ ├── SDKPriorityOrderFactory.ts │ └── SDKDutchOrderV2Factory.ts ├── unit │ ├── errors │ │ └── UnexpectedOrderTypeError.ts │ ├── preconditions │ │ └── preconditions.test.ts │ ├── util │ │ ├── nonce.test.ts │ │ ├── comparison.test.ts │ │ └── address.test.ts │ ├── services │ │ └── check-order-status │ │ │ └── util.test.ts │ ├── handlers │ │ ├── post-order │ │ │ └── PostOrderRequestFactory.ts │ │ ├── get-docs.test.ts │ │ └── check-order-status │ │ │ └── util.test.ts │ ├── providers │ │ └── json-webhook-provider.test.ts │ ├── builders │ │ ├── QueryParamsBuilder.ts │ │ └── QueryParamsBuilder.test.ts │ ├── models │ │ └── RelayOrder.test.ts │ ├── fixtures.ts │ └── repositories │ │ └── quote-metadata-repository.test.ts ├── e2e │ ├── constants.ts │ └── nonce.test.ts ├── HeaderExpectation.ts └── integ │ └── repositories │ └── deleteAllRepoEntries.ts ├── cdk.context.json ├── docker-compose.yml ├── Dockerfile ├── __mocks__ └── aws-embedded-metrics.ts ├── .CONTRIBUTING.md ├── tsconfig.cdk.json ├── .eslintrc ├── bin ├── stacks │ ├── kms-stack.ts │ ├── cron-stack.ts │ └── reaper-stack.ts ├── constants.ts ├── config.ts └── definitions │ └── order-tracking-sfn.json ├── jest.config.js ├── tsconfig.json ├── CLAUDE.md ├── .gitignore ├── README.md ├── .github └── workflows │ └── CI.yml └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.17.1 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | cdk.out/ 5 | -------------------------------------------------------------------------------- /lib/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Order' 2 | export * from './RelayOrderEntity' 3 | -------------------------------------------------------------------------------- /lib/errors/InvalidTokenInAddress.ts: -------------------------------------------------------------------------------- 1 | export class InvalidTokenInAddress extends Error {} 2 | -------------------------------------------------------------------------------- /lib/errors/TooManyOpenOrdersError.ts: -------------------------------------------------------------------------------- 1 | export class TooManyOpenOrdersError extends Error {} 2 | -------------------------------------------------------------------------------- /lib/handlers/shared/post.ts: -------------------------------------------------------------------------------- 1 | export interface ContainerInjected { 2 | [n: string]: never 3 | } 4 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --project=tsconfig.cdk.json bin/app.ts", 3 | "context": {} 4 | } 5 | -------------------------------------------------------------------------------- /lib/util/time.ts: -------------------------------------------------------------------------------- 1 | export const currentTimestampInSeconds = () => Math.floor(Date.now() / 1000).toString() 2 | -------------------------------------------------------------------------------- /lib/util/stage.ts: -------------------------------------------------------------------------------- 1 | export enum STAGE { 2 | BETA = 'beta', 3 | PROD = 'prod', 4 | LOCAL = 'local', 5 | } 6 | -------------------------------------------------------------------------------- /lib/handlers/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-handler' 2 | export * from './ErrorCode' 3 | export * from './sfn-handler' 4 | -------------------------------------------------------------------------------- /lib/util/OrderValidationResponse.ts: -------------------------------------------------------------------------------- 1 | export type OrderValidationResponse = { 2 | valid: boolean 3 | errorString?: string 4 | } 5 | -------------------------------------------------------------------------------- /lib/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/models/Order.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | 3 | export abstract class Order { 4 | abstract get orderType(): OrderType 5 | } 6 | -------------------------------------------------------------------------------- /lib/util/errors.ts: -------------------------------------------------------------------------------- 1 | export class InjectionError extends Error {} 2 | export class SfnInputValidationError extends Error {} 3 | export class DynamoStreamInputValidationError extends Error {} 4 | -------------------------------------------------------------------------------- /lib/Logging.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@aws-lambda-powertools/logger' 2 | import { SERVICE_NAME } from '../bin/constants' 3 | 4 | export const log = new Logger({ 5 | serviceName: SERVICE_NAME, 6 | }) 7 | -------------------------------------------------------------------------------- /lib/util/encryption.ts: -------------------------------------------------------------------------------- 1 | export const decode = (str: string): string => Buffer.from(str, 'base64').toString('binary') 2 | export const encode = (str: string): string => Buffer.from(str, 'binary').toString('base64') 3 | -------------------------------------------------------------------------------- /lib/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DutchV1Order' 2 | export * from './DutchV2Order' 3 | export * from './LimitOrder' 4 | export * from './Order' 5 | export * from './RelayOrder' 6 | export * from './UniswapXOrder' 7 | -------------------------------------------------------------------------------- /lib/preconditions/preconditions.ts: -------------------------------------------------------------------------------- 1 | export function checkDefined(value: T | null | undefined, message: string): T { 2 | if (value === null || value === undefined) { 3 | throw new Error(message) 4 | } 5 | return value 6 | } 7 | -------------------------------------------------------------------------------- /lib/errors/OrderValidationFailedError.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from './ValidationError' 2 | 3 | export class OrderValidationFailedError extends ValidationError { 4 | constructor(message = '') { 5 | super(message) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/handlers/shared/index.ts: -------------------------------------------------------------------------------- 1 | import { StaticJsonRpcProvider } from '@ethersproject/providers' 2 | import { SUPPORTED_CHAINS } from '../../util/chain' 3 | 4 | export type ProviderMap = Map 5 | -------------------------------------------------------------------------------- /lib/models/UniswapXOrder.ts: -------------------------------------------------------------------------------- 1 | import { DutchV1Order } from './DutchV1Order' 2 | import { DutchV2Order } from './DutchV2Order' 3 | import { LimitOrder } from './LimitOrder' 4 | 5 | export type UniswapXOrder = DutchV1Order | LimitOrder | DutchV2Order 6 | -------------------------------------------------------------------------------- /lib/errors/UnexpectedOrderTypeError.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | 3 | export class UnexpectedOrderTypeError extends Error { 4 | constructor(orderType: OrderType) { 5 | super(`Unexpected orderType: ${orderType}`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/errors/NoHandlerConfiguredError.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | 3 | export class NoHandlerConfiguredError extends Error { 4 | constructor(orderType: OrderType) { 5 | super(`No handler configured for orderType: ${orderType}`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/factories/SDKRelayOrderFactory.test.ts: -------------------------------------------------------------------------------- 1 | import { SDKRelayOrderFactory } from './SDKRelayOrderFactory' 2 | 3 | describe('SDKRelayOrderFactory', () => { 4 | it('smoke test - builds a default Relay Order', () => { 5 | expect(SDKRelayOrderFactory.buildRelayOrder(1)).toBeDefined() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/GetOrderTypeQueryParamEnum.ts: -------------------------------------------------------------------------------- 1 | export enum GetOrderTypeQueryParamEnum { 2 | Dutch = 'Dutch', 3 | Dutch_V2 = 'Dutch_V2', 4 | Dutch_V3 = 'Dutch_V3', 5 | Relay = 'Relay', 6 | Limit = 'Limit', 7 | Priority = 'Priority', 8 | 9 | Dutch_V1_V2 = 'Dutch_V1_V2', 10 | } 11 | -------------------------------------------------------------------------------- /cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "availability-zones:account=316116520258:region=us-east-2": [ 3 | "us-east-2a", 4 | "us-east-2b", 5 | "us-east-2c" 6 | ], 7 | "availability-zones:account=321377678687:region=us-east-2": [ 8 | "us-east-2a", 9 | "us-east-2b", 10 | "us-east-2c" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/HttpStatusCode.ts: -------------------------------------------------------------------------------- 1 | export enum HttpStatusCode { 2 | Ok = 200, 3 | Created = 201, 4 | BadRequest = 400, 5 | Unauthorized = 401, 6 | Forbidden = 403, 7 | NotFound = 404, 8 | Conflict = 409, 9 | InternalServerError = 500, 10 | NotImplemented = 501, 11 | BadGateway = 502, 12 | ServiceUnavailable = 503, 13 | } 14 | -------------------------------------------------------------------------------- /lib/handlers/check-order-status/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import FieldValidator from '../../util/field-validator' 3 | 4 | export const CheckOrderStatusInputJoi = Joi.object({ 5 | orderHash: FieldValidator.isValidOrderHash().required(), 6 | orderStatus: FieldValidator.isValidOrderStatus().required(), 7 | chainId: FieldValidator.isValidChainId().required(), 8 | }) 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | dynamodb-local: 4 | command: '-jar DynamoDBLocal.jar -sharedDb -dbPath ./data' 5 | image: 'amazon/dynamodb-local:latest' 6 | container_name: dynamodb-local 7 | ports: 8 | - '8000:8000' 9 | volumes: 10 | - './docker/dynamodb:/home/dynamodblocal/data' 11 | working_dir: /home/dynamodblocal 12 | -------------------------------------------------------------------------------- /lib/handlers/base/ErrorCode.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCode { 2 | OrderParseFail = 'ORDER_PARSE_FAIL', 3 | InvalidOrder = 'INVALID_ORDER', 4 | TooManyOpenOrders = 'TOO_MANY_OPEN_ORDERS', 5 | InternalError = 'INTERNAL_ERROR', 6 | ValidationError = 'VALIDATION_ERROR', 7 | TooManyRequests = 'TOO_MANY_REQUESTS', 8 | InvalidTokenInAddress = 'INVALID_TOKEN_IN_ADDRESS' 9 | } 10 | -------------------------------------------------------------------------------- /lib/handlers/get-nonce/index.ts: -------------------------------------------------------------------------------- 1 | import { GetNonceHandler } from './handler' 2 | import { GetNonceInjector } from './injector' 3 | 4 | const getNonceInjectorPromise = new GetNonceInjector('getNonceInjector').build() 5 | const getNonceHandler = new GetNonceHandler('getNonceHandler', getNonceInjectorPromise) 6 | 7 | module.exports = { 8 | getNonceHandler: getNonceHandler.handler, 9 | } 10 | -------------------------------------------------------------------------------- /lib/handlers/get-unimind/index.ts: -------------------------------------------------------------------------------- 1 | import { GetUnimindHandler } from './handler' 2 | import { GetUnimindInjector } from './injector' 3 | 4 | const getUnimindInjectorPromise = new GetUnimindInjector('getUnimindInjector').build() 5 | const getUnimindHandler = new GetUnimindHandler('getUnimindHandler', getUnimindInjectorPromise) 6 | 7 | module.exports = { 8 | getUnimindHandler: getUnimindHandler.handler, 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/node:18-alpine@sha256:ef0861618e36d8e5339583a63e2b1082b7ad9cb59a53529bf7d742afa3e2f06b 2 | 3 | WORKDIR /app 4 | 5 | # Install dependencies 6 | COPY package*.json yarn.lock ./ 7 | RUN yarn install 8 | 9 | # Copy source code 10 | COPY . . 11 | 12 | # Build TypeScript 13 | RUN yarn build 14 | 15 | # Run the service 16 | CMD ["node", "dist/lib/crons/gs-reaper/gs-reaper.js"] -------------------------------------------------------------------------------- /__mocks__/aws-embedded-metrics.ts: -------------------------------------------------------------------------------- 1 | const original = jest.requireActual('aws-embedded-metrics') 2 | 3 | class MockMetricsLogger { 4 | public setNamespace = jest.fn() 5 | public putMetric = jest.fn() 6 | public flush = jest.fn() 7 | } 8 | 9 | const metricScope = jest.fn((fn) => fn(new MockMetricsLogger())) 10 | 11 | module.exports = { 12 | ...original, 13 | metricScope, 14 | MockMetricsLogger, 15 | } 16 | -------------------------------------------------------------------------------- /lib/providers/base.ts: -------------------------------------------------------------------------------- 1 | import { FILTER_FIELD, Webhook } from './types' 2 | 3 | export type OrderFilter = { 4 | [FILTER_FIELD.OFFERER]: string 5 | [FILTER_FIELD.ORDER_STATUS]: string 6 | [FILTER_FIELD.FILLER]?: string 7 | [FILTER_FIELD.ORDER_TYPE]?: string 8 | } 9 | 10 | export interface WebhookProvider { 11 | getEndpoints(filter: OrderFilter): Promise 12 | getExclusiveFillerEndpoints(filler: string): Promise 13 | } 14 | -------------------------------------------------------------------------------- /test/unit/errors/UnexpectedOrderTypeError.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import { UnexpectedOrderTypeError } from '../../../lib/errors/UnexpectedOrderTypeError' 3 | 4 | describe('UnexpectedErrorTypeError', () => { 5 | it('encodes the order type into the message', () => { 6 | expect(() => { 7 | throw new UnexpectedOrderTypeError(OrderType.Relay) 8 | }).toEqual('unexpected order type: Relay') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /lib/RpcUrlMap.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from './util/chain' 2 | 3 | export class RpcUrlMap { 4 | private chainIdToUrl: Map = new Map() 5 | 6 | get(chainId: ChainId): string { 7 | const url = this.chainIdToUrl.get(chainId) 8 | if (!url) { 9 | throw new Error(`No RPC url defined for chain ${chainId}`) 10 | } 11 | 12 | return url 13 | } 14 | 15 | set(chainId: ChainId, url: string): void { 16 | this.chainIdToUrl.set(chainId, url) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/util/address.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | /** 4 | * Checks if the given filler address represents an exclusive filler 5 | * An exclusive filler is any non-zero address 6 | * 7 | * @param filler - The filler address to check 8 | * @returns true if the filler is exclusive (not zero address), false otherwise 9 | */ 10 | export function hasExclusiveFiller(filler?: string): filler is string { 11 | return !!filler && filler.toLowerCase() !== ethers.constants.AddressZero.toLowerCase() 12 | } -------------------------------------------------------------------------------- /lib/handlers/order-notification/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderNotificationHandler } from './handler' 2 | import { OrderNotificationInjector } from './injector' 3 | 4 | const orderNotificationInjectorPromise = new OrderNotificationInjector('orderNotificationInjector').build() 5 | const orderNotificationHandler = new OrderNotificationHandler( 6 | 'orderNotificationHandler', 7 | orderNotificationInjectorPromise 8 | ) 9 | 10 | module.exports = { 11 | orderNotificationHandler: orderNotificationHandler.handler, 12 | } 13 | -------------------------------------------------------------------------------- /lib/util/analytics-events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Analytics event type constants 3 | * Used by analytics service (producer) and subscription filters (consumer) 4 | */ 5 | export const ANALYTICS_EVENTS = { 6 | ORDER_POSTED: 'OrderPosted', 7 | ORDER_CANCELLED: 'Cancelled', 8 | INSUFFICIENT_FUNDS: 'InsufficientFunds', 9 | UNIMIND_RESPONSE: 'UnimindResponse', 10 | UNIMIND_PARAMETER_UPDATE: 'UnimindParameterUpdate', 11 | } as const 12 | 13 | export type AnalyticsEventType = typeof ANALYTICS_EVENTS[keyof typeof ANALYTICS_EVENTS] -------------------------------------------------------------------------------- /.CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Adding swagger documentation 2 | 3 | When changing the parameters or endpoints of the API, please update the swagger documentation. 4 | 5 | 1. Visit the open source [editor](https://editor.swagger.io/) 6 | 2. Copy and paste the swagger from [swagger.json](./swagger.json) in the `docs` folder 7 | 3. The editor will ask you to transform the file into yaml, click yes 8 | 4. Make your changes and make sure the swagger is valid. The editor when if it is not 9 | 5. Click on `File`, then `Convert and save as json` and update the [swagger.json](./swagger.json) file -------------------------------------------------------------------------------- /lib/handlers/get-nonce/schema/index.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import FieldValidator from '../../../util/field-validator' 3 | 4 | export const GetNonceQueryParamsJoi = Joi.object({ 5 | address: FieldValidator.isValidEthAddress().required(), 6 | chainId: FieldValidator.isValidChainId(), 7 | }) 8 | 9 | export type GetNonceQueryParams = { 10 | address: string 11 | chainId?: number 12 | } 13 | 14 | export type GetNonceResponse = { 15 | nonce: string 16 | } 17 | 18 | export const GetNonceResponseJoi = Joi.object({ 19 | nonce: FieldValidator.isValidNonce(), 20 | }) 21 | -------------------------------------------------------------------------------- /test/unit/preconditions/preconditions.test.ts: -------------------------------------------------------------------------------- 1 | import { checkDefined } from '../../../lib/preconditions/preconditions' 2 | 3 | describe('checkDefined', () => { 4 | it('throws on null value with message', async () => { 5 | expect(() => checkDefined(null, 'foo')).toThrow(new Error('foo')) 6 | }) 7 | 8 | it('throws on undefined value with message', async () => { 9 | expect(() => checkDefined(undefined, 'foo')).toThrow(new Error('foo')) 10 | }) 11 | 12 | it('returns defined value', async () => { 13 | expect(checkDefined('foo', 'bar')).toEqual('foo') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /lib/util/chain.ts: -------------------------------------------------------------------------------- 1 | export enum ChainId { 2 | MAINNET = 1, 3 | UNICHAIN = 130, 4 | BASE = 8453, 5 | OPTIMISM = 10, 6 | ARBITRUM_ONE = 42161, 7 | POLYGON = 137, 8 | SEPOLIA = 11155111, 9 | GÖRLI = 5, 10 | } 11 | 12 | // If you update SUPPORTED_CHAINS, ensure you add a corresponding RPC_${chainId} environment variable. 13 | // lib/config.py will require it to be defined. 14 | export const SUPPORTED_CHAINS = [ 15 | ChainId.MAINNET, 16 | ChainId.POLYGON, 17 | ChainId.SEPOLIA, 18 | ChainId.GÖRLI, 19 | ChainId.ARBITRUM_ONE, 20 | ChainId.BASE, 21 | ChainId.UNICHAIN, 22 | ] 23 | -------------------------------------------------------------------------------- /lib/Config.ts: -------------------------------------------------------------------------------- 1 | import { checkDefined } from './preconditions/preconditions' 2 | import { RpcUrlMap } from './RpcUrlMap' 3 | import { SUPPORTED_CHAINS } from './util/chain' 4 | 5 | type Config = { 6 | rpcUrls: RpcUrlMap 7 | } 8 | 9 | export const buildConfig = (): Config => { 10 | const rpcUrls = new RpcUrlMap() 11 | for (const chainId of SUPPORTED_CHAINS) { 12 | const url = checkDefined(process.env[`RPC_${chainId}`], `Missing env variable: RPC_${chainId}`) 13 | rpcUrls.set(chainId, url) 14 | } 15 | 16 | return { 17 | rpcUrls, 18 | } 19 | } 20 | 21 | export const CONFIG = buildConfig() 22 | -------------------------------------------------------------------------------- /test/unit/util/nonce.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { generateRandomNonce } from '../../../lib/util/nonce' 3 | 4 | const MAX_UINT256 = ethers.BigNumber.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') 5 | 6 | describe('random nonce generation test', () => { 7 | it('should generate an in-range nonce with prefixed uniswapx bits', () => { 8 | const nonceBN = ethers.BigNumber.from(generateRandomNonce()) 9 | 10 | expect(nonceBN.lt(ethers.BigNumber.from(MAX_UINT256))).toBeTruthy() 11 | expect(nonceBN.toHexString().startsWith('0x046832')).toBeTruthy() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /lib/repositories/IndexMappers/IndexMapper.ts: -------------------------------------------------------------------------------- 1 | import { ORDER_STATUS } from '../../entities' 2 | import { GetOrdersQueryParams } from '../../handlers/get-orders/schema' 3 | import { OrderEntityType } from '../base' 4 | 5 | export type IndexFieldsForUpdate = { 6 | [key: string]: string 7 | } 8 | 9 | export interface IndexMapper { 10 | getIndexFromParams(queryFilters: GetOrdersQueryParams): { index: string; partitionKey: string | number } | undefined 11 | getIndexFieldsForUpdate(order: T): IndexFieldsForUpdate 12 | getIndexFieldsForStatusUpdate(order: T, newStatus: ORDER_STATUS): IndexFieldsForUpdate 13 | } 14 | -------------------------------------------------------------------------------- /lib/util/metrics.ts: -------------------------------------------------------------------------------- 1 | import { StorageResolution, Unit } from 'aws-embedded-metrics' 2 | 3 | export interface IMetrics { 4 | putMetric(key: string, value: number, unit?: Unit | string, storageResolution?: StorageResolution | number): void 5 | } 6 | 7 | export class NullMetrics implements IMetrics { 8 | /* eslint-disable-next-line @typescript-eslint/no-empty-function */ 9 | putMetric(_key: string, _value: number, _unit?: Unit | string, _storageResolution?: StorageResolution | number) {} 10 | } 11 | 12 | export let metrics: IMetrics = new NullMetrics() 13 | 14 | export const setGlobalMetrics = (_metric: IMetrics) => { 15 | metrics = _metric 16 | } 17 | -------------------------------------------------------------------------------- /test/factories/PartialDeep.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Allow for "Partials of Partials". This allows you to do a nested partial and 3 | only provide a subset of the values. 4 | 5 | So if your type is: 6 | type Foo { 7 | bar: { 8 | cat: string, 9 | dog: string 10 | }, 11 | frog: number 12 | } 13 | A PartialDeep could be: 14 | { 15 | bar: { 16 | cat: 'Meow' 17 | } 18 | } 19 | Pulled from https://stackblitz.com/edit/typescript-49xodt?file=index.ts 20 | */ 21 | export type PartialDeep = { 22 | [attr in keyof K]?: K[attr] extends object ? PartialDeep : K[attr] 23 | } 24 | -------------------------------------------------------------------------------- /test/e2e/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers" 2 | 3 | export const ANVIL_TEST_WALLET_PK = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' 4 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' 5 | export const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3' 6 | 7 | export const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' 8 | export const UNI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' 9 | export const WETH_GOERLI = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' 10 | export const UNI_GOERLI = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' 11 | 12 | export const MAX_UINT96 = BigNumber.from('79228162514264337593543950335') 13 | -------------------------------------------------------------------------------- /lib/handlers/get-docs/index.ts: -------------------------------------------------------------------------------- 1 | import { GetDocsHandler } from './GetDocsHandler' 2 | import { GetDocsInjector } from './GetDocsInjector' 3 | import { GetDocsUIHandler } from './GetDocsUIHandler' 4 | import { GetDocsUIInjector } from './GetDocsUIInjector' 5 | 6 | const getDocsInjectorPromise = new GetDocsInjector('getDocsInjector').build() 7 | const getDocsHandler = new GetDocsHandler('get-docs', getDocsInjectorPromise) 8 | 9 | const getDocsUIInjectorPromise = new GetDocsUIInjector('getDocsUIInjector').build() 10 | const getDocsUIHandler = new GetDocsUIHandler('get-docs', getDocsUIInjectorPromise) 11 | 12 | module.exports = { 13 | getDocsHandler: getDocsHandler.handler, 14 | getDocsUIHandler: getDocsUIHandler.handler, 15 | } 16 | -------------------------------------------------------------------------------- /test/HeaderExpectation.ts: -------------------------------------------------------------------------------- 1 | export class HeaderExpectation { 2 | private headers: { [header: string]: string | number | boolean } | undefined 3 | 4 | constructor(headers: { [header: string]: string | number | boolean } | undefined) { 5 | this.headers = headers 6 | } 7 | 8 | public toReturnJsonContentType() { 9 | expect(this.headers).toHaveProperty('Content-Type', 'application/json') 10 | return this 11 | } 12 | 13 | public toAllowAllOrigin() { 14 | expect(this.headers).toHaveProperty('Access-Control-Allow-Origin', '*') 15 | return this 16 | } 17 | 18 | public toAllowCredentials() { 19 | expect(this.headers).toHaveProperty('Access-Control-Allow-Credentials', true) 20 | return this 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/factories/SDKPriorityOrderFactory.test.ts: -------------------------------------------------------------------------------- 1 | import { SDKDutchOrderV2Factory } from './SDKDutchOrderV2Factory' 2 | import { SDKDutchOrderV3Factory } from './SDKDutchOrderV3Factory' 3 | import { SDKPriorityOrderFactory } from './SDKPriorityOrderFactory' 4 | 5 | describe('SDKOrderFactories', () => { 6 | it('smoke test - builds a default DutchV2 Order', () => { 7 | expect(SDKDutchOrderV2Factory.buildDutchV2Order()).toBeDefined() 8 | }) 9 | it('smoke test - builds a default DutchV3 Order', () => { 10 | expect(SDKDutchOrderV3Factory.buildDutchV3Order()).toBeDefined() 11 | }) 12 | it('smoke test - builds a default Priority Order', () => { 13 | expect(SDKPriorityOrderFactory.buildPriorityOrder()).toBeDefined() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /lib/providers/types.ts: -------------------------------------------------------------------------------- 1 | export enum FILTER_FIELD { 2 | OFFERER = 'offerer', 3 | ORDER_STATUS = 'orderStatus', 4 | FILLER = 'filler', 5 | ORDER_TYPE = 'orderType', 6 | } 7 | 8 | export type WebhookDefinition = { 9 | /** Webhooks that are notified when the filter condition is matched. */ 10 | filter: WebhookFilterMapping 11 | /** Webhooks that are notified on every order update. */ 12 | ['*']?: Webhook[] 13 | } 14 | 15 | export type Webhook = { 16 | url: string 17 | headers?: { [key: string]: string } 18 | } 19 | 20 | export type WebhookFilterMapping = { 21 | [FILTER_FIELD.OFFERER]: { [key: string]: Webhook[] } 22 | [FILTER_FIELD.FILLER]: { [key: string]: Webhook[] } 23 | [FILTER_FIELD.ORDER_STATUS]: { [key: string]: Webhook[] } 24 | } 25 | -------------------------------------------------------------------------------- /test/integ/repositories/deleteAllRepoEntries.ts: -------------------------------------------------------------------------------- 1 | import { UniswapXOrderEntity } from '../../../lib/entities' 2 | import { BaseOrdersRepository } from '../../../lib/repositories/base' 3 | import { DYNAMO_BATCH_WRITE_MAX } from '../../../lib/util/constants' 4 | 5 | export async function deleteAllRepoEntries(ordersRepository: BaseOrdersRepository) { 6 | for (const chainId of [1, 137]) { 7 | let orders = await ordersRepository.getOrders(DYNAMO_BATCH_WRITE_MAX, { chainId }) 8 | if (!orders.orders.length) { 9 | return 10 | } 11 | do { 12 | await ordersRepository.deleteOrders(orders.orders.map((o) => o.orderHash)) 13 | } while (orders.cursor && (orders = await ordersRepository.getOrders(DYNAMO_BATCH_WRITE_MAX, { chainId }))) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "strictPropertyInitialization": true, 21 | "outDir": "dist", 22 | "typeRoots": ["./node_modules/@types"] 23 | }, 24 | "files": ["./.env.js"], 25 | "exclude": ["cdk.out", "./dist/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/services/check-order-status/util.test.ts: -------------------------------------------------------------------------------- 1 | import { calculateDutchRetryWaitSeconds } from '../../../../lib/handlers/check-order-status/util' 2 | 3 | describe('calculateDutchRetryWaitSeconds', () => { 4 | it('should do exponential backoff when retry count > 300', async () => { 5 | const response = calculateDutchRetryWaitSeconds(1, 301) 6 | expect(response).toEqual(13) 7 | }) 8 | 9 | it('should do exponential backoff when retry count > 300', async () => { 10 | const response = calculateDutchRetryWaitSeconds(1, 350) 11 | expect(response).toEqual(138) 12 | }) 13 | 14 | it('should cap exponential backoff when wait interval reaches 18000 seconds', async () => { 15 | const response = calculateDutchRetryWaitSeconds(1, 501) 16 | expect(response).toEqual(18000) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /lib/util/nonce.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | 3 | /** 4 | * uses crypto.randomBytes() under the hood to generate a 'cryptographically strong' 5 | * random data of 28 bytes and prefix that with uniswapx specific 3-byte value 6 | * (in total 248 bits, which is the number of words in the Permit2 unorderd nonceBitmap). 7 | * We then left shin total ift by 8 bits to form the complete uint256 nonce value; we do 8 | * this because we want the first nonce to land on the word boundary to save gas (clean sstore 9 | * for the next 256 nonce value) 10 | * @returns random nonce generated for new wallet addresses 11 | */ 12 | export function generateRandomNonce(): string { 13 | // TODO: store the prefix bits in an env/config file that is not open-sourced. 14 | return ethers.BigNumber.from('0x046832') 15 | .shl(224) // 28 bytes 16 | .or(ethers.BigNumber.from(ethers.utils.randomBytes(28))) 17 | .shl(8) 18 | .toString() 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "node": true, 6 | "es6": true 7 | }, 8 | "plugins": ["@typescript-eslint", "jest"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:@typescript-eslint/recommended" 13 | ], 14 | "rules": { 15 | "@typescript-eslint/no-this-alias": [ 16 | "error", 17 | { 18 | "allowDestructuring": true, // Allow `const { props, state } = this`; false by default 19 | "allowedNames": [ 20 | "self" // Allow `const self= this`; `[]` by default 21 | ] 22 | } 23 | ], 24 | "@typescript-eslint/ban-types": "warn", 25 | "jest/no-disabled-tests": "warn", 26 | "jest/no-focused-tests": "error", 27 | "jest/no-identical-title": "warn", 28 | "jest/prefer-to-have-length": "warn", 29 | "jest/valid-expect": "error" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/handlers/OrderParser.ts: -------------------------------------------------------------------------------- 1 | import { CosignedPriorityOrder, CosignedV2DutchOrder, CosignedV3DutchOrder, DutchOrder, UniswapXOrder, OrderType } from '@uniswap/uniswapx-sdk' 2 | import { ChainId } from '../util/chain' 3 | import { UniswapXOrderEntity } from '../entities' 4 | 5 | export function parseOrder(order: UniswapXOrderEntity, chainId: ChainId): UniswapXOrder { 6 | switch (order.type) { 7 | case OrderType.Dutch: 8 | case OrderType.Limit: 9 | return DutchOrder.parse(order.encodedOrder, chainId) 10 | case OrderType.Dutch_V2: 11 | return CosignedV2DutchOrder.parse(order.encodedOrder, chainId) 12 | case OrderType.Dutch_V3: 13 | return CosignedV3DutchOrder.parse(order.encodedOrder, chainId) 14 | case OrderType.Priority: 15 | return CosignedPriorityOrder.parse(order.encodedOrder, chainId) 16 | default: 17 | throw new Error(`Unsupported OrderType ${JSON.stringify(order)}, No Parser Configured`) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/util/comparison.ts: -------------------------------------------------------------------------------- 1 | import { SORT_REGEX } from './field-validator' 2 | 3 | enum COMPARISON_OPERATORS { 4 | GT = 'gt', 5 | LT = 'lt', 6 | GTE = 'gte', 7 | LTE = 'lte', 8 | BETWEEN = 'between', 9 | } 10 | 11 | export type ComparisonFilter = { 12 | operator: string 13 | values: number[] 14 | } 15 | 16 | export function parseComparisonFilter(queryParam: string | undefined): ComparisonFilter { 17 | const match = queryParam?.match(SORT_REGEX) 18 | if (!match || match.length != 4) { 19 | throw new Error(`Unable to parse operator and value for query param: ${queryParam}`) 20 | } 21 | const operator = match[1] 22 | 23 | if (!Object.values(COMPARISON_OPERATORS).includes(operator as COMPARISON_OPERATORS)) { 24 | throw new Error(`Unsupported comparison operator ${operator} in query param ${queryParam}`) 25 | } 26 | 27 | const values = match 28 | .slice(2) 29 | .map((v) => parseInt(v)) 30 | .filter((v) => !!v) 31 | 32 | return { operator: operator, values: values } 33 | } 34 | -------------------------------------------------------------------------------- /bin/stacks/kms-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib' 2 | import { CfnOutput, RemovalPolicy } from 'aws-cdk-lib' 3 | import { KeySpec, KeyUsage } from 'aws-cdk-lib/aws-kms' 4 | import { Construct } from 'constructs' 5 | 6 | export class KmsStack extends cdk.NestedStack { 7 | public readonly key: cdk.aws_kms.Key 8 | 9 | constructor(parent: Construct, name: string) { 10 | super(parent, name) 11 | 12 | /** 13 | * Unless absolutely necessary, DO NOT change this construct. 14 | * This uses the 'Retain' DeletionPolicy, which will cause the resource to be retained 15 | * in the account, but orphaned from the stack if the Key construct is ever changed. 16 | */ 17 | this.key = new cdk.aws_kms.Key(this, name, { 18 | removalPolicy: RemovalPolicy.RETAIN, 19 | keySpec: KeySpec.ECC_SECG_P256K1, 20 | keyUsage: KeyUsage.SIGN_VERIFY, 21 | alias: name, 22 | }) 23 | 24 | new CfnOutput(this, `${name}KeyId`, { 25 | value: this.key.keyId, 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bin/constants.ts: -------------------------------------------------------------------------------- 1 | import { ANALYTICS_EVENTS } from '../lib/util/analytics-events' 2 | 3 | // IMPORANT: Once this has been changed once from the original value of 'Template', 4 | // do not change again. Changing would cause every piece of infrastructure to change 5 | // name, and thus be redeployed. Should be camel case and contain no non-alphanumeric characters. 6 | export const SERVICE_NAME = 'GoudaService' 7 | export const HEALTH_CHECK_PORT = 80 8 | export const UNIMIND_ALGORITHM_CRON_INTERVAL = 15 // minutes 9 | 10 | // CloudWatch Logs subscription filter patterns 11 | export const FILTER_PATTERNS = { 12 | ORDER_POSTED: `{ $.eventType = "${ANALYTICS_EVENTS.ORDER_POSTED}" }`, 13 | UNIMIND_RESPONSE: `{ $.eventType = "${ANALYTICS_EVENTS.UNIMIND_RESPONSE}" }`, 14 | UNIMIND_PARAMETER_UPDATE: `{ $.eventType = "${ANALYTICS_EVENTS.UNIMIND_PARAMETER_UPDATE}" }`, 15 | TERMINAL_ORDER_STATE: '{ $.orderInfo.orderStatus = "filled" || $.orderInfo.orderStatus = "cancelled" }', 16 | INSUFFICIENT_FUNDS: '{ $.orderInfo.orderStatus = "insufficient-funds" }', 17 | } as const -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const ts_preset = require('ts-jest/jest-preset') 2 | const dynamo_preset = require('@shelf/jest-dynamodb/jest-preset') 3 | 4 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 5 | module.exports = { 6 | ...ts_preset, 7 | ...dynamo_preset, 8 | testEnvironment: 'node', 9 | testPathIgnorePatterns: ['bin/', 'dist/', 'cdk.out/'], 10 | coverageThreshold: { 11 | global: { 12 | statements: 80, 13 | branches: 75, 14 | functions: 80, 15 | lines: 80, 16 | }, 17 | }, 18 | transform: { 19 | // Use swc to speed up ts-jest's sluggish compilation times. 20 | // Using this cuts the initial time to compile from 6-12 seconds to 21 | // ~1 second consistently. 22 | // Inspiration from: https://github.com/kulshekhar/ts-jest/issues/259#issuecomment-1332269911 23 | // 24 | // https://swc.rs/docs/usage/jest#usage 25 | '^.+\\.(t|j)s?$': '@swc/jest', 26 | }, 27 | moduleNameMapper: { 28 | '^@uniswap/uniswapx-sdk/dist/cjs/(.*)$': '/node_modules/@uniswap/uniswapx-sdk/dist/cjs/$1' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/handlers/order-notification/schema/index.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import FieldValidator from '../../../util/field-validator' 3 | 4 | export const OrderNotificationInputJoi = Joi.object({ 5 | Records: Joi.array() 6 | .items( 7 | Joi.object({ 8 | eventName: Joi.string().required(), 9 | dynamodb: Joi.object({ 10 | NewImage: Joi.object({ 11 | filler: Joi.object({ S: FieldValidator.isValidEthAddress() }), 12 | offerer: Joi.object({ S: FieldValidator.isValidEthAddress().required() }).required(), 13 | orderHash: Joi.object({ S: FieldValidator.isValidOrderHash().required() }).required(), 14 | encodedOrder: Joi.object({ S: FieldValidator.isValidEncodedOrder().required() }).required(), 15 | orderStatus: Joi.object({ S: FieldValidator.isValidOrderStatus().required() }).required(), 16 | signature: Joi.object({ S: FieldValidator.isValidSignature().required() }).required(), 17 | }).required(), 18 | }).required(), 19 | }) 20 | ) 21 | .required(), 22 | }) 23 | -------------------------------------------------------------------------------- /test/unit/handlers/post-order/PostOrderRequestFactory.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent } from 'aws-lambda' 2 | import { QUOTE_ID, REQUEST_ID, SIGNATURE } from '../../fixtures' 3 | 4 | export class PostOrderRequestFactory { 5 | static request = ( 6 | config: { 7 | encodedOrder?: string 8 | signature?: string 9 | chainId?: number 10 | quoteId?: string 11 | requestId?: string 12 | orderType?: string 13 | } = {} 14 | ): APIGatewayProxyEvent => { 15 | const { 16 | encodedOrder = '0x01', 17 | signature = SIGNATURE, 18 | chainId = 1, 19 | quoteId = QUOTE_ID, 20 | requestId = REQUEST_ID, 21 | orderType = undefined, 22 | } = config 23 | return { 24 | queryStringParameters: {}, 25 | body: JSON.stringify({ 26 | encodedOrder, 27 | signature, 28 | chainId, 29 | quoteId, 30 | requestId, 31 | orderType, 32 | }), 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | } as any as APIGatewayProxyEvent 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/config/dynamodb.ts: -------------------------------------------------------------------------------- 1 | export enum DYNAMODB_TYPES { 2 | STRING = 'string', 3 | NUMBER = 'number', 4 | BINARY = 'binary', 5 | BOOLEAN = 'boolean', 6 | MAP = 'map', 7 | LIST = 'list', 8 | SET = 'set', 9 | } 10 | 11 | export enum TABLE_KEY { 12 | ORDER_HASH = 'orderHash', 13 | OFFERER = 'offerer', 14 | CREATED_AT = 'createdAt', 15 | NONCE = 'nonce', 16 | ENCODED_ORDER = 'encodedOrder', 17 | SIGNATURE = 'signature', 18 | SELL_TOKEN = 'sellToken', 19 | ORDER_STATUS = 'orderStatus', 20 | DEADLINE = 'deadline', 21 | CREATED_AT_MONTH = 'createdAtMonth', 22 | FILLER = 'filler', 23 | TX_HASH = 'txHash', 24 | CHAIN_ID = 'chainId', 25 | TYPE = 'type', 26 | PAIR = 'pair', 27 | 28 | // compound table keys 29 | CHAIN_ID_FILLER = 'chainId_filler', 30 | CHAIN_ID_ORDER_STATUS = 'chainId_orderStatus', 31 | CHAIN_ID_ORDER_STATUS_FILLER = 'chainId_orderStatus_filler', 32 | FILLER_OFFERER = 'filler_offerer', 33 | FILLER_OFFERER_ORDER_STATUS = 'filler_offerer_orderStatus', 34 | FILLER_ORDER_STATUS = 'filler_orderStatus', 35 | OFFERER_ORDER_STATUS = 'offerer_orderStatus', 36 | } 37 | -------------------------------------------------------------------------------- /lib/handlers/get-docs/GetDocsInjector.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 2 | import { default as bunyan, default as Logger } from 'bunyan' 3 | import { ApiInjector, ApiRInj } from '../base/index' 4 | 5 | export type RequestInjected = ApiRInj 6 | 7 | export interface ContainerInjected { 8 | [n: string]: never 9 | } 10 | 11 | export class GetDocsInjector extends ApiInjector { 12 | public async buildContainerInjected(): Promise { 13 | return {} 14 | } 15 | 16 | public async getRequestInjected( 17 | containerInjected: ContainerInjected, 18 | _requestBody: void, 19 | _requestQueryParams: void, 20 | _event: APIGatewayProxyEvent, 21 | context: Context, 22 | log: Logger 23 | ): Promise { 24 | const requestId = context.awsRequestId 25 | 26 | log = log.child({ 27 | serializers: bunyan.stdSerializers, 28 | containerInjected: containerInjected, 29 | requestId, 30 | }) 31 | 32 | return { 33 | requestId, 34 | log, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/handlers/get-docs/GetDocsUIInjector.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 2 | import { default as bunyan, default as Logger } from 'bunyan' 3 | import { ApiInjector, ApiRInj } from '../base/index' 4 | 5 | export type RequestInjected = ApiRInj 6 | 7 | export interface ContainerInjected { 8 | [n: string]: never 9 | } 10 | 11 | export class GetDocsUIInjector extends ApiInjector { 12 | public async buildContainerInjected(): Promise { 13 | return {} 14 | } 15 | 16 | public async getRequestInjected( 17 | containerInjected: ContainerInjected, 18 | _requestBody: void, 19 | _requestQueryParams: void, 20 | _event: APIGatewayProxyEvent, 21 | context: Context, 22 | log: Logger 23 | ): Promise { 24 | const requestId = context.awsRequestId 25 | 26 | log = log.child({ 27 | serializers: bunyan.stdSerializers, 28 | containerInjected: containerInjected, 29 | requestId, 30 | }) 31 | 32 | return { 33 | requestId, 34 | log, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/unit/util/comparison.test.ts: -------------------------------------------------------------------------------- 1 | import { parseComparisonFilter } from '../../../lib/util/comparison' 2 | 3 | describe('Test Comparison filter parsing', () => { 4 | it('successfully matches comparison filter with single value.', async () => { 5 | const param = 'gt(123)' 6 | const res = parseComparisonFilter(param) 7 | expect(res).toEqual({ 8 | operator: 'gt', 9 | values: [123], 10 | }) 11 | }) 12 | 13 | it('successfully matches comparison filter with two values.', async () => { 14 | const param = 'between(1,3)' 15 | const res = parseComparisonFilter(param) 16 | expect(res).toEqual({ 17 | operator: 'between', 18 | values: [1, 3], 19 | }) 20 | }) 21 | 22 | it('throws error if three comma-delimited values are present.', async () => { 23 | const param = 'between(1,2,3)' 24 | expect(() => { 25 | parseComparisonFilter(param) 26 | }).toThrowError(Error) 27 | }) 28 | 29 | it('throws error if parsed operator is not supported.', async () => { 30 | const param = 'foo(1234)' 31 | expect(() => { 32 | parseComparisonFilter(param) 33 | }).toThrowError(Error) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /lib/handlers/OnChainValidatorMap.ts: -------------------------------------------------------------------------------- 1 | import { OrderValidator, RelayOrderValidator } from '@uniswap/uniswapx-sdk' 2 | import { ChainId } from '../util/chain' 3 | 4 | export class OnChainValidatorMap { 5 | private chainIdToValidators: Map = new Map() 6 | 7 | constructor(initial: Array<[ChainId, T]> = []) { 8 | for (const [chainId, validator] of initial) { 9 | this.chainIdToValidators.set(chainId, validator) 10 | } 11 | } 12 | 13 | get(chainId: ChainId): T { 14 | const validator = this.chainIdToValidators.get(chainId) 15 | if (!validator) { 16 | throw new Error(`No onchain validator for chain ${chainId}`) 17 | } 18 | 19 | return validator 20 | } 21 | 22 | set(chainId: ChainId, validator: T): void { 23 | this.chainIdToValidators.set(chainId, validator) 24 | } 25 | 26 | debug(): { 27 | [chainId: number]: string 28 | } { 29 | const result: Record = {} 30 | 31 | for (const [chainId, validator] of this.chainIdToValidators.entries()) { 32 | result[chainId] = validator.orderQuoterAddress 33 | } 34 | return result 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/handlers/get-docs/GetDocsUIHandler.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { APIGLambdaHandler, APIHandleRequestParams, ErrorResponse, Response } from '../base/index' 3 | import { ContainerInjected, RequestInjected } from './GetDocsInjector' 4 | import SWAGGER_UI from './swagger-ui' 5 | 6 | export class GetDocsUIHandler extends APIGLambdaHandler { 7 | public async handleRequest( 8 | _params: APIHandleRequestParams 9 | ): Promise | ErrorResponse> { 10 | try { 11 | return { 12 | statusCode: 200, 13 | headers: { 14 | 'Content-Type': 'text/html', 15 | }, 16 | body: SWAGGER_UI, 17 | } 18 | } catch (e: any) { 19 | return { 20 | // TODO: differentiate between input errors 21 | statusCode: 500, 22 | errorCode: e.message, 23 | } 24 | } 25 | } 26 | 27 | protected requestBodySchema(): Joi.ObjectSchema | null { 28 | return null 29 | } 30 | 31 | protected requestQueryParamsSchema(): Joi.ObjectSchema | null { 32 | return null 33 | } 34 | 35 | protected responseBodySchema(): Joi.ObjectSchema | null { 36 | return null 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/handlers/get-docs/swagger-ui.ts: -------------------------------------------------------------------------------- 1 | const SWAGGER_UI = ` 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | SwaggerUI 13 | 14 | 15 | 16 |
17 | 18 | 19 | 32 | 33 | 34 | ` 35 | export default SWAGGER_UI 36 | -------------------------------------------------------------------------------- /lib/entities/RelayOrderEntity.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import { ORDER_STATUS, SettledAmount } from './Order' 3 | 4 | export type RelayOrderEntity = { 5 | type: OrderType.Relay 6 | orderStatus: ORDER_STATUS 7 | signature: string 8 | encodedOrder: string 9 | 10 | nonce: string 11 | orderHash: string 12 | chainId: number 13 | offerer: string 14 | reactor: string 15 | pair: string 16 | 17 | deadline: number 18 | input: { 19 | token: string 20 | amount: string 21 | recipient: string 22 | } 23 | relayFee: { 24 | token: string 25 | startAmount: string 26 | endAmount: string 27 | startTime: number 28 | endTime: number 29 | } 30 | 31 | createdAt?: number 32 | // Filler field is defined when the order has been filled and the status tracking function has recorded the filler address. 33 | filler?: string 34 | // QuoteId field is defined when the order has a quote associated with it. 35 | quoteId?: string 36 | // TxHash field is defined when the order has been filled and there is a txHash associated with the fill. 37 | txHash?: string 38 | // SettledAmount field is defined when the order has been filled and the fill amounts have been recorded. 39 | settledAmounts?: SettledAmount[] 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | // these options are overrides used only by ts-node 4 | // same as the --compilerOptions flag and the TS_NODE_COMPILER_OPTIONS environment variable 5 | "compilerOptions": { 6 | "module": "commonjs" 7 | } 8 | }, 9 | "compilerOptions": { 10 | "target": "ES2020", 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "lib": ["ES2020"], 14 | "declaration": true, 15 | "esModuleInterop": true, 16 | "resolveJsonModule": true, 17 | "strict": true, 18 | "noImplicitAny": true, 19 | "strictNullChecks": true, 20 | "noImplicitThis": true, 21 | "alwaysStrict": true, 22 | "noUnusedLocals": true /* Report errors on unused locals. */, 23 | "noUnusedParameters": true /* Report errors on unused parameters. */, 24 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 25 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 26 | "inlineSourceMap": true, 27 | "inlineSources": true, 28 | "strictPropertyInitialization": true, 29 | "outDir": "dist", 30 | "allowJs": true, 31 | "typeRoots": ["./node_modules/@types"], 32 | "types": ["node", "jest"] 33 | }, 34 | "exclude": ["cdk.out", "./dist/**/*"] 35 | } 36 | -------------------------------------------------------------------------------- /lib/handlers/get-docs/GetDocsHandler.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import swagger from '../../../swagger.json' 3 | import { APIGLambdaHandler, APIHandleRequestParams, ErrorCode, ErrorResponse, Response } from '../base/index' 4 | import { ContainerInjected, RequestInjected } from './GetDocsUIInjector' 5 | 6 | export class GetDocsHandler extends APIGLambdaHandler { 7 | public async handleRequest( 8 | params: APIHandleRequestParams 9 | ): Promise> { 10 | const { 11 | requestInjected: { log }, 12 | } = params 13 | 14 | try { 15 | return { 16 | statusCode: 200, 17 | body: swagger, 18 | } 19 | } catch (e: unknown) { 20 | log.error({ e }, 'Error getting api docs json.') 21 | return { 22 | statusCode: 500, 23 | errorCode: ErrorCode.InternalError, 24 | ...(e instanceof Error && { detail: e.message }), 25 | } 26 | } 27 | } 28 | 29 | protected requestBodySchema(): Joi.ObjectSchema | null { 30 | return null 31 | } 32 | 33 | protected requestQueryParamsSchema(): Joi.ObjectSchema | null { 34 | return null 35 | } 36 | 37 | protected responseBodySchema(): Joi.ObjectSchema | null { 38 | return null 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/handlers/get-docs.test.ts: -------------------------------------------------------------------------------- 1 | import { GetDocsHandler } from '../../../lib/handlers/get-docs/GetDocsHandler' 2 | import schema from '../../../swagger.json' 3 | import { HeaderExpectation } from '../../HeaderExpectation' 4 | 5 | describe('Testing get api docs json handler.', () => { 6 | // Creating mocks for all the handler dependencies. 7 | const requestInjectedMock = { 8 | log: { info: () => jest.fn(), error: () => jest.fn() }, 9 | } 10 | const injectorPromiseMock: any = { 11 | getContainerInjected: () => { 12 | return {} 13 | }, 14 | getRequestInjected: () => requestInjectedMock, 15 | } 16 | const event = { 17 | queryStringParameters: {}, 18 | body: null, 19 | } 20 | 21 | const getDocsHandler = new GetDocsHandler('get-api-docs', injectorPromiseMock) 22 | 23 | it('Testing valid request and response.', async () => { 24 | const getApiDocsJsonResponse = await getDocsHandler.handler(event as any, {} as any) 25 | expect(getApiDocsJsonResponse).toMatchObject({ 26 | statusCode: 200, 27 | body: JSON.stringify(schema), 28 | }) 29 | expect(getApiDocsJsonResponse.headers).not.toBeUndefined() 30 | const headerExpectation = new HeaderExpectation(getApiDocsJsonResponse.headers) 31 | headerExpectation.toAllowAllOrigin().toAllowCredentials().toReturnJsonContentType() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/unit/util/address.test.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { hasExclusiveFiller } from '../../../lib/util/address' 3 | 4 | describe('hasExclusiveFiller', () => { 5 | it('should return true for non-zero addresses', () => { 6 | expect(hasExclusiveFiller('0xabc1234567890123456789012345678901234567')).toBe(true) 7 | expect(hasExclusiveFiller('0x123456789abcdef123456789abcdef123456789a')).toBe(true) 8 | expect(hasExclusiveFiller('0xdef456789012345678901234567890123456789b')).toBe(true) 9 | }) 10 | 11 | it('should return false for zero address', () => { 12 | expect(hasExclusiveFiller(ethers.constants.AddressZero)).toBe(false) 13 | expect(hasExclusiveFiller('0x0000000000000000000000000000000000000000')).toBe(false) 14 | expect(hasExclusiveFiller('0x0000000000000000000000000000000000000000'.toUpperCase())).toBe(false) 15 | }) 16 | 17 | it('should return false for undefined or empty values', () => { 18 | expect(hasExclusiveFiller(undefined)).toBe(false) 19 | expect(hasExclusiveFiller('')).toBe(false) 20 | }) 21 | 22 | it('should be case insensitive', () => { 23 | const testAddress = '0xabc1234567890123456789012345678901234567' 24 | expect(hasExclusiveFiller(testAddress.toLowerCase())).toBe(true) 25 | expect(hasExclusiveFiller(testAddress.toUpperCase())).toBe(true) 26 | expect(hasExclusiveFiller(testAddress)).toBe(true) 27 | }) 28 | }) -------------------------------------------------------------------------------- /test/unit/providers/json-webhook-provider.test.ts: -------------------------------------------------------------------------------- 1 | import { JsonWebhookProvider } from '../../../lib/providers/json-webhook-provider' 2 | 3 | describe('JsonWebHookProvider test', () => { 4 | it('should return registed endpoints', async () => { 5 | const webhookProvider = JsonWebhookProvider.create({ 6 | filter: { 7 | filler: { 8 | '0x1': [{ url: 'webhook.com/1' }], 9 | }, 10 | orderStatus: { open: [{ url: 'webhook.com/2' }, { url: 'webhook.com/1' }] }, 11 | offerer: { '0x2': [{ url: 'webhook.com/4' }] }, 12 | }, 13 | } as any) 14 | expect( 15 | await webhookProvider.getEndpoints({ 16 | filler: '0x1', 17 | orderStatus: 'open', 18 | offerer: '0x2', 19 | } as any) 20 | ).toEqual([{ url: 'webhook.com/1' }, { url: 'webhook.com/2' }, { url: 'webhook.com/4' }]) 21 | }) 22 | }) 23 | 24 | describe('getExclusiveFillerEndpoints', () => { 25 | it('Returns endpoints for a filler', async () => { 26 | const webhookProvider = JsonWebhookProvider.create({ 27 | filter: { 28 | filler: { 29 | '0x1': [{ url: 'webhook.com/1' }], 30 | }, 31 | orderStatus: { open: [{ url: 'webhook.com/2' }, { url: 'webhook.com/3' }] }, 32 | }, 33 | } as any) 34 | expect(await webhookProvider.getExclusiveFillerEndpoints('0x1')).toEqual([{ url: 'webhook.com/1' }]) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/handlers/order-notification/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Canonical webhook order data structure 3 | * This is the standardized format for all webhook notifications 4 | */ 5 | export interface WebhookOrderData { 6 | orderHash: string 7 | createdAt: number 8 | signature: string 9 | offerer: string // Standardized field name used in webhook payloads 10 | orderStatus: string 11 | encodedOrder: string 12 | chainId: number 13 | orderType?: string 14 | quoteId?: string 15 | filler?: string 16 | } 17 | 18 | /** 19 | * Webhook order data for exclusive filler notifications 20 | * Guarantees that filler field is present and non-null 21 | */ 22 | export type ExclusiveFillerWebhookOrder = WebhookOrderData & { 23 | filler: string 24 | } 25 | 26 | /** 27 | * Logger interface for webhook operations 28 | */ 29 | export interface WebhookLogger { 30 | info: (obj: any, msg: string) => void 31 | warn: (obj: any, msg: string) => void 32 | error: (obj: any, msg: string) => void 33 | } 34 | 35 | /** 36 | * Webhook provider interface 37 | */ 38 | export interface WebhookProviderInterface { 39 | getEndpoints: (filter: { 40 | offerer: string 41 | orderStatus: string 42 | filler?: string 43 | orderType?: string 44 | }) => Promise> 45 | getExclusiveFillerEndpoints: (filler: string) => Promise> 46 | } 47 | -------------------------------------------------------------------------------- /lib/handlers/shared/sfn.ts: -------------------------------------------------------------------------------- 1 | import { SFNClient, StartExecutionCommand } from '@aws-sdk/client-sfn' 2 | import { OrderType } from '@uniswap/uniswapx-sdk' 3 | import { ORDER_STATUS } from '../../entities' 4 | import { log } from '../../Logging' 5 | import { checkDefined } from '../../preconditions/preconditions' 6 | 7 | export type OrderTrackingSfnInput = { 8 | orderHash: string 9 | chainId: number 10 | orderStatus: ORDER_STATUS 11 | quoteId: string 12 | orderType: OrderType 13 | stateMachineArn: string 14 | runIndex?: number 15 | } 16 | 17 | export async function kickoffOrderTrackingSfn( 18 | sfnInput: OrderTrackingSfnInput, 19 | stateMachineArn: string 20 | ) { 21 | log.info('starting state machine') 22 | const region = checkDefined(process.env['REGION'], 'REGION is undefined') 23 | const sfnClient = new SFNClient({ region: region }) 24 | 25 | // Use runIndex if provided, otherwise fall back to random number 26 | const BIG_NUMBER = 1000000000000 27 | const rand = Math.floor(Math.random() * BIG_NUMBER) 28 | const nameSuffix = sfnInput.runIndex !== undefined ? sfnInput.runIndex : rand 29 | const startExecutionCommand = new StartExecutionCommand({ 30 | stateMachineArn: stateMachineArn, 31 | input: JSON.stringify(sfnInput), 32 | name: sfnInput.orderHash + '_' + nameSuffix, 33 | }) 34 | log.info('Starting state machine execution', { startExecutionCommand }) 35 | await sfnClient.send(startExecutionCommand) 36 | } 37 | -------------------------------------------------------------------------------- /lib/handlers/get-limit-orders/injector.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 3 | import { DynamoDB } from 'aws-sdk' 4 | import { default as Logger } from 'bunyan' 5 | import { LimitOrdersRepository } from '../../repositories/limit-orders-repository' 6 | import { ApiInjector, ApiRInj } from '../base/index' 7 | import { GetOrdersQueryParams, RawGetOrdersQueryParams } from '../get-orders/schema' 8 | import { ContainerInjected, getSharedRequestInjected } from '../shared/get' 9 | 10 | export interface RequestInjected extends ApiRInj { 11 | limit: number 12 | queryFilters: GetOrdersQueryParams 13 | cursor?: string 14 | } 15 | 16 | export class GetLimitOrdersInjector extends ApiInjector< 17 | ContainerInjected, 18 | RequestInjected, 19 | void, 20 | RawGetOrdersQueryParams 21 | > { 22 | public async buildContainerInjected(): Promise { 23 | return { 24 | dbInterface: LimitOrdersRepository.create(new DynamoDB.DocumentClient()), 25 | } 26 | } 27 | 28 | public async getRequestInjected( 29 | containerInjected: ContainerInjected, 30 | _requestBody: void, 31 | requestQueryParams: RawGetOrdersQueryParams, 32 | _event: APIGatewayProxyEvent, 33 | context: Context, 34 | log: Logger, 35 | metrics: MetricsLogger 36 | ): Promise { 37 | return getSharedRequestInjected({ containerInjected, requestQueryParams, log, metrics, context }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bin/config.ts: -------------------------------------------------------------------------------- 1 | import { BillingMode } from 'aws-cdk-lib/aws-dynamodb' 2 | import { IndexCapacityConfig, TableCapacityConfig } from './stacks/dynamo-stack' 3 | 4 | export const PROD_INDEX_CAPACITY: IndexCapacityConfig = { 5 | orderStatus: { readCapacity: 2000, writeCapacity: 100 }, 6 | fillerOrderStatus: { readCapacity: 2000, writeCapacity: 100 }, 7 | filler: { readCapacity: 2000, writeCapacity: 100 }, 8 | offerer: { readCapacity: 2000, writeCapacity: 100 }, 9 | fillerOfferer: { readCapacity: 2000, writeCapacity: 100 }, 10 | fillerOrderStatusOfferer: { readCapacity: 2000, writeCapacity: 100 }, 11 | offererOrderStatus: { readCapacity: 2000, writeCapacity: 100 }, 12 | chainId: { readCapacity: 2000, writeCapacity: 100 }, 13 | chainIdFiller: { readCapacity: 2000, writeCapacity: 100 }, 14 | chaindIdOrderStatus: { readCapacity: 2000, writeCapacity: 100 }, 15 | chainIdFillerOrderStatus: { readCapacity: 2000, writeCapacity: 100 }, 16 | pair: { readCapacity: 2000, writeCapacity: 100 }, 17 | } 18 | 19 | export const PROD_TABLE_CAPACITY: TableCapacityConfig = { 20 | order: { billingMode: BillingMode.PAY_PER_REQUEST }, 21 | limitOrder: { billingMode: BillingMode.PAY_PER_REQUEST }, 22 | relayOrder: { billingMode: BillingMode.PAY_PER_REQUEST }, 23 | nonce: { billingMode: BillingMode.PROVISIONED, readCapacity: 2000, writeCapacity: 1000 }, 24 | quoteMetadata: { billingMode: BillingMode.PROVISIONED, readCapacity: 2000, writeCapacity: 1000 }, // TODO: Update numbers 25 | unimindParameters: { billingMode: BillingMode.PAY_PER_REQUEST }, 26 | } 27 | -------------------------------------------------------------------------------- /test/unit/builders/QueryParamsBuilder.ts: -------------------------------------------------------------------------------- 1 | import { SORT_FIELDS } from '../../../lib/entities' 2 | import { GetOrdersQueryParams } from '../../../lib/handlers/get-orders/schema' 3 | 4 | export class QueryParamsBuilder { 5 | constructor(private params: GetOrdersQueryParams = {}) {} 6 | 7 | withFiller(value?: string) { 8 | this.params.filler = value || '0xFiller' 9 | return this 10 | } 11 | 12 | withOfferer(value?: string) { 13 | this.params.offerer = value || '0xOfferer' 14 | return this 15 | } 16 | 17 | withOrderStatus(value?: string) { 18 | this.params.orderStatus = value || 'open' 19 | return this 20 | } 21 | 22 | withChainId(value?: number) { 23 | this.params.chainId = value || 1 24 | return this 25 | } 26 | 27 | withPair(value?: string) { 28 | this.params.pair = value || '0x0000000000000000000000000000000000000000-0x1111111111111111111111111111111111111111-123' 29 | return this 30 | } 31 | 32 | withDesc(value?: boolean) { 33 | if (value === undefined) { 34 | this.params.desc = true 35 | } else { 36 | this.params.desc = value 37 | } 38 | return this 39 | } 40 | 41 | withSortKey(value?: SORT_FIELDS) { 42 | this.params.sortKey = value || SORT_FIELDS.CREATED_AT 43 | return this 44 | } 45 | 46 | withSort(value?: string) { 47 | this.params.sort = value || 'desc' 48 | return this 49 | } 50 | 51 | public build(): GetOrdersQueryParams { 52 | const result = { ...this.params } 53 | this.params = {} 54 | return result 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/handlers/post-order/injector.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { APIGatewayEvent, Context } from 'aws-lambda' 3 | import { default as Logger } from 'bunyan' 4 | import { setGlobalLogger } from '../../util/log' 5 | import { setGlobalMetrics } from '../../util/metrics' 6 | import { ApiInjector, ApiRInj } from '../base' 7 | import { DEFAULT_MAX_OPEN_ORDERS, HIGH_MAX_OPEN_ORDERS, HIGH_MAX_OPEN_ORDERS_SWAPPERS } from '../constants' 8 | import { ContainerInjected as PostContainerInjected } from '../shared/post' 9 | import { PostOrderRequestBody } from './schema' 10 | 11 | export class PostOrderInjector extends ApiInjector { 12 | public async buildContainerInjected(): Promise { 13 | return {} 14 | } 15 | 16 | public async getRequestInjected( 17 | _containerInjected: PostContainerInjected, 18 | _requestBody: PostOrderRequestBody, 19 | _requestQueryParams: void, 20 | _event: APIGatewayEvent, 21 | context: Context, 22 | log: Logger, 23 | metrics: MetricsLogger 24 | ): Promise { 25 | metrics.setNamespace('Uniswap') 26 | metrics.setDimensions({ Service: 'UniswapXService' }) 27 | setGlobalMetrics(metrics) 28 | setGlobalLogger(log) 29 | 30 | return { 31 | requestId: context.awsRequestId, 32 | log, 33 | } 34 | } 35 | } 36 | 37 | export function getMaxOpenOrders(offerer: string): number { 38 | if (HIGH_MAX_OPEN_ORDERS_SWAPPERS.includes(offerer.toLowerCase())) { 39 | return HIGH_MAX_OPEN_ORDERS 40 | } 41 | 42 | return DEFAULT_MAX_OPEN_ORDERS 43 | } 44 | -------------------------------------------------------------------------------- /lib/handlers/post-limit-order/injector.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { APIGatewayEvent, Context } from 'aws-lambda' 3 | import { default as Logger } from 'bunyan' 4 | import { setGlobalLogger } from '../../util/log' 5 | import { setGlobalMetrics } from '../../util/metrics' 6 | import { ApiInjector, ApiRInj } from '../base' 7 | import { DEFAULT_MAX_OPEN_LIMIT_ORDERS, HIGH_MAX_OPEN_ORDERS, HIGH_MAX_OPEN_ORDERS_SWAPPERS } from '../constants' 8 | import { PostOrderRequestBody } from '../post-order/schema' 9 | import { ContainerInjected as PostContainerInjected } from '../shared/post' 10 | 11 | export class PostLimitOrderInjector extends ApiInjector { 12 | public async buildContainerInjected(): Promise { 13 | return {} 14 | } 15 | 16 | public async getRequestInjected( 17 | _containerInjected: PostContainerInjected, 18 | _requestBody: PostOrderRequestBody, 19 | _requestQueryParams: void, 20 | _event: APIGatewayEvent, 21 | context: Context, 22 | log: Logger, 23 | metrics: MetricsLogger 24 | ): Promise { 25 | metrics.setNamespace('Uniswap') 26 | metrics.setDimensions({ Service: 'UniswapXService' }) 27 | setGlobalMetrics(metrics) 28 | setGlobalLogger(log) 29 | 30 | return { 31 | requestId: context.awsRequestId, 32 | log, 33 | } 34 | } 35 | } 36 | 37 | export function getMaxLimitOpenOrders(offerer: string): number { 38 | if (HIGH_MAX_OPEN_ORDERS_SWAPPERS.includes(offerer.toLowerCase())) { 39 | return HIGH_MAX_OPEN_ORDERS 40 | } 41 | 42 | return DEFAULT_MAX_OPEN_LIMIT_ORDERS 43 | } 44 | -------------------------------------------------------------------------------- /lib/Metrics.ts: -------------------------------------------------------------------------------- 1 | import { Metrics, MetricUnits } from '@aws-lambda-powertools/metrics' 2 | import { SERVICE_NAME } from '../bin/constants' 3 | 4 | export const powertoolsMetric = new Metrics({ namespace: 'Uniswap', serviceName: SERVICE_NAME }) 5 | 6 | const OnChainStatusCheckerPrefix = 'OnChainStatusChecker-' 7 | export const OnChainStatusCheckerMetricNames = { 8 | TotalProcessedOpenOrders: OnChainStatusCheckerPrefix + 'TotalProcessedOpenOrders', 9 | TotalOrderProcessingErrors: OnChainStatusCheckerPrefix + 'TotalOrderProcessingErrors', 10 | TotalLoopProcessingTime: OnChainStatusCheckerPrefix + 'TotalLoopProcessingTime', 11 | LoopError: OnChainStatusCheckerPrefix + 'LoopError', 12 | LoopCompleted: OnChainStatusCheckerPrefix + 'LoopCompleted', 13 | LoopEnded: OnChainStatusCheckerPrefix + 'LoopEnded', 14 | } 15 | 16 | const CheckOrderStatusHandlerPrefix = 'CheckOrderStatusHandler-' 17 | export const CheckOrderStatusHandlerMetricNames = { 18 | StepFunctionKickedOffCount: CheckOrderStatusHandlerPrefix + 'StepFunctionKickedOffCount', 19 | GetFromDynamoTime: CheckOrderStatusHandlerPrefix + 'GetFromDynamoTime', 20 | GetBlockNumberTime: CheckOrderStatusHandlerPrefix + 'GetBlockNumberTime', 21 | GetValidationTime: CheckOrderStatusHandlerPrefix + 'GetValidationTime', 22 | GetFillEventsTime: CheckOrderStatusHandlerPrefix + 'GetFillEventsTime', 23 | } 24 | 25 | export async function wrapWithTimerMetric(promise: Promise, metricName: string): Promise { 26 | const start = Date.now() 27 | const result = await promise 28 | const end = Date.now() 29 | powertoolsMetric.addMetric(metricName, MetricUnits.Milliseconds, end - start) 30 | powertoolsMetric.publishStoredMetrics() 31 | return result 32 | } 33 | -------------------------------------------------------------------------------- /lib/handlers/EventWatcherMap.ts: -------------------------------------------------------------------------------- 1 | import { OrderType, REACTOR_ADDRESS_MAPPING, RelayEventWatcher, UniswapXEventWatcher } from '@uniswap/uniswapx-sdk' 2 | import { ethers } from 'ethers' 3 | import { CONFIG } from '../Config' 4 | import { ChainId, SUPPORTED_CHAINS } from '../util/chain' 5 | import { RPC_HEADERS } from '../util/constants' 6 | 7 | export class EventWatcherMap { 8 | private chainIdToEventWatcher: Map = new Map() 9 | 10 | constructor(initial: Array<[ChainId, T]> = []) { 11 | for (const [chainId, eventWatcher] of initial) { 12 | this.chainIdToEventWatcher.set(chainId, eventWatcher) 13 | } 14 | } 15 | 16 | get(chainId: ChainId): T { 17 | const eventWatcher = this.chainIdToEventWatcher.get(chainId) 18 | if (!eventWatcher) { 19 | throw new Error(`No eventWatcher for chain ${chainId}`) 20 | } 21 | 22 | return eventWatcher 23 | } 24 | 25 | set(chainId: ChainId, validator: T): void { 26 | this.chainIdToEventWatcher.set(chainId, validator) 27 | } 28 | 29 | public static createRelayEventWatcherMap() { 30 | const map = new EventWatcherMap() 31 | for (const chainId of SUPPORTED_CHAINS) { 32 | const address = REACTOR_ADDRESS_MAPPING[chainId][OrderType.Relay] 33 | if (!address) { 34 | throw new Error(`No Reactor Address Configured for ${chainId}, ${OrderType.Relay}`) 35 | } 36 | map.set( 37 | chainId, 38 | new RelayEventWatcher(new ethers.providers.StaticJsonRpcProvider({ 39 | url: CONFIG.rpcUrls.get(chainId), 40 | headers: RPC_HEADERS 41 | }), address) 42 | ) 43 | } 44 | return map 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/repositories/base.ts: -------------------------------------------------------------------------------- 1 | import { ORDER_STATUS, RelayOrderEntity, SettledAmount, SORT_FIELDS, UniswapXOrderEntity } from '../entities' 2 | import { GetOrdersQueryParams } from '../handlers/get-orders/schema' 3 | 4 | export const MODEL_NAME = { 5 | DUTCH: 'Order', 6 | LIMIT: 'LimitOrder', 7 | Relay: 'RelayOrder', 8 | } 9 | 10 | export type QueryResult = { 11 | orders: T[] 12 | cursor?: string 13 | } 14 | 15 | export type OrderEntityType = UniswapXOrderEntity | RelayOrderEntity 16 | 17 | export interface BaseOrdersRepository { 18 | getByHash: (hash: string) => Promise 19 | putOrderAndUpdateNonceTransaction: (order: T) => Promise 20 | countOrdersByOffererAndStatus: (offerer: string, orderStatus: ORDER_STATUS) => Promise 21 | getOrders: (limit: number, queryFilters: GetOrdersQueryParams, cursor?: string) => Promise> 22 | getOrdersFilteredByType: ( 23 | limit: number, 24 | queryFilters: GetOrdersQueryParams, 25 | types: string[], 26 | cursor?: string 27 | ) => Promise> 28 | getByOfferer: (offerer: string, limit: number) => Promise> 29 | getByOrderStatus: ( 30 | orderStatus: string, 31 | limit: number, 32 | cursor?: string, 33 | sortKey?: SORT_FIELDS, 34 | sort?: string, 35 | desc?: boolean 36 | ) => Promise> 37 | getNonceByAddressAndChain: (address: string, chainId: number) => Promise 38 | updateOrderStatus: ( 39 | orderHash: string, 40 | status: ORDER_STATUS, 41 | txHash?: string, 42 | fillBlock?: number, 43 | settledAmounts?: SettledAmount[] 44 | ) => Promise 45 | deleteOrders: (orderHashes: string[]) => Promise 46 | } 47 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/Common.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import FieldValidator from "../../../util/field-validator"; 3 | 4 | export const SettledAmountsValidation = Joi.array().items( 5 | Joi.object({ 6 | tokenOut: FieldValidator.isValidEthAddress(), 7 | amountOut: FieldValidator.isValidAmount(), 8 | tokenIn: FieldValidator.isValidEthAddress(), 9 | amountIn: FieldValidator.isValidAmount(), 10 | }) 11 | ); 12 | 13 | export const RouteValidation = Joi.object({ 14 | quote: FieldValidator.isValidAmount(), 15 | quoteGasAdjusted: FieldValidator.isValidAmount(), 16 | gasPriceWei: FieldValidator.isValidAmount(), 17 | gasUseEstimateQuote: FieldValidator.isValidAmount(), 18 | gasUseEstimate: FieldValidator.isValidAmount(), 19 | methodParameters: Joi.object({ 20 | calldata: Joi.string(), 21 | value: Joi.string(), 22 | to: FieldValidator.isValidEthAddress(), 23 | }), 24 | }) 25 | 26 | export const CommonOrderValidationFields = { 27 | encodedOrder: FieldValidator.isValidEncodedOrder().required(), 28 | signature: FieldValidator.isValidSignature().required(), 29 | orderStatus: FieldValidator.isValidOrderStatus().required(), 30 | orderHash: FieldValidator.isValidOrderHash().required(), 31 | chainId: FieldValidator.isValidChainId().required(), 32 | swapper: FieldValidator.isValidEthAddress().required(), 33 | txHash: FieldValidator.isValidTxHash(), 34 | quoteId: FieldValidator.isValidQuoteId(), 35 | requestId: FieldValidator.isValidRequestId(), 36 | nonce: FieldValidator.isValidNonce(), 37 | cosignature: Joi.string(), 38 | createdAt: Joi.number(), 39 | settledAmounts: SettledAmountsValidation, 40 | route: RouteValidation, 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/injector.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 3 | import { DynamoDB } from 'aws-sdk' 4 | import { default as Logger } from 'bunyan' 5 | import { UniswapXOrderEntity } from '../../entities' 6 | import { BaseOrdersRepository } from '../../repositories/base' 7 | import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository' 8 | import { ApiInjector, ApiRInj } from '../base/index' 9 | import { getSharedRequestInjected } from '../shared/get' 10 | import { GetOrdersQueryParams, RawGetOrdersQueryParams } from './schema' 11 | import { GetOrderTypeQueryParamEnum } from './schema/GetOrderTypeQueryParamEnum' 12 | 13 | export interface RequestInjected extends ApiRInj { 14 | limit: number 15 | queryFilters: GetOrdersQueryParams 16 | cursor?: string 17 | orderType?: GetOrderTypeQueryParamEnum 18 | executeAddress?: string 19 | } 20 | 21 | export interface ContainerInjected { 22 | dbInterface: BaseOrdersRepository 23 | } 24 | 25 | export class GetOrdersInjector extends ApiInjector { 26 | public async buildContainerInjected(): Promise { 27 | return { 28 | dbInterface: DutchOrdersRepository.create(new DynamoDB.DocumentClient()), 29 | } 30 | } 31 | 32 | public async getRequestInjected( 33 | containerInjected: ContainerInjected, 34 | _requestBody: void, 35 | requestQueryParams: RawGetOrdersQueryParams, 36 | _event: APIGatewayProxyEvent, 37 | context: Context, 38 | log: Logger, 39 | metrics: MetricsLogger 40 | ): Promise { 41 | return getSharedRequestInjected({ containerInjected, requestQueryParams, log, metrics, context }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/models/DutchV1Order.ts: -------------------------------------------------------------------------------- 1 | import { DutchOrder as SDKDutchOrder, OrderType } from '@uniswap/uniswapx-sdk' 2 | import { ORDER_STATUS, UniswapXOrderEntity } from '../entities' 3 | import { Order } from './Order' 4 | 5 | export class DutchV1Order extends Order { 6 | constructor( 7 | readonly inner: SDKDutchOrder, 8 | readonly signature: string, 9 | readonly chainId: number, 10 | readonly quoteId?: string, 11 | readonly requestId?: string 12 | ) { 13 | super() 14 | } 15 | 16 | get orderType(): OrderType { 17 | return OrderType.Dutch 18 | } 19 | 20 | public toEntity(orderStatus: ORDER_STATUS): UniswapXOrderEntity { 21 | const { input, outputs } = this.inner.info 22 | const decodedOrder = this.inner 23 | const order: UniswapXOrderEntity = { 24 | type: OrderType.Dutch, 25 | encodedOrder: decodedOrder.serialize(), 26 | signature: this.signature, 27 | nonce: decodedOrder.info.nonce.toString(), 28 | orderHash: decodedOrder.hash().toLowerCase(), 29 | chainId: decodedOrder.chainId, 30 | orderStatus: orderStatus, 31 | offerer: decodedOrder.info.swapper.toLowerCase(), 32 | input: { 33 | token: input.token, 34 | startAmount: input.startAmount.toString(), 35 | endAmount: input.endAmount.toString(), 36 | }, 37 | outputs: outputs.map((output) => ({ 38 | token: output.token, 39 | startAmount: output.startAmount.toString(), 40 | endAmount: output.endAmount.toString(), 41 | recipient: output.recipient.toLowerCase(), 42 | })), 43 | reactor: decodedOrder.info.reactor.toLowerCase(), 44 | decayStartTime: decodedOrder.info.decayStartTime, 45 | decayEndTime: decodedOrder.info.deadline, 46 | deadline: decodedOrder.info.deadline, 47 | filler: decodedOrder.info?.exclusiveFiller.toLowerCase(), 48 | } 49 | 50 | return order 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/models/LimitOrder.ts: -------------------------------------------------------------------------------- 1 | import { DutchOrder as SDKDutchOrder, OrderType } from '@uniswap/uniswapx-sdk' 2 | import { ORDER_STATUS, UniswapXOrderEntity } from '../entities' 3 | import { Order } from './Order' 4 | 5 | export class LimitOrder extends Order { 6 | constructor( 7 | readonly inner: SDKDutchOrder, 8 | readonly signature: string, 9 | readonly chainId: number, 10 | readonly quoteId?: string, 11 | readonly requestId?: string 12 | ) { 13 | super() 14 | } 15 | 16 | get orderType(): OrderType { 17 | return OrderType.Limit 18 | } 19 | 20 | public toEntity(orderStatus: ORDER_STATUS): UniswapXOrderEntity { 21 | const { input, outputs } = this.inner.info 22 | const decodedOrder = this.inner 23 | const order: UniswapXOrderEntity = { 24 | type: OrderType.Dutch, 25 | encodedOrder: decodedOrder.serialize(), 26 | signature: this.signature, 27 | nonce: decodedOrder.info.nonce.toString(), 28 | orderHash: decodedOrder.hash().toLowerCase(), 29 | chainId: decodedOrder.chainId, 30 | orderStatus: orderStatus, 31 | offerer: decodedOrder.info.swapper.toLowerCase(), 32 | input: { 33 | token: input.token, 34 | startAmount: input.startAmount.toString(), 35 | endAmount: input.endAmount.toString(), 36 | }, 37 | outputs: outputs.map((output) => ({ 38 | token: output.token, 39 | startAmount: output.startAmount.toString(), 40 | endAmount: output.endAmount.toString(), 41 | recipient: output.recipient.toLowerCase(), 42 | })), 43 | reactor: decodedOrder.info.reactor.toLowerCase(), 44 | decayStartTime: decodedOrder.info.decayStartTime, 45 | decayEndTime: decodedOrder.info.deadline, 46 | deadline: decodedOrder.info.deadline, 47 | filler: decodedOrder.info?.exclusiveFiller.toLowerCase(), 48 | } 49 | 50 | return order 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/handlers/constants.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../util/chain' 2 | 3 | export const HIGH_MAX_OPEN_ORDERS_SWAPPERS: string[] = [ 4 | // canaries 5 | '0xa7152fad7467857dc2d4060fecaadf9f6b8227d3', 6 | '0xf82af5cd1f0d24cdcf9d35875107d5e43ce9b3d0', 7 | '0xa50dac48d61bb52b339c7ef0dcefa7688338d00a', 8 | '0x5b062dc717983be67f7e1b44a6557d7da7d399bd', 9 | // integ tests 10 | '0xe001e6f6879c07b9ac24291a490f2795106d348c', 11 | '0x8943ea25bbfe135450315ab8678f2f79559f4630', 12 | ] 13 | export const DEFAULT_MAX_OPEN_ORDERS = 5 14 | export const DEFAULT_MAX_OPEN_LIMIT_ORDERS = 100 15 | export const HIGH_MAX_OPEN_ORDERS = 200 16 | 17 | export const PRIORITY_ORDER_TARGET_BLOCK_BUFFER: Record = { 18 | [ChainId.MAINNET]: 3, 19 | [ChainId.UNICHAIN]: 4, 20 | [ChainId.BASE]: 3, 21 | [ChainId.OPTIMISM]: 3, 22 | [ChainId.ARBITRUM_ONE]: 3, 23 | [ChainId.POLYGON]: 3, 24 | [ChainId.SEPOLIA]: 3, 25 | [ChainId.GÖRLI]: 3, 26 | } 27 | 28 | export const DUTCHV2_ORDER_LATENCY_THRESHOLD_SEC = 20; 29 | 30 | export const UR_EXECUTE_SELECTOR = "24856bc3" 31 | export const UR_EXECUTE_WITH_DEADLINE_SELECTOR = "3593564c" 32 | export const UR_EXECUTE_FUNCTION = "execute" 33 | export const UR_FUNCTION_SIGNATURES: Record = { 34 | [UR_EXECUTE_SELECTOR]: "function execute(bytes commands, bytes[] inputs)", 35 | [UR_EXECUTE_WITH_DEADLINE_SELECTOR]: "function execute(bytes commands, bytes[] inputs, uint256 deadline)" 36 | }; 37 | export const UR_EXECUTE_DEADLINE_BUFFER = 60; // Seconds to extend calldata deadline 38 | export const UR_UNWRAP_WETH_PARAMETERS = ['address', 'uint256'] 39 | export const UR_SWEEP_PARAMETERS = ['address', 'address', 'uint256'] 40 | export const UR_ACTIONS_PARAMETERS = ['bytes', 'bytes[]'] 41 | export const UR_TAKE_PARAMETERS = ['address', 'address', 'uint256'] 42 | 43 | // Constants for hex string manipulation 44 | export const HEX_PREFIX = "0x"; 45 | export const HEX_BASE = 16; 46 | export const CHARS_PER_BYTE = 2; 47 | export const UR_SELECTOR_BYTES = 4; 48 | export const UR_BYTES_PER_ACTION = 2; -------------------------------------------------------------------------------- /lib/handlers/get-unimind/schema/index.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { QuoteMetadata } from '../../../repositories/quote-metadata-repository' 3 | import { TradeType } from '../../../util/constants' 4 | 5 | // Route struct we expect after parsing 6 | const routeSchema = Joi.object({ 7 | quote: Joi.string().required(), 8 | quoteGasAdjusted: Joi.string().required(), 9 | gasPriceWei: Joi.string().required(), 10 | gasUseEstimateQuote: Joi.string().required(), 11 | gasUseEstimate: Joi.string().required(), 12 | methodParameters: Joi.object({ 13 | calldata: Joi.string().required(), 14 | value: Joi.string().required(), 15 | to: Joi.string().required(), 16 | }).required(), 17 | }) 18 | 19 | export const unimindQueryParamsSchema = Joi.object({ 20 | quoteId: Joi.string().required(), 21 | pair: Joi.string().required(), 22 | referencePrice: Joi.string().required(), 23 | priceImpact: Joi.number().required(), 24 | blockNumber: Joi.number().optional(), 25 | route: Joi.string() 26 | .optional() 27 | .custom((value, helpers) => { 28 | try { 29 | const parsed = JSON.parse(value) 30 | const { error } = routeSchema.validate(parsed) 31 | if (error) { 32 | return helpers.error('string.routeInvalid') 33 | } 34 | return value 35 | } catch (err) { 36 | return helpers.error('string.invalidJson') 37 | } 38 | }, 'validate route JSON') 39 | .messages({ 40 | 'string.invalidJson': 'route must be a valid JSON string', 41 | 'string.routeInvalid': 'route structure is invalid after parsing', 42 | }), 43 | logOnly: Joi.boolean().optional().truthy('true').falsy('false').sensitive(), 44 | // All other values are rejected for a 400 error 45 | swapper: Joi.string().optional(), 46 | tradeType: Joi.string().optional().valid(TradeType.EXACT_INPUT, TradeType.EXACT_OUTPUT), 47 | }) 48 | 49 | export type UnimindQueryParams = Omit & { 50 | route: string // route is now a JSON string to be used as a GET query param 51 | logOnly?: boolean 52 | swapper?: string 53 | } 54 | -------------------------------------------------------------------------------- /lib/handlers/order-notification/injector.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { DynamoDBStreamEvent } from 'aws-lambda' 3 | import { default as bunyan, default as Logger } from 'bunyan' 4 | import { checkDefined } from '../../preconditions/preconditions' 5 | import { WebhookProvider } from '../../providers/base' 6 | import { S3WebhookConfigurationProvider } from '../../providers/s3-webhook-provider' 7 | import { BETA_WEBHOOK_CONFIG_KEY, PRODUCTION_WEBHOOK_CONFIG_KEY, WEBHOOK_CONFIG_BUCKET } from '../../util/constants' 8 | import { setGlobalLogger } from '../../util/log' 9 | import { setGlobalMetrics } from '../../util/metrics' 10 | import { STAGE } from '../../util/stage' 11 | import { DynamoStreamInjector } from '../base/dynamo-stream-handler' 12 | 13 | export interface RequestInjected { 14 | log: Logger 15 | event: DynamoDBStreamEvent 16 | } 17 | 18 | export interface ContainerInjected { 19 | webhookProvider: WebhookProvider 20 | } 21 | 22 | export class OrderNotificationInjector extends DynamoStreamInjector { 23 | public async buildContainerInjected(): Promise { 24 | const stage = checkDefined(process.env['stage'], 'stage should be defined in the .env') 25 | const s3Key = stage === STAGE.BETA ? BETA_WEBHOOK_CONFIG_KEY : PRODUCTION_WEBHOOK_CONFIG_KEY 26 | const webhookProvider = new S3WebhookConfigurationProvider(`${WEBHOOK_CONFIG_BUCKET}-${stage}-1`, s3Key) 27 | return { webhookProvider } 28 | } 29 | 30 | public async getRequestInjected( 31 | containerInjected: ContainerInjected, 32 | event: DynamoDBStreamEvent, 33 | log: Logger, 34 | metrics: MetricsLogger 35 | ): Promise { 36 | log = log.child({ 37 | serializers: bunyan.stdSerializers, 38 | containerInjected: containerInjected, 39 | }) 40 | metrics.setNamespace('Uniswap') 41 | metrics.setDimensions({ Service: 'UniswapXService' }) 42 | setGlobalMetrics(metrics) 43 | setGlobalLogger(log) 44 | 45 | return { 46 | log, 47 | event: event, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/providers/s3-webhook-provider.ts: -------------------------------------------------------------------------------- 1 | import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3' 2 | import { checkDefined } from '../preconditions/preconditions' 3 | import { OrderFilter, WebhookProvider } from './base' 4 | import { findEndpointsMatchingFilter } from './json-webhook-provider' 5 | import { FILTER_FIELD, Webhook, WebhookDefinition } from './types' 6 | 7 | export class S3WebhookConfigurationProvider implements WebhookProvider { 8 | private static UPDATE_ENDPOINTS_PERIOD_MS = 5 * 60000 9 | 10 | private cachedDefinition: WebhookDefinition | null 11 | private lastUpdatedEndpointsTimestamp: number 12 | 13 | constructor(private bucket: string, private key: string) { 14 | this.cachedDefinition = null 15 | this.lastUpdatedEndpointsTimestamp = Date.now() 16 | } 17 | 18 | // get registered endpoints for a filter set 19 | public async getEndpoints(filter: OrderFilter): Promise { 20 | const definition = await this.getDefinition() 21 | return findEndpointsMatchingFilter(filter, definition) 22 | } 23 | 24 | public async getExclusiveFillerEndpoints(filler: string): Promise { 25 | const definition = await this.getDefinition() 26 | return definition.filter[FILTER_FIELD.FILLER][filler] ?? [] 27 | } 28 | 29 | async getDefinition(): Promise { 30 | // if we already have a cached one just return it 31 | if ( 32 | this.cachedDefinition !== null && 33 | Date.now() - this.lastUpdatedEndpointsTimestamp < S3WebhookConfigurationProvider.UPDATE_ENDPOINTS_PERIOD_MS 34 | ) { 35 | return this.cachedDefinition 36 | } 37 | 38 | const s3Client = new S3Client({}) 39 | const s3Res = await s3Client.send( 40 | new GetObjectCommand({ 41 | Bucket: this.bucket, 42 | Key: this.key, 43 | }) 44 | ) 45 | const s3Body = checkDefined(s3Res.Body, 's3Res.Body is undefined') 46 | this.cachedDefinition = JSON.parse(await s3Body.transformToString()) as WebhookDefinition 47 | this.lastUpdatedEndpointsTimestamp = Date.now() 48 | return this.cachedDefinition 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/handlers/get-nonce/injector.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { APIGatewayProxyEvent, Context } from 'aws-lambda' 3 | import { DynamoDB } from 'aws-sdk' 4 | import { default as bunyan, default as Logger } from 'bunyan' 5 | import { UniswapXOrderEntity } from '../../entities' 6 | import { BaseOrdersRepository } from '../../repositories/base' 7 | import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository' 8 | import { setGlobalLogger } from '../../util/log' 9 | import { setGlobalMetrics } from '../../util/metrics' 10 | import { ApiInjector, ApiRInj } from '../base/index' 11 | import { GetNonceQueryParams } from './schema/index' 12 | 13 | export interface RequestInjected extends ApiRInj { 14 | address: string 15 | chainId: number 16 | } 17 | 18 | export interface ContainerInjected { 19 | dbInterface: BaseOrdersRepository 20 | } 21 | 22 | export class GetNonceInjector extends ApiInjector { 23 | public async buildContainerInjected(): Promise { 24 | return { 25 | dbInterface: DutchOrdersRepository.create(new DynamoDB.DocumentClient()), 26 | } 27 | } 28 | 29 | public async getRequestInjected( 30 | containerInjected: ContainerInjected, 31 | _requestBody: void, 32 | requestQueryParams: GetNonceQueryParams, 33 | _event: APIGatewayProxyEvent, 34 | context: Context, 35 | log: Logger, 36 | metrics: MetricsLogger 37 | ): Promise { 38 | const requestId = context.awsRequestId 39 | 40 | metrics.setNamespace('Uniswap') 41 | metrics.setDimensions({ Service: 'UniswapXService' }) 42 | setGlobalMetrics(metrics) 43 | 44 | log = log.child({ 45 | serializers: bunyan.stdSerializers, 46 | containerInjected: containerInjected, 47 | requestId, 48 | }) 49 | 50 | setGlobalLogger(log) 51 | 52 | return { 53 | log, 54 | requestId, 55 | address: requestQueryParams.address, 56 | chainId: requestQueryParams.chainId ?? 1, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/GetPriorityOrderResponse.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import Joi from 'joi' 3 | import FieldValidator from '../../../util/field-validator' 4 | import { GetDutchV2OrderResponse } from './GetDutchV2OrderResponse' 5 | import { Route } from '../../../repositories/quote-metadata-repository' 6 | import { CommonOrderValidationFields } from './Common' 7 | 8 | export type GetPriorityOrderResponse = Omit & { 9 | type: OrderType.Priority 10 | input: { 11 | token: string 12 | amount: string 13 | mpsPerPriorityFeeWei: string 14 | } 15 | outputs: { 16 | token: string 17 | amount: string 18 | mpsPerPriorityFeeWei: string 19 | recipient: string 20 | }[] 21 | cosignerData: { 22 | auctionTargetBlock: number 23 | } 24 | auctionStartBlock: number 25 | baselinePriorityFeeWei: string 26 | cosignature: string 27 | nonce: string 28 | quoteId: string | undefined 29 | requestId: string | undefined 30 | createdAt: number | undefined 31 | route: Route | undefined 32 | } 33 | 34 | export const PriorityCosignerDataJoi = Joi.object({ 35 | auctionTargetBlock: Joi.number(), 36 | }) 37 | 38 | export const GetPriorityOrderResponseEntryJoi = Joi.object({ 39 | ...CommonOrderValidationFields, 40 | type: Joi.string().valid(OrderType.Priority).required(), 41 | input: Joi.object({ 42 | token: FieldValidator.isValidEthAddress().required(), 43 | amount: FieldValidator.isValidAmount().required(), 44 | mpsPerPriorityFeeWei: FieldValidator.isValidAmount().required(), 45 | }), 46 | outputs: Joi.array().items( 47 | Joi.object({ 48 | token: FieldValidator.isValidEthAddress().required(), 49 | amount: FieldValidator.isValidAmount().required(), 50 | mpsPerPriorityFeeWei: FieldValidator.isValidAmount().required(), 51 | recipient: FieldValidator.isValidEthAddress().required(), 52 | }) 53 | ), 54 | auctionStartBlock: Joi.number().min(0), 55 | baselinePriorityFeeWei: FieldValidator.isValidAmount(), 56 | cosignerData: PriorityCosignerDataJoi, 57 | }) 58 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # UniswapX Service 2 | 3 | ## Overview 4 | 5 | TypeScript API service for propagating signed UniswapX orders. Swappers post signed orders which fillers can fetch for execution. Built with AWS CDK for infrastructure. 6 | 7 | ## Commands 8 | 9 | ```bash 10 | yarn && yarn build # Install dependencies and compile 11 | yarn test # Run unit tests 12 | yarn test:integ # Run integration tests (requires Java) 13 | yarn test:e2e # Run end-to-end tests (requires deployed API) 14 | yarn lint # ESLint check 15 | yarn fix # Auto-fix lint and prettier issues 16 | yarn coverage # Run tests with coverage 17 | cdk deploy GoudaServiceStack # Deploy to AWS 18 | ``` 19 | 20 | ## Key Dependencies 21 | 22 | 23 | 24 | - **@uniswap/uniswapx-sdk** - UniswapX order types and encoding 25 | - **@uniswap/permit2-sdk** - Permit2 signature validation 26 | - **aws-cdk-lib** - AWS infrastructure as code 27 | - **dynamodb-toolbox** - DynamoDB ORM utilities 28 | - **joi** - Request validation schemas 29 | - **bunyan** - Structured logging 30 | - **axios** - HTTP client for webhooks 31 | 32 | ## Project Structure 33 | 34 | - `bin/` - CDK app entry and stack definitions 35 | - `lib/handlers/` - Lambda handlers (get-orders, post-order, check-status, etc.) 36 | - `lib/models/` - Order types (DutchV1/V2/V3, Priority, Relay, Limit) 37 | - `lib/repositories/` - DynamoDB repositories 38 | - `lib/services/` - Business logic (OrderDispatcher, UniswapXOrderService) 39 | - `lib/util/` - Validators, helpers, constants 40 | - `test/` - Unit, integration, and e2e tests 41 | 42 | ## Environment Variables 43 | 44 | Required for deployment: 45 | - `RPC_1`, `RPC_137`, `RPC_42161`, `RPC_10` - Chain RPC URLs 46 | - `FAILED_EVENT_DESTINATION_ARN` - Failed event SNS ARN 47 | 48 | For tests: 49 | - `UNISWAP_API` - Deployed API URL (e2e tests) 50 | - `LABS_COSIGNER` - Valid EVM address (unit tests) 51 | 52 | ## Auto-Update Instructions 53 | 54 | After changes to files in this directory, run `/update-claude-md` to keep this documentation synchronized with the codebase. 55 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/GetRelayOrderResponse.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import Joi from 'joi' 3 | import { ORDER_STATUS } from '../../../entities' 4 | import FieldValidator from '../../../util/field-validator' 5 | import { SettledAmount } from './GetOrdersResponse' 6 | 7 | export type GetRelayOrderResponse = { 8 | type: OrderType.Relay 9 | orderStatus: ORDER_STATUS 10 | signature: string 11 | encodedOrder: string 12 | 13 | orderHash: string 14 | chainId: number 15 | swapper: string 16 | reactor: string 17 | 18 | deadline: number 19 | input: { 20 | token: string 21 | amount: string 22 | recipient: string 23 | } 24 | relayFee: { 25 | token: string 26 | startAmount: string 27 | endAmount: string 28 | startTime: number 29 | endTime: number 30 | } 31 | } 32 | 33 | export const RelayOrderResponseEntryJoi = Joi.object({ 34 | encodedOrder: FieldValidator.isValidEncodedOrder().required(), 35 | signature: FieldValidator.isValidSignature().required(), 36 | chainId: FieldValidator.isValidChainId().required(), 37 | orderStatus: FieldValidator.isValidOrderStatus().required(), 38 | orderHash: FieldValidator.isValidOrderHash().required(), 39 | swapper: FieldValidator.isValidEthAddress().required(), 40 | //apply to only relay 41 | type: Joi.string().valid(OrderType.Relay).required(), 42 | 43 | createdAt: FieldValidator.isValidCreatedAt(), 44 | txHash: FieldValidator.isValidTxHash(), 45 | input: { 46 | token: FieldValidator.isValidEthAddress(), 47 | amount: FieldValidator.isValidAmount(), 48 | recipient: FieldValidator.isValidEthAddress(), 49 | }, 50 | relayFee: { 51 | token: FieldValidator.isValidEthAddress(), 52 | startAmount: FieldValidator.isValidAmount(), 53 | endAmount: FieldValidator.isValidAmount(), 54 | startTime: Joi.number(), 55 | endTime: Joi.number(), 56 | }, 57 | 58 | settledAmounts: Joi.array().items(SettledAmount), 59 | }) 60 | 61 | export const GetRelayOrdersResponseJoi = Joi.object({ 62 | orders: Joi.array().items(RelayOrderResponseEntryJoi), 63 | cursor: FieldValidator.isValidCursor(), 64 | }) 65 | -------------------------------------------------------------------------------- /lib/providers/json-webhook-provider.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import { OrderFilter, WebhookProvider } from './base' 3 | import { FILTER_FIELD, Webhook, WebhookDefinition } from './types' 4 | 5 | export class JsonWebhookProvider implements WebhookProvider { 6 | static create(jsonDocument: WebhookDefinition): JsonWebhookProvider { 7 | return new JsonWebhookProvider(jsonDocument) 8 | } 9 | 10 | private constructor(private readonly jsonDocument: WebhookDefinition) {} 11 | 12 | // get registered endpoints for a filter set 13 | public async getEndpoints(filter: OrderFilter): Promise { 14 | return findEndpointsMatchingFilter(filter, this.jsonDocument) 15 | } 16 | 17 | public async getExclusiveFillerEndpoints(filler: string): Promise { 18 | return this.jsonDocument.filter[FILTER_FIELD.FILLER][filler] ?? [] 19 | } 20 | } 21 | 22 | export function findEndpointsMatchingFilter(filter: OrderFilter, definition: WebhookDefinition): Webhook[] { 23 | const endpoints: Webhook[] = [] 24 | 25 | const catchallEndpoints = definition['*'] ?? [] 26 | endpoints.push(...catchallEndpoints) 27 | 28 | // remove limit orders orders when matching webhooks 29 | // webhook is currently used only to fill dutch, dutch_v2, and dutch_v3 orders 30 | if (filter.orderType !== OrderType.Limit) { 31 | const supportedFilterKeys: (FILTER_FIELD.FILLER | FILTER_FIELD.OFFERER | FILTER_FIELD.ORDER_STATUS)[] = [ 32 | FILTER_FIELD.FILLER, 33 | FILTER_FIELD.ORDER_STATUS, 34 | FILTER_FIELD.OFFERER, 35 | ] 36 | const filterMapping = definition.filter 37 | for (const filterKey of supportedFilterKeys) { 38 | const filterValue = filter[filterKey] 39 | if (filterValue && Object.keys(filterMapping[filterKey]).includes(filterValue)) { 40 | const filterEndpoints = filterMapping[filterKey][filterValue] 41 | endpoints.push(...filterEndpoints) 42 | } 43 | } 44 | } 45 | 46 | const urls: Set = new Set() 47 | return endpoints.filter((endpoint) => { 48 | if (urls.has(endpoint.url)) { 49 | return false 50 | } 51 | urls.add(endpoint.url) 52 | return true 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /lib/handlers/get-unimind/injector.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayEvent, Context } from 'aws-lambda' 2 | import { default as Logger } from 'bunyan' 3 | import { ApiInjector, ApiRInj } from '../base' 4 | import { MetricsLogger } from 'aws-embedded-metrics' 5 | import { setGlobalLogger } from '../../util/log' 6 | import { setGlobalMetrics } from '../../util/metrics' 7 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 8 | import { DynamoQuoteMetadataRepository, QuoteMetadataRepository } from '../../repositories/quote-metadata-repository' 9 | import { DynamoUnimindParametersRepository, UnimindParametersRepository } from '../../repositories/unimind-parameters-repository' 10 | import { UnimindQueryParams } from './schema' 11 | import { AnalyticsService } from '../../services/analytics-service' 12 | 13 | export type RequestInjected = ApiRInj 14 | export interface ContainerInjected { 15 | quoteMetadataRepository: QuoteMetadataRepository 16 | unimindParametersRepository: UnimindParametersRepository 17 | analyticsService: AnalyticsService 18 | } 19 | 20 | export class GetUnimindInjector extends ApiInjector { 21 | private readonly documentClient: DocumentClient 22 | 23 | constructor(name: string) { 24 | super(name) 25 | this.documentClient = new DocumentClient() 26 | } 27 | 28 | public async buildContainerInjected(): Promise { 29 | return { 30 | quoteMetadataRepository: DynamoQuoteMetadataRepository.create(this.documentClient), 31 | unimindParametersRepository: DynamoUnimindParametersRepository.create(this.documentClient), 32 | analyticsService: AnalyticsService.create() 33 | } 34 | } 35 | 36 | public async getRequestInjected( 37 | _containerInjected: ContainerInjected, 38 | _requestBody: void, 39 | _requestQueryParams: UnimindQueryParams, 40 | _event: APIGatewayEvent, 41 | context: Context, 42 | log: Logger, 43 | metrics: MetricsLogger 44 | ): Promise { 45 | metrics.setNamespace('Uniswap') 46 | metrics.setDimensions({ Service: 'UniswapXService' }) 47 | setGlobalMetrics(metrics) 48 | setGlobalLogger(log) 49 | return { requestId: context.awsRequestId, log } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | cdk.out/ 10 | cache/ 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # AWS CDK 110 | cdk.out 111 | 112 | -------------------------------------------------------------------------------- /test/factories/SDKRelayOrderFactory.ts: -------------------------------------------------------------------------------- 1 | import { RelayOrder as SDKRelayOrder, RelayOrderBuilder, RelayOrderInfoJSON } from '@uniswap/uniswapx-sdk' 2 | import { BigNumber } from 'ethers' 3 | import { ChainId } from '../../lib/util/chain' 4 | import { Tokens } from '../unit/fixtures' 5 | import { PartialDeep } from './PartialDeep' 6 | 7 | /** 8 | * Helper class for building RelayOrders. 9 | * All values adpated from https://github.com/Uniswap/uniswapx-sdk/blob/7949043e7d2434553f84f588e1405e87d249a5aa/src/builder/RelayOrderBuilder.test.ts#L30 10 | */ 11 | export class SDKRelayOrderFactory { 12 | static buildRelayOrder(chainId = ChainId.MAINNET, overrides: PartialDeep = {}): SDKRelayOrder { 13 | // Values adapted from https://github.com/Uniswap/uniswapx-sdk/blob/7949043e7d2434553f84f588e1405e87d249a5aa/src/utils/order.test.ts#L28 14 | const nowInSeconds = Math.floor(Date.now() / 1000) 15 | 16 | // Arbitrary default future time ten seconds in future 17 | const futureTime = nowInSeconds + 10 18 | 19 | let builder = new RelayOrderBuilder(chainId) 20 | 21 | builder = builder 22 | .deadline(overrides.deadline ?? futureTime) 23 | .swapper(overrides.swapper ?? '0x0000000000000000000000000000000000000001') 24 | .nonce(overrides.nonce ? BigNumber.from(overrides.nonce) : BigNumber.from(100)) 25 | .universalRouterCalldata(overrides.universalRouterCalldata ?? '0x') 26 | .input({ 27 | token: overrides.input?.token ?? Tokens.MAINNET.WETH, 28 | amount: overrides.input?.amount ? BigNumber.from(overrides.input?.amount) : BigNumber.from('1000000'), 29 | recipient: overrides.input?.recipient ?? '0x0000000000000000000000000000000000000000', 30 | }) 31 | .fee({ 32 | token: overrides.fee?.token ?? Tokens.MAINNET.WETH, 33 | startAmount: overrides.fee?.startAmount 34 | ? BigNumber.from(overrides.fee?.startAmount) 35 | : BigNumber.from('1000000'), 36 | endAmount: overrides.fee?.endAmount ? BigNumber.from(overrides.fee?.endAmount) : BigNumber.from('1000000'), 37 | startTime: overrides.fee?.startTime ?? nowInSeconds, 38 | endTime: overrides.fee?.endTime ?? futureTime, 39 | }) 40 | return builder.build() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/factories/SDKDutchOrderV1Factory.test.ts: -------------------------------------------------------------------------------- 1 | import { SDKDutchOrderFactory } from './SDKDutchOrderV1Factory' 2 | 3 | describe('SDKDutchOrderV1Factory', () => { 4 | describe('Dutch Order', () => { 5 | it('smoke test - builds a default Dutch Order', () => { 6 | expect(SDKDutchOrderFactory.buildDutchOrder(1)).toBeDefined() 7 | }) 8 | 9 | it('smoke test - accepts multiple outputs', () => { 10 | expect( 11 | SDKDutchOrderFactory.buildDutchOrder(1, { 12 | outputs: [ 13 | { 14 | startAmount: '20', 15 | endAmount: '10', 16 | token: '0xabc', 17 | recipient: '0def', 18 | }, 19 | { 20 | startAmount: '40', 21 | endAmount: '30', 22 | token: '0xghi', 23 | recipient: '0jkl', 24 | }, 25 | ], 26 | }) 27 | ).toBeDefined() 28 | }) 29 | }) 30 | 31 | describe('Limit Order', () => { 32 | it('smoke test - builds a default Limit order', () => { 33 | expect(SDKDutchOrderFactory.buildLimitOrder(1)).toBeDefined() 34 | }) 35 | 36 | it('smoke test - accepts multiple outputs are provided', () => { 37 | expect( 38 | SDKDutchOrderFactory.buildLimitOrder(1, { 39 | outputs: [ 40 | { 41 | startAmount: '10', 42 | endAmount: '10', 43 | token: '0xabc', 44 | recipient: '0def', 45 | }, 46 | { 47 | startAmount: '20', 48 | endAmount: '20', 49 | token: '0xghi', 50 | recipient: '0jkl', 51 | }, 52 | ], 53 | }) 54 | ).toBeDefined() 55 | }) 56 | 57 | it('throws if an override output has mismatched start and end amounts', () => { 58 | expect(() => 59 | SDKDutchOrderFactory.buildLimitOrder(1, { 60 | outputs: [ 61 | { 62 | startAmount: '10', 63 | endAmount: '20', 64 | token: '0xabc', 65 | recipient: '0def', 66 | }, 67 | ], 68 | }) 69 | ).toThrow('Limit order with output overrides must have matching startAmount + endAmount') 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /lib/config/unimind-list.ts: -------------------------------------------------------------------------------- 1 | import { ChainId, Token } from '@uniswap/sdk-core' 2 | 3 | const USDC_ARBITRUM = new Token( 4 | ChainId.ARBITRUM_ONE, 5 | '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', 6 | 6, 7 | 'USDC', 8 | 'USD Coin' 9 | ) 10 | const USDT_ARBITRUM = new Token( 11 | ChainId.ARBITRUM_ONE, 12 | '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', 13 | 6, 14 | 'USDT', 15 | 'Tether USD' 16 | ) 17 | const ZRO_ARBITRUM = new Token( 18 | ChainId.ARBITRUM_ONE, 19 | '0x6985884C4392D348587B19cb9eAAf157F13271cd', 20 | 18, 21 | 'ZRO', 22 | 'LayerZero' 23 | ) 24 | const ARB_ARBITRUM = new Token( 25 | ChainId.ARBITRUM_ONE, 26 | '0x912CE59144191C1204E64559FE8253a0e49E6548', 27 | 18, 28 | 'ARB', 29 | 'Arbitrum' 30 | ) 31 | const WETH_ARBITRUM = new Token( 32 | ChainId.ARBITRUM_ONE, 33 | '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 34 | 18, 35 | 'WETH', 36 | 'Wrapped Ether' 37 | ) 38 | const WBTC_ARBITRUM = new Token( 39 | ChainId.ARBITRUM_ONE, 40 | '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f', 41 | 8, 42 | 'WBTC', 43 | 'Wrapped BTC' 44 | ) 45 | const LINK_ARBITRUM = new Token( 46 | ChainId.ARBITRUM_ONE, 47 | '0xf97f4df75117a78c1A5a0DBb814Af92458539FB4', 48 | 18, 49 | 'LINK', 50 | 'ChainLink Token' 51 | ) 52 | 53 | const CRV_ARBITRUM = new Token(ChainId.ARBITRUM_ONE, '0x11cDb42B0EB46D95f990BeDD4695A6e3fA034978', 18, 'CRV', 'Curve') 54 | const GRT_ARBITRUM = new Token( 55 | ChainId.ARBITRUM_ONE, 56 | '0x9623063377AD1B27544C965cCd7342f7EA7e88C7', 57 | 18, 58 | 'GRT', 59 | 'The Graph' 60 | ) 61 | const GMX_ARBITRUM = new Token(ChainId.ARBITRUM_ONE, '0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a', 18, 'GMX', 'GMX') 62 | const AAVE_ARBITRUM = new Token( 63 | ChainId.ARBITRUM_ONE, 64 | '0xba5DdD1f9d7F570dc94a51479a000E3BCE967196', 65 | 18, 66 | 'AAVE', 67 | 'Aave Token' 68 | ) 69 | const PENDLE_ARBITRUM = new Token( 70 | ChainId.ARBITRUM_ONE, 71 | '0x0c880f6761F1af8d9Aa9C466984b80DAb9a8c9e8', 72 | 18, 73 | 'PENDLE', 74 | 'Pendle' 75 | ) 76 | 77 | export const UNIMIND_LIST = [ 78 | USDC_ARBITRUM, 79 | USDT_ARBITRUM, 80 | ZRO_ARBITRUM, 81 | ARB_ARBITRUM, 82 | WETH_ARBITRUM, 83 | CRV_ARBITRUM, 84 | GRT_ARBITRUM, 85 | GMX_ARBITRUM, 86 | AAVE_ARBITRUM, 87 | PENDLE_ARBITRUM, 88 | LINK_ARBITRUM, 89 | WBTC_ARBITRUM, 90 | ] 91 | -------------------------------------------------------------------------------- /bin/stacks/cron-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib' 2 | import * as aws_events from 'aws-cdk-lib/aws-events' 3 | import * as aws_iam from 'aws-cdk-lib/aws-iam' 4 | import * as aws_lambda_nodejs from 'aws-cdk-lib/aws-lambda-nodejs' 5 | import * as aws_logs from 'aws-cdk-lib/aws-logs' 6 | import { Construct } from 'constructs' 7 | import path from 'path' 8 | 9 | import { SERVICE_NAME, UNIMIND_ALGORITHM_CRON_INTERVAL, FILTER_PATTERNS } from '../constants' 10 | 11 | export interface CronStackProps extends cdk.NestedStackProps { 12 | lambdaRole: aws_iam.Role 13 | chatbotSNSArn?: string 14 | envVars?: { [key: string]: string } 15 | } 16 | 17 | export class CronStack extends cdk.NestedStack { 18 | public readonly unimindAlgorithmCronLambda?: aws_lambda_nodejs.NodejsFunction 19 | 20 | constructor(scope: Construct, name: string, props: CronStackProps) { 21 | super(scope, name, props) 22 | const { lambdaRole } = props 23 | 24 | this.unimindAlgorithmCronLambda = new aws_lambda_nodejs.NodejsFunction(this, 'unimindAlgorithmCronLambda', { 25 | role: lambdaRole, 26 | runtime: cdk.aws_lambda.Runtime.NODEJS_20_X, 27 | entry: path.join(__dirname, '../../lib/crons/unimind-algorithm.ts'), 28 | handler: 'handler', 29 | timeout: cdk.Duration.minutes(1), 30 | memorySize: 512, 31 | bundling: { 32 | minify: true, 33 | sourceMap: true, 34 | }, 35 | environment: { 36 | NODE_OPTIONS: '--enable-source-maps', 37 | }, 38 | }) 39 | 40 | new aws_events.Rule(this, `${SERVICE_NAME}UnimindAlgorithmCron`, { 41 | schedule: aws_events.Schedule.rate(cdk.Duration.minutes(UNIMIND_ALGORITHM_CRON_INTERVAL)), 42 | targets: [new cdk.aws_events_targets.LambdaFunction(this.unimindAlgorithmCronLambda)], 43 | }) 44 | 45 | // Subscription filter for UnimindParameterUpdate analytics events 46 | if (props.envVars && props.envVars['UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN']) { 47 | new aws_logs.CfnSubscriptionFilter(this, 'UnimindParameterUpdateSub', { 48 | destinationArn: props.envVars['UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN'], 49 | filterPattern: FILTER_PATTERNS.UNIMIND_PARAMETER_UPDATE, 50 | logGroupName: this.unimindAlgorithmCronLambda.logGroup.logGroupName, 51 | }) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /bin/definitions/order-tracking-sfn.json: -------------------------------------------------------------------------------- 1 | { 2 | "StartAt": "checkOrderStatus", 3 | "States": { 4 | "checkOrderStatus": { 5 | "Next": "latestOrderStatus", 6 | "Retry": [ 7 | { 8 | "ErrorEquals": [ 9 | "States.TaskFailed", 10 | "Lambda.ServiceException", 11 | "Lambda.AWSLambdaException", 12 | "Lambda.SdkClientException" 13 | ], 14 | "IntervalSeconds": 2, 15 | "MaxAttempts": 100, 16 | "BackoffRate": 2 17 | } 18 | ], 19 | "Catch": [ 20 | { 21 | "ErrorEquals": ["States.ALL"], 22 | "ResultPath": "$.errorInfo", 23 | "Next": "orderInFailedState" 24 | } 25 | ], 26 | "Type": "Task", 27 | "Resource": "arn:aws:states:::lambda:invoke", 28 | "Parameters": { 29 | "FunctionName": "${checkOrderStatusLambdaArn}", 30 | "Payload.$": "$" 31 | } 32 | }, 33 | "latestOrderStatus": { 34 | "Type": "Choice", 35 | "InputPath": "$.Payload", 36 | "Choices": [ 37 | { 38 | "Or": [ 39 | { 40 | "Variable": "$.orderStatus", 41 | "StringEquals": "cancelled" 42 | }, 43 | { 44 | "Variable": "$.orderStatus", 45 | "StringEquals": "filled" 46 | }, 47 | { 48 | "Variable": "$.orderStatus", 49 | "StringEquals": "expired" 50 | }, 51 | { 52 | "Variable": "$.orderStatus", 53 | "StringEquals": "error" 54 | } 55 | ], 56 | "Next": "orderInTerminalState" 57 | }, 58 | { 59 | "Variable": "$.retryCount", 60 | "NumericGreaterThan": 301, 61 | "Next": "orderRetried" 62 | } 63 | ], 64 | "Default": "waitStep" 65 | }, 66 | "waitStep": { 67 | "Type": "Wait", 68 | "SecondsPath": "$.retryWaitSeconds", 69 | "Next": "checkOrderStatus" 70 | }, 71 | "orderInFailedState": { 72 | "Type": "Fail", 73 | "Error": "Order in failed state" 74 | }, 75 | "orderInTerminalState": { 76 | "Type": "Succeed" 77 | }, 78 | "orderRetried": { 79 | "Type": "Succeed" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/util/Permit2Validator.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers' 2 | import { OrderValidation, UniswapXOrder } from '@uniswap/uniswapx-sdk' 3 | import { ChainId } from './chain' 4 | import { permit2Address, SignatureProvider, PermitTransferFrom, TokenPermissions } from '@uniswap/permit2-sdk' 5 | 6 | export interface ValidationContext { 7 | chainId: ChainId 8 | currentBlock: number 9 | currentTimestamp: number 10 | provider: ethers.providers.Provider 11 | } 12 | 13 | /** 14 | * Permit2Validator performs on-chain validation checks for UniswapX orders. 15 | * It checks for Expired and NonceUsed conditions. 16 | * 17 | * This validator is designed to be used in scenarios where the OrderQuoter is not 18 | * usable (e.g. for permissioned tokens). 19 | */ 20 | export class Permit2Validator { 21 | private provider: ethers.providers.Provider 22 | private chainId: ChainId 23 | 24 | constructor(provider: ethers.providers.Provider, chainId: ChainId) { 25 | this.provider = provider 26 | this.chainId = chainId 27 | } 28 | 29 | /** 30 | * Validates an order for all supported validation checks 31 | * @param order - The UniswapX order to validate 32 | * @returns Promise 33 | */ 34 | public async validate( 35 | order: UniswapXOrder 36 | ): Promise { 37 | 38 | // Check if order deadline has passed 39 | const currentTimestamp = Math.floor(Date.now() / 1000) 40 | if (currentTimestamp > order.info.deadline) { 41 | return OrderValidation.Expired 42 | } 43 | 44 | const address = permit2Address(this.chainId) 45 | const signatureProvider = new SignatureProvider(this.provider, address) 46 | 47 | // Construct PermitTransferFrom from order data 48 | const permitTransferFrom: PermitTransferFrom = { 49 | permitted: { 50 | token: order.info.input.token, 51 | amount: 0 // Amount is not used in validation 52 | } as TokenPermissions, 53 | spender: order.info.swapper, 54 | nonce: order.info.nonce, 55 | deadline: order.info.deadline 56 | } 57 | 58 | const permitValidation = await signatureProvider.validatePermit(permitTransferFrom) 59 | 60 | if (permitValidation.isUsed) { 61 | return OrderValidation.NonceUsed 62 | } 63 | 64 | if (permitValidation.isExpired) { 65 | return OrderValidation.Expired 66 | } 67 | 68 | return OrderValidation.OK 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/e2e/nonce.test.ts: -------------------------------------------------------------------------------- 1 | import { DutchOrderBuilder } from '@uniswap/uniswapx-sdk' 2 | import axios from 'axios' 3 | import dotenv from 'dotenv' 4 | import { BigNumber, ethers } from 'ethers' 5 | import { checkDefined } from '../../lib/preconditions/preconditions' 6 | import { ANVIL_TEST_WALLET_PK, ZERO_ADDRESS } from './constants' 7 | dotenv.config() 8 | 9 | const URL = checkDefined(process.env.UNISWAPX_SERVICE_URL, 'UNISWAPX_SERVICE_URL must be defined') 10 | 11 | const wallet = new ethers.Wallet(ANVIL_TEST_WALLET_PK) 12 | const amount = BigNumber.from(10).pow(18) 13 | 14 | axios.defaults.timeout = 10000 15 | 16 | xdescribe('get nonce', () => { 17 | it('should get current nonce for address, and increment it by one after the address posts an order', async () => { 18 | const address = (await wallet.getAddress()).toLowerCase() 19 | const getResponse = await axios.get(`${URL}dutch-auction/nonce?address=${address}`) 20 | expect(getResponse.status).toEqual(200) 21 | const nonce = BigNumber.from(getResponse.data.nonce) 22 | expect(nonce.lt(ethers.constants.MaxUint256)).toBeTruthy() 23 | 24 | const deadline = Math.round(new Date().getTime() / 1000) + 10 25 | const order = new DutchOrderBuilder(1) 26 | .deadline(deadline) 27 | .decayEndTime(deadline) 28 | .decayStartTime(deadline - 5) 29 | .swapper(await wallet.getAddress()) 30 | .nonce(nonce.add(1)) 31 | .input({ 32 | token: ZERO_ADDRESS, 33 | startAmount: amount, 34 | endAmount: amount, 35 | }) 36 | .output({ 37 | token: ZERO_ADDRESS, 38 | startAmount: amount, 39 | endAmount: amount, 40 | recipient: address, 41 | }) 42 | .build() 43 | 44 | const { domain, types, values } = order.permitData() 45 | const signature = await wallet._signTypedData(domain, types, values) 46 | const postResponse = await axios.post(`${URL}dutch-auction/order`, { 47 | encodedOrder: order.serialize(), 48 | signature: signature, 49 | chainId: 1, 50 | }) 51 | 52 | expect(postResponse.status).toEqual(201) 53 | // orderHash = postResponse.data.hash 54 | const newGetResponse = await axios.get(`${URL}dutch-auction/nonce?address=${address}`) 55 | expect(newGetResponse.status).toEqual(200) 56 | const newNonce = BigNumber.from(newGetResponse.data.nonce) 57 | expect(newNonce.eq(nonce.add(1))).toBeTruthy() 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniswapX Service 2 | 3 | [![Unit Tests](https://github.com/Uniswap/uniswapx-service/actions/workflows/CI.yml/badge.svg)](https://github.com/Uniswap/uniswapx-service/actions/workflows/CI.yml) 4 | 5 | UniswapX Service is an API to propagate signed, executable UniswapX orders. Swappers can post their signed orders which can be fetched by fillers for execution. 6 | 7 | ## Getting Started 8 | 9 | 1. Install and build the package 10 | ``` 11 | yarn && yarn build 12 | ``` 13 | 2. To deploy the API to your AWS account run: 14 | 15 | ``` 16 | cdk deploy GoudaServiceStack 17 | ``` 18 | 19 | Once complete it will output the url of your api: 20 | 21 | ``` 22 | GoudaServiceStack.Url = https://... 23 | ``` 24 | 25 | 3. (optional) To run dynamo-db integration tests, you need to have Java Runtime installed (https://www.java.com/en/download/manual.jsp). 26 | 27 | ## End-to-end Tests 28 | 29 | 1. Deploy your API using the intructions above. 30 | 31 | 1. Add your API url to your `.env` file as `UNISWAP_API` 32 | 33 | ``` 34 | UNISWAP_API='' 35 | ``` 36 | 37 | 1. Run the tests with: 38 | ``` 39 | yarn test:e2e 40 | ``` 41 | 42 | ## Development Cycle 43 | 44 | To test your changes you must redeploy your service. The dev cycle is thus: 45 | 46 | 1. Make code changes. Make sure all env variables are present in the .env file: 47 | 48 | ``` 49 | FAILED_EVENT_DESTINATION_ARN=<> 50 | RPC_1=<> 51 | RPC_5=<> 52 | RPC_137=<> 53 | RPC_11155111=<> 54 | RPC_42161=<> 55 | RPC_10=<> 56 | 57 | # Only need these if testing against custom contract deployments 58 | DL_REACTOR_TENDERLY=<> 59 | QUOTER_TENDERLY=<> 60 | PERMIT_TENDERLY=<> 61 | 62 | # Only needed to run tests 63 | LABS_COSIGNER= # needed for certain unit tests 64 | ``` 65 | 66 | 1. `yarn build && cdk deploy GoudaServiceStack` 67 | 68 | 1. `yarn test:e2e` 69 | 70 | 1. If failures, look at logs in Cloudwatch Insights 71 | 72 | 1. Repeat 73 | 74 | ## Order Notification Schema 75 | 76 | Depending on the filler preferences, the notification webhook can POST orders with a specific exclusive filler address or all new orders. The following schema is what the filler execution endpoint can expect to receive. 77 | 78 | ``` 79 | { 80 | orderHash: string, 81 | createdAt: number, 82 | signature: string, 83 | offerer: string, 84 | orderStatus: string, 85 | encodedOrder: string, 86 | chainId: number, 87 | quoteId?: string, 88 | filler?: string, 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /test/unit/handlers/check-order-status/util.test.ts: -------------------------------------------------------------------------------- 1 | import { StaticJsonRpcProvider } from '@ethersproject/providers' 2 | import { OrderType } from '@uniswap/uniswapx-sdk' 3 | import { mock } from 'jest-mock-extended' 4 | import { getWatcher } from '../../../../lib/handlers/check-order-status/util' 5 | 6 | describe('getWatcher', () => { 7 | test('works with OrderType.Dutch', () => { 8 | const watcher = getWatcher(mock(), 1, OrderType.Dutch) 9 | expect(watcher).toBeDefined() 10 | }) 11 | 12 | test('works with OrderType.Dutch_V2', () => { 13 | const watcher = getWatcher(mock(), 1, OrderType.Dutch_V2) 14 | expect(watcher).toBeDefined() 15 | }) 16 | 17 | test('works with OrderType.Dutch_V3', () => { 18 | const watcher = getWatcher(mock(), 42161, OrderType.Dutch_V3) 19 | expect(watcher).toBeDefined() 20 | }) 21 | 22 | test('works with OrderType.Limit', () => { 23 | const watcher = getWatcher(mock(), 1, OrderType.Limit) 24 | expect(watcher).toBeDefined() 25 | }) 26 | 27 | test('caches already used UniswapXEventWatcher', () => { 28 | const watcher = getWatcher(mock(), 1, OrderType.Dutch) 29 | const watcher2 = getWatcher(mock(), 1, OrderType.Dutch) 30 | 31 | expect(watcher).toBe(watcher2) 32 | }) 33 | 34 | test('caches Dutch and Limit as the same', () => { 35 | const watcher = getWatcher(mock(), 1, OrderType.Dutch) 36 | const watcher2 = getWatcher(mock(), 1, OrderType.Limit) 37 | 38 | expect(watcher).toBe(watcher2) 39 | }) 40 | 41 | test('does not mix up cached values', () => { 42 | const watcher = getWatcher(mock(), 1, OrderType.Dutch) 43 | const watcher2 = getWatcher(mock(), 1, OrderType.Dutch_V2) 44 | 45 | expect(watcher).not.toBe(watcher2) 46 | }) 47 | 48 | test('does not mix up cached chainIds', () => { 49 | const watcher = getWatcher(mock(), 1, OrderType.Dutch) 50 | const watcher2 = getWatcher(mock(), 137, OrderType.Dutch) 51 | 52 | expect(watcher).not.toBe(watcher2) 53 | }) 54 | 55 | test('throws an error with no reactor mapping', () => { 56 | expect(() => { 57 | getWatcher(mock(), 1, 'someOtherType' as OrderType) 58 | }).toThrow(`No Reactor Address Defined in UniswapX SDK for chainId:1, orderType:someOtherType`) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/GetDutchV2OrderResponse.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import Joi from 'joi' 3 | import { ORDER_STATUS } from '../../../entities' 4 | import FieldValidator from '../../../util/field-validator' 5 | import { Route } from '../../../repositories/quote-metadata-repository' 6 | import { CommonOrderValidationFields } from './Common' 7 | 8 | export type GetDutchV2OrderResponse = { 9 | type: OrderType.Dutch_V2 10 | orderStatus: ORDER_STATUS 11 | signature: string 12 | encodedOrder: string 13 | 14 | orderHash: string 15 | chainId: number 16 | swapper: string 17 | reactor: string 18 | 19 | txHash: string | undefined 20 | deadline: number 21 | input: { 22 | token: string 23 | startAmount: string 24 | endAmount: string 25 | } 26 | outputs: { 27 | token: string 28 | startAmount: string 29 | endAmount: string 30 | recipient: string 31 | }[] 32 | settledAmounts: { 33 | tokenOut: string 34 | amountOut: string 35 | tokenIn: string 36 | amountIn: string 37 | }[] | undefined 38 | cosignerData: { 39 | decayStartTime: number 40 | decayEndTime: number 41 | exclusiveFiller: string 42 | inputOverride: string 43 | outputOverrides: string[] 44 | } 45 | cosignature: string 46 | nonce: string 47 | quoteId: string | undefined 48 | requestId: string | undefined 49 | createdAt: number | undefined 50 | route: Route | undefined 51 | } 52 | 53 | export const CosignerDataJoi = Joi.object({ 54 | decayStartTime: Joi.number(), 55 | decayEndTime: Joi.number(), 56 | exclusiveFiller: FieldValidator.isValidEthAddress(), 57 | inputOverride: FieldValidator.isValidAmount(), 58 | outputOverrides: Joi.array().items(FieldValidator.isValidAmount()), 59 | }) 60 | 61 | export const GetDutchV2OrderResponseEntryJoi = Joi.object({ 62 | ...CommonOrderValidationFields, 63 | type: Joi.string().valid(OrderType.Dutch_V2).required(), 64 | input: Joi.object({ 65 | token: FieldValidator.isValidEthAddress().required(), 66 | startAmount: FieldValidator.isValidAmount().required(), 67 | endAmount: FieldValidator.isValidAmount().required(), 68 | }), 69 | outputs: Joi.array().items( 70 | Joi.object({ 71 | token: FieldValidator.isValidEthAddress().required(), 72 | startAmount: FieldValidator.isValidAmount().required(), 73 | endAmount: FieldValidator.isValidAmount().required(), 74 | recipient: FieldValidator.isValidEthAddress().required(), 75 | }) 76 | ), 77 | cosignerData: CosignerDataJoi, 78 | }) 79 | -------------------------------------------------------------------------------- /test/unit/builders/QueryParamsBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { SORT_FIELDS } from '../../../lib/entities' 2 | import { QueryParamsBuilder } from './QueryParamsBuilder' 3 | 4 | describe('QueryParamsBuilder', () => { 5 | test('withFiller undefined sets default value', () => { 6 | const queryParams = new QueryParamsBuilder().withFiller().build() 7 | expect(queryParams.filler).toEqual('0xFiller') 8 | }) 9 | test('withFiller set', () => { 10 | const queryParams = new QueryParamsBuilder().withFiller('other').build() 11 | expect(queryParams.filler).toEqual('other') 12 | }) 13 | 14 | test('withOfferer undefined sets default value', () => { 15 | const queryParams = new QueryParamsBuilder().withOfferer().build() 16 | expect(queryParams.offerer).toEqual('0xOfferer') 17 | }) 18 | test('withOfferer set', () => { 19 | const queryParams = new QueryParamsBuilder().withOfferer('other').build() 20 | expect(queryParams.offerer).toEqual('other') 21 | }) 22 | 23 | test('withOrderStatus undefined sets default value', () => { 24 | const queryParams = new QueryParamsBuilder().withOrderStatus().build() 25 | expect(queryParams.orderStatus).toEqual('open') 26 | }) 27 | test('withOrderStatus set', () => { 28 | const queryParams = new QueryParamsBuilder().withOrderStatus('filled').build() 29 | expect(queryParams.orderStatus).toEqual('filled') 30 | }) 31 | 32 | test('withDesc undefined sets default value', () => { 33 | const queryParams = new QueryParamsBuilder().withDesc().build() 34 | expect(queryParams.desc).toEqual(true) 35 | }) 36 | test('withDesc false', () => { 37 | const queryParams = new QueryParamsBuilder().withDesc(false).build() 38 | expect(queryParams.desc).toEqual(false) 39 | }) 40 | 41 | test('withSortKey undefined sets default value', () => { 42 | const queryParams = new QueryParamsBuilder().withSortKey().build() 43 | expect(queryParams.sortKey).toEqual('createdAt') 44 | }) 45 | test('withSortKey set', () => { 46 | //only 1 sort field currently 47 | const queryParams = new QueryParamsBuilder().withSortKey(SORT_FIELDS.CREATED_AT).build() 48 | expect(queryParams.sortKey).toEqual('createdAt') 49 | }) 50 | 51 | test('withSort undefined sets default value', () => { 52 | const queryParams = new QueryParamsBuilder().withSort().build() 53 | expect(queryParams.sort).toEqual('desc') 54 | }) 55 | test('withSort set', () => { 56 | const queryParams = new QueryParamsBuilder().withSort('asc').build() 57 | expect(queryParams.sort).toEqual('asc') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /lib/repositories/unimind-parameters-repository.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | import Logger from 'bunyan' 3 | import { Entity, Table } from 'dynamodb-toolbox' 4 | import { DYNAMODB_TYPES } from '../config/dynamodb' 5 | import { TABLE_NAMES } from './util' 6 | 7 | export interface UnimindParameters { 8 | pair: string 9 | intrinsicValues: string 10 | count: number 11 | version: number 12 | batchNumber: number // Tracks parameter update iterations 13 | lastUpdatedAt?: number // Unix timestamp for update tracking 14 | } 15 | 16 | export interface UnimindParametersRepository { 17 | put(values: UnimindParameters): Promise 18 | getByPair(pair: string): Promise 19 | } 20 | 21 | export class DynamoUnimindParametersRepository implements UnimindParametersRepository { 22 | private readonly entity: Entity 23 | 24 | static create(documentClient: DocumentClient): UnimindParametersRepository { 25 | const log = Logger.createLogger({ 26 | name: 'UnimindParametersRepository', 27 | serializers: Logger.stdSerializers, 28 | }) 29 | 30 | const table = new Table({ 31 | name: TABLE_NAMES.UnimindParameters, 32 | partitionKey: 'pair', 33 | DocumentClient: documentClient, 34 | }) 35 | 36 | const entity = new Entity({ 37 | name: 'UnimindParameters', 38 | attributes: { 39 | pair: { partitionKey: true, type: DYNAMODB_TYPES.STRING }, 40 | intrinsicValues: { type: DYNAMODB_TYPES.STRING, required: true }, 41 | count: { type: DYNAMODB_TYPES.NUMBER, required: true }, 42 | version: { type: DYNAMODB_TYPES.NUMBER, required: true }, 43 | batchNumber: { type: DYNAMODB_TYPES.NUMBER, required: true, default: 0 }, 44 | lastUpdatedAt: { type: DYNAMODB_TYPES.NUMBER, required: false }, 45 | }, 46 | table, 47 | } as const) 48 | 49 | return new DynamoUnimindParametersRepository(entity, log) 50 | } 51 | 52 | constructor(entity: Entity, private readonly log: Logger) { 53 | this.entity = entity 54 | } 55 | 56 | async put(values: UnimindParameters): Promise { 57 | try { 58 | await this.entity.put(values) 59 | } catch (error) { 60 | this.log.error({ error, values }, 'Failed to put unimind parameters') 61 | throw error 62 | } 63 | } 64 | 65 | async getByPair(pair: string): Promise { 66 | const result = await this.entity.get({ pair }, { execute: true }) 67 | return result.Item as UnimindParameters | undefined 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bin/stacks/reaper-stack.ts: -------------------------------------------------------------------------------- 1 | import { aws_ecs, aws_iam, Stack, StackProps } from "aws-cdk-lib"; 2 | import { DockerImageAsset, Platform } from "aws-cdk-lib/aws-ecr-assets"; 3 | import { Cluster, ContainerImage } from "aws-cdk-lib/aws-ecs"; 4 | import { Construct } from "constructs"; 5 | import { SERVICE_NAME } from "../constants"; 6 | 7 | // Expect RPC_[chainId] to be set in environmentVariables 8 | export interface ReaperStackProps extends StackProps { 9 | environmentVariables: { [key: string]: string }; 10 | } 11 | 12 | export class ReaperStack extends Stack { 13 | public readonly logDriver: aws_ecs.AwsLogDriver; 14 | 15 | constructor(scope: Construct, id: string, props: ReaperStackProps) { 16 | super(scope, id, props); 17 | 18 | const { environmentVariables } = props; 19 | 20 | this.logDriver = new aws_ecs.AwsLogDriver({ 21 | streamPrefix: `${SERVICE_NAME}-ReaperStack`, 22 | }); 23 | 24 | const cluster = new Cluster(this, `ReaperCluster`); 25 | 26 | const reaperRole = new aws_iam.Role(this, `ReaperStackRole`, { 27 | assumedBy: new aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"), 28 | managedPolicies: [ 29 | aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess'), 30 | aws_iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchFullAccess"), 31 | ], 32 | }); 33 | 34 | const taskDefinition = new aws_ecs.FargateTaskDefinition( 35 | this, 36 | `ReaperTask`, 37 | { 38 | taskRole: reaperRole, 39 | memoryLimitMiB: 1024, 40 | cpu: 512, 41 | runtimePlatform: { 42 | operatingSystemFamily: aws_ecs.OperatingSystemFamily.LINUX, 43 | cpuArchitecture: aws_ecs.CpuArchitecture.X86_64, 44 | }, 45 | } 46 | ); 47 | 48 | const image = new DockerImageAsset(this, `ReaperImage`, { 49 | directory: ".", 50 | platform: Platform.LINUX_AMD64, 51 | buildArgs: { 52 | DOCKER_BUILDKIT: "1", 53 | }, 54 | }); 55 | 56 | taskDefinition 57 | .addContainer(`ReaperBase`, { 58 | image: ContainerImage.fromDockerImageAsset(image), 59 | cpu: 512, 60 | environment: { 61 | ...environmentVariables, 62 | AWS_EMF_ENVIRONMENT: "Local", 63 | // update to trigger deployment 64 | VERSION: "2", 65 | }, 66 | logging: this.logDriver, 67 | }) 68 | 69 | new aws_ecs.FargateService(this, `ReaperService`, { 70 | cluster, 71 | taskDefinition, 72 | desiredCount: 1, 73 | assignPublicIp: false, 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/util/constants.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from './chain' 2 | 3 | export const WEBHOOK_CONFIG_BUCKET = 'order-webhook-notification-config' 4 | export const PRODUCTION_WEBHOOK_CONFIG_KEY = 'production.json' 5 | export const BETA_WEBHOOK_CONFIG_KEY = 'beta.json' 6 | export const NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000' 7 | export const ONE_HOUR_IN_SECONDS = 60 * 60 8 | export const ONE_DAY_IN_SECONDS = 60 * 60 * 24 9 | export const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365 10 | export const OLDEST_BLOCK_BY_CHAIN = { 11 | [ChainId.MAINNET]: 20120259, 12 | [ChainId.ARBITRUM_ONE]: 253597707, 13 | [ChainId.BASE]: 22335646, 14 | [ChainId.UNICHAIN]: 6747397, 15 | } 16 | export const BLOCK_TIME_MS_BY_CHAIN = { 17 | [ChainId.MAINNET]: 12000, 18 | [ChainId.ARBITRUM_ONE]: 250, 19 | [ChainId.BASE]: 2000, 20 | [ChainId.UNICHAIN]: 1000, 21 | } 22 | export const BLOCKS_IN_24_HOURS = (chainId: ChainId) => { 23 | const dayInMs = 24 * 60 * 60 * 1000 24 | return Math.floor(dayInMs / (BLOCK_TIME_MS_BY_CHAIN[chainId as keyof typeof BLOCK_TIME_MS_BY_CHAIN] ?? 12000)) 25 | } 26 | export const BLOCK_RANGE = 10000 27 | export const REAPER_MAX_ATTEMPTS = 10 28 | export const REAPER_RANGES_PER_RUN = 10 29 | //Dynamo limits batch write to 25 30 | export const DYNAMO_BATCH_WRITE_MAX = 25 31 | 32 | export const UNIMIND_ALGORITHM_VERSION = 4 33 | 34 | export enum UnimindUpdateType { 35 | NEW_PAIR = 'new_pair', 36 | ALGORITHM_UPDATE = 'algorithm_update', 37 | THRESHOLD_REACHED = 'threshold_reached', 38 | } 39 | export const DEFAULT_UNIMIND_PARAMETERS = JSON.stringify({ 40 | lambda1: 0, 41 | lambda2: 5, 42 | Sigma: -9.21034, 43 | }) 44 | export const UNIMIND_UPDATE_THRESHOLD = 25 45 | export const UNIMIND_DEV_SWAPPER_ADDRESS = '0x2b813964306D8F12bdaB5504073a52e5802f049D' 46 | // Direct pi and tau to use for curve; Not intrinsicValues 47 | export const PUBLIC_STATIC_PARAMETERS = { 48 | pi: 15, 49 | tau: 15, 50 | batchNumber: -1, // -1 indicates Unimind was not used to calculate these params 51 | algorithmVersion: -1, // -1 indicates not using Unimind algorithm 52 | } 53 | export const UNIMIND_MAX_TAU_BPS = 25 54 | export const UNIMIND_LARGE_PRICE_IMPACT_THRESHOLD = 2 // 2% price impact threshold 55 | 56 | // When pi = 0, AMM will be favored over Dutch Auction 57 | export const USE_CLASSIC_PARAMETERS = { 58 | pi: 0, 59 | tau: 0, 60 | // batchNumber and algorithmVersion are added dynamically 61 | } 62 | 63 | export const RPC_HEADERS = { 64 | 'x-uni-service-id': 'x_order_service', 65 | } as const 66 | 67 | export enum TradeType { 68 | EXACT_INPUT = 'EXACT_INPUT', 69 | EXACT_OUTPUT = 'EXACT_OUTPUT', 70 | } 71 | -------------------------------------------------------------------------------- /lib/unimind/batchedStrategy.ts: -------------------------------------------------------------------------------- 1 | import { UnimindStatistics } from "../crons/unimind-algorithm"; 2 | import { QuoteMetadata } from "../repositories/quote-metadata-repository"; 3 | import { UnimindParameters } from "../repositories/unimind-parameters-repository"; 4 | import { IUnimindAlgorithm } from "../util/unimind"; 5 | import { default as Logger } from 'bunyan' 6 | 7 | export type BatchedIntrinsicParameters = { 8 | pi: number; 9 | tau: number; 10 | } 11 | 12 | export class BatchedStrategy implements IUnimindAlgorithm { 13 | 14 | public unimindAlgorithm(statistics: UnimindStatistics, pairData: UnimindParameters, log: Logger): BatchedIntrinsicParameters { 15 | const objective_wait_time = 2; 16 | const objective_fill_rate = 0.96; 17 | const learning_rate = 2; 18 | const auction_duration = 32; 19 | const previousParameters = JSON.parse(pairData.intrinsicValues); 20 | 21 | if (statistics.waitTimes.length === 0 || statistics.fillStatuses.length === 0 || statistics.priceImpacts.length === 0) { 22 | return previousParameters; 23 | } 24 | // Set negative wait times to 0 25 | statistics.waitTimes = statistics.waitTimes.map((waitTime) => (waitTime && waitTime < 0) ? 0 : waitTime); 26 | 27 | const average_wait_time = statistics.waitTimes.reduce((a: number, b) => a + (b === undefined ? auction_duration : b), 0) / statistics.waitTimes.length; 28 | const average_fill_rate = statistics.fillStatuses.reduce((a: number, b) => a + b, 0) / statistics.fillStatuses.length; 29 | log.info(`Unimind unimindAlgorithm: average_wait_time: ${average_wait_time}, average_fill_rate: ${average_fill_rate}`) 30 | 31 | const wait_time_proportion = (objective_wait_time - average_wait_time) / objective_wait_time; 32 | const fill_rate_proportion = (objective_fill_rate - average_fill_rate) / objective_fill_rate; 33 | 34 | const pi = previousParameters.pi + learning_rate * wait_time_proportion; 35 | const tau = previousParameters.tau + learning_rate * fill_rate_proportion; 36 | 37 | //return a record of pi and tau 38 | return { 39 | pi: pi, 40 | tau: tau, 41 | }; 42 | } 43 | 44 | public computePi(intrinsicValues: BatchedIntrinsicParameters, extrinsicValues: QuoteMetadata): number { 45 | return intrinsicValues.pi * extrinsicValues.priceImpact 46 | } 47 | 48 | public computeTau(intrinsicValues: BatchedIntrinsicParameters, extrinsicValues: QuoteMetadata): number { 49 | return intrinsicValues.tau * extrinsicValues.priceImpact 50 | } 51 | } -------------------------------------------------------------------------------- /lib/util/unimind.ts: -------------------------------------------------------------------------------- 1 | import { default as Logger } from 'bunyan' 2 | import { keccak256 } from 'ethers/lib/utils' 3 | import { UNIMIND_LIST } from '../config/unimind-list' 4 | import { UnimindStatistics } from '../crons/unimind-algorithm' 5 | import { QuoteMetadata } from '../repositories/quote-metadata-repository' 6 | import { UnimindParameters } from '../repositories/unimind-parameters-repository' 7 | 8 | export const UNIMIND_TRADE_SAMPLE_PERCENT = 66 9 | 10 | export function unimindTradeFilter(quoteId: string): boolean { 11 | // Hash the quoteId for deterministic, consistent sampling 12 | const hash = keccak256(Buffer.from(quoteId.toLowerCase())) 13 | 14 | // Same technique as address filter - use last 4 hex chars 15 | const lastFourChars = hash.slice(-4) 16 | const value = parseInt(lastFourChars, 16) 17 | 18 | // Check if in sample range (1-100) 19 | return (value % 100) + 1 <= UNIMIND_TRADE_SAMPLE_PERCENT 20 | } 21 | 22 | export function supportedUnimindTokens(pair: string) { 23 | // Extract addresses from pair (address1-address2-chainId) 24 | const [address1, address2, chainId] = pair.split('-') 25 | const chainIdInt = parseInt(chainId) 26 | // Check if both addresses are in the UNIMIND_LIST 27 | const token1 = UNIMIND_LIST.find( 28 | (token) => token.address.toLowerCase() === address1.toLowerCase() && token.chainId === chainIdInt 29 | ) 30 | const token2 = UNIMIND_LIST.find( 31 | (token) => token.address.toLowerCase() === address2.toLowerCase() && token.chainId === chainIdInt 32 | ) 33 | return token1 !== undefined && token2 !== undefined 34 | } 35 | 36 | /** 37 | * Calculates median of an array of numbers 38 | */ 39 | export function median(values: number[]): number { 40 | if (values.length === 0) return 0 41 | 42 | const sorted = [...values].sort((a, b) => a - b) 43 | const mid = Math.floor(sorted.length / 2) 44 | 45 | if (sorted.length % 2 === 0) { 46 | // Even number of elements: average of two middle values 47 | return (sorted[mid - 1] + sorted[mid]) / 2 48 | } else { 49 | // Odd number of elements: middle value 50 | return sorted[mid] 51 | } 52 | } 53 | 54 | export interface IUnimindAlgorithm { 55 | /** 56 | * @notice Adjusts Unimind parameters (intrinsic values) based on historical order statistics 57 | * @param statistics Aggregated order data containing arrays of wait times, fill statuses, and price impacts 58 | * @param pairData Previous parameters intrinsic values stored for the pair 59 | * @return Updated intrinsic parameters 60 | */ 61 | unimindAlgorithm(statistics: UnimindStatistics, pairData: UnimindParameters, log: Logger): T 62 | computePi(intrinsicValues: T, extrinsicValues: QuoteMetadata): number 63 | computeTau(intrinsicValues: T, extrinsicValues: QuoteMetadata): number 64 | } 65 | -------------------------------------------------------------------------------- /lib/handlers/post-order/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import Joi from 'joi' 3 | import FieldValidator from '../../../util/field-validator' 4 | 5 | // TODO(andy.smith): update schemas to accept any one of the below formats. For now, just validate the original request body. 6 | export const PostOrderRequestBodyJoi = Joi.object({ 7 | encodedOrder: FieldValidator.isValidEncodedOrder().required(), 8 | signature: FieldValidator.isValidSignature().required(), 9 | chainId: FieldValidator.isValidChainId().required(), 10 | quoteId: FieldValidator.isValidQuoteId(), 11 | requestId: FieldValidator.isValidQuoteId(), 12 | orderType: FieldValidator.isValidOrderType(), 13 | }) 14 | 15 | export const PostOrderResponseJoi = Joi.object({ 16 | hash: FieldValidator.isValidOrderHash(), 17 | }) 18 | 19 | export type LegacyDutchOrderPostRequestBody = { 20 | // To maintain backwards compatibility, we assume if an orderType is undefined 21 | // on the object, this is a legacy DutchOrderRequest which means it can either be a 22 | // Dutch order or a limit order. The order type will be decided by the parser. 23 | orderType: undefined 24 | chainId: number 25 | encodedOrder: string 26 | signature: string 27 | quoteId?: string 28 | requestId?: string 29 | } 30 | 31 | export type DutchV1OrderPostRequestBody = { 32 | orderType: OrderType.Dutch 33 | chainId: number 34 | encodedOrder: string 35 | signature: string 36 | quoteId?: string 37 | requestId?: string 38 | } 39 | 40 | export type LimitOrderPostRequestBody = { 41 | orderType: OrderType.Limit 42 | chainId: number 43 | encodedOrder: string 44 | signature: string 45 | quoteId?: string 46 | } 47 | 48 | export type DutchV2OrderPostRequestBody = { 49 | orderType: OrderType.Dutch_V2 50 | chainId: number 51 | encodedOrder: string 52 | signature: string 53 | quoteId?: string 54 | requestId?: string 55 | } 56 | 57 | export type DutchV3OrderPostRequestBody = { 58 | orderType: OrderType.Dutch_V3 59 | chainId: number 60 | encodedOrder: string 61 | signature: string 62 | quoteId?: string 63 | requestId?: string 64 | } 65 | 66 | export type PriorityOrderPostRequestBody = { 67 | orderType: OrderType.Priority 68 | chainId: number 69 | encodedOrder: string 70 | signature: string 71 | quoteId?: string 72 | requestId?: string 73 | } 74 | 75 | export type RelayOrderPostRequestBody = { 76 | orderType: OrderType.Relay 77 | chainId: number 78 | encodedOrder: string 79 | signature: string 80 | } 81 | 82 | export type PostOrderRequestBody = 83 | | LegacyDutchOrderPostRequestBody 84 | | DutchV1OrderPostRequestBody 85 | | DutchV2OrderPostRequestBody 86 | | DutchV3OrderPostRequestBody 87 | | LimitOrderPostRequestBody 88 | | RelayOrderPostRequestBody 89 | | PriorityOrderPostRequestBody 90 | 91 | export type PostOrderResponse = { 92 | hash: string 93 | } 94 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/index.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import { SORT_FIELDS } from '../../../entities' 3 | import FieldValidator from '../../../util/field-validator' 4 | import { GetOrderTypeQueryParamEnum } from './GetOrderTypeQueryParamEnum' 5 | 6 | const sortKeyJoi = FieldValidator.isValidSortKey() 7 | 8 | export const GetOrdersQueryParamsJoi = Joi.object({ 9 | limit: FieldValidator.isValidLimit(), 10 | orderHash: FieldValidator.isValidOrderHash(), 11 | orderHashes: FieldValidator.isValidOrderHashes(), 12 | sortKey: FieldValidator.isValidSortKey() 13 | .when('sort', { 14 | is: Joi.exist(), 15 | then: sortKeyJoi.required(), 16 | otherwise: sortKeyJoi, 17 | }) 18 | .when('desc', { 19 | is: Joi.exist(), 20 | then: sortKeyJoi.required(), 21 | otherwise: sortKeyJoi, 22 | }), 23 | sort: FieldValidator.isValidSort(), 24 | cursor: FieldValidator.isValidCursor(), 25 | chainId: FieldValidator.isValidChainId(), 26 | filler: FieldValidator.isValidEthAddress(), 27 | swapper: FieldValidator.isValidEthAddress(), 28 | orderStatus: FieldValidator.isValidOrderStatus(), 29 | desc: Joi.boolean(), 30 | orderType: FieldValidator.isValidGetQueryParamOrderType(), 31 | executeAddress: FieldValidator.isValidEthAddress(), 32 | pair: Joi.string(), 33 | }) 34 | .or('orderHash', 'orderHashes', 'chainId', 'orderStatus', 'swapper', 'filler', 'pair') 35 | .when('.chainId', { 36 | is: Joi.exist(), 37 | then: Joi.object({ 38 | swapper: Joi.forbidden().error(new Error('Querying with both swapper and chainId is not currently supported.')), 39 | }), 40 | }) 41 | .when('.sortKey', { 42 | is: Joi.exist(), 43 | then: Joi.object({ 44 | orderHashes: Joi.forbidden().error( 45 | new Error('Querying with both orderHashes and sortKey is not currently supported.') 46 | ), 47 | }), 48 | }) 49 | 50 | export type SharedGetOrdersQueryParams = { 51 | limit?: number 52 | orderStatus?: string 53 | orderHash?: string 54 | sortKey?: SORT_FIELDS 55 | sort?: string 56 | filler?: string 57 | cursor?: string 58 | chainId?: number 59 | desc?: boolean 60 | orderType?: GetOrderTypeQueryParamEnum 61 | executeAddress?: string 62 | pair?: string 63 | } 64 | export type RawGetOrdersQueryParams = SharedGetOrdersQueryParams & { 65 | swapper?: string 66 | orderHashes: string 67 | } 68 | export type GetOrdersQueryParams = SharedGetOrdersQueryParams & { 69 | offerer?: string 70 | orderHashes?: string[] 71 | } 72 | 73 | export enum GET_QUERY_PARAMS { 74 | LIMIT = 'limit', 75 | OFFERER = 'offerer', 76 | ORDER_STATUS = 'orderStatus', 77 | ORDER_HASH = 'orderHash', 78 | ORDER_HASHES = 'orderHashes', 79 | SORT_KEY = 'sortKey', 80 | SORT = 'sort', 81 | FILLER = 'filler', 82 | CHAIN_ID = 'chainId', 83 | DESC = 'desc', 84 | ORDER_TYPE = 'orderType', 85 | EXECUTE_ADDRESS = 'executeAddress', 86 | PAIR = 'pair', 87 | } 88 | -------------------------------------------------------------------------------- /lib/repositories/quote-metadata-repository.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | import Logger from 'bunyan' 3 | import { Entity, Table } from 'dynamodb-toolbox' 4 | import { DYNAMODB_TYPES } from '../config/dynamodb' 5 | import { TABLE_NAMES } from './util' 6 | 7 | interface MethodParameters { 8 | calldata: string 9 | value: string 10 | to: string 11 | } 12 | 13 | export interface Route { 14 | quote: string 15 | quoteGasAdjusted: string 16 | gasPriceWei: string 17 | gasUseEstimateQuote: string 18 | gasUseEstimate: string 19 | methodParameters: MethodParameters 20 | } 21 | 22 | export interface QuoteMetadata { 23 | quoteId: string 24 | referencePrice: string 25 | priceImpact: number 26 | blockNumber: number 27 | route: Route 28 | pair: string 29 | usedUnimind: boolean 30 | tradeType?: string 31 | } 32 | 33 | export interface QuoteMetadataRepository { 34 | put(values: QuoteMetadata): Promise 35 | getByQuoteId(quoteId: string): Promise 36 | } 37 | 38 | export class DynamoQuoteMetadataRepository implements QuoteMetadataRepository { 39 | private readonly entity: Entity 40 | 41 | static create(documentClient: DocumentClient): QuoteMetadataRepository { 42 | const log = Logger.createLogger({ 43 | name: 'QuoteMetadataRepository', 44 | serializers: Logger.stdSerializers, 45 | }) 46 | 47 | const table = new Table({ 48 | name: TABLE_NAMES.QuoteMetadata, 49 | partitionKey: 'quoteId', 50 | DocumentClient: documentClient, 51 | }) 52 | 53 | const entity = new Entity({ 54 | name: 'QuoteMetadata', 55 | attributes: { 56 | quoteId: { partitionKey: true, type: DYNAMODB_TYPES.STRING }, 57 | referencePrice: { type: DYNAMODB_TYPES.STRING, required: true }, 58 | priceImpact: { type: DYNAMODB_TYPES.NUMBER, required: true }, 59 | pair: { type: DYNAMODB_TYPES.STRING, required: true }, 60 | blockNumber: { type: DYNAMODB_TYPES.NUMBER, required: false }, 61 | route: { type: DYNAMODB_TYPES.MAP, required: false }, 62 | usedUnimind: { type: DYNAMODB_TYPES.BOOLEAN, required: true }, 63 | tradeType: { type: DYNAMODB_TYPES.STRING, required: false }, 64 | }, 65 | table, 66 | } as const) 67 | 68 | return new DynamoQuoteMetadataRepository(entity, log) 69 | } 70 | 71 | constructor(entity: Entity, private readonly log: Logger) { 72 | this.entity = entity 73 | } 74 | 75 | async put(values: QuoteMetadata): Promise { 76 | try { 77 | await this.entity.put(values) 78 | } catch (error) { 79 | this.log.error({ error, values }, 'Failed to put quote metadata') 80 | throw error 81 | } 82 | } 83 | 84 | async getByQuoteId(quoteId: string): Promise { 85 | const result = await this.entity.get({ quoteId }, { execute: true }) 86 | return result.Item as QuoteMetadata | undefined 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: Pre-Push Actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | env: 9 | GOUDA_SERVICE_URL: ${{ secrets.GOUDA_SERVICE_URL }} 10 | 11 | jobs: 12 | lint-and-test: 13 | name: lint-and-test 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: bullfrogsec/bullfrog@dcde5841b19b7ef693224207a7fdec67fce604db # v0.8.3 18 | with: 19 | # List of IPs to allow outbound connections to. 20 | # By default, only localhost and IPs required for the essential operations of Github Actions are allowed. 21 | # allowed-ips: | 22 | 23 | # List of domains to allow outbound connections to. 24 | # Wildcards are accepted. For example, if allowing `*.google.com`, this will allow `www.google.com`, `console.cloud.google.com` but not `google.com`. 25 | # By default, only domains required for essential operations of Github Actions and uploading job summaries are allowed. 26 | # Refer to https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#communication-requirements-for-github-hosted-runners-and-github for additional domains that should be allowed for additional Github Actions features. 27 | #allowed-domains: 28 | 29 | # The egress policy to enforce. Valid values are `audit` and `block`. 30 | # Default: audit 31 | egress-policy: audit 32 | - name: Check out Git repository 33 | uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 34 | 35 | - name: Set up node 36 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 37 | with: 38 | node-version: 20.x 39 | registry-url: https://registry.npmjs.org 40 | 41 | - name: Install dependencies 42 | run: | 43 | npm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_AUTH_TOKEN }}" \ 44 | && yarn install --frozen-lockfile 45 | 46 | - name: Run linters 47 | run: yarn lint 48 | 49 | - name: Run Build 50 | run: yarn build 51 | 52 | - name: Run swagger validation 53 | uses: readmeio/rdme@51a80867c45de15e2b41af0c4bd5bbc61b932804 # 51a80867c45de15e2b41af0c4bd5bbc61b932804 54 | with: 55 | rdme: openapi:validate swagger.json 56 | 57 | - name: Setup Java 58 | uses: actions/setup-java@17f84c3641ba7b8f6deff6309fc4c864478f5d62 # v3 59 | with: 60 | distribution: 'temurin' 61 | java-version: '17' 62 | 63 | - name: Create Env File 64 | run: | 65 | touch .env 66 | echo "LABS_COSIGNER=0x0000000000000000000000000000000000000000" > .env 67 | echo "LABS_PRIORITY_COSIGNER=0x0000000000000000000000000000000000000000" >> .env 68 | echo "KMS_KEY_ID=testtest" >> .env 69 | cat .env 70 | 71 | - name: Check test coverage 72 | run: yarn coverage 73 | -------------------------------------------------------------------------------- /test/unit/models/RelayOrder.test.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import { ORDER_STATUS, RelayOrderEntity } from '../../../lib/entities' 3 | import { GetRelayOrderResponse } from '../../../lib/handlers/get-orders/schema/GetRelayOrderResponse' 4 | import { RelayOrder } from '../../../lib/models' 5 | import { ChainId } from '../../../lib/util/chain' 6 | import { SDKRelayOrderFactory } from '../../factories/SDKRelayOrderFactory' 7 | import { MOCK_SIGNATURE } from '../../test-data' 8 | 9 | describe('RelayOrder Model', () => { 10 | test('toEntity', () => { 11 | const order = new RelayOrder(SDKRelayOrderFactory.buildRelayOrder(), MOCK_SIGNATURE, ChainId.MAINNET) 12 | const entity: RelayOrderEntity = order.toEntity(ORDER_STATUS.OPEN) 13 | 14 | expect(entity.signature).toEqual(MOCK_SIGNATURE) 15 | expect(entity.encodedOrder).toEqual(order.inner.serialize()) 16 | expect(entity.orderStatus).toEqual(ORDER_STATUS.OPEN) 17 | expect(entity.orderHash).toEqual(order.inner.hash()) 18 | expect(entity.type).toEqual(OrderType.Relay) 19 | }) 20 | 21 | test('fromEntity', () => { 22 | const order = new RelayOrder( 23 | SDKRelayOrderFactory.buildRelayOrder(), 24 | MOCK_SIGNATURE, 25 | ChainId.MAINNET, 26 | ORDER_STATUS.OPEN 27 | ) 28 | const entity: RelayOrderEntity = order.toEntity(ORDER_STATUS.OPEN) 29 | const fromEntity = RelayOrder.fromEntity(entity) 30 | 31 | expect(order).toEqual(fromEntity) 32 | }) 33 | 34 | test('toGetResponse', () => { 35 | const order = new RelayOrder( 36 | SDKRelayOrderFactory.buildRelayOrder(), 37 | MOCK_SIGNATURE, 38 | ChainId.MAINNET, 39 | ORDER_STATUS.OPEN 40 | ) 41 | const response: GetRelayOrderResponse = order.toGetResponse() 42 | 43 | expect(response.type).toEqual(OrderType.Relay) 44 | expect(response.orderStatus).toEqual(order.orderStatus) 45 | expect(response.signature).toEqual(order.signature) 46 | expect(response.encodedOrder).toEqual(order.inner.serialize()) 47 | expect(response.chainId).toEqual(order.chainId) 48 | expect(response.orderHash).toEqual(order.inner.hash()) 49 | expect(response.swapper).toEqual(order.inner.info.swapper) 50 | expect(response.reactor).toEqual(order.inner.info.reactor) 51 | expect(response.deadline).toEqual(order.inner.info.deadline) 52 | expect(response.input.token).toEqual(order.inner.info.input.token) 53 | expect(response.input.amount).toEqual(order.inner.info.input.amount.toString()) 54 | expect(response.input.recipient).toEqual(order.inner.info.input.recipient) 55 | expect(response.relayFee.token).toEqual(order.inner.info.fee.token) 56 | expect(response.relayFee.startAmount).toEqual(order.inner.info.fee.startAmount.toString()) 57 | expect(response.relayFee.endAmount).toEqual(order.inner.info.fee.endAmount.toString()) 58 | expect(response.relayFee.startTime).toEqual(order.inner.info.fee.startTime) 59 | expect(response.relayFee.endTime).toEqual(order.inner.info.fee.endTime) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /lib/handlers/get-limit-orders/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderValidator, RelayOrderValidator } from '@uniswap/uniswapx-sdk' 2 | import { DynamoDB } from 'aws-sdk' 3 | import { RelayOrderRepository } from '../../repositories/RelayOrderRepository' 4 | import { AnalyticsService } from '../../services/analytics-service' 5 | import { RelayOrderService } from '../../services/RelayOrderService' 6 | import { UniswapXOrderService } from '../../services/UniswapXOrderService' 7 | import { ONE_DAY_IN_SECONDS } from '../../util/constants' 8 | 9 | import { log } from '../../Logging' 10 | import { LimitOrdersRepository } from '../../repositories/limit-orders-repository' 11 | import { OrderDispatcher } from '../../services/OrderDispatcher' 12 | import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator' 13 | import { OffChainUniswapXOrderValidator } from '../../util/OffChainUniswapXOrderValidator' 14 | import { FillEventLogger } from '../check-order-status/fill-event-logger' 15 | import { FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../check-order-status/util' 16 | import { EventWatcherMap } from '../EventWatcherMap' 17 | import { GetOrdersHandler } from '../get-orders/handler' 18 | import { OnChainValidatorMap } from '../OnChainValidatorMap' 19 | import { getMaxOpenOrders } from '../post-order/injector' 20 | import { GetLimitOrdersInjector } from './injector' 21 | import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository' 22 | 23 | const repo = LimitOrdersRepository.create(new DynamoDB.DocumentClient()) 24 | const quoteMetadataRepository = DynamoQuoteMetadataRepository.create(new DynamoDB.DocumentClient()) 25 | const orderValidator = new OffChainUniswapXOrderValidator(() => new Date().getTime() / 1000, ONE_DAY_IN_SECONDS) 26 | const onChainValidatorMap = new OnChainValidatorMap() 27 | 28 | const uniswapXOrderService = new UniswapXOrderService( 29 | orderValidator, 30 | onChainValidatorMap, 31 | repo, 32 | repo, //same as normal repo for limit orders 33 | quoteMetadataRepository, 34 | log, 35 | getMaxOpenOrders, 36 | AnalyticsService.create(), 37 | new Map() 38 | ) 39 | 40 | const relayOrderValidator = new OffChainRelayOrderValidator(() => new Date().getTime() / 1000) 41 | const relayOrderValidatorMap = new OnChainValidatorMap() 42 | 43 | const relayOrderService = new RelayOrderService( 44 | relayOrderValidator, 45 | relayOrderValidatorMap, 46 | EventWatcherMap.createRelayEventWatcherMap(), 47 | RelayOrderRepository.create(new DynamoDB.DocumentClient()), 48 | log, 49 | getMaxOpenOrders, 50 | new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create()) 51 | ) 52 | 53 | const getLimitOrdersInjectorPromise = new GetLimitOrdersInjector('getLimitOrdersInjector').build() 54 | const getLimitOrdersHandler = new GetOrdersHandler( 55 | 'getLimitOrdersHandler', 56 | getLimitOrdersInjectorPromise, 57 | new OrderDispatcher(uniswapXOrderService, relayOrderService, log) 58 | ) 59 | 60 | module.exports = { 61 | getLimitOrdersHandler: getLimitOrdersHandler.handler, 62 | } 63 | -------------------------------------------------------------------------------- /lib/handlers/get-nonce/handler.ts: -------------------------------------------------------------------------------- 1 | import { Unit } from 'aws-embedded-metrics' 2 | import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda' 3 | import Joi from 'joi' 4 | import { metrics } from '../../util/metrics' 5 | import { APIGLambdaHandler, APIHandleRequestParams, ErrorCode, ErrorResponse, Response } from '../base/index' 6 | import { ContainerInjected, RequestInjected } from './injector' 7 | import { GetNonceQueryParams, GetNonceQueryParamsJoi, GetNonceResponse, GetNonceResponseJoi } from './schema/index' 8 | 9 | export class GetNonceHandler extends APIGLambdaHandler< 10 | ContainerInjected, 11 | RequestInjected, 12 | void, 13 | GetNonceQueryParams, 14 | GetNonceResponse 15 | > { 16 | public async handleRequest( 17 | params: APIHandleRequestParams 18 | ): Promise> { 19 | const { 20 | requestInjected: { address, chainId, log }, 21 | containerInjected: { dbInterface }, 22 | } = params 23 | 24 | try { 25 | log.info({ address: address }, 'Getting nonce for address') 26 | const nonce = await dbInterface.getNonceByAddressAndChain(address.toLowerCase(), chainId) 27 | return { 28 | statusCode: 200, 29 | body: { 30 | nonce: nonce, 31 | }, 32 | } 33 | } catch (e: unknown) { 34 | log.error({ e }, `Error getting nonce for address ${address} on chain ${chainId}`) 35 | return { 36 | statusCode: 500, 37 | errorCode: ErrorCode.InternalError, 38 | ...(e instanceof Error && { detail: e.message }), 39 | } 40 | } 41 | } 42 | 43 | protected afterResponseHook(event: APIGatewayProxyEvent, _context: Context, response: APIGatewayProxyResult): void { 44 | const { statusCode } = response 45 | 46 | // Try and extract the chain id from the raw json. 47 | let chainId = '0' 48 | try { 49 | chainId = event.queryStringParameters?.chainId ?? '0' 50 | } catch (err) { 51 | // no-op. If we can't get chainId still log the metric as chain 0 52 | } 53 | 54 | const statusCodeMod = (Math.floor(statusCode / 100) * 100).toString().replace(/0/g, 'X') 55 | 56 | const getNonceStatusByChain = `GetNonceChainId${chainId.toString()}Status${statusCodeMod}` 57 | metrics.putMetric(getNonceStatusByChain, 1, Unit.Count) 58 | 59 | const getNonceStatus = `GetNonceStatus${statusCodeMod}` 60 | metrics.putMetric(getNonceStatus, 1, Unit.Count) 61 | 62 | const getNonceChainId = `GetNonceRequestChainId${chainId.toString()}` 63 | metrics.putMetric(getNonceChainId, 1, Unit.Count) 64 | 65 | const getNonce = `GetNonceRequest` 66 | metrics.putMetric(getNonce, 1, Unit.Count) 67 | } 68 | 69 | protected requestBodySchema(): Joi.ObjectSchema | null { 70 | return null 71 | } 72 | 73 | protected requestQueryParamsSchema(): Joi.ObjectSchema | null { 74 | return GetNonceQueryParamsJoi 75 | } 76 | 77 | protected responseBodySchema(): Joi.ObjectSchema | null { 78 | return GetNonceResponseJoi 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/util/order.ts: -------------------------------------------------------------------------------- 1 | import { DutchOrder, OrderType, UniswapXOrderParser } from '@uniswap/uniswapx-sdk' 2 | import { DynamoDBRecord } from 'aws-lambda' 3 | import { ORDER_STATUS, UniswapXOrderEntity } from '../entities' 4 | 5 | export const DUTCH_LIMIT = 'DutchLimit' 6 | 7 | type ParsedOrder = { 8 | encodedOrder: string 9 | signature: string 10 | orderHash: string 11 | orderStatus: ORDER_STATUS 12 | swapper: string 13 | createdAt: number 14 | chainId: number 15 | filler?: string 16 | quoteId?: string 17 | orderType?: string 18 | } 19 | 20 | export const eventRecordToOrder = (record: DynamoDBRecord): ParsedOrder => { 21 | const newOrder = record?.dynamodb?.NewImage 22 | if (!newOrder) { 23 | throw new Error('There is no new order.') 24 | } 25 | 26 | try { 27 | const chainId = parseInt(newOrder.chainId.N as string) 28 | const encodedOrder = newOrder.encodedOrder.S as string 29 | const orderType = new UniswapXOrderParser().getOrderTypeFromEncoded(encodedOrder, chainId) 30 | 31 | return { 32 | swapper: newOrder.offerer.S as string, 33 | orderStatus: newOrder.orderStatus.S as ORDER_STATUS, 34 | encodedOrder: encodedOrder, 35 | signature: newOrder.signature.S as string, 36 | createdAt: parseInt(newOrder.createdAt.N as string), 37 | orderHash: newOrder.orderHash.S as string, 38 | chainId: chainId, 39 | orderType: orderType, 40 | ...(newOrder?.quoteId?.S && { quoteId: newOrder.quoteId.S as string }), 41 | ...(newOrder?.filler?.S && { filler: newOrder.filler.S as string }), 42 | } 43 | } catch (e) { 44 | throw new Error(`Error parsing new record to order: ${e instanceof Error ? e.message : e}`) 45 | } 46 | } 47 | 48 | export const formatOrderEntity = ( 49 | decodedOrder: DutchOrder, 50 | signature: string, 51 | orderType: OrderType.Dutch | OrderType.Limit, 52 | orderStatus: ORDER_STATUS, 53 | quoteId?: string 54 | ): UniswapXOrderEntity => { 55 | const { input, outputs } = decodedOrder.info 56 | const order: UniswapXOrderEntity = { 57 | type: orderType, 58 | encodedOrder: decodedOrder.serialize(), 59 | signature, 60 | nonce: decodedOrder.info.nonce.toString(), 61 | orderHash: decodedOrder.hash().toLowerCase(), 62 | chainId: decodedOrder.chainId, 63 | orderStatus: orderStatus, 64 | offerer: decodedOrder.info.swapper.toLowerCase(), 65 | input: { 66 | token: input.token, 67 | startAmount: input.startAmount.toString(), 68 | endAmount: input.endAmount.toString(), 69 | }, 70 | outputs: outputs.map((output) => ({ 71 | token: output.token, 72 | startAmount: output.startAmount.toString(), 73 | endAmount: output.endAmount.toString(), 74 | recipient: output.recipient.toLowerCase(), 75 | })), 76 | reactor: decodedOrder.info.reactor.toLowerCase(), 77 | decayStartTime: decodedOrder.info.decayStartTime, 78 | decayEndTime: decodedOrder.info.deadline, 79 | deadline: decodedOrder.info.deadline, 80 | filler: decodedOrder.info?.exclusiveFiller?.toLowerCase(), 81 | ...(quoteId && { quoteId: quoteId }), 82 | } 83 | 84 | return order 85 | } 86 | -------------------------------------------------------------------------------- /test/factories/SDKPriorityOrderFactory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CosignedPriorityOrder as SDKPriorityOrder, 3 | CosignedPriorityOrderInfoJSON, 4 | PriorityOrderBuilder, 5 | } from '@uniswap/uniswapx-sdk' 6 | import { BigNumber, constants } from 'ethers' 7 | import { ChainId } from '../../lib/util/chain' 8 | import { MOCK_LATEST_BLOCK, Tokens } from '../unit/fixtures' 9 | import { PartialDeep } from './PartialDeep' 10 | 11 | /** 12 | * Helper class for building CosignedPriorityOrders. 13 | */ 14 | export class SDKPriorityOrderFactory { 15 | static buildPriorityOrder( 16 | chainId = ChainId.MAINNET, 17 | overrides: PartialDeep = {} 18 | ): SDKPriorityOrder { 19 | // Values adapted from https://github.com/Uniswap/uniswapx-sdk/blob/7949043e7d2434553f84f588e1405e87d249a5aa/src/utils/order.test.ts#L28 20 | const nowInSeconds = Math.floor(Date.now() / 1000) 21 | 22 | // Arbitrary default future time ten seconds in future 23 | const futureTime = nowInSeconds + 10 24 | 25 | let builder = new PriorityOrderBuilder(chainId) 26 | 27 | builder = builder 28 | .cosigner(overrides.cosigner ?? constants.AddressZero) 29 | .cosignature(overrides.cosignature ?? '0x') 30 | .deadline(overrides.deadline ?? futureTime) 31 | .swapper(overrides.swapper ?? '0x0000000000000000000000000000000000000000') 32 | .nonce(overrides.nonce ? BigNumber.from(overrides.nonce) : BigNumber.from(100)) 33 | .input({ 34 | token: overrides.input?.token ?? Tokens.MAINNET.USDC, 35 | amount: overrides.input?.amount ? BigNumber.from(overrides.input?.amount) : BigNumber.from('1000000'), 36 | mpsPerPriorityFeeWei: overrides.input?.mpsPerPriorityFeeWei 37 | ? BigNumber.from(overrides.input?.mpsPerPriorityFeeWei) 38 | : BigNumber.from(0), 39 | }) 40 | .auctionStartBlock( 41 | overrides.auctionStartBlock 42 | ? BigNumber.from(overrides.auctionStartBlock) 43 | : BigNumber.from(MOCK_LATEST_BLOCK + 10) 44 | ) 45 | .auctionTargetBlock( 46 | overrides.cosignerData?.auctionTargetBlock 47 | ? BigNumber.from(overrides.cosignerData?.auctionTargetBlock) 48 | : BigNumber.from(MOCK_LATEST_BLOCK + 1) 49 | ) 50 | .baselinePriorityFeeWei(BigNumber.from(0)) 51 | 52 | const outputs = overrides.outputs ?? [ 53 | { 54 | token: Tokens.MAINNET.WETH, 55 | amount: '1000000000000000000', 56 | mpsPerPriorityFeeWei: '1', 57 | recipient: '0x0000000000000000000000000000000000000000', 58 | }, 59 | ] 60 | for (const output of outputs) { 61 | builder = builder.output({ 62 | token: output?.token ?? Tokens.MAINNET.WETH, 63 | amount: output?.amount ? BigNumber.from(output?.amount) : BigNumber.from('1000000000000000000'), 64 | mpsPerPriorityFeeWei: output?.mpsPerPriorityFeeWei 65 | ? BigNumber.from(output?.mpsPerPriorityFeeWei) 66 | : BigNumber.from('1'), 67 | recipient: output?.recipient ?? '0x0000000000000000000000000000000000000000', 68 | }) 69 | } 70 | 71 | return builder.build() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/GetOrdersResponse.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import Joi from 'joi' 3 | import { UniswapXOrderEntity } from '../../../entities' 4 | import FieldValidator from '../../../util/field-validator' 5 | import { DUTCH_LIMIT } from '../../../util/order' 6 | import { GetDutchV2OrderResponse, GetDutchV2OrderResponseEntryJoi } from './GetDutchV2OrderResponse' 7 | import { GetPriorityOrderResponse, GetPriorityOrderResponseEntryJoi } from './GetPriorityOrderResponse' 8 | import { GetRelayOrderResponse } from './GetRelayOrderResponse' 9 | import { GetDutchV3OrderResponse, GetDutchV3OrderResponseEntryJoi } from './GetDutchV3OrderResponse' 10 | 11 | export type GetOrdersResponse< 12 | T extends 13 | | UniswapXOrderEntity 14 | | GetRelayOrderResponse 15 | | GetDutchV2OrderResponse 16 | | GetDutchV3OrderResponse 17 | | GetPriorityOrderResponse 18 | | undefined 19 | > = { 20 | orders: T[] 21 | cursor?: string 22 | } 23 | 24 | export const OrderInputJoi = Joi.object({ 25 | token: FieldValidator.isValidEthAddress().required(), 26 | startAmount: FieldValidator.isValidAmount(), 27 | endAmount: FieldValidator.isValidAmount(), 28 | }) 29 | 30 | export const OrderOutputJoi = Joi.object({ 31 | token: FieldValidator.isValidEthAddress().required(), 32 | startAmount: FieldValidator.isValidAmount().required(), 33 | endAmount: FieldValidator.isValidAmount().required(), 34 | recipient: FieldValidator.isValidEthAddress().required(), 35 | }) 36 | 37 | export const SettledAmount = Joi.object({ 38 | tokenOut: FieldValidator.isValidEthAddress(), 39 | amountOut: FieldValidator.isValidAmount(), 40 | tokenIn: FieldValidator.isValidEthAddress(), 41 | amountIn: FieldValidator.isValidAmount(), 42 | }) 43 | const OrderRepsonseEntryJoiMigrations = { 44 | chainId: FieldValidator.isValidChainId().valid(12341234), 45 | } 46 | 47 | export const OrderResponseEntryJoi = Joi.object({ 48 | createdAt: FieldValidator.isValidCreatedAt(), 49 | encodedOrder: FieldValidator.isValidEncodedOrder(), 50 | signature: FieldValidator.isValidSignature(), 51 | orderStatus: FieldValidator.isValidOrderStatus(), 52 | orderHash: FieldValidator.isValidOrderHash(), 53 | swapper: FieldValidator.isValidEthAddress(), 54 | txHash: FieldValidator.isValidTxHash(), 55 | type: Joi.string().valid(OrderType.Dutch, DUTCH_LIMIT, OrderType.Limit, OrderType.Priority, OrderType.Dutch_V3), 56 | input: OrderInputJoi, 57 | outputs: Joi.array().items(OrderOutputJoi), 58 | settledAmounts: Joi.array().items(SettledAmount), 59 | chainId: FieldValidator.isValidChainId(), 60 | quoteId: FieldValidator.isValidQuoteId(), 61 | requestId: FieldValidator.isValidRequestId(), 62 | nonce: FieldValidator.isValidNonce(), 63 | }).keys({ 64 | ...OrderRepsonseEntryJoiMigrations, 65 | }) 66 | 67 | export const GetOrdersResponseJoi = Joi.object({ 68 | orders: Joi.array().items( 69 | Joi.alternatives( 70 | OrderResponseEntryJoi, 71 | GetDutchV2OrderResponseEntryJoi, 72 | GetDutchV3OrderResponseEntryJoi, 73 | GetPriorityOrderResponseEntryJoi 74 | ) 75 | ), 76 | cursor: FieldValidator.isValidCursor(), 77 | }) 78 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/index.ts: -------------------------------------------------------------------------------- 1 | import { GetOrdersHandler } from './handler' 2 | import { GetOrdersInjector } from './injector' 3 | 4 | import { OrderValidator, RelayOrderValidator } from '@uniswap/uniswapx-sdk' 5 | import { DynamoDB } from 'aws-sdk' 6 | import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository' 7 | import { RelayOrderRepository } from '../../repositories/RelayOrderRepository' 8 | import { AnalyticsService } from '../../services/analytics-service' 9 | import { OrderDispatcher } from '../../services/OrderDispatcher' 10 | import { RelayOrderService } from '../../services/RelayOrderService' 11 | import { UniswapXOrderService } from '../../services/UniswapXOrderService' 12 | import { ONE_DAY_IN_SECONDS } from '../../util/constants' 13 | 14 | import { log } from '../../Logging' 15 | import { LimitOrdersRepository } from '../../repositories/limit-orders-repository' 16 | import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator' 17 | import { OffChainUniswapXOrderValidator } from '../../util/OffChainUniswapXOrderValidator' 18 | import { FillEventLogger } from '../check-order-status/fill-event-logger' 19 | import { FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../check-order-status/util' 20 | import { EventWatcherMap } from '../EventWatcherMap' 21 | import { OnChainValidatorMap } from '../OnChainValidatorMap' 22 | import { getMaxOpenOrders } from '../post-order/injector' 23 | import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository' 24 | 25 | const repo = DutchOrdersRepository.create(new DynamoDB.DocumentClient()) 26 | const limitRepo = LimitOrdersRepository.create(new DynamoDB.DocumentClient()) 27 | const quoteMetadataRepository = DynamoQuoteMetadataRepository.create(new DynamoDB.DocumentClient()) 28 | const orderValidator = new OffChainUniswapXOrderValidator(() => new Date().getTime() / 1000, ONE_DAY_IN_SECONDS) 29 | const onChainValidatorMap = new OnChainValidatorMap() 30 | const providerMap = new Map() 31 | 32 | const uniswapXOrderService = new UniswapXOrderService( 33 | orderValidator, 34 | onChainValidatorMap, 35 | repo, 36 | limitRepo, 37 | quoteMetadataRepository, 38 | log, 39 | getMaxOpenOrders, 40 | AnalyticsService.create(), 41 | providerMap 42 | ) 43 | 44 | const relayOrderValidator = new OffChainRelayOrderValidator(() => new Date().getTime() / 1000) 45 | const relayOrderValidatorMap = new OnChainValidatorMap() 46 | 47 | const relayOrderService = new RelayOrderService( 48 | relayOrderValidator, 49 | relayOrderValidatorMap, 50 | EventWatcherMap.createRelayEventWatcherMap(), 51 | RelayOrderRepository.create(new DynamoDB.DocumentClient()), 52 | log, 53 | getMaxOpenOrders, 54 | new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create()) 55 | ) 56 | const getOrdersInjectorPromise = new GetOrdersInjector('getOrdersInjector').build() 57 | const getOrdersHandler = new GetOrdersHandler( 58 | 'getOrdersHandler', 59 | getOrdersInjectorPromise, 60 | new OrderDispatcher(uniswapXOrderService, relayOrderService, log) 61 | ) 62 | 63 | module.exports = { 64 | getOrdersHandler: getOrdersHandler.handler, 65 | } 66 | -------------------------------------------------------------------------------- /lib/models/RelayOrder.ts: -------------------------------------------------------------------------------- 1 | import { OrderType, RelayOrder as SDKRelayOrder } from '@uniswap/uniswapx-sdk' 2 | import { ORDER_STATUS, RelayOrderEntity } from '../entities' 3 | import { GetRelayOrderResponse } from '../handlers/get-orders/schema/GetRelayOrderResponse' 4 | import { Order } from './Order' 5 | 6 | export class RelayOrder extends Order { 7 | constructor( 8 | readonly inner: SDKRelayOrder, 9 | readonly signature: string, 10 | readonly chainId: number, 11 | readonly orderStatus?: ORDER_STATUS 12 | ) { 13 | super() 14 | } 15 | 16 | get orderType(): OrderType { 17 | return OrderType.Relay 18 | } 19 | 20 | public toEntity(orderStatus: ORDER_STATUS): RelayOrderEntity { 21 | const { input } = this.inner.info 22 | const decodedOrder = this.inner 23 | const order: RelayOrderEntity = { 24 | type: OrderType.Relay, 25 | encodedOrder: decodedOrder.serialize(), 26 | signature: this.signature, 27 | nonce: decodedOrder.info.nonce.toString(), 28 | orderHash: decodedOrder.hash().toLowerCase(), 29 | chainId: decodedOrder.chainId, 30 | orderStatus: orderStatus, 31 | offerer: decodedOrder.info.swapper.toLowerCase(), 32 | input: { 33 | token: input.token, 34 | amount: input.amount.toString(), 35 | recipient: input.recipient.toString(), 36 | }, 37 | relayFee: { 38 | token: decodedOrder.info.fee.token, 39 | startAmount: decodedOrder.info.fee.startAmount.toString(), 40 | endAmount: decodedOrder.info.fee.endAmount.toString(), 41 | startTime: decodedOrder.info.fee.startTime, 42 | endTime: decodedOrder.info.fee.endTime, 43 | }, 44 | reactor: decodedOrder.info.reactor.toLowerCase(), 45 | deadline: decodedOrder.info.deadline, 46 | pair: `${input.token}-${decodedOrder.info.fee.token}-${decodedOrder.chainId}`, 47 | } 48 | return order 49 | } 50 | 51 | public static fromEntity(entity: RelayOrderEntity) { 52 | return new RelayOrder( 53 | SDKRelayOrder.parse(entity.encodedOrder, entity.chainId), 54 | entity.signature, 55 | entity.chainId, 56 | entity.orderStatus 57 | ) 58 | } 59 | 60 | public toGetResponse(): GetRelayOrderResponse { 61 | return { 62 | type: OrderType.Relay, 63 | orderStatus: this.orderStatus as ORDER_STATUS, 64 | signature: this.signature, 65 | encodedOrder: this.inner.serialize(), 66 | chainId: this.chainId, 67 | 68 | orderHash: this.inner.hash(), 69 | swapper: this.inner.info.swapper, 70 | reactor: this.inner.info.reactor, 71 | deadline: this.inner.info.deadline, 72 | input: { 73 | token: this.inner.info.input.token, 74 | amount: this.inner.info.input.amount.toString(), 75 | recipient: this.inner.info.input.recipient, 76 | }, 77 | relayFee: { 78 | token: this.inner.info.fee.token, 79 | startAmount: this.inner.info.fee.startAmount.toString(), 80 | endAmount: this.inner.info.fee.endAmount.toString(), 81 | startTime: this.inner.info.fee.startTime, 82 | endTime: this.inner.info.fee.endTime, 83 | }, 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/repositories/util.ts: -------------------------------------------------------------------------------- 1 | import { TABLE_KEY } from '../config/dynamodb' 2 | 3 | export enum TABLE_NAMES { 4 | LimitOrders = 'LimitOrders', 5 | RelayOrders = 'RelayOrders', 6 | Orders = 'Orders', 7 | Nonces = 'Nonces', 8 | QuoteMetadata = 'QuoteMetadata', 9 | UnimindParameters = 'UnimindParameters', 10 | } 11 | 12 | export const getTableIndices = (tableName: TABLE_NAMES) => { 13 | switch (tableName) { 14 | case TABLE_NAMES.LimitOrders: 15 | case TABLE_NAMES.Orders: 16 | case TABLE_NAMES.RelayOrders: 17 | default: 18 | return { 19 | [`${TABLE_KEY.OFFERER}-${TABLE_KEY.CREATED_AT}-all`]: { 20 | partitionKey: TABLE_KEY.OFFERER, 21 | sortKey: TABLE_KEY.CREATED_AT, 22 | }, 23 | [`${TABLE_KEY.ORDER_STATUS}-${TABLE_KEY.CREATED_AT}-all`]: { 24 | partitionKey: TABLE_KEY.ORDER_STATUS, 25 | sortKey: TABLE_KEY.CREATED_AT, 26 | }, 27 | [`${TABLE_KEY.FILLER}-${TABLE_KEY.CREATED_AT}-all`]: { 28 | partitionKey: TABLE_KEY.FILLER, 29 | sortKey: TABLE_KEY.CREATED_AT, 30 | }, 31 | [`${TABLE_KEY.CHAIN_ID}-${TABLE_KEY.CREATED_AT}-all`]: { 32 | partitionKey: TABLE_KEY.CHAIN_ID, 33 | sortKey: TABLE_KEY.CREATED_AT, 34 | }, 35 | [`${TABLE_KEY.FILLER}_${TABLE_KEY.ORDER_STATUS}-${TABLE_KEY.CREATED_AT}-all`]: { 36 | partitionKey: `${TABLE_KEY.FILLER}_${TABLE_KEY.ORDER_STATUS}`, 37 | sortKey: TABLE_KEY.CREATED_AT, 38 | }, 39 | [`${TABLE_KEY.FILLER}_${TABLE_KEY.OFFERER}-${TABLE_KEY.CREATED_AT}-all`]: { 40 | partitionKey: `${TABLE_KEY.FILLER}_${TABLE_KEY.OFFERER}`, 41 | sortKey: TABLE_KEY.CREATED_AT, 42 | }, 43 | [`${TABLE_KEY.FILLER}_${TABLE_KEY.OFFERER}_${TABLE_KEY.ORDER_STATUS}-${TABLE_KEY.CREATED_AT}-all`]: { 44 | partitionKey: `${TABLE_KEY.FILLER}_${TABLE_KEY.OFFERER}_${TABLE_KEY.ORDER_STATUS}`, 45 | sortKey: TABLE_KEY.CREATED_AT, 46 | }, 47 | [`${TABLE_KEY.OFFERER}_${TABLE_KEY.ORDER_STATUS}-${TABLE_KEY.CREATED_AT}-all`]: { 48 | partitionKey: `${TABLE_KEY.OFFERER}_${TABLE_KEY.ORDER_STATUS}`, 49 | sortKey: TABLE_KEY.CREATED_AT, 50 | }, 51 | [`${TABLE_KEY.CHAIN_ID}_${TABLE_KEY.FILLER}-${TABLE_KEY.CREATED_AT}-all`]: { 52 | partitionKey: `${TABLE_KEY.CHAIN_ID}_${TABLE_KEY.FILLER}`, 53 | sortKey: TABLE_KEY.CREATED_AT, 54 | }, 55 | [`${TABLE_KEY.CHAIN_ID}_${TABLE_KEY.ORDER_STATUS}-${TABLE_KEY.CREATED_AT}-all`]: { 56 | partitionKey: `${TABLE_KEY.CHAIN_ID}_${TABLE_KEY.ORDER_STATUS}`, 57 | sortKey: TABLE_KEY.CREATED_AT, 58 | }, 59 | [`${TABLE_KEY.CHAIN_ID}_${TABLE_KEY.ORDER_STATUS}_${TABLE_KEY.FILLER}-${TABLE_KEY.CREATED_AT}-all`]: { 60 | partitionKey: `${TABLE_KEY.CHAIN_ID}_${TABLE_KEY.ORDER_STATUS}_${TABLE_KEY.FILLER}`, 61 | sortKey: TABLE_KEY.CREATED_AT, 62 | }, 63 | [`${TABLE_KEY.PAIR}-${TABLE_KEY.CREATED_AT}-all`]: { 64 | partitionKey: TABLE_KEY.PAIR, 65 | sortKey: TABLE_KEY.CREATED_AT, 66 | }, 67 | offererNonceIndex: { partitionKey: TABLE_KEY.OFFERER, sortKey: TABLE_KEY.NONCE }, 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/unit/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { DutchOrderInfo, OrderType, REACTOR_ADDRESS_MAPPING } from '@uniswap/uniswapx-sdk' 2 | import { Context } from 'aws-lambda' 3 | import { BigNumber } from 'ethers' 4 | import { ChainId } from '../../lib/util/chain' 5 | 6 | export const COSIGNATURE = 7 | '0xf2e1e1aa8584396c5536afbd10f065b13beedbeea678dd0be884bd110a7b4c4425eb5fe7c28ebd2b97b69fb7ebc582f1ea2340961460b0a4ba2b3e71d94006b41c' 8 | 9 | export const ORDER_INFO: DutchOrderInfo = { 10 | deadline: 10, 11 | swapper: '0x0000000000000000000000000000000000000001', 12 | reactor: REACTOR_ADDRESS_MAPPING[1][OrderType.Dutch]!.toLowerCase(), 13 | decayStartTime: 20, 14 | decayEndTime: 25, 15 | input: { 16 | token: '0x0000000000000000000000000000000000000003', 17 | endAmount: BigNumber.from(30), 18 | startAmount: BigNumber.from(30), 19 | }, 20 | nonce: BigNumber.from('40'), 21 | outputs: [ 22 | { 23 | endAmount: BigNumber.from(50), 24 | startAmount: BigNumber.from(60), 25 | recipient: '0x0000000000000000000000000000000000000004', 26 | token: '0x0000000000000000000000000000000000000005', 27 | }, 28 | ], 29 | exclusiveFiller: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 30 | exclusivityOverrideBps: BigNumber.from(5), 31 | additionalValidationContract: '0x0000000000000000000000000000000000000000', 32 | additionalValidationData: '0x', 33 | } 34 | 35 | export const QUOTE_ID = '55e2cfca-5521-4a0a-b597-7bfb569032d7' 36 | export const REQUEST_ID = '55e2cfca-5521-4a0a-b597-7bfb569032d8' 37 | export const SIGNATURE = 38 | '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010' 39 | 40 | export const EVENT_CONTEXT = {} as unknown as Context 41 | 42 | export const Tokens = { 43 | MAINNET: { 44 | USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 45 | WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 46 | }, 47 | ARBITRUM_ONE: { 48 | USDC: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', 49 | WETH: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', 50 | }, 51 | } 52 | 53 | export const MOCK_LATEST_BLOCK = 100 54 | const providerGetLatestBlockMock = jest.fn().mockResolvedValue(MOCK_LATEST_BLOCK) 55 | const providerGetBlockMock = jest.fn().mockImplementation(() => { 56 | return Promise.resolve({ 57 | number: MOCK_LATEST_BLOCK, 58 | timestamp: Math.floor(Date.now() / 1000), // Current timestamp at execution time 59 | }) 60 | }) 61 | 62 | export const MOCK_PROVIDER_MAP = new Map([ 63 | [ 64 | ChainId.MAINNET, 65 | { 66 | getBlockNumber: providerGetLatestBlockMock, 67 | getBlock: providerGetBlockMock, 68 | }, 69 | ], 70 | [ 71 | ChainId.UNICHAIN, 72 | { 73 | getBlockNumber: providerGetLatestBlockMock, 74 | getBlock: providerGetBlockMock, 75 | }, 76 | ], 77 | [ 78 | ChainId.BASE, 79 | { 80 | getBlockNumber: providerGetLatestBlockMock, 81 | getBlock: providerGetBlockMock, 82 | }, 83 | ], 84 | [ 85 | ChainId.POLYGON, 86 | { 87 | getBlockNumber: providerGetLatestBlockMock, 88 | getBlock: providerGetBlockMock, 89 | }, 90 | ], 91 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 92 | ]) as any 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uniswap/uniswapx-service", 3 | "version": "0.1.0", 4 | "bin": { 5 | "app": "./dist/bin/app.js" 6 | }, 7 | "license": "GPL", 8 | "type": "commonjs", 9 | "repository": "https://github.com/Uniswap/uniswapx-service", 10 | "scripts": { 11 | "build": "tsc", 12 | "clean": "rm -rf dist cdk.out", 13 | "watch": "tsc -w", 14 | "test": "jest --testPathPattern=\"test/(unit|factories)\"", 15 | "test:watch": "yarn test --watch", 16 | "test:integ": "jest --detectOpenHandles --forceExit --testPathPattern=test/integ/", 17 | "test:e2e": "jest --detectOpenHandles --runInBand --testPathPattern=test/e2e/", 18 | "cdk": "cdk", 19 | "fix": "run-s fix:*", 20 | "fix:prettier": "prettier \"lib/**/*.ts\" --write", 21 | "fix:lint": "eslint lib --ext .ts --fix", 22 | "lint": "eslint lib test --ext .ts", 23 | "coverage": "jest --detectOpenHandles --runInBand --forceExit --coverage --testPathPattern=\"test/(factories/|unit/|integ/)\"" 24 | }, 25 | "devDependencies": { 26 | "@shelf/jest-dynamodb": "^3.4.4", 27 | "@types/aws-lambda": "^8.10.108", 28 | "@types/bunyan": "^1.8.8", 29 | "@types/chai-subset": "^1.3.3", 30 | "@types/expect": "^24.3.0", 31 | "@types/express": "^4.17.21", 32 | "@types/jest": "^29.2.0", 33 | "@types/lodash": "^4.14.195", 34 | "@types/node": "^20.14.8", 35 | "@types/qs": "^6.9.7", 36 | "@types/uuid": "^8.3.4", 37 | "@typescript-eslint/eslint-plugin": "^5.40.1", 38 | "@typescript-eslint/parser": "^5.40.1", 39 | "aws-sdk-client-mock": "^2.1.1", 40 | "esbuild": "^0.15.12", 41 | "eslint": "^8.57.0", 42 | "eslint-config-prettier": "^8.5.0", 43 | "eslint-plugin-eslint-comments": "^3.2.0", 44 | "eslint-plugin-import": "^2.26.0", 45 | "eslint-plugin-jest": "^27.9.0", 46 | "ethers": "^5.7.2", 47 | "jest": "^29.2.2", 48 | "jest-mock-extended": "^3.0.5", 49 | "prettier": "^2.7.1", 50 | "prettier-plugin-organize-imports": "^3.1.1", 51 | "sinon": "^14.0.1", 52 | "ts-jest": "^29.0.3", 53 | "ts-node": "^10.9.1", 54 | "typechain": "^8.1.0", 55 | "typescript": "^4.8.4" 56 | }, 57 | "dependencies": { 58 | "@aws-lambda-powertools/logger": "^1.18.0", 59 | "@aws-lambda-powertools/metrics": "^1.18.0", 60 | "@aws-sdk/client-kms": "^3.621.0", 61 | "@aws-sdk/client-s3": "^3.341.0", 62 | "@aws-sdk/client-sfn": "^3.341.0", 63 | "@swc/core": "^1.3.101", 64 | "@swc/jest": "^0.2.29", 65 | "@types/sinon": "^10.0.13", 66 | "@uniswap/permit2-sdk": "^1.4.0", 67 | "@uniswap/signer": "^0.0.4", 68 | "@uniswap/uniswapx-sdk": "3.0.0-beta.10", 69 | "@uniswap/universal-router-sdk": "^4.18.1", 70 | "aws-cdk-lib": "2.200.1", 71 | "aws-embedded-metrics": "^4.1.0", 72 | "aws-sdk": "^2.1238.0", 73 | "axios": "^1.2.1", 74 | "bunyan": "^1.8.15", 75 | "constructs": "^10.1.137", 76 | "dotenv": "^16.0.3", 77 | "dynamodb-toolbox": "^0.5.0", 78 | "env-cmd": "^10.1.0", 79 | "esm": "^3.2.25", 80 | "express": "^4.18.2", 81 | "joi": "^17.7.0", 82 | "source-map-support": "^0.5.21", 83 | "uuid": "^9.0.0" 84 | }, 85 | "prettier": { 86 | "printWidth": 120, 87 | "semi": false, 88 | "singleQuote": true, 89 | "endOfLine": "lf", 90 | "insertFinalNewline": true 91 | }, 92 | "engines": { 93 | "node": ">=20.0.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/handlers/check-order-status/injector.ts: -------------------------------------------------------------------------------- 1 | import { OrderType, OrderValidator, UniswapXEventWatcher } from '@uniswap/uniswapx-sdk' 2 | import { MetricsLogger } from 'aws-embedded-metrics' 3 | import { DynamoDB } from 'aws-sdk' 4 | import { default as bunyan, default as Logger } from 'bunyan' 5 | import { ethers } from 'ethers' 6 | import { ORDER_STATUS, UniswapXOrderEntity } from '../../entities' 7 | import { checkDefined } from '../../preconditions/preconditions' 8 | import { BaseOrdersRepository } from '../../repositories/base' 9 | import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository' 10 | import { setGlobalMetrics } from '../../util/metrics' 11 | import { SfnInjector, SfnStateInputOutput } from '../base/index' 12 | import { getWatcher } from './util' 13 | import { RPC_HEADERS } from '../../util/constants' 14 | 15 | export interface RequestInjected { 16 | log: Logger 17 | chainId: number 18 | quoteId: string 19 | orderHash: string 20 | startingBlockNumber: number 21 | orderStatus: ORDER_STATUS 22 | getFillLogAttempts: number 23 | retryCount: number 24 | runIndex: number 25 | provider: ethers.providers.StaticJsonRpcProvider 26 | orderWatcher: UniswapXEventWatcher 27 | orderQuoter: OrderValidator 28 | orderType: OrderType 29 | stateMachineArn: string 30 | } 31 | 32 | export interface ContainerInjected { 33 | dbInterface: BaseOrdersRepository 34 | } 35 | 36 | export class CheckOrderStatusInjector extends SfnInjector { 37 | public async buildContainerInjected(): Promise { 38 | return { 39 | dbInterface: DutchOrdersRepository.create(new DynamoDB.DocumentClient()), 40 | } 41 | } 42 | 43 | public async getRequestInjected( 44 | event: SfnStateInputOutput, 45 | log: Logger, 46 | metrics: MetricsLogger 47 | ): Promise { 48 | metrics.setNamespace('Uniswap') 49 | metrics.setDimensions({ Service: 'UniswapXService' }) 50 | setGlobalMetrics(metrics) 51 | 52 | log = log.child({ 53 | serializers: bunyan.stdSerializers, 54 | }) 55 | 56 | const chainId = checkDefined(event.chainId, 'chainId not defined') as number 57 | const rpcURL = process.env[`RPC_${chainId}`] 58 | if (!rpcURL) { 59 | throw new Error(`RPC_${chainId} not set`) 60 | } 61 | const provider = new ethers.providers.StaticJsonRpcProvider({ 62 | url: rpcURL, 63 | headers: RPC_HEADERS 64 | }, chainId) 65 | const quoter = new OrderValidator(provider, chainId) 66 | const orderType = event.orderType as OrderType 67 | 68 | const watcher = getWatcher(provider, chainId, orderType) 69 | 70 | return { 71 | log, 72 | chainId: event.chainId as number, 73 | orderHash: event.orderHash as string, 74 | quoteId: event.quoteId as string, 75 | startingBlockNumber: event.startingBlockNumber ? (event.startingBlockNumber as number) : 0, 76 | orderStatus: event.orderStatus as ORDER_STATUS, 77 | getFillLogAttempts: event.getFillLogAttempts ? (event.getFillLogAttempts as number) : 0, 78 | retryCount: event.retryCount ? (event.retryCount as number) : 0, 79 | runIndex: event.runIndex ? (event.runIndex as number) : 0, 80 | provider: provider, 81 | orderWatcher: watcher, 82 | orderQuoter: quoter, 83 | orderType: orderType, 84 | stateMachineArn: event.stateMachineArn as string, 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/repositories/RelayOrderRepository.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | import Logger from 'bunyan' 3 | import { Entity, Table } from 'dynamodb-toolbox' 4 | 5 | import { DYNAMODB_TYPES } from '../config/dynamodb' 6 | import { RelayOrderEntity } from '../entities' 7 | import { BaseOrdersRepository, MODEL_NAME } from './base' 8 | import { GenericOrdersRepository } from './generic-orders-repository' 9 | import { OffchainOrderIndexMapper } from './IndexMappers/OffchainOrderIndexMapper' 10 | import { getTableIndices, TABLE_NAMES } from './util' 11 | 12 | export class RelayOrderRepository extends GenericOrdersRepository { 13 | static create(documentClient: DocumentClient): BaseOrdersRepository { 14 | const log = Logger.createLogger({ 15 | name: 'RelayOrdersRepository', 16 | serializers: Logger.stdSerializers, 17 | }) 18 | 19 | const relayOrdersTable = new Table({ 20 | name: TABLE_NAMES.RelayOrders, 21 | partitionKey: 'orderHash', 22 | DocumentClient: documentClient, 23 | indexes: getTableIndices(TABLE_NAMES.RelayOrders), 24 | }) 25 | 26 | const relayOrderEntity = new Entity({ 27 | name: MODEL_NAME.Relay, 28 | attributes: { 29 | orderHash: { partitionKey: true, type: DYNAMODB_TYPES.STRING }, 30 | encodedOrder: { type: DYNAMODB_TYPES.STRING, required: true }, 31 | signature: { type: DYNAMODB_TYPES.STRING, required: true }, 32 | orderStatus: { type: DYNAMODB_TYPES.STRING, required: true }, 33 | nonce: { type: DYNAMODB_TYPES.STRING, required: true }, 34 | offerer: { type: DYNAMODB_TYPES.STRING, required: true }, 35 | filler: { type: DYNAMODB_TYPES.STRING }, 36 | deadline: { type: DYNAMODB_TYPES.NUMBER }, 37 | createdAt: { type: DYNAMODB_TYPES.NUMBER }, 38 | reactor: { type: DYNAMODB_TYPES.STRING }, 39 | type: { type: DYNAMODB_TYPES.STRING }, 40 | chainId: { type: DYNAMODB_TYPES.NUMBER }, 41 | input: { type: DYNAMODB_TYPES.MAP }, 42 | relayFee: { type: DYNAMODB_TYPES.MAP }, 43 | quoteId: { type: DYNAMODB_TYPES.STRING }, 44 | txHash: { type: DYNAMODB_TYPES.STRING }, 45 | fillBlock: { type: DYNAMODB_TYPES.NUMBER }, 46 | settledAmounts: { type: DYNAMODB_TYPES.LIST }, 47 | 48 | offerer_orderStatus: { type: DYNAMODB_TYPES.STRING }, 49 | filler_orderStatus: { type: DYNAMODB_TYPES.STRING }, 50 | filler_offerer: { type: DYNAMODB_TYPES.STRING }, 51 | chainId_filler: { type: DYNAMODB_TYPES.STRING }, 52 | chainId_orderStatus: { type: DYNAMODB_TYPES.STRING }, 53 | chainId_orderStatus_filler: { type: DYNAMODB_TYPES.STRING }, 54 | filler_offerer_orderStatus: { type: DYNAMODB_TYPES.STRING }, 55 | }, 56 | table: relayOrdersTable, 57 | }) 58 | 59 | const nonceTable = new Table({ 60 | name: TABLE_NAMES.Nonces, 61 | partitionKey: 'offerer', 62 | DocumentClient: documentClient, 63 | }) 64 | 65 | const nonceEntity = new Entity({ 66 | name: 'Nonce', 67 | attributes: { 68 | offerer: { partitionKey: true, type: DYNAMODB_TYPES.STRING }, 69 | nonce: { type: DYNAMODB_TYPES.STRING, required: true }, 70 | }, 71 | table: nonceTable, 72 | } as const) 73 | 74 | return new RelayOrderRepository( 75 | relayOrdersTable, 76 | relayOrderEntity, 77 | nonceEntity, 78 | log, 79 | new OffchainOrderIndexMapper() 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/handlers/get-orders/schema/GetDutchV3OrderResponse.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@uniswap/uniswapx-sdk' 2 | import Joi from 'joi' 3 | import { ORDER_STATUS } from '../../../entities' 4 | import FieldValidator from '../../../util/field-validator' 5 | import { Route } from '../../../repositories/quote-metadata-repository' 6 | import { CommonOrderValidationFields } from './Common' 7 | 8 | export type GetDutchV3OrderResponse = { 9 | type: OrderType.Dutch_V3 10 | orderStatus: ORDER_STATUS 11 | signature: string 12 | encodedOrder: string 13 | 14 | orderHash: string 15 | chainId: number 16 | swapper: string 17 | reactor: string 18 | 19 | txHash: string | undefined 20 | fillBlock: number | undefined 21 | deadline: number 22 | input: { 23 | token: string 24 | startAmount: string 25 | curve: { 26 | relativeBlocks: number[] 27 | relativeAmounts: string[] 28 | } 29 | maxAmount: string 30 | adjustmentPerGweiBaseFee: string 31 | } 32 | outputs: { 33 | token: string 34 | startAmount: string 35 | curve: { 36 | relativeBlocks: number[] 37 | relativeAmounts: string[] 38 | } 39 | recipient: string 40 | minAmount: string 41 | adjustmentPerGweiBaseFee: string 42 | }[] 43 | settledAmounts: { 44 | tokenOut: string 45 | amountOut: string 46 | tokenIn: string 47 | amountIn: string 48 | }[] | undefined 49 | startingBaseFee: string 50 | cosignerData: { 51 | decayStartBlock: number 52 | exclusiveFiller: string 53 | inputOverride: string 54 | outputOverrides: string[] 55 | } 56 | cosignature: string 57 | nonce: string 58 | quoteId: string | undefined 59 | requestId: string | undefined 60 | createdAt: number | undefined 61 | route: Route | undefined 62 | } 63 | 64 | export const CosignerDataJoi = Joi.object({ 65 | decayStartBlock: Joi.number(), 66 | exclusiveFiller: FieldValidator.isValidEthAddress(), 67 | inputOverride: FieldValidator.isValidAmount(), 68 | outputOverrides: Joi.array().items(FieldValidator.isValidAmount()), 69 | }) 70 | 71 | export const GetDutchV3OrderResponseEntryJoi = Joi.object({ 72 | ...CommonOrderValidationFields, 73 | //only Dutch_V3 74 | type: Joi.string().valid(OrderType.Dutch_V3).required(), 75 | startingBaseFee: FieldValidator.isValidAmount(), 76 | fillBlock: FieldValidator.isValidNumber(), 77 | input: Joi.object({ 78 | token: FieldValidator.isValidEthAddress().required(), 79 | startAmount: FieldValidator.isValidAmount().required(), 80 | curve: Joi.object({ 81 | relativeBlocks: Joi.array().items(FieldValidator.isValidNumber()), 82 | relativeAmounts: Joi.array().items(FieldValidator.isValidBigIntString()), 83 | }), 84 | maxAmount: FieldValidator.isValidAmount(), 85 | adjustmentPerGweiBaseFee: FieldValidator.isValidAmount(), 86 | }), 87 | outputs: Joi.array().items( 88 | Joi.object({ 89 | token: FieldValidator.isValidEthAddress().required(), 90 | startAmount: FieldValidator.isValidAmount().required(), 91 | curve: Joi.object({ 92 | relativeBlocks: Joi.array().items(FieldValidator.isValidNumber()), 93 | relativeAmounts: Joi.array().items(FieldValidator.isValidBigIntString()), 94 | }), 95 | recipient: FieldValidator.isValidEthAddress().required(), 96 | minAmount: FieldValidator.isValidAmount(), 97 | adjustmentPerGweiBaseFee: FieldValidator.isValidAmount(), 98 | }) 99 | ), 100 | cosignerData: CosignerDataJoi, 101 | }) 102 | -------------------------------------------------------------------------------- /test/unit/repositories/quote-metadata-repository.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | import { mock } from 'jest-mock-extended' 3 | import { DynamoQuoteMetadataRepository, QuoteMetadata } from '../../../lib/repositories/quote-metadata-repository' 4 | 5 | describe('QuoteMetadataRepository', () => { 6 | const mockDocumentClient = mock() 7 | 8 | const mockQuoteMetadata: QuoteMetadata = { 9 | quoteId: 'test-quote-id', 10 | referencePrice: "21212121", 11 | priceImpact: 0.21, 12 | blockNumber: 123456, 13 | route: { 14 | quote: "1234", 15 | quoteGasAdjusted: "5678", 16 | gasPriceWei: "1234", 17 | gasUseEstimateQuote: "2345", 18 | gasUseEstimate: "3456", 19 | methodParameters: { 20 | calldata: "0xabcdef", 21 | value: "1234", 22 | to: "0abcdef" 23 | } 24 | }, 25 | pair: '0x0000000000000000000000000000000000000000-0x1111111111111111111111111111111111111111-123', 26 | usedUnimind: false 27 | } 28 | 29 | beforeEach(() => { 30 | jest.clearAllMocks() 31 | }) 32 | 33 | describe('put', () => { 34 | it('successfully puts quote metadata', async () => { 35 | const repository = DynamoQuoteMetadataRepository.create(mockDocumentClient) 36 | mockDocumentClient.put.mockReturnValue({ 37 | promise: () => Promise.resolve({}), 38 | } as any) 39 | 40 | await repository.put(mockQuoteMetadata) 41 | 42 | expect(mockDocumentClient.put).toHaveBeenCalledTimes(1) 43 | }) 44 | 45 | it('throws error when put fails', async () => { 46 | const repository = DynamoQuoteMetadataRepository.create(mockDocumentClient) 47 | const error = new Error('DynamoDB Error') 48 | mockDocumentClient.put.mockReturnValue({ 49 | promise: () => Promise.reject(error), 50 | } as any) 51 | 52 | await expect(repository.put(mockQuoteMetadata)).rejects.toThrow(error) 53 | }) 54 | 55 | it('throws error when missing required fields', async () => { 56 | const repository = DynamoQuoteMetadataRepository.create(mockDocumentClient) 57 | 58 | const incompleteValues = { 59 | quoteId: 'messed-up', 60 | // missing referencePrice 61 | priceImpact: 0.21, 62 | } 63 | 64 | await expect(repository.put(incompleteValues as any)).rejects.toThrow( 65 | "'referencePrice' is a required field" 66 | ) 67 | expect(mockDocumentClient.put).not.toHaveBeenCalled() 68 | }) 69 | }) 70 | 71 | describe('getByQuoteId', () => { 72 | it('successfully gets quote metadata by quoteId', async () => { 73 | const repository = DynamoQuoteMetadataRepository.create(mockDocumentClient) 74 | mockDocumentClient.get.mockReturnValue({ 75 | promise: () => Promise.resolve({ Item: mockQuoteMetadata }), 76 | } as any) 77 | 78 | const result = await repository.getByQuoteId('test-quote-id') 79 | 80 | expect(result).toEqual(mockQuoteMetadata) 81 | expect(mockDocumentClient.get).toHaveBeenCalledTimes(1) 82 | }) 83 | 84 | it('returns undefined when item not found', async () => { 85 | const repository = DynamoQuoteMetadataRepository.create(mockDocumentClient) 86 | mockDocumentClient.get.mockReturnValue({ 87 | promise: () => Promise.resolve({ Item: undefined }), 88 | } as any) 89 | 90 | const result = await repository.getByQuoteId('the-truth') 91 | 92 | expect(result).toBeUndefined() 93 | expect(mockDocumentClient.get).toHaveBeenCalledTimes(1) 94 | }) 95 | }) 96 | }) -------------------------------------------------------------------------------- /lib/handlers/check-order-status/fill-event-logger.ts: -------------------------------------------------------------------------------- 1 | import { FillInfo, OrderType } from '@uniswap/uniswapx-sdk' 2 | import { Unit } from 'aws-embedded-metrics' 3 | import { BigNumber, ethers } from 'ethers' 4 | import { 5 | DutchV1OrderEntity, 6 | DutchV2OrderEntity, 7 | RelayOrderEntity, 8 | SettledAmount, 9 | UniswapXOrderEntity, 10 | } from '../../entities' 11 | import { AnalyticsService } from '../../services/analytics-service' 12 | import { ChainId } from '../../util/chain' 13 | import { metrics } from '../../util/metrics' 14 | import { log } from '../../util/log' 15 | 16 | export type ProcessFillEventRequest = { 17 | fillEvent: FillInfo 18 | order: UniswapXOrderEntity | RelayOrderEntity 19 | chainId: number 20 | startingBlockNumber: number 21 | settledAmounts: SettledAmount[] 22 | quoteId?: string 23 | tx?: ethers.providers.TransactionResponse 24 | block?: ethers.providers.Block 25 | fillTimeBlocks?: number 26 | timestamp: number 27 | } 28 | export class FillEventLogger { 29 | constructor( 30 | private fillEventBlockLookback: (chainId: ChainId) => number, 31 | private analyticsService: AnalyticsService 32 | ) {} 33 | 34 | public async processFillEvent({ 35 | fillEvent, 36 | quoteId, 37 | order, 38 | chainId, 39 | startingBlockNumber, 40 | settledAmounts, 41 | tx, 42 | block, 43 | fillTimeBlocks, 44 | timestamp, 45 | }: ProcessFillEventRequest): Promise { 46 | if (tx && block) { 47 | const receipt = await tx.wait() 48 | const gasCostInETH = ethers.utils.formatEther(receipt.effectiveGasPrice.mul(receipt.gasUsed)) 49 | 50 | let filteredOutputs = settledAmounts 51 | if (order.type != OrderType.Relay) { 52 | filteredOutputs = settledAmounts.filter(amount => amount.tokenOut == order.outputs[0].token) 53 | } 54 | if (filteredOutputs.length > 0) { 55 | this.analyticsService.logFillInfo( 56 | fillEvent, 57 | order, 58 | quoteId, 59 | timestamp, 60 | gasCostInETH, 61 | receipt.effectiveGasPrice.toString(), 62 | receipt.gasUsed.toString(), 63 | receipt.effectiveGasPrice.sub(block.baseFeePerGas ?? 0).toString(), 64 | fillTimeBlocks ?? -1, // -1 means we don't have a fill time in blocks 65 | filteredOutputs.reduce((prev, cur) => (prev && BigNumber.from(prev.amountOut).gt(cur.amountOut) ? prev : cur)) 66 | ) 67 | } else { 68 | log.error('no matching settled amounts found for fill event', { fillEvent }) 69 | } 70 | 71 | if (order.type === OrderType.Dutch || order.type === OrderType.Dutch_V2) { 72 | const percentDecayed = this.calculatePercentDecayed(order, timestamp) 73 | metrics.putMetric(`OrderSfn-PercentDecayedUntilFill-chain-${chainId}`, percentDecayed, Unit.Percent) 74 | } 75 | 76 | // blocks until fill is the number of blocks between the fill event and the starting block number (need to add back the look back blocks) 77 | if (startingBlockNumber != 0) { 78 | const blocksUntilFill = fillEvent.blockNumber - (startingBlockNumber + this.fillEventBlockLookback(chainId)) 79 | metrics.putMetric(`OrderSfn-BlocksUntilFill-chain-${chainId}`, blocksUntilFill, Unit.Count) 80 | } 81 | return settledAmounts 82 | } else { 83 | return [] 84 | } 85 | } 86 | 87 | private calculatePercentDecayed(order: DutchV1OrderEntity | DutchV2OrderEntity, timestamp: number): number { 88 | if (order.decayStartTime && order.decayEndTime) { 89 | return (timestamp - order.decayStartTime) / (order.decayEndTime - order.decayStartTime) 90 | } else return 0 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/repositories/limit-orders-repository.ts: -------------------------------------------------------------------------------- 1 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 2 | import Logger from 'bunyan' 3 | import { Entity, Table } from 'dynamodb-toolbox' 4 | 5 | import { DYNAMODB_TYPES } from '../config/dynamodb' 6 | import { UniswapXOrderEntity } from '../entities' 7 | import { BaseOrdersRepository, MODEL_NAME } from './base' 8 | import { GenericOrdersRepository } from './generic-orders-repository' 9 | import { OffchainOrderIndexMapper } from './IndexMappers/OffchainOrderIndexMapper' 10 | import { getTableIndices, TABLE_NAMES } from './util' 11 | 12 | export class LimitOrdersRepository extends GenericOrdersRepository { 13 | static create(documentClient: DocumentClient): BaseOrdersRepository { 14 | const log = Logger.createLogger({ 15 | name: 'LimitOrdersRepository', 16 | serializers: Logger.stdSerializers, 17 | }) 18 | 19 | const limitOrdersTable = new Table({ 20 | name: TABLE_NAMES.LimitOrders, 21 | partitionKey: 'orderHash', 22 | DocumentClient: documentClient, 23 | indexes: getTableIndices(TABLE_NAMES.LimitOrders), 24 | }) 25 | 26 | const limitOrderEntity = new Entity({ 27 | name: MODEL_NAME.LIMIT, 28 | attributes: { 29 | orderHash: { partitionKey: true, type: DYNAMODB_TYPES.STRING }, 30 | encodedOrder: { type: DYNAMODB_TYPES.STRING, required: true }, 31 | signature: { type: DYNAMODB_TYPES.STRING, required: true }, 32 | orderStatus: { type: DYNAMODB_TYPES.STRING, required: true }, 33 | nonce: { type: DYNAMODB_TYPES.STRING, required: true }, 34 | offerer: { type: DYNAMODB_TYPES.STRING, required: true }, 35 | filler: { type: DYNAMODB_TYPES.STRING }, 36 | decayStartTime: { type: DYNAMODB_TYPES.NUMBER }, 37 | decayEndTime: { type: DYNAMODB_TYPES.NUMBER }, 38 | deadline: { type: DYNAMODB_TYPES.NUMBER }, 39 | createdAt: { type: DYNAMODB_TYPES.NUMBER }, 40 | reactor: { type: DYNAMODB_TYPES.STRING }, 41 | type: { type: DYNAMODB_TYPES.STRING }, 42 | chainId: { type: DYNAMODB_TYPES.NUMBER }, 43 | input: { type: DYNAMODB_TYPES.MAP }, 44 | outputs: { type: DYNAMODB_TYPES.LIST }, 45 | offerer_orderStatus: { type: DYNAMODB_TYPES.STRING }, 46 | filler_orderStatus: { type: DYNAMODB_TYPES.STRING }, 47 | filler_offerer: { type: DYNAMODB_TYPES.STRING }, 48 | chainId_filler: { type: DYNAMODB_TYPES.STRING }, 49 | chainId_orderStatus: { type: DYNAMODB_TYPES.STRING }, 50 | chainId_orderStatus_filler: { type: DYNAMODB_TYPES.STRING }, 51 | filler_offerer_orderStatus: { type: DYNAMODB_TYPES.STRING }, 52 | quoteId: { type: DYNAMODB_TYPES.STRING }, 53 | txHash: { type: DYNAMODB_TYPES.STRING }, 54 | fillBlock: { type: DYNAMODB_TYPES.NUMBER }, 55 | settledAmounts: { type: DYNAMODB_TYPES.LIST }, 56 | pair: { type: DYNAMODB_TYPES.STRING }, 57 | }, 58 | table: limitOrdersTable, 59 | } as const) 60 | 61 | const nonceTable = new Table({ 62 | name: TABLE_NAMES.Nonces, 63 | partitionKey: 'offerer', 64 | DocumentClient: documentClient, 65 | }) 66 | 67 | const nonceEntity = new Entity({ 68 | name: 'Nonce', 69 | attributes: { 70 | offerer: { partitionKey: true, type: DYNAMODB_TYPES.STRING }, 71 | nonce: { type: DYNAMODB_TYPES.STRING, required: true }, 72 | }, 73 | table: nonceTable, 74 | } as const) 75 | 76 | return new LimitOrdersRepository( 77 | limitOrdersTable, 78 | limitOrderEntity, 79 | nonceEntity, 80 | log, 81 | new OffchainOrderIndexMapper() 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/handlers/check-order-status/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderType, RelayOrderValidator as OnChainRelayOrderValidator } from '@uniswap/uniswapx-sdk' 2 | import { DynamoDB } from 'aws-sdk' 3 | import { DocumentClient } from 'aws-sdk/clients/dynamodb' 4 | import { ethers } from 'ethers' 5 | import { CONFIG } from '../../Config' 6 | import { log } from '../../Logging' 7 | import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository' 8 | import { LimitOrdersRepository } from '../../repositories/limit-orders-repository' 9 | import { RelayOrderRepository } from '../../repositories/RelayOrderRepository' 10 | import { AnalyticsService } from '../../services/analytics-service' 11 | import { RelayOrderService } from '../../services/RelayOrderService' 12 | import { SUPPORTED_CHAINS } from '../../util/chain' 13 | import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator' 14 | import { FillEventLogger } from '../check-order-status/fill-event-logger' 15 | import { calculateDutchRetryWaitSeconds, FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../check-order-status/util' 16 | import { EventWatcherMap } from '../EventWatcherMap' 17 | import { OnChainValidatorMap } from '../OnChainValidatorMap' 18 | import { getMaxOpenOrders } from '../post-order/injector' 19 | import { CheckOrderStatusHandler } from './handler' 20 | import { CheckOrderStatusInjector } from './injector' 21 | import { CheckOrderStatusService, CheckOrderStatusUtils } from './service' 22 | import { RPC_HEADERS } from '../../util/constants' 23 | 24 | const relayOrderValidator = new OffChainRelayOrderValidator(() => new Date().getTime() / 1000) 25 | const relayOrderValidatorMap = new OnChainValidatorMap() 26 | for (const chainId of SUPPORTED_CHAINS) { 27 | relayOrderValidatorMap.set( 28 | chainId, 29 | new OnChainRelayOrderValidator(new ethers.providers.StaticJsonRpcProvider({ 30 | url: CONFIG.rpcUrls.get(chainId), 31 | headers: RPC_HEADERS 32 | }), chainId) 33 | ) 34 | } 35 | 36 | const relayOrderService = new RelayOrderService( 37 | relayOrderValidator, 38 | relayOrderValidatorMap, 39 | EventWatcherMap.createRelayEventWatcherMap(), 40 | RelayOrderRepository.create(new DynamoDB.DocumentClient()), 41 | log, 42 | getMaxOpenOrders, 43 | new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create()) 44 | ) 45 | 46 | const documentClient = new DocumentClient() 47 | const dutchOrdersRepository = DutchOrdersRepository.create(documentClient) 48 | const limitOrdersRepository = LimitOrdersRepository.create(documentClient) 49 | 50 | const checkOrderStatusInjectorPromise = new CheckOrderStatusInjector('checkOrderStatusInjector').build() 51 | const checkOrderStatusHandler = new CheckOrderStatusHandler( 52 | 'checkOrderStatusHandler', 53 | checkOrderStatusInjectorPromise, 54 | new CheckOrderStatusService( 55 | dutchOrdersRepository, 56 | FILL_EVENT_LOOKBACK_BLOCKS_ON, 57 | new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create()), 58 | new CheckOrderStatusUtils( 59 | OrderType.Dutch, 60 | AnalyticsService.create(), 61 | dutchOrdersRepository, 62 | calculateDutchRetryWaitSeconds 63 | ) 64 | ), 65 | 66 | new CheckOrderStatusService( 67 | LimitOrdersRepository.create(documentClient), 68 | FILL_EVENT_LOOKBACK_BLOCKS_ON, 69 | new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create()), 70 | new CheckOrderStatusUtils( 71 | OrderType.Limit, 72 | AnalyticsService.create(), 73 | limitOrdersRepository, 74 | calculateDutchRetryWaitSeconds 75 | ) 76 | ), 77 | relayOrderService 78 | ) 79 | 80 | module.exports = { 81 | checkOrderStatusHandler: checkOrderStatusHandler.handler, 82 | } 83 | -------------------------------------------------------------------------------- /test/factories/SDKDutchOrderV2Factory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CosignedV2DutchOrder as SDKDutchOrderV2, 3 | CosignedV2DutchOrderInfoJSON, 4 | encodeExclusiveFillerData, 5 | V2DutchOrderBuilder, 6 | } from '@uniswap/uniswapx-sdk' 7 | import { BigNumber, constants } from 'ethers' 8 | import { ChainId } from '../../lib/util/chain' 9 | import { Tokens } from '../unit/fixtures' 10 | import { PartialDeep } from './PartialDeep' 11 | 12 | /** 13 | * Helper class for building CosignedV2DutchOrders. 14 | * All values adpated from https://github.com/Uniswap/uniswapx-sdk/blob/7949043e7d2434553f84f588e1405e87d249a5aa/src/builder/V2DutchOrderBuilder.test.ts#L22 15 | */ 16 | export class SDKDutchOrderV2Factory { 17 | static buildDutchV2Order( 18 | chainId = ChainId.MAINNET, 19 | overrides: PartialDeep = {} 20 | ): SDKDutchOrderV2 { 21 | // Values adapted from https://github.com/Uniswap/uniswapx-sdk/blob/7949043e7d2434553f84f588e1405e87d249a5aa/src/utils/order.test.ts#L28 22 | const nowInSeconds = Math.floor(Date.now() / 1000) 23 | 24 | // Arbitrary default future time ten seconds in future 25 | const futureTime = nowInSeconds + 10 26 | 27 | let builder = new V2DutchOrderBuilder(chainId) 28 | 29 | builder = builder 30 | .cosigner(overrides.cosigner ?? constants.AddressZero) 31 | .cosignature(overrides.cosignature ?? '0x') 32 | .deadline(overrides.deadline ?? futureTime) 33 | .decayEndTime(overrides.cosignerData?.decayEndTime ?? futureTime) 34 | .decayStartTime(overrides.cosignerData?.decayStartTime ?? nowInSeconds) 35 | .swapper(overrides.swapper ?? '0x0000000000000000000000000000000000000000') 36 | .nonce(overrides.nonce ? BigNumber.from(overrides.nonce) : BigNumber.from(100)) 37 | .input({ 38 | token: overrides.input?.token ?? Tokens.MAINNET.USDC, 39 | startAmount: overrides.input?.startAmount 40 | ? BigNumber.from(overrides.input?.startAmount) 41 | : BigNumber.from('1000000'), 42 | endAmount: overrides.input?.endAmount ? BigNumber.from(overrides.input?.endAmount) : BigNumber.from('1000000'), 43 | }) 44 | .inputOverride( 45 | overrides.cosignerData?.inputOverride 46 | ? BigNumber.from(overrides.cosignerData?.inputOverride) 47 | : BigNumber.from('1000000') 48 | ) 49 | 50 | const outputs = overrides.outputs ?? [ 51 | { 52 | token: Tokens.MAINNET.WETH, 53 | startAmount: '1000000000000000000', 54 | endAmount: '1000000000000000000', 55 | recipient: '0x0000000000000000000000000000000000000000', 56 | }, 57 | ] 58 | for (const output of outputs) { 59 | builder = builder.output({ 60 | token: output?.token ?? Tokens.MAINNET.WETH, 61 | startAmount: output?.startAmount ? BigNumber.from(output?.startAmount) : BigNumber.from('1000000000000000000'), 62 | endAmount: output?.endAmount ? BigNumber.from(output?.endAmount) : BigNumber.from('1000000000000000000'), 63 | recipient: output?.recipient ?? '0x0000000000000000000000000000000000000000', 64 | }) 65 | } 66 | 67 | const outputOverrides = overrides.cosignerData?.outputOverrides 68 | ? overrides.cosignerData?.outputOverrides.map((num) => BigNumber.from(num)) 69 | : [BigNumber.from('1000000000000000000')] 70 | 71 | const validationInfo = encodeExclusiveFillerData( 72 | overrides.cosignerData?.exclusiveFiller ?? '0x1111111111111111111111111111111111111111', 73 | overrides.deadline ?? futureTime, 74 | chainId, 75 | overrides.additionalValidationContract ?? '0x2222222222222222222222222222222222222222' 76 | ) 77 | builder = builder.outputOverrides(outputOverrides).validation(validationInfo) 78 | 79 | return builder.build() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/handlers/base/sfn-handler.ts: -------------------------------------------------------------------------------- 1 | import { metricScope, MetricsLogger } from 'aws-embedded-metrics' 2 | import { default as bunyan, default as Logger } from 'bunyan' 3 | import Joi from 'joi' 4 | import { checkDefined } from '../../preconditions/preconditions' 5 | import { InjectionError, SfnInputValidationError } from '../../util/errors' 6 | 7 | export type SfnStateInputOutput = Record 8 | 9 | export type SfnHandler = (event: SfnStateInputOutput) => Promise 10 | 11 | export abstract class SfnInjector { 12 | protected containerInjected: CInj | undefined 13 | 14 | public constructor(protected readonly injectorName: string) { 15 | checkDefined(injectorName, 'Injector name must be defined') 16 | } 17 | 18 | protected abstract buildContainerInjected(): Promise 19 | 20 | public async build() { 21 | this.containerInjected = await this.buildContainerInjected() 22 | return this 23 | } 24 | 25 | public getContainerInjected(): CInj { 26 | return checkDefined(this.containerInjected, 'Container injected undefined. Must call build() before using.') 27 | } 28 | 29 | public abstract getRequestInjected(event: SfnStateInputOutput, log: Logger, metrics: MetricsLogger): Promise 30 | } 31 | 32 | export abstract class SfnLambdaHandler { 33 | protected abstract inputSchema(): Joi.ObjectSchema | null 34 | 35 | constructor( 36 | protected readonly handlerName: string, 37 | private readonly injectorPromise: Promise> 38 | ) {} 39 | 40 | get handler(): SfnHandler { 41 | return async (event: SfnStateInputOutput): Promise => { 42 | const handler = this.buildHandler() 43 | return await handler(event) 44 | } 45 | } 46 | 47 | protected buildHandler(): SfnHandler { 48 | return metricScope((metrics: MetricsLogger) => { 49 | const handle = async (sfnInput: SfnStateInputOutput): Promise => { 50 | const log: Logger = bunyan.createLogger({ 51 | name: this.handlerName, 52 | serializers: bunyan.stdSerializers, 53 | level: process.env.NODE_ENV == 'test' ? bunyan.FATAL + 1 : bunyan.INFO, 54 | }) 55 | 56 | await this.validateInput(sfnInput, log) 57 | 58 | const injector = await this.injectorPromise 59 | 60 | const containerInjected = injector.getContainerInjected() 61 | 62 | let requestInjected: RInj 63 | try { 64 | requestInjected = await injector.getRequestInjected(sfnInput, log, metrics) 65 | } catch (err) { 66 | log.error({ err, sfnInput }, 'Unexpected error building request injected.') 67 | throw new InjectionError(`Unexpected error building request injected:\n${err}`) 68 | } 69 | 70 | return await this.handleRequest({ containerInjected, requestInjected }) 71 | } 72 | 73 | return async (sfnInput: SfnStateInputOutput): Promise => { 74 | const response = await handle(sfnInput) 75 | 76 | return response 77 | } 78 | }) 79 | } 80 | 81 | private async validateInput(input: SfnStateInputOutput, log: Logger): Promise { 82 | const schema = this.inputSchema() 83 | 84 | if (schema) { 85 | const inputValidation = schema.validate(input, { 86 | allowUnknown: true, 87 | stripUnknown: true, 88 | }) 89 | if (inputValidation.error) { 90 | log.info({ inputValidation }, 'Input failed validation') 91 | throw new SfnInputValidationError(inputValidation.error.message) 92 | } 93 | } 94 | return input 95 | } 96 | 97 | public abstract handleRequest(input: { containerInjected: CInj; requestInjected: RInj }): Promise 98 | } 99 | -------------------------------------------------------------------------------- /lib/handlers/shared/get.ts: -------------------------------------------------------------------------------- 1 | import { MetricsLogger } from 'aws-embedded-metrics' 2 | import { Context } from 'aws-lambda' 3 | import bunyan, { default as Logger } from 'bunyan' 4 | import { UniswapXOrderEntity } from '../../entities' 5 | import { BaseOrdersRepository } from '../../repositories/base' 6 | import { setGlobalLogger } from '../../util/log' 7 | import { setGlobalMetrics } from '../../util/metrics' 8 | import { GetOrdersQueryParams, RawGetOrdersQueryParams } from '../get-orders/schema' 9 | import { GetOrderTypeQueryParamEnum } from '../get-orders/schema/GetOrderTypeQueryParamEnum' 10 | 11 | export interface ContainerInjected { 12 | dbInterface: BaseOrdersRepository 13 | } 14 | 15 | export type GetRequestInjected = { 16 | limit: number 17 | queryFilters: GetOrdersQueryParams 18 | requestId: string 19 | log: Logger 20 | cursor?: string 21 | executeAddress?: string 22 | } 23 | 24 | type RequestInjectedParams = { 25 | containerInjected: ContainerInjected 26 | requestQueryParams: RawGetOrdersQueryParams 27 | context: Context 28 | log: Logger 29 | metrics: MetricsLogger 30 | } 31 | 32 | export function getSharedRequestInjected({ 33 | containerInjected, 34 | requestQueryParams, 35 | context, 36 | log, 37 | metrics, 38 | }: RequestInjectedParams): GetRequestInjected { 39 | const requestId = context.awsRequestId 40 | 41 | log = log.child({ 42 | serializers: bunyan.stdSerializers, 43 | containerInjected: containerInjected, 44 | requestId, 45 | }) 46 | 47 | setGlobalLogger(log) 48 | 49 | metrics.setNamespace('Uniswap') 50 | metrics.setDimensions({ Service: 'UniswapXService' }) 51 | setGlobalMetrics(metrics) 52 | 53 | return { 54 | ...parseGetQueryParams(requestQueryParams), 55 | requestId, 56 | log, 57 | } 58 | } 59 | 60 | export const parseGetQueryParams = ( 61 | requestQueryParams: RawGetOrdersQueryParams 62 | ): { limit: number; queryFilters: GetOrdersQueryParams; cursor?: string; orderType?: string, executeAddress?: string } => { 63 | // default to no limit 64 | const limit = requestQueryParams?.limit ?? 0 65 | const orderStatus = requestQueryParams?.orderStatus 66 | const orderHash = requestQueryParams?.orderHash?.toLowerCase() 67 | // externally we use swapper 68 | const offerer = requestQueryParams?.swapper?.toLowerCase() 69 | const sortKey = requestQueryParams?.sortKey 70 | const defaultSort = sortKey ? 'gt(0)' : undefined 71 | const sort = requestQueryParams?.sort ?? defaultSort 72 | const filler = requestQueryParams?.filler 73 | const cursor = requestQueryParams?.cursor 74 | const chainId = requestQueryParams?.chainId 75 | const desc = requestQueryParams?.desc 76 | const orderHashes = requestQueryParams?.orderHashes?.split(',').map((orderHash: string) => orderHash.toLowerCase()) 77 | const orderType = 78 | requestQueryParams?.orderType && requestQueryParams?.orderType in GetOrderTypeQueryParamEnum 79 | ? requestQueryParams?.orderType 80 | : undefined 81 | const executeAddress = requestQueryParams?.executeAddress 82 | const pair = requestQueryParams?.pair 83 | 84 | return { 85 | limit: limit, 86 | orderType, 87 | executeAddress, 88 | queryFilters: { 89 | ...(orderStatus && { orderStatus: orderStatus }), 90 | ...(orderHash && { orderHash: orderHash }), 91 | ...(offerer && { offerer: offerer.toLowerCase() }), 92 | ...(sortKey && { sortKey: sortKey }), 93 | ...(filler && { filler: filler.toLowerCase() }), 94 | ...(sort && { sort: sort }), 95 | ...(chainId && { chainId: chainId }), 96 | ...(desc !== undefined && { desc: desc }), 97 | ...(orderHashes && { orderHashes: [...new Set(orderHashes)] }), 98 | ...(pair && { pair: pair }), 99 | }, 100 | ...(cursor && { cursor: cursor }), 101 | } 102 | } 103 | --------------------------------------------------------------------------------