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

cowprotocol / cow-sdk / #1714

16 May 2025 06:06PM UTC coverage: 77.476% (+0.03%) from 77.449%
#1714

Pull #314

anxolin
chore: apply nit
Pull Request #314: Slippage calculation rounding

470 of 662 branches covered (71.0%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

4 existing lines in 2 files now uncovered.

1071 of 1327 relevant lines covered (80.71%)

20.19 hits per line

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

90.14
/src/bridging/BridgingSdk/getQuoteWithBridge.ts
1
import { latest } from '@cowprotocol/app-data'
2
import { areHooksEqual, getHookMockForCostEstimation } from '../../hooks/utils'
3
import {
4
  AppDataInfo,
5
  mergeAppDataDoc,
6
  postSwapOrderFromQuote,
7
  QuoteResults,
8
  SwapAdvancedSettings,
9
  TradeParameters,
10
  TradingSdk,
11
  WithPartialTraderParams,
12
} from '../../trading'
13
import {
14
  BridgeHook,
15
  BridgeProvider,
16
  BridgeQuoteAndPost,
17
  BridgeQuoteResult,
18
  BridgeQuoteResults,
19
  GetErc20Decimals,
20
  QuoteBridgeRequest,
21
  QuoteBridgeRequestWithoutAmount,
22
} from '../types'
23
import { Signer } from '@ethersproject/abstract-signer'
24
import { getSigner } from '../../common/utils/wallet'
25
import { log } from '../../common/utils/log'
26
import { OrderKind } from '../../order-book'
27
import { jsonWithBigintReplacer } from '../../common/utils/serialize'
28
import { parseUnits } from '@ethersproject/units'
29
import { SignerLike } from '../../common'
30
import { QuoteResultsWithSigner } from '../../trading/getQuote'
31
import { BridgeProviderQuoteError } from '../errors'
32
import { getTradeParametersAfterQuote } from '../../trading/utils/misc'
33

34
type GetQuoteWithBridgeParams<T extends BridgeQuoteResult> = {
35
  /**
36
   * Overall request for the swap and the bridge.
37
   */
38
  swapAndBridgeRequest: QuoteBridgeRequest
39

40
  /**
41
   * Advanced settings for the swap.
42
   */
43
  advancedSettings?: SwapAdvancedSettings
44

45
  /**
46
   * Provider for the bridge.
47
   */
48
  provider: BridgeProvider<T>
49

50
  /**
51
   * Trading SDK.
52
   */
53
  tradingSdk: TradingSdk
54

55
  /**
56
   * Function to get the decimals of the ERC20 tokens.
57
   */
58
  getErc20Decimals: GetErc20Decimals
59
  /**
60
   * For quote fetching we have to sign bridging hooks.
61
   * But we won't do that using users wallet and will use some static PK.
62
   */
63
  bridgeHookSigner?: SignerLike
64
}
65

66
export async function getQuoteWithBridge<T extends BridgeQuoteResult>(
67
  params: GetQuoteWithBridgeParams<T>,
68
): Promise<BridgeQuoteAndPost> {
69
  const { provider, swapAndBridgeRequest, advancedSettings, getErc20Decimals, tradingSdk, bridgeHookSigner } = params
4✔
70
  const {
71
    kind,
72
    sellTokenChainId,
73
    sellTokenAddress,
74
    buyTokenChainId,
75
    buyTokenAddress,
76
    amount,
77
    signer: signerLike,
78
    ...rest
79
  } = swapAndBridgeRequest
4✔
80

81
  const signer = getSigner(signerLike)
4✔
82

83
  if (kind !== OrderKind.SELL) {
4!
84
    throw new Error('Bridging only support SELL orders')
×
85
  }
86

87
  log(
4✔
88
    `Cross-chain ${kind} ${amount} ${sellTokenAddress} (source chain ${sellTokenChainId}) for ${buyTokenAddress} (target chain ${buyTokenChainId})`,
89
  )
90

91
  // Get the mocked hook (for estimating the additional swap costs)
92
  const bridgeRequestWithoutAmount = await getBaseBridgeQuoteRequest({
4✔
93
    swapAndBridgeRequest: swapAndBridgeRequest,
94
    provider,
95
    getErc20Decimals,
96
  })
97

98
  // Get the hook mock for cost estimation
99
  const gasLimit = provider.getGasLimitEstimationForHook(bridgeRequestWithoutAmount)
4✔
100
  const mockedHook = getHookMockForCostEstimation(gasLimit)
4✔
101
  log(`Using mocked hook for swap gas estimation: ${JSON.stringify(mockedHook)}`)
4✔
102

103
  const { sellTokenAddress: intermediateToken, sellTokenDecimals: intermediaryTokenDecimals } =
104
    bridgeRequestWithoutAmount
4✔
105

106
  // Estimate the expected amount of intermediate tokens received in CoW Protocol's swap
107
  const swapParams: WithPartialTraderParams<TradeParameters> = {
4✔
108
    ...rest,
109
    kind,
110
    chainId: sellTokenChainId,
111
    sellToken: sellTokenAddress,
112
    buyToken: intermediateToken,
113
    buyTokenDecimals: intermediaryTokenDecimals,
114
    amount: amount.toString(),
115
    signer,
116
  }
117
  const { signer: _, ...swapParamsToLog } = swapParams
4✔
118

119
  log(
4✔
120
    `Getting a quote for the swap (sell token to buy intermediate token). Delegate to trading SDK with params: ${JSON.stringify(
121
      swapParamsToLog,
122
      jsonWithBigintReplacer,
123
    )}`,
124
  )
125

126
  const advancedSettingsHooks = advancedSettings?.appData?.metadata?.hooks
4✔
127

128
  const { result: swapResult, orderBookApi } = await tradingSdk.getQuoteResults(swapParams, {
4✔
129
    ...advancedSettings,
130
    appData: {
131
      ...advancedSettings?.appData,
132
      metadata: {
133
        hooks: {
134
          pre: advancedSettingsHooks?.pre,
135
          post: [...(advancedSettingsHooks?.post || []), mockedHook],
8✔
136
        },
137
      },
138
    },
139
  })
140

141
  const intermediateTokenAmount = swapResult.amountsAndCosts.afterSlippage.buyAmount // Estimated, as it will likely have surplus
4✔
142
  log(
4✔
143
    `Expected to receive ${intermediateTokenAmount} of the intermediate token (${parseUnits(
144
      intermediateTokenAmount.toString(),
145
      intermediaryTokenDecimals,
146
    ).toString()})`,
147
  )
148

149
  // Get the bridge result
150
  async function signHooksAndSetSwapResult(
151
    signer: Signer,
152
    advancedSettings?: SwapAdvancedSettings,
153
  ): Promise<{ swapResult: QuoteResults; bridgeResult: BridgeQuoteResults }> {
154
    const appDataOverride = advancedSettings?.appData
7✔
155
    const receiverOverride = advancedSettings?.quoteRequest?.receiver
7✔
156

157
    const {
158
      bridgeHook,
159
      appDataInfo: { doc: appData, fullAppData, appDataKeccak256 },
160
      bridgeResult,
161
    } = await getBridgeResult({
7✔
162
      swapAndBridgeRequest: { ...swapAndBridgeRequest, kind: OrderKind.SELL },
163
      swapResult,
164
      bridgeRequestWithoutAmount: {
165
        ...bridgeRequestWithoutAmount,
166
        receiver: receiverOverride || bridgeRequestWithoutAmount.receiver,
12✔
167
      },
168
      provider,
169
      intermediateTokenAmount,
170
      signer,
171
      mockedHook,
172
      appDataOverride,
173
    })
174
    log(`Bridge hook for swap: ${JSON.stringify(bridgeHook)}`)
7✔
175

176
    // Update the receiver and appData (both were mocked before we had the bridge hook)
177
    swapResult.tradeParameters.receiver = bridgeHook.recipient
7✔
178

179
    log(`App data for swap: appDataKeccak256=${appDataKeccak256}, fullAppData="${fullAppData}"`)
7✔
180
    swapResult.appDataInfo = {
7✔
181
      fullAppData,
182
      appDataKeccak256,
183
      doc: appData,
184
    }
185

186
    return {
7✔
187
      bridgeResult,
188
      swapResult: {
189
        ...swapResult,
190
        tradeParameters: {
191
          ...swapResult.tradeParameters,
192
          receiver: bridgeHook.recipient,
193
        },
194
      },
195
    }
196
  }
197

198
  // Sign the hooks with bridgeHookSigner if provided
199
  const result = await signHooksAndSetSwapResult(bridgeHookSigner ? getSigner(bridgeHookSigner) : signer)
4!
200

201
  return {
4✔
202
    swap: result.swapResult,
203
    bridge: result.bridgeResult,
204
    async postSwapOrderFromQuote(advancedSettings?: SwapAdvancedSettings) {
205
      // Sign the hooks with the real signer
206
      const { swapResult } = await signHooksAndSetSwapResult(signer, advancedSettings)
3✔
207

208
      const quoteResults: QuoteResultsWithSigner = {
3✔
209
        result: {
210
          ...swapResult,
211
          tradeParameters: getTradeParametersAfterQuote({
212
            quoteParameters: swapResult.tradeParameters,
213
            sellToken: sellTokenAddress,
214
          }),
215
          signer,
216
        },
217
        orderBookApi,
218
      }
219

220
      return postSwapOrderFromQuote(quoteResults, {
3✔
221
        ...advancedSettings,
222
        appData: swapResult.appDataInfo.doc,
223
        quoteRequest: {
224
          ...advancedSettings?.quoteRequest,
225
          // Changing receiver back to account proxy
226
          receiver: swapResult.tradeParameters.receiver,
227
        },
228
      })
229
    },
230
  }
231
}
232

233
/**
234
 * Ge the base params (without the amount) for the bridge quote request
235
 */
236
async function getBaseBridgeQuoteRequest<T extends BridgeQuoteResult>(params: {
237
  swapAndBridgeRequest: QuoteBridgeRequest
238
  provider: BridgeProvider<T>
239
  getErc20Decimals: GetErc20Decimals
240
}): Promise<QuoteBridgeRequestWithoutAmount> {
241
  const { provider, getErc20Decimals, swapAndBridgeRequest: quoteBridgeRequest } = params
4✔
242
  const { sellTokenChainId } = quoteBridgeRequest
4✔
243

244
  const intermediateTokens = await provider.getIntermediateTokens(quoteBridgeRequest)
4✔
245

246
  if (intermediateTokens.length === 0) {
4!
UNCOV
247
    throw new BridgeProviderQuoteError('No path found (not intermediate token for bridging)', {})
×
248
  }
249

250
  // We just pick the first intermediate token for now
251
  const intermediateTokenAddress = intermediateTokens[0]
4✔
252
  log(`Using ${intermediateTokenAddress} as intermediate tokens`)
4✔
253

254
  // Get intermediate token decimals
255
  const intermediaryTokenDecimals = await getErc20Decimals(sellTokenChainId, intermediateTokenAddress)
4✔
256

257
  // Get the gas limit estimation for the hook
258
  return {
4✔
259
    ...quoteBridgeRequest,
260
    sellTokenAddress: intermediateTokenAddress,
261
    sellTokenDecimals: intermediaryTokenDecimals,
262
  }
263
}
264

265
interface GetBridgeResultResult {
266
  bridgeResult: BridgeQuoteResults
267
  bridgeHook: BridgeHook
268
  appDataInfo: AppDataInfo
269
}
270

271
interface BridgeResultContext {
272
  swapAndBridgeRequest: QuoteBridgeRequest
273
  swapResult: QuoteResults
274
  intermediateTokenAmount: bigint
275
  bridgeRequestWithoutAmount: QuoteBridgeRequestWithoutAmount
276
  provider: BridgeProvider<BridgeQuoteResult>
277
  signer: Signer
278
  mockedHook: latest.CoWHook
279
  appDataOverride?: SwapAdvancedSettings['appData']
280
}
281

282
async function getBridgeResult(context: BridgeResultContext): Promise<GetBridgeResultResult> {
283
  const {
284
    swapResult,
285
    bridgeRequestWithoutAmount,
286
    provider,
287
    intermediateTokenAmount,
288
    signer,
289
    mockedHook,
290
    appDataOverride,
291
  } = context
7✔
292

293
  const bridgeRequest: QuoteBridgeRequest = {
7✔
294
    ...bridgeRequestWithoutAmount,
295
    amount: intermediateTokenAmount,
296
  }
297

298
  // Get the quote for the bridging of the intermediate token to the final token
299
  const bridgingQuote = await provider.getQuote(bridgeRequest)
7✔
300

301
  // Get the bridging call
302
  const unsignedBridgeCall = await provider.getUnsignedBridgeCall(bridgeRequest, bridgingQuote)
7✔
303

304
  // Get the pre-authorized hook
305
  const bridgeHook = await provider.getSignedHook(bridgeRequest.sellTokenChainId, unsignedBridgeCall, signer)
7✔
306

307
  const swapAppData = await mergeAppDataDoc(swapResult.appDataInfo.doc, appDataOverride || {})
7✔
308

309
  const swapResultHooks = swapAppData.doc.metadata.hooks
7✔
310
  const postHooks = swapResultHooks?.post || []
7!
311

312
  const isBridgeHookAlreadyPresent = postHooks.some((hook) => areHooksEqual(hook, bridgeHook.postHook))
7✔
313

314
  const appDataInfo = await mergeAppDataDoc(swapAppData.doc, {
7✔
315
    metadata: {
316
      hooks: {
317
        pre: swapResultHooks?.pre,
318
        // Remove the mocked hook from the post hooks after receiving quote
319
        post: [...(swapResultHooks?.post || []), ...(isBridgeHookAlreadyPresent ? [] : [bridgeHook.postHook])].filter(
14!
320
          (hook) => !areHooksEqual(hook, mockedHook),
10✔
321
        ),
322
      },
323
    },
324
  })
325

326
  // Prepare the bridge result
327
  const bridgeResult: BridgeQuoteResults = {
7✔
328
    providerInfo: provider.info,
329
    tradeParameters: bridgeRequest, // Just the bridge (not the swap & bridge)
330
    bridgeCallDetails: {
331
      unsignedBridgeCall: unsignedBridgeCall,
332
      preAuthorizedBridgingHook: bridgeHook,
333
    },
334
    isSell: bridgingQuote.isSell,
335
    expectedFillTimeSeconds: bridgingQuote.expectedFillTimeSeconds,
336
    fees: bridgingQuote.fees,
337
    limits: bridgingQuote.limits,
338
    quoteTimestamp: bridgingQuote.quoteTimestamp,
339
    amountsAndCosts: bridgingQuote.amountsAndCosts,
340
  }
341

342
  return { bridgeResult, bridgeHook, appDataInfo }
7✔
343
}
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