diff --git a/src/composable/ConditionalOrder.spec.ts b/src/composable/ConditionalOrder.spec.ts index 2b3829df..01560c65 100644 --- a/src/composable/ConditionalOrder.spec.ts +++ b/src/composable/ConditionalOrder.spec.ts @@ -1,3 +1,4 @@ +import { mockGetOrder } from '../order-book/__mock__/api' import { DEFAULT_ORDER_PARAMS, TestConditionalOrder, @@ -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 +const mockComputeOrderUid = computeOrderUid as jest.MockedFunction const TWAP_SERIALIZED = (salt?: string, handler?: string): string => { return ( @@ -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 () => { diff --git a/src/composable/ConditionalOrder.ts b/src/composable/ConditionalOrder.ts index 241bdf2d..87399faa 100644 --- a/src/composable/ConditionalOrder.ts +++ b/src/composable/ConditionalOrder.ts @@ -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 { @@ -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 = {} /** * An abstract base class from which all conditional orders should inherit. @@ -281,6 +286,33 @@ export abstract class ConditionalOrder { [] ) + 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, @@ -330,6 +362,20 @@ export abstract class ConditionalOrder { */ protected abstract pollValidate(params: PollParams): Promise + /** + * 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 + /** * Convert the struct that the contract expect as an encoded `staticInput` into a friendly data object modelling the smart order. * diff --git a/src/composable/Multiplexer.ts b/src/composable/Multiplexer.ts index 3674c53e..c6c48f4a 100644 --- a/src/composable/Multiplexer.ts +++ b/src/composable/Multiplexer.ts @@ -1,3 +1,4 @@ +import 'src/order-book/__mock__/api' import { StandardMerkleTree } from '@openzeppelin/merkle-tree' import { BigNumber, providers, utils } from 'ethers' @@ -434,7 +435,7 @@ export class Multiplexer { */ public static registerOrderType( orderType: string, - conditionalOrderClass: new (...args: unknown[]) => ConditionalOrder + conditionalOrderClass: new (...args: any[]) => ConditionalOrder ) { Multiplexer.orderTypeRegistry[orderType] = conditionalOrderClass } diff --git a/src/composable/orderTypes/Twap.spec.ts b/src/composable/orderTypes/Twap.spec.ts index c3469802..9e93603c 100644 --- a/src/composable/orderTypes/Twap.spec.ts +++ b/src/composable/orderTypes/Twap.spec.ts @@ -1,3 +1,4 @@ +import '../../order-book/__mock__/api' import { DurationType, StartTimeValue, Twap, TWAP_ADDRESS, TwapData } from './Twap' import { BigNumber, utils, constants } from 'ethers' diff --git a/src/composable/orderTypes/Twap.ts b/src/composable/orderTypes/Twap.ts index f0712d44..6ab04f5c 100644 --- a/src/composable/orderTypes/Twap.ts +++ b/src/composable/orderTypes/Twap.ts @@ -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' @@ -350,6 +351,14 @@ export class Twap extends ConditionalOrder { return undefined } + protected async handlePollFailedAlreadyPresent( + _orderUid: string, + _order: GPv2Order.DataStructOutput, + _params: PollParams + ): Promise { + return undefined + } + /** * Serialize the TWAP order into it's ABI-encoded form. * @returns {string} The ABI-encoded TWAP order. diff --git a/src/composable/orderTypes/test/TestConditionalOrder.ts b/src/composable/orderTypes/test/TestConditionalOrder.ts index bb621618..22cd6e48 100644 --- a/src/composable/orderTypes/test/TestConditionalOrder.ts +++ b/src/composable/orderTypes/test/TestConditionalOrder.ts @@ -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 = { @@ -48,7 +49,14 @@ export class TestConditionalOrder extends ConditionalOrder { return params } - protected async pollValidate(): Promise { + protected async pollValidate(_params: PollParams): Promise { + return undefined + } + protected async handlePollFailedAlreadyPresent( + _orderUid: string, + _order: GPv2Order.DataStructOutput, + _params: PollParams + ): Promise { return undefined } diff --git a/src/composable/utils.ts b/src/composable/utils.ts index d21ff4fc..43e43d86 100644 --- a/src/composable/utils.ts +++ b/src/composable/utils.ts @@ -1,3 +1,4 @@ +import 'src/order-book/__mock__/api' import { utils, providers } from 'ethers' import { COMPOSABLE_COW_CONTRACT_ADDRESS, diff --git a/src/order-book/__mock__/api.ts b/src/order-book/__mock__/api.ts new file mode 100644 index 00000000..23c4f5ba --- /dev/null +++ b/src/order-book/__mock__/api.ts @@ -0,0 +1,9 @@ +jest.mock('../api', () => { + return { + OrderBookApi: class MockedOrderBookApi { + getOrder = mockGetOrder + }, + } +}) + +export const mockGetOrder = jest.fn() diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..ac4938dd --- /dev/null +++ b/src/utils.ts @@ -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 { + const { computeOrderUid: _computeOrderUid } = await import('@cowprotocol/contracts') + const domain = await OrderSigningUtils.getDomain(chainId) + + return _computeOrderUid(domain, order, owner) +}