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

cowprotocol / cow-sdk / 14001954831

21 Mar 2025 10:26PM UTC coverage: 73.659% (-3.4%) from 77.101%
14001954831

Pull #253

github

anxolin
chore: improve signatures
Pull Request #253: feat(bridging): get quote + post cross-chain swap

392 of 552 branches covered (71.01%)

Branch coverage included in aggregate %.

41 of 126 new or added lines in 16 files covered. (32.54%)

23 existing lines in 5 files now uncovered.

844 of 1126 relevant lines covered (74.96%)

16.39 hits per line

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

80.61
/src/order-book/api.ts
1
import 'cross-fetch/polyfill'
2
import { RateLimiter } from 'limiter'
3
import { SupportedChainId } from '../chains/types'
4
import { ApiBaseUrls, ApiContext, CowEnv, PartialApiContext } from '../common/types/config'
5
import { CowError } from '../common/types/cow-error'
6
import {
7
  Address,
8
  AppDataHash,
9
  AppDataObject,
10
  CompetitionOrderStatus,
11
  NativePriceResponse,
12
  Order,
13
  OrderCancellations,
14
  OrderCreation,
15
  OrderQuoteRequest,
16
  OrderQuoteResponse,
17
  SolverCompetitionResponse,
18
  TotalSurplus,
19
  Trade,
20
  TransactionHash,
21
  UID,
22
} from './generated'
23
import { DEFAULT_BACKOFF_OPTIONS, DEFAULT_LIMITER_OPTIONS, FetchParams, OrderBookApiError, request } from './request'
24
import { transformOrder } from './transformOrder'
25
import { EnrichedOrder } from './types'
26
import { DEFAULT_COW_API_CONTEXT, ENVS_LIST } from '../common'
27

28
/**
29
 * An object containing *production* environment base URLs for each supported `chainId`.
30
 * @see {@link https://api.cow.fi/docs/#/}
31
 */
32
export const ORDER_BOOK_PROD_CONFIG: ApiBaseUrls = {
14✔
33
  [SupportedChainId.MAINNET]: 'https://api.cow.fi/mainnet',
34
  [SupportedChainId.GNOSIS_CHAIN]: 'https://api.cow.fi/xdai',
35
  [SupportedChainId.ARBITRUM_ONE]: 'https://api.cow.fi/arbitrum_one',
36
  [SupportedChainId.BASE]: 'https://api.cow.fi/base',
37
  [SupportedChainId.SEPOLIA]: 'https://api.cow.fi/sepolia',
38
}
39

40
/**
41
 * An object containing *staging* environment base URLs for each supported `chainId`.
42
 */
43
export const ORDER_BOOK_STAGING_CONFIG: ApiBaseUrls = {
14✔
44
  [SupportedChainId.MAINNET]: 'https://barn.api.cow.fi/mainnet',
45
  [SupportedChainId.GNOSIS_CHAIN]: 'https://barn.api.cow.fi/xdai',
46
  [SupportedChainId.ARBITRUM_ONE]: 'https://barn.api.cow.fi/arbitrum_one',
47
  [SupportedChainId.BASE]: 'https://barn.api.cow.fi/base',
48
  [SupportedChainId.SEPOLIA]: 'https://barn.api.cow.fi/sepolia',
49
}
50

51
function cleanObjectFromUndefinedValues(obj: Record<string, string>): typeof obj {
52
  return Object.keys(obj).reduce((acc, key) => {
8✔
53
    const val = obj[key]
12✔
54
    if (typeof val !== 'undefined') acc[key] = val
12!
55
    return acc
12✔
56
  }, {} as typeof obj)
57
}
58

59
/**
60
 * The parameters for the `getOrders` request.
61
 */
62
export type GetOrdersRequest = {
63
  owner: Address
64
  offset?: number
65
  limit?: number
66
}
67

