@wagmi/core 2.0.0-alpha.3 → 2.0.0-alpha.4

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.
Files changed (44) hide show
  1. package/dist/esm/actions/connect.js +4 -4
  2. package/dist/esm/actions/connect.js.map +1 -1
  3. package/dist/esm/actions/disconnect.js +3 -3
  4. package/dist/esm/actions/disconnect.js.map +1 -1
  5. package/dist/esm/actions/reconnect.js +9 -5
  6. package/dist/esm/actions/reconnect.js.map +1 -1
  7. package/dist/esm/connectors/createConnector.js +8 -0
  8. package/dist/esm/connectors/createConnector.js.map +1 -0
  9. package/dist/esm/connectors/injected.js +380 -0
  10. package/dist/esm/connectors/injected.js.map +1 -0
  11. package/dist/esm/createConfig.js +40 -27
  12. package/dist/esm/createConfig.js.map +1 -1
  13. package/dist/esm/exports/index.js +4 -3
  14. package/dist/esm/exports/index.js.map +1 -1
  15. package/dist/esm/tsconfig.build.tsbuildinfo +1 -1
  16. package/dist/esm/version.js +1 -1
  17. package/dist/types/actions/connect.d.ts +1 -1
  18. package/dist/types/actions/connect.d.ts.map +1 -1
  19. package/dist/types/actions/reconnect.d.ts +1 -1
  20. package/dist/types/actions/reconnect.d.ts.map +1 -1
  21. package/dist/types/{createConnector.d.ts → connectors/createConnector.d.ts} +5 -3
  22. package/dist/types/connectors/createConnector.d.ts.map +1 -0
  23. package/dist/types/connectors/injected.d.ts +345 -0
  24. package/dist/types/connectors/injected.d.ts.map +1 -0
  25. package/dist/types/createConfig.d.ts +8 -12
  26. package/dist/types/createConfig.d.ts.map +1 -1
  27. package/dist/types/exports/index.d.ts +2 -1
  28. package/dist/types/exports/index.d.ts.map +1 -1
  29. package/dist/types/query/connect.d.ts +1 -1
  30. package/dist/types/query/getWalletClient.d.ts +10 -10
  31. package/dist/types/query/signTypedData.d.ts +8 -8
  32. package/dist/types/version.d.ts +1 -1
  33. package/package.json +2 -1
  34. package/src/actions/connect.ts +4 -4
  35. package/src/actions/disconnect.ts +3 -3
  36. package/src/actions/reconnect.ts +9 -5
  37. package/src/{createConnector.ts → connectors/createConnector.ts} +7 -4
  38. package/src/connectors/injected.ts +533 -0
  39. package/src/createConfig.ts +60 -42
  40. package/src/exports/index.ts +14 -9
  41. package/src/version.ts +1 -1
  42. package/dist/esm/createConnector.js +0 -8
  43. package/dist/esm/createConnector.js.map +0 -1
  44. package/dist/types/createConnector.d.ts.map +0 -1
