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

cowprotocol / cow-sdk / #1693

14 May 2025 06:27PM UTC coverage: 76.468% (+0.9%) from 75.588%
#1693

push

anxolin
fix: don't treat slippage 0 as AUTO

450 of 640 branches covered (70.31%)

Branch coverage included in aggregate %.

1009 of 1268 relevant lines covered (79.57%)

18.91 hits per line

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

95.0
/src/composable/Multiplexer.ts
1
import { StandardMerkleTree } from '@openzeppelin/merkle-tree'
2
import { BigNumber, providers, utils } from 'ethers'
3

4
import { SupportedChainId } from '../chains'
5

6
import { ComposableCoW, GPv2Order } from '../common/generated/ComposableCoW'
7
import { ProofLocation, ProofWithParams, ConditionalOrderParams } from './types'
8
import { ConditionalOrder } from './ConditionalOrder'
9
import { getComposableCow } from './contracts'
10

11
const CONDITIONAL_ORDER_LEAF_ABI = ['address', 'bytes32', 'bytes']
1✔
12

13
const PAYLOAD_EMITTED_ABI = ['tuple(bytes32[] proof, tuple(address handler, bytes32 salt, bytes staticInput) params)[]']
1✔
14

15
export type Orders = Record<string, ConditionalOrder<unknown, unknown>>
16

17
/**
18
 * Multiplexer for conditional orders - using `ComposableCoW`!
19
 *
20
 * This class provides functionality to:
21
 * - Generate a merkle tree of conditional orders
22
 * - Generate proofs for all orders in the merkle tree
23
 * - Save proofs, with the ability to omit / skip specific conditional orders
24
 * - Support for passing an optional upload function to upload the proofs to a decentralized storage network
25
 */