68
/**
69
 * The CoW Protocol OrderBook API client.
70
 *
71
 * This is the main entry point for interacting with the CoW Protocol OrderBook API. The main advantage of using
72
 * this client is the batteries-included approach to interacting with the API. It handles:
73
 *
74
 * - Environment configuration (mainnet, staging, etc.)
75
 * - Rate limiting
76
 * - Retries
77
 * - Backoff
78
 * - Error handling
79
 * - Request signing
80
 * - Request validation
81
 *
82
 * @example
83
 *
84
 * ```typescript
85
 * import { OrderBookApi, OrderSigningUtils, SupportedChainId } from '@cowprotocol/cow-sdk'
86
 * import { Web3Provider } from '@ethersproject/providers'
87
 *
88
 * const account = 'YOUR_WALLET_ADDRESS'
89
 * const chainId = 100 // Gnosis chain
90
 * const provider = new Web3Provider(window.ethereum)
91
 * const signer = provider.getSigner()
92
 *
93
 * const quoteRequest = {
94
 *   sellToken: '0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1', // WETH gnosis chain
95
 *   buyToken: '0x9c58bacc331c9aa871afd802db6379a98e80cedb', // GNO gnosis chain
96
 *   from: account,
97
 *   receiver: account,
98
 *   sellAmountBeforeFee: (0.4 * 10 ** 18).toString(), // 0.4 WETH
99
 *   kind: OrderQuoteSide.kind.SELL,
100
 * }
101
 *
102
 * const orderBookApi = new OrderBookApi({ chainId: SupportedChainId.GNOSIS_CHAIN })
103
 *
104
 * async function main() {
105
 *     const { quote } = await orderBookApi.getQuote(quoteRequest)
106
 *
107
 *     const orderSigningResult = await OrderSigningUtils.signOrder(quote, chainId, signer)
108
 *
109
 *     const orderId = await orderBookApi.sendOrder({ ...quote, ...orderSigningResult })
110
 *
111
 *     const order = await orderBookApi.getOrder(orderId)
112
 *
113
 *     const trades = await orderBookApi.getTrades({ orderId })
114
 *
115
 *     const orderCancellationSigningResult = await OrderSigningUtils.signOrderCancellations([orderId], chainId, signer)
116
 *
117
 *     const cancellationResult = await orderBookApi.sendSignedOrderCancellations({...orderCancellationSigningResult, orderUids: [orderId] })
118
 *
119
 *     console.log('Results: ', { orderId, order, trades, orderCancellationSigningResult, cancellationResult })
120
 * }
121
 * ```
122
 *
123
 * @see {@link Swagger documentation https://api.cow.fi/docs/#/}
124
 * @see {@link OrderBook API https://github.com/cowprotocol/services}
125
 */
