accounts 0.12.0 → 0.12.2

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 (58) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -6
  3. package/dist/cli/adapter.d.ts.map +1 -1
  4. package/dist/cli/adapter.js +7 -5
  5. package/dist/cli/adapter.js.map +1 -1
  6. package/dist/core/AccessKey.d.ts +80 -3
  7. package/dist/core/AccessKey.d.ts.map +1 -1
  8. package/dist/core/AccessKey.js +154 -10
  9. package/dist/core/AccessKey.js.map +1 -1
  10. package/dist/core/Adapter.d.ts +0 -2
  11. package/dist/core/Adapter.d.ts.map +1 -1
  12. package/dist/core/Provider.d.ts +24 -11
  13. package/dist/core/Provider.d.ts.map +1 -1
  14. package/dist/core/Provider.js +97 -49
  15. package/dist/core/Provider.js.map +1 -1
  16. package/dist/core/adapters/dialog.d.ts.map +1 -1
  17. package/dist/core/adapters/dialog.js +11 -8
  18. package/dist/core/adapters/dialog.js.map +1 -1
  19. package/dist/core/adapters/local.d.ts.map +1 -1
  20. package/dist/core/adapters/local.js +36 -52
  21. package/dist/core/adapters/local.js.map +1 -1
  22. package/dist/core/adapters/turnkey.d.ts.map +1 -1
  23. package/dist/core/adapters/turnkey.js +28 -44
  24. package/dist/core/adapters/turnkey.js.map +1 -1
  25. package/dist/core/zod/rpc.d.ts +12 -12
  26. package/dist/core/zod/rpc.d.ts.map +1 -1
  27. package/dist/core/zod/rpc.js +7 -3
  28. package/dist/core/zod/rpc.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/react-native/adapter.d.ts.map +1 -1
  34. package/dist/react-native/adapter.js +7 -5
  35. package/dist/react-native/adapter.js.map +1 -1
  36. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  37. package/dist/server/internal/handlers/relay.js +16 -1
  38. package/dist/server/internal/handlers/relay.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/cli/adapter.ts +7 -5
  41. package/src/core/AccessKey.test.ts +259 -8
  42. package/src/core/AccessKey.ts +247 -14
  43. package/src/core/Adapter.ts +0 -2
  44. package/src/core/Provider.test-d.ts +15 -0
  45. package/src/core/Provider.test.ts +113 -7
  46. package/src/core/Provider.ts +119 -60
  47. package/src/core/Remote.test.ts +78 -6
  48. package/src/core/adapters/dialog.test.ts +66 -1
  49. package/src/core/adapters/dialog.ts +12 -7
  50. package/src/core/adapters/local.test.ts +35 -0
  51. package/src/core/adapters/local.ts +38 -64
  52. package/src/core/adapters/turnkey.ts +29 -62
  53. package/src/core/mppx.test.ts +81 -10
  54. package/src/core/zod/rpc.ts +8 -6
  55. package/src/index.ts +1 -0
  56. package/src/react-native/adapter.ts +7 -5
  57. package/src/server/internal/handlers/relay.test.ts +24 -0
  58. package/src/server/internal/handlers/relay.ts +17 -1
@@ -1,7 +1,7 @@
1
- import { WebCryptoP256 } from 'ox'
1
+ import { Hex, WebCryptoP256 } from 'ox'
2
2
  import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
3
  import { encodeErrorResult } from 'viem'
4
- import { Abis, Account as TempoAccount } from 'viem/tempo'
4
+ import { Abis, Account as TempoAccount, Actions } from 'viem/tempo'
5
5
  import { describe, expect, test } from 'vp/test'
6
6
 
7
7
  import { accounts, privateKeys } from '../../test/config.js'
@@ -16,7 +16,11 @@ const rootAddress = accounts[0]!.address
16
16
 
