Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,139 @@ LABS_COSIGNER=<valid evm address> # needed for certain unit tests

1. Repeat

## API Endpoints

### POST /order

Submit a signed UniswapX order. The endpoint URL format is:

```
POST https://<your-api-url>/order
```

#### Request Body

The request body should include the signed order:

```json
{
"encodedOrder": "0x...",
"signature": "0x...",
"chainId": 1,
"orderType": "Dutch_V2",
"quoteId": "optional-quote-id",
"requestId": "optional-request-id"
}
```

For hybrid orders, an optional `hardQuote` field can be included (see Hybrid Orders section below).

#### Response

On success (HTTP 201), the endpoint returns the order hash:

```json
{
"hash": "0x..."
}
```

#### Hybrid Orders

Hybrid orders currently mutually-exclusively support both Dutch auction (price curve) and priority order (basefee scaling) mechanisms.

##### Dutch-style Hybrid Orders
**Hybrid orders with a price curve (priceCurve.length > 0)**

These orders use Dutch auction mechanics. They can optionally include a `hardQuote` field to calculate the supplemental price curve:

```
POST https://<your-api-url>/order
Content-Type: application/json

{
"encodedOrder": "0x...",
"signature": "0x...",
"chainId": 1,
"orderType": "Hybrid",
"quoteId": "quote-id",
"requestId": "request-id",
"hardQuote": {
"quoteId": "quote-id",
"requestId": "request-id",
"tokenInChainId": 1,
"tokenOutChainId": 1,
"tokenIn": "0x...",
"tokenOut": "0x...",
"input": {
"token": "0x...",
"amount": "1000000"
},
"outputs": [{
"token": "0x...",
"amount": "2000000",
"recipient": "0x..."
}],
"swapper": "0x...",
"filler": "0x...",
"orderHash": "0x...",
"createdAt": 1234567890,
"createdAtMs": "1234567890000"
}
}
```

##### Priority-style Hybrid Orders
**Hybrid orders without a price curve (priceCurve.length == 0)**

These orders use priority fee scaling mechanics and do not require a `hardQuote`:

```
POST https://<your-api-url>/order
Content-Type: application/json

{
"encodedOrder": "0x...",
"signature": "0x...",
"chainId": 1,
"orderType": "Hybrid",
"quoteId": "optional-quote-id",
"requestId": "optional-request-id"
}
```

### GET /orders

Retrieve orders from the service (example):

```
GET https://<your-api-url>/orders?chainId=1&orderStatus=open&orderType=Hybrid
```

Query parameters:
- `chainId`: The chain ID to filter orders by
- `orderStatus`: Filter by order status (e.g., `open`, `filled`, `cancelled`)
- `orderHash`: Get a specific order by hash
- `orderHashes`: Comma-separated list of order hashes to retrieve
- `swapper`: Filter orders by swapper address
- `filler`: Filter orders by filler address
- `orderType`: Filter by order type. Valid values:
- `Dutch` - Dutch V1 orders
- `Dutch_V2` - Dutch V2 orders
- `Dutch_V3` - Dutch V3 orders
- `Dutch_V1_V2` - Both Dutch V1 and V2 orders
- `Priority` - Priority orders
- `Hybrid` - Hybrid orders
- `Limit` - Limit orders
- `Relay` - Relay orders
- `limit`: Maximum number of orders to return
- `cursor`: Pagination cursor for retrieving additional results
- `sortKey`: Field to sort by (requires `sort` parameter)
- `sort`: Sort order (e.g., `gt(0)` for ascending)
- `desc`: Boolean to sort in descending order
- `executeAddress`: Filter orders by execution address
- `pair`: Filter orders by token pair

## Order Notification Schema

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.
Expand Down
11 changes: 9 additions & 2 deletions lib/crons/gs-reaper/gs-reaper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ 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, CosignedHybridOrder, 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'
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
import { PermissionedTokenValidator } from '@uniswap/uniswapx-sdk'
import { Permit2Validator } from '../../util/Permit2Validator'

