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

cowprotocol / cow-sdk / #1780

29 May 2025 01:25PM UTC coverage: 76.278% (-1.2%) from 77.485%
#1780

Pull #324

shoom3301
chore: unknown status
Pull Request #324: feat(bridge): track order progress

485 of 689 branches covered (70.39%)

Branch coverage included in aggregate %.

27 of 76 new or added lines in 13 files covered. (35.53%)

2 existing lines in 2 files now uncovered.

1097 of 1385 relevant lines covered (79.21%)

20.11 hits per line

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

74.58
/src/bridging/providers/across/util.ts
1
import { Log } from '@ethersproject/providers'
2
import { OrderKind } from '@cowprotocol/contracts'
3

4
import { BridgeQuoteAmountsAndCosts, BridgeStatus, QuoteBridgeRequest } from '../../types'
5
import { SupportedChainId, TargetChainId } from '../../../chains'
6
import { getBigNumber } from '../../../order-book'
7

8
import { AcrossDepositEvent, CowTradeEvent, DepositStatusResponse, SuggestedFeesResponse } from './types'
9
import { AcrossQuoteResult } from './AcrossBridgeProvider'
10
import { ACROSS_DEPOSIT_EVENT_INTERFACE, COW_TRADE_EVENT_INTERFACE } from './const/interfaces'
11
import { ACROSS_TOKEN_MAPPING, AcrossChainConfig } from './const/tokens'
12
import { ACROSS_DEPOSIT_EVENT_TOPIC, COW_TRADE_EVENT_TOPIC } from './const/misc'
13
import { ACROSS_SPOOK_CONTRACT_ADDRESSES } from './const/contracts'
14
import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS } from '../../../common'
15
import { defaultAbiCoder } from 'ethers/lib/utils'
16

17
const PCT_100_PERCENT = 10n ** 18n
2✔
18

19
/**
20
 * Return the chain configs
21
 *
22
 * This is a temporary implementation. We should use the Across API to get the intermediate tokens (see this.getAvailableRoutes())
23
 */
24
export function getChainConfigs(
25
  sourceChainId: TargetChainId,
26
  targetChainId: TargetChainId,
27
): { sourceChainConfig: AcrossChainConfig; targetChainConfig: AcrossChainConfig } | undefined {
28
  const sourceChainConfig = getChainConfig(sourceChainId)
3✔
29
  const targetChainConfig = getChainConfig(targetChainId)
3✔
30

31
  if (!sourceChainConfig || !targetChainConfig) return undefined
3✔
32

33
  return { sourceChainConfig, targetChainConfig }
1✔
34
}
35

36
function getChainConfig(chainId: number): AcrossChainConfig | undefined {
37
  return ACROSS_TOKEN_MAPPING[chainId as unknown as TargetChainId]
6✔
38
}
39

40
export function getTokenSymbol(tokenAddress: string, chainConfig: AcrossChainConfig): string | undefined {
41
  return Object.keys(chainConfig.tokens).find((key) => chainConfig.tokens[key] === tokenAddress)
2✔
42
}
43

44
export function getTokenAddress(tokenSymbol: string, chainConfig: AcrossChainConfig): string | undefined {
45
  return chainConfig.tokens[tokenSymbol]
2✔
46
}
47

48
export function toBridgeQuoteResult(
49
  request: QuoteBridgeRequest,
50
  slippageBps: number,
51
  suggestedFees: SuggestedFeesResponse,
52
): AcrossQuoteResult {
53
  const { kind } = request
2✔
54

55
  return {
2✔
56
    isSell: kind === OrderKind.SELL,
57
    amountsAndCosts: toAmountsAndCosts(request, slippageBps, suggestedFees),
58
    quoteTimestamp: Number(suggestedFees.timestamp),
59
    expectedFillTimeSeconds: Number(suggestedFees.estimatedFillTimeSec),
60
    fees: {
61
      bridgeFee: BigInt(suggestedFees.relayerCapitalFee.total),
62
      destinationGasFee: BigInt(suggestedFees.relayerGasFee.total),
63
    },
64
    limits: {
65
      minDeposit: BigInt(suggestedFees.limits.minDeposit),
66
      maxDeposit: BigInt(suggestedFees.limits.maxDeposit),
67
    },
68
    suggestedFees,
69
  }
70
}
71

