Skip to content

Commit

Permalink
feat: add arbitrum support to cowswap (#7132)
Browse files Browse the repository at this point in the history
* feat: add arbitrum support to cowswap

* stub env for tests

---------

Co-authored-by: Apotheosis <[email protected]>
  • Loading branch information
kaladinlight and 0xApotheosis authored Jun 14, 2024
1 parent c26628b commit b5a6c8d
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 20 deletions.
1 change: 1 addition & 0 deletions .env.base
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ REACT_APP_FEATURE_READ_ONLY_ASSETS=true
# swapper feature flags - other .env files will override these
REACT_APP_FEATURE_COWSWAP=true
REACT_APP_FEATURE_COWSWAP_GNOSIS=true
REACT_APP_FEATURE_COWSWAP_ARBITRUM=false
REACT_APP_FEATURE_LIFI_SWAP=true
REACT_APP_FEATURE_ONE_INCH=false
REACT_APP_FEATURE_THOR_SWAP=true
Expand Down
1 change: 1 addition & 0 deletions .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
REACT_APP_FEATURE_RFOX=true
REACT_APP_FEATURE_RFOX_REWARDS_TAB=true
REACT_APP_FEATURE_ARBITRUM_BRIDGE=true
REACT_APP_FEATURE_COWSWAP_ARBITRUM=true

# logging
REACT_APP_REDUX_WINDOW=false
Expand Down
1 change: 1 addition & 0 deletions .env.develop
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ REACT_APP_FEATURE_CHATWOOT=true
REACT_APP_FEATURE_RFOX=true
REACT_APP_FEATURE_RFOX_REWARDS_TAB=true
REACT_APP_FEATURE_ARBITRUM_BRIDGE=true
REACT_APP_FEATURE_COWSWAP_ARBITRUM=true

# mixpanel
REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Button, Card, CardBody, Link, Tooltip, VStack } from '@chakra-ui/react'
import type {
SupportedTradeQuoteStepIndex,
SwapperName,
TradeQuoteStep,
} from '@shapeshiftoss/swapper'
import type { SupportedTradeQuoteStepIndex, TradeQuoteStep } from '@shapeshiftoss/swapper'
import { SwapperName } from '@shapeshiftoss/swapper'
import type { KnownChainIds } from '@shapeshiftoss/types'
import { useCallback, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
Expand Down Expand Up @@ -77,7 +74,7 @@ export const HopTransactionStep = ({
txLink: getTxLink({
name: tradeQuoteStep.source,
defaultExplorerBaseUrl: tradeQuoteStep.buyAsset.explorerTxLink,
tradeId: buyTxHash,
txId: buyTxHash,
}),
txHash: buyTxHash,
})
Expand All @@ -88,7 +85,13 @@ export const HopTransactionStep = ({
txLink: getTxLink({
name: tradeQuoteStep.source,
defaultExplorerBaseUrl: tradeQuoteStep.sellAsset.explorerTxLink,
tradeId: sellTxHash,
...(tradeQuoteStep.source === SwapperName.CowSwap
? {
tradeId: sellTxHash,
}
: {
txId: sellTxHash,
}),
}),
txHash: sellTxHash,
})
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const validators = {
REACT_APP_FEATURE_LIFI_SWAP: bool({ default: false }),
REACT_APP_FEATURE_COWSWAP: bool({ default: false }),
REACT_APP_FEATURE_COWSWAP_GNOSIS: bool({ default: false }),
REACT_APP_FEATURE_COWSWAP_ARBITRUM: bool({ default: false }),
REACT_APP_FEATURE_JAYPEGZ: bool({ default: false }),
REACT_APP_FEATURE_OPTIMISM: bool({ default: false }),
REACT_APP_FEATURE_BNBSMARTCHAIN: bool({ default: false }),
Expand Down
12 changes: 10 additions & 2 deletions src/lib/swapper/swappers/CowSwapper/CowSwapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { describe, expect, it, vi } from 'vitest'
import {
BTC,
ETH,
ETH_ARBITRUM,
FOX_GNOSIS,
FOX_MAINNET,
USDC_ARBITRUM,
WBTC,
WETH,
XDAI,
Expand Down Expand Up @@ -53,6 +55,8 @@ vi.mock('state/slices/assetsSlice/selectors', async () => {
WBTC,
WETH,
XDAI,
ETH_ARBITRUM,
USDC_ARBITRUM,
} = require('lib/swapper/swappers/utils/test-data/assets')

const actual = await vi.importActual('state/slices/assetsSlice/selectors')
Expand All @@ -66,11 +70,13 @@ vi.mock('state/slices/assetsSlice/selectors', async () => {
[WBTC.assetId]: WBTC,
[WETH.assetId]: WETH,
[XDAI.assetId]: XDAI,
[ETH_ARBITRUM.assetId]: ETH_ARBITRUM,
[USDC_ARBITRUM.assetId]: USDC_ARBITRUM,
})),
}
})