26
export class Multiplexer {
27
  static orderTypeRegistry: Record<string, new (...args: unknown[]) => ConditionalOrder<unknown, unknown>> = {}
1✔
28

29
  public chain: SupportedChainId
30
  public location: ProofLocation
31

32
  private orders: Orders = {}
16✔
33
  private tree?: StandardMerkleTree<string[]>
34
  private ctx?: string
35

36
  /**
37
   * @param chain The `chainId` for where we're using `ComposableCoW`.
38
   * @param orders An optional array of conditional orders to initialize the merkle tree with.
39
   * @param root An optional root to verify against.
40
   * @param location The location of the proofs for the conditional orders.
41
   */
42
  constructor(
43
    chain: SupportedChainId,
44
    orders?: Orders,
45
    root?: string,
46
    location: ProofLocation = ProofLocation.PRIVATE,
16✔
47
  ) {
48
    this.chain = chain
16✔
49
    this.location = location
16✔
50

51
    // If orders are provided, the length must be > 0
52
    if (orders && Object.keys(orders).length === 0) {
16✔
53
      throw new Error('orders must have non-zero length')
1✔
54
    }
55

56
    // If orders are provided, so must a root, and vice versa
57
    if ((orders && !root) || (!orders && root)) {
15✔
58
      throw new Error('orders cannot have undefined root')
1✔
59
    }
60

61
    // can only proceed past here if both orders and root are provided, or neither are
62

63
    // validate that no unknown order types are provided
64
    for (const orderKey in orders) {
14✔
65
      if (orders.hasOwnProperty(orderKey)) {
12!
66
        const order = orders[orderKey]
12✔
67
        if (!Multiplexer.orderTypeRegistry.hasOwnProperty(order.orderType)) {
12✔
68
          throw new Error(`Unknown order type: ${order.orderType}`)
1✔
69
        }
70
      }
71
    }
72

73
    // If orders (and therefore the root) are provided, generate the merkle tree
74
    if (orders) {
13✔
75
      this.orders = orders
2✔
76

77
      // if generate was successful, we can verify the root
78
      if (this.getOrGenerateTree().root !== root) {
2✔
79
        throw new Error('root mismatch')
1✔
80
      }
81
    }
82
  }
83

84
  // --- user facing serialization methods ---
85

86
  /**
87
   * Given a serialized multiplexer, create the multiplexer and rehydrate all conditional orders.
88
   * Integrity of the multiplexer will be verified by generating the merkle tree and verifying
89
   * the root.
90
   *
91
   * **NOTE**: Before using this method, you must register all conditional order types using `Multiplexer.registerOrderType`.
92
   * @param s The serialized multiplexer.
93
   * @returns The multiplexer with all conditional orders rehydrated.
94
   * @throws If the multiplexer cannot be deserialized.
95
   * @throws If the merkle tree cannot be generated.
96
   * @throws If the merkle tree cannot be verified against the root.
97
   */
98
  static fromJSON(s: string): Multiplexer {
99
    // reviver function to deserialize the orders
100
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
    const reviver = (k: string, v: any) => {
2✔
102
      if (k === 'orders' && typeof v === 'object' && v !== null) {
556✔
103
        const orders: Orders = {}
2✔
104

105
        for (const orderKey in v) {
2✔
106
          if (v.hasOwnProperty(orderKey)) {
11!
107
            const { orderType, ...orderData } = v[orderKey]
11✔
108

109
            if (Multiplexer.orderTypeRegistry.hasOwnProperty(orderType)) {
11✔
110
              const OrderConstructor = Multiplexer.orderTypeRegistry[orderType]
10✔
111
              orders[orderKey] = new OrderConstructor(orderData)
10✔
112
            } else {
113
              throw new Error(`Unknown order type: ${orderType}`)
1✔
114
            }
115
          }
116
        }
117

118
        return orders
1✔
119
      }
120

121
      // Make sure we deserialize `BigNumber` correctly
122
      if (typeof v === 'object' && v !== null && v.hasOwnProperty('type') && v.hasOwnProperty('hex')) {
554✔
123
        if (v.type === 'BigNumber') {
110!
124
          return BigNumber.from(v)
110✔
125
        }
126
      }
127

128
      return v
444✔
129
    }
130

131
    const { chain, orders, root, location } = JSON.parse(s, reviver)
2✔
132
    const m = new Multiplexer(chain, orders, root)
1✔
133
    m.location = location
1✔
134
    return m
1✔
135
  }
136

137
  /**
138
   * Serialize the multiplexer to JSON.
139
   *
140
   * This will include all state necessary to reconstruct the multiplexer, including the root.
141
   * @remarks This will **NOT** include the merkle tree.
142
   * @returns The JSON representation of the multiplexer, including the root but excluding the merkle tree.
143
   */
144
  toJSON(): string {
145
    const root = this.getOrGenerateTree().root
4✔
146

147
    // serialize the multiplexer, including the root but excluding the merkle tree.
148
    return JSON.stringify({ ...this, root }, (k, v) => {
4✔
149
      // filter out the merkle tree
150
      if (k === 'tree') return undefined
674✔
151
      if (typeof v === 'object' && v !== null && 'orderType' in v) {
670✔
152
        const conditionalOrder = v as ConditionalOrder<unknown, unknown>
13✔
153
        return {
13✔
154
          ...conditionalOrder,
155
          orderType: conditionalOrder.orderType,
156
        }
157
      }
158
      // We do not do any custom serialization of `BigNumber` in order to preserve it's type.
159
      return v
657✔
160
    })
161
  }
162

163
  // --- crud methods ---
164

165
  /**
166
   * Add a conditional order to the merkle tree.
167
   * @param order The order to add to the merkle tree.
168
   */
169
  add<T, P>(order: ConditionalOrder<T, P>): void {
170
    order.assertIsValid()
38✔
171

172
    this.orders[order.id] = order
37✔
173
    this.reset()
37✔
174
  }
175

176
  /**
177
   * Remove a conditional order from the merkle tree.
178
   * @param id The id of the `ConditionalOrder` to remove from the merkle tree.
179
   */
180
  remove(id: string): void {
181
    delete this.orders[id]
1✔
182
    this.reset()
1✔
183
  }
184

185
  /**
186
   * Update a given conditional order in the merkle tree.
187
   * @param id The id of the `ConditionalOrder` to update.
188
   * @param updater A function that takes the existing `ConditionalOrder` and context, returning an updated `ConditionalOrder`.
189
   */
190
  update(
191
    id: string,
192
    updater: (order: ConditionalOrder<unknown, unknown>, ctx?: string) => ConditionalOrder<unknown, unknown>,
193
  ): void {
194
    // copy the existing order and update it, given the existing context (if any)
195
    const order = updater(this.orders[id], this.ctx)
1✔
196
    // delete the existing order
197
    delete this.orders[id]
1✔
198

199
    // add the updated order
200
    this.orders[order.id] = order
1✔
201
    this.reset()
1✔
202
  }
203

204
  // --- accessors ---
205

206
  /**
207
   * Accessor for a given conditional order in the multiplexer.
208
   * @param id The `id` of the `ConditionalOrder` to retrieve.
209
   * @returns A `ConditionalOrder` with the given `id`.
210
   */
211
  getById(id: string): ConditionalOrder<unknown, unknown> {
212
    return this.orders[id]
3✔
213
  }
214

215
  /**
216
   * Accessor for a given conditional order in the multiplexer.
217
   * @param i The index of the `ConditionalOrder` to retrieve.
218
   * @returns A `ConditionalOrder` at the given index.
219
   */
220
  getByIndex(i: number): ConditionalOrder<unknown, unknown> {
221
    return this.orders[this.orderIds[i]]
3✔
222
  }
223

224
  /**
225
   * Get all the conditional order ids in the multiplexer.
226
   */
227
  get orderIds(): string[] {
228
    return Object.keys(this.orders)
6✔
229
  }
230

231
  get root(): string {
232
    return this.getOrGenerateTree().root
4✔
233
  }
234

235
  /**
236
   * Retrieve the merkle tree of orders, or generate it if it doesn't exist.
237
   *
238
   * **CAUTION**: Developers of the SDK should prefer to use this method instead of generating the
239
   *              merkle tree themselves. This method makes use of caching to avoid generating the
240
   *              merkle tree needlessly.
241
   * @throws If the merkle tree cannot be generated.
242
   * @returns The merkle tree for the current set of conditional orders.
243
   */
244
  private getOrGenerateTree(): StandardMerkleTree<string[]> {
245
    if (!this.tree) {
36✔
246
      this.tree = StandardMerkleTree.of(
11✔
247
        Object.values(this.orders).map((order) => [...Object.values(order.leaf)]),
48✔
248
        CONDITIONAL_ORDER_LEAF_ABI,
249
      )
250
    }
251

252
    return this.tree
36✔
253
  }
254

255
  // --- serialization for watchtowers / indexers ---
256

257
  /**
258
   * The primary method for watch towers to use when deserializing the proofs and parameters for the conditional orders.
259
   * @param s The serialized proofs with parameters for consumption by watchtowers / indexers.
260
   * @returns The `ProofWithParams` array.
261
   * @throws If the `ProofWithParams` array cannot be deserialized.
262
   */
263
  static decodeFromJSON(s: string): ProofWithParams[] {
264
    // no need to rehydrate `BigNumber` as this is fully ABI encoded
265
    return JSON.parse(s)
1✔
266
  }
267

268
  /**
269
   * The primary entry point for dapps integrating with `ComposableCoW` to generate the proofs and
270
   * parameters for the conditional orders.
271
   *
272
   * After populating the multiplexer with conditional orders, this method can be used to generate
273
   * the proofs and parameters for the conditional orders. The returned `ProofStruct` can then be
274
   * used with `setRoot` or `setRootWithContext` on a `ComposableCoW`-enabled Safe.
275
   *
276
   * @param filter {@link getProofs}
277
   * @parma locFn A function that takes the off-chain encoded input, and returns the `location`
278
   *        for the `ProofStruct`, and the `data` for the `ProofStruct`.
279
   * @returns The ABI-encoded `ProofStruct` for `setRoot` and `setRootWithContext`.
280
   */
281
  async prepareProofStruct(
282
    location: ProofLocation = this.location,
1✔
283
    filter?: (v: string[]) => boolean,
284
    uploader?: (offChainEncoded: string) => Promise<string>,
285
  ): Promise<ComposableCoW.ProofStruct> {
286
    const data = async (): Promise<string> => {
6✔
287
      switch (location) {
6✔
288
        case ProofLocation.PRIVATE:
289
          return '0x'
1✔
290
        case ProofLocation.EMITTED:
291
          return this.encodeToABI(filter)
1✔
292
        case ProofLocation.SWARM:
293
        case ProofLocation.WAKU:
294
        case ProofLocation.IPFS:
295
          if (!uploader) throw new Error('Must provide an uploader function')
3✔
296
          try {
2✔
297
            return await uploader(this.encodeToJSON(filter))
2✔
298
          } catch (e) {
299
            throw new Error(`Error uploading to decentralized storage ${location}: ${e}`)
1✔
300
          }
301
        default:
302
          throw new Error('Unsupported location')
1✔
303
      }
304
    }
305

306
    return await data()
6✔
307
      .then((d) => {
308
        try {
3✔
309
          // validate that `d` is a valid `bytes` ready to be abi-encoded
310
          utils.hexlify(utils.arrayify(d))
3✔
311

312
          // if we get here, we have a valid `data` field for the `ProofStruct`
313
          // This means that if there was an upload function, it was called and the upload was successful
314
          // note: we don't check if the location has changed because we don't care
315
          this.location = location
2✔
316

317
          return {
2✔
318
            location,
319
            data: d,
320
          }
321
        } catch {
322
          throw new Error(`data returned by uploader is invalid`)
1✔
323
        }
324
      })
325
      .catch((e) => {
326
        throw new Error(`Error preparing proof struct: ${e}`)
4✔
327
      })
328
  }
329

330
  /**
331
   * Poll a conditional order to see if it is tradeable.
332
   * @param owner The owner of the conditional order.
333
   * @param p The proof and parameters.
334
   * @param chain Which chain to use for the ComposableCoW contract.
335
   * @param provider An RPC provider for the chain.
336
   * @param offChainInputFn A function, if provided, that will return the off-chain input for the conditional order.
337
   * @throws If the conditional order is not tradeable.
338
   * @returns The tradeable `GPv2Order.Data` struct and the `signature` for the conditional order.
339
   */
340
  static async poll(
341
    owner: string,
342
    p: ProofWithParams,
343
    chain: SupportedChainId,
344
    provider: providers.Provider,
345
    offChainInputFn?: (owner: string, params: ConditionalOrderParams) => Promise<string>,
346
  ): Promise<[GPv2Order.DataStruct, string]> {
347
    const composableCow = getComposableCow(chain, provider)
×
348

349
    const offChainInput = offChainInputFn ? await offChainInputFn(owner, p.params) : '0x'
×
350
    return await composableCow.getTradeableOrderWithSignature(owner, p.params, offChainInput, p.proof)
×
351
  }
352

353
  /**
354
   * The primary entry point for dumping the proofs and parameters for the conditional orders.
355
   *
356
   * This is to be used by watchtowers / indexers to store the proofs and parameters for the
357
   * conditional orders off-chain. The encoding returned by this method may **NOT** contain all
358
   * proofs and parameters, depending on the `filter` provided, and therefore should not be used
359
   * to rehydrate the multiplexer from a user's perspective.
360
   * @param filter {@link getProofs}
361
   * @returns A JSON-encoded string of the proofs and parameters for the conditional orders.
362
   */
363
  dumpProofs(filter?: (v: string[]) => boolean): string {
364
    return this.encodeToJSON(filter)
1✔
365
  }
366

367
  dumpProofsAndParams(filter?: (v: string[]) => boolean): ProofWithParams[] {
368
    return this.getProofs(filter)
1✔
369
  }
370

371
  /**
372
   * Get the proofs with parameters for the conditional orders in the merkle tree.
373
   * @param filter A function that takes a conditional order and returns a boolean indicating
374
   *               whether the order should be included in the proof.
375
   * @returns An array of proofs and their order's parameters for the conditional orders in the
376
   *          merkle tree.
377
   */
378
  private getProofs(filter?: (v: string[]) => boolean): ProofWithParams[] {
379
    // Get a list of all entry indices in the tree, excluding any that don't match the filter
380
    return [...this.getOrGenerateTree().entries()]
5✔
381
      .map(([i, v]) => {
382
        if ((filter && filter(v)) || filter === undefined) {
23✔
383
          return { idx: i, value: v }
21✔
384
        } else {
385
          return undefined
2✔
386
        }
387
      })
388
      .reduce((acc: ProofWithParams[], x) => {
389
        if (x) {
23✔
390
          const p: ConditionalOrderParams = {
21✔
391
            handler: x.value[0],
392
            salt: x.value[1],
393
            staticInput: x.value[2],
394
          }
395
          acc.push({
21✔
396
            proof: this.getOrGenerateTree().getProof(x.idx),
397
            params: p,
398
          })
399
        }
400
        return acc
23✔
401
      }, [])
402
  }
403

404
  /**
405
   * ABI-encode the proofs and parameters for the conditional orders in the merkle tree.
406
   * @param filter {@link getProofs}
407
   * @returns ABI-encoded `data` for the `ProofStruct`.
408
   */
409
  private encodeToABI(filter?: (v: string[]) => boolean): string {
410
    return utils.defaultAbiCoder.encode(PAYLOAD_EMITTED_ABI, [this.getProofs(filter)])
1✔
411
  }
412

413
  /**
414
   * JSON-encode the proofs and parameters for the conditional orders in the merkle tree.
415
   * @param filter {@link getProofs}
416
   * @returns The JSON-encoded data for storage off-chain.
417
   */
418
  private encodeToJSON(filter?: (v: string[]) => boolean): string {
419
    return JSON.stringify(this.getProofs(filter))
3✔
420
  }
421

422
  /**
423
   * A helper to reset the merkle tree.
424
   */
425
  private reset(): void {
426
    this.tree = undefined
39✔
427
  }
428

429
  /**
430
   * Register a conditional order type with the multiplexer.
431
   *
432
   * **CAUTION**: This is required for using `Multiplexer.fromJSON` and `Multiplexer.toJSON`.
433
   * @param orderType The order type to register.
434
   * @param conditionalOrderClass The class to use for the given order type.
435
   */
436
  public static registerOrderType(
437
    orderType: string,
438
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
439
    conditionalOrderClass: new (...args: any[]) => ConditionalOrder<unknown, unknown>,
440
  ) {
441
    Multiplexer.orderTypeRegistry[orderType] = conditionalOrderClass
15✔
442
  }
443

444
  /**
445
   * Reset the order type registry.
446
   */
447
  public static resetOrderTypeRegistry() {
448
    Multiplexer.orderTypeRegistry = {}
2✔
449
  }
450
}
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

© 2026 Coveralls, Inc