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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/core/AccessKey.d.ts +55 -0
- package/dist/core/AccessKey.d.ts.map +1 -0
- package/dist/core/AccessKey.js +69 -0
- package/dist/core/AccessKey.js.map +1 -0
- package/dist/core/Account.d.ts +91 -0
- package/dist/core/Account.d.ts.map +1 -0
- package/dist/core/Account.js +64 -0
- package/dist/core/Account.js.map +1 -0
- package/dist/core/Adapter.d.ts +187 -0
- package/dist/core/Adapter.d.ts.map +1 -0
- package/dist/core/Adapter.js +7 -0
- package/dist/core/Adapter.js.map +1 -0
- package/dist/core/Ceremony.d.ts +109 -0
- package/dist/core/Ceremony.d.ts.map +1 -0
- package/dist/core/Ceremony.js +104 -0
- package/dist/core/Ceremony.js.map +1 -0
- package/dist/core/Client.d.ts +16 -0
- package/dist/core/Client.d.ts.map +1 -0
- package/dist/core/Client.js +18 -0
- package/dist/core/Client.js.map +1 -0
- package/dist/core/Dialog.d.ts +52 -0
- package/dist/core/Dialog.d.ts.map +1 -0
- package/dist/core/Dialog.js +342 -0
- package/dist/core/Dialog.js.map +1 -0
- package/dist/core/Expiry.d.ts +15 -0
- package/dist/core/Expiry.d.ts.map +1 -0
- package/dist/core/Expiry.js +29 -0
- package/dist/core/Expiry.js.map +1 -0
- package/dist/core/Messenger.d.ts +86 -0
- package/dist/core/Messenger.d.ts.map +1 -0
- package/dist/core/Messenger.js +127 -0
- package/dist/core/Messenger.js.map +1 -0
- package/dist/core/Provider.d.ts +69 -0
- package/dist/core/Provider.d.ts.map +1 -0
- package/dist/core/Provider.js +401 -0
- package/dist/core/Provider.js.map +1 -0
- package/dist/core/Remote.d.ts +114 -0
- package/dist/core/Remote.d.ts.map +1 -0
- package/dist/core/Remote.js +116 -0
- package/dist/core/Remote.js.map +1 -0
- package/dist/core/Schema.d.ts +805 -0
- package/dist/core/Schema.d.ts.map +1 -0
- package/dist/core/Schema.js +43 -0
- package/dist/core/Schema.js.map +1 -0
- package/dist/core/Storage.d.ts +42 -0
- package/dist/core/Storage.d.ts.map +1 -0
- package/dist/core/Storage.js +173 -0
- package/dist/core/Storage.js.map +1 -0
- package/dist/core/Store.d.ts +58 -0
- package/dist/core/Store.d.ts.map +1 -0
- package/dist/core/Store.js +58 -0
- package/dist/core/Store.js.map +1 -0
- package/dist/core/adapters/dangerous_secp256k1.d.ts +30 -0
- package/dist/core/adapters/dangerous_secp256k1.d.ts.map +1 -0
- package/dist/core/adapters/dangerous_secp256k1.js +39 -0
- package/dist/core/adapters/dangerous_secp256k1.js.map +1 -0
- package/dist/core/adapters/dialog.d.ts +31 -0
- package/dist/core/adapters/dialog.d.ts.map +1 -0
- package/dist/core/adapters/dialog.js +306 -0
- package/dist/core/adapters/dialog.js.map +1 -0
- package/dist/core/adapters/local.d.ts +33 -0
- package/dist/core/adapters/local.d.ts.map +1 -0
- package/dist/core/adapters/local.js +227 -0
- package/dist/core/adapters/local.js.map +1 -0
- package/dist/core/adapters/webAuthn.d.ts +36 -0
- package/dist/core/adapters/webAuthn.d.ts.map +1 -0
- package/dist/core/adapters/webAuthn.js +93 -0
- package/dist/core/adapters/webAuthn.js.map +1 -0
- package/dist/core/internal/withDedupe.d.ts +12 -0
- package/dist/core/internal/withDedupe.d.ts.map +1 -0
- package/dist/core/internal/withDedupe.js +12 -0
- package/dist/core/internal/withDedupe.js.map +1 -0
- package/dist/core/zod/request.d.ts +31 -0
- package/dist/core/zod/request.d.ts.map +1 -0
- package/dist/core/zod/request.js +41 -0
- package/dist/core/zod/request.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +603 -0
- package/dist/core/zod/rpc.d.ts.map +1 -0
- package/dist/core/zod/rpc.js +293 -0
- package/dist/core/zod/rpc.js.map +1 -0
- package/dist/core/zod/utils.d.ts +18 -0
- package/dist/core/zod/utils.d.ts.map +1 -0
- package/dist/core/zod/utils.js +21 -0
- package/dist/core/zod/utils.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/types.d.ts +284 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/server/Handler.d.ts +257 -0
- package/dist/server/Handler.d.ts.map +1 -0
- package/dist/server/Handler.js +433 -0
- package/dist/server/Handler.js.map +1 -0
- package/dist/server/Kv.d.ts +16 -0
- package/dist/server/Kv.d.ts.map +1 -0
- package/dist/server/Kv.js +30 -0
- package/dist/server/Kv.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/internal/requestListener.d.ts +124 -0
- package/dist/server/internal/requestListener.d.ts.map +1 -0
- package/dist/server/internal/requestListener.js +173 -0
- package/dist/server/internal/requestListener.js.map +1 -0
- package/dist/wagmi/Connector.d.ts +93 -0
- package/dist/wagmi/Connector.d.ts.map +1 -0
- package/dist/wagmi/Connector.js +238 -0
- package/dist/wagmi/Connector.js.map +1 -0
- package/dist/wagmi/index.d.ts +3 -0
- package/dist/wagmi/index.d.ts.map +1 -0
- package/dist/wagmi/index.js +3 -0
- package/dist/wagmi/index.js.map +1 -0
- package/package.json +56 -2
- package/src/core/AccessKey.test.ts +257 -0
- package/src/core/AccessKey.ts +123 -0
- package/src/core/Account.test.ts +309 -0
- package/src/core/Account.ts +152 -0
- package/src/core/Adapter.ts +238 -0
- package/src/core/Ceremony.browser.test.ts +239 -0
- package/src/core/Ceremony.test.ts +151 -0
- package/src/core/Ceremony.ts +203 -0
- package/src/core/Client.ts +36 -0
- package/src/core/Dialog.browser.test.ts +309 -0
- package/src/core/Dialog.test-d.ts +19 -0
- package/src/core/Dialog.ts +442 -0
- package/src/core/Expiry.ts +34 -0
- package/src/core/Messenger.ts +206 -0
- package/src/core/Provider.browser.test.ts +774 -0
- package/src/core/Provider.connect.browser.test.ts +415 -0
- package/src/core/Provider.test-d.ts +53 -0
- package/src/core/Provider.test.ts +1566 -0
- package/src/core/Provider.ts +559 -0
- package/src/core/Remote.ts +262 -0
- package/src/core/Schema.test-d.ts +211 -0
- package/src/core/Schema.ts +143 -0
- package/src/core/Storage.ts +213 -0
- package/src/core/Store.test.ts +287 -0
- package/src/core/Store.ts +129 -0
- package/src/core/adapters/dangerous_secp256k1.ts +53 -0
- package/src/core/adapters/dialog.ts +379 -0
- package/src/core/adapters/local.test.ts +97 -0
- package/src/core/adapters/local.ts +277 -0
- package/src/core/adapters/webAuthn.ts +129 -0
- package/src/core/internal/withDedupe.test.ts +116 -0
- package/src/core/internal/withDedupe.ts +20 -0
- package/src/core/mppx.test.ts +83 -0
- package/src/core/zod/request.test.ts +121 -0
- package/src/core/zod/request.ts +70 -0
- package/src/core/zod/rpc.ts +374 -0
- package/src/core/zod/utils.test.ts +69 -0
- package/src/core/zod/utils.ts +40 -0
- package/src/index.ts +14 -0
- package/src/internal/types.ts +378 -0
- package/src/server/Handler.test.ts +1014 -0
- package/src/server/Handler.ts +605 -0
- package/src/server/Kv.ts +46 -0
- package/src/server/index.ts +2 -0
- package/src/server/internal/requestListener.ts +273 -0
- package/src/tsconfig.json +9 -0
- package/src/wagmi/Connector.ts +287 -0
- 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
|
+
}
|