Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poll conditional orders + Allow custom logic to delay checks to a given date, block, or forever #149

Merged
merged 17 commits into from
Aug 24, 2023
22 changes: 17 additions & 5 deletions src/composable/ConditionalOrder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BigNumber } from 'ethers'
import { ConditionalOrder } from './ConditionalOrder'
import { IsValidResult, PollResultErrors } from './types'
import { Twap } from './types/Twap'
import { encodeParams } from './utils'

Expand Down Expand Up @@ -79,11 +79,11 @@ describe('ConditionalOrder', () => {
})

class TestConditionalOrder extends ConditionalOrder<string, string> {
constructor(address: string, salt?: string, staticInput = '0x') {
constructor(address: string, salt?: string, data = '0x') {
super({
handler: address,
salt,
staticInput,
data,
})
}

Expand All @@ -99,14 +99,26 @@ class TestConditionalOrder extends ConditionalOrder<string, string> {
return super.encodeStaticInputHelper(['uint256'], this.staticInput)
}

transformParamsToData(params: string): string {
transformStructToData(params: string): string {
return params
}

isValid(_o: unknown): boolean {
transformDataToStruct(params: string): string {
return params
}

protected pollValidate(): Promise<PollResultErrors | undefined> {
throw new Error('Method not implemented.')
}

isValid(): IsValidResult {
throw new Error('Method not implemented.')
}
serialize(): string {
return encodeParams(this.leaf)
}

toString(): string {
throw new Error('Method not implemented.')
}
}
164 changes: 145 additions & 19 deletions src/composable/ConditionalOrder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { BigNumber, ethers, utils } from 'ethers'
import { BigNumber, ethers, utils, providers } from 'ethers'
import { IConditionalOrder } from './generated/ComposableCoW'

import { ComposableCoW__factory } from './generated'
import { decodeParams, encodeParams } from './utils'
import { ConditionalOrderArguments, ConditionalOrderParams, ContextFactory } from './types'
import {
ConditionalOrderArguments,
ConditionalOrderParams,
ContextFactory,
IsValidResult,
PollResult,
PollResultCode,
PollResultErrors,
} from './types'
import { SupportedChainId } from 'src/common'
import { getComposableCow, getComposableCowInterface } from './contracts'

/**
* An abstract base class from which all conditional orders should inherit.
Expand All @@ -19,10 +28,11 @@ import { ConditionalOrderArguments, ConditionalOrderParams, ContextFactory } fro
* **NOTE**: Instances of conditional orders have an `id` property that is a `keccak256` hash of
* the serialized conditional order.
*/
export abstract class ConditionalOrder<Data, Params> {
export abstract class ConditionalOrder<D, S> {
public readonly handler: string
mfw78 marked this conversation as resolved.
Show resolved Hide resolved
public readonly salt: string
public readonly staticInput: Data
public readonly data: D
public readonly staticInput: S
public readonly hasOffChainInput: boolean

/**
Expand All @@ -33,13 +43,13 @@ export abstract class ConditionalOrder<Data, Params> {
* **NOTE**: The salt is optional and will be randomly generated if not provided.
* @param handler The address of the handler for the conditional order.
* @param salt A 32-byte string used to salt the conditional order.
* @param staticInput The static input for the conditional order.
* @param data The data of the order
* @param hasOffChainInput Whether the conditional order has off-chain input.
* @throws If the handler is not a valid ethereum address.
* @throws If the salt is not a valid 32-byte string.
*/
constructor(params: ConditionalOrderArguments<Params>) {
const { handler, salt = utils.keccak256(utils.randomBytes(32)), staticInput, hasOffChainInput = false } = params
constructor(params: ConditionalOrderArguments<D>) {
const { handler, salt = utils.keccak256(utils.randomBytes(32)), data, hasOffChainInput = false } = params
mfw78 marked this conversation as resolved.
Show resolved Hide resolved
// Verify input to the constructor
// 1. Verify that the handler is a valid ethereum address
if (!ethers.utils.isAddress(handler)) {
Expand All @@ -53,7 +63,9 @@ export abstract class ConditionalOrder<Data, Params> {

this.handler = handler
this.salt = salt
this.staticInput = this.transformParamsToData(staticInput)
this.data = data
this.staticInput = this.transformDataToStruct(data)

this.hasOffChainInput = hasOffChainInput
}

Expand All @@ -74,6 +86,15 @@ export abstract class ConditionalOrder<Data, Params> {
return undefined
}

assertIsValid(): void {
const isValidResult = this.isValid()
if (!isValidResult.isValid) {
throw new Error(`Invalid order: ${isValidResult.reason}`)
}
}

abstract isValid(): IsValidResult

/**
* Get the calldata for creating the conditional order.
*
Expand All @@ -84,8 +105,10 @@ export abstract class ConditionalOrder<Data, Params> {
* @returns The calldata for creating the conditional order.
*/
get createCalldata(): string {
this.assertIsValid()

const context = this.context
const composableCow = ComposableCoW__factory.createInterface()
const composableCow = getComposableCowInterface()
const paramsStruct: IConditionalOrder.ConditionalOrderParamsStruct = {
handler: this.handler,
salt: this.salt,
Expand Down Expand Up @@ -114,8 +137,9 @@ export abstract class ConditionalOrder<Data, Params> {
* @returns The calldata for removing the conditional order.
*/
get removeCalldata(): string {
const composableCow = ComposableCoW__factory.createInterface()
return composableCow.encodeFunctionData('remove', [this.id])
this.assertIsValid()

return getComposableCowInterface().encodeFunctionData('remove', [this.id])
}

/**
Expand Down Expand Up @@ -189,23 +213,125 @@ export abstract class ConditionalOrder<Data, Params> {
* A helper function for generically serializing a conditional order's static input.
*
* @param orderDataTypes ABI types for the order's data struct.
* @param staticInput The order's data struct.
* @param data The order's data struct.
* @returns An ABI-encoded representation of the order's data struct.
anxolin marked this conversation as resolved.
Show resolved Hide resolved
*/
protected encodeStaticInputHelper(orderDataTypes: string[], staticInput: Data): string {
protected encodeStaticInputHelper(orderDataTypes: string[], staticInput: S): string {
return utils.defaultAbiCoder.encode(orderDataTypes, [staticInput])
}

/**
* Apply any transformations to the parameters that are passed in to the constructor.
* Poll a conditional order to see if it is tradeable.
*
* @param owner The owner of the conditional order.
* @param p The proof and parameters.
* @param chain Which chain to use for the ComposableCoW contract.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proof isn't potentially passed in. Suggest that this be an optional parameter (array of strings).

* @param provider An RPC provider for the chain.
* @param offChainInputFn A function, if provided, that will return the off-chain input for the conditional order.
* @throws If the conditional order is not tradeable.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This param comment can be removed as the getter for offChainInput would be sufficient.

* @returns The tradeable `GPv2Order.Data` struct and the `signature` for the conditional order.
*/
async poll(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<PollResult> {
const composableCow = getComposableCow(chain, provider)

try {
const isValid = this.isValid()
// Do a validation first
if (!isValid.isValid) {
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `InvalidConditionalOrder. Reason: ${isValid.reason}`,
}
}

// Let the concrete Conditional Order decide about the poll result
const pollResult = await this.pollValidate(owner, chain, provider)
if (pollResult) {
return pollResult
}

// Check if the owner authorised the order
const isAuthorized = await this.isAuthorized(owner, chain, provider)
if (!isAuthorized) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, the proof parameter would be passed as an optional parameter.

return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `NotAuthorised: Order ${this.id} is not authorised for ${owner} on chain ${chain}`,
}
}

// Lastly, try to get the tradeable order and signature
const [order, signature] = await composableCow.getTradeableOrderWithSignature(
owner,
this.leaf,
this.offChainInput,
[]
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass the proof


return {
result: PollResultCode.SUCCESS,
order,
signature,
}
} catch (error) {
return {
result: PollResultCode.UNEXPECTED_ERROR,
error: error,
}
}
}

/**
* Checks if the owner authorized the conditional order.
*
* @param owner The owner of the conditional order.
* @param chain Which chain to use for the ComposableCoW contract.
* @param provider An RPC provider for the chain.
* @returns true if the owner authorized the order, false otherwise.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A parameter should be inserted in here to take into account proofs (and be optional).

*/
public isAuthorized(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<boolean> {
const composableCow = getComposableCow(chain, provider)
return composableCow.callStatic.singleOrders(owner, this.id)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To verify the proof, one would calculate the root from the proof given, then verify this against the root stored in composableCow for that owner.


/**
* Allow concrete conditional orders to perform additional validation for the poll method.
*
* This will allow the concrete orders to decide when an order shouldn't be polled again. For example, if the orders is expired.
* It also allows to signal when should the next check be done. For example, an order could signal that the validations will fail until a certain time or block.
*
* @param owner The owner of the conditional order.
* @param chain Which chain to use for the ComposableCoW contract.
* @param provider An RPC provider for the chain.
*
* @returns undefined if the concrete order can't make a decision. Otherwise, it returns a PollResultErrors object.
*/
protected abstract pollValidate(
owner: string,
chain: SupportedChainId,
provider: providers.Provider
): Promise<PollResultErrors | undefined>

/**
* Convert the struct that the contract expect as an encoded `staticInput` into a friendly data object modeling the smart order.
*
* **NOTE**: This should be overridden by any conditional order that requires transformations.
* This implementation is a no-op if you use the same type for both.
*
* @param params {S} Parameters that are passed in to the constructor.
* @returns {D} The static input for the conditional order.
*/
abstract transformStructToData(params: S): D

/**
* Converts a friendly data object modeling the smart order into the struct that the contract expect as an encoded `staticInput`.
*
* **NOTE**: This should be overridden by any conditional order that requires transformations.
* This implementation is a no-op.
* This implementation is a no-op if you use the same type for both.
*
* @param params {Params} Parameters that are passed in to the constructor.
* @returns {Data} The static input for the conditional order.
* @param params {S} Parameters that are passed in to the constructor.
* @returns {D} The static input for the conditional order.
*/
abstract transformParamsToData(params: Params): Data
abstract transformDataToStruct(params: D): S

/**
* A helper function for generically deserializing a conditional order.
Expand Down
Loading
Loading