@wagmi/core 3.0.2 → 3.2.0

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