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
@@ -69,7 +69,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
69
69
  listeners.delete(listener)
70
70
 
71
71
  if (queued.status === 'success') resolve(queued.result)
72
- else reject(new ox_Provider.UserRejectedRequestError({ message: queued.error.message }))
72
+ else reject(ox_Provider.parseError(queued.error))
73
73
 
74
74
  // Remove the resolved request from the queue.
75
75
  store.setState((x) => ({
@@ -154,7 +154,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
154
154
  account: TempoAccount.Account,
155
155
  keyAuthorization?: KeyAuthorization.Signed,
156
156
  ) => Promise<result>,
157
- ): Promise<result | undefined> {
157
+ ): Promise<{ account: TempoAccount.Account; result: result } | undefined> {
158
158
  if (!options.from || typeof options.chainId === 'undefined') return undefined
159
159
  const account = AccessKey.selectAccount({
160
160
  address: options.from,
@@ -166,8 +166,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
166
166
  const keyAuthorization = AccessKey.getPending(account, { store })
167
167
  try {
168
168
  const result = await fn(account, keyAuthorization ?? undefined)
169
- AccessKey.removePending(account, { store })
170
- return result
169
+ return { account, result }
171
170
  } catch (err) {
172
171
  if (AccessKey.invalidate(account, err, { store }))
173
172
  console.warn('[accounts] access key invalidated, falling through to dialog:', err)
@@ -307,7 +306,7 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
307
306
  })
308
307
  return await account.signTransaction(prepared as never)
309
308
  })
310
- if (result !== undefined) return result
309
+ if (result !== undefined) return result.result
311
310
  return await provider.request({
312
311
  ...request,
313
312
  params: [z.encode(Rpc.transactionRequest, parameters)] as const,
@@ -342,7 +341,10 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
342
341
  params: [signed],
343
342
  })
344
343
  })
345
- if (result !== undefined) return result
344
+ if (result !== undefined) {
345
+ AccessKey.removePending(result.account, { store })
346
+ return result.result
347
+ }
346
348
  return await provider.request({
347
349
  ...request,
348
350
  params: [z.encode(Rpc.transactionRequest, parameters)] as const,
@@ -373,7 +375,10 @@ export function dialog(options: dialog.Options = {}): Adapter.Adapter {
373
375
  params: [signed],
374
376
  })
375
377
  })
