accounts 0.3.0 → 0.4.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +97 -0
  4. package/dist/core/AccessKey.d.ts +55 -0
  5. package/dist/core/AccessKey.d.ts.map +1 -0
  6. package/dist/core/AccessKey.js +69 -0
  7. package/dist/core/AccessKey.js.map +1 -0
  8. package/dist/core/Account.d.ts +91 -0
  9. package/dist/core/Account.d.ts.map +1 -0
  10. package/dist/core/Account.js +64 -0
  11. package/dist/core/Account.js.map +1 -0
  12. package/dist/core/Adapter.d.ts +187 -0
  13. package/dist/core/Adapter.d.ts.map +1 -0
  14. package/dist/core/Adapter.js +7 -0
  15. package/dist/core/Adapter.js.map +1 -0
  16. package/dist/core/Ceremony.d.ts +109 -0
  17. package/dist/core/Ceremony.d.ts.map +1 -0
  18. package/dist/core/Ceremony.js +104 -0
  19. package/dist/core/Ceremony.js.map +1 -0
  20. package/dist/core/Client.d.ts +16 -0
  21. package/dist/core/Client.d.ts.map +1 -0
  22. package/dist/core/Client.js +18 -0
  23. package/dist/core/Client.js.map +1 -0
  24. package/dist/core/Dialog.d.ts +52 -0
  25. package/dist/core/Dialog.d.ts.map +1 -0
  26. package/dist/core/Dialog.js +342 -0
  27. package/dist/core/Dialog.js.map +1 -0
  28. package/dist/core/Expiry.d.ts +15 -0
  29. package/dist/core/Expiry.d.ts.map +1 -0
  30. package/dist/core/Expiry.js +29 -0
  31. package/dist/core/Expiry.js.map +1 -0
  32. package/dist/core/Messenger.d.ts +86 -0
  33. package/dist/core/Messenger.d.ts.map +1 -0
  34. package/dist/core/Messenger.js +127 -0
  35. package/dist/core/Messenger.js.map +1 -0
  36. package/dist/core/Provider.d.ts +69 -0
  37. package/dist/core/Provider.d.ts.map +1 -0
  38. package/dist/core/Provider.js +401 -0
  39. package/dist/core/Provider.js.map +1 -0
  40. package/dist/core/Remote.d.ts +114 -0
  41. package/dist/core/Remote.d.ts.map +1 -0
  42. package/dist/core/Remote.js +116 -0
  43. package/dist/core/Remote.js.map +1 -0
  44. package/dist/core/Schema.d.ts +805 -0
  45. package/dist/core/Schema.d.ts.map +1 -0
  46. package/dist/core/Schema.js +43 -0
  47. package/dist/core/Schema.js.map +1 -0
  48. package/dist/core/Storage.d.ts +42 -0
  49. package/dist/core/Storage.d.ts.map +1 -0
  50. package/dist/core/Storage.js +173 -0
  51. package/dist/core/Storage.js.map +1 -0
  52. package/dist/core/Store.d.ts +58 -0
  53. package/dist/core/Store.d.ts.map +1 -0
  54. package/dist/core/Store.js +58 -0
  55. package/dist/core/Store.js.map +1 -0
  56. package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
  57. package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
  58. package/dist/core/adapters/dangerous_secp256k1.js +39 -0
  59. package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
  60. package/dist/core/adapters/dialog.d.ts +31 -0
  61. package/dist/core/adapters/dialog.d.ts.map +1 -0
  62. package/dist/core/adapters/dialog.js +306 -0
  63. package/dist/core/adapters/dialog.js.map +1 -0
  64. package/dist/core/adapters/local.d.ts +33 -0
  65. package/dist/core/adapters/local.d.ts.map +1 -0
  66. package/dist/core/adapters/local.js +227 -0
  67. package/dist/core/adapters/local.js.map +1 -0
  68. package/dist/core/adapters/webAuthn.d.ts +36 -0
  69. package/dist/core/adapters/webAuthn.d.ts.map +1 -0
  70. package/dist/core/adapters/webAuthn.js +93 -0
  71. package/dist/core/adapters/webAuthn.js.map +1 -0
  72. package/dist/core/internal/withDedupe.d.ts +12 -0
  73. package/dist/core/internal/withDedupe.d.ts.map +1 -0
  74. package/dist/core/internal/withDedupe.js +12 -0
  75. package/dist/core/internal/withDedupe.js.map +1 -0
  76. package/dist/core/zod/request.d.ts +31 -0
  77. package/dist/core/zod/request.d.ts.map +1 -0
  78. package/dist/core/zod/request.js +41 -0
  79. package/dist/core/zod/request.js.map +1 -0
  80. package/dist/core/zod/rpc.d.ts +603 -0
  81. package/dist/core/zod/rpc.d.ts.map +1 -0
  82. package/dist/core/zod/rpc.js +293 -0
  83. package/dist/core/zod/rpc.js.map +1 -0
  84. package/dist/core/zod/utils.d.ts +18 -0
  85. package/dist/core/zod/utils.d.ts.map +1 -0
  86. package/dist/core/zod/utils.js +21 -0
  87. package/dist/core/zod/utils.js.map +1 -0
  88. package/dist/index.d.ts +15 -0
  89. package/dist/index.d.ts.map +1 -0
  90. package/dist/index.js +15 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/internal/types.d.ts +284 -0
  93. package/dist/internal/types.d.ts.map +1 -0
  94. package/dist/internal/types.js +2 -0
  95. package/dist/internal/types.js.map +1 -0
  96. package/dist/server/Handler.d.ts +257 -0
  97. package/dist/server/Handler.d.ts.map +1 -0
  98. package/dist/server/Handler.js +433 -0
  99. package/dist/server/Handler.js.map +1 -0
  100. package/dist/server/Kv.d.ts +16 -0
  101. package/dist/server/Kv.d.ts.map +1 -0
  102. package/dist/server/Kv.js +30 -0
  103. package/dist/server/Kv.js.map +1 -0
  104. package/dist/server/index.d.ts +3 -0
  105. package/dist/server/index.d.ts.map +1 -0
  106. package/dist/server/index.js +3 -0
  107. package/dist/server/index.js.map +1 -0
  108. package/dist/server/internal/requestListener.d.ts +124 -0
  109. package/dist/server/internal/requestListener.d.ts.map +1 -0
  110. package/dist/server/internal/requestListener.js +173 -0
  111. package/dist/server/internal/requestListener.js.map +1 -0
  112. package/dist/wagmi/Connector.d.ts +93 -0
  113. package/dist/wagmi/Connector.d.ts.map +1 -0
  114. package/dist/wagmi/Connector.js +238 -0
  115. package/dist/wagmi/Connector.js.map +1 -0
  116. package/dist/wagmi/index.d.ts +3 -0
  117. package/dist/wagmi/index.d.ts.map +1 -0
  118. package/dist/wagmi/index.js +3 -0
  119. package/dist/wagmi/index.js.map +1 -0
  120. package/package.json +56 -2
  121. package/src/core/AccessKey.test.ts +257 -0
  122. package/src/core/AccessKey.ts +123 -0
  123. package/src/core/Account.test.ts +309 -0
  124. package/src/core/Account.ts +152 -0
  125. package/src/core/Adapter.ts +238 -0
  126. package/src/core/Ceremony.browser.test.ts +239 -0
  127. package/src/core/Ceremony.test.ts +151 -0
  128. package/src/core/Ceremony.ts +203 -0
  129. package/src/core/Client.ts +36 -0
  130. package/src/core/Dialog.browser.test.ts +309 -0
  131. package/src/core/Dialog.test-d.ts +19 -0
  132. package/src/core/Dialog.ts +442 -0
  133. package/src/core/Expiry.ts +34 -0
  134. package/src/core/Messenger.ts +206 -0
  135. package/src/core/Provider.browser.test.ts +774 -0
  136. package/src/core/Provider.connect.browser.test.ts +415 -0
  137. package/src/core/Provider.test-d.ts +53 -0
  138. package/src/core/Provider.test.ts +1566 -0
  139. package/src/core/Provider.ts +559 -0
  140. package/src/core/Remote.ts +262 -0
  141. package/src/core/Schema.test-d.ts +211 -0
  142. package/src/core/Schema.ts +143 -0
  143. package/src/core/Storage.ts +213 -0
  144. package/src/core/Store.test.ts +287 -0
  145. package/src/core/Store.ts +129 -0
  146. package/src/core/adapters/dangerous_secp256k1.ts +53 -0
  147. package/src/core/adapters/dialog.ts +379 -0
  148. package/src/core/adapters/local.test.ts +97 -0
  149. package/src/core/adapters/local.ts +277 -0
  150. package/src/core/adapters/webAuthn.ts +129 -0
  151. package/src/core/internal/withDedupe.test.ts +116 -0
  152. package/src/core/internal/withDedupe.ts +20 -0
  153. package/src/core/mppx.test.ts +83 -0
  154. package/src/core/zod/request.test.ts +121 -0
  155. package/src/core/zod/request.ts +70 -0
  156. package/src/core/zod/rpc.ts +374 -0
  157. package/src/core/zod/utils.test.ts +69 -0
  158. package/src/core/zod/utils.ts +40 -0
  159. package/src/index.ts +14 -0
  160. package/src/internal/types.ts +378 -0
  161. package/src/server/Handler.test.ts +1014 -0
  162. package/src/server/Handler.ts +605 -0
  163. package/src/server/Kv.ts +46 -0
  164. package/src/server/index.ts +2 -0
  165. package/src/server/internal/requestListener.ts +273 -0
  166. package/src/tsconfig.json +9 -0
  167. package/src/wagmi/Connector.ts +287 -0
  168. package/src/wagmi/index.ts +2 -0