@@ -0,0 +1,533 @@
1
+ import {
2
+ type Address,
3
+ type EIP1193Provider,
4
+ type ProviderConnectInfo,
5
+ ProviderRpcError,
6
+ ResourceUnavailableRpcError,
7
+ RpcError,
8
+ SwitchChainError,
9
+ UserRejectedRequestError,
10
+ getAddress,
11
+ numberToHex,
12
+ } from 'viem'
13
+
14
+ import { ChainNotConfiguredError } from '../errors/config.js'
15
+ import { ProviderNotFoundError } from '../errors/connector.js'
16
+ import type { Evaluate } from '../types/utils.js'
17
+ import { normalizeChainId } from '../utils/normalizeChainId.js'
18
+ import { createConnector } from './createConnector.js'
19
+
20
+ export type InjectedParameters = {
21
+ /**
22
+ * MetaMask and other injected providers do not support programmatic disconnect.
23
+ * This flag simulates the disconnect behavior by keeping track of connection status in storage. See [GitHub issue](https://github.com/MetaMask/metamask-extension/issues/10353) for more info.
24
+ * @default true
25
+ */
26
+ shimDisconnect?: boolean | undefined
27
+ unstable_shimAsyncInject?: boolean | number | undefined
28
+ /**
29
+ * [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) Ethereum Provider to target
30
+ */
31
+ target?:
32
+ | TargetId
33
+ | TargetMap[TargetId]
34
+ | (() => TargetMap[TargetId] | undefined)
35
+ | undefined
36
+ }
37
+
38
+ const targetMap = {
39
+ coinbaseWallet: {
40
+ id: 'coinbaseWallet',
41
+ name: 'Coinbase Wallet',
42
+ provider(window) {
43
+ if (window?.coinbaseWalletExtension) return window.coinbaseWalletExtension
44
+ return findProvider(window, 'isCoinbaseWallet')
45
+ },
46
+ },
47
+ metaMask: {
48
+ id: 'metaMask',
49
+ name: 'MetaMask',
50
+ provider(window) {
51
+ return findProvider(window, (provider) => {
52
+ if (!provider.isMetaMask) return false
53
+ // Brave tries to make itself look like MetaMask
54
+ // Could also try RPC `web3_clientVersion` if following is unreliable
55
+ if (provider.isBraveWallet && !provider._events && !provider._state)
56
+ return false
57
+ // Other wallets that try to look like MetaMask
58
+ const flags: WalletProviderFlags[] = [
59
+ 'isApexWallet',
60
+ 'isAvalanche',
61
+ 'isBitKeep',
62
+ 'isBlockWallet',
63
+ 'isKuCoinWallet',
64
+ 'isMathWallet',
65
+ 'isOkxWallet',
66
+ 'isOKExWallet',
67
+ 'isOneInchIOSWallet',
68
+ 'isOneInchAndroidWallet',
69
+ 'isOpera',
70
+ 'isPortal',
71
+ 'isRabby',
72
+ 'isTokenPocket',
73
+ 'isTokenary',
74
+ 'isZerion',
75
+ ]
76
+ for (const flag of flags) if (provider[flag]) return false
77
+ return true
78
+ })
79
+ },
80
+ },
81
+ phantom: {
82
+ id: 'phantom',
83
+ name: 'Phantom',
84
+ provider(window) {
85
+ if (window?.phantom?.ethereum) return window.phantom?.ethereum
86
+ return findProvider(window, 'isPhantom')
87
+ },
88
+ },
89
+ } as const satisfies TargetMap
90
+
91
+ export function injected(parameters: InjectedParameters = {}) {
92
+ const { shimDisconnect = true, unstable_shimAsyncInject } = parameters
93
+
94
+ function getTarget(): Evaluate<TargetMap[TargetId] & { id: string }> {
95
+ const target = parameters.target
96
+ if (typeof target === 'function') {
97
+ const result = target()
98
+ if (result) return result
99
+ }
100
+
101
+ if (typeof target === 'object') return target
102
+
103
+ if (typeof target === 'string')
104
+ return {
105
+ ...(targetMap[target as keyof typeof targetMap] ?? {
106
+ id: target,
107
+ name: `${target[0]!.toUpperCase()}${target.slice(1)}`,
108
+ provider: `is${target[0]!.toUpperCase()}${target.slice(1)}`,
109
+ }),
110
+ }
111
+
112
+ return {
113
+ id: 'injected',
114
+ name: 'Injected',
115
+ provider(window) {
116
+ return window?.ethereum
117
+ },
118
+ }
119
+ }
120
+
121
+ type Provider = WalletProvider | undefined
122
+ type Properties = {
123
+ onConnect(connectInfo: ProviderConnectInfo): void
124
+ }
125
+ type StorageItem = { [_ in `${string}.disconnected`]: true }
126
+
127
+ return createConnector<Provider, Properties, StorageItem>((config) => ({
128
+ get icon() {
129
+ return getTarget().icon
130
+ },
131
+ get id() {
132
+ return getTarget().id
133
+ },
134
+ get name() {
135
+ return getTarget().name
136
+ },
137
+ async setup() {
138
+ const provider = await this.getProvider()
139
+ // Only start listening for events if `target` is set, otherwise `injected()` will also receive events
140
+ if (provider && parameters.target) {
141
+ provider.on('connect', this.onConnect.bind(this))
142
+ }
143
+ },
144
+ async connect({ chainId, isReconnecting } = {}) {
145
+ const provider = await this.getProvider()
146
+ if (!provider) throw new ProviderNotFoundError()
147
+
148
+ let accounts: readonly Address[] | null = null
149
+ if (!isReconnecting) {
150
+ accounts = await this.getAccounts().catch(() => null)
151
+ const isAuthorized = !!accounts?.length
152
+ if (isAuthorized)
153
+ // Attempt to show another prompt for selecting connector if already connected
154
+ try {
155
+ const permissions = await provider.request({
156
+ method: 'wallet_requestPermissions',
157
+ params: [{ eth_accounts: {} }],
158
+ })
159
+ accounts = permissions[0]?.caveats?.[0]?.value?.map(getAddress)
160
+ } catch (err) {
161
+ const error = err as RpcError
162
+ // Not all injected providers support `wallet_requestPermissions` (e.g. MetaMask iOS).
163
+ // Only bubble up error if user rejects request
164
+ if (error.code === UserRejectedRequestError.code)
165
+ throw new UserRejectedRequestError(error)
166
+ // Or prompt is already open
167
+ if (error.code === ResourceUnavailableRpcError.code) throw error
168
+ }
169
+ }
170
+
171
+ try {
172
+ if (!accounts?.length) {
173
+ const requestedAccounts = await provider.request({
174
+ method: 'eth_requestAccounts',
175
+ })
176
+ accounts = requestedAccounts.map(getAddress)
177
+ }
178
+
179
+ provider.removeListener('connect', this.onConnect.bind(this))
180
+ provider.on('accountsChanged', this.onAccountsChanged.bind(this))
181
+ provider.on('chainChanged', this.onChainChanged)
182
+ provider.on('disconnect', this.onDisconnect.bind(this))
183
+
184
+ // Switch to chain if provided
185
+ let currentChainId = await this.getChainId()
186
+ if (chainId && currentChainId !== chainId) {
187
+ const chain = await this.switchChain?.({ chainId }).catch(() => ({
188
+ id: currentChainId,
189
+ }))
190
+ currentChainId = chain?.id ?? currentChainId
191
+ }
192
+
193
+ // Remove disconnected shim if it exists
194
+ if (shimDisconnect)
195
+ await config.storage?.removeItem(`${this.id}.disconnected`)
196
+
197
+ return { accounts, chainId: currentChainId }
198
+ } catch (err) {
199
+ const error = err as RpcError
200
+ if (error.code === UserRejectedRequestError.code)
201
+ throw new UserRejectedRequestError(error)
202
+ if (error.code === ResourceUnavailableRpcError.code)
203
+ throw new ResourceUnavailableRpcError(error)
204
+ throw error
205
+ }
206
+ },
207
+ async disconnect() {
208
+ const provider = await this.getProvider()
209
+ if (!provider) throw new ProviderNotFoundError()
210
+
211
+ provider.removeListener(
212
+ 'accountsChanged',
213
+ this.onAccountsChanged.bind(this),
214
+ )
215
+ provider.removeListener('chainChanged', this.onChainChanged)
216
+ provider.removeListener('disconnect', this.onDisconnect.bind(this))
217
+ provider.on('connect', this.onConnect.bind(this))
218
+
219
+ // Add shim signalling connector is disconnected
220
+ if (shimDisconnect)
221
+ await config.storage?.setItem(`${this.id}.disconnected`, true)
222
+ },
223
+ async getAccounts() {
224
+ const provider = await this.getProvider()
225
+ if (!provider) throw new ProviderNotFoundError()
226
+ const accounts = await provider.request({ method: 'eth_accounts' })
227
+ return accounts.map(getAddress)
228
+ },
229
+ async getChainId() {
230
+ const provider = await this.getProvider()
231
+ if (!provider) throw new ProviderNotFoundError()
232
+ const hexChainId = await provider.request({ method: 'eth_chainId' })
233
+ return normalizeChainId(hexChainId)
234
+ },
235
+ async getProvider() {
236
+ if (typeof window === 'undefined') return undefined
237
+ const target = getTarget()
238
+ if (typeof target.provider === 'function')
239
+ return target.provider(window as Window | undefined)
240
+ if (typeof target.provider === 'string')
241
+ return findProvider(window, target.provider)
242
+ return target.provider
243
+ },
244
+ async isAuthorized() {
245
+ try {
246
+ const isDisconnected =
247
+ shimDisconnect &&
248
+ // If shim exists in storage, connector is disconnected
249
+ (await config.storage?.getItem(`${this.id}.disconnected`))
250
+ if (isDisconnected) return false
251
+
252
+ const provider = await this.getProvider()
253
+ if (!provider) {
254
+ if (
255
+ unstable_shimAsyncInject !== undefined &&
256
+ unstable_shimAsyncInject !== false
257
+ ) {
258
+ // If no provider is found, check for async injection
259
+ // https://github.com/wagmi-dev/references/issues/167
260
+ // https://github.com/MetaMask/detect-provider
261
+ const handleEthereum = async () => {
262
+ if (typeof window !== 'undefined')
263
+ window.removeEventListener(
264
+ 'ethereum#initialized',
265
+ handleEthereum,
266
+ )
267
+ const provider = await this.getProvider()
268
+ return !!provider
269
+ }
270
+ const timeout =
271
+ typeof unstable_shimAsyncInject === 'number'
272
+ ? unstable_shimAsyncInject
273
+ : 1_000
274
+ const res = await Promise.race([
275
+ ...(typeof window !== 'undefined'
276
+ ? [
277
+ new Promise<boolean>((resolve) =>
278
+ window.addEventListener(
279
+ 'ethereum#initialized',
280
+ () => resolve(handleEthereum()),
281
+ { once: true },
282
+ ),
283
+ ),
284
+ ]
285
+ : []),
286
+ new Promise<boolean>((resolve) =>
287
+ setTimeout(() => resolve(handleEthereum()), timeout),
288
+ ),
289
+ ])
290
+ if (res) return true
291
+ }
292
+
293
+ throw new ProviderNotFoundError()
294
+ }
295
+
296
+ const accounts = await this.getAccounts()
297
+ return !!accounts.length
298
+ } catch {
299
+ return false
300
+ }
301
+ },
302
+ async switchChain({ chainId }) {
303
+ const provider = await this.getProvider()
304
+ if (!provider) throw new ProviderNotFoundError()
305
+
306
+ const chain = config.chains.find((x) => x.id === chainId)
307
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
308
+
309
+ const id = numberToHex(chainId)
310
+
311
+ try {
312
+ await Promise.all([
313
+ provider.request({
314
+ method: 'wallet_switchEthereumChain',
315
+ params: [{ chainId: id }],
316
+ }),
317
+ new Promise<void>((resolve) =>
318
+ config.emitter.once('change', ({ chainId: currentChainId }) => {
319
+ if (currentChainId === chainId) resolve()
320
+ }),
321
+ ),
322
+ ])
323
+ return chain
324
+ } catch (err) {
325
+ const error = err as RpcError
326
+
327
+ // Indicates chain is not added to provider
328
+ if (
329
+ error.code === 4902 ||
330
+ // Unwrapping for MetaMask Mobile
331
+ // https://github.com/MetaMask/metamask-mobile/issues/2944#issuecomment-976988719
332
+ (error as ProviderRpcError<{ originalError?: { code: number } }>)
333
+ ?.data?.originalError?.code === 4902
334
+ ) {
335
+ try {
336
+ const { default: blockExplorer, ...blockExplorers } =
337
+ chain.blockExplorers ?? {}
338
+ let blockExplorerUrls: string[] = []
339
+ if (blockExplorer)
340
+ blockExplorerUrls = [
341
+ blockExplorer.url,
342
+ ...Object.values(blockExplorers).map((x) => x.url),
343
+ ]
344
+
345
+ await provider.request({
346
+ method: 'wallet_addEthereumChain',
347
+ params: [
348
+ {
349
+ chainId: id,
350
+ chainName: chain.name,
351
+ nativeCurrency: chain.nativeCurrency,
352
+ rpcUrls: [chain.rpcUrls.public?.http[0] ?? ''],
353
+ blockExplorerUrls,
354
+ },
355
+ ],
356
+ })
357
+
358
+ const currentChainId = await this.getChainId()
359
+ if (currentChainId !== chainId)
360
+ throw new UserRejectedRequestError(
361
+ new Error('User rejected switch after adding network.'),
362
+ )
363
+
364
+ return chain
365
+ } catch (error) {
366
+ throw new UserRejectedRequestError(error as Error)
367
+ }
368
+ }
369
+
370
+ if (error.code === UserRejectedRequestError.code)
371
+ throw new UserRejectedRequestError(error)
372
+ throw new SwitchChainError(error)
373
+ }
374
+ },
375
+ async onAccountsChanged(accounts) {
376
+ // Disconnect if there are no accounts
377
+ if (accounts.length === 0) this.onDisconnect()
378
+ // Connect if emitter is listening for connect event (e.g. is disconnected and connects through wallet interface)
379
+ else if (config.emitter.listenerCount('connect')) {
380
+ const chainId = (await this.getChainId()).toString()
381
+ this.onConnect({ chainId })
382
+ // Remove disconnected shim if it exists
383
+ if (shimDisconnect)
384
+ await config.storage?.removeItem(`${this.id}.disconnected`)
385
+ }
386
+ // Regular change event
387
+ else config.emitter.emit('change', { accounts: accounts.map(getAddress) })
388
+ },
389
+ onChainChanged(chain) {
390
+ const chainId = normalizeChainId(chain)
391
+ config.emitter.emit('change', { chainId })
392
+ },
393
+ async onConnect(connectInfo) {
394
+ const accounts = await this.getAccounts()
395
+ if (accounts.length === 0) return
396
+
397
+ const chainId = normalizeChainId(connectInfo.chainId)
398
+ config.emitter.emit('connect', { accounts, chainId })
399
+
400
+ const provider = await this.getProvider()
401
+ if (provider) {
402
+ provider.removeListener('connect', this.onConnect.bind(this))
403
+ provider.on('accountsChanged', this.onAccountsChanged.bind(this))
404
+ provider.on('chainChanged', this.onChainChanged)
405
+ provider.on('disconnect', this.onDisconnect.bind(this))
406
+ }
407
+ },
408
+ async onDisconnect(error) {
409
+ const provider = await this.getProvider()
410
+
411
+ // If MetaMask emits a `code: 1013` error, wait for reconnection before disconnecting
412
+ // https://github.com/MetaMask/providers/pull/120
413
+ if (error && (error as RpcError<1013>).code === 1013) {
414
+ if (provider && !!(await this.getAccounts()).length) return
415
+ }
416
+
417
+ // No need to remove `${this.id}.disconnected` from storage because `onDisconnect` is typically
418
+ // only called when the wallet is disconnected through the wallet's interface, meaning the wallet
419
+ // actually disconnected and we don't need to simulate it.
420
+ config.emitter.emit('disconnect')
421
+
422
+ if (provider) {
423
+ provider.removeListener(
424
+ 'accountsChanged',
425
+ this.onAccountsChanged.bind(this),
426
+ )
427
+ provider.removeListener('chainChanged', this.onChainChanged)
428
+ provider.removeListener('disconnect', this.onDisconnect.bind(this))
429
+ provider.on('connect', this.onConnect.bind(this))
430
+ }
431
+ },
432
+ }))
433
+ }
434
+
435
+ type Target = {
436
+ icon?: string | undefined
437
+ id: string
438
+ name: string
439
+ provider:
440
+ | WalletProviderFlags
441
+ | WalletProvider
442
+ | ((window?: Window | undefined) => WalletProvider | undefined)
443
+ }
444
+
445
+ export type TargetId = Evaluate<WalletProviderFlags> extends `is${infer name}`
446
+ ? name extends `${infer char}${infer rest}`
447
+ ? `${Lowercase<char>}${rest}`
448
+ : never
449
+ : never
450
+
451
+ type TargetMap = { [_ in TargetId]?: Target | undefined }
452
+
453
+ type WalletProviderFlags =
454
+ | 'isApexWallet'
455
+ | 'isAvalanche'
456
+ | 'isBackpack'
457
+ | 'isBifrost'
458
+ | 'isBitKeep'
459
+ | 'isBitski'
460
+ | 'isBlockWallet'
461
+ | 'isBraveWallet'
462
+ | 'isCoinbaseWallet'
463
+ | 'isDawn'
464
+ | 'isEnkrypt'
465
+ | 'isExodus'
466
+ | 'isFrame'
467
+ | 'isFrontier'
468
+ | 'isGamestop'
469
+ | 'isHyperPay'
470
+ | 'isImToken'
471
+ | 'isKuCoinWallet'
472
+ | 'isMathWallet'
473
+ | 'isMetaMask'
474
+ | 'isOkxWallet'
475
+ | 'isOKExWallet'
476
+ | 'isOneInchAndroidWallet'
477
+ | 'isOneInchIOSWallet'
478
+ | 'isOpera'
479
+ | 'isPhantom'
480
+ | 'isPortal'
481
+ | 'isRabby'
482
+ | 'isRainbow'
483
+ | 'isStatus'
484
+ | 'isTally'
485
+ | 'isTokenPocket'
486
+ | 'isTokenary'
487
+ | 'isTrust'
488
+ | 'isTrustWallet'
489
+ | 'isXDEFI'
490
+ | 'isZerion'
491
+
492
+ type WalletProvider = Evaluate<
493
+ EIP1193Provider & {
494
+ [key in WalletProviderFlags]?: true | undefined
495
+ } & {
496
+ providers?: WalletProvider[] | undefined
497
+ /** Only exists in MetaMask as of 2022/04/03 */
498
+ _events?: { connect?: (() => void) | undefined } | undefined
499
+ /** Only exists in MetaMask as of 2022/04/03 */
500
+ _state?:
501
+ | {
502
+ accounts?: string[]
503
+ initialized?: boolean
504
+ isConnected?: boolean
505
+ isPermanentlyDisconnected?: boolean
506
+ isUnlocked?: boolean
507
+ }
508
+ | undefined
509
+ }
510
+ >
511
+
512
+ type Window = {
513
+ coinbaseWalletExtension?: WalletProvider | undefined
514
+ ethereum?: WalletProvider | undefined
515
+ phantom?: { ethereum: WalletProvider } | undefined
516
+ }
517
+
518
+ function findProvider(
519
+ window: globalThis.Window | Window | undefined,
520
+ select?: WalletProviderFlags | ((provider: WalletProvider) => boolean),
521
+ ) {
522
+ function isProvider(provider: WalletProvider) {
523
+ if (typeof select === 'function') return select(provider)
524
+ if (typeof select === 'string') return provider[select]
525
+ return true
526
+ }
527
+
528
+ const ethereum = (window as Window).ethereum
529
+ if (ethereum?.providers)
530
+ return ethereum.providers.find((provider) => isProvider(provider))
531
+ if (ethereum && isProvider(ethereum)) return ethereum
532
+ return undefined
533
+ }