376
- if (result !== undefined) return result
378
+ if (result !== undefined) {
379
+ AccessKey.removePending(result.account, { store })
380
+ return result.result
381
+ }
377
382
  return await provider.request({
378
383
  ...request,
379
384
  params: [z.encode(Rpc.transactionRequest, parameters)] as const,
@@ -31,6 +31,41 @@ describe('local', () => {
31
31
  ]
32
32
  `)
33
33
  })
34
+
35
+ test('default: authorizeAccessKey folds the key authorization digest into the ceremony', async () => {
36
+ const captured: { digest: Hex | undefined }[] = []
37
+ const { adapter } = setup({
38
+ loadAccounts: makeLoadAccounts(0, captured),
39
+ })
40
+
41
+ const result = await adapter.actions.loadAccounts(
42
+ {
43
+ authorizeAccessKey: {
44
+ address: core_accounts[1]!.address,
45
+ expiry: 0,
46
+ keyType: 'secp256k1',
47
+ },
48
+ },
49
+ { method: 'wallet_connect', params: undefined },
50
+ )
51
+
52
+ expect({
53
+ digest: captured[0]?.digest,
54
+ hasSignature: typeof result.signature === 'string',
55
+ keyAuthorizationSignature: result.keyAuthorization?.signature,
56
+ }).toMatchInlineSnapshot(`
57
+ {
58
+ "digest": "0x64d5413088ae92221fde7900d29b540efc040ac134ccf50d3e916a9011f81bd0",
59
+ "hasSignature": true,
60
+ "keyAuthorizationSignature": {
61
+ "r": "0x876bd6f1719bdffc65382322939303ef37a804df5011b73704e7f4d9e4603cc8",
62
+ "s": "0x6c377e36d7a76b2dd15fdc9599ed663136a8e0faa33c3950e72c3b503bc18bab",
63
+ "type": "secp256k1",
64
+ "yParity": "0x1",
65
+ },
66
+ }
67
+ `)
68
+ })
34
69
  })
35
70
 
36
71
  describe('loadAccounts: personalSign', () => {
@@ -1,5 +1,5 @@
1
- import { Hex, Provider as ox_Provider } from 'ox'
2
- import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
1
+ import { Provider as ox_Provider } from 'ox'
2
+ import { KeyAuthorization } from 'ox/tempo'
3
3
  import { BaseError, hashMessage } from 'viem'
4
4
  import { prepareTransactionRequest } from 'viem/actions'
5
5
  import { Account as TempoAccount, Actions } from 'viem/tempo'
@@ -28,48 +28,6 @@ export function local(options: local.Options): Adapter.Adapter {
28
28
  const { createAccount, icon, loadAccounts, name, rdns } = options
29
29
 
30
30
  return Adapter.define({ icon, name, rdns }, ({ getAccount, getClient, store }) => {
31
- /**
32
- * Resolves access key params into an unsigned key authorization.
33
- */
34
- async function prepareKeyAuthorization(options: Adapter.authorizeAccessKey.Parameters) {
35
- const { address, expiry, keyType, limits, publicKey, scopes } = options
36
- return await AccessKey.prepare({
37
- address,
38
- chainId: options.chainId ?? getClient().chain.id,
39
- expiry,
40
- keyType,
41
- limits,
42
- publicKey,
43
- scopes,
44
- })
45
- }
46
-
47
- /**
48
- * Signs (or wraps a pre-computed signature into) a key authorization
49
- * and saves the result to the store.
50
- */
51
- async function signKeyAuthorization(
52
- account: TempoAccount.Account,
53
- prepared: Awaited<ReturnType<typeof prepareKeyAuthorization>>,
54
- options: {
55
- signature?: Hex.Hex | undefined
56
- } = {},
57
- ) {
58
- const { keyPair } = prepared
59
-
60
- const keyAuthorization = await (async () => {
61
- const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
62
- const signature = options.signature ?? (await account.sign({ hash: digest }))
63
- return KeyAuthorization.from(prepared.keyAuthorization, {
64
- signature: SignatureEnvelope.from(signature),
65
- })
66
- })()
67
-
68
- AccessKey.save({ address: account.address, keyAuthorization, keyPair, store })
69
-
70
- return KeyAuthorization.toRpc(keyAuthorization)
71
- }
72
-
73
31
  async function withAccessKey<result>(
74
32
  options: Pick<Account.find.Options, 'address' | 'calls' | 'chainId'>,
75
33
  fn: (
@@ -80,9 +38,7 @@ export function local(options: local.Options): Adapter.Adapter {
80
38
  const account = getAccount({ ...options, signable: true })
81
39
  const keyAuthorization = AccessKey.getPending(account, { store })
82
40
  try {
83
- const result = await fn(account, keyAuthorization ?? undefined)
84
- AccessKey.removePending(account, { store })
85
- return result
41
+ return await fn(account, keyAuthorization ?? undefined)
86
42
  } catch (error) {
87
43
  if (account.source !== 'accessKey') throw error
88
44
  AccessKey.invalidate(account, error, { store })
@@ -128,8 +84,12 @@ export function local(options: local.Options): Adapter.Adapter {
128
84
 
129
85
  const keyAuthorization = await (async () => {
130
86
  if (!grantOptions) return undefined
131
- const prepared = await prepareKeyAuthorization(grantOptions)
132
- return await signKeyAuthorization(account, prepared)
87
+ return await AccessKey.authorize({
88
+ account,
89
+ chainId: getClient().chain.id,
90
+ parameters: grantOptions,
91
+ store,
92
+ })
133
93
  })()
134
94
 
135
95
  return {
@@ -142,10 +102,12 @@ export function local(options: local.Options): Adapter.Adapter {
142
102
  }
143
103
  },
144
104
  async authorizeAccessKey(parameters) {
145
- const prepared = await prepareKeyAuthorization(parameters)
146
105
  const account = getAccount({ accessKey: false, signable: true })
147
- const keyAuthorization = await signKeyAuthorization(account, prepared, {
148
- signature: parameters.signature,
106
+ const keyAuthorization = await AccessKey.authorize({
107
+ account,
108
+ chainId: getClient().chain.id,
109
+ parameters,
110
+ store,
149
111
  })
150
112
  return { keyAuthorization, rootAddress: account.address }
151
113
  },
@@ -165,7 +127,14 @@ export function local(options: local.Options): Adapter.Adapter {
165
127
  const peronsalSign_digest = personalSign ? hashMessage(personalSign.message) : undefined
166
128
 
167
129
  const keyAuthorization_unsigned = authorizeAccessKey
168
- ? await prepareKeyAuthorization(authorizeAccessKey)
130
+ ? await AccessKey.prepareAuthorization({
131
+ ...authorizeAccessKey,
132
+ chainId: authorizeAccessKey.chainId ?? getClient().chain.id,
133
+ })
134
+ : undefined
135
+
136
+ const keyAuthorization_digest = keyAuthorization_unsigned
137
+ ? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization)
169
138
  : undefined
170
139
 
171
140
  // Slot allocation:
@@ -176,11 +145,7 @@ export function local(options: local.Options): Adapter.Adapter {
176
145
  // `personalSign` wins the load-accounts ceremony and the key
177
146
  // authorization gets its own follow-up `account.sign` ceremony
178
147
  // (2 prompts total).
179
- const digest =
180
- peronsalSign_digest ??
181
- (keyAuthorization_unsigned
182
- ? KeyAuthorization.getSignPayload(keyAuthorization_unsigned.keyAuthorization)
183
- : rest.digest)
148
+ const digest = peronsalSign_digest ?? keyAuthorization_digest ?? rest.digest
184
149
 
185
150
  // Pass the prepared digest (or the caller's) into loadAccounts so
186
151
  // the ceremony can sign it in a single biometric prompt.
@@ -200,10 +165,15 @@ export function local(options: local.Options): Adapter.Adapter {
200
165
  // - Else (key-auth digest took the slot), reuse `signature_`.
201
166
  const keyAuthorization = await (async () => {
202
167
  if (!keyAuthorization_unsigned || !account) return undefined
203
- if (peronsalSign_digest)
204
- return await signKeyAuthorization(account, keyAuthorization_unsigned)
205
- return await signKeyAuthorization(account, keyAuthorization_unsigned, {
206
- signature: signature_,
168
+ const signature_keyAuthorization =
169
+ peronsalSign_digest || !signature_
170
+ ? await account.sign({ hash: keyAuthorization_digest! })
171
+ : signature_
172
+ return AccessKey.saveAuthorization({
173
+ address: account.address,
174
+ prepared: keyAuthorization_unsigned,
175
+ signature: signature_keyAuthorization,
176
+ store,
207
177
  })
208
178
  })()
209
179
 
@@ -301,10 +271,12 @@ export function local(options: local.Options): Adapter.Adapter {
301
271
  }),
302
272
  )
303
273
  const signed = await account.signTransaction(prepared as never)
304
- return await client.request({
274
+ const result = await client.request({
305
275
  method: 'eth_sendRawTransaction' as never,
306
276
  params: [signed],
307
277
  })
278
+ AccessKey.removePending(account, { store })
279
+ return result
308
280
  },
309
281
  async sendTransactionSync(parameters) {
310
282
  const { feePayer, ...rest } = parameters
@@ -330,10 +302,12 @@ export function local(options: local.Options): Adapter.Adapter {
330
302
  }),
331
303
  )
332
304
  const signed = await account.signTransaction(prepared as never)
333
- return await client.request({
305
+ const result = await client.request({
334
306
  method: 'eth_sendRawTransactionSync' as never,
335
307
  params: [signed],
336
308
  })
309
+ AccessKey.removePending(account, { store })
310
+ return result
337
311
  },
338
312
  },
339
313
  }
@@ -7,7 +7,7 @@ import {
7
7
  RpcResponse,
8
8
  Secp256k1,
9
9
  } from 'ox'
10
- import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
10
+ import { KeyAuthorization } from 'ox/tempo'
11
11
  import { hashMessage, hashTypedData, isAddressEqual } from 'viem'
12
12
  import type { Address } from 'viem/accounts'
13
13
  import { prepareTransactionRequest } from 'viem/actions'
@@ -273,48 +273,6 @@ export function turnkey<const client extends turnkey.Client>(
273
273
  return signatureToHex(result)
274
274
  }
275
275
 
276
- async function prepareKeyAuthorization(options: Adapter.authorizeAccessKey.Parameters) {
277
- const { address, expiry, keyType, limits, publicKey, scopes } = options
278
- return await AccessKey.prepare({
279
- address,
280
- chainId: options.chainId ?? getClient().chain.id,
281
- expiry,
282
- keyType,
283
- limits,
284
- publicKey,
285
- scopes,
286
- })
287
- }
288
-
289
- async function signKeyAuthorization(
290
- account: turnkey.WalletAccount,
291
- prepared: Awaited<ReturnType<typeof prepareKeyAuthorization>>,
292
- options: {
293
- signature?: Hex.Hex | undefined
294
- } = {},
295
- ) {
296
- const digest = KeyAuthorization.getSignPayload(prepared.keyAuthorization)
297
- const signature =
298
- options.signature ??
299
- (await signPayload({
300
- payload: digest,
301
- turnkeyClient: await getTurnkeyClient(),
302
- walletAccount: account,
303
- }))
304
- const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
305
- signature: SignatureEnvelope.from(signature),
306
- })
307
-
308
- AccessKey.save({
309
- address: core_Address.from(account.address),
310
- keyAuthorization,
311
- ...(prepared.keyPair ? { keyPair: prepared.keyPair } : {}),
312
- store,
313
- })
314
-
315
- return KeyAuthorization.toRpc(keyAuthorization)
316
- }
317
-
318
276
  async function withAccessKey<result>(
319
277
  options: {
320
278
  address?: Address | undefined
@@ -325,7 +283,7 @@ export function turnkey<const client extends turnkey.Client>(
325
283
  account: TempoAccount.Account,
326
284
  keyAuthorization?: KeyAuthorization.Signed,
327
285
  ) => Promise<result>,
328
- ) {
286
+ ): Promise<{ account: TempoAccount.Account; result: result } | undefined> {
329
287
  const account = (() => {
330
288
  try {
331
289
  return getAccount({ ...options, signable: true })
@@ -338,8 +296,7 @@ export function turnkey<const client extends turnkey.Client>(
338
296
  const keyAuthorization = AccessKey.getPending(account, { store })
339
297
  try {
340
298
  const result = await fn(account, keyAuthorization ?? undefined)
341
- AccessKey.removePending(account, { store })
342
- return result
299
+ return { account, result }
343
300
  } catch (error) {
344
301
  AccessKey.invalidate(account, error, { store })
345
302
  return undefined
@@ -420,11 +377,12 @@ export function turnkey<const client extends turnkey.Client>(
420
377
  const account = walletAccounts[0]
421
378
  const keyAuthorization = authorizeAccessKey
422
379
  ? account
423
- ? await signKeyAuthorization(
424
- account,
425
- await prepareKeyAuthorization(authorizeAccessKey),
426
- { signature: authorizeAccessKey.signature },
427
- )
380
+ ? await AccessKey.authorize({
381
+ account: toTempoAccount(account),
382
+ chainId: getClient().chain.id,
383
+ parameters: authorizeAccessKey,
384
+ store,
385
+ })
428
386
  : undefined
429
387
  : undefined
430
388
 
@@ -463,11 +421,12 @@ export function turnkey<const client extends turnkey.Client>(
463
421
  const account = walletAccounts[0]
464
422
  const keyAuthorization =
465
423
  authorizeAccessKey && account
466
- ? await signKeyAuthorization(
467
- account,
468
- await prepareKeyAuthorization(authorizeAccessKey),
469
- { signature: authorizeAccessKey.signature },
470
- )
424
+ ? await AccessKey.authorize({
425
+ account: toTempoAccount(account),
426
+ chainId: getClient().chain.id,
427
+ parameters: authorizeAccessKey,
428
+ store,
429
+ })
471
430
  : undefined
472
431
 
473
432
  return {
@@ -486,9 +445,11 @@ export function turnkey<const client extends turnkey.Client>(
486
445
  },
487
446
  async authorizeAccessKey(parameters) {
488
447
  const account = await accountForSigning(undefined)
489
- const prepared = await prepareKeyAuthorization(parameters)
490
- const keyAuthorization = await signKeyAuthorization(account, prepared, {
491
- signature: parameters.signature,
448
+ const keyAuthorization = await AccessKey.authorize({
449
+ account: toTempoAccount(account),
450
+ chainId: getClient().chain.id,
451
+ parameters,
452
+ store,
492
453
  })
493
454
  return { keyAuthorization, rootAddress: core_Address.from(account.address) }
494
455
  },
@@ -520,7 +481,7 @@ export function turnkey<const client extends turnkey.Client>(
520
481
  return await account.signTransaction(prepared as never)
521
482
  },
522
483
  )
523
- if (result !== undefined) return result
484
+ if (result !== undefined) return result.result
524
485
  return await signTransaction(parameters)
525
486
  },
526
487
  async signTypedData(parameters) {
@@ -561,7 +522,10 @@ export function turnkey<const client extends turnkey.Client>(
561
522
  })
562
523
  },
563
524
  )
564
- if (result !== undefined) return result
525
+ if (result !== undefined) {
526
+ AccessKey.removePending(result.account, { store })
527
+ return result.result
528
+ }
565
529
  const signed = await signTransaction(parameters)
566
530
  const viemClient = getClient({
567
531
  chainId: parameters.chainId,
@@ -595,7 +559,10 @@ export function turnkey<const client extends turnkey.Client>(
595
559
  })
596
560
  },
597
561
  )
598
- if (result !== undefined) return result
562
+ if (result !== undefined) {
563
+ AccessKey.removePending(result.account, { store })
564
+ return result.result
565
+ }
599
566
  const signed = await signTransaction(parameters)
600
567
  const viemClient = getClient({
601
568
  chainId: parameters.chainId,
@@ -1,12 +1,14 @@
1
+ import { Fetch } from 'mppx/client'
1
2
  import { Mppx as ServerMppx, tempo } from 'mppx/server'
2
3
  import { parseUnits } from 'viem'
3
4
  import { Addresses } from 'viem/tempo'
4
5
  import { Actions } from 'viem/tempo'
5
- import { afterAll, beforeAll, describe, expect, test } from 'vp/test'
6
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
6
7
 
7
8
  import { headlessWebAuthn } from '../../test/adapters.js'
8
9
  import { accounts, chain, getClient } from '../../test/config.js'
9
10
  import { type Server, createServer } from '../../test/utils.js'
11
+ import * as Expiry from './Expiry.js'
10
12
  import * as Provider from './Provider.js'
11
13
 
12
14
  const client = getClient()
@@ -40,6 +42,8 @@ beforeAll(async () => {
40
42
 
41
43
  afterAll(() => server?.closeAsync())
42
44
 
45
+ afterEach(() => Fetch.restore())
46
+
43
47
  describe('mppx integration', () => {
44
48
  test('polyfilled fetch handles 402 charge automatically', async () => {
45
49
  const provider = Provider.create({
@@ -49,15 +53,7 @@ describe('mppx integration', () => {
49
53
  })
50
54
 
51
55
  const address = await connect(provider)
52
-
53
- const client = getClient()
54
- await Actions.token.transferSync(client, {
55
- account: accounts[0]!,
56
- feeToken: Addresses.pathUsd,
57
- to: address,
58
- token: Addresses.pathUsd,
59
- amount: parseUnits('10', 6),
60
- })
56
+ await fund(address)
61
57
 
62
58
  const res = await fetch(`${server.url}/fortune`)
63
59
  expect(res.status).toBe(200)
@@ -69,6 +65,71 @@ describe('mppx integration', () => {
69
65
  }
70
66
  `)
71
67
  })