@@ -0,0 +1,213 @@
1
+ import { createStore, del, get, set } from 'idb-keyval'
2
+ import { Json } from 'ox'
3
+
4
+ import type { MaybePromise } from '../internal/types.js'
5
+
6
+ /** Pluggable storage adapter for persisting provider state. */
7
+ export type Storage = {
8
+ getItem: <value>(name: string) => MaybePromise<value | null>
9
+ setItem: (name: string, value: unknown) => MaybePromise<void>
10
+ removeItem: (name: string) => MaybePromise<void>
11
+ }
12
+
13
+ /** Creates a storage adapter from a custom implementation, optionally scoping all keys under a prefix. */
14
+ export function from(storage: Storage, options: from.Options = {}): Storage {
15
+ const key = options.key ?? 'tempo'
16
+ const prefix = `${key}.`
17
+ return {
18
+ getItem: (name) => storage.getItem(`${prefix}${name}`),
19
+ setItem: (name, value) => storage.setItem(`${prefix}${name}`, value),
20
+ removeItem: (name) => storage.removeItem(`${prefix}${name}`),
21
+ }
22
+ }
23
+
24
+ export declare namespace from {
25
+ type Options = {
26
+ /** Key prefix for all stored items. @default "tempo" */
27
+ key?: string | undefined
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Combines multiple storage adapters into one. Reads return the first
33
+ * non-null result; writes propagate to all storages (failures are isolated
34
+ * via `Promise.allSettled`).
35
+ */
36
+ export function combine(...storages: readonly Storage[]): Storage {
37
+ return {
38
+ async getItem<value>(name: string) {
39
+ const results = await Promise.allSettled(storages.map((x) => x.getItem<value>(name)))
40
+ const result = results.find((x) => x.status === 'fulfilled' && x.value !== null)
41
+ if (result?.status !== 'fulfilled') return null
42
+ return result.value as value
43
+ },
44
+ async removeItem(name) {
45
+ await Promise.allSettled(storages.map((x) => x.removeItem(name)))
46
+ },
47
+ async setItem(name, value) {
48
+ await Promise.allSettled(storages.map((x) => x.setItem(name, value)))
49
+ },
50
+ }
51
+ }
52
+
53
+ /** Creates a `document.cookie`-backed storage adapter. Uses `SameSite=None; Secure` with a 1-year expiry. Deep objects are flattened into individual cookies to stay within the 4KB-per-cookie browser limit. */
54
+ export function cookie(options: cookie.Options = {}): Storage {
55
+ function getRaw(name: string): string | undefined {
56
+ return document.cookie.split('; ').find((x) => x.startsWith(`${name}=`))
57
+ }
58
+
59
+ function setRaw(name: string, value: string) {
60
+ document.cookie = `${name}=${value};path=/;samesite=None;secure;max-age=31536000`
61
+ }
62
+
63
+ function removeRaw(name: string) {
64
+ document.cookie = `${name}=;max-age=-1;path=/`
65
+ }
66
+
67
+ function flatten(prefix: string, value: unknown, result: [string, string][] = []) {
68
+ if (Array.isArray(value)) {
69
+ for (let i = 0; i < value.length; i++) flatten(`${prefix}[${i}]`, value[i], result)
70
+ // Store length so we know how many indices to reconstruct.
71
+ result.push([`${prefix}.__length`, Json.stringify(value.length)])
72
+ } else if (value !== null && typeof value === 'object') {
73
+ for (const [k, v] of Object.entries(value as Record<string, unknown>))
74
+ flatten(`${prefix}.${k}`, v, result)
75
+ } else result.push([prefix, Json.stringify(value)])
76
+ return result
77
+ }
78
+
79
+ function unflatten(prefix: string): unknown {
80
+ // Check for a direct (leaf) cookie first.
81
+ const direct = getRaw(prefix)
82
+ if (direct) {
83
+ try {
84
+ return Json.parse(direct.substring(prefix.length + 1))
85
+ } catch {
86
+ return null
87
+ }
88
+ }
89
+ // Check if this is an array (has a __length cookie).
90
+ const lengthCookie = getRaw(`${prefix}.__length`)
91
+ if (lengthCookie) {
92
+ const length = Json.parse(lengthCookie.substring(`${prefix}.__length`.length + 1)) as number
93
+ const result: unknown[] = []
94
+ for (let i = 0; i < length; i++) result.push(unflatten(`${prefix}[${i}]`))
95
+ return result
96
+ }
97
+ // Collect all sub-keys (object children use `.`, array children use `[`).
98
+ const dotPrefix = `${prefix}.`
99
+ const bracketPrefix = `${prefix}[`
100
+ const children = document.cookie
101
+ .split('; ')
102
+ .filter((x) => x.startsWith(dotPrefix) || x.startsWith(bracketPrefix))
103
+ if (children.length === 0) return null
104
+ const result: Record<string, unknown> = {}
105
+ for (const entry of children) {
106
+ const key = entry.substring(dotPrefix.length, entry.indexOf('='))
107
+ const segment = key.split(/[.[]/)[0]!
108
+ if (segment === '__length') continue
109
+ if (!(segment in result)) result[segment] = unflatten(`${dotPrefix}${segment}`)
110
+ }
111
+ return result
112
+ }
113
+
114
+ return from(
115
+ {
116
+ getItem(name) {
117
+ return unflatten(name) as any
118
+ },
119
+ setItem(name, value) {
120
+ // Remove existing keys before writing.
121
+ this.removeItem(name)
122
+ for (const [k, v] of flatten(name, value)) setRaw(k, v)
123
+ },
124
+ removeItem(name) {
125
+ removeRaw(name)
126
+ for (const entry of document.cookie.split('; '))
127
+ if (entry.startsWith(`${name}.`) || entry.startsWith(`${name}[`))
128
+ removeRaw(entry.substring(0, entry.indexOf('=')))
129
+ },
130
+ },
131
+ options,
132
+ )
133
+ }
134
+
135
+ export declare namespace cookie {
136
+ type Options = from.Options
137
+ }
138
+
139
+ /** Creates an IndexedDB-backed storage adapter. Stores raw values (no JSON serialization). */
140
+ export function idb(options: idb.Options = {}): Storage {
141
+ const store = typeof indexedDB !== 'undefined' ? createStore('tempo', 'store') : undefined
142
+ return from(
143
+ {
144
+ async getItem(name) {
145
+ const value = await get(name, store)
146
+ if (value === null) return null
147
+ return value
148
+ },
149
+ async setItem(name, value) {
150
+ await set(name, value, store)
151
+ },
152
+ async removeItem(name) {
153
+ await del(name, store)
154
+ },
155
+ },
156
+ options,
157
+ )
158
+ }
159
+
160
+ export declare namespace idb {
161
+ type Options = from.Options
162
+ }
163
+
164
+ /** Creates a `localStorage`-backed storage adapter. */
165
+ export function localStorage(options: localStorage.Options = {}): Storage {
166
+ return from(
167
+ {
168
+ getItem(name) {
169
+ const value = globalThis.localStorage.getItem(name)
170
+ if (value === null) return null
171
+ try {
172
+ return Json.parse(value)
173
+ } catch {
174
+ return null
175
+ }
176
+ },
177
+ setItem(name, value) {
178
+ globalThis.localStorage.setItem(name, Json.stringify(value))
179
+ },
180
+ removeItem(name) {
181
+ globalThis.localStorage.removeItem(name)
182
+ },
183
+ },
184
+ options,
185
+ )
186
+ }
187
+
188
+ export declare namespace localStorage {
189
+ type Options = from.Options
190
+ }
191
+
192
+ /** Creates an in-memory storage adapter. Useful for SSR and tests. */
193
+ export function memory(options: memory.Options = {}): Storage {
194
+ const store = new Map<string, unknown>()
195
+ return from(
196
+ {
197
+ getItem(name) {
198
+ return (store.get(name) as any) ?? null
199
+ },
200
+ setItem(name, value) {
201
+ store.set(name, value)
202
+ },
203
+ removeItem(name) {
204
+ store.delete(name)
205
+ },
206
+ },
207
+ options,
208
+ )
209
+ }
210
+
211
+ export declare namespace memory {
212
+ type Options = from.Options
213
+ }
@@ -0,0 +1,287 @@
1
+ import { describe, expect, test } from 'vp/test'
2
+
3
+ import * as Storage from './Storage.js'
4
+ import * as Store from './Store.js'
5
+
6
+ describe('create', () => {
7
+ test('default', () => {
8
+ const store = Store.create({ chainId: 123 })
9
+ expect(store.getState()).toMatchInlineSnapshot(`
10
+ {
11
+ "accessKeys": [],
12
+ "accounts": [],
13
+ "activeAccount": 0,
14
+ "chainId": 123,
15
+ "requestQueue": [],
16
+ }
17
+ `)
18
+ })
19
+
20
+ test('behavior: setState updates state', () => {
21
+ const store = Store.create({ chainId: 123 })
22
+
23
+ store.setState({
24
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
25
+ })
26
+
27
+ expect(store.getState()).toMatchInlineSnapshot(`
28
+ {
29
+ "accessKeys": [],
30
+ "accounts": [
31
+ {
32
+ "address": "0x0000000000000000000000000000000000000001",
33
+ },
34
+ ],
35
+ "activeAccount": 0,
36
+ "chainId": 123,
37
+ "requestQueue": [],
38
+ }
39
+ `)
40
+ })
41
+
42
+ test('behavior: subscribe fires on state change', () => {
43
+ const store = Store.create({ chainId: 123 })
44
+ const events: number[] = []
45
+
46
+ store.subscribe((state) => events.push(state.chainId))
47
+ store.setState({ chainId: 456 })
48
+
49
+ expect(events).toMatchInlineSnapshot(`
50
+ [
51
+ 456,
52
+ ]
53
+ `)
54
+ })
55
+
56
+ test('behavior: subscribeWithSelector for granular subscriptions', () => {
57
+ const store = Store.create({ chainId: 123 })
58
+ const chainIds: number[] = []
59
+
60
+ store.subscribe(
61
+ (state) => state.chainId,
62
+ (chainId) => chainIds.push(chainId),
63
+ )
64
+
65
+ store.setState({ chainId: 456 })
66
+ expect(chainIds).toMatchInlineSnapshot(`
67
+ [
68
+ 456,
69
+ ]
70
+ `)
71
+
72
+ // Changing accounts does NOT trigger the chainId subscription
73
+ store.setState({
74
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
75
+ })
76
+ expect(chainIds).toMatchInlineSnapshot(`
77
+ [
78
+ 456,
79
+ ]
80
+ `)
81
+ })
82
+ })
83
+
84
+ describe('persistence', () => {
85
+ test('default: persists accounts, activeAccount, and chainId to storage', async () => {
86
+ const storage = Storage.memory()
87
+ const store = Store.create({ chainId: 123, storage })
88
+ await Store.waitForHydration(store)
89
+
90
+ store.setState({
91
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
92
+ activeAccount: 1,
93
+ chainId: 456,
94
+ })
95
+
96
+ const raw = storage.getItem('store') as any
97
+ expect(raw.state).toMatchInlineSnapshot(`
98
+ {
99
+ "accessKeys": [],
100
+ "accounts": [
101
+ {
102
+ "address": "0x0000000000000000000000000000000000000001",
103
+ },
104
+ ],
105
+ "activeAccount": 1,
106
+ "chainId": 456,
107
+ }
108
+ `)
109
+ })
110
+
111
+ test('behavior: hydrates from storage', async () => {
112
+ const storage = Storage.memory()
113
+
114
+ const store1 = Store.create({ chainId: 123, storage })
115
+ await Store.waitForHydration(store1)
116
+
117
+ store1.setState({
118
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
119
+ activeAccount: 0,
120
+ chainId: 456,
121
+ })
122
+
123
+ const store2 = Store.create({ chainId: 123, storage })
124
+ await Store.waitForHydration(store2)
125
+
126
+ expect(store2.getState()).toMatchInlineSnapshot(`
127
+ {
128
+ "accessKeys": [],
129
+ "accounts": [
130
+ {
131
+ "address": "0x0000000000000000000000000000000000000001",
132
+ },
133
+ ],
134
+ "activeAccount": 0,
135
+ "chainId": 456,
136
+ "requestQueue": [],
137
+ }
138
+ `)
139
+ })
140
+
141
+ test('behavior: persists accessKeys to storage', async () => {
142
+ const storage = Storage.memory()
143
+ const store = Store.create({ chainId: 123, storage })
144
+ await Store.waitForHydration(store)
145
+
146
+ store.setState({
147
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
148
+ accessKeys: [
149
+ {
150
+ address: '0x0000000000000000000000000000000000000099',
151
+ access: '0x0000000000000000000000000000000000000001',
152
+ keyType: 'webCrypto',
153
+ keyPair: {} as any,
154
+ },
155
+ ],
156
+ })
157
+
158
+ const raw = storage.getItem('store') as any
159
+ expect(raw.state.accessKeys).toMatchInlineSnapshot(`
160
+ [
161
+ {
162
+ "access": "0x0000000000000000000000000000000000000001",
163
+ "address": "0x0000000000000000000000000000000000000099",
164
+ "keyPair": {},
165
+ "keyType": "webCrypto",
166
+ },
167
+ ]
168
+ `)
169
+ })
170
+
171
+ test('behavior: hydrates accessKeys from storage', async () => {
172
+ const storage = Storage.memory()
173
+
174
+ const store1 = Store.create({ chainId: 123, storage })
175
+ await Store.waitForHydration(store1)
176
+
177
+ store1.setState({
178
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
179
+ accessKeys: [
180
+ {
181
+ address: '0x0000000000000000000000000000000000000099',
182
+ access: '0x0000000000000000000000000000000000000001',
183
+ expiry: 9999999999,
184
+ limits: [{ token: '0x0000000000000000000000000000000000000abc', limit: 500n }],
185
+ keyType: 'webCrypto',
186
+ keyPair: {} as any,
187
+ },
188
+ ],
189
+ })
190
+
191
+ const store2 = Store.create({ chainId: 123, storage })
192
+ await Store.waitForHydration(store2)
193
+
194
+ expect(store2.getState().accessKeys).toMatchInlineSnapshot(`
195
+ [
196
+ {
197
+ "access": "0x0000000000000000000000000000000000000001",
198
+ "address": "0x0000000000000000000000000000000000000099",
199
+ "expiry": 9999999999,
200
+ "keyPair": {},
201
+ "keyType": "webCrypto",
202
+ "limits": [
203
+ {
204
+ "limit": 500n,
205
+ "token": "0x0000000000000000000000000000000000000abc",
206
+ },
207
+ ],
208
+ },
209
+ ]
210
+ `)
211
+ })
212
+
213
+ test('behavior: disconnect clears accessKeys', async () => {
214
+ const storage = Storage.memory()
215
+ const store = Store.create({ chainId: 123, storage })
216
+ await Store.waitForHydration(store)
217
+
218
+ store.setState({
219
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
220
+ accessKeys: [
221
+ {
222
+ address: '0x0000000000000000000000000000000000000099',
223
+ access: '0x0000000000000000000000000000000000000001',
224
+ keyType: 'webCrypto',
225
+ keyPair: {} as any,
226
+ },
227
+ ],
228
+ })
229
+
230
+ store.setState({ accessKeys: [], accounts: [], activeAccount: 0 })
231
+
232
+ expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
233
+ expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
234
+ })
235
+
236
+ test('behavior: custom storage key', async () => {
237
+ const storage = Storage.memory({ key: 'custom' })
238
+ const store = Store.create({ chainId: 123, storage })
239
+ await Store.waitForHydration(store)
240
+
241
+ store.setState({ chainId: 789 })
242
+
243
+ const raw = storage.getItem('store') as any
244
+ expect(raw.state.chainId).toMatchInlineSnapshot(`789`)
245
+ })
246
+ })
247
+
248
+ describe('waitForHydration', () => {
249
+ test('default: resolves after hydration', async () => {
250
+ const storage = Storage.memory()
251
+
252
+ storage.setItem('store', {
253
+ state: {
254
+ accounts: [{ address: '0x0000000000000000000000000000000000000001' }],
255
+ activeAccount: 0,
256
+ chainId: 789,
257
+ },
258
+ version: 0,
259
+ })
260
+
261
+ const store = Store.create({ chainId: 123, storage })
262
+ await Store.waitForHydration(store)
263
+
264
+ expect(store.getState()).toMatchInlineSnapshot(`
265
+ {
266
+ "accessKeys": [],
267
+ "accounts": [
268
+ {
269
+ "address": "0x0000000000000000000000000000000000000001",
270
+ },
271
+ ],
272
+ "activeAccount": 0,
273
+ "chainId": 789,
274
+ "requestQueue": [],
275
+ }
276
+ `)
277
+ })
278
+
279
+ test('behavior: resolves multiple times', async () => {
280
+ const store = Store.create({ chainId: 123 })
281
+
282
+ await Store.waitForHydration(store)
283
+ await Store.waitForHydration(store)
284
+
285
+ expect(store.getState().chainId).toMatchInlineSnapshot(`123`)
286
+ })
287
+ })
@@ -0,0 +1,129 @@
1
+ import type { RpcRequest, RpcResponse } from 'ox'
2
+ import type { Mutate, StoreApi } from 'zustand'
3
+ import { persist } from 'zustand/middleware'
4
+ import { subscribeWithSelector } from 'zustand/middleware'
5
+ import { createStore } from 'zustand/vanilla'
6
+
7
+ import type { OneOf } from '../internal/types.js'
8
+ import type { AccessKey, Store as Account } from './Account.js'
9
+ import * as Storage from './Storage.js'
10
+
11
+ export type { AccessKey, Account }
12
+
13
+ /** Reactive state for the provider. */
14
+ export type State = {
15
+ /** Stored access keys. */
16
+ accessKeys: readonly AccessKey[]
17
+ /** Connected accounts. */
18
+ accounts: readonly Account[]
19
+ /** Index of the active account. */
20
+ activeAccount: number
21
+ /** Active chain ID. */
22
+ chainId: number
23
+ /** Queued RPC requests pending resolution by the dialog. */
24
+ requestQueue: readonly QueuedRequest[]
25
+ }
26
+
27
+ /** Zustand vanilla store with `subscribeWithSelector` and `persist` middleware. */
28
+ export type Store = Mutate<
29
+ StoreApi<State>,
30
+ [['zustand/subscribeWithSelector', never], ['zustand/persist', State]]
31
+ >
32
+
33
+ /** Options for {@link create}. */
34
+ export type Options = {
35
+ /** Initial chain ID. */
36
+ chainId: number
37
+ /** Whether to persist credentials and access keys to storage. When `false`, only account addresses are persisted. @default true */
38
+ persistCredentials?: boolean | undefined
39
+ /** Storage adapter for persistence. */
40
+ storage?: Storage.Storage | undefined
41
+ }
42
+
43
+ /** A queued JSON-RPC request tracked in the store. */
44
+ export type QueuedRequest<result = unknown> = OneOf<
45
+ | {
46
+ request: RpcRequest.RpcRequest
47
+ status: 'pending'
48
+ }
49
+ | {
50
+ request: RpcRequest.RpcRequest
51
+ result: result
52
+ status: 'success'
53
+ }
54
+ | {
55
+ request: RpcRequest.RpcRequest
56
+ error: RpcResponse.ErrorObject
57
+ status: 'error'
58
+ }
59
+ >
60
+
61
+ /**
62
+ * Creates a Zustand vanilla store with `subscribeWithSelector` and `persist` middleware.
63
+ */
64
+ export function create(options: Options): Store {
65
+ const {
66
+ chainId,
67
+ persistCredentials = true,
68
+ storage = typeof window !== 'undefined'
69
+ ? Storage.idb({ key: 'tempo' })
70
+ : Storage.memory({ key: 'tempo' }),
71
+ } = options
72
+
73
+ return createStore(
74
+ subscribeWithSelector(
75
+ persist<State>(
76
+ () => ({
77
+ accessKeys: [],
78
+ accounts: [],
79
+ activeAccount: 0,
80
+ chainId,
81
+ requestQueue: [],
82
+ }),
83
+ {
84
+ merge(persisted, current) {
85
+ const state = persisted as State
86
+ return {
87
+ ...state,
88
+ ...current,
89
+ // Preserve in-memory credentials when persisted accounts only have addresses.
90
+ accounts:
91
+ state.accounts?.map((persisted) => {
92
+ const account = current.accounts.find(
93
+ (a) => a.address.toLowerCase() === persisted.address.toLowerCase(),
94
+ )
95
+ return account ?? persisted
96
+ }) ?? current.accounts,
97
+ accessKeys: state.accessKeys ?? current.accessKeys,
98
+ chainId: state.chainId ?? current.chainId,
99
+ }
100
+ },
101
+ name: 'store',
102
+ partialize: (state) =>
103
+ ({
104
+ accounts: state.accounts,
105
+ activeAccount: state.activeAccount,
106
+ ...(persistCredentials ? { accessKeys: state.accessKeys } : {}),
107
+ chainId: state.chainId,
108
+ }) as unknown as State,
109
+ storage,
110
+ version: 0,
111
+ },
112
+ ),
113
+ ),
114
+ )
115
+ }
116
+
117
+ /**
118
+ * Waits for the store to finish hydrating from storage.
119
+ *
120
+ * Returns immediately if the store has already hydrated. Otherwise, waits
121
+ * for the `onFinishHydration` callback with a 100ms safety timeout fallback.
122
+ */
123
+ export async function waitForHydration(store: Store): Promise<void> {
124
+ if (store.persist.hasHydrated()) return
125
+ await new Promise<void>((resolve) => {
126
+ store.persist.onFinishHydration(() => resolve())
127
+ setTimeout(() => resolve(), 100)
128
+ })
129
+ }
@@ -0,0 +1,53 @@
1
+ import { Account, Secp256k1 } from 'viem/tempo'
2
+
3
+ import * as Adapter from '../Adapter.js'
4
+ import { local } from './local.js'
5
+
6
+ /**
7
+ * Creates a secp256k1 adapter that generates random private keys and persists them to storage.
8
+ *
9
+ * ⚠️ **Dangerous**: Private keys are stored in plaintext via the provider's storage adapter
10
+ * (e.g. localStorage, cookies). Use only for development, testing, or when the threat model allows it.
11
+ *
12
+ * Wraps the {@link local} adapter with automatic key generation.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { dangerous_secp256k1, Provider } from 'accounts'
17
+ *
18
+ * const provider = Provider.create({
19
+ * adapter: dangerous_secp256k1(),
20
+ * })
21
+ * ```
22
+ */
23
+ export function dangerous_secp256k1(options: dangerous_secp256k1.Options = {}): Adapter.Adapter {
24
+ const { icon, name, rdns } = options
25
+
26
+ return Adapter.define({ icon, name, rdns }, (config) => {
27
+ const { store } = config
28
+
29
+ return local({
30
+ async createAccount() {
31
+ const privateKey = Secp256k1.randomPrivateKey()
32
+ const account = Account.fromSecp256k1(privateKey)
33
+ return {
34
+ accounts: [{ address: account.address, keyType: 'secp256k1' as const, privateKey }],
35
+ }
36
+ },
37
+ async loadAccounts() {
38
+ return { accounts: [...store.getState().accounts] }
39
+ },
40
+ })(config)
41
+ })
42
+ }
43
+
44
+ export declare namespace dangerous_secp256k1 {
45
+ type Options = {
46
+ /** Data URI of the provider icon. @default Black 1×1 SVG. */
47
+ icon?: `data:image/${string}` | undefined
48
+ /** Display name of the provider (e.g. `"My Wallet"`). @default "Injected Wallet" */
49
+ name?: string | undefined
50
+ /** Reverse DNS identifier. @default `com.{lowercase name}` */
51
+ rdns?: string | undefined
52
+ }
53
+ }