diff --git a/actions/checkForAndPlaceOrder.ts b/actions/checkForAndPlaceOrder.ts index 6da719d..62b39de 100644 --- a/actions/checkForAndPlaceOrder.ts +++ b/actions/checkForAndPlaceOrder.ts @@ -18,8 +18,15 @@ import { init, writeRegistry, } from "./utils"; -import { ChainContext, ConditionalOrder, OrderStatus } from "./model"; +import { + ChainContext, + ConditionalOrder, + OrderStatus, + SmartOrderValidationResult, + ValidationResult, +} from "./model"; import { GPv2Order } from "./types/ComposableCoW"; +import { validateOrder } from "./handlers"; const GPV2SETTLEMENT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"; @@ -148,6 +155,24 @@ async function _processConditionalOrder( } } + // Do custom Conditional Order checks + const [handler, salt, staticInput] = await (() => { + const { handler, salt, staticInput } = conditionalOrder.params; + return Promise.all([handler, salt, staticInput]); + })(); + const validateResult = await validateOrder({ + handler, + salt, + staticInput, + }); + if (validateResult && validateResult.result !== ValidationResult.Success) { + const { result, deleteConditionalOrder } = validateResult; + return { + error: result !== ValidationResult.FailedButIsExpected, // If we expected the call to fail, then we don't consider it an error + deleteConditionalOrder, + }; + } + // Get GPv2 Order const tradeableOrderResult = await _getTradeableOrderWithSignature( owner, @@ -157,10 +182,10 @@ async function _processConditionalOrder( ); // Return early if the simulation fails - if (tradeableOrderResult.result != CallResult.Success) { + if (tradeableOrderResult.result != ValidationResult.Success) { const { deleteConditionalOrder, result } = tradeableOrderResult; return { - error: result !== CallResult.FailedButIsExpected, // If we expected the call to fail, then we don't consider it an error + error: result !== ValidationResult.FailedButIsExpected, // If we expected the call to fail, then we don't consider it an error deleteConditionalOrder, }; } @@ -328,36 +353,17 @@ function _handleOrderBookError( return { shouldThrow: true }; } -type GetTradeableOrderWithSignatureResult = - | GetTradeableOrderWithSignatureSuccess - | GetTradeableOrderWithSignatureError; - -enum CallResult { - Success, - Failed, - FailedButIsExpected, -} - -type GetTradeableOrderWithSignatureSuccess = { - result: CallResult.Success; - deleteConditionalOrder: boolean; - data: { - order: GPv2Order.DataStructOutput; - signature: string; - }; -}; -type GetTradeableOrderWithSignatureError = { - result: CallResult.Failed | CallResult.FailedButIsExpected; - deleteConditionalOrder: boolean; - errorObj: any; -}; +export type TradableOrderWithSignatureResult = SmartOrderValidationResult<{ + order: GPv2Order.DataStructOutput; + signature: string; +}>; async function _getTradeableOrderWithSignature( owner: string, network: string, conditionalOrder: ConditionalOrder, contract: ComposableCoW -): Promise { +): Promise { const proof = conditionalOrder.proof ? conditionalOrder.proof.path : []; const offchainInput = "0x"; const { to, data } = @@ -380,7 +386,10 @@ async function _getTradeableOrderWithSignature( proof ); - return { result: CallResult.Success, deleteConditionalOrder: false, data }; + return { + result: ValidationResult.Success, + data, + }; } catch (error: any) { // Print and handle the error // We need to decide if the error is final or not (if a re-attempt might help). If it doesn't, we delete the order @@ -400,7 +409,7 @@ function _handleGetTradableOrderCall( error: any, owner: string ): { - result: CallResult.Failed | CallResult.FailedButIsExpected; + result: ValidationResult.Failed | ValidationResult.FailedButIsExpected; deleteConditionalOrder: boolean; } { if (error.code === Logger.errors.CALL_EXCEPTION) { @@ -411,7 +420,7 @@ function _handleGetTradableOrderCall( // The conditional order has not expired, or been cancelled, but the order is not valid // For example, with TWAPs, this may be after `span` seconds have passed in the epoch. return { - result: CallResult.FailedButIsExpected, + result: ValidationResult.FailedButIsExpected, deleteConditionalOrder: false, }; case "SingleOrderNotAuthed": @@ -423,7 +432,7 @@ function _handleGetTradableOrderCall( `${errorMessagePrefix}: Single order on safe ${owner} not authed. Deleting order...` ); return { - result: CallResult.FailedButIsExpected, + result: ValidationResult.FailedButIsExpected, deleteConditionalOrder: true, }; case "ProofNotAuthed": @@ -435,7 +444,7 @@ function _handleGetTradableOrderCall( `${errorMessagePrefix}: Proof on safe ${owner} not authed. Deleting order...` ); return { - result: CallResult.FailedButIsExpected, + result: ValidationResult.FailedButIsExpected, deleteConditionalOrder: true, }; default: @@ -448,13 +457,16 @@ function _handleGetTradableOrderCall( error ); // If we don't know the reason, is better to not delete the order - return { result: CallResult.Failed, deleteConditionalOrder: false }; + return { + result: ValidationResult.Failed, + deleteConditionalOrder: false, + }; } } console.error("[getTradeableOrderWithSignature] Unexpected error", error); // If we don't know the reason, is better to not delete the order - return { result: CallResult.Failed, deleteConditionalOrder: false }; + return { result: ValidationResult.Failed, deleteConditionalOrder: false }; } /** diff --git a/actions/handlers/index.ts b/actions/handlers/index.ts new file mode 100644 index 0000000..4ab0b54 --- /dev/null +++ b/actions/handlers/index.ts @@ -0,0 +1,27 @@ +import { + SmartOrderValidationResult, + SmartOrderValidator, + ValidateOrderParams, +} from "../model"; + +import { + handlerAddress as handlerTwap, + validateOrder as validateOrderTwap, +} from "./twap"; + +const validatorsForHandler: Record = { + [handlerTwap.toLowerCase()]: validateOrderTwap, +}; + +/** + * Validate a smart order using the custom handler logic (if known) + * + * @returns The result of the validation if the handler is known, undefined otherwise + */ +export async function validateOrder( + params: ValidateOrderParams +): Promise | undefined> { + const { handler } = params; + const validateFn = validatorsForHandler[handler.toLowerCase()]; + return validateFn ? validateFn(params) : undefined; +} diff --git a/actions/handlers/twap.ts b/actions/handlers/twap.ts new file mode 100644 index 0000000..58f3750 --- /dev/null +++ b/actions/handlers/twap.ts @@ -0,0 +1,14 @@ +import { TWAP_ADDRESS } from "@cowprotocol/cow-sdk"; + +import { SmartOrderValidator, ValidationResult } from "../model"; + +export const handlerAddress = TWAP_ADDRESS; + +export const validateOrder: SmartOrderValidator = async (_params) => { + // TODO: Implement validation logic, ideally delegate to the SDK + + return { + result: ValidationResult.Success, + data: undefined, + }; +}; diff --git a/actions/model.ts b/actions/model.ts index 94a4e8f..ee4b000 100644 --- a/actions/model.ts +++ b/actions/model.ts @@ -182,3 +182,32 @@ export function replacer(_key: any, value: any) { return value; } } + +export type SmartOrderValidationResult = + | SmartOrderValidationSuccess + | SmartOrderValidationError; + +export enum ValidationResult { + Success, + Failed, + FailedButIsExpected, +} + +export type SmartOrderValidationSuccess = { + result: ValidationResult.Success; + data: T; +}; +export type SmartOrderValidationError = { + result: ValidationResult.Failed | ValidationResult.FailedButIsExpected; + deleteConditionalOrder: boolean; + errorObj: any; +}; + +export type SmartOrderValidator = ( + params: ValidateOrderParams +) => Promise>; +export interface ValidateOrderParams { + handler: string; + salt: BytesLike; + staticInput: BytesLike; +} diff --git a/actions/package.json b/actions/package.json index 102c8eb..3ca2f40 100644 --- a/actions/package.json +++ b/actions/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@cowprotocol/contracts": "^1.4.0", + "@cowprotocol/cow-sdk": "^2.4.0-rc.0", "@sentry/integrations": "^7.64.0", "@sentry/node": "^7.64.0", "@tenderly/actions": "^0.0.13",