68
+
69
+ test('pull mode publishes a pending access key on first charge', async () => {
70
+ const provider = Provider.create({
71
+ adapter: headlessWebAuthn(),
72
+ chains: [chain],
73
+ mpp: { mode: 'pull' },
74
+ })
75
+ const address = await connect(provider)
76
+ await fund(address)
77
+
78
+ await provider.request({
79
+ method: 'wallet_authorizeAccessKey',
80
+ params: [{ expiry: Expiry.days(1) }],
81
+ })
82
+
83
+ const key = provider.store.getState().accessKeys[0]!
84
+ expect(key.keyAuthorization).toBeDefined()
85
+
86
+ const res = await fetch(`${server.url}/fortune`)
87
+ expect(res.status).toBe(200)
88
+ expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeUndefined()
89
+
90
+ const metadata = await Actions.accessKey.getMetadata(client, {
91
+ account: address,
92
+ accessKey: key.address,
93
+ })
94
+ expect(metadata.isRevoked).toMatchInlineSnapshot(`false`)
95
+ })
96
+
97
+ test('pull mode keeps pending access key after failed verification', async () => {
98
+ const failingServer = await createServer(async (req, res) => {
99
+ if (req.headers.authorization) {
100
+ res.writeHead(402, { 'Content-Type': 'application/json' })
101
+ res.end(JSON.stringify({ title: 'Verification Failed' }))
102
+ return
103
+ }
104
+
105
+ await ServerMppx.toNodeListener(
106
+ payment.charge({
107
+ amount: '1',
108
+ }),
109
+ )(req, res)
110
+ })
111
+
112
+ try {
113
+ const provider = Provider.create({
114
+ adapter: headlessWebAuthn(),
115
+ chains: [chain],
116
+ mpp: { mode: 'pull' },
117
+ })
118
+ const address = await connect(provider)
119
+ await fund(address)
120
+
121
+ await provider.request({
122
+ method: 'wallet_authorizeAccessKey',
123
+ params: [{ expiry: Expiry.days(1) }],
124
+ })
125
+
126
+ const res = await fetch(`${failingServer.url}/fortune`)
127
+ expect(res.status).toMatchInlineSnapshot(`402`)
128
+ expect(provider.store.getState().accessKeys[0]!.keyAuthorization).toBeDefined()
129
+ } finally {
130
+ await failingServer.closeAsync()
131
+ }
132
+ })
72
133
  })
