Skip to content

Commit

Permalink
fix: upgrades @walletconnect/ethereum-provider and `@walletconnect/…
Browse files Browse the repository at this point in the history
…modal` (#868)

* fix: update wcv2 to remove polyfills, fix crypto.decode errors, allow empty required chains, improve deep link ux

* update to newer package version

* enforce new rules on optional/required chains parameters

* fix test types

* fix defaultChainId

* remove unnecessary changes

* assign this.provider

* fiddle with tests

* add test for optional chains config

* pr review

* refactor to validate chainProps on init

* Update packages/walletconnect-v2/src/index.ts

Co-authored-by: Zach Pomerantz <[email protected]>

* Update packages/walletconnect-v2/src/index.ts

Co-authored-by: Zach Pomerantz <[email protected]>

* Update packages/walletconnect-v2/src/index.ts

Co-authored-by: Zach Pomerantz <[email protected]>

* Update packages/walletconnect-v2/src/index.ts

Co-authored-by: Zach Pomerantz <[email protected]>

* pr review

---------

Co-authored-by: Zach Pomerantz <[email protected]>
  • Loading branch information
JFrankfurt and zzmp authored Aug 10, 2023
1 parent 634d146 commit 4d4180d
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 120 deletions.
8 changes: 3 additions & 5 deletions example/components/ConnectWithSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,12 @@ function ChainSelect({
}}
disabled={switchChain === undefined}
>
<option hidden disabled selected={activeChainId === undefined}>
<option hidden disabled>
Select chain
</option>
<option value={-1} selected={activeChainId === -1}>
Default
</option>
<option value={-1}>Default</option>
{chainIds.map((chainId) => (
<option key={chainId} value={chainId} selected={chainId === activeChainId}>
<option key={chainId} value={chainId}>
{CHAINS[chainId]?.name ?? chainId}
</option>
))}
Expand Down
2 changes: 0 additions & 2 deletions example/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import CoinbaseWalletCard from '../components/connectorCards/CoinbaseWalletCard'
import GnosisSafeCard from '../components/connectorCards/GnosisSafeCard'
import MetaMaskCard from '../components/connectorCards/MetaMaskCard'
import NetworkCard from '../components/connectorCards/NetworkCard'
import WalletConnectCard from '../components/connectorCards/WalletConnectCard'
import WalletConnectV2Card from '../components/connectorCards/WalletConnectV2Card'
import ProviderExample from '../components/ProviderExample'

Expand All @@ -13,7 +12,6 @@ export default function Home() {
<div style={{ display: 'flex', flexFlow: 'wrap', fontFamily: 'sans-serif' }}>
<MetaMaskCard />
<WalletConnectV2Card />
<WalletConnectCard />
<CoinbaseWalletCard />
<NetworkCard />
<GnosisSafeCard />
Expand Down
4 changes: 2 additions & 2 deletions packages/walletconnect-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"start": "tsc --watch"
},
"dependencies": {
"@walletconnect/ethereum-provider": "^2.8.6",
"@walletconnect/modal": "^2.5.9",
"@walletconnect/ethereum-provider": "^2.9.2",
"@walletconnect/modal": "^2.6.1",
"@web3-react/types": "^8.2.0",
"eventemitter3": "^4.0.7"
},
Expand Down
32 changes: 20 additions & 12 deletions packages/walletconnect-v2/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ class MockWalletConnectProvider extends MockEIP1193Provider<number> {

constructor(opts: EthereumProviderOptions) {
super()
this.chainId = opts.chains[0]
if (opts.chains && opts.chains.length > 0) {
this.chainId = opts.chains[0]
} else if (opts.optionalChains && opts.optionalChains.length > 0) {
this.chainId = opts.optionalChains[0]
}
this.opts = opts
}

Expand Down Expand Up @@ -68,7 +72,7 @@ class MockWalletConnectProvider extends MockEIP1193Provider<number> {
* We mock this method later in the test suite to test behavior when optional chains are not supported.
*/
public getConnectedChains() {
return this.opts.chains.concat(this.opts.optionalChains || [])
return (this.opts.chains || []).concat(this.opts.optionalChains || [])
}

// session is an object when connected, undefined otherwise
Expand All @@ -91,21 +95,17 @@ class MockWalletConnectProvider extends MockEIP1193Provider<number> {
}

describe('WalletConnect', () => {
let wc2InitMock: jest.Mock
let wc2InitMock: jest.SpyInstance<ReturnType<typeof EthereumProvider.init>, Parameters<typeof EthereumProvider.init>>;

beforeEach(() => {
/*
* TypeScript error is expected here. We're mocking a factory `init` method
* to only define a subset of `EthereumProvider` that we use internally
*/
// @ts-ignore
wc2InitMock = jest
.spyOn(EthereumProvider, 'init')
// @ts-ignore
.mockImplementation(async (opts) => {
const provider = new MockWalletConnectProvider(opts)
return provider
})
// @ts-expect-error
.mockImplementation((opts) => Promise.resolve(new MockWalletConnectProvider(opts)))
})

describe('#connectEagerly', () => {
Expand All @@ -121,6 +121,14 @@ describe('WalletConnect', () => {
connector.activate()
connector.activate()
expect(wc2InitMock).toHaveBeenCalledTimes(1)
wc2InitMock.mockClear()
})
test('should be able to initialize with only optionalChains', async () => {
const { connector } = createTestEnvironment({ chains: undefined, optionalChains: chains })
connector.activate()
connector.activate()
expect(wc2InitMock).toHaveBeenCalledTimes(1)
wc2InitMock.mockClear()
})
})

Expand Down Expand Up @@ -148,11 +156,11 @@ describe('WalletConnect', () => {
await expect(connector.activate(99)).rejects.toThrow()
})

test('should throw an error when using optional chain as default', async () => {
const { connector } = createTestEnvironment({ chains, optionalChains: [8] }, 8)
await expect(connector.activate()).rejects.toThrow()
test('should throw an error when using optional chain as default', () => {
expect(() => createTestEnvironment({ chains, optionalChains: [8] }, 8)).toThrow('Invalid chainId 8. Make sure default chain is included in "chains" - chains specified in "optionalChains" may not be selected as the default, as they may not be supported by the wallet.')
})


test('should switch to an optional chain', async () => {
const { connector, store } = createTestEnvironment({
chains,
Expand Down
85 changes: 63 additions & 22 deletions packages/walletconnect-v2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Actions, ProviderRpcError } from '@web3-react/types'
import { Connector } from '@web3-react/types'
import EventEmitter3 from 'eventemitter3'

import { getBestUrlMap, getChainsWithDefault } from './utils'
import { ArrayOneOrMore, getBestUrlMap, getChainsWithDefault, isArrayOneOrMore } from './utils'

export const URI_AVAILABLE = 'URI_AVAILABLE'
const DEFAULT_TIMEOUT = 5000
Expand All @@ -23,6 +23,19 @@ export type WalletConnectOptions = Omit<Parameters<typeof WalletConnectProvider.
rpc?: { [chainId: number]: string | string[] }
}

/**
* Necessary type to interface with @walletconnect/[email protected] which is currently unexported
*/
type ChainsProps =
| {
chains: ArrayOneOrMore<number>
optionalChains?: number[]
}
| {
chains?: number[]
optionalChains: ArrayOneOrMore<number>
}

/**
* Options to configure the WalletConnect connector.
*/
Expand Down Expand Up @@ -51,22 +64,26 @@ export class WalletConnect extends Connector {
private readonly options: Omit<WalletConnectOptions, 'rpcMap' | 'chains'>

private readonly rpcMap?: Record<number, string | string[]>
private readonly chains: number[]
private readonly chains: number[] | ArrayOneOrMore<number> | undefined
private readonly optionalChains: number[] | ArrayOneOrMore<number> | undefined
private readonly defaultChainId?: number
private readonly timeout: number

private eagerConnection?: Promise<WalletConnectProvider>

constructor({ actions, options, defaultChainId, timeout = DEFAULT_TIMEOUT, onError }: WalletConnectConstructorArgs) {
constructor({ actions, defaultChainId, options, timeout = DEFAULT_TIMEOUT, onError }: WalletConnectConstructorArgs) {
super(actions, onError)

const { rpcMap, rpc, chains, ...rest } = options
const { rpcMap, rpc, ...rest } = options

this.options = rest
this.chains = chains
this.defaultChainId = defaultChainId
this.rpcMap = rpcMap || rpc
this.timeout = timeout

const { chains, optionalChains } = this.getChainProps(rest.chains, rest.optionalChains, defaultChainId)
this.chains = chains
this.optionalChains = optionalChains
}

private disconnectListener = (error: ProviderRpcError) => {
Expand All @@ -86,27 +103,51 @@ export class WalletConnect extends Connector {
this.events.emit(URI_AVAILABLE, uri)
}

private async initializeProvider(
desiredChainId: number | undefined = this.defaultChainId
): Promise<WalletConnectProvider> {
const rpcMap = this.rpcMap ? getBestUrlMap(this.rpcMap, this.timeout) : undefined
const chainProps = this.getChainProps(this.chains, this.optionalChains, desiredChainId)

const ethProviderModule = await import('@walletconnect/ethereum-provider')
this.provider = await ethProviderModule.default.init({
...this.options,
...chainProps,
rpcMap: await rpcMap,
})

return this.provider
.on('disconnect', this.disconnectListener)
.on('chainChanged', this.chainChangedListener)
.on('accountsChanged', this.accountsChangedListener)
.on('display_uri', this.URIListener)
}

private getChainProps(
chains: number[] | ArrayOneOrMore<number> | undefined,
optionalChains: number[] | ArrayOneOrMore<number> | undefined,
desiredChainId: number | undefined = this.defaultChainId
): ChainsProps {
// Reorder chains and optionalChains if necessary
const orderedChains = getChainsWithDefault(chains, desiredChainId)
const orderedOptionalChains = getChainsWithDefault(optionalChains, desiredChainId)

// Validate and return the result.
// Type discrimination requires that we use these typeguard checks to guarantee a valid return type.
if (isArrayOneOrMore(orderedChains)) {
return { chains: orderedChains, optionalChains: orderedOptionalChains }
} else if (isArrayOneOrMore(orderedOptionalChains)) {
return { chains: orderedChains, optionalChains: orderedOptionalChains }
}

throw new Error('Either chains or optionalChains must have at least one item.')
}

private isomorphicInitialize(
desiredChainId: number | undefined = this.defaultChainId
): Promise<WalletConnectProvider> {
if (this.eagerConnection) return this.eagerConnection

const rpcMap = this.rpcMap ? getBestUrlMap(this.rpcMap, this.timeout) : undefined
const chains = desiredChainId ? getChainsWithDefault(this.chains, desiredChainId) : this.chains

return (this.eagerConnection = import('@walletconnect/ethereum-provider').then(async (ethProviderModule) => {
const provider = (this.provider = await ethProviderModule.default.init({
...this.options,
chains,
rpcMap: await rpcMap,
}))

return provider
.on('disconnect', this.disconnectListener)
.on('chainChanged', this.chainChangedListener)
.on('accountsChanged', this.accountsChangedListener)
.on('display_uri', this.URIListener)
}))
return (this.eagerConnection = this.initializeProvider(desiredChainId))
}

/** {@inheritdoc Connector.connectEagerly} */
Expand Down
22 changes: 21 additions & 1 deletion packages/walletconnect-v2/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
/**
* Necessary type to interface with @walletconnect/[email protected] which is currently unexported
*/
export type ArrayOneOrMore<T> = {
0: T
} & Array<T>

/**
* This is a type guard for ArrayOneOrMore
*/
export function isArrayOneOrMore<T>(input: T[] = []): input is ArrayOneOrMore<T> {
return input.length > 0
}

/**
* @param rpcMap - Map of chainIds to rpc url(s).
* @param timeout - Timeout, in milliseconds, after which to consider network calls failed.
Expand Down Expand Up @@ -85,7 +99,13 @@ async function getBestUrl(urls: string | string[], timeout: number): Promise<str
* @param chains - An array of chain IDs.
* @param defaultChainId - The chain ID to treat as the default (it will be the first element in the returned array).
*/
export function getChainsWithDefault(chains: number[], defaultChainId: number) {
export function getChainsWithDefault(
chains: number[] | ArrayOneOrMore<number> | undefined,
defaultChainId: number | undefined
) {
if (!chains || !defaultChainId || chains.length === 0) {
return chains
}
const idx = chains.indexOf(defaultChainId)
if (idx === -1) {
throw new Error(
Expand Down
Loading

2 comments on commit 4d4180d

@vercel
Copy link

@vercel vercel bot commented on 4d4180d Aug 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

web3-react – ./

web3-react-uniswap.vercel.app
web3-react-mu.vercel.app
web3-react-git-main-uniswap.vercel.app

@saraanvar
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great

Please sign in to comment.