• 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

89.01
/src/bridging/providers/across/AcrossApi.ts
1
import { TargetChainId } from '../../../chains'
2

3
const ACROSS_API_URL = 'https://app.across.to/api'
2✔
4

5
export interface AvailableRoutesRequest {
6
  originChainId: string
7
  originToken: string
8
  destinationChainId: string
9
  destinationToken: string
10
}
11

12
export interface Route {
13
  originChainId: string
14
  originToken: string
15
  destinationChainId: string
16
  destinationToken: string
17
  originTokenSymbol: string
18
  destinationTokenSymbol: string
19
}
20

21
export interface SuggestedFeesRequest {
22
  token: string
23
  // inputToken: string
24
  // outputToken: string
25
  originChainId: TargetChainId
26
  destinationChainId: TargetChainId
27

28
  /**
29
   * Amount of the token to transfer.
30
   *
31
   * Note that this amount is in the native decimals of the token. So, for WETH, this would be the amount of
32
   * human-readable WETH multiplied by 1e18.
33
   *
34
   * For USDC, you would multiply the number of human-readable USDC by 1e6.
35
   *
36
   * Example: 1000000000000000000
37
   */
38
  amount: bigint
39

40
  /**
41
   * Recipient of the deposit. Can be an EOA or a contract. If this is an EOA and message is defined, then the API will throw a 4xx error.
42
   *
43
   * Example: 0xc186fA914353c44b2E33eBE05f21846F1048bEda
44
   */
45
  recipient?: string
46

47
  /**
48
   * The quote timestamp used to compute the LP fees. When bridging with across, the user only specifies the quote
49
   * timestamp in their transaction. The relayer then determines the utilization at that timestamp to determine the
50
   * user's fee. This timestamp must be close (within 10 minutes or so) to the current time on the chain where the
51
   * user is depositing funds and it should be <= the current block timestamp on mainnet. This allows the user to know
52
   * exactly what LP fee they will pay before sending the transaction.
53
   *
54
   * If this value isn't provided in the request, the API will assume the latest block timestamp on mainnet.
55
   *
56
   * Example: 1653547649
57
   */
58
  timestamp?: number
59

60
  /**
61
   * Optionally override the relayer address used to simulate the fillRelay() call that estimates the gas costs
62
   * needed to fill a deposit. This simulation result impacts the returned suggested-fees. The reason to customize the
63
   * EOA would be primarily if the recipientAddress is a contract and requires a certain relayer to submit the fill,
64
   * or if one specific relayer has the necessary token balance to make the fill.
65
   *
66
   * Example: 0x428AB2BA90Eba0a4Be7aF34C9Ac451ab061AC010
67
   */
68
  relayer?: string
69
}
70

71
export interface SuggestedFeesLimits {
72
  /**
73
   * The minimum deposit size in the tokens' units.
74
   *
75
   * Note: USDC has 6 decimals, so this value would be the number of USDC multiplied by 1e6. For WETH, that would be 1e18.
76
   */
77
  minDeposit: string
78

79
  /**
80
   * The maximum deposit size in the tokens' units. Note: The formatting of this number is the same as minDeposit.
81
   */
82
  maxDeposit: string
83

84
  /**
85
   * The max deposit size that can be relayed "instantly" on the destination chain.
86
   *
87
   * Instantly means that there is relayer capital readily available and that a relayer is expected to relay within
88
   * seconds to 5 minutes of the deposit.
89
   */
90
  maxDepositInstant: string
91

92
  /**
93
   * The max deposit size that can be relayed with a "short delay" on the destination chain.
94
   *
95
   * This means that there is relayer capital available on mainnet and that a relayer will immediately begin moving
96
   * that capital over the canonical bridge to relay the deposit. Depending on the chain, the time for this can vary.
97
   *
98
   * Polygon is the worst case where it can take between 20 and 35 minutes for the relayer to receive the funds
99
   * and relay.
100
   *
101
   * Arbitrum is much faster, with a range between 5 and 15 minutes. Note: if the transfer size is greater than this,
102
   * the estimate should be between 2-4 hours for a slow relay to be processed from the mainnet pool.
103
   */
104
  maxDepositShortDelay: string
105

106
  /**
107
   * The recommended deposit size that can be relayed "instantly" on the destination chain.
108
   *
109
   * Instantly means that there is relayer capital readily available and that a relayer is expected to relay
110
   * within seconds to 5 minutes of the deposit. Value is in the smallest unit of the respective token.
111
   */
112
  recommendedDepositInstant: string
113
}
114

