From cdc30e9fb3730c77e3689a167f5807ee7b8dbf99 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 13 Nov 2023 12:36:52 +0100 Subject: [PATCH] Fix: remove deprecated mobile pairing --- .../common/AppStoreButton/index.tsx | 1 - .../common/ConnectWallet/styles.module.css | 7 - .../PairingDeprecationWarning.tsx | 12 - .../PairingDetails/PairingDescription.tsx | 24 -- .../common/PairingDetails/PairingQRCode.tsx | 53 ---- src/components/common/WalletInfo/index.tsx | 7 +- .../common/WalletInfo/styles.module.css | 7 + .../WcSessionList/WcNoSessions.tsx | 4 +- src/hooks/wallets/consts.ts | 2 - src/hooks/wallets/useOnboard.ts | 19 +- src/hooks/wallets/wallets.ts | 2 - src/pages/_app.tsx | 3 - src/services/exceptions/ErrorCodes.ts | 1 - src/services/pairing/QRModal.tsx | 55 ---- src/services/pairing/__tests__/utils.test.ts | 103 ------- src/services/pairing/connector.ts | 58 ---- src/services/pairing/hooks.ts | 142 ---------- src/services/pairing/icon.ts | 8 - src/services/pairing/module.ts | 257 ------------------ src/services/pairing/utils.ts | 60 ---- 20 files changed, 14 insertions(+), 811 deletions(-) delete mode 100644 src/components/common/PairingDetails/PairingDeprecationWarning.tsx delete mode 100644 src/components/common/PairingDetails/PairingDescription.tsx delete mode 100644 src/components/common/PairingDetails/PairingQRCode.tsx delete mode 100644 src/services/pairing/QRModal.tsx delete mode 100644 src/services/pairing/__tests__/utils.test.ts delete mode 100644 src/services/pairing/connector.ts delete mode 100644 src/services/pairing/hooks.ts delete mode 100644 src/services/pairing/icon.ts delete mode 100644 src/services/pairing/module.ts delete mode 100644 src/services/pairing/utils.ts diff --git a/src/components/common/AppStoreButton/index.tsx b/src/components/common/AppStoreButton/index.tsx index 6687afe3d2..65fe9eb30a 100644 --- a/src/components/common/AppStoreButton/index.tsx +++ b/src/components/common/AppStoreButton/index.tsx @@ -6,7 +6,6 @@ import { MOBILE_APP_EVENTS, trackEvent } from '@/services/analytics' // App Store campaigns track the user interaction enum LINKS { - pairing = 'https://apps.apple.com/app/apple-store/id1515759131?pt=119497694&ct=Web%20App%20Connect&mt=8', footer = 'https://apps.apple.com/app/apple-store/id1515759131?pt=119497694&ct=Web%20App%20Footer&mt=8', } diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index b41bbc8399..71036103a6 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -64,13 +64,6 @@ border-bottom: 1px solid var(--color-border-light); } -.pairingDetails { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-2); -} - .loginButton { min-height: 42px; } diff --git a/src/components/common/PairingDetails/PairingDeprecationWarning.tsx b/src/components/common/PairingDetails/PairingDeprecationWarning.tsx deleted file mode 100644 index dd720461e0..0000000000 --- a/src/components/common/PairingDetails/PairingDeprecationWarning.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Alert } from '@mui/material' - -const PairingDeprecationWarning = (): React.ReactElement => { - return ( - - The {'Safe{Wallet}'} web-mobile pairing feature will be discontinued from 15th November 2023. Please migrate to a - different signer wallet before this date. - - ) -} - -export default PairingDeprecationWarning diff --git a/src/components/common/PairingDetails/PairingDescription.tsx b/src/components/common/PairingDetails/PairingDescription.tsx deleted file mode 100644 index bdcad66d90..0000000000 --- a/src/components/common/PairingDetails/PairingDescription.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import AppStoreButton from '@/components/common/AppStoreButton' -import ExternalLink from '../ExternalLink' -import { HelpCenterArticle } from '@/config/constants' - -const PairingDescription = (): ReactElement => { - return ( - <> - - Scan this code in the {'Safe{Wallet}'} mobile app to sign transactions with your mobile device. -
- - Learn more about this feature. - -
- - - - ) -} - -export default PairingDescription diff --git a/src/components/common/PairingDetails/PairingQRCode.tsx b/src/components/common/PairingDetails/PairingQRCode.tsx deleted file mode 100644 index a53f5dbce1..0000000000 --- a/src/components/common/PairingDetails/PairingQRCode.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState } from 'react' -import { IconButton, Box } from '@mui/material' -import RefreshIcon from '@mui/icons-material/Refresh' -import type { ReactElement } from 'react' - -import { getPairingConnector, usePairingConnector, WalletConnectEvents } from '@/services/pairing/connector' -import usePairingUri from '@/services/pairing/hooks' -import useChainId from '@/hooks/useChainId' -import QRCode from '@/components/common/QRCode' - -const QR_CODE_SIZE = 100 - -const PairingQRCode = ({ size = QR_CODE_SIZE }: { size?: number }): ReactElement => { - const chainId = useChainId() - const uri = usePairingUri() - const connector = usePairingConnector() - const [displayRefresh, setDisplayRefresh] = useState(false) - - // Workaround because the disconnect listener in useInitPairing is not picking up the event - useEffect(() => { - connector?.on(WalletConnectEvents.DISCONNECT, () => { - setDisplayRefresh(true) - }) - }, [connector]) - - const handleRefresh = () => { - setDisplayRefresh(false) - getPairingConnector()?.createSession({ chainId: +chainId }) - } - - if (displayRefresh || (connector && !connector.handshakeTopic)) { - return ( - theme.palette.background.main, - }} - > - - - - - ) - } - - return -} - -export default PairingQRCode diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index 1afdf0d573..d3e081d7d1 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -59,9 +59,10 @@ export const WalletInfo = ({ const isSocialLogin = isSocialLoginWallet(wallet.label) return ( - <> + + {isSocialLogin ? ( <> @@ -108,10 +109,10 @@ export const WalletInfo = ({ {!IS_PRODUCTION && isSocialLogin && ( )} - + ) } diff --git a/src/components/common/WalletInfo/styles.module.css b/src/components/common/WalletInfo/styles.module.css index a8c3a97308..e12fa9a690 100644 --- a/src/components/common/WalletInfo/styles.module.css +++ b/src/components/common/WalletInfo/styles.module.css @@ -1,3 +1,10 @@ +.container { + width: 100%; + display: flex; + flex-direction: column; + gap: var(--space-2); +} + .accountContainer { width: 100%; margin-bottom: var(--space-1); diff --git a/src/components/walletconnect/WcSessionList/WcNoSessions.tsx b/src/components/walletconnect/WcSessionList/WcNoSessions.tsx index 17d7960b07..e7183678a6 100644 --- a/src/components/walletconnect/WcSessionList/WcNoSessions.tsx +++ b/src/components/walletconnect/WcSessionList/WcNoSessions.tsx @@ -21,9 +21,9 @@ const WcSampleDapps = ({ onUnload }: { onUnload: () => void }) => { return ( {SAMPLE_DAPPS.map((item) => ( - + - {item.name} + {item.name} {item.name} diff --git a/src/hooks/wallets/consts.ts b/src/hooks/wallets/consts.ts index 6bb6a61f93..506fac0513 100644 --- a/src/hooks/wallets/consts.ts +++ b/src/hooks/wallets/consts.ts @@ -3,7 +3,6 @@ export const enum WALLET_KEYS { WALLETCONNECT_V2 = 'WALLETCONNECT_V2', SOCIAL = 'SOCIAL_LOGIN', COINBASE = 'COINBASE', - PAIRING = 'PAIRING', LEDGER = 'LEDGER', TREZOR = 'TREZOR', KEYSTONE = 'KEYSTONE', @@ -14,7 +13,6 @@ export const CGW_NAMES: { [key in WALLET_KEYS]: string | undefined } = { [WALLET_KEYS.INJECTED]: 'detectedwallet', [WALLET_KEYS.WALLETCONNECT_V2]: 'walletConnect_v2', [WALLET_KEYS.COINBASE]: 'coinbase', - [WALLET_KEYS.PAIRING]: 'safeMobile', [WALLET_KEYS.SOCIAL]: 'socialSigner', [WALLET_KEYS.LEDGER]: 'ledger', [WALLET_KEYS.TREZOR]: 'trezor', diff --git a/src/hooks/wallets/useOnboard.ts b/src/hooks/wallets/useOnboard.ts index 4d8f2c1ab9..6d98c20ae0 100644 --- a/src/hooks/wallets/useOnboard.ts +++ b/src/hooks/wallets/useOnboard.ts @@ -6,7 +6,6 @@ import useChains, { useCurrentChain } from '@/hooks/useChains' import ExternalStore from '@/services/ExternalStore' import { logError, Errors } from '@/services/exceptions' import { trackEvent, WALLET_EVENTS } from '@/services/analytics' -import { useInitPairing } from '@/services/pairing/hooks' import { useAppSelector } from '@/store' import { type EnvState, selectRpc } from '@/store/settingsSlice' import { E2E_WALLET_NAME } from '@/tests/e2e-wallet' @@ -43,7 +42,7 @@ export const getConnectedWallet = (wallets: WalletState[]): ConnectedWallet | nu const primaryWallet = wallets[0] if (!primaryWallet) return null - const account = primaryWallet.accounts[0] + const account = primaryWallet.accounts[primaryWallet.accounts.length - 1] if (!account) return null try { @@ -93,21 +92,11 @@ const isMobile = () => /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) // Detect injected wallet const hasInjectedWallet = () => typeof window !== 'undefined' && !!window?.ethereum -// `connectWallet` is called when connecting/switching wallets and on pairing `connect` event (when prev. session connects) -// This re-entrant lock prevents multiple `connectWallet`/tracking calls that would otherwise occur for pairing module -let isConnecting = false - // Wrapper that tracks/sets the last used wallet export const connectWallet = async ( onboard: OnboardAPI, options?: Parameters[0], ): Promise => { - if (isConnecting) { - return - } - - isConnecting = true - // On mobile, automatically choose WalletConnect if there is no injected wallet if (!options && isMobile() && !hasInjectedWallet()) { options = { @@ -121,13 +110,9 @@ export const connectWallet = async ( wallets = await onboard.connectWallet(options) } catch (e) { logError(Errors._302, e) - - isConnecting = false return } - isConnecting = false - return wallets } @@ -153,8 +138,6 @@ export const useInitOnboard = () => { const onboard = useStore() const customRpc = useAppSelector(selectRpc) - useInitPairing() - useEffect(() => { if (configs.length > 0 && chain) { void initOnboard(configs, chain, customRpc) diff --git a/src/hooks/wallets/wallets.ts b/src/hooks/wallets/wallets.ts index 2de9efdd29..4195f57adf 100644 --- a/src/hooks/wallets/wallets.ts +++ b/src/hooks/wallets/wallets.ts @@ -9,7 +9,6 @@ import ledgerModule from '@web3-onboard/ledger/dist/index' import trezorModule from '@web3-onboard/trezor' import walletConnect from '@web3-onboard/walletconnect' -import pairingModule from '@/services/pairing/module' import e2eWalletModule from '@/tests/e2e-wallet' import { CGW_NAMES, WALLET_KEYS } from './consts' import MpcModule from '@/services/mpc/SocialLoginModule' @@ -42,7 +41,6 @@ const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } [WALLET_KEYS.INJECTED]: () => injectedWalletModule(), [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain), [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }), - [WALLET_KEYS.PAIRING]: () => pairingModule(), [WALLET_KEYS.SOCIAL]: (chain) => MpcModule(chain), [WALLET_KEYS.LEDGER]: () => ledgerModule(), [WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }), diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index e7b3d6d3d7..b4dc4d94c5 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -43,7 +43,6 @@ import { WalletConnectProvider } from '@/services/walletconnect/WalletConnectCon import useABTesting from '@/services/tracking/useAbTesting' import { AbTest } from '@/services/tracking/abTesting' import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' -import MobilePairingModal from '@/services/pairing/QRModal' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -126,8 +125,6 @@ const WebCoreApp = ({ - - diff --git a/src/services/exceptions/ErrorCodes.ts b/src/services/exceptions/ErrorCodes.ts index 90993a46ef..15c40d3592 100644 --- a/src/services/exceptions/ErrorCodes.ts +++ b/src/services/exceptions/ErrorCodes.ts @@ -15,7 +15,6 @@ enum ErrorCodes { _106 = '106: Failed to get connected wallet', _302 = '302: Error connecting to the wallet', - _303 = '303: Error creating pairing session', _304 = '304: Error enabling MFA', _305 = '305: Error exporting account key', _306 = '306: Error logging in', diff --git a/src/services/pairing/QRModal.tsx b/src/services/pairing/QRModal.tsx deleted file mode 100644 index 47b2e18b14..0000000000 --- a/src/services/pairing/QRModal.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Dialog, DialogContent, DialogTitle, IconButton } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import PairingQRCode from '@/components/common/PairingDetails/PairingQRCode' -import PairingDescription from '@/components/common/PairingDetails/PairingDescription' -import { PAIRING_MODULE_LABEL } from '@/services/pairing/module' -import PairingDeprecationWarning from '@/components/common/PairingDetails/PairingDeprecationWarning' -import ExternalStore from '@/services/ExternalStore' - -const { useStore: useCloseCallback, setStore: setCloseCallback } = new ExternalStore<() => void>() - -export const open = (cb: () => void) => { - setCloseCallback(() => cb) -} - -export const close = () => { - setCloseCallback(undefined) -} - -const QRModal = () => { - const closeCallback = useCloseCallback() - const open = !!closeCallback - - const handleClose = () => { - closeCallback?.() - setCloseCallback(undefined) - close() - } - - if (!open) return null - - return ( - - - {PAIRING_MODULE_LABEL} - - - - - - - -
- -
-
- ) -} - -export default QRModal diff --git a/src/services/pairing/__tests__/utils.test.ts b/src/services/pairing/__tests__/utils.test.ts deleted file mode 100644 index 7551a55ca8..0000000000 --- a/src/services/pairing/__tests__/utils.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { addDays } from 'date-fns' -// @ts-expect-error - with native WalletConnect v2, the type is no longer present -import type { IWalletConnectSession } from '@walletconnect/types' - -import { formatPairingUri, isPairingSupported, hasValidPairingSession, _isPairingSessionExpired } from '../utils' - -describe('Pairing utils', () => { - describe('formatPairingUri', () => { - it('should return a prefixed URI', () => { - const uri = 'wc:1-2@1?bridge=https://test.com/&key=1234' - - const result = formatPairingUri(uri) - expect(result).toBe('safe-wc:1-2@1?bridge=https://test.com/&key=1234') - }) - - it('should return undefined if no URI exists', () => { - const result = formatPairingUri('') - expect(result).toBeUndefined() - }) - - it("should return undefined if the URI doesn't end with a key", () => { - const uri = 'wc:1-2@1?bridge=https://test.com/&key=' - - const result = formatPairingUri(uri) - expect(result).toBeUndefined() - }) - }) - - describe('isPairingSupported', () => { - it('should return true if the wallet is enabled', () => { - const disabledWallets = ['walletConnect'] - const result = isPairingSupported(disabledWallets) - expect(result).toBe(true) - }) - - it('should return false if the wallet is disabled', () => { - const disabledWallets1: string[] = [] - const result1 = isPairingSupported(disabledWallets1) - expect(result1).toBe(true) - - const disabledWallets2 = ['safeMobile'] - const result2 = isPairingSupported(disabledWallets2) - expect(result2).toBe(false) - }) - }) - - describe('isPairingSessionExpired', () => { - it('should return true if the session is older than 24h', () => { - const session: Pick = { - handshakeId: 1000000000000123, - } - - expect(_isPairingSessionExpired(session as IWalletConnectSession)).toBe(true) - }) - - it('should return false if the session is within the last 24h', () => { - const session: Pick = { - handshakeId: +`${Date.now()}123`, - } - - expect(_isPairingSessionExpired(session as IWalletConnectSession)).toBe(false) - }) - }) - - describe('hasValidPairingSession', () => { - beforeEach(() => { - window.localStorage.clear() - }) - - it('should return false if there is no cached session', () => { - expect(hasValidPairingSession()).toBe(false) - }) - - it('should return true if the cached session date is within the last 24h', () => { - const session: Pick = { - handshakeId: 1000000000000123, - } - - window.localStorage.setItem('SAFE_v2__pairingConnector', JSON.stringify(session)) - - jest.spyOn(Date, 'now').mockImplementation(() => +session.handshakeId.toString().slice(0, -3) + 1) - - expect(hasValidPairingSession()).toBe(true) - }) - - it('should return false and clear the cache if the cached session date is older than 24h', () => { - const session: Pick = { - handshakeId: 1000000000000123, - } - - window.localStorage.setItem('SAFE_v2__pairingConnector', JSON.stringify(session)) - - const sessionTimestamp = session.handshakeId.toString().slice(0, -3) - const expirationDate = addDays(new Date(+sessionTimestamp), 1) - - jest.spyOn(Date, 'now').mockImplementation(() => expirationDate.getTime() + 1) - - expect(hasValidPairingSession()).toBe(false) - - expect(window.localStorage.getItem('SAFE_v2__pairingConnector')).toBeNull() - }) - }) -}) diff --git a/src/services/pairing/connector.ts b/src/services/pairing/connector.ts deleted file mode 100644 index c60436f48a..0000000000 --- a/src/services/pairing/connector.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type WalletConnect from '@walletconnect/client' -import bowser from 'bowser' - -import packageJson from '../../../package.json' -import { IS_PRODUCTION } from '@/config/constants' -import ExternalStore from '@/services/ExternalStore' -import PairingIcon from '@/public/images/safe-logo-green.png' - -export const PAIRING_MODULE_STORAGE_ID = 'pairingConnector' - -export const getClientMeta = () => { - const host = location.origin - - const APP_META = { - name: `Safe{Wallet} web v${packageJson.version}`, - url: host, - icons: [`${host}${PairingIcon.src}`], - } - - if (typeof window === 'undefined') { - return { - description: APP_META.name, - ...APP_META, - } - } - - const parsed = bowser.getParser(window.navigator.userAgent) - const os = parsed.getOS() - const browser = parsed.getBrowser() - - return { - description: `${browser.name} ${browser.version} (${os.name});${APP_META.name}`, - ...APP_META, - } -} - -export const { - getStore: getPairingConnector, - setStore: setPairingConnector, - useStore: usePairingConnector, -} = new ExternalStore() - -export enum WalletConnectEvents { - CONNECT = 'connect', - DISPLAY_URI = 'display_uri', - DISCONNECT = 'disconnect', - CALL_REQUEST = 'call_request', - SESSION_REQUEST = 'session_request', - SESSION_UPDATE = 'session_update', - WC_SESSION_REQUEST = 'wc_sessionRequest', - WC_SESSION_UPDATE = 'wc_sessionUpdate', -} - -if (!IS_PRODUCTION) { - Object.values(WalletConnectEvents).forEach((event) => { - getPairingConnector()?.on(event, (...args) => console.info('[Pairing]', event, ...args)) - }) -} diff --git a/src/services/pairing/hooks.ts b/src/services/pairing/hooks.ts deleted file mode 100644 index 07292eb0f8..0000000000 --- a/src/services/pairing/hooks.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' - -import { useCurrentChain } from '@/hooks/useChains' -import useOnboard, { connectWallet, getConnectedWallet } from '@/hooks/wallets/useOnboard' -import { logError, Errors } from '@/services/exceptions' -import { - getClientMeta, - PAIRING_MODULE_STORAGE_ID, - setPairingConnector, - usePairingConnector, - WalletConnectEvents, -} from '@/services/pairing/connector' -import { PAIRING_MODULE_LABEL } from '@/services/pairing/module' -import { formatPairingUri, isPairingSupported, killPairingSession } from '@/services/pairing/utils' -import WalletConnect from '@walletconnect/client' -import { WC_BRIDGE } from '@/config/constants' -import local from '@/services/local-storage/local' - -/** - * `useInitPairing` is responsible for WC session management, creating a session when: - * - * - no wallet is connected to onboard, deemed "initializing" pairing (disconnecting wallets via the UI) - * - on WC 'disconnect' event (disconnecting via the app) - */ - -// First session will be created by onboard's state subscription, only -// when there is no connected wallet -let hasInitialized = false - -// WC has no flag to determine if a session is currently being created -let isConnecting = false - -export const useInitPairing = () => { - const onboard = useOnboard() - const chain = useCurrentChain() - const connector = usePairingConnector() - - const canConnect = !connector?.connected && !isConnecting - const isSupported = isPairingSupported(chain?.disabledWallets) - - useEffect(() => { - const _pairingConnector = new WalletConnect({ - bridge: WC_BRIDGE, - storageId: local.getPrefixedKey(PAIRING_MODULE_STORAGE_ID), - clientMeta: getClientMeta(), - }) - - setPairingConnector(_pairingConnector) - }, []) - - const createSession = useCallback(() => { - if (!canConnect || !chain || !isSupported || !onboard) { - return - } - - isConnecting = true - connector - ?.createSession({ chainId: +chain.chainId }) - .then(() => { - isConnecting = false - }) - .catch((e) => logError(Errors._303, e)) - }, [canConnect, chain, isSupported, onboard, connector]) - - useEffect(() => { - if (!onboard || !isSupported) { - return - } - - // Upon successful WC connection, connect it to onboard - connector?.on(WalletConnectEvents.CONNECT, () => { - connectWallet(onboard, { - autoSelect: { - label: PAIRING_MODULE_LABEL, - disableModals: true, - }, - }) - }) - - connector?.on(WalletConnectEvents.DISCONNECT, () => { - createSession() - }) - - // Create new session when no wallet is connected to onboard - const subscription = onboard.state.select('wallets').subscribe((wallets) => { - if (!getConnectedWallet(wallets) && !hasInitialized) { - createSession() - hasInitialized = true - } - }) - - return () => { - subscription.unsubscribe() - } - }, [onboard, createSession, isSupported, connector]) - - /** - * It's not possible to update the `chainId` of the current WC session - * We therefore kill the current session when switching chain to trigger - * a new `createSession` above - */ - useEffect(() => { - // We need to wait for chains to have been fetched before killing the session - if (!chain) { - return - } - - const isConnected = +chain.chainId === connector?.chainId - const shouldKillSession = !isSupported || (!isConnected && hasInitialized && canConnect) - - if (!shouldKillSession || !connector) { - return - } - - killPairingSession(connector) - }, [chain, isSupported, canConnect, connector]) -} - -/** - * `usePairingUri` is responsible for returning to pairing URI - * @returns uri - "safe-" prefixed WC connection URI - */ -const usePairingUri = () => { - const connector = usePairingConnector() - const [uri, setUri] = useState(connector ? formatPairingUri(connector.uri) : undefined) - - useEffect(() => { - connector?.on(WalletConnectEvents.DISPLAY_URI, (_, { params }) => { - setUri(formatPairingUri(params[0])) - }) - - // Prevent the `connector` from setting state when not mounted. - // Note: `off` clears _all_ listeners associated with that event - return () => { - connector?.off(WalletConnectEvents.DISPLAY_URI) - } - }, [connector]) - - return uri -} - -export default usePairingUri diff --git a/src/services/pairing/icon.ts b/src/services/pairing/icon.ts deleted file mode 100644 index b7ddc80874..0000000000 --- a/src/services/pairing/icon.ts +++ /dev/null @@ -1,8 +0,0 @@ -const pairingIcon = ` - - - - -` - -export default pairingIcon diff --git a/src/services/pairing/module.ts b/src/services/pairing/module.ts deleted file mode 100644 index add033b65c..0000000000 --- a/src/services/pairing/module.ts +++ /dev/null @@ -1,257 +0,0 @@ -import type { Chain, ProviderAccounts, WalletInit, EIP1193Provider } from '@web3-onboard/common' -// @ts-expect-error - with native WalletConnect v2, the type is no longer present -import type { ITxData } from '@walletconnect/types' - -import { getPairingConnector, PAIRING_MODULE_STORAGE_ID } from '@/services/pairing/connector' -import local from '@/services/local-storage/local' -import { killPairingSession } from '@/services/pairing/utils' -import * as QRModal from '@/services/pairing/QRModal' - -enum ProviderEvents { - ACCOUNTS_CHANGED = 'accountsChanged', - CHAIN_CHANGED = 'chainChanged', - DISCONNECT = 'disconnect', - CONNECT = 'connect', - WC_SESSION_UPDATE = 'session_update', -} - -enum ProviderMethods { - PERSONAL_SIGN = 'personal_sign', - ETH_CHAIN_ID = 'eth_chainId', - ETH_REQUEST_ACCOUNTS = 'eth_requestAccounts', - ETH_SELECT_ACCOUNTS = 'eth_selectAccounts', - ETH_SEND_TRANSACTION = 'eth_sendTransaction', - ETH_SIGN_TRANSACTION = 'eth_signTransaction', - ETH_SIGN = 'eth_sign', - ETH_SIGN_TYPED_DATA = 'eth_signTypedData', - ETH_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3', - ETH_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4', - ETH_ACCOUNTS = 'eth_accounts', - WALLET_SWITCH_ETHEREUM_CHAIN = 'wallet_switchEthereumChain', -} - -export const PAIRING_MODULE_LABEL = 'Safe{Wallet}' - -// Modified version of: https://github.com/blocknative/web3-onboard/blob/v2-web3-onboard-develop/packages/walletconnect/src/index.ts -const pairingModule = (): WalletInit => { - return () => { - return { - label: PAIRING_MODULE_LABEL, - getIcon: async () => (await import('./icon')).default, - getInterface: async ({ chains, EventEmitter }) => { - const { StaticJsonRpcProvider } = await import('@ethersproject/providers') - - const { ProviderRpcError, ProviderRpcErrorCode } = await import('@web3-onboard/common') - - const { default: WalletConnect } = await import('@walletconnect/client') - - const { Subject, fromEvent } = await import('rxjs') - const { takeUntil, take } = await import('rxjs/operators') - - const emitter = new EventEmitter() - - class EthProvider { - public request: EIP1193Provider['request'] - public connector: InstanceType - public chains: Chain[] - public disconnect: EIP1193Provider['disconnect'] - // @ts-expect-error - 'emit' does not exist on `typeof EventEmitter` - public emit: EventEmitter['emit'] - // @ts-expect-error - 'on' does not exist on `typeof EventEmitter` - public on: EventEmitter['on'] - // @ts-expect-error - 'removeListener' does not exist on `typeof EventEmitter` - public removeListener: EventEmitter['removeListener'] - - private disconnected$: InstanceType - private providers: Record> - - constructor({ connector, chains }: { connector: InstanceType; chains: Chain[] }) { - this.emit = emitter.emit.bind(emitter) - this.on = emitter.on.bind(emitter) - this.removeListener = emitter.removeListener.bind(emitter) - - this.connector = connector - this.chains = chains - this.disconnected$ = new Subject() - this.providers = {} - - // @ts-expect-error - `payload` type (`ISessionStatus`) is not correctly `pipe`ed - fromEvent(this.connector, ProviderEvents.WC_SESSION_UPDATE, (error, payload) => { - if (error) { - throw error - } - - return payload - }) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: ({ params }) => { - const [{ accounts, chainId }] = params - - this.emit(ProviderEvents.ACCOUNTS_CHANGED, accounts) - this.emit(ProviderEvents.CHAIN_CHANGED, `0x${chainId.toString(16)}`) - }, - error: console.warn, - }) - - // @ts-expect-error - `this.connector` does not satisfy the event target type - fromEvent(this.connector, ProviderEvents.DISCONNECT, (error, payload) => { - if (error) { - throw error - } - - return payload - }) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: () => { - this.emit(ProviderEvents.ACCOUNTS_CHANGED, []) - - this.disconnected$.next(true) - - local.removeItem(PAIRING_MODULE_STORAGE_ID) - }, - error: console.warn, - }) - - fromEvent(window, 'unload').subscribe(() => { - this.disconnect?.() - }) - - this.disconnect = () => killPairingSession(this.connector) - - this.request = async ({ method, params }) => { - switch (method) { - case ProviderMethods.ETH_CHAIN_ID: { - return `0x${this.connector.chainId.toString(16)}` - } - - case ProviderMethods.ETH_REQUEST_ACCOUNTS: { - return new Promise((resolve, reject) => { - if (!this.connector.connected) { - this.connector.createSession().then(() => { - QRModal.open(() => - reject( - new ProviderRpcError({ - code: 4001, - message: 'User rejected the request.', - }), - ), - ) - }) - } else { - const { accounts, chainId } = this.connector.session - - this.emit(ProviderEvents.CHAIN_CHANGED, `0x${chainId.toString(16)}`) - - return resolve(accounts) - } - - // @ts-ignore - fromEvent(this.connector, ProviderEvents.CONNECT, (error, payload) => { - if (error) { - throw error - } - - return payload - }) - .pipe(take(1)) - .subscribe({ - next: ({ params }) => { - const [{ accounts, chainId }] = params - - this.emit(ProviderEvents.ACCOUNTS_CHANGED, accounts) - this.emit(ProviderEvents.CHAIN_CHANGED, `0x${chainId.toString(16)}`) - - QRModal.close() - - resolve(accounts) - }, - error: reject, - }) - }) - } - - case ProviderMethods.ETH_SEND_TRANSACTION: { - const txData = params![0] as ITxData - - return this.connector.sendTransaction({ - ...txData, - // Mobile app expects `value` - value: txData.value || '0x0', - }) - } - - case ProviderMethods.ETH_SIGN_TRANSACTION: { - return this.connector.signTransaction(params![0] as ITxData) - } - - // Mobile app only supports `eth_sign` but emits `personal_sign` event - case ProviderMethods.PERSONAL_SIGN: { - const [safeTxHash, sender] = params as [string, string] - return this.connector.signMessage([sender, safeTxHash]) // `eth_sign` - } - - case ProviderMethods.ETH_ACCOUNTS: { - return this.connector.sendCustomRequest({ - id: 1337, - jsonrpc: '2.0', - method, - params, - }) - } - - // Not supported by mobile app - case ProviderMethods.ETH_SIGN: - case ProviderMethods.ETH_SIGN_TYPED_DATA: - case ProviderMethods.ETH_SIGN_TYPED_DATA_V3: - case ProviderMethods.ETH_SIGN_TYPED_DATA_V4: - // Not supported by WC - case ProviderMethods.ETH_SELECT_ACCOUNTS: { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, - message: `Safe{Wallet} mobile does not support the requested method: ${method}`, - }) - } - - // Switch wallet chain - case ProviderMethods.WALLET_SWITCH_ETHEREUM_CHAIN: { - this.connector.updateSession({ - chainId: parseInt((params as [{ chainId: string }])[0].chainId), - accounts: this.connector.accounts, - }) - return - } - - default: { - const chainId = await this.request({ method: ProviderMethods.ETH_CHAIN_ID }) - - if (!this.providers[chainId]) { - const currentChain = chains.find(({ id }) => id === chainId) - - if (!currentChain) { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.CHAIN_NOT_ADDED, - message: `The Provider does not have an RPC to request the method: ${method}`, - }) - } - - this.providers[chainId] = new StaticJsonRpcProvider(currentChain.rpcUrl) - } - - return this.providers[chainId].send(method, params!) - } - } - } - } - } - - return { - provider: new EthProvider({ chains, connector: getPairingConnector()! }), - } - }, - } - } -} - -export default pairingModule diff --git a/src/services/pairing/utils.ts b/src/services/pairing/utils.ts deleted file mode 100644 index 4423a2cfa5..0000000000 --- a/src/services/pairing/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { addDays, isAfter } from 'date-fns' -// @ts-expect-error - with native WalletConnect v2, the type is no longer present -import type { IWalletConnectSession } from '@walletconnect/types' -import type WalletConnect from '@walletconnect/client' - -import { CGW_NAMES, WALLET_KEYS } from '@/hooks/wallets/consts' -import local from '@/services/local-storage/local' -import { PAIRING_MODULE_STORAGE_ID } from '@/services/pairing/connector' - -export const formatPairingUri = (wcUri: string) => { - const PAIRING_MODULE_URI_PREFIX = 'safe-' - - // A disconnected session returns URI with an empty `key` - if (!wcUri || !/key=.+/.test(wcUri)) { - return - } - - return `${PAIRING_MODULE_URI_PREFIX}${wcUri}` -} - -export const killPairingSession = (connector: InstanceType) => { - const TEMP_PEER_ID = '_tempPeerId' - - // WalletConnect throws if no `peerId` is set when attempting to `killSession` - // We therefore manually set it in order to `killSession` without throwing - if (!connector.peerId) { - connector.peerId = TEMP_PEER_ID - } - - return connector.killSession() -} - -export const isPairingSupported = (disabledWallets?: string[]) => { - return disabledWallets && !disabledWallets.includes(CGW_NAMES[WALLET_KEYS.PAIRING] as string) -} - -export const _isPairingSessionExpired = (session: IWalletConnectSession): boolean => { - // WC appends 3 digits to the timestamp. NOTE: This may change in WC v2 - const sessionTimestamp = session.handshakeId.toString().slice(0, -3) - // The session is valid for 24h (mobile clears it on their end) - const expirationDate = addDays(new Date(+sessionTimestamp), 1) - - return isAfter(Date.now(), expirationDate) -} - -export const hasValidPairingSession = (): boolean => { - const cachedSession = local.getItem(PAIRING_MODULE_STORAGE_ID) - - if (!cachedSession) { - return false - } - - const isExpired = _isPairingSessionExpired(cachedSession) - - if (isExpired) { - local.removeItem(PAIRING_MODULE_STORAGE_ID) - } - - return !isExpired -}