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
4 changes: 2 additions & 2 deletions packages/bridging/src/BridgingSdk/getQuoteWithBridge.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getEthFlowContract, TradingSdk } from '@cowprotocol/sdk-trading'
import { MockHookBridgeProvider } from '../providers/mock/HookMockBridgeProvider'
import { MockReceiverAccountBridgeProvider } from '../providers/mock/ReceiverAccountMockBridgeProvider'
import { QuoteBridgeRequest } from '../types'
import { BridgeQuoteAndPost, QuoteBridgeRequest } from '../types'
import { getQuoteWithBridge } from './getQuoteWithBridge'
import {
bridgeCallDetails,
Expand Down Expand Up @@ -85,7 +85,7 @@ adapterNames.forEach((adapterName) => {
return getQuoteWithBridge(mockProvider, {
swapAndBridgeRequest: request,
tradingSdk,
})
}) as Promise<BridgeQuoteAndPost>
}

async function postOrderWithCustomReceiver(customReceiver: string) {
Expand Down
17 changes: 15 additions & 2 deletions packages/bridging/src/BridgingSdk/getQuoteWithBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
BridgeQuoteAndPost,
BridgeQuoteResult,
BridgeQuoteResults,
BridgeThenSwapProvider,

Check failure on line 19 in packages/bridging/src/BridgingSdk/getQuoteWithBridge.ts

View workflow job for this annotation

GitHub Actions / eslint

'BridgeThenSwapProvider' is defined but never used

Check failure on line 19 in packages/bridging/src/BridgingSdk/getQuoteWithBridge.ts

View workflow job for this annotation

GitHub Actions / eslint

'BridgeThenSwapProvider' is defined but never used. Allowed unused vars must match /^_/u

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import BridgeThenSwapProvider to fix ESLint error.

The BridgeThenSwapProvider type is imported but never used in this file. The type guard isBridgeThenSwapProvider (line 33) is used instead for the runtime check. This unused import is causing the ESLint pipeline failure.

🔧 Proposed fix
 import {
   BridgeQuoteAndPost,
   BridgeQuoteResult,
   BridgeQuoteResults,
-  BridgeThenSwapProvider,
   BridgeThenSwapQuoteAndPost,
   QuoteBridgeRequest,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BridgeThenSwapProvider,
import {
BridgeQuoteAndPost,
BridgeQuoteResult,
BridgeQuoteResults,
BridgeThenSwapQuoteAndPost,
QuoteBridgeRequest,
🧰 Tools
🪛 GitHub Actions: ESLint check

[error] 19-3: ESLint (@typescript-eslint/no-unused-vars): 'BridgeThenSwapProvider' is defined but never used. Allowed unused vars must match /^_/u


[error] lint step failed: eslint src/**/*.ts

🪛 GitHub Check: eslint

[failure] 19-19:
'BridgeThenSwapProvider' is defined but never used


[failure] 19-19:
'BridgeThenSwapProvider' is defined but never used. Allowed unused vars must match /^_/u

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bridging/src/BridgingSdk/getQuoteWithBridge.ts` at line 19, Remove
the unused type import BridgeThenSwapProvider from the import list (it's causing
the ESLint error) while keeping the runtime type guard isBridgeThenSwapProvider
intact; locate the import that currently includes BridgeThenSwapProvider and
delete that symbol from the import clause so only used symbols remain, then run
lint to verify the ESLint error is resolved.

BridgeThenSwapQuoteAndPost,
QuoteBridgeRequest,
QuoteBridgeRequestWithoutAmount,
BridgeProvider,
Expand All @@ -28,20 +30,31 @@
import { getBridgeSignedHook } from './getBridgeSignedHook'
import { HOOK_DAPP_BRIDGE_PROVIDER_PREFIX } from '../const'
import { getHookMockForCostEstimation } from '../hooks/utils'
import { isHookBridgeProvider, isReceiverAccountBridgeProvider } from '../utils'
import { isHookBridgeProvider, isReceiverAccountBridgeProvider, isBridgeThenSwapProvider } from '../utils'
import { getIntermediateSwapResult } from './getIntermediateSwapResult'
import { getQuoteWithBridgeThenSwap } from './getQuoteWithBridgeThenSwap'

export async function getQuoteWithBridge<T extends BridgeQuoteResult>(
provider: BridgeProvider<T>,
params: GetQuoteWithBridgeParams,
): Promise<BridgeQuoteAndPost> {
): Promise<BridgeQuoteAndPost | BridgeThenSwapQuoteAndPost> {
const { kind } = params.swapAndBridgeRequest

// Ensure the quote request is for a sell order (only type supported for now)
if (kind !== OrderKind.SELL) {
throw new Error('Bridging only support SELL orders')
}

// If the provider uses bridge-then-swap (reverse flow)
if (isBridgeThenSwapProvider(provider)) {
return getQuoteWithBridgeThenSwap(provider, {
swapAndBridgeRequest: params.swapAndBridgeRequest,
advancedSettings: params.advancedSettings,
tradingSdk: params.tradingSdk,
intermediateTokensCache: params.intermediateTokensCache,
})
}

// If the provider relies on hooks
if (isHookBridgeProvider(provider)) {
return getQuoteWithHookBridge(provider, params)
Expand Down
274 changes: 274 additions & 0 deletions packages/bridging/src/BridgingSdk/getQuoteWithBridgeThenSwap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import {
QuoteResults,
SwapAdvancedSettings,
TradingSdk,
WithPartialTraderParams,
TradeParameters,
} from '@cowprotocol/sdk-trading'
import { getGlobalAdapter, log, SignerLike } from '@cowprotocol/sdk-common'
import { OrderKind } from '@cowprotocol/sdk-order-book'
import { TokenInfo } from '@cowprotocol/sdk-config'
import {
BridgeQuoteResult,
BridgeQuoteResults,
BridgeThenSwapProvider,
BridgeThenSwapQuoteAndPost,
QuoteBridgeRequest,
} from '../types'
import { GetQuoteWithBridgeThenSwapParams } from './types'
import { BridgeProviderQuoteError, BridgeQuoteErrors } from '../errors'
import { determineIntermediateToken } from './determineIntermediateToken'

const DESTINATION_GAS_BUFFER = 30_000

/**
* Get a quote for a bridge-then-swap operation.
*
* Flow:
* 1. Determine the intermediate token on the destination chain (what can be bridged)
* 2. Get a bridge quote from source chain to destination chain (for the intermediate token)
* 3. Get a swap quote on the destination chain (intermediate token → final buy token)
* 4. Compute the OrderFlow address (CREATE2)
* 5. Encode the destination order data
* 6. Build the bridge transaction (includes destination payload)
* 7. Return the combined quote
*/
export async function getQuoteWithBridgeThenSwap<T extends BridgeQuoteResult>(
provider: BridgeThenSwapProvider<T>,
params: GetQuoteWithBridgeThenSwapParams,
): Promise<BridgeThenSwapQuoteAndPost> {
const { swapAndBridgeRequest, tradingSdk, advancedSettings } = params
const {
kind,
sellTokenChainId,
sellTokenAddress,
sellTokenDecimals,
buyTokenChainId,
buyTokenAddress,
buyTokenDecimals,
amount,
signer: signerLike,
} = swapAndBridgeRequest

if (kind !== OrderKind.SELL) {
throw new Error('Bridge-then-swap only supports SELL orders')
}

const adapter = getGlobalAdapter()
const signer = signerLike ? adapter.createSigner(signerLike) : adapter.signer
const owner = swapAndBridgeRequest.owner ?? swapAndBridgeRequest.account
if (!owner) {
throw new BridgeProviderQuoteError(BridgeQuoteErrors.QUOTE_ERROR, {
error: 'Owner address is required for bridge-then-swap',
})
}

log(
`Bridge-then-swap: ${amount} ${sellTokenAddress} (chain ${sellTokenChainId}) → bridge → swap for ${buyTokenAddress} (chain ${buyTokenChainId})`,
)

// Step 1: Get intermediate tokens (tokens that can be bridged to the destination chain)
const intermediateTokens = await provider.getIntermediateTokens(swapAndBridgeRequest)

if (intermediateTokens.length === 0) {
throw new BridgeProviderQuoteError(BridgeQuoteErrors.NO_INTERMEDIATE_TOKENS)
}

// Determine the best intermediate token for bridging
const intermediateToken = await determineIntermediateToken(
sellTokenChainId,
sellTokenAddress,
intermediateTokens,
advancedSettings?.getCorrelatedTokens,
)

log(`Using ${intermediateToken?.name ?? intermediateToken?.address} as bridge intermediate token`)

// Step 2: Get bridge quote (source chain sell token → destination chain intermediate token)
const bridgeRequest: QuoteBridgeRequest = {
...swapAndBridgeRequest,
// The sell token is the user's source token
sellTokenAddress,
sellTokenChainId,
sellTokenDecimals,
// The buy token for the bridge is the intermediate token on the destination chain
buyTokenAddress: intermediateToken.address,
buyTokenChainId,
buyTokenDecimals: intermediateToken.decimals,
}

const bridgeQuote = await provider.getQuote(bridgeRequest)

// The amount that arrives on the destination chain after bridge fees
const bridgedAmount = bridgeQuote.amountsAndCosts.afterSlippage.buyAmount

log(`Bridge quote: ${amount} → ${bridgedAmount} of ${intermediateToken.symbol} on chain ${buyTokenChainId}`)

// Step 3: Get swap quote on the destination chain (intermediate token → final buy token)
// This is an indicative quote — the actual order is created on-chain by OrderFlowFactory
const destinationSwapResult = await getDestinationSwapQuote({
tradingSdk,
bridgedAmount,
intermediateToken,
buyTokenAddress,
buyTokenDecimals,
buyTokenChainId,
swapAndBridgeRequest,
signer,
advancedSettings,
})

const destinationBuyAmount = destinationSwapResult.swapResult.amountsAndCosts.afterSlippage.buyAmount
const estimatedBuyAmount = destinationSwapResult.swapResult.amountsAndCosts.afterPartnerFees.buyAmount

// Step 4: Compute the OrderFlow address
const orderFlowAddress = provider.getOrderFlowAddress(owner, buyTokenChainId)

log(`OrderFlow address for ${owner} on chain ${buyTokenChainId}: ${orderFlowAddress}`)

// Determine validTo for the destination order
// Use a generous validity window since we need time for bridge + order settlement
const validTo = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now

// Step 5: Encode destination order data
const receiver = swapAndBridgeRequest.receiver ?? owner
const destinationOrderParams = {
sellToken: intermediateToken.address,
buyToken: buyTokenAddress,
receiver,
owner,
sellAmount: bridgedAmount,
buyAmount: destinationBuyAmount,
validTo,
appData: destinationSwapResult.swapResult.appDataInfo.appDataKeccak256,
feeAmount: 0n,
partiallyFillable: false,
quoteId: undefined as number | undefined,
}

const destinationPayload = provider.encodeDestinationOrderData(destinationOrderParams)

// Step 6: Estimate destination gas
const baseGasLimit = await provider.getDestinationGasLimit(destinationOrderParams)
const destinationGasLimit = baseGasLimit + DESTINATION_GAS_BUFFER

log(`Destination gas limit: ${destinationGasLimit} (${baseGasLimit} + ${DESTINATION_GAS_BUFFER} buffer)`)

// Step 7: Build bridge transaction
const bridgeTransaction = await provider.getBridgeTransaction({
bridgeQuote,
destinationPayload,
destinationGasLimit,
})

// Prepare bridge result
const bridgeResult: BridgeQuoteResults = {
providerInfo: provider.info,
id: bridgeQuote.id,
signature: bridgeQuote.signature,
quoteBody: bridgeQuote.quoteBody,
tradeParameters: bridgeRequest,
isSell: bridgeQuote.isSell,
expectedFillTimeSeconds: bridgeQuote.expectedFillTimeSeconds,
fees: bridgeQuote.fees,
limits: bridgeQuote.limits,
quoteTimestamp: bridgeQuote.quoteTimestamp,
amountsAndCosts: bridgeQuote.amountsAndCosts,
}

// Build buy token info
const buyTokenInfo: TokenInfo = {
address: buyTokenAddress,
decimals: buyTokenDecimals,
chainId: buyTokenChainId,
name: destinationSwapResult.swapResult.tradeParameters.buyToken,
symbol: destinationSwapResult.swapResult.tradeParameters.buyToken,
}
Comment on lines +180 to +186

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

buyTokenInfo.name and symbol are set to the token address, not actual metadata.

tradeParameters.buyToken is the token address string, not the token name or symbol. This will result in incorrect metadata in the returned buyTokenInfo.

🛠️ Proposed fix

Consider passing token metadata from the request or fetching it separately:

   // Build buy token info
   const buyTokenInfo: TokenInfo = {
     address: buyTokenAddress,
     decimals: buyTokenDecimals,
     chainId: buyTokenChainId,
-    name: destinationSwapResult.swapResult.tradeParameters.buyToken,
-    symbol: destinationSwapResult.swapResult.tradeParameters.buyToken,
+    name: swapAndBridgeRequest.buyTokenName,
+    symbol: swapAndBridgeRequest.buyTokenSymbol,
   }

Alternatively, if the metadata isn't available in the request, you could leave these fields as undefined or fetch token info from a registry.

🧰 Tools
🪛 GitHub Actions: ESLint check

[error] lint step failed: eslint src/**/*.ts

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bridging/src/BridgingSdk/getQuoteWithBridgeThenSwap.ts` around lines
180 - 186, The buyTokenInfo object incorrectly assigns name and symbol from
destinationSwapResult.swapResult.tradeParameters.buyToken (which is the token
address); update the construction of buyTokenInfo so name and symbol come from
actual token metadata (e.g., use provided request metadata fields if available,
or fetch token details from a registry/service using buyTokenAddress and
buyTokenChainId), and if metadata is not available set name and symbol to
undefined/null or a safe placeholder; locate this change around the buyTokenInfo
variable and references to buyTokenAddress and
destinationSwapResult.swapResult.tradeParameters.buyToken.


// Build sell token info for destination (the intermediate token)
const destSellTokenInfo: TokenInfo = {
...intermediateToken,
chainId: buyTokenChainId,
}

return {
bridge: bridgeResult,
destinationSwap: {
sellToken: destSellTokenInfo,
buyToken: buyTokenInfo,
estimatedBuyAmount,
minBuyAmount: destinationBuyAmount,
validTo,
quoteId: undefined,
},
orderFlowAddress,
bridgeTransaction,
approvalData: undefined, // TODO: Extract from Bungee build-tx approvalData if needed
submitBridgeTransaction: async (txSigner: SignerLike) => {
const resolvedSigner = adapter.createSigner(txSigner)
const txResponse = await resolvedSigner.sendTransaction({
to: bridgeTransaction.to,
data: bridgeTransaction.data,
value: BigInt(bridgeTransaction.value),
})
return { txHash: txResponse.hash }
},
}
}

interface GetDestinationSwapQuoteParams {
tradingSdk: TradingSdk
bridgedAmount: bigint
intermediateToken: TokenInfo
buyTokenAddress: string
buyTokenDecimals: number
buyTokenChainId: number
swapAndBridgeRequest: QuoteBridgeRequest
signer: SignerLike
advancedSettings?: SwapAdvancedSettings
}

async function getDestinationSwapQuote(
params: GetDestinationSwapQuoteParams,
): Promise<{ swapResult: QuoteResults }> {
const {
tradingSdk,
bridgedAmount,
intermediateToken,
buyTokenAddress,
buyTokenDecimals,
buyTokenChainId,
swapAndBridgeRequest,
signer,
advancedSettings,
} = params

const {
kind,
swapSlippageBps,
owner,
account,
receiver,
} = swapAndBridgeRequest

const swapParams: WithPartialTraderParams<TradeParameters> = {
kind,
chainId: buyTokenChainId,
sellToken: intermediateToken.address,
sellTokenDecimals: intermediateToken.decimals,
buyToken: buyTokenAddress,
buyTokenDecimals,
amount: bridgedAmount.toString(),
slippageBps: swapSlippageBps,
signer,
receiver: receiver ?? owner ?? account,
}

log(
`Getting destination swap quote on chain ${buyTokenChainId}: ${intermediateToken.symbol} → ${buyTokenAddress}. Amount: ${bridgedAmount}`,
)

const { result: swapResult } = await tradingSdk.getQuoteResults(swapParams, advancedSettings)

return { swapResult }
}
22 changes: 22 additions & 0 deletions packages/bridging/src/BridgingSdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ export type GetQuoteWithBridgeParams = {
allowIntermediateEqSellToken?: boolean
}

export type GetQuoteWithBridgeThenSwapParams = {
/**
* Overall request for the bridge-then-swap operation.
*/
swapAndBridgeRequest: QuoteBridgeRequest

/**
* Advanced settings for the destination swap.
*/
advancedSettings?: SwapAdvancedSettings

/**
* Trading SDK.
*/
tradingSdk: TradingSdk

/**
* Cache for intermediate tokens.
*/
intermediateTokensCache?: TTLCache<TokenInfo[]>
}

export type BridgingSdkConfig = Required<Omit<BridgingSdkOptions, 'enableLogging' | 'cacheConfig'>>

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/bridging/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export type { AcrossQuoteResult, AcrossBridgeProviderOptions } from './providers
export { BungeeBridgeProvider } from './providers/bungee/BungeeBridgeProvider'
export type { BungeeQuoteResult, BungeeBridgeProviderOptions } from './providers/bungee/BungeeBridgeProvider'

export { BungeeBridgeThenSwapProvider } from './providers/bungee/BungeeBridgeThenSwapProvider'
export type {
BungeeBridgeThenSwapQuoteResult,
BungeeBridgeThenSwapProviderOptions,
} from './providers/bungee/BungeeBridgeThenSwapProvider'

export { NearIntentsBridgeProvider } from './providers/near-intents/NearIntentsBridgeProvider'

export type {
Expand Down
Loading
Loading