Skip to content

Commit 03e6842

Browse files
cowdanalfetopito
andauthored
feat: add Gnosis bridge to BungeeApi (#418)
* feat: add Gnosis bridget to BungeeApi * chore: fix Gnosis bridge name and add specs * chore: add Bungee transaction spec * chore: add Gnosis bridge full transaction test * chore: add getWallet test, update `BungeeTxDataBytesIndices` * chore: update tests * chore: address coderabbit CR comments * chore: remove console.log * chore: update version * chore: improve `getWallet` test coverage * feat: method `getBuyTokens` of `BridgingSdk` returns if route is available * chore: bump version * feat: increase gas for hook if mainnet to gnosis bridge * chore: bump version * chore: bump version * chore: update `getGasLimitEstimationForHook` documentation * chore: address coderabbit comments * chore: address PR comments * Update package.json Co-authored-by: Leandro <[email protected]> * chore: update description --------- Co-authored-by: Leandro <[email protected]>
1 parent 747e2ad commit 03e6842

20 files changed

+827
-224
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cowprotocol/cow-sdk",
3-
"version": "6.2.0",
3+
"version": "6.3.0-RC.0",
44
"license": "(MIT OR Apache-2.0)",
55
"files": [
66
"/dist"
@@ -24,6 +24,7 @@
2424
"test": "jest",
2525
"test:coverage": "jest --coverage --json --outputFile=jest.results.json && cat ./coverage/lcov.info | coveralls",
2626
"test:coverage:html": "jest --silent=false --coverage --coverageReporters html",
27+
"test:bungeeGnosisBridge": "yarn test --testPathPattern=BungeeGnosisBridge",
2728
"codegen": "npm run swagger:codegen && npm run typechain:codegen",
2829
"prepare": "npm run build",
2930
"prepublishOnly": "npm test && npm run lint",

src/bridging/BridgingSdk/BridgingSdk.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {
66
BuyTokensParams,
77
CrossChainOrder,
88
CrossChainQuoteAndPost,
9+
GetProviderBuyTokens,
910
QuoteBridgeRequest,
1011
} from '../types'
11-
import { ALL_SUPPORTED_CHAINS, CowEnv, TokenInfo, enableLogging } from '../../common'
12+
import { ALL_SUPPORTED_CHAINS, CowEnv, enableLogging } from '../../common'
1213
import { ChainInfo, SupportedChainId } from '../../chains'
1314
import { getQuoteWithoutBridge } from './getQuoteWithoutBridge'
1415
import { getQuoteWithBridge } from './getQuoteWithBridge'
@@ -120,7 +121,7 @@ export class BridgingSdk {
120121
121122
* @param params
122123
*/
123-
async getBuyTokens(params: BuyTokensParams): Promise<TokenInfo[]> {
124+
async getBuyTokens(params: BuyTokensParams): Promise<GetProviderBuyTokens> {
124125
return this.provider.getBuyTokens(params)
125126
}
126127

src/bridging/BridgingSdk/mock/bridgeRequestMocks.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SupportedChainId } from '../../../chains'
22
import { parseUnits } from '@ethersproject/units'
33

4-
import { BridgeCallDetails, BridgeQuoteResult, QuoteBridgeRequest } from '../../types'
4+
import { BridgeCallDetails, BridgeQuoteResult, BuyTokensParams, QuoteBridgeRequest } from '../../types'
55
import { OrderKind } from '@cowprotocol/contracts'
66
import {
77
BuyTokenDestination,
@@ -234,3 +234,8 @@ export const orderTypedData: OrderTypedData = {
234234
types: {},
235235
message: orderToSign,
236236
}
237+
238+
export const buyTokensParams: BuyTokensParams = {
239+
sellChainId: SupportedChainId.MAINNET,
240+
buyChainId: SupportedChainId.GNOSIS_CHAIN,
241+
}

src/bridging/PROVIDER_README.md

Lines changed: 56 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This guide explains how to develop a new bridge provider for the `CoW Protocol` `BridgingSDK`.
44
Bridge providers integrate third-party bridging protocols into the `CoW ecosystem`, enabling cross-chain token swaps.
55

6-
>You can see existing providers code in [`src/bridging/providers`](./providers) directory.
6+
> You can see existing providers code in [`src/bridging/providers`](./providers) directory.
77
88
## Table of Contents
99

@@ -92,37 +92,31 @@ interface BridgeProvider<Q extends BridgeQuoteResult> {
9292

9393
### Required Methods
9494

95-
| Method | Purpose | Required |
96-
|--------|---------|----------|
97-
| `getNetworks()` | Get supported destination chains ||
98-
| `getBuyTokens()` | Get supported tokens for a chain ||
99-
| `getIntermediateTokens()` | Get bridgeable tokens on source chain ||
100-
| `getQuote()` | Generate bridge quote ||
101-
| `getUnsignedBridgeCall()` | Create unsigned bridge transaction ||
102-
| `getGasLimitEstimationForHook()` | Estimate gas for hook execution ||
103-
| `getSignedHook()` | Generate pre-authorized hook ||
104-
| `getStatus()` | Check bridge transaction status ||
105-
| `getBridgingParams()` | Extract bridge params from settlement ||
106-
| `getExplorerUrl()` | Get bridge explorer URL ||
107-
| `decodeBridgeHook()` | Decode hook data | Optional* |
108-
| `getCancelBridgingTx()` | Create cancel transaction | Optional* |
109-
| `getRefundBridgingTx()` | Create refund transaction | Optional* |
110-
111-
*Can throw "Not implemented" error if unsupported
95+
| Method | Purpose | Required |
96+
| -------------------------------- | ------------------------------------- | ---------- |
97+
| `getNetworks()` | Get supported destination chains | |
98+
| `getBuyTokens()` | Get supported tokens for a chain | |
99+
| `getIntermediateTokens()` | Get bridgeable tokens on source chain | |
100+
| `getQuote()` | Generate bridge quote | |
101+
| `getUnsignedBridgeCall()` | Create unsigned bridge transaction | |
102+
| `getGasLimitEstimationForHook()` | Estimate gas for hook execution | |
103+
| `getSignedHook()` | Generate pre-authorized hook | |
104+
| `getStatus()` | Check bridge transaction status | |
105+
| `getBridgingParams()` | Extract bridge params from settlement | |
106+
| `getExplorerUrl()` | Get bridge explorer URL | |
107+
| `decodeBridgeHook()` | Decode hook data | Optional\* |
108+
| `getCancelBridgingTx()` | Create cancel transaction | Optional\* |
109+
| `getRefundBridgingTx()` | Create refund transaction | Optional\* |
110+
111+
\*Can throw "Not implemented" error if unsupported
112112

113113
## Implementation Guide
114114

115115
### Step 1: Basic Provider Class
116116

117117
```typescript
118-
import {
119-
BridgeProvider,
120-
BridgeProviderInfo,
121-
BridgeQuoteResult
122-
} from '../../types'
123-
import {
124-
SupportedChainId, mainnet, polygon, arbitrumOne, optimism, ChainInfo
125-
} from '../../../chains'
118+
import { BridgeProvider, BridgeProviderInfo, BridgeQuoteResult } from '../../types'
119+
import { SupportedChainId, mainnet, polygon, arbitrumOne, optimism, ChainInfo } from '../../../chains'
126120
import { CowShedSdk } from '../../../cow-shed'
127121
import { JsonRpcProvider } from '@ethersproject/providers'
128122
import { HOOK_DAPP_BRIDGE_PROVIDER_PREFIX } from '../../const'
@@ -134,7 +128,7 @@ export const YOUR_BRIDGE_SUPPORTED_NETWORKS = [
134128
mainnet,
135129
polygon,
136130
arbitrumOne,
137-
optimism
131+
optimism,
138132
// Add your supported chains
139133
// If there are no needed chains, add them to `src/chains`
140134
]
@@ -187,7 +181,7 @@ export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult>
187181
sellTokenAddress: params.sellTokenAddress,
188182
})
189183

190-
return tokens.map(token => ({
184+
return tokens.map((token) => ({
191185
chainId: token.chainId,
192186
address: token.address,
193187
name: token.name,
@@ -204,14 +198,11 @@ export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult>
204198
async getIntermediateTokens(request: QuoteBridgeRequest): Promise<TokenInfo[]> {
205199
// Validate order kind
206200
if (request.kind !== OrderKind.SELL) {
207-
throw new BridgeProviderQuoteError(
208-
BridgeQuoteErrors.ONLY_SELL_ORDER_SUPPORTED,
209-
{kind: request.kind}
210-
)
201+
throw new BridgeProviderQuoteError(BridgeQuoteErrors.ONLY_SELL_ORDER_SUPPORTED, { kind: request.kind })
211202
}
212203

213204
// Get tokens on source chain that can bridge to target token
214-
const {sellTokenChainId, buyTokenChainId, buyTokenAddress} = request
205+
const { sellTokenChainId, buyTokenChainId, buyTokenAddress } = request
215206

216207
const intermediateTokens = await this.api.getIntermediateTokens({
217208
fromChainId: sellTokenChainId,
@@ -238,15 +229,12 @@ export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult>
238229
amount,
239230
receiver,
240231
account,
241-
owner
232+
owner,
242233
} = request
243234

244235
// Get CoW Shed account for the owner
245236
const ownerAddress = owner ?? account
246-
const cowShedAccount = this.cowShedSdk.getCowShedAccount(
247-
sellTokenChainId,
248-
ownerAddress
249-
)
237+
const cowShedAccount = this.cowShedSdk.getCowShedAccount(sellTokenChainId, ownerAddress)
250238

251239
// Request quote from external bridge
252240
const externalQuote = await this.api.getQuote({
@@ -266,23 +254,14 @@ export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult>
266254
return this.convertToBridgeQuote(externalQuote, request)
267255
}
268256

269-
private async validateQuote(
270-
externalQuote: YourBridgeQuote,
271-
request: QuoteBridgeRequest
272-
): Promise<void> {
257+
private async validateQuote(externalQuote: YourBridgeQuote, request: QuoteBridgeRequest): Promise<void> {
273258
// Add validation logic
274259
if (!externalQuote.isValid) {
275-
throw new BridgeProviderQuoteError(
276-
BridgeQuoteErrors.NO_ROUTES_FOUND,
277-
{quote: externalQuote}
278-
)
260+
throw new BridgeProviderQuoteError(BridgeQuoteErrors.NO_ROUTES_FOUND, { quote: externalQuote })
279261
}
280262
}
281263

282-
private convertToBridgeQuote(
283-
externalQuote: YourBridgeQuote,
284-
request: QuoteBridgeRequest
285-
): YourBridgeQuoteResult {
264+
private convertToBridgeQuote(externalQuote: YourBridgeQuote, request: QuoteBridgeRequest): YourBridgeQuoteResult {
286265
return {
287266
isSell: true,
288267
amountsAndCosts: {
@@ -328,17 +307,15 @@ export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult>
328307

329308
> It very depends on your smart-contract implementation.
330309
> Basically, the smart-contract should:
310+
>
331311
> 1. Approve sell token spending from `CoW Shed proxy`
332312
> 2. Transfer funds from `CoW Shed proxy` to your deposit smart-contract
333313
>
334314
> See `createBungeeDepositCall` as an example
335315
336316
```typescript
337317
export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult> {
338-
async getUnsignedBridgeCall(
339-
request: QuoteBridgeRequest,
340-
quote: YourBridgeQuoteResult
341-
): Promise<EvmCall> {
318+
async getUnsignedBridgeCall(request: QuoteBridgeRequest, quote: YourBridgeQuoteResult): Promise<EvmCall> {
342319
// Create the bridge transaction that will be executed by CoW Shed
343320
return createYourBridgeCall({
344321
request,
@@ -356,7 +333,10 @@ import { EvmCall } from '../../../common'
356333
import { YourBridgeQuoteResult } from '../types'
357334
import { YOUR_BRIDGE_CONTRACTS, YOUR_BRIDGE_ABI } from '../const'
358335

359-
export async function createYourBridgeCall({ request, quote }: {
336+
export async function createYourBridgeCall({
337+
request,
338+
quote,
339+
}: {
360340
request: QuoteBridgeRequest
361341
quote: YourBridgeQuoteResult
362342
}): Promise<EvmCall> {
@@ -401,13 +381,14 @@ import { getGasLimitEstimationForHook } from '../utils/getGasLimitEstimationForH
401381

402382
export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult> {
403383
async getGasLimitEstimationForHook(
404-
request: Omit<QuoteBridgeRequest, 'amount'>
384+
request: Omit<QuoteBridgeRequest, 'amount'> & { extraGas?: number },
405385
): Promise<number> {
406386
// Use utility function or implement custom gas estimation
407387
return getGasLimitEstimationForHook(
408388
this.cowShedSdk,
409389
request as QuoteBridgeRequest, // cast needed due to omit
410-
this.getRpcProvider(request.sellTokenChainId)
390+
this.getRpcProvider(request.sellTokenChainId),
391+
request.extraGas, // to add extra gas to the hook.
411392
)
412393
}
413394

@@ -421,21 +402,23 @@ export class YourBridgeProvider implements BridgeProvider<YourBridgeQuoteResult>
421402
): Promise<BridgeHook> {
422403
// Sign the multicall using CoW Shed SDK
423404
const { signedMulticall, cowShedAccount, gasLimit } = await this.cowShedSdk.signCalls({
424-
calls: [{
425-
target: unsignedCall.to,
426-
value: unsignedCall.value,
427-
callData: unsignedCall.data,
428-
allowFailure: false,
429-
isDelegateCall: true,
430-
}],
405+
calls: [
406+
{
407+
target: unsignedCall.to,
408+
value: unsignedCall.value,
409+
callData: unsignedCall.data,
410+
allowFailure: false,
411+
isDelegateCall: true,
412+
},
413+
],
431414
chainId,
432415
signer,
433416
gasLimit: BigInt(hookGasLimit),
434417
deadline,
435418
nonce: bridgeHookNonce,
436419
})
437420

438-
const {to, data} = signedMulticall
421+
const { to, data } = signedMulticall
439422
return {
440423
postHook: {
441424
target: to,
@@ -647,9 +630,7 @@ describe('YourBridgeProvider', () => {
647630
// ... rest of request
648631
}
649632

650-
await expect(provider.getQuote(request))
651-
.rejects
652-
.toThrow('Only sell orders are supported')
633+
await expect(provider.getQuote(request)).rejects.toThrow('Only sell orders are supported')
653634
})
654635
})
655636

@@ -663,23 +644,17 @@ describe('YourBridgeProvider', () => {
663644

664645
```typescript
665646
// Use specific error types
666-
throw new BridgeProviderQuoteError(
667-
BridgeQuoteErrors.INSUFFICIENT_LIQUIDITY,
668-
{
669-
availableLiquidity: '1000',
670-
requestedAmount: '2000'
671-
}
672-
)
647+
throw new BridgeProviderQuoteError(BridgeQuoteErrors.INSUFFICIENT_LIQUIDITY, {
648+
availableLiquidity: '1000',
649+
requestedAmount: '2000',
650+
})
673651

674652
// Handle API failures gracefully
675653
try {
676654
return await this.api.getQuote(params)
677655
} catch (error) {
678656
if (error.status === 404) {
679-
throw new BridgeProviderQuoteError(
680-
BridgeQuoteErrors.NO_ROUTES_FOUND,
681-
{ params }
682-
)
657+
throw new BridgeProviderQuoteError(BridgeQuoteErrors.NO_ROUTES_FOUND, { params })
683658
}
684659
throw error
685660
}
@@ -768,6 +743,7 @@ const bridgingSdk = new BridgingSdk({
768743
### Common Issues
769744

770745
1. **Gas Estimation Failures**
746+
771747
```typescript
772748
import { getGasLimitEstimationForHook } from '../utils/getGasLimitEstimationForHook'
773749

@@ -783,6 +759,7 @@ const bridgingSdk = new BridgingSdk({
783759
```
784760

785761
2. **API Rate Limiting**
762+
786763
```typescript
787764
// Implement retry logic with exponential backoff
788765
private async withRetry<T>(operation: () => Promise<T>, maxRetries = 3): Promise<T> {
@@ -816,4 +793,3 @@ const bridgingSdk = new BridgingSdk({
816793
- Test with small amounts first
817794
- Validate against existing providers like `MockBridgeProvider`
818795
- Monitor transaction status on both chains
819-

src/bridging/const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { RAW_FILES_PATH } from '../common/consts/path'
33
export const RAW_PROVIDERS_FILES_PATH = `${RAW_FILES_PATH}/src/bridging/providers`
44
// Based on https://dashboard.tenderly.co/shoom/project/simulator/a5e29dac-d0f2-407f-9e3d-d1b916da595b
55
export const DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION = 240_000
6+
export const DEFAULT_EXTRA_GAS_FOR_HOOK_ESTIMATION = 100_000
67
// Based on https://dashboard.tenderly.co/shoom/project/tx/0x2971aee9aa7237d24b254da8ccd4345ff77410c8829c8e825e9a02cb2cece5e6/gas-usage
78
export const COW_SHED_PROXY_CREATION_GAS = 360_000
89
export const HOOK_DAPP_BRIDGE_PROVIDER_PREFIX = 'cow-sdk://bridging/providers'

0 commit comments

Comments
 (0)