From 5412f4cbdc0c90ffea513cccbbba4568ea85e16a Mon Sep 17 00:00:00 2001 From: Douglas Daniel Date: Thu, 23 May 2024 16:47:09 -0500 Subject: [PATCH] feat(wallet): Enable Bridging in UI --- .../browser/brave_wallet_constants.h | 14 +- .../desktop/wallet-menus/asset-item-menu.tsx | 10 +- .../brave_wallet_ui/options/nav-options.ts | 30 +- components/brave_wallet_ui/page/container.tsx | 9 + .../composer_controls.style.ts | 44 +- .../composer_controls/composer_controls.tsx | 57 +- .../from_asset/from_asset.style.ts | 15 +- .../composer_ui/from_asset/from_asset.tsx | 18 +- .../select_address_button.style.ts | 42 +- .../select_address_button.tsx | 10 +- .../select_button/select_button.style.ts | 50 +- .../select_button/select_button.tsx | 15 +- .../select_token_modal/select_token_modal.tsx | 176 ++++- .../composer_ui/shared_composer.style.ts | 47 +- .../composer_ui/to_asset/to_asset.style.ts | 2 +- .../screens/composer_ui/to_asset/to_asset.tsx | 12 +- .../virtualized_tokens_list.tsx | 4 +- .../screens/send/send_screen/send.style.ts | 2 +- .../screens/send/send_screen/send_screen.tsx | 27 +- .../screens/swap/assets/lp-icons/jupiter.svg | 1 + .../screens/swap/assets/lp-icons/lifi.svg | 1 + .../swap/quote-info/quote-info.style.ts | 99 ++- .../components/swap/quote-info/quote-info.tsx | 647 +++++++++++++----- .../page/screens/swap/constants/LpMetadata.ts | 5 + .../page/screens/swap/constants/types.ts | 5 +- .../page/screens/swap/hooks/useSwap.ts | 116 +++- .../page/screens/swap/swap.tsx | 126 ++-- .../page/screens/swap/swap.utils.ts | 13 +- components/brave_wallet_ui/stories/locale.ts | 14 +- .../brave_wallet_ui/utils/datetime-utils.ts | 16 +- .../brave_wallet_ui/utils/routes-utils.ts | 13 +- components/resources/wallet_strings.grdp | 16 +- ui/webui/resources/BUILD.gn | 1 + 33 files changed, 1034 insertions(+), 623 deletions(-) create mode 100644 components/brave_wallet_ui/page/screens/swap/assets/lp-icons/jupiter.svg create mode 100644 components/brave_wallet_ui/page/screens/swap/assets/lp-icons/lifi.svg diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index 6341457d5e874..07299656e607d 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -257,6 +257,17 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletOwned", IDS_BRAVE_WALLET_OWNED}, {"braveWalletNotOwned", IDS_BRAVE_WALLET_NOT_OWNED}, {"braveWalletAmount24H", IDS_BRAVE_WALLET_AMOUNT_24H}, + {"braveWalletChooseAssetToBridge", IDS_BRAVE_WALLET_CHOOSE_ASSET_TO_BRIDGE}, + {"braveWalletBridgingVia", IDS_BRAVE_WALLET_BRIDGING_VIA}, + {"braveWalletSwappingVia", IDS_BRAVE_WALLET_SWAPPING_VIA}, + {"braveWalletReviewSwap", IDS_BRAVE_WALLET_REVIEW_SWAP}, + {"braveWalletEstFees", IDS_BRAVE_WALLET_EST_FEES}, + {"braveWalletEstTime", IDS_BRAVE_WALLET_EST_TIME}, + {"braveWalletExchangeRate", IDS_BRAVE_WALLET_EXCHANGE_RATE}, + {"braveWalletExchangeFor", IDS_BRAVE_WALLET_EXCHANGE_FOR}, + {"braveWalletProviderApi", IDS_BRAVE_WALLET_PROVIDER_API}, + {"braveWalletRecipient", IDS_BRAVE_WALLET_RECIPIENT}, + {"braveWalletReviewBridge", IDS_BRAVE_WALLET_REVIEW_BRIDGE}, {"braveWalletSlippageToleranceWarning", IDS_BRAVE_WALLET_SLIPPAGE_TOLERANCE_WARNING}, {"braveWalletSlippageToleranceExplanation", @@ -1291,7 +1302,6 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = { // Brave Swap {"braveSwap", IDS_BRAVE_SWAP}, - {"braveSwapReviewOrder", IDS_BRAVE_SWAP_REVIEW_ORDER}, {"braveSwapApproveToken", IDS_BRAVE_SWAP_APPROVE_TOKEN}, {"braveSwapInsufficientBalance", IDS_BRAVE_SWAP_INSUFFICIENT_BALANCE}, {"braveSwapInsufficientLiquidity", IDS_BRAVE_SWAP_INSUFFICIENT_LIQUIDITY}, @@ -1311,14 +1321,12 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = { IDS_BRAVE_SWAP_HIDE_TOKENS_WITH_ZERO_BALANCES}, {"braveSwapSearchToken", IDS_BRAVE_SWAP_SEARCH_TOKEN}, {"braveSwapOption", IDS_BRAVE_SWAP_OPTION}, - {"braveSwapRate", IDS_BRAVE_SWAP_RATE}, {"braveSwapPriceImpact", IDS_BRAVE_SWAP_PRICE_IMPACT}, {"braveSwapMinimumReceivedAfterSlippage", IDS_BRAVE_SWAP_MINIMUM_RECEIVED_AFTER_SLIPPAGE}, {"braveSwapNetworkFee", IDS_BRAVE_SWAP_NETWORK_FEE}, {"braveSwapBraveFee", IDS_BRAVE_SWAP_BRAVE_FEE}, {"braveSwapFree", IDS_BRAVE_SWAP_FREE}, - {"braveSwapLiquidityProvider", IDS_BRAVE_SWAP_LIQUIDITY_PROVIDER}, {"braveSwapSwapAndSend", IDS_BRAVE_SWAP_SWAP_AND_SEND}, {"braveSwapNoExtraFees", IDS_BRAVE_SWAP_NO_EXTRA_FEES}, {"braveSwapConfirmAddress", IDS_BRAVE_SWAP_CONFIRM_ADDRESS}, diff --git a/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx b/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx index 6cd289f982fb2..30a45cb0562a0 100644 --- a/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx +++ b/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx @@ -27,7 +27,7 @@ import { makeDepositFundsRoute, makeFundWalletRoute, makeSendRoute, - makeSwapRoute + makeSwapOrBridgeRoute } from '../../../utils/routes-utils' import { getAssetIdKey } from '../../../utils/asset-utils' @@ -115,7 +115,13 @@ export const AssetItemMenu = (props: Props) => { const onClickSwap = React.useCallback(() => { if (account) { - history.push(makeSwapRoute({ fromToken: asset, fromAccount: account })) + history.push( + makeSwapOrBridgeRoute({ + fromToken: asset, + fromAccount: account, + routeType: 'swap' + }) + ) } }, [account, history, asset]) diff --git a/components/brave_wallet_ui/options/nav-options.ts b/components/brave_wallet_ui/options/nav-options.ts index be9a54a0b3064..f5b477e868ccc 100644 --- a/components/brave_wallet_ui/options/nav-options.ts +++ b/components/brave_wallet_ui/options/nav-options.ts @@ -42,6 +42,13 @@ export const isValidPanelNavigationOption = ( ) } +const BridgeOption: NavOption = { + id: 'bridge', + name: 'braveWalletBridge', + icon: 'web3-bridge', + route: WalletRoutes.Bridge +} + export const BuySendSwapDepositOptions: NavOption[] = [ { id: 'buy', @@ -61,6 +68,7 @@ export const BuySendSwapDepositOptions: NavOption[] = [ icon: 'currency-exchange', route: WalletRoutes.Swap }, + BridgeOption, { id: 'deposit', name: 'braveWalletDepositCryptoButton', @@ -212,25 +220,3 @@ export const AccountDetailsOptions: NavOption[] = [ route: AccountPageTabs.AccountTransactionsSub } ] - -export const SendSwapBridgeOptions: NavOption[] = [ - { - id: 'send', - name: 'braveWalletSend', - icon: 'send', - route: WalletRoutes.Send - }, - { - id: 'swap', - name: 'braveWalletSwap', - icon: 'currency-exchange', - route: WalletRoutes.Swap - } - // Bridge is not yet implemented - // { - // id: 'bridge', - // name: 'braveWalletBridge', - // icon: 'bridge', - // route: WalletRoutes.Bridge - // } -] diff --git a/components/brave_wallet_ui/page/container.tsx b/components/brave_wallet_ui/page/container.tsx index 1c411b1f1220f..b1f77d8bd52dc 100644 --- a/components/brave_wallet_ui/page/container.tsx +++ b/components/brave_wallet_ui/page/container.tsx @@ -182,6 +182,15 @@ export const Container = () => { + + + + ` - --leo-icon-size: 20px; - color: ${leo.color.icon.interactive}; - margin-left: 8px; - transition-duration: 0.3s; - transform: ${(p) => (p.isOpen ? 'rotate(180deg)' : 'unset')}; -` - export const SettingsIcon = styled(Icon).attrs({ name: 'settings' })` diff --git a/components/brave_wallet_ui/page/screens/composer_ui/composer_controls/composer_controls.tsx b/components/brave_wallet_ui/page/screens/composer_ui/composer_controls/composer_controls.tsx index d6c8af50982ba..5ee316fbdb7d7 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/composer_controls/composer_controls.tsx +++ b/components/brave_wallet_ui/page/screens/composer_ui/composer_controls/composer_controls.tsx @@ -4,29 +4,16 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import * as React from 'react' -import { useHistory, useLocation } from 'react-router-dom' // Hooks import { useOnClickOutside } from '../../../../common/hooks/useOnClickOutside' -// Utils -import { getLocale } from '../../../../../common/locale' - -// Types -import { NavOption } from '../../../../constants/types' - -// Options -import { SendSwapBridgeOptions } from '../../../../options/nav-options' - // Styled Components import { - ComposerButton, - ComposerButtonMenu, FlipButton, FlipIcon, SettingsButton, - SettingsIcon, - CaratIcon + SettingsIcon } from './composer_controls.style' import { Row } from '../../../../components/shared/style' @@ -39,10 +26,6 @@ interface Props { export const ComposerControls = (props: Props) => { const { onFlipAssets, onOpenSettings, flipAssetsDisabled } = props - // Routing - const history = useHistory() - const { pathname: walletLocation } = useLocation() - // State const [showComposerMenu, setShowComposerMenu] = React.useState(false) @@ -56,31 +39,6 @@ export const ComposerControls = (props: Props) => { showComposerMenu ) - // Methods - const onChange = (option?: NavOption) => { - if (showComposerMenu && option) { - history.push(option.route) - } - setShowComposerMenu((prev) => !prev) - } - - // Computed - const selectedOption = SendSwapBridgeOptions.find((option) => - walletLocation.includes(option.route) - ) - - // Moves the selectedOption to the front of the list. - const buttonOptions = showComposerMenu - ? [ - SendSwapBridgeOptions.find( - (option) => option.id === selectedOption?.id - ), - ...SendSwapBridgeOptions.filter( - (option) => option.id !== selectedOption?.id - ) - ] - : [selectedOption] - return ( {onFlipAssets && ( @@ -91,19 +49,6 @@ export const ComposerControls = (props: Props) => { )} - {buttonOptions ? ( - - {buttonOptions.map((option, i) => ( - onChange(option)} - > - {getLocale(option?.name ?? '')} - {i === 0 && } - - ))} - - ) : null} {onOpenSettings && ( diff --git a/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.style.ts b/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.style.ts index 3eb8b44f087f9..0d745452ed72e 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.style.ts +++ b/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.style.ts @@ -14,22 +14,19 @@ import { import { Column, Text, Row } from '../../../../components/shared/style' export const Wrapper = styled(Column)` - padding: 32px 32px 0px 32px; + padding: 24px 24px 0px 24px; @media screen and (max-width: ${layoutPanelWidth}px) { - padding: 16px 0px 0px 0px; + padding: 16px 16px 0px 16px; } ` -export const FromText = styled(Text)` - line-height: 26px; - color: ${leo.color.text.tertiary}; +export const BalanceText = styled(Text)` + line-height: 22px; margin-right: 4px; ` -export const BalanceText = styled(Text)` - line-height: 26px; - color: ${leo.color.text.primary}; - margin-right: 4px; +export const FromText = styled(BalanceText)` + color: ${leo.color.text.tertiary}; ` export const NetworkText = styled(Text)` diff --git a/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.tsx b/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.tsx index 5a6e5264725e8..8dac018b9637b 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.tsx +++ b/components/brave_wallet_ui/page/screens/composer_ui/from_asset/from_asset.tsx @@ -173,7 +173,7 @@ export const FromAsset = (props: Props) => { if (!token || !account) { return ( {getLocale('braveWalletFrom')} @@ -183,7 +183,7 @@ export const FromAsset = (props: Props) => { if (token.isNft) { return ( {account.name} @@ -196,7 +196,7 @@ export const FromAsset = (props: Props) => { justifyContent='flex-start' > {account.name}: @@ -213,8 +213,9 @@ export const FromAsset = (props: Props) => { ) : ( <> {formatTokenBalanceWithSymbol( tokenBalance, @@ -226,8 +227,9 @@ export const FromAsset = (props: Props) => { {token.coin === BraveWallet.CoinType.BTC && hasPendingBalance && ( <> {`(${getLocale('braveWalletAvailable')})`} @@ -280,13 +282,12 @@ export const FromAsset = (props: Props) => { fullWidth={true} justifyContent='space-between' alignItems='center' - padding='32px 0px 58px 0px' + padding='0px 0px 32px 0px' > {accountNameAndBalance} @@ -306,7 +307,6 @@ export const FromAsset = (props: Props) => { width='100%' alignItems='center' justifyContent='space-between' - padding='0px 16px 0px 6px' marginBottom={10} > @@ -333,12 +333,12 @@ export const FromAsset = (props: Props) => { width='100%' alignItems='center' justifyContent='space-between' - padding='0px 16px' > {network && token && ( {getLocale('braveWalletPortfolioAssetNetworkDescription') .replace('$1', '') diff --git a/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.style.ts b/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.style.ts index 046880292123e..f1076e14bf607 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.style.ts +++ b/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.style.ts @@ -4,51 +4,13 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import styled from 'styled-components' -import * as leo from '@brave/leo/tokens/css/variables' -import Icon from '@brave/leo/react/icon' // Shared Styles -import { WalletButton, Text } from '../../../../components/shared/style' +import { Button } from '../shared_composer.style' -export const Button = styled(WalletButton) <{ +export const SelectButton = styled(Button)<{ isPlaceholder: boolean }>` - cursor: pointer; - display: flex; - outline: none; - border: none; - flex-direction: row; - align-items: center; justify-content: space-between; - background-color: transparent; - border-radius: 12px; - padding: 10px 12px; - white-space: nowrap; width: ${(p) => (p.isPlaceholder ? 'unset' : '100%')}; - &:disabled { - opacity: 0.4; - cursor: not-allowed; - } - &:hover:not([disabled]) { - background-color: ${leo.color.container.background}; - } -` - -export const ButtonIcon = styled(Icon).attrs({ - name: 'carat-right' -})` - --leo-icon-size: 24px; - color: ${leo.color.icon.default}; - margin-left: 8px; -` - -export const ButtonText = styled(Text) <{ - isPlaceholder: boolean -}>` - overflow: hidden; - color: ${(p) => - p.isPlaceholder ? leo.color.text.tertiary : leo.color.text.primary}; - white-space: pre-wrap; - word-break: break-all; - font-weight: 600; ` diff --git a/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.tsx b/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.tsx index 90c96a517bd2b..94bf7eae48f4c 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.tsx +++ b/components/brave_wallet_ui/page/screens/composer_ui/select_address_button/select_address_button.tsx @@ -19,8 +19,9 @@ import { } from '../../../../components/shared/create-account-icon/create-account-icon' // Styled Components -import { Button, ButtonIcon, ButtonText } from './select_address_button.style' +import { SelectButton } from './select_address_button.style' import { Row } from '../../../../components/shared/style' +import { CaratIcon, ButtonText } from '../shared_composer.style' interface Props { onClick: () => void @@ -37,7 +38,7 @@ export const SelectAddressButton = (props: Props) => { ) return ( - + + ) } diff --git a/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.style.ts b/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.style.ts index 9b1c83d9c895e..6da8638897e61 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.style.ts +++ b/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.style.ts @@ -5,15 +5,13 @@ import styled from 'styled-components' import * as leo from '@brave/leo/tokens/css/variables' -import Icon from '@brave/leo/react/icon' // Shared Styles import { AssetIconProps, - AssetIconFactory, - WalletButton, - Text + AssetIconFactory } from '../../../../components/shared/style' +import { ButtonText } from '../shared_composer.style' export const AssetIcon = AssetIconFactory({ width: '40px', @@ -33,42 +31,6 @@ export const NetworkIconWrapper = styled.div` padding: 2px; ` -export const Button = styled(WalletButton)<{ - morePadding?: boolean - isNFT: boolean -}>` - --button-background-hover: #f5f6fc; - @media (prefers-color-scheme: dark) { - --button-background-hover: ${leo.color.container.background}; - } - cursor: pointer; - display: flex; - outline: none; - border: none; - flex-direction: row; - align-items: center; - justify-content: center; - background-color: transparent; - border-radius: ${(p) => (p.isNFT ? 8 : 12)}px; - justify-content: center; - padding: ${(p) => (p.morePadding ? 10 : 8)}px 12px; - white-space: nowrap; - :disabled { - cursor: not-allowed; - } - &:hover { - background-color: var(--button-background-hover); - } -` - -export const ButtonIcon = styled(Icon).attrs({ - name: 'carat-right' -})` - --leo-icon-size: 24px; - color: ${leo.color.icon.default}; - margin-left: 8px; -` - export const IconsWrapper = styled.div<{ marginRight?: number }>` @@ -80,15 +42,9 @@ export const IconsWrapper = styled.div<{ margin-right: ${(p) => p.marginRight || 6}px; ` -export const ButtonText = styled(Text)<{ +export const SelectButtonText = styled(ButtonText)<{ isNFT: boolean - isPlaceholder: boolean }>` max-width: ${(p) => (p.isNFT ? '100%' : 'unset')}; - overflow: hidden; - color: ${(p) => - p.isPlaceholder ? leo.color.text.tertiary : leo.color.text.primary}; white-space: ${(p) => (p.isNFT ? 'pre-wrap' : 'nowrap')}; - word-break: break-all; - font-weight: 500; ` diff --git a/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.tsx b/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.tsx index 733bcf46b79ab..306b8f2a4dc65 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.tsx +++ b/components/brave_wallet_ui/page/screens/composer_ui/select_button/select_button.tsx @@ -29,12 +29,11 @@ import { NftIcon } from '../../../../components/shared/nft-icon/nft-icon' import { AssetIcon, NetworkIconWrapper, - Button, - ButtonIcon, IconsWrapper, - ButtonText + SelectButtonText } from './select_button.style' import { Row } from '../../../../components/shared/style' +import { CaratIcon, Button } from '../shared_composer.style' interface Props { onClick: () => void @@ -69,8 +68,7 @@ export const SelectButton = (props: Props) => { return ( ) } diff --git a/components/brave_wallet_ui/page/screens/composer_ui/select_token_modal/select_token_modal.tsx b/components/brave_wallet_ui/page/screens/composer_ui/select_token_modal/select_token_modal.tsx index a6b8fe9ddd279..35208a1a4bf90 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/select_token_modal/select_token_modal.tsx +++ b/components/brave_wallet_ui/page/screens/composer_ui/select_token_modal/select_token_modal.tsx @@ -100,7 +100,7 @@ import { SearchInput } from './select_token_modal.style' -const checkIsDropdownOptionDisabled = ( +const checkIsSwapDropdownOptionDisabled = ( account: BraveWallet.AccountInfo, network: BraveWallet.NetworkInfo ) => { @@ -113,6 +113,13 @@ const checkIsDropdownOptionDisabled = ( return account.accountId.coin !== network.coin } +const checkIsBridgeNetworkDropdownOptionDisabled = ( + networkChainId: string, + tokenChainId: string +) => { + return networkChainId === tokenChainId +} + const getFullAssetBalance = ( asset: BraveWallet.BlockchainToken, networks: BraveWallet.NetworkInfo[], @@ -179,7 +186,11 @@ export const SelectTokenModal = React.forwardRef( } = props // State - const [searchValue, setSearchValue] = React.useState('') + const [searchValue, setSearchValue] = React.useState( + modalType === 'bridge' && selectingFromOrTo === 'to' && selectedFromToken + ? selectedFromToken.symbol + : '' + ) const [selectedNetworkFilter, setSelectedNetworkFilter] = React.useState( selectedNetwork || AllNetworksOption @@ -207,7 +218,7 @@ export const SelectTokenModal = React.forwardRef( const { data: swapNetworks = [] } = useGetSwapSupportedNetworksQuery( undefined, { - skip: modalType !== 'swap' + skip: modalType === 'send' } ) @@ -238,7 +249,44 @@ export const SelectTokenModal = React.forwardRef( }) }) - const networks = modalType === 'swap' ? swapNetworks : visibleNetworks + const bridgeAndSwapNetworks = React.useMemo(() => { + if ( + modalType === 'bridge' && + selectingFromOrTo === 'to' && + selectedFromToken + ) { + return swapNetworks.filter( + (network) => + !checkIsBridgeNetworkDropdownOptionDisabled( + network.chainId, + selectedFromToken.chainId + ) + ) + } + if ( + modalType === 'bridge' && + selectingFromOrTo === 'from' && + selectedToToken + ) { + return swapNetworks.filter( + (network) => + !checkIsBridgeNetworkDropdownOptionDisabled( + network.chainId, + selectedToToken.chainId + ) + ) + } + return swapNetworks + }, [ + modalType, + selectingFromOrTo, + selectedFromToken, + selectedToToken, + swapNetworks + ]) + + const networks = + modalType === 'send' ? visibleNetworks : bridgeAndSwapNetworks const { data: tokenBalancesRegistry, isLoading: isLoadingBalances } = useBalancesFetcher({ @@ -263,9 +311,9 @@ export const SelectTokenModal = React.forwardRef( ) const tokensBySelectedComposerOption = React.useMemo(() => { - if (modalType === 'swap') { + if (modalType === 'swap' || modalType === 'bridge') { return fullVisibleFungibleTokensList.filter((token) => - swapNetworks.some(({ chainId }) => chainId === token.chainId) + bridgeAndSwapNetworks.some(({ chainId }) => chainId === token.chainId) ) } @@ -281,7 +329,7 @@ export const SelectTokenModal = React.forwardRef( }, [ modalType, fullVisibleFungibleTokensList, - swapNetworks, + bridgeAndSwapNetworks, userVisibleNfts, userVisibleFungibleTokens, selectedSendOption, @@ -451,7 +499,7 @@ export const SelectTokenModal = React.forwardRef( // No need to select an account when selecting // a token to receive right now. // This will change with bridge. - if (selectingFromOrTo === 'to') { + if (selectingFromOrTo === 'to' && modalType === 'swap') { onSelectAsset(token) onClose() return @@ -459,7 +507,7 @@ export const SelectTokenModal = React.forwardRef( setPendingSelectedAsset(token) }, - [onSelectAsset, onClose, selectingFromOrTo] + [onSelectAsset, onClose, selectingFromOrTo, modalType] ) const handleSelectAccount = React.useCallback( @@ -472,16 +520,94 @@ export const SelectTokenModal = React.forwardRef( [onSelectAsset, onClose, pendingSelectedAsset] ) + const checkIsNetworkOptionDisabled = React.useCallback( + (network: BraveWallet.NetworkInfo) => { + if (modalType === 'swap') { + return checkIsSwapDropdownOptionDisabled( + selectedAccountFilter, + network + ) + } + if ( + modalType === 'bridge' && + selectingFromOrTo === 'to' && + selectedFromToken + ) { + return checkIsBridgeNetworkDropdownOptionDisabled( + selectedFromToken.chainId, + network.chainId + ) + } + if ( + modalType === 'bridge' && + selectingFromOrTo === 'from' && + selectedToToken + ) { + return checkIsBridgeNetworkDropdownOptionDisabled( + selectedToToken.chainId, + network.chainId + ) + } + return false + }, + [ + modalType, + selectingFromOrTo, + selectedAccountFilter, + selectedFromToken, + selectedToToken + ] + ) + + const checkIsAccountOptionDisabled = React.useCallback( + (account: BraveWallet.AccountInfo) => { + if (modalType === 'swap') { + return checkIsSwapDropdownOptionDisabled( + account, + selectedNetworkFilter + ) + } + if ( + modalType === 'bridge' && + selectingFromOrTo === 'to' && + selectedFromToken + ) { + return ( + account.accountId.coin === BraveWallet.CoinType.SOL && + selectedFromToken.coin === BraveWallet.CoinType.SOL + ) + } + if ( + modalType === 'bridge' && + selectingFromOrTo === 'from' && + selectedToToken + ) { + return ( + account.accountId.coin === BraveWallet.CoinType.SOL && + selectedToToken.coin === BraveWallet.CoinType.SOL + ) + } + return false + }, + [ + modalType, + selectingFromOrTo, + selectedFromToken, + selectedToToken, + selectedNetworkFilter + ] + ) + const onSelectNetworkFilter = React.useCallback( (chainId: string) => { const network = networks.find((n) => n.chainId === chainId) ?? AllNetworksOption - if (checkIsDropdownOptionDisabled(selectedAccountFilter, network)) { + if (checkIsNetworkOptionDisabled(network)) { return } setSelectedNetworkFilter(network) }, - [networks, selectedAccountFilter] + [networks, checkIsNetworkOptionDisabled] ) const onSelectAccountFilter = React.useCallback( @@ -489,12 +615,12 @@ export const SelectTokenModal = React.forwardRef( const account = accounts.find((a) => a.accountId.uniqueKey === uniqueKey) ?? AllAccountsOption - if (checkIsDropdownOptionDisabled(account, selectedNetworkFilter)) { + if (checkIsAccountOptionDisabled(account)) { return } setSelectedAccountFilter(account) }, - [accounts, selectedNetworkFilter] + [accounts, checkIsAccountOptionDisabled] ) // Computed & Memos @@ -611,6 +737,8 @@ export const SelectTokenModal = React.forwardRef( title={getLocale( modalType === 'swap' ? 'braveWalletChooseAssetToSwap' + : modalType === 'bridge' + ? 'braveWalletChooseAssetToBridge' : 'braveWalletChooseAssetToSend' )} width='560px' @@ -647,26 +775,19 @@ export const SelectTokenModal = React.forwardRef( - {selectingFromOrTo === 'from' && ( + {!(selectingFromOrTo === 'to' && modalType === 'swap') && ( - checkIsDropdownOptionDisabled( - account, - selectedNetworkFilter - ) - } + checkIsAccountOptionDisabled={checkIsAccountOptionDisabled} /> ( selectedNetwork={selectedNetworkFilter} showAllNetworksOption={true} onSelectNetwork={onSelectNetworkFilter} - checkIsNetworkOptionDisabled={( - network: BraveWallet.NetworkInfo - ) => - checkIsDropdownOptionDisabled( - selectedAccountFilter, - network - ) - } + checkIsNetworkOptionDisabled={checkIsNetworkOptionDisabled} /> )} diff --git a/components/brave_wallet_ui/page/screens/composer_ui/shared_composer.style.ts b/components/brave_wallet_ui/page/screens/composer_ui/shared_composer.style.ts index c1a5319301ae6..875bf915adb8e 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/shared_composer.style.ts +++ b/components/brave_wallet_ui/page/screens/composer_ui/shared_composer.style.ts @@ -5,6 +5,7 @@ import styled from 'styled-components' import * as leo from '@brave/leo/tokens/css/variables' +import Icon from '@brave/leo/react/icon' // Shared Styles import { @@ -12,7 +13,8 @@ import { WalletButton, Row, AssetIconFactory, - AssetIconProps + AssetIconProps, + Text } from '../../../components/shared/style' import { layoutPanelWidth // @@ -25,12 +27,12 @@ export const ToSectionWrapper = styled(Column)<{ @media (prefers-color-scheme: dark) { --default-background: ${leo.color.container.highlight}; } - padding: 0px 32px 32px 32px; + padding: 0px 24px 24px 24px; border-radius: 0px 0px 24px 24px; background-color: ${(p) => p.tokenColor ?? 'var(--default-background)'}; @media screen and (max-width: ${layoutPanelWidth}px) { border-radius: 0px; - padding: 0px 0px 16px 0px; + padding: 0px 16px 16px 16px; height: 100%; } ` @@ -104,3 +106,42 @@ export const AssetIcon = AssetIconFactory({ width: '40px', height: 'auto' }) + +export const CaratIcon = styled(Icon).attrs({ + name: 'carat-right' +})` + --leo-icon-size: 24px; + color: inherit; + margin-left: 8px; +` + +export const ButtonText = styled(Text)` + overflow: hidden; + color: inherit; + white-space: pre-wrap; + word-break: break-all; + font-weight: 500; +` + +export const Button = styled(WalletButton)<{ + isPlaceholder: boolean +}>` + cursor: pointer; + display: flex; + outline: none; + border: none; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: transparent; + padding: 10px 0px; + color: ${(p) => + p.isPlaceholder ? leo.color.text.tertiary : leo.color.text.primary}; + white-space: nowrap; + :disabled { + cursor: not-allowed; + } + &:hover:not([disabled]) { + color: ${leo.color.text.interactive}; + } +` diff --git a/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.style.ts b/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.style.ts index 89d2df848c049..ed2359286dde5 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.style.ts +++ b/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.style.ts @@ -10,7 +10,7 @@ import * as leo from '@brave/leo/tokens/css/variables' import { Text, Row } from '../../../../components/shared/style' export const ReceiveAndQuoteText = styled(Text)` - line-height: 26px; + line-height: 18px; color: ${leo.color.text.tertiary}; ` diff --git a/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.tsx b/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.tsx index 3b64aff9a3d74..5e26fc1899ecb 100644 --- a/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.tsx +++ b/components/brave_wallet_ui/page/screens/composer_ui/to_asset/to_asset.tsx @@ -126,26 +126,26 @@ export const ToAsset = (props: Props) => { > {getLocale('braveWalletReceiveEstimate')} {isFetchingQuote && ( {getLocale('braveSwapFindingPrice')} @@ -156,7 +156,6 @@ export const ToAsset = (props: Props) => { width='100%' alignItems='center' justifyContent='space-between' - padding='0px 16px 0px 6px' marginBottom={10} > @@ -182,7 +181,6 @@ export const ToAsset = (props: Props) => { width='100%' alignItems='center' justifyContent='space-between' - padding='0px 16px' > {network && token && ( { const groupingLabel = React.useMemo(() => { if ( - modalType === 'swap' && + modalType !== 'send' && index === 0 && firstNoBalanceTokenKey !== getAssetIdKey(token) ) { return 'owned' } - return modalType === 'swap' && + return modalType !== 'send' && firstNoBalanceTokenKey === getAssetIdKey(token) ? 'not-owned' : undefined diff --git a/components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts b/components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts index 0a374aa1cc60a..0781ffb0c00b1 100644 --- a/components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts +++ b/components/brave_wallet_ui/page/screens/send/send_screen/send.style.ts @@ -15,7 +15,7 @@ export const InputRow = styled(Row)` ` export const ToText = styled(Text)` - line-height: 26px; + line-height: 22px; color: ${leo.color.text.tertiary}; ` diff --git a/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx b/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx index 95cbe3da6060b..9d56d6fcf7f52 100644 --- a/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx +++ b/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx @@ -76,7 +76,6 @@ import { import { WalletPageWrapper // } from '../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper' -import { ComposerControls } from '../../composer_ui/composer_controls/composer_controls' import { FromAsset } from '../../composer_ui/from_asset/from_asset' import { PanelActionHeader // @@ -470,7 +469,6 @@ export const SendScreen = React.memo((props: Props) => { noCardPadding={true} hideNav={isAndroid || isPanel} hideHeader={isAndroid} - noMinCardHeight={true} hideDivider={true} cardHeader={ isPanel ? ( @@ -481,7 +479,10 @@ export const SendScreen = React.memo((props: Props) => { ) : undefined } > - <> + { isLoadingBalances={isLoadingBalances} tokenBalancesRegistry={tokenBalancesRegistry} /> - @@ -504,7 +505,7 @@ export const SendScreen = React.memo((props: Props) => { fullHeight={true} justifyContent='space-between' alignItems='center' - padding='48px 0px 0px 0px' + padding='32px 0px 0px 0px' > { width='100%' alignItems='center' justifyContent='flex-start' - padding='0px 16px' marginBottom={10} > - {getLocale('braveWalletSwapTo')} + + {getLocale('braveWalletSwapTo')} + { /> )} - + { - + {showSelectAddressModal && ( diff --git a/components/brave_wallet_ui/page/screens/swap/assets/lp-icons/lifi.svg b/components/brave_wallet_ui/page/screens/swap/assets/lp-icons/lifi.svg new file mode 100644 index 0000000000000..10fae82140cda --- /dev/null +++ b/components/brave_wallet_ui/page/screens/swap/assets/lp-icons/lifi.svg @@ -0,0 +1 @@ + diff --git a/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.style.ts b/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.style.ts index f3bb826985161..64024a1c49ac4 100644 --- a/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.style.ts +++ b/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.style.ts @@ -5,64 +5,93 @@ import styled from 'styled-components' import * as leo from '@brave/leo/tokens/css/variables' +import Icon from '@brave/leo/react/icon' + +// Shared Styles import { - Icon, + WalletButton, + Column, Row, - StyledDiv, - Text, - IconButton -} from '../../shared-swap.styles' - -export const HorizontalArrows = styled(Icon)` - color: ${(p) => p.theme.color.text03}; - margin-left: 8px; -` + Text // +} from '../../../../../../components/shared/style' -export const FuelTank = styled(Icon)` - color: ${(p) => p.theme.color.text02}; - margin-right: 6px; +export const Bubble = styled(Row)` + border-radius: 4px; + background-color: ${leo.color.green[10]}; ` -export const Bubble = styled(Row)` - padding: 2px 8px; - border-radius: 8px; - background-color: ${leo.color.purple[10]}; - @media (prefers-color-scheme: dark) { - /* #282B37 does not exist in the design system */ - background-color: #282b37; - } +export const FreeText = styled(Text)` + text-transform: uppercase; + line-height: 10px; ` -export const LPIcon = styled(StyledDiv)<{ icon: string; size: number }>` +export const LPIcon = styled.div<{ icon: string }>` background-image: url(${(p) => p.icon}); background-size: cover; background-position: center; background-repeat: no-repeat; - height: ${(p) => p.size}px; - width: ${(p) => p.size}px; - margin-left: 6px; - border-radius: 50px; + height: 16px; + width: 16px; + margin-right: 8px; ` export const LPSeparator = styled(Text)` padding: 0 6px; ` -export const BraveFeeContainer = styled(Row)` - gap: 4px; -` - export const BraveFeeDiscounted = styled(Text)` text-decoration: line-through; ` -export const ExpandButton = styled(IconButton)<{ - isExpanded: boolean -}>` - transform: ${(p) => (p.isExpanded ? 'rotate(180deg)' : 'unset')}; - transition: transform 300ms ease; +export const Button = styled(WalletButton)` + cursor: pointer; + display: flex; + flex-direction: row; + background-color: none; + background: none; + outline: none; + border: none; + padding: 0px; + margin: 0px; + :disabled { + cursor: default; + } +` + +export const ExpandButton = styled(Button)` + background-color: ${leo.color.container.background}; + border-radius: 100%; + padding: 1px; ` export const LPRow = styled(Row)` flex-wrap: wrap; ` + +export const Section = styled(Column)` + background-color: ${leo.color.container.background}; + border-radius: ${leo.radius.m}; + position: relative; +` + +export const CaratDownIcon = styled(Icon).attrs({ + name: 'carat-down' +})<{ isOpen: boolean }>` + --leo-icon-size: 18px; + color: ${leo.color.icon.default}; + transition-duration: 0.3s; + transform: ${(p) => (p.isOpen ? 'rotate(180deg)' : 'unset')}; +` + +export const ExpandRow = styled(Row)` + position: absolute; + bottom: 0px; + max-height: 0px; +` + +export const CaratRightIcon = styled(Icon).attrs({ + name: 'carat-right' +})` + --leo-icon-size: 18px; + color: ${leo.color.icon.default}; +` diff --git a/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.tsx b/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.tsx index 7ae833a330414..8ca06d46c4cc6 100644 --- a/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.tsx +++ b/components/brave_wallet_ui/page/screens/swap/components/swap/quote-info/quote-info.tsx @@ -4,13 +4,36 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import * as React from 'react' +import { skipToken } from '@reduxjs/toolkit/query/react' // Types +import { BraveWallet } from '../../../../../../constants/types' +import { LiquiditySource, QuoteOption } from '../../../constants/types' + +// Queries +import { + useGetAccountInfosRegistryQuery, + useGetDefaultFiatCurrencyQuery, // + useGetSwapSupportedNetworksQuery, + useGetTokenSpotPricesQuery +} from '../../../../../../common/slices/api.slice' +import { + selectAllAccountInfosFromQuery // +} from '../../../../../../common/slices/entities/account-info.entity' +import { + querySubscriptionOptions60s // +} from '../../../../../../common/slices/constants' + +// Selectors +import { + useSafeUISelector // +} from '../../../../../../common/hooks/use-safe-selector' +import { UISelectors } from '../../../../../../common/selectors' + +// Hooks import { - BraveWallet, - SpotPriceRegistry -} from '../../../../../../constants/types' -import { QuoteOption } from '../../../constants/types' + useBalancesFetcher // +} from '../../../../../../common/hooks/use-balances-fetcher' // Constants import LPMetadata from '../../../constants/LpMetadata' @@ -19,45 +42,124 @@ import LPMetadata from '../../../constants/LpMetadata' import Amount from '../../../../../../utils/amount' import { getLocale } from '../../../../../../../common/locale' import { - getTokenPriceAmountFromRegistry // + formatDateAsRelative // +} from '../../../../../../utils/datetime-utils' +import { getBalance } from '../../../../../../utils/balance-utils' +import { + getTokenPriceAmountFromRegistry, + getTokenPriceFromRegistry // } from '../../../../../../utils/pricing-utils' +import { + getPriceIdForToken // +} from '../../../../../../utils/api-utils' + +// Components +import { + PopupModal // +} from '../../../../../../components/desktop/popup-modals/index' +import { + SelectAccount // +} from '../../../../composer_ui/select_token_modal/select_account/select_account' +import { + BottomSheet // +} from '../../../../../../components/shared/bottom_sheet/bottom_sheet' // Styled Components import { - BraveFeeContainer, BraveFeeDiscounted, Bubble, - ExpandButton, - FuelTank, - HorizontalArrows, + Button, LPIcon, LPSeparator, - LPRow + LPRow, + Section, + CaratDownIcon, + FreeText, + CaratRightIcon, + ExpandRow, + ExpandButton } from './quote-info.style' import { - Column, - Row, Text, - VerticalSpacer, - HorizontalSpacer, - Icon -} from '../../shared-swap.styles' + Row, + Column, + HorizontalSpace +} from '../../../../../../components/shared/style' + +const getLPIcon = (source: LiquiditySource) => { + if (source.icon) { + return `chrome://image?${source.icon}` + } + return LPMetadata[source.name] ?? '' +} interface Props { selectedQuoteOption: QuoteOption | undefined fromToken: BraveWallet.BlockchainToken | undefined toToken: BraveWallet.BlockchainToken | undefined - toAmount: string - spotPrices?: SpotPriceRegistry + isBridge: boolean + onChangeRecipient: (address: string) => void + + toAccount?: BraveWallet.AccountInfo swapFees?: BraveWallet.SwapFees } export const QuoteInfo = (props: Props) => { - const { selectedQuoteOption, fromToken, toToken, spotPrices, swapFees } = - props + const { + selectedQuoteOption, + fromToken, + toToken, + swapFees, + isBridge, + toAccount, + onChangeRecipient + } = props // State const [showProviders, setShowProviders] = React.useState(false) + const [showAccountSelector, setShowAccountSelector] = + React.useState(false) + const [showAdvancedInformation, setShowAdvancedInformation] = + React.useState(false) + + // Selectors + const isPanel = useSafeUISelector(UISelectors.isPanel) + + // Queries + const { accounts } = useGetAccountInfosRegistryQuery(undefined, { + selectFromResult: (res) => ({ + accounts: selectAllAccountInfosFromQuery(res) + }) + }) + + const { data: networks = [] } = useGetSwapSupportedNetworksQuery() + + const { data: tokenBalancesRegistry, isLoading: isLoadingBalances } = + useBalancesFetcher({ + accounts, + networks + }) + + const { data: defaultFiatCurrency } = useGetDefaultFiatCurrencyQuery() + + const { data: spotPriceRegistry } = useGetTokenSpotPricesQuery( + !isLoadingBalances && toToken && fromToken && defaultFiatCurrency + ? { + ids: [getPriceIdForToken(toToken), getPriceIdForToken(fromToken)], + toCurrency: defaultFiatCurrency + } + : skipToken, + querySubscriptionOptions60s + ) + + // Methods + const handleSelectAccount = React.useCallback( + (account: BraveWallet.AccountInfo) => { + onChangeRecipient(account.accountId.address) + setShowAccountSelector(false) + }, + [onChangeRecipient] + ) // Memos const swapRate: string = React.useMemo(() => { @@ -65,20 +167,29 @@ export const QuoteInfo = (props: Props) => { return '' } - return `1 ${ - selectedQuoteOption.fromToken.symbol - } ≈ ${selectedQuoteOption.rate.format(6)} ${ - selectedQuoteOption.toToken.symbol - }` + return getLocale('braveWalletExchangeFor') + .replace('$1', `1 ${selectedQuoteOption.fromToken.symbol}`) + .replace( + '$2', + `${selectedQuoteOption.rate.format(6)} ${ + selectedQuoteOption.toToken.symbol + }` + ) }, [selectedQuoteOption]) const coinGeckoDelta: Amount = React.useMemo(() => { if ( fromToken !== undefined && toToken !== undefined && - spotPrices && - !getTokenPriceAmountFromRegistry(spotPrices, fromToken).isUndefined() && - !getTokenPriceAmountFromRegistry(spotPrices, toToken).isUndefined() && + spotPriceRegistry && + !getTokenPriceAmountFromRegistry( + spotPriceRegistry, + fromToken + ).isUndefined() && + !getTokenPriceAmountFromRegistry( + spotPriceRegistry, + toToken + ).isUndefined() && selectedQuoteOption !== undefined ) { // Exchange rate is the value in the following equation: @@ -89,9 +200,9 @@ export const QuoteInfo = (props: Props) => { // 1 FROM/USD = TO/USD // => = (FROM/USD) / (TO/USD) const coinGeckoRate = getTokenPriceAmountFromRegistry( - spotPrices, + spotPriceRegistry, fromToken - ).div(getTokenPriceAmountFromRegistry(spotPrices, toToken)) + ).div(getTokenPriceAmountFromRegistry(spotPriceRegistry, toToken)) // Quote rate computation: // FROM = TO @@ -104,7 +215,7 @@ export const QuoteInfo = (props: Props) => { } return Amount.zero() - }, [spotPrices, fromToken, toToken, selectedQuoteOption]) + }, [spotPriceRegistry, fromToken, toToken, selectedQuoteOption]) const coinGeckoDeltaText: string = React.useMemo(() => { if (coinGeckoDelta.gte(0)) { @@ -173,191 +284,351 @@ export const QuoteInfo = (props: Props) => { } }, [swapFees]) + const estimatedDuration = React.useMemo(() => { + if (!selectedQuoteOption?.executionDuration) { + return '' + } + const date = new Date() + date.setTime( + date.getTime() - 1000 * Number(selectedQuoteOption.executionDuration) + ) + return formatDateAsRelative(date, undefined, true) + }, [selectedQuoteOption]) + + const accountsForReceivingToken = React.useMemo(() => { + if (!toToken) { + return [] + } + return accounts + .filter((account) => account.accountId.coin === toToken.coin) + .sort(function (a, b) { + return new Amount( + getBalance(b.accountId, toToken, tokenBalancesRegistry) + ) + .minus(getBalance(a.accountId, toToken, tokenBalancesRegistry)) + .toNumber() + }) + }, [accounts, toToken, tokenBalancesRegistry]) + + const AccountSelector = React.useMemo(() => { + if (!toToken) { + return + } + return ( + + ) + }, [ + toToken, + accountsForReceivingToken, + tokenBalancesRegistry, + spotPriceRegistry, + handleSelectAccount + ]) + return ( - - - - {getLocale('braveSwapRate')} - - {swapRate} - - - - - - - - {coinGeckoDeltaText} - - - - +
- {getLocale('braveSwapPriceImpact')} - - {swapImpact === '0' ? `${swapImpact}%` : `~ ${swapImpact}%`} - - - {minimumReceived !== '' && ( - - - {getLocale('braveSwapMinimumReceivedAfterSlippage')} - - - {minimumReceived} - - - )} - {selectedQuoteOption && selectedQuoteOption.sources.length > 0 && ( - - + {!isBridge && ( + - {getLocale('braveSwapLiquidityProvider')} + {getLocale('braveWalletExchangeRate')} - - {selectedQuoteOption.sources.length} - - setShowProviders((prev) => !prev)} - > - - - - - {showProviders && ( - - {selectedQuoteOption.sources.map((source, idx) => ( - - - - {source.name.split('_').join(' ')} - - {LPMetadata[source.name] ? ( - - ) : null} - - - {idx !== selectedQuoteOption.sources.length - 1 && ( - - {selectedQuoteOption.routing === 'split' ? '+' : '×'} - - )} + {swapRate} + + + )} + + {(isBridge || showAdvancedInformation) && + selectedQuoteOption && + selectedQuoteOption.sources.length > 0 && ( + + + + {isBridge + ? getLocale('braveWalletBridgingVia') + : getLocale('braveWalletSwappingVia')} + + + + + {getLocale('braveWalletProviderApi').replace( + '$1', + selectedQuoteOption.provider + )} + + + - ))} - + + {showProviders && ( + + {selectedQuoteOption.sources.map((source, idx) => ( + + {getLPIcon(source) !== '' ? ( + + ) : null} + + {source.name.split('_').join(' ')} + + + {idx !== selectedQuoteOption.sources.length - 1 && ( + + {selectedQuoteOption.routing === 'split' ? '+' : '×'} + + )} + + ))} + + )} + )} - - )} - {selectedQuoteOption && ( - - {getLocale('braveSwapNetworkFee')} - - - {selectedQuoteOption.networkFeeFiat} - - - )} - {braveFee && ( - - {getLocale('braveSwapBraveFee')} - - + + {braveFee && ( + + + {getLocale('braveSwapBraveFee')} + + {braveFee.discountCode === BraveWallet.SwapDiscountCode.kNone && ( - {braveFee.effectiveFeePct}% + + {braveFee.effectiveFeePct}% + )} {braveFee.discountCode !== BraveWallet.SwapDiscountCode.kNone && ( <> {new Amount(braveFee.effectiveFeePct).isZero() ? ( - - {getLocale('braveSwapFree')} - + + + {getLocale('braveSwapFree')} + + ) : ( {braveFee.effectiveFeePct}% )} {braveFee.feePct}% {new Amount(braveFee.effectiveFeePct).gt(0) && ( - (-{braveFee.discountPct}%) + + (-{braveFee.discountPct}%) + )} )} - - - + + + )} + + {showAdvancedInformation && ( + <> + {estimatedDuration !== '' && ( + + + {getLocale('braveWalletEstTime')} + + + {estimatedDuration} + + + )} + + {minimumReceived !== '' && ( + + + {getLocale('braveSwapMinimumReceivedAfterSlippage')} + + + {minimumReceived} + + + )} + + + + {getLocale('braveSwapPriceImpact')} + + + {swapImpact === '0' ? `${swapImpact}%` : `~ ${swapImpact}%`} + + + + + + {coinGeckoDeltaText} + + + + {isBridge && ( + + + {getLocale('braveWalletRecipient')} + + + + )} + + )} + + + setShowAdvancedInformation((prev) => !prev)} + > + + + +
+ + {selectedQuoteOption && ( +
+ + + {getLocale('braveWalletEstFees')} + + + ~{selectedQuoteOption.networkFeeFiat} + + +
)} + + {showAccountSelector && + (isPanel ? ( + setShowAccountSelector(false)}> + {AccountSelector} + + ) : ( + setShowAccountSelector(false)} + width='560px' + showDivider={false} + > + {AccountSelector} + + ))}
) } diff --git a/components/brave_wallet_ui/page/screens/swap/constants/LpMetadata.ts b/components/brave_wallet_ui/page/screens/swap/constants/LpMetadata.ts index baa621ea398a6..01639136fbe94 100644 --- a/components/brave_wallet_ui/page/screens/swap/constants/LpMetadata.ts +++ b/components/brave_wallet_ui/page/screens/swap/constants/LpMetadata.ts @@ -24,9 +24,11 @@ import FireBirdOneSwapIcon from '../assets/lp-icons/firebirdoneswap.png' import GooseFXIcon from '../assets/lp-icons/goosefx.svg' import InvariantIcon from '../assets/lp-icons/invariant.svg' import IronSwapIcon from '../assets/lp-icons/ironswap.svg' +import JupiterIcon from '../assets/lp-icons/jupiter.svg' import KyberDMMIcon from '../assets/lp-icons/kyberdmm.svg' import LidoIcon from '../assets/lp-icons/lido.svg' import LifinityIcon from '../assets/lp-icons/lifinity.jpg' +import LiFiIcon from '../assets/lp-icons/lifi.svg' import MakerPsmIcon from '../assets/lp-icons/makerpsm.svg' import MarinadeIcon from '../assets/lp-icons/marinade.svg' import MDexIcon from '../assets/lp-icons/mdex.svg' @@ -82,8 +84,10 @@ const LPMetadata: LPMetadataType = { 'GooseFX': GooseFXIcon, 'Invariant': InvariantIcon, 'IronSwap': IronSwapIcon, + 'Jupiter': JupiterIcon, 'KyberDMM': KyberDMMIcon, 'Lido': LidoIcon, + 'Li.Fi': LiFiIcon, 'Lifinity': LifinityIcon, // LiquidityProvider info unknown 'LiquidityProvider': '', @@ -99,6 +103,7 @@ const LPMetadata: LPMetadataType = { 'QuickSwap': QuickSwapIcon, 'PancakeSwap': PancakeSwapIcon, 'PancakeSwap_V2': PancakeSwapIcon, + 'PancakeSwap_V3': PancakeSwapIcon, 'Penguin': PenguinIcon, 'Saber': SaberIcon, 'Saddle': SaddleIcon, diff --git a/components/brave_wallet_ui/page/screens/swap/constants/types.ts b/components/brave_wallet_ui/page/screens/swap/constants/types.ts index 7a006c76fb893..b6edeedae334c 100644 --- a/components/brave_wallet_ui/page/screens/swap/constants/types.ts +++ b/components/brave_wallet_ui/page/screens/swap/constants/types.ts @@ -7,9 +7,10 @@ import { BraveWallet } from '../../../../constants/types' import Amount from '../../../../utils/amount' -type LiquiditySource = { +export type LiquiditySource = { name: string proportion: Amount + icon?: string } export type QuoteOption = { @@ -35,6 +36,8 @@ export type QuoteOption = { networkFee: Amount networkFeeFiat: string + provider: string + executionDuration?: string } export type SwapAndSend = { diff --git a/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts b/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts index 9aab7e7b812b4..a45fc3ed2a968 100644 --- a/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts +++ b/components/brave_wallet_ui/page/screens/swap/hooks/useSwap.ts @@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { skipToken } from '@reduxjs/toolkit/query/react' -import { useHistory } from 'react-router' +import { useHistory, useLocation } from 'react-router' // Options import { SwapAndSendOptions } from '../../../../options/swap-and-send-options' @@ -58,7 +58,9 @@ import { getLiFiFromAmount, getLiFiToAmount } from '../swap.utils' -import { makeSwapRoute } from '../../../../utils/routes-utils' +import { + makeSwapOrBridgeRoute // +} from '../../../../utils/routes-utils' // Queries import { @@ -115,6 +117,8 @@ export const useSwap = () => { // routing const query = useQuery() const history = useHistory() + const { pathname } = useLocation() + const isBridge = pathname.includes(WalletRoutes.Bridge) // Queries // FIXME(onyb): what happens when defaultFiatCurrency is empty @@ -124,8 +128,11 @@ export const useSwap = () => { const { account: fromAccount } = useAccountFromAddressQuery( query.get('fromAccountId') ?? undefined ) + const { account: toAccount } = useAccountFromAddressQuery( + query.get('toAddress') ?? undefined + ) // TODO: deprecate toAccountId in favour of toAddress + toCoin - const toAccountId = fromAccount?.accountId + const toAccountId = toAccount?.accountId const toCoinFromParams = query.get('toCoin') ?? undefined const toCoin = toCoinFromParams ? Number(toCoinFromParams) : undefined const toAddress = query.get('toAddress') ?? undefined @@ -606,34 +613,36 @@ export const useSwap = () => { const nativeAssetBalance = nativeAsset && getAssetBalance(nativeAsset) const onClickFlipSwapTokens = useCallback(async () => { - if (!fromAccount || !fromToken || !toToken) { + if (!fromAccount || !toAccount || !fromToken || !toToken) { return } if ( - fromAccount.accountId.coin !== toToken.coin || - toCoin !== fromToken.coin + !isBridge && + (fromAccount.accountId.coin !== toToken.coin || toCoin !== fromToken.coin) ) { - history.replace(WalletRoutes.Swap) + history.replace(isBridge ? WalletRoutes.Bridge : WalletRoutes.Swap) } else { history.replace( - makeSwapRoute({ + makeSwapOrBridgeRoute({ fromToken: toToken, - fromAccount, + fromAccount: toAccount, toToken: fromToken, - toAddress, - toCoin + toAddress: fromAccount.accountId.address, + toCoin: fromToken.coin, + routeType: isBridge ? 'bridge' : 'swap' }) ) } await handleOnSetFromAmount('') }, [ fromAccount, + toAccount, fromToken, toToken, toCoin, handleOnSetFromAmount, history, - toAddress + isBridge ]) // Changing the To asset does the following: @@ -644,18 +653,22 @@ export const useSwap = () => { // debouncing. // 5. Fetch spot price. const onSelectToToken = useCallback( - async (token: BraveWallet.BlockchainToken) => { + async ( + token: BraveWallet.BlockchainToken, + account?: BraveWallet.AccountInfo + ) => { if (!fromToken || !fromAccount) { return } setEditingFromOrToAmount('from') history.replace( - makeSwapRoute({ + makeSwapOrBridgeRoute({ fromToken, fromAccount, toToken: token, - toAddress, - toCoin: token.coin + toAddress: account?.accountId.address, + toCoin: token.coin, + routeType: isBridge ? 'bridge' : 'swap' }) ) setSelectingFromOrTo(undefined) @@ -671,7 +684,7 @@ export const useSwap = () => { fromToken, fromAccount, history, - toAddress, + isBridge, reset, handleQuoteRefreshInternal ] @@ -691,23 +704,46 @@ export const useSwap = () => { return } setEditingFromOrToAmount('from') - // ToDo: Until cross-chain swaps is supported, - // we have this check to make sure that the toToken + + if (isBridge) { + history.replace( + makeSwapOrBridgeRoute({ + fromToken: token, + fromAccount: account, + toToken, + toAddress, + toCoin, + routeType: isBridge ? 'bridge' : 'swap' + }) + ) + setSelectingFromOrTo(undefined) + setFromAmount('') + setToAmount('') + reset() + return + } + + // For regular Swaps we check that the toToken // and the incoming fromToken are on the same network. // If not we clear the toToken from params. if (toToken && toToken.chainId === token.chainId) { history.replace( - makeSwapRoute({ + makeSwapOrBridgeRoute({ fromToken: token, fromAccount: account, toToken, toAddress, - toCoin + toCoin, + routeType: isBridge ? 'bridge' : 'swap' }) ) } else { history.replace( - makeSwapRoute({ fromToken: token, fromAccount: account }) + makeSwapOrBridgeRoute({ + fromToken: token, + fromAccount: account, + routeType: isBridge ? 'bridge' : 'swap' + }) ) } setSelectingFromOrTo(undefined) @@ -715,7 +751,7 @@ export const useSwap = () => { setToAmount('') reset() }, - [toToken, reset, history, toAddress, toCoin] + [toToken, reset, history, toAddress, toCoin, isBridge] ) const onSetSelectedSwapAndSendOption = useCallback((value: string) => { @@ -736,6 +772,25 @@ export const useSwap = () => { [] ) + const onChangeRecipient = useCallback( + async (address: string) => { + if (!fromToken || !fromAccount || !toToken) { + return + } + history.replace( + makeSwapOrBridgeRoute({ + fromToken, + fromAccount, + toToken: toToken, + toAddress: address, + toCoin: toToken.coin, + routeType: isBridge ? 'bridge' : 'swap' + }) + ) + }, + [fromToken, toToken, fromAccount, history, isBridge] + ) + // Memos const fiatValue = useMemo(() => { if ( @@ -959,8 +1014,12 @@ export const useSwap = () => { ]) const submitButtonText = useMemo(() => { + const defaultText = isBridge + ? getLocale('braveWalletReviewBridge') + : getLocale('braveWalletReviewSwap') + if (!fromToken || !fromNetwork) { - return getLocale('braveSwapReviewOrder') + return defaultText } if (swapValidationError === 'insufficientBalance') { @@ -992,8 +1051,8 @@ export const useSwap = () => { return getLocale('braveWalletSwapUnknownError') } - return getLocale('braveSwapReviewOrder') - }, [fromToken, fromNetwork, swapValidationError]) + return defaultText + }, [isBridge, fromToken, fromNetwork, swapValidationError]) const isSubmitButtonDisabled = useMemo(() => { return ( @@ -1094,12 +1153,15 @@ export const useSwap = () => { setSlippageTolerance, setUseDirectRoute, onSubmit, + onChangeRecipient, submitButtonText, isSubmitButtonDisabled, swapValidationError, spotPrices: spotPriceRegistry, tokenBalancesRegistry, - isLoadingBalances + isLoadingBalances, + isBridge, + toAccount } } export default useSwap diff --git a/components/brave_wallet_ui/page/screens/swap/swap.tsx b/components/brave_wallet_ui/page/screens/swap/swap.tsx index 58bf5be4a3337..1adc2c6441091 100644 --- a/components/brave_wallet_ui/page/screens/swap/swap.tsx +++ b/components/brave_wallet_ui/page/screens/swap/swap.tsx @@ -33,7 +33,11 @@ import WalletPageWrapper from '../../../components/desktop/wallet-page-wrapper/w import { PanelActionHeader } from '../../../components/desktop/card-headers/panel-action-header' // Styled Components -import { LeoSquaredButton } from '../../../components/shared/style' +import { + Column, + LeoSquaredButton, + VerticalSpace +} from '../../../components/shared/style' import { ReviewButtonRow } from '../composer_ui/shared_composer.style' export const Swap = () => { @@ -63,13 +67,15 @@ export const Swap = () => { setSelectedGasFeeOption, setSlippageTolerance, onSubmit, + onChangeRecipient, submitButtonText, isSubmitButtonDisabled, swapValidationError, - spotPrices, tokenBalancesRegistry, isLoadingBalances, - swapFees + swapFees, + isBridge, + toAccount } = swap // State @@ -120,8 +126,12 @@ export const Swap = () => { cardHeader={ isPanel ? ( ) : undefined } @@ -161,54 +171,60 @@ export const Swap = () => { isFetchingQuote={isFetchingQuote} buttonDisabled={!fromToken} > - {/* TODO: QuoteOptions is currently unused - selectedNetwork?.coin === BraveWallet.CoinType.SOL && - quoteOptions.length > 0 && ( - - ) */} - {quoteOptions.length > 0 && ( - <> - - - {/* TODO: Swap and Send is currently unavailable - - )} - - - {submitButtonText} - - + {/* TODO: QuoteOptions is currently unused + {selectedNetwork?.coin === BraveWallet.CoinType.SOL && + quoteOptions.length > 0 && ( + + )} */} + {quoteOptions.length > 0 ? ( + <> + + + {/* TODO: Swap and Send is currently unavailable + */} + + ) : ( + + )} + + + {submitButtonText} + + + {showSwapSettings && ( { selectingFromOrTo={selectingFromOrTo} selectedFromToken={fromToken} selectedToToken={toToken} - selectedNetwork={selectingFromOrTo === 'to' ? fromNetwork : undefined} - modalType='swap' + selectedNetwork={ + !isBridge && selectingFromOrTo === 'to' ? fromNetwork : undefined + } + modalType={isBridge ? 'bridge' : 'swap'} selectedSendOption='#token' /> )} diff --git a/components/brave_wallet_ui/page/screens/swap/swap.utils.ts b/components/brave_wallet_ui/page/screens/swap/swap.utils.ts index 7f89bb436d779..f130a19a3b2c5 100644 --- a/components/brave_wallet_ui/page/screens/swap/swap.utils.ts +++ b/components/brave_wallet_ui/page/screens/swap/swap.utils.ts @@ -93,7 +93,8 @@ export function getZeroExQuoteOptions({ makeNetworkAsset(fromNetwork) ) ) - .formatAsFiat(defaultFiatCurrency) + .formatAsFiat(defaultFiatCurrency), + provider: '0x' } ] } @@ -195,7 +196,8 @@ export function getJupiterQuoteOptions({ makeNetworkAsset(fromNetwork) ) ) - .formatAsFiat(defaultFiatCurrency) + .formatAsFiat(defaultFiatCurrency), + provider: 'Jupiter' } ] } @@ -268,11 +270,14 @@ export function getLiFiQuoteOptions({ { name: route.steps[0].toolDetails.name, // TODO: assumption - proportion: new Amount(1) + proportion: new Amount(1), + icon: route.steps[0].toolDetails.logo } ], // TODO toAmount: toAmount, - toToken: toToken + toToken: toToken, + executionDuration: route.steps[0].estimate.executionDuration, + provider: 'Li.Fi' } }) } diff --git a/components/brave_wallet_ui/stories/locale.ts b/components/brave_wallet_ui/stories/locale.ts index faeb2dade4776..2d9034e6316a2 100644 --- a/components/brave_wallet_ui/stories/locale.ts +++ b/components/brave_wallet_ui/stories/locale.ts @@ -119,7 +119,6 @@ provideStrings({ braveWalletNotEnoughFunds: 'Not enough funds', braveWalletSendHalf: 'HALF', braveWalletSendMax: 'MAX', - braveSwapReviewOrder: 'Review order', braveWalletReviewSend: 'Review send', braveWalletNoAvailableTokens: 'No available tokens', braveWalletSearchTokens: 'Search by name or paste address', @@ -139,6 +138,17 @@ provideStrings({ braveWalletNotOwned: 'Not owned', braveWalletAmount24H: 'Amount/24h', + // Bridge + braveWalletChooseAssetToBridge: 'Choose asset to bridge', + braveWalletBridgingVia: 'Bridging via', + braveWalletEstFees: 'Est fees', + braveWalletEstTime: 'Est time', + braveWalletExchangeRate: 'Exchange rate', + braveWalletExchangeFor: '$1 for $2', + braveWalletProviderApi: '$1 API', + braveWalletRecipient: 'Recipient', + braveWalletReviewBridge: 'Review bridge', + // Create Account Tab braveWalletCreateAccountDescription: 'You don’t yet have a $1 account. Create one now?', @@ -591,6 +601,8 @@ provideStrings({ braveWalletSwapReviewHeader: 'Confirm order', braveWalletSolanaSwap: 'Solana Swap', braveWalletNoRoutesFound: 'No routes found', + braveWalletSwappingVia: 'Swapping via', + braveWalletReviewSwap: 'Review swap', // Buy braveWalletBuyTitle: 'Test faucet', diff --git a/components/brave_wallet_ui/utils/datetime-utils.ts b/components/brave_wallet_ui/utils/datetime-utils.ts index 65e62fcf133ae..40cd3728d6063 100644 --- a/components/brave_wallet_ui/utils/datetime-utils.ts +++ b/components/brave_wallet_ui/utils/datetime-utils.ts @@ -20,7 +20,11 @@ const monthMap = [ 'Dec' ] -export function formatDateAsRelative(date: Date, now: Date = new Date()) { +export function formatDateAsRelative( + date: Date, + now: Date = new Date(), + usePrecision?: boolean +) { // the difference in milliseconds const diff = now.getTime() - date.getTime() @@ -32,14 +36,20 @@ export function formatDateAsRelative(date: Date, now: Date = new Date()) { // convert diff to minutes const min = Math.floor(diff / (1000 * 60)) + const secRemaining = sec - min * 60 if (min < 60) { - return `${min}m` + return usePrecision && secRemaining > 0 + ? `${min}m: ${secRemaining}s` + : `${min}m` } // convert diff to hours const hour = Math.floor(diff / (1000 * 60 * 60)) + const minRemaining = min - hour * 60 if (hour < 24) { - return `${hour}h` + return usePrecision && minRemaining > 0 + ? `${hour}h: ${minRemaining}m` + : `${hour}h` } // convert diff to days diff --git a/components/brave_wallet_ui/utils/routes-utils.ts b/components/brave_wallet_ui/utils/routes-utils.ts index c8c1061fc0b59..0d4c405a93c8c 100644 --- a/components/brave_wallet_ui/utils/routes-utils.ts +++ b/components/brave_wallet_ui/utils/routes-utils.ts @@ -43,7 +43,8 @@ export function isPersistableSessionRoute( return ( isPersistableInPanel || route.includes(WalletRoutes.Swap) || - route.includes(WalletRoutes.Send) + route.includes(WalletRoutes.Send) || + route.includes(WalletRoutes.Bridge) ) } @@ -226,18 +227,20 @@ export const makeSendRoute = ( return `${WalletRoutes.Send}?${params.toString()}${SendPageTabHashes.token}` } -export const makeSwapRoute = ({ +export const makeSwapOrBridgeRoute = ({ fromToken, fromAccount, toToken, toAddress, - toCoin + toCoin, + routeType }: { fromToken: BraveWallet.BlockchainToken fromAccount: BraveWallet.AccountInfo toToken?: BraveWallet.BlockchainToken toAddress?: string toCoin?: BraveWallet.CoinType + routeType?: 'swap' | 'bridge' }) => { const baseQueryParams = { fromChainId: fromToken.chainId, @@ -258,7 +261,9 @@ export const makeSwapRoute = ({ : baseQueryParams ) - return `${WalletRoutes.Swap}?${params.toString()}` + const route = routeType === 'bridge' ? WalletRoutes.Bridge : WalletRoutes.Swap + + return `${route}?${params.toString()}` } export const makeTransactionDetailsRoute = (transactionId: string) => { diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index a6a0c40b7c87b..edc01e3fe006d 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -114,6 +114,17 @@ Owned Not owned Amount/24h + Choose asset to bridge + Bridging via + Swapping via + Review swap + Review bridge + Est fees + Est time + Exchange rate + 1 BAT$1 for 5 USDC$2 + Ox$1 API + Recipient Buy not supported for selected network Searching for domain... Brave supports using off-chain gateways to resolve .eth domains. @@ -907,7 +918,6 @@ Enter a password to continue Enter your Brave Wallet password to start backing up wallet. Swap - Review order Approve $1 Insufficient $1 balance Insufficient liquidity @@ -925,13 +935,11 @@ Hide zero balance Search token by name Option - Rate Price impact Minimum received after slippage Network fee - Brave fee + Included Brave fee Free - Liquidity provider Swap & send NO extra fees! I confirm that the address above is correct diff --git a/ui/webui/resources/BUILD.gn b/ui/webui/resources/BUILD.gn index ab13fc3251272..11d7d5d7d5eb0 100644 --- a/ui/webui/resources/BUILD.gn +++ b/ui/webui/resources/BUILD.gn @@ -374,6 +374,7 @@ leo_icons = [ "warning-triangle-filled.svg", "warning-triangle-outline.svg", "web3.svg", + "web3-bridge.svg", "widget-generic.svg", "window-content.svg", "window-tab-close.svg",