From 28424ffdb9271b3d9f72a8945e52f9473bd4d065 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 12 Sep 2023 10:24:17 +0200 Subject: [PATCH 1/4] Check existence of the order in the orderbook, and add polledOrderInOrderbook --- src/composable/ConditionalOrder.ts | 58 ++++++++++++++++++- src/composable/orderTypes/Twap.ts | 9 +++ .../orderTypes/test/TestConditionalOrder.ts | 10 +++- src/utils.ts | 10 ++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/utils.ts diff --git a/src/composable/ConditionalOrder.ts b/src/composable/ConditionalOrder.ts index 241bdf2d..95f51243 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,43 @@ export abstract class ConditionalOrder { [] ) + let orderBookApi = orderBookCache[chainId] + if (!orderBookApi) { + orderBookApi = new OrderBookApi({ chainId }) + orderBookCache[chainId] = orderBookApi + } + + // TODO: Derive orderId from the order data + /* + { + name: "Gnosis Protocol", + version: "v2", + chainId: chainId, + verifyingContract: GPV2SETTLEMENT, + } + */ + + 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.polledOrderInOrderbook(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 +372,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 has been already created. + * + * 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 time of the next part. + * + * @param params + */ + protected abstract polledOrderInOrderbook( + 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/orderTypes/Twap.ts b/src/composable/orderTypes/Twap.ts index f0712d44..5f828a76 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 polledOrderInOrderbook( + _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..d7930deb 100644 --- a/src/composable/orderTypes/test/TestConditionalOrder.ts +++ b/src/composable/orderTypes/test/TestConditionalOrder.ts @@ -1,5 +1,6 @@ +import { GPv2Order } from 'src/composable/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 = { @@ -51,6 +52,13 @@ export class TestConditionalOrder extends ConditionalOrder { protected async pollValidate(): Promise { return undefined } + protected async polledOrderInOrderbook( + _orderUid: string, + _order: GPv2Order.DataStructOutput, + _params: PollParams + ): Promise { + return undefined + } isValid(): IsValidResult { return { isValid: true } 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) +} From 3036a2958c7efff6d9d3c19238cac2e11a7858f2 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 12 Sep 2023 16:56:25 +0200 Subject: [PATCH 2/4] Fix unit tests by mocking --- src/composable/ConditionalOrder.spec.ts | 15 +++++++++++++++ src/composable/ConditionalOrder.ts | 10 ---------- .../orderTypes/test/TestConditionalOrder.ts | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/composable/ConditionalOrder.spec.ts b/src/composable/ConditionalOrder.spec.ts index 2b3829df..92b89508 100644 --- a/src/composable/ConditionalOrder.spec.ts +++ b/src/composable/ConditionalOrder.spec.ts @@ -10,9 +10,21 @@ 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('../order-book/api', () => { + return { + OrderBookApi: class MockedOrderBookApi { + getOrder = mockGetOrder //jest.fn(), + }, + } +}) +jest.mock('../utils') + const mockGetComposableCow = getComposableCow as jest.MockedFunction +const mockComputeOrderUid = computeOrderUid as jest.MockedFunction +const mockGetOrder = jest.fn() const TWAP_SERIALIZED = (salt?: string, handler?: string): string => { return ( @@ -187,6 +199,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 95f51243..6eea97b2 100644 --- a/src/composable/ConditionalOrder.ts +++ b/src/composable/ConditionalOrder.ts @@ -292,16 +292,6 @@ export abstract class ConditionalOrder { orderBookCache[chainId] = orderBookApi } - // TODO: Derive orderId from the order data - /* - { - name: "Gnosis Protocol", - version: "v2", - chainId: chainId, - verifyingContract: GPV2SETTLEMENT, - } - */ - const orderUid = await computeOrderUid(chainId, owner, order as Order) // Check if the order is already in the order book diff --git a/src/composable/orderTypes/test/TestConditionalOrder.ts b/src/composable/orderTypes/test/TestConditionalOrder.ts index d7930deb..a84ba866 100644 --- a/src/composable/orderTypes/test/TestConditionalOrder.ts +++ b/src/composable/orderTypes/test/TestConditionalOrder.ts @@ -1,4 +1,4 @@ -import { GPv2Order } from 'src/composable/generated/ComposableCoW' +import { GPv2Order } from '../../generated/ComposableCoW' import { ConditionalOrder } from '../../ConditionalOrder' import { IsValidResult, PollParams, PollResultErrors } from '../../types' import { encodeParams } from '../../utils' @@ -49,7 +49,7 @@ export class TestConditionalOrder extends ConditionalOrder { return params } - protected async pollValidate(): Promise { + protected async pollValidate(_params: PollParams): Promise { return undefined } protected async polledOrderInOrderbook( From 1d772526fb7ad1434c717a4f8fb854066696e5f2 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 12 Sep 2023 17:15:05 +0200 Subject: [PATCH 3/4] Rename function to handle order in API --- src/composable/ConditionalOrder.spec.ts | 2 +- src/composable/ConditionalOrder.ts | 8 ++++---- src/composable/orderTypes/Twap.ts | 2 +- src/composable/orderTypes/test/TestConditionalOrder.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/composable/ConditionalOrder.spec.ts b/src/composable/ConditionalOrder.spec.ts index 92b89508..2b8499fd 100644 --- a/src/composable/ConditionalOrder.spec.ts +++ b/src/composable/ConditionalOrder.spec.ts @@ -16,7 +16,7 @@ jest.mock('./contracts') jest.mock('../order-book/api', () => { return { OrderBookApi: class MockedOrderBookApi { - getOrder = mockGetOrder //jest.fn(), + getOrder = mockGetOrder }, } }) diff --git a/src/composable/ConditionalOrder.ts b/src/composable/ConditionalOrder.ts index 6eea97b2..87399faa 100644 --- a/src/composable/ConditionalOrder.ts +++ b/src/composable/ConditionalOrder.ts @@ -302,7 +302,7 @@ export abstract class ConditionalOrder { // 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.polledOrderInOrderbook(orderUid, order, params) + const pollResult = await this.handlePollFailedAlreadyPresent(orderUid, order, params) if (pollResult) { return pollResult } @@ -363,14 +363,14 @@ 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 has been already created. + * 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 time of the next part. + * 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 polledOrderInOrderbook( + protected abstract handlePollFailedAlreadyPresent( orderUid: UID, order: GPv2Order.DataStructOutput, params: PollParams diff --git a/src/composable/orderTypes/Twap.ts b/src/composable/orderTypes/Twap.ts index 5f828a76..6ab04f5c 100644 --- a/src/composable/orderTypes/Twap.ts +++ b/src/composable/orderTypes/Twap.ts @@ -351,7 +351,7 @@ export class Twap extends ConditionalOrder { return undefined } - protected async polledOrderInOrderbook( + protected async handlePollFailedAlreadyPresent( _orderUid: string, _order: GPv2Order.DataStructOutput, _params: PollParams diff --git a/src/composable/orderTypes/test/TestConditionalOrder.ts b/src/composable/orderTypes/test/TestConditionalOrder.ts index a84ba866..22cd6e48 100644 --- a/src/composable/orderTypes/test/TestConditionalOrder.ts +++ b/src/composable/orderTypes/test/TestConditionalOrder.ts @@ -52,7 +52,7 @@ export class TestConditionalOrder extends ConditionalOrder { protected async pollValidate(_params: PollParams): Promise { return undefined } - protected async polledOrderInOrderbook( + protected async handlePollFailedAlreadyPresent( _orderUid: string, _order: GPv2Order.DataStructOutput, _params: PollParams From dcfd3acab019e35a441ca1ce7a4c1fd116708ae8 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 14 Sep 2023 16:47:44 +0200 Subject: [PATCH 4/4] Fix tests --- src/composable/ConditionalOrder.spec.ts | 10 ++-------- src/composable/Multiplexer.ts | 3 ++- src/composable/orderTypes/Twap.spec.ts | 1 + src/composable/utils.ts | 1 + src/order-book/__mock__/api.ts | 9 +++++++++ 5 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 src/order-book/__mock__/api.ts diff --git a/src/composable/ConditionalOrder.spec.ts b/src/composable/ConditionalOrder.spec.ts index 2b8499fd..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, @@ -13,18 +14,11 @@ import { BuyTokenDestination, OrderKind, SellTokenSource } from '../order-book/g import { computeOrderUid } from '../utils' jest.mock('./contracts') -jest.mock('../order-book/api', () => { - return { - OrderBookApi: class MockedOrderBookApi { - getOrder = mockGetOrder - }, - } -}) + jest.mock('../utils') const mockGetComposableCow = getComposableCow as jest.MockedFunction const mockComputeOrderUid = computeOrderUid as jest.MockedFunction -const mockGetOrder = jest.fn() const TWAP_SERIALIZED = (salt?: string, handler?: string): string => { return ( 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/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()