From dbe7e88d62c458054fd7c5180fbf040ba29bb1e0 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:50:40 +1100 Subject: [PATCH 1/9] chore: initial refactor for limit orders (#7912) * chore: break out trade input components for re-use * chore: un-inside-out the trade input * chore: attempt to organize * chore: use the shared trade input body * chore: move shared trade input layout into separate component * chore: parameterize trade input header top-right content * chore: alpha sort props separately to callbacks * chore: trade input tab managed by the header instead of prop drilling * fix: drill tab change handler to sidestep react-router upgrade * fix: trade input not centered * chore: better account id naming --------- Co-authored-by: gomes <17035424+gomesalexandre@users.noreply.github.com> --- .../Acknowledgement/Acknowledgement.tsx | 10 +- .../MultiHopTrade/MultiHopTrade.tsx | 32 +- .../SharedTradeInput/SharedTradeInput.tsx | 122 ++++++++ .../SharedTradeInputBody.tsx} | 77 +++-- .../SharedTradeInputHeader.tsx | 71 +++++ .../components/TradeInput/TradeInput.tsx | 289 +++++++++--------- .../TradeInput/components/Claim/Claim.tsx | 25 +- .../components/TradeInputHeader.tsx | 122 -------- .../components/TradeSettingsMenu.tsx | 41 +++ .../Deposit/components/Deposit.tsx | 7 +- 10 files changed, 447 insertions(+), 349 deletions(-) create mode 100644 src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx rename src/components/MultiHopTrade/components/{TradeInput/components/TradeInputBody.tsx => SharedTradeInput/SharedTradeInputBody.tsx} (81%) create mode 100644 src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputHeader.tsx delete mode 100644 src/components/MultiHopTrade/components/TradeInput/components/TradeInputHeader.tsx create mode 100644 src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx diff --git a/src/components/Acknowledgement/Acknowledgement.tsx b/src/components/Acknowledgement/Acknowledgement.tsx index 6a27d22fb3e..7c841f46c0d 100644 --- a/src/components/Acknowledgement/Acknowledgement.tsx +++ b/src/components/Acknowledgement/Acknowledgement.tsx @@ -1,6 +1,5 @@ -import type { ComponentWithAs, IconProps, ResponsiveValue, ThemeTypings } from '@chakra-ui/react' +import type { BoxProps, ComponentWithAs, IconProps, ThemeTypings } from '@chakra-ui/react' import { Box, Button, Checkbox, Link, useColorModeValue } from '@chakra-ui/react' -import type * as CSS from 'csstype' import type { AnimationDefinition, MotionStyle } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion' import type { InterpolationOptions } from 'node-polyglot' @@ -92,7 +91,7 @@ type AcknowledgementProps = { buttonTranslation?: string | [string, InterpolationOptions] icon?: ComponentWithAs<'svg', IconProps> disableButton?: boolean - position?: ResponsiveValue + boxProps?: BoxProps } type StreamingAcknowledgementProps = Omit & { @@ -115,7 +114,7 @@ export const Acknowledgement = ({ buttonTranslation, disableButton, icon: CustomIcon, - position = 'relative', + boxProps, }: AcknowledgementProps) => { const translate = useTranslate() const [isShowing, setIsShowing] = useState(false) @@ -152,10 +151,11 @@ export const Acknowledgement = ({ return ( {shouldShowAcknowledgement && ( diff --git a/src/components/MultiHopTrade/MultiHopTrade.tsx b/src/components/MultiHopTrade/MultiHopTrade.tsx index 5d84586edd9..38ac6ff405b 100644 --- a/src/components/MultiHopTrade/MultiHopTrade.tsx +++ b/src/components/MultiHopTrade/MultiHopTrade.tsx @@ -1,8 +1,9 @@ import type { AssetId } from '@shapeshiftoss/caip' +import { assertUnreachable } from '@shapeshiftoss/utils' import { AnimatePresence } from 'framer-motion' -import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' -import { MemoryRouter, Route, Switch, useLocation, useParams } from 'react-router-dom' +import { MemoryRouter, Route, Switch, useHistory, useLocation, useParams } from 'react-router-dom' import { selectAssetById } from 'state/slices/assetsSlice/selectors' import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' @@ -14,7 +15,7 @@ import { Claim } from './components/TradeInput/components/Claim/Claim' import { TradeInput } from './components/TradeInput/TradeInput' import { VerifyAddresses } from './components/VerifyAddresses/VerifyAddresses' import { useGetTradeQuotes } from './hooks/useGetTradeQuotes/useGetTradeQuotes' -import { TradeRoutePaths } from './types' +import { TradeInputTab, TradeRoutePaths } from './types' const TradeRouteEntries = [ TradeRoutePaths.Input, @@ -80,6 +81,7 @@ type TradeRoutesProps = { } const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { + const history = useHistory() const location = useLocation() const dispatch = useAppDispatch() @@ -102,12 +104,32 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { ) }, [location.pathname]) + const handleChangeTab = useCallback( + (newTab: TradeInputTab) => { + switch (newTab) { + case TradeInputTab.Trade: + history.push(TradeRoutePaths.Input) + break + case TradeInputTab.Claim: + history.push(TradeRoutePaths.Claim) + break + default: + assertUnreachable(newTab) + } + }, + [history], + ) + return ( <> - + @@ -122,7 +144,7 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { /> - + diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx new file mode 100644 index 00000000000..67b17e1e6be --- /dev/null +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx @@ -0,0 +1,122 @@ +import { Card, Center, Flex, useMediaQuery } from '@chakra-ui/react' +import type { AccountId } from '@shapeshiftoss/caip' +import type { TradeQuote } from '@shapeshiftoss/swapper' +import type { Asset } from '@shapeshiftoss/types' +import type { FormEvent } from 'react' +import { TradeInputTab } from 'components/MultiHopTrade/types' +import { breakpoints } from 'theme/theme' + +import { SharedTradeInputBody } from '../SharedTradeInput/SharedTradeInputBody' +import { SharedTradeInputHeader } from '../SharedTradeInput/SharedTradeInputHeader' +import { ConfirmSummary } from '../TradeInput/components/ConfirmSummary' +import { WithLazyMount } from '../TradeInput/components/WithLazyMount' +import { useSharedHeight } from '../TradeInput/hooks/useSharedHeight' + +type SharedTradeInputProps = { + activeQuote: TradeQuote | undefined + buyAmountAfterFeesCryptoPrecision: string | undefined + buyAmountAfterFeesUserCurrency: string | undefined + buyAsset: Asset + hasUserEnteredAmount: boolean + headerRightContent: JSX.Element + buyAssetAccountId: AccountId | undefined + sellAssetAccountId: AccountId | undefined + isCompact: boolean | undefined + isLoading: boolean + manualReceiveAddress: string | undefined + sellAsset: Asset + sideComponent: React.ComponentType + tradeInputRef: React.RefObject + walletReceiveAddress: string | undefined + handleSwitchAssets: () => void + onChangeTab: (newTab: TradeInputTab) => void + onSubmit: (e: FormEvent) => void + setBuyAsset: (asset: Asset) => void + setBuyAssetAccountId: (accountId: string) => void + setSellAsset: (asset: Asset) => void + setSellAssetAccountId: (accountId: string) => void +} + +export const SharedTradeInput: React.FC = ({ + activeQuote, + buyAmountAfterFeesCryptoPrecision, + buyAmountAfterFeesUserCurrency, + buyAsset, + hasUserEnteredAmount, + headerRightContent, + buyAssetAccountId, + sellAssetAccountId, + isCompact, + isLoading, + manualReceiveAddress, + sellAsset, + sideComponent, + tradeInputRef, + walletReceiveAddress, + handleSwitchAssets, + onChangeTab, + onSubmit, + setBuyAsset, + setBuyAssetAccountId, + setSellAsset, + setSellAssetAccountId, +}) => { + const totalHeight = useSharedHeight(tradeInputRef) + const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false }) + + return ( + +
+ + + + + + +
+
+ ) +} diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx similarity index 81% rename from src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx rename to src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx index 87b823e4a58..1a27daecaaa 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/TradeInputBody.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx @@ -8,6 +8,7 @@ import { Stack, } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' +import type { TradeQuote } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' import { positiveOrZero } from '@shapeshiftoss/utils' import { useCallback, useEffect, useMemo } from 'react' @@ -21,22 +22,14 @@ import { isToken } from 'lib/utils' import { selectHasUserEnteredAmount, selectHighestMarketCapFeeAsset, - selectInputBuyAsset, - selectInputSellAsset, selectIsAccountMetadataLoadingByAccountId, selectIsAccountsMetadataLoading, selectWalletConnectedChainIds, } from 'state/slices/selectors' -import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' -import { - selectActiveQuote, - selectBuyAmountAfterFeesCryptoPrecision, - selectBuyAmountAfterFeesUserCurrency, -} from 'state/slices/tradeQuoteSlice/selectors' -import { useAppDispatch, useAppSelector } from 'state/store' +import { useAppSelector } from 'state/store' -import { TradeAssetInput } from '../../TradeAssetInput' -import { SellAssetInput } from './SellAssetInput' +import { TradeAssetInput } from '../TradeAssetInput' +import { SellAssetInput } from '../TradeInput/components/SellAssetInput' const formControlProps = { borderRadius: 0, @@ -46,65 +39,63 @@ const formControlProps = { const arrowDownIcon = const emptyPercentOptions: number[] = [] -type TradeInputBodyProps = { +type SharedTradeInputBodyProps = { + activeQuote: TradeQuote | undefined isLoading: boolean | undefined manualReceiveAddress: string | undefined - initialSellAssetAccountId: AccountId | undefined - initialBuyAssetAccountId: AccountId | undefined + sellAssetAccountId: AccountId | undefined + buyAssetAccountId: AccountId | undefined setSellAssetAccountId: (accountId: AccountId) => void setBuyAssetAccountId: (accountId: AccountId) => void + buyAmountAfterFeesCryptoPrecision: string | undefined + buyAmountAfterFeesUserCurrency: string | undefined + buyAsset: Asset + sellAsset: Asset + setBuyAsset: (asset: Asset) => void + setSellAsset: (asset: Asset) => void + handleSwitchAssets: () => void } -export const TradeInputBody = ({ +export const SharedTradeInputBody = ({ + buyAmountAfterFeesCryptoPrecision, + buyAmountAfterFeesUserCurrency, + buyAsset, + sellAsset, isLoading, manualReceiveAddress, - initialSellAssetAccountId, - initialBuyAssetAccountId, + sellAssetAccountId, + buyAssetAccountId, setSellAssetAccountId, setBuyAssetAccountId, -}: TradeInputBodyProps) => { + setBuyAsset, + setSellAsset, + handleSwitchAssets, + activeQuote, +}: SharedTradeInputBodyProps) => { const translate = useTranslate() - const dispatch = useAppDispatch() const { state: { wallet }, } = useWallet() - const isAccountMetadataLoadingByAccountId = useAppSelector( - selectIsAccountMetadataLoadingByAccountId, - ) - const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) - const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) - const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency) const walletConnectedChainIds = useAppSelector(selectWalletConnectedChainIds) const defaultSellAsset = useAppSelector(selectHighestMarketCapFeeAsset) const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) - const buyAsset = useAppSelector(selectInputBuyAsset) - const sellAsset = useAppSelector(selectInputSellAsset) + const isAccountsMetadataLoading = useAppSelector(selectIsAccountsMetadataLoading) + const isAccountMetadataLoadingByAccountId = useAppSelector( + selectIsAccountMetadataLoadingByAccountId, + ) const walletSupportsBuyAssetChain = useWalletSupportsChain(buyAsset.chainId, wallet) const buyAssetSearch = useModal('buyTradeAssetSearch') const sellAssetSearch = useModal('sellTradeAssetSearch') - const setBuyAsset = useCallback( - (asset: Asset) => dispatch(tradeInput.actions.setBuyAsset(asset)), - [dispatch], - ) - const setSellAsset = useCallback( - (asset: Asset) => dispatch(tradeInput.actions.setSellAsset(asset)), - [dispatch], - ) - const handleSwitchAssets = useCallback( - () => dispatch(tradeInput.actions.switchAssets()), - [dispatch], - ) - const percentOptions = useMemo(() => { if (!sellAsset?.assetId) return [] if (!isToken(sellAsset.assetId)) return [] return [1] }, [sellAsset.assetId]) - const activeQuote = useAppSelector(selectActiveQuote) + const inputOutputDifferenceDecimalPercentage = useInputOutputDifferenceDecimalPercentage(activeQuote) @@ -174,7 +165,7 @@ export const TradeInputBody = ({ return ( void +} + +export const SharedTradeInputHeader = ({ + initialTab, + rightContent, + onChangeTab, +}: SharedTradeInputHeaderProps) => { + const translate = useTranslate() + const [selectedTab, setSelectedTab] = useState(initialTab) + + const enableBridgeClaims = useFeatureFlag('ArbitrumBridgeClaims') + + const handleChangeTab = useCallback( + (newTab: TradeInputTab) => { + setSelectedTab(newTab) + onChangeTab(newTab) + }, + [onChangeTab], + ) + + const handleClickTrade = useCallback(() => { + handleChangeTab(TradeInputTab.Trade) + }, [handleChangeTab]) + + const handleClickClaim = useCallback(() => { + handleChangeTab(TradeInputTab.Claim) + }, [handleChangeTab]) + + return ( + + + + + {translate('navBar.trade')} + + {enableBridgeClaims && ( + + {translate('bridge.claim')} + + )} + + + {rightContent} + + + + ) +} diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index 0f3907bb17b..91cf07f86f6 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -1,9 +1,9 @@ -import { Box, Card, Center, Flex, Stack, useMediaQuery } from '@chakra-ui/react' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import { isArbitrumBridgeTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/getThorTradeQuote/getTradeQuote' +import type { Asset } from '@shapeshiftoss/types' import type { FormEvent } from 'react' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useFormContext } from 'react-hook-form' import { useTranslate } from 'react-polyglot' import { useHistory } from 'react-router' @@ -15,8 +15,8 @@ import { import { MessageOverlay } from 'components/MessageOverlay/MessageOverlay' import { getMixpanelEventData } from 'components/MultiHopTrade/helpers' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' -import { TradeInputTab, TradeRoutePaths } from 'components/MultiHopTrade/types' -import { SlideTransition } from 'components/SlideTransition' +import type { TradeInputTab } from 'components/MultiHopTrade/types' +import { TradeRoutePaths } from 'components/MultiHopTrade/types' import { WalletActions } from 'context/WalletProvider/actions' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' @@ -28,11 +28,15 @@ import { isKeplrHDWallet } from 'lib/utils' import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' import { selectHasUserEnteredAmount, + selectInputBuyAsset, selectInputSellAsset, selectIsAnyAccountMetadataLoadedForChainId, } from 'state/slices/selectors' +import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { selectActiveQuote, + selectBuyAmountAfterFeesCryptoPrecision, + selectBuyAmountAfterFeesUserCurrency, selectFirstHop, selectIsTradeQuoteRequestAborted, selectIsUnsafeActiveQuote, @@ -40,49 +44,63 @@ import { } from 'state/slices/tradeQuoteSlice/selectors' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { useAppDispatch, useAppSelector } from 'state/store' -import { breakpoints } from 'theme/theme' import { useAccountIds } from '../../hooks/useAccountIds' +import { SharedTradeInput } from '../SharedTradeInput/SharedTradeInput' import { CollapsibleQuoteList } from './components/CollapsibleQuoteList' -import { ConfirmSummary } from './components/ConfirmSummary' -import { TradeInputBody } from './components/TradeInputBody' -import { TradeInputHeader } from './components/TradeInputHeader' -import { WithLazyMount } from './components/WithLazyMount' -import { useSharedHeight } from './hooks/useSharedHeight' +import { TradeSettingsMenu } from './components/TradeSettingsMenu' const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' } +const acknowledgementBoxProps = { + display: 'flex', + justifyContent: 'center', +} const STREAM_ACKNOWLEDGEMENT_MINIMUM_TIME_THRESHOLD = 1_000 * 60 * 5 type TradeInputProps = { tradeInputRef: React.MutableRefObject isCompact?: boolean + onChangeTab: (newTab: TradeInputTab) => void } -export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { +export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInputProps) => { const { dispatch: walletDispatch, state: { isConnected, isDemoWallet, wallet }, } = useWallet() - const bodyRef = useRef(null) - const totalHeight = useSharedHeight(tradeInputRef) - const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false }) + const { handleSubmit } = useFormContext() const dispatch = useAppDispatch() + const translate = useTranslate() const mixpanel = getMixPanel() const history = useHistory() const { showErrorToast } = useErrorHandler() + const { manualReceiveAddress, walletReceiveAddress } = useReceiveAddress({ + fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)), + }) + const { sellAssetAccountId, buyAssetAccountId, setSellAssetAccountId, setBuyAssetAccountId } = + useAccountIds() + const [isConfirmationLoading, setIsConfirmationLoading] = useState(false) const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) const [shouldShowStreamingAcknowledgement, setShouldShowStreamingAcknowledgement] = useState(false) const [shouldShowArbitrumBridgeAcknowledgement, setShouldShowArbitrumBridgeAcknowledgement] = useState(false) - const isKeplr = useMemo(() => !!wallet && isKeplrHDWallet(wallet), [wallet]) - const sellAsset = useAppSelector(selectInputSellAsset) + + const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) + const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency) + const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) + const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) + const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) + const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) const tradeQuoteStep = useAppSelector(selectFirstHop) const isUnsafeQuote = useAppSelector(selectIsUnsafeActiveQuote) - + const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) + const sellAsset = useAppSelector(selectInputSellAsset) + const buyAsset = useAppSelector(selectInputBuyAsset) + const activeQuote = useAppSelector(selectActiveQuote) const isAnyAccountMetadataLoadedForChainIdFilter = useMemo( () => ({ chainId: sellAsset.chainId }), [sellAsset.chainId], @@ -91,35 +109,13 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), ) - const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) - const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) - const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) - const activeQuote = useAppSelector(selectActiveQuote) - - const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) - const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) + const isKeplr = useMemo(() => !!wallet && isKeplrHDWallet(wallet), [wallet]) const isVotingPowerLoading = useMemo( () => isSnapshotApiQueriesPending && votingPower === undefined, [isSnapshotApiQueriesPending, votingPower], ) - const { - sellAssetAccountId: initialSellAssetAccountId, - buyAssetAccountId: initialBuyAssetAccountId, - setSellAssetAccountId, - setBuyAssetAccountId, - } = useAccountIds() - - const useReceiveAddressArgs = useMemo( - () => ({ - fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)), - }), - [wallet], - ) - - const { manualReceiveAddress, walletReceiveAddress } = useReceiveAddress(useReceiveAddressArgs) - const isLoading = useMemo( () => // No account meta loaded for that chain @@ -139,12 +135,63 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { ], ) - const translate = useTranslate() const overlayTitle = useMemo( () => translate('trade.swappingComingSoonForWallet', { walletName: 'Keplr' }), [translate], ) + useEffect(() => { + // Reset the trade warning if the active quote has changed, i.e. a better quote has come in and the + // user has not yet confirmed the previous one + if (shouldShowWarningAcknowledgement) setShouldShowWarningAcknowledgement(false) + // We also need to reset the streaming acknowledgement if the active quote has changed + if (shouldShowStreamingAcknowledgement) setShouldShowStreamingAcknowledgement(false) + if (shouldShowArbitrumBridgeAcknowledgement) setShouldShowArbitrumBridgeAcknowledgement(false) + // We need to ignore changes to shouldShowWarningAcknowledgement or this effect will react to itself + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeQuote]) + + const isEstimatedExecutionTimeOverThreshold = useMemo(() => { + if (!tradeQuoteStep?.estimatedExecutionTimeMs) return false + + if (tradeQuoteStep?.estimatedExecutionTimeMs >= STREAM_ACKNOWLEDGEMENT_MINIMUM_TIME_THRESHOLD) + return true + + return false + }, [tradeQuoteStep?.estimatedExecutionTimeMs]) + + const warningAcknowledgementMessage = useMemo(() => { + const recommendedMinimumCryptoBaseUnit = (activeQuote as ThorTradeQuote) + ?.recommendedMinimumCryptoBaseUnit + if (!recommendedMinimumCryptoBaseUnit) return translate('warningAcknowledgement.unsafeTrade') + const recommendedMinimumCryptoPrecision = fromBaseUnit( + recommendedMinimumCryptoBaseUnit, + sellAsset.precision, + ) + const message = translate('trade.errors.unsafeQuote', { + symbol: sellAsset.symbol, + recommendedMin: recommendedMinimumCryptoPrecision, + }) + return message + }, [activeQuote, sellAsset.precision, sellAsset.symbol, translate]) + + const headerRightContent = useMemo(() => { + return + }, [isCompact, isLoading]) + + const setBuyAsset = useCallback( + (asset: Asset) => dispatch(tradeInput.actions.setBuyAsset(asset)), + [dispatch], + ) + const setSellAsset = useCallback( + (asset: Asset) => dispatch(tradeInput.actions.setSellAsset(asset)), + [dispatch], + ) + const handleSwitchAssets = useCallback( + () => dispatch(tradeInput.actions.switchAssets()), + [dispatch], + ) + const handleConnect = useCallback(() => { walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) }, [walletDispatch]) @@ -193,37 +240,8 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { wallet, ]) - useEffect(() => { - // Reset the trade warning if the active quote has changed, i.e. a better quote has come in and the - // user has not yet confirmed the previous one - if (shouldShowWarningAcknowledgement) setShouldShowWarningAcknowledgement(false) - // We also need to reset the streaming acknowledgement if the active quote has changed - if (shouldShowStreamingAcknowledgement) setShouldShowStreamingAcknowledgement(false) - if (shouldShowArbitrumBridgeAcknowledgement) setShouldShowArbitrumBridgeAcknowledgement(false) - // We need to ignore changes to shouldShowWarningAcknowledgement or this effect will react to itself - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeQuote]) - - const isEstimatedExecutionTimeOverThreshold = useMemo(() => { - if (!tradeQuoteStep?.estimatedExecutionTimeMs) return false - - if (tradeQuoteStep?.estimatedExecutionTimeMs >= STREAM_ACKNOWLEDGEMENT_MINIMUM_TIME_THRESHOLD) - return true - - return false - }, [tradeQuoteStep?.estimatedExecutionTimeMs]) - const handleFormSubmit = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]) - const handleChangeTab = useCallback( - (newTab: TradeInputTab) => { - if (newTab === TradeInputTab.Claim) { - history.push(TradeRoutePaths.Claim) - } - }, - [history], - ) - // If the warning acknowledgement is shown, we need to handle the submit differently because we might want to show the streaming acknowledgement const handleWarningAcknowledgementSubmit = useCallback(() => { if (activeQuote?.isStreaming && isEstimatedExecutionTimeOverThreshold) @@ -247,92 +265,57 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => { [isUnsafeQuote, activeQuote, isEstimatedExecutionTimeOverThreshold, handleFormSubmit], ) - const warningAcknowledgementMessage = (() => { - const recommendedMinimumCryptoBaseUnit = (activeQuote as ThorTradeQuote) - ?.recommendedMinimumCryptoBaseUnit - if (!recommendedMinimumCryptoBaseUnit) return translate('warningAcknowledgement.unsafeTrade') - const recommendedMinimumCryptoPrecision = fromBaseUnit( - recommendedMinimumCryptoBaseUnit, - sellAsset.precision, - ) - const message = translate('trade.errors.unsafeQuote', { - symbol: sellAsset.symbol, - recommendedMin: recommendedMinimumCryptoPrecision, - }) - return message - })() - return ( - -
- - - - - - - - - - - - - - - - - - - -
-
+ + + + + +
) } diff --git a/src/components/MultiHopTrade/components/TradeInput/components/Claim/Claim.tsx b/src/components/MultiHopTrade/components/TradeInput/components/Claim/Claim.tsx index a7ceca2a8f8..adc392c698d 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/Claim/Claim.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/Claim/Claim.tsx @@ -1,10 +1,10 @@ import { Card } from '@chakra-ui/react' import type { TxStatus } from '@shapeshiftoss/unchained-client' import { useCallback, useState } from 'react' -import { MemoryRouter, Route, Switch, useHistory, useLocation } from 'react-router' -import { TradeInputTab, TradeRoutePaths } from 'components/MultiHopTrade/types' +import { MemoryRouter, Route, Switch, useLocation } from 'react-router' +import { TradeInputTab } from 'components/MultiHopTrade/types' -import { TradeInputHeader } from '../TradeInputHeader' +import { SharedTradeInputHeader } from '../../../SharedTradeInput/SharedTradeInputHeader' import { ClaimConfirm } from './ClaimConfirm' import { ClaimSelect } from './ClaimSelect' import { ClaimStatus } from './ClaimStatus' @@ -13,23 +13,13 @@ import { ClaimRoutePaths } from './types' const ClaimRouteEntries = [ClaimRoutePaths.Select, ClaimRoutePaths.Confirm, ClaimRoutePaths.Status] -export const Claim = ({ isCompact }: { isCompact?: boolean }) => { +export const Claim = ({ onChangeTab }: { onChangeTab: (newTab: TradeInputTab) => void }) => { const location = useLocation() - const history = useHistory() const [activeClaim, setActiveClaim] = useState() const [claimTxHash, setClaimTxHash] = useState() const [claimTxStatus, setClaimTxStatus] = useState() - const handleChangeTab = useCallback( - (newTab: TradeInputTab) => { - if (newTab === TradeInputTab.Trade) { - history.push(TradeRoutePaths.Input) - } - }, - [history], - ) - const renderClaimSelect = useCallback(() => { return }, []) @@ -64,12 +54,7 @@ export const Claim = ({ isCompact }: { isCompact?: boolean }) => { - + void -} - -const TradeInputHeaderRightComponent = ({ - isCompact, - isLoading, -}: TradeInputHeaderRightComponentProps) => { - const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false }) - const activeQuote = useAppSelector(selectActiveQuote) - const activeSwapperName = useAppSelector(selectActiveSwapperName) - const isTradeQuoteApiQueryPending = useAppSelector(selectIsTradeQuoteApiQueryPending) - - const pollingInterval = useMemo(() => { - if (!activeSwapperName) return DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL - return swappers[activeSwapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL - }, [activeSwapperName]) - - const isRefetching = useMemo( - () => Boolean(activeSwapperName && isTradeQuoteApiQueryPending[activeSwapperName] === true), - [activeSwapperName, isTradeQuoteApiQueryPending], - ) - - return ( - <> - {activeQuote && (isCompact || isSmallerThanXl) && ( - - )} - - - ) -} - -export const TradeInputHeader = ({ - initialTab, - isCompact, - isLoading, - onChangeTab, -}: FakeTabHeaderProps) => { - const translate = useTranslate() - const [selectedTab, setSelectedTab] = useState(initialTab) - - const enableBridgeClaims = useFeatureFlag('ArbitrumBridgeClaims') - - const handleClickTrade = useCallback(() => { - setSelectedTab(TradeInputTab.Trade) - onChangeTab(TradeInputTab.Trade) - }, [onChangeTab]) - - const handleClickClaim = useCallback(() => { - setSelectedTab(TradeInputTab.Claim) - onChangeTab(TradeInputTab.Claim) - }, [onChangeTab]) - - const rightComponent = useMemo(() => { - return (() => { - switch (selectedTab) { - case TradeInputTab.Trade: - return - case TradeInputTab.Claim: - return null - default: - assertUnreachable(selectedTab) - } - })() - }, [selectedTab, isLoading, isCompact]) - - return ( - - - - - {translate('navBar.trade')} - - {enableBridgeClaims && ( - - {translate('bridge.claim')} - - )} - - - {rightComponent} - - - - ) -} diff --git a/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx b/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx new file mode 100644 index 00000000000..94629868090 --- /dev/null +++ b/src/components/MultiHopTrade/components/TradeInput/components/TradeSettingsMenu.tsx @@ -0,0 +1,41 @@ +import { useMediaQuery } from '@chakra-ui/react' +import { DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL, swappers } from '@shapeshiftoss/swapper' +import { useMemo } from 'react' +import { selectIsTradeQuoteApiQueryPending } from 'state/apis/swapper/selectors' +import { selectActiveQuote, selectActiveSwapperName } from 'state/slices/tradeQuoteSlice/selectors' +import { useAppSelector } from 'state/store' +import { breakpoints } from 'theme/theme' + +import { SlippagePopover } from '../../SlippagePopover' +import { CountdownSpinner } from './TradeQuotes/components/CountdownSpinner' + +type TradeSettingsMenuProps = { + isCompact: boolean | undefined + isLoading: boolean +} + +export const TradeSettingsMenu = ({ isCompact, isLoading }: TradeSettingsMenuProps) => { + const [isSmallerThanXl] = useMediaQuery(`(max-width: ${breakpoints.xl})`, { ssr: false }) + const activeQuote = useAppSelector(selectActiveQuote) + const activeSwapperName = useAppSelector(selectActiveSwapperName) + const isTradeQuoteApiQueryPending = useAppSelector(selectIsTradeQuoteApiQueryPending) + + const pollingInterval = useMemo(() => { + if (!activeSwapperName) return DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL + return swappers[activeSwapperName]?.pollingInterval ?? DEFAULT_GET_TRADE_QUOTE_POLLING_INTERVAL + }, [activeSwapperName]) + + const isRefetching = useMemo( + () => Boolean(activeSwapperName && isTradeQuoteApiQueryPending[activeSwapperName] === true), + [activeSwapperName, isTradeQuoteApiQueryPending], + ) + + return ( + <> + {activeQuote && (isCompact || isSmallerThanXl) && ( + + )} + + + ) +} diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx index bb53ff68ad9..842e8403d77 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx @@ -1,9 +1,11 @@ +import type { ResponsiveValue } from '@chakra-ui/react' import { Skeleton, useToast } from '@chakra-ui/react' import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId, fromAssetId, thorchainAssetId, toAssetId } from '@shapeshiftoss/caip' import { ContractType, getOrCreateContractByType } from '@shapeshiftoss/contracts' import type { Asset } from '@shapeshiftoss/types' import { useQueryClient } from '@tanstack/react-query' +import type * as CSS from 'csstype' import type { DepositValues } from 'features/defi/components/Deposit/Deposit' import { Deposit as ReusableDeposit } from 'features/defi/components/Deposit/Deposit' import type { @@ -76,6 +78,9 @@ type DepositProps = StepComponentProps & { } const percentOptions = [0.25, 0.5, 0.75, 1] +const infoAcknowledgementBoxProps = { + position: 'static' as ResponsiveValue, +} export const Deposit: React.FC = ({ accountId, @@ -861,7 +866,7 @@ export const Deposit: React.FC = ({ onAcknowledge={handleAcknowledge} shouldShowAcknowledgement={shouldShowInfoAcknowledgement} setShouldShowAcknowledgement={setShouldShowInfoAcknowledgement} - position='static' + boxProps={infoAcknowledgementBoxProps} > Date: Tue, 15 Oct 2024 12:54:29 +1100 Subject: [PATCH 2/9] feat: placeholder ui for limit orders development (#7938) --- .env.develop | 1 + src/assets/translations/en/main.json | 3 + .../MultiHopTrade/MultiHopTrade.tsx | 12 ++ .../components/LimitOrder/LimitOrder.tsx | 194 ++++++++++++++++++ .../SharedTradeInput/SharedTradeInput.tsx | 6 +- .../SharedTradeInputHeader.tsx | 16 ++ .../components/TradeInput/TradeInput.tsx | 4 +- src/components/MultiHopTrade/types.ts | 2 + src/config.ts | 1 + .../preferencesSlice/preferencesSlice.ts | 2 + src/test/mocks/store.ts | 1 + 11 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx diff --git a/.env.develop b/.env.develop index fec09cb3922..af8a2bb3196 100644 --- a/.env.develop +++ b/.env.develop @@ -1,4 +1,5 @@ # feature flags +REACT_APP_FEATURE_LIMIT_ORDERS=true # mixpanel REACT_APP_MIXPANEL_TOKEN=1c1369f6ea23a6404bac41b42817cc4b diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 453573f5fc3..175c202f4dd 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -987,6 +987,9 @@ "slippage": "Slippage: %{slippageFormatted}" } }, + "limitOrder": { + "heading": "Limit Order" + }, "modals": { "assetSearch": { "myAssets": "My Assets", diff --git a/src/components/MultiHopTrade/MultiHopTrade.tsx b/src/components/MultiHopTrade/MultiHopTrade.tsx index 38ac6ff405b..68016a67d1f 100644 --- a/src/components/MultiHopTrade/MultiHopTrade.tsx +++ b/src/components/MultiHopTrade/MultiHopTrade.tsx @@ -9,6 +9,7 @@ import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { tradeQuoteSlice } from 'state/slices/tradeQuoteSlice/tradeQuoteSlice' import { useAppDispatch, useAppSelector } from 'state/store' +import { LimitOrder } from './components/LimitOrder/LimitOrder' import { MultiHopTradeConfirm } from './components/MultiHopTradeConfirm/MultiHopTradeConfirm' import { QuoteListRoute } from './components/QuoteList/QuoteListRoute' import { Claim } from './components/TradeInput/components/Claim/Claim' @@ -23,6 +24,7 @@ const TradeRouteEntries = [ TradeRoutePaths.VerifyAddresses, TradeRoutePaths.QuoteList, TradeRoutePaths.Claim, + TradeRoutePaths.LimitOrder, ] export type TradeCardProps = { @@ -110,6 +112,9 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { case TradeInputTab.Trade: history.push(TradeRoutePaths.Input) break + case TradeInputTab.LimitOrder: + history.push(TradeRoutePaths.LimitOrder) + break case TradeInputTab.Claim: history.push(TradeRoutePaths.Claim) break @@ -146,6 +151,13 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => { + + +
{/* Stop polling for quotes by unmounting the hook. This prevents trade execution getting */} diff --git a/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx b/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx new file mode 100644 index 00000000000..d378f512cb9 --- /dev/null +++ b/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx @@ -0,0 +1,194 @@ +import { isLedger } from '@shapeshiftoss/hdwallet-ledger' +import type { Asset } from '@shapeshiftoss/types' +import type { FormEvent } from 'react' +import { useCallback, useMemo, useState } from 'react' +import { useFormContext } from 'react-hook-form' +import { ethereum, fox } from 'test/mocks/assets' +import { WarningAcknowledgement } from 'components/Acknowledgement/Acknowledgement' +import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' +import { TradeInputTab } from 'components/MultiHopTrade/types' +import { WalletActions } from 'context/WalletProvider/actions' +import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' +import { useWallet } from 'hooks/useWallet/useWallet' +import type { ParameterModel } from 'lib/fees/parameters/types' +import { selectIsSnapshotApiQueriesPending, selectVotingPower } from 'state/apis/snapshot/selectors' +import { + selectHasUserEnteredAmount, + selectIsAnyAccountMetadataLoadedForChainId, +} from 'state/slices/selectors' +import { + selectActiveQuote, + selectBuyAmountAfterFeesCryptoPrecision, + selectBuyAmountAfterFeesUserCurrency, + selectIsTradeQuoteRequestAborted, + selectShouldShowTradeQuoteOrAwaitInput, +} from 'state/slices/tradeQuoteSlice/selectors' +import { useAppSelector } from 'state/store' + +import { useAccountIds } from '../../hooks/useAccountIds' +import { SharedTradeInput } from '../SharedTradeInput/SharedTradeInput' + +const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' } +const acknowledgementBoxProps = { + display: 'flex', + justifyContent: 'center', +} + +type LimitOrderProps = { + tradeInputRef: React.MutableRefObject + isCompact?: boolean + onChangeTab: (newTab: TradeInputTab) => void +} + +// TODO: Implement me +const CollapsibleLimitOrderList = () => <> + +export const LimitOrder = ({ isCompact, tradeInputRef, onChangeTab }: LimitOrderProps) => { + const { + dispatch: walletDispatch, + state: { isConnected, isDemoWallet, wallet }, + } = useWallet() + + const { handleSubmit } = useFormContext() + const { showErrorToast } = useErrorHandler() + const { manualReceiveAddress, walletReceiveAddress } = useReceiveAddress({ + fetchUnchainedAddress: Boolean(wallet && isLedger(wallet)), + }) + const { sellAssetAccountId, buyAssetAccountId, setSellAssetAccountId, setBuyAssetAccountId } = + useAccountIds() + + const [isConfirmationLoading, setIsConfirmationLoading] = useState(false) + const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) + + const buyAmountAfterFeesCryptoPrecision = useAppSelector(selectBuyAmountAfterFeesCryptoPrecision) + const buyAmountAfterFeesUserCurrency = useAppSelector(selectBuyAmountAfterFeesUserCurrency) + const shouldShowTradeQuoteOrAwaitInput = useAppSelector(selectShouldShowTradeQuoteOrAwaitInput) + const isSnapshotApiQueriesPending = useAppSelector(selectIsSnapshotApiQueriesPending) + const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) + const hasUserEnteredAmount = useAppSelector(selectHasUserEnteredAmount) + const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) + const sellAsset = ethereum // TODO: Implement me + const buyAsset = fox // TODO: Implement me + const activeQuote = useAppSelector(selectActiveQuote) + const isAnyAccountMetadataLoadedForChainIdFilter = useMemo( + () => ({ chainId: sellAsset.chainId }), + [sellAsset.chainId], + ) + const isAnyAccountMetadataLoadedForChainId = useAppSelector(state => + selectIsAnyAccountMetadataLoadedForChainId(state, isAnyAccountMetadataLoadedForChainIdFilter), + ) + + const isVotingPowerLoading = useMemo( + () => isSnapshotApiQueriesPending && votingPower === undefined, + [isSnapshotApiQueriesPending, votingPower], + ) + + const isLoading = useMemo( + () => + // No account meta loaded for that chain + !isAnyAccountMetadataLoadedForChainId || + (!shouldShowTradeQuoteOrAwaitInput && !isTradeQuoteRequestAborted) || + isConfirmationLoading || + // Only consider snapshot API queries as pending if we don't have voting power yet + // if we do, it means we have persisted or cached (both stale) data, which is enough to let the user continue + // as we are optimistic and don't want to be waiting for a potentially very long time for the snapshot API to respond + isVotingPowerLoading, + [ + isAnyAccountMetadataLoadedForChainId, + shouldShowTradeQuoteOrAwaitInput, + isTradeQuoteRequestAborted, + isConfirmationLoading, + isVotingPowerLoading, + ], + ) + + const warningAcknowledgementMessage = useMemo(() => { + // TODO: Implement me + return '' + }, []) + + const headerRightContent = useMemo(() => { + // TODO: Implement me + return <> + }, []) + + const setBuyAsset = useCallback((_asset: Asset) => { + // TODO: Implement me + }, []) + const setSellAsset = useCallback((_asset: Asset) => { + // TODO: Implement me + }, []) + const handleSwitchAssets = useCallback(() => { + // TODO: Implement me + }, []) + + const handleConnect = useCallback(() => { + walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) + }, [walletDispatch]) + + const onSubmit = useCallback(() => { + // No preview happening if wallet isn't connected i.e is using the demo wallet + if (!isConnected || isDemoWallet) { + return handleConnect() + } + + setIsConfirmationLoading(true) + try { + // TODO: Implement me + } catch (e) { + showErrorToast(e) + } + + setIsConfirmationLoading(false) + }, [handleConnect, isConnected, isDemoWallet, showErrorToast]) + + const handleFormSubmit = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]) + + const handleWarningAcknowledgementSubmit = useCallback(() => { + handleFormSubmit() + }, [handleFormSubmit]) + + const handleTradeQuoteConfirm = useCallback( + (e: FormEvent) => { + e.preventDefault() + handleFormSubmit() + }, + [handleFormSubmit], + ) + + return ( + + + + ) +} diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx index 67b17e1e6be..2a08e62a3d1 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInput.tsx @@ -3,7 +3,7 @@ import type { AccountId } from '@shapeshiftoss/caip' import type { TradeQuote } from '@shapeshiftoss/swapper' import type { Asset } from '@shapeshiftoss/types' import type { FormEvent } from 'react' -import { TradeInputTab } from 'components/MultiHopTrade/types' +import type { TradeInputTab } from 'components/MultiHopTrade/types' import { breakpoints } from 'theme/theme' import { SharedTradeInputBody } from '../SharedTradeInput/SharedTradeInputBody' @@ -27,6 +27,7 @@ type SharedTradeInputProps = { sellAsset: Asset sideComponent: React.ComponentType tradeInputRef: React.RefObject + tradeInputTab: TradeInputTab walletReceiveAddress: string | undefined handleSwitchAssets: () => void onChangeTab: (newTab: TradeInputTab) => void @@ -51,6 +52,7 @@ export const SharedTradeInput: React.FC = ({ manualReceiveAddress, sellAsset, sideComponent, + tradeInputTab, tradeInputRef, walletReceiveAddress, handleSwitchAssets, @@ -81,7 +83,7 @@ export const SharedTradeInput: React.FC = ({ onSubmit={onSubmit} > diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputHeader.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputHeader.tsx index d557a90cec5..3e5a6ef28f8 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputHeader.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputHeader.tsx @@ -20,6 +20,7 @@ export const SharedTradeInputHeader = ({ const [selectedTab, setSelectedTab] = useState(initialTab) const enableBridgeClaims = useFeatureFlag('ArbitrumBridgeClaims') + const enableLimitOrders = useFeatureFlag('LimitOrders') const handleChangeTab = useCallback( (newTab: TradeInputTab) => { @@ -33,6 +34,10 @@ export const SharedTradeInputHeader = ({ handleChangeTab(TradeInputTab.Trade) }, [handleChangeTab]) + const handleClickLimitOrder = useCallback(() => { + handleChangeTab(TradeInputTab.LimitOrder) + }, [handleChangeTab]) + const handleClickClaim = useCallback(() => { handleChangeTab(TradeInputTab.Claim) }, [handleChangeTab]) @@ -50,6 +55,17 @@ export const SharedTradeInputHeader = ({ > {translate('navBar.trade')} + {enableLimitOrders && ( + + {translate('limitOrder.heading')} + + )} {enableBridgeClaims && ( ({ errors }: envalid.ReporterOptions) { diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 9d3d914dca0..181e19c5686 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -67,6 +67,7 @@ export type FeatureFlags = { FoxPage: boolean FoxPageRFOX: boolean FoxPageFoxSection: boolean + LimitOrders: boolean } export type Flag = keyof FeatureFlags @@ -159,6 +160,7 @@ const initialState: Preferences = { FoxPage: getConfig().REACT_APP_FEATURE_FOX_PAGE, FoxPageRFOX: getConfig().REACT_APP_FEATURE_FOX_PAGE_RFOX, FoxPageFoxSection: getConfig().REACT_APP_FEATURE_FOX_PAGE_FOX_SECTION, + LimitOrders: getConfig().REACT_APP_FEATURE_LIMIT_ORDERS, }, selectedLocale: simpleLocale(), balanceThreshold: '0', diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index dea203be645..2bc5d97b54a 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -124,6 +124,7 @@ export const mockStore: ReduxState = { FoxPage: false, FoxPageRFOX: false, FoxPageFoxSection: false, + LimitOrders: false, }, selectedLocale: 'en', balanceThreshold: '0', From c542ca27370efb05d51a5d160d69077a243c641d Mon Sep 17 00:00:00 2001 From: kevin <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:59:01 -0600 Subject: [PATCH 3/9] feat: solana standard tx parsing (#7924) * feat: solana standard tx parsing * comment --------- Co-authored-by: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> --- packages/unchained-client/openapitools.json | 13 ++ packages/unchained-client/src/index.ts | 1 + packages/unchained-client/src/solana/index.ts | 5 + .../parser/__tests__/mockData/solSelfSend.ts | 72 ++++++++++ .../parser/__tests__/mockData/solStandard.ts | 60 +++++++++ .../solana/parser/__tests__/solana.test.ts | 126 ++++++++++++++++++ .../src/solana/parser/index.ts | 99 ++++++++++++++ .../src/solana/parser/types.ts | 14 ++ 8 files changed, 390 insertions(+) create mode 100644 packages/unchained-client/src/solana/index.ts create mode 100644 packages/unchained-client/src/solana/parser/__tests__/mockData/solSelfSend.ts create mode 100644 packages/unchained-client/src/solana/parser/__tests__/mockData/solStandard.ts create mode 100644 packages/unchained-client/src/solana/parser/__tests__/solana.test.ts create mode 100644 packages/unchained-client/src/solana/parser/index.ts create mode 100644 packages/unchained-client/src/solana/parser/types.ts diff --git a/packages/unchained-client/openapitools.json b/packages/unchained-client/openapitools.json index 3e66633fe22..a8062c8f840 100644 --- a/packages/unchained-client/openapitools.json +++ b/packages/unchained-client/openapitools.json @@ -187,6 +187,19 @@ "useSingleRequestParameter": true } }, + "solana": { + "inputSpec": "https://raw.githubusercontent.com/shapeshift/unchained/develop/node/coinstacks/solana/api/src/swagger.json", + "generatorName": "typescript-fetch", + "output": "#{cwd}/src/generated/solana", + "enablePostProcessFile": true, + "reservedWordsMappings": { + "in": "in" + }, + "additionalProperties": { + "supportsES6": "true", + "useSingleRequestParameter": true + } + }, "thorchain": { "inputSpec": "https://raw.githubusercontent.com/shapeshift/unchained/develop/go/coinstacks/thorchain/api/swagger.json", "generatorName": "typescript-fetch", diff --git a/packages/unchained-client/src/index.ts b/packages/unchained-client/src/index.ts index 7ea558079ae..185b08a6e48 100644 --- a/packages/unchained-client/src/index.ts +++ b/packages/unchained-client/src/index.ts @@ -5,6 +5,7 @@ export * as ws from './websocket' export * as evm from './evm' export * as utxo from './utxo' export * as cosmossdk from './cosmossdk' +export * as solana from './solana' export * as ethereum from './evm/ethereum' export * as avalanche from './evm/avalanche' diff --git a/packages/unchained-client/src/solana/index.ts b/packages/unchained-client/src/solana/index.ts new file mode 100644 index 00000000000..f498d679d1c --- /dev/null +++ b/packages/unchained-client/src/solana/index.ts @@ -0,0 +1,5 @@ +import type { V1Api } from '../generated/solana' + +export type Api = V1Api + +export * from './parser' diff --git a/packages/unchained-client/src/solana/parser/__tests__/mockData/solSelfSend.ts b/packages/unchained-client/src/solana/parser/__tests__/mockData/solSelfSend.ts new file mode 100644 index 00000000000..53da088c75f --- /dev/null +++ b/packages/unchained-client/src/solana/parser/__tests__/mockData/solSelfSend.ts @@ -0,0 +1,72 @@ +import type { Tx } from '../../..' + +const tx: Tx = { + txid: '3owXWn8Em7FE7Dyao3kPLkTPySiGGSSo9e7VGiWDifk6GfQRrm2JYHdHStBzVRr6b6o1PztbGpuDsXb8o2yPxoV3', + blockHeight: 293321352, + description: + 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV transferred 0.000000001 SOL to DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV.', + type: 'TRANSFER', + source: 'SYSTEM_PROGRAM', + fee: 25000, + feePayer: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV', + signature: + '3owXWn8Em7FE7Dyao3kPLkTPySiGGSSo9e7VGiWDifk6GfQRrm2JYHdHStBzVRr6b6o1PztbGpuDsXb8o2yPxoV3', + slot: 293321352, + timestamp: 1727896282, + tokenTransfers: [], + nativeTransfers: [ + { + fromUserAccount: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV', + toUserAccount: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV', + amount: 1, + }, + ], + accountData: [ + { + account: 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV', + nativeBalanceChange: -25000, + tokenBalanceChanges: [], + }, + { + account: 'ComputeBudget111111111111111111111111111111', + nativeBalanceChange: 0, + tokenBalanceChanges: [], + }, + { + account: '11111111111111111111111111111111', + nativeBalanceChange: 0, + tokenBalanceChanges: [], + }, + ], + transactionError: null, + instructions: [ + { + accounts: [], + data: '3gJqkocMWaMm', + programId: 'ComputeBudget111111111111111111111111111111', + innerInstructions: [], + }, + { + accounts: [], + data: 'Fj2Eoy', + programId: 'ComputeBudget111111111111111111111111111111', + innerInstructions: [], + }, + { + accounts: [ + 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV', + 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV', + ], + data: '3Bxs412MvVNQj175', + programId: '11111111111111111111111111111111', + innerInstructions: [], + }, + ], + events: { + compressed: null, + nft: null, + swap: null, + }, +} + +export default { tx } diff --git a/packages/unchained-client/src/solana/parser/__tests__/mockData/solStandard.ts b/packages/unchained-client/src/solana/parser/__tests__/mockData/solStandard.ts new file mode 100644 index 00000000000..6a2c59109a2 --- /dev/null +++ b/packages/unchained-client/src/solana/parser/__tests__/mockData/solStandard.ts @@ -0,0 +1,60 @@ +import type { Tx } from '../../..' + +const tx: Tx = { + txid: 'qN3jbqvw2ypfmTVJuUiohgLQgV4mq8oZ6QzuKhNeM8MX1bdAxCK7EoXJbvBUD61mhGmrFr1KQi5FqgcadfYi7CS', + blockHeight: 294850279, + description: + 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW transferred 0.010000388 SOL to DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL.', + type: 'TRANSFER', + source: 'SYSTEM_PROGRAM', + fee: 5000, + feePayer: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW', + signature: + 'qN3jbqvw2ypfmTVJuUiohgLQgV4mq8oZ6QzuKhNeM8MX1bdAxCK7EoXJbvBUD61mhGmrFr1KQi5FqgcadfYi7CS', + slot: 294850279, + timestamp: 1728580091, + tokenTransfers: [], + nativeTransfers: [ + { + fromUserAccount: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW', + toUserAccount: 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL', + amount: 10000388, + }, + ], + accountData: [ + { + account: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW', + nativeBalanceChange: -10005388, + tokenBalanceChanges: [], + }, + { + account: 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL', + nativeBalanceChange: 10000388, + tokenBalanceChanges: [], + }, + { + account: '11111111111111111111111111111111', + nativeBalanceChange: 0, + tokenBalanceChanges: [], + }, + ], + transactionError: null, + instructions: [ + { + accounts: [ + 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW', + 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL', + ], + data: '3Bxs41dFLGCCYtUF', + programId: '11111111111111111111111111111111', + innerInstructions: [], + }, + ], + events: { + compressed: null, + nft: null, + swap: null, + }, +} + +export default { tx } diff --git a/packages/unchained-client/src/solana/parser/__tests__/solana.test.ts b/packages/unchained-client/src/solana/parser/__tests__/solana.test.ts new file mode 100644 index 00000000000..d6a5eb71511 --- /dev/null +++ b/packages/unchained-client/src/solana/parser/__tests__/solana.test.ts @@ -0,0 +1,126 @@ +import { solanaChainId, solAssetId } from '@shapeshiftoss/caip' +import { beforeAll, describe, expect, it, vi } from 'vitest' + +import { TransferType, TxStatus } from '../../../types' +import type { ParsedTx } from '../../parser' +import { TransactionParser } from '../index' +import solSelfSend from './mockData/solSelfSend' +import solStandard from './mockData/solStandard' + +const txParser = new TransactionParser({ assetId: solAssetId, chainId: solanaChainId }) + +describe('parseTx', () => { + beforeAll(() => { + vi.clearAllMocks() + }) + + describe('standard', () => { + it('should be able to parse sol send', async () => { + const { tx } = solStandard + const address = 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW' + + const expected: ParsedTx = { + address, + blockHash: tx.blockHash, + blockHeight: tx.blockHeight, + blockTime: tx.timestamp, + chainId: solanaChainId, + confirmations: 1, + fee: { + assetId: solAssetId, + value: '5000', + }, + status: TxStatus.Confirmed, + transfers: [ + { + assetId: solAssetId, + components: [{ value: '10000388' }], + from: address, + to: 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL', + totalValue: '10000388', + type: TransferType.Send, + }, + ], + txid: tx.txid, + } + + const actual = await txParser.parse(tx, address) + + expect(actual).toEqual(expected) + }) + + it('should be able to parse sol receive', async () => { + const { tx } = solStandard + const address = 'DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL' + + const expected: ParsedTx = { + address, + blockHash: tx.blockHash, + blockHeight: tx.blockHeight, + blockTime: tx.timestamp, + chainId: solanaChainId, + confirmations: 1, + status: TxStatus.Confirmed, + transfers: [ + { + assetId: solAssetId, + components: [{ value: '10000388' }], + from: 'B1fnGVnz6Q2eZPXG1FPa8wix88yyNApwGhJTURHPh4qW', + to: address, + totalValue: '10000388', + type: TransferType.Receive, + }, + ], + txid: tx.txid, + } + + const actual = await txParser.parse(tx, address) + + expect(actual).toEqual(expected) + }) + }) + + describe('self send', () => { + it('should be able to parse sol', async () => { + const { tx } = solSelfSend + const address = 'DsYwEVzeSNMkU5PVwjwtZ8EDRQxaR6paXfFAdhMQxmaV' + + const expected: ParsedTx = { + txid: tx.txid, + blockHash: tx.blockHash, + blockHeight: tx.blockHeight, + blockTime: tx.timestamp, + address, + chainId: solanaChainId, + confirmations: 1, + status: TxStatus.Confirmed, + fee: { + value: '25000', + assetId: solAssetId, + }, + transfers: [ + { + type: TransferType.Send, + from: address, + to: address, + assetId: solAssetId, + totalValue: '1', + components: [{ value: '1' }], + }, + { + type: TransferType.Receive, + from: address, + to: address, + assetId: solAssetId, + totalValue: '1', + components: [{ value: '1' }], + }, + ], + } + + const actual = await txParser.parse(tx, address) + + expect(actual).toEqual(expected) + }) + }) +}) diff --git a/packages/unchained-client/src/solana/parser/index.ts b/packages/unchained-client/src/solana/parser/index.ts new file mode 100644 index 00000000000..c514f2e5ba9 --- /dev/null +++ b/packages/unchained-client/src/solana/parser/index.ts @@ -0,0 +1,99 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' + +import { TransferType, TxStatus } from '../../types' +import { aggregateTransfer } from '../../utils' +import type { ParsedTx, SubParser, Tx } from './types' + +export * from './types' + +export interface TransactionParserArgs { + chainId: ChainId + assetId: AssetId +} + +export class TransactionParser { + chainId: ChainId + assetId: AssetId + + private parsers: SubParser[] = [] + + constructor(args: TransactionParserArgs) { + this.chainId = args.chainId + this.assetId = args.assetId + } + + /** + * Register custom transaction sub parser to parse custom op return data + * + * _parsers should be registered from most generic first to most specific last_ + */ + registerParser(parser: SubParser): void { + this.parsers.unshift(parser) + } + + protected registerParsers(parsers: SubParser[]): void { + parsers.forEach(parser => this.registerParser(parser)) + } + + async parse(tx: T, address: string): Promise { + const parserResult = await (async () => { + for (const parser of this.parsers) { + const result = await parser.parse(tx, address) + if (result) return result + } + })() + + const parsedTx: ParsedTx = { + address, + blockHeight: tx.blockHeight, + blockTime: tx.timestamp, + chainId: this.chainId, + // all transactions from unchained are finalized with at least 1 confirmation (unused throughout web) + confirmations: 1, + status: this.getStatus(tx), + trade: parserResult?.trade, + transfers: parserResult?.transfers ?? [], + txid: tx.txid, + } + + // network fee + if (tx.feePayer === address && tx.fee) { + parsedTx.fee = { assetId: this.assetId, value: BigInt(tx.fee).toString() } + } + + tx.nativeTransfers?.forEach(nativeTransfer => { + const { amount, fromUserAccount, toUserAccount } = nativeTransfer + + // send amount + if (nativeTransfer.fromUserAccount === address) { + parsedTx.transfers = aggregateTransfer({ + assetId: this.assetId, + from: fromUserAccount ?? '', + to: toUserAccount ?? '', + transfers: parsedTx.transfers, + type: TransferType.Send, + value: BigInt(amount).toString(), + }) + } + + // receive amount + if (nativeTransfer.toUserAccount === address) { + parsedTx.transfers = aggregateTransfer({ + assetId: this.assetId, + from: fromUserAccount ?? '', + to: toUserAccount ?? '', + transfers: parsedTx.transfers, + type: TransferType.Receive, + value: BigInt(amount).toString(), + }) + } + }) + + return parsedTx + } + + private getStatus(tx: T): TxStatus { + if (tx.transactionError) return TxStatus.Failed + return TxStatus.Confirmed + } +} diff --git a/packages/unchained-client/src/solana/parser/types.ts b/packages/unchained-client/src/solana/parser/types.ts new file mode 100644 index 00000000000..de2b3774dc0 --- /dev/null +++ b/packages/unchained-client/src/solana/parser/types.ts @@ -0,0 +1,14 @@ +import type * as solana from '../../generated/solana' +import type { StandardTx } from '../../types' + +export * from '../../generated/solana' + +export type Tx = solana.Tx + +export interface ParsedTx extends StandardTx {} + +export type TxSpecific = Partial> + +export interface SubParser { + parse(tx: T, address: string): Promise +} From b1418719ae1dd9f74d6d8cd15668370bc83edb3a Mon Sep 17 00:00:00 2001 From: kevin <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:12:52 -0600 Subject: [PATCH 4/9] feat: solana chain adapter (#7925) * feat: solana standard tx parsing * comment * feat: solana chain adapter * update asset namespace --------- Co-authored-by: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com> Co-authored-by: Apotheosis <0xapotheosis@gmail.com> --- package.json | 28 +- packages/chain-adapters/package.json | 1 + packages/chain-adapters/src/index.ts | 1 + .../src/solana/SolanaChainAdapter.ts | 360 ++++++++++++++++++ packages/chain-adapters/src/solana/index.ts | 3 + packages/chain-adapters/src/solana/types.ts | 41 ++ packages/chain-adapters/src/solana/utils.ts | 5 + packages/chain-adapters/src/types.ts | 8 + yarn.lock | 174 ++++----- 9 files changed, 521 insertions(+), 100 deletions(-) create mode 100644 packages/chain-adapters/src/solana/SolanaChainAdapter.ts create mode 100644 packages/chain-adapters/src/solana/index.ts create mode 100644 packages/chain-adapters/src/solana/types.ts create mode 100644 packages/chain-adapters/src/solana/utils.ts diff --git a/package.json b/package.json index 3fdef3c3966..87ca5998080 100644 --- a/package.json +++ b/package.json @@ -93,20 +93,20 @@ "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/errors": "workspace:^", - "@shapeshiftoss/hdwallet-coinbase": "1.55.9", - "@shapeshiftoss/hdwallet-core": "1.55.9", - "@shapeshiftoss/hdwallet-keepkey": "1.55.9", - "@shapeshiftoss/hdwallet-keepkey-webusb": "1.55.9", - "@shapeshiftoss/hdwallet-keplr": "1.55.9", - "@shapeshiftoss/hdwallet-ledger": "1.55.9", - "@shapeshiftoss/hdwallet-ledger-webusb": "1.55.9", - "@shapeshiftoss/hdwallet-metamask": "1.55.9", - "@shapeshiftoss/hdwallet-native": "1.55.9", - "@shapeshiftoss/hdwallet-native-vault": "1.55.9", - "@shapeshiftoss/hdwallet-phantom": "1.55.9", - "@shapeshiftoss/hdwallet-shapeshift-multichain": "1.55.9", - "@shapeshiftoss/hdwallet-walletconnectv2": "1.55.9", - "@shapeshiftoss/hdwallet-xdefi": "1.55.9", + "@shapeshiftoss/hdwallet-coinbase": "1.55.10", + "@shapeshiftoss/hdwallet-core": "1.55.10", + "@shapeshiftoss/hdwallet-keepkey": "1.55.10", + "@shapeshiftoss/hdwallet-keepkey-webusb": "1.55.10", + "@shapeshiftoss/hdwallet-keplr": "1.55.10", + "@shapeshiftoss/hdwallet-ledger": "1.55.10", + "@shapeshiftoss/hdwallet-ledger-webusb": "1.55.10", + "@shapeshiftoss/hdwallet-metamask": "1.55.10", + "@shapeshiftoss/hdwallet-native": "1.55.10", + "@shapeshiftoss/hdwallet-native-vault": "1.55.10", + "@shapeshiftoss/hdwallet-phantom": "1.55.10", + "@shapeshiftoss/hdwallet-shapeshift-multichain": "1.55.10", + "@shapeshiftoss/hdwallet-walletconnectv2": "1.55.10", + "@shapeshiftoss/hdwallet-xdefi": "1.55.10", "@shapeshiftoss/swapper": "workspace:^", "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", diff --git a/packages/chain-adapters/package.json b/packages/chain-adapters/package.json index 226578dcef8..65e5a6d6e43 100644 --- a/packages/chain-adapters/package.json +++ b/packages/chain-adapters/package.json @@ -21,6 +21,7 @@ "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", "@shapeshiftoss/utils": "workspace:^", + "@solana/web3.js": "^1.95.3", "bech32": "^2.0.0", "coinselect": "^3.1.13", "multicoin-address-validator": "^0.5.12", diff --git a/packages/chain-adapters/src/index.ts b/packages/chain-adapters/src/index.ts index 58f466a97bd..f6399218305 100644 --- a/packages/chain-adapters/src/index.ts +++ b/packages/chain-adapters/src/index.ts @@ -5,3 +5,4 @@ export * from './types' export * from './evm' export * from './utxo' export * from './cosmossdk' +export * as solana from './solana' diff --git a/packages/chain-adapters/src/solana/SolanaChainAdapter.ts b/packages/chain-adapters/src/solana/SolanaChainAdapter.ts new file mode 100644 index 00000000000..e4ee69389de --- /dev/null +++ b/packages/chain-adapters/src/solana/SolanaChainAdapter.ts @@ -0,0 +1,360 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { + ASSET_NAMESPACE, + ASSET_REFERENCE, + solanaChainId, + solAssetId, + toAssetId, +} from '@shapeshiftoss/caip' +import type { SolanaSignTx } from '@shapeshiftoss/hdwallet-core' +import { supportsSolana } from '@shapeshiftoss/hdwallet-core' +import type { BIP44Params } from '@shapeshiftoss/types' +import { KnownChainIds } from '@shapeshiftoss/types' +import * as unchained from '@shapeshiftoss/unchained-client' +import { bn } from '@shapeshiftoss/utils' +import { Connection, PublicKey } from '@solana/web3.js' +import PQueue from 'p-queue' + +import type { ChainAdapter as IChainAdapter } from '../api' +import { ErrorHandler } from '../error/ErrorHandler' +import type { + Account, + BroadcastTransactionInput, + BuildSendTxInput, + FeeDataEstimate, + GetAddressInput, + GetBIP44ParamsInput, + GetFeeDataInput, + SignAndBroadcastTransactionInput, + SignTx, + SignTxInput, + SubscribeError, + SubscribeTxsInput, + Transaction, + TxHistoryInput, + TxHistoryResponse, + ValidAddressResult, +} from '../types' +import { ChainAdapterDisplayName, CONTRACT_INTERACTION, ValidAddressResultType } from '../types' +import { toAddressNList, toRootDerivationPath } from '../utils' +import { assertAddressNotSanctioned } from '../utils/validateAddress' +import { microLamportsToLamports } from './utils' + +export interface ChainAdapterArgs { + providers: { + http: unchained.solana.Api + ws: unchained.ws.Client + } + rpcUrl: string +} + +export class ChainAdapter implements IChainAdapter { + static readonly defaultBIP44Params: BIP44Params = { + purpose: 44, + coinType: Number(ASSET_REFERENCE.Solana), + accountNumber: 0, + } + + protected readonly chainId = solanaChainId + protected readonly assetId = solAssetId + + protected readonly providers: { + http: unchained.solana.Api + ws: unchained.ws.Client + } + + protected connection: Connection + protected parser: unchained.solana.TransactionParser + + constructor(args: ChainAdapterArgs) { + this.providers = args.providers + + this.connection = new Connection(args.rpcUrl) + + this.parser = new unchained.solana.TransactionParser({ + assetId: this.assetId, + chainId: this.chainId, + }) + } + + getName() { + const enumIndex = Object.values(ChainAdapterDisplayName).indexOf(ChainAdapterDisplayName.Solana) + return Object.keys(ChainAdapterDisplayName)[enumIndex] + } + + getDisplayName() { + return ChainAdapterDisplayName.Solana + } + + getType(): KnownChainIds.SolanaMainnet { + return KnownChainIds.SolanaMainnet + } + + getFeeAssetId(): AssetId { + return this.assetId + } + + getChainId(): ChainId { + return this.chainId + } + + getBIP44Params({ accountNumber }: GetBIP44ParamsInput): BIP44Params { + if (accountNumber < 0) { + throw new Error('accountNumber must be >= 0') + } + return { ...ChainAdapter.defaultBIP44Params, accountNumber } + } + + async getAddress(input: GetAddressInput): Promise { + try { + const { accountNumber, pubKey, wallet, showOnDevice = false } = input + + if (pubKey) return pubKey + + if (!supportsSolana(wallet)) throw new Error('Wallet does not support Solana.') + + const address = await wallet.solanaGetAddress({ + addressNList: toAddressNList(this.getBIP44Params({ accountNumber })), + showDisplay: showOnDevice, + }) + + if (!address) throw new Error('Unable to generate Solana address.') + + return address + } catch (err) { + return ErrorHandler(err) + } + } + + async getAccount(pubkey: string): Promise> { + try { + const data = await this.providers.http.getAccount({ pubkey }) + + const balance = BigInt(data.balance) + BigInt(data.unconfirmedBalance) + + return { + balance: balance.toString(), + chainId: this.chainId, + assetId: this.assetId, + chain: this.getType(), + chainSpecific: { + tokens: data.tokens.map(token => ({ + assetId: toAssetId({ + chainId: this.chainId, + assetNamespace: ASSET_NAMESPACE.splToken, + assetReference: token.id, + }), + balance: token.balance, + name: token.name, + precision: token.decimals, + symbol: token.symbol, + })), + }, + pubkey, + } + } catch (err) { + return ErrorHandler(err) + } + } + + async getTxHistory(input: TxHistoryInput): Promise { + const requestQueue = input.requestQueue ?? new PQueue() + + try { + const data = await requestQueue.add(() => + this.providers.http.getTxHistory({ + pubkey: input.pubkey, + pageSize: input.pageSize, + cursor: input.cursor, + }), + ) + + const txs = await Promise.all( + data.txs.map(tx => requestQueue.add(() => this.parseTx(tx, input.pubkey))), + ) + + return { + cursor: data.cursor ?? '', + pubkey: input.pubkey, + transactions: txs, + } + } catch (err) { + return ErrorHandler(err) + } + } + + async buildSendTransaction(input: BuildSendTxInput): Promise<{ + txToSign: SignTx + }> { + try { + const { accountNumber, to, value, chainSpecific } = input + + const { blockhash } = await this.connection.getLatestBlockhash() + + const computeUnitLimit = chainSpecific.computeUnitLimit + ? Number(chainSpecific.computeUnitLimit) + : undefined + + const computeUnitPrice = chainSpecific.computeUnitPrice + ? Number(chainSpecific.computeUnitPrice) + : undefined + + const txToSign: SignTx = { + addressNList: toAddressNList(this.getBIP44Params({ accountNumber })), + blockHash: blockhash, + computeUnitLimit, + computeUnitPrice, + // TODO: handle extra instructions + instructions: undefined, + to, + value, + } + + return { txToSign } + } catch (err) { + return ErrorHandler(err) + } + } + + async signTransaction(signTxInput: SignTxInput): Promise { + try { + const { txToSign, wallet } = signTxInput + + if (!supportsSolana(wallet)) throw new Error('Wallet does not support Solana.') + + const signedTx = await wallet.solanaSignTx(txToSign) + + if (!signedTx?.serialized) throw new Error('Error signing tx') + + return signedTx.serialized + } catch (err) { + return ErrorHandler(err) + } + } + + async signAndBroadcastTransaction({ + senderAddress, + receiverAddress, + signTxInput, + }: SignAndBroadcastTransactionInput): Promise { + try { + const { txToSign, wallet } = signTxInput + + await Promise.all([ + assertAddressNotSanctioned(senderAddress), + receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), + ]) + + if (!supportsSolana(wallet)) throw new Error('Wallet does not support Solana.') + + const tx = await wallet.solanaSendTx?.(txToSign) + + if (!tx) throw new Error('Error signing & broadcasting tx') + + return tx.signature + } catch (err) { + return ErrorHandler(err) + } + } + + async broadcastTransaction({ + senderAddress, + receiverAddress, + hex, + }: BroadcastTransactionInput): Promise { + try { + await Promise.all([ + assertAddressNotSanctioned(senderAddress), + receiverAddress !== CONTRACT_INTERACTION && assertAddressNotSanctioned(receiverAddress), + ]) + + return this.providers.http.sendTx({ sendTxBody: { hex } }) + } catch (err) { + return ErrorHandler(err) + } + } + + async getFeeData( + input: GetFeeDataInput, + ): Promise> { + const { baseFee, fast, average, slow } = await this.providers.http.getPriorityFees() + + const computeUnits = await this.providers.http.estimateFees({ + estimateFeesBody: { message: input.chainSpecific?.message }, + }) + + return { + fast: { + txFee: bn(microLamportsToLamports(fast)).times(computeUnits).plus(baseFee).toFixed(), + chainSpecific: { computeUnits }, + }, + average: { + txFee: bn(microLamportsToLamports(average)).times(computeUnits).plus(baseFee).toFixed(), + chainSpecific: { computeUnits }, + }, + slow: { + txFee: bn(microLamportsToLamports(slow)).times(computeUnits).plus(baseFee).toFixed(), + chainSpecific: { computeUnits }, + }, + } + } + + // eslint-disable-next-line require-await + async validateAddress(address: string): Promise { + try { + new PublicKey(address) + return { valid: true, result: ValidAddressResultType.Valid } + } catch (err) { + return { valid: false, result: ValidAddressResultType.Invalid } + } + } + + async subscribeTxs( + input: SubscribeTxsInput, + onMessage: (msg: Transaction) => void, + onError: (err: SubscribeError) => void, + ): Promise { + const { pubKey, accountNumber, wallet } = input + + const bip44Params = this.getBIP44Params({ accountNumber }) + const address = await this.getAddress({ accountNumber, wallet, pubKey }) + const subscriptionId = toRootDerivationPath(bip44Params) + + await this.providers.ws.subscribeTxs( + subscriptionId, + { topic: 'txs', addresses: [address] }, + async msg => onMessage(await this.parseTx(msg.data, msg.address)), + err => onError({ message: err.message }), + ) + } + + unsubscribeTxs(input?: SubscribeTxsInput): void { + if (!input) return this.providers.ws.unsubscribeTxs() + + const { accountNumber } = input + const bip44Params = this.getBIP44Params({ accountNumber }) + const subscriptionId = toRootDerivationPath(bip44Params) + + this.providers.ws.unsubscribeTxs(subscriptionId, { topic: 'txs', addresses: [] }) + } + + closeTxs(): void { + this.providers.ws.close('txs') + } + + protected async parseTx(tx: unchained.solana.Tx, pubkey: string): Promise { + const { address: _, ...parsedTx } = await this.parser.parse(tx, pubkey) + + return { + ...parsedTx, + pubkey, + transfers: parsedTx.transfers.map(transfer => ({ + assetId: transfer.assetId, + from: [transfer.from], + to: [transfer.to], + type: transfer.type, + value: transfer.totalValue, + })), + } + } +} diff --git a/packages/chain-adapters/src/solana/index.ts b/packages/chain-adapters/src/solana/index.ts new file mode 100644 index 00000000000..79fd113c81a --- /dev/null +++ b/packages/chain-adapters/src/solana/index.ts @@ -0,0 +1,3 @@ +export { ChainAdapter } from './SolanaChainAdapter' + +export * from './types' diff --git a/packages/chain-adapters/src/solana/types.ts b/packages/chain-adapters/src/solana/types.ts new file mode 100644 index 00000000000..cc60abbd761 --- /dev/null +++ b/packages/chain-adapters/src/solana/types.ts @@ -0,0 +1,41 @@ +import type { SolanaTxInstruction } from '@shapeshiftoss/hdwallet-core' +import type { CosmosSdkChainId } from '@shapeshiftoss/types' + +import type * as types from '../types' + +export type Account = { + tokens?: Token[] +} + +export type Token = types.AssetBalance & { + symbol: string + name: string + precision: number +} + +export type BuildTransactionInput = { + account: types.Account + accountNumber: number + memo?: string +} & types.ChainSpecificBuildTxData + +export type BuildTxInput = { + computeUnitLimit?: string + computeUnitPrice?: string + instructions?: SolanaTxInstruction[] +} + +export type GetFeeDataInput = { + message: string +} + +export type FeeData = { + computeUnits: string +} + +export type PriorityFeeData = { + baseFee: string + [types.FeeDataKey.Fast]: string + [types.FeeDataKey.Average]: string + [types.FeeDataKey.Slow]: string +} diff --git a/packages/chain-adapters/src/solana/utils.ts b/packages/chain-adapters/src/solana/utils.ts new file mode 100644 index 00000000000..f8e6e1ed087 --- /dev/null +++ b/packages/chain-adapters/src/solana/utils.ts @@ -0,0 +1,5 @@ +import { bn, bnOrZero } from '@shapeshiftoss/utils' + +export const microLamportsToLamports = (microLamports: string): string => { + return bnOrZero(microLamports).div(bn(10).pow(6)).toFixed() +} diff --git a/packages/chain-adapters/src/types.ts b/packages/chain-adapters/src/types.ts index 29cc7a7e4fe..15158d40771 100644 --- a/packages/chain-adapters/src/types.ts +++ b/packages/chain-adapters/src/types.ts @@ -4,6 +4,7 @@ import type { CosmosSignTx, ETHSignTx, HDWallet, + SolanaSignTx, ThorchainSignTx, } from '@shapeshiftoss/hdwallet-core' import type { @@ -17,6 +18,7 @@ import type PQueue from 'p-queue' import type * as cosmossdk from './cosmossdk/types' import type * as evm from './evm/types' +import type * as solana from './solana/types' import type * as utxo from './utxo/types' // this placeholder forces us to be explicit about transactions not transferring funds to humans @@ -41,6 +43,7 @@ type ChainSpecificAccount = ChainSpecific< [KnownChainIds.LitecoinMainnet]: utxo.Account [KnownChainIds.CosmosMainnet]: cosmossdk.Account [KnownChainIds.ThorchainMainnet]: cosmossdk.Account + [KnownChainIds.SolanaMainnet]: solana.Account } > @@ -81,6 +84,7 @@ type ChainSpecificFeeData = ChainSpecific< [KnownChainIds.LitecoinMainnet]: utxo.FeeData [KnownChainIds.CosmosMainnet]: cosmossdk.FeeData [KnownChainIds.ThorchainMainnet]: cosmossdk.FeeData + [KnownChainIds.SolanaMainnet]: solana.FeeData } > @@ -154,6 +158,7 @@ export type ChainSignTx = { [KnownChainIds.LitecoinMainnet]: BTCSignTx [KnownChainIds.CosmosMainnet]: CosmosSignTx [KnownChainIds.ThorchainMainnet]: ThorchainSignTx + [KnownChainIds.SolanaMainnet]: SolanaSignTx } export type SignTx = T extends keyof ChainSignTx ? ChainSignTx[T] : never @@ -196,6 +201,7 @@ export type ChainSpecificBuildTxData = ChainSpecific< [KnownChainIds.LitecoinMainnet]: utxo.BuildTxInput [KnownChainIds.CosmosMainnet]: cosmossdk.BuildTxInput [KnownChainIds.ThorchainMainnet]: cosmossdk.BuildTxInput + [KnownChainIds.SolanaMainnet]: solana.BuildTxInput } > @@ -288,6 +294,7 @@ type ChainSpecificGetFeeDataInput = ChainSpecific< [KnownChainIds.BitcoinCashMainnet]: utxo.GetFeeDataInput [KnownChainIds.DogecoinMainnet]: utxo.GetFeeDataInput [KnownChainIds.LitecoinMainnet]: utxo.GetFeeDataInput + [KnownChainIds.SolanaMainnet]: solana.GetFeeDataInput } > export type GetFeeDataInput = { @@ -349,6 +356,7 @@ export enum ChainAdapterDisplayName { BitcoinCash = 'Bitcoin Cash', Dogecoin = 'Dogecoin', Litecoin = 'Litecoin', + Solana = 'Solana', } export type BroadcastTransactionInput = { diff --git a/yarn.lock b/yarn.lock index ec157436d3f..c7e8105599c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11233,6 +11233,7 @@ __metadata: "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" "@shapeshiftoss/utils": "workspace:^" + "@solana/web3.js": ^1.95.3 "@types/multicoin-address-validator": ^0.5.0 bech32: ^2.0.0 coinselect: ^3.1.13 @@ -11308,15 +11309,15 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-coinbase@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.55.9" +"@shapeshiftoss/hdwallet-coinbase@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.55.10" dependencies: "@coinbase/wallet-sdk": ^3.6.6 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: 0d551f0e3a1b0787933aae756ecaca05e61b6a084fb000808ee2950ce44eeeebc45b95490ca370f06004b09784a1e14d4cf1ad574ad8502c7b4f0cba82ac9717 + checksum: ab6a11a68b3365f7197bcb1913b70430b4a5cf3104bf3667fd6c6682e7af442f88e27eb0c698981f2278dfcfad796c7925a5335cf5b68fbc608460a143a53249 languageName: node linkType: hard @@ -11346,9 +11347,9 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-core@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-core@npm:1.55.9" +"@shapeshiftoss/hdwallet-core@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-core@npm:1.55.10" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 "@shapeshiftoss/proto-tx-builder": ^0.8.0 @@ -11358,30 +11359,30 @@ __metadata: lodash: ^4.17.21 rxjs: ^6.4.0 type-assertions: ^1.1.0 - checksum: e441830860c07518a871513b98d45ac881e2d1a80384d5fbc780c61c181c0f9a0131811633a93096fd4c48581ff2e097351be9bb6078d0821f270d7af24b0849 + checksum: cd85d7890ff6ffdcafd27a57169fa9cf589403362baf77804f2c9010e8a115c439910b6bf997ec718623b955dfc43e88ff44dde1d66bedfc98b1c26db0882ff2 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.9" +"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.10" dependencies: - "@shapeshiftoss/hdwallet-core": 1.55.9 - "@shapeshiftoss/hdwallet-keepkey": 1.55.9 - checksum: fcb35e56f78390f7f654328943b59dee0cbbdcb0e3a037e8f0359c787c1f4e2dcabc576da3a8e6a9aab199c9108b0d3f72a3bf1b18c53d86ed08f8ba97e7e06b + "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-keepkey": 1.55.10 + checksum: 5f109d8fa878a338ad67ca2468d172090e0c6e7e1db90672a0b11f7edbc3f681a4e439e373c9729369dc9c4f30fda00c3262071d5dec6506da65c61156b19b82 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.55.9" +"@shapeshiftoss/hdwallet-keepkey@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.55.10" dependencies: "@ethereumjs/common": ^2.4.0 "@ethereumjs/tx": ^3.3.0 "@keepkey/device-protocol": ^7.12.2 "@metamask/eth-sig-util": ^7.0.0 "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 "@shapeshiftoss/proto-tx-builder": ^0.8.0 bignumber.js: ^9.0.1 bnb-javascript-sdk-nobroadcast: ^2.16.14 @@ -11393,27 +11394,27 @@ __metadata: p-lazy: ^3.1.0 semver: ^7.3.8 tiny-secp256k1: ^1.1.6 - checksum: 7ea0164111c2bf4eb52067645ff3ee8ff0bcf05b95583b708dfb2f1aa4a07bee5bda5c1b0da7d4cc0abb20e4901998ba5ef78a9d1d99f687420378815a7f54c1 + checksum: 298f9c3589769cbc6d1e9d4fbb0176b4559ec5f0352f15762e2c3acea2ca8fd085ba472e3c34b58cf6a01df33e408f8fb9dd25e7c7a520adf84380dcd8d3b787 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keplr@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.55.9" +"@shapeshiftoss/hdwallet-keplr@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.55.10" dependencies: "@shapeshiftoss/caip": 8.15.0 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 "@shapeshiftoss/proto-tx-builder": ^0.8.0 "@shapeshiftoss/types": 3.1.3 base64-js: ^1.5.1 lodash: ^4.17.21 - checksum: 6fc0e10a19d520da7935fae0d3d1b21177a7ede3eb5305143b5a5208e6c86ca4b50f19d9a21afed756e451b244c9cb5cfe44e0fb79ff9ab5ffbba43a0f6b7e91 + checksum: a04c4a070e4db365ffb4a86c596ac37c17e3517a0e69c1bffcb3d90d3b9660393d3db6f7fad5e9c31478eea007938409ebf6a6559e1dfe2d5b475a273ffc379f languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.9" +"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.10" dependencies: "@ledgerhq/hw-app-btc": ^10.4.1 "@ledgerhq/hw-app-eth": ^6.38.0 @@ -11421,23 +11422,23 @@ __metadata: "@ledgerhq/hw-transport-webusb": ^6.29.2 "@ledgerhq/live-common": ^21.8.2 "@ledgerhq/logs": ^6.10.1 - "@shapeshiftoss/hdwallet-core": 1.55.9 - "@shapeshiftoss/hdwallet-ledger": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-ledger": 1.55.10 "@types/w3c-web-usb": ^1.0.4 p-queue: ^7.4.1 - checksum: aac21bbf07075063da25675f942421093850d1db702323e8e431d514d9c3c243f9d358be552e356e6df82d42ee064a817b0b77fac9612c17abeef24e70e7ff01 + checksum: 8c645914a42cd76e1b9c21b85aa6085e83c42aea32deff2a1bb06ca92613677f6d93b1a8f3e2dd7bcee1eddce08038612f06bd2b4088735c1aaa635584a4e4c3 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.55.9" +"@shapeshiftoss/hdwallet-ledger@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.55.10" dependencies: "@ethereumjs/common": ^2.4.0 "@ethereumjs/tx": ^3.3.0 "@ledgerhq/hw-app-cosmos": ^6.29.1 "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 base64-js: ^1.5.1 bchaddrjs: ^0.4.4 bitcoinjs-message: ^2.0.0 @@ -11445,28 +11446,28 @@ __metadata: ethereumjs-tx: 1.3.7 ethereumjs-util: ^6.1.0 lodash: ^4.17.21 - checksum: 944b0277bb41aae5a8318b56f27285e36380cb6a536e4f23d0192a75930ee8c30b3bebbd01719bdf97a5c10fb603f22e1613df375680b4720275bc0ee1b9c41b + checksum: df341fe41c9cf75cc7c698cc7b36547c95c97c56d663ac939f516ebb7e9dbe74f682ac68fa841ae1d6fb1f5080fb9550eca932df59c721bea6fe15ceebd9d830 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-metamask@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-metamask@npm:1.55.9" +"@shapeshiftoss/hdwallet-metamask@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-metamask@npm:1.55.10" dependencies: "@metamask/detect-provider": ^1.2.0 "@metamask/onboarding": ^1.0.1 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: 2a80175faddf0a4f07ffe85a900d5a2763dfc5f15f2012fd8f168d8d4d02b78f02418a272e4f0cf4e5220f3d8e65f289c980b65bfc7d1f7890428dfb61e0b4f2 + checksum: db3803db2c49694052539aba19dd23df76fed55e5889708de21b6b912f53ecf8288e137d1a4ae34883897b2d02d7f6254ccb8c3cb641c3746bfd9ca76a10c137 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native-vault@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.55.9" +"@shapeshiftoss/hdwallet-native-vault@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.55.10" dependencies: - "@shapeshiftoss/hdwallet-native": 1.55.9 + "@shapeshiftoss/hdwallet-native": 1.55.10 bip39: ^3.0.4 hash-wasm: ^4.11.0 idb-keyval: ^6.0.3 @@ -11475,17 +11476,17 @@ __metadata: type-assertions: ^1.1.0 uuid: ^8.3.2 web-encoding: ^1.1.0 - checksum: 1f378982fb3a5368093eb7e6f1221e1e628d7fc3055667d33546b6e31f36d4a9e1255f3d5c9bbf15e02c46b856d07984de408abe4f61ba95b3765c60bb5a0f97 + checksum: 7713ed5ceeaea737d2d58de99e83f8c33b8bdad5450f420a95b82503cc1058ff5caf9e34de20e9c160182ca856ba9d042ddbf3afe0df79329af983d82aef959a languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-native@npm:1.55.9" +"@shapeshiftoss/hdwallet-native@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-native@npm:1.55.10" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 "@shapeshiftoss/fiosdk": 1.2.1-shapeshift.6 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 "@shapeshiftoss/proto-tx-builder": ^0.8.0 "@zxing/text-encoding": ^0.9.0 bchaddrjs: ^0.4.9 @@ -11507,7 +11508,7 @@ __metadata: tendermint-tx-builder: ^1.0.9 tiny-secp256k1: ^1.1.6 web-encoding: ^1.1.0 - checksum: a1e54cabc84fa28db420b20cde735a41720c43b14ccbf14108a519eaa637fee97caf48694ea95088eb801a05e1fc75b3d9ec8ccb56924426d822bcef8f810bd9 + checksum: 1e78caad03836d37233fb6edb3435286bc993573f068a2d96c980dffb1e3066e98834241599471b678b3f7fc7e1ca55fb16b0268e18398bc41cbae797d0cadc0 languageName: node linkType: hard @@ -11543,55 +11544,56 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-phantom@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-phantom@npm:1.55.9" +"@shapeshiftoss/hdwallet-phantom@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-phantom@npm:1.55.10" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 + "@solana/web3.js": ^1.95.3 base64-js: ^1.5.1 bitcoinjs-message: ^2.0.0 ethers: 5.7.2 lodash: ^4.17.21 - checksum: db324c2ca0a79a6f499d7e7cbe4d8765849f75f9f5e4abcabd70752415036e4fdaaa75ee9b8d0c992df6cd612469357099f3f05ffb6e8d6df34d4fa5e79c32cf + checksum: 2a187ed9c2c48e3106068d1f3993631c980e21c2aabaf9361ca70b21c165b6ac384a6eed67b934f3fe53db3b27a4197cee09e51a6402739818076b21e5c7840f languageName: node linkType: hard -"@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.55.9" +"@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.55.10" dependencies: "@metamask/detect-provider": ^1.2.0 "@metamask/onboarding": ^1.0.1 "@shapeshiftoss/common-api": ^9.3.0 - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 "@shapeshiftoss/metamask-snaps-adapter": ^1.0.10 "@shapeshiftoss/metamask-snaps-types": ^1.0.10 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: eb1b1cd8d0a6724a3218aa0bd009524d828c7ba214b1b52d610fee42685b9af6845bf13db19358b6b52ebe3e8aee13c3d06430c429e33f91a4b03e6b19d99d44 + checksum: b0bf636b247dbf649576294dc066852a6049213b9b2e98bc33323c83534317837220020fd2333289859d84f86d39c5a53764291b9d4417f8cdc8e4f6f4c1aba3 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.9" +"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.10" dependencies: - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 "@walletconnect/ethereum-provider": ^2.10.1 "@walletconnect/modal": ^2.6.2 ethers: ^5.6.5 - checksum: bcc771b5e9215f0ac0a2b234ff0400ad927aefc548def9156e18c400b384ec882a464af11d6a52ba0e84fff630b04541937570c98baf5c92b246f9184bc27b3a + checksum: 43bffab85fdaaaad13987f8d0abfd1141fa1b4c8f86455f265e3b604ecf3be219b063f15041aa630292b1279eac53cae7188bc566b655914536092671bba47c2 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-xdefi@npm:1.55.9": - version: 1.55.9 - resolution: "@shapeshiftoss/hdwallet-xdefi@npm:1.55.9" +"@shapeshiftoss/hdwallet-xdefi@npm:1.55.10": + version: 1.55.10 + resolution: "@shapeshiftoss/hdwallet-xdefi@npm:1.55.10" dependencies: - "@shapeshiftoss/hdwallet-core": 1.55.9 + "@shapeshiftoss/hdwallet-core": 1.55.10 lodash: ^4.17.21 - checksum: a1c22e0ce75484aef8a20311c1289c04ef18470b45c73da47f29ea019c017d645af0b3f7f76bf79b77c14f1f6b69fffd9373117e98c554d4215701c1349485c5 + checksum: 5058663f3f4f78a76a17322539c3a83fd708eb7606133d2f10b46d78fcee6ff00ba6cefcc3093c5c6d456bd00f1eb3247dd9edf33d665c7a3750d84e4a3756e0 languageName: node linkType: hard @@ -11786,20 +11788,20 @@ __metadata: "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/errors": "workspace:^" - "@shapeshiftoss/hdwallet-coinbase": 1.55.9 - "@shapeshiftoss/hdwallet-core": 1.55.9 - "@shapeshiftoss/hdwallet-keepkey": 1.55.9 - "@shapeshiftoss/hdwallet-keepkey-webusb": 1.55.9 - "@shapeshiftoss/hdwallet-keplr": 1.55.9 - "@shapeshiftoss/hdwallet-ledger": 1.55.9 - "@shapeshiftoss/hdwallet-ledger-webusb": 1.55.9 - "@shapeshiftoss/hdwallet-metamask": 1.55.9 - "@shapeshiftoss/hdwallet-native": 1.55.9 - "@shapeshiftoss/hdwallet-native-vault": 1.55.9 - "@shapeshiftoss/hdwallet-phantom": 1.55.9 - "@shapeshiftoss/hdwallet-shapeshift-multichain": 1.55.9 - "@shapeshiftoss/hdwallet-walletconnectv2": 1.55.9 - "@shapeshiftoss/hdwallet-xdefi": 1.55.9 + "@shapeshiftoss/hdwallet-coinbase": 1.55.10 + "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-keepkey": 1.55.10 + "@shapeshiftoss/hdwallet-keepkey-webusb": 1.55.10 + "@shapeshiftoss/hdwallet-keplr": 1.55.10 + "@shapeshiftoss/hdwallet-ledger": 1.55.10 + "@shapeshiftoss/hdwallet-ledger-webusb": 1.55.10 + "@shapeshiftoss/hdwallet-metamask": 1.55.10 + "@shapeshiftoss/hdwallet-native": 1.55.10 + "@shapeshiftoss/hdwallet-native-vault": 1.55.10 + "@shapeshiftoss/hdwallet-phantom": 1.55.10 + "@shapeshiftoss/hdwallet-shapeshift-multichain": 1.55.10 + "@shapeshiftoss/hdwallet-walletconnectv2": 1.55.10 + "@shapeshiftoss/hdwallet-xdefi": 1.55.10 "@shapeshiftoss/swapper": "workspace:^" "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" @@ -12133,7 +12135,7 @@ __metadata: languageName: node linkType: hard -"@solana/web3.js@npm:^1.95.2": +"@solana/web3.js@npm:^1.95.2, @solana/web3.js@npm:^1.95.3": version: 1.95.3 resolution: "@solana/web3.js@npm:1.95.3" dependencies: From ddd7c540789b519e0cfb1d14aa00b965167c81dc Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 15 Oct 2024 07:27:57 +0200 Subject: [PATCH 5/9] fix: reset tradeInput AccountIds on walletId change (#7916) * fix: reset tradeInput AccountIds on walletId change * fix: move reset to `` * feat: debugger-friendly * feat: temp remove effect * Revert "feat: temp remove effect" This reverts commit f13b90ae35b6d0ab5109b35117ea96f526a5bfc6. * feat: leverage prevWalletId * chore: trigger CI --- .../MultiHopTrade/hooks/useAccountIds.tsx | 10 +-- src/context/AppProvider/AppContext.tsx | 22 ++++-- .../slices/portfolioSlice/portfolioSlice.ts | 70 +++++++------------ .../slices/tradeInputSlice/tradeInputSlice.ts | 4 +- 4 files changed, 52 insertions(+), 54 deletions(-) diff --git a/src/components/MultiHopTrade/hooks/useAccountIds.tsx b/src/components/MultiHopTrade/hooks/useAccountIds.tsx index c3220e3dbf5..0cf6d4a9b30 100644 --- a/src/components/MultiHopTrade/hooks/useAccountIds.tsx +++ b/src/components/MultiHopTrade/hooks/useAccountIds.tsx @@ -19,14 +19,16 @@ export const useAccountIds = (): { // Setters - the selectors above initially select a *default* value, but eventually onAccountIdChange may fire if the user changes the account const setSellAssetAccountId = useCallback( - (accountId: AccountId | undefined) => - dispatch(tradeInput.actions.setSellAssetAccountNumber(accountId)), + (accountId: AccountId | undefined) => { + dispatch(tradeInput.actions.setSellAssetAccountId(accountId)) + }, [dispatch], ) const setBuyAssetAccountId = useCallback( - (accountId: AccountId | undefined) => - dispatch(tradeInput.actions.setBuyAssetAccountNumber(accountId)), + (accountId: AccountId | undefined) => { + dispatch(tradeInput.actions.setBuyAssetAccountId(accountId)) + }, [dispatch], ) diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index e0d7add9a74..13cf25e289b 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -11,7 +11,6 @@ import { DEFAULT_HISTORY_TIMEFRAME } from 'constants/Config' import { LanguageTypeEnum } from 'constants/LanguageTypeEnum' import React, { useEffect } from 'react' import { useTranslate } from 'react-polyglot' -import { useSelector } from 'react-redux' import { useNfts } from 'components/Nfts/hooks/useNfts' import { usePlugins } from 'context/PluginProvider/PluginProvider' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' @@ -39,7 +38,9 @@ import { selectPortfolioLoadingStatus, selectSelectedCurrency, selectSelectedLocale, + selectWalletId, } from 'state/slices/selectors' +import { tradeInput } from 'state/slices/tradeInputSlice/tradeInputSlice' import { txHistoryApi } from 'state/slices/txHistorySlice/txHistorySlice' import { useAppDispatch, useAppSelector } from 'state/store' @@ -59,10 +60,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const dispatch = useAppDispatch() const { supportedChains } = usePlugins() const { wallet, isConnected } = useWallet().state - const assetIds = useSelector(selectAssetIds) - const requestedAccountIds = useSelector(selectEnabledWalletAccountIds) - const portfolioLoadingStatus = useSelector(selectPortfolioLoadingStatus) - const portfolioAssetIds = useSelector(selectPortfolioAssetIds) + const assetIds = useAppSelector(selectAssetIds) + const requestedAccountIds = useAppSelector(selectEnabledWalletAccountIds) + const portfolioLoadingStatus = useAppSelector(selectPortfolioLoadingStatus) + const portfolioAssetIds = useAppSelector(selectPortfolioAssetIds) + const walletId = useAppSelector(selectWalletId) + const prevWalletId = usePrevious(walletId) const routeAssetId = useRouteAssetId() const { isSnapInstalled } = useIsSnapInstalled() const previousIsSnapInstalled = usePrevious(isSnapInstalled) @@ -317,6 +320,15 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { ) }, [dispatch, isConnected, portfolioLoadingStatus]) + // Resets the sell and buy asset AccountIDs on wallet change to that we don't get stale trade input account selections while we're loading the new wallet + useEffect(() => { + if (!prevWalletId) return + if (walletId === prevWalletId) return + + dispatch(tradeInput.actions.setSellAssetAccountId(undefined)) + dispatch(tradeInput.actions.setBuyAssetAccountId(undefined)) + }, [dispatch, prevWalletId, walletId]) + const marketDataPollingInterval = 60 * 15 * 1000 // refetch data every 15 minutes useQueries({ queries: portfolioAssetIds.map(assetId => ({ diff --git a/src/state/slices/portfolioSlice/portfolioSlice.ts b/src/state/slices/portfolioSlice/portfolioSlice.ts index 1a9a24ee69a..7ff1fa2a52b 100644 --- a/src/state/slices/portfolioSlice/portfolioSlice.ts +++ b/src/state/slices/portfolioSlice/portfolioSlice.ts @@ -1,4 +1,4 @@ -import { createSlice, prepareAutoBatched } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' import { createApi } from '@reduxjs/toolkit/query/react' import type { AccountId, ChainId } from '@shapeshiftoss/caip' import { ASSET_NAMESPACE, bscChainId, fromAccountId, isNft, toAssetId } from '@shapeshiftoss/caip' @@ -86,52 +86,36 @@ export const portfolio = createSlice({ Object.assign(state.connectedWallet, { supportedChainIds: payload }) }, - upsertAccountMetadata: { - reducer: ( - draftState, - { - payload, - }: { payload: { accountMetadataByAccountId: AccountMetadataById; walletId: string } }, - ) => { - // WARNING: don't use the current state.connectedWallet.id here because it's updated async - // to this and results in account data corruption - const { accountMetadataByAccountId, walletId } = payload - draftState.accountMetadata.byId = merge( - draftState.accountMetadata.byId, - accountMetadataByAccountId, - ) - draftState.accountMetadata.ids = Object.keys(draftState.accountMetadata.byId) - - if (!draftState.connectedWallet) return // realistically, at this point, we should have a wallet set - const existingWalletAccountIds = draftState.wallet.byId[walletId] ?? [] - const newWalletAccountIds = Object.keys(accountMetadataByAccountId) - // keep an index of what account ids belong to this wallet - draftState.wallet.byId[walletId] = uniq( - existingWalletAccountIds.concat(newWalletAccountIds), - ) - }, + upsertAccountMetadata: ( + draftState, + { + payload, + }: { payload: { accountMetadataByAccountId: AccountMetadataById; walletId: string } }, + ) => { + // WARNING: don't use the current state.connectedWallet.id here because it's updated async + // to this and results in account data corruption + const { accountMetadataByAccountId, walletId } = payload + draftState.accountMetadata.byId = merge( + draftState.accountMetadata.byId, + accountMetadataByAccountId, + ) + draftState.accountMetadata.ids = Object.keys(draftState.accountMetadata.byId) - // Use the `prepareAutoBatched` utility to automatically - // add the `action.meta[SHOULD_AUTOBATCH]` field the enhancer needs - prepare: prepareAutoBatched<{ - accountMetadataByAccountId: AccountMetadataById - walletId: string - }>(), + if (!draftState.connectedWallet) return // realistically, at this point, we should have a wallet set + const existingWalletAccountIds = draftState.wallet.byId[walletId] ?? [] + const newWalletAccountIds = Object.keys(accountMetadataByAccountId) + // keep an index of what account ids belong to this wallet + draftState.wallet.byId[walletId] = uniq(existingWalletAccountIds.concat(newWalletAccountIds)) }, - clearWalletMetadata: { - reducer: (draftState, { payload }: { payload: WalletId }) => { - const walletId = payload - // Clear AccountIds that were previously associated with that wallet - draftState.wallet.byId[walletId] = [] - draftState.wallet.ids = draftState.wallet.ids.filter(id => id !== walletId) - // TODO(gomes): do we also want to clear draftState.accountMetadata entries themselves? - // Theoretically, not doing so would make reloading these easier? - }, + clearWalletMetadata: (draftState, { payload }: { payload: WalletId }) => { + const walletId = payload + // Clear AccountIds that were previously associated with that wallet + draftState.wallet.byId[walletId] = [] + draftState.wallet.ids = draftState.wallet.ids.filter(id => id !== walletId) - // Use the `prepareAutoBatched` utility to automatically - // add the `action.meta[SHOULD_AUTOBATCH]` field the enhancer needs - prepare: prepareAutoBatched(), + // TODO(gomes): do we also want to clear draftState.accountMetadata entries themselves? + // Theoretically, not doing so would make reloading these easier? }, upsertPortfolio: (draftState, { payload }: { payload: Portfolio }) => { // upsert all diff --git a/src/state/slices/tradeInputSlice/tradeInputSlice.ts b/src/state/slices/tradeInputSlice/tradeInputSlice.ts index 1a316ce47e1..b2e75338605 100644 --- a/src/state/slices/tradeInputSlice/tradeInputSlice.ts +++ b/src/state/slices/tradeInputSlice/tradeInputSlice.ts @@ -78,10 +78,10 @@ export const tradeInput = createSlice({ state.sellAsset = action.payload }, - setSellAssetAccountNumber: (state, action: PayloadAction) => { + setSellAssetAccountId: (state, action: PayloadAction) => { state.sellAssetAccountId = action.payload }, - setBuyAssetAccountNumber: (state, action: PayloadAction) => { + setBuyAssetAccountId: (state, action: PayloadAction) => { state.buyAssetAccountId = action.payload }, setSellAmountCryptoPrecision: (state, action: PayloadAction) => { From 0d5c449e86cd542450d978e87c4a00bf33286af7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:11:14 +0200 Subject: [PATCH 6/9] feat: eip-6963 (#7869) --- .env.base | 3 - package.json | 28 +- public/manifest.json | 1 + react-app-rewired/headers/csps/defi/safe.ts | 1 + src/assets/translations/en/main.json | 41 +- src/components/Icons/XDEFIIcon.tsx | 32 -- src/components/Layout/Header/Header.tsx | 53 ++- .../NavBar/KeepKey/ChangePassphrase.tsx | 2 +- .../Layout/Header/NavBar/UserMenu.tsx | 29 +- .../Layout/Header/NavBar/WalletImage.tsx | 18 +- .../components/ImportAccounts.tsx | 6 +- src/components/Modals/Send/utils.ts | 10 +- src/components/Modals/Snaps/Snaps.tsx | 3 - .../components/ManualAddressEntry.tsx | 7 +- src/config.ts | 2 - src/context/AppProvider/AppContext.tsx | 4 +- .../Coinbase/components/Connect.tsx | 17 +- .../KeepKey/components/Connect.tsx | 17 +- .../KeepKey/hooks/useKeepKeyEventHandler.ts | 10 +- .../Keplr/components/Connect.tsx | 11 +- src/context/WalletProvider/KeyManager.ts | 9 +- .../Ledger/components/Connect.tsx | 11 +- .../MetaMask/components/Connect.tsx | 119 ++++-- .../MetaMask/components/Failure.tsx | 25 +- .../MetaMask/components/MetaMaskMenu.tsx | 8 +- src/context/WalletProvider/MetaMask/config.ts | 9 +- .../MobileWallet/components/MobileLoad.tsx | 7 +- .../MobileWallet/components/MobileSuccess.tsx | 22 +- .../NativeWallet/components/EnterPassword.tsx | 7 +- .../NativeWallet/components/NativeLoad.tsx | 11 +- .../NativeWallet/hooks/useNativeSuccess.ts | 11 +- .../Phantom/components/Connect.tsx | 17 +- src/context/WalletProvider/SelectModal.tsx | 180 ++++++-- .../WalletConnectV2/components/Connect.tsx | 19 +- .../useWalletConnectV2EventHandler.ts | 17 +- src/context/WalletProvider/WalletContext.tsx | 9 +- .../WalletProvider/WalletProvider.test.tsx | 25 +- src/context/WalletProvider/WalletProvider.tsx | 397 ++++++------------ .../WalletProvider/WalletViewsSwitch.tsx | 7 +- .../XDEFI/components/Connect.tsx | 103 ----- .../XDEFI/components/Failure.tsx | 10 - src/context/WalletProvider/XDEFI/config.ts | 15 - src/context/WalletProvider/actions.ts | 16 +- .../components/ConnectModal.tsx | 5 +- .../components/FailureModal.tsx | 3 +- src/context/WalletProvider/config.ts | 28 +- src/context/WalletProvider/local-wallet.ts | 17 +- src/context/WalletProvider/types.ts | 4 +- .../WalletProvider/useEip1993EventHandler.ts | 131 ++++++ .../useIsSnapInstalled/useIsSnapInstalled.tsx | 145 ++----- .../useWalletSupportsChain.ts | 20 +- src/lib/account/cosmosSdk.ts | 4 +- src/lib/account/evm.ts | 4 +- src/lib/account/utxo.ts | 4 +- src/lib/mipd.ts | 45 ++ src/pages/Accounts/Accounts.tsx | 4 +- src/pages/Accounts/AddAccountModal.tsx | 4 +- src/pages/ConnectWallet/ConnectWallet.tsx | 66 ++- src/pages/ConnectWallet/MobileConnect.tsx | 9 +- .../useStakingAction/useStakingAction.tsx | 8 +- src/state/migrations/clearLocalWallet.ts | 8 + src/state/migrations/index.ts | 6 + src/state/reducer.ts | 4 +- .../localWalletSlice/localWalletSlice.ts | 10 +- .../slices/localWalletSlice/selectors.ts | 4 + .../preferencesSlice/preferencesSlice.ts | 4 - src/test/mocks/store.ts | 3 +- src/utils/snaps.ts | 20 +- yarn.lock | 223 +++++----- 69 files changed, 1067 insertions(+), 1065 deletions(-) delete mode 100644 src/components/Icons/XDEFIIcon.tsx delete mode 100644 src/context/WalletProvider/XDEFI/components/Connect.tsx delete mode 100644 src/context/WalletProvider/XDEFI/components/Failure.tsx delete mode 100644 src/context/WalletProvider/XDEFI/config.ts create mode 100644 src/context/WalletProvider/useEip1993EventHandler.ts create mode 100644 src/lib/mipd.ts create mode 100644 src/state/migrations/clearLocalWallet.ts diff --git a/.env.base b/.env.base index 44d2a7252c3..aab9475c90f 100644 --- a/.env.base +++ b/.env.base @@ -20,7 +20,6 @@ REACT_APP_FEATURE_WHEREVER=true REACT_APP_FEATURE_YAT=true REACT_APP_FEATURE_NFT_METADATA=false REACT_APP_FEATURE_CHATWOOT=false -REACT_APP_FEATURE_COINBASE_WALLET=true REACT_APP_FEATURE_ADVANCED_SLIPPAGE=true REACT_APP_FEATURE_LEDGER_WALLET=true REACT_APP_FEATURE_WALLET_CONNECT_V2=true @@ -170,8 +169,6 @@ REACT_APP_SNAP_ID=npm:@shapeshiftoss/metamask-snaps REACT_APP_SNAP_VERSION=1.0.9 # REACT_APP_SNAP_ID=local:http://localhost:9000 -REACT_APP_EXPERIMENTAL_MM_SNAPPY_FINGERS=true - # Experemental features (not production ready) REACT_APP_EXPERIMENTAL_CUSTOM_SEND_NONCE=false diff --git a/package.json b/package.json index 87ca5998080..56286126628 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "@ledgerhq/hw-transport-webusb": "^6.29.2", "@lifi/sdk": "^3.1.5", "@lukemorales/query-key-factory": "^1.3.4", - "@metamask/detect-provider": "^2.0.0", "@react-spring/web": "^9.7.4", "@reduxjs/toolkit": "^1.9.7", "@sentry-internal/browser-utils": "^8.26.0", @@ -93,20 +92,18 @@ "@shapeshiftoss/caip": "workspace:^", "@shapeshiftoss/chain-adapters": "workspace:^", "@shapeshiftoss/errors": "workspace:^", - "@shapeshiftoss/hdwallet-coinbase": "1.55.10", - "@shapeshiftoss/hdwallet-core": "1.55.10", - "@shapeshiftoss/hdwallet-keepkey": "1.55.10", - "@shapeshiftoss/hdwallet-keepkey-webusb": "1.55.10", - "@shapeshiftoss/hdwallet-keplr": "1.55.10", - "@shapeshiftoss/hdwallet-ledger": "1.55.10", - "@shapeshiftoss/hdwallet-ledger-webusb": "1.55.10", - "@shapeshiftoss/hdwallet-metamask": "1.55.10", - "@shapeshiftoss/hdwallet-native": "1.55.10", - "@shapeshiftoss/hdwallet-native-vault": "1.55.10", - "@shapeshiftoss/hdwallet-phantom": "1.55.10", - "@shapeshiftoss/hdwallet-shapeshift-multichain": "1.55.10", - "@shapeshiftoss/hdwallet-walletconnectv2": "1.55.10", - "@shapeshiftoss/hdwallet-xdefi": "1.55.10", + "@shapeshiftoss/hdwallet-coinbase": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-core": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-keepkey": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-keepkey-webusb": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-keplr": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-ledger": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-ledger-webusb": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-metamask-multichain": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-native": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-native-vault": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-phantom": "1.55.11-mipd.5", + "@shapeshiftoss/hdwallet-walletconnectv2": "1.55.11-mipd.5", "@shapeshiftoss/swapper": "workspace:^", "@shapeshiftoss/types": "workspace:^", "@shapeshiftoss/unchained-client": "workspace:^", @@ -159,6 +156,7 @@ "localforage": "^1.10.0", "lodash": "^4.17.21", "match-sorter": "^6.3.0", + "mipd": "^0.0.7", "mixpanel-browser": "^2.45.0", "myzod": "^1.10.1", "node-polyglot": "^2.4.0", diff --git a/public/manifest.json b/public/manifest.json index 5c7fd77e01e..4012564f93f 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -7,6 +7,7 @@ "name": "ShapeShift", "short_name": "ShapeShift", "description": "Your Web3 & DeFi Portal", + "iconPath": "/icon-512x512.png", "icons": [ { "src": "/icon-192x192.png", diff --git a/react-app-rewired/headers/csps/defi/safe.ts b/react-app-rewired/headers/csps/defi/safe.ts index a06a0226d76..5659e31f236 100644 --- a/react-app-rewired/headers/csps/defi/safe.ts +++ b/react-app-rewired/headers/csps/defi/safe.ts @@ -2,6 +2,7 @@ import type { Csp } from '../../types' export const csp: Csp = { 'connect-src': [ + 'https://app.safe.global', 'https://safe-transaction-mainnet.safe.global', 'https://safe-transaction-avalanche.safe.global', 'https://safe-transaction-optimism.safe.global', diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 175c202f4dd..574f468e964 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -1518,20 +1518,21 @@ "connectWarning": "Before connecting a chain, make sure you have the app open on your device.", "signWarning": "Before signing, make sure you have the %{chain} app open on your device." }, - "metaMask": { - "errors": { - "unknown": "An unexpected error occurred communicating with MetaMask", - "connectFailure": "Unable to connect MetaMask wallet", - "multipleWallets": "Detected Ethereum provider is not MetaMask. Do you have multiple wallets installed?" - }, + "mipd": { "connect": { - "header": "Pair MetaMask", - "body": "Click Pair and login to MetaMask from the popup window", + "header": "Pair %{name}", + "body": "Click Pair and login to %{name} from the popup window", "button": "Pair" }, - "failure": { - "body": "Unable to connect MetaMask wallet" + "errors": { + "unknown": "An unexpected error occurred communicating with %{name}", + "connectFailure": "Unable to connect %{name}" }, + "failure": { + "body": "Unable to connect %{name}" + } + }, + "metaMask": { "redirect": { "header": "Open in MetaMask App", "body": "Click to open ShapeShift dashboard in MetaMask", @@ -1620,26 +1621,6 @@ "body": "Unable to connect WalletConnect wallet" } }, - "xdefi": { - "errors": { - "unknown": "An unexpected error occurred communicating with XDEFI", - "connectFailure": "Unable to connect XDEFI wallet", - "multipleWallets": "Detected Ethereum provider is not XDEFI. Do you have multiple wallets installed and switched on? Prioritize XDEFI in settings and press pair again." - }, - "connect": { - "header": "Pair XDEFI", - "body": "Click Pair and login to XDEFI from the popup window", - "button": "Pair" - }, - "failure": { - "body": "Unable to connect XDEFI wallet" - }, - "redirect": { - "header": "Open in XDEFI App", - "body": "Click to open ShapeShift dashboard in XDEFI", - "button": "Open" - } - }, "shapeShift": { "load": { "error": { diff --git a/src/components/Icons/XDEFIIcon.tsx b/src/components/Icons/XDEFIIcon.tsx deleted file mode 100644 index 526b433c08f..00000000000 --- a/src/components/Icons/XDEFIIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { createIcon } from '@chakra-ui/react' - -export const XDEFIIcon = createIcon({ - displayName: 'XDeFiIcon', - path: ( - - - - - - - ), - viewBox: '0 0 318.6 318.6', -}) diff --git a/src/components/Layout/Header/Header.tsx b/src/components/Layout/Header/Header.tsx index 13ce4da98d9..234c667cc67 100644 --- a/src/components/Layout/Header/Header.tsx +++ b/src/components/Layout/Header/Header.tsx @@ -1,7 +1,8 @@ import { InfoIcon } from '@chakra-ui/icons' import { Box, Flex, HStack, useMediaQuery, usePrevious, useToast } from '@chakra-ui/react' -import { btcAssetId } from '@shapeshiftoss/caip' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { btcAssetId, fromAccountId } from '@shapeshiftoss/caip' +import { isEvmChainId } from '@shapeshiftoss/chain-adapters' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import { useScroll } from 'framer-motion' import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslate } from 'react-polyglot' @@ -13,7 +14,8 @@ import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useModal } from 'hooks/useModal/useModal' import { useWallet } from 'hooks/useWallet/useWallet' -import { isUtxoAccountId } from 'lib/utils/utxo' +import { METAMASK_RDNS } from 'lib/mipd' +import { selectWalletRdns } from 'state/slices/localWalletSlice/selectors' import { portfolio } from 'state/slices/portfolioSlice/portfolioSlice' import { selectEnabledWalletAccountIds, @@ -21,7 +23,7 @@ import { selectShowSnapsModal, selectWalletId, } from 'state/slices/selectors' -import { useAppDispatch } from 'state/store' +import { useAppDispatch, useAppSelector } from 'state/store' import { breakpoints } from 'theme/theme' import { AppLoadingIcon } from './AppLoadingIcon' @@ -97,21 +99,23 @@ export const Header = memo(() => { [dispatch], ) - const currentWalletId = useSelector(selectWalletId) - const walletAccountIds = useSelector(selectEnabledWalletAccountIds) - const hasUtxoAccountIds = useMemo( - () => walletAccountIds.some(accountId => isUtxoAccountId(accountId)), + const connectedRdns = useAppSelector(selectWalletRdns) + const previousConnectedRdns = usePrevious(connectedRdns) + const currentWalletId = useAppSelector(selectWalletId) + const walletAccountIds = useAppSelector(selectEnabledWalletAccountIds) + const hasNonEvmAccountIds = useMemo( + () => walletAccountIds.some(accountId => !isEvmChainId(fromAccountId(accountId).chainId)), [walletAccountIds], ) useEffect(() => { - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet + const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet if (!(currentWalletId && isMetaMaskMultichainWallet && isSnapInstalled === false)) return // We have just detected that the user doesn't have the snap installed currently // We need to check whether or not the user had previous non-EVM AccountIds and clear those - if (hasUtxoAccountIds) appDispatch(portfolio.actions.clearWalletMetadata(currentWalletId)) - }, [appDispatch, currentWalletId, hasUtxoAccountIds, isSnapInstalled, wallet, walletAccountIds]) + if (hasNonEvmAccountIds) appDispatch(portfolio.actions.clearWalletMetadata(currentWalletId)) + }, [appDispatch, currentWalletId, hasNonEvmAccountIds, isSnapInstalled, wallet, walletAccountIds]) useEffect(() => { if (!isCorrectVersion && isSnapInstalled) return @@ -122,18 +126,27 @@ export const Header = memo(() => { isSnapInstalled === false && previousIsCorrectVersion === true ) { - // they uninstalled the snap - toast({ - status: 'success', - title: translate('walletProvider.metaMaskSnap.snapUninstalledToast'), - position: 'bottom', - }) + if (previousConnectedRdns === METAMASK_RDNS && connectedRdns === METAMASK_RDNS) { + // they uninstalled the snap + toast({ + status: 'success', + title: translate('walletProvider.metaMaskSnap.snapUninstalledToast'), + position: 'bottom', + }) + } const walletId = currentWalletId if (!walletId) return appDispatch(portfolio.actions.clearWalletMetadata(walletId)) - return snapModal.open({ isRemoved: true }) + if (previousConnectedRdns === METAMASK_RDNS && connectedRdns === METAMASK_RDNS) { + return snapModal.open({ isRemoved: true }) + } } - if (previousSnapInstall === false && isSnapInstalled === true) { + if ( + previousSnapInstall === false && + isSnapInstalled === true && + previousConnectedRdns === METAMASK_RDNS && + connectedRdns === METAMASK_RDNS + ) { history.push(`/assets/${btcAssetId}`) // they installed the snap @@ -146,11 +159,13 @@ export const Header = memo(() => { } }, [ appDispatch, + connectedRdns, currentWalletId, dispatch, history, isCorrectVersion, isSnapInstalled, + previousConnectedRdns, previousIsCorrectVersion, previousSnapInstall, showSnapModal, diff --git a/src/components/Layout/Header/NavBar/KeepKey/ChangePassphrase.tsx b/src/components/Layout/Header/NavBar/KeepKey/ChangePassphrase.tsx index f2d440a05a7..790b317382d 100644 --- a/src/components/Layout/Header/NavBar/KeepKey/ChangePassphrase.tsx +++ b/src/components/Layout/Header/NavBar/KeepKey/ChangePassphrase.tsx @@ -72,7 +72,7 @@ export const ChangePassphrase = () => { // Trigger a refresh of the wallet metadata only once the settings have been applied // and the previous wallet meta is gone from the store dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) - connect(KeyManager.KeepKey) + connect(KeyManager.KeepKey, false) }, [ appDispatch, connect, diff --git a/src/components/Layout/Header/NavBar/UserMenu.tsx b/src/components/Layout/Header/NavBar/UserMenu.tsx index 158a0f12055..89966538096 100644 --- a/src/components/Layout/Header/NavBar/UserMenu.tsx +++ b/src/components/Layout/Header/NavBar/UserMenu.tsx @@ -1,4 +1,5 @@ import { ChevronDownIcon, WarningTwoIcon } from '@chakra-ui/icons' +import type { ComponentWithAs, IconProps } from '@chakra-ui/react' import { Box, Button, @@ -27,6 +28,9 @@ import { RawText, Text } from 'components/Text' import { WalletActions } from 'context/WalletProvider/actions' import type { InitialState } from 'context/WalletProvider/WalletProvider' import { useWallet } from 'hooks/useWallet/useWallet' +import { useMipdProviders } from 'lib/mipd' +import { selectWalletRdns } from 'state/slices/localWalletSlice/selectors' +import { useAppSelector } from 'state/store' export const entries = [WalletConnectedRoutes.Connected] @@ -50,7 +54,11 @@ export type WalletConnectedProps = { onDisconnect: () => void onSwitchProvider: () => void onClose?: () => void -} & Pick + walletInfo: { + icon: ComponentWithAs<'svg', IconProps> | string + name: string + } | null +} & Pick export const WalletConnected = (props: WalletConnectedProps) => { return ( @@ -86,6 +94,14 @@ const WalletButton: FC = ({ const bgColor = useColorModeValue('gray.200', 'gray.800') const [ensName, setEnsName] = useState('') + const maybeRdns = useAppSelector(selectWalletRdns) + + const mipdProviders = useMipdProviders() + const maybeMipdProvider = useMemo( + () => mipdProviders.find(provider => provider.info.rdns === maybeRdns), + [mipdProviders, maybeRdns], + ) + useEffect(() => { if (!walletInfo?.meta?.address) return viemEthMainnetClient @@ -118,10 +134,10 @@ const WalletButton: FC = ({ () => ( {!(isConnected || isDemoWallet) && } - + ), - [isConnected, isDemoWallet, walletInfo], + [isConnected, isDemoWallet, maybeMipdProvider, walletInfo], ) const connectIcon = useMemo(() => , []) @@ -162,6 +178,11 @@ export const UserMenu: React.FC<{ onClick?: () => void }> = memo(({ onClick }) = const { isConnected, isDemoWallet, walletInfo, connectedType, isLocked, isLoadingLocalWallet } = state + const maybeRdns = useAppSelector(selectWalletRdns) + + const mipdProviders = useMipdProviders() + const maybeMipdProvider = mipdProviders.find(provider => provider.info.rdns === maybeRdns) + const hasWallet = Boolean(walletInfo?.deviceId) const handleConnect = useCallback(() => { onClick && onClick() @@ -189,7 +210,7 @@ export const UserMenu: React.FC<{ onClick?: () => void }> = memo(({ onClick }) = {hasWallet || isLoadingLocalWallet ? ( +type WalletImageProps = { + walletInfo: { + icon: ComponentWithAs<'svg', IconProps> | string + } | null +} export const WalletImage = ({ walletInfo }: WalletImageProps) => { - const Icon = walletInfo?.icon - if (Icon) { - return - } - return null + if (!walletInfo) return null + if (typeof walletInfo.icon === 'string') + return Wallet Icon + const Icon = walletInfo.icon + return } diff --git a/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx b/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx index 10e063e03d3..8e683a3961c 100644 --- a/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx +++ b/src/components/ManageAccountsDrawer/components/ImportAccounts.tsx @@ -13,7 +13,7 @@ import { import type { ChainId } from '@shapeshiftoss/caip' import { type AccountId, fromAccountId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { Asset } from '@shapeshiftoss/types' import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -172,10 +172,10 @@ export const ImportAccounts = ({ chainId, onClose }: ImportAccountsProps) => { dispatch: walletDispatch, } = useWallet() const asset = useAppSelector(state => selectFeeAssetByChainId(state, chainId)) - const isSnapInstalled = useIsSnapInstalled() + const { isSnapInstalled } = useIsSnapInstalled() const isLedgerWallet = useMemo(() => wallet && isLedger(wallet), [wallet]) const isMetaMaskMultichainWallet = useMemo( - () => wallet instanceof MetaMaskShapeShiftMultiChainHDWallet, + () => wallet instanceof MetaMaskMultiChainHDWallet, [wallet], ) const chainNamespaceDisplayName = asset?.networkName ?? '' diff --git a/src/components/Modals/Send/utils.ts b/src/components/Modals/Send/utils.ts index cd0c812d2f6..56c1a7e78f3 100644 --- a/src/components/Modals/Send/utils.ts +++ b/src/components/Modals/Send/utils.ts @@ -7,7 +7,6 @@ import { supportsETH } from '@shapeshiftoss/hdwallet-core' import type { CosmosSdkChainId, EvmChainId, KnownChainIds, UtxoChainId } from '@shapeshiftoss/types' import { checkIsMetaMaskDesktop, - checkIsMetaMaskImpersonator, checkIsSnapInstalled, } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { bn, bnOrZero } from 'lib/bignumber/bignumber' @@ -104,15 +103,12 @@ export const handleSend = async ({ const acccountMetadataFilter = { accountId: sendInput.accountId } const accountMetadata = selectPortfolioAccountMetadataByAccountId(state, acccountMetadataFilter) - const isMetaMaskDesktop = await checkIsMetaMaskDesktop(wallet) - const isMetaMaskImpersonator = await checkIsMetaMaskImpersonator(wallet) + const isMetaMaskDesktop = checkIsMetaMaskDesktop(wallet) if ( fromChainId(asset.chainId).chainNamespace === CHAIN_NAMESPACE.CosmosSdk && !wallet.supportsOfflineSigning() && - // MM impersonators don't support Cosmos SDK chains - (!isMetaMaskDesktop || - isMetaMaskImpersonator || - (isMetaMaskDesktop && !(await checkIsSnapInstalled()))) + // MM only supports snap things... if the snap is installed + (!isMetaMaskDesktop || (isMetaMaskDesktop && !(await checkIsSnapInstalled()))) ) { throw new Error(`unsupported wallet: ${await wallet.getModel()}`) } diff --git a/src/components/Modals/Snaps/Snaps.tsx b/src/components/Modals/Snaps/Snaps.tsx index 6b456db35b8..bcaef7d1b5f 100644 --- a/src/components/Modals/Snaps/Snaps.tsx +++ b/src/components/Modals/Snaps/Snaps.tsx @@ -1,6 +1,5 @@ import { Modal, ModalCloseButton, ModalContent, ModalOverlay } from '@chakra-ui/react' import { useCallback, useEffect } from 'react' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useModal } from 'hooks/useModal/useModal' @@ -12,7 +11,6 @@ export type SnapsModalProps = { export const Snaps: React.FC = ({ isRemoved }) => { const { close, isOpen } = useModal('snaps') - const isSnapsEnabled = useFeatureFlag('Snaps') const { isSnapInstalled, isCorrectVersion } = useIsSnapInstalled() useEffect(() => { @@ -25,7 +23,6 @@ export const Snaps: React.FC = ({ isRemoved }) => { close() }, [close]) - if (!isSnapsEnabled) return null if (isSnapInstalled === null) return null if (isCorrectVersion === null) return null diff --git a/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx b/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx index c38836a52a1..864e645f82c 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/ManualAddressEntry.tsx @@ -1,7 +1,7 @@ import { FormControl, FormLabel, Link } from '@chakra-ui/react' import { type ChainId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import { isMetaMask } from '@shapeshiftoss/hdwallet-metamask' +import { isMetaMask } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { FC } from 'react' import { memo, useCallback, useEffect, useMemo } from 'react' import { useFormContext } from 'react-hook-form' @@ -10,7 +10,6 @@ import { AddressInput } from 'components/Modals/Send/AddressInput/AddressInput' import { SendFormFields } from 'components/Modals/Send/SendCommon' import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useModal } from 'hooks/useModal/useModal' import { useWallet } from 'hooks/useWallet/useWallet' @@ -44,7 +43,6 @@ export const ManualAddressEntry: FC = memo( setValue: setFormValue, } = useFormContext() const translate = useTranslate() - const isSnapEnabled = useFeatureFlag('Snaps') const { open: openSnapsModal } = useModal('snaps') const { open: openManageAccountsModal } = useModal('manageAccounts') @@ -158,7 +156,7 @@ export const ManualAddressEntry: FC = memo( {description ?? translate('trade.receiveAddressDescription', { chainName: buyAssetChainName })} - {!isSnapInstalled && isSnapEnabled && wallet && isMetaMask(wallet) && ( + {!isSnapInstalled && wallet && isMetaMask(wallet) && ( {translate('trade.or')}  {translate('trade.enableMetaMaskSnap')} @@ -184,7 +182,6 @@ export const ManualAddressEntry: FC = memo( buyAssetChainName, handleAddAccount, handleEnableShapeShiftSnap, - isSnapEnabled, isSnapInstalled, rules, translate, diff --git a/src/config.ts b/src/config.ts index f7fbb98421d..6188e4fc1fa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -88,7 +88,6 @@ const validators = { // A flag encapsulating all WalletConnect to dApps - v1 and v2 REACT_APP_FEATURE_WALLET_CONNECT_TO_DAPPS: bool({ default: false }), REACT_APP_FEATURE_WALLET_CONNECT_TO_DAPPS_V2: bool({ default: false }), - REACT_APP_FEATURE_COINBASE_WALLET: bool({ default: false }), REACT_APP_FEATURE_LEDGER_WALLET: bool({ default: false }), REACT_APP_FEATURE_WALLET_CONNECT_V2: bool({ default: false }), REACT_APP_WALLET_CONNECT_TO_DAPPS_PROJECT_ID: str({ default: '' }), @@ -152,7 +151,6 @@ const validators = { REACT_APP_FEATURE_CHATWOOT: bool({ default: false }), REACT_APP_FEATURE_ADVANCED_SLIPPAGE: bool({ default: false }), REACT_APP_EXPERIMENTAL_CUSTOM_SEND_NONCE: bool({ default: false }), - REACT_APP_EXPERIMENTAL_MM_SNAPPY_FINGERS: bool({ default: false }), REACT_APP_SNAP_ID: str(), REACT_APP_SNAP_VERSION: str(), REACT_APP_FEATURE_THORCHAIN_LENDING: bool({ default: false }), diff --git a/src/context/AppProvider/AppContext.tsx b/src/context/AppProvider/AppContext.tsx index 13cf25e289b..fc643027dc3 100644 --- a/src/context/AppProvider/AppContext.tsx +++ b/src/context/AppProvider/AppContext.tsx @@ -4,7 +4,7 @@ import { fromAccountId } from '@shapeshiftoss/caip' import type { LedgerOpenAppEventArgs } from '@shapeshiftoss/chain-adapters' import { emitter } from '@shapeshiftoss/chain-adapters' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { AccountMetadataById } from '@shapeshiftoss/types' import { useQueries } from '@tanstack/react-query' import { DEFAULT_HISTORY_TIMEFRAME } from 'constants/Config' @@ -170,7 +170,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const accountMetadataByAccountId: AccountMetadataById = {} const isMultiAccountWallet = wallet.supportsBip44Accounts() - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet + const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet for (let accountNumber = 0; chainIds.size > 0; accountNumber++) { if ( accountNumber > 0 && diff --git a/src/context/WalletProvider/Coinbase/components/Connect.tsx b/src/context/WalletProvider/Coinbase/components/Connect.tsx index e82e221ee5e..615ffa14211 100644 --- a/src/context/WalletProvider/Coinbase/components/Connect.tsx +++ b/src/context/WalletProvider/Coinbase/components/Connect.tsx @@ -4,7 +4,6 @@ import type { ActionTypes } from 'context/WalletProvider/actions' import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' import { useWallet } from 'hooks/useWallet/useWallet' import { ConnectModal } from '../../components/ConnectModal' @@ -21,7 +20,7 @@ export interface CoinbaseSetupProps } export const CoinbaseConnect = ({ history }: CoinbaseSetupProps) => { - const { dispatch, getAdapter, onProviderChange } = useWallet() + const { dispatch, getAdapter } = useWallet() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -39,17 +38,12 @@ export const CoinbaseConnect = ({ history }: CoinbaseSetupProps) => { const adapter = await getAdapter(KeyManager.Coinbase) if (adapter) { try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - const wallet = await adapter.pairDevice() if (!wallet) { setErrorLoading('walletProvider.errors.walletNotFound') throw new Error('Call to hdwallet-coinbase::pairDevice returned null or undefined') } - await onProviderChange(KeyManager.Coinbase, wallet) - const { name, icon } = CoinbaseConfig const deviceId = await wallet.getDeviceID() const isLocked = await wallet.isLocked() @@ -58,9 +52,12 @@ export const CoinbaseConnect = ({ history }: CoinbaseSetupProps) => { type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId, connectedType: KeyManager.Coinbase }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: isLocked }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Coinbase, deviceId) + localWallet.setLocalWallet({ type: KeyManager.Coinbase, deviceId }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) } catch (e: any) { console.error(e, 'Coinbase Connect: There was an error initializing the wallet') @@ -69,7 +66,7 @@ export const CoinbaseConnect = ({ history }: CoinbaseSetupProps) => { } } setLoading(false) - }, [dispatch, getAdapter, history, localWallet, onProviderChange]) + }, [dispatch, getAdapter, history, localWallet]) return ( { const wallet = await (async () => { try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - const sdk = await setupKeepKeySDK() // There is no need to instantiate KkRestAdapter and attempt pairing if SDK is undefined @@ -113,11 +109,14 @@ export const KeepKeyConnect = () => { connectedType: KeyManager.KeepKey, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - localWallet.setLocalWalletTypeAndDeviceId( - KeyManager.KeepKey, - state.keyring.getAlias(deviceId), - ) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) + localWallet.setLocalWallet({ + type: KeyManager.KeepKey, + deviceId: state.keyring.getAlias(deviceId), + }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) } catch (e) { console.error(e) diff --git a/src/context/WalletProvider/KeepKey/hooks/useKeepKeyEventHandler.ts b/src/context/WalletProvider/KeepKey/hooks/useKeepKeyEventHandler.ts index b60ce00f781..abaf5118fa0 100644 --- a/src/context/WalletProvider/KeepKey/hooks/useKeepKeyEventHandler.ts +++ b/src/context/WalletProvider/KeepKey/hooks/useKeepKeyEventHandler.ts @@ -230,7 +230,10 @@ export const useKeepKeyEventHandler = ( icon: state.walletInfo.icon, // We're reconnecting the same wallet so we can reuse the walletInfo }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) } } catch (e) { console.error(e) @@ -241,7 +244,10 @@ export const useKeepKeyEventHandler = ( try { const id = keyring.getAlias(deviceId) if (id === state.walletInfo?.deviceId) { - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: false }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: false, + }) } if (modal) { // Little trick to send the user back to the wallet select route diff --git a/src/context/WalletProvider/Keplr/components/Connect.tsx b/src/context/WalletProvider/Keplr/components/Connect.tsx index 3d32efef4f8..9f914f13551 100644 --- a/src/context/WalletProvider/Keplr/components/Connect.tsx +++ b/src/context/WalletProvider/Keplr/components/Connect.tsx @@ -4,7 +4,6 @@ import type { ActionTypes } from 'context/WalletProvider/actions' import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' import { useWallet } from 'hooks/useWallet/useWallet' import { ConnectModal } from '../../components/ConnectModal' @@ -32,9 +31,6 @@ export const KeplrConnect = ({ history }: KeplrSetupProps) => { setLoading(true) const adapter = await getAdapter(KeyManager.Keplr) if (adapter) { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - const wallet = await adapter.pairDevice() if (!wallet) { setErrorLoading('walletProvider.errors.walletNotFound') @@ -56,8 +52,11 @@ export const KeplrConnect = ({ history }: KeplrSetupProps) => { type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId, connectedType: KeyManager.Keplr }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Keplr, deviceId) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) + localWallet.setLocalWallet({ type: KeyManager.Keplr, deviceId }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) /** Reinitialize wallet when user changes accounts */ diff --git a/src/context/WalletProvider/KeyManager.ts b/src/context/WalletProvider/KeyManager.ts index f2c9a8895c8..3f702c9b900 100644 --- a/src/context/WalletProvider/KeyManager.ts +++ b/src/context/WalletProvider/KeyManager.ts @@ -1,13 +1,12 @@ export enum KeyManager { Mobile = 'mobile', Native = 'native', - KeepKey = 'keepkey', - MetaMask = 'metamask', + Coinbase = 'coinbase', Phantom = 'phantom', - Demo = 'demo', - XDefi = 'xdefi', + MetaMask = 'metamask', + KeepKey = 'keepkey', Keplr = 'keplr', - Coinbase = 'coinbase', + Demo = 'demo', WalletConnectV2 = 'walletconnectv2', Ledger = 'ledger', } diff --git a/src/context/WalletProvider/Ledger/components/Connect.tsx b/src/context/WalletProvider/Ledger/components/Connect.tsx index 6bd7edb82b1..ad807f34025 100644 --- a/src/context/WalletProvider/Ledger/components/Connect.tsx +++ b/src/context/WalletProvider/Ledger/components/Connect.tsx @@ -7,7 +7,6 @@ import type { ActionTypes } from 'context/WalletProvider/actions' import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useWallet } from 'hooks/useWallet/useWallet' import { portfolio, portfolioApi } from 'state/slices/portfolioSlice/portfolioSlice' @@ -65,9 +64,6 @@ export const LedgerConnect = ({ history }: LedgerSetupProps) => { const adapter = await getAdapter(KeyManager.Ledger) if (adapter) { try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - // Pair the device, which gets approval from the browser to communicate with the Ledger USB device const wallet = await adapter.pairDevice() @@ -102,8 +98,11 @@ export const LedgerConnect = ({ history }: LedgerSetupProps) => { type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId, connectedType: KeyManager.Ledger }, }) - walletDispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Ledger, deviceId) + walletDispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) + localWallet.setLocalWallet({ type: KeyManager.Ledger, deviceId }) // If account management is enabled, exit the WalletProvider context, which doesn't have access to the ModalProvider // The Account drawer will be opened further down the tree diff --git a/src/context/WalletProvider/MetaMask/components/Connect.tsx b/src/context/WalletProvider/MetaMask/components/Connect.tsx index b98d931689c..65336cb2e1f 100644 --- a/src/context/WalletProvider/MetaMask/components/Connect.tsx +++ b/src/context/WalletProvider/MetaMask/components/Connect.tsx @@ -1,6 +1,9 @@ import { getConfig } from 'config' -import React, { useCallback, useState } from 'react' +import uniqBy from 'lodash/uniqBy' +import type { InterpolationOptions } from 'node-polyglot' +import React, { useCallback, useMemo, useState } from 'react' import { isMobile } from 'react-device-detect' +import { useTranslate } from 'react-polyglot' import { useSelector } from 'react-redux' import type { RouteComponentProps } from 'react-router-dom' import { getSnapVersion } from 'utils/snaps' @@ -8,15 +11,13 @@ import type { ActionTypes } from 'context/WalletProvider/actions' import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { checkIsMetaMaskDesktop, - checkIsMetaMaskImpersonator, checkIsMetaMaskMobileWebView, checkIsSnapInstalled, } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useWallet } from 'hooks/useWallet/useWallet' +import { METAMASK_RDNS, staticMipdProviders, useMipdProviders } from 'lib/mipd' import { selectShowSnapsModal } from 'state/slices/selectors' import { ConnectModal } from '../../components/ConnectModal' @@ -34,36 +35,66 @@ export interface MetaMaskSetupProps } export const MetaMaskConnect = ({ history }: MetaMaskSetupProps) => { + const translate = useTranslate() const isMetaMaskMobileWebView = checkIsMetaMaskMobileWebView() - const { dispatch, getAdapter, onProviderChange } = useWallet() + const { + dispatch, + getAdapter, + state: { modalType }, + } = useWallet() const localWallet = useLocalWallet() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const showSnapModal = useSelector(selectShowSnapsModal) + const detectedMipdProviders = useMipdProviders() + const mipdProviders = useMemo( + () => uniqBy(detectedMipdProviders.concat(staticMipdProviders), 'info.rdns'), + [detectedMipdProviders], + ) + const maybeMipdProvider = mipdProviders.find(provider => provider.info.rdns === modalType) + + const headerText: [string, InterpolationOptions] = useMemo( + () => [ + 'walletProvider.mipd.connect.header', + { name: maybeMipdProvider?.info.name ?? 'MetaMask' }, + ], + [maybeMipdProvider?.info.name], + ) + + const bodyText: [string, InterpolationOptions] = useMemo( + () => [ + 'walletProvider.mipd.connect.body', + { name: maybeMipdProvider?.info.name ?? 'MetaMask' }, + ], + [maybeMipdProvider?.info.name], + ) + const setErrorLoading = useCallback((e: string | null) => { setError(e) setLoading(false) }, []) - const isSnapsEnabled = useFeatureFlag('Snaps') - const pairDevice = useCallback(async () => { setError(null) setLoading(true) const adapter = await getAdapter(KeyManager.MetaMask) + if (!maybeMipdProvider?.provider) { + if (modalType === 'io.xdefi') window.open('https://www.xdefi.io/', '_blank') + if (modalType === 'io.rabby') window.open('https://rabby.io/', '_blank') + } if (adapter) { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - const wallet = await adapter.pairDevice() - if (!wallet) { - setErrorLoading('walletProvider.errors.walletNotFound') - throw new Error('Call to hdwallet-metamask::pairDevice returned null or undefined') - } - - const { name, icon } = MetaMaskConfig try { + const wallet = await adapter.pairDevice() + if (!wallet) { + setErrorLoading('walletProvider.errors.walletNotFound') + throw new Error( + 'Call to hdwallet-metamask-multichain::pairDevice returned null or undefined', + ) + } + + const { name, icon } = MetaMaskConfig const deviceId = await wallet.getDeviceID() const isLocked = await wallet.isLocked() @@ -74,21 +105,30 @@ export const MetaMaskConnect = ({ history }: MetaMaskSetupProps) => { type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId, connectedType: KeyManager.MetaMask }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: isLocked }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.MetaMask, deviceId) - const provider = await onProviderChange(KeyManager.MetaMask, wallet) - - if (!provider) { - throw new Error('walletProvider.metaMask.errors.connectFailure') + if (!maybeMipdProvider?.provider) { + throw new Error( + translate('walletProvider.mipd.errors.connectFailure', { + name: maybeMipdProvider?.info.name ?? 'MetaMask', + }), + ) } + localWallet.setLocalWallet({ + type: KeyManager.MetaMask, + deviceId, + rdns: maybeMipdProvider?.info.rdns, + }) + await (async () => { - const isMetaMaskDesktop = await checkIsMetaMaskDesktop(wallet) - const isMetaMaskImpersonator = await checkIsMetaMaskImpersonator(wallet) - // Wallets other than MM desktop - including MM impersonators - don't support MM snaps - if (!isMetaMaskDesktop || isMetaMaskImpersonator || isMetaMaskMobileWebView) + const isMetaMaskDesktop = checkIsMetaMaskDesktop(wallet) + // Wallets other than MM desktop don't support MM snaps + if (!isMetaMaskDesktop || isMetaMaskMobileWebView) return dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) const isSnapInstalled = await checkIsSnapInstalled() @@ -96,10 +136,10 @@ export const MetaMaskConnect = ({ history }: MetaMaskSetupProps) => { const isCorrectVersion = snapVersion === getConfig().REACT_APP_SNAP_VERSION - if (isSnapsEnabled && isSnapInstalled && !isCorrectVersion && showSnapModal) { + if (isSnapInstalled && !isCorrectVersion && showSnapModal) { return history.push('/metamask/snap/update') } - if (isSnapsEnabled && !isSnapInstalled && showSnapModal) { + if (!isSnapInstalled && showSnapModal) { return history.push('/metamask/snap/install') } @@ -110,7 +150,11 @@ export const MetaMaskConnect = ({ history }: MetaMaskSetupProps) => { console.error(e) setErrorLoading(e?.message) } else { - setErrorLoading('walletProvider.metaMask.errors.unknown') + setErrorLoading( + translate('walletProvider.mipd.errors.unknown', { + name: maybeMipdProvider?.info.name ?? 'MetaMask', + }), + ) history.push('/metamask/failure') } } @@ -118,12 +162,15 @@ export const MetaMaskConnect = ({ history }: MetaMaskSetupProps) => { setLoading(false) }, [ getAdapter, - setErrorLoading, + maybeMipdProvider?.provider, + maybeMipdProvider?.info.rdns, + maybeMipdProvider?.info.name, + modalType, dispatch, localWallet, - onProviderChange, + setErrorLoading, + translate, isMetaMaskMobileWebView, - isSnapsEnabled, showSnapModal, history, ]) @@ -140,7 +187,7 @@ export const MetaMaskConnect = ({ history }: MetaMaskSetupProps) => { return window.location.assign(`${METAMASK_DEEP_LINK_BASE_URL}/${mmDeeplinkTarget}`) }, []) - return isMobile && !isMetaMaskMobileWebView ? ( + return isMobile && !isMetaMaskMobileWebView && modalType === METAMASK_RDNS ? ( { /> ) : ( { - return ( - + const { + state: { modalType }, + } = useWallet() + const detectedMipdProviders = useMipdProviders() + const mipdProviders = useMemo( + () => uniqBy(detectedMipdProviders.concat(staticMipdProviders), 'info.rdns'), + [detectedMipdProviders], ) + const maybeMipdProvider = mipdProviders.find(provider => provider.info.rdns === modalType) + + const bodyText: [string, InterpolationOptions] = useMemo( + () => [ + 'walletProvider.mipd.failure.body', + { name: maybeMipdProvider?.info.name ?? 'MetaMask' }, + ], + [maybeMipdProvider?.info.name], + ) + + return } diff --git a/src/context/WalletProvider/MetaMask/components/MetaMaskMenu.tsx b/src/context/WalletProvider/MetaMask/components/MetaMaskMenu.tsx index 638824b3ac2..99b6f114654 100644 --- a/src/context/WalletProvider/MetaMask/components/MetaMaskMenu.tsx +++ b/src/context/WalletProvider/MetaMask/components/MetaMaskMenu.tsx @@ -4,7 +4,6 @@ import { useTranslate } from 'react-polyglot' import { ManageAccountsMenuItem } from 'components/Layout/Header/NavBar/ManageAccountsMenuItem' import { checkIsMetaMaskDesktop, - checkIsMetaMaskImpersonator, useIsSnapInstalled, } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useModal } from 'hooks/useModal/useModal' @@ -26,11 +25,8 @@ export const MetaMaskMenu: React.FC = ({ onClose }) => { useEffect(() => { if (!wallet) return - ;(async () => { - const isMetaMaskDesktop = await checkIsMetaMaskDesktop(wallet) - const isMetaMaskImpersonator = await checkIsMetaMaskImpersonator(wallet) - setIsMetaMask(isMetaMaskDesktop && !isMetaMaskImpersonator) - })() + const isMetaMaskDesktop = checkIsMetaMaskDesktop(wallet) + setIsMetaMask(isMetaMaskDesktop) }, [wallet]) const handleClick = useCallback(() => { diff --git a/src/context/WalletProvider/MetaMask/config.ts b/src/context/WalletProvider/MetaMask/config.ts index fa183c97024..e66d38e673b 100644 --- a/src/context/WalletProvider/MetaMask/config.ts +++ b/src/context/WalletProvider/MetaMask/config.ts @@ -1,5 +1,4 @@ -import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask' -import { getConfig } from 'config' +import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask-multichain' import { MetaMaskIcon } from 'components/Icons/MetaMaskIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' @@ -9,11 +8,7 @@ export const MetaMaskConfig: MetaMaskConfigType = { adapters: [ { loadAdapter: () => - getConfig().REACT_APP_EXPERIMENTAL_MM_SNAPPY_FINGERS - ? import('@shapeshiftoss/hdwallet-shapeshift-multichain').then( - m => m.MetaMaskAdapter as typeof MetaMaskAdapter, - ) - : import('@shapeshiftoss/hdwallet-metamask').then(m => m.MetaMaskAdapter), + import('@shapeshiftoss/hdwallet-metamask-multichain').then(m => m.MetaMaskAdapter), }, ], supportsMobile: 'browser', diff --git a/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx b/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx index b18238df961..f5d02d0d7e4 100644 --- a/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx +++ b/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx @@ -164,10 +164,13 @@ export const MobileLoad = ({ history }: RouteComponentProps) => { connectedType: KeyManager.Mobile, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Mobile, deviceId) + localWallet.setLocalWallet({ type: KeyManager.Mobile, deviceId }) localWallet.setLocalNativeWalletName(item?.label ?? 'label') } catch (e) { console.log(e) diff --git a/src/context/WalletProvider/MobileWallet/components/MobileSuccess.tsx b/src/context/WalletProvider/MobileWallet/components/MobileSuccess.tsx index a8638535088..c31e1e41197 100644 --- a/src/context/WalletProvider/MobileWallet/components/MobileSuccess.tsx +++ b/src/context/WalletProvider/MobileWallet/components/MobileSuccess.tsx @@ -17,7 +17,7 @@ export const MobileSuccess = ({ location }: MobileSetupProps) => { const appDispatch = useAppDispatch() const { setWelcomeModal } = preferences.actions const [isSuccessful, setIsSuccessful] = useStateIfMounted(null) - const { getAdapter, dispatch } = useWallet() + const { state, getAdapter, dispatch } = useWallet() const localWallet = useLocalWallet() const { vault } = location.state @@ -46,12 +46,15 @@ export const MobileSuccess = ({ location }: MobileSetupProps) => { connectedType: KeyManager.Mobile, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Mobile, deviceId) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) + localWallet.setLocalWallet({ type: KeyManager.Mobile, deviceId }) localWallet.setLocalNativeWalletName(walletLabel) dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, - payload: KeyManager.Mobile, + payload: { modalType: KeyManager.Mobile, isMipdProvider: false }, }) appDispatch(setWelcomeModal({ show: true })) return setIsSuccessful(true) @@ -67,7 +70,16 @@ export const MobileSuccess = ({ location }: MobileSetupProps) => { // Make sure the component is completely unmounted before we revoke the mnemonic setTimeout(() => vault?.revoke(), 500) } - }, [appDispatch, dispatch, getAdapter, localWallet, setIsSuccessful, setWelcomeModal, vault]) + }, [ + appDispatch, + dispatch, + getAdapter, + localWallet, + setIsSuccessful, + setWelcomeModal, + state.modalType, + vault, + ]) return ( <> diff --git a/src/context/WalletProvider/NativeWallet/components/EnterPassword.tsx b/src/context/WalletProvider/NativeWallet/components/EnterPassword.tsx index cf91481fad0..9a48401dbbf 100644 --- a/src/context/WalletProvider/NativeWallet/components/EnterPassword.tsx +++ b/src/context/WalletProvider/NativeWallet/components/EnterPassword.tsx @@ -79,7 +79,10 @@ export const EnterPassword = () => { meta: { label: vault.meta.get('name') as string }, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) } catch (e) { @@ -93,7 +96,7 @@ export const EnterPassword = () => { ) } }, - [deviceId, dispatch, setError, translate, keyring], + [keyring, deviceId, dispatch, setError, translate], ) const handleFormSubmit = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit]) diff --git a/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx b/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx index 488d45a72cc..b4bad70d145 100644 --- a/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx +++ b/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx @@ -22,7 +22,6 @@ import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' import { NativeWalletRoutes } from 'context/WalletProvider/types' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' import { useWallet } from 'hooks/useWallet/useWallet' import { NativeConfig } from '../config' @@ -82,9 +81,6 @@ export const NativeLoad = ({ history }: RouteComponentProps) => { if (adapter) { const { name, icon } = NativeConfig try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - const wallet = await adapter.pairDevice(deviceId) if (!(await wallet?.isInitialized())) { // This will trigger the password modal and the modal will set the wallet on state @@ -103,12 +99,15 @@ export const NativeLoad = ({ history }: RouteComponentProps) => { connectedType: KeyManager.Native, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) // The wallet is already initialized so we can close the modal dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) } - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Native, deviceId) + localWallet.setLocalWallet({ type: KeyManager.Native, deviceId }) localWallet.setLocalNativeWalletName(item.name) } catch (e) { setError('walletProvider.shapeShift.load.error.pair') diff --git a/src/context/WalletProvider/NativeWallet/hooks/useNativeSuccess.ts b/src/context/WalletProvider/NativeWallet/hooks/useNativeSuccess.ts index b2109d29048..bd2510539a0 100644 --- a/src/context/WalletProvider/NativeWallet/hooks/useNativeSuccess.ts +++ b/src/context/WalletProvider/NativeWallet/hooks/useNativeSuccess.ts @@ -4,7 +4,6 @@ import { useEffect } from 'react' import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' import { useStateIfMounted } from 'hooks/useStateIfMounted/useStateIfMounted' import { useWallet } from 'hooks/useWallet/useWallet' import { preferences } from 'state/slices/preferencesSlice/preferencesSlice' @@ -26,9 +25,6 @@ export const useNativeSuccess = ({ vault }: UseNativeSuccessPropTypes) => { const adapter = await getAdapter(KeyManager.Native) if (!adapter) throw new Error('Native adapter not found') try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - await new Promise(resolve => setTimeout(resolve, 250)) await Promise.all([navigator.storage?.persist?.(), vault.save()]) @@ -50,9 +46,12 @@ export const useNativeSuccess = ({ vault }: UseNativeSuccessPropTypes) => { connectedType: KeyManager.Native, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Native, deviceId) + localWallet.setLocalWallet({ type: KeyManager.Native, deviceId }) localWallet.setLocalNativeWalletName(walletLabel) setIsSuccessful(true) //Set to show the native onboarding diff --git a/src/context/WalletProvider/Phantom/components/Connect.tsx b/src/context/WalletProvider/Phantom/components/Connect.tsx index e8866bb0b72..c778bae3066 100644 --- a/src/context/WalletProvider/Phantom/components/Connect.tsx +++ b/src/context/WalletProvider/Phantom/components/Connect.tsx @@ -4,7 +4,6 @@ import type { ActionTypes } from 'context/WalletProvider/actions' import { WalletActions } from 'context/WalletProvider/actions' import { KeyManager } from 'context/WalletProvider/KeyManager' import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' import { useWallet } from 'hooks/useWallet/useWallet' import { ConnectModal } from '../../components/ConnectModal' @@ -21,7 +20,7 @@ export interface PhantomSetupProps } export const PhantomConnect = ({ history }: PhantomSetupProps) => { - const { dispatch, getAdapter, onProviderChange } = useWallet() + const { dispatch, getAdapter } = useWallet() const localWallet = useLocalWallet() const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -38,17 +37,12 @@ export const PhantomConnect = ({ history }: PhantomSetupProps) => { const adapter = await getAdapter(KeyManager.Phantom) if (adapter) { try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - const wallet = await adapter.pairDevice() if (!wallet) { setErrorLoading('walletProvider.errors.walletNotFound') throw new Error('Call to hdwallet-phantom::pairDevice returned null or undefined') } - await onProviderChange(KeyManager.Phantom, wallet) - const { name, icon } = PhantomConfig const deviceId = await wallet.getDeviceID() const isLocked = await wallet.isLocked() @@ -57,9 +51,12 @@ export const PhantomConnect = ({ history }: PhantomSetupProps) => { type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId, connectedType: KeyManager.Phantom }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: isLocked }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Phantom, deviceId) + localWallet.setLocalWallet({ type: KeyManager.Phantom, deviceId }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) } catch (e: any) { console.error(e, 'Phantom Connect: There was an error initializing the wallet') @@ -68,7 +65,7 @@ export const PhantomConnect = ({ history }: PhantomSetupProps) => { } } setLoading(false) - }, [dispatch, getAdapter, history, localWallet, onProviderChange, setErrorLoading]) + }, [dispatch, getAdapter, history, localWallet, setErrorLoading]) return ( void }) => { - const greenColor = useColorModeValue('green.500', 'green.200') - const option = SUPPORTED_WALLETS[walletType] // some wallets (e.g. keepkey) do not exist on mobile @@ -55,50 +61,103 @@ const WalletSelectItem = ({ const handleConnect = useCallback(() => connect(walletType), [connect, walletType]) + const OptionIcon = option.icon + const Icon = useMemo(() => , [OptionIcon]) + const isLedgerEnabled = useFeatureFlag('LedgerWallet') if (walletType === KeyManager.Ledger && !isLedgerEnabled) return null if (!isSupported) return null - const isCoinbaseEnabled = getConfig().REACT_APP_FEATURE_COINBASE_WALLET - if (walletType === KeyManager.Coinbase && !isCoinbaseEnabled) return null - const isPhantomEnabled = getConfig().REACT_APP_FEATURE_PHANTOM_WALLET if (walletType === KeyManager.Phantom && !isPhantomEnabled) return null const isWalletConnectV2Enabled = getConfig().REACT_APP_FEATURE_WALLET_CONNECT_V2 if (walletType === KeyManager.WalletConnectV2 && !isWalletConnectV2Enabled) return null - const Icon = option.icon const activeWallet = walletInfo?.name === option.name - const walletSubText = activeWallet ? 'common.connected' : null + + return ( + + ) +} + +const SelectItem = ({ + id, + isActive, + onClick, + name, + Icon, +}: { + id: string + isActive: boolean + onClick: () => void + name: string + Icon: ReactElement +}) => { + const greenColor = useColorModeValue('green.500', 'green.200') + const walletSubText = isActive ? 'common.connected' : null return ( ) } +const MipdProviderSelectItem = ({ + provider, + connect, +}: { + provider: EIP6963ProviderDetail + connect: (adapter: string) => void +}) => { + const connectedRdns = useAppSelector(selectWalletRdns) + + const handleConnect = useCallback( + () => connect(provider.info.rdns), + [connect, provider.info.rdns], + ) + + const detectedMipdProviders = useMipdProviders() + const connectedMipdProvider = detectedMipdProviders.find( + provider => provider.info.rdns === connectedRdns, + ) + + const icon = provider.info.icon + const activeWallet = provider.info.name === connectedMipdProvider?.info.name + + const Icon = useMemo(() => , [icon]) + + return ( + + ) +} export const SelectModal = () => { const { @@ -108,9 +167,21 @@ export const SelectModal = () => { importWallet, } = useWallet() const translate = useTranslate() + const detectedMipdProviders = useMipdProviders() + const supportedStaticProviders = useMemo(() => { + // Mobile app doesn't support MM and the like + if (isMobileApp) return [] + if (isMobile) return staticMipdProviders.filter(provider => provider.supportsMobileBrowser) + return staticMipdProviders + }, []) + const mipdProviders = useMemo( + () => uniqBy(detectedMipdProviders.concat(supportedStaticProviders), 'info.rdns'), + [detectedMipdProviders, supportedStaticProviders], + ) const wallets = useMemo( - () => Object.values(KeyManager).filter(key => key !== KeyManager.Demo), + () => + Object.values(KeyManager).filter(key => key !== KeyManager.Demo && key !== KeyManager.Native), [], ) @@ -124,6 +195,54 @@ export const SelectModal = () => { [importWallet], ) + const handleConnectMipd = useCallback( + (rdns: string) => connect(rdns as KeyManager, true), + [connect], + ) + + const handleConnect = useCallback((name: KeyManager) => connect(name, false), [connect]) + + const allProviders = useMemo( + () => ( + <> + {mipdProviders + .filter( + // EIP-1193 provider for Keplr is for EVM, but our implementation is for Cosmos SDK + // TODO(gomes): leverage EIP-1193 provider in keplr hdwallet as a quick win to get EVM support there and keep only our own + provider => + provider.info.rdns !== 'app.keplr' && + // And similarly for Phantom, the EIP-1193 provider is only an EVM provider, but we have our own implementation with EVMs + Bitcoin + Solana + provider.info.rdns !== 'app.phantom' && + // Ensure we leverage the first-class implementation + provider.info.rdns !== 'com.coinbase.wallet', + ) + .map(provider => ( + + ))} + { + // TODO: KeepKey adapter may fail due to the USB interface being in use by another tab + // So not all of the supported wallets will have an initialized adapter + wallets + // Remove MM dupe and leverage MIPD provider for MetaMask, ensuring MM is consistently working with multiple wallets installed + .filter(keyManager => keyManager !== KeyManager.MetaMask) + .map(walletType => ( + + )) + } + + ), + [handleConnect, handleConnectMipd, mipdProviders, walletInfo, wallets], + ) + return ( <> @@ -131,19 +250,16 @@ export const SelectModal = () => { + + + - { - // TODO: KeepKey adapter may fail due to the USB interface being in use by another tab - // So not all of the supported wallets will have an initialized adapter - wallets.map(walletType => ( - - )) - } + {allProviders} // This is a bit blunt, and we might want to consider a more targeted approach. // https://github.com/orgs/WalletConnect/discussions/3010 clearWalletConnectLocalStorage() - const { dispatch, state, getAdapter, onProviderChange } = useWallet() + const { dispatch, state, getAdapter } = useWallet() const localWallet = useLocalWallet() const [loading, setLoading] = useState(false) @@ -37,9 +36,6 @@ export const WalletConnectV2Connect = ({ history }: WalletConnectSetupProps) => try { if (adapter) { if (!state.wallet || !isWalletConnectWallet(state.wallet)) { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - setLoading(true) // trigger the web3 modal @@ -47,11 +43,9 @@ export const WalletConnectV2Connect = ({ history }: WalletConnectSetupProps) => if (!wallet) throw new WalletNotFoundError() - await onProviderChange(KeyManager.WalletConnectV2, wallet) - dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) dispatch({ - type: WalletActions.SET_PROVIDER, + type: WalletActions.SET_WCV2_PROVIDER, payload: wallet.provider as unknown as EthereumProvider, }) @@ -62,8 +56,11 @@ export const WalletConnectV2Connect = ({ history }: WalletConnectSetupProps) => type: WalletActions.SET_WALLET, payload: { wallet, name, icon, deviceId, connectedType: KeyManager.WalletConnectV2 }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.WalletConnectV2, deviceId) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) + localWallet.setLocalWallet({ type: KeyManager.WalletConnectV2, deviceId }) } } dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) @@ -74,7 +71,7 @@ export const WalletConnectV2Connect = ({ history }: WalletConnectSetupProps) => history.push('/walletconnect/failure') } } - }, [dispatch, getAdapter, history, localWallet, onProviderChange, state.wallet]) + }, [dispatch, getAdapter, history, localWallet, state.wallet]) return ( , ) => { - const { provider } = state - const ethereumProvider = provider as EthereumProvider + const { wcV2Provider } = state const localWallet = useLocalWallet() @@ -27,17 +25,16 @@ export const useWalletConnectV2EventHandler = ( }, [dispatch, localWallet, state.wallet]) useEffect(() => { - // This effect should run and attach event handlers on WalletConnectV2 only - // Failure to check for the localWalletType will result in a bunch of random bugs on other wallets - // being mistakenly identified as WalletConnectV2 + // This effect should never run for wallets other than WalletConnectV2 since we explicitly tap into @walletconnect/ethereum-provider provider + // but... const localWalletType = localWallet.localWalletType if (localWalletType !== KeyManager.WalletConnectV2) return - if (ethereumProvider) { - ethereumProvider.on('disconnect', handleDisconnect) + if (wcV2Provider) { + wcV2Provider.on('disconnect', handleDisconnect) return () => { - ethereumProvider.off?.('disconnect', handleDisconnect) + wcV2Provider.off?.('disconnect', handleDisconnect) } } - }, [dispatch, ethereumProvider, handleDisconnect, localWallet.localWalletType, state.wallet]) + }, [dispatch, handleDisconnect, localWallet.localWalletType, state.wallet, wcV2Provider]) } diff --git a/src/context/WalletProvider/WalletContext.tsx b/src/context/WalletProvider/WalletContext.tsx index 0e4beb844c9..3add1493b2c 100644 --- a/src/context/WalletProvider/WalletContext.tsx +++ b/src/context/WalletProvider/WalletContext.tsx @@ -1,27 +1,22 @@ -import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import type React from 'react' import { createContext } from 'react' import type { ActionTypes } from './actions' import type { KeyManager } from './KeyManager' import type { GetAdapter } from './types' -import type { DeviceState, InitialState, KeyManagerWithProvider } from './WalletProvider' +import type { DeviceState, InitialState } from './WalletProvider' export interface IWalletContext { state: InitialState getAdapter: GetAdapter dispatch: React.Dispatch - connect: (adapter: KeyManager) => void + connect: (adapter: KeyManager | string, isMipdProvider: boolean) => void create: (adapter: KeyManager) => void importWallet: (adapter: KeyManager) => void disconnect: () => void load: () => void setDeviceState: (deviceState: Partial) => void connectDemo: () => Promise - onProviderChange: ( - localWalletType: KeyManagerWithProvider, - wallet: HDWallet | null, - ) => Promise } export const WalletContext = createContext(null) diff --git a/src/context/WalletProvider/WalletProvider.test.tsx b/src/context/WalletProvider/WalletProvider.test.tsx index be7910e054c..dba7a170744 100644 --- a/src/context/WalletProvider/WalletProvider.test.tsx +++ b/src/context/WalletProvider/WalletProvider.test.tsx @@ -1,6 +1,6 @@ import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import { WebUSBKeepKeyAdapter } from '@shapeshiftoss/hdwallet-keepkey-webusb' -import { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask' +import { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask-multichain' import { act, renderHook } from '@testing-library/react' import type { PropsWithChildren } from 'react' import { TestProviders } from 'test/TestProviders' @@ -28,12 +28,16 @@ vi.mock('friendly-challenge', () => ({ WidgetInstance: {}, })) -vi.mock('@shapeshiftoss/hdwallet-metamask', () => ({ +vi.mock('@shapeshiftoss/hdwallet-metamask-multichain', () => ({ MetaMaskAdapter: { useKeyring: vi.fn(), }, })) +vi.mock('./useEip1993EventHandler', () => ({ + useEip1993EventHandler: vi.fn(), +})) + const walletInfoPayload = { name: SUPPORTED_WALLETS.native.name, icon: SUPPORTED_WALLETS.native.icon, @@ -87,11 +91,17 @@ describe('WalletProvider', () => { expect(result.current.state.isConnected).toBe(false) act(() => { - result.current.dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + result.current.dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) }) expect(result.current.state.isConnected).toBe(true) act(() => { - result.current.dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: false }) + result.current.dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: false, + }) }) expect(result.current.state.isConnected).toBe(false) }) @@ -120,7 +130,7 @@ describe('WalletProvider', () => { expect(result.current.state.isConnected).toBe(false) act(() => { - result.current.connect(type) + result.current.connect(type, false) }) expect(result.current.state.modalType).toBe(type) @@ -163,7 +173,10 @@ describe('WalletProvider', () => { ...walletInfoPayload, }, }) - result.current.dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + result.current.dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) }) expect(result.current.state.wallet).toBeTruthy() diff --git a/src/context/WalletProvider/WalletProvider.tsx b/src/context/WalletProvider/WalletProvider.tsx index e086021cbe7..1549ec8ae70 100644 --- a/src/context/WalletProvider/WalletProvider.tsx +++ b/src/context/WalletProvider/WalletProvider.tsx @@ -1,9 +1,8 @@ import type { ComponentWithAs, IconProps } from '@chakra-ui/react' import { useColorModeValue } from '@chakra-ui/react' -import detectEthereumProvider from '@metamask/detect-provider' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import { Keyring } from '@shapeshiftoss/hdwallet-core' -import type { MetaMaskHDWallet } from '@shapeshiftoss/hdwallet-metamask' +import type { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { NativeHDWallet } from '@shapeshiftoss/hdwallet-native' import { Dummy } from '@shapeshiftoss/hdwallet-native/dist/crypto/isolation/engines' import type { EthereumProvider as EthereumProviderType } from '@walletconnect/ethereum-provider/dist/types/EthereumProvider' @@ -12,7 +11,6 @@ import type { BrowserProvider } from 'ethers' import findIndex from 'lodash/findIndex' import omit from 'lodash/omit' import React, { useCallback, useEffect, useMemo, useReducer } from 'react' -import { isMobile } from 'react-device-detect' import type { Entropy } from 'context/WalletProvider/KeepKey/components/RecoverySettings' import { VALID_ENTROPY } from 'context/WalletProvider/KeepKey/components/RecoverySettings' import { useKeepKeyEventHandler } from 'context/WalletProvider/KeepKey/hooks/useKeepKeyEventHandler' @@ -20,7 +18,7 @@ import { MobileConfig } from 'context/WalletProvider/MobileWallet/config' import { getWallet } from 'context/WalletProvider/MobileWallet/mobileMessageHandlers' import { KeepKeyRoutes } from 'context/WalletProvider/routes' import { useWalletConnectV2EventHandler } from 'context/WalletProvider/WalletConnectV2/useWalletConnectV2EventHandler' -import { isSome } from 'lib/utils' +import { useMipdProviders } from 'lib/mipd' import { localWalletSlice } from 'state/slices/localWalletSlice/localWalletSlice' import { selectWalletDeviceId, selectWalletType } from 'state/slices/localWalletSlice/selectors' import { portfolio } from 'state/slices/portfolioSlice/portfolioSlice' @@ -37,6 +35,7 @@ import { useLedgerEventHandler } from './Ledger/hooks/useLedgerEventHandler' import { useLocalWallet } from './local-wallet' import { useNativeEventHandler } from './NativeWallet/hooks/useNativeEventHandler' import { type AdaptersByKeyManager, type GetAdapter, NativeWalletRoutes } from './types' +import { useEip1993EventHandler } from './useEip1993EventHandler' import type { IWalletContext } from './WalletContext' import { WalletContext } from './WalletContext' import { WalletViewsRouter } from './WalletViewsRouter' @@ -76,25 +75,16 @@ const initialDeviceState: DeviceState = { } export type MetaMaskLikeProvider = BrowserProvider -// A subset of wallets which have an EIP-1193-like provider -export type KeyManagerWithProvider = - | KeyManager.XDefi - | KeyManager.MetaMask - | KeyManager.Phantom - | KeyManager.WalletConnectV2 - | KeyManager.Coinbase - -export interface InitialState { +export type InitialState = { keyring: Keyring adapters: Partial wallet: HDWallet | null - modalType: KeyManager | null connectedType: KeyManager | null initialRoute: string | null walletInfo: WalletInfo | null isConnected: boolean isDemoWallet: boolean - provider: MetaMaskLikeProvider | EthereumProviderType | null + wcV2Provider: EthereumProviderType | null isLocked: boolean modal: boolean isLoadingLocalWallet: boolean @@ -103,19 +93,29 @@ export interface InitialState { keepKeyPinRequestType: PinMatrixRequestType | null deviceState: DeviceState disconnectOnCloseModal: boolean -} +} & ( + | { + modalType: KeyManager | null + isMipdProvider: false + } + | { + modalType: string | null + isMipdProvider: true + } +) const initialState: InitialState = { keyring: new Keyring(), adapters: {}, wallet: null, modalType: null, + isMipdProvider: false, connectedType: null, initialRoute: null, walletInfo: null, isConnected: false, isDemoWallet: false, - provider: null, + wcV2Provider: null, isLocked: false, modal: false, isLoadingLocalWallet: false, @@ -126,61 +126,6 @@ const initialState: InitialState = { disconnectOnCloseModal: false, } -export const isKeyManagerWithProvider = ( - keyManager: KeyManager | null, -): keyManager is KeyManagerWithProvider => - Boolean( - keyManager && - [ - KeyManager.XDefi, - KeyManager.MetaMask, - KeyManager.Phantom, - KeyManager.WalletConnectV2, - KeyManager.Coinbase, - ].includes(keyManager), - ) - -export const removeAccountsAndChainListeners = async () => { - const providers = Object.values(KeyManager).filter(isKeyManagerWithProvider) - const maybeProviders = ( - await Promise.all(providers.map(keyManager => getMaybeProvider(keyManager))) - ).filter(isSome) as BrowserProvider[] - - maybeProviders.forEach(maybeProvider => { - maybeProvider.removeAllListeners('accountsChanged') - maybeProvider.removeAllListeners('chainChanged') - }) -} - -export const getMaybeProvider = async ( - localWalletType: KeyManager | null, -): Promise => { - if (!localWalletType) return null - if (!isKeyManagerWithProvider(localWalletType)) return null - - if (localWalletType === KeyManager.MetaMask) { - return (await detectEthereumProvider()) as MetaMaskLikeProvider - } - if (localWalletType === KeyManager.Phantom) { - return (globalThis as any).phantom?.ethereum as unknown as MetaMaskLikeProvider - } - if (localWalletType === KeyManager.XDefi) { - try { - return globalThis?.xfi?.ethereum as unknown as MetaMaskLikeProvider - } catch (error) { - console.error(error) - throw new Error('walletProvider.xdefi.errors.connectFailure') - } - } - - if (localWalletType === KeyManager.WalletConnectV2) { - // provider is created when getting the wallet in WalletConnectV2Connect pairDevice - return null - } - - return null -} - const reducer = (state: InitialState, action: ActionTypes): InitialState => { switch (action.type) { case WalletActions.SET_ADAPTERS: @@ -210,18 +155,21 @@ const reducer = (state: InitialState, action: ActionTypes): InitialState => { deviceId, meta: { label: meta?.label ?? '', - address: (wallet as MetaMaskHDWallet).ethAddress ?? '', + address: (wallet as MetaMaskMultiChainHDWallet).ethAddress ?? '', }, }, } - case WalletActions.SET_PROVIDER: - return { ...state, provider: action.payload } + case WalletActions.SET_WCV2_PROVIDER: + return { ...state, wcV2Provider: action.payload } case WalletActions.SET_IS_CONNECTED: - return { ...state, isConnected: action.payload } + return { + ...state, + isConnected: action.payload, + } case WalletActions.SET_IS_LOCKED: return { ...state, isLocked: action.payload } case WalletActions.SET_CONNECTOR_TYPE: - return { ...state, modalType: action.payload } + return { ...state, ...action.payload } case WalletActions.SET_INITIAL_ROUTE: return { ...state, initialRoute: action.payload } case WalletActions.SET_PIN_REQUEST_TYPE: @@ -391,26 +339,38 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX // External, exposed state to be consumed with useWallet() const [state, dispatch] = useReducer(reducer, getInitialState()) const isDarkMode = useColorModeValue(false, true) - // Internal state, for memoization purposes only + + // Internal state, for memoization and persistence purposes only const { localWalletType: walletType, localWalletDeviceId, - setLocalWalletTypeAndDeviceId, + rdns, + setLocalWallet, setLocalNativeWalletName, } = useLocalWallet() + const mipdProviders = useMipdProviders() + + const maybeMipdProvider = useMemo(() => { + return mipdProviders.find(provider => provider.info.rdns === (state.modalType ?? rdns)) + }, [mipdProviders, rdns, state.modalType]) + const getAdapter: GetAdapter = useCallback( async (keyManager, index = 0) => { let currentStateAdapters = state.adapters // Check if adapter is already in the state + // Note - for MM (and other mipd providers), we always re-instantiate/set, because the MIPD provider may have changed let adapterInstance = currentStateAdapters[keyManager] - if (!adapterInstance) { + if (!adapterInstance || !!maybeMipdProvider) { // If not, create a new instance of the adapter try { const Adapter = await SUPPORTED_WALLETS[keyManager].adapters[index].loadAdapter() - const keyManagerOptions = getKeyManagerOptions(keyManager, isDarkMode) + const keyManagerOptions = + keyManager === KeyManager.MetaMask && maybeMipdProvider + ? maybeMipdProvider.info.rdns + : getKeyManagerOptions(keyManager, isDarkMode) // @ts-ignore tsc is drunk as well, not narrowing to the specific adapter and its KeyManager options here // eslint is drunk, this isn't a hook // eslint-disable-next-line react-hooks/rules-of-hooks @@ -431,7 +391,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX return adapterInstance }, - [isDarkMode, state.adapters, state.keyring], + [isDarkMode, maybeMipdProvider, state.adapters, state.keyring], ) const disconnect = useCallback(() => { @@ -444,32 +404,6 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX store.dispatch(localWalletSlice.actions.clearLocalWallet()) }, [state.wallet]) - // Register a MetaMask-like (EIP-1193) provider on wallet connect or load - const onProviderChange = useCallback( - async ( - localWalletType: KeyManagerWithProvider | null, - // consuming state.wallet in setProviderEvents below won't cut it because of stale closure references - // so we need to explicitly pass the wallet for which we're setting the provider events - wallet: HDWallet | null, - ): Promise => { - if (!localWalletType) return - try { - const maybeProvider = await getMaybeProvider(localWalletType) - - if (maybeProvider) { - setProviderEvents(maybeProvider, localWalletType, wallet) - dispatch({ type: WalletActions.SET_PROVIDER, payload: maybeProvider }) - return maybeProvider - } - } catch (e) { - if (!isMobile) console.error(e) - } - }, - // avoid being too reactive here and setting too many event listeners with setProviderEvents() - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ) - const load = useCallback(() => { const localWalletType = walletType @@ -487,7 +421,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = mobileAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) } const w = await getWallet(localWalletDeviceId) if (w && w.mnemonic && w.label) { @@ -507,7 +444,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX connectedType: KeyManager.Mobile, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) // Turn off the loading spinner for the wallet button in dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) } else { @@ -529,7 +469,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = nativeAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) } const localNativeWallet = await nativeAdapter?.pairDevice(localWalletDeviceId) @@ -563,7 +506,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = keepKeyAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) // @ts-ignore TODO(gomes): FIXME, most likely borked because of WebUSBKeepKeyAdapter return await keepKeyAdapter.pairDevice(sdk) @@ -594,7 +540,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX connectedType: KeyManager.KeepKey, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) } else { disconnect() } @@ -611,12 +560,13 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = metamaskAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: metamaskAdapter.providerRdns, isMipdProvider: true }, + }) } const localMetaMaskWallet = await metamaskAdapter?.pairDevice() - // Set the provider again on refresh to ensure event handlers are properly set - await onProviderChange(KeyManager.MetaMask, localMetaMaskWallet ?? null) if (localMetaMaskWallet) { const { name, icon } = SUPPORTED_WALLETS[KeyManager.MetaMask] try { @@ -633,7 +583,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX }, }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) } catch (e) { disconnect() } @@ -650,12 +603,13 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = phantomAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) } const localPhantomWallet = await phantomAdapter?.pairDevice() - // Set the provider again on refresh to ensure event handlers are properly set - await onProviderChange(KeyManager.Phantom, localPhantomWallet ?? null) if (localPhantomWallet) { const { name, icon } = SUPPORTED_WALLETS[KeyManager.Phantom] try { @@ -672,7 +626,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX }, }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) } catch (e) { disconnect() } @@ -681,7 +638,6 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX } dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) break - case KeyManager.Coinbase: // Get the adapter again in each switch case to narrow down the adapter type const coinbaseAdapter = await getAdapter(localWalletType) @@ -690,12 +646,13 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = coinbaseAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) } const localCoinbaseWallet = await coinbaseAdapter?.pairDevice() - // Set the provider again on refresh to ensure event handlers are properly set - await onProviderChange(KeyManager.Coinbase, localCoinbaseWallet ?? null) if (localCoinbaseWallet) { const { name, icon } = SUPPORTED_WALLETS[KeyManager.Coinbase] try { @@ -712,45 +669,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX }, }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - } catch (e) { - disconnect() - } - } else { - disconnect() - } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break - case KeyManager.XDefi: - // Get the adapter again in each switch case to narrow down the adapter type - const xdefiAdapter = await getAdapter(localWalletType) - - if (xdefiAdapter) { - currentAdapters[localWalletType] = xdefiAdapter - dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) - // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) - } - - const localXDEFIWallet = await xdefiAdapter?.pairDevice() - // Set the provider again on refresh to ensure event handlers are properly set - await onProviderChange(KeyManager.XDefi, localXDEFIWallet ?? null) - if (localXDEFIWallet) { - const { name, icon } = SUPPORTED_WALLETS[KeyManager.XDefi] - try { - await localXDEFIWallet.initialize() - const deviceId = await localXDEFIWallet.getDeviceID() dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localXDEFIWallet, - name, - icon, - deviceId, - connectedType: KeyManager.XDefi, - }, + type: WalletActions.SET_IS_CONNECTED, + payload: true, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) } catch (e) { disconnect() } @@ -759,6 +681,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX } dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) break + case KeyManager.Keplr: // Get the adapter again in each switch case to narrow down the adapter type const keplrAdapter = await getAdapter(localWalletType) @@ -767,7 +690,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = keplrAdapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) } const localKeplrWallet = await keplrAdapter?.pairDevice() @@ -786,7 +712,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX connectedType: KeyManager.Keplr, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) } catch (e) { disconnect() } @@ -803,12 +732,13 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX currentAdapters[localWalletType] = walletConnectV2Adapter dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: localWalletType, isMipdProvider: false }, + }) } const localWalletConnectWallet = await walletConnectV2Adapter?.pairDevice() - // Re-trigger the modal on refresh - await onProviderChange(KeyManager.WalletConnectV2, localWalletConnectWallet ?? null) if (localWalletConnectWallet) { const { name, icon } = SUPPORTED_WALLETS[KeyManager.WalletConnectV2] try { @@ -825,7 +755,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX }, }) dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) } catch (e) { disconnect() } @@ -848,103 +781,25 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.adapters, state.keyring]) - const handleAccountsOrChainChanged = useCallback( - async (localWalletType: KeyManagerWithProvider | null, accountsOrChains: string[] | string) => { - if (!localWalletType || !state.adapters) return - - // Note, we NEED to use store.getState instead of the walletType variable above - // The reason is handleAccountsOrChainChanged exists in the context of a closure, hence will keep a stale reference forever - const _walletType = selectWalletType(store.getState()) - - // This shouldn't happen if event listeners are properly removed, but they may not be - // This fixes the case of switching from e.g MM, to another wallet, then switching accounts/chains in MM and MM becoming connected again - if (_walletType && localWalletType !== _walletType) return - - const _isLocked = Array.isArray(accountsOrChains) && accountsOrChains.length === 0 - - if (_isLocked) { - dispatch({ type: WalletActions.SET_IS_LOCKED, payload: true }) - } else { - // Either a chain change or a wallet unlock - ensure we set isLocked to false before continuing to avoid bad states - dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - } - - const adapter = await getAdapter(localWalletType) - - // Re-pair - which in case of accounts changed means the user will be prompted to connect their current account if they didn't do so - // Note, this isn't guaranteed to work, not all wallets are the same, some (i.e MM) have this weird flow where connecting to an unconnected account - // from a connected account can only be done from the wallet itself and not programmatically - const localWallet = await adapter?.pairDevice() - - if (!localWallet) return - - await localWallet.initialize() - const deviceId = await localWallet?.getDeviceID() - - if (!deviceId) return - - const { icon, name } = SUPPORTED_WALLETS[localWalletType] - - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localWallet, - name, - icon, - deviceId, - connectedType: localWalletType, - }, - }) - }, - [getAdapter, state.adapters], - ) - - const setProviderEvents = useCallback( - ( - maybeProvider: InitialState['provider'], - localWalletType: KeyManagerWithProvider | null, - // consuming state.wallet in setProviderEvents below won't cut it because of stale closure references - // so we need to explicitly pass the wallet for which we're setting the provider events - wallet: HDWallet | null, - ) => { - if (!(maybeProvider && localWalletType)) return - - maybeProvider?.on?.('accountsChanged', (e: string[]) => { - return handleAccountsOrChainChanged(localWalletType, e) - }) - maybeProvider?.on?.('chainChanged', (e: string) => { - return handleAccountsOrChainChanged(localWalletType, e) - }) - - if (wallet) { - const oldDisconnect = wallet.disconnect.bind(wallet) - const removeEventListeners = () => { - maybeProvider?.removeListener?.('accountsChanged', (e: string[]) => - handleAccountsOrChainChanged(localWalletType, e), - ) - maybeProvider?.removeListener?.('chainChanged', (e: string) => - handleAccountsOrChainChanged(localWalletType, e), - ) - } - - wallet.disconnect = () => { - removeEventListeners() - return oldDisconnect() - } - } - }, - [handleAccountsOrChainChanged], - ) - - const connect = useCallback((type: KeyManager) => { - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: type }) - const routeIndex = findIndex(SUPPORTED_WALLETS[type]?.routes, ({ path }) => + const connect = useCallback((type: KeyManager | string, isMipdProvider: boolean) => { + // TODO(gomes): here we'll probably need to add some isMipdProvider checks too + // and maybe ProviderInfo (name, icon etc) so that the modal can be fully programmatic e2e? + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: isMipdProvider + ? { modalType: type, isMipdProvider } + : { modalType: type as KeyManager, isMipdProvider }, + }) + const supportedWallet = isMipdProvider + ? SUPPORTED_WALLETS[KeyManager.MetaMask] + : SUPPORTED_WALLETS[type as KeyManager] + const routeIndex = findIndex(supportedWallet.routes, ({ path }) => String(path).endsWith('connect'), ) if (routeIndex > -1) { dispatch({ type: WalletActions.SET_INITIAL_ROUTE, - payload: SUPPORTED_WALLETS[type].routes[routeIndex].path as string, + payload: supportedWallet.routes[routeIndex].path as string, }) } }, []) @@ -953,7 +808,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX const { name, icon, adapters } = SUPPORTED_WALLETS[KeyManager.Demo] // For the demo wallet, we use the name, DemoWallet, as the deviceId const deviceId = name - setLocalWalletTypeAndDeviceId(KeyManager.Demo, deviceId) + setLocalWallet({ type: KeyManager.Demo, deviceId }) setLocalNativeWalletName(name) dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: true }) @@ -982,16 +837,22 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX connectedType: KeyManager.Demo, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: false }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: false, + }) } catch (error) { console.error(error) } finally { dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) } - }, [setLocalNativeWalletName, setLocalWalletTypeAndDeviceId, state.keyring]) + }, [setLocalNativeWalletName, setLocalWallet, state.keyring]) const create = useCallback((type: KeyManager) => { - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: type }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: type, isMipdProvider: false }, + }) const routeIndex = findIndex(SUPPORTED_WALLETS[type]?.routes, ({ path }) => String(path).endsWith('create'), ) @@ -1004,7 +865,10 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX }, []) const importWallet = useCallback((type: KeyManager) => { - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: type }) + dispatch({ + type: WalletActions.SET_CONNECTOR_TYPE, + payload: { modalType: type, isMipdProvider: false }, + }) const routeIndex = findIndex(SUPPORTED_WALLETS[type]?.routes, ({ path }) => String(path).endsWith('import'), ) @@ -1030,6 +894,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX useWalletConnectV2EventHandler(state, dispatch) useKeepKeyEventHandler(state, dispatch, load, setDeviceState) useLedgerEventHandler(state, dispatch, load, setDeviceState) + useEip1993EventHandler({ state, getAdapter, dispatch }) const value: IWalletContext = useMemo( () => ({ @@ -1042,7 +907,6 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX disconnect, load, setDeviceState, - onProviderChange, connectDemo, }), [ @@ -1054,7 +918,6 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX disconnect, load, setDeviceState, - onProviderChange, connectDemo, ], ) diff --git a/src/context/WalletProvider/WalletViewsSwitch.tsx b/src/context/WalletProvider/WalletViewsSwitch.tsx index 1782494578f..b500dac2fd0 100644 --- a/src/context/WalletProvider/WalletViewsSwitch.tsx +++ b/src/context/WalletProvider/WalletViewsSwitch.tsx @@ -19,6 +19,7 @@ import { localWalletSlice } from 'state/slices/localWalletSlice/localWalletSlice import { store } from 'state/store' import { SUPPORTED_WALLETS } from './config' +import { KeyManager } from './KeyManager' import { SelectModal } from './SelectModal' import { NativeWalletRoutes } from './types' @@ -105,10 +106,12 @@ export const WalletViewsSwitch = () => { /** * Memoize the routes list to avoid unnecessary re-renders unless the wallet changes */ + const supportedWallet = + SUPPORTED_WALLETS[modalType as KeyManager] || SUPPORTED_WALLETS[KeyManager.MetaMask] const walletRoutesList = useMemo( () => modalType - ? SUPPORTED_WALLETS[modalType].routes.map(route => { + ? supportedWallet.routes.map(route => { const Component = route.component return !Component ? null : ( { ) }) : [], - [modalType], + [modalType, supportedWallet.routes], ) const renderSelectModal = useCallback(() => , []) diff --git a/src/context/WalletProvider/XDEFI/components/Connect.tsx b/src/context/WalletProvider/XDEFI/components/Connect.tsx deleted file mode 100644 index b59454c65a6..00000000000 --- a/src/context/WalletProvider/XDEFI/components/Connect.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type { XDEFIHDWallet } from '@shapeshiftoss/hdwallet-xdefi' -import React, { useCallback, useState } from 'react' -import type { RouteComponentProps } from 'react-router-dom' -import type { ActionTypes } from 'context/WalletProvider/actions' -import { WalletActions } from 'context/WalletProvider/actions' -import { KeyManager } from 'context/WalletProvider/KeyManager' -import { useLocalWallet } from 'context/WalletProvider/local-wallet' -import { removeAccountsAndChainListeners } from 'context/WalletProvider/WalletProvider' -import { useWallet } from 'hooks/useWallet/useWallet' - -import { ConnectModal } from '../../components/ConnectModal' -import type { LocationState } from '../../NativeWallet/types' -import { XDEFIConfig } from '../config' - -export interface XDEFISetupProps - extends RouteComponentProps< - {}, - any, // history - LocationState - > { - dispatch: React.Dispatch -} - -export const XDEFIConnect = ({ history }: XDEFISetupProps) => { - const { setLocalWalletTypeAndDeviceId } = useLocalWallet() - const { dispatch, state, getAdapter, onProviderChange } = useWallet() - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - // eslint-disable-next-line no-sequences - const setErrorLoading = (e: string | null) => (setError(e), setLoading(false)) - - const pairDevice = useCallback(async () => { - setError(null) - setLoading(true) - - const adapter = await getAdapter(KeyManager.XDefi) - if (adapter) { - try { - // Remove all provider event listeners from previously connected wallets - await removeAccountsAndChainListeners() - - const wallet = (await adapter.pairDevice()) as XDEFIHDWallet | undefined - if (!wallet) { - setErrorLoading('walletProvider.errors.walletNotFound') - throw new Error('Call to hdwallet-xdefi::pairDevice returned null or undefined') - } - - await onProviderChange(KeyManager.XDefi, wallet) - - const { name, icon } = XDEFIConfig - - const deviceId = await wallet.getDeviceID() - - if (state.provider !== (globalThis as any).xfi.ethereum) { - throw new Error('walletProvider.xdefi.errors.multipleWallets') - } - - await wallet.initialize() - - dispatch({ - type: WalletActions.SET_WALLET, - payload: { wallet, name, icon, deviceId, connectedType: KeyManager.XDefi }, - }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - setLocalWalletTypeAndDeviceId(KeyManager.XDefi, deviceId) - dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) - } catch (e: any) { - if (e?.message?.startsWith('walletProvider.')) { - console.error(e) - setErrorLoading(e?.message) - } else { - setErrorLoading('walletProvider.xdefi.errors.unknown') - history.push('/xdefi/failure') - // Safely navigate user to website if XDEFI is not found - if (e?.message === 'XDEFI provider not found') { - const newWindow = window.open('https://xdefi.io', '_blank', 'noopener noreferrer') - if (newWindow) newWindow.opener = null - } - } - } - } - setLoading(false) - }, [ - onProviderChange, - getAdapter, - state.provider, - dispatch, - setLocalWalletTypeAndDeviceId, - history, - ]) - - return ( - - ) -} diff --git a/src/context/WalletProvider/XDEFI/components/Failure.tsx b/src/context/WalletProvider/XDEFI/components/Failure.tsx deleted file mode 100644 index 3dd553b64bb..00000000000 --- a/src/context/WalletProvider/XDEFI/components/Failure.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { FailureModal } from 'context/WalletProvider/components/FailureModal' - -export const XDEFIFailure = () => { - return ( - - ) -} diff --git a/src/context/WalletProvider/XDEFI/config.ts b/src/context/WalletProvider/XDEFI/config.ts deleted file mode 100644 index 82a2710f25e..00000000000 --- a/src/context/WalletProvider/XDEFI/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { XDEFIAdapter } from '@shapeshiftoss/hdwallet-xdefi' -import { XDEFIIcon } from 'components/Icons/XDEFIIcon' -import type { SupportedWalletInfo } from 'context/WalletProvider/config' - -type XDEFIConfigType = Omit, 'routes'> - -export const XDEFIConfig: XDEFIConfigType = { - adapters: [ - { - loadAdapter: () => import('@shapeshiftoss/hdwallet-xdefi').then(m => m.XDEFIAdapter), - }, - ], - icon: XDEFIIcon, - name: 'XDEFI', -} diff --git a/src/context/WalletProvider/actions.ts b/src/context/WalletProvider/actions.ts index e78fcbe4958..ca7e5acb850 100644 --- a/src/context/WalletProvider/actions.ts +++ b/src/context/WalletProvider/actions.ts @@ -11,7 +11,7 @@ export enum WalletActions { SET_CONNECTOR_TYPE = 'SET_CONNECTOR_TYPE', SET_INITIAL_ROUTE = 'SET_INITIAL_ROUTE', SET_IS_CONNECTED = 'SET_IS_CONNECTED', - SET_PROVIDER = 'SET_PROVIDER', + SET_WCV2_PROVIDER = 'SET_WCV2_PROVIDER', SET_IS_LOCKED = 'SET_IS_LOCKED', SET_WALLET_MODAL = 'SET_WALLET_MODAL', RESET_STATE = 'RESET_STATE', @@ -40,10 +40,18 @@ export type ActionTypes = connectedType: KeyManager } } - | { type: WalletActions.SET_IS_CONNECTED; payload: boolean } - | { type: WalletActions.SET_PROVIDER; payload: InitialState['provider'] } + | { + type: WalletActions.SET_IS_CONNECTED + payload: boolean + } + | { type: WalletActions.SET_WCV2_PROVIDER; payload: InitialState['wcV2Provider'] } | { type: WalletActions.SET_IS_LOCKED; payload: boolean } - | { type: WalletActions.SET_CONNECTOR_TYPE; payload: KeyManager } + | { + type: WalletActions.SET_CONNECTOR_TYPE + payload: + | { modalType: KeyManager | null; isMipdProvider: false } + | { modalType: string | null; isMipdProvider: true } + } | { type: WalletActions.SET_INITIAL_ROUTE; payload: string } | { type: WalletActions.SET_WALLET_MODAL; payload: boolean } | { type: WalletActions.DOWNLOAD_UPDATER; payload: boolean } diff --git a/src/context/WalletProvider/components/ConnectModal.tsx b/src/context/WalletProvider/components/ConnectModal.tsx index cf4aa451267..84c6169b303 100644 --- a/src/context/WalletProvider/components/ConnectModal.tsx +++ b/src/context/WalletProvider/components/ConnectModal.tsx @@ -7,12 +7,13 @@ import { ModalHeader, Spinner, } from '@chakra-ui/react' +import type { InterpolationOptions } from 'node-polyglot' import type { ReactNode } from 'react' import { Text } from 'components/Text' export type ConnectModalProps = { - headerText: string - bodyText: string + headerText: string | [string, InterpolationOptions] + bodyText: string | [string, InterpolationOptions] buttonText: string onPairDeviceClick: () => void loading: boolean diff --git a/src/context/WalletProvider/components/FailureModal.tsx b/src/context/WalletProvider/components/FailureModal.tsx index 8dcc8d1e28f..3f127ca6f96 100644 --- a/src/context/WalletProvider/components/FailureModal.tsx +++ b/src/context/WalletProvider/components/FailureModal.tsx @@ -1,11 +1,12 @@ import { NotAllowedIcon } from '@chakra-ui/icons' import { ModalBody } from '@chakra-ui/react' +import type { InterpolationOptions } from 'node-polyglot' import type { ReactNode } from 'react' import { Text } from 'components/Text' export type FailureModalProps = { headerText: string - bodyText: string + bodyText: string | [string, InterpolationOptions] children?: ReactNode } diff --git a/src/context/WalletProvider/config.ts b/src/context/WalletProvider/config.ts index c65a2a49531..a6aad14ba47 100644 --- a/src/context/WalletProvider/config.ts +++ b/src/context/WalletProvider/config.ts @@ -4,12 +4,10 @@ import type { CoinbaseAdapter } from '@shapeshiftoss/hdwallet-coinbase' import type { WebUSBKeepKeyAdapter } from '@shapeshiftoss/hdwallet-keepkey-webusb' import type { KeplrAdapter } from '@shapeshiftoss/hdwallet-keplr' import type { WebUSBLedgerAdapter as LedgerAdapter } from '@shapeshiftoss/hdwallet-ledger-webusb' -import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask' +import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { NativeAdapter } from '@shapeshiftoss/hdwallet-native' import type { PhantomAdapter } from '@shapeshiftoss/hdwallet-phantom' -import type { MetaMaskAdapter as MetaMaskMultiChainAdapter } from '@shapeshiftoss/hdwallet-shapeshift-multichain' import type { WalletConnectV2Adapter } from '@shapeshiftoss/hdwallet-walletconnectv2' -import type { XDEFIAdapter } from '@shapeshiftoss/hdwallet-xdefi' import { getConfig } from 'config' import { lazy } from 'react' import type { RouteProps } from 'react-router-dom' @@ -32,7 +30,6 @@ import { KeepKeyRoutes } from './routes' import { NativeWalletRoutes } from './types' import { WalletConnectV2Config } from './WalletConnectV2/config' import type { EthereumProviderOptions } from './WalletConnectV2/constants' -import { XDEFIConfig } from './XDEFI/config' const WalletConnectV2Connect = lazy(() => import('./WalletConnectV2/components/Connect').then(({ WalletConnectV2Connect }) => ({ @@ -45,12 +42,6 @@ const NativeTestPhrase = lazy(() => default: NativeTestPhrase, })), ) -const XDEFIFailure = lazy(() => - import('./XDEFI/components/Failure').then(({ XDEFIFailure }) => ({ default: XDEFIFailure })), -) -const XDEFIConnect = lazy(() => - import('./XDEFI/components/Connect').then(({ XDEFIConnect }) => ({ default: XDEFIConnect })), -) const NativeSuccess = lazy(() => import('./NativeWallet/components/NativeSuccess').then(({ NativeSuccess }) => ({ default: NativeSuccess, @@ -340,11 +331,8 @@ export type SupportedWalletInfoByKeyManager = { [KeyManager.Keplr]: SupportedWalletInfo [KeyManager.Ledger]: SupportedWalletInfo [KeyManager.Phantom]: SupportedWalletInfo - [KeyManager.MetaMask]: SupportedWalletInfo< - typeof MetaMaskAdapter | typeof MetaMaskMultiChainAdapter - > + [KeyManager.MetaMask]: SupportedWalletInfo [KeyManager.WalletConnectV2]: SupportedWalletInfo - [KeyManager.XDefi]: SupportedWalletInfo } export const SUPPORTED_WALLETS: SupportedWalletInfoByKeyManager = { @@ -426,14 +414,6 @@ export const SUPPORTED_WALLETS: SupportedWalletInfoByKeyManager = { { path: '/phantom/failure', component: PhantomFailure }, ], }, - - [KeyManager.XDefi]: { - ...XDEFIConfig, - routes: [ - { path: '/xdefi/connect', component: XDEFIConnect }, - { path: '/xdefi/failure', component: XDEFIFailure }, - ], - }, [KeyManager.Coinbase]: { ...CoinbaseConfig, routes: [ @@ -484,8 +464,6 @@ type GetKeyManagerOptions = (keyManager: KeyManager, isDarkMode: boolean) => Key export const getKeyManagerOptions: GetKeyManagerOptions = (keyManager, isDarkMode) => { switch (keyManager) { - case KeyManager.WalletConnectV2: - return walletConnectV2ProviderConfig case KeyManager.Coinbase: return { appName: 'ShapeShift', @@ -494,6 +472,8 @@ export const getKeyManagerOptions: GetKeyManagerOptions = (keyManager, isDarkMod defaultChainId: 1, darkMode: isDarkMode, } + case KeyManager.WalletConnectV2: + return walletConnectV2ProviderConfig default: return undefined } diff --git a/src/context/WalletProvider/local-wallet.ts b/src/context/WalletProvider/local-wallet.ts index ff77c85ae40..a0667286151 100644 --- a/src/context/WalletProvider/local-wallet.ts +++ b/src/context/WalletProvider/local-wallet.ts @@ -2,6 +2,7 @@ import { localWalletSlice } from 'state/slices/localWalletSlice/localWalletSlice import { selectNativeWalletName, selectWalletDeviceId, + selectWalletRdns, selectWalletType, } from 'state/slices/localWalletSlice/selectors' import { useAppDispatch, useAppSelector } from 'state/store' @@ -11,8 +12,16 @@ import type { KeyManager } from './KeyManager' export const useLocalWallet = () => { const dispatch = useAppDispatch() - const setLocalWalletTypeAndDeviceId = (type: KeyManager, deviceId: string) => { - dispatch(localWalletSlice.actions.setWalletTypeAndDeviceId({ type, deviceId })) + const setLocalWallet = ({ + type, + deviceId, + rdns, + }: { + type: KeyManager + deviceId: string + rdns?: string | null + }) => { + dispatch(localWalletSlice.actions.setLocalWallet({ type, deviceId, rdns: rdns ?? null })) } const clearLocalWallet = () => { @@ -24,13 +33,15 @@ export const useLocalWallet = () => { const nativeLocalWalletName = useAppSelector(selectNativeWalletName) const localWalletType = useAppSelector(selectWalletType) const localWalletDeviceId = useAppSelector(selectWalletDeviceId) + const rdns = useAppSelector(selectWalletRdns) return { - setLocalWalletTypeAndDeviceId, + setLocalWallet, clearLocalWallet, setLocalNativeWalletName, nativeLocalWalletName, localWalletType, localWalletDeviceId, + rdns, } } diff --git a/src/context/WalletProvider/types.ts b/src/context/WalletProvider/types.ts index 21e6c5dd413..4656f390fee 100644 --- a/src/context/WalletProvider/types.ts +++ b/src/context/WalletProvider/types.ts @@ -3,11 +3,10 @@ import type { CoinbaseAdapter } from '@shapeshiftoss/hdwallet-coinbase' import type { WebUSBKeepKeyAdapter } from '@shapeshiftoss/hdwallet-keepkey-webusb' import type { KeplrAdapter } from '@shapeshiftoss/hdwallet-keplr' import type { WebUSBLedgerAdapter } from '@shapeshiftoss/hdwallet-ledger-webusb' -import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask' +import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { NativeAdapter } from '@shapeshiftoss/hdwallet-native' import type { PhantomAdapter } from '@shapeshiftoss/hdwallet-phantom' import type { WalletConnectV2Adapter } from '@shapeshiftoss/hdwallet-walletconnectv2' -import type { XDEFIAdapter } from '@shapeshiftoss/hdwallet-xdefi' import type { KeyManager } from './KeyManager' @@ -22,7 +21,6 @@ export type AdaptersByKeyManager = { [KeyManager.MetaMask]: MetaMaskAdapter [KeyManager.Phantom]: PhantomAdapter [KeyManager.Coinbase]: CoinbaseAdapter - [KeyManager.XDefi]: XDEFIAdapter } export enum NativeWalletRoutes { diff --git a/src/context/WalletProvider/useEip1993EventHandler.ts b/src/context/WalletProvider/useEip1993EventHandler.ts new file mode 100644 index 00000000000..699fd945525 --- /dev/null +++ b/src/context/WalletProvider/useEip1993EventHandler.ts @@ -0,0 +1,131 @@ +import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' +import type { PhantomAdapter } from '@shapeshiftoss/hdwallet-phantom' +import { PhantomHDWallet } from '@shapeshiftoss/hdwallet-phantom' +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { isMobile } from 'react-device-detect' +import { useMipdProviders } from 'lib/mipd' +import { selectWalletType } from 'state/slices/localWalletSlice/selectors' +import { store } from 'state/store' + +import { WalletActions } from './actions' +import { SUPPORTED_WALLETS } from './config' +import { KeyManager } from './KeyManager' +import { useLocalWallet } from './local-wallet' +import type { IWalletContext } from './WalletContext' + +export const useEip1993EventHandler = ({ + state, + getAdapter, + dispatch, +}: Pick) => { + const { rdns: _rdns, localWalletType } = useLocalWallet() + const mipdProviders = useMipdProviders() + const rdns = useMemo(() => { + // Uses rdns magic to detect provider for our first-class wallets + if (localWalletType === KeyManager.Phantom) return 'app.phantom' + if (localWalletType === KeyManager.Coinbase) return 'com.coinbase.wallet' + return _rdns + }, [_rdns, localWalletType]) + const maybeMipdProvider = mipdProviders.find(provider => provider.info.rdns === rdns) + + const currentRdnsRef = useRef(rdns) + const providerRef = useRef(maybeMipdProvider) + + useEffect(() => { + currentRdnsRef.current = rdns + providerRef.current = maybeMipdProvider + }, [maybeMipdProvider, maybeMipdProvider?.provider, rdns]) + + const handleAccountsOrChainChanged = useCallback( + async (accountsOrChains: string[] | string) => { + if (!providerRef.current || !state.adapters) return + if ( + ![KeyManager.MetaMask, KeyManager.Phantom, KeyManager.Coinbase].includes( + localWalletType as KeyManager, + ) + ) + return + // Never ever under any circumstances remove me. We attach event handlers to EIP-1993 providers, + // and trying to detach them as we did previously is a guaranteed spaghetti code disaster and a recipe for bugs. + // This ensures that we do *not* try to run this fn with stale event listeners from previously connected EIP-1993 wallets. + if (providerRef.current.info.rdns !== currentRdnsRef.current) return + + // Note, we NEED to use store.getState instead of the walletType variable above + // The reason is handleAccountsOrChainChanged exists in the context of a closure, hence will keep a stale reference forever + const _walletType = selectWalletType(store.getState()) + + // This shouldn't happen if event listeners are properly removed, but they may not be + // This fixes the case of switching from e.g MM, to another wallet, then switching accounts/chains in MM and MM becoming connected again + if (_walletType && localWalletType !== _walletType) return + if (!localWalletType) return + + const _isLocked = Array.isArray(accountsOrChains) && accountsOrChains.length === 0 + + if (_isLocked) { + dispatch({ type: WalletActions.SET_IS_LOCKED, payload: true }) + } else { + // Either a chain change or a wallet unlock - ensure we set isLocked to false before continuing to avoid bad states + dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) + } + + const adapter = (await getAdapter(localWalletType)) as MetaMaskAdapter | PhantomAdapter | null + + // Re-pair - which in case of accounts changed means the user will be prompted to connect their current account if they didn't do so + // Note, this isn't guaranteed to work, not all wallets are the same, some (i.e MM) have this weird flow where connecting to an unconnected account + // from a connected account can only be done from the wallet itself and not programmatically + const localWallet = await adapter?.pairDevice() + + if (!localWallet) return + + await localWallet.initialize() + const deviceId = await localWallet?.getDeviceID() + + if (!deviceId) return + + const { icon, name } = SUPPORTED_WALLETS[localWalletType] + + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: localWallet, + name, + icon, + deviceId, + connectedType: localWalletType, + }, + }) + }, + [dispatch, getAdapter, localWalletType, state.adapters], + ) + + const setProviderEvents = useCallback(() => { + // Always remove before setting + providerRef.current?.provider.removeListener?.('accountsChanged', handleAccountsOrChainChanged) + providerRef.current?.provider.removeListener?.('chainChanged', handleAccountsOrChainChanged) + + providerRef.current?.provider.on?.('accountsChanged', handleAccountsOrChainChanged) + providerRef.current?.provider.on?.('chainChanged', handleAccountsOrChainChanged) + }, [handleAccountsOrChainChanged]) + + // Register a MetaMask-like (EIP-1193) provider's event handlers on mipd provider change + useEffect(() => { + const isMetaMaskMultichainWallet = state.wallet instanceof MetaMaskMultiChainHDWallet + const isPhantomHdWallet = state.wallet instanceof PhantomHDWallet + if (!(isMetaMaskMultichainWallet || isPhantomHdWallet)) return + if (!providerRef.current) return + try { + setProviderEvents() + } catch (e) { + if (!isMobile) console.error(e) + } + + return () => { + providerRef.current?.provider.removeListener?.( + 'accountsChanged', + handleAccountsOrChainChanged, + ) + providerRef.current?.provider.removeListener?.('chainChanged', handleAccountsOrChainChanged) + } + }, [handleAccountsOrChainChanged, maybeMipdProvider, setProviderEvents, state.wallet]) +} diff --git a/src/hooks/useIsSnapInstalled/useIsSnapInstalled.tsx b/src/hooks/useIsSnapInstalled/useIsSnapInstalled.tsx index 5baf2b8a43c..815424b6c02 100644 --- a/src/hooks/useIsSnapInstalled/useIsSnapInstalled.tsx +++ b/src/hooks/useIsSnapInstalled/useIsSnapInstalled.tsx @@ -1,20 +1,18 @@ -import detectEthereumProvider from '@metamask/detect-provider' import type { ChainId } from '@shapeshiftoss/caip' import { CHAIN_NAMESPACE, fromChainId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import type { HDWallet } from '@shapeshiftoss/hdwallet-core' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' -import { shapeShiftSnapInstalled } from '@shapeshiftoss/metamask-snaps-adapter' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import { getConfig } from 'config' -import type { Eip1193Provider } from 'ethers' import pDebounce from 'p-debounce' -import pMemoize from 'p-memoize' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { getSnapVersion } from 'utils/snaps' import { useWallet } from 'hooks/useWallet/useWallet' +import { METAMASK_RDNS } from 'lib/mipd' +import { selectWalletRdns } from 'state/slices/localWalletSlice/selectors' +import { useAppSelector } from 'state/store' const POLL_INTERVAL = 3000 // tune me to make this "feel" right -const snapId = getConfig().REACT_APP_SNAP_ID const snapVersion = getConfig().REACT_APP_SNAP_VERSION // Many many user-agents to detect mobile MM and other in-app dApp browsers @@ -24,111 +22,20 @@ const isBrowser = () => typeof window !== 'undefined' export const checkIsMetaMaskMobileWebView = () => isBrowser() && /(MetaMaskMobile)/i.test(window.navigator.userAgent ?? '') -// https://github.com/wevm/wagmi/blob/21245be51d7c6dff1c7b285226d0c89c4a9d8cac/packages/connectors/src/utils/getInjectedName.ts#L6-L56 -// This will need to be kept up-to-date with the latest list of impersonators -const METAMASK_IMPERSONATORS = [ - 'isBraveWallet', - 'isTokenary', - 'isFrame', - 'isLiquality', - 'isOpera', - 'isTally', - 'isStatus', - 'isXDEFI', - 'isNifty', - 'isRonin', - 'isBinance', - 'isCoinbase', - 'isExodus', - 'isPhantom', - 'isGlow', - 'isOneInch', - 'isRabby', - 'isTrezor', - 'isLedger', - 'isKeystone', - 'isBitBox', - 'isGridPlus', - 'isJade', - 'isPortis', - 'isFortmatic', - 'isTorus', - 'isAuthereum', - 'isWalletLink', - 'isWalletConnect', - 'isDapper', - 'isBitski', - 'isVenly', - 'isSequence', - 'isGamestop', - 'isZerion', - 'isDeBank', - 'isKukai', - 'isTemple', - 'isSpire', - 'isWallet', - 'isCore', - 'isAnchor', - 'isWombat', - 'isMathWallet', - 'isMeetone', - 'isHyperPay', - 'isTokenPocket', - 'isBitpie', - 'isAToken', - 'isOwnbit', - 'isHbWallet', - 'isMYKEY', - 'isHuobiWallet', - 'isEidoo', - 'isTrust', - 'isImToken', - 'isONTO', - 'isSafePal', - 'isCoin98', - 'isVision', -] - export const checkIsSnapInstalled = pDebounce.promise( - (): Promise => shapeShiftSnapInstalled(snapId), + (): Promise => getSnapVersion().then(Boolean), ) -export const checkIsMetaMaskDesktop = pMemoize( - async (wallet: HDWallet | null): Promise => { - const isMetaMaskMobileWebView = checkIsMetaMaskMobileWebView() - if (isMetaMaskMobileWebView) return false - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet - // We don't want to run this hook altogether if using any wallet other than MM - if (!isMetaMaskMultichainWallet) return false - - const provider = (await detectEthereumProvider()) as Eip1193Provider - // MetaMask impersonators don't support the methods we need to check for snap installation, and will throw - // `as any` because isMetaMask is gone from the providers in ethers v6 - if (!(provider as any).isMetaMask) return false - - return true - }, - { - cacheKey: ([_wallet]) => (_wallet as MetaMaskShapeShiftMultiChainHDWallet | null)?._isMetaMask, - }, -) +export const checkIsMetaMaskDesktop = (wallet: HDWallet | null): boolean => { + const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet + // We don't want to run this hook altogether if using any wallet other than MM + if (!isMetaMaskMultichainWallet) return false + if (wallet.providerRdns !== METAMASK_RDNS) return false + const isMetaMaskMobileWebView = checkIsMetaMaskMobileWebView() + if (isMetaMaskMobileWebView) return false -export const checkIsMetaMaskImpersonator = pMemoize( - async (wallet: HDWallet | null): Promise => { - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet - // We don't want to run this hook altogether if using any wallet other than MM - if (!isMetaMaskMultichainWallet) return false - - const provider = (await detectEthereumProvider()) as Eip1193Provider - // Some impersonators really like to make it difficult for us to detect *actual* MetaMask - // Note, checking for the truthiness of the value isn't enough - some impersonators have the key present but undefined - // This is weird, but welcome to the world of web3 - return METAMASK_IMPERSONATORS.some(impersonator => impersonator in provider) - }, - { - cacheKey: ([_wallet]) => (_wallet as MetaMaskShapeShiftMultiChainHDWallet | null)?._isMetaMask, - }, -) + return true +} export const useIsSnapInstalled = (): { isSnapInstalled: boolean | null @@ -141,11 +48,12 @@ export const useIsSnapInstalled = (): { state: { wallet, isConnected, isDemoWallet }, } = useWallet() + const connectedRdns = useAppSelector(selectWalletRdns) + const checkSnapInstallation = useCallback(async () => { - if (!isConnected || isDemoWallet) return - const isMetaMaskDesktop = await checkIsMetaMaskDesktop(wallet) - const isMetaMaskImpersonator = await checkIsMetaMaskImpersonator(wallet) - if (isMetaMaskImpersonator) return + if (connectedRdns !== METAMASK_RDNS) return setIsSnapInstalled(null) + if (!isConnected || isDemoWallet) return setIsSnapInstalled(null) + const isMetaMaskDesktop = checkIsMetaMaskDesktop(wallet) if (!isMetaMaskDesktop) return const version = await getSnapVersion() @@ -153,7 +61,7 @@ export const useIsSnapInstalled = (): { setIsCorrectVersion(version === snapVersion) setIsSnapInstalled(_isSnapInstalled) - }, [isConnected, isDemoWallet, wallet]) + }, [connectedRdns, isConnected, isDemoWallet, wallet]) useEffect(() => { // Call the function immediately @@ -166,7 +74,12 @@ export const useIsSnapInstalled = (): { return () => clearInterval(intervalId) }, [checkSnapInstallation, wallet]) - return { isSnapInstalled, isCorrectVersion } + const data = useMemo( + () => ({ isSnapInstalled, isCorrectVersion }), + [isSnapInstalled, isCorrectVersion], + ) + + return data } export const canAddMetaMaskAccount = ({ @@ -180,9 +93,9 @@ export const canAddMetaMaskAccount = ({ wallet: HDWallet isSnapInstalled: boolean }) => { - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet + const isMetaMaskMultichainHdWallet = wallet instanceof MetaMaskMultiChainHDWallet - if (!isMetaMaskMultichainWallet) + if (!isMetaMaskMultichainHdWallet) throw new Error( 'canAddMetaMaskAccount should only be called in the context of a MetaMask adapter', ) diff --git a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts index d9a3b378c26..9c63699bb7e 100644 --- a/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts +++ b/src/hooks/useWalletSupportsChain/useWalletSupportsChain.ts @@ -32,10 +32,11 @@ import { supportsPolygon, supportsThorchain, } from '@shapeshiftoss/hdwallet-core' +import { isMetaMask } from '@shapeshiftoss/hdwallet-metamask-multichain' import { PhantomHDWallet } from '@shapeshiftoss/hdwallet-phantom' -import { isMetaMask } from '@shapeshiftoss/hdwallet-shapeshift-multichain' import { useMemo } from 'react' import { useIsSnapInstalled } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' +import { METAMASK_RDNS } from 'lib/mipd' import { selectAccountIdsByChainIdFilter } from 'state/slices/portfolioSlice/selectors' import { selectFeatureFlag } from 'state/slices/selectors' import { store, useAppSelector } from 'state/store' @@ -64,7 +65,14 @@ export const walletSupportsChain = ({ // e.g MM without snaps installed const hasRuntimeSupport = (() => { // Non-EVM ChainIds are only supported with the MM multichain snap installed - if (isMetaMask(wallet) && !isSnapInstalled && !isEvmChainId(chainId)) return false + if ( + isMetaMask(wallet) && + // snap installation checks may take a render or two too many to kick in after switching from MM with snaps to another mipd wallet + // however, we get a new wallet ref instantly, so this ensures we don't wrongly derive non-EVM accounts for another EIP1193 wallet + (!isSnapInstalled || wallet.providerRdns !== METAMASK_RDNS) && + !isEvmChainId(chainId) + ) + return false // We are now sure we have runtime support for the chain. // This is either a Ledger with supported chain account ids, a MM wallet with snaps installed, or @@ -118,11 +126,9 @@ export const useWalletSupportsChain = ( chainId: ChainId, wallet: HDWallet | null, ): boolean | null => { - // We might be in a state where the wallet adapter is MetaMaskShapeShiftMultiChainHDWallet, but the actual underlying wallet - // doesn't have multichain capabilities since snaps isn't installed - // This should obviously belong at hdwallet-core, and feature detection should be made async, with hdwallet-shapeshift-multichain able to do feature detection - // programatically depending on whether the snaps is installed or not, but in the meantime, this will make things happy - // If this evaluates to false, the wallet feature detection will be short circuit in supportsBTC, supportsCosmos and supports Thorchain methods + // MetaMaskMultiChainHDWallet is the reference EIP-1193 JavaScript provider implementation, but also includes snaps support hardcoded in feature capabilities + // However we might be in a state where the wallet adapter is MetaMaskMultiChainHDWallet, but the actual underlying wallet + // doesn't have multichain capabilities since snaps isn't installed/the connected wallet isn't *actual* MM const { isSnapInstalled } = useIsSnapInstalled() const chainAccountIdsFilter = useMemo(() => ({ chainId }), [chainId]) diff --git a/src/lib/account/cosmosSdk.ts b/src/lib/account/cosmosSdk.ts index a9b2af2ae60..6d402e1448b 100644 --- a/src/lib/account/cosmosSdk.ts +++ b/src/lib/account/cosmosSdk.ts @@ -1,6 +1,6 @@ import { CHAIN_REFERENCE, fromChainId, toAccountId } from '@shapeshiftoss/caip' import { supportsCosmos, supportsThorchain } from '@shapeshiftoss/hdwallet-core' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { AccountMetadataById } from '@shapeshiftoss/types' import { canAddMetaMaskAccount } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { assertGetCosmosSdkChainAdapter } from 'lib/utils/cosmosSdk' @@ -21,7 +21,7 @@ export const deriveCosmosSdkAccountIdsAndMetadata: DeriveAccountIdsAndMetadata = if (!supportsThorchain(wallet)) continue } if ( - wallet instanceof MetaMaskShapeShiftMultiChainHDWallet && + wallet instanceof MetaMaskMultiChainHDWallet && !canAddMetaMaskAccount({ accountNumber, chainId, wallet, isSnapInstalled }) ) { continue diff --git a/src/lib/account/evm.ts b/src/lib/account/evm.ts index a11d17c7a0d..f8ea6b2cde5 100644 --- a/src/lib/account/evm.ts +++ b/src/lib/account/evm.ts @@ -23,7 +23,7 @@ import { supportsOptimism, supportsPolygon, } from '@shapeshiftoss/hdwallet-core' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import type { AccountMetadataById } from '@shapeshiftoss/types' import { canAddMetaMaskAccount } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { isSmartContractAddress } from 'lib/address/utils' @@ -48,7 +48,7 @@ export const deriveEvmAccountIdsAndMetadata: DeriveAccountIdsAndMetadata = async if (chainId === arbitrumNovaChainId && !supportsArbitrumNova(wallet)) continue if (chainId === baseChainId && !supportsBase(wallet)) continue if ( - wallet instanceof MetaMaskShapeShiftMultiChainHDWallet && + wallet instanceof MetaMaskMultiChainHDWallet && !canAddMetaMaskAccount({ accountNumber, chainId, wallet, isSnapInstalled }) ) { continue diff --git a/src/lib/account/utxo.ts b/src/lib/account/utxo.ts index b35357eafc8..68a1f4dd76f 100644 --- a/src/lib/account/utxo.ts +++ b/src/lib/account/utxo.ts @@ -1,8 +1,8 @@ import { toAccountId } from '@shapeshiftoss/caip' import { utxoAccountParams, utxoChainIds } from '@shapeshiftoss/chain-adapters' import { supportsBTC } from '@shapeshiftoss/hdwallet-core' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import { PhantomHDWallet } from '@shapeshiftoss/hdwallet-phantom' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' import type { AccountMetadataById, UtxoChainId } from '@shapeshiftoss/types' import { UtxoAccountType } from '@shapeshiftoss/types' import { assertGetUtxoChainAdapter } from 'lib/utils/utxo' @@ -20,7 +20,7 @@ export const deriveUtxoAccountIdsAndMetadata: DeriveAccountIdsAndMetadata = asyn const adapter = assertGetUtxoChainAdapter(chainId) let supportedAccountTypes = adapter.getSupportedAccountTypes() - if (wallet instanceof MetaMaskShapeShiftMultiChainHDWallet) { + if (wallet instanceof MetaMaskMultiChainHDWallet) { // MetaMask snaps adapter only supports legacy for BTC and LTC supportedAccountTypes = [UtxoAccountType.P2pkh] } diff --git a/src/lib/mipd.ts b/src/lib/mipd.ts new file mode 100644 index 00000000000..d4bdeebf78f --- /dev/null +++ b/src/lib/mipd.ts @@ -0,0 +1,45 @@ +import type { EIP6963ProviderDetail } from 'mipd' +import { createStore } from 'mipd' +import { useSyncExternalStore } from 'react' + +export const mipdStore = createStore() + +export const useMipdProviders = () => + useSyncExternalStore(mipdStore.subscribe, mipdStore.getProviders) + +export const METAMASK_RDNS = 'io.metamask' + +// Static MIPD providers - we *always* want to display these +// Note these are partial and missing provider, so you *can* see them even if uninstalled, but will be overriden with detected ones if installed +export const staticMipdProviders = [ + { + supportsMobileBrowser: true, + provider: undefined, + info: { + uuid: '3fae44d7-3daa-479c-b5bf-c54a55aede67', + name: 'MetaMask', + icon: 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjMzIiB2aWV3Qm94PSIwIDAgMzUgMzMiIHdpZHRoPSIzNSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iLjI1Ij48cGF0aCBkPSJtMzIuOTU4MiAxLTEzLjEzNDEgOS43MTgzIDIuNDQyNC01LjcyNzMxeiIgZmlsbD0iI2UxNzcyNiIgc3Ryb2tlPSIjZTE3NzI2Ii8+PGcgZmlsbD0iI2UyNzYyNSIgc3Ryb2tlPSIjZTI3NjI1Ij48cGF0aCBkPSJtMi42NjI5NiAxIDEzLjAxNzE0IDkuODA5LTIuMzI1NC01LjgxODAyeiIvPjxwYXRoIGQ9Im0yOC4yMjk1IDIzLjUzMzUtMy40OTQ3IDUuMzM4NiA3LjQ4MjkgMi4wNjAzIDIuMTQzNi03LjI4MjN6Ii8+PHBhdGggZD0ibTEuMjcyODEgMjMuNjUwMSAyLjEzMDU1IDcuMjgyMyA3LjQ2OTk0LTIuMDYwMy0zLjQ4MTY2LTUuMzM4NnoiLz48cGF0aCBkPSJtMTAuNDcwNiAxNC41MTQ5LTIuMDc4NiAzLjEzNTggNy40MDUuMzM2OS0uMjQ2OS03Ljk2OXoiLz48cGF0aCBkPSJtMjUuMTUwNSAxNC41MTQ5LTUuMTU3NS00LjU4NzA0LS4xNjg4IDguMDU5NzQgNy40MDQ5LS4zMzY5eiIvPjxwYXRoIGQ9Im0xMC44NzMzIDI4Ljg3MjEgNC40ODE5LTIuMTYzOS0zLjg1ODMtMy4wMDYyeiIvPjxwYXRoIGQ9Im0yMC4yNjU5IDI2LjcwODIgNC40Njg5IDIuMTYzOS0uNjEwNS01LjE3MDF6Ii8+PC9nPjxwYXRoIGQ9Im0yNC43MzQ4IDI4Ljg3MjEtNC40NjktMi4xNjM5LjM2MzggMi45MDI1LS4wMzkgMS4yMzF6IiBmaWxsPSIjZDViZmIyIiBzdHJva2U9IiNkNWJmYjIiLz48cGF0aCBkPSJtMTAuODczMiAyOC44NzIxIDQuMTU3MiAxLjk2OTYtLjAyNi0xLjIzMS4zNTA4LTIuOTAyNXoiIGZpbGw9IiNkNWJmYjIiIHN0cm9rZT0iI2Q1YmZiMiIvPjxwYXRoIGQ9Im0xNS4xMDg0IDIxLjc4NDItMy43MTU1LTEuMDg4NCAyLjYyNDMtMS4yMDUxeiIgZmlsbD0iIzIzMzQ0NyIgc3Ryb2tlPSIjMjMzNDQ3Ii8+PHBhdGggZD0ibTIwLjUxMjYgMjEuNzg0MiAxLjA5MTMtMi4yOTM1IDIuNjM3MiAxLjIwNTF6IiBmaWxsPSIjMjMzNDQ3IiBzdHJva2U9IiMyMzM0NDciLz48cGF0aCBkPSJtMTAuODczMyAyOC44NzIxLjY0OTUtNS4zMzg2LTQuMTMxMTcuMTE2N3oiIGZpbGw9IiNjYzYyMjgiIHN0cm9rZT0iI2NjNjIyOCIvPjxwYXRoIGQ9Im0yNC4wOTgyIDIzLjUzMzUuNjM2NiA1LjMzODYgMy40OTQ2LTUuMjIxOXoiIGZpbGw9IiNjYzYyMjgiIHN0cm9rZT0iI2NjNjIyOCIvPjxwYXRoIGQ9Im0yNy4yMjkxIDE3LjY1MDctNy40MDUuMzM2OS42ODg1IDMuNzk2NiAxLjA5MTMtMi4yOTM1IDIuNjM3MiAxLjIwNTF6IiBmaWxsPSIjY2M2MjI4IiBzdHJva2U9IiNjYzYyMjgiLz48cGF0aCBkPSJtMTEuMzkyOSAyMC42OTU4IDIuNjI0Mi0xLjIwNTEgMS4wOTEzIDIuMjkzNS42ODg1LTMuNzk2Ni03LjQwNDk1LS4zMzY5eiIgZmlsbD0iI2NjNjIyOCIgc3Ryb2tlPSIjY2M2MjI4Ii8+PHBhdGggZD0ibTguMzkyIDE3LjY1MDcgMy4xMDQ5IDYuMDUxMy0uMTAzOS0zLjAwNjJ6IiBmaWxsPSIjZTI3NTI1IiBzdHJva2U9IiNlMjc1MjUiLz48cGF0aCBkPSJtMjQuMjQxMiAyMC42OTU4LS4xMTY5IDMuMDA2MiAzLjEwNDktNi4wNTEzeiIgZmlsbD0iI2UyNzUyNSIgc3Ryb2tlPSIjZTI3NTI1Ii8+PHBhdGggZD0ibTE1Ljc5NyAxNy45ODc2LS42ODg2IDMuNzk2Ny44NzA0IDQuNDgzMy4xOTQ5LTUuOTA4N3oiIGZpbGw9IiNlMjc1MjUiIHN0cm9rZT0iI2UyNzUyNSIvPjxwYXRoIGQ9Im0xOS44MjQyIDE3Ljk4NzYtLjM2MzggMi4zNTg0LjE4MTkgNS45MjE2Ljg3MDQtNC40ODMzeiIgZmlsbD0iI2UyNzUyNSIgc3Ryb2tlPSIjZTI3NTI1Ii8+PHBhdGggZD0ibTIwLjUxMjcgMjEuNzg0Mi0uODcwNCA0LjQ4MzQuNjIzNi40NDA2IDMuODU4NC0zLjAwNjIuMTE2OS0zLjAwNjJ6IiBmaWxsPSIjZjU4NDFmIiBzdHJva2U9IiNmNTg0MWYiLz48cGF0aCBkPSJtMTEuMzkyOSAyMC42OTU4LjEwNCAzLjAwNjIgMy44NTgzIDMuMDA2Mi42MjM2LS40NDA2LS44NzA0LTQuNDgzNHoiIGZpbGw9IiNmNTg0MWYiIHN0cm9rZT0iI2Y1ODQxZiIvPjxwYXRoIGQ9Im0yMC41OTA2IDMwLjg0MTcuMDM5LTEuMjMxLS4zMzc4LS4yODUxaC00Ljk2MjZsLS4zMjQ4LjI4NTEuMDI2IDEuMjMxLTQuMTU3Mi0xLjk2OTYgMS40NTUxIDEuMTkyMSAyLjk0ODkgMi4wMzQ0aDUuMDUzNmwyLjk2Mi0yLjAzNDQgMS40NDItMS4xOTIxeiIgZmlsbD0iI2MwYWM5ZCIgc3Ryb2tlPSIjYzBhYzlkIi8+PHBhdGggZD0ibTIwLjI2NTkgMjYuNzA4Mi0uNjIzNi0uNDQwNmgtMy42NjM1bC0uNjIzNi40NDA2LS4zNTA4IDIuOTAyNS4zMjQ4LS4yODUxaDQuOTYyNmwuMzM3OC4yODUxeiIgZmlsbD0iIzE2MTYxNiIgc3Ryb2tlPSIjMTYxNjE2Ii8+PHBhdGggZD0ibTMzLjUxNjggMTEuMzUzMiAxLjEwNDMtNS4zNjQ0Ny0xLjY2MjktNC45ODg3My0xMi42OTIzIDkuMzk0NCA0Ljg4NDYgNC4xMjA1IDYuODk4MyAyLjAwODUgMS41Mi0xLjc3NTItLjY2MjYtLjQ3OTUgMS4wNTIzLS45NTg4LS44MDU0LS42MjIgMS4wNTIzLS44MDM0eiIgZmlsbD0iIzc2M2UxYSIgc3Ryb2tlPSIjNzYzZTFhIi8+PHBhdGggZD0ibTEgNS45ODg3MyAxLjExNzI0IDUuMzY0NDctLjcxNDUxLjUzMTMgMS4wNjUyNy44MDM0LS44MDU0NS42MjIgMS4wNTIyOC45NTg4LS42NjI1NS40Nzk1IDEuNTE5OTcgMS43NzUyIDYuODk4MzUtMi4wMDg1IDQuODg0Ni00LjEyMDUtMTIuNjkyMzMtOS4zOTQ0eiIgZmlsbD0iIzc2M2UxYSIgc3Ryb2tlPSIjNzYzZTFhIi8+PHBhdGggZD0ibTMyLjA0ODkgMTYuNTIzNC02Ljg5ODMtMi4wMDg1IDIuMDc4NiAzLjEzNTgtMy4xMDQ5IDYuMDUxMyA0LjEwNTItLjA1MTloNi4xMzE4eiIgZmlsbD0iI2Y1ODQxZiIgc3Ryb2tlPSIjZjU4NDFmIi8+PHBhdGggZD0ibTEwLjQ3MDUgMTQuNTE0OS02Ljg5ODI4IDIuMDA4NS0yLjI5OTQ0IDcuMTI2N2g2LjExODgzbDQuMTA1MTkuMDUxOS0zLjEwNDg3LTYuMDUxM3oiIGZpbGw9IiNmNTg0MWYiIHN0cm9rZT0iI2Y1ODQxZiIvPjxwYXRoIGQ9Im0xOS44MjQxIDE3Ljk4NzYuNDQxNy03LjU5MzIgMi4wMDA3LTUuNDAzNGgtOC45MTE5bDIuMDAwNiA1LjQwMzQuNDQxNyA3LjU5MzIuMTY4OSAyLjM4NDIuMDEzIDUuODk1OGgzLjY2MzVsLjAxMy01Ljg5NTh6IiBmaWxsPSIjZjU4NDFmIiBzdHJva2U9IiNmNTg0MWYiLz48L2c+PC9zdmc+', + rdns: 'io.metamask', + }, + }, + { + supportsMobileBrowser: false, + provider: undefined, + info: { + uuid: 'cc12b1b5-58a7-4df4-b12d-dd01fca161ce', + name: 'Rabby Wallet', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzc0MV8yNzUxKSI+CjxtYXNrIGlkPSJtYXNrMF83NDFfMjc1MSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjAiIHk9IjAiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+CjxwYXRoIGQ9Ik0zMiAxNkMzMiA3LjE2MzQ0IDI0LjgzNjYgMCAxNiAwQzcuMTYzNDQgMCAwIDcuMTYzNDQgMCAxNkMwIDI0LjgzNjYgNy4xNjM0NCAzMiAxNiAzMkMyNC44MzY2IDMyIDMyIDI0LjgzNjYgMzIgMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8L21hc2s+CjxnIG1hc2s9InVybCgjbWFzazBfNzQxXzI3NTEpIj4KPHBhdGggZD0iTTMyIDE2QzMyIDcuMTYzNDQgMjQuODM2NiAwIDE2IDBDNy4xNjM0NCAwIDAgNy4xNjM0NCAwIDE2QzAgMjQuODM2NiA3LjE2MzQ0IDMyIDE2IDMyQzI0LjgzNjYgMzIgMzIgMjQuODM2NiAzMiAxNloiIGZpbGw9IiM3MDg0RkYiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjBfZF83NDFfMjc1MSkiPgo8cGF0aCBkPSJNMjcuNjAxOSAxNy4zODc2QzI4LjUyMTYgMTUuMzI2MSAyMy45NzQ4IDkuNTY2MzIgMTkuNjMxIDcuMTY2NzZDMTYuODkyOSA1LjMwNzc5IDE0LjAzOTkgNS41NjMxOCAxMy40NjIgNi4zNzkzOEMxMi4xOTQgOC4xNzA2OSAxNy42NjExIDkuNjg4NTEgMjEuMzE3NCAxMS40NTk3QzIwLjUzMTQgMTEuODAyMiAxOS43OTA4IDEyLjQxNjkgMTkuMzU1MiAxMy4yMDI5QzE3Ljk5MjEgMTEuNzA5OCAxNS4wMDAzIDEwLjQyMzkgMTEuNDg5NyAxMS40NTk3QzkuMTIzOTcgMTIuMTU3NyA3LjE1NzkxIDEzLjgwMzIgNi4zOTgwNCAxNi4yODg1QzYuMjEzMzcgMTYuMjA2MiA2LjAwODk0IDE2LjE2MDQgNS43OTM4NyAxNi4xNjA0QzQuOTcxNDIgMTYuMTYwNCA0LjMwNDY5IDE2LjgyOTQgNC4zMDQ2OSAxNy42NTQ2QzQuMzA0NjkgMTguNDc5OSA0Ljk3MTQyIDE5LjE0ODggNS43OTM4NyAxOS4xNDg4QzUuOTQ2MzIgMTkuMTQ4OCA2LjQyMjk4IDE5LjA0NjMgNi40MjI5OCAxOS4wNDYzTDE0LjAzOTkgMTkuMTAxNkMxMC45OTM3IDIzLjk1MDQgOC41ODYzNSAyNC42NTkxIDguNTg2MzUgMjUuNDk5MkM4LjU4NjM1IDI2LjMzOTIgMTAuODg5OCAyNi4xMTE2IDExLjc1NDcgMjUuNzk4NEMxNS44OTQ5IDI0LjI5OTUgMjAuMzQxNyAxOS42MjggMjEuMTA0OCAxOC4yODMzQzI0LjMwOTIgMTguNjg0NCAyNy4wMDIyIDE4LjczMTggMjcuNjAxOSAxNy4zODc2WiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzc0MV8yNzUxKSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTIxLjMwMjkgMTEuNDUzOEMyMS4zMDY3IDExLjQ1NTUgMjEuMzEwNiAxMS40NTcxIDIxLjMxNDQgMTEuNDU4OEMyMS40ODM5IDExLjM5MTggMjEuNDU2NSAxMS4xNDA3IDIxLjQwOTkgMTAuOTQzNUMyMS4zMDMgMTAuNDkwMSAxOS40NTc1IDguNjYxNjUgMTcuNzI0NSA3Ljg0MjY1QzE1LjM2MjkgNi43MjY2NSAxMy42MjQgNi43ODQyMSAxMy4zNjcyIDcuMjk4NjVDMTMuODQ3MiA4LjI4ODIxIDE2LjA3NzkgOS4yMTcyNyAxOC40MDc3IDEwLjE4NzZDMTkuMzk3MSAxMC41OTk2IDIwLjQwNDMgMTEuMDE5MSAyMS4zMDI5IDExLjQ1MzhaIiBmaWxsPSJ1cmwoI3BhaW50MV9saW5lYXJfNzQxXzI3NTEpIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTguMzIyOCAyMS40MTY3QzE3Ljg0NTMgMjEuMjMzNyAxNy4zMDYgMjEuMDY1OCAxNi42OTI5IDIwLjkxMzNDMTcuMzQ2OSAxOS43MzkzIDE3LjQ4NDEgMTguMDAxMSAxNi44NjY1IDE2LjkwMjJDMTUuOTk5OCAxNS4zNTk5IDE0LjkxMTcgMTQuNTM5MSAxMi4zODM0IDE0LjUzOTFDMTAuOTkyOCAxNC41MzkxIDcuMjQ4NzcgMTUuMDA5IDcuMTgyMjcgMTguMTQ1QzcuMTc1MzQgMTguNDczOCA3LjE4MjA5IDE4Ljc3NTEgNy4yMDU3NyAxOS4wNTIxTDE0LjA0MyAxOS4xMDE5QzEzLjEyMSAyMC41Njk0IDEyLjI1NzUgMjEuNjU3NyAxMS41MDE2IDIyLjQ4NTJDMTIuNDA5MiAyMi43MTg2IDEzLjE1ODEgMjIuOTE0NCAxMy44NDU3IDIzLjA5NDNDMTQuNDk3OCAyMy4yNjQ4IDE1LjA5NDYgMjMuNDIwOSAxNS43MTkzIDIzLjU4MDlDMTYuNjYyIDIyLjg5MTggMTcuNTQ4MyAyMi4xNDA0IDE4LjMyMjggMjEuNDE2N1oiIGZpbGw9InVybCgjcGFpbnQyX2xpbmVhcl83NDFfMjc1MSkiLz4KPHBhdGggZD0iTTYuMzA4NzQgMTguNzI4M0M2LjU4ODA1IDIxLjExMDUgNy45MzczNiAyMi4wNDQxIDEwLjY5NDYgMjIuMzIwNUMxMy40NTE5IDIyLjU5NjggMTUuMDMzNSAyMi40MTE0IDE3LjEzOTEgMjIuNjAzNkMxOC44OTc3IDIyLjc2NDEgMjAuNDY4IDIzLjY2MzMgMjEuMDUwNSAyMy4zNTI2QzIxLjU3NDcgMjMuMDczIDIxLjI4MTQgMjIuMDYyNiAyMC41Nzk5IDIxLjQxNDRDMTkuNjcwNiAyMC41NzQxIDE4LjQxMjEgMTkuOTkgMTYuMTk3NyAxOS43ODI2QzE2LjYzOSAxOC41NzAyIDE2LjUxNTQgMTYuODcwMyAxNS44Mjk5IDE1Ljk0NTVDMTQuODM4OSAxNC42MDgyIDEzLjAwOTcgMTQuMDAzNiAxMC42OTQ2IDE0LjI2NzhDOC4yNzU4NiAxNC41NDM4IDUuOTU4MjEgMTUuNzM4NiA2LjMwODc0IDE4LjcyODNaIiBmaWxsPSJ1cmwoI3BhaW50M19saW5lYXJfNzQxXzI3NTEpIi8+CjwvZz4KPC9nPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNzQxXzI3NTEiIHg9Ii03Ny42MTUzIiB5PSItNzYuMTYwMiIgd2lkdGg9IjE4Ny4yNTQiIGhlaWdodD0iMTg0LjE2MiIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz4KPGZlT2Zmc2V0Lz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNDAuOTYiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAuMTUxOTMzIDAgMCAwIDAgMC4yMzkyMzggMCAwIDAgMCAwLjQ5MDI0MSAwIDAgMCAwLjU0IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNzQxXzI3NTEiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNzQxXzI3NTEiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl83NDFfMjc1MSIgeDE9IjExLjIxNDIiIHkxPSIxNS41NjIiIHgyPSIyNy40MTE5IiB5Mj0iMjAuMTM5OSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IndoaXRlIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxX2xpbmVhcl83NDFfMjc1MSIgeDE9IjI0LjY3NDUiIHkxPSIxNS4yNTE4IiB4Mj0iMTIuOTUzNiIgeTI9IjMuNTQxNjMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzg2OTdGRiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM4Njk3RkYiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Ml9saW5lYXJfNzQxXzI3NTEiIHgxPSIxOC42NDc4IiB5MT0iMjEuODI2MSIgeDI9IjcuNDA4MDIiIHkyPSIxNS4zODU5IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM4Njk3RkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjODY5N0ZGIiBzdG9wLW9wYWNpdHk9IjAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDNfbGluZWFyXzc0MV8yNzUxIiB4MT0iMTIuMTgyNyIgeTE9IjE1LjQzOTQiIHgyPSIxOS43OTkxIiB5Mj0iMjUuMDg0MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSJ3aGl0ZSIvPgo8c3RvcCBvZmZzZXQ9IjAuOTgzODk1IiBzdG9wLWNvbG9yPSIjRDFEOEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfNzQxXzI3NTEiPgo8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==', + rdns: 'io.rabby', + }, + }, + { + supportsMobileBrowser: false, + provider: undefined, + info: { + uuid: 'f932d0ca-0e8b-405d-aa9a-2d9dcddff53d', + name: 'XDEFI', + icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8yMTkxXzQyOTApIj4KPHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSIxMDQiIGZpbGw9IiMzMzVERTUiLz4KPHBhdGggZD0iTTQ2My40MzggMjQxLjI0N0M0NjAuNzY1IDIwNS4xMzUgNDQ4LjU4IDE3MC4zMzggNDI4LjA4NyAxNDAuMjcxQzQwNy41OTkgMTEwLjIwOSAzNzkuNTA5IDg1LjkxODIgMzQ2LjU3OSA2OS43OTZDMzEzLjY1MyA1My42NzggMjc3LjAzMiA0Ni4yODA0IDI0MC4zMiA0OC4zMzYzQzIwMy42MDUgNTAuMzkyMSAxNjguMDY5IDYxLjgyNTUgMTM3LjIxMiA4MS41MjAxQzEwNi4zNTQgMTAxLjIxIDgxLjIzOTQgMTI4LjQ4IDY0LjMzODYgMTYwLjYzNkw2My41MzYgMTYyLjI1N0M1OC4xOTcxIDE3My4xMjYgNTQuNTg3OCAxODQuNzQ1IDUyLjg0MTEgMTk2LjY5N0M0Ny44NDU1IDIzMi4wMjEgNTUuNTkyIDI2My40NiA3NS44Mjc1IDI4Ny42NjdDOTcuOTU5OSAzMTQuMTM2IDEzMy45OTMgMzI5Ljg3OSAxNzcuMjE1IDMzMS45NDdDMjI5LjgzMiAzMzQuNTU1IDI4Mi4xNDggMzIwLjQzNCAzMTkuMjc1IDI5NC40NThMMzQxLjI4NyAzMDcuMzUzQzMyMC4yNTggMzI1LjI5MSAyNzEuNjM3IDM1Ny41OTQgMTkxLjExMiAzNjIuMDA5QzEwMC43MTkgMzY2LjkzIDYzLjA0MjUgMzM4LjAwMSA2Mi42OTA1IDMzNy43MDZMNTYuMzUxNyAzNDUuNDAzTDQ4IDM1NS4yNzNDNDkuNjAwOCAzNTYuNiA4NS43Mjg1IDM4NS4zMzUgMTcwLjU3NiAzODUuMzM1QzE3Ny41MiAzODUuMzM1IDE4NC44MTYgMzg1LjMzNSAxOTIuNDEyIDM4NC43NDZDMjg5Ljg3MyAzNzkuMzkxIDM0My40NzYgMzM3LjU3NSAzNjIuMjMxIDMxOS42MjlMMzgwLjY0MiAzMzAuNjM3QzM2OC4yNjEgMzQ2LjY1OCAzNTMuMDI1IDM2MC4zMzMgMzM1LjY3IDM3MC45ODJDMjc0LjUwNCA0MDkuODQ5IDE5Ni43MDggNDE0Ljg2NyAxNDIuMjQyIDQxMi4xNjJMMTQxLjA5NiA0MzQuODMxQzE1MC4yNDIgNDM1LjI3MyAxNTkuMDM1IDQzNS40NzEgMTY3LjU4IDQzNS40NzFDMzIxLjA0OCA0MzUuNDcxIDM4My4xMzIgMzY2LjcxOSA0MDAuNTM5IDM0Mi4wNjJMNDE0LjkyNSAzNTAuNDg3QzQwMS4xMzUgMzczLjYwMyAzODIuMzkzIDM5My41NjcgMzU5LjkzMSA0MDguOTM5QzMzMy4wMzQgNDI3LjM0NSAzMDEuNzM1IDQzOC41MzggMjY5LjExNCA0NDEuNDE1TDI3MS4xMTQgNDY0QzMwNy43MzkgNDYwLjc4NiAzNDIuODg4IDQ0OC4yMzYgMzczLjA4OSA0MjcuNTgxQzQwMy4yOSA0MDYuOTI2IDQyNy41MDggMzc4Ljg4MSA0NDMuMzQ5IDM0Ni4yMDdDNDU5LjE4NSAzMTMuNTI5IDQ2Ni4xMTYgMjc3LjM1OCA0NjMuNDM4IDI0MS4yNDdaTTM3NC44MSAyNDQuNzM5QzM2NC42MjYgMjQ0LjczOSAzNTYuMzY4IDIzNi42MTMgMzU2LjM2OCAyMjYuNTg2QzM1Ni4zNjggMjE2LjU2IDM2NC42MjEgMjA4LjQzMyAzNzQuODEgMjA4LjQzM0MzODQuOTkgMjA4LjQzMyAzOTMuMjQ3IDIxNi41NiAzOTMuMjQ3IDIyNi41ODZDMzkzLjI0NyAyMzYuNjEzIDM4NC45OTQgMjQ0LjczOSAzNzQuODEgMjQ0LjczOVoiIGZpbGw9IiNFQ0VDRUMiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8yMTkxXzQyOTAiPgo8cmVjdCB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K', + rdns: 'io.xdefi', + }, + }, +] as unknown as (EIP6963ProviderDetail & { supportsMobileBrowser: boolean })[] diff --git a/src/pages/Accounts/Accounts.tsx b/src/pages/Accounts/Accounts.tsx index e618b840460..44beea2cdec 100644 --- a/src/pages/Accounts/Accounts.tsx +++ b/src/pages/Accounts/Accounts.tsx @@ -1,6 +1,6 @@ import { AddIcon, EditIcon } from '@chakra-ui/icons' import { Button, Heading, List, Skeleton, Stack } from '@chakra-ui/react' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import { useEffect, useMemo, useState } from 'react' import { useTranslate } from 'react-polyglot' import { useSelector } from 'react-redux' @@ -34,7 +34,7 @@ const AccountHeader = ({ isLoading }: { isLoading?: boolean }) => { const [isMultiAccountWallet, setIsMultiAccountWallet] = useState(false) const isSnapInstalled = useIsSnapInstalled() - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet + const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet useEffect(() => { if (!wallet) return if (isMetaMaskMultichainWallet && !isSnapInstalled) return setIsMultiAccountWallet(false) diff --git a/src/pages/Accounts/AddAccountModal.tsx b/src/pages/Accounts/AddAccountModal.tsx index 5d9b6d573a0..029a32abedc 100644 --- a/src/pages/Accounts/AddAccountModal.tsx +++ b/src/pages/Accounts/AddAccountModal.tsx @@ -15,7 +15,7 @@ import { useToast, } from '@chakra-ui/react' import { type ChainId } from '@shapeshiftoss/caip' -import { MetaMaskShapeShiftMultiChainHDWallet } from '@shapeshiftoss/hdwallet-shapeshift-multichain' +import { MetaMaskMultiChainHDWallet } from '@shapeshiftoss/hdwallet-metamask-multichain' import { useCallback, useEffect, useMemo, useState } from 'react' import { FaInfoCircle } from 'react-icons/fa' import { useTranslate } from 'react-polyglot' @@ -62,7 +62,7 @@ export const AddAccountModal = () => { const { isSnapInstalled } = useIsSnapInstalled() - const isMetaMaskMultichainWallet = wallet instanceof MetaMaskShapeShiftMultiChainHDWallet + const isMetaMaskMultichainWallet = wallet instanceof MetaMaskMultiChainHDWallet const unsupportedSnapChainIds = useMemo(() => { if (!isMetaMaskMultichainWallet) return [] if (nextAccountNumber === null) return [] diff --git a/src/pages/ConnectWallet/ConnectWallet.tsx b/src/pages/ConnectWallet/ConnectWallet.tsx index 1f3454b847a..18eeaf645d5 100644 --- a/src/pages/ConnectWallet/ConnectWallet.tsx +++ b/src/pages/ConnectWallet/ConnectWallet.tsx @@ -26,11 +26,10 @@ import { SEO } from 'components/Layout/Seo' import { RawText, Text } from 'components/Text' import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { WalletActions } from 'context/WalletProvider/actions' -import { KeyManager } from 'context/WalletProvider/KeyManager' -import { useFeatureFlag } from 'hooks/useFeatureFlag/useFeatureFlag' import { useQuery } from 'hooks/useQuery/useQuery' import { useWallet } from 'hooks/useWallet/useWallet' import { isMobile } from 'lib/globals' +import { METAMASK_RDNS } from 'lib/mipd' import { isSome } from 'lib/utils' import { selectAssetById } from 'state/slices/selectors' import { store } from 'state/store' @@ -64,7 +63,6 @@ const metamaskIcon = export const ConnectWallet = () => { const { state, dispatch, connectDemo, connect } = useWallet() const hasWallet = Boolean(state.walletInfo?.deviceId) - const isSnapEnabled = useFeatureFlag('Snaps') const snapInfoBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50') const allNativeAssets = useMemo(() => { @@ -103,7 +101,7 @@ export const ConnectWallet = () => { const handleMetaMaskConnect = useCallback(() => { dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) - connect(KeyManager.MetaMask) + connect(METAMASK_RDNS, true) }, [connect, dispatch]) const renderChains = useMemo(() => { @@ -185,38 +183,34 @@ export const ConnectWallet = () => { > - {isSnapEnabled && ( - <> - - - - - - - - {renderChains} - - - {translate('connectWalletPage.snapDescription')} - - - - - )} + + + + + + + + {renderChains} + + + {translate('connectWalletPage.snapDescription')} + + + diff --git a/src/pages/ConnectWallet/MobileConnect.tsx b/src/pages/ConnectWallet/MobileConnect.tsx index 63ef38000dc..39a08e2533b 100644 --- a/src/pages/ConnectWallet/MobileConnect.tsx +++ b/src/pages/ConnectWallet/MobileConnect.tsx @@ -128,14 +128,17 @@ export const MobileConnect = () => { connectedType: KeyManager.Mobile, }, }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + dispatch({ + type: WalletActions.SET_IS_CONNECTED, + payload: true, + }) dispatch({ type: WalletActions.SET_WALLET_MODAL, payload: false }) dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, - payload: KeyManager.Mobile, + payload: { modalType: KeyManager.Mobile, isMipdProvider: false }, }) - localWallet.setLocalWalletTypeAndDeviceId(KeyManager.Mobile, deviceId) + localWallet.setLocalWallet({ type: KeyManager.Mobile, deviceId }) localWallet.setLocalNativeWalletName(item?.label ?? 'label') } catch (e) { console.log(e) diff --git a/src/plugins/cosmos/hooks/useStakingAction/useStakingAction.tsx b/src/plugins/cosmos/hooks/useStakingAction/useStakingAction.tsx index d463e40ab23..30d758f64ca 100644 --- a/src/plugins/cosmos/hooks/useStakingAction/useStakingAction.tsx +++ b/src/plugins/cosmos/hooks/useStakingAction/useStakingAction.tsx @@ -8,7 +8,6 @@ import { import { getChainAdapterManager } from 'context/PluginProvider/chainAdapterSingleton' import { checkIsMetaMaskDesktop, - checkIsMetaMaskImpersonator, checkIsSnapInstalled, } from 'hooks/useIsSnapInstalled/useIsSnapInstalled' import { useWallet } from 'hooks/useWallet/useWallet' @@ -46,13 +45,10 @@ export const useStakingAction = () => { try { // Native and KeepKey hdwallets only support offline signing, not broadcasting signed TXs like e.g Metamask - const isMetaMaskDesktop = await checkIsMetaMaskDesktop(wallet) - const isMetaMaskImpersonator = await checkIsMetaMaskImpersonator(wallet) + const isMetaMaskDesktop = checkIsMetaMaskDesktop(wallet) if ( !wallet.supportsOfflineSigning() && - (!isMetaMaskDesktop || - isMetaMaskImpersonator || - (isMetaMaskDesktop && !(await checkIsSnapInstalled()))) + (!isMetaMaskDesktop || (isMetaMaskDesktop && !(await checkIsSnapInstalled()))) ) { throw new Error(`unsupported wallet: ${await wallet.getModel()}`) } diff --git a/src/state/migrations/clearLocalWallet.ts b/src/state/migrations/clearLocalWallet.ts new file mode 100644 index 00000000000..474bc73c899 --- /dev/null +++ b/src/state/migrations/clearLocalWallet.ts @@ -0,0 +1,8 @@ +import type { PersistPartial } from 'redux-persist/es/persistReducer' +import type { LocalWalletState } from 'state/slices/localWalletSlice/localWalletSlice' +import { initialState } from 'state/slices/localWalletSlice/localWalletSlice' +import type { Portfolio } from 'state/slices/portfolioSlice/portfolioSliceCommon' + +export const clearLocalWallet = (_state: Portfolio): LocalWalletState & PersistPartial => { + return initialState as LocalWalletState & PersistPartial +} diff --git a/src/state/migrations/index.ts b/src/state/migrations/index.ts index a562194519b..003d00f018b 100644 --- a/src/state/migrations/index.ts +++ b/src/state/migrations/index.ts @@ -1,6 +1,7 @@ import type { MigrationManifest } from 'redux-persist' import { clearAssets } from './clearAssets' +import { clearLocalWallet } from './clearLocalWallet' import { clearMarketData } from './clearMarketData' import { clearNfts } from './clearNfts' import { clearOpportunities } from './clearOpportunities' @@ -18,6 +19,11 @@ export const clearOpportunitiesMigrations = { export const clearPortfolioMigrations = { 0: clearPortfolio, + 1: clearPortfolio, +} as unknown as Omit + +export const localWalletMigrations = { + 0: clearLocalWallet, } as unknown as Omit export const clearNftsMigrations = { diff --git a/src/state/reducer.ts b/src/state/reducer.ts index 49b785d8728..c2b8f5fd0b6 100644 --- a/src/state/reducer.ts +++ b/src/state/reducer.ts @@ -21,6 +21,7 @@ import { clearPortfolioMigrations, clearSnapshotMigrations, clearTxHistoryMigrations, + localWalletMigrations, } from './migrations' import type { AssetsState } from './slices/assetsSlice/assetsSlice' import { assetApi, assets } from './slices/assetsSlice/assetsSlice' @@ -97,7 +98,8 @@ const snapshotPersistConfig = { const localWalletSlicePersistConfig = { key: 'localWalletSlice', storage: localforage, - // no migrations for localWalletSlice yet - stay tuned! + version: Math.max(...Object.keys(localWalletMigrations).map(Number)), + migrate: createMigrate(localWalletMigrations, { debug: false }), } const marketDataPersistConfig = { diff --git a/src/state/slices/localWalletSlice/localWalletSlice.ts b/src/state/slices/localWalletSlice/localWalletSlice.ts index f77f0bcac19..b9e12efab5b 100644 --- a/src/state/slices/localWalletSlice/localWalletSlice.ts +++ b/src/state/slices/localWalletSlice/localWalletSlice.ts @@ -6,12 +6,14 @@ export type LocalWalletState = { walletType: KeyManager | null walletDeviceId: string | null nativeWalletName: string | null + rdns: string | null } -const initialState: LocalWalletState = { +export const initialState: LocalWalletState = { walletType: null, walletDeviceId: null, nativeWalletName: null, + rdns: null, } export const localWalletSlice = createSlice({ @@ -19,17 +21,19 @@ export const localWalletSlice = createSlice({ initialState, reducers: { clear: () => initialState, - setWalletTypeAndDeviceId: ( + setLocalWallet: ( state, - action: PayloadAction<{ type: KeyManager; deviceId: string }>, + action: PayloadAction<{ type: KeyManager; deviceId: string; rdns: string | null }>, ) => { state.walletType = action.payload.type state.walletDeviceId = action.payload.deviceId + state.rdns = action.payload.rdns }, clearLocalWallet: state => { state.walletType = null state.walletDeviceId = null state.nativeWalletName = null + state.rdns = null }, setNativeWalletName: (state, action: PayloadAction) => { state.nativeWalletName = action.payload diff --git a/src/state/slices/localWalletSlice/selectors.ts b/src/state/slices/localWalletSlice/selectors.ts index 3c1cef04ef1..3415d1a94c0 100644 --- a/src/state/slices/localWalletSlice/selectors.ts +++ b/src/state/slices/localWalletSlice/selectors.ts @@ -18,3 +18,7 @@ export const selectNativeWalletName = createSelector( selectLocalWalletState, (localWalletState): string | null => localWalletState.nativeWalletName, ) +export const selectWalletRdns = createSelector( + selectLocalWalletState, + (localWalletState): string | null => localWalletState.rdns, +) diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 181e19c5686..52aa3c1ca72 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -40,11 +40,9 @@ export type FeatureFlags = { Portals: boolean CovalentJaypegs: boolean Chatwoot: boolean - CoinbaseWallet: boolean AdvancedSlippage: boolean WalletConnectV2: boolean CustomSendNonce: boolean - Snaps: boolean ThorchainLending: boolean ThorchainLendingBorrow: boolean ThorchainLendingRepay: boolean @@ -133,11 +131,9 @@ const initialState: Preferences = { ArbitrumBridge: getConfig().REACT_APP_FEATURE_ARBITRUM_BRIDGE, Portals: getConfig().REACT_APP_FEATURE_PORTALS_SWAPPER, Chatwoot: getConfig().REACT_APP_FEATURE_CHATWOOT, - CoinbaseWallet: getConfig().REACT_APP_FEATURE_COINBASE_WALLET, AdvancedSlippage: getConfig().REACT_APP_FEATURE_ADVANCED_SLIPPAGE, WalletConnectV2: getConfig().REACT_APP_FEATURE_WALLET_CONNECT_V2, CustomSendNonce: getConfig().REACT_APP_EXPERIMENTAL_CUSTOM_SEND_NONCE, - Snaps: getConfig().REACT_APP_EXPERIMENTAL_MM_SNAPPY_FINGERS, ThorchainLending: getConfig().REACT_APP_FEATURE_THORCHAIN_LENDING, ThorchainLendingBorrow: getConfig().REACT_APP_FEATURE_THORCHAIN_LENDING_BORROW, ThorchainLendingRepay: getConfig().REACT_APP_FEATURE_THORCHAIN_LENDING_REPAY, diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 2bc5d97b54a..eee963b98ca 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -96,11 +96,9 @@ export const mockStore: ReduxState = { OneInch: false, CovalentJaypegs: false, Chatwoot: false, - CoinbaseWallet: false, AdvancedSlippage: false, WalletConnectV2: false, CustomSendNonce: false, - Snaps: false, ThorchainLending: false, ThorchainLendingBorrow: false, ThorchainLendingRepay: false, @@ -257,5 +255,6 @@ export const mockStore: ReduxState = { walletType: null, walletDeviceId: null, nativeWalletName: null, + rdns: null, }, } diff --git a/src/utils/snaps.ts b/src/utils/snaps.ts index c67bab6bc4f..4f7b4bb0dd4 100644 --- a/src/utils/snaps.ts +++ b/src/utils/snaps.ts @@ -1,7 +1,6 @@ -import detectEthereumProvider from '@metamask/detect-provider' -import assert from 'assert' import { getConfig } from 'config' import type { Eip1193Provider } from 'ethers' +import { METAMASK_RDNS, mipdStore } from 'lib/mipd' type GetSnapsResult = Record< string, @@ -14,11 +13,14 @@ type GetSnapsResult = Record< > export const enableShapeShiftSnap = async (): Promise => { - const provider = (await detectEthereumProvider()) as Eip1193Provider + const mipdProvider = mipdStore + .getProviders() + .find(provider => provider.info.rdns === METAMASK_RDNS) + if (!mipdProvider) throw new Error("EIP-1193 provider isn't MetaMask") + + const provider = mipdProvider.provider as Eip1193Provider const snapVersion = getConfig().REACT_APP_SNAP_VERSION - const isSnapFeatureEnabled = getConfig().REACT_APP_EXPERIMENTAL_MM_SNAPPY_FINGERS - assert(isSnapFeatureEnabled, 'Snap feature flag is disabled') const snapId = getConfig().REACT_APP_SNAP_ID await provider.request({ @@ -33,8 +35,12 @@ export const enableShapeShiftSnap = async (): Promise => { export const getSnapVersion = async (): Promise => { const snapId = getConfig().REACT_APP_SNAP_ID - const provider = (await detectEthereumProvider()) as Eip1193Provider - if (!(provider as any).isMetaMask) return null + const mipdProvider = mipdStore + .getProviders() + .find(provider => provider.info.rdns === METAMASK_RDNS) + if (!mipdProvider) return null + + const provider = mipdProvider.provider as Eip1193Provider const snaps: GetSnapsResult = await provider.request({ method: 'wallet_getSnaps', diff --git a/yarn.lock b/yarn.lock index c7e8105599c..429972530dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11309,15 +11309,15 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-coinbase@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.55.10" +"@shapeshiftoss/hdwallet-coinbase@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-coinbase@npm:1.55.11-mipd.5" dependencies: "@coinbase/wallet-sdk": ^3.6.6 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: ab6a11a68b3365f7197bcb1913b70430b4a5cf3104bf3667fd6c6682e7af442f88e27eb0c698981f2278dfcfad796c7925a5335cf5b68fbc608460a143a53249 + checksum: 547c0707f47a9f76d4712ce2c075938bdacc4866a043bcc4dc15aa44277627e529a84ff76186a9e2d5a95d03a9293df339449d53e3eaa708e6b9270160cd423b languageName: node linkType: hard @@ -11347,9 +11347,9 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-core@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-core@npm:1.55.10" +"@shapeshiftoss/hdwallet-core@npm:1.55.11-mipd.5, @shapeshiftoss/hdwallet-core@npm:^1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-core@npm:1.55.11-mipd.5" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 "@shapeshiftoss/proto-tx-builder": ^0.8.0 @@ -11359,30 +11359,30 @@ __metadata: lodash: ^4.17.21 rxjs: ^6.4.0 type-assertions: ^1.1.0 - checksum: cd85d7890ff6ffdcafd27a57169fa9cf589403362baf77804f2c9010e8a115c439910b6bf997ec718623b955dfc43e88ff44dde1d66bedfc98b1c26db0882ff2 + checksum: f42913ad548993c7624fa76b699189d0c4dbe7e9e6816ff18de30b4ab57e60c4101f7606ca5914237853314491a16659f65b8671cfeb232e9e644815f7a89b58 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.10" +"@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-keepkey-webusb@npm:1.55.11-mipd.5" dependencies: - "@shapeshiftoss/hdwallet-core": 1.55.10 - "@shapeshiftoss/hdwallet-keepkey": 1.55.10 - checksum: 5f109d8fa878a338ad67ca2468d172090e0c6e7e1db90672a0b11f7edbc3f681a4e439e373c9729369dc9c4f30fda00c3262071d5dec6506da65c61156b19b82 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-keepkey": ^1.55.11-mipd.5 + checksum: f05ec54493d7ef049edef648aec89dad55d460164088ba130d9a66374f1436815a39f188d71932667e54dc97a9e99239409016f9ba623eb567e04ca099fa6068 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keepkey@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.55.10" +"@shapeshiftoss/hdwallet-keepkey@npm:1.55.11-mipd.5, @shapeshiftoss/hdwallet-keepkey@npm:^1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-keepkey@npm:1.55.11-mipd.5" dependencies: "@ethereumjs/common": ^2.4.0 "@ethereumjs/tx": ^3.3.0 "@keepkey/device-protocol": ^7.12.2 "@metamask/eth-sig-util": ^7.0.0 "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 "@shapeshiftoss/proto-tx-builder": ^0.8.0 bignumber.js: ^9.0.1 bnb-javascript-sdk-nobroadcast: ^2.16.14 @@ -11394,27 +11394,27 @@ __metadata: p-lazy: ^3.1.0 semver: ^7.3.8 tiny-secp256k1: ^1.1.6 - checksum: 298f9c3589769cbc6d1e9d4fbb0176b4559ec5f0352f15762e2c3acea2ca8fd085ba472e3c34b58cf6a01df33e408f8fb9dd25e7c7a520adf84380dcd8d3b787 + checksum: f2f209df768e133635f859106294f12555d3108fc1c37d911b83440c6a140509f84b73f50ca22e44c30c1a486472c02e3a6e385b46282504bc62cbf6684681f5 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-keplr@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.55.10" +"@shapeshiftoss/hdwallet-keplr@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-keplr@npm:1.55.11-mipd.5" dependencies: "@shapeshiftoss/caip": 8.15.0 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 "@shapeshiftoss/proto-tx-builder": ^0.8.0 "@shapeshiftoss/types": 3.1.3 base64-js: ^1.5.1 lodash: ^4.17.21 - checksum: a04c4a070e4db365ffb4a86c596ac37c17e3517a0e69c1bffcb3d90d3b9660393d3db6f7fad5e9c31478eea007938409ebf6a6559e1dfe2d5b475a273ffc379f + checksum: d84136341af78313ce17aa9126fd2232bf4a846848d06484d3cab2f76352abea6820c6bcd7bdeda53a2843683ea06bf225ef2f4f124001379b3d5eced92e1dae languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.10" +"@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-ledger-webusb@npm:1.55.11-mipd.5" dependencies: "@ledgerhq/hw-app-btc": ^10.4.1 "@ledgerhq/hw-app-eth": ^6.38.0 @@ -11422,23 +11422,23 @@ __metadata: "@ledgerhq/hw-transport-webusb": ^6.29.2 "@ledgerhq/live-common": ^21.8.2 "@ledgerhq/logs": ^6.10.1 - "@shapeshiftoss/hdwallet-core": 1.55.10 - "@shapeshiftoss/hdwallet-ledger": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-ledger": ^1.55.11-mipd.5 "@types/w3c-web-usb": ^1.0.4 p-queue: ^7.4.1 - checksum: 8c645914a42cd76e1b9c21b85aa6085e83c42aea32deff2a1bb06ca92613677f6d93b1a8f3e2dd7bcee1eddce08038612f06bd2b4088735c1aaa635584a4e4c3 + checksum: ceee1a0c9726f00fe9ec2b0d5355cf43e1531acea24f8a9fecd09a6c24a315793bffd94e1a56cec026d3de839f7107fe4abe345ce7976c6e19e34c9bae9ea456 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-ledger@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.55.10" +"@shapeshiftoss/hdwallet-ledger@npm:1.55.11-mipd.5, @shapeshiftoss/hdwallet-ledger@npm:^1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-ledger@npm:1.55.11-mipd.5" dependencies: "@ethereumjs/common": ^2.4.0 "@ethereumjs/tx": ^3.3.0 "@ledgerhq/hw-app-cosmos": ^6.29.1 "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 base64-js: ^1.5.1 bchaddrjs: ^0.4.4 bitcoinjs-message: ^2.0.0 @@ -11446,28 +11446,32 @@ __metadata: ethereumjs-tx: 1.3.7 ethereumjs-util: ^6.1.0 lodash: ^4.17.21 - checksum: df341fe41c9cf75cc7c698cc7b36547c95c97c56d663ac939f516ebb7e9dbe74f682ac68fa841ae1d6fb1f5080fb9550eca932df59c721bea6fe15ceebd9d830 + checksum: 47f4f75e14a140ab4ea70e43f9e0c44ba900a055e2960992cd57c5caac588e3c5013121b449bb344675856d55318c707d352bf2e5729b19eb5fbc2ea2e2f52d8 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-metamask@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-metamask@npm:1.55.10" +"@shapeshiftoss/hdwallet-metamask-multichain@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-metamask-multichain@npm:1.55.11-mipd.5" dependencies: "@metamask/detect-provider": ^1.2.0 "@metamask/onboarding": ^1.0.1 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/common-api": ^9.3.0 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 + "@shapeshiftoss/metamask-snaps-adapter": ^1.0.12 + "@shapeshiftoss/metamask-snaps-types": ^1.0.12 eth-rpc-errors: ^4.0.3 lodash: ^4.17.21 - checksum: db3803db2c49694052539aba19dd23df76fed55e5889708de21b6b912f53ecf8288e137d1a4ae34883897b2d02d7f6254ccb8c3cb641c3746bfd9ca76a10c137 + mipd: ^0.0.7 + checksum: 5be7e3a3c881fddfc9ae9dda003e77d1c690a1822d91ad7aafa269093a12b43ee2624c43e7bc3e249b33b0479f52cc5431bad61cc9627136bd483851834d9ea9 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native-vault@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.55.10" +"@shapeshiftoss/hdwallet-native-vault@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-native-vault@npm:1.55.11-mipd.5" dependencies: - "@shapeshiftoss/hdwallet-native": 1.55.10 + "@shapeshiftoss/hdwallet-native": ^1.55.11-mipd.5 bip39: ^3.0.4 hash-wasm: ^4.11.0 idb-keyval: ^6.0.3 @@ -11476,17 +11480,17 @@ __metadata: type-assertions: ^1.1.0 uuid: ^8.3.2 web-encoding: ^1.1.0 - checksum: 7713ed5ceeaea737d2d58de99e83f8c33b8bdad5450f420a95b82503cc1058ff5caf9e34de20e9c160182ca856ba9d042ddbf3afe0df79329af983d82aef959a + checksum: 8576bac8a02f70f314c63110f1773f2c3d124ccfe64ece5632ddbfb9e61708b830c15e412addb09bc8e688d49c42c7ab0c7853e6b59bfd1dca26fcf1a4b16ce4 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-native@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-native@npm:1.55.10" +"@shapeshiftoss/hdwallet-native@npm:1.55.11-mipd.5, @shapeshiftoss/hdwallet-native@npm:^1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-native@npm:1.55.11-mipd.5" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 "@shapeshiftoss/fiosdk": 1.2.1-shapeshift.6 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 "@shapeshiftoss/proto-tx-builder": ^0.8.0 "@zxing/text-encoding": ^0.9.0 bchaddrjs: ^0.4.9 @@ -11508,7 +11512,7 @@ __metadata: tendermint-tx-builder: ^1.0.9 tiny-secp256k1: ^1.1.6 web-encoding: ^1.1.0 - checksum: 1e78caad03836d37233fb6edb3435286bc993573f068a2d96c980dffb1e3066e98834241599471b678b3f7fc7e1ca55fb16b0268e18398bc41cbae797d0cadc0 + checksum: 274cb1e22f56db6169d6bb500684b31cbfa84918de2a1931488984194fd651f7ae8743963f1739acb5090c877c308c224169b82d6f5993c31294c7206eb1ec29 languageName: node linkType: hard @@ -11544,56 +11548,30 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/hdwallet-phantom@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-phantom@npm:1.55.10" +"@shapeshiftoss/hdwallet-phantom@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-phantom@npm:1.55.11-mipd.5" dependencies: "@shapeshiftoss/bitcoinjs-lib": 5.2.0-shapeshift.2 - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 "@solana/web3.js": ^1.95.3 base64-js: ^1.5.1 bitcoinjs-message: ^2.0.0 ethers: 5.7.2 lodash: ^4.17.21 - checksum: 2a187ed9c2c48e3106068d1f3993631c980e21c2aabaf9361ca70b21c165b6ac384a6eed67b934f3fe53db3b27a4197cee09e51a6402739818076b21e5c7840f - languageName: node - linkType: hard - -"@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-shapeshift-multichain@npm:1.55.10" - dependencies: - "@metamask/detect-provider": ^1.2.0 - "@metamask/onboarding": ^1.0.1 - "@shapeshiftoss/common-api": ^9.3.0 - "@shapeshiftoss/hdwallet-core": 1.55.10 - "@shapeshiftoss/metamask-snaps-adapter": ^1.0.10 - "@shapeshiftoss/metamask-snaps-types": ^1.0.10 - eth-rpc-errors: ^4.0.3 - lodash: ^4.17.21 - checksum: b0bf636b247dbf649576294dc066852a6049213b9b2e98bc33323c83534317837220020fd2333289859d84f86d39c5a53764291b9d4417f8cdc8e4f6f4c1aba3 + checksum: 10d43dce5dda54516e8502c60a786700adefcd3be9a16c015ad01526952b9d6b852589aa59059cbb78e081c4a9e8b3928eec172274167fb24564420388bbc5d2 languageName: node linkType: hard -"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.10" +"@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.11-mipd.5": + version: 1.55.11-mipd.5 + resolution: "@shapeshiftoss/hdwallet-walletconnectv2@npm:1.55.11-mipd.5" dependencies: - "@shapeshiftoss/hdwallet-core": 1.55.10 + "@shapeshiftoss/hdwallet-core": ^1.55.11-mipd.5 "@walletconnect/ethereum-provider": ^2.10.1 "@walletconnect/modal": ^2.6.2 ethers: ^5.6.5 - checksum: 43bffab85fdaaaad13987f8d0abfd1141fa1b4c8f86455f265e3b604ecf3be219b063f15041aa630292b1279eac53cae7188bc566b655914536092671bba47c2 - languageName: node - linkType: hard - -"@shapeshiftoss/hdwallet-xdefi@npm:1.55.10": - version: 1.55.10 - resolution: "@shapeshiftoss/hdwallet-xdefi@npm:1.55.10" - dependencies: - "@shapeshiftoss/hdwallet-core": 1.55.10 - lodash: ^4.17.21 - checksum: 5058663f3f4f78a76a17322539c3a83fd708eb7606133d2f10b46d78fcee6ff00ba6cefcc3093c5c6d456bd00f1eb3247dd9edf33d665c7a3750d84e4a3756e0 + checksum: e9a67a66753b38be127d51854f058e96301530d5403182eccf84f81d11bcc3a62212513a9af39d93dfb52a9e9ca222f016c84d3824f8134db093eb6d3a8293d3 languageName: node linkType: hard @@ -11604,17 +11582,18 @@ __metadata: languageName: node linkType: hard -"@shapeshiftoss/metamask-snaps-adapter@npm:^1.0.10": - version: 1.0.10 - resolution: "@shapeshiftoss/metamask-snaps-adapter@npm:1.0.10" +"@shapeshiftoss/metamask-snaps-adapter@npm:^1.0.12": + version: 1.0.12 + resolution: "@shapeshiftoss/metamask-snaps-adapter@npm:1.0.12" dependencies: "@ethersproject/providers": ^5.7.2 "@metamask/detect-provider": ^2.0.0 "@metamask/snaps-ui": 1.0.2 "@shapeshiftoss/hdwallet-core": ^1.55.1 "@shapeshiftoss/logger": ^1.1.3 - "@shapeshiftoss/metamask-snaps": ^1.0.10 - "@shapeshiftoss/metamask-snaps-types": ^1.0.10 + "@shapeshiftoss/metamask-snaps": ^1.0.12 + "@shapeshiftoss/metamask-snaps-types": ^1.0.12 + mipd: ^0.0.7 p-queue: ^7.4.1 peerDependencies: "@shapeshiftoss/caip": "*" @@ -11622,25 +11601,25 @@ __metadata: eslint-plugin-react: "*" eslint-plugin-react-hooks: "*" webpack: "*" - checksum: df32601ba40ecaf2e5516038374b29177c54a8a289422817a0201a339d9c5a4a6f1100b2db449c75d6ec8085b7e83a2b1cd716c478517b49306d91c5c6b991df + checksum: f30285da36727713926d1fc99e2d8e9c1ef388053336581d4bebee9d004555d8feda8902f33fbf6e52c0194fb53ac8344016e7bf3b4b4431b1d97fc4ec3c30b5 languageName: node linkType: hard -"@shapeshiftoss/metamask-snaps-types@npm:^1.0.10": - version: 1.0.10 - resolution: "@shapeshiftoss/metamask-snaps-types@npm:1.0.10" +"@shapeshiftoss/metamask-snaps-types@npm:^1.0.12": + version: 1.0.12 + resolution: "@shapeshiftoss/metamask-snaps-types@npm:1.0.12" dependencies: "@metamask/types": ^1.1.0 "@shapeshiftoss/hdwallet-core": ^1.55.1 "@shapeshiftoss/hdwallet-native": ^1.55.1 "@shapeshiftoss/unchained-client": ^10.1.1 - checksum: 1f047089827b8c49e7c2f123a2537df357f7b9c0e66d04c61563f4bfbc05c023fb054457d639ed2c6859a2cbf5a02395d0770f16ae3802266559f0436af351c4 + checksum: 6b8a9988037f37a966d9b09e2aeb880672aa66465eee56f680a95111aa26d7ecf7579aed23e7067396309f1d16c59c15170739f5357eb568a9057a6e5cda7081 languageName: node linkType: hard -"@shapeshiftoss/metamask-snaps@npm:^1.0.10": - version: 1.0.10 - resolution: "@shapeshiftoss/metamask-snaps@npm:1.0.10" +"@shapeshiftoss/metamask-snaps@npm:^1.0.12": + version: 1.0.12 + resolution: "@shapeshiftoss/metamask-snaps@npm:1.0.12" dependencies: "@ethersproject/providers": ^5.7.0 "@metamask/detect-provider": ^2.0.0 @@ -11651,7 +11630,7 @@ __metadata: "@shapeshiftoss/hdwallet-core": ^1.55.1 "@shapeshiftoss/hdwallet-native": ^1.55.1 "@shapeshiftoss/logger": ^1.1.2 - "@shapeshiftoss/metamask-snaps-types": ^1.0.10 + "@shapeshiftoss/metamask-snaps-types": ^1.0.12 "@shapeshiftoss/types": ^8.3.0 "@shapeshiftoss/unchained-client": 10.1.1 eth-rpc-errors: ^4.0.3 @@ -11668,7 +11647,7 @@ __metadata: eslint-plugin-react: "*" eslint-plugin-react-hooks: "*" webpack: "*" - checksum: 57ed54d80903a83ef738d1c9c4487059f7f371f7e0271afd14176883ca91b5d012df861ec8c447b6c7d10f7dd9dfc034921ff70bdbdc829c6046b398e372bf31 + checksum: ea517da5ade8d29d8febd033c4cba8fc7c9588023598359822cbb1a12ec8ea3313c62774e0da34ff0a1d3e3cd756041b85d39c27ee858ba4e6456e3bdbad6dab languageName: node linkType: hard @@ -11779,7 +11758,6 @@ __metadata: "@ledgerhq/hw-transport-webusb": ^6.29.2 "@lifi/sdk": ^3.1.5 "@lukemorales/query-key-factory": ^1.3.4 - "@metamask/detect-provider": ^2.0.0 "@peculiar/webcrypto": ^1.3.3 "@react-spring/web": ^9.7.4 "@reduxjs/toolkit": ^1.9.7 @@ -11788,20 +11766,18 @@ __metadata: "@shapeshiftoss/caip": "workspace:^" "@shapeshiftoss/chain-adapters": "workspace:^" "@shapeshiftoss/errors": "workspace:^" - "@shapeshiftoss/hdwallet-coinbase": 1.55.10 - "@shapeshiftoss/hdwallet-core": 1.55.10 - "@shapeshiftoss/hdwallet-keepkey": 1.55.10 - "@shapeshiftoss/hdwallet-keepkey-webusb": 1.55.10 - "@shapeshiftoss/hdwallet-keplr": 1.55.10 - "@shapeshiftoss/hdwallet-ledger": 1.55.10 - "@shapeshiftoss/hdwallet-ledger-webusb": 1.55.10 - "@shapeshiftoss/hdwallet-metamask": 1.55.10 - "@shapeshiftoss/hdwallet-native": 1.55.10 - "@shapeshiftoss/hdwallet-native-vault": 1.55.10 - "@shapeshiftoss/hdwallet-phantom": 1.55.10 - "@shapeshiftoss/hdwallet-shapeshift-multichain": 1.55.10 - "@shapeshiftoss/hdwallet-walletconnectv2": 1.55.10 - "@shapeshiftoss/hdwallet-xdefi": 1.55.10 + "@shapeshiftoss/hdwallet-coinbase": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-core": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-keepkey": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-keepkey-webusb": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-keplr": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-ledger": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-ledger-webusb": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-metamask-multichain": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-native": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-native-vault": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-phantom": 1.55.11-mipd.5 + "@shapeshiftoss/hdwallet-walletconnectv2": 1.55.11-mipd.5 "@shapeshiftoss/swapper": "workspace:^" "@shapeshiftoss/types": "workspace:^" "@shapeshiftoss/unchained-client": "workspace:^" @@ -11913,6 +11889,7 @@ __metadata: localforage: ^1.10.0 lodash: ^4.17.21 match-sorter: ^6.3.0 + mipd: ^0.0.7 mixpanel-browser: ^2.45.0 module-alias: ^2.2.3 msw: ^0.36.5 @@ -29526,6 +29503,18 @@ __metadata: languageName: node linkType: hard +"mipd@npm:^0.0.7": + version: 0.0.7 + resolution: "mipd@npm:0.0.7" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 14526f78d6d1bc8580898922508d64714f5abc7293b5998fe93c54237fd1cea120dc98674fe2b329ba3803bda5a85f3e442c3b1fa880e4c6b443bf73018514a8 + languageName: node + linkType: hard + "mitt@npm:^2.1.0": version: 2.1.0 resolution: "mitt@npm:2.1.0" From 8ddc35882a7c75f90a9b18bc758ed2aaf69096f1 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:18:07 +0200 Subject: [PATCH 7/9] fix: eip6963 refresh issues (#7950) --- src/context/WalletProvider/WalletProvider.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/context/WalletProvider/WalletProvider.tsx b/src/context/WalletProvider/WalletProvider.tsx index 1549ec8ae70..a028945270b 100644 --- a/src/context/WalletProvider/WalletProvider.tsx +++ b/src/context/WalletProvider/WalletProvider.tsx @@ -20,7 +20,11 @@ import { KeepKeyRoutes } from 'context/WalletProvider/routes' import { useWalletConnectV2EventHandler } from 'context/WalletProvider/WalletConnectV2/useWalletConnectV2EventHandler' import { useMipdProviders } from 'lib/mipd' import { localWalletSlice } from 'state/slices/localWalletSlice/localWalletSlice' -import { selectWalletDeviceId, selectWalletType } from 'state/slices/localWalletSlice/selectors' +import { + selectWalletDeviceId, + selectWalletRdns, + selectWalletType, +} from 'state/slices/localWalletSlice/selectors' import { portfolio } from 'state/slices/portfolioSlice/portfolioSlice' import { store } from 'state/store' @@ -344,19 +348,22 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX const { localWalletType: walletType, localWalletDeviceId, - rdns, setLocalWallet, setLocalNativeWalletName, } = useLocalWallet() const mipdProviders = useMipdProviders() - const maybeMipdProvider = useMemo(() => { - return mipdProviders.find(provider => provider.info.rdns === (state.modalType ?? rdns)) - }, [mipdProviders, rdns, state.modalType]) - const getAdapter: GetAdapter = useCallback( async (keyManager, index = 0) => { + // DO NOT MOVE ME OUTSIDE OF THIS FUNCTION + // This not being a `useSelector` hook is intentional, we are rugged by stale closures here + // and cannot be exhaustive in the deps we use either or the app will be turbo broken + const rdns = selectWalletRdns(store.getState()) + const maybeMipdProvider = mipdProviders.find( + provider => provider.info.rdns === (state.modalType ?? rdns), + ) + let currentStateAdapters = state.adapters // Check if adapter is already in the state @@ -391,7 +398,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX return adapterInstance }, - [isDarkMode, maybeMipdProvider, state.adapters, state.keyring], + [isDarkMode, mipdProviders, state.adapters, state.keyring, state.modalType], ) const disconnect = useCallback(() => { From 2c5942822149a9bea258db5de5fd420d5252e5f6 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:31:02 +1100 Subject: [PATCH 8/9] fix: migrate acknowledgement ui to modal to resolve layout issues (#7953) --- .../Acknowledgement/Acknowledgement.tsx | 219 ++++-------- .../components/LimitOrder/LimitOrder.tsx | 20 +- .../MultiHopTradeConfirm.tsx | 67 ++-- .../components/TradeInput/TradeInput.tsx | 90 +++-- .../TradeAssetSearch/TradeAssetSearch.tsx | 15 +- .../components/CustomAssetAcknowledgement.tsx | 9 +- .../Deposit/components/Deposit.tsx | 33 +- .../Pool/components/Borrow/BorrowInput.tsx | 327 +++++++++--------- .../RFOX/components/Stake/StakeInput.tsx | 211 ++++++----- .../RFOX/components/Unstake/UnstakeInput.tsx | 214 ++++++------ .../AddLiquidity/AddLiquidityInput.tsx | 296 ++++++++-------- 11 files changed, 679 insertions(+), 822 deletions(-) diff --git a/src/components/Acknowledgement/Acknowledgement.tsx b/src/components/Acknowledgement/Acknowledgement.tsx index 7c841f46c0d..b039c7b54c2 100644 --- a/src/components/Acknowledgement/Acknowledgement.tsx +++ b/src/components/Acknowledgement/Acknowledgement.tsx @@ -1,86 +1,25 @@ -import type { BoxProps, ComponentWithAs, IconProps, ThemeTypings } from '@chakra-ui/react' -import { Box, Button, Checkbox, Link, useColorModeValue } from '@chakra-ui/react' -import type { AnimationDefinition, MotionStyle } from 'framer-motion' -import { AnimatePresence, motion } from 'framer-motion' +import type { ComponentWithAs, IconProps, ThemeTypings } from '@chakra-ui/react' +import { + Box, + Button, + Checkbox, + Link, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalOverlay, + useColorModeValue, +} from '@chakra-ui/react' import type { InterpolationOptions } from 'node-polyglot' -import type { PropsWithChildren } from 'react' -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { FiAlertTriangle } from 'react-icons/fi' import { useTranslate } from 'react-polyglot' import { StreamIcon } from 'components/Icons/Stream' import { RawText, Text } from 'components/Text' import { formatSecondsToDuration } from 'lib/utils/time' -const initialProps = { opacity: 0 } -const animateProps = { opacity: 1 } -const exitProps = { opacity: 0, transition: { duration: 0.5 } } -const transitionProps = { delay: 0.2, duration: 0.1 } -const motionStyle: MotionStyle = { - backgroundColor: 'var(--chakra-colors-blanket)', - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - zIndex: 4, -} - -const AcknowledgementOverlay: React.FC = ({ children }) => { - return ( - - {children} - - ) -} - -const popoverVariants = { - initial: { - y: '100%', - }, - animate: { - y: 0, - transition: { - type: 'spring', - bounce: 0.2, - duration: 0.55, - }, - }, - exit: { - y: '100%', - opacity: 0, - transition: { - duration: 0.2, - }, - }, -} - -const popoverStyle: MotionStyle = { - backgroundColor: 'var(--chakra-colors-background-surface-overlay-base)', - position: 'absolute', - borderTopLeftRadius: 'var(--chakra-radii-2xl)', - borderTopRightRadius: 'var(--chakra-radii-2xl)', - bottom: 0, - left: 0, - right: 0, - zIndex: 5, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - paddingLeft: '2rem', - paddingRight: '2rem', - paddingBottom: '2rem', - paddingTop: '4rem', -} - type AcknowledgementProps = { - children: React.ReactNode content?: JSX.Element message: string | JSX.Element onAcknowledge: (() => void) | undefined @@ -91,7 +30,6 @@ type AcknowledgementProps = { buttonTranslation?: string | [string, InterpolationOptions] icon?: ComponentWithAs<'svg', IconProps> disableButton?: boolean - boxProps?: BoxProps } type StreamingAcknowledgementProps = Omit & { @@ -100,10 +38,8 @@ type StreamingAcknowledgementProps = Omit & { type ArbitrumAcknowledgementProps = Omit const cancelHoverProps = { bg: 'rgba(255, 255, 255, 0.2)' } -const boxBorderRadius = { base: 'none', md: 'xl' } export const Acknowledgement = ({ - children, content, message, onAcknowledge, @@ -114,10 +50,8 @@ export const Acknowledgement = ({ buttonTranslation, disableButton, icon: CustomIcon, - boxProps, }: AcknowledgementProps) => { const translate = useTranslate() - const [isShowing, setIsShowing] = useState(false) const understandHoverProps = useMemo( () => ({ bg: `${buttonColorScheme}.600` }), @@ -135,88 +69,51 @@ export const Acknowledgement = ({ setShouldShowAcknowledgement(false) }, [setShouldShowAcknowledgement]) - const handleAnimationComplete = useCallback((def: AnimationDefinition) => { - if (def === 'exit') { - setIsShowing(false) - } - }, []) - - useEffect(() => { - // enters with overflow: hidden - // exit after animation complete return to overflow: visible - if (shouldShowAcknowledgement) { - setIsShowing(true) - } - }, [shouldShowAcknowledgement]) - return ( - - - {shouldShowAcknowledgement && ( - - - {CustomIcon ? ( - - ) : ( - - )} - - - {message} - {content} - - - - - - )} - - - {children} - + + + + + {CustomIcon ? ( + + ) : ( + + )} + + + {message} + {content} + + + + + + + + + ) } diff --git a/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx b/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx index d378f512cb9..8db0a52def1 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/LimitOrder.tsx @@ -29,10 +29,6 @@ import { useAccountIds } from '../../hooks/useAccountIds' import { SharedTradeInput } from '../SharedTradeInput/SharedTradeInput' const votingPowerParams: { feeModel: ParameterModel } = { feeModel: 'SWAPPER' } -const acknowledgementBoxProps = { - display: 'flex', - justifyContent: 'center', -} type LimitOrderProps = { tradeInputRef: React.MutableRefObject @@ -157,13 +153,13 @@ export const LimitOrder = ({ isCompact, tradeInputRef, onChangeTab }: LimitOrder ) return ( - + <> + - + ) } diff --git a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx index d6e7f6a573f..24795f80c49 100644 --- a/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx +++ b/src/components/MultiHopTrade/components/MultiHopTradeConfirm/MultiHopTradeConfirm.tsx @@ -111,40 +111,39 @@ export const MultiHopTradeConfirm = memo(() => { onAcknowledge={handleTradeConfirm} shouldShowAcknowledgement={shouldShowWarningAcknowledgement} setShouldShowAcknowledgement={setShouldShowWarningAcknowledgement} - > - - - - - - - - {isTradeComplete ? ( - - - - ) : ( - <> - - - -