@wagmi/connectors 5.0.0 → 5.0.1

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,21 +1,32 @@
1
1
  import {
2
2
  ChainNotConfiguredError,
3
+ type Connector,
3
4
  ProviderNotFoundError,
4
5
  createConnector,
5
- normalizeChainId,
6
6
  } from '@wagmi/core'
7
- import type { Evaluate, ExactPartial, Omit } from '@wagmi/core/internal'
8
- import { EthereumProvider } from '@walletconnect/ethereum-provider'
9
7
  import {
8
+ type Evaluate,
9
+ type ExactPartial,
10
+ type Omit,
11
+ } from '@wagmi/core/internal'
12
+ import { type EthereumProvider } from '@walletconnect/ethereum-provider'
13
+ import {
14
+ type AddEthereumChainParameter,
10
15
  type Address,
11
16
  type ProviderConnectInfo,
12
17
  type ProviderRpcError,
18
+ type RpcError,
13
19
  SwitchChainError,
14
20
  UserRejectedRequestError,
15
21
  getAddress,
16
22
  numberToHex,
17
23
  } from 'viem'
18
24
 
25
+ type WalletConnectConnector = Connector & {
26
+ onDisplayUri(uri: string): void
27
+ onSessionDelete(data: { topic: string }): void
28
+ }
29
+
19
30
  type EthereumProviderOptions = Parameters<typeof EthereumProvider['init']>[0]
20
31
 
21
32
  export type WalletConnectParameters = Evaluate<
@@ -26,10 +37,6 @@ export type WalletConnectParameters = Evaluate<
26
37
  * WalletConnect has yet to establish a relationship with (e.g. the user has not approved or
27
38
  * rejected the chain).
28
39
  *
29
- * Preface: Whereas WalletConnect v1 supported dynamic chain switching, WalletConnect v2 requires
30
- * the user to pre-approve a set of chains up-front. This comes with consequent UX nuances (see below) when
31
- * a user tries to switch to a chain that they have not approved.
32
- *
33
40
  * This flag mainly affects the behavior when a wallet does not support dynamic chain authorization
34
41
  * with WalletConnect v2.
35
42
  *
@@ -41,10 +48,11 @@ export type WalletConnectParameters = Evaluate<
41
48
  * be a confusing user experience (e.g. the user will not know they have to reconnect
42
49
  * unless the dapp handles these types of errors).
43
50
  *
44
- * 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
45
52
  * has yet to establish a relationship with the chain in their WalletConnect session, wagmi will successfully
46
53
  * auto-connect the user. This comes with the trade-off that the connector will throw an error
47
- * 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
48
56
  * modifies their configured chains, and they do not want to disconnect the user upon
49
57
  * auto-connecting. If the user decides to switch to the unapproved chain, it is important that the
50
58
  * dapp handles this error and prompts the user to reconnect to the dapp in order to approve