17
17
  function createKeyAuthorization(
18
18
  address: `0x${string}`,
19
- options: { expiry?: number | undefined; limits?: { token: `0x${string}`; limit: bigint }[] } = {},
19
+ options: {
20
+ expiry?: number | undefined
21
+ limits?: { token: `0x${string}`; limit: bigint }[] | undefined
22
+ scopes?: KeyAuthorization.Scope[] | undefined
23
+ } = {},
20
24
  ) {
21
25
  return KeyAuthorization.from(
22
26
  {
@@ -24,6 +28,7 @@ function createKeyAuthorization(
24
28
  chainId: 1n,
25
29
  expiry: options.expiry,
26
30
  limits: options.limits,
31
+ scopes: options.scopes,
27
32
  type: 'p256',
28
33
  },
29
34
  { signature: SignatureEnvelope.from(`0x${'00'.repeat(65)}`) },
@@ -285,9 +290,9 @@ describe('generate', () => {
285
290
  })
286
291
  })
287
292
 
288
- describe('prepare', () => {
293
+ describe('prepareAuthorization', () => {
289
294
  test('default: prepares generated p256 key authorization', async () => {
290
- const result = await AccessKey.prepare({ chainId: 1, expiry: 123 })
295
+ const result = await AccessKey.prepareAuthorization({ chainId: 1, expiry: 123 })
291
296
 
292
297
  expect(result.keyAuthorization.address).toMatch(/^0x[0-9a-f]{40}$/i)
293
298
  expect(result.keyAuthorization.chainId).toMatchInlineSnapshot(`1n`)
@@ -297,7 +302,7 @@ describe('prepare', () => {
297
302
  })
298
303
 
299
304
  test('behavior: prepares external key authorization from address', async () => {
300
- const result = await AccessKey.prepare({
305
+ const result = await AccessKey.prepareAuthorization({
301
306
  address: accounts[1]!.address,
302
307
  chainId: 123n,
303
308
  expiry: 456,
@@ -349,7 +354,7 @@ describe('prepare', () => {
349
354
  const keyPair = await WebCryptoP256.createKeyPair()
350
355
  const account = TempoAccount.fromWebCryptoP256(keyPair)
351
356
 
352
- const result = await AccessKey.prepare({
357
+ const result = await AccessKey.prepareAuthorization({
353
358
  chainId: 123n,
354
359
  expiry: 456,
355
360
  keyType: 'p256',
@@ -370,7 +375,7 @@ describe('prepare', () => {
370
375
  })
371
376
 
372
377
  test('behavior: defaults external key type to secp256k1', async () => {
373
- const result = await AccessKey.prepare({
378
+ const result = await AccessKey.prepareAuthorization({
374
379
  address: accounts[1]!.address,
375
380
  chainId: 1,
376
381
  expiry: 123,
@@ -380,6 +385,87 @@ describe('prepare', () => {
380
385
  })
381
386
  })
382
387
 
388
+ describe('saveAuthorization', () => {
389
+ test('default: saves prepared authorization with provided signature', async () => {
390
+ const store = createStore()
391
+ const prepared = await AccessKey.prepareAuthorization({
392
+ address: accounts[1]!.address,
393
+ chainId: 1,
394
+ expiry: 123,
395
+ })
396
+ const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const
397
+
398
+ const result = AccessKey.saveAuthorization({
399
+ address: rootAddress,
400
+ prepared,
401
+ signature,
402
+ store,
403
+ })
404
+
405
+ expect(result).toMatchInlineSnapshot(`
406
+ {
407
+ "chainId": "0x1",
408
+ "expiry": "0x7b",
409
+ "keyId": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650",
410
+ "keyType": "secp256k1",
411
+ "limits": undefined,
412
+ "signature": {
413
+ "r": "0x1111111111111111111111111111111111111111111111111111111111111111",
414
+ "s": "0x2222222222222222222222222222222222222222222222222222222222222222",
415
+ "type": "secp256k1",
416
+ "yParity": "0x0",
417
+ },
418
+ }
419
+ `)
420
+ expect(store.getState().accessKeys.map(({ keyAuthorization: _, ...accessKey }) => accessKey))
421
+ .toMatchInlineSnapshot(`
422
+ [
423
+ {
424
+ "access": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
425
+ "address": "0x8C8d35429F74ec245F8Ef2f4Fd1e551cFF97d650",
426
+ "chainId": 1,
427
+ "expiry": 123,
428
+ "keyType": "secp256k1",
429
+ "limits": undefined,
430
+ "scopes": undefined,
431
+ },
432
+ ]
433
+ `)
434
+ })
435
+ })
436
+
437
+ describe('authorize', () => {
438
+ test('default: prepares, signs, and saves authorization', async () => {
439
+ const store = createStore()
440
+ const digests: Hex.Hex[] = []
441
+ const signature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b` as const
442
+ const account = {
443
+ ...accounts[0]!,
444
+ sign: async (parameters: { hash: Hex.Hex }) => {
445
+ digests.push(parameters.hash)
446
+ return signature
447
+ },
448
+ } as TempoAccount.Account
449
+
450
+ await AccessKey.authorize({
451
+ account,
452
+ chainId: 1,
453
+ parameters: {
454
+ address: accounts[1]!.address,
455
+ expiry: 123,
456
+ },
457
+ store,
458
+ })
459
+
460
+ expect(digests).toMatchInlineSnapshot(`
461
+ [
462
+ "0xea47721547363fc82a5dca62b4544e4718d861b3df10bfac65d30102594b5c26",
463
+ ]
464
+ `)
465
+ expect(store.getState().accessKeys.length).toMatchInlineSnapshot(`1`)
466
+ })
467
+ })
468
+
383
469
  describe('hydrate', () => {
384
470
  test('default: hydrates webCrypto access key to signable account', async () => {
385
471
  const keyPair = await WebCryptoP256.createKeyPair()
@@ -660,6 +746,96 @@ describe('selectAccount', () => {
660
746
  expect(result?.source).toMatchInlineSnapshot(`"accessKey"`)
661
747
  })
662
748
 
749
+ test('behavior: scoped access key checks recipient allowlist', async () => {
750
+ const keyPair = await WebCryptoP256.createKeyPair()
751
+ const token = '0x0000000000000000000000000000000000000abc' as const
752
+ const recipient = '0x0000000000000000000000000000000000000def' as const
753
+ const store = setup([
754
+ {
755
+ access: rootAddress,
756
+ address: '0x0000000000000000000000000000000000000099',
757
+ chainId: 1,
758
+ keyPair,
759
+ keyType: 'webCrypto',
760
+ scopes: [
761
+ { address: token, selector: 'transfer(address,uint256)', recipients: [recipient] },
762
+ ],
763
+ },
764
+ ])
765
+ const call = Actions.token.transfer.call({
766
+ amount: 1n,
767
+ to: recipient,
768
+ token,
769
+ })
770
+
771
+ const result = AccessKey.selectAccount({
772
+ address: rootAddress,
773
+ chainId: 1,
774
+ store,
775
+ calls: [call],
776
+ })
777
+
778
+ expect(result?.source).toMatchInlineSnapshot(`"accessKey"`)
779
+ })
780
+
781
+ test('behavior: scoped access key skips non-allowlisted recipients', async () => {
782
+ const keyPair = await WebCryptoP256.createKeyPair()
783
+ const token = '0x0000000000000000000000000000000000000abc' as const
784
+ const store = setup([
785
+ {
786
+ access: rootAddress,
787
+ address: '0x0000000000000000000000000000000000000099',
788
+ chainId: 1,
789
+ keyPair,
790
+ keyType: 'webCrypto',
791
+ scopes: [
792
+ {
793
+ address: token,
794
+ selector: 'transfer(address,uint256)',
795
+ recipients: ['0x0000000000000000000000000000000000000def'],
796
+ },
797
+ ],
798
+ },
799
+ ])
800
+ const call = Actions.token.transfer.call({
801
+ amount: 1n,
802
+ to: '0x0000000000000000000000000000000000000fed',
803
+ token,
804
+ })
805
+
806
+ const result = AccessKey.selectAccount({
807
+ address: rootAddress,
808
+ chainId: 1,
809
+ store,
810
+ calls: [call],
811
+ })
812
+
813
+ expect(result).toMatchInlineSnapshot(`undefined`)
814
+ })
815
+
816
+ test('behavior: malformed scopes skip the access key', async () => {
817
+ const keyPair = await WebCryptoP256.createKeyPair()
818
+ const store = setup([
819
+ {
820
+ access: rootAddress,
821
+ address: '0x0000000000000000000000000000000000000099',
822
+ chainId: 1,
823
+ keyPair,
824
+ keyType: 'webCrypto',
825
+ scopes: [{}],
826
+ } as never,
827
+ ])
828
+
829
+ const result = AccessKey.selectAccount({
830
+ address: rootAddress,
831
+ chainId: 1,
832
+ store,
833
+ calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }],
834
+ })
835
+
836
+ expect(result).toMatchInlineSnapshot(`undefined`)
837
+ })
838
+
663
839
  test('behavior: scoped access key without selector allows any call to that address', async () => {
664
840
  const keyPair = await WebCryptoP256.createKeyPair()
665
841
  const token = '0x0000000000000000000000000000000000000abc' as const
@@ -703,6 +879,81 @@ describe('selectAccount', () => {
703
879
  })
704
880
  })
705
881
 
882
+ describe('getStatus', () => {
883
+ test('behavior: returns pending for locally stored key authorization', async () => {
884
+ const store = createStore()
885
+ const keyPair = await WebCryptoP256.createKeyPair()
886
+ const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
887
+ const keyAuthorization = createKeyAuthorization(accessKey.address)
888
+
889
+ AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
890
+
891
+ const result = await AccessKey.getStatus({
892
+ address: rootAddress,
893
+ chainId: 1,
894
+ store,
895
+ })
896
+
897
+ expect(result).toMatchInlineSnapshot(`"pending"`)
898
+ })
899
+
900
+ test('behavior: returns published for local key without pending authorization', async () => {
901
+ const store = createStore()
902
+ const keyPair = await WebCryptoP256.createKeyPair()
903
+ const accessKey = TempoAccount.fromWebCryptoP256(keyPair, { access: rootAddress })
904
+ const keyAuthorization = createKeyAuthorization(accessKey.accessKeyAddress)
905
+
906
+ AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
907
+ AccessKey.removePending(accessKey, { store })
908
+
909
+ const result = await AccessKey.getStatus({
910
+ address: rootAddress,
911
+ chainId: 1,
912
+ store,
913
+ })
914
+
915
+ expect(result).toMatchInlineSnapshot(`"published"`)
916
+ })
917
+
918
+ test('behavior: returns expired for expired local key', async () => {
919
+ const store = createStore()
920
+ const keyPair = await WebCryptoP256.createKeyPair()
921
+ const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
922
+ const keyAuthorization = createKeyAuthorization(accessKey.address, { expiry: 100 })
923
+
924
+ AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
925
+
926
+ const result = await AccessKey.getStatus({
927
+ address: rootAddress,
928
+ chainId: 1,
929
+ now: 101,
930
+ store,
931
+ })
932
+
933
+ expect(result).toMatchInlineSnapshot(`"expired"`)
934
+ })
935
+
936
+ test('behavior: returns missing when no local key matches the policy', async () => {
937
+ const store = createStore()
938
+ const keyPair = await WebCryptoP256.createKeyPair()
939
+ const accessKey = TempoAccount.fromWebCryptoP256(keyPair)
940
+ const keyAuthorization = createKeyAuthorization(accessKey.address, {
941
+ scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
942
+ })
943
+
944
+ AccessKey.save({ address: rootAddress, keyAuthorization, keyPair, store })
945
+
946
+ const result = await AccessKey.getStatus({
947
+ address: rootAddress,
948
+ calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
949
+ chainId: 1,
950
+ store,
951
+ })
952
+
953
+ expect(result).toMatchInlineSnapshot(`"missing"`)
954
+ })
955
+ })
956
+
706
957
  describe('revoke', () => {
707
958
  test('default: removes access keys by root address', async () => {
708
959
  const store = createStore()
@@ -1,6 +1,7 @@
1
1
  import { AbiFunction, Address, Hex, Provider, PublicKey, WebCryptoP256 } from 'ox'
2
- import { KeyAuthorization } from 'ox/tempo'
3
- import { Account as TempoAccount } from 'viem/tempo'
2
+ import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
+ import type { Client, Transport } from 'viem'
4
+ import { Account as TempoAccount, Actions } from 'viem/tempo'
4
5
 
5
6
  import type { OneOf } from '../internal/types.js'
6
7
  import * as ExecutionError from './ExecutionError.js'
@@ -16,6 +17,21 @@ const removalErrorNames = new Set([
16
17
  'SignatureTypeMismatch',
17
18
  ])
18
19
 
20
+ /** Access-key publication states. */
21
+ export const status = {
22
+ /** No matching usable access key was found. */
23
+ missing: 'missing',
24
+ /** A matching key exists locally and still needs its first transaction to publish the authorization. */
25
+ pending: 'pending',
26
+ /** A matching key exists on-chain and can be used. */
27
+ published: 'published',
28
+ /** A matching key exists but is past its expiry. */
29
+ expired: 'expired',
30
+ } as const
31
+
32
+ /** Publication state for an access key. */
33
+ export type Status = (typeof status)[keyof typeof status]
34
+
19
35
  /** Access key entry stored alongside accounts. */
20
36
  export type AccessKey = {
21
37
  /** Access key address. */
@@ -91,7 +107,9 @@ export declare namespace generate {
91
107
  }
92
108
 
93
109
  /** Prepares an unsigned key authorization and local key material when needed. */
94
- export async function prepare(options: prepare.Options): Promise<prepare.ReturnType> {
110
+ export async function prepareAuthorization(
111
+ options: prepareAuthorization.Options,
112
+ ): Promise<prepareAuthorization.ReturnType> {
95
113
  const { address, chainId, expiry, keyType, limits, publicKey, scopes } = options
96
114
 
97
115
  if (address || publicKey) {
@@ -118,8 +136,8 @@ export async function prepare(options: prepare.Options): Promise<prepare.ReturnT
118
136
  return { keyAuthorization, keyPair }
119
137
  }
120
138
 
121
- export declare namespace prepare {
122
- /** Options for {@link prepare}. */
139
+ export declare namespace prepareAuthorization {
140
+ /** Options for {@link prepareAuthorization}. */
123
141
  type Options = {
124
142
  /** External access key address. Alternative to `publicKey`. */
125
143
  address?: Address.Address | undefined
@@ -146,6 +164,91 @@ export declare namespace prepare {
146
164
  }
147
165
  }
148
166
 
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
172
+ const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
173
+ signature: SignatureEnvelope.from(signature),
174
+ })
175
+
176
+ save({
177
+ address,
178
+ keyAuthorization,
179
+ ...(prepared.keyPair ? { keyPair: prepared.keyPair } : {}),
180
+ store,
181
+ })
182
+
183
+ return KeyAuthorization.toRpc(keyAuthorization)
184
+ }
185
+
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
+ export declare namespace authorize {
214
+ /** Options for {@link authorize}. */
215
+ type Options = {
216
+ /** Root account that owns this access key and signs its authorization. */
217
+ account: TempoAccount.Account
218
+ /** Default chain ID for the authorization when `parameters.chainId` is not set. */
219
+ chainId: bigint | number
220
+ /** Access key authorization parameters. */
221
+ parameters: Omit<prepareAuthorization.Options, 'chainId'> & {
222
+ /** Chain ID the key authorization is scoped to. */
223
+ chainId?: bigint | number | undefined
224
+ }
225
+ /** Reactive state store. */
226
+ store: Store.Store
227
+ }
228
+
229
+ /** Signed key authorization in RPC form. */
230
+ type ReturnType = KeyAuthorization.Rpc
231
+ }
232
+
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
+ }
251
+
149
252
  /** Hydrates an access key entry to a viem Account. Only works for locally-generated keys. */
150
253
  export function hydrate(accessKey: AccessKey): TempoAccount.Account {
151
254
  if ('keyPair' in accessKey && accessKey.keyPair)
@@ -249,6 +352,48 @@ export declare namespace selectAccount {
249
352
  }
250
353
  }
251
354
 
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
368
+ }
369
+
370
+ if (accessKey && client) return await getPublishedStatus(client, { accessKey, address, now })
371
+ return status.missing
372
+ }
373
+
374
+ export declare namespace getStatus {
375
+ /** Options for {@link getStatus}. */
376
+ 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
389
+ /** Reactive state store. */
390
+ store: Store.Store
391
+ }
392
+
393
+ /** Access-key publication status. */
394
+ type ReturnType = Status
395
+ }
396
+
252
397
  /** Removes an access key from the store. */
253
398
  export function revoke(options: revoke.Options): void {
254
399
  const { address, store } = options
@@ -273,25 +418,113 @@ function scopesMatch(
273
418
  calls?: readonly { to?: Address.Address | undefined; data?: Hex.Hex | undefined }[] | undefined
274
419
  },
275
420
  ): boolean {
276
- if (!key.scopes) return true
421
+ const scopes = key.scopes
422
+ if (typeof scopes === 'undefined') return true
423
+ if (!Array.isArray(scopes)) return false
277
424
  if (!options.calls) return false
278
425
  return options.calls.every((call) => {
279
426
  if (!call.to) return false
280
427
  const callTo = call.to.toLowerCase()
281
428
  const callSelector = call.data?.slice(0, 10).toLowerCase()
282
- return key.scopes!.some((scope) => {
429
+ return scopes.some((scope) => {
430
+ if (!isScope(scope)) return false
283
431
  if (scope.address.toLowerCase() !== callTo) return false
284
- if (!scope.selector) return true
285
- const scopeSelector = (
286
- scope.selector.startsWith('0x') && scope.selector.length === 10
287
- ? scope.selector
288
- : AbiFunction.getSelector(scope.selector)
289
- ).toLowerCase()
290
- return callSelector === scopeSelector
432
+ if (!scope.selector) return scope.recipients ? scope.recipients.length === 0 : true
433
+ const scopeSelector = getSelector(scope.selector)
434
+ if (!scopeSelector || callSelector !== scopeSelector) return false
435
+ return recipientsMatch(scope.recipients, call.data)
291
436
  })
292
437
  })
293
438
  }
294
439
 
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
+ function isScope(scope: unknown): scope is NonNullable<AccessKey['scopes']>[number] {
483
+ if (!scope || typeof scope !== 'object') return false
484
+ const value = scope as {
485
+ address?: unknown
486
+ recipients?: unknown
487
+ selector?: unknown
488
+ }
489
+ if (typeof value.address !== 'string' || !Address.validate(value.address)) return false
490
+ if (typeof value.selector !== 'undefined' && typeof value.selector !== 'string') return false
491
+ if (typeof value.recipients !== 'undefined') {
492
+ if (!Array.isArray(value.recipients)) return false
493
+ if (value.recipients.some((recipient) => typeof recipient !== 'string')) return false
494
+ if (value.recipients.some((recipient) => !Address.validate(recipient))) return false
495
+ }
496
+ return true
497
+ }
498
+
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
508
+ }
509
+ }
510
+
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())
519
+ }
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
526
+ }
527
+
295
528
  function shouldRemoveForError(error: unknown): boolean {
296
529
  if (!(error instanceof Error)) return false
297
530
  const parsed = ExecutionError.parse(error)
@@ -311,8 +311,6 @@ export declare namespace authorizeAccessKey {
311
311
  recipients?: readonly Address[] | undefined
312
312
  }[]
313
313
  | undefined
314
- /** Pre-computed signature over the key authorization digest (skips a second signing ceremony). */
315
- signature?: Hex | undefined
316
314
  }
317
315
 
318
316
  type ReturnType = {
@@ -1,6 +1,7 @@
1
1
  import type { RpcSchema } from 'ox'
2
2
  import { describe, expectTypeOf, test } from 'vp/test'
3
3
 
4
+ import * as Provider from './Provider.js'
4
5
  import type * as Schema from './Schema.js'
5
6
 
6
7
  type Result<method extends RpcSchema.MethodNameGeneric<Schema.Ox>> = RpcSchema.ExtractReturnType<
@@ -57,3 +58,17 @@ describe('request', () => {
57
58
  expectTypeOf<Result<'wallet_switchEthereumChain'>>().toEqualTypeOf<undefined>()
58
59
  })
59
60
  })
61
+
62
+ describe('create options', () => {
63
+ test('mpp accepts session deposit options', () => {
64
+ expectTypeOf<NonNullable<Parameters<typeof Provider.create>[0]>>().toMatchTypeOf<{
65
+ mpp?:
66
+ | boolean
67
+ | {
68
+ deposit?: string | undefined
69
+ maxDeposit?: string | undefined
70
+ }
71
+ | undefined
72
+ }>()
73
+ })
74
+ })