73
134
 
74
135
  async function connect(provider: ReturnType<typeof Provider.create>) {
@@ -80,3 +141,13 @@ async function connect(provider: ReturnType<typeof Provider.create>) {
80
141
  })
81
142
  return register.accounts[0]!.address
82
143
  }
144
+
145
+ async function fund(address: `0x${string}`) {
146
+ await Actions.token.transferSync(client, {
147
+ account: accounts[0]!,
148
+ feeToken: Addresses.pathUsd,
149
+ to: address,
150
+ token: Addresses.pathUsd,
151
+ amount: parseUnits('10', 6),
152
+ })
153
+ }
@@ -393,19 +393,21 @@ export namespace wallet_authorizeAccessKey_strict {
393
393
  expiry: z.number(),
394
394
  keyType: z.optional(keyType),
395
395
  limits: z.readonly(
396
- z.array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) })),
396
+ z
397
+ .array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) }))
398
+ .check(z.minLength(1)),
397
399
  ),
398
400
  publicKey: z.optional(u.hex()),
399
- scopes: z.optional(
400
- z.readonly(
401
- z.array(
401
+ scopes: z.readonly(
402
+ z
403
+ .array(
402
404
  z.object({
403
405
  address: u.address(),
404
406
  selector: z.optional(z.union([u.hex(), z.string()])),
405
407
  recipients: z.optional(z.readonly(z.array(u.address()))),
406
408
  }),
407
- ),
408
- ),
409
+ )
410
+ .check(z.minLength(1)),
409
411
  ),
410
412
  })
