From c790afb76a572f372a694ce0ba5c15098487d64c Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Thu, 19 Oct 2023 00:43:49 +0200 Subject: [PATCH] feat: walletprovider adapters type narrowing (#5468) * wip: walletprovider type narrowing * feat: cast KKRestAdapter type * feat: cast multichain adapter as MetaMaskAdapter * feat: rm ts-ignore * fix: infinite dispatch * feat: rm useless ts-ignore * wip * feat: gm, she compiles again * feat: cleanup --------- Co-authored-by: woodenfurniture <125113430+woodenfurniture@users.noreply.github.com> Co-authored-by: Apotheosis <97164662+0xApotheosis@users.noreply.github.com> --- src/context/WalletProvider/Coinbase/config.ts | 5 +- .../WalletProvider/DemoWallet/config.ts | 5 +- .../KeepKey/components/Connect.tsx | 7 +- src/context/WalletProvider/KeepKey/config.ts | 9 +- src/context/WalletProvider/Keplr/config.ts | 5 +- src/context/WalletProvider/Ledger/config.ts | 5 +- src/context/WalletProvider/MetaMask/config.ts | 9 +- .../MobileWallet/components/MobileLoad.tsx | 7 +- .../WalletProvider/MobileWallet/config.ts | 5 +- .../NativeWallet/components/NativeLoad.tsx | 4 +- .../WalletProvider/NativeWallet/config.ts | 5 +- .../WalletProvider/WalletConnectV2/config.ts | 5 +- src/context/WalletProvider/WalletContext.tsx | 3 +- src/context/WalletProvider/WalletProvider.tsx | 559 ++++++++++-------- src/context/WalletProvider/XDEFI/config.ts | 5 +- src/context/WalletProvider/actions.ts | 5 +- src/context/WalletProvider/config.ts | 36 +- src/context/WalletProvider/types.ts | 29 + 18 files changed, 433 insertions(+), 275 deletions(-) create mode 100644 src/context/WalletProvider/types.ts diff --git a/src/context/WalletProvider/Coinbase/config.ts b/src/context/WalletProvider/Coinbase/config.ts index 3bd4c962415..e15368c503b 100644 --- a/src/context/WalletProvider/Coinbase/config.ts +++ b/src/context/WalletProvider/Coinbase/config.ts @@ -1,7 +1,10 @@ +import type { CoinbaseAdapter } from '@shapeshiftoss/hdwallet-coinbase' import { CoinbaseIcon } from 'components/Icons/CoinbaseIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const CoinbaseConfig: Omit = { +type CoinbaseConfigType = Omit, 'routes'> + +export const CoinbaseConfig: CoinbaseConfigType = { adapters: [ { loadAdapter: () => import('@shapeshiftoss/hdwallet-coinbase').then(m => m.CoinbaseAdapter), diff --git a/src/context/WalletProvider/DemoWallet/config.ts b/src/context/WalletProvider/DemoWallet/config.ts index 7b4d55aa3c5..9b29cd29f51 100644 --- a/src/context/WalletProvider/DemoWallet/config.ts +++ b/src/context/WalletProvider/DemoWallet/config.ts @@ -1,7 +1,10 @@ +import type { NativeAdapter } from '@shapeshiftoss/hdwallet-native' import { FoxIcon } from 'components/Icons/FoxIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const DemoConfig: Omit = { +type DemoConfigType = Omit, 'routes'> + +export const DemoConfig: DemoConfigType = { adapters: [ { loadAdapter: () => import('@shapeshiftoss/hdwallet-native').then(m => m.NativeAdapter), diff --git a/src/context/WalletProvider/KeepKey/components/Connect.tsx b/src/context/WalletProvider/KeepKey/components/Connect.tsx index 28b3873695a..b348bcdf592 100644 --- a/src/context/WalletProvider/KeepKey/components/Connect.tsx +++ b/src/context/WalletProvider/KeepKey/components/Connect.tsx @@ -6,6 +6,7 @@ import { ModalBody, ModalHeader, } from '@chakra-ui/react' +import type { KkRestAdapter } from '@keepkey/hdwallet-keepkey-rest' import type { Event } from '@shapeshiftoss/hdwallet-core' import { useCallback, useState } from 'react' import { CircularProgress } from 'components/CircularProgress/CircularProgress' @@ -54,11 +55,12 @@ export const KeepKeyConnect = () => { setError(null) setLoading(true) - const firstAdapter = await getAdapter(KeyManager.KeepKey) + const firstAdapter = (await getAdapter(KeyManager.KeepKey)) as KkRestAdapter | null if (firstAdapter) { const wallet = await (async () => { try { const sdk = await setupKeepKeySDK() + if (!sdk) throw new Error('Failed to setup KeepKey SDK') const wallet = await firstAdapter.pairDevice(sdk) if (!wallet) { setErrorLoading('walletProvider.errors.walletNotFound') @@ -67,7 +69,8 @@ export const KeepKeyConnect = () => { return wallet } catch (e) { const secondAdapter = await getAdapter(KeyManager.KeepKey, 1) - const wallet = await secondAdapter?.pairDevice().catch((err: Error) => { + // @ts-ignore TODO(gomes): FIXME, most likely borked because of WebUSBKeepKeyAdapter + const wallet = await secondAdapter.pairDevice().catch(err => { if (err.name === 'ConflictingApp') { setErrorLoading('walletProvider.keepKey.connect.conflictingApp') return diff --git a/src/context/WalletProvider/KeepKey/config.ts b/src/context/WalletProvider/KeepKey/config.ts index 15c46e95f91..bba4e42846c 100644 --- a/src/context/WalletProvider/KeepKey/config.ts +++ b/src/context/WalletProvider/KeepKey/config.ts @@ -1,7 +1,14 @@ +import type { KkRestAdapter } from '@keepkey/hdwallet-keepkey-rest' +import type { WebUSBKeepKeyAdapter } from '@shapeshiftoss/hdwallet-keepkey-webusb' import { KeepKeyIcon } from 'components/Icons/KeepKeyIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const KeepKeyConfig: Omit = { +type KeepKeyConfigType = Omit< + SupportedWalletInfo, + 'routes' +> + +export const KeepKeyConfig: KeepKeyConfigType = { adapters: [ { loadAdapter: () => import('@keepkey/hdwallet-keepkey-rest').then(m => m.KkRestAdapter), diff --git a/src/context/WalletProvider/Keplr/config.ts b/src/context/WalletProvider/Keplr/config.ts index af5b0bf502c..15910552d1f 100644 --- a/src/context/WalletProvider/Keplr/config.ts +++ b/src/context/WalletProvider/Keplr/config.ts @@ -1,7 +1,10 @@ +import type { KeplrAdapter } from '@shapeshiftoss/hdwallet-keplr' import { KeplrIcon } from 'components/Icons/KeplrIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const KeplrConfig: Omit = { +type KeplrConfigType = Omit, 'routes'> + +export const KeplrConfig: KeplrConfigType = { adapters: [ { loadAdapter: () => import('@shapeshiftoss/hdwallet-keplr').then(m => m.KeplrAdapter), diff --git a/src/context/WalletProvider/Ledger/config.ts b/src/context/WalletProvider/Ledger/config.ts index 790986e6990..09191864ea6 100644 --- a/src/context/WalletProvider/Ledger/config.ts +++ b/src/context/WalletProvider/Ledger/config.ts @@ -1,7 +1,10 @@ +import type { WebUSBLedgerAdapter } from '@shapeshiftoss/hdwallet-ledger-webusb' import { LedgerIcon } from 'components/Icons/LedgerIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const LedgerConfig: Omit = { +type LedgerConfigType = Omit, 'routes'> + +export const LedgerConfig: LedgerConfigType = { adapters: [ { loadAdapter: () => diff --git a/src/context/WalletProvider/MetaMask/config.ts b/src/context/WalletProvider/MetaMask/config.ts index 0b078f552c5..fa183c97024 100644 --- a/src/context/WalletProvider/MetaMask/config.ts +++ b/src/context/WalletProvider/MetaMask/config.ts @@ -1,13 +1,18 @@ +import type { MetaMaskAdapter } from '@shapeshiftoss/hdwallet-metamask' import { getConfig } from 'config' import { MetaMaskIcon } from 'components/Icons/MetaMaskIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const MetaMaskConfig: Omit = { +type MetaMaskConfigType = Omit, 'routes'> + +export const MetaMaskConfig: MetaMaskConfigType = { adapters: [ { loadAdapter: () => getConfig().REACT_APP_EXPERIMENTAL_MM_SNAPPY_FINGERS - ? import('@shapeshiftoss/hdwallet-shapeshift-multichain').then(m => m.MetaMaskAdapter) + ? import('@shapeshiftoss/hdwallet-shapeshift-multichain').then( + m => m.MetaMaskAdapter as typeof MetaMaskAdapter, + ) : import('@shapeshiftoss/hdwallet-metamask').then(m => m.MetaMaskAdapter), }, ], diff --git a/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx b/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx index 619f65cf09c..f15273a6922 100644 --- a/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx +++ b/src/context/WalletProvider/MobileWallet/components/MobileLoad.tsx @@ -144,11 +144,12 @@ export const MobileLoad = ({ history }: RouteComponentProps) => { try { const revoker = await getWallet(deviceId) if (!revoker?.mnemonic) throw new Error(`Mobile wallet not found: ${deviceId}`) + if (!revoker?.id) throw new Error(`Revoker ID not found: ${deviceId}`) const wallet = await adapter.pairDevice(revoker.id) - await wallet.loadDevice({ mnemonic: revoker.mnemonic }) - if (!(await wallet.isInitialized())) { - await wallet.initialize() + await wallet?.loadDevice({ mnemonic: revoker.mnemonic }) + if (!(await wallet?.isInitialized())) { + await wallet?.initialize() } dispatch({ type: WalletActions.SET_WALLET, diff --git a/src/context/WalletProvider/MobileWallet/config.ts b/src/context/WalletProvider/MobileWallet/config.ts index dc79935ee61..42ef0fd351d 100644 --- a/src/context/WalletProvider/MobileWallet/config.ts +++ b/src/context/WalletProvider/MobileWallet/config.ts @@ -1,7 +1,10 @@ +import type { NativeAdapter } from '@shapeshiftoss/hdwallet-native' import { FoxIcon } from 'components/Icons/FoxIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const MobileConfig: Omit = { +type MobileConfigType = Omit, 'routes'> + +export const MobileConfig: MobileConfigType = { adapters: [ { loadAdapter: () => import('@shapeshiftoss/hdwallet-native').then(m => m.NativeAdapter), diff --git a/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx b/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx index 5ed1693ee12..0e8edbf8a91 100644 --- a/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx +++ b/src/context/WalletProvider/NativeWallet/components/NativeLoad.tsx @@ -75,11 +75,11 @@ export const NativeLoad = ({ history }: RouteComponentProps) => { const { name, icon } = NativeConfig try { const wallet = await adapter.pairDevice(deviceId) - if (!(await wallet.isInitialized())) { + if (!(await wallet?.isInitialized())) { // This will trigger the password modal and the modal will set the wallet on state // after the wallet has been decrypted. If we set it now, `getPublicKeys` calls will // return null, and we don't have a retry mechanism - await wallet.initialize() + await wallet?.initialize() } else { dispatch({ type: WalletActions.SET_WALLET, diff --git a/src/context/WalletProvider/NativeWallet/config.ts b/src/context/WalletProvider/NativeWallet/config.ts index 52c1f9a44c9..8d588ebb8ca 100644 --- a/src/context/WalletProvider/NativeWallet/config.ts +++ b/src/context/WalletProvider/NativeWallet/config.ts @@ -1,7 +1,10 @@ +import type { NativeAdapter } from '@shapeshiftoss/hdwallet-native' import { FoxIcon } from 'components/Icons/FoxIcon' // Ensure the import path is correct import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const NativeConfig: Omit = { +type NativeConfigType = Omit, 'routes'> + +export const NativeConfig: NativeConfigType = { adapters: [ { loadAdapter: () => import('@shapeshiftoss/hdwallet-native').then(m => m.NativeAdapter), diff --git a/src/context/WalletProvider/WalletConnectV2/config.ts b/src/context/WalletProvider/WalletConnectV2/config.ts index 9b86d655567..d2d7cb6a677 100644 --- a/src/context/WalletProvider/WalletConnectV2/config.ts +++ b/src/context/WalletProvider/WalletConnectV2/config.ts @@ -1,4 +1,5 @@ import { CHAIN_REFERENCE } from '@shapeshiftoss/caip' +import type { WalletConnectV2Adapter } from '@shapeshiftoss/hdwallet-walletconnectv2' import { getConfig } from 'config' import type { Chain } from 'viem/chains' import { arbitrum, avalanche, bsc, gnosis, mainnet, optimism, polygon } from 'viem/chains' @@ -7,7 +8,9 @@ import type { SupportedWalletInfo } from 'context/WalletProvider/config' import type { EthereumProviderOptions } from './constants' -export const WalletConnectV2Config: Omit = { +type WalletConnectV2ConfigType = Omit, 'routes'> + +export const WalletConnectV2Config: WalletConnectV2ConfigType = { adapters: [ { loadAdapter: () => diff --git a/src/context/WalletProvider/WalletContext.tsx b/src/context/WalletProvider/WalletContext.tsx index 4002eeb79ce..c4e9cbf23a3 100644 --- a/src/context/WalletProvider/WalletContext.tsx +++ b/src/context/WalletProvider/WalletContext.tsx @@ -3,11 +3,12 @@ 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' export interface IWalletContext { state: InitialState - getAdapter: (keyManager: KeyManager, index?: number) => Promise + getAdapter: GetAdapter dispatch: React.Dispatch connect: (adapter: KeyManager) => void create: (adapter: KeyManager) => void diff --git a/src/context/WalletProvider/WalletProvider.tsx b/src/context/WalletProvider/WalletProvider.tsx index 33ca1a35b38..a844f204f36 100644 --- a/src/context/WalletProvider/WalletProvider.tsx +++ b/src/context/WalletProvider/WalletProvider.tsx @@ -40,17 +40,11 @@ import { setLocalWalletTypeAndDeviceId, } from './local-wallet' import { useNativeEventHandler } from './NativeWallet/hooks/useNativeEventHandler' +import type { AdaptersByKeyManager, GetAdapter } from './types' import type { IWalletContext } from './WalletContext' import { WalletContext } from './WalletContext' import { WalletViewsRouter } from './WalletViewsRouter' -type GenericAdapter = { - initialize: (...args: any[]) => Promise - pairDevice: (...args: any[]) => Promise -} - -export type Adapters = Map - export type WalletInfo = { name: string icon: ComponentWithAs<'svg', IconProps> @@ -95,7 +89,7 @@ export type KeyManagerWithProvider = export interface InitialState { keyring: Keyring - adapters: Adapters | null + adapters: Partial wallet: HDWallet | null modalType: KeyManager | null connectedType: KeyManager | null @@ -116,7 +110,7 @@ export interface InitialState { const initialState: InitialState = { keyring: new Keyring(), - adapters: null, + adapters: {}, wallet: null, modalType: null, connectedType: null, @@ -359,28 +353,25 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX // Internal state, for memoization purposes only const [walletType, setWalletType] = useState(null) - const getAdapter = useCallback( - async (keyManager: KeyManager, index: number = 0) => { - let currentStateAdapters = state.adapters ?? new Map() + const getAdapter: GetAdapter = useCallback( + async (keyManager, index = 0) => { + let currentStateAdapters = state.adapters // Check if adapter is already in the state - let adapterInstance = currentStateAdapters.get(keyManager)?.[index] - const currentKeyManagerAdapters = currentStateAdapters.get(keyManager) ?? [] + let adapterInstance = currentStateAdapters[keyManager] if (!adapterInstance) { // If not, create a new instance of the adapter try { const Adapter = await SUPPORTED_WALLETS[keyManager].adapters[index].loadAdapter() + const keyManagerOptions = 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 - adapterInstance = Adapter.useKeyring( - state.keyring, - getKeyManagerOptions(keyManager, isDarkMode), - ) + adapterInstance = Adapter.useKeyring(state.keyring, keyManagerOptions) if (adapterInstance) { - currentKeyManagerAdapters[index] = adapterInstance - currentStateAdapters.set(keyManager, currentKeyManagerAdapters) + currentStateAdapters[keyManager] = adapterInstance // Set it in wallet state for later use dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentStateAdapters }) } @@ -390,6 +381,8 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX } } + if (!adapterInstance) return null + return adapterInstance }, [isDarkMode, state.adapters, state.keyring], @@ -410,261 +403,325 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX const localWalletDeviceId = getLocalWalletDeviceId() if (localWalletType && localWalletDeviceId) { ;(async () => { - const currentAdapters = state.adapters ?? new Map() - const adapter = await getAdapter(localWalletType) - - if (adapter) { - try { - currentAdapters.set(localWalletType, [adapter]) - dispatch({ type: WalletActions.SET_ADAPTERS, payload: currentAdapters }) - } catch (e) { - console.error(e) - } - // Fixes issue with wallet `type` being null when the wallet is loaded from state - dispatch({ type: WalletActions.SET_CONNECTOR_TYPE, payload: localWalletType }) + const currentAdapters = state.adapters ?? ({} as AdaptersByKeyManager) - switch (localWalletType) { - case KeyManager.Mobile: - try { - const w = await getWallet(localWalletDeviceId) - if (w && w.mnemonic && w.label) { - const localMobileWallet = await adapter.pairDevice(localWalletDeviceId) - - if (localMobileWallet) { - localMobileWallet.loadDevice({ label: w.label, mnemonic: w.mnemonic }) - const { name, icon } = MobileConfig - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localMobileWallet, - name, - icon, - deviceId: w.id || localWalletDeviceId, - meta: { label: w.label }, - connectedType: KeyManager.Mobile, - }, - }) - 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 { - disconnect() - } - } else { - // in the case we return a null from the mobile app and fail to get the wallet - // we want to disconnect and return the user back to the splash screen - disconnect() - } - } catch (e) { - console.error(e) - } - break - case KeyManager.Native: - const localNativeWallet = await adapter.pairDevice(localWalletDeviceId) - if (localNativeWallet) { - /** - * This will eventually fire an event, which the ShapeShift wallet - * password modal will be shown - */ - await localNativeWallet.initialize() - } else { - disconnect() + switch (localWalletType) { + case KeyManager.Mobile: + try { + // Get the adapter again in each switch case to narrow down the adapter type + const mobileAdapter = await getAdapter(localWalletType, 0) + + if (mobileAdapter) { + 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 }) } - break - // We don't want to pairDevice() for ledger here - this will run on app load and won't work, as WebUSB `requestPermission` must be - // called from a user gesture. Instead, we'll pair the device when the user clicks the "Pair Device` button in Ledger `` - // case KeyManager.Ledger: - // const ledgerWallet = await state.adapters.get(KeyManager.Ledger)?.[0].pairDevice() - // return ledgerWallet - case KeyManager.KeepKey: - try { - const localKeepKeyWallet = await (async () => { - const maybeWallet = state.keyring.get(localWalletDeviceId) - if (maybeWallet) return maybeWallet - const keepKeyAdapters = state.adapters?.get(KeyManager.KeepKey) - if (!keepKeyAdapters) return - const sdk = await setupKeepKeySDK() - return await keepKeyAdapters[0]?.pairDevice(sdk) - })() - - /** - * if localKeepKeyWallet is not null it means - * KeepKey remained connected during the reload - */ - if (localKeepKeyWallet) { - const { name, icon } = SUPPORTED_WALLETS[KeyManager.KeepKey] - const deviceId = await localKeepKeyWallet.getDeviceID() - // This gets the firmware version needed for some KeepKey "supportsX" functions - await localKeepKeyWallet.getFeatures() - // Show the label from the wallet instead of a generic name - const label = (await localKeepKeyWallet.getLabel()) || name - - await localKeepKeyWallet.initialize() + const w = await getWallet(localWalletDeviceId) + if (w && w.mnemonic && w.label) { + const localMobileWallet = await mobileAdapter?.pairDevice(localWalletDeviceId) + if (localMobileWallet) { + localMobileWallet.loadDevice({ label: w.label, mnemonic: w.mnemonic }) + const { name, icon } = MobileConfig dispatch({ type: WalletActions.SET_WALLET, payload: { - wallet: localKeepKeyWallet, + wallet: localMobileWallet, name, icon, - deviceId, - meta: { label }, - connectedType: KeyManager.KeepKey, + deviceId: w.id || localWalletDeviceId, + meta: { label: w.label }, + connectedType: KeyManager.Mobile, }, }) 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 { disconnect() } - } catch (e) { + } else { + // in the case we return a null from the mobile app and fail to get the wallet + // we want to disconnect and return the user back to the splash screen disconnect() } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break - case KeyManager.MetaMask: - const localMetaMaskWallet = await adapter?.pairDevice() - if (localMetaMaskWallet) { - const { name, icon } = SUPPORTED_WALLETS[KeyManager.MetaMask] - try { - await localMetaMaskWallet.initialize() - const deviceId = await localMetaMaskWallet.getDeviceID() - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localMetaMaskWallet, - name, - icon, - deviceId, - connectedType: KeyManager.MetaMask, - }, - }) - dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - } catch (e) { - disconnect() - } + } catch (e) { + console.error(e) + } + break + case KeyManager.Native: + // Get the adapter again in each switch case to narrow down the adapter type + const nativeAdapter = await getAdapter(localWalletType) + if (nativeAdapter) { + 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 }) + } + + const localNativeWallet = await nativeAdapter?.pairDevice(localWalletDeviceId) + if (localNativeWallet) { + /** + * This will eventually fire an event, which the ShapeShift wallet + * password modal will be shown + */ + await localNativeWallet.initialize() + } else { + disconnect() + } + break + // We don't want to pairDevice() for ledger here - this will run on app load and won't work, as WebUSB `requestPermission` must be + // called from a user gesture. Instead, we'll pair the device when the user clicks the "Pair Device` button in Ledger `` + // case KeyManager.Ledger: + // const ledgerWallet = await state.adapters.get(KeyManager.Ledger)?.[0].pairDevice() + // return ledgerWallet + case KeyManager.KeepKey: + try { + const localKeepKeyWallet = await (async () => { + const maybeWallet = state.keyring.get(localWalletDeviceId) + if (maybeWallet) return maybeWallet + // Get the adapter again in each switch case to narrow down the adapter type + const keepKeyAdapter = await getAdapter(KeyManager.KeepKey) + if (!keepKeyAdapter) return + + 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 }) + + const sdk = await setupKeepKeySDK() + // @ts-ignore TODO(gomes): FIXME, most likely borked because of WebUSBKeepKeyAdapter + return await keepKeyAdapter.pairDevice(sdk) + })() + + /** + * if localKeepKeyWallet is not null it means + * KeepKey remained connected during the reload + */ + if (localKeepKeyWallet) { + const { name, icon } = SUPPORTED_WALLETS[KeyManager.KeepKey] + const deviceId = await localKeepKeyWallet.getDeviceID() + // This gets the firmware version needed for some KeepKey "supportsX" functions + await localKeepKeyWallet.getFeatures() + // Show the label from the wallet instead of a generic name + const label = (await localKeepKeyWallet.getLabel()) || name + + await localKeepKeyWallet.initialize() + + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: localKeepKeyWallet, + name, + icon, + deviceId, + meta: { label }, + connectedType: KeyManager.KeepKey, + }, + }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) } else { disconnect() } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break - case KeyManager.Coinbase: - const localCoinbaseWallet = await adapter?.pairDevice() - if (localCoinbaseWallet) { - const { name, icon } = SUPPORTED_WALLETS[KeyManager.Coinbase] - try { - await localCoinbaseWallet.initialize() - const deviceId = await localCoinbaseWallet.getDeviceID() - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localCoinbaseWallet, - name, - icon, - deviceId, - connectedType: KeyManager.Coinbase, - }, - }) - dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - } catch (e) { - disconnect() - } - } else { + } catch (e) { + disconnect() + } + dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) + break + case KeyManager.MetaMask: + // Get the adapter again in each switch case to narrow down the adapter type + const metamaskAdapter = await getAdapter(localWalletType) + + if (metamaskAdapter) { + 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 }) + } + + const localMetaMaskWallet = await metamaskAdapter?.pairDevice() + if (localMetaMaskWallet) { + const { name, icon } = SUPPORTED_WALLETS[KeyManager.MetaMask] + try { + await localMetaMaskWallet.initialize() + const deviceId = await localMetaMaskWallet.getDeviceID() + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: localMetaMaskWallet, + name, + icon, + deviceId, + connectedType: KeyManager.MetaMask, + }, + }) + dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + } catch (e) { disconnect() } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break - case KeyManager.XDefi: - const localXDEFIWallet = await adapter?.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, - connectedType: KeyManager.XDefi, - }, - }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - } catch (e) { - disconnect() - } - } else { + } else { + disconnect() + } + 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) + + if (coinbaseAdapter) { + 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 }) + } + + const localCoinbaseWallet = await coinbaseAdapter?.pairDevice() + if (localCoinbaseWallet) { + const { name, icon } = SUPPORTED_WALLETS[KeyManager.Coinbase] + try { + await localCoinbaseWallet.initialize() + const deviceId = await localCoinbaseWallet.getDeviceID() + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: localCoinbaseWallet, + name, + icon, + deviceId, + connectedType: KeyManager.Coinbase, + }, + }) + dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + } catch (e) { disconnect() } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break - case KeyManager.Keplr: - const localKeplrWallet = await adapter?.pairDevice() - if (localKeplrWallet) { - const { name, icon } = SUPPORTED_WALLETS[KeyManager.Keplr] - try { - await localKeplrWallet.initialize() - const deviceId = await localKeplrWallet.getDeviceID() - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localKeplrWallet, - name, - icon, - deviceId, - connectedType: KeyManager.Keplr, - }, - }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - } catch (e) { - disconnect() - } - } else { + } 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() + 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, + }, + }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + } catch (e) { disconnect() } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break - case KeyManager.WalletConnectV2: { - // Re-trigger the modal on refresh - await onProviderChange(KeyManager.WalletConnectV2) - const localWalletConnectWallet = await adapter?.pairDevice() - if (localWalletConnectWallet) { - const { name, icon } = SUPPORTED_WALLETS[KeyManager.WalletConnectV2] - try { - await localWalletConnectWallet.initialize() - const deviceId = await localWalletConnectWallet.getDeviceID() - dispatch({ - type: WalletActions.SET_WALLET, - payload: { - wallet: localWalletConnectWallet, - name, - icon, - deviceId, - connectedType: KeyManager.WalletConnectV2, - }, - }) - dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) - dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) - } catch (e) { - disconnect() - } - } else { + } else { + disconnect() + } + 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) + + if (keplrAdapter) { + 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 }) + } + + const localKeplrWallet = await keplrAdapter?.pairDevice() + if (localKeplrWallet) { + const { name, icon } = SUPPORTED_WALLETS[KeyManager.Keplr] + try { + await localKeplrWallet.initialize() + const deviceId = await localKeplrWallet.getDeviceID() + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: localKeplrWallet, + name, + icon, + deviceId, + connectedType: KeyManager.Keplr, + }, + }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + } catch (e) { disconnect() } - dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) - break + } else { + disconnect() } - default: - /** - * The fall-through case also handles clearing - * any demo wallet state on refresh/rerender. - */ + dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) + break + case KeyManager.WalletConnectV2: { + // Get the adapter again in each switch case to narrow down the adapter type + const walletConnectV2Adapter = await getAdapter(localWalletType) + + if (walletConnectV2Adapter) { + 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 }) + } + + // Re-trigger the modal on refresh + await onProviderChange(KeyManager.WalletConnectV2) + const localWalletConnectWallet = await walletConnectV2Adapter?.pairDevice() + if (localWalletConnectWallet) { + const { name, icon } = SUPPORTED_WALLETS[KeyManager.WalletConnectV2] + try { + await localWalletConnectWallet.initialize() + const deviceId = await localWalletConnectWallet.getDeviceID() + dispatch({ + type: WalletActions.SET_WALLET, + payload: { + wallet: localWalletConnectWallet, + name, + icon, + deviceId, + connectedType: KeyManager.WalletConnectV2, + }, + }) + dispatch({ type: WalletActions.SET_IS_LOCKED, payload: false }) + dispatch({ type: WalletActions.SET_IS_CONNECTED, payload: true }) + } catch (e) { + disconnect() + } + } else { disconnect() - break + } + dispatch({ type: WalletActions.SET_LOCAL_WALLET_LOADING, payload: false }) + break } + default: + /** + * The fall-through case also handles clearing + * any demo wallet state on refresh/rerender. + */ + disconnect() + break } })() } @@ -674,7 +731,8 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX const handleAccountsOrChainChanged = useCallback(async () => { if (!walletType || !state.adapters) return - const localWallet = await state.adapters.get(walletType)?.[0]?.pairDevice() + const adapter = await getAdapter(walletType) + const localWallet = await adapter?.pairDevice() if (!localWallet) return @@ -695,7 +753,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX connectedType: walletType, }, }) - }, [state, walletType]) + }, [getAdapter, state.adapters, walletType]) const setProviderEvents = useCallback( async (maybeProvider: InitialState['provider']) => { @@ -704,7 +762,8 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX maybeProvider?.on?.('accountsChanged', handleAccountsOrChainChanged) maybeProvider?.on?.('chainChanged', handleAccountsOrChainChanged) - const wallet = await state.adapters?.get(walletType)?.[0]?.pairDevice() + const adapter = await getAdapter(walletType) + const wallet = await adapter?.pairDevice() if (wallet) { const oldDisconnect = wallet.disconnect.bind(wallet) wallet.disconnect = () => { @@ -714,7 +773,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX } } }, - [state.adapters, walletType, handleAccountsOrChainChanged], + [walletType, handleAccountsOrChainChanged, getAdapter], ) // Register a MetaMask-like (EIP-1193) provider on wallet connect or load @@ -841,7 +900,7 @@ export const WalletProvider = ({ children }: { children: React.ReactNode }): JSX }) }, []) - useEffect(() => load(), [load, state.adapters, state.keyring]) + useEffect(() => load(), [load, state.keyring]) useKeyringEventHandler(state) useNativeEventHandler(state, dispatch) diff --git a/src/context/WalletProvider/XDEFI/config.ts b/src/context/WalletProvider/XDEFI/config.ts index 5a25bc9765d..82a2710f25e 100644 --- a/src/context/WalletProvider/XDEFI/config.ts +++ b/src/context/WalletProvider/XDEFI/config.ts @@ -1,7 +1,10 @@ +import type { XDEFIAdapter } from '@shapeshiftoss/hdwallet-xdefi' import { XDEFIIcon } from 'components/Icons/XDEFIIcon' import type { SupportedWalletInfo } from 'context/WalletProvider/config' -export const XDEFIConfig: Omit = { +type XDEFIConfigType = Omit, 'routes'> + +export const XDEFIConfig: XDEFIConfigType = { adapters: [ { loadAdapter: () => import('@shapeshiftoss/hdwallet-xdefi').then(m => m.XDEFIAdapter), diff --git a/src/context/WalletProvider/actions.ts b/src/context/WalletProvider/actions.ts index 2d44f456643..e78fcbe4958 100644 --- a/src/context/WalletProvider/actions.ts +++ b/src/context/WalletProvider/actions.ts @@ -2,7 +2,8 @@ import type { HDWallet } from '@shapeshiftoss/hdwallet-core' import type { PinMatrixRequestType } from './KeepKey/KeepKeyTypes' import type { KeyManager } from './KeyManager' -import type { Adapters, DeviceState, InitialState, WalletInfo } from './WalletProvider' +import type { AdaptersByKeyManager } from './types' +import type { DeviceState, InitialState, WalletInfo } from './WalletProvider' export enum WalletActions { SET_ADAPTERS = 'SET_ADAPTERS', @@ -30,7 +31,7 @@ export enum WalletActions { } export type ActionTypes = - | { type: WalletActions.SET_ADAPTERS; payload: Adapters } + | { type: WalletActions.SET_ADAPTERS; payload: Partial } | { type: WalletActions.SET_WALLET payload: WalletInfo & { diff --git a/src/context/WalletProvider/config.ts b/src/context/WalletProvider/config.ts index d19b10ecbd4..db292dfc251 100644 --- a/src/context/WalletProvider/config.ts +++ b/src/context/WalletProvider/config.ts @@ -1,4 +1,14 @@ import type { ComponentWithAs, IconProps } from '@chakra-ui/react' +import type { KkRestAdapter } from '@keepkey/hdwallet-keepkey-rest' +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 { NativeAdapter } from '@shapeshiftoss/hdwallet-native' +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 type { RouteProps } from 'react-router-dom' import { WalletConnectedRoutes } from 'components/Layout/Header/NavBar/hooks/useMenuRoutes' @@ -75,10 +85,9 @@ import { XDEFIConnect } from './XDEFI/components/Connect' import { XDEFIFailure } from './XDEFI/components/Failure' import { XDEFIConfig } from './XDEFI/config' -export interface SupportedWalletInfo { +export type SupportedWalletInfo = { adapters: { - // TODO(gomes): can we type this? - loadAdapter: () => Promise + loadAdapter: () => Promise }[] supportsMobile?: 'browser' | 'app' | 'both' icon: ComponentWithAs<'svg', IconProps> @@ -89,7 +98,26 @@ export interface SupportedWalletInfo { connectedWalletMenuInitialPath?: WalletConnectedRoutes connectedMenuComponent?: React.ComponentType } -export const SUPPORTED_WALLETS: Record = { + +export type SupportedWalletInfoByKeyManager = { + [KeyManager.Coinbase]: SupportedWalletInfo + // Native, Mobile, and Demo wallets are all native wallets + [KeyManager.Native]: SupportedWalletInfo + [KeyManager.Mobile]: SupportedWalletInfo + [KeyManager.Demo]: SupportedWalletInfo + // TODO(gomes): export WebUSBKeepKeyAdapter as a type in hdwallet, not a declare const + // this effectively means we keep on importing the akschual package for now + [KeyManager.KeepKey]: SupportedWalletInfo + [KeyManager.Keplr]: SupportedWalletInfo + [KeyManager.Ledger]: SupportedWalletInfo + [KeyManager.MetaMask]: SupportedWalletInfo< + typeof MetaMaskAdapter | typeof MetaMaskMultiChainAdapter + > + [KeyManager.WalletConnectV2]: SupportedWalletInfo + [KeyManager.XDefi]: SupportedWalletInfo +} + +export const SUPPORTED_WALLETS: SupportedWalletInfoByKeyManager = { [KeyManager.Mobile]: { ...MobileConfig, routes: [ diff --git a/src/context/WalletProvider/types.ts b/src/context/WalletProvider/types.ts new file mode 100644 index 00000000000..efcfe2d02ff --- /dev/null +++ b/src/context/WalletProvider/types.ts @@ -0,0 +1,29 @@ +import type { KkRestAdapter } from '@keepkey/hdwallet-keepkey-rest' +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 { NativeAdapter } from '@shapeshiftoss/hdwallet-native' +import type { WalletConnectV2Adapter } from '@shapeshiftoss/hdwallet-walletconnectv2' +import type { XDEFIAdapter } from '@shapeshiftoss/hdwallet-xdefi' + +import type { KeyManager } from './KeyManager' + +export type AdaptersByKeyManager = { + [KeyManager.Mobile]: NativeAdapter + [KeyManager.Native]: NativeAdapter + [KeyManager.Demo]: NativeAdapter + [KeyManager.KeepKey]: KkRestAdapter | typeof WebUSBKeepKeyAdapter + [KeyManager.Ledger]: WebUSBLedgerAdapter + [KeyManager.Keplr]: KeplrAdapter + [KeyManager.WalletConnectV2]: WalletConnectV2Adapter + [KeyManager.MetaMask]: MetaMaskAdapter + [KeyManager.Coinbase]: CoinbaseAdapter + [KeyManager.XDefi]: XDEFIAdapter +} + +export type GetAdapter = ( + keyManager: K, + index?: K extends KeyManager.KeepKey ? 0 | 1 : 0, // only used for keepkey +) => Promise