@wagmi/connectors 4.3.8 → 4.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ChainNotConfiguredError,
3
+ type Connector,
3
4
  ProviderNotFoundError,
4
5
  createConnector,
5
6
  } from '@wagmi/core'
@@ -8,18 +9,24 @@ import {
8
9
  type ExactPartial,
9
10
  type Omit,
10
11
  } from '@wagmi/core/internal'
11
- import type { EthereumProvider } from '@walletconnect/ethereum-provider'
12
+ import { type EthereumProvider } from '@walletconnect/ethereum-provider'
12
13
  import {
13
14
  type AddEthereumChainParameter,
14
15
  type Address,
15
16
  type ProviderConnectInfo,
16
17
  type ProviderRpcError,
18
+ type RpcError,
17
19
  SwitchChainError,
18
20
  UserRejectedRequestError,
19
21
  getAddress,
20
22
  numberToHex,
21
23
  } from 'viem'
22
24
 
25
+ type WalletConnectConnector = Connector & {
26
+ onDisplayUri(uri: string): void
27
+ onSessionDelete(data: { topic: string }): void
28
+ }
29
+
23
30
  type EthereumProviderOptions = Parameters<typeof EthereumProvider['init']>[0]
24
31
 
25
32
  export type WalletConnectParameters = Evaluate<
@@ -30,10 +37,6 @@ export type WalletConnectParameters = Evaluate<
30
37
  * WalletConnect has yet to establish a relationship with (e.g. the user has not approved or
31
38
  * rejected the chain).
32
39
  *
33
- * Preface: Whereas WalletConnect v1 supported dynamic chain switching, WalletConnect v2 requires
34
- * the user to pre-approve a set of chains up-front. This comes with consequent UX nuances (see below) when
35
- * a user tries to switch to a chain that they have not approved.
36
- *
37
40
  * This flag mainly affects the behavior when a wallet does not support dynamic chain authorization
38
41
  * with WalletConnect v2.
39
42
  *
@@ -45,10 +48,11 @@ export type WalletConnectParameters = Evaluate<
45
48
  * be a confusing user experience (e.g. the user will not know they have to reconnect
46
49
  * unless the dapp handles these types of errors).
47
50
  *
48
- * If `false`, the new chain will be treated as a validated chain. This means that if the user
51
+ * If `false`, the new chain will be treated as a potentially valid chain. This means that if the user
49
52
  * has yet to establish a relationship with the chain in their WalletConnect session, wagmi will successfully
50
53
  * auto-connect the user. This comes with the trade-off that the connector will throw an error
51
- * when attempting to switch to the unapproved chain. This may be useful in cases where a dapp constantly
54
+ * when attempting to switch to the unapproved chain if the wallet does not support dynamic session updates.
55
+ * This may be useful in cases where a dapp constantly
52
56
  * modifies their configured chains, and they do not want to disconnect the user upon
53
57
  * auto-connecting. If the user decides to switch to the unapproved chain, it is important that the
54
58
  * dapp handles this error and prompts the user to reconnect to the dapp in order to approve
