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.
- package/CHANGELOG.md +23 -0
- package/README.md +6 -5
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +53 -67
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/AccessKey.d.ts +89 -106
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +226 -210
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Account.d.ts +0 -9
- package/dist/core/Account.d.ts.map +1 -1
- package/dist/core/Account.js +1 -13
- package/dist/core/Account.js.map +1 -1
- package/dist/core/Adapter.d.ts +10 -2
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Provider.d.ts +4 -5
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +66 -76
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Schema.d.ts +7 -7
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +114 -124
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +92 -105
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/adapters/privy.d.ts +164 -0
- package/dist/core/adapters/privy.d.ts.map +1 -0
- package/dist/core/adapters/privy.js +574 -0
- package/dist/core/adapters/privy.js.map +1 -0
- package/dist/core/adapters/turnkey.d.ts.map +1 -1
- package/dist/core/adapters/turnkey.js +136 -133
- package/dist/core/adapters/turnkey.js.map +1 -1
- package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
- package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
- package/dist/core/internal/AccessKeyTransaction.js +155 -0
- package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +9 -4
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +9 -4
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +61 -72
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/Kv.d.ts +19 -9
- package/dist/server/Kv.d.ts.map +1 -1
- package/dist/server/Kv.js +32 -9
- package/dist/server/Kv.js.map +1 -1
- package/dist/server/internal/handlers/auth.d.ts.map +1 -1
- package/dist/server/internal/handlers/auth.js +2 -0
- package/dist/server/internal/handlers/auth.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +18 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +115 -2
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +16 -9
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/adapter.ts +69 -74
- package/src/core/AccessKey.test.ts +525 -559
- package/src/core/AccessKey.ts +323 -327
- package/src/core/Account.test.ts +5 -50
- package/src/core/Account.ts +1 -19
- package/src/core/Adapter.ts +6 -2
- package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +97 -31
- package/src/core/Provider.ts +110 -110
- package/src/core/Schema.test-d.ts +18 -0
- package/src/core/adapters/dialog.test.ts +137 -13
- package/src/core/adapters/dialog.ts +109 -121
- package/src/core/adapters/local.ts +95 -119
- package/src/core/adapters/privy.test-d.ts +128 -0
- package/src/core/adapters/privy.test.ts +634 -0
- package/src/core/adapters/privy.ts +805 -0
- package/src/core/adapters/turnkey.test.ts +117 -13
- package/src/core/adapters/turnkey.ts +132 -149
- package/src/core/internal/AccessKeyTransaction.ts +243 -0
- package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
- package/src/core/zod/request.test.ts +36 -0
- package/src/core/zod/rpc.ts +17 -10
- package/src/index.ts +1 -1
- package/src/react-native/adapter.ts +77 -84
- package/src/server/Kv.test.ts +51 -0
- package/src/server/Kv.ts +43 -9
- package/src/server/internal/handlers/auth.test.ts +10 -5
- package/src/server/internal/handlers/auth.ts +3 -0
- package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +3 -0
- package/src/server/internal/handlers/relay.ts +138 -13
- package/src/server/internal/handlers/session.test.ts +1 -1
- package/src/server/internal/handlers/webAuthn.test.ts +20 -0
- package/src/server/internal/handlers/webAuthn.ts +24 -12
- /package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
- /package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +0 -0
- /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
package/src/core/AccessKey.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import { AbiFunction, Address, Hex,
|
|
1
|
+
import { AbiFunction, Address, Hex, PublicKey, WebCryptoP256 } from 'ox'
|
|
2
2
|
import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
|
|
3
|
-
import type
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
/**
|
|
168
|
-
export function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
/**
|
|
302
|
-
export function
|
|
303
|
-
account
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
/**
|
|
356
|
-
export
|
|
357
|
-
const {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
|
375
|
-
/** Options for {@link
|
|
377
|
+
export declare namespace add {
|
|
378
|
+
/** Options for {@link add}. */
|
|
376
379
|
type Options = {
|
|
377
|
-
/** Root account address that owns
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
|
|
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
|
-
/**
|
|
394
|
-
type ReturnType =
|
|
392
|
+
/** Stored access key record. */
|
|
393
|
+
type ReturnType = AccessKey
|
|
395
394
|
}
|
|
396
395
|
|
|
397
|
-
/**
|
|
398
|
-
export function
|
|
399
|
-
const {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
|
500
|
-
|
|
501
|
-
return (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
512
|
-
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
558
|
-
...
|
|
559
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
}
|