From 4df45f2d0acc4048f2eae976d19ed63139b81603 Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 16 Oct 2023 17:28:22 +0200 Subject: [PATCH] fix: session management when switching chains --- .../walletconnect/HeaderWidget/Icon.tsx | 5 +- .../walletconnect/HeaderWidget/index.tsx | 3 + .../SuccessBanner/styles.module.css | 1 + .../walletconnect/WalletConnectContext.tsx | 10 +++ .../walletconnect/WalletConnectWallet.ts | 62 ++++++++----------- src/services/walletconnect/utils.ts | 39 +++++++++++- 6 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/components/walletconnect/HeaderWidget/Icon.tsx b/src/components/walletconnect/HeaderWidget/Icon.tsx index c2b8c7915e..6f6e358716 100644 --- a/src/components/walletconnect/HeaderWidget/Icon.tsx +++ b/src/components/walletconnect/HeaderWidget/Icon.tsx @@ -10,10 +10,11 @@ type IconProps = { name: string iconUrl: string } + disabled: boolean } -const Icon = ({ sessionCount, sessionInfo, ...props }: IconProps): React.ReactElement => ( - +const Icon = ({ sessionCount, sessionInfo, onClick, disabled }: IconProps): React.ReactElement => ( + 1 diff --git a/src/components/walletconnect/HeaderWidget/index.tsx b/src/components/walletconnect/HeaderWidget/index.tsx index 4880eda65b..8ca78ee31b 100644 --- a/src/components/walletconnect/HeaderWidget/index.tsx +++ b/src/components/walletconnect/HeaderWidget/index.tsx @@ -10,9 +10,11 @@ import SessionManager from '../SessionManager' import Popup from '../Popup' import { useWalletConnectSearchParamUri } from '@/services/walletconnect/useWalletConnectSearchParamUri' import { SuccessBanner } from '../SuccessBanner' +import useSafeInfo from '@/hooks/useSafeInfo' const WalletConnectHeaderWidget = (): ReactElement => { const { error, walletConnect } = useContext(WalletConnectContext) + const { safeLoaded } = useSafeInfo() const [popupOpen, setPopupOpen] = useState(false) const [wcUri] = useWalletConnectSearchParamUri() const iconRef = useRef(null) @@ -56,6 +58,7 @@ const WalletConnectHeaderWidget = (): ReactElement => { ? { name: sessions[0].peer.metadata.name, iconUrl: sessions[0].peer.metadata.icons[0] } : undefined } + disabled={!safeLoaded} /> diff --git a/src/components/walletconnect/SuccessBanner/styles.module.css b/src/components/walletconnect/SuccessBanner/styles.module.css index ff4033c6a5..4fc7171268 100644 --- a/src/components/walletconnect/SuccessBanner/styles.module.css +++ b/src/components/walletconnect/SuccessBanner/styles.module.css @@ -2,5 +2,6 @@ display: flex; flex-direction: column; align-items: center; + text-align: center; padding-top: var(--space-2); } diff --git a/src/services/walletconnect/WalletConnectContext.tsx b/src/services/walletconnect/WalletConnectContext.tsx index f0e7adbacb..5d90946b8e 100644 --- a/src/services/walletconnect/WalletConnectContext.tsx +++ b/src/services/walletconnect/WalletConnectContext.tsx @@ -8,6 +8,7 @@ import WalletConnectWallet from './WalletConnectWallet' import { asError } from '../exceptions/utils' import { stripEip155Prefix } from './utils' import { useWalletConnectSearchParamUri } from './useWalletConnectSearchParamUri' +import { useRouter } from 'next/router' const walletConnectSingleton = new WalletConnectWallet() @@ -28,6 +29,8 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => const [wcUri, setWcUri] = useWalletConnectSearchParamUri() const [error, setError] = useState(null) const safeWalletProvider = useSafeWalletProvider() + const router = useRouter() + const isSafeConnected = router.isReady && router.query.safe // Init WalletConnect useEffect(() => { @@ -55,6 +58,13 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => walletConnect.updateSessions(chainId, safeAddress).catch(setError) }, [walletConnect, chainId, safeAddress]) + // Disconnect all sessions if no Safe is loaded + useEffect(() => { + if (!walletConnect || isSafeConnected) return + + walletConnect.disconnectAllSessions().catch(setError) + }, [walletConnect, isSafeConnected]) + // Subscribe to requests useEffect(() => { if (!walletConnect || !safeWalletProvider || !chainId) return diff --git a/src/services/walletconnect/WalletConnectWallet.ts b/src/services/walletconnect/WalletConnectWallet.ts index ef95cce699..5cc29f3df9 100644 --- a/src/services/walletconnect/WalletConnectWallet.ts +++ b/src/services/walletconnect/WalletConnectWallet.ts @@ -1,15 +1,15 @@ import { Core } from '@walletconnect/core' import { Web3Wallet } from '@walletconnect/web3wallet' -import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils' +import { getSdkError } from '@walletconnect/utils' import type Web3WalletType from '@walletconnect/web3wallet' import type { Web3WalletTypes } from '@walletconnect/web3wallet' import type { SessionTypes } from '@walletconnect/types' import { type JsonRpcResponse } from '@walletconnect/jsonrpc-utils' import { IS_PRODUCTION, WC_PROJECT_ID } from '@/config/constants' -import { EIP155, SAFE_COMPATIBLE_METHODS, SAFE_WALLET_METADATA } from './constants' +import { EIP155, SAFE_WALLET_METADATA } from './constants' import { invariant } from '@/utils/helpers' -import { getEip155ChainId, stripEip155Prefix } from './utils' +import { getEip155ChainId, getNamespaces } from './utils' const SESSION_ADD_EVENT = 'session_add' as Web3WalletTypes.Event // Workaround: WalletConnect doesn't emit session_add event @@ -82,43 +82,16 @@ class WalletConnectWallet { public async approveSession(proposal: Web3WalletTypes.SessionProposal, currentChainId: string, safeAddress: string) { assertWeb3Wallet(this.web3Wallet) - // Actual safe chainId - const safeChains = [currentChainId] - - const getNamespaces = (chainIds: string[], methods: string[]) => { - const eip155ChainIds = chainIds.map(getEip155ChainId) - - // Create a list of addresses for each chainId - const eip155Accounts = eip155ChainIds.map((eip155ChainId) => `${eip155ChainId}:${safeAddress}`) - - return buildApprovedNamespaces({ - proposal: proposal.params, - supportedNamespaces: { - [EIP155]: { - chains: eip155ChainIds, - methods, - accounts: eip155Accounts, - // Don't include optionalNamespaces events - events: proposal.params.requiredNamespaces[EIP155]?.events || [], - }, - }, - }) - } + const namespaces = getNamespaces(proposal, currentChainId, safeAddress) // Approve the session proposal - // Most dapps require mainnet, but we aren't always on mainnet - // A workaround, pretend to support all required chains - const requiredChains = proposal.params.requiredNamespaces[EIP155]?.chains || [] - // TODO: Filter against those which we support - const optionalChains = proposal.params.optionalNamespaces[EIP155]?.chains || [] - const chains = safeChains.concat(requiredChains.map(stripEip155Prefix), optionalChains.map(stripEip155Prefix)) - const session = await this.web3Wallet.approveSession({ id: proposal.id, - namespaces: getNamespaces(chains, proposal.params.requiredNamespaces[EIP155]?.methods ?? SAFE_COMPATIBLE_METHODS), + namespaces, }) - await this.updateSession(session, currentChainId, safeAddress) + // Align the session with the current chainId + await this.chainChanged(session.topic, currentChainId) // Workaround: WalletConnect doesn't have a session_add event this.web3Wallet?.events.emit(SESSION_ADD_EVENT, session) @@ -139,8 +112,13 @@ class WalletConnectWallet { const hasNewChainId = !currentEip155ChainIds.includes(newEip155ChainId) const hasNewAccount = !currentEip155Accounts.includes(newEip155Account) - // Add new chainId and/or account to the session namespace - if (hasNewChainId || hasNewAccount) { + // Switching to unsupported chain + if (hasNewChainId) { + return this.disconnectSession(session) + } + + // Add new account to the session namespace + if (hasNewAccount) { const namespaces: SessionTypes.Namespaces = { [EIP155]: { ...session.namespaces[EIP155], @@ -227,6 +205,18 @@ class WalletConnectWallet { this.web3Wallet?.events.emit('session_delete', session) } + /** + * Disconnect all sessions + */ + public async disconnectAllSessions() { + assertWeb3Wallet(this.web3Wallet) + + // We have to await previous session to be disconnected before disconnecting the next one + for await (const session of this.getActiveSessions()) { + await this.disconnectSession(session) + } + } + /** * Get active sessions */ diff --git a/src/services/walletconnect/utils.ts b/src/services/walletconnect/utils.ts index ad1368d3fd..63dd464084 100644 --- a/src/services/walletconnect/utils.ts +++ b/src/services/walletconnect/utils.ts @@ -1,7 +1,9 @@ +import { buildApprovedNamespaces } from '@walletconnect/utils' +import type { Web3WalletTypes } from '@walletconnect/web3wallet' import type { ChainInfo } from '@safe-global/safe-apps-sdk' import type { ProposalTypes } from '@walletconnect/types' -import { EIP155 } from './constants' +import { EIP155, SAFE_COMPATIBLE_METHODS } from './constants' export const getEip155ChainId = (chainId: string): string => { return `${EIP155}:${chainId}` @@ -24,3 +26,38 @@ export const getSupportedChainIds = (configs: Array, params: Proposal }) .map((chain) => chain.chainId) } + +export const getNamespaces = ( + proposal: Web3WalletTypes.SessionProposal, + currentChainId: string, + safeAddress: string, +) => { + // Most dApps require mainnet, but we aren't always on mainnet + // As workaround, we pretend include all required and optional chains with the Safe chainId + const requiredChains = proposal.params.requiredNamespaces[EIP155]?.chains || [] + const optionalChains = proposal.params.optionalNamespaces[EIP155]?.chains || [] + + const supportedChainIds = [currentChainId].concat( + requiredChains.map(stripEip155Prefix), + optionalChains.map(stripEip155Prefix), + ) + + const eip155ChainIds = supportedChainIds.map(getEip155ChainId) + const eip155Accounts = eip155ChainIds.map((eip155ChainId) => `${eip155ChainId}:${safeAddress}`) + + // Don't include optionalNamespaces methods/events + const methods = proposal.params.requiredNamespaces[EIP155]?.methods ?? SAFE_COMPATIBLE_METHODS + const events = proposal.params.requiredNamespaces[EIP155]?.events || [] + + return buildApprovedNamespaces({ + proposal: proposal.params, + supportedNamespaces: { + [EIP155]: { + chains: eip155ChainIds, + accounts: eip155Accounts, + methods, + events, + }, + }, + }) +}