// Type for legacy orders that have input at the info level
type LegacyUniswapXOrder = DutchOrder | CosignedV2DutchOrder | CosignedV3DutchOrder | CosignedPriorityOrder

type OrderUpdate = {
status: ORDER_STATUS,
txHash?: string,
Expand Down Expand Up @@ -342,7 +345,11 @@ 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)
// For v4 orders like Hybrid, input is at a different level
const inputToken = order instanceof CosignedHybridOrder
? order.info.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
}
1 change: 1 addition & 0 deletions lib/errors/CosigningError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class CosigningError extends Error {}
8 changes: 6 additions & 2 deletions lib/handlers/OnChainValidatorMap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OrderValidator, RelayOrderValidator } from '@uniswap/uniswapx-sdk'
import { OrderValidator, RelayOrderValidator, V4OrderValidator } from '@uniswap/uniswapx-sdk'
import { ChainId } from '../util/chain'

export class OnChainValidatorMap<T extends OrderValidator | RelayOrderValidator> {
export class OnChainValidatorMap<T extends OrderValidator | RelayOrderValidator | V4OrderValidator> {
private chainIdToValidators: Map<ChainId, T> = new Map()

constructor(initial: Array<[ChainId, T]> = []) {
Expand All @@ -19,6 +19,10 @@ export class OnChainValidatorMap<T extends OrderValidator | RelayOrderValidator>
return validator
}

has(chainId: ChainId): boolean {
return this.chainIdToValidators.has(chainId)
}

set(chainId: ChainId, validator: T): void {
this.chainIdToValidators.set(chainId, validator)
}
Expand Down
14 changes: 13 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,
CosignedHybridOrder,
} from '@uniswap/uniswapx-sdk'
import { ethers } from 'ethers'
import { ORDER_STATUS, RelayOrderEntity, SettledAmount, UniswapXOrderEntity } from '../../entities'
Expand All @@ -28,6 +29,9 @@ import { Permit2Validator } from '../../util/Permit2Validator'

const FILL_CHECK_OVERLAP_BLOCK = 20

// Type for legacy orders that have input at the info level
type LegacyUniswapXOrder = DutchOrder | CosignedV2DutchOrder | CosignedV3DutchOrder | CosignedPriorityOrder

export type CheckOrderStatusRequest = {
chainId: number
orderHash: string
Expand Down Expand Up @@ -81,7 +85,11 @@ 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)
// For v4 orders like Hybrid, input is at a different level. Get input token safely.
const inputToken = parsedOrder instanceof CosignedHybridOrder
? parsedOrder.info.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 +178,10 @@ export class CheckOrderStatusService {
fillTimeBlocks = fillBlock - orderCreationBlock;
break;
}
case OrderType.Hybrid: { // Exact
fillTimeBlocks = fillBlock - order.cosignerData.auctionTargetBlock;
break;
}
}

const settledAmounts = getSettledAmounts(
Expand Down
44 changes: 30 additions & 14 deletions lib/handlers/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BigNumber } from 'ethers'
import { ChainId } from '../util/chain'

export const HIGH_MAX_OPEN_ORDERS_SWAPPERS: string[] = [
Expand All @@ -22,27 +23,42 @@ export const PRIORITY_ORDER_TARGET_BLOCK_BUFFER: Record<ChainId, number> = {
[ChainId.ARBITRUM_ONE]: 3,
[ChainId.POLYGON]: 3,
[ChainId.SEPOLIA]: 3,
[ChainId.GÖRLI]: 3,
[ChainId.UNICHAIN_SEPOLIA]: 4,
}

export const DUTCHV2_ORDER_LATENCY_THRESHOLD_SEC = 20;
// 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.UNICHAIN_SEPOLIA]: 4,
}
Comment thread
ericneil-sanc marked this conversation as resolved.

