@wagmi/core 3.4.2 → 3.4.3

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,680 +1,137 @@
1
- import * as Address from 'ox/Address'
2
- import type * as Hex from 'ox/Hex'
3
- import * as PublicKey from 'ox/PublicKey'
4
- import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
1
+ import type {
2
+ Provider as AccountsProvider,
3
+ Rpc as AccountsRpc,
4
+ dangerous_secp256k1 as accountsDangerousSecp256k1,
5
+ dialog as accountsDialog,
6
+ webAuthn as accountsWebAuthn,
7
+ } from 'accounts'
5
8
  import {
6
- createClient,
7
- defineChain,
8
- type EIP1193Provider,
9
- getAddress,
9
+ type Address,
10
+ numberToHex,
11
+ type ProviderConnectInfo,
12
+ type RpcError,
10
13
  SwitchChainError,
14
+ UserRejectedRequestError,
15
+ withRetry,
11
16
  } from 'viem'
12
- import {
13
- generatePrivateKey,
14
- type LocalAccount,
15
- privateKeyToAccount,
16
- } from 'viem/accounts'
17
- import {
18
- Account,
19
- WebAuthnP256,
20
- WebCryptoP256,
21
- walletNamespaceCompat,
22
- } from 'viem/tempo'
17
+
23
18
  import { createConnector } from '../connectors/createConnector.js'
19
+ import type { Connector } from '../createConfig.js'
24
20
  import { ChainNotConfiguredError } from '../errors/config.js'
25
- import type { OneOf } from '../types/utils.js'
26
- import type * as KeyManager from './KeyManager.js'
27
-
28
- /** @deprecated use `webAuthn.Parameters` instead */
29
- export type WebAuthnParameters = webAuthn.Parameters
30
21
 
31
- webAuthn.type = 'webAuthn' as const
22
+ type AccountsModule = {
23
+ dialog: typeof accountsDialog
24
+ Provider: typeof AccountsProvider
25
+ dangerous_secp256k1: typeof accountsDangerousSecp256k1
26
+ webAuthn: typeof accountsWebAuthn
27
+ }
28
+ type AccountsDialogParameters = NonNullable<
29
+ Parameters<typeof accountsDialog>[0]
30
+ >
31
+ type AccountsProviderParameters = NonNullable<
32
+ Parameters<typeof AccountsProvider.create>[0]
33
+ >
34
+ type AccountsAdapter = NonNullable<AccountsProviderParameters['adapter']>
35
+ type AccountsDangerousSecp256k1Parameters = NonNullable<
36
+ Parameters<typeof accountsDangerousSecp256k1>[0]
37
+ >
38
+ type AccountsStorage = NonNullable<AccountsProviderParameters['storage']>
39
+ type AccountsWebAuthnParameters = NonNullable<
40
+ Parameters<typeof accountsWebAuthn>[0]
41
+ >
42
+ type Provider = Pick<
43
+ ReturnType<typeof AccountsProvider.create>,
44
+ 'getAccount' | 'getClient' | 'on' | 'removeListener' | 'request'
45
+ >
46
+ type AccountsConnectParameters = NonNullable<
47
+ AccountsRpc.wallet_connect.Decoded['params']
48
+ >[0]
49
+ type CapabilitiesRequest = AccountsConnectParameters['capabilities']
50
+ type InternalAccount =
51
+ AccountsRpc.wallet_connect.Encoded['returns']['accounts'][number]
52
+
53
+ const tempoWalletIcon =
54
+ 'data:image/svg+xml,<svg width="269" height="269" viewBox="0 0 269 269" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="269" height="269" fill="black"/><path d="M123.273 190.794H93.445L121.09 105.318H85.7334L93.445 80.2642H191.95L184.238 105.318H150.773L123.273 190.794Z" fill="white"/></svg>'
55
+
56
+ /** @deprecated use `tempoWallet.Parameters` instead */
57
+ export type TempoWalletParameters = tempoWallet.Parameters
32
58
 
33
59
  /**
34
- * Connector for a WebAuthn EOA.
60
+ * Connector for the Tempo Wallet dialog.
35
61
  */