411
413
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * as AccessKey from './core/AccessKey.js'
1
2
  export * as Adapter from './core/Adapter.js'
2
3
  export * as IntersectionObserver from './core/IntersectionObserver.js'
3
4
  export * as Dialog from './core/Dialog.js'
@@ -205,9 +205,7 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
205
205
  keyAuthorization = await reauthorizeManagedKey(rootAddress, managedKey)
206
206
 
207
207
  try {
208
- const result = await fn(account, keyAuthorization ?? undefined)
209
- AccessKey.removePending(account, { store })
210
- return result
208
+ return await fn(account, keyAuthorization ?? undefined)
211
209
  } catch (error) {
212
210
  AccessKey.remove(account, { store })
213
211
  throw error
@@ -348,10 +346,12 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
348
346
  }),
349
347
  )
350
348
  const signed = await account.signTransaction(prepared as never)
351
- return await client.request({
349
+ const result = await client.request({
352
350
  method: 'eth_sendRawTransaction' as never,
353
351
  params: [signed],
354
352
  })
353
+ AccessKey.removePending(account, { store })
354
+ return result
355
355
  },
356
356
  async sendTransactionSync(parameters) {
357
357
  const { feePayer, ...rest } = parameters
@@ -369,10 +369,12 @@ export function reactNative(options: reactNative.Options): Adapter.Adapter {
369
369
  }),
370
370
  )
