Skip to content

Commit ae56511

Browse files
feat: allow other bridge provider families in CoW Swap: Recipient based (#552)
* Make bridge provider simpler, and create interfaces for the two families of providers * chore: add assertion functions for provider types * chore: adapt providers * chore: adapt bridging logic * chore: fix test * chore: basic implementation of account bridge provider * chore: refactor * feat: refactor and cleanup * chore: refactor * tests: add mixed provider types tests * chore: improve types * tests: add tests * fix: fix types of get status * fix: error with type Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * chore: attempt to release a release candidate --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent f502fa9 commit ae56511

28 files changed

+1677
-595
lines changed

.release-please-config.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
},
1111
"packages/bridging": {
1212
"package-name": "@cowprotocol/sdk-bridging",
13-
"release-type": "node"
13+
"release-type": "node",
14+
"versioning": "prerelease",
15+
"prerelease": true
1416
},
1517
"packages/common": {
1618
"package-name": "@cowprotocol/sdk-common",

packages/bridging/src/BridgingSdk/BridgingSdk.test.ts

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BridgingSdk } from './BridgingSdk'
2-
import { MockBridgeProvider } from '../providers/mock/MockBridgeProvider'
3-
import { assertIsBridgeQuoteAndPost } from '../utils'
2+
import { MockHookBridgeProvider } from '../providers/mock/HookMockBridgeProvider'
3+
import { MockReceiverAccountBridgeProvider } from '../providers/mock/ReceiverAccountMockBridgeProvider'
4+
import { assertIsBridgeQuoteAndPost, isHookBridgeProvider, isReceiverAccountBridgeProvider } from '../utils'
45
import {
56
amountsAndCosts,
67
appDataInfo,
@@ -36,7 +37,7 @@ adapterNames.forEach((adapterName) => {
3637
let orderBookApi: OrderBookApi
3738
let quoteResult: QuoteResultsWithSigner
3839

39-
const mockProvider = new MockBridgeProvider()
40+
const mockProvider = new MockHookBridgeProvider()
4041
mockProvider.getQuote = jest.fn().mockResolvedValue(bridgeQuoteResult)
4142
mockProvider.getUnsignedBridgeCall = jest.fn().mockResolvedValue(bridgeCallDetails.unsignedBridgeCall)
4243
mockProvider.getSignedHook = jest.fn().mockResolvedValue(bridgeCallDetails.preAuthorizedBridgingHook)
@@ -367,18 +368,18 @@ adapterNames.forEach((adapterName) => {
367368
})
368369

369370
describe('getMultiQuotes', () => {
370-
let mockProvider2: MockBridgeProvider
371-
let mockProvider3: MockBridgeProvider
371+
let mockProvider2: MockHookBridgeProvider
372+
let mockProvider3: MockHookBridgeProvider
372373

373374
beforeEach(() => {
374-
mockProvider2 = new MockBridgeProvider()
375+
mockProvider2 = new MockHookBridgeProvider()
375376
mockProvider2.info.dappId = 'cow-sdk://bridging/providers/mock2'
376377
mockProvider2.info.name = 'Mock Bridge Provider 2'
377378
mockProvider2.getQuote = jest.fn().mockResolvedValue(bridgeQuoteResult)
378379
mockProvider2.getUnsignedBridgeCall = jest.fn().mockResolvedValue(bridgeCallDetails.unsignedBridgeCall)
379380
mockProvider2.getSignedHook = jest.fn().mockResolvedValue(bridgeCallDetails.preAuthorizedBridgingHook)
380381

381-
mockProvider3 = new MockBridgeProvider()
382+
mockProvider3 = new MockHookBridgeProvider()
382383
mockProvider3.info.dappId = 'cow-sdk://bridging/providers/mock3'
383384
mockProvider3.info.name = 'Mock Bridge Provider 3'
384385
mockProvider3.getQuote = jest.fn().mockResolvedValue(bridgeQuoteResult)
@@ -399,7 +400,7 @@ adapterNames.forEach((adapterName) => {
399400

400401
// Verify that the strategy pattern is working
401402
expect(results).toHaveLength(3)
402-
expect(results[0]?.providerDappId).toBe('mockProvider')
403+
expect(results[0]?.providerDappId).toBe('dapp-id-MockHookBridgeProvider')
403404
expect(results[1]?.providerDappId).toBe('cow-sdk://bridging/providers/mock2')
404405
expect(results[2]?.providerDappId).toBe('cow-sdk://bridging/providers/mock3')
405406

@@ -430,22 +431,22 @@ adapterNames.forEach((adapterName) => {
430431
it('should pass provider filter to strategy', async () => {
431432
const results = await bridgingSdk.getMultiQuotes({
432433
quoteBridgeRequest,
433-
providerDappIds: ['mockProvider', 'cow-sdk://bridging/providers/mock3'],
434+
providerDappIds: ['dapp-id-MockHookBridgeProvider', 'cow-sdk://bridging/providers/mock3'],
434435
})
435436

436437
// Verify only specified providers were used
437438
expect(results).toHaveLength(2)
438-
expect(results[0]?.providerDappId).toBe('mockProvider')
439+
expect(results[0]?.providerDappId).toBe('dapp-id-MockHookBridgeProvider')
439440
expect(results[1]?.providerDappId).toBe('cow-sdk://bridging/providers/mock3')
440441
})
441442
})
442443

443444
describe('getBestQuote', () => {
444-
let mockProvider2: MockBridgeProvider
445-
let mockProvider3: MockBridgeProvider
445+
let mockProvider2: MockHookBridgeProvider
446+
let mockProvider3: MockHookBridgeProvider
446447

447448
beforeEach(async () => {
448-
mockProvider2 = new MockBridgeProvider()
449+
mockProvider2 = new MockHookBridgeProvider()
449450
mockProvider2.info.dappId = 'cow-sdk://bridging/providers/mock2'
450451
mockProvider2.info.name = 'Mock Bridge Provider 2'
451452
// Override mockProvider to have a medium quote (50 ETH)
@@ -473,7 +474,7 @@ adapterNames.forEach((adapterName) => {
473474
mockProvider2.getUnsignedBridgeCall = jest.fn().mockResolvedValue(bridgeCallDetails.unsignedBridgeCall)
474475
mockProvider2.getSignedHook = jest.fn().mockResolvedValue(bridgeCallDetails.preAuthorizedBridgingHook)
475476

476-
mockProvider3 = new MockBridgeProvider()
477+
mockProvider3 = new MockHookBridgeProvider()
477478
mockProvider3.info.dappId = 'cow-sdk://bridging/providers/mock3'
478479
mockProvider3.info.name = 'Mock Bridge Provider 3'
479480
mockProvider3.getQuote = jest.fn().mockResolvedValue({
@@ -530,14 +531,124 @@ adapterNames.forEach((adapterName) => {
530531
it('should pass provider filter to strategy', async () => {
531532
const result = await bridgingSdk.getBestQuote({
532533
quoteBridgeRequest,
533-
providerDappIds: ['mockProvider'],
534+
providerDappIds: ['dapp-id-MockHookBridgeProvider'],
534535
})
535536

536537
// Verify only specified provider was used
537538
expect(result).toBeTruthy()
538-
expect(result?.providerDappId).toBe('mockProvider')
539+
expect(result?.providerDappId).toBe('dapp-id-MockHookBridgeProvider')
539540
expect(result?.quote).toBeTruthy()
540541
})
541542
})
543+
544+
describe('Mixed Provider Types', () => {
545+
let hookProvider: MockHookBridgeProvider
546+
let receiverAccountProvider: MockReceiverAccountBridgeProvider
547+
548+
beforeEach(() => {
549+
hookProvider = new MockHookBridgeProvider()
550+
hookProvider.getQuote = jest.fn().mockResolvedValue(bridgeQuoteResult)
551+
hookProvider.getUnsignedBridgeCall = jest.fn().mockResolvedValue(bridgeCallDetails.unsignedBridgeCall)
552+
hookProvider.getSignedHook = jest.fn().mockResolvedValue(bridgeCallDetails.preAuthorizedBridgingHook)
553+
554+
receiverAccountProvider = new MockReceiverAccountBridgeProvider()
555+
receiverAccountProvider.getQuote = jest.fn().mockResolvedValue({
556+
...bridgeQuoteResult,
557+
amountsAndCosts: {
558+
...bridgeQuoteResult.amountsAndCosts,
559+
costs: {
560+
bridgingFee: {
561+
feeBps: 5,
562+
amountInSellCurrency: 50000n,
563+
amountInBuyCurrency: 50000n,
564+
},
565+
},
566+
},
567+
})
568+
569+
bridgingSdk = new BridgingSdk({
570+
providers: [hookProvider, receiverAccountProvider],
571+
tradingSdk,
572+
})
573+
})
574+
575+
it('should return the muxed types for the providers', () => {
576+
const providers = bridgingSdk.getProviders()
577+
578+
expect(providers).toHaveLength(2)
579+
expect(providers[0]).toEqual(hookProvider)
580+
expect(isHookBridgeProvider(hookProvider)).toBe(true)
581+
582+
expect(providers[1]).toEqual(receiverAccountProvider)
583+
expect(isReceiverAccountBridgeProvider(receiverAccountProvider)).toBe(true)
584+
})
585+
586+
it('should get quotes from both provider types', async () => {
587+
const results = await bridgingSdk.getMultiQuotes({
588+
quoteBridgeRequest,
589+
})
590+
591+
expect(results).toHaveLength(2)
592+
expect(results[0]?.providerDappId).toBe('dapp-id-MockHookBridgeProvider')
593+
expect(results[1]?.providerDappId).toBe('dapp-id-ReceiverAccountBridgeProvider')
594+
595+
// Both should return quotes
596+
expect(results[0]?.quote).toBeTruthy()
597+
expect(results[1]?.quote).toBeTruthy()
598+
})
599+
600+
it('should compare quotes from different provider types in getBestQuote', async () => {
601+
// Make hook provider have a lower quote
602+
hookProvider.getQuote = jest.fn().mockResolvedValue({
603+
...bridgeQuoteResult,
604+
amountsAndCosts: {
605+
...bridgeQuoteResult.amountsAndCosts,
606+
afterSlippage: {
607+
...bridgeQuoteResult.amountsAndCosts.afterSlippage,
608+
buyAmount: BigInt('50000000000000000000'), // 50 ETH
609+
},
610+
},
611+
})
612+
613+
// Make receiver account provider have better quote
614+
receiverAccountProvider.getQuote = jest.fn().mockResolvedValue({
615+
...bridgeQuoteResult,
616+
amountsAndCosts: {
617+
...bridgeQuoteResult.amountsAndCosts,
618+
afterSlippage: {
619+
...bridgeQuoteResult.amountsAndCosts.afterSlippage,
620+
buyAmount: BigInt('70000000000000000000'), // 70 ETH - better than hook provider
621+
},
622+
},
623+
})
624+
625+
const result = await bridgingSdk.getBestQuote({
626+
quoteBridgeRequest,
627+
})
628+
629+
// Should select the receiver account provider as it has better quote
630+
expect(result).toBeTruthy()
631+
expect(result?.providerDappId).toBe('dapp-id-ReceiverAccountBridgeProvider')
632+
expect(result?.quote?.bridge.amountsAndCosts.afterSlippage.buyAmount).toBe(BigInt('70000000000000000000'))
633+
})
634+
635+
it('should correctly identify provider types', () => {
636+
const providers = bridgingSdk.getProviders()
637+
638+
const hook = providers.find((p) => p.info.dappId === 'dapp-id-MockHookBridgeProvider')
639+
const receiver = providers.find((p) => p.info.dappId === 'dapp-id-ReceiverAccountBridgeProvider')
640+
641+
expect(hook).toBeDefined()
642+
expect(receiver).toBeDefined()
643+
644+
if (!hook || !receiver) throw new Error('Hook or receiver not found') // Just to satisfy the linter (asserted above)
645+
646+
expect(isHookBridgeProvider(hook)).toBe(true)
647+
expect(isReceiverAccountBridgeProvider(hook)).toBe(false)
648+
649+
expect(isHookBridgeProvider(receiver)).toBe(false)
650+
expect(isReceiverAccountBridgeProvider(receiver)).toBe(true)
651+
})
652+
})
542653
})
543654
})

packages/bridging/src/BridgingSdk/BridgingSdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
BridgeProvider,
32
BridgeQuoteResult,
43
BridgeStatusResult,
54
BuyTokensParams,
@@ -9,6 +8,7 @@ import {
98
MultiQuoteResult,
109
MultiQuoteRequest,
1110
QuoteBridgeRequest,
11+
BridgeProvider,
1212
} from '../types'
1313
import { getCrossChainOrder } from './getCrossChainOrder'
1414
import { findBridgeProviderFromHook } from './findBridgeProviderFromHook'

packages/bridging/src/BridgingSdk/getBridgeSignedHook.test.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { getBridgeSignedHook } from './getBridgeSignedHook'
2-
import { QuoteBridgeRequest } from '../types'
3-
import { BridgeResultContext } from './types'
2+
import { BridgeQuoteResult, HookBridgeProvider, QuoteBridgeRequest } from '../types'
43
import { createAdapters } from '../../tests/setup'
54
import { setGlobalAdapter } from '@cowprotocol/sdk-common'
5+
import { HookBridgeResultContext } from './getQuoteWithBridge'
66

77
jest.mock('@cowprotocol/sdk-order-book', () => ({
88
...jest.requireActual('@cowprotocol/sdk-order-book'),
@@ -25,6 +25,11 @@ const unsignedBridgeCallMock = {
2525
const signedHookMock = {}
2626

2727
const owner = '0x000a1'
28+
const providerMock = {
29+
getQuote: jest.fn().mockResolvedValue(bridgingQuoteMock),
30+
getUnsignedBridgeCall: jest.fn().mockResolvedValue(unsignedBridgeCallMock),
31+
getSignedHook: jest.fn().mockResolvedValue(signedHookMock),
32+
} as unknown as HookBridgeProvider<BridgeQuoteResult>
2833

2934
const contextMock = {
3035
swapResult: {
@@ -35,14 +40,9 @@ const contextMock = {
3540
owner,
3641
},
3742
},
38-
provider: {
39-
getQuote: jest.fn().mockResolvedValue(bridgingQuoteMock),
40-
getUnsignedBridgeCall: jest.fn().mockResolvedValue(unsignedBridgeCallMock),
41-
getSignedHook: jest.fn().mockResolvedValue(signedHookMock),
42-
},
4343
signer: {},
4444
hookGasLimit: 100000n,
45-
} as unknown as BridgeResultContext
45+
} as unknown as HookBridgeResultContext
4646

4747
const adapters = createAdapters()
4848
const adapterNames = Object.keys(adapters) as Array<keyof typeof adapters>
@@ -60,10 +60,10 @@ adapterNames.forEach((adapterName) => {
6060
})
6161

6262
it('Should create a bridge hook nonce based on orderId and specified owner', async () => {
63-
await getBridgeSignedHook(bridgeRequestMock, contextMock)
63+
await getBridgeSignedHook(providerMock, bridgeRequestMock, contextMock)
6464

65-
expect(contextMock.provider.getSignedHook).toHaveBeenCalledTimes(1)
66-
expect(contextMock.provider.getSignedHook).toHaveBeenCalledWith(
65+
expect(providerMock.getSignedHook).toHaveBeenCalledTimes(1)
66+
expect(providerMock.getSignedHook).toHaveBeenCalledWith(
6767
bridgeRequestMock.sellTokenChainId,
6868
unsignedBridgeCallMock,
6969
// nonce
@@ -78,17 +78,17 @@ adapterNames.forEach((adapterName) => {
7878
it('Should calculate deadline based on orderToSign.validTo first of all', async () => {
7979
const expectedValidTo = 1750000000
8080

81-
await getBridgeSignedHook(bridgeRequestMock, {
81+
await getBridgeSignedHook(providerMock, bridgeRequestMock, {
8282
...contextMock,
8383
swapResult: {
8484
orderToSign: {
8585
validTo: expectedValidTo,
8686
},
8787
},
88-
} as unknown as BridgeResultContext)
88+
} as unknown as HookBridgeResultContext)
8989

90-
expect(contextMock.provider.getSignedHook).toHaveBeenCalledTimes(1)
91-
expect(contextMock.provider.getSignedHook).toHaveBeenCalledWith(
90+
expect(providerMock.getSignedHook).toHaveBeenCalledTimes(1)
91+
expect(providerMock.getSignedHook).toHaveBeenCalledWith(
9292
expect.anything(),
9393
expect.anything(),
9494
expect.anything(),
@@ -102,18 +102,18 @@ adapterNames.forEach((adapterName) => {
102102
it('Should use validTo override for deadline if set', async () => {
103103
const expectedValidTo = 2900000000
104104

105-
await getBridgeSignedHook(bridgeRequestMock, {
105+
await getBridgeSignedHook(providerMock, bridgeRequestMock, {
106106
...contextMock,
107107
swapResult: {
108108
orderToSign: {
109109
validTo: 1750000000,
110110
},
111111
},
112112
validToOverride: expectedValidTo,
113-
} as unknown as BridgeResultContext)
113+
} as unknown as HookBridgeResultContext)
114114

115-
expect(contextMock.provider.getSignedHook).toHaveBeenCalledTimes(1)
116-
expect(contextMock.provider.getSignedHook).toHaveBeenCalledWith(
115+
expect(providerMock.getSignedHook).toHaveBeenCalledTimes(1)
116+
expect(providerMock.getSignedHook).toHaveBeenCalledWith(
117117
expect.anything(),
118118
expect.anything(),
119119
expect.anything(),

packages/bridging/src/BridgingSdk/getBridgeSignedHook.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { EvmCall } from '@cowprotocol/sdk-config'
22
import { getGlobalAdapter } from '@cowprotocol/sdk-common'
33

4-
import { BridgeHook, BridgeQuoteResult, QuoteBridgeRequest } from '../types'
5-
import { BridgeResultContext } from './types'
4+
import { BridgeHook, BridgeQuoteResult, HookBridgeProvider, QuoteBridgeRequest } from '../types'
5+
import { HookBridgeResultContext } from './getQuoteWithBridge'
66

77
export async function getBridgeSignedHook(
8+
provider: HookBridgeProvider<BridgeQuoteResult>,
89
bridgeRequest: QuoteBridgeRequest,
9-
{ provider, signer, hookGasLimit, swapResult, validToOverride }: BridgeResultContext,
10+
{ signer, hookGasLimit, swapResult, validToOverride }: HookBridgeResultContext,
1011
): Promise<{ hook: BridgeHook; unsignedBridgeCall: EvmCall; bridgingQuote: BridgeQuoteResult }> {
1112
const adapter = getGlobalAdapter()
1213

packages/bridging/src/BridgingSdk/getCrossChainOrder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BridgeProvider, BridgeQuoteResult, BridgeStatus, CrossChainOrder } from '../types'
1+
import { BridgeQuoteResult, BridgeStatus, CrossChainOrder, BridgeProvider } from '../types'
22

33
import { BridgeOrderParsingError } from '../errors'
44
import { findBridgeProviderFromHook } from './findBridgeProviderFromHook'

0 commit comments

Comments
 (0)