72
function toAmountsAndCosts(
73
  request: QuoteBridgeRequest,
74
  slippageBps: number,
75
  suggestedFees: SuggestedFeesResponse,
76
): BridgeQuoteAmountsAndCosts {
77
  const { amount, sellTokenDecimals, buyTokenDecimals } = request
2✔
78

79
  // Get the amounts before fees
80
  const sellAmountBeforeFeeBig = getBigNumber(amount, sellTokenDecimals)
2✔
81
  const sellAmountBeforeFee = sellAmountBeforeFeeBig.big
2✔
82
  const buyAmountBeforeFee = getBigNumber(sellAmountBeforeFeeBig.num, buyTokenDecimals).big // Sell and buy token should be the same asset for current implementation, but technically they can have different decimals
2✔
83

84
  // Apply the fee to the buy amount (sell amount doesn't change)
85
  const totalRelayerFeePct = BigInt(suggestedFees.totalRelayFee.pct)
2✔
86
  const buyAmountAfterFee = applyPctFee(buyAmountBeforeFee, totalRelayerFeePct)
2✔
87

88
  // Calculate the fee
89
  const feeSellToken = sellAmountBeforeFee - applyPctFee(sellAmountBeforeFee, totalRelayerFeePct)
2✔
90
  const feeBuyToken = buyAmountBeforeFee - buyAmountAfterFee
2✔
91
  // TODO: Do we need to use any of the other fees, or they are included in totalRelayFee? I know 'lpFee' fee is, as stated in the docs, but not sure about the others.
92
  // const relayerCapitalFee = suggestedFees.relayerCapitalFee
93
  // const relayerGasFee = suggestedFees.relayerGasFee
94
  // const lpFee = suggestedFees.lpFee
95

96
  // Apply slippage
97
  const buyAmountAfterSlippage = applyBps(buyAmountAfterFee, slippageBps)
2✔
98

99
  return {
2✔
100
    beforeFee: {
101
      sellAmount: sellAmountBeforeFee,
102
      buyAmount: buyAmountBeforeFee, // Assuming the price is 1:1 (before fee). This is because we are exchanging the same asset
103
    },
104
    afterFee: {
105
      sellAmount: sellAmountBeforeFee, // Sell amount does't change (fee is applied to the buy amount)
106
      buyAmount: buyAmountAfterFee,
107
    },
108
    afterSlippage: {
109
      sellAmount: sellAmountBeforeFee, // Sell amount does't change (slippage is applied to the buy amount)
110
      buyAmount: buyAmountAfterSlippage,
111
    },
112

113
    costs: {
114
      bridgingFee: {
115
        feeBps: pctToBps(totalRelayerFeePct),
116
        amountInSellCurrency: feeSellToken,
117
        amountInBuyCurrency: feeBuyToken,
118
      },
119
    },
120
    slippageBps,
121
  }
122
}
123

124
function bytes32ToAddress(address: string): string {
NEW
125
  return defaultAbiCoder.decode(['address'], address).toString()
×
126
}
127

128
/**
129
 * Assert that a percentage is valid.
130
 */
131
function assertValidPct(pct: bigint): void {
132
  if (pct > PCT_100_PERCENT || pct < 0n) {
15✔
133
    throw new Error('Fee cannot exceed 100% or be negative')
1✔
134
  }
135
}
136

137
/**
138
 * pct represents a percentage.
139
 *
140
 * Note: 1% is represented as 1e16, 100% is 1e18, 50% is 5e17, etc. These values are in the same format that the contract understands.
141
 *
142
 * Bps is a percentage in basis points (1/100th of a percent). For example, 1% is 100 bps.
143
 *
144
 * @param pct - The percentage to convert to bps
145
 * @returns The percentage in bps
146
 * @throws If the percentage is greater than 100% or less than 0%
147
 */
148
export function pctToBps(pct: bigint): number {
149
  assertValidPct(pct)
6✔
150

151
  return Number((pct * 10_000n) / PCT_100_PERCENT)
6✔
152
}
153