const ASSETS = [ETH, WBTC, WETH, BTC, FOX_MAINNET, XDAI]
const ASSETS = [ETH, WBTC, WETH, BTC, FOX_MAINNET, XDAI, ETH_ARBITRUM, USDC_ARBITRUM]

describe('CowSwapper', () => {
describe('filterAssetIdsBySellable', () => {
Expand All @@ -83,15 +89,17 @@ describe('CowSwapper', () => {
WBTC.assetId,
WETH.assetId,
FOX_MAINNET.assetId,
USDC_ARBITRUM.assetId,
])
})

it('returns array filtered out of unsupported tokens', async () => {
const assetIds = [FOX_MAINNET, FOX_GNOSIS, BTC]
const assetIds = [FOX_MAINNET, FOX_GNOSIS, USDC_ARBITRUM, BTC]

expect(await cowSwapper.filterAssetIdsBySellable(assetIds)).toEqual([
FOX_MAINNET.assetId,
FOX_GNOSIS.assetId,
USDC_ARBITRUM.assetId,
])
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import { Ok } from '@sniptt/monads'
import type { AxiosResponse } from 'axios'
import { describe, expect, it, vi } from 'vitest'

import { ETH, FOX_MAINNET, USDC_GNOSIS, WETH, XDAI } from '../../utils/test-data/assets'
import {
ETH,
ETH_ARBITRUM,
FOX_MAINNET,
USDC_ARBITRUM,
USDC_GNOSIS,
WETH,
XDAI,
} from '../../utils/test-data/assets'
import type { CowSwapQuoteResponse } from '../types'
import {
COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS,
Expand Down Expand Up @@ -112,6 +120,20 @@ const expectedApiInputUsdcGnosisToXdai: CowSwapSellQuoteApiInput = {
validTo: 1656797787,
}

const expectedApiInputUsdcToEthArbitrum: CowSwapSellQuoteApiInput = {
appData:
'{"appCode":"shapeshift","metadata":{"orderClass":{"orderClass":"market"},"quote":{"slippageBips":"50"}},"version":"0.9.0"}',
appDataHash: '0x9b3c15b566e3b432f1ba3533bb0b071553fd03cec359caf3e6559b29fec1e62e',
buyToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
from: '0x0000000000000000000000000000000000000000',
kind: 'sell',
partiallyFillable: false,
receiver: '0x0000000000000000000000000000000000000000',
sellAmountBeforeFee: '500000',
sellToken: '0xaf88d065e77c8cc2239327c5edb3a432268e5831',
validTo: 1656797787,
}

const expectedTradeQuoteWethToFox: TradeQuote = {
id: '123',
receiveAddress: '0x0000000000000000000000000000000000000000',
Expand Down Expand Up @@ -211,6 +233,39 @@ const expectedTradeQuoteUsdcToXdai: TradeQuote = {
],
}

const expectedTradeQuoteUsdcToEthArbitrum: TradeQuote = {
id: '123',
receiveAddress: '0x0000000000000000000000000000000000000000',
affiliateBps: '0',
potentialAffiliateBps: '0',
rate: '0.00028787191526496171',
slippageTolerancePercentageDecimal: '0.005',
steps: [
{
allowanceContract: '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110',
rate: '0.00028787191526496171',
estimatedExecutionTimeMs: undefined,
feeData: {
networkFeeCryptoBaseUnit: '0',
protocolFees: {
[USDC_ARBITRUM.assetId]: {
amountCryptoBaseUnit: '7944',
requiresBalance: false,
asset: USDC_ARBITRUM,
},
},
},
sellAmountIncludingProtocolFeesCryptoBaseUnit: '500000',
buyAmountBeforeFeesCryptoBaseUnit: '143935957632481',
buyAmountAfterFeesCryptoBaseUnit: '141649103137616',
source: SwapperName.CowSwap,
buyAsset: ETH_ARBITRUM,
sellAsset: USDC_ARBITRUM,
accountNumber: 0,
},
],
}

const expectedTradeQuoteSmallAmountWethToFox: TradeQuote = {
id: '123',
receiveAddress: '0x0000000000000000000000000000000000000000',
Expand Down Expand Up @@ -403,6 +458,50 @@ describe('getCowTradeQuote', () => {
)
})

it('should call cowService with correct parameters, handle the fees and return the correct trade quote when buying ETH on Arbitrum', async () => {
const input: GetTradeQuoteInput = {
chainId: KnownChainIds.ArbitrumMainnet,
sellAsset: USDC_ARBITRUM,
buyAsset: ETH_ARBITRUM,
sellAmountIncludingProtocolFeesCryptoBaseUnit: '500000',
accountNumber: 0,
receiveAddress: DEFAULT_ADDRESS,
affiliateBps: '0',
potentialAffiliateBps: '0',
supportsEIP1559: false,
allowMultiHop: false,
slippageTolerancePercentageDecimal: '0.005', // 0.5%
}

mockedCowService.post.mockReturnValue(
Promise.resolve(
Ok({
data: {
id: 123,
quote: {
...expectedApiInputUsdcToEthArbitrum,
sellAmountBeforeFee: undefined,
sellAmount: '492056',
buyAmount: '141649103137616',
feeAmount: '7944',
sellTokenBalance: ERC20_TOKEN_BALANCE,
buyTokenBalance: ERC20_TOKEN_BALANCE,
},
},
} as unknown as AxiosResponse<CowSwapQuoteResponse>),
),
)

const maybeTradeQuote = await getCowSwapTradeQuote(input)

expect(maybeTradeQuote.isOk()).toBe(true)
expect(maybeTradeQuote.unwrap()).toEqual(expectedTradeQuoteUsdcToEthArbitrum)
expect(cowService.post).toHaveBeenCalledWith(
'https://api.cow.fi/arbitrum_one/api/v1/quote/',
expectedApiInputUsdcToEthArbitrum,
)
})

it('should call cowService with correct parameters and return quote with original sellAmount when selling a very small amount of WETH', async () => {
const input: GetTradeQuoteInput = {
chainId: KnownChainIds.EthereumMainnet,
Expand Down
6 changes: 5 additions & 1 deletion src/lib/swapper/swappers/CowSwapper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ export type CowSwapQuoteError = {
export enum CowNetwork {
Mainnet = 'mainnet',
Xdai = 'xdai',
ArbitrumOne = 'arbitrum_one',
}

export type CowChainId = KnownChainIds.EthereumMainnet | KnownChainIds.GnosisMainnet
export type CowChainId =
| KnownChainIds.EthereumMainnet
| KnownChainIds.GnosisMainnet
| KnownChainIds.ArbitrumMainnet

export type CowSwapGetTradesResponse = {
txHash: string
Expand Down
28 changes: 19 additions & 9 deletions src/lib/swapper/swappers/CowSwapper/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@ import { KnownChainIds } from '@shapeshiftoss/types'
import { zeroAddress } from 'viem'
import type { SupportedChainIds } from 'lib/swapper/types'

import type { CowChainId } from '../types'

export const MIN_COWSWAP_USD_TRADE_VALUES_BY_CHAIN_ID: Record<CowChainId, string> = {
[KnownChainIds.EthereumMainnet]: '20',
[KnownChainIds.GnosisMainnet]: '0.01',
}

export const DEFAULT_ADDRESS = zeroAddress

export const COW_SWAP_VAULT_RELAYER_ADDRESS = '0xc92e8bdf79f0507f65a392b0ab4667716bfe0110'
Expand All @@ -24,9 +17,26 @@ export const ERC20_TOKEN_BALANCE = 'erc20'
export const COW_SWAP_NATIVE_ASSET_MARKER_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'

export const SUPPORTED_CHAIN_IDS: ChainId[] = [
KnownChainIds.GnosisMainnet,
KnownChainIds.EthereumMainnet,
]
KnownChainIds.GnosisMainnet,
KnownChainIds.ArbitrumMainnet,
].filter(chainId => {
if (
process.env['REACT_APP_FEATURE_COWSWAP_GNOSIS'] !== 'true' &&
chainId === KnownChainIds.GnosisMainnet
) {
return false
}

if (
process.env['REACT_APP_FEATURE_COWSWAP_ARBITRUM'] !== 'true' &&
chainId === KnownChainIds.ArbitrumMainnet
) {
return false
}

return true
})

export const COW_SWAP_SUPPORTED_CHAIN_IDS: SupportedChainIds = {
sell: SUPPORTED_CHAIN_IDS,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/swapper/swappers/CowSwapper/utils/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const getCowswapNetwork = (chainId: ChainId): Result<CowNetwork, SwapErro
return Ok(CowNetwork.Mainnet)
case KnownChainIds.GnosisMainnet:
return Ok(CowNetwork.Xdai)
case KnownChainIds.ArbitrumMainnet:
return Ok(CowNetwork.ArbitrumOne)
default:
return Err(
makeSwapErrorRight({
Expand Down
30 changes: 30 additions & 0 deletions src/lib/swapper/swappers/utils/test-data/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,36 @@ export const USDC_GNOSIS: Asset = {
explorerTxLink: 'https://gnosis.blockscout.com/tx/',
}

export const ETH_ARBITRUM: Asset = {
assetId: 'eip155:42161/slip44:60',
chainId: 'eip155:42161',
name: 'Ethereum on Arbitrum One',
networkName: 'Arbitrum One',
symbol: 'ETH',
precision: 18,
color: '#5C6BC0',
networkColor: '#213147',
icon: 'https://assets.coincap.io/assets/icons/256/eth.png',
networkIcon:
'https://assets.coingecko.com/coins/images/16547/large/photo_2023-03-29_21.47.00.jpeg?1680097630',
explorer: 'https://arbiscan.io',
explorerAddressLink: 'https://arbiscan.io/address/',
explorerTxLink: 'https://arbiscan.io/tx/',
}

export const USDC_ARBITRUM: Asset = {
assetId: 'eip155:42161/erc20:0xaf88d065e77c8cc2239327c5edb3a432268e5831',
chainId: 'eip155:42161',
name: 'USDC on Arbitrum One',
precision: 6,
color: '#2E7ACD',
icon: 'https://assets.coingecko.com/coins/images/6319/thumb/usdc.png?1696506694',
symbol: 'USDC',
explorer: 'https://arbiscan.io',
explorerAddressLink: 'https://arbiscan.io/address/',
explorerTxLink: 'https://arbiscan.io/tx/',
}

export const AVAX: Asset = {
assetId: avalancheAssetId,
chainId: avalancheChainId,
Expand Down
2 changes: 2 additions & 0 deletions src/setupVitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ moduleAlias.addAlias('ethers', (fromPath: string) => {

vi.hoisted(() => {
vi.stubEnv('REACT_APP_FEATURE_MIXPANEL', 'false')
vi.stubEnv('REACT_APP_FEATURE_COWSWAP_GNOSIS', 'true')
vi.stubEnv('REACT_APP_FEATURE_COWSWAP_ARBITRUM', 'true')
})

beforeAll(() => {
Expand Down
2 changes: 2 additions & 0 deletions src/state/slices/preferencesSlice/preferencesSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type FeatureFlags = {
TradeRates: boolean
Cowswap: boolean
CowswapGnosis: boolean
CowswapArbitrum: boolean
ZrxSwap: boolean
Mixpanel: boolean
LifiSwap: boolean
Expand Down Expand Up @@ -122,6 +123,7 @@ const initialState: Preferences = {
TradeRates: getConfig().REACT_APP_FEATURE_TRADE_RATES,
Cowswap: getConfig().REACT_APP_FEATURE_COWSWAP,
CowswapGnosis: getConfig().REACT_APP_FEATURE_COWSWAP_GNOSIS,
CowswapArbitrum: getConfig().REACT_APP_FEATURE_COWSWAP_ARBITRUM,
ZrxSwap: getConfig().REACT_APP_FEATURE_ZRX_SWAP,
LifiSwap: getConfig().REACT_APP_FEATURE_LIFI_SWAP,
CovalentJaypegs: getConfig().REACT_APP_FEATURE_COVALENT_JAYPEGS,
Expand Down
1 change: 1 addition & 0 deletions src/test/mocks/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const mockStore: ReduxState = {
ThorSwapStreamingSwaps: false,
Cowswap: false,
CowswapGnosis: false,
CowswapArbitrum: false,
Yat: false,
WalletConnectToDapps: false,
WalletConnectToDappsV2: false,
Expand Down

0 comments on commit b5a6c8d

Please sign in to comment.