Skip to content

Commit

Permalink
Check existence of the order in the orderbook, and add polledOrderInO…
Browse files Browse the repository at this point in the history
…rderbook (#166)

* Check existence of the order in the orderbook, and add polledOrderInOrderbook

* Fix unit tests by mocking

* Rename function to handle order in API

* Fix tests
  • Loading branch information
anxolin authored Sep 14, 2023
1 parent c0cb55f commit 2ca4e6b
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/composable/ConditionalOrder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mockGetOrder } from '../order-book/__mock__/api'
import {
DEFAULT_ORDER_PARAMS,
TestConditionalOrder,
Expand All @@ -10,9 +11,14 @@ import { getComposableCow } from './contracts'
import { constants } from 'ethers'
import { OwnerContext, PollParams, PollResultCode, PollResultErrors } from './types'
import { BuyTokenDestination, OrderKind, SellTokenSource } from '../order-book/generated'
import { computeOrderUid } from '../utils'

jest.mock('./contracts')

jest.mock('../utils')

const mockGetComposableCow = getComposableCow as jest.MockedFunction<typeof getComposableCow>
const mockComputeOrderUid = computeOrderUid as jest.MockedFunction<typeof computeOrderUid>

const TWAP_SERIALIZED = (salt?: string, handler?: string): string => {
return (
Expand Down Expand Up @@ -187,6 +193,9 @@ describe('Poll Single Orders', () => {
getTradeableOrderWithSignature: mockGetTradeableOrderWithSignature,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)

mockComputeOrderUid.mockReturnValue(Promise.resolve(SINGLE_ORDER.id))
mockGetOrder.mockImplementation(() => Promise.reject('Pretend the order does not exist'))
})

test('[SUCCESS] Happy path', async () => {
Expand Down
48 changes: 47 additions & 1 deletion src/composable/ConditionalOrder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BigNumber, constants, ethers, utils } from 'ethers'
import { IConditionalOrder } from './generated/ComposableCoW'
import { GPv2Order, IConditionalOrder } from './generated/ComposableCoW'

import { decodeParams, encodeParams } from './utils'
import {
Expand All @@ -14,6 +14,11 @@ import {
PollResultErrors,
} from './types'
import { getComposableCow, getComposableCowInterface } from './contracts'
import { OrderBookApi, UID } from 'src/order-book'
import { computeOrderUid } from 'src/utils'
import { Order } from '@cowprotocol/contracts'

const orderBookCache: Record<string, OrderBookApi> = {}

/**
* An abstract base class from which all conditional orders should inherit.
Expand Down Expand Up @@ -281,6 +286,33 @@ export abstract class ConditionalOrder<D, S> {
[]
)

let orderBookApi = orderBookCache[chainId]
if (!orderBookApi) {
orderBookApi = new OrderBookApi({ chainId })
orderBookCache[chainId] = orderBookApi
}

const orderUid = await computeOrderUid(chainId, owner, order as Order)

// Check if the order is already in the order book
const isOrderInOrderbook = await orderBookApi
.getOrder(orderUid)
.then(() => true)
.catch(() => false)

// Let the concrete Conditional Order decide about the poll result (in the case the order is already in the orderbook)
if (isOrderInOrderbook) {
const pollResult = await this.handlePollFailedAlreadyPresent(orderUid, order, params)
if (pollResult) {
return pollResult
}

return {
result: PollResultCode.TRY_NEXT_BLOCK,
reason: 'Order already in orderbook',
}
}

return {
result: PollResultCode.SUCCESS,
order,
Expand Down Expand Up @@ -330,6 +362,20 @@ export abstract class ConditionalOrder<D, S> {
*/
protected abstract pollValidate(params: PollParams): Promise<PollResultErrors | undefined>

/**
* This method lets the concrete conditional order decide what to do if the order yielded in the polling is already present in the Orderbook API.
*
* The concrete conditional order will have a chance to schedule the next poll.
* For example, a TWAP order that has the current part already in the orderbook, can signal that the next poll should be done at the start time of the next part.
*
* @param params
*/
protected abstract handlePollFailedAlreadyPresent(
orderUid: UID,
order: GPv2Order.DataStructOutput,
params: PollParams
): Promise<PollResultErrors | undefined>

/**
* Convert the struct that the contract expect as an encoded `staticInput` into a friendly data object modelling the smart order.
*
Expand Down
3 changes: 2 additions & 1 deletion src/composable/Multiplexer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'src/order-book/__mock__/api'
import { StandardMerkleTree } from '@openzeppelin/merkle-tree'
import { BigNumber, providers, utils } from 'ethers'

Expand Down Expand Up @@ -434,7 +435,7 @@ export class Multiplexer {
*/
public static registerOrderType(
orderType: string,
conditionalOrderClass: new (...args: unknown[]) => ConditionalOrder<unknown, unknown>
conditionalOrderClass: new (...args: any[]) => ConditionalOrder<unknown, unknown>
) {
Multiplexer.orderTypeRegistry[orderType] = conditionalOrderClass
}
Expand Down
1 change: 1 addition & 0 deletions src/composable/orderTypes/Twap.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '../../order-book/__mock__/api'
import { DurationType, StartTimeValue, Twap, TWAP_ADDRESS, TwapData } from './Twap'
import { BigNumber, utils, constants } from 'ethers'

Expand Down
9 changes: 9 additions & 0 deletions src/composable/orderTypes/Twap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
PollResultErrors,
} from '../types'
import { encodeParams, formatEpoch, getBlockInfo, isValidAbi } from '../utils'
import { GPv2Order } from '../generated/ComposableCoW'

// The type of Conditional Order
const TWAP_ORDER_TYPE = 'twap'
Expand Down Expand Up @@ -350,6 +351,14 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
return undefined
}

protected async handlePollFailedAlreadyPresent(
_orderUid: string,
_order: GPv2Order.DataStructOutput,
_params: PollParams
): Promise<PollResultErrors | undefined> {
return undefined
}

/**
* Serialize the TWAP order into it's ABI-encoded form.
* @returns {string} The ABI-encoded TWAP order.
Expand Down
12 changes: 10 additions & 2 deletions src/composable/orderTypes/test/TestConditionalOrder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GPv2Order } from '../../generated/ComposableCoW'
import { ConditionalOrder } from '../../ConditionalOrder'
import { IsValidResult, PollResultErrors } from '../../types'
import { IsValidResult, PollParams, PollResultErrors } from '../../types'
import { encodeParams } from '../../utils'

export const DEFAULT_ORDER_PARAMS: TestConditionalOrderParams = {
Expand Down Expand Up @@ -48,7 +49,14 @@ export class TestConditionalOrder extends ConditionalOrder<string, string> {
return params
}

protected async pollValidate(): Promise<PollResultErrors | undefined> {
protected async pollValidate(_params: PollParams): Promise<PollResultErrors | undefined> {
return undefined
}
protected async handlePollFailedAlreadyPresent(
_orderUid: string,
_order: GPv2Order.DataStructOutput,
_params: PollParams
): Promise<PollResultErrors | undefined> {
return undefined
}

Expand Down
1 change: 1 addition & 0 deletions src/composable/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'src/order-book/__mock__/api'
import { utils, providers } from 'ethers'
import {
COMPOSABLE_COW_CONTRACT_ADDRESS,
Expand Down
9 changes: 9 additions & 0 deletions src/order-book/__mock__/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
jest.mock('../api', () => {
return {
OrderBookApi: class MockedOrderBookApi {
getOrder = mockGetOrder
},
}
})

export const mockGetOrder = jest.fn()
10 changes: 10 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Order } from '@cowprotocol/contracts'
import type { SupportedChainId } from './common'
import { OrderSigningUtils } from './order-signing'

export async function computeOrderUid(chainId: SupportedChainId, owner: string, order: Order): Promise<string> {
const { computeOrderUid: _computeOrderUid } = await import('@cowprotocol/contracts')
const domain = await OrderSigningUtils.getDomain(chainId)

return _computeOrderUid(domain, order, owner)
}

0 comments on commit 2ca4e6b

Please sign in to comment.