Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
29abcc5
initial poc commit
ericneil-sanc Dec 9, 2025
d7d034c
initial commit
ericneil-sanc Dec 10, 2025
e448046
nits on permit2 test
ericneil-sanc Dec 11, 2025
cef5cb0
revert yarn.lock
ericneil-sanc Dec 11, 2025
fac755c
add fields to dutch repository
ericneil-sanc Dec 11, 2025
fff30cc
adjust comments
ericneil-sanc Dec 12, 2025
cef2257
add clarity to generateCosignerData comment
ericneil-sanc Dec 12, 2025
e09a3c3
convert generateCosignerData to JSDoc
ericneil-sanc Dec 12, 2025
960d111
add hybrid order tests
ericneil-sanc Dec 15, 2025
96051e1
add hybrid order info readme
ericneil-sanc Dec 17, 2025
4d5c589
use cosigned version
ericneil-sanc Dec 17, 2025
4bc28f8
add more tests
ericneil-sanc Dec 17, 2025
a96031a
address PR comments
ericneil-sanc Dec 17, 2025
67274e2
update to use latest sdk changes
ericneil-sanc Dec 19, 2025
b49c54f
update to beta 0.13 sdk
ericneil-sanc Jan 8, 2026
b2f3534
use .quote() on v4Validator
ericneil-sanc Jan 8, 2026
fd49abe
fix: match baselinePriorityFeeWei field for Dynamo
alanhwu Jan 8, 2026
eaa0bb7
rm unused logs
ericneil-sanc Jan 9, 2026
f7d80bf
rm unused
ericneil-sanc Jan 9, 2026
c5dddce
test curve generation
ericneil-sanc Jan 12, 2026
4055e2f
correct test suite
ericneil-sanc Jan 12, 2026
4227be4
update axios version
ericneil-sanc Jan 12, 2026
abd1233
proper multi output handling and tests
ericneil-sanc Jan 12, 2026
6c60996
proper payload for initializer
ericneil-sanc Jan 13, 2026
6162ccd
throw err if onchain validator not found for v4
ericneil-sanc Jan 13, 2026
9a1ca35
address comments
ericneil-sanc Jan 23, 2026
1e6144e
increase test coverage
ericneil-sanc Jan 23, 2026
a4caf09
Merge branch 'main' into hybrid-impl
ericneil-sanc Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/crons/gs-reaper/gs-reaper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BaseOrdersRepository, QueryResult } from '../../repositories/base'
import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository'
import { BLOCK_RANGE, REAPER_MAX_ATTEMPTS, DYNAMO_BATCH_WRITE_MAX, OLDEST_BLOCK_BY_CHAIN, REAPER_RANGES_PER_RUN, RPC_HEADERS, BLOCKS_IN_24_HOURS } from '../../util/constants'
import { ethers } from 'ethers'
import { CosignedPriorityOrder, CosignedV2DutchOrder, CosignedV3DutchOrder, DutchOrder, FillInfo, OrderType, OrderValidation, OrderValidator, REACTOR_ADDRESS_MAPPING, UniswapXEventWatcher, UniswapXOrder } from '@uniswap/uniswapx-sdk'
import { CosignedPriorityOrder, CosignedV2DutchOrder, CosignedV3DutchOrder, DutchOrder, FillInfo, HybridOrderClass, OrderType, OrderValidation, OrderValidator, REACTOR_ADDRESS_MAPPING, UniswapXEventWatcher, UniswapXOrder } from '@uniswap/uniswapx-sdk'
import { parseOrder } from '../../handlers/OrderParser'
import { getSettledAmounts } from '../../handlers/check-order-status/util'
import { ChainId } from '../../util/chain'
Expand Down Expand Up @@ -342,7 +342,13 @@ async function checkCancelledOrders(
const { order, signature } = await getOrderByHash(repo, orderHash)
// We only check for nonce used and expired for permissioned tokens
// since the order quoter can't move input tokens
const validation = PermissionedTokenValidator.isPermissionedToken(order.info.input.token, chainId)
// Type for legacy orders that have input at the info level
type LegacyUniswapXOrder = DutchOrder | CosignedV2DutchOrder | CosignedV3DutchOrder | CosignedPriorityOrder
Comment thread
ericneil-sanc marked this conversation as resolved.
Outdated
// Note: For v4 orders like Hybrid, input is at a different level
const inputToken = order instanceof HybridOrderClass
? order.order.input.token
: (order as LegacyUniswapXOrder).info.input.token
const validation = PermissionedTokenValidator.isPermissionedToken(inputToken, chainId)
? await new Permit2Validator(provider, chainId).validate(order)
: await quoter.validate({
order: order,
Expand Down
35 changes: 33 additions & 2 deletions lib/entities/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,36 @@ export type PriorityOrderEntity = SharedXOrderEntity & {
cosignature: string
}

// Db representation of Dutch V1, Dutch V2, Dutch V3, or Limit Order
export type HybridOrderInput = {
token: string
maxAmount: string
}

export type HybridOrderOutput = {
token: string
minAmount: string
recipient: string
}

export type HybridOrderEntity = SharedXOrderEntity & {
type: OrderType.Hybrid
auctionStartBlock: number
baselinePriorityFeeWei: string
scalingFactor: string
input: HybridOrderInput
outputs: HybridOrderOutput[]
priceCurve: string[]
cosigner: string
cosignerData: {
auctionTargetBlock: number
supplementalPriceCurve: string[]
}
cosignature: string
}

// Db representation of Dutch V1, Dutch V2, Dutch V3, Priority, Hybrid, or Limit Order
// indexes are returned at runtime but not represented on this type. Ideally we will include a mapping at repo layer boundary
export type UniswapXOrderEntity = DutchV1OrderEntity | DutchV2OrderEntity | PriorityOrderEntity | DutchV3OrderEntity
export type UniswapXOrderEntity = DutchV1OrderEntity | DutchV2OrderEntity | PriorityOrderEntity | DutchV3OrderEntity | HybridOrderEntity

export enum SORT_FIELDS {
CREATED_AT = 'createdAt',
Expand All @@ -167,3 +194,7 @@ export function isDutchV2OrderEntity(order: UniswapXOrderEntity): order is Dutch
export function isDutchV1OrderEntity(order: UniswapXOrderEntity): order is DutchV1OrderEntity {
return order.type === OrderType.Dutch || order.type === OrderType.Limit
}

export function isHybridOrderEntity(order: UniswapXOrderEntity): order is HybridOrderEntity {
return order.type === OrderType.Hybrid
}
13 changes: 12 additions & 1 deletion lib/handlers/check-order-status/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
OrderValidation,
OrderValidator,
UniswapXEventWatcher,
HybridOrderClass,
} from '@uniswap/uniswapx-sdk'
import { ethers } from 'ethers'
import { ORDER_STATUS, RelayOrderEntity, SettledAmount, UniswapXOrderEntity } from '../../entities'
Expand Down Expand Up @@ -81,7 +82,13 @@ export class CheckOrderStatusService {
const parsedOrder = parseOrder(order, chainId)
// We only check for nonce used and expired for permissioned tokens
// since the order quoter can't move input tokens
const isPermissionedToken = PermissionedTokenValidator.isPermissionedToken(parsedOrder.info.input.token, chainId)
// Type for legacy orders that have input at the info level
type LegacyUniswapXOrder = DutchOrder | CosignedV2DutchOrder | CosignedV3DutchOrder | CosignedPriorityOrder
// Note: For v4 orders like Hybrid, input is at a different level. Get input token safely.
const inputToken = parsedOrder instanceof HybridOrderClass
? parsedOrder.order.input.token
: (parsedOrder as LegacyUniswapXOrder).info.input.token
const isPermissionedToken = PermissionedTokenValidator.isPermissionedToken(inputToken, chainId)
const validationPromise = isPermissionedToken
? new Permit2Validator(provider, chainId).validate(parsedOrder)
: orderQuoter.validate({
Expand Down Expand Up @@ -170,6 +177,10 @@ export class CheckOrderStatusService {
fillTimeBlocks = fillBlock - orderCreationBlock;
break;
}
case OrderType.Hybrid: { // Exact
fillTimeBlocks = fillBlock - order.cosignerData.auctionTargetBlock;
break;
}
}

const settledAmounts = getSettledAmounts(
Expand Down
12 changes: 12 additions & 0 deletions lib/handlers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export const PRIORITY_ORDER_TARGET_BLOCK_BUFFER: Record<ChainId, number> = {
[ChainId.GÖRLI]: 3,
}

// Hybrid orders use target block to determine when the price curve starts
export const HYBRID_ORDER_TARGET_BLOCK_BUFFER: Record<ChainId, number> = {
[ChainId.MAINNET]: 3,
[ChainId.UNICHAIN]: 4,
[ChainId.BASE]: 3,
[ChainId.OPTIMISM]: 3,
[ChainId.ARBITRUM_ONE]: 3,
[ChainId.POLYGON]: 3,
[ChainId.SEPOLIA]: 3,
[ChainId.GÖRLI]: 3,
}
Comment thread
ericneil-sanc marked this conversation as resolved.

export const DUTCHV2_ORDER_LATENCY_THRESHOLD_SEC = 20;

export const UR_EXECUTE_SELECTOR = "24856bc3"
Expand Down
5 changes: 3 additions & 2 deletions lib/handlers/get-orders/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ContainerInjected, RequestInjected } from './injector'
import { GetDutchV2OrderResponse } from './schema/GetDutchV2OrderResponse'
import { GetOrdersResponse, GetOrdersResponseJoi } from './schema/GetOrdersResponse'
import { GetPriorityOrderResponse } from './schema/GetPriorityOrderResponse'
import { GetHybridOrderResponse } from './schema/GetHybridOrderResponse'
import { GetRelayOrderResponse, GetRelayOrdersResponseJoi } from './schema/GetRelayOrderResponse'
import { GetOrdersQueryParams, GetOrdersQueryParamsJoi, RawGetOrdersQueryParams } from './schema/index'
import { GetDutchV3OrderResponse } from './schema/GetDutchV3OrderResponse'
Expand All @@ -27,7 +28,7 @@ export class GetOrdersHandler extends APIGLambdaHandler<
void,
RawGetOrdersQueryParams,
GetOrdersResponse<
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | undefined
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | GetHybridOrderResponse | undefined
>
> {
constructor(
Expand All @@ -43,7 +44,7 @@ export class GetOrdersHandler extends APIGLambdaHandler<
): Promise<
| Response<
GetOrdersResponse<
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | undefined
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | GetHybridOrderResponse | undefined
>
>
| ErrorResponse
Expand Down
79 changes: 79 additions & 0 deletions lib/handlers/get-orders/schema/GetHybridOrderResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { OrderType } from '@uniswap/uniswapx-sdk'
import Joi from 'joi'
import FieldValidator from '../../../util/field-validator'
import { ORDER_STATUS } from '../../../entities'
import { Route } from '../../../repositories/quote-metadata-repository'
import { CommonOrderValidationFields } from './Common'

export type GetHybridOrderResponse = {
type: OrderType.Hybrid
orderStatus: ORDER_STATUS
signature: string
encodedOrder: string

orderHash: string
chainId: number
swapper: string
reactor: string

txHash: string | undefined
deadline: number
auctionStartBlock: number
baselinePriorityFeeWei: string
scalingFactor: string
input: {
token: string
maxAmount: string
}
outputs: {
token: string
minAmount: string
recipient: string
}[]
settledAmounts: {
tokenOut: string
amountOut: string
tokenIn: string
amountIn: string
}[] | undefined
priceCurve: string[]
cosigner: string
cosignerData: {
auctionTargetBlock: number
supplementalPriceCurve: string[]
}
cosignature: string
nonce: string
quoteId: string | undefined
requestId: string | undefined
createdAt: number | undefined
route: Route | undefined
}

export const HybridCosignerDataJoi = Joi.object({
auctionTargetBlock: Joi.number(),
supplementalPriceCurve: Joi.array().items(FieldValidator.isValidAmount()),
})

export const GetHybridOrderResponseEntryJoi = Joi.object({
...CommonOrderValidationFields,
type: Joi.string().valid(OrderType.Hybrid).required(),
input: Joi.object({
token: FieldValidator.isValidEthAddress().required(),
maxAmount: FieldValidator.isValidAmount().required(),
}),
outputs: Joi.array().items(
Joi.object({
token: FieldValidator.isValidEthAddress().required(),
minAmount: FieldValidator.isValidAmount().required(),
recipient: FieldValidator.isValidEthAddress().required(),
})
),
auctionStartBlock: Joi.number().min(0),
baselinePriorityFeeWei: FieldValidator.isValidAmount(),
scalingFactor: FieldValidator.isValidAmount(),
priceCurve: Joi.array().items(FieldValidator.isValidAmount()),
Comment thread
ericneil-sanc marked this conversation as resolved.
Outdated
cosigner: FieldValidator.isValidEthAddress(),
cosignerData: HybridCosignerDataJoi,
})

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum GetOrderTypeQueryParamEnum {
Relay = 'Relay',
Limit = 'Limit',
Priority = 'Priority',
Hybrid = 'Hybrid',

Dutch_V1_V2 = 'Dutch_V1_V2',
}
7 changes: 5 additions & 2 deletions lib/handlers/get-orders/schema/GetOrdersResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FieldValidator from '../../../util/field-validator'
import { DUTCH_LIMIT } from '../../../util/order'
import { GetDutchV2OrderResponse, GetDutchV2OrderResponseEntryJoi } from './GetDutchV2OrderResponse'
import { GetPriorityOrderResponse, GetPriorityOrderResponseEntryJoi } from './GetPriorityOrderResponse'
import { GetHybridOrderResponse, GetHybridOrderResponseEntryJoi } from './GetHybridOrderResponse'
import { GetRelayOrderResponse } from './GetRelayOrderResponse'
import { GetDutchV3OrderResponse, GetDutchV3OrderResponseEntryJoi } from './GetDutchV3OrderResponse'

Expand All @@ -15,6 +16,7 @@ export type GetOrdersResponse<
| GetDutchV2OrderResponse
| GetDutchV3OrderResponse
| GetPriorityOrderResponse
| GetHybridOrderResponse
| undefined
> = {
orders: T[]
Expand Down Expand Up @@ -52,7 +54,7 @@ export const OrderResponseEntryJoi = Joi.object({
orderHash: FieldValidator.isValidOrderHash(),
swapper: FieldValidator.isValidEthAddress(),
txHash: FieldValidator.isValidTxHash(),
type: Joi.string().valid(OrderType.Dutch, DUTCH_LIMIT, OrderType.Limit, OrderType.Priority, OrderType.Dutch_V3),
type: Joi.string().valid(OrderType.Dutch, DUTCH_LIMIT, OrderType.Limit, OrderType.Priority, OrderType.Dutch_V3, OrderType.Hybrid),
input: OrderInputJoi,
outputs: Joi.array().items(OrderOutputJoi),
settledAmounts: Joi.array().items(SettledAmount),
Expand All @@ -70,7 +72,8 @@ export const GetOrdersResponseJoi = Joi.object({
OrderResponseEntryJoi,
GetDutchV2OrderResponseEntryJoi,
GetDutchV3OrderResponseEntryJoi,
GetPriorityOrderResponseEntryJoi
GetPriorityOrderResponseEntryJoi,
GetHybridOrderResponseEntryJoi
)
),
cursor: FieldValidator.isValidCursor(),
Expand Down
28 changes: 27 additions & 1 deletion lib/handlers/post-order/PostOrderBodyParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UniswapXOrderParser,
CosignedV3DutchOrder,
CosignedV3DutchOrder as SDKV3DutchOrder,
CosignedHybridOrder as SDKHybridOrder,
} from '@uniswap/uniswapx-sdk'
import { UnexpectedOrderTypeError } from '../../errors/UnexpectedOrderTypeError'
import { DutchV1Order } from '../../models/DutchV1Order'
Expand All @@ -18,7 +19,8 @@ import { LimitOrder } from '../../models/LimitOrder'
import { Order } from '../../models/Order'
import { PriorityOrder } from '../../models/PriorityOrder'
import { RelayOrder } from '../../models/RelayOrder'
import { PostOrderRequestBody } from './schema'
import { HybridOrder } from '../../models/HybridOrder'
import { PostOrderRequestBody, HardQuote } from './schema'
import { DutchV3Order } from '../../models/DutchV3Order'
import { metrics } from '../../util/metrics'
import { Unit } from 'aws-embedded-metrics'
Expand Down Expand Up @@ -46,6 +48,8 @@ export class PostOrderBodyParser {
return this.tryParseRelayOrder(encodedOrder, signature, chainId)
case OrderType.Priority:
return this.tryParsePriorityOrder(encodedOrder, signature, chainId, body.quoteId, body.requestId)
case OrderType.Hybrid:
return this.tryParseHybridOrder(encodedOrder, signature, chainId, body.quoteId, body.requestId, body.hardQuote)
case undefined:
// If an OrderType is not explicitly set, it is the legacy format which is either a DutchOrderV1 or a LimitOrder.
// Try to parse both and see which hits.
Expand Down Expand Up @@ -171,6 +175,28 @@ export class PostOrderBodyParser {
}
}

private tryParseHybridOrder(
encodedOrder: string,
signature: string,
chainId: number,
quoteId?: string,
requestId?: string,
hardQuote?: HardQuote
): HybridOrder {
try {
const order = SDKHybridOrder.parse(encodedOrder, chainId)
return new HybridOrder(order, signature, chainId, undefined, undefined, quoteId, requestId, undefined, undefined, undefined, hardQuote)
} catch (err) {
this.logger.error('Unable to parse Hybrid order', {
err,
encodedOrder,
chainId,
signature,
})
throw err
}
}

private tryParseLimitOrder(encodedOrder: string, signature: string, chainId: number, quoteId?: string): LimitOrder {
try {
const order = this.tryParseDutchOrder(encodedOrder, signature, chainId, quoteId)
Expand Down
40 changes: 40 additions & 0 deletions lib/handlers/post-order/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const PostOrderRequestBodyJoi = Joi.object({
quoteId: FieldValidator.isValidQuoteId(),
requestId: FieldValidator.isValidQuoteId(),
orderType: FieldValidator.isValidOrderType(),
hardQuote: FieldValidator.isValidHardQuote(),
})

export const PostOrderResponseJoi = Joi.object({
Expand Down Expand Up @@ -79,6 +80,44 @@ export type RelayOrderPostRequestBody = {
signature: string
}

export type HardQuoteInput = {
token: string
amount: string
}

export type HardQuoteOutput = {
token: string
amount: string
recipient: string
}

// Matches V3HardQuote from GAPI (V2 and V3 quotes get converted to this)
export type HardQuote = {
quoteId: string
requestId: string
tokenInChainId: number
tokenOutChainId: number
tokenIn: string
input: HardQuoteInput
tokenOut: string
outputs: HardQuoteOutput[]
swapper: string
filler: string
orderHash: string
createdAt: number
createdAtMs: string
}
Comment thread
ericneil-sanc marked this conversation as resolved.

export type HybridOrderPostRequestBody = {
orderType: OrderType.Hybrid
chainId: number
encodedOrder: string
signature: string
quoteId?: string
requestId?: string
hardQuote?: HardQuote
}

export type PostOrderRequestBody =
| LegacyDutchOrderPostRequestBody
| DutchV1OrderPostRequestBody
Expand All @@ -87,6 +126,7 @@ export type PostOrderRequestBody =
| LimitOrderPostRequestBody
| RelayOrderPostRequestBody
| PriorityOrderPostRequestBody
| HybridOrderPostRequestBody

export type PostOrderResponse = {
hash: string
Expand Down
Loading
Loading