126
export class OrderBookApi {
127
  public context: ApiContext
128

129
  private rateLimiter: RateLimiter
130

131
  /**
132
   * Creates a new instance of the CoW Protocol OrderBook API client.
133
   * @param context - The API context to use. If not provided, the default context will be used.
134
   */
135
  constructor(context: PartialApiContext = {}) {
×
136
    this.context = { ...DEFAULT_COW_API_CONTEXT, ...context }
1✔
137
    this.rateLimiter = new RateLimiter(context.limiterOpts || DEFAULT_LIMITER_OPTIONS)
1✔
138
  }
139

140
  /**
141
   * Get the version of the API.
142
   * @param contextOverride Optional context override for this request.
143
   * @returns The version of the API.
144
   * @see {@link https://api.cow.fi/docs/#/default/get_api_v1_version}
145
   */
146
  getVersion(contextOverride: PartialApiContext = {}): Promise<string> {
1✔
147
    return this.fetch({ path: '/api/v1/version', method: 'GET' }, contextOverride)
1✔
148
  }
149

150
  /**
151
   * Get all the trades for either an `owner` **OR** `orderUid`.
152
   *
153
   * Given that an order *may* be partially fillable, it is possible that a discrete order (`orderUid`)
154
   * may have *multiple* trades. Therefore, this method returns a list of trades, either for *all* the orders
155
   * of a given `owner`, or for a discrete order (`orderUid`).
156
   * @param request Either an `owner` or an `orderUid` **MUST** be specified.
157
   * @param contextOverride Optional context override for this request.
158
   * @returns A list of trades matching the request.
159
   */
160
  getTrades(
161
    request: { owner?: Address; orderUid?: UID },
162
    contextOverride: PartialApiContext = {}
5✔
163
  ): Promise<Array<Trade>> {
164
    if (request.owner && request.orderUid) {
5✔
165
      return Promise.reject(new CowError('Cannot specify both owner and orderId'))
1✔
166
    } else if (!request.owner && !request.orderUid) {
4!
UNCOV
167
      return Promise.reject(new CowError('Must specify either owner or orderId'))
×
168
    }
169

170
    const query = new URLSearchParams(cleanObjectFromUndefinedValues(request))
4✔
171

172
    return this.fetch({ path: '/api/v1/trades', method: 'GET', query }, contextOverride)
4✔
173
  }
174

175
  /**
176
   * Get a list of orders for a given `owner`.
177
   * @param request The request parameters with `request.offset = 0` and `request.limit = 1000` by default.
178
   * @param contextOverride Optional context override for this request.
179
   * @returns A list of orders matching the request.
180
   * @see {@link GetOrdersRequest}
181
   * @see {@link EnrichedOrder}
182
   */
183
  getOrders(
184
    { owner, offset = 0, limit = 1000 }: GetOrdersRequest,
×
185
    contextOverride: PartialApiContext = {}
4✔
186
  ): Promise<Array<EnrichedOrder>> {
187
    const query = new URLSearchParams(
4✔
188
      cleanObjectFromUndefinedValues({ offset: offset.toString(), limit: limit.toString() })
189
    )
190

191
    return this.fetch<Array<EnrichedOrder>>(
4✔
192
      { path: `/api/v1/account/${owner}/orders`, method: 'GET', query },
193
      contextOverride
194
    ).then((orders) => {
195
      return orders.map(transformOrder)
3✔
196
    })
197
  }
198

199
  /**
200
   * Get a list of orders from a given settlement transaction hash.
201
   * @param txHash The transaction hash.
202
   * @param contextOverride Optional context override for this request.
203
   * @returns A list of orders matching the request.
204
   * @see {@link EnrichedOrder}
205
   */
206
  getTxOrders(txHash: TransactionHash, contextOverride: PartialApiContext = {}): Promise<Array<EnrichedOrder>> {
3✔
207
    return this.fetch<Array<EnrichedOrder>>(
3✔
208
      { path: `/api/v1/transactions/${txHash}/orders`, method: 'GET' },
209
      contextOverride
210
    ).then((orders) => {
211
      return orders.map(transformOrder)
2✔
212
    })
213
  }
214

215
  /**
216
   * Get an order by its unique identifier, `orderUid`.
217
   * @param orderUid The unique identifier of the order.
218
   * @param contextOverride Optional context override for this request.
219
   * @returns The order matching the request.
220
   */
221
  getOrder(orderUid: UID, contextOverride: PartialApiContext = {}): Promise<EnrichedOrder> {
4✔
222
    return this.fetch<Order>({ path: `/api/v1/orders/${orderUid}`, method: 'GET' }, contextOverride).then((order) => {
7✔
223
      return transformOrder(order)
5✔
224
    })
225
  }
226

227
  /**
228
   * Get the order status while open
229
   */
230
  getOrderCompetitionStatus(orderUid: UID, contextOverride: PartialApiContext = {}): Promise<CompetitionOrderStatus> {
×
UNCOV
231
    return this.fetch({ path: `/api/v1/orders/${orderUid}/status`, method: 'GET' }, contextOverride)
×
232
  }
233

234
  /**
235
   * Attempt to get an order by its unique identifier, `orderUid`, from multiple environments.
236
   *
237
   * **NOTE**: The environment refers to either `prod` or `staging`. This allows a conveience method to
238
   * attempt to get an order from both environments, in the event that the order is not found in the
239
   * environment specified in the context.
240
   * @param orderUid The unique identifier of the order.
241
   * @param contextOverride Optional context override for this request.
242
   * @returns The order matching the request.
243
   * @throws {OrderBookApiError} If the order is not found in any of the environments.
244
   */
245
  getOrderMultiEnv(orderUid: UID, contextOverride: PartialApiContext = {}): Promise<EnrichedOrder> {
×
246
    const { env } = this.getContextWithOverride(contextOverride)
1✔
247
    const otherEnvs = ENVS_LIST.filter((i) => i !== env)
2✔
248

249
    let attemptsCount = 0
1✔
250

251
    const fallback = (error: Error | OrderBookApiError): Promise<EnrichedOrder> => {
1✔
252
      const nextEnv = otherEnvs[attemptsCount]
1✔
253

254
      if (error instanceof OrderBookApiError && error.response.status === 404 && nextEnv) {
1!
255
        attemptsCount++
1✔
256

257
        return this.getOrder(orderUid, { ...contextOverride, env: nextEnv }).catch(fallback)
1✔
258
      }
259

UNCOV
260
      return Promise.reject(error)
×
261
    }
262

263
    return this.getOrder(orderUid, { ...contextOverride, env }).catch(fallback)
1✔
264
  }
265

266
  /**
267
   * Get a quote for an order.
268
   * This allows for the calculation of the total cost of an order, including fees, before signing and submitting.
269
   * @param requestBody The parameters for the order quote request.
270
   * @param contextOverride Optional context override for this request.
271
   * @returns A hydrated order matching the request ready to be signed.
272
   */
273
  getQuote(requestBody: OrderQuoteRequest, contextOverride: PartialApiContext = {}): Promise<OrderQuoteResponse> {
×
UNCOV
274
    return this.fetch({ path: '/api/v1/quote', method: 'POST', body: requestBody }, contextOverride)
×
275
  }
276

277
  /**
278
   * Cancel one or more orders.
279
   *
280
   * **NOTE**: Cancellation is on a best-effort basis. Orders that are already in the process of being settled
281
   * (ie. transaction has been submitted to chain by the solver) cannot not be cancelled.
282
   * **CAUTION**: This method can only be used to cancel orders that were signed using `EIP-712` or `eth_sign (EIP-191)`.
283
   * @param requestBody Orders to be cancelled and signed instructions to cancel them.
284
   * @param contextOverride Optional context override for this request.
285
   * @returns A list of order unique identifiers that were successfully cancelled.
286
   */
287
  sendSignedOrderCancellations(
288
    requestBody: OrderCancellations,
289
    contextOverride: PartialApiContext = {}
2✔
290
  ): Promise<void> {
291
    return this.fetch({ path: '/api/v1/orders', method: 'DELETE', body: requestBody }, contextOverride)
2✔
292
  }
293

294
  /**
295
   * Submit an order to the order book.
296
   * @param requestBody The signed order to be submitted.
297
   * @param contextOverride Optional context override for this request.
298
   * @returns The unique identifier of the order.
299
   */
300
  sendOrder(requestBody: OrderCreation, contextOverride: PartialApiContext = {}): Promise<UID> {
2✔
301
    return this.fetch({ path: '/api/v1/orders', method: 'POST', body: requestBody }, contextOverride)
2✔
302
  }
303

304
  /**
305
   * Get the native price of a token.
306
   *
307
   * **NOTE**: The native price is the price of the token in the native currency of the chain. For example, on Ethereum
308
   * this would be the price of the token in ETH.
309
   * @param tokenAddress The address of the ERC-20 token.
310
   * @param contextOverride Optional context override for this request.
311
   * @returns The native price of the token.
312
   */
313
  getNativePrice(tokenAddress: Address, contextOverride: PartialApiContext = {}): Promise<NativePriceResponse> {
×
UNCOV
314
    return this.fetch({ path: `/api/v1/token/${tokenAddress}/native_price`, method: 'GET' }, contextOverride)
×
315
  }
316

317
  /**
318
   * Given a user's address, get the total surplus that they have earned.
319
   * @param address The user's address
320
   * @param contextOverride Optional context override for this request.
321
   * @returns Calculated user's surplus
322
   */
323
  getTotalSurplus(address: Address, contextOverride: PartialApiContext = {}): Promise<TotalSurplus> {
1✔
324
    return this.fetch({ path: `/api/v1/users/${address}/total_surplus`, method: 'GET' }, contextOverride)
1✔
325
  }
326

327
  /**
328
   * Retrieve the full app data for a given app data hash.
329
   * @param appDataHash `bytes32` hash of the app data
330
   * @param contextOverride Optional context override for this request.
331
   * @returns Full app data that was uploaded
332
   */
333
  getAppData(appDataHash: AppDataHash, contextOverride: PartialApiContext = {}): Promise<AppDataObject> {
1✔
334
    return this.fetch({ path: `/api/v1/app_data/${appDataHash}`, method: 'GET' }, contextOverride)
1✔
335
  }
336

337
  /**
338
   * Upload the full app data that corresponds to a given app data hash.
339
   * @param appDataHash `bytes32` hash of the app data
340
   * @param fullAppData Full app data to be uploaded
341
   * @param contextOverride Optional context override for this request.
342
   * @returns The string encoding of the full app data that was uploaded.
343
   */
344
  uploadAppData(
345
    appDataHash: AppDataHash,
346
    fullAppData: string,
347
    contextOverride: PartialApiContext = {}
1✔
348
  ): Promise<AppDataObject> {
349
    return this.fetch(
1✔
350
      { path: `/api/v1/app_data/${appDataHash}`, method: 'PUT', body: { fullAppData } },
351
      contextOverride
352
    )
353
  }
354

355
  getSolverCompetition(auctionId: number, contextOverride?: PartialApiContext): Promise<SolverCompetitionResponse>
356

357
  getSolverCompetition(txHash: string, contextOverride?: PartialApiContext): Promise<SolverCompetitionResponse>
358

359
  /**
360
   * Given an auction id or tx hash, get the details of the solver competition for that auction.
361
   * @param auctionIdorTx auction id or tx hash corresponding to the auction
362
   * @param contextOverride Optional context override for this request.
363
   * @returns An object containing the solver competition details
364
   */
365
  getSolverCompetition(
366
    auctionIdorTx: number | string,
367
    contextOverride: PartialApiContext = {}
2✔
368
  ): Promise<SolverCompetitionResponse> {
369
    return this.fetch(
2✔
370
      {
371
        path: `/api/v1/solver_competition${typeof auctionIdorTx === 'string' ? '/by_tx_hash' : ''}/${auctionIdorTx}`,
2✔
372
        method: 'GET',
373
      },
374
      contextOverride
375
    )
376
  }
377

378
  /**
379
   * Generate an API endpoint for an order by its unique identifier, `orderUid`.
380
   * @param orderUid The unique identifier of the order.
381
   * @param contextOverride Optional context override for this request.
382
   * @returns The API endpoint to get the order.
383
   */
384
  getOrderLink(orderUid: UID, contextOverride?: PartialApiContext): string {
385
    const { chainId, env } = this.getContextWithOverride(contextOverride)
1✔
386
    return this.getApiBaseUrls(env)[chainId] + `/api/v1/orders/${orderUid}`
1✔
387
  }
388

389
  /**
390
   * Apply an override to the context for a request.
391
   * @param contextOverride Optional context override for this request.
392
   * @returns New context with the override applied.
393
   */
394
  private getContextWithOverride(contextOverride: PartialApiContext = {}): ApiContext {
1✔
395
    return { ...this.context, ...contextOverride }
30✔
396
  }
397

398
  /**
399
   * Get the base URLs for the API endpoints given the environment.
400
   * @param env The environment to get the base URLs for.
401
   * @returns The base URLs for the API endpoints.
402
   */
403
  private getApiBaseUrls(env: CowEnv): ApiBaseUrls {
404
    if (this.context.baseUrls) return this.context.baseUrls
29!
405

406
    return env === 'prod' ? ORDER_BOOK_PROD_CONFIG : ORDER_BOOK_STAGING_CONFIG
29✔
407
  }
408

409
  /**
410
   * Make a request to the API.
411
   * @param params The parameters for the request.
412
   * @param contextOverride Optional context override for this request.
413
   * @returns The response from the API.
414
   */
415
  private fetch<T>(params: FetchParams, contextOverride: PartialApiContext = {}): Promise<T> {
×
416
    const { chainId, env, backoffOpts: _backoffOpts } = this.getContextWithOverride(contextOverride)
28✔
417
    const baseUrl = this.getApiBaseUrls(env)[chainId]
28✔
418
    const backoffOpts = _backoffOpts || DEFAULT_BACKOFF_OPTIONS
28!
419
    const rateLimiter = contextOverride.limiterOpts ? new RateLimiter(contextOverride.limiterOpts) : this.rateLimiter
28!
420

421
    return request(baseUrl, params, rateLimiter, backoffOpts)
28✔
422
  }
423
}
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