diff --git a/package.json b/package.json index 62164b15780..81b9053eabe 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@shapeshiftoss/hdwallet-native": "^1.21.2", "@shapeshiftoss/hdwallet-native-vault": "^1.21.2", "@shapeshiftoss/hdwallet-portis": "^1.21.2", + "@shapeshiftoss/hdwallet-xdefi": "^1.21.2", "@shapeshiftoss/investor-foxy": "^3.0.0", "@shapeshiftoss/investor-yearn": "^2.0.0", "@shapeshiftoss/logger": "^1.1.2", diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 6e2c8c0ea5f..d6d0caf7bba 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -781,6 +781,28 @@ "body": "Unable to connect Portis wallet" } }, + "xdefi": { + "errors": { + "unknown": "An unexpected error occurred communicating with XDEFI", + "connectFailure": "Unable to connect XDEFI wallet", + "network": "XDEFI isn't set to Ethereum Mainnet", + "multipleWallets": "Detected Ethereum provider is not XDEFI. Do you have multiple wallets installed?" + }, + "connect": { + "header": "Pair XDEFI", + "body": "Click Pair and login to XDEFI from the popup window", + "button": "Pair" + }, + "failure": { + "header": "Error", + "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 new file mode 100644 index 00000000000..526b433c08f --- /dev/null +++ b/src/components/Icons/XDEFIIcon.tsx @@ -0,0 +1,32 @@ +import { createIcon } from '@chakra-ui/react' + +export const XDEFIIcon = createIcon({ + displayName: 'XDeFiIcon', + path: ( + + + + + + + ), + viewBox: '0 0 318.6 318.6', +}) diff --git a/src/context/WalletProvider/KeyManager.ts b/src/context/WalletProvider/KeyManager.ts index 306a7844df8..2292db9a513 100644 --- a/src/context/WalletProvider/KeyManager.ts +++ b/src/context/WalletProvider/KeyManager.ts @@ -4,4 +4,5 @@ export enum KeyManager { MetaMask = 'metamask', Portis = 'portis', Demo = 'demo', + XDefi = 'xdefi', } diff --git a/src/context/WalletProvider/WalletProvider.tsx b/src/context/WalletProvider/WalletProvider.tsx index eb963f2130d..4d67169f964 100644 --- a/src/context/WalletProvider/WalletProvider.tsx +++ b/src/context/WalletProvider/WalletProvider.tsx @@ -381,6 +381,31 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX } dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) break + case KeyManager.XDefi: + const localXDEFIWallet = await state.adapters.get(KeyManager.XDefi)?.pairDevice() + 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, + }, + }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + } catch (e) { + disconnect() + } + } else { + disconnect() + } + dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) + break default: /** * The fall-through case also handles clearing diff --git a/src/context/WalletProvider/XDEFI/components/Connect.tsx b/src/context/WalletProvider/XDEFI/components/Connect.tsx new file mode 100644 index 00000000000..8d3cf55dc77 --- /dev/null +++ b/src/context/WalletProvider/XDEFI/components/Connect.tsx @@ -0,0 +1,113 @@ +import { XDEFIHDWallet } from '@shapeshiftoss/hdwallet-xdefi' +import React, { useState } from 'react' +import { RouteComponentProps } from 'react-router-dom' +import { ActionTypes, WalletActions } from 'context/WalletProvider/actions' +import { KeyManager } from 'context/WalletProvider/KeyManager' +import { setLocalWalletTypeAndDeviceId } from 'context/WalletProvider/local-wallet' +import { useWallet } from 'hooks/useWallet/useWallet' + +import { ConnectModal } from '../../components/ConnectModal' +import { 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 { dispatch, state } = useWallet() + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + let provider: any + + // eslint-disable-next-line no-sequences + const setErrorLoading = (e: string | null) => (setError(e), setLoading(false)) + + const pairDevice = async () => { + setError(null) + setLoading(true) + + try { + provider = (globalThis as any).xfi && (globalThis as any).xfi.ethereum + } catch (error) { + throw new Error('walletProvider.xdefi.errors.connectFailure') + } + + if (state.adapters && state.adapters?.has(KeyManager.XDefi)) { + try { + const wallet = (await state.adapters.get(KeyManager.XDefi)?.pairDevice()) as + | XDEFIHDWallet + | undefined + if (!wallet) { + setErrorLoading('walletProvider.errors.walletNotFound') + throw new Error('Call to hdwallet-xdefi::pairDevice returned null or undefined') + } + + const { name, icon } = XDEFIConfig + + const deviceId = await wallet.getDeviceID() + + if (provider !== (globalThis as any).xfi.ethereum) { + throw new Error('walletProvider.xdefi.errors.multipleWallets') + } + + if (provider?.chainId !== 1) { + throw new Error('walletProvider.xdefi.errors.network') + } + + // Hack to handle XDEFI account changes + //TODO: handle this properly + const resetState = () => dispatch({ type: WalletActions.RESET_STATE }) + provider?.on?.('accountsChanged', resetState) + provider?.on?.('chainChanged', resetState) + + const oldDisconnect = wallet.disconnect.bind(wallet) + wallet.disconnect = () => { + provider?.removeListener?.('accountsChanged', resetState) + provider?.removeListener?.('chainChanged', resetState) + return oldDisconnect() + } + + await wallet.initialize() + + dispatch({ + type: WalletActions.SET_WALLET, + payload: { wallet, name, icon, deviceId }, + }) + 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('XDEFI Connect: There was an error initializing the wallet', 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) + } + + return ( + + ) +} diff --git a/src/context/WalletProvider/XDEFI/components/Failure.tsx b/src/context/WalletProvider/XDEFI/components/Failure.tsx new file mode 100644 index 00000000000..1d36059b6e2 --- /dev/null +++ b/src/context/WalletProvider/XDEFI/components/Failure.tsx @@ -0,0 +1,10 @@ +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 new file mode 100644 index 00000000000..0bc1f84659e --- /dev/null +++ b/src/context/WalletProvider/XDEFI/config.ts @@ -0,0 +1,8 @@ +import { XDEFIAdapter } from '@shapeshiftoss/hdwallet-xdefi' +import { XDEFIIcon } from 'components/Icons/XDEFIIcon' + +export const XDEFIConfig = { + adapter: XDEFIAdapter, + icon: XDEFIIcon, + name: 'XDEFI', +} diff --git a/src/context/WalletProvider/config.ts b/src/context/WalletProvider/config.ts index f7d8f965a3e..ff4d1fe0a37 100644 --- a/src/context/WalletProvider/config.ts +++ b/src/context/WalletProvider/config.ts @@ -34,6 +34,9 @@ import { NativeConfig } from './NativeWallet/config' import { PortisConnect } from './Portis/components/Connect' import { PortisFailure } from './Portis/components/Failure' import { PortisConfig } from './Portis/config' +import { XDEFIConnect } from './XDEFI/components/Connect' +import { XDEFIFailure } from './XDEFI/components/Failure' +import { XDEFIConfig } from './XDEFI/config' export interface SupportedWalletInfo { adapter: any @@ -89,6 +92,13 @@ export const SUPPORTED_WALLETS: Record = { { path: '/portis/failure', component: PortisFailure }, ], }, + [KeyManager.XDefi]: { + ...XDEFIConfig, + routes: [ + { path: '/xdefi/connect', component: XDEFIConnect }, + { path: '/xdefi/failure', component: XDEFIFailure }, + ], + }, [KeyManager.Demo]: { ...DemoConfig, routes: [], diff --git a/src/hooks/useBalanceChartData/useBalanceChartData.ts b/src/hooks/useBalanceChartData/useBalanceChartData.ts index ad43b55c1d6..9e5eb5b5227 100644 --- a/src/hooks/useBalanceChartData/useBalanceChartData.ts +++ b/src/hooks/useBalanceChartData/useBalanceChartData.ts @@ -407,7 +407,6 @@ export const useBalanceChartData: UseBalanceChartData = args => { const hasNoDeviceId = isNil(walletInfo?.deviceId) const hasNoAssetIds = !assetIds.length const hasNoPriceHistoryData = isEmpty(cryptoPriceHistoryData) || !fiatPriceHistoryData?.length - if (hasNoDeviceId || hasNoAssetIds || hasNoPriceHistoryData) { return setBalanceChartDataLoading(true) } diff --git a/yarn.lock b/yarn.lock index 4731cd7b4b9..9e53264160c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4004,6 +4004,14 @@ p-lazy "^3.1.0" web3 "^1.5.1" +"@shapeshiftoss/hdwallet-xdefi@^1.21.2": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shapeshiftoss/hdwallet-xdefi/-/hdwallet-xdefi-1.22.0.tgz#74b39347bb39d165cdc4cc0a90b786b2bef7a6d2" + integrity sha512-XsczkfSteFLhNNgWEKiOqJO6ZPWTwJB9jevwxPks+yOzk4E/tEoGRRyybI3BJgnmnqCghb+7OwbIAhEnMgf4dA== + dependencies: + "@shapeshiftoss/hdwallet-core" "1.22.0" + lodash "^4.17.21" + "@shapeshiftoss/investor-foxy@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@shapeshiftoss/investor-foxy/-/investor-foxy-3.0.1.tgz#64ae368f117ad98c5933b7bc2baff5798e880641"