115
export interface SuggestedFeesResponse {
116
  /**
117
   * Percentage of the transfer amount that should go to the relayer as a fee in total. The value is inclusive of lpFee.pct.
118
   *
119
   * This is the strongly recommended minimum value to ensure a relayer will perform the transfer under the current
120
   * network conditions.
121
   *
122
   * The value returned in this field is guaranteed to be at least 0.03% in order to meet minimum relayer fee requirements
123
   */
124
  totalRelayFee: PctFee
125

126
  /**
127
   * The percentage of the transfer amount that should go the relayer as a fee to cover relayer capital costs.
128
   */
129
  relayerCapitalFee: PctFee
130

131
  /**
132
   * The percentage of the transfer amount that should go the relayer as a fee to cover relayer gas costs.
133
   */
134
  relayerGasFee: PctFee
135

136
  /**
137
   * The percent of the amount that will go to the LPs as a fee for borrowing their funds.
138
   */
139
  lpFee: PctFee
140

141
  /**
142
   * The quote timestamp that was used to compute the lpFeePct. To pay the quoted LP fee, the user would need to pass
143
   * this quote timestamp to the protocol when sending their bridge transaction.
144
   */
145
  timestamp: string
146

147
  /**
148
   * Is the input amount below the minimum transfer amount.
149
   */
150
  isAmountTooLow: boolean
151

152
  /**
153
   * The block used associated with this quote, used to compute lpFeePct.
154
   */
155
  quoteBlock: string
156

157
  /**
158
   * The contract address of the origin SpokePool.
159
   */
160
  spokePoolAddress: string
161

162
  /**
163
   * The relayer that is suggested to be set as the exclusive relayer for in the depositV3 call for the fastest fill.
164
   *
165
   * Note: when set to "0x0000000000000000000000000000000000000000", relayer exclusivity will be disabled.
166
   * This value is returned in cases where using an exclusive relayer is not recommended.
167
   */
168
  exclusiveRelayer: string
169

170
  /**
171
   * The suggested exclusivity period (in seconds) the exclusive relayer should be given to fill before other relayers
172
   * are allowed to take the fill. Note: when set to "0", relayer exclusivity will be disabled.
173
   *
174
   * This value is returned in cases where using an exclusive relayer is not recommended.
175
   */
176
  exclusivityDeadline: string
177

178
  /**
179
   * The expected time (in seconds) for a fill to be made. Represents 75th percentile of the 7-day rolling average of times (updated daily). Times are dynamic by origin/destination token/chain for a given amount.
180
   */
181
  expectedFillTimeSec: string
182

183
  /**
184
   * The recommended deadline (UNIX timestamp in seconds) for the relayer to fill the deposit. After this destination chain timestamp, the fill will revert on the destination chain.
185
   */
186
  fillDeadline: string
187

188
  limits: SuggestedFeesLimits
189
}
190

191
export interface PctFee {
192
  /**
193
   * Note: 1% is represented as 1e16, 100% is 1e18, 50% is 5e17, etc. These values are in the same format that the contract understands.
194
   *
195
   * Example: 100200000000000
196
   */
197
  pct: string
198

199
  total: string
200
}
201

202
export interface AcrossApiOptions {
203
  apiBaseUrl?: string
204
}
205

206
export class AcrossApi {
207
  constructor(private readonly options: AcrossApiOptions = {}) {}
6✔
208

209
  /**
210
   * Retrieve available routes for transfers
211
   *
212
   * Returns available routes based on specified parameters. If no parameters are provided, available routes on all
213
   * chains are returned.
214
   *
215
   * See https://docs.across.to/reference/api-reference#available-routes
216
   */
217
  async getAvailableRoutes({
218
    originChainId,
219
    originToken,
220
    destinationChainId,
221
    destinationToken,
222
  }: AvailableRoutesRequest): Promise<Route[]> {
223
    const params: Record<string, string> = {}
3✔
224
    if (originChainId) params.originChainId = originChainId
3!
225
    if (originToken) params.originToken = originToken
3!
226
    if (destinationChainId) params.destinationChainId = destinationChainId
3!
227
    if (destinationToken) params.destinationToken = destinationToken
3!
228

229
    return this.fetchApi('/available-routes', params, isValidRoutes)
3✔
230
  }
231

232
  /**
233
   * Retrieve suggested fee quote for a deposit.
234
   *
235
   * Returns suggested fees based inputToken+outputToken, originChainId, destinationChainId, and amount.
236
   * Also includes data used to compute the fees.
237
   *
238
   * * See https://docs.across.to/reference/api-reference#suggested-fees
239
   */
240
  async getSuggestedFees(request: SuggestedFeesRequest): Promise<SuggestedFeesResponse> {
241
    const params: Record<string, string> = {
3✔
242
      token: request.token,
243
      originChainId: request.originChainId.toString(),
244
      destinationChainId: request.destinationChainId.toString(),
245
      amount: request.amount.toString(),
246
    }
247

248
    if (request.recipient) {
3✔
249
      params.recipient = request.recipient
1✔
250
    }
251

252
    // Get the quote from the Across API (see https://docs.across.to/reference/api-reference#suggested-fees)
253
    // Example: https://app.across.to/api/suggested-fees?token=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&originChainId=8453&destinationChainId=137&amount=100000000
254
    //
255
    // TODO: The API documented params don't match with the example above. Ideally I would use 'inputToken' and 'outputToken', but the example above uses 'token'. This will work for current implementation, since we bridge the canonical token, but this will need to be reviewed
256
    //       https://app.across.to/api/suggested-fees?inputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&originChainId=8453&destinationChainId=137&outputToken=0xc2132D05D31c914a87C6611C10748AEb04B58e8F&amount=100000000
257
    const fees = await this.fetchApi('/suggested-fees', params, isValidSuggestedFeesResponse)
3✔
258

259
    return fees
2✔
260
  }
261

262
  protected async fetchApi<T>(
263
    path: string,
264
    params: Record<string, string>,
265
    isValidResponse?: (response: unknown) => response is T
266
  ): Promise<T> {
267
    const baseUrl = this.options.apiBaseUrl || ACROSS_API_URL
6✔
268
    const url = `${baseUrl}${path}?${new URLSearchParams(params).toString()}`
6✔
269

270
    const response = await fetch(url, {
6✔
271
      method: 'GET',
272
      headers: {
273
        'Content-Type': 'application/json',
274
      },
275
    })
276

277
    if (!response.ok) {
6✔
278
      const errorBody = await response.text()
2✔
279
      throw new Error(`HTTP error! Status: ${response.status}, Body: ${errorBody}`)
2✔
280
    }
281

282
    // Validate the response
283
    const json = await response.json()
4✔
284
    if (isValidResponse) {
4!
285
      if (isValidResponse(json)) {
4!
286
        return json
4✔
287
      } else {
UNCOV
288
        throw new Error(
×
289
          `Invalid response for Across API call ${path}. The response doesn't pass the validation. Did the API change?`
290
        )
291
      }
292
    }
293

294
    return json
×
295
  }
296
}
297

