• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cowprotocol / cow-sdk / 6027054998

30 Aug 2023 03:39PM UTC coverage: 72.617% (-3.8%) from 76.379%
6027054998

Pull #153

github

anxolin
Decode cabinet and verify its value
Pull Request #153: Handle Expired TWAP and not started TWAP

185 of 273 branches covered (0.0%)

Branch coverage included in aggregate %.

31 of 31 new or added lines in 3 files covered. (100.0%)

356 of 472 relevant lines covered (75.42%)

19.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.24
/src/composable/ConditionalOrder.ts
1
import { BigNumber, constants, ethers, utils } from 'ethers'
2
import { IConditionalOrder } from './generated/ComposableCoW'
3

4
import { decodeParams, encodeParams } from './utils'
5
import {
6
  ConditionalOrderArguments,
7
  ConditionalOrderParams,
8
  ContextFactory,
9
  IsValidResult,
10
  OwnerContext,
11
  PollParams,
12
  PollResult,
13
  PollResultCode,
14
  PollResultErrors,
15
} from './types'
16
import { getComposableCow, getComposableCowInterface } from './contracts'
17

18
/**
19
 * An abstract base class from which all conditional orders should inherit.
20
 *
21
 * This class provides some basic functionality to help with handling conditional orders,
22
 * such as:
23
 * - Validating the conditional order
24
 * - Creating a human-readable string representation of the conditional order
25
 * - Serializing the conditional order for use with the `IConditionalOrder` struct
26
 * - Getting any dependencies for the conditional order
27
 * - Getting the off-chain input for the conditional order
28
 *
29
 * **NOTE**: Instances of conditional orders have an `id` property that is a `keccak256` hash of
30
 *           the serialized conditional order.
31
 */
