@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,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 ""
29
- */
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
64
+ * Preference for the type of wallet to display.
65
+ * @default 'all'
39
66
  */
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,259 @@ 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
+ // Unwrap import
171
+ const { default: SDK_ } = await import('@coinbase/wallet-sdk')
172
+ let SDK: typeof SDK_.default
173
+ if (typeof SDK_ !== 'function' && typeof SDK_.default === 'function')
174
+ SDK = SDK_.default
175
+ else SDK = SDK_ as unknown as typeof SDK_.default
176
+ sdk = new SDK({
177
+ ...parameters,
178
+ appChainIds: config.chains.map((x) => x.id),
179
+ })
180
+
181
+ walletProvider = sdk.makeWeb3Provider({
182
+ ...parameters,
183
+ options: parameters.preference ?? 'all',
184
+ })
185
+ }
186
+
187
+ return walletProvider
188
+ },
189
+ async isAuthorized() {
190
+ try {
191
+ const accounts = await this.getAccounts()
192
+ return !!accounts.length
193
+ } catch {
194
+ return false
195
+ }
196
+ },
197
+ async switchChain({ addEthereumChainParameter, chainId }) {
198
+ const chain = config.chains.find((chain) => chain.id === chainId)
199
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
200
+
201
+ const provider = await this.getProvider()
202
+
203
+ try {
204
+ await provider.request({
205
+ method: 'wallet_switchEthereumChain',
206
+ params: [{ chainId: numberToHex(chain.id) }],
207
+ })
208
+ return chain
209
+ } catch (error) {
210
+ // Indicates chain is not added to provider
211
+ if ((error as ProviderRpcError).code === 4902) {
212
+ try {
213
+ let blockExplorerUrls
214
+ if (addEthereumChainParameter?.blockExplorerUrls)
215
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
216
+ else
217
+ blockExplorerUrls = chain.blockExplorers?.default.url
218
+ ? [chain.blockExplorers?.default.url]
219
+ : []
220
+
221
+ let rpcUrls
222
+ if (addEthereumChainParameter?.rpcUrls?.length)
223
+ rpcUrls = addEthereumChainParameter.rpcUrls
224
+ else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']
225
+
226
+ const addEthereumChain = {
227
+ blockExplorerUrls,
228
+ chainId: numberToHex(chainId),
229
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
230
+ iconUrls: addEthereumChainParameter?.iconUrls,
231
+ nativeCurrency:
232
+ addEthereumChainParameter?.nativeCurrency ??
233
+ chain.nativeCurrency,
234
+ rpcUrls,
235
+ } satisfies AddEthereumChainParameter
236
+
237
+ await provider.request({
238
+ method: 'wallet_addEthereumChain',
239
+ params: [addEthereumChain],
240
+ })
241
+
242
+ return chain
243
+ } catch (error) {
244
+ throw new UserRejectedRequestError(error as Error)
245
+ }
246
+ }
247
+
248
+ throw new SwitchChainError(error as Error)
249
+ }
250
+ },
251
+ onAccountsChanged(accounts) {
252
+ if (accounts.length === 0) this.onDisconnect()
253
+ else
254
+ config.emitter.emit('change', {
255
+ accounts: accounts.map((x) => getAddress(x)),
256
+ })
257
+ },
258
+ onChainChanged(chain) {
259
+ const chainId = Number(chain)
260
+ config.emitter.emit('change', { chainId })
261
+ },
262
+ async onDisconnect(_error) {
263
+ config.emitter.emit('disconnect')
264
+
265
+ const provider = await this.getProvider()
266
+ if (accountsChanged) {
267
+ provider.removeListener('accountsChanged', accountsChanged)
268
+ accountsChanged = undefined
269
+ }
270
+ if (chainChanged) {
271
+ provider.removeListener('chainChanged', chainChanged)
272
+ chainChanged = undefined
273
+ }
274
+ if (disconnect) {
275
+ provider.removeListener('disconnect', disconnect)
276
+ disconnect = undefined
277
+ }
278
+ },
279
+ }))
280
+ }
281
+
282
+ type Version3Parameters = Mutable<
283
+ Omit<
284
+ ConstructorParameters<typeof CBW_SDK>[0],
285
+ 'reloadOnDisconnect' // remove property since TSDoc says default is `true`
286
+ >
287
+ > & {
288
+ /**
289
+ * Fallback Ethereum JSON RPC URL
290
+ * @default ""
291
+ */
292
+ jsonRpcUrl?: string | undefined
293
+ /**
294
+ * Fallback Ethereum Chain ID
295
+ * @default 1
296
+ */
297
+ chainId?: number | undefined
298
+ /**
299
+ * Whether or not to reload dapp automatically after disconnect.
300
+ * @default false
301
+ */
302
+ reloadOnDisconnect?: boolean | undefined
303
+ }
304
+
305
+ function version3(parameters: Version3Parameters) {
306
+ const reloadOnDisconnect = false
307
+
308
+ type Provider = CBW_Provider
309
+ type Properties = {}
310
+
311
+ let sdk: CBW_SDK | undefined
312
+ let walletProvider: Provider | undefined
313
+
314
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
315
+ let chainChanged: Connector['onChainChanged'] | undefined
316
+ let disconnect: Connector['onDisconnect'] | undefined
317
+
318
+ return createConnector<Provider, Properties>((config) => ({
319
+ id: 'coinbaseWalletSDK',
320
+ name: 'Coinbase Wallet',
321
+ supportsSimulation: true,
322
+ type: coinbaseWallet.type,
323
+ async connect({ chainId } = {}) {
324
+ try {
325
+ const provider = await this.getProvider()
326
+ const accounts = (
327
+ (await provider.request({
328
+ method: 'eth_requestAccounts',
329
+ })) as string[]
330
+ ).map((x) => getAddress(x))
117
331
 
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
332
+ if (!accountsChanged) {
333
+ accountsChanged = this.onAccountsChanged.bind(this)
334
+ provider.on('accountsChanged', accountsChanged)
335
+ }
336
+ if (!chainChanged) {
337
+ chainChanged = this.onChainChanged.bind(this)
338
+ provider.on('chainChanged', chainChanged)
122
339
  }
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
340
+ if (!disconnect) {
341
+ disconnect = this.onDisconnect.bind(this)
342
+ provider.on('disconnect', disconnect)
343
+ }
344
+
345
+ // Switch to chain if provided
346
+ let currentChainId = await this.getChainId()
347
+ if (chainId && currentChainId !== chainId) {
348
+ const chain = await this.switchChain!({ chainId }).catch((error) => {
349
+ if (error.code === UserRejectedRequestError.code) throw error
350
+ return { id: currentChainId }
351
+ })
352
+ currentChainId = chain?.id ?? currentChainId
126
353
  }
354
+
355
+ return { accounts, chainId: currentChainId }
356
+ } catch (error) {
357
+ if (
358
+ /(user closed modal|accounts received is empty|user denied account)/i.test(
359
+ (error as Error).message,
360
+ )
361
+ )
362
+ throw new UserRejectedRequestError(error as Error)
363
+ throw error
364
+ }
365
+ },
366
+ async disconnect() {
367
+ const provider = await this.getProvider()
368
+
369
+ if (accountsChanged) {
370
+ provider.removeListener('accountsChanged', accountsChanged)
371
+ accountsChanged = undefined
372
+ }
373
+ if (chainChanged) {
374
+ provider.removeListener('chainChanged', chainChanged)
375
+ chainChanged = undefined
376
+ }
377
+ if (disconnect) {
378
+ provider.removeListener('disconnect', disconnect)
379
+ disconnect = undefined
380
+ }
381
+
382
+ provider.disconnect()
383
+ provider.close()
384
+ },
385
+ async getAccounts() {
386
+ const provider = await this.getProvider()
387
+ return (
388
+ await provider.request<string[]>({
389
+ method: 'eth_accounts',
390
+ })
391
+ ).map((x) => getAddress(x))
392
+ },
393
+ async getChainId() {
394
+ const provider = await this.getProvider()
395
+ const chainId = await provider.request<number>({ method: 'eth_chainId' })
396
+ return Number(chainId)
397
+ },
398
+ async getProvider() {
399
+ if (!walletProvider) {
400
+ const { default: SDK_ } = await import('cbw-sdk')
401
+ let SDK: typeof SDK_.default
402
+ if (typeof SDK_ !== 'function' && typeof SDK_.default === 'function')
403
+ SDK = SDK_.default
404
+ else SDK = SDK_ as unknown as typeof SDK_.default
405
+ sdk = new SDK({ reloadOnDisconnect, ...parameters })
406
+
407
+ // Force types to retrieve private `walletExtension` method from the Coinbase Wallet SDK.
127
408
  const walletExtensionChainId = (
128
- sdk as unknown as SDK
409
+ sdk as unknown as {
410
+ get walletExtension(): { getChainId(): number } | undefined
411
+ }
129
412
  ).walletExtension?.getChainId()
130
413
 
131
414
  const chain =
@@ -140,6 +423,7 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
140
423
 
141
424
  walletProvider = sdk.makeWeb3Provider(jsonRpcUrl, chainId)
142
425
  }
426
+
143
427
  return walletProvider
144
428
  },
145
429
  async isAuthorized() {
@@ -150,35 +434,51 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
150
434
  return false
151
435
  }
152
436
  },
153
- async switchChain({ chainId }) {
437
+ async switchChain({ addEthereumChainParameter, chainId }) {
154
438
  const chain = config.chains.find((chain) => chain.id === chainId)
155
439
  if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
156
440
 
157
441
  const provider = await this.getProvider()
158
- const chainId_ = numberToHex(chain.id)
159
442
 
160
443
  try {
161
444
  await provider.request({
162
445
  method: 'wallet_switchEthereumChain',
163
- params: [{ chainId: chainId_ }],
446
+ params: [{ chainId: numberToHex(chain.id) }],
164
447
  })
165
448
  return chain
166
449
  } catch (error) {
167
450
  // Indicates chain is not added to provider
168
451
  if ((error as ProviderRpcError).code === 4902) {
169
452
  try {
453
+ let blockExplorerUrls
454
+ if (addEthereumChainParameter?.blockExplorerUrls)
455
+ blockExplorerUrls = addEthereumChainParameter.blockExplorerUrls
456
+ else
457
+ blockExplorerUrls = chain.blockExplorers?.default.url
458
+ ? [chain.blockExplorers?.default.url]
459
+ : []
460
+
461
+ let rpcUrls
462
+ if (addEthereumChainParameter?.rpcUrls?.length)
463
+ rpcUrls = addEthereumChainParameter.rpcUrls
464
+ else rpcUrls = [chain.rpcUrls.default?.http[0] ?? '']
465
+
466
+ const addEthereumChain = {
467
+ blockExplorerUrls,
468
+ chainId: numberToHex(chainId),
469
+ chainName: addEthereumChainParameter?.chainName ?? chain.name,
470
+ iconUrls: addEthereumChainParameter?.iconUrls,
471
+ nativeCurrency:
472
+ addEthereumChainParameter?.nativeCurrency ??
473
+ chain.nativeCurrency,
474
+ rpcUrls,
475
+ } satisfies AddEthereumChainParameter
476
+
170
477
  await provider.request({
171
478
  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
- ],
479
+ params: [addEthereumChain],
181
480
  })
481
+
182
482
  return chain
183
483
  } catch (error) {
184
484
  throw new UserRejectedRequestError(error as Error)
@@ -189,20 +489,32 @@ export function coinbaseWallet(parameters: CoinbaseWalletParameters) {
189
489
  }
190
490
  },
191
491
  onAccountsChanged(accounts) {
192
- if (accounts.length === 0) config.emitter.emit('disconnect')
193
- else config.emitter.emit('change', { accounts: accounts.map(getAddress) })
492
+ if (accounts.length === 0) this.onDisconnect()
493
+ else
494
+ config.emitter.emit('change', {
495
+ accounts: accounts.map((x) => getAddress(x)),
496
+ })
194
497
  },
195
498
  onChainChanged(chain) {
196
- const chainId = normalizeChainId(chain)
499
+ const chainId = Number(chain)
197
500
  config.emitter.emit('change', { chainId })
198
501
  },
199
502
  async onDisconnect(_error) {
200
503
  config.emitter.emit('disconnect')
201
504
 
202
505
  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))
506
+ if (accountsChanged) {
507
+ provider.removeListener('accountsChanged', accountsChanged)
508
+ accountsChanged = undefined
509
+ }
510
+ if (chainChanged) {
511
+ provider.removeListener('chainChanged', chainChanged)
512
+ chainChanged = undefined
513
+ }
514
+ if (disconnect) {
515
+ provider.removeListener('disconnect', disconnect)
516
+ disconnect = undefined
517
+ }
206
518
  },
207
519
  }))
208
520
  }