298
/**
299
 * Validate the response from the Across API is a SuggestedFeesResponse
300
 *
301
 * @param response - The response from the Across API
302
 * @returns True if the response is a SuggestedFeesResponse, false otherwise
303
 */
304
function isValidSuggestedFeesResponse(response: unknown): response is SuggestedFeesResponse {
305
  return (
2✔
306
    typeof response === 'object' &&
40✔
307
    response !== null &&
308
    'totalRelayFee' in response &&
309
    isValidPctFee(response.totalRelayFee) &&
310
    'relayerCapitalFee' in response &&
311
    isValidPctFee(response.relayerCapitalFee) &&
312
    'relayerGasFee' in response &&
313
    isValidPctFee(response.relayerGasFee) &&
314
    'lpFee' in response &&
315
    isValidPctFee(response.lpFee) &&
316
    'timestamp' in response &&
317
    'isAmountTooLow' in response &&
318
    'quoteBlock' in response &&
319
    'spokePoolAddress' in response &&
320
    'exclusiveRelayer' in response &&
321
    'exclusivityDeadline' in response &&
322
    'expectedFillTimeSec' in response &&
323
    'fillDeadline' in response &&
324
    'limits' in response &&
325
    isValidSuggestedFeeLimits(response.limits)
326
  )
327
}
328

329
function isValidPctFee(pctFee: unknown): pctFee is PctFee {
330
  return typeof pctFee === 'object' && pctFee !== null && 'pct' in pctFee && 'total' in pctFee
8✔
331
}
332

333
function isValidSuggestedFeeLimits(limits: unknown): limits is SuggestedFeesLimits {
334
  return (
2✔
335
    typeof limits === 'object' &&
14✔
336
    limits !== null &&
337
    'minDeposit' in limits &&
338
    'maxDeposit' in limits &&
339
    'maxDepositInstant' in limits &&
340
    'maxDepositShortDelay' in limits &&
341
    'recommendedDepositInstant' in limits
342
  )
343
}
344

345
/**
346
 * Validate the response from the Across API is an AvailableRoutesResponse
347
 *
348
 * @param response - The response from the Across API
349
 * @returns True if the response is an AvailableRoutesResponse, false otherwise
350
 */
351
function isValidRoutes(response: unknown): response is Route[] {
352
  // make sure the response is an array
353
  if (!Array.isArray(response)) {
2!
UNCOV
354
    return false
×
355
  }
356

357
  // make sure each item in the array is an AvailableRoutesResponseItem
358
  return response.every((item) => isValidRoute(item))
2✔
359
}
360

361
function isValidRoute(item: unknown): item is Route {
362
  return (
1✔
363
    typeof item === 'object' &&
8✔
364
    item !== null &&
365
    'originChainId' in item &&
366
    'originToken' in item &&
367
    'destinationChainId' in item &&
368
    'destinationToken' in item &&
369
    'originTokenSymbol' in item &&
370
    'destinationTokenSymbol' in item
371
  )
372
}
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