diff --git a/assets/icons/bebop.svg b/assets/icons/bebop.svg new file mode 100644 index 000000000..1d451b365 --- /dev/null +++ b/assets/icons/bebop.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/matomoClickEvents.ts b/config/matomoClickEvents.ts index 4d213a932..7384bf81d 100644 --- a/config/matomoClickEvents.ts +++ b/config/matomoClickEvents.ts @@ -49,6 +49,7 @@ export const enum MATOMO_CLICK_EVENTS_TYPES { withdrawalOtherFactorsTooltipMode = 'withdrawalOtherFactorsTooltipMode', withdrawalFAQtooltipEthAmount = 'withdrawalFAQtooltipEthAmount', withdrawalGoTo1inch = 'withdrawalGoTo1inch', + withdrawalGoToBebop = 'withdrawalGoToBebop', withdrawalGoToCowSwap = 'withdrawalGoToCowSwap', withdrawalGoToParaswap = 'withdrawalGoToParaswap', withdrawalGoToOpenOcean = 'withdrawalGoToOpenOcean', @@ -279,6 +280,11 @@ export const MATOMO_CLICK_EVENTS: Record< 'Click on «Go to 1inch» in aggregators list on Request tab', 'eth_withdrawals_request_go_to_1inch', ], + [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToBebop]: [ + 'Ethereum_Withdrawals_Widget', + 'Click on «Go to Bebop» in aggregators list on Request tab', + 'eth_withdrawals_request_go_to_1inch', + ], [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToCowSwap]: [ 'Ethereum_Withdrawals_Widget', 'Click on «Go to CowSwap» in aggregators list on Request tab', diff --git a/features/withdrawals/request/withdrawal-rates/icons.tsx b/features/withdrawals/request/withdrawal-rates/icons.tsx index e6343474f..a598f670d 100644 --- a/features/withdrawals/request/withdrawal-rates/icons.tsx +++ b/features/withdrawals/request/withdrawal-rates/icons.tsx @@ -2,6 +2,7 @@ import styled from 'styled-components'; import OpenOcean from 'assets/icons/open-ocean.svg'; import Paraswap from 'assets/icons/paraswap-circle.svg'; import Oneinch from 'assets/icons/oneinch-circle.svg'; +import Bebop from 'assets/icons/bebop.svg'; export const OpenOceanIcon = styled.img.attrs({ src: OpenOcean, @@ -23,3 +24,10 @@ export const OneInchIcon = styled.img.attrs({ })` display: block; `; + +export const BebopIcon = styled.img.attrs({ + src: Bebop, + alt: 'Bebop', +})` + display: block; +`; diff --git a/features/withdrawals/request/withdrawal-rates/integrations.ts b/features/withdrawals/request/withdrawal-rates/integrations.ts index a4db98ad6..18be27353 100644 --- a/features/withdrawals/request/withdrawal-rates/integrations.ts +++ b/features/withdrawals/request/withdrawal-rates/integrations.ts @@ -1,15 +1,17 @@ import { Zero } from '@ethersproject/constants'; import { getTokenAddress, CHAINS, TOKENS } from '@lido-sdk/constants'; import { BigNumber } from 'ethers'; +import { getAddress } from 'ethers/lib/utils.js'; import { formatEther } from '@ethersproject/units'; import { getOneInchRate } from 'utils/get-one-inch-rate'; +import { getBebopRate } from 'utils/get-bebop-rate'; import { getOpenOceanRate } from 'utils/get-open-ocean-rate'; import { standardFetcher } from 'utils/standardFetcher'; import { OPEN_OCEAN_REFERRAL_ADDRESS } from 'config/external-links'; import { MATOMO_CLICK_EVENTS_TYPES } from 'config/matomoClickEvents'; -import { OneInchIcon, OpenOceanIcon, ParaSwapIcon } from './icons'; +import { BebopIcon, OneInchIcon, OpenOceanIcon, ParaSwapIcon } from './icons'; import type { DexWithdrawalApi, @@ -124,6 +126,23 @@ const getOneInchWithdrawalRate: GetRateType = async (params) => { }; }; +const getBebopWithdrawalRate: GetRateType = async ({ amount, token }) => { + try { + if (amount.gt(Zero)) { + return await getBebopRate(amount, token, 'ETH'); + } + } catch (e) { + console.warn( + '[getOneInchWithdrawalRate] Failed to receive withdraw rate', + e, + ); + } + return { + rate: null, + toReceive: null, + }; +}; + const dexWithdrawalMap: DexWithdrawalIntegrationMap = { 'open-ocean': { title: 'OpenOcean', @@ -158,6 +177,16 @@ const dexWithdrawalMap: DexWithdrawalIntegrationMap = { token == TOKENS.STETH ? 'stETH' : 'wstETH' }/ETH?sourceTokenAmount=${formatEther(amount)}`, }, + bebop: { + title: 'Bebop', + icon: BebopIcon, + fetcher: getBebopWithdrawalRate, + matomoEvent: MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToBebop, + link: (amount, token) => + `https://bebop.xyz/trade?network=ethereum&buy=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&sell=${getAddress( + getTokenAddress(CHAINS.Mainnet, token), + )}&sellAmounts=${formatEther(amount)}`, + }, } as const; export const getDexConfig = (dexKey: DexWithdrawalApi) => diff --git a/features/withdrawals/request/withdrawal-rates/types.ts b/features/withdrawals/request/withdrawal-rates/types.ts index d434b7ab2..5bc9458a9 100644 --- a/features/withdrawals/request/withdrawal-rates/types.ts +++ b/features/withdrawals/request/withdrawal-rates/types.ts @@ -19,7 +19,7 @@ export type SingleWithdrawalRateResult = { toReceive: BigNumber | null; }; -export type DexWithdrawalApi = 'paraswap' | 'open-ocean' | 'one-inch'; +export type DexWithdrawalApi = 'paraswap' | 'open-ocean' | 'one-inch' | 'bebop'; export type DexWithdrawalIntegration = { title: string; diff --git a/features/withdrawals/withdrawals-constants/index.ts b/features/withdrawals/withdrawals-constants/index.ts index 037375acc..3c7f39a62 100644 --- a/features/withdrawals/withdrawals-constants/index.ts +++ b/features/withdrawals/withdrawals-constants/index.ts @@ -13,6 +13,6 @@ export const VALIDATION_CONTEXT_TIMEOUT = 4000; export const ENABLED_WITHDRAWAL_DEXES: DexWithdrawalApi[] = [ 'one-inch', - 'open-ocean', + 'bebop', 'paraswap', ]; diff --git a/utils/get-bebop-rate.ts b/utils/get-bebop-rate.ts new file mode 100644 index 000000000..79536b911 --- /dev/null +++ b/utils/get-bebop-rate.ts @@ -0,0 +1,81 @@ +import { CHAINS, TOKENS, getTokenAddress } from '@lido-sdk/constants'; +import { BigNumber } from 'ethers'; +import { standardFetcher } from './standardFetcher'; +import { ESTIMATE_ACCOUNT } from 'config'; +import { getAddress } from 'ethers/lib/utils.js'; + +type BebopGetQuotePartial = { + routes: { + quote: { + buyTokens: Record< + string, + { + amount: string; + amountBeforeFee: string; + } + >; + sellTokens: Record< + string, + { + amount: string; + priceBeforeFee: number; + } + >; + }; + }[]; +}; + +type RateToken = TOKENS.STETH | TOKENS.WSTETH | 'ETH'; + +type RateCalculationResult = { rate: number; toReceive: BigNumber }; + +const getRateTokenAddress = (token: RateToken) => + token === 'ETH' + ? '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' + : getTokenAddress(CHAINS.Mainnet, token); + +export const getBebopRate = async ( + amount: BigNumber, + fromToken: RateToken, + toToken: RateToken, +): Promise => { + const basePath = 'https://api.bebop.xyz/router/ethereum/v1/quote'; + + const sell_tokens = getAddress(getRateTokenAddress(fromToken)); + const buy_tokens = getAddress(getRateTokenAddress(toToken)); + + const params = new URLSearchParams({ + sell_tokens, + buy_tokens, + taker_address: ESTIMATE_ACCOUNT, + sell_amounts: amount.toString(), + approval_type: 'Standard', + }); + + const data = await standardFetcher( + `${basePath}/?${params.toString()}`, + ); + + const bestRoute = data.routes.toSorted( + (r1, r2) => + r2.quote.sellTokens[sell_tokens].priceBeforeFee - + r1.quote.sellTokens[sell_tokens].priceBeforeFee, + )[0]; + + if ( + bestRoute && + bestRoute.quote.sellTokens[sell_tokens] && + bestRoute.quote.buyTokens[buy_tokens] + ) { + const rate = data.routes[0].quote.sellTokens[sell_tokens].priceBeforeFee; + + const toAmount = BigNumber.from( + data.routes[0].quote.buyTokens[buy_tokens].amountBeforeFee, + ); + return { + rate, + toReceive: toAmount, + }; + } + throw new Error('[getBebopRate] Could not get quote, invalid response body'); +};