Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c6fa1ce
feat(across): update ABI and API for token bridging with input/output…
KanishkKhurana Feb 27, 2026
cc895ff
chore: address TODOs
shoom3301 Mar 2, 2026
72228f2
fix(across): support different assets as destination
shoom3301 Mar 2, 2026
c6dd81e
fix(across): add quote id to deposit message
shoom3301 Mar 2, 2026
be25a6b
chore: fix encodeAbi
shoom3301 Mar 2, 2026
2a90564
chore: update getDepositParams
shoom3301 Mar 2, 2026
2ebad6e
chore: test
shoom3301 Mar 2, 2026
9596bea
chore: fix getDepositParams
shoom3301 Mar 2, 2026
9935214
fix: improve tokens cache
shoom3301 Mar 2, 2026
f14c415
chore: fix getDepositParams
shoom3301 Mar 2, 2026
5e9c770
chore: fix type
shoom3301 Mar 2, 2026
990c93a
chore: support ink
shoom3301 Mar 2, 2026
3acbed3
fix(across): fix tokens caching
shoom3301 Mar 3, 2026
75577f5
fix(across): map native token address
shoom3301 Mar 3, 2026
b5a60b9
fix(across): add tx link
shoom3301 Mar 3, 2026
682ebb8
fix(across): apply eth/weth logic
shoom3301 Mar 3, 2026
3bad058
fix(across): enhance getIntermediateTokens logic
shoom3301 Mar 3, 2026
3dfd7a2
fix(across): enhance getIntermediateTokens logic
shoom3301 Mar 3, 2026
4ecd38a
fix(across): enhance getIntermediateTokens logic
shoom3301 Mar 3, 2026
8b61bd0
fix(across): fix eth/weth intermediate token
shoom3301 Mar 3, 2026
c0b9b79
fix(across): fix eth/weth intermediate token
shoom3301 Mar 3, 2026
d0f349b
fix(across): map eth/weth token address
shoom3301 Mar 3, 2026
e563a99
fix(across): fix native token address in deposit call
shoom3301 Mar 3, 2026
d9439cc
fix(across): fix native token in intermediates
shoom3301 Mar 3, 2026
cfcb7e1
fix(across): enhance tokens and api errors
shoom3301 Mar 3, 2026
538367a
chore: ban native token as intemediate for across
shoom3301 Mar 3, 2026
33f2b05
chore: fix tests
shoom3301 Mar 3, 2026
99ab6b3
chore: fix types
shoom3301 Mar 4, 2026
ec6bf6a
Merge branch 'main' of https://github.com/cowprotocol/cow-sdk into fe…
shoom3301 Mar 5, 2026
3a78b0c
chore: fix build
shoom3301 Mar 5, 2026
cfcdb57
chore: fix build
shoom3301 Mar 5, 2026
e320ad5
feat(across): add default bridge slippage
shoom3301 Mar 5, 2026
562dd10
fix(across): map outputTokenAddress
shoom3301 Mar 5, 2026
bfe0b66
fix(across): map outputTokenAddress
shoom3301 Mar 5, 2026
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
2 changes: 1 addition & 1 deletion packages/bridging/src/BridgingSdk/getCrossChainOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function getCrossChainOrder(params: GetCrossChainOrderParams): Prom
}

