accounts 0.12.2 → 0.13.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 (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -5
  3. package/dist/cli/adapter.d.ts.map +1 -1
  4. package/dist/cli/adapter.js +53 -67
  5. package/dist/cli/adapter.js.map +1 -1
  6. package/dist/core/AccessKey.d.ts +89 -106
  7. package/dist/core/AccessKey.d.ts.map +1 -1
  8. package/dist/core/AccessKey.js +226 -210
  9. package/dist/core/AccessKey.js.map +1 -1
  10. package/dist/core/Account.d.ts +0 -9
  11. package/dist/core/Account.d.ts.map +1 -1
  12. package/dist/core/Account.js +1 -13
  13. package/dist/core/Account.js.map +1 -1
  14. package/dist/core/Adapter.d.ts +10 -2
  15. package/dist/core/Adapter.d.ts.map +1 -1
  16. package/dist/core/Provider.d.ts +4 -5
  17. package/dist/core/Provider.d.ts.map +1 -1
  18. package/dist/core/Provider.js +66 -76
  19. package/dist/core/Provider.js.map +1 -1
  20. package/dist/core/Schema.d.ts +7 -7
  21. package/dist/core/adapters/dialog.d.ts.map +1 -1
  22. package/dist/core/adapters/dialog.js +114 -124
  23. package/dist/core/adapters/dialog.js.map +1 -1
  24. package/dist/core/adapters/local.d.ts.map +1 -1
  25. package/dist/core/adapters/local.js +92 -105
  26. package/dist/core/adapters/local.js.map +1 -1
  27. package/dist/core/adapters/privy.d.ts +164 -0
  28. package/dist/core/adapters/privy.d.ts.map +1 -0
  29. package/dist/core/adapters/privy.js +574 -0
  30. package/dist/core/adapters/privy.js.map +1 -0
  31. package/dist/core/adapters/turnkey.d.ts.map +1 -1
  32. package/dist/core/adapters/turnkey.js +136 -133
  33. package/dist/core/adapters/turnkey.js.map +1 -1
  34. package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
  35. package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
  36. package/dist/core/internal/AccessKeyTransaction.js +155 -0
  37. package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
  38. package/dist/core/zod/rpc.d.ts +9 -4
  39. package/dist/core/zod/rpc.d.ts.map +1 -1
  40. package/dist/core/zod/rpc.js +9 -4
  41. package/dist/core/zod/rpc.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/react-native/adapter.d.ts.map +1 -1
  47. package/dist/react-native/adapter.js +61 -72
  48. package/dist/react-native/adapter.js.map +1 -1
  49. package/dist/server/Kv.d.ts +19 -9
  50. package/dist/server/Kv.d.ts.map +1 -1
  51. package/dist/server/Kv.js +32 -9
  52. package/dist/server/Kv.js.map +1 -1
  53. package/dist/server/internal/handlers/auth.d.ts.map +1 -1
  54. package/dist/server/internal/handlers/auth.js +2 -0
  55. package/dist/server/internal/handlers/auth.js.map +1 -1
  56. package/dist/server/internal/handlers/relay.d.ts +18 -12
  57. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  58. package/dist/server/internal/handlers/relay.js +115 -2
  59. package/dist/server/internal/handlers/relay.js.map +1 -1
  60. package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
  61. package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
  62. package/dist/server/internal/handlers/webAuthn.js +16 -9
  63. package/dist/server/internal/handlers/webAuthn.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/cli/adapter.ts +69 -74
  66. package/src/core/AccessKey.test.ts +525 -559
  67. package/src/core/AccessKey.ts +323 -327
  68. package/src/core/Account.test.ts +5 -50
  69. package/src/core/Account.ts +1 -19
  70. package/src/core/Adapter.ts +6 -2
  71. package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +97 -31
  72. package/src/core/Provider.ts +110 -110
  73. package/src/core/Schema.test-d.ts +18 -0
  74. package/src/core/adapters/dialog.test.ts +137 -13
  75. package/src/core/adapters/dialog.ts +109 -121
  76. package/src/core/adapters/local.ts +95 -119
  77. package/src/core/adapters/privy.test-d.ts +128 -0
  78. package/src/core/adapters/privy.test.ts +634 -0
  79. package/src/core/adapters/privy.ts +805 -0
  80. package/src/core/adapters/turnkey.test.ts +117 -13
  81. package/src/core/adapters/turnkey.ts +132 -149
  82. package/src/core/internal/AccessKeyTransaction.ts +243 -0
  83. package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
  84. package/src/core/zod/request.test.ts +36 -0
  85. package/src/core/zod/rpc.ts +17 -10
  86. package/src/index.ts +1 -1
  87. package/src/react-native/adapter.ts +77 -84
  88. package/src/server/Kv.test.ts +51 -0
  89. package/src/server/Kv.ts +43 -9
  90. package/src/server/internal/handlers/auth.test.ts +10 -5
  91. package/src/server/internal/handlers/auth.ts +3 -0
  92. package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +3 -0
  93. package/src/server/internal/handlers/relay.ts +138 -13
  94. package/src/server/internal/handlers/session.test.ts +1 -1
  95. package/src/server/internal/handlers/webAuthn.test.ts +20 -0
  96. package/src/server/internal/handlers/webAuthn.ts +24 -12
  97. /package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
  98. /package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
  99. /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
@@ -1,24 +1,13 @@
1
- import { AbiFunction, Address, Hex, Provider, PublicKey, WebCryptoP256 } from 'ox'
1
+ import { AbiFunction, Address, Hex, PublicKey, WebCryptoP256 } from 'ox'
2
2
  import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
- import type { Client, Transport } from 'viem'
3
+ import { BaseError, type Client, type Transport } from 'viem'
4
4
  import { Account as TempoAccount, Actions } from 'viem/tempo'
5
5
 
6
6
  import type { OneOf } from '../internal/types.js'
7
7
  import * as ExecutionError from './ExecutionError.js'
8
8
  import type * as Store from './Store.js'
9
9
 
10
- const removalErrorNames = new Set([
11
- 'InvalidSignature',
12
- 'InvalidSignatureFormat',
13
- 'InvalidSignatureType',
14
- 'KeyAlreadyRevoked',
15
- 'KeyExpired',
16
- 'KeyNotFound',
17
- 'SignatureTypeMismatch',
18
- ])
19
-
20
- /** Access-key publication states. */
21
- export const status = {
10
+ const status = {
22
11
  /** No matching usable access key was found. */
23
12
  missing: 'missing',
24
13
  /** A matching key exists locally and still needs its first transaction to publish the authorization. */
@@ -29,8 +18,9 @@ export const status = {
29
18
  expired: 'expired',
30
19
  } as const
31
20
 
32
- /** Publication state for an access key. */
33
- export type Status = (typeof status)[keyof typeof status]
21
+ const unavailableErrorNames = new Set(['KeyAlreadyRevoked', 'KeyNotFound'])
22
+
23
+ type Status = (typeof status)[keyof typeof status]
34
24
 
35
25
  /** Access key entry stored alongside accounts. */
36
26
  export type AccessKey = {
@@ -42,8 +32,10 @@ export type AccessKey = {
42
32
  chainId: number
43
33
  /** Unix timestamp when the access key expires. */
44
34
  expiry?: number | undefined
45
- /** Signed key authorization to attach to the first transaction. Consumed on use. */
35
+ /** Signed key authorization to attach until the key is observed on-chain. */
46
36
  keyAuthorization?: KeyAuthorization.Signed | undefined
37
+ /** Whether the key authorization is pending confirmation on-chain. */
38
+ keyAuthorizationPending?: boolean | undefined
47
39
  /** Key type. */
48
40
  keyType: 'secp256k1' | 'p256' | 'webAuthn' | 'webCrypto'
49
41
  /** TIP-20 spending limits for the access key. */
@@ -68,17 +60,81 @@ export type AccessKey = {
68
60
  }
69
61
  >
70
62
 
71
- /** Returns the pending key authorization for an access key account without removing it. */
72
- export function getPending(
73
- account: TempoAccount.Account,
74
- options: { store: Store.Store },
75
- ): KeyAuthorization.Signed | undefined {
76
- if (account.source !== 'accessKey') return undefined
77
- const { store } = options
78
- const accessKeyAddress = (account as TempoAccount.AccessKeyAccount).accessKeyAddress
79
- const { accessKeys } = store.getState()
80
- const entry = accessKeys.find((a) => a.address?.toLowerCase() === accessKeyAddress.toLowerCase())
81
- return entry?.keyAuthorization
63
+ /** Calls used to match access key scopes. */
64
+ type Call = {
65
+ /** Contract address being called. */
66
+ to?: Address.Address | undefined
67
+ /** Calldata being sent. */
68
+ data?: Hex.Hex | undefined
69
+ }
70
+
71
+ /** Access key status query. */
72
+ type StatusQuery = {
73
+ /** Root account address. */
74
+ account: Address.Address
75
+ /** Specific access key address to match. */
76
+ accessKey?: Address.Address | undefined
77
+ /** Calls to match against access key scopes. */
78
+ calls?: readonly Call[] | undefined
79
+ /** Chain ID the access key must be authorized on. */
80
+ chainId: number
81
+ /** Client used to verify publication state on-chain. */
82
+ client: Client<Transport>
83
+ /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
84
+ now?: number | undefined
85
+ /** Reactive state store. */
86
+ store: Store.Store
87
+ }
88
+
89
+ /** Access key selection query. */
90
+ type SelectQuery = {
91
+ /** Root account address. */
92
+ account: Address.Address
93
+ /** Calls to match against access key scopes. */
94
+ calls?: readonly Call[] | undefined
95
+ /** Chain ID the access key must be authorized on. */
96
+ chainId: number
97
+ /** Client used to verify publication state on-chain. */
98
+ client: Client<Transport>
99
+ /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
100
+ now?: number | undefined
101
+ /** Reactive state store. */
102
+ store: Store.Store
103
+ }
104
+
105
+ /** Access key record identity. */
106
+ type Key = {
107
+ /** Root account address. */
108
+ account: Address.Address
109
+ /** Access key address. */
110
+ accessKey: Address.Address
111
+ /** Chain ID the access key is scoped to. */
112
+ chainId: number
113
+ /** Reactive state store. */
114
+ store: Store.Store
115
+ }
116
+
117
+ type ListQuery = {
118
+ /** Root account address. */
119
+ account: Address.Address
120
+ /** Specific access key address to match. */
121
+ accessKey?: Address.Address | undefined
122
+ /** Chain ID the access key is scoped to. */
123
+ chainId: number
124
+ /** Reactive state store. */
125
+ store: Store.Store
126
+ }
127
+
128
+ /** Selected access key for an intent. */
129
+ type Selection = {
130
+ /** Hydrated locally-signable access key account. */
131
+ account: TempoAccount.AccessKeyAccount
132
+ /** Access key address. */
133
+ accessKey: Address.Address
134
+ /** Pending key authorization to attach, if the key is not yet known published. */
135
+ authorization?: KeyAuthorization.Signed | undefined
136
+ /** Stored access key record. */
137
+ record: AccessKey
82
138
  }
83
139
 
84
140
  /** Generates a P256 key pair and access key account. */
@@ -164,18 +220,22 @@ export declare namespace prepareAuthorization {
164
220
  }
165
221
  }
166
222
 
167
- /** Saves a prepared access key authorization with an existing signature. */
168
- export function saveAuthorization(
169
- options: saveAuthorization.Options,
170
- ): saveAuthorization.ReturnType {
171
- const { address, prepared, signature, store } = options
223
+ /** Prepares, signs, and saves an access key authorization. */
224
+ export async function authorize(options: authorize.Options): Promise<authorize.ReturnType> {
225
+ const { account, chainId, parameters, store } = options
226
+ const prepared = await prepareAuthorization({
227
+ ...parameters,
228
+ chainId: parameters.chainId ?? chainId,
229
+ })
230
+ const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
231
+ const signature = await account.sign({ hash: digest })
172
232
  const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
173
233
  signature: SignatureEnvelope.from(signature),
174
234
  })
175
235
 
176
- save({
177
- address,
178
- keyAuthorization,
236
+ add({
237
+ account: account.address,
238
+ authorization: keyAuthorization,
179
239
  ...(prepared.keyPair ? { keyPair: prepared.keyPair } : {}),
180
240
  store,
181
241
  })
@@ -183,33 +243,6 @@ export function saveAuthorization(
183
243
  return KeyAuthorization.toRpc(keyAuthorization)
184
244
  }
185
245
 
186
- export declare namespace saveAuthorization {
187
- /** Options for {@link saveAuthorization}. */
188
- type Options = {
189
- /** Root account address that owns this access key. */
190
- address: Address.Address
191
- /** Prepared unsigned key authorization returned by {@link prepareAuthorization}. */
192
- prepared: prepareAuthorization.ReturnType
193
- /** Signature over the key authorization digest. */
194
- signature: Hex.Hex
195
- /** Reactive state store. */
196
- store: Store.Store
197
- }
198
-
199
- /** Signed key authorization in RPC form. */
200
- type ReturnType = KeyAuthorization.Rpc
201
- }
202
-
203
- /** Prepares, signs, and saves an access key authorization. */
204
- export async function authorize(options: authorize.Options): Promise<authorize.ReturnType> {
205
- const { account, chainId, parameters, store } = options
206
- const prepared = await prepareAuthorization({
207
- ...parameters,
208
- chainId: parameters.chainId ?? chainId,
209
- })
210
- return await signAuthorization({ account, prepared, store })
211
- }
212
-
213
246
  export declare namespace authorize {
214
247
  /** Options for {@link authorize}. */
215
248
  type Options = {
@@ -230,192 +263,184 @@ export declare namespace authorize {
230
263
  type ReturnType = KeyAuthorization.Rpc
231
264
  }
232
265
 
233
- async function signAuthorization(
234
- options: signAuthorization.Options,
235
- ): Promise<signAuthorization.ReturnType> {
236
- const { account, prepared, store } = options
237
- const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
238
- const signature = await account.sign({ hash: digest })
239
- return saveAuthorization({ address: account.address, prepared, signature, store })
240
- }
241
-
242
- declare namespace signAuthorization {
243
- type Options = {
244
- account: TempoAccount.Account
245
- prepared: prepareAuthorization.ReturnType
246
- store: Store.Store
247
- }
248
-
249
- type ReturnType = KeyAuthorization.Rpc
250
- }
266
+ /** Returns publication status for a stored or on-chain access key. */
267
+ export async function getStatus(options: StatusQuery): Promise<Status> {
268
+ const { accessKey, account, calls, chainId, client, store } = options
269
+ const now = options.now ?? Date.now() / 1000
270
+ const local = list({ account, accessKey, chainId, store }).find((key) =>
271
+ scopesMatch(key, { calls }),
272
+ )
251
273
 
252
- /** Hydrates an access key entry to a viem Account. Only works for locally-generated keys. */
253
- export function hydrate(accessKey: AccessKey): TempoAccount.Account {
254
- if ('keyPair' in accessKey && accessKey.keyPair)
255
- return TempoAccount.fromWebCryptoP256(accessKey.keyPair, { access: accessKey.access })
256
- if ('privateKey' in accessKey && accessKey.privateKey) {
257
- switch (accessKey.keyType) {
258
- case 'secp256k1':
259
- return TempoAccount.fromSecp256k1(accessKey.privateKey, { access: accessKey.access })
260
- case 'p256':
261
- return TempoAccount.fromP256(accessKey.privateKey, { access: accessKey.access })
274
+ if (local) {
275
+ if (isExpired(local.expiry, now)) return status.expired
276
+ if (local.keyAuthorization) {
277
+ if (local.keyAuthorizationPending) {
278
+ const publicationStatus = await getPublishedStatus(client, {
279
+ accessKey: local.address,
280
+ account,
281
+ now,
282
+ }).catch(() => status.pending)
283
+ if (publicationStatus === status.published) {
284
+ markPublished({
285
+ accessKey: local.address,
286
+ account,
287
+ chainId,
288
+ store,
289
+ })
290
+ return status.published
291
+ }
292
+ }
293
+ return status.pending
262
294
  }
295
+ return await getPublishedStatus(client, { accessKey: local.address, account, now })
263
296
  }
264
- throw new Provider.UnauthorizedError({
265
- message: 'External access key cannot be hydrated for signing.',
266
- })
267
- }
268
297
 
269
- /** Removes an access key entry for the given account from the store. */
270
- export function remove(account: TempoAccount.Account, options: { store: Store.Store }): void {
271
- if (account.source !== 'accessKey') return
272
- const { store } = options
273
- const accessKeyAddress = account.accessKeyAddress
274
- store.setState((state) => ({
275
- accessKeys: state.accessKeys.filter(
276
- (a) => a.address?.toLowerCase() !== accessKeyAddress?.toLowerCase(),
277
- ),
278
- }))
279
- }
280
-
281
- /** Invalidates a stored access key when the error proves it is no longer usable. */
282
- export function invalidate(
283
- account: TempoAccount.Account,
284
- error: unknown,
285
- options: invalidate.Options,
286
- ): boolean {
287
- if (account.source !== 'accessKey') return false
288
- if (!shouldRemoveForError(error)) return false
289
- remove(account, options)
290
- return true
291
- }
292
-
293
- export declare namespace invalidate {
294
- /** Options for {@link invalidate}. */
295
- type Options = {
296
- /** Reactive state store. */
297
- store: Store.Store
298
- }
298
+ if (accessKey) return await getPublishedStatus(client, { accessKey, account, now })
299
+ return status.missing
299
300
  }
300
301
 
301
- /** Permanently removes the pending key authorization for an access key account. */
302
- export function removePending(
303
- account: TempoAccount.Account,
304
- options: { store: Store.Store },
305
- ): void {
306
- if (account.source !== 'accessKey') return
307
- const { store } = options
308
- const accessKeyAddress = (account as TempoAccount.AccessKeyAccount).accessKeyAddress
309
- store.setState((state) => ({
310
- accessKeys: state.accessKeys.map((a) =>
311
- a.address.toLowerCase() === accessKeyAddress.toLowerCase()
312
- ? { ...a, keyAuthorization: undefined }
313
- : a,
314
- ),
315
- }))
316
- }
302
+ /** Selects a locally-signable access key for an intent. */
303
+ export async function select(options: SelectQuery): Promise<Selection | undefined> {
304
+ const { account, calls, chainId, client, store } = options
305
+ const now = options.now ?? Date.now() / 1000
306
+ const records = list({ account, chainId, store })
317
307
 
318
- /** Selects and hydrates a locally-signable access key account for a root account. */
319
- export function selectAccount(
320
- options: selectAccount.Options,
321
- ): TempoAccount.AccessKeyAccount | undefined {
322
- const { address, calls, chainId, store } = options
323
- const { accessKeys } = store.getState()
324
- let accessKeys_next = accessKeys
325
- for (const key of accessKeys) {
326
- if (key.access.toLowerCase() !== address.toLowerCase()) continue
327
- if (key.chainId !== chainId) continue
328
- if (!('keyPair' in key && !!key.keyPair) && !('privateKey' in key && !!key.privateKey)) continue
329
-
330
- if (key.expiry && key.expiry < Date.now() / 1000) {
331
- accessKeys_next = accessKeys_next.filter((a) => a !== key)
332
- store.setState({ accessKeys: accessKeys_next })
308
+ for (const record of records) {
309
+ if (!scopesMatch(record, { calls })) continue
310
+ if (isExpired(record.expiry, now)) {
311
+ remove({ accessKey: record.address, account: record.access, chainId: record.chainId, store })
333
312
  continue
334
313
  }
335
314
 
336
- if (scopesMatch(key, { calls })) return hydrate(key) as TempoAccount.AccessKeyAccount
337
- }
338
- return undefined
339
- }
315
+ const account_accessKey = hydrate(record)
316
+ if (!account_accessKey) continue
317
+
318
+ let authorization = record.keyAuthorization
319
+ if (authorization && record.keyAuthorizationPending) {
320
+ const publicationStatus = await getPublishedStatus(client, {
321
+ accessKey: record.address,
322
+ account: record.access,
323
+ now: Date.now() / 1000,
324
+ }).catch(() => status.pending)
325
+ if (publicationStatus === status.published) {
326
+ markPublished({
327
+ accessKey: record.address,
328
+ account: record.access,
329
+ chainId: record.chainId,
330
+ store,
331
+ })
332
+ authorization = undefined
333
+ }
334
+ }
340
335
 
341
- export declare namespace selectAccount {
342
- /** Options for {@link selectAccount}. */
343
- type Options = {
344
- /** Root account address. */
345
- address: Address.Address
346
- /** Calls to match against access key scopes. */
347
- calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
348
- /** Chain ID the access key must be authorized on. */
349
- chainId: number
350
- /** Reactive state store. */
351
- store: Store.Store
336
+ return {
337
+ account: account_accessKey,
338
+ accessKey: record.address,
339
+ ...(authorization ? { authorization } : {}),
340
+ record,
341
+ }
352
342
  }
353
343
  }
354
344
 
355
- /** Returns publication status for a stored or on-chain access key. */
356
- export async function getStatus(options: getStatus.Options): Promise<getStatus.ReturnType> {
357
- const { accessKey, address, calls, chainId, client, store } = options
358
- const now = options.now ?? Date.now() / 1000
359
- const local = store
360
- .getState()
361
- .accessKeys.find((key) => matches(key, { accessKey, address, calls, chainId }))
362
-
363
- if (local) {
364
- if (isExpired(local.expiry, now)) return status.expired
365
- if (local.keyAuthorization) return status.pending
366
- if (client) return await getPublishedStatus(client, { accessKey: local.address, address, now })
367
- return status.published
345
+ /** Adds a signed access key authorization. */
346
+ export function add(options: add.Options): add.ReturnType {
347
+ const { account, authorization, keyPair, privateKey, store } = options
348
+ const base = {
349
+ address: authorization.address,
350
+ access: account,
351
+ chainId: Number(authorization.chainId),
352
+ expiry: authorization.expiry ?? undefined,
353
+ keyAuthorization: authorization,
354
+ keyType: authorization.type,
355
+ limits: authorization.limits as AccessKey['limits'],
356
+ scopes: authorization.scopes as AccessKey['scopes'],
368
357
  }
369
-
370
- if (accessKey && client) return await getPublishedStatus(client, { accessKey, address, now })
371
- return status.missing
358
+ const record = (
359
+ privateKey ? { ...base, privateKey } : keyPair ? { ...base, keyPair } : base
360
+ ) as AccessKey
361
+ store.setState((state) => ({
362
+ accessKeys: [
363
+ record,
364
+ ...state.accessKeys.filter(
365
+ (entry) =>
366
+ !matches(entry, {
367
+ account: record.access,
368
+ accessKey: record.address,
369
+ chainId: record.chainId,
370
+ }),
371
+ ),
372
+ ],
373
+ }))
374
+ return record
372
375
  }
373
376
 
374
- export declare namespace getStatus {
375
- /** Options for {@link getStatus}. */
377
+ export declare namespace add {
378
+ /** Options for {@link add}. */
376
379
  type Options = {
377
- /** Root account address that owns the access key. */
378
- address: Address.Address
379
- /** Specific access key address to query. When omitted, the first locally matching key is used. */
380
- accessKey?: Address.Address | undefined
381
- /** Calls to match against access key scopes. */
382
- calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
383
- /** Chain ID the access key must be authorized on. */
384
- chainId: number
385
- /** Client used to verify published state on-chain. */
386
- client?: Client<Transport> | undefined
387
- /** Current Unix timestamp in seconds. Defaults to `Date.now() / 1000`. */
388
- now?: number | undefined
380
+ /** Root account address that owns this access key. */
381
+ account: Address.Address
382
+ /** Signed key authorization for the access key. */
383
+ authorization: KeyAuthorization.Signed
384
+ /** The exported private key backing the access key. */
385
+ privateKey?: Hex.Hex | undefined
386
+ /** The WebCrypto key pair backing the access key. */
387
+ keyPair?: Awaited<globalThis.ReturnType<typeof WebCryptoP256.createKeyPair>> | undefined
389
388
  /** Reactive state store. */
390
389
  store: Store.Store
391
390
  }
392
391
 
393
- /** Access-key publication status. */
394
- type ReturnType = Status
392
+ /** Stored access key record. */
393
+ type ReturnType = AccessKey
395
394
  }
396
395
 
397
- /** Removes an access key from the store. */
398
- export function revoke(options: revoke.Options): void {
399
- const { address, store } = options
400
- const { accessKeys } = store.getState()
401
- store.setState({
402
- accessKeys: accessKeys.filter((a) => a.access.toLowerCase() !== address.toLowerCase()),
396
+ /** Marks a key authorization as pending confirmation on-chain. */
397
+ export function markPending(options: Key): void {
398
+ const { store, ...key } = options
399
+ const record = list({ ...key, store })[0]
400
+ if (!record?.keyAuthorization) return
401
+ patch({
402
+ ...key,
403
+ patch: { keyAuthorizationPending: true },
404
+ store,
403
405
  })
404
406
  }
405
407
 
406
- export declare namespace revoke {
407
- type Options = {
408
- /** Root account address. */
409
- address: Address.Address
410
- /** Reactive state store. */
411
- store: Store.Store
408
+ /** Marks an access key as published on-chain and clears its pending authorization. */
409
+ export function markPublished(options: Key): void {
410
+ const { store, ...key } = options
411
+ patch({
412
+ ...key,
413
+ patch: { keyAuthorization: undefined, keyAuthorizationPending: undefined },
414
+ store,
415
+ })
416
+ }
417
+
418
+ /** Removes an access key record. */
419
+ export function remove(options: Key): void {
420
+ const { store, ...key } = options
421
+ store.setState((state) => ({
422
+ accessKeys: state.accessKeys.filter((record) => !matches(record, key)),
423
+ }))
424
+ }
425
+
426
+ /** Returns whether an error means an access key is already unavailable on-chain. */
427
+ export function isUnavailableError(error: unknown): boolean {
428
+ if (error instanceof BaseError) {
429
+ const found = error.walk((e) => {
430
+ const errorName = (e as { data?: { errorName?: string } }).data?.errorName
431
+ return !!errorName && unavailableErrorNames.has(errorName)
432
+ })
433
+ if (found) return true
412
434
  }
435
+
436
+ if (!(error instanceof Error)) return false
437
+ return unavailableErrorNames.has(ExecutionError.parse(error).errorName)
413
438
  }
414
439
 
415
440
  function scopesMatch(
416
441
  key: AccessKey,
417
442
  options: {
418
- calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
443
+ calls?: readonly Call[] | undefined
419
444
  },
420
445
  ): boolean {
421
446
  const scopes = key.scopes
@@ -429,56 +454,29 @@ function scopesMatch(
429
454
  return scopes.some((scope) => {
430
455
  if (!isScope(scope)) return false
431
456
  if (scope.address.toLowerCase() !== callTo) return false
432
- if (!scope.selector) return scope.recipients ? scope.recipients.length === 0 : true
433
- const scopeSelector = getSelector(scope.selector)
457
+ const selector = scope.selector
458
+ if (!selector) return scope.recipients ? scope.recipients.length === 0 : true
459
+ const scopeSelector = (() => {
460
+ try {
461
+ return (
462
+ selector.startsWith('0x') && selector.length === 10
463
+ ? selector
464
+ : AbiFunction.getSelector(selector)
465
+ ).toLowerCase()
466
+ } catch {
467
+ return undefined
468
+ }
469
+ })()
434
470
  if (!scopeSelector || callSelector !== scopeSelector) return false
435
- return recipientsMatch(scope.recipients, call.data)
471
+ if (!scope.recipients || scope.recipients.length === 0) return true
472
+ if (!call.data || call.data.length < 74) return false
473
+ const recipient = `0x${call.data.slice(34, 74)}` as Address.Address
474
+ if (!Address.validate(recipient)) return false
475
+ return scope.recipients.some((address) => address.toLowerCase() === recipient.toLowerCase())
436
476
  })
437
477
  })
438
478
  }
439
479
 
440
- function matches(
441
- key: AccessKey,
442
- options: {
443
- accessKey?: Address.Address | undefined
444
- address: Address.Address
445
- calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
446
- chainId: number
447
- },
448
- ): boolean {
449
- const { accessKey, address, calls, chainId } = options
450
- if (key.access.toLowerCase() !== address.toLowerCase()) return false
451
- if (key.chainId !== chainId) return false
452
- if (accessKey && key.address.toLowerCase() !== accessKey.toLowerCase()) return false
453
- return scopesMatch(key, { calls })
454
- }
455
-
456
- function isExpired(expiry: number | undefined, now: number): boolean {
457
- return typeof expiry === 'number' && expiry < now
458
- }
459
-
460
- async function getPublishedStatus(
461
- client: Client<Transport>,
462
- options: { accessKey: Address.Address; address: Address.Address; now: number },
463
- ): Promise<Status> {
464
- const { accessKey, address, now } = options
465
- try {
466
- const metadata = await Actions.accessKey.getMetadata(client, {
467
- account: address,
468
- accessKey,
469
- })
470
- if (metadata.isRevoked) return status.missing
471
- if (metadata.expiry > 0n && metadata.expiry < BigInt(Math.floor(now))) return status.expired
472
- return status.published
473
- } catch (error) {
474
- if (!(error instanceof Error)) throw error
475
- const parsed = ExecutionError.parse(error)
476
- if (parsed.errorName === 'KeyNotFound' || parsed.errorName === 'KeyAlreadyRevoked')
477
- return status.missing
478
- throw error
479
- }
480
- }
481
-
482
480
  function isScope(scope: unknown): scope is NonNullable<AccessKey['scopes']>[number] {
483
481
  if (!scope || typeof scope !== 'object') return false
484
482
  const value = scope as {
@@ -496,83 +494,81 @@ function isScope(scope: unknown): scope is NonNullable<AccessKey['scopes']>[numb
496
494
  return true
497
495
  }
498
496
 
499
- function getSelector(selector: string): string | undefined {
500
- try {
501
- return (
502
- selector.startsWith('0x') && selector.length === 10
503
- ? selector
504
- : AbiFunction.getSelector(selector)
505
- ).toLowerCase()
506
- } catch {
507
- return undefined
497
+ function hydrate(accessKey: AccessKey): TempoAccount.AccessKeyAccount | undefined {
498
+ if ('keyPair' in accessKey && accessKey.keyPair)
499
+ return TempoAccount.fromWebCryptoP256(accessKey.keyPair, {
500
+ access: accessKey.access,
501
+ }) as TempoAccount.AccessKeyAccount
502
+ if ('privateKey' in accessKey && accessKey.privateKey) {
503
+ switch (accessKey.keyType) {
504
+ case 'secp256k1':
505
+ return TempoAccount.fromSecp256k1(accessKey.privateKey, {
506
+ access: accessKey.access,
507
+ }) as TempoAccount.AccessKeyAccount
508
+ case 'p256':
509
+ return TempoAccount.fromP256(accessKey.privateKey, {
510
+ access: accessKey.access,
511
+ }) as TempoAccount.AccessKeyAccount
512
+ }
508
513
  }
514
+ return undefined
509
515
  }
510
516
 
511
- function recipientsMatch(
512
- recipients: readonly Address.Address[] | undefined,
513
- data: Hex.Hex | undefined,
514
- ): boolean {
515
- if (!recipients || recipients.length === 0) return true
516
- const recipient = getCallRecipient(data)
517
- if (!recipient) return false
518
- return recipients.some((address) => address.toLowerCase() === recipient.toLowerCase())
517
+ function isExpired(expiry: number | undefined, now: number): boolean {
518
+ return typeof expiry === 'number' && expiry < now
519
519
  }
520
520
 
521
- function getCallRecipient(data: Hex.Hex | undefined): Address.Address | undefined {
522
- if (!data || data.length < 74) return undefined
523
- const recipient = `0x${data.slice(34, 74)}` as Address.Address
524
- if (!Address.validate(recipient)) return undefined
525
- return recipient
521
+ async function getPublishedStatus(
522
+ client: Client<Transport>,
523
+ options: { accessKey: Address.Address; account: Address.Address; now: number },
524
+ ): Promise<Status> {
525
+ const { accessKey, account, now } = options
526
+ try {
527
+ const metadata = await Actions.accessKey.getMetadata(client, {
528
+ account,
529
+ accessKey,
530
+ })
531
+ if (metadata.address.toLowerCase() !== accessKey.toLowerCase()) return status.missing
532
+ if (metadata.isRevoked) return status.missing
533
+ if (metadata.expiry > 0n && metadata.expiry < BigInt(Math.floor(now))) return status.expired
534
+ return status.published
535
+ } catch (error) {
536
+ if (isUnavailableError(error)) return status.missing
537
+ throw error
538
+ }
526
539
  }
527
540
 
528
- function shouldRemoveForError(error: unknown): boolean {
529
- if (!(error instanceof Error)) return false
530
- const parsed = ExecutionError.parse(error)
531
- return removalErrorNames.has(parsed.errorName)
541
+ function list(options: ListQuery): readonly AccessKey[] {
542
+ const { store, ...query } = options
543
+ return store.getState().accessKeys.filter((key) => matches(key, query))
532
544
  }
533
545
 
534
- /** Saves an access key to the store with its one-time key authorization. */
535
- export function save(options: save.Options): void {
536
- const { address, keyAuthorization, keyPair, privateKey, store } = options
537
-
538
- const base = {
539
- address: keyAuthorization.address,
540
- access: address,
541
- chainId: Number(keyAuthorization.chainId),
542
- expiry: keyAuthorization.expiry ?? undefined,
543
- keyAuthorization,
544
- keyType: keyAuthorization.type,
545
- limits: keyAuthorization.limits as AccessKey['limits'],
546
- scopes: keyAuthorization.scopes as AccessKey['scopes'],
547
- }
548
-
549
- const accessKey: AccessKey = privateKey
550
- ? { ...base, privateKey }
551
- : keyPair
552
- ? { ...base, keyPair }
553
- : { ...base }
554
-
546
+ function patch(options: Key & { patch: Partial<AccessKey> }): void {
547
+ const { patch, store, ...key } = options
555
548
  store.setState((state) => ({
556
- accessKeys: [
557
- accessKey,
558
- ...state.accessKeys.filter(
559
- (entry) => entry.address.toLowerCase() !== keyAuthorization.address.toLowerCase(),
560
- ),
561
- ],
549
+ accessKeys: state.accessKeys.map((record) => {
550
+ if (!matches(record, key)) return record
551
+ const next = { ...record } as Record<string, unknown>
552
+ for (const [name, value] of Object.entries(patch)) {
553
+ if (typeof value === 'undefined') delete next[name]
554
+ else next[name] = value
555
+ }
556
+ return next as AccessKey
557
+ }),
562
558
  }))
563
559
  }
564
560
 
565
- export declare namespace save {
566
- type Options = {
567
- /** Root account address that owns this access key. */
568
- address: Address.Address
569
- /** Signed key authorization to attach to the first transaction. */
570
- keyAuthorization: KeyAuthorization.Signed
571
- /** The exported private key backing the access key. */
572
- privateKey?: Hex.Hex | undefined
573
- /** The WebCrypto key pair backing the access key. Only present for locally-generated keys. */
574
- keyPair?: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>> | undefined
575
- /** Reactive state store. */
576
- store: Store.Store
577
- }
561
+ function matches(
562
+ record: AccessKey,
563
+ options: {
564
+ account: Address.Address
565
+ accessKey?: Address.Address | undefined
566
+ chainId: number
567
+ },
568
+ ): boolean {
569
+ const { accessKey, account, chainId } = options
570
+ if (record.access.toLowerCase() !== account.toLowerCase()) return false
571
+ if (record.chainId !== chainId) return false
572
+ if (accessKey && record.address.toLowerCase() !== accessKey.toLowerCase()) return false
573
+ return true
578
574
  }