32
export abstract class ConditionalOrder<D, S> {
33
  public readonly handler: string
34
  public readonly salt: string
35
  public readonly data: D
36
  public readonly staticInput: S
37
  public readonly hasOffChainInput: boolean
38

39
  /**
40
   * A constructor that provides some basic validation for the conditional order.
41
   *
42
   * This constructor **MUST** be called by any class that inherits from `ConditionalOrder`.
43
   *
44
   * **NOTE**: The salt is optional and will be randomly generated if not provided.
45
   * @param handler The address of the handler for the conditional order.
46
   * @param salt A 32-byte string used to salt the conditional order.
47
   * @param data The data of the order
48
   * @param hasOffChainInput Whether the conditional order has off-chain input.
49
   * @throws If the handler is not a valid ethereum address.
50
   * @throws If the salt is not a valid 32-byte string.
51
   */
52
  constructor(params: ConditionalOrderArguments<D>) {
53
    const { handler, salt = utils.keccak256(utils.randomBytes(32)), data, hasOffChainInput = false } = params
96✔
54
    // Verify input to the constructor
55
    // 1. Verify that the handler is a valid ethereum address
56
    if (!ethers.utils.isAddress(handler)) {
96✔
57
      throw new Error(`Invalid handler: ${handler}`)
1✔
58
    }
59

60
    // 2. Verify that the salt is a valid 32-byte string usable with ethers
61
    if (!ethers.utils.isHexString(salt) || ethers.utils.hexDataLength(salt) !== 32) {
95✔
62
      throw new Error(`Invalid salt: ${salt}`)
3✔
63
    }
64

65
    this.handler = handler
92✔
66
    this.salt = salt
92✔
67
    this.data = data
92✔
68
    this.staticInput = this.transformDataToStruct(data)
92✔
69

70
    this.hasOffChainInput = hasOffChainInput
92✔
71
  }
72

73
  abstract get isSingleOrder(): boolean
74

75
  /**
76
   * Get a descriptive name for the type of the conditional order (i.e twap, dca, etc).
77
   *
78
   * @returns {string} The concrete type of the conditional order.
79
   */
80
  abstract get orderType(): string
81

82
  /**
83
   * Get the context dependency for the conditional order.
84
   *
85
   * This is used when calling `createWithContext` or `setRootWithContext` on a ComposableCoW-enabled Safe.
86
   * @returns The context dependency.
87
   */
88
  get context(): ContextFactory | undefined {
89
    return undefined
2✔
90
  }
91

92
  assertIsValid(): void {
93
    const isValidResult = this.isValid()
38✔
94
    if (!isValidResult.isValid) {
38✔
95
      throw new Error(`Invalid order: ${isValidResult.reason}`)
1✔
96
    }
97
  }
98

99
  abstract isValid(): IsValidResult
100

101
  /**
102
   * Get the calldata for creating the conditional order.
103
   *
104
   * This will automatically determine whether or not to use `create` or `createWithContext` based on the
105
   * order type's context dependency.
106
   *
107
   * **NOTE**: By default, this will cause the create to emit the `ConditionalOrderCreated` event.
108
   * @returns The calldata for creating the conditional order.
109
   */
110
  get createCalldata(): string {
111
    this.assertIsValid()
×
112

113
    const context = this.context
×
114
    const composableCow = getComposableCowInterface()
×
115
    const paramsStruct: IConditionalOrder.ConditionalOrderParamsStruct = {
×
116
      handler: this.handler,
117
      salt: this.salt,
118
      staticInput: this.encodeStaticInput(),
119
    }
120

121
    if (context) {
×
122
      // Create (with context)
123
      const contextArgsAbi = context.factoryArgs
×
124
        ? utils.defaultAbiCoder.encode(context.factoryArgs.argsType, context.factoryArgs.args)
125
        : '0x'
126
      return composableCow.encodeFunctionData('createWithContext', [
×
127
        paramsStruct,
128
        context.address,
129
        contextArgsAbi,
130
        true,
131
      ])
132
    } else {
133
      // Create
134
      return composableCow.encodeFunctionData('create', [paramsStruct, true])
×
135
    }
136
  }
137

138
  /**
139
   * Get the calldata for removing a conditional order that was created as a single order.
140
   * @returns The calldata for removing the conditional order.
141
   */
142
  get removeCalldata(): string {
143
    this.assertIsValid()
×
144

145
    return getComposableCowInterface().encodeFunctionData('remove', [this.id])
×
146
  }
147

148
  /**
149
   * Calculate the id of the conditional order (which also happens to be the key used for `ctx` in the ComposableCoW contract).
150
   *
151
   * This is a `keccak256` hash of the serialized conditional order.
152
   * @returns The id of the conditional order.
153
   */
154
  get id(): string {
155
    return utils.keccak256(this.serialize())
47✔
156
  }
157

158
  /**
159
   * Get the `leaf` of the conditional order. This is the data that is used to create the merkle tree.
160
   *
161
   * For the purposes of this library, the `leaf` is the `ConditionalOrderParams` struct.
162
   * @returns The `leaf` of the conditional order.
163
   * @see ConditionalOrderParams
164
   */
165
  get leaf(): ConditionalOrderParams {
166
    return {
99✔
167
      handler: this.handler,
168
      salt: this.salt,
169
      staticInput: this.encodeStaticInput(),
170
    }
171
  }
172

173
  /**
174
   * Calculate the id of the conditional order.
175
   * @param leaf The `leaf` representing the conditional order.
176
   * @returns The id of the conditional order.
177
   * @see ConditionalOrderParams
178
   */
179
  static leafToId(leaf: ConditionalOrderParams): string {
180
    return utils.keccak256(encodeParams(leaf))
1✔
181
  }
182

183
  /**
184
   * If the conditional order has off-chain input, return it!
185
   *
186
   * **NOTE**: This should be overridden by any conditional order that has off-chain input.
187
   * @returns The off-chain input.
188
   */
189
  get offChainInput(): string {
190
    return '0x'
2✔
191
  }
192

193
  /**
194
   * Create a human-readable string representation of the conditional order.
195
   *
196
   * @param tokenFormatter An optional function that takes an address and an amount and returns a human-readable string.
197
   */
198
  abstract toString(tokenFormatter?: (address: string, amount: BigNumber) => string): string
199

200
  /**
201
   * Serializes the conditional order into it's ABI-encoded form.
202
   *
203
   * @returns The equivalent of `IConditionalOrder.Params` for the conditional order.
204
   */
205
  abstract serialize(): string
206

207
  /**
208
   * Encode the `staticInput` for the conditional order.
209
   *
210
   * @returns The ABI-encoded `staticInput` for the conditional order.
211
   * @see ConditionalOrderParams
212
   */
213
  abstract encodeStaticInput(): string
214

215
  /**
216
   * A helper function for generically serializing a conditional order's static input.
217
   *
218
   * @param orderDataTypes ABI types for the order's data struct.
219
   * @param data The order's data struct.
220
   * @returns An ABI-encoded representation of the order's data struct.
221
   */
222
  protected encodeStaticInputHelper(orderDataTypes: string[], staticInput: S): string {
223
    return utils.defaultAbiCoder.encode(orderDataTypes, [staticInput])
98✔
224
  }
225

226
  /**
227
   * Poll a conditional order to see if it is tradeable.
228
   *
229
   * @param owner The owner of the conditional order.
230
   * @param p The proof and parameters.
231
   * @param chain Which chain to use for the ComposableCoW contract.
232
   * @param provider An RPC provider for the chain.
233
   * @param offChainInputFn A function, if provided, that will return the off-chain input for the conditional order.
234
   * @throws If the conditional order is not tradeable.
235
   * @returns The tradeable `GPv2Order.Data` struct and the `signature` for the conditional order.
236
   */
237
  async poll(params: PollParams): Promise<PollResult> {
238
    const { chainId, owner, provider } = params
×
239
    const composableCow = getComposableCow(chainId, provider)
×
240

241
    try {
×
242
      const isValid = this.isValid()
×
243
      // Do a validation first
244
      if (!isValid.isValid) {
×
245
        return {
×
246
          result: PollResultCode.DONT_TRY_AGAIN,
247
          reason: `InvalidConditionalOrder. Reason: ${isValid.reason}`,
248
        }
249
      }
250

251
      // Let the concrete Conditional Order decide about the poll result
252
      const pollResult = await this.pollValidate(params)
×
253
      if (pollResult) {
×
254
        return pollResult
×
255
      }
256

257
      // Check if the owner authorised the order
258
      const isAuthorized = await this.isAuthorized(params)
×
259
      if (!isAuthorized) {
×
260
        return {
×
261
          result: PollResultCode.DONT_TRY_AGAIN,
262
          reason: `NotAuthorised: Order ${this.id} is not authorised for ${owner} on chain ${chainId}`,
263
        }
264
      }
265

266
      // Lastly, try to get the tradeable order and signature
267
      const [order, signature] = await composableCow.getTradeableOrderWithSignature(
×
268
        owner,
269
        this.leaf,
270
        this.offChainInput,
271
        []
272
      )
273

274
      return {
×
275
        result: PollResultCode.SUCCESS,
276
        order,
277
        signature,
278
      }
279
    } catch (error) {
280
      return {
×
281
        result: PollResultCode.UNEXPECTED_ERROR,
282
        error: error,
283
      }
284
    }
285
  }
286

287
  /**
288
   * Checks if the owner authorized the conditional order.
289
   *
290
   * @param params owner context, to be able to check if the order is authorized
291
   * @returns true if the owner authorized the order, false otherwise.
292
   */
293
  public isAuthorized(params: OwnerContext): Promise<boolean> {
294
    const { chainId, owner, provider } = params
×
295
    const composableCow = getComposableCow(chainId, provider)
×
296
    return composableCow.callStatic.singleOrders(owner, this.id)
×
297
  }
298

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

307
    const slotId = this.isSingleOrder ? this.id : constants.HashZero
×
308

309
    const composableCow = getComposableCow(chainId, provider)
×
310
    return composableCow.callStatic.cabinet(owner, slotId)
×
311
  }