try {
const explorerUrl = provider.getExplorerUrl(bridgingParams.bridgingId)
const explorerUrl = provider.getExplorerUrl(bridgingParams.bridgingId, tradeTxHash)

return {
...state,
Expand Down
14 changes: 8 additions & 6 deletions packages/bridging/src/providers/across/AcrossApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('AcrossApi: Shape of API response', () => {
// Attempt to make a REAL API call. The API implementation will assert the result shape matches the expected object
// Example: https://app.across.to/api/suggested-fees?token=0x82aF49447D8a07e3bd95BD0d56f35241523fBab1&originChainId=42161&destinationChainId=8453&amount=2389939424141418&recipient=0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3
const result = await api.getSuggestedFees({
token: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // weth
inputToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // weth
outputToken: '0x4200000000000000000000000000000000000006', // weth
originChainId: SupportedChainId.ARBITRUM_ONE,
destinationChainId: SupportedChainId.BASE,
amount: 2389939424141418n,
Expand All @@ -30,7 +31,8 @@ describe('AcrossApi: Shape of API response', () => {
// Attempt to make a REAL API call. The API implementation will assert the result shape matches the expected object
// Example: https://app.across.to/api/suggested-fees?token=0x82aF49447D8a07e3bd95BD0d56f35241523fBab1&originChainId=42161&destinationChainId=8453&amount=2389939424141418&recipient=0x016f34D4f2578c3e9DFfC3f2b811Ba30c0c9e7f3
const result = await api.getSuggestedFees({
token: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // weth
inputToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // weth
outputToken: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', // wpol
originChainId: SupportedChainId.MAINNET,
destinationChainId: SupportedChainId.POLYGON,
amount: 1000000000000000000n,
Expand All @@ -43,10 +45,10 @@ describe('AcrossApi: Shape of API response', () => {
it('getAvailableRoutes from ARBITRUM_ONE to BASE', async () => {
// Attempt to make a REAL API call. The API implementation will assert the result shape matches the expected object
const result = await api.getAvailableRoutes({
originChainId: SupportedChainId.ARBITRUM_ONE.toString(),
originChainId: SupportedChainId.ARBITRUM_ONE,
originToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // weth

destinationChainId: SupportedChainId.BASE.toString(),
destinationChainId: SupportedChainId.BASE,
destinationToken: '0x4200000000000000000000000000000000000006', // weth
})

Expand All @@ -56,10 +58,10 @@ describe('AcrossApi: Shape of API response', () => {
it('getAvailableRoutes from POLYGON to ARBITRUM_ONE', async () => {
// Attempt to make a REAL API call. The API implementation will assert the result shape matches the expected object
const result = await api.getAvailableRoutes({
originChainId: SupportedChainId.POLYGON.toString(),
originChainId: SupportedChainId.POLYGON,
originToken: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', // wpol

destinationChainId: SupportedChainId.ARBITRUM_ONE.toString(),
destinationChainId: SupportedChainId.ARBITRUM_ONE,
destinationToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // weth
})

Expand Down
30 changes: 20 additions & 10 deletions packages/bridging/src/providers/across/AcrossApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { SupportedChainId } from '@cowprotocol/sdk-config'
const mockFetch = jest.fn()
global.fetch = mockFetch

const TOKEN_A = { chainId: SupportedChainId.POLYGON, address: '0x123', decimals: 18, symbol: 'TOKEN1', name: 'Token 1' }
const TOKEN_B = { chainId: SupportedChainId.POLYGON, address: '0x456', decimals: 6, symbol: 'TOKEN2', name: 'Token 2' }

describe('AcrossApi', () => {
let api: AcrossApi

Expand Down Expand Up @@ -36,9 +39,9 @@ describe('AcrossApi', () => {

it('should fetch available routes with all parameters', async () => {
const params = {
originChainId: '1',
originChainId: 1,
originToken: '0x0000000000000000000000000000000000000001',
destinationChainId: '137',
destinationChainId: 137,
destinationToken: '0x0000000000000000000000000000000000000002',
}

Expand All @@ -60,9 +63,9 @@ describe('AcrossApi', () => {

await expect(
api.getAvailableRoutes({
originChainId: '1',
originChainId: 1,
originToken: '0x0000000000000000000000000000000000000001',
destinationChainId: '137',
destinationChainId: 137,
destinationToken: '0x0000000000000000000000000000000000000002',
}),
).rejects.toThrow(BridgeQuoteErrors.API_ERROR)
Expand All @@ -71,6 +74,10 @@ describe('AcrossApi', () => {

describe('getSuggestedFees', () => {
const mockResponse: SuggestedFeesResponse = {
id: '1',
inputToken: TOKEN_A,
outputToken: TOKEN_B,
outputAmount: '300010000000',
totalRelayFee: { pct: '100000000000000', total: '100000' },
relayerCapitalFee: { pct: '50000000000000', total: '50000' },
relayerGasFee: { pct: '50000000000000', total: '50000' },
Expand Down Expand Up @@ -101,7 +108,8 @@ describe('AcrossApi', () => {

it('should fetch suggested fees with required parameters', async () => {
const request: SuggestedFeesRequest = {
token: '0x0000000000000000000000000000000000000001',
inputToken: '0x0000000000000000000000000000000000000001',
outputToken: '0x0000000000000000000000000000000000000002',
originChainId: SupportedChainId.MAINNET,
destinationChainId: SupportedChainId.POLYGON,
amount: 1000000000000000000n,
Expand All @@ -111,14 +119,15 @@ describe('AcrossApi', () => {

expect(fees).toEqual(mockResponse)
expect(mockFetch).toHaveBeenCalledWith(
`https://app.across.to/api/suggested-fees?token=${request.token}&originChainId=${request.originChainId}&destinationChainId=${request.destinationChainId}&amount=${request.amount}`,
`https://app.across.to/api/suggested-fees?inputToken=${request.inputToken}&outputToken=${request.outputToken}&originChainId=${request.originChainId}&destinationChainId=${request.destinationChainId}&amount=${request.amount}&allowUnmatchedDecimals=true`,
expect.any(Object),
)
})

it('should include recipient when provided', async () => {
const request: SuggestedFeesRequest = {
token: '0x0000000000000000000000000000000000000001',
inputToken: '0x0000000000000000000000000000000000000001',
outputToken: '0x0000000000000000000000000000000000000002',
originChainId: SupportedChainId.MAINNET,
destinationChainId: SupportedChainId.POLYGON,
amount: 1000000000000000000n,
Expand All @@ -142,7 +151,8 @@ describe('AcrossApi', () => {

await expect(
api.getSuggestedFees({
token: '0x0000000000000000000000000000000000000001',
inputToken: '0x0000000000000000000000000000000000000001',
outputToken: '0x0000000000000000000000000000000000000002',
originChainId: SupportedChainId.MAINNET,
destinationChainId: SupportedChainId.POLYGON,
amount: 1000000000000000000n,
Expand All @@ -162,9 +172,9 @@ describe('AcrossApi', () => {
})

await customApi.getAvailableRoutes({
originChainId: '1',
originChainId: 1,
originToken: '0x0000000000000000000000000000000000000001',
destinationChainId: '137',
destinationChainId: 137,
destinationToken: '0x0000000000000000000000000000000000000002',
})

Expand Down
61 changes: 35 additions & 26 deletions packages/bridging/src/providers/across/AcrossApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { log } from '@cowprotocol/sdk-common'
import { ALL_SUPPORTED_CHAINS_MAP, SupportedChainId, TokenInfo } from '@cowprotocol/sdk-config'

import {
AvailableRoutesRequest,
DepositStatusRequest,
Expand All @@ -9,16 +12,22 @@ import {
SuggestedFeesResponse,
} from './types'
import { BridgeProviderQuoteError, BridgeQuoteErrors } from '../../errors'
import { TokenInfo } from '@cowprotocol/sdk-config'
import { log } from '@cowprotocol/sdk-common'

const ACROSS_API_URL = 'https://app.across.to/api'

enum AcrossApiErrors {
AMOUNT_TOO_LOW = 'AMOUNT_TOO_LOW',
}

type AcrossApiToken = TokenInfo & { logoURI?: string; isNative: boolean }

export interface AcrossApiOptions {
apiBaseUrl?: string
}

export class AcrossApi {
private cachedTokens: TokenInfo[] = []

constructor(private readonly options: AcrossApiOptions = {}) {}

/**
Expand All @@ -29,19 +38,8 @@ export class AcrossApi {
*
* See https://docs.across.to/reference/api-reference#available-routes
*/
async getAvailableRoutes({
originChainId,
originToken,
destinationChainId,
destinationToken,
}: AvailableRoutesRequest): Promise<Route[]> {
const params: Record<string, string> = {}
if (originChainId) params.originChainId = originChainId
if (originToken) params.originToken = originToken
if (destinationChainId) params.destinationChainId = destinationChainId
if (destinationToken) params.destinationToken = destinationToken

return this.fetchApi('/available-routes', params, isValidRoutes)
async getAvailableRoutes(params: AvailableRoutesRequest): Promise<Route[]> {
return this.fetchApi('/available-routes', params as never as Record<string | number, string>, isValidRoutes)
}

/**
Expand All @@ -54,28 +52,34 @@ export class AcrossApi {
*/
async getSuggestedFees(request: SuggestedFeesRequest): Promise<SuggestedFeesResponse> {
const params: Record<string, string> = {
token: request.token,
inputToken: request.inputToken,
outputToken: request.outputToken,
originChainId: request.originChainId.toString(),
destinationChainId: request.destinationChainId.toString(),
amount: request.amount.toString(),
allowUnmatchedDecimals: 'true', // Always set to true since we adjust for destination token decimals
}

if (request.recipient) {
params.recipient = request.recipient
}

// Get the quote from the Across API (see https://docs.across.to/reference/api-reference#suggested-fees)
// Example: https://app.across.to/api/suggested-fees?token=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&originChainId=8453&destinationChainId=137&amount=100000000
//
// TODO: The API documented params don't match with the example above. Ideally I would use 'inputToken' and 'outputToken', but the example above uses 'token'. This will work for current implementation, since we bridge the canonical token, but this will need to be reviewed
// https://app.across.to/api/suggested-fees?inputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&originChainId=8453&destinationChainId=137&outputToken=0xc2132D05D31c914a87C6611C10748AEb04B58e8F&amount=100000000
return this.fetchApi('/suggested-fees', params, isValidSuggestedFeesResponse)
}

async getSupportedTokens(): Promise<TokenInfo[]> {
return this.fetchApi<(TokenInfo & { logoURI?: string })[]>('/token-list', {}).then((tokens) =>
tokens.map((token) => ({ ...token, logoUrl: token.logoURI })),
)
if (this.cachedTokens.length === 0) {
const response = await this.fetchApi<AcrossApiToken[]>('/token-list', {}).then((tokens) =>
tokens.map((token) => ({
...token,
logoUrl: token.logoURI,
address: token.isNative
? (ALL_SUPPORTED_CHAINS_MAP[token.chainId as SupportedChainId]?.nativeCurrency.address ?? token.address)
: token.address,
})),
)
this.cachedTokens = response
}
return this.cachedTokens
}

async getDepositStatus(request: DepositStatusRequest): Promise<DepositStatusResponse> {
Expand All @@ -89,7 +93,7 @@ export class AcrossApi {

protected async fetchApi<T>(
path: string,
params: Record<string, string>,
params: Record<string | number, string>,
isValidResponse?: (response: unknown) => response is T,
): Promise<T> {
const baseUrl = this.options.apiBaseUrl || ACROSS_API_URL
Expand All @@ -103,6 +107,11 @@ export class AcrossApi {

if (!response.ok) {
const errorBody = await response.json()

if (errorBody.code === AcrossApiErrors.AMOUNT_TOO_LOW) {
throw new BridgeProviderQuoteError(BridgeQuoteErrors.SELL_AMOUNT_TOO_SMALL, errorBody)
}

throw new BridgeProviderQuoteError(BridgeQuoteErrors.API_ERROR, errorBody)
}

Expand Down
26 changes: 15 additions & 11 deletions packages/bridging/src/providers/across/AcrossBridgeProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ import { SuggestedFeesResponse } from './types'
import { SupportedChainId, TargetChainId } from '@cowprotocol/sdk-config'
import { setGlobalAdapter } from '@cowprotocol/sdk-common'
import { createAdapters } from '../../../tests/setup'
import { DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION } from '../../const'
import { DEFAULT_EXTRA_GAS_FOR_HOOK_ESTIMATION, DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION } from '../../const'
import stringify from 'json-stable-stringify'

// Mock AcrossApi
jest.mock('./AcrossApi')

const mockTokens = [
{ chainId: SupportedChainId.POLYGON, address: '0x123', decimals: 18, symbol: 'TOKEN1', name: 'Token 1' },
{ chainId: SupportedChainId.POLYGON, address: '0x456', decimals: 6, symbol: 'TOKEN2', name: 'Token 2' },
]
const TOKEN_A = { chainId: SupportedChainId.POLYGON, address: '0x123', decimals: 18, symbol: 'TOKEN1', name: 'Token 1' }
const TOKEN_B = { chainId: SupportedChainId.POLYGON, address: '0x456', decimals: 6, symbol: 'TOKEN2', name: 'Token 2' }
const mockTokens = [TOKEN_A, TOKEN_B]

class AcrossBridgeProviderTest extends AcrossBridgeProvider {
constructor(options: AcrossBridgeProviderOptions) {
Expand Down Expand Up @@ -98,6 +97,10 @@ adapterNames.forEach((adapterName) => {

describe('getQuote', () => {
const mockSuggestedFees: SuggestedFeesResponse = {
id: '1',
inputToken: TOKEN_A,
outputToken: TOKEN_B,
outputAmount: '20000',
totalRelayFee: { pct: '100000000000000', total: '100000' },
relayerCapitalFee: { pct: '50000000000000', total: '50000' },
relayerGasFee: { pct: '50000000000000', total: '50000' },
Expand Down Expand Up @@ -147,17 +150,18 @@ adapterNames.forEach((adapterName) => {
expect(suggestedFees).toEqual(mockSuggestedFees)

const expectedQuote: BridgeQuoteResult = {
id: '1',
isSell: true,
quoteBody: stringify(mockSuggestedFees),
amountsAndCosts: {
beforeFee: { sellAmount: 1000000000000000000n, buyAmount: 1000000n },
afterFee: { sellAmount: 1000000000000000000n, buyAmount: 999900n },
afterSlippage: { sellAmount: 1000000000000000000n, buyAmount: 999900n },
beforeFee: { sellAmount: 1000000000000000000n, buyAmount: 20000n },
afterFee: { sellAmount: 1000000000000000000n, buyAmount: 19998n },
afterSlippage: { sellAmount: 1000000000000000000n, buyAmount: 19998n },
costs: {
bridgingFee: {
feeBps: 1,
amountInSellCurrency: 100000000000000n,
amountInBuyCurrency: 100n,
amountInBuyCurrency: 2n,
},
},
slippageBps: 0,
Expand Down Expand Up @@ -200,7 +204,7 @@ adapterNames.forEach((adapterName) => {

describe('getExplorerUrl', () => {
it('should return explorer url', () => {
expect(provider.getExplorerUrl('123')).toEqual('https://app.across.to/transactions')
expect(provider.getExplorerUrl('123', '0xaaa')).toEqual('https://app.across.to/transaction/0xaaa')
})
})

Expand Down Expand Up @@ -262,7 +266,7 @@ adapterNames.forEach((adapterName) => {

const gasLimit = await provider.getGasLimitEstimationForHook(request)

expect(gasLimit).toEqual(DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION)
expect(gasLimit).toEqual(DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION + DEFAULT_EXTRA_GAS_FOR_HOOK_ESTIMATION)
})
})
})
Expand Down
Loading
Loading