@@ -76,16 +80,12 @@ export function walletConnect(parameters: WalletConnectParameters) {
76
80
  const isNewChainsStale = parameters.isNewChainsStale ?? true
77
81
 
78
82
  type Provider = Awaited<ReturnType<typeof EthereumProvider['init']>>
79
- type NamespaceMethods =
80
- | 'wallet_addEthereumChain'
81
- | 'wallet_switchEthereumChain'
82
83
  type Properties = {
83
84
  connect(parameters?: { chainId?: number; pairingTopic?: string }): Promise<{
84
85
  accounts: readonly Address[]
85
86
  chainId: number
86
87
  }>
87
88
  getNamespaceChainsIds(): number[]
88
- getNamespaceMethods(): NamespaceMethods[]
89
89
  getRequestedChainsIds(): Promise<number[]>
90
90
  isChainsStale(): Promise<boolean>
91
91
  onConnect(connectInfo: ProviderConnectInfo): void
@@ -102,6 +102,13 @@ export function walletConnect(parameters: WalletConnectParameters) {
102
102
  let providerPromise: Promise<typeof provider_>
103
103
  const NAMESPACE = 'eip155'
104
104
 
105
+ let accountsChanged: WalletConnectConnector['onAccountsChanged'] | undefined
106
+ let chainChanged: WalletConnectConnector['onChainChanged'] | undefined
107
+ let connect: WalletConnectConnector['onConnect'] | undefined
108
+ let displayUri: WalletConnectConnector['onDisplayUri'] | undefined
109
+ let sessionDelete: WalletConnectConnector['onSessionDelete'] | undefined
110
+ let disconnect: WalletConnectConnector['onDisconnect'] | undefined
111
+
105
112
  return createConnector<Provider, Properties, StorageItem>((config) => ({
106
113
  id: 'walletConnect',
107
114
  name: 'WalletConnect',
@@ -109,14 +116,23 @@ export function walletConnect(parameters: WalletConnectParameters) {
109
116
  async setup() {
110
117
  const provider = await this.getProvider().catch(() => null)
111
118
  if (!provider) return
112
- provider.on('connect', this.onConnect.bind(this))
113
- provider.on('session_delete', this.onSessionDelete.bind(this))
119
+ if (!connect) {
120
+ connect = this.onConnect.bind(this)
121
+ provider.on('connect', connect)
122
+ }
123
+ if (!sessionDelete) {
124
+ sessionDelete = this.onSessionDelete.bind(this)
125
+ provider.on('session_delete', sessionDelete)
126
+ }
114
127
  },
115
128
  async connect({ chainId, ...rest } = {}) {
116
129
  try {
117
130
  const provider = await this.getProvider()
118
131
  if (!provider) throw new ProviderNotFoundError()
119
- provider.on('display_uri', this.onDisplayUri)
132
+ if (!displayUri) {
133
+ displayUri = this.onDisplayUri
134
+ provider.on('display_uri', displayUri)
135
+ }
120
136
 
121
137
  let targetChainId = chainId
122
138
  if (!targetChainId) {
@@ -152,12 +168,30 @@ export function walletConnect(parameters: WalletConnectParameters) {
152
168
  const accounts = (await provider.enable()).map((x) => getAddress(x))
153
169
  const currentChainId = await this.getChainId()
154
170
 
155
- provider.removeListener('display_uri', this.onDisplayUri)
156
- provider.removeListener('connect', this.onConnect.bind(this))
157
- provider.on('accountsChanged', this.onAccountsChanged.bind(this))
158
- provider.on('chainChanged', this.onChainChanged)
159
- provider.on('disconnect', this.onDisconnect.bind(this))
160
- provider.on('session_delete', this.onSessionDelete.bind(this))
171
+ if (displayUri) {
172
+ provider.removeListener('display_uri', displayUri)
173
+ displayUri = undefined
174
+ }
175
+ if (connect) {
176
+ provider.removeListener('connect', connect)
177
+ connect = undefined
178
+ }
179
+ if (!accountsChanged) {
180
+ accountsChanged = this.onAccountsChanged.bind(this)
181
+ provider.on('accountsChanged', accountsChanged)
182
+ }
183
+ if (!chainChanged) {
184
+ chainChanged = this.onChainChanged.bind(this)
185
+ provider.on('chainChanged', chainChanged)
186
+ }
187
+ if (!disconnect) {
188
+ disconnect = this.onDisconnect.bind(this)
189
+ provider.on('disconnect', disconnect)
190
+ }
191
+ if (!sessionDelete) {
192
+ sessionDelete = this.onSessionDelete.bind(this)
193
+ provider.on('session_delete', sessionDelete)
194
+ }
161
195
 
162
196
  return { accounts, chainId: currentChainId }
163
197
  } catch (error) {
@@ -178,17 +212,26 @@ export function walletConnect(parameters: WalletConnectParameters) {
178
212
  } catch (error) {
179
213
  if (!/No matching key/i.test((error as Error).message)) throw error
180
214
  } finally {
181
- provider?.removeListener(
182
- 'accountsChanged',
183
- this.onAccountsChanged.bind(this),
184
- )
185
- provider?.removeListener('chainChanged', this.onChainChanged)
186
- provider?.removeListener('disconnect', this.onDisconnect.bind(this))
187
- provider?.removeListener(
188
- 'session_delete',
189
- this.onSessionDelete.bind(this),
190
- )
191
- provider?.on('connect', this.onConnect.bind(this))
215
+ if (chainChanged) {
216
+ provider?.removeListener('chainChanged', chainChanged)
217
+ chainChanged = undefined
218
+ }
219
+ if (disconnect) {
220
+ provider?.removeListener('disconnect', disconnect)
221
+ disconnect = undefined
222
+ }
223
+ if (!connect) {
224
+ connect = this.onConnect.bind(this)
225
+ provider?.on('connect', connect)
226
+ }
227
+ if (accountsChanged) {
228
+ provider?.removeListener('accountsChanged', accountsChanged)
229
+ accountsChanged = undefined
230
+ }
231
+ if (sessionDelete) {
232
+ provider?.removeListener('session_delete', sessionDelete)
233
+ sessionDelete = undefined
234
+ }
192
235
 
193
236
  this.setRequestedChainsIds([])
194
237
  }
@@ -253,19 +296,43 @@ export function walletConnect(parameters: WalletConnectParameters) {
253
296
  }
254
297
  },
255
298
  async switchChain({ addEthereumChainParameter, chainId }) {
256
- const chain = config.chains.find((chain) => chain.id === chainId)
299
+ const provider = await this.getProvider()
300
+ if (!provider) throw new ProviderNotFoundError()
301
+
302
+ const chain = config.chains.find((x) => x.id === chainId)
257
303
  if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
258
304
 
259
305
  try {
260
- const provider = await this.getProvider()
261
- const namespaceChains = this.getNamespaceChainsIds()
262
- const namespaceMethods = this.getNamespaceMethods()
263
- const isChainApproved = namespaceChains.includes(chainId)
306
+ await Promise.all([
307
+ new Promise<void>((resolve) => {
308
+ const listener = ({
309
+ chainId: currentChainId,
310
+ }: { chainId?: number }) => {
311
+ if (currentChainId === chainId) {
312
+ config.emitter.off('change', listener)
313
+ resolve()
314
+ }
315
+ }
316
+ config.emitter.on('change', listener)
317
+ }),
318
+ provider.request({
319
+ method: 'wallet_switchEthereumChain',
320
+ params: [{ chainId: numberToHex(chainId) }],
321
+ }),
322
+ ])
264
323
 
265
- if (
266
- !isChainApproved &&
267
- namespaceMethods.includes('wallet_addEthereumChain')
268
- ) {
324
+ const requestedChains = await this.getRequestedChainsIds()
325
+ this.setRequestedChainsIds([...requestedChains, chainId])
326
+
327
+ return chain
328
+ } catch (err) {
329
+ const error = err as RpcError
330
+
331
+ if (/(user rejected)/i.test(error.message))
332
+ throw new UserRejectedRequestError(error)
333
+
334
+ // Indicates chain is not added to provider
335
+ try {
269
336
  let blockExplorerUrls
270
337
  if (addEthereumChainParameter?.blockExplorerUrls)
271
338
  blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
@@ -293,23 +360,13 @@ export function walletConnect(parameters: WalletConnectParameters) {
293
360
  method: 'wallet_addEthereumChain',
294
361
  params: [addEthereumChain],
295
362
  })
363
+
296
364
  const requestedChains = await this.getRequestedChainsIds()
297
365
  this.setRequestedChainsIds([...requestedChains, chainId])
298
- }
299
-
300
- await provider.request({
301
- method: 'wallet_switchEthereumChain',
302
- params: [{ chainId: numberToHex(chainId) }],
303
- })
304
- return chain
305
- } catch (error) {
306
- const message =
307
- typeof error === 'string'
308
- ? error
309
- : (error as ProviderRpcError)?.message
310
- if (/user rejected request/i.test(message))
366
+ return chain
367
+ } catch (error) {
311
368
  throw new UserRejectedRequestError(error as Error)
312
- throw new SwitchChainError(error as Error)
369
+ }
313
370
  }
314
371
  },
315
372
  onAccountsChanged(accounts) {
@@ -333,14 +390,26 @@ export function walletConnect(parameters: WalletConnectParameters) {
333
390
  config.emitter.emit('disconnect')
334
391
 
335
392
  const provider = await this.getProvider()
336
- provider.removeListener(
337
- 'accountsChanged',
338
- this.onAccountsChanged.bind(this),
339
- )
340
- provider.removeListener('chainChanged', this.onChainChanged)
341
- provider.removeListener('disconnect', this.onDisconnect.bind(this))
342
- provider.removeListener('session_delete', this.onSessionDelete.bind(this))
343
- provider.on('connect', this.onConnect.bind(this))
393
+ if (accountsChanged) {
394
+ provider.removeListener('accountsChanged', accountsChanged)
395
+ accountsChanged = undefined
396
+ }
397
+ if (chainChanged) {
398
+ provider.removeListener('chainChanged', chainChanged)
399
+ chainChanged = undefined
400
+ }
401
+ if (disconnect) {
402
+ provider.removeListener('disconnect', disconnect)
403
+ disconnect = undefined
404
+ }
405
+ if (sessionDelete) {
406
+ provider.removeListener('session_delete', sessionDelete)
407
+ sessionDelete = undefined
408
+ }
409
+ if (!connect) {
410
+ connect = this.onConnect.bind(this)
411
+ provider.on('connect', connect)
412
+ }
344
413
  },
345
414
  onDisplayUri(uri) {
346
415
  config.emitter.emit('message', { type: 'display_uri', data: uri })
@@ -355,12 +424,6 @@ export function walletConnect(parameters: WalletConnectParameters) {
355
424
  )
356
425
  return chainIds ?? []
357
426
  },
358
- getNamespaceMethods() {
359
- if (!provider_) return []
360
- const methods = provider_.session?.namespaces[NAMESPACE]
361
- ?.methods as NamespaceMethods[]
362
- return methods ?? []
363
- },
364
427
  async getRequestedChainsIds() {
365
428
  return (
366
429
  (await config.storage?.getItem(this.requestedChainsStorageKey)) ?? []
@@ -376,21 +439,8 @@ export function walletConnect(parameters: WalletConnectParameters) {
376
439
  * There may be a scenario where a dapp adds a chain to the
377
440
  * connector later on, however, this chain will not have been approved or rejected
378
441
  * by the wallet. In this case, the chain is considered stale.
379
- *
380
- * There are exceptions however:
381
- * - If the wallet supports dynamic chain addition via `eth_addEthereumChain`,
382
- * then the chain is not considered stale.
383
- * - If the `isNewChainsStale` flag is falsy on the connector, then the chain is
384
- * not considered stale.
385
- *
386
- * For the above cases, chain validation occurs dynamically when the user
387
- * attempts to switch chain.
388
- *
389
- * Also check that dapp supports at least 1 chain from previously approved session.
390
442
  */
391
443
  async isChainsStale() {
392
- const namespaceMethods = this.getNamespaceMethods()
393
- if (namespaceMethods.includes('wallet_addEthereumChain')) return false
394
444
  if (!isNewChainsStale) return false
395
445
 
396
446
  const connectorChains = config.chains.map((x) => x.id)