export const DUTCHV2_ORDER_LATENCY_THRESHOLD_SEC = 20

export const UR_EXECUTE_SELECTOR = "24856bc3"
export const UR_EXECUTE_WITH_DEADLINE_SELECTOR = "3593564c"
export const UR_EXECUTE_FUNCTION = "execute"
export const UR_EXECUTE_SELECTOR = '24856bc3'
export const UR_EXECUTE_WITH_DEADLINE_SELECTOR = '3593564c'
export const UR_EXECUTE_FUNCTION = 'execute'
export const UR_FUNCTION_SIGNATURES: Record<string, string> = {
[UR_EXECUTE_SELECTOR]: "function execute(bytes commands, bytes[] inputs)",
[UR_EXECUTE_WITH_DEADLINE_SELECTOR]: "function execute(bytes commands, bytes[] inputs, uint256 deadline)"
};
export const UR_EXECUTE_DEADLINE_BUFFER = 60; // Seconds to extend calldata deadline
[UR_EXECUTE_SELECTOR]: 'function execute(bytes commands, bytes[] inputs)',
[UR_EXECUTE_WITH_DEADLINE_SELECTOR]: 'function execute(bytes commands, bytes[] inputs, uint256 deadline)',
}
export const UR_EXECUTE_DEADLINE_BUFFER = 60 // Seconds to extend calldata deadline
export const UR_UNWRAP_WETH_PARAMETERS = ['address', 'uint256']
export const UR_SWEEP_PARAMETERS = ['address', 'address', 'uint256']
export const UR_ACTIONS_PARAMETERS = ['bytes', 'bytes[]']
export const UR_TAKE_PARAMETERS = ['address', 'address', 'uint256']

// Constants for hex string manipulation
export const HEX_PREFIX = "0x";
export const HEX_BASE = 16;
export const CHARS_PER_BYTE = 2;
export const UR_SELECTOR_BYTES = 4;
export const UR_BYTES_PER_ACTION = 2;
export const HEX_PREFIX = '0x'
export const HEX_BASE = 16
export const CHARS_PER_BYTE = 2
export const UR_SELECTOR_BYTES = 4
export const UR_BYTES_PER_ACTION = 2

export const BASE_SCALING_FACTOR = BigNumber.from(10).pow(18)
export const SCALING_FACTOR_MASK = BigNumber.from(1).shl(240).sub(1)
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 { 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 @@
void,
RawGetOrdersQueryParams,
GetOrdersResponse<
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | undefined
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | GetHybridOrderResponse | undefined
>
> {
constructor(
Expand All @@ -43,7 +44,7 @@
): Promise<
| Response<
GetOrdersResponse<
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | undefined
UniswapXOrderEntity | GetDutchV2OrderResponse | GetDutchV3OrderResponse | GetRelayOrderResponse | GetPriorityOrderResponse | GetHybridOrderResponse | undefined
>
>
| ErrorResponse
Expand Down Expand Up @@ -78,7 +79,7 @@
body: {
// w/o specifying orderType, the orderDispatcher uses the legacy get implementation
// and for priority orders, the returned object will contain offerer instead of swapper
orders: getOrdersResult.orders.map((order: any) => {

Check warning on line 82 in lib/handlers/get-orders/handler.ts

View workflow job for this annotation

GitHub Actions / lint-and-test

Unexpected any. Specify a different type
if (order.offerer) {
const { offerer, ...rest } = order
return {
Expand Down Expand Up @@ -124,7 +125,7 @@
// Try and extract the chain id from the raw json.
let chainId = '0'
try {
const rawBody = JSON.parse(event.body!)

Check warning on line 128 in lib/handlers/get-orders/handler.ts

View workflow job for this annotation

GitHub Actions / lint-and-test

Forbidden non-null assertion
chainId = rawBody.chainId ?? chainId
} catch (err) {
// no-op. If we can't get chainId still log the metric as chain 0
Expand Down
Loading
Loading