@wagmi/connectors 5.0.0 → 5.0.2

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,14 +1,16 @@
1
- import {
2
- type CoinbaseWalletProvider,
3
- CoinbaseWalletSDK,
4
- } from '@coinbase/wallet-sdk'
1
+ import type { CoinbaseWalletSDK, ProviderInterface } from '@coinbase/wallet-sdk'
5
2
  import {
6
3
  ChainNotConfiguredError,
4
+ type Connector,
7
5
  createConnector,
8
- normalizeChainId,
9
6
  } from '@wagmi/core'
10
7
  import type { Evaluate, Mutable, Omit } from '@wagmi/core/internal'
8
+ import type {
9
+ CoinbaseWalletProvider as CBW_Provider,
10
+ CoinbaseWalletSDK as CBW_SDK,
11
+ } from 'cbw-sdk'
11
12
  import {
13
+ type AddEthereumChainParameter,
12
14
  type ProviderRpcError,
13
15
  SwitchChainError,
14
16
  UserRejectedRequestError,
@@ -16,44 +18,75 @@ import {
16
18
  numberToHex,
17
19
  } from 'viem'
18
20
 
19
- export type CoinbaseWalletParameters = Evaluate<
20
- Mutable<
21
- Omit<
22
- ConstructorParameters<typeof CoinbaseWalletSDK>[0],
23
- 'reloadOnDisconnect' // remove property since TSDoc says default is `true`
24
- >
21
+ type Version = '3' | '4'
22
+
23
+ export type CoinbaseWalletParameters<version extends Version = '3'> =
24
+ version extends '4'
25
+ ? Evaluate<
26
+ {
27
+ headlessMode?: false | undefined
28
+ /** Coinbase Wallet SDK version */
29
+ version?: version | '3' | undefined
30
+ } & Version4Parameters
31
+ >
32
+ : Evaluate<
33
+ {
34
+ /**
35
+ * @deprecated `headlessMode` will be removed in the next major version. Upgrade to `version: '4'`.
36
+ */
37
+ headlessMode?: true | undefined
38
+ /**
39
+ * Coinbase Wallet SDK version
40
+ * @deprecated Version 3 will be removed in the next major version. Upgrade to `version: '4'`.
41
+ * @default '4'
42
+ */
43
+ version?: version | '4' | undefined
44
+ } & Version3Parameters
45
+ >
46
+
47
+ coinbaseWallet.type = 'coinbaseWallet' as const
48
+ export function coinbaseWallet<version extends Version>(
49
+ parameters: CoinbaseWalletParameters<version> = {} as any,
50
+ ): version extends '4'
51
+ ? ReturnType<typeof version4>
52
+ : ReturnType<typeof version3> {
53
+ if (parameters.version === '3' || parameters.headlessMode)
54
+ return version3(parameters as Version3Parameters) as any
55
+ return version4(parameters as Version4Parameters) as any
56
+ }
57
+
58
+ type Version4Parameters = Mutable<
59
+ Omit<
60
+ ConstructorParameters<typeof CoinbaseWalletSDK>[0],
61
+ 'appChainIds' // set via wagmi config
25
62
  > & {
26
63
  /**
27
- * Fallback Ethereum JSON RPC URL
28
- * @default ""
64
+ * Preference for the type of wallet to display.
65
+ * @default 'all'
29
66
  */
30
- jsonRpcUrl?: string | undefined
31
- /**
32
- * Fallback Ethereum Chain ID
33
- * @default 1
34
- */
35
- chainId?: number | undefined
36
- /**
37
- * Whether or not to reload dapp automatically after disconnect.
38
- * @default false
39
- */
40
- reloadOnDisconnect?: boolean | undefined
67
+ preference?: 'all' | 'smartWalletOnly' | 'eoaOnly' | undefined
68
+ keysUrl?: string | undefined
41
69
  }
42
70
  >
43
71
 
44
- coinbaseWallet.type = 'coinbaseWallet' as const
45
- export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
46
- const reloadOnDisconnect = false
47
-
48
- type Provider = CoinbaseWalletProvider
72
+ function version4(parameters: Version4Parameters) {
73
+ type Provider = ProviderInterface & {
74
+ // for backwards compatibility
75
+ close?(): void
76
+ }
49
77
  type Properties = {}
50
78
 
51
79
  let sdk: CoinbaseWalletSDK | undefined
52
80
  let walletProvider: Provider | undefined
53
81
 
82
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
83
+ let chainChanged: Connector['onChainChanged'] | undefined
84
+ let disconnect: Connector['onDisconnect'] | undefined
85
+
54
86
  return createConnector<Provider, Properties>((config) => ({
55
87
  id: 'coinbaseWalletSDK',
56
88
  name: 'Coinbase Wallet',
89
+ supportsSimulation: true,
57
90
  type: coinbaseWallet.type,
58
91
  async connect({ chainId } = {}) {
59
92
  try {
@@ -62,11 +95,20 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
62
95
  (await provider.request({
63
96
  method: 'eth_requestAccounts',
64
97
  })) as string[]
65
- ).map(getAddress)
98
+ ).map((x) => getAddress(x))
66
99
 
67
- provider.on('accountsChanged', this.onAccountsChanged)
68
- provider.on('chainChanged', this.onChainChanged)
69
- provider.on('disconnect', this.onDisconnect.bind(this))
100
+ if (!accountsChanged) {
101
+ accountsChanged = this.onAccountsChanged.bind(this)
102
+ provider.on('accountsChanged', accountsChanged)
103
+ }
104
+ if (!chainChanged) {
105
+ chainChanged = this.onChainChanged.bind(this)
106
+ provider.on('chainChanged', chainChanged)
107
+ }
108
+ if (!disconnect) {
109
+ disconnect = this.onDisconnect.bind(this)
110
+ provider.on('disconnect', disconnect)
111
+ }
70
112
 
71
113
  // Switch to chain if provided
72
114
  let currentChainId = await this.getChainId()
@@ -92,12 +134,21 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
92
134
  async disconnect() {
93
135
  const provider = await this.getProvider()
94
136
 
95
- provider.removeListener('accountsChanged', this.onAccountsChanged)
96
- provider.removeListener('chainChanged', this.onChainChanged)
97
- provider.removeListener('disconnect', this.onDisconnect.bind(this))
137
+ if (accountsChanged) {
138
+ provider.removeListener('accountsChanged', accountsChanged)
139
+ accountsChanged = undefined
140
+ }
141
+ if (chainChanged) {
142
+ provider.removeListener('chainChanged', chainChanged)
143
+ chainChanged = undefined
144
+ }
145
+ if (disconnect) {
146
+ provider.removeListener('disconnect', disconnect)
147
+ disconnect = undefined
148
+ }
98
149
 
99
150
  provider.disconnect()
100
- provider.close()
151
+ provider.close?.()
101
152
  },
102
153
  async getAccounts() {
103
154
  const provider = await this.getProvider()
@@ -105,27 +156,265 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
105
156
  await provider.request<string[]>({
106
157
  method: 'eth_accounts',
107
158
  })
108
- ).map(getAddress)
159
+ ).map((x) => getAddress(x))
109
160
  },
110
161
  async getChainId() {
111
162
  const provider = await this.getProvider()
112
- return normalizeChainId(provider.chainId)
163
+ const chainId = await provider.request<number>({
164
+ method: 'eth_chainId',
165
+ })
166
+ return Number(chainId)
113
167
  },
114
168
  async getProvider() {
115
169
  if (!walletProvider) {
116
- sdk = new CoinbaseWalletSDK({ reloadOnDisconnect, ...parameters })
170
+ // Unwrapping import for Vite compatibility.
171
+ // See: https://github.com/vitejs/vite/issues/9703
172
+ const { default: CoinbaseSDK_ } = await import('@coinbase/wallet-sdk')
173
+ const CoinbaseSDK = (() => {
174
+ if (
175
+ typeof CoinbaseSDK_ !== 'function' &&
176
+ typeof CoinbaseSDK_.default === 'function'
177
+ )
178
+ return CoinbaseSDK_.default
179
+ return CoinbaseSDK_ as unknown as typeof CoinbaseSDK_.default
180
+ })()
181
+
182
+ sdk = new CoinbaseSDK({
183
+ ...parameters,
184
+ appChainIds: config.chains.map((x) => x.id),
185
+ })
186
+
187
+ walletProvider = sdk.makeWeb3Provider({
188
+ ...parameters,
189
+ options: parameters.preference ?? 'all',
190
+ })
191
+ }
192
+
193
+ return walletProvider
194
+ },
195
+ async isAuthorized() {
196
+ try {
197
+ const accounts = await this.getAccounts()
198
+ return !!accounts.length
199
+ } catch {
200
+ return false
201
+ }
202
+ },
203
+ async switchChain({ addEthereumChainParameter, chainId }) {
204
+ const chain = config.chains.find((chain) => chain.id === chainId)
205
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
206
+
207
+ const provider = await this.getProvider()
208
+
209
+ try {
210
+ await provider.request({
211
+ method: 'wallet_switchEthereumChain',
212
+ params: [{ chainId: numberToHex(chain.id) }],
213
+ })
214
+ return chain
215
+ } catch (error) {
216
+ // Indicates chain is not added to provider
217
+ if ((error as ProviderRpcError).code === 4902) {
218
+ try {
219
+ let blockExplorerUrls
220
+ if (addEthereumChainParameter?.blockExplorerUrls)
221
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
222
+ else
223
+ blockExplorerUrls = chain.blockExplorers?.default.url
224
+ ? [chain.blockExplorers?.default.url]
225
+ : []
226
+
227
+ let rpcUrls
228
+ if (addEthereumChainParameter?.rpcUrls?.length)
229
+ rpcUrls = addEthereumChainParameter.rpcUrls
230
+ else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']
231
+
232
+ const addEthereumChain = {
233
+ blockExplorerUrls,
234
+ chainId: numberToHex(chainId),
235
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
236
+ iconUrls: addEthereumChainParameter?.iconUrls,
237
+ nativeCurrency:
238
+ addEthereumChainParameter?.nativeCurrency ??
239
+ chain.nativeCurrency,
240
+ rpcUrls,
241
+ } satisfies AddEthereumChainParameter
242
+
243
+ await provider.request({
244
+ method: 'wallet_addEthereumChain',
245
+ params: [addEthereumChain],
246
+ })
247
+
248
+ return chain
249
+ } catch (error) {
250
+ throw new UserRejectedRequestError(error as Error)
251
+ }
252
+ }
253
+
254
+ throw new SwitchChainError(error as Error)
255
+ }
256
+ },
257
+ onAccountsChanged(accounts) {
258
+ if (accounts.length === 0) this.onDisconnect()
259
+ else
260
+ config.emitter.emit('change', {
261
+ accounts: accounts.map((x) => getAddress(x)),
262
+ })
263
+ },
264
+ onChainChanged(chain) {
265
+ const chainId = Number(chain)
266
+ config.emitter.emit('change', { chainId })
267
+ },
268
+ async onDisconnect(_error) {
269
+ config.emitter.emit('disconnect')
270
+
271
+ const provider = await this.getProvider()
272
+ if (accountsChanged) {
273
+ provider.removeListener('accountsChanged', accountsChanged)
274
+ accountsChanged = undefined
275
+ }
276
+ if (chainChanged) {
277
+ provider.removeListener('chainChanged', chainChanged)
278
+ chainChanged = undefined
279
+ }
280
+ if (disconnect) {
281
+ provider.removeListener('disconnect', disconnect)
282
+ disconnect = undefined
283
+ }
284
+ },
285
+ }))
286
+ }
287
+
288
+ type Version3Parameters = Mutable<
289
+ Omit<
290
+ ConstructorParameters<typeof CBW_SDK>[0],
291
+ 'reloadOnDisconnect' // remove property since TSDoc says default is `true`
292
+ >
293
+ > & {
294
+ /**
295
+ * Fallback Ethereum JSON RPC URL
296
+ * @default ""
297
+ */
298
+ jsonRpcUrl?: string | undefined
299
+ /**
300
+ * Fallback Ethereum Chain ID
301
+ * @default 1
302
+ */
303
+ chainId?: number | undefined
304
+ /**
305
+ * Whether or not to reload dapp automatically after disconnect.
306
+ * @default false
307
+ */
308
+ reloadOnDisconnect?: boolean | undefined
309
+ }
117
310
 
