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

Handle Expired TWAP and not started TWAP #153

Merged
merged 14 commits into from
Aug 30, 2023
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cowprotocol/cow-sdk",
"version": "3.0.0-rc.3",
"version": "3.0.0-rc.5",
"license": "(MIT OR Apache-2.0)",
"files": [
"/dist"
Expand Down Expand Up @@ -37,6 +37,7 @@
"cross-fetch": "^3.1.5",
"exponential-backoff": "^3.1.1",
"graphql-request": "^4.3.0",
"graphql": "^16.3.0",
"limiter": "^2.1.0"
},
"peerDependencies": {
Expand All @@ -62,7 +63,6 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-unused-imports": "^3.0.0",
"ethers": "^5.7.2",
"graphql": "^16.3.0",
"jest": "^29.4.2",
"jest-fetch-mock": "^3.0.3",
"microbundle": "^0.15.1",
Expand Down
2 changes: 2 additions & 0 deletions src/composable/ConditionalOrder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ describe('ConditionalOrder', () => {
})

class TestConditionalOrder extends ConditionalOrder<string, string> {
isSingleOrder = true

constructor(address: string, salt?: string, data = '0x') {
super({
handler: address,
Expand Down
51 changes: 31 additions & 20 deletions src/composable/ConditionalOrder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, ethers, utils, providers } from 'ethers'
import { BigNumber, constants, ethers, utils } from 'ethers'
import { IConditionalOrder } from './generated/ComposableCoW'

import { decodeParams, encodeParams } from './utils'
Expand All @@ -7,11 +7,12 @@
ConditionalOrderParams,
ContextFactory,
IsValidResult,
OwnerContext,
PollParams,
PollResult,
PollResultCode,
PollResultErrors,
} from './types'
import { SupportedChainId } from '../common'
import { getComposableCow, getComposableCowInterface } from './contracts'

/**
Expand Down Expand Up @@ -69,6 +70,8 @@
this.hasOffChainInput = hasOffChainInput
}

abstract get isSingleOrder(): boolean

/**
* Get a descriptive name for the type of the conditional order (i.e twap, dca, etc).
*
Expand Down Expand Up @@ -231,8 +234,9 @@
* @throws If the conditional order is not tradeable.
* @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)
async poll(params: PollParams): Promise<PollResult> {
const { chainId, owner, provider } = params
const composableCow = getComposableCow(chainId, provider)

try {
const isValid = this.isValid()
Expand All @@ -245,17 +249,17 @@
}

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

// Check if the owner authorised the order
const isAuthorized = await this.isAuthorized(owner, chain, provider)
const isAuthorized = await this.isAuthorized(params)
if (!isAuthorized) {
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `NotAuthorised: Order ${this.id} is not authorised for ${owner} on chain ${chain}`,
reason: `NotAuthorised: Order ${this.id} is not authorised for ${owner} on chain ${chainId}`,
}
}

Expand Down Expand Up @@ -283,33 +287,40 @@
/**
* 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.
* @param params owner context, to be able to check if the order is authorized
* @returns true if the owner authorized the order, false otherwise.
*/
public isAuthorized(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<boolean> {
const composableCow = getComposableCow(chain, provider)
public isAuthorized(params: OwnerContext): Promise<boolean> {
const { chainId, owner, provider } = params
const composableCow = getComposableCow(chainId, provider)
return composableCow.callStatic.singleOrders(owner, this.id)
}

/**
* Checks the value in the cabinet for a given owner and chain
*
* @param params owner context, to be able to check the cabinet
*/
public cabinet(params: OwnerContext): Promise<string> {
const { chainId, owner, provider } = params

const slotId = this.isSingleOrder ? this.id : constants.HashZero

const composableCow = getComposableCow(chainId, provider)
return composableCow.callStatic.cabinet(owner, slotId)
}

/**
* 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.
* @param params The poll parameters
*
* @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>
protected abstract pollValidate(params: PollParams): Promise<PollResultErrors | undefined>

/**
* Convert the struct that the contract expect as an encoded `staticInput` into a friendly data object modeling the smart order.
Expand Down Expand Up @@ -345,7 +356,7 @@
s: string,
handler: string,
orderDataTypes: string[],
callback: (d: any, salt: string) => T

Check warning on line 359 in src/composable/ConditionalOrder.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
): T {
try {
// First, decode the `IConditionalOrder.Params` struct
Expand All @@ -359,7 +370,7 @@

// Create a new instance of the class
return callback(d, salt)
} catch (e: any) {

Check warning on line 373 in src/composable/ConditionalOrder.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
if (e.message === 'HandlerMismatch') {
throw e
} else {
Expand Down
72 changes: 62 additions & 10 deletions src/composable/orderTypes/Twap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, constants, providers } from 'ethers'
import { BigNumber, constants, utils } from 'ethers'

import { ConditionalOrder } from '../ConditionalOrder'
import {
Expand All @@ -7,10 +7,12 @@ import {
ContextFactory,
IsNotValid,
IsValid,
OwnerContext,
PollParams,
PollResultCode,
PollResultErrors,
} from '../types'
import { encodeParams, isValidAbi } from '../utils'
import { SupportedChainId } from '../../common'
import { encodeParams, formatEpoch, getBlockInfo, isValidAbi } from '../utils'

// The type of Conditional Order
const TWAP_ORDER_TYPE = 'twap'
Expand Down Expand Up @@ -161,6 +163,8 @@ const DEFAULT_DURATION_OF_PART: DurationOfPart = { durationType: DurationType.AU
* @author mfw78 <[email protected]>
*/
export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
isSingleOrder = true

/**
* @see {@link ConditionalOrder.constructor}
* @throws If the TWAP order is invalid.
Expand Down Expand Up @@ -263,6 +267,23 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
return error ? { isValid: false, reason: error } : { isValid: true }
}

private async startTimestamp(params: OwnerContext): Promise<number> {
const { startTime } = this.data

if (startTime?.startType === StartTimeValue.AT_EPOC) {
return startTime.epoch.toNumber()
}

const cabinet = await this.cabinet(params)
const cabinetEpoc = utils.defaultAbiCoder.decode(['uint256'], cabinet)[0]

if (cabinetEpoc === 0) {
throw new Error('Cabinet is not set. Required for TWAP orders that start at mining time.')
}

return parseInt(cabinet, 16)
}

/**
* Checks if the owner authorized the conditional order.
*
Expand All @@ -271,13 +292,44 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
* @param provider An RPC provider for the chain.
* @returns true if the owner authorized the order, false otherwise.
*/
protected async pollValidate(
_owner: string,
_chain: SupportedChainId,
_provider: providers.Provider
): Promise<PollResultErrors | undefined> {
// TODO: Do not check again expired order
// TODO: Calculate the next part start time, signal to not check again until then
protected async pollValidate(params: PollParams): Promise<PollResultErrors | undefined> {
const { blockInfo = await getBlockInfo(params.provider) } = params
const { blockTimestamp } = blockInfo
const { numberOfParts, timeBetweenParts } = this.data

const startTimestamp = await this.startTimestamp(params)

if (startTimestamp > blockTimestamp) {
// The start time hasn't started
return {
result: PollResultCode.TRY_AT_EPOCH,
epoch: startTimestamp,
reason: `TWAP hasn't started yet. Starts at ${startTimestamp} (${formatEpoch(startTimestamp)})`,
}
}

const expirationTimestamp = startTimestamp + numberOfParts.mul(timeBetweenParts).toNumber()
if (blockTimestamp >= expirationTimestamp) {
// The order has expired
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `TWAP has expired. Expired at ${expirationTimestamp} (${formatEpoch(expirationTimestamp)})`,
}
}

// TODO: Do not check between parts
// - 1. Check whats the order parameters for the current partNumber
// - 2. Derive discrete orderUid
// - 3. Verify if this is already created in the API
// - 4. If so, we know we should return
// return {
// result: PollResultCode.TRY_AT_EPOCH,
// epoch: nextPartStartTime,
// reason: `Current active TWAP part is already created. The next one doesn't start until ${nextPartStartTime} (${formatEpoch(nextPartStartTime)})`,
// }
// // Get current part number
// const partNumber = Math.floor(blockTimestamp - startTimestamp / timeBetweenParts.toNumber())

return undefined
}

Expand Down
26 changes: 25 additions & 1 deletion src/composable/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { SupportedChainId } from '../common'
import { GPv2Order } from './generated/ComposableCoW'
import { providers } from 'ethers'

export interface ConditionalOrderArguments<T> {
handler: string
Expand Down Expand Up @@ -79,6 +81,27 @@ export type ProofWithParams = {
params: ConditionalOrderParams
}

export type OwnerContext = {
owner: string
chainId: SupportedChainId
provider: providers.Provider
}

export type PollParams = OwnerContext & {
offchainInput: string
proof: string[]

/**
* If present, it can be used for custom conditional order validations. If not present, the orders will need to get the block info themselves
*/
blockInfo?: BlockInfo
}

export type BlockInfo = {
blockNumber: number
blockTimestamp: number
}

export type PollResult = PollResultSuccess | PollResultErrors

export type PollResultErrors =
Expand All @@ -93,7 +116,7 @@ export enum PollResultCode {
UNEXPECTED_ERROR = 'UNEXPECTED_ERROR',
TRY_NEXT_BLOCK = 'TRY_NEXT_BLOCK',
TRY_ON_BLOCK = 'TRY_ON_BLOCK',
TRY_AT_EPOCH = 'TRY_AT_DATE',
TRY_AT_EPOCH = 'TRY_AT_EPOCH',
DONT_TRY_AGAIN = 'DONT_TRY_AGAIN',
}
export interface PollResultSuccess {
Expand All @@ -105,6 +128,7 @@ export interface PollResultSuccess {
export interface PollResultUnexpectedError {
readonly result: PollResultCode.UNEXPECTED_ERROR
readonly error: unknown
reason?: string
}

export interface PollResultTryNextBlock {
Expand Down
15 changes: 14 additions & 1 deletion src/composable/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
SupportedChainId,
} from '../common'
import { ExtensibleFallbackHandler__factory } from './generated'
import { ConditionalOrderParams } from './types'
import { BlockInfo, ConditionalOrderParams } from './types'

// Define the ABI tuple for the ConditionalOrderParams struct
export const CONDITIONAL_ORDER_PARAMS_ABI = ['tuple(address handler, bytes32 salt, bytes staticInput)']
Expand Down Expand Up @@ -66,7 +66,7 @@
* @param values The values to validate.
* @returns {boolean} Whether the values are valid ABI for the given types.
*/
export function isValidAbi(types: readonly (string | utils.ParamType)[], values: any[]): boolean {

Check warning on line 69 in src/composable/utils.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
try {
utils.defaultAbiCoder.encode(types, values)
} catch (e) {
Expand All @@ -74,3 +74,16 @@
}
return true
}

export async function getBlockInfo(provider: providers.Provider): Promise<BlockInfo> {
const block = await provider.getBlock('latest')

return {
blockNumber: block.number,
blockTimestamp: block.timestamp,
}
}

export function formatEpoch(epoch: number): string {
return new Date(epoch * 1000).toISOString()
}
Loading