@@ -72,16 +80,12 @@ export function walletConnect(parameters: WalletConnectParameters) {
72
80
  const isNewChainsStale = parameters.isNewChainsStale ?? true
73
81
 
74
82
  type Provider = Awaited<ReturnType<typeof EthereumProvider['init']>>
75
- type NamespaceMethods =
76
- | 'wallet_addEthereumChain'
77
- | 'wallet_switchEthereumChain'
78
83
  type Properties = {
79
84
  connect(parameters?: { chainId?: number; pairingTopic?: string }): Promise<{
80
85
  accounts: readonly Address[]
81
86
  chainId: number
82
87
  }>
83
88
  getNamespaceChainsIds(): number[]
84
- getNamespaceMethods(): NamespaceMethods[]
85
89
  getRequestedChainsIds(): Promise<number[]>
86
90
  isChainsStale(): Promise<boolean>
87
91
  onConnect(connectInfo: ProviderConnectInfo): void
@@ -98,6 +102,13 @@ export function walletConnect(parameters: WalletConnectParameters) {
98
102
  let providerPromise: Promise<typeof provider_>
99
103
  const NAMESPACE = 'eip155'
100
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
+
101
112
  return createConnector<Provider, Properties, StorageItem>((config) => ({
102
113
  id: 'walletConnect',
103
114
  name: 'WalletConnect',
@@ -105,14 +116,23 @@ export function walletConnect(parameters: WalletConnectParameters) {
105
116
  async setup() {
106
117
  const provider = await this.getProvider().catch(() => null)
107
118
  if (!provider) return
108
- provider.on('connect', this.onConnect.bind(this))
109
- 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
+ }
110
127
  },
111
128
  async connect({ chainId, ...rest } = {}) {
112
129
  try {
113
130
  const provider = await this.getProvider()
114
131
  if (!provider) throw new ProviderNotFoundError()
115
- provider.on('display_uri', this.onDisplayUri)
132
+ if (!displayUri) {
133
+ displayUri = this.onDisplayUri
134
+ provider.on('display_uri', displayUri)
135
+ }
116
136
 
117
137
  let targetChainId = chainId
118
138
  if (!targetChainId) {
@@ -145,15 +165,33 @@ export function walletConnect(parameters: WalletConnectParameters) {
145
165
  }
146
166
 
147
167
  // If session exists and chains are authorized, enable provider for required chain
148
- const accounts = (await provider.enable()).map(getAddress)
168
+ const accounts = (await provider.enable()).map((x) => getAddress(x))
149
169
  const currentChainId = await this.getChainId()
150
170
 
151
- provider.removeListener('display_uri', this.onDisplayUri)
152
- provider.removeListener('connect', this.onConnect.bind(this))
153
- provider.on('accountsChanged', this.onAccountsChanged.bind(this))
154
- provider.on('chainChanged', this.onChainChanged)
155
- provider.on('disconnect', this.onDisconnect.bind(this))
156
- 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
+ }
157
195
 
158
196
  return { accounts, chainId: currentChainId }
159
197
  } catch (error) {
@@ -174,29 +212,41 @@ export function walletConnect(parameters: WalletConnectParameters) {
174
212
  } catch (error) {
175
213
  if (!/No matching key/i.test((error as Error).message)) throw error
176
214
  } finally {
177
- provider?.removeListener(
178
- 'accountsChanged',
179
- this.onAccountsChanged.bind(this),
180
- )
181
- provider?.removeListener('chainChanged', this.onChainChanged)
182
- provider?.removeListener('disconnect', this.onDisconnect.bind(this))
183
- provider?.removeListener(
184
- 'session_delete',
185
- this.onSessionDelete.bind(this),
186
- )
187
- 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
+ }
188
235
 
189
236
  this.setRequestedChainsIds([])
190
237
  }
191
238
  },
192
239
  async getAccounts() {
193
240
  const provider = await this.getProvider()
194
- return provider.accounts.map(getAddress)
241
+ return provider.accounts.map((x) => getAddress(x))
195
242
  },
196
243
  async getProvider({ chainId } = {}) {
197
244
  async function initProvider() {
198
245
  const optionalChains = config.chains.map((x) => x.id) as [number]
199
246
  if (!optionalChains.length) return
247
+ const { EthereumProvider } = await import(
248
+ '@walletconnect/ethereum-provider'
249
+ )
200
250
  return await EthereumProvider.init({
201
251
  ...parameters,
202
252
  disableProviderPing: true,
@@ -245,62 +295,93 @@ export function walletConnect(parameters: WalletConnectParameters) {
245
295
  return false
246
296
  }
247
297
  },
248
- async switchChain({ chainId }) {
249
- const chain = config.chains.find((chain) => chain.id === chainId)
298
+ async switchChain({ addEthereumChainParameter, chainId }) {
299
+ const provider = await this.getProvider()
300
+ if (!provider) throw new ProviderNotFoundError()
301
+
302
+ const chain = config.chains.find((x) => x.id === chainId)
250
303
  if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
251
304
 
252
305
  try {
253
- const provider = await this.getProvider()
254
- const namespaceChains = this.getNamespaceChainsIds()
255
- const namespaceMethods = this.getNamespaceMethods()
256
- 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
+ ])
323
+
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 {
336
+ let blockExplorerUrls
337
+ if (addEthereumChainParameter?.blockExplorerUrls)
338
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
339
+ else
340
+ blockExplorerUrls = chain.blockExplorers?.default.url
341
+ ? [chain.blockExplorers?.default.url]
342
+ : []
343
+
344
+ let rpcUrls
345
+ if (addEthereumChainParameter?.rpcUrls?.length)
346
+ rpcUrls = addEthereumChainParameter.rpcUrls
347
+ else rpcUrls = [...chain.rpcUrls.default.http]
348
+
349
+ const addEthereumChain = {
350
+ blockExplorerUrls,
351
+ chainId: numberToHex(chainId),
352
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
353
+ iconUrls: addEthereumChainParameter?.iconUrls,
354
+ nativeCurrency:
355
+ addEthereumChainParameter?.nativeCurrency ?? chain.nativeCurrency,
356
+ rpcUrls,
357
+ } satisfies AddEthereumChainParameter
257
358
 
258
- if (
259
- !isChainApproved &&
260
- namespaceMethods.includes('wallet_addEthereumChain')
261
- ) {
262
359
  await provider.request({
263
360
  method: 'wallet_addEthereumChain',
264
- params: [
265
- {
266
- chainId: numberToHex(chain.id),
267
- blockExplorerUrls: [chain.blockExplorers?.default.url],
268
- chainName: chain.name,
269
- nativeCurrency: chain.nativeCurrency,
270
- rpcUrls: [...chain.rpcUrls.default.http],
271
- },
272
- ],
361
+ params: [addEthereumChain],
273
362
  })
363
+
274
364
  const requestedChains = await this.getRequestedChainsIds()
275
365
  this.setRequestedChainsIds([...requestedChains, chainId])
276
- }
277
-
278
- await provider.request({
279
- method: 'wallet_switchEthereumChain',
280
- params: [{ chainId: numberToHex(chainId) }],
281
- })
282
- return chain
283
- } catch (error) {
284
- console.log({ error })
285
- const message =
286
- typeof error === 'string'
287
- ? error
288
- : (error as ProviderRpcError)?.message
289
- if (/user rejected request/i.test(message))
366
+ return chain
367
+ } catch (error) {
290
368
  throw new UserRejectedRequestError(error as Error)
291
- throw new SwitchChainError(error as Error)
369
+ }
292
370
  }
293
371
  },
294
372
  onAccountsChanged(accounts) {
295
373
  if (accounts.length === 0) this.onDisconnect()
296
- else config.emitter.emit('change', { accounts: accounts.map(getAddress) })
374
+ else
375
+ config.emitter.emit('change', {
376
+ accounts: accounts.map((x) => getAddress(x)),
377
+ })
297
378
  },
298
379
  onChainChanged(chain) {
299
- const chainId = normalizeChainId(chain)
380
+ const chainId = Number(chain)
300
381
  config.emitter.emit('change', { chainId })
301
382
  },
302
383
  async onConnect(connectInfo) {
303
- const chainId = normalizeChainId(connectInfo.chainId)
384
+ const chainId = Number(connectInfo.chainId)
304
385
  const accounts = await this.getAccounts()
305
386
  config.emitter.emit('connect', { accounts, chainId })
306
387
  },
@@ -309,14 +390,26 @@ export function walletConnect(parameters: WalletConnectParameters) {
309
390
  config.emitter.emit('disconnect')
310
391
 
311
392
  const provider = await this.getProvider()
312
- provider.removeListener(
313
- 'accountsChanged',
314
- this.onAccountsChanged.bind(this),
315
- )
316
- provider.removeListener('chainChanged', this.onChainChanged)
317
- provider.removeListener('disconnect', this.onDisconnect.bind(this))
318
- provider.removeListener('session_delete', this.onSessionDelete.bind(this))
319
- 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
+ }
320
413
  },
321
414
  onDisplayUri(uri) {
322
415
  config.emitter.emit('message', { type: 'display_uri', data: uri })
@@ -331,12 +424,6 @@ export function walletConnect(parameters: WalletConnectParameters) {
331
424
  )
332
425
  return chainIds ?? []
333
426
  },
334
- getNamespaceMethods() {
335
- if (!provider_) return []
336
- const methods = provider_.session?.namespaces[NAMESPACE]
337
- ?.methods as NamespaceMethods[]
338
- return methods ?? []
339
- },
340
427
  async getRequestedChainsIds() {
341
428
  return (
342
429
  (await config.storage?.getItem(this.requestedChainsStorageKey)) ?? []
@@ -352,21 +439,8 @@ export function walletConnect(parameters: WalletConnectParameters) {
352
439
  * There may be a scenario where a dapp adds a chain to the
353
440
  * connector later on, however, this chain will not have been approved or rejected
354
441
  * by the wallet. In this case, the chain is considered stale.
355
- *
356
- * There are exceptions however:
357
- * - If the wallet supports dynamic chain addition via `eth_addEthereumChain`,
358
- * then the chain is not considered stale.
359
- * - If the `isNewChainsStale` flag is falsy on the connector, then the chain is
360
- * not considered stale.
361
- *
362
- * For the above cases, chain validation occurs dynamically when the user
363
- * attempts to switch chain.
364
- *
365
- * Also check that dapp supports at least 1 chain from previously approved session.
366
442
  */
367
443
  async isChainsStale() {
368
- const namespaceMethods = this.getNamespaceMethods()
369
- if (namespaceMethods.includes('wallet_addEthereumChain')) return false
370
444
  if (!isNewChainsStale) return false
371
445
 
372
446
  const connectorChains = config.chains.map((x) => x.id)
@@ -1,4 +0,0 @@
1
- import { expectTypeOf } from 'vitest';
2
- // noop test because vitest typecheck fails unless each workspace project has type test
3
- expectTypeOf(1).toEqualTypeOf();
4
- //# sourceMappingURL=index.test-d.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test-d.js","sourceRoot":"","sources":["../../../src/exports/index.test-d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAErC,uFAAuF;AACvF,YAAY,CAAC,CAAC,CAAC,CAAC,aAAa,EAAU,CAAA"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=index.test-d.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.test-d.d.ts","sourceRoot":"","sources":["../../../src/exports/index.test-d.ts"],"names":[],"mappings":""}