118
- // Mock implementations to retrieve private `walletExtension` method from the Coinbase Wallet SDK.
119
- abstract class WalletProvider {
120
- // https://github.com/coinbase/coinbase-wallet-sdk/blob/b4cca90022ffeb46b7bbaaab9389a33133fe0844/packages/wallet-sdk/src/provider/CoinbaseWalletProvider.ts#L927-L936
121
- abstract getChainId(): number
311
+ function version3(parameters: Version3Parameters) {
312
+ const reloadOnDisconnect = false
313
+
314
+ type Provider = CBW_Provider
315
+ type Properties = {}
316
+
317
+ let sdk: CBW_SDK | undefined
318
+ let walletProvider: Provider | undefined
319
+
320
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
321
+ let chainChanged: Connector['onChainChanged'] | undefined
322
+ let disconnect: Connector['onDisconnect'] | undefined
323
+
324
+ return createConnector<Provider, Properties>((config) => ({
325
+ id: 'coinbaseWalletSDK',
326
+ name: 'Coinbase Wallet',
327
+ supportsSimulation: true,
328
+ type: coinbaseWallet.type,
329
+ async connect({ chainId } = {}) {
330
+ try {
331
+ const provider = await this.getProvider()
332
+ const accounts = (
333
+ (await provider.request({
334
+ method: 'eth_requestAccounts',
335
+ })) as string[]
336
+ ).map((x) => getAddress(x))
337
+
338
+ if (!accountsChanged) {
339
+ accountsChanged = this.onAccountsChanged.bind(this)
340
+ provider.on('accountsChanged', accountsChanged)
341
+ }
342
+ if (!chainChanged) {
343
+ chainChanged = this.onChainChanged.bind(this)
344
+ provider.on('chainChanged', chainChanged)
122
345
  }
123
- abstract class SDK {
124
- // https://github.com/coinbase/coinbase-wallet-sdk/blob/b4cca90022ffeb46b7bbaaab9389a33133fe0844/packages/wallet-sdk/src/CoinbaseWalletSDK.ts#L233-L235
125
- abstract get walletExtension(): WalletProvider | undefined
346
+ if (!disconnect) {
347
+ disconnect = this.onDisconnect.bind(this)
348
+ provider.on('disconnect', disconnect)
349
+ }
350
+
351
+ // Switch to chain if provided
352
+ let currentChainId = await this.getChainId()
353
+ if (chainId && currentChainId !== chainId) {
354
+ const chain = await this.switchChain!({ chainId }).catch((error) => {
355
+ if (error.code === UserRejectedRequestError.code) throw error
356
+ return { id: currentChainId }
357
+ })
358
+ currentChainId = chain?.id ?? currentChainId
126
359
  }
360
+
361
+ return { accounts, chainId: currentChainId }
362
+ } catch (error) {
363
+ if (
364
+ /(user closed modal|accounts received is empty|user denied account)/i.test(
365
+ (error as Error).message,
366
+ )
367
+ )
368
+ throw new UserRejectedRequestError(error as Error)
369
+ throw error
370
+ }
371
+ },
372
+ async disconnect() {
373
+ const provider = await this.getProvider()
374
+
375
+ if (accountsChanged) {
376
+ provider.removeListener('accountsChanged', accountsChanged)
377
+ accountsChanged = undefined
378
+ }
379
+ if (chainChanged) {
380
+ provider.removeListener('chainChanged', chainChanged)
381
+ chainChanged = undefined
382
+ }
383
+ if (disconnect) {
384
+ provider.removeListener('disconnect', disconnect)
385
+ disconnect = undefined
386
+ }
387
+
388
+ provider.disconnect()
389
+ provider.close()
390
+ },
391
+ async getAccounts() {
392
+ const provider = await this.getProvider()
393
+ return (
394
+ await provider.request<string[]>({
395
+ method: 'eth_accounts',
396
+ })
397
+ ).map((x) => getAddress(x))
398
+ },
399
+ async getChainId() {
400
+ const provider = await this.getProvider()
401
+ const chainId = await provider.request<number>({ method: 'eth_chainId' })
402
+ return Number(chainId)
403
+ },
404
+ async getProvider() {
405
+ if (!walletProvider) {
406
+ const { default: SDK_ } = await import('cbw-sdk')
407
+ let SDK: typeof SDK_.default
408
+ if (typeof SDK_ !== 'function' && typeof SDK_.default === 'function')
409
+ SDK = SDK_.default
410
+ else SDK = SDK_ as unknown as typeof SDK_.default
411
+ sdk = new SDK({ reloadOnDisconnect, ...parameters })
412
+
413
+ // Force types to retrieve private `walletExtension` method from the Coinbase Wallet SDK.
127
414
  const walletExtensionChainId = (
128
- sdk as unknown as SDK
415
+ sdk as unknown as {
416
+ get walletExtension(): { getChainId(): number } | undefined
417
+ }
129
418
  ).walletExtension?.getChainId()
130
419
 
131
420
  const chain =
@@ -140,6 +429,7 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
140
429
 
141
430
  walletProvider = sdk.makeWeb3Provider(jsonRpcUrl, chainId)
142
431
  }
432
+
143
433
  return walletProvider
144
434
  },
145
435
  async isAuthorized() {
@@ -150,35 +440,51 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
150
440
  return false
151
441
  }
152
442
  },
153
- async switchChain({ chainId }) {
443
+ async switchChain({ addEthereumChainParameter, chainId }) {
154
444
  const chain = config.chains.find((chain) => chain.id === chainId)
155
445
  if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
156
446
 
157
447
  const provider = await this.getProvider()
158
- const chainId_ = numberToHex(chain.id)
159
448
 
160
449
  try {
161
450
  await provider.request({
162
451
  method: 'wallet_switchEthereumChain',
163
- params: [{ chainId: chainId_ }],
452
+ params: [{ chainId: numberToHex(chain.id) }],
164
453
  })
165
454
  return chain
166
455
  } catch (error) {
167
456
  // Indicates chain is not added to provider
168
457
  if ((error as ProviderRpcError).code === 4902) {
169
458
  try {
459
+ let blockExplorerUrls
460
+ if (addEthereumChainParameter?.blockExplorerUrls)
461
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
462
+ else
463
+ blockExplorerUrls = chain.blockExplorers?.default.url
464
+ ? [chain.blockExplorers?.default.url]
465
+ : []
466
+
467
+ let rpcUrls
468
+ if (addEthereumChainParameter?.rpcUrls?.length)
469
+ rpcUrls = addEthereumChainParameter.rpcUrls
470
+ else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']
471
+
472
+ const addEthereumChain = {
473
+ blockExplorerUrls,
474
+ chainId: numberToHex(chainId),
475
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
476
+ iconUrls: addEthereumChainParameter?.iconUrls,
477
+ nativeCurrency:
478
+ addEthereumChainParameter?.nativeCurrency ??
479
+ chain.nativeCurrency,
480
+ rpcUrls,
481
+ } satisfies AddEthereumChainParameter
482
+
170
483
  await provider.request({
171
484
  method: 'wallet_addEthereumChain',
172
- params: [
173
- {
174
- chainId: chainId_,
175
- chainName: chain.name,
176
- nativeCurrency: chain.nativeCurrency,
177
- rpcUrls: [chain.rpcUrls.default?.http[0] ?? ''],
178
- blockExplorerUrls: [chain.blockExplorers?.default.url],
179
- },
180
- ],
485
+ params: [addEthereumChain],
181
486
  })
487
+
182
488
  return chain
183
489
  } catch (error) {
184
490
  throw new UserRejectedRequestError(error as Error)
@@ -189,20 +495,32 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
189
495
  }
190
496
  },
191
497
  onAccountsChanged(accounts) {
192
- if (accounts.length === 0) config.emitter.emit('disconnect')
193
- else config.emitter.emit('change', { accounts: accounts.map(getAddress) })
498
+ if (accounts.length === 0) this.onDisconnect()
499
+ else
500
+ config.emitter.emit('change', {
501
+ accounts: accounts.map((x) => getAddress(x)),
502
+ })
194
503
  },
195
504
  onChainChanged(chain) {
196
- const chainId = normalizeChainId(chain)
505
+ const chainId = Number(chain)
197
506
  config.emitter.emit('change', { chainId })
198
507
  },
199
508
  async onDisconnect(_error) {
200
509
  config.emitter.emit('disconnect')
201
510
 
202
511
  const provider = await this.getProvider()
203
- provider.removeListener('accountsChanged', this.onAccountsChanged)
204
- provider.removeListener('chainChanged', this.onChainChanged)
205
- provider.removeListener('disconnect', this.onDisconnect.bind(this))
512
+ if (accountsChanged) {
513
+ provider.removeListener('accountsChanged', accountsChanged)
514
+ accountsChanged = undefined
515
+ }
516
+ if (chainChanged) {
517
+ provider.removeListener('chainChanged', chainChanged)
518
+ chainChanged = undefined
519
+ }
520
+ if (disconnect) {
521
+ provider.removeListener('disconnect', disconnect)
522
+ disconnect = undefined
523
+ }
206
524
  },
207
525
  }))
208
526
  }