36
- export function webAuthn(options: webAuthn.Parameters) {
37
- let account: Account.RootAccount | undefined
38
- let accessKey: Account.AccessKeyAccount | undefined
39
-
40
- const defaultAccessKeyOptions = {
41
- expiry: Math.floor(
42
- (Date.now() + 24 * 60 * 60 * 1000) / 1000, // one day
43
- ),
44
- strict: false,
45
- }
46
- const accessKeyOptions = (() => {
47
- if (typeof options.grantAccessKey === 'object')
48
- return { ...defaultAccessKeyOptions, ...options.grantAccessKey }
49
- if (options.grantAccessKey === true) return defaultAccessKeyOptions
50
- return undefined
51
- })()
52
-
53
- type Properties = {
54
- // TODO(v3): Make `withCapabilities: true` default behavior
55
- connect<withCapabilities extends boolean = false>(parameters: {
56
- chainId?: number | undefined
57
- capabilities?:
58
- | (OneOf<
59
- | {
60
- label?: string | undefined
61
- type: 'sign-up'
62
- }
63
- | {
64
- selectAccount?: boolean | undefined
65
- type: 'sign-in'
66
- }
67
- | {
68
- type?: undefined
69
- }
70
- > & {
71
- credential?: { id: string; publicKey: Hex.Hex } | undefined
72
- sign?:
73
- | {
74
- hash: Hex.Hex
75
- }
76
- | undefined
77
- })
78
- | undefined
79
- isReconnecting?: boolean | undefined
80
- withCapabilities?: withCapabilities | boolean | undefined
81
- }): Promise<{
82
- accounts: withCapabilities extends true
83
- ? readonly {
84
- address: Address.Address
85
- capabilities: { signature?: Hex.Hex | undefined }
86
- }[]
87
- : readonly Address.Address[]
88
- chainId: number
89
- }>
90
- }
91
- type Provider = Pick<EIP1193Provider, 'request'>
92
- type StorageItem = {
93
- [
94
- key: `pendingKeyAuthorization:${string}`
95
- ]: KeyAuthorization.KeyAuthorization
96
- 'webAuthn.activeCredential': WebAuthnP256.P256Credential
97
- 'webAuthn.lastActiveCredential': WebAuthnP256.P256Credential
98
- }
99
-
100
- return createConnector<Provider, Properties, StorageItem>((config) => ({
101
- id: 'webAuthn',
102
- name: 'EOA (WebAuthn)',
103
- type: 'webAuthn',
104
- async setup() {
105
- const credential = await config.storage?.getItem(
106
- 'webAuthn.activeCredential',
107
- )
108
- if (!credential) return
109
- account = Account.fromWebAuthnP256(credential, {
110
- rpId: options.getOptions?.rpId ?? options.rpId,
62
+ export function tempoWallet(parameters: tempoWallet.Parameters = {}) {
63
+ const {
64
+ dialog: dialogOption,
65
+ host,
66
+ icon = tempoWalletIcon,
67
+ name,
68
+ rdns,
69
+ ...providerParameters
70
+ } = parameters
71
+
72
+ return _setup({
73
+ createAdapter(accounts) {
74
+ return accounts.dialog({
75
+ dialog: dialogOption,
76
+ host,
77
+ icon,
78
+ name,
79
+ rdns,
111
80
  })
112
81
  },
113
- async connect(parameters = {}) {
114
- const capabilities =
115
- 'capabilities' in parameters ? (parameters.capabilities ?? {}) : {}
116
- const signHash =
117
- 'sign' in capabilities ? capabilities.sign?.hash : undefined
118
-
119
- // Fast path: if a credential is provided directly, use it.
120
- if ('credential' in capabilities && capabilities.credential) {
121
- const credential =
122
- capabilities.credential as WebAuthnP256.P256Credential
123
- config.storage?.setItem(
124
- 'webAuthn.activeCredential',
125
- normalizeValue(credential),
126
- )
127
- config.storage?.setItem(
128
- 'webAuthn.lastActiveCredential',
129
- normalizeValue(credential),
130
- )
131
- account = Account.fromWebAuthnP256(credential, {
132
- rpId: options.getOptions?.rpId ?? options.rpId,
133
- })
134
- const address = getAddress(account.address)
135
- const chainId = parameters.chainId ?? config.chains[0]?.id
136
- if (!chainId) throw new ChainNotConfiguredError()
137
- return {
138
- accounts: (parameters.withCapabilities
139
- ? [{ address }]
140
- : [address]) as never,
141
- chainId,
142
- }
143
- }
144
-
145
- if (
146
- accessKeyOptions?.strict &&
147
- accessKeyOptions.expiry &&
148
- accessKeyOptions.expiry < Date.now() / 1000
149
- )
150
- throw new Error(
151
- `\`grantAccessKey.expiry = ${accessKeyOptions.expiry}\` is in the past (${new Date(accessKeyOptions.expiry * 1000).toLocaleString()}). Please provide a valid expiry.`,
152
- )
153
-
154
- // We are going to need to find:
155
- // - a WebAuthn `credential` to instantiate an account
156
- // - optionally, a `keyPair` to use as the access key for the account
157
- // - optionally, a signed `keyAuthorization` to provision the access key
158
- const {
159
- credential,
160
- keyAuthorization,
161
- keyPair,
162
- signature: signedHash,
163
- } = await (async () => {
164
- // If the connection type is of "sign-up", we are going to create a new credential
165
- // and provision an access key (if needed).
166
- if (capabilities.type === 'sign-up') {
167
- // Create credential (sign up)
168
- const createOptions_remote = await options.keyManager.getChallenge?.()
169
- const label =
170
- capabilities.label ??
171
- options.createOptions?.label ??
172
- new Date().toISOString()
173
- const rpId =
174
- createOptions_remote?.rp?.id ??
175
- options.createOptions?.rpId ??
176
- options.rpId
177
- const credential = await WebAuthnP256.createCredential({
178
- ...(options.createOptions ?? {}),
179
- label,
180
- rpId,
181
- ...(createOptions_remote ?? {}),
182
- })
183
- await options.keyManager.setPublicKey({
184
- credential: credential.raw,
185
- publicKey: credential.publicKey,
186
- })
187
-
188
- // Get key pair (access key) to use for the account.
189
- // Skip if signing a hash — access key provisioning is deferred.
190
- const keyPair = await (async () => {
191
- if (signHash) return undefined
192
- if (!accessKeyOptions) return undefined
193
- return await WebCryptoP256.createKeyPair()
194
- })()
195
-
196
- return { credential, keyPair, signature: undefined }
197
- }
198
-
199
- // If we are not selecting an account, we will check if an active credential is present in
200
- // storage and if so, we will use it to instantiate an account.
201
- if (!capabilities.selectAccount) {
202
- const credential = (await config.storage?.getItem(
203
- 'webAuthn.activeCredential',
204
- )) as WebAuthnP256.getCredential.ReturnValue | undefined
205
-
206
- if (credential) {
207
- // If signing a hash, skip local keypair checks and return
208
- // the stored credential — the hash will be signed via
209
- // `account.sign` since `createCredential` cannot sign.
210
- if (signHash)
211
- return { credential, keyPair: undefined, signature: undefined }
212
-
213
- // Get key pair (access key) to use for the account.
214
- const keyPair = await (async () => {
215
- if (!accessKeyOptions) return undefined
216
- const address = Address.fromPublicKey(
217
- PublicKey.fromHex(credential.publicKey),
218
- )
219
- return await idb.get(`accessKey:${address}`)
220
- })()
221
-
222
- // If the access key provisioning is not in strict mode, return the credential and key pair (if exists).
223
- if (!accessKeyOptions?.strict)
224
- return { credential, keyPair, signature: undefined }
225
-
226
- // If a key pair is found, return the credential and key pair.
227
- if (keyPair) return { credential, keyPair, signature: undefined }
228
-
229
- // If we are reconnecting, throw an error if not found.
230
- if (parameters.isReconnecting)
231
- throw new Error('credential not found.')
232
-
233
- // Otherwise, we want to continue to sign up or register against new key pair.
234
- }
235
- }
236
-
237
- // Discover credential
238
- {
239
- // Get key pair (access key) to use for the account.
240
- // Skip if signing a hash — access key provisioning is deferred.
241
- const keyPair = await (async () => {
242
- if (signHash) return undefined
243
- if (!accessKeyOptions) return undefined
244
- return await WebCryptoP256.createKeyPair()
245
- })()
246
-
247
- // If we are provisioning an access key, we will need to sign a key authorization.
248
- // We will need the hash (digest) to sign, and the address of the access key to construct the key authorization.
249
- const { hash, keyAuthorization_unsigned } = await (async () => {
250
- const accessKeyAddress = keyPair
251
- ? Address.fromPublicKey(keyPair.publicKey)
252
- : undefined
253
-
254
- if (!accessKeyAddress)
255
- return { keyAuthorization_unsigned: undefined, hash: undefined }
256
-
257
- const chainId = parameters.chainId ?? config.chains[0]?.id
258
- const keyAuthorization_unsigned = KeyAuthorization.from({
259
- address: accessKeyAddress,
260
- chainId: chainId ? BigInt(chainId) : undefined,
261
- expiry: accessKeyOptions?.expiry,
262
- strict: accessKeyOptions?.strict ?? false,
263
- type: 'p256',
264
- })
265
- const hash = KeyAuthorization.getSignPayload(
266
- keyAuthorization_unsigned,
267
- )
268
- return { keyAuthorization_unsigned, hash }
269
- })()
270
-
271
- // If no active credential, we will attempt to load the last active credential from storage.
272
- const lastActiveCredential = !capabilities.selectAccount
273
- ? await config.storage?.getItem('webAuthn.lastActiveCredential')
274
- : undefined
275
- const credential = await WebAuthnP256.getCredential({
276
- ...(options.getOptions ?? {}),
277
- credentialId: lastActiveCredential?.id,
278
- async getPublicKey(credential) {
279
- const publicKey = await options.keyManager.getPublicKey({
280
- credential,
281
- })
282
- if (!publicKey) throw new Error('publicKey not found.')
283
- return publicKey
284
- },
285
- hash: signHash ?? hash,
286
- rpId: options.getOptions?.rpId ?? options.rpId,
287
- })
288
-
289
- const envelope = SignatureEnvelope.from({
290
- metadata: credential.metadata,
291
- signature: credential.signature,
292
- publicKey: PublicKey.fromHex(credential.publicKey),
293
- type: 'webAuthn',
294
- })
295
-
296
- const keyAuthorization = keyAuthorization_unsigned
297
- ? KeyAuthorization.from({
298
- ...keyAuthorization_unsigned,
299
- signature: envelope,
300
- })
301
- : undefined
302
-
303
- const signature =
304
- signHash && !keyAuthorization_unsigned
305
- ? SignatureEnvelope.serialize(envelope)
306
- : undefined
307
-
308
- return { credential, keyAuthorization, keyPair, signature }
309
- }
310
- })()
311
-
312
- config.storage?.setItem(
313
- 'webAuthn.lastActiveCredential',
314
- normalizeValue(credential),
315
- )
316
- config.storage?.setItem(
317
- 'webAuthn.activeCredential',
318
- normalizeValue(credential),
319
- )
320
-
321
- account = Account.fromWebAuthnP256(credential, {
322
- rpId: options.getOptions?.rpId ?? options.rpId,
323
- })
324
-
325
- let signature: Hex.Hex | undefined
326
- if (signHash && !signedHash) {
327
- signature = await account.sign({ hash: signHash })
328
- } else if (signedHash) {
329
- signature = signedHash
330
- } else if (keyPair) {
331
- accessKey = Account.fromWebCryptoP256(keyPair, {
332
- access: account,
333
- })
334
-
335
- // If we are reconnecting, check if the access key is expired.
336
- if (parameters.isReconnecting) {
337
- if (
338
- 'keyAuthorization' in keyPair &&
339
- keyPair.keyAuthorization.expiry &&
340
- keyPair.keyAuthorization.expiry < Date.now() / 1000
341
- ) {
342
- // remove any pending key authorizations from storage.
343
- await config?.storage?.removeItem(
344
- `pendingKeyAuthorization:${account.address.toLowerCase()}`,
345
- )
82
+ icon,
83
+ id: rdns ?? 'xyz.tempo',
84
+ name: name ?? 'Tempo Wallet',
85
+ providerParameters,
86
+ rdns: rdns ?? 'xyz.tempo',
87
+ type: 'injected',
88
+ })
89
+ }
346
90
 
347
- const message = `Access key expired (on ${new Date(keyPair.keyAuthorization.expiry * 1000).toLocaleString()}).`
348
- accessKey = undefined
349
-
350
- // if in strict mode, disconnect and throw an error.
351
- if (accessKeyOptions?.strict) {
352
- await this.disconnect()
353
- throw new Error(message)
354
- }
355
- // otherwise, fall back to the root account.
356
- // biome-ignore lint/suspicious/noConsole: notify
357
- console.warn(`${message} Falling back to passkey.`)
358
- }
359
- }
360
- // If we are not reconnecting, orchestrate the provisioning of the access key.
361
- else {
362
- const keyAuth =
363
- keyAuthorization ??
364
- (await account.signKeyAuthorization(accessKey, {
365
- ...accessKeyOptions,
366
- chainId: BigInt(parameters.chainId ?? config.chains[0]?.id ?? 0),
367
- } as never))
368
-
369
- await config?.storage?.setItem(
370
- `pendingKeyAuthorization:${account.address.toLowerCase()}`,
371
- keyAuth as never,
372
- )
373
- await idb.set(`accessKey:${account.address.toLowerCase()}`, {
374
- ...keyPair,
375
- keyAuthorization: keyAuth,
376
- })
377
- }
378
- // If we are granting an access key and it is in strict mode, throw an error if the access key is not provisioned.
379
- } else if (accessKeyOptions?.strict) {
380
- await config.storage?.removeItem('webAuthn.activeCredential')
381
- throw new Error('access key not found')
382
- }
383
-
384
- const address = getAddress(account.address)
385
-
386
- const chainId = parameters.chainId ?? config.chains[0]?.id
387
- if (!chainId) throw new ChainNotConfiguredError()
388
-
389
- return {
390
- accounts: (parameters.withCapabilities
391
- ? [{ address, capabilities: { signature } }]
392
- : [address]) as never,
393
- chainId,
394
- }
395
- },
396
- async disconnect() {
397
- await config.storage?.removeItem('webAuthn.activeCredential')
398
- config.emitter.emit('disconnect')
399
- account = undefined
400
- },
401
- async getAccounts() {
402
- if (!account) return []
403
- return [getAddress(account.address)]
404
- },
405
- async getChainId() {
406
- return config.chains[0]?.id!
407
- },
408
- async isAuthorized() {
409
- try {
410
- const accounts = await this.getAccounts()
411
- return !!accounts.length
412
- } catch (error) {
413
- // biome-ignore lint/suspicious/noConsole: notify
414
- console.error(
415
- 'Connector.webAuthn: Failed to check authorization',
416
- error,
417
- )
418
- return false
419
- }
420
- },
421
- async switchChain({ chainId }) {
422
- const chain = config.chains.find((chain) => chain.id === chainId)
423
- if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
424
- return chain
425
- },
426
- onAccountsChanged() {},
427
- onChainChanged(chain) {
428
- const chainId = Number(chain)
429
- config.emitter.emit('change', { chainId })
430
- },
431
- async onDisconnect() {
432
- config.emitter.emit('disconnect')
433
- account = undefined
434
- },
435
- async getClient({ chainId } = {}) {
436
- const chain =
437
- config.chains.find((x) => x.id === chainId) ?? config.chains[0]
438
- if (!chain) throw new ChainNotConfiguredError()
439
-
440
- const transports = config.transports
441
- if (!transports) throw new ChainNotConfiguredError()
442
-
443
- const transport = transports[chain.id]
444
- if (!transport) throw new ChainNotConfiguredError()
445
-
446
- const targetAccount = await (async () => {
447
- if (!accessKey) return account
448
- if (!account) throw new Error('account not found.')
449
-
450
- const item = await idb.get(`accessKey:${account.address.toLowerCase()}`)
451
- if (
452
- item?.keyAuthorization.expiry &&
453
- item.keyAuthorization.expiry < Date.now() / 1000
454
- ) {
455
- // remove any pending key authorizations from storage.
456
- await config?.storage?.removeItem(
457
- `pendingKeyAuthorization:${account.address.toLowerCase()}`,
458
- )
459
-
460
- const message = `Access key expired (on ${new Date(item.keyAuthorization.expiry * 1000).toLocaleString()}).`
461
-
462
- // if in strict mode, disconnect and throw an error.
463
- if (accessKeyOptions?.strict) {
464
- await this.disconnect()
465
- throw new Error(message)
466
- }
91
+ export declare namespace tempoWallet {
92
+ export type Parameters = Omit<
93
+ AccountsProviderParameters,
94
+ 'adapter' | 'chains'
95
+ > &
96
+ AccountsDialogParameters
467
97
 
468
- // otherwise, fall back to the root account.
469
- // biome-ignore lint/suspicious/noConsole: notify
470
- console.warn(`${message} Falling back to passkey.`)
471
- return account
472
- }
473
- return accessKey
474
- })()
475
- if (!targetAccount) throw new Error('account not found.')
476
-
477
- const targetChain = defineChain({
478
- ...chain,
479
- prepareTransactionRequest: [
480
- async (args, { phase }) => {
481
- const keyAuthorization = await (async () => {
482
- {
483
- const keyAuthorization = (
484
- args as {
485
- keyAuthorization?:
486
- | KeyAuthorization.KeyAuthorization
487
- | undefined
488
- }
489
- ).keyAuthorization
490
- if (keyAuthorization) return keyAuthorization
491
- }
492
-
493
- const keyAuthorization = await config.storage?.getItem(
494
- `pendingKeyAuthorization:${targetAccount?.address.toLowerCase()}`,
495
- )
496
- await config.storage?.removeItem(
497
- `pendingKeyAuthorization:${targetAccount?.address.toLowerCase()}`,
498
- )
499
- return keyAuthorization
500
- })()
501
-
502
- const [prepareTransactionRequestFn, options] = (() => {
503
- if (!chain.prepareTransactionRequest)
504
- return [undefined, undefined]
505
- if (typeof chain.prepareTransactionRequest === 'function')
506
- return [chain.prepareTransactionRequest, undefined]
507
- return chain.prepareTransactionRequest
508
- })()
509
-
510
- const request = await (async () => {
511
- if (!prepareTransactionRequestFn) return {}
512
- if (!options || options?.runAt?.includes(phase))
513
- return await prepareTransactionRequestFn(args, { phase })
514
- return {}
515
- })()
516
-
517
- return {
518
- ...args,
519
- ...request,
520
- keyAuthorization,
521
- }
522
- },
523
- {
524
- runAt: [
525
- 'afterFillParameters',
526
- 'beforeFillParameters',
527
- 'beforeFillTransaction',
528
- ],
529
- },
530
- ],
531
- })
98
+ export type ConnectParameters<withCapabilities extends boolean = false> =
99
+ setup.ConnectParameters<withCapabilities>
532
100
 
533
- return createClient({
534
- account: targetAccount,
535
- chain: targetChain,
536
- transport: walletNamespaceCompat(transport, {
537
- account: targetAccount,
538
- }),
539
- })
540
- },
541
- async getProvider({ chainId } = {}) {
542
- const { request } = await this.getClient!({ chainId })
543
- return { request }
544
- },
545
- }))
101
+ export type ConnectReturnType<withCapabilities extends boolean = false> =
102
+ setup.ConnectReturnType<withCapabilities>
546
103
  }
547
104
 
548
- export namespace webAuthn {
549
- export type Parameters = {
550
- /** Options for WebAuthn registration. */
551
- createOptions?:
552
- | Pick<
553
- WebAuthnP256.createCredential.Parameters,
554
- 'createFn' | 'label' | 'rpId' | 'userId' | 'timeout'
555
- >
556
- | undefined
557
- /** Options for WebAuthn authentication. */
558
- getOptions?:
559
- | Pick<WebAuthnP256.getCredential.Parameters, 'getFn' | 'rpId'>
560
- | undefined
561
- /**
562
- * Whether or not to grant an access key upon connection, and optionally, expiry + limits to assign to the key.
563
- */
564
- grantAccessKey?:
565
- | boolean
566
- | (Pick<KeyAuthorization.KeyAuthorization, 'expiry' | 'limits'> & {
567
- /** Whether or not to throw an error and disconnect if the access key is not provisioned or is expired. */
568
- strict?: boolean | undefined
569
- })
570
- /** Public key manager. */
571
- keyManager: KeyManager.KeyManager
572
- /** The RP ID to use for WebAuthn. */
573
- rpId?: string | undefined
574
- }
575
- }
105
+ /** @deprecated use `webAuthn.Parameters` instead */
106
+ export type WebAuthnParameters = webAuthn.Parameters
107
+
108
+ webAuthn.type = 'webAuthn' as const
576
109
 
577
110
  /**
578
- * Normalizes a value into a structured-clone compatible format.
579
- *
580
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone
111
+ * Connector for a WebAuthn EOA.
581
112
  */
582
- function normalizeValue<type>(value: type): type {
583
- if (Array.isArray(value)) return value.map(normalizeValue) as never
584
- if (typeof value === 'function') return undefined as never
585
- if (typeof value !== 'object' || value === null) return value
586
- if (Object.getPrototypeOf(value) !== Object.prototype)
587
- try {
588
- return structuredClone(value)
589
- } catch {
590
- return undefined as never
591
- }
592
-
593
- const normalized: Record<string, unknown> = {}
594
- for (const [k, v] of Object.entries(value)) normalized[k] = normalizeValue(v)
595
- return normalized as never
113
+ export function webAuthn(parameters: webAuthn.Parameters = {}) {
114
+ const { authUrl, ceremony, icon, name, rdns, ...providerParameters } =
115
+ parameters
116
+
117
+ return _setup({
118
+ createAdapter(accounts) {
119
+ return ceremony
120
+ ? accounts.webAuthn({ ceremony, icon, name, rdns })
121
+ : accounts.webAuthn({ authUrl, icon, name, rdns })
122
+ },
123
+ icon,
124
+ id: 'webAuthn',
125
+ name: name ?? 'EOA (WebAuthn)',
126
+ providerParameters,
127
+ rdns,
128
+ type: 'webAuthn',
129
+ })
596
130
  }
597
131
 
598
- // Based on `idb-keyval`
599
- // https://github.com/jakearchibald/idb-keyval
600
- let defaultGetStoreFunc:
601
- | (<type>(
602
- txMode: IDBTransactionMode,
603
- callback: (store: IDBObjectStore) => type | PromiseLike<type>,
604
- ) => Promise<type>)
605
- | undefined
606
-
607
- const idb = {
608
- /**
609
- * Get a value by its key.
610
- *
611
- * @param key
612
- * @param customStore Method to get a custom store. Use with caution (see the docs).
613
- */
614
- get<type = any>(key: IDBValidKey): Promise<type | undefined> {
615
- return this.defaultGetStore()('readonly', (store) =>
616
- this.promisifyRequest(store.get(key)),
617
- )
618
- },
619
- /**
620
- * Set a value with a key.
621
- *
622
- * @param key
623
- * @param value
624
- * @param customStore Method to get a custom store. Use with caution (see the docs).
625
- */
626
- set(key: IDBValidKey, value: any): Promise<void> {
627
- return this.defaultGetStore()('readwrite', (store) => {
628
- store.put(value, key)
629
- return this.promisifyRequest(store.transaction)
630
- })
631
- },
632
- defaultGetStore() {
633
- if (!defaultGetStoreFunc)
634
- defaultGetStoreFunc = this.createStore('keyval-store', 'keyval')
635
- return defaultGetStoreFunc
636
- },
637
- createStore(
638
- dbName: string,
639
- storeName: string,
640
- ): NonNullable<typeof defaultGetStoreFunc> {
641
- let dbp: Promise<IDBDatabase> | undefined
642
-
643
- const getDB = () => {
644
- if (dbp) return dbp
645
- const request = indexedDB.open(dbName)
646
- request.onupgradeneeded = () =>
647
- request.result.createObjectStore(storeName)
648
- dbp = this.promisifyRequest(request)
649
-
650
- dbp.then(
651
- (db) => {
652
- // It seems like Safari sometimes likes to just close the connection.
653
- // It's supposed to fire this event when that happens. Let's hope it does!
654
- db.onclose = () => {
655
- dbp = undefined
656
- }
657
- },
658
- () => {},
659
- )
660
- return dbp
661
- }
662
-
663
- return (txMode, callback) =>
664
- getDB().then((db) =>
665
- callback(db.transaction(storeName, txMode).objectStore(storeName)),
666
- )
667
- },
668
- promisifyRequest<type = undefined>(
669
- request: IDBRequest<type> | IDBTransaction,
670
- ): Promise<type> {
671
- return new Promise<type>((resolve, reject) => {
672
- // @ts-ignore - file size hacks
673
- request.oncomplete = request.onsuccess = () => resolve(request.result)
674
- // @ts-ignore - file size hacks
675
- request.onabort = request.onerror = () => reject(request.error)
676
- })
677
- },
132
+ export declare namespace webAuthn {
133
+ export type Parameters = AccountsWebAuthnParameters &
134
+ Omit<AccountsProviderParameters, 'adapter' | 'chains'>
678
135
  }
679
136
 
680
137
  /** @deprecated use `dangerous_secp256k1.Parameters` instead */
@@ -690,167 +147,318 @@ dangerous_secp256k1.type = 'dangerous_secp256k1' as const
690
147
  * length of the storage used.
691
148
  */
692
149
  export function dangerous_secp256k1(
693
- options: dangerous_secp256k1.Parameters = {},
150
+ parameters: dangerous_secp256k1.Parameters = {},
694
151
  ) {
695
- let account: LocalAccount | undefined
696
-
697
- type Properties = {
698
- // TODO(v3): Make `withCapabilities: true` default behavior
699
- connect<withCapabilities extends boolean = false>(parameters: {
700
- capabilities?: { type?: 'sign-up' | undefined } | undefined
701
- chainId?: number | undefined
702
- isReconnecting?: boolean | undefined
703
- withCapabilities?: withCapabilities | boolean | undefined
704
- }): Promise<{
705
- accounts: readonly Address.Address[]
706
- chainId: number
707
- }>
708
- }
709
- type Provider = Pick<EIP1193Provider, 'request'>
710
- type StorageItem = {
711
- 'secp256k1.activeAddress': Address.Address
712
- 'secp256k1.lastActiveAddress': Address.Address
713
- [key: `secp256k1.${string}.privateKey`]: Hex.Hex
714
- }
152
+ const { icon, name, privateKey, rdns, ...providerParameters } = parameters
715
153
 
716
- return createConnector<Provider, Properties, StorageItem>((config) => ({
154
+ return _setup({
155
+ createAdapter(accounts) {
156
+ return accounts.dangerous_secp256k1({ icon, name, privateKey, rdns })
157
+ },
158
+ icon,
717
159
  id: 'secp256k1',
718
- name: 'EOA (Secp256k1)',
160
+ name: name ?? 'EOA (Secp256k1)',
161
+ providerParameters,
162
+ rdns,
719
163
  type: 'secp256k1',
720
- async setup() {
721
- const address = await config.storage?.getItem('secp256k1.activeAddress')
722
- const privateKey = await config.storage?.getItem(
723
- `secp256k1.${address}.privateKey`,
724
- )
725
- if (privateKey) account = privateKeyToAccount(privateKey)
726
- else if (
727
- address &&
728
- options.account &&
729
- Address.isEqual(address, options.account.address)
730
- )
731
- account = options.account
732
- },
733
- async connect(parameters = {}) {
734
- const address = await (async () => {
735
- if (
736
- 'capabilities' in parameters &&
737
- parameters.capabilities?.type === 'sign-up'
738
- ) {
739
- const privateKey = generatePrivateKey()
740
- const account = privateKeyToAccount(privateKey)
741
- const address = account.address
742
- await config.storage?.setItem(
743
- `secp256k1.${address}.privateKey`,
744
- privateKey,
745
- )
746
- await config.storage?.setItem('secp256k1.activeAddress', address)
747
- await config.storage?.setItem('secp256k1.lastActiveAddress', address)
748
- return address
749
- }
750
-
751
- const address = await config.storage?.getItem(
752
- 'secp256k1.lastActiveAddress',
753
- )
754
- const privateKey = await config.storage?.getItem(
755
- `secp256k1.${address}.privateKey`,
756
- )
757
-
758
- if (privateKey) account = privateKeyToAccount(privateKey)
759
- else if (options.account) {
760
- account = options.account
761
- await config.storage?.setItem(
762
- 'secp256k1.lastActiveAddress',
763
- account.address,
764
- )
765
- }
766
-
767
- if (!account) throw new Error('account not found.')
768
-
769
- await config.storage?.setItem(
770
- 'secp256k1.activeAddress',
771
- account.address,
772
- )
773
- return account.address
774
- })()
164
+ })
165
+ }
775
166
 
776
- const chainId = parameters.chainId ?? config.chains[0]?.id
777
- if (!chainId) throw new ChainNotConfiguredError()
167
+ export declare namespace dangerous_secp256k1 {
168
+ export type Parameters = AccountsDangerousSecp256k1Parameters &
169
+ Omit<AccountsProviderParameters, 'adapter' | 'chains'>
170
+ }
778
171
 
779
- return {
780
- accounts: (parameters.withCapabilities
781
- ? [{ address }]
782
- : [address]) as never,
783
- chainId,
784
- }
785
- },
786
- async disconnect() {
787
- await config.storage?.removeItem('secp256k1.activeAddress')
788
- account = undefined
789
- },
790
- async getAccounts() {
791
- if (!account) return []
792
- return [getAddress(account.address)]
172
+ function createAccountsStorage(
173
+ storage: {
174
+ getItem(key: string, defaultValue?: null | undefined): unknown
175
+ setItem(key: string, value: unknown): void | Promise<void>
176
+ removeItem(key: string): void | Promise<void>
177
+ },
178
+ namespace: string,
179
+ ): AccountsStorage {
180
+ const prefix = `accounts.${namespace}`
181
+ return {
182
+ async getItem<value>(key: string) {
183
+ return ((await storage.getItem(`${prefix}.${key}`, null)) ??
184
+ null) as value | null
793
185
  },
794
- async getChainId() {
795
- return config.chains[0]?.id!
186
+ async removeItem(key) {
187
+ await storage.removeItem(`${prefix}.${key}`)
796
188
  },
797
- async isAuthorized() {
798
- try {
799
- const accounts = await this.getAccounts()
800
- return !!accounts.length
801
- } catch (error) {
802
- // biome-ignore lint/suspicious/noConsole: notify
803
- console.error(
804
- 'Connector.secp256k1: Failed to check authorization',
805
- error,
806
- )
807
- return false
808
- }
189
+ async setItem(key, value) {
190
+ await storage.setItem(`${prefix}.${key}`, value)
809
191
  },
810
- async switchChain({ chainId }) {
811
- const chain = config.chains.find((chain) => chain.id === chainId)
812
- if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
813
- return chain
192
+ }
193
+ }
194
+
195
+ function createMemoryAccountsStorage(): AccountsStorage {
196
+ const map = new Map<string, unknown>()
197
+ return {
198
+ async getItem<value>(key: string) {
199
+ return (map.get(key) ?? null) as value | null
814
200
  },
815
- onAccountsChanged() {},
816
- onChainChanged(chain) {
817
- const chainId = Number(chain)
818
- config.emitter.emit('change', { chainId })
201
+ async removeItem(key) {
202
+ map.delete(key)
819
203
  },
820
- async onDisconnect() {
821
- config.emitter.emit('disconnect')
822
- account = undefined
204
+ async setItem(key, value) {
205
+ map.set(key, value)
823
206
  },
824
- async getClient({ chainId } = {}) {
825
- const chain =
826
- config.chains.find((x) => x.id === chainId) ?? config.chains[0]
827
- if (!chain) throw new ChainNotConfiguredError()
207
+ }
208
+ }
828
209
 
829
- const transports = config.transports
830
- if (!transports) throw new ChainNotConfiguredError()
210
+ function _setup(parameters: setup.Parameters) {
211
+ type Properties = {
212
+ connect<withCapabilities extends boolean = false>(
213
+ parameters?: setup.ConnectParameters<withCapabilities>,
214
+ ): Promise<setup.ConnectReturnType<withCapabilities>>
215
+ }
831
216
 
832
- const transport = transports[chain.id]
833
- if (!transport) throw new ChainNotConfiguredError()
217
+ return createConnector<Provider, Properties>((config) => {
218
+ const chains = config.chains
219
+ let providerPromise: Promise<Provider> | undefined
834
220
 
835
- if (!account) throw new Error('account not found.')
221
+ let accountsChanged: Connector['onAccountsChanged'] | undefined
222
+ let chainChanged: ((chain: string) => void) | undefined
223
+ let connect: ((connectInfo: ProviderConnectInfo) => void) | undefined
224
+ let disconnect: ((error?: Error | undefined) => void) | undefined
836
225
 
837
- return createClient({
838
- account,
839
- chain,
840
- transport: walletNamespaceCompat(transport, {
841
- account,
842
- }),
226
+ async function getAccountsModule() {
227
+ return await import('accounts').catch(() => {
228
+ throw new Error('dependency "accounts" not found')
843
229
  })
844
- },
845
- async getProvider({ chainId } = {}) {
846
- const { request } = await this.getClient!({ chainId })
847
- return { request }
848
- },
849
- }))
230
+ }
231
+
232
+ async function getProvider() {
233
+ providerPromise ??= (async () => {
234
+ const accounts = await getAccountsModule()
235
+ return accounts.Provider.create({
236
+ ...parameters.providerParameters,
237
+ adapter: parameters.createAdapter(accounts),
238
+ chains: config.chains as never,
239
+ storage:
240
+ parameters.providerParameters.storage ??
241
+ (config.storage
242
+ ? createAccountsStorage(config.storage, parameters.id)
243
+ : createMemoryAccountsStorage()),
244
+ }) as unknown as Provider
245
+ })()
246
+
247
+ return await providerPromise
248
+ }
249
+
250
+ return {
251
+ icon: parameters.icon,
252
+ id: parameters.id,
253
+ name: parameters.name,
254
+ rdns: parameters.rdns,
255
+ type: parameters.type,
256
+ async connect(connectParameters = {}) {
257
+ const { chainId, isReconnecting, withCapabilities } = connectParameters
258
+ const capabilities =
259
+ 'capabilities' in connectParameters
260
+ ? connectParameters.capabilities
261
+ : undefined
262
+
263
+ let accounts: readonly InternalAccount[] = []
264
+ let currentChainId: number | undefined
265
+
266
+ if (isReconnecting) {
267
+ accounts = await this.getAccounts()
268
+ .then((accounts) =>
269
+ accounts.map((address) => ({ address, capabilities: {} })),
270
+ )
271
+ .catch(() => [])
272
+ }
273
+
274
+ try {
275
+ if (!accounts.length && !isReconnecting) {
276
+ const provider = await getProvider()
277
+ const response = (await provider.request({
278
+ method: 'wallet_connect',
279
+ params: [
280
+ {
281
+ ...(chainId ? { chainId } : {}),
282
+ ...(capabilities ? { capabilities } : {}),
283
+ },
284
+ ] as never,
285
+ })) as AccountsRpc.wallet_connect.Encoded['returns']
286
+ accounts = response.accounts
287
+ }
288
+
289
+ currentChainId ??= await this.getChainId()
290
+ if (!currentChainId) throw new ChainNotConfiguredError()
291
+
292
+ const provider = await getProvider()
293
+ if (connect) {
294
+ provider.removeListener('connect', connect)
295
+ connect = undefined
296
+ }
297
+ if (!accountsChanged) {
298
+ accountsChanged = this.onAccountsChanged.bind(this)
299
+ provider.on('accountsChanged', accountsChanged as never)
300
+ }
301
+ if (!chainChanged) {
302
+ chainChanged = this.onChainChanged.bind(this)
303
+ provider.on('chainChanged', chainChanged)
304
+ }
305
+ if (!disconnect) {
306
+ disconnect = this.onDisconnect.bind(this)
307
+ provider.on('disconnect', disconnect)
308
+ }
309
+
310
+ return {
311
+ accounts: (withCapabilities
312
+ ? accounts
313
+ : accounts.map((account) => account.address)) as never,
314
+ chainId: currentChainId,
315
+ }
316
+ } catch (error) {
317
+ const rpcError = error as RpcError
318
+ if (rpcError.code === UserRejectedRequestError.code)
319
+ throw new UserRejectedRequestError(rpcError)
320
+ throw rpcError
321
+ }
322
+ },
323
+ async disconnect() {
324
+ const provider = await getProvider()
325
+
326
+ if (chainChanged) {
327
+ provider.removeListener('chainChanged', chainChanged)
328
+ chainChanged = undefined
329
+ }
330
+ if (disconnect) {
331
+ provider.removeListener('disconnect', disconnect)
332
+ disconnect = undefined
333
+ }
334
+ if (!connect) {
335
+ connect = this.onConnect?.bind(this)
336
+ if (connect) provider.on('connect', connect)
337
+ }
338
+
339
+ await provider.request({ method: 'wallet_disconnect' })
340
+ },
341
+ async getAccounts() {
342
+ const provider = await getProvider()
343
+ return await provider.request({ method: 'eth_accounts' })
344
+ },
345
+ async getChainId() {
346
+ const provider = await getProvider()
347
+ return Number(await provider.request({ method: 'eth_chainId' }))
348
+ },
349
+ async getClient({ chainId } = {}) {
350
+ const provider = await getProvider()
351
+ return Object.assign(provider.getClient({ chainId }), {
352
+ account: provider.getAccount(),
353
+ }) as never
354
+ },
355
+ async getProvider() {
356
+ return await getProvider()
357
+ },
358
+ async isAuthorized() {
359
+ try {
360
+ const accounts = await withRetry(() => this.getAccounts())
361
+ return !!accounts.length
362
+ } catch {
363
+ return false
364
+ }
365
+ },
366
+ onAccountsChanged(accounts) {
367
+ config.emitter.emit('change', {
368
+ accounts: accounts as readonly Address[],
369
+ })
370
+ },
371
+ onChainChanged(chain) {
372
+ config.emitter.emit('change', { chainId: Number(chain) })
373
+ },
374
+ async onConnect(connectInfo) {
375
+ const accounts = await this.getAccounts()
376
+ if (accounts.length === 0) return
377
+
378
+ const chainId = Number(connectInfo.chainId)
379
+ config.emitter.emit('connect', { accounts, chainId })
380
+
381
+ const provider = await getProvider()
382
+ if (connect) {
383
+ provider.removeListener('connect', connect)
384
+ connect = undefined
385
+ }
386
+ if (!accountsChanged) {
387
+ accountsChanged = this.onAccountsChanged.bind(this)
388
+ provider.on('accountsChanged', accountsChanged as never)
389
+ }
390
+ if (!chainChanged) {
391
+ chainChanged = this.onChainChanged.bind(this)
392
+ provider.on('chainChanged', chainChanged)
393
+ }
394
+ if (!disconnect) {
395
+ disconnect = this.onDisconnect.bind(this)
396
+ provider.on('disconnect', disconnect)
397
+ }
398
+ },
399
+ async onDisconnect(_error) {
400
+ const provider = await getProvider()
401
+
402
+ config.emitter.emit('disconnect')
403
+
404
+ if (chainChanged) {
405
+ provider.removeListener('chainChanged', chainChanged)
406
+ chainChanged = undefined
407
+ }
408
+ if (disconnect) {
409
+ provider.removeListener('disconnect', disconnect)
410
+ disconnect = undefined
411
+ }
412
+ if (!connect) {
413
+ connect = this.onConnect?.bind(this)
414
+ if (connect) provider.on('connect', connect)
415
+ }
416
+ },
417
+ async setup() {
418
+ if (!connect) {
419
+ const provider = await getProvider()
420
+ connect = this.onConnect?.bind(this)
421
+ if (connect) provider.on('connect', connect)
422
+ }
423
+ },
424
+ async switchChain({ chainId }) {
425
+ const chain = chains.find((chain) => chain.id === chainId)
426
+ if (!chain) throw new SwitchChainError(new ChainNotConfiguredError())
427
+
428
+ const provider = await getProvider()
429
+ await provider.request({
430
+ method: 'wallet_switchEthereumChain',
431
+ params: [{ chainId: numberToHex(chainId) }],
432
+ })
433
+
434
+ return chain
435
+ },
436
+ }
437
+ })
850
438
  }
851
439
 
852
- export declare namespace dangerous_secp256k1 {
440
+ export declare namespace setup {
853
441
  export type Parameters = {
854
- account?: LocalAccount | undefined
442
+ createAdapter: (accounts: AccountsModule) => AccountsAdapter
443
+ icon?: string | undefined
444
+ id: string
445
+ name: string
446
+ providerParameters: Omit<AccountsProviderParameters, 'adapter' | 'chains'>
447
+ rdns?: string | readonly string[] | undefined
448
+ type: string
449
+ }
450
+
451
+ export type ConnectParameters<withCapabilities extends boolean = false> = {
452
+ capabilities?: CapabilitiesRequest | undefined
453
+ chainId?: number | undefined
454
+ isReconnecting?: boolean | undefined
455
+ withCapabilities?: withCapabilities | boolean | undefined
456
+ }
457
+
458
+ export type ConnectReturnType<withCapabilities extends boolean = false> = {
459
+ accounts: withCapabilities extends true
460
+ ? readonly InternalAccount[]
461
+ : readonly Address[]
462
+ chainId: number
855
463
  }
856
464
  }