371
371
  const signed = await account.signTransaction(prepared as never)
372
- return await client.request({
372
+ const result = await client.request({
373
373
  method: 'eth_sendRawTransactionSync' as never,
374
374
  params: [signed],
375
375
  })
376
+ AccessKey.removePending(account, { store })
377
+ return result
376
378
  },
377
379
  async signPersonalMessage({ address, data }) {
378
380
  await loadManagedKey(address)
@@ -1426,6 +1426,30 @@ describe('behavior: path A — guaranteed sponsorship (no validate)', () => {
1426
1426
  expect(result.transaction.feePayerSignature).toBeDefined()
1427
1427
  expect(result.capabilities?.sponsored).toBe(true)
1428
1428
  })
1429
+
1430
+ test('behavior: defaults feeToken to chain default when caller omits it', async () => {
1431
+ // Without the default, the broadcast envelope has no feeToken and
1432
+ // the chain falls back to the sender's account token, which often
1433
+ // lacks FeeAMM liquidity.
1434
+ const result = await fillTransaction(client, {
1435
+ account: userAccount.address,
1436
+ calls: [transferCall()],
1437
+ })
1438
+
1439
+ expect(result.transaction.feeToken?.toLowerCase()).toBe(
1440
+ localnetTokens[0]!.address.toLowerCase(),
1441
+ )
1442
+ })
1443
+
1444
+ test('behavior: preserves caller-supplied feeToken', async () => {
1445
+ const result = await fillTransaction(client, {
1446
+ account: userAccount.address,
1447
+ calls: [transferCall()],
1448
+ feeToken: addresses.alphaUsd as Address,
1449
+ })
1450
+
1451
+ expect(result.transaction.feeToken?.toLowerCase()).toBe(addresses.alphaUsd.toLowerCase())
1452
+ })
1429
1453
  })
1430
1454
 
1431
1455
  describe('behavior: path B — conditional sponsorship (validate)', () => {