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
35 changes: 22 additions & 13 deletions src/composable/ConditionalOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ConditionalOrderParams,
ContextFactory,
IsValidResult,
PollParams,
PollResult,
PollResultCode,
PollResultErrors,
Expand Down Expand Up @@ -231,8 +232,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 +247,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(owner, chainId, provider)
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 @@ -293,23 +295,30 @@
return composableCow.callStatic.singleOrders(owner, this.id)
}

/**
* Checks the value in the cabinet for a given owner and chain
*
* @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.
*/
public cabinet(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<string> {
anxolin marked this conversation as resolved.
Show resolved Hide resolved
const composableCow = getComposableCow(chain, provider)
return composableCow.callStatic.cabinet(owner, this.id)
}

anxolin marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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 +354,7 @@
s: string,
handler: string,
orderDataTypes: string[],
callback: (d: any, salt: string) => T

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

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / deploy

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

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

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

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

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

View workflow job for this annotation

GitHub Actions / deploy

Unexpected any. Specify a different type
if (e.message === 'HandlerMismatch') {
throw e
} else {
Expand Down
60 changes: 52 additions & 8 deletions src/composable/orderTypes/Twap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
ContextFactory,
IsNotValid,
IsValid,
PollParams,
PollResultCode,
PollResultErrors,
} from '../types'
import { encodeParams, isValidAbi } from '../utils'
import { encodeParams, formatEpoc, getBlockInfo, isValidAbi } from '../utils'
import { SupportedChainId } from '../../common'

// The type of Conditional Order
Expand Down Expand Up @@ -263,6 +265,17 @@ export class Twap extends ConditionalOrder<TwapData, TwapStruct> {
return error ? { isValid: false, reason: error } : { isValid: true }
}

private async startTimestamp(owner: string, chain: SupportedChainId, provider: providers.Provider): Promise<number> {
anxolin marked this conversation as resolved.
Show resolved Hide resolved
const { startTime } = this.data

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

const cabinet = await this.cabinet(owner, chain, provider)
return parseInt(cabinet, 16)
anxolin marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Checks if the owner authorized the conditional order.
*
Expand All @@ -271,13 +284,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), owner, chainId, provider } = params
const { blockTimestamp } = blockInfo
const { numberOfParts, timeBetweenParts } = this.data

const startTimestamp = await this.startTimestamp(owner, chainId, provider)

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} (${formatEpoc(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} (${formatEpoc(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} (${formatEpoc(nextPartStartTime)})`,
// }
// // Get current part number
// const partNumber = Math.floor(blockTimestamp - startTimestamp / timeBetweenParts.toNumber())

return undefined
}

Expand Down
19 changes: 19 additions & 0 deletions 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,22 @@ export type ProofWithParams = {
params: ConditionalOrderParams
}

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

/**
* 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 @@ -105,6 +123,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

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

View workflow job for this annotation

GitHub Actions / deploy

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 formatEpoc(epoch: number): string {
anxolin marked this conversation as resolved.
Show resolved Hide resolved
return new Date(epoch * 1000).toISOString()
}
Loading