Skip to content

Commit

Permalink
fix: session management when switching chains
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Oct 16, 2023
1 parent a17eb18 commit 4df45f2
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 39 deletions.
5 changes: 3 additions & 2 deletions src/components/walletconnect/HeaderWidget/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ type IconProps = {
name: string
iconUrl: string
}
disabled: boolean
}

const Icon = ({ sessionCount, sessionInfo, ...props }: IconProps): React.ReactElement => (
<ButtonBase disableRipple onClick={props.onClick}>
const Icon = ({ sessionCount, sessionInfo, onClick, disabled }: IconProps): React.ReactElement => (
<ButtonBase disableRipple onClick={onClick} disabled={disabled}>
<Badge
badgeContent={
sessionCount > 1
Expand Down
3 changes: 3 additions & 0 deletions src/components/walletconnect/HeaderWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null)
Expand Down Expand Up @@ -56,6 +58,7 @@ const WalletConnectHeaderWidget = (): ReactElement => {
? { name: sessions[0].peer.metadata.name, iconUrl: sessions[0].peer.metadata.icons[0] }
: undefined
}
disabled={!safeLoaded}
/>
<Badge color="error" variant="dot" invisible={!error} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: var(--space-2);
}
10 changes: 10 additions & 0 deletions src/services/walletconnect/WalletConnectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -28,6 +29,8 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) =>
const [wcUri, setWcUri] = useWalletConnectSearchParamUri()
const [error, setError] = useState<Error | null>(null)
const safeWalletProvider = useSafeWalletProvider()
const router = useRouter()
const isSafeConnected = router.isReady && router.query.safe

// Init WalletConnect
useEffect(() => {
Expand Down Expand Up @@ -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
Expand Down
62 changes: 26 additions & 36 deletions src/services/walletconnect/WalletConnectWallet.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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],
Expand Down Expand Up @@ -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
*/
Expand Down
39 changes: 38 additions & 1 deletion src/services/walletconnect/utils.ts
Original file line number Diff line number Diff line change
@@ -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}`
Expand All @@ -24,3 +26,38 @@ export const getSupportedChainIds = (configs: Array<ChainInfo>, 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,
},
},
})
}

0 comments on commit 4df45f2

Please sign in to comment.