Skip to content

Commit

Permalink
add chain switching to walletconnect (Uniswap#397)
Browse files Browse the repository at this point in the history
* add chain switching

* fix example

* handle more edge cases, better metamask support

* nit
  • Loading branch information
NoahZinsmeister authored Feb 1, 2022
1 parent 6ce4c6d commit 87aeb2a
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 64 deletions.
25 changes: 0 additions & 25 deletions packages/example/components/Connect.tsx

This file was deleted.

26 changes: 16 additions & 10 deletions packages/example/components/ConnectWithSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Web3ReactHooks } from '@web3-react/core'
import type { MetaMask } from '@web3-react/metamask'
import { Network } from '@web3-react/network'
import { WalletConnect } from '@web3-react/walletconnect'
import type { WalletLink } from '@web3-react/walletlink'
import { useCallback, useState } from 'react'
import { CHAINS, getAddChainParameters, URLS } from '../chains'
Expand Down Expand Up @@ -41,7 +42,7 @@ export function ConnectWithSelect({
error,
isActive,
}: {
connector: MetaMask | WalletLink | Network
connector: MetaMask | WalletConnect | WalletLink | Network
chainId: ReturnType<Web3ReactHooks['useChainId']>
isActivating: ReturnType<Web3ReactHooks['useIsActivating']>
error: ReturnType<Web3ReactHooks['useError']>
Expand All @@ -56,10 +57,15 @@ export function ConnectWithSelect({
const switchChain = useCallback(
async (desiredChainId: number) => {
setDesiredChainId(desiredChainId)
if (connector instanceof Network) {
await connector.activate(desiredChainId)
} else if (desiredChainId !== -1 && desiredChainId !== chainId) {
await connector.activate(getAddChainParameters(desiredChainId))
// if we're already connected to the desired chain, return
if (desiredChainId === chainId) return
// if they want to connect to the default chain and we're already connected, return
if (desiredChainId === -1 && chainId !== undefined) return

if (connector instanceof WalletConnect || connector instanceof Network) {
await connector.activate(desiredChainId === -1 ? undefined : desiredChainId)
} else {
await connector.activate(desiredChainId === -1 ? undefined : getAddChainParameters(desiredChainId))
}
},
[connector, chainId]
Expand All @@ -77,8 +83,8 @@ export function ConnectWithSelect({
<div style={{ marginBottom: '1rem' }} />
<button
onClick={() =>
connector instanceof Network
? connector.activate(desiredChainId)
connector instanceof WalletConnect || connector instanceof Network
? connector.activate(desiredChainId === -1 ? undefined : desiredChainId)
: connector.activate(desiredChainId === -1 ? undefined : getAddChainParameters(desiredChainId))
}
>
Expand All @@ -96,7 +102,7 @@ export function ConnectWithSelect({
chainIds={chainIds}
/>
<div style={{ marginBottom: '1rem' }} />
<button onClick={connector.deactivate}>Disconnect</button>
<button onClick={() => connector.deactivate()}>Disconnect</button>
</div>
)
} else {
Expand All @@ -114,8 +120,8 @@ export function ConnectWithSelect({
isActivating
? undefined
: () =>
connector instanceof Network
? connector.activate(desiredChainId)
connector instanceof WalletConnect || connector instanceof Network
? connector.activate(desiredChainId === -1 ? undefined : desiredChainId)
: connector.activate(desiredChainId === -1 ? undefined : getAddChainParameters(desiredChainId))
}
disabled={isActivating}
Expand Down
8 changes: 4 additions & 4 deletions packages/example/components/connectors/WalletConnectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { hooks, walletConnect } from '../../connectors/walletConnect'
import { Accounts } from '../Accounts'
import { Card } from '../Card'
import { Chain } from '../Chain'
import { Connect } from '../Connect'
import { ConnectWithSelect } from '../ConnectWithSelect'
import { Status } from '../Status'

const { useChainId, useAccounts, useError, useIsActivating, useIsActive, useProvider, useENSNames } = hooks
Expand All @@ -28,9 +28,9 @@ export default function WalletConnectCard() {
<Accounts accounts={accounts} provider={provider} ENSNames={ENSNames} />
</div>
<div style={{ marginBottom: '1rem' }} />
<Connect
activate={() => walletConnect.activate()}
deactivate={() => walletConnect.deactivate()}
<ConnectWithSelect
connector={walletConnect}
chainId={chainId}
isActivating={isActivating}
error={error}
isActive={isActive}
Expand Down
113 changes: 88 additions & 25 deletions packages/walletconnect/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,31 @@ interface MockWalletConnectProvider
extends Omit<WalletConnectProvider, 'on' | 'off' | 'once' | 'removeListener'>,
EventEmitter {}

function parseChainId(chainId: string | number) {
return typeof chainId === 'string' ? Number.parseInt(chainId) : chainId
}

export class WalletConnect extends Connector {
/** {@inheritdoc Connector.provider} */
provider: MockWalletConnectProvider | undefined

private readonly options?: IWCEthRpcConnectionOptions
private eagerConnection?: Promise<void>
private treatModalCloseAsError: boolean

/**
* @param options - Options to pass to `@walletconnect/ethereum-provider`
* @param connectEagerly - A flag indicating whether connection should be initiated when the class is constructed.
*/
constructor(actions: Actions, options: IWCEthRpcConnectionOptions, connectEagerly = true) {
constructor(
actions: Actions,
options: IWCEthRpcConnectionOptions,
connectEagerly = true,
treatModalCloseAsError = true
) {
super(actions)
this.options = options
this.treatModalCloseAsError = treatModalCloseAsError

if (connectEagerly) {
this.eagerConnection = this.initialize(true)
Expand All @@ -32,22 +43,25 @@ export class WalletConnect extends Connector {
this.actions.reportError(error)
}

private chainChangedListener = (chainId: number): void => {
this.actions.update({ chainId })
private chainChangedListener = (chainId: number | string): void => {
this.actions.update({ chainId: parseChainId(chainId) })
}

private accountsChangedListener = (accounts: string[]): void => {
this.actions.update({ accounts })
}

private async initialize(connectEagerly: boolean): Promise<void> {
private async initialize(connectEagerly: boolean, chainId?: number): Promise<void> {
let cancelActivation: () => void
if (connectEagerly) {
cancelActivation = this.actions.startActivation()
}

return import('@walletconnect/ethereum-provider').then((m) => {
this.provider = new m.default(this.options) as unknown as MockWalletConnectProvider
this.provider = new m.default({
...this.options,
...(chainId ? { chainId } : undefined),
}) as unknown as MockWalletConnectProvider

this.provider.on('disconnect', this.disconnectListener)
this.provider.on('chainChanged', this.chainChangedListener)
Expand All @@ -59,11 +73,11 @@ export class WalletConnect extends Connector {
Promise.all([
this.provider.request({ method: 'eth_chainId' }),
this.provider.request({ method: 'eth_accounts' }),
]) as Promise<[number, string[]]>
]) as Promise<[number | string, string[]]>
)
.then(([chainId, accounts]) => {
if (accounts?.length) {
this.actions.update({ chainId, accounts })
this.actions.update({ chainId: parseChainId(chainId), accounts })
} else {
throw new Error('No accounts returned')
}
Expand All @@ -79,45 +93,94 @@ export class WalletConnect extends Connector {
})
}

/** {@inheritdoc Connector.activate} */
public async activate(): Promise<void> {
/**
* Initiates a connection.
*
* @param desiredChainId - If defined, indicates the desired chain to connect to. If the user is
* already connected to this chain, no additional steps will be taken. Otherwise, the user will be prompted to switch
* to the chain, if their wallet supports it.
*/
public async activate(desiredChainId?: number): Promise<void> {
// this early return clause catches some common cases if we're already connected
if (this.provider?.connected) {
if (!desiredChainId) return
if (desiredChainId === this.provider.chainId) return

const desiredChainIdHex = `0x${desiredChainId.toString(16)}`
return this.provider
.request<void>({
method: 'wallet_switchEthereumChain',
params: [{ chainId: desiredChainIdHex }],
})
.catch(() => {
void 0
})
}

// if we're trying to connect to a specific chain, we may have to re-initialize
if (desiredChainId && desiredChainId !== this.provider?.chainId) {
await this.deactivate()
}

this.actions.startActivation()

if (!this.eagerConnection) {
this.eagerConnection = this.initialize(false)
this.eagerConnection = this.initialize(false, desiredChainId)
}
await this.eagerConnection

const wasConnected = !!this.provider?.connected

try {
// these are sequential instead of parallel because otherwise, chainId defaults to 1 even
// if the connecting wallet isn't on mainnet
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const accounts: string[] = await this.provider!.request({ method: 'eth_requestAccounts' })
const accounts = await this.provider!.request<string[]>({ method: 'eth_requestAccounts' })
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const chainId: number = await this.provider!.request({ method: 'eth_chainId' })
const chainId = parseChainId(await this.provider!.request<string | number>({ method: 'eth_chainId' }))

if (!desiredChainId || desiredChainId === chainId) {
return this.actions.update({ chainId, accounts })
}

this.actions.update({ chainId, accounts })
// because e.g. metamask doesn't support wallet_switchEthereumChain, we have to report first-time connections,
// even if the chainId isn't necessarily the desired one. this is ok because in e.g. rainbow,
// we won't report a connection to the wrong chain while the switch is pending because of the re-initialization
// logic above, which ensures first-time connections are to the correct chain in the first place
if (!wasConnected) this.actions.update({ chainId, accounts })

// try to switch to the desired chain, ignoring errors
if (desiredChainId && desiredChainId !== chainId) {
const desiredChainIdHex = `0x${desiredChainId.toString(16)}`
return this.provider
?.request<void>({
method: 'wallet_switchEthereumChain',
params: [{ chainId: desiredChainIdHex }],
})
.catch(() => {
void 0
})
}
} catch (error) {
// this condition is a bit of a hack :/
// if a user triggers the walletconnect modal, closes it, and then tries to connect again, the modal will not trigger.
// the logic below prevents this from happening
if ((error as Error).message === 'User closed modal') {
await this.deactivate()
await this.deactivate(this.treatModalCloseAsError ? (error as Error) : undefined)
} else {
this.actions.reportError(error as Error)
}

this.actions.reportError(error as Error)
}
}

/** {@inheritdoc Connector.deactivate} */
public async deactivate(): Promise<void> {
if (this.provider) {
await this.provider.disconnect()
this.provider.off('disconnect', this.disconnectListener)
this.provider.off('chainChanged', this.chainChangedListener)
this.provider.off('accountsChanged', this.accountsChangedListener)
this.provider = undefined
this.eagerConnection = undefined
}
public async deactivate(error?: Error): Promise<void> {
this.provider?.off('disconnect', this.disconnectListener)
this.provider?.off('chainChanged', this.chainChangedListener)
this.provider?.off('accountsChanged', this.accountsChangedListener)
await this.provider?.disconnect()
this.provider = undefined
this.eagerConnection = undefined
this.actions.reportError(error)
}
}

0 comments on commit 87aeb2a

Please sign in to comment.