154
/**
155
 * Apply a percentage fee to an amount.
156
 *
157
 * @param amount - The amount to apply the fee to
158
 * @param pct - The percentage fee to apply
159
 * @throws If the percentage fee is greater than 100% or less than 0%
160
 * @returns The amount after the fee has been applied
161
 */
162
export function applyPctFee(amount: bigint, pct: bigint): bigint {
163
  assertValidPct(pct)
9✔
164

165
  // Compute amount after fee: amount * (1 - pct / 1e18)
166
  return (amount * (PCT_100_PERCENT - pct)) / PCT_100_PERCENT
8✔
167
}
168

169
export function applyBps(amount: bigint, bps: number): bigint {
170
  return (amount * BigInt(10_000 - bps)) / 10_000n
2✔
171
}
172

173
const AcrossStatusToBridgeStatus: Record<DepositStatusResponse['status'], BridgeStatus> = {
2✔
174
  filled: BridgeStatus.EXECUTED,
175
  slowFillRequested: BridgeStatus.EXECUTED,
176
  pending: BridgeStatus.IN_PROGRESS,
177
  expired: BridgeStatus.EXPIRED,
178
  refunded: BridgeStatus.REFUND,
179
}
180

181
export function mapAcrossStatusToBridgeStatus(status: DepositStatusResponse['status']): BridgeStatus {
182
  return AcrossStatusToBridgeStatus[status]
6✔
183
}
184

185
export function getAcrossDepositEvents(chainId: SupportedChainId, logs: Log[]): AcrossDepositEvent[] {
186
  const spookContractAddress = ACROSS_SPOOK_CONTRACT_ADDRESSES[chainId]?.toLowerCase()
2✔
187

188
  if (!spookContractAddress) {
2✔
189
    return []
1✔
190
  }
191

192
  // Get accross deposit events
193
  const depositEvents = logs.filter((log) => {
1✔
NEW
194
    return log.address.toLocaleLowerCase() === spookContractAddress && log.topics[0] === ACROSS_DEPOSIT_EVENT_TOPIC
×
195
  })
196

197
  // Parse logs
198
  return depositEvents.map((event) => {
1✔
199
    const {
200
      inputToken,
201
      outputToken,
202
      inputAmount,
203
      outputAmount,
204
      destinationChainId,
205
      depositId,
206
      quoteTimestamp,
207
      fillDeadline,
208
      exclusivityDeadline,
209
      depositor,
210
      recipient,
211
      exclusiveRelayer,
212
      message,
NEW
213
    } = ACROSS_DEPOSIT_EVENT_INTERFACE.parseLog(event).args as unknown as AcrossDepositEvent
×
NEW
214
    return {
×
215
      inputToken: bytes32ToAddress(inputToken),
216
      outputToken: bytes32ToAddress(outputToken),
217
      inputAmount,
218
      outputAmount,
219
      destinationChainId,
220
      depositId,
221
      quoteTimestamp,
222
      fillDeadline,
223
      exclusivityDeadline,
224
      depositor: bytes32ToAddress(depositor),
225
      recipient: bytes32ToAddress(recipient),
226
      exclusiveRelayer,
227
      message,
228
    }
229
  })
230
}
231

232
export function getCowTradeEvents(chainId: SupportedChainId, logs: Log[]): CowTradeEvent[] {
NEW
233
  const cowTradeEvents = logs.filter((log) => {
×
NEW
234
    return (
×
235
      log.address.toLowerCase() === COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId].toLowerCase() &&
×
236
      log.topics[0] === COW_TRADE_EVENT_TOPIC
237
    )
238
  })
239

NEW
240
  return cowTradeEvents.map((event) => {
×
NEW
241
    const result = COW_TRADE_EVENT_INTERFACE.parseLog(event).args as unknown as CowTradeEvent
×
242

NEW
243
    const { owner, sellToken, buyToken, sellAmount, buyAmount, feeAmount } = result
×
244
    // TODO: idk, ethers cannot parse orderUid because it's bytes and ethers tries to cast it as a number
NEW
245
    const orderUid = '0x' + event.data.slice(450, 450 + 112)
×
246

NEW
247
    return {
×
248
      owner,
249
      sellToken,
250
      buyToken,
251
      sellAmount,
252
      buyAmount,
253
      feeAmount,
254
      orderUid,
255
    }
256
  })
257
}
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