312

313
  /**
314
   * Allow concrete conditional orders to perform additional validation for the poll method.
315
   *
316
   * This will allow the concrete orders to decide when an order shouldn't be polled again. For example, if the orders is expired.
317
   * 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.
318
   *
319
   * @param params The poll parameters
320
   *
321
   * @returns undefined if the concrete order can't make a decision. Otherwise, it returns a PollResultErrors object.
322
   */
323
  protected abstract pollValidate(params: PollParams): Promise<PollResultErrors | undefined>
324

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

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

347
  /**
348
   * A helper function for generically deserializing a conditional order.
349
   * @param s The ABI-encoded `IConditionalOrder.Params` struct to deserialize.
350
   * @param handler Address of the handler for the conditional order.
351
   * @param orderDataTypes ABI types for the order's data struct.
352
   * @param callback A callback function that takes the deserialized data struct and the salt and returns an instance of the class.
353
   * @returns An instance of the conditional order class.
354
   */
355
  protected static deserializeHelper<T>(
356
    s: string,
357
    handler: string,
358
    orderDataTypes: string[],
359
    callback: (d: any, salt: string) => T
360
  ): T {
361
    try {
5✔
362
      // First, decode the `IConditionalOrder.Params` struct
363
      const { handler: recoveredHandler, salt, staticInput } = decodeParams(s)
5✔
364

365
      // Second, verify that the recovered handler is the correct handler
366
      if (!(recoveredHandler == handler)) throw new Error('HandlerMismatch')
3✔
367

368
      // Third, decode the data struct
369
      const [d] = utils.defaultAbiCoder.decode(orderDataTypes, staticInput)
2✔
370

371
      // Create a new instance of the class
372
      return callback(d, salt)
2✔
373
    } catch (e: any) {
374
      if (e.message === 'HandlerMismatch') {
3✔
375
        throw e
1✔
376
      } else {
377
        throw new Error('InvalidSerializedConditionalOrder')
2✔
378
      }
379
    }
380
  }
381
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc