Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/bridging/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The BridgingSdk supports two types of operations:

Supported bridge providers:

- **Bungee/Socket**: Multi-bridge aggregation platform with support for multiple underlying bridges including Across, CCTP, and others
- **Bungee/Socket**: Multi-bridge aggregation platform with support for multiple underlying bridges including Across, CCTP V2, and others

> Since `BridgingSdk` is compatible with [TradingSDK](https://github.com/cowprotocol/cow-sdk/tree/main/packages/trading/README.md), almost everything described for `TradingSDK` applies to `BridgingSdk` as well.
> The main difference is smart contract wallet support. Currently, `BridgingSdk` only supports EOA wallets; this will likely change soon — stay tuned!
Expand Down Expand Up @@ -137,7 +137,7 @@ const adapter = new ViemAdapter({
// Initialize the bridge provider
const bungeeProvider = new BungeeBridgeProvider({
apiOptions: {
includeBridges: ['across', 'cctp'], // Optional: specify which bridges to include
includeBridges: ['across', 'cctp-v2'], // Optional: specify which bridges to include
},
})

Expand Down Expand Up @@ -288,7 +288,7 @@ const tradingSdk: TradingSdk = new TradingSdk(

const bungeeProvider: BungeeBridgeProvider = new BungeeBridgeProvider({
apiOptions: {
includeBridges: ['across', 'cctp'],
includeBridges: ['across', 'cctp-v2'],
},
})

Expand Down Expand Up @@ -411,7 +411,7 @@ const adapter = new ViemAdapter({

const bungeeProvider = new BungeeBridgeProvider({
apiOptions: {
includeBridges: ['across', 'cctp'], // Optional: specify which bridges to include
includeBridges: ['across', 'cctp-v2'], // Optional: specify which bridges to include
},
})

Expand Down Expand Up @@ -526,7 +526,7 @@ async function main() {
// Initialize bridge provider
const bungeeProvider = new BungeeBridgeProvider({
apiOptions: {
includeBridges: ['across', 'cctp'], // Optional: specify which bridges to include
includeBridges: ['across', 'cctp-v2'], // Optional: specify which bridges to include
},
})

Expand Down
30 changes: 22 additions & 8 deletions packages/bridging/src/providers/bungee/BungeeApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const mockQuoteRequest: BungeeQuoteAPIRequest = {
disableAuto: true,
}

describe.skip('BungeeApi', () => {
describe('BungeeApi', () => {
let api: BungeeApi

beforeEach(() => {
Expand Down Expand Up @@ -149,6 +149,20 @@ describe.skip('BungeeApi', () => {
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/quote'), expect.any(Object))
})

it('should normalize "cctp" bridge to "cctp-v2"', async () => {
const request: BungeeQuoteAPIRequest = {
...mockQuoteRequest,
includeBridges: ['cctp', 'across'],
}

await api.getBungeeQuote(request)

expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('includeBridges=cctp-v2%2Cacross'),
expect.any(Object),
)
})

it('should handle API errors', async () => {
mockFetch.mockResolvedValue({
ok: false,
Expand All @@ -160,7 +174,7 @@ describe.skip('BungeeApi', () => {
})
})

describe.skip('getBungeeBuildTx', () => {
describe('getBungeeBuildTx', () => {
const mockBuildTxResponse = {
success: true,
statusCode: 200,
Expand Down Expand Up @@ -383,7 +397,7 @@ describe.skip('BungeeApi', () => {
})
})

describe.skip('getEvents', () => {
describe('getEvents', () => {
const mockEventsResponse = {
success: true,
result: [
Expand Down Expand Up @@ -454,7 +468,7 @@ describe.skip('BungeeApi', () => {
})
})

describe.skip('getAcrossStatus', () => {
describe('getAcrossStatus', () => {
const mockAcrossStatusResponse = {
status: 'filled',
}
Expand Down Expand Up @@ -484,7 +498,7 @@ describe.skip('BungeeApi', () => {
})
})

describe.skip('custom API URL', () => {
describe('custom API URL', () => {
it('should use custom API URL when provided', async () => {
const customUrl = 'https://custom-api.example.com'
const customApi = new BungeeApi({ apiBaseUrl: customUrl })
Expand All @@ -500,7 +514,7 @@ describe.skip('BungeeApi', () => {
})
})

describe.skip('fallback mechanism', () => {
describe('fallback mechanism', () => {
beforeEach(() => {
jest.useFakeTimers()
})
Expand All @@ -509,7 +523,7 @@ describe.skip('BungeeApi', () => {
jest.useRealTimers()
})

describe.skip('fallback configuration', () => {
describe('fallback configuration', () => {
it('should use default fallback timeout (5 minutes) when not specified', () => {
const api = new BungeeApi()
// Access private property for testing
Expand Down Expand Up @@ -843,7 +857,7 @@ describe.skip('BungeeApi', () => {
})
})

describe.skip('apiKey and customApiBaseUrl', () => {
describe('apiKey and customApiBaseUrl', () => {
describe('when both apiKey and customApiBaseUrl are present', () => {
const customApiBaseUrl = 'https://custom-bungee-api.example.com'
const apiKey = 'test-api-key-123'
Expand Down
5 changes: 4 additions & 1 deletion packages/bridging/src/providers/bungee/BungeeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ export class BungeeApi {
}

private getSupportedBridges(bridges?: SupportedBridge[]): SupportedBridge[] {
return bridges ?? this.options.includeBridges ?? SUPPORTED_BRIDGES
const bridgeList = bridges ?? this.options.includeBridges ?? SUPPORTED_BRIDGES
const normalized = bridgeList.map((bridge) => (bridge === 'cctp' ? 'cctp-v2' : bridge))

return Array.from(new Set(normalized))
}

private isBungeeApi(apiType: BungeeApiType): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,14 @@ export class BungeeBridgeProvider implements HookBridgeProvider<BungeeQuoteResul

async getCancelBridgingTx(_bridgingId: string): Promise<EvmCall> {
// Support for cancellation will depend on the actual bridge an order went through.
// Across & CCTP doesn't support cancellation.
// Across & CCTP V2 don't support cancellation.
// Therefore, not implementing cancellation
throw new Error('Not implemented')
}

async getRefundBridgingTx(_bridgingId: string): Promise<EvmCall> {
// Support for refund will depend on the actual bridge an order went through.
// CCTP doesn't support refund.
// CCTP V2 doesn't support refund.
// Across auto-relays refund txns some time after the order expires. No user action needed.
// Therefore, not implementing refund
throw new Error('Not implemented')
Expand Down
10 changes: 8 additions & 2 deletions packages/bridging/src/providers/bungee/const/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,21 @@ export const BungeeTxDataBytesIndices: BungeeTxDataBytesIndicesType = {
},
},
},
cctp: {
'cctp-v2': {
// bridgeERC20To
['0xb7dfe9d0'.toLowerCase()]: {
['0x3ca7f5bc'.toLowerCase()]: {
inputAmount: {
bytes_startIndex: 8, // first 8 bytes are the routeId, followed by the function selector
bytes_length: 32, // first 32 bytes of the params are the amount
bytesString_startIndex: 2 + 8 * 2, // first two characters are 0x and 8 bytes = 16 chars for the amount
bytesString_length: 32 * 2, // 32 bytes = 64 chars for the amount
},
outputAmount: {
bytes_startIndex: 200, // feeAmount is the 7th parameter -> 8 + 6*32 = 200
bytes_length: 32,
bytesString_startIndex: 2 + 200 * 2,
bytesString_length: 32 * 2,
},
},
},
'gnosis-native-bridge': {
Expand Down
2 changes: 1 addition & 1 deletion packages/bridging/src/providers/bungee/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const BUNGEE_MANUAL_API_URL = `${BUNGEE_BASE_URL}${BUNGEE_MANUAL_API_PATH
export const BUNGEE_EVENTS_API_URL = 'https://microservices.socket.tech/loki'
export const ACROSS_API_URL = 'https://app.across.to/api'

export const SUPPORTED_BRIDGES: SupportedBridge[] = ['across', 'cctp', 'gnosis-native-bridge']
export const SUPPORTED_BRIDGES: SupportedBridge[] = ['across', 'cctp-v2', 'gnosis-native-bridge']

export const errorMessageMap = {
bungee: 'Bungee Api Error',
Expand Down
191 changes: 191 additions & 0 deletions packages/bridging/src/providers/bungee/createBungeeDepositCall.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { setGlobalAdapter } from '@cowprotocol/sdk-common'
import { SupportedChainId } from '@cowprotocol/sdk-config'
import { OrderKind } from '@cowprotocol/sdk-order-book'
import { createAdapters } from '../../../tests/setup'
import { QuoteBridgeRequest } from '../../types'
import { BUNGEE_APPROVE_AND_BRIDGE_V1_ABI } from './abi'
import { createBungeeDepositCall } from './createBungeeDepositCall'
import { BungeeBridge, BungeeQuoteWithBuildTx } from './types'
import { decodeAmountsBungeeTxData, toBridgeQuoteResult } from './util'
import { CCTP_V2_TX_DATA } from './testData'

function applyPctDiff(base: bigint, compare: bigint, target: bigint): bigint {
return (target * compare) / base
}

describe('createBungeeDepositCall', () => {
const adapters = createAdapters()

beforeEach(() => {
setGlobalAdapter(adapters.ethersV5Adapter)
})

it('encodes cctp-v2 calldata rewrite metadata for feeAmount as the proportional secondary field', async () => {
const request: QuoteBridgeRequest = {
kind: OrderKind.SELL,
amount: 100000000n,
owner: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
sellTokenChainId: SupportedChainId.BASE,
sellTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
sellTokenDecimals: 6,
buyTokenChainId: SupportedChainId.MAINNET,
buyTokenAddress: '0xA0b86991c6218b36c1d19d4a2e9Eb0cE3606eB48',
buyTokenDecimals: 6,
appCode: 'bungee',
account: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
signer: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
}

const quoteWithBuildTx: BungeeQuoteWithBuildTx = {
bungeeQuote: {
originChainId: SupportedChainId.BASE,
destinationChainId: SupportedChainId.MAINNET,
userAddress: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
receiverAddress: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
input: {
token: {
chainId: SupportedChainId.BASE,
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
logoURI: '',
icon: '',
},
amount: '100000000',
priceInUsd: 1,
valueInUsd: 100,
},
route: {
quoteId: 'fd868dfdece0f3b1',
quoteExpiry: 0,
output: {
token: {
chainId: SupportedChainId.MAINNET,
address: '0xA0b86991c6218b36c1d19d4a2e9Eb0cE3606eB48',
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
logoURI: '',
icon: '',
},
amount: '98000000',
priceInUsd: 1,
valueInUsd: 98,
minAmountOut: '98000000',
effectiveReceivedInUsd: 98,
},
affiliateFee: null,
approvalData: {
spenderAddress: '0x3a23f943181408eac424116af7b7790c94cb97a5',
amount: '100000000',
tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
userAddress: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
},
gasFee: {
gasToken: {
chainId: SupportedChainId.BASE,
address: '0x0000000000000000000000000000000000000000',
symbol: 'ETH',
name: 'Ethereum',
decimals: 18,
icon: '',
logoURI: '',
chainAgnosticId: null,
},
gasLimit: '0',
gasPrice: '0',
estimatedFee: '0',
feeInUsd: 0,
},
slippage: 0,
estimatedTime: 0,
routeDetails: {
name: 'Circle CCTP V2',
logoURI: '',
routeFee: {
token: {
chainId: SupportedChainId.BASE,
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
logoURI: '',
icon: '',
},
amount: '0',
feeInUsd: 0,
priceInUsd: 1,
},
dexDetails: null,
},
refuel: null,
},
routeBridge: BungeeBridge.CircleCCTPV2,
quoteTimestamp: 0,
},
buildTx: {
approvalData: {
spenderAddress: '0x3a23f943181408eac424116af7b7790c94cb97a5',
amount: '100000000',
tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
userAddress: '0x664B591AB924c6bb2caCA533Ed702386934A11d6',
},
txData: {
data: CCTP_V2_TX_DATA,
to: '0x3a23f943181408eac424116af7b7790c94cb97a5',
chainId: SupportedChainId.BASE,
value: '0',
},
userOp: '',
},
}

const quote = toBridgeQuoteResult(request, 0, quoteWithBuildTx)
const call = await createBungeeDepositCall({ request, quote })
const decoded = adapters.ethersV5Adapter.utils.decodeFunctionData(
BUNGEE_APPROVE_AND_BRIDGE_V1_ABI,
'approveAndBridge',
call.data,
) as [string, { toString(): string }, { toString(): string }, string]

expect(decoded[0]).toBe(request.sellTokenAddress)
expect(decoded[1].toString()).toBe('100000000')
expect(decoded[2].toString()).toBe('0')
expect(decoded[3].startsWith(CCTP_V2_TX_DATA)).toBe(true)

const modifyCalldataParams = `0x${decoded[3].slice(-192)}`
const [inputAmountStartIndex, modifyOutputAmount, outputAmountStartIndex] = adapters.ethersV5Adapter.utils.decodeAbi(
['uint256', 'bool', 'uint256'],
modifyCalldataParams,
) as [bigint, boolean, bigint]

expect(inputAmountStartIndex).toBe(8n)
expect(modifyOutputAmount).toBe(true)
expect(outputAmountStartIndex).toBe(200n)
})

it('would lower feeAmount proportionally when the actual bridged amount is below the quote', () => {
const result = decodeAmountsBungeeTxData(CCTP_V2_TX_DATA, BungeeBridge.CircleCCTPV2)

const quotedAmount = result.inputAmountBigNumber
const quotedFeeAmount = result.outputAmountBigNumber!
const actualBridgedAmount = 90000000n
const adjustedFeeAmount = applyPctDiff(quotedAmount, actualBridgedAmount, quotedFeeAmount)

expect(adjustedFeeAmount).toBe(1800000n)
expect(adjustedFeeAmount).toBeLessThan(quotedFeeAmount)
})

it('would raise feeAmount proportionally when the actual bridged amount is above the quote', () => {
const result = decodeAmountsBungeeTxData(CCTP_V2_TX_DATA, BungeeBridge.CircleCCTPV2)

const quotedAmount = result.inputAmountBigNumber
const quotedFeeAmount = result.outputAmountBigNumber!
const actualBridgedAmount = 110000000n
const adjustedFeeAmount = applyPctDiff(quotedAmount, actualBridgedAmount, quotedFeeAmount)

expect(adjustedFeeAmount).toBe(2200000n)
expect(adjustedFeeAmount).toBeGreaterThan(quotedFeeAmount)
})
})
Loading
Loading