accounts 0.7.0 → 0.7.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 (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/AccessKey.d.ts.map +1 -1
  3. package/dist/core/AccessKey.js +12 -26
  4. package/dist/core/AccessKey.js.map +1 -1
  5. package/dist/core/Account.d.ts +12 -0
  6. package/dist/core/Account.d.ts.map +1 -1
  7. package/dist/core/Account.js +24 -2
  8. package/dist/core/Account.js.map +1 -1
  9. package/dist/core/Adapter.d.ts +9 -0
  10. package/dist/core/Adapter.d.ts.map +1 -1
  11. package/dist/core/Provider.d.ts +1 -1
  12. package/dist/core/Provider.d.ts.map +1 -1
  13. package/dist/core/Provider.js +2 -2
  14. package/dist/core/Provider.js.map +1 -1
  15. package/dist/core/Schema.d.ts +54 -0
  16. package/dist/core/Schema.d.ts.map +1 -1
  17. package/dist/core/adapters/local.d.ts.map +1 -1
  18. package/dist/core/adapters/local.js +4 -2
  19. package/dist/core/adapters/local.js.map +1 -1
  20. package/dist/core/zod/rpc.d.ts +110 -0
  21. package/dist/core/zod/rpc.d.ts.map +1 -1
  22. package/dist/core/zod/rpc.js +16 -5
  23. package/dist/core/zod/rpc.js.map +1 -1
  24. package/dist/react-native/adapter.d.ts.map +1 -1
  25. package/dist/react-native/adapter.js +76 -21
  26. package/dist/react-native/adapter.js.map +1 -1
  27. package/dist/server/internal/handlers/codeAuth.d.ts +1 -1
  28. package/dist/server/internal/handlers/codeAuth.js +2 -2
  29. package/dist/server/internal/handlers/codeAuth.js.map +1 -1
  30. package/dist/server/internal/handlers/relay.d.ts +2 -1
  31. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  32. package/dist/server/internal/handlers/relay.js +8 -2
  33. package/dist/server/internal/handlers/relay.js.map +1 -1
  34. package/dist/wagmi/index.d.ts +38 -2
  35. package/dist/wagmi/index.d.ts.map +1 -1
  36. package/dist/wagmi/index.js +35 -2
  37. package/dist/wagmi/index.js.map +1 -1
  38. package/package.json +11 -7
  39. package/src/core/AccessKey.ts +13 -30
  40. package/src/core/Account.test.ts +145 -0
  41. package/src/core/Account.ts +34 -3
  42. package/src/core/Adapter.ts +11 -1
  43. package/src/core/Provider.test.ts +27 -19
  44. package/src/core/Provider.ts +3 -3
  45. package/src/core/adapters/local.ts +4 -2
  46. package/src/core/zod/rpc.ts +39 -4
  47. package/src/react-native/Provider.test.ts +115 -0
  48. package/src/react-native/adapter.ts +96 -22
  49. package/src/server/internal/handlers/codeAuth.ts +3 -3
  50. package/src/server/internal/handlers/relay.ts +9 -3
  51. package/src/wagmi/index.ts +38 -2
  52. package/dist/wagmi/Connector.d.ts +0 -130
  53. package/dist/wagmi/Connector.d.ts.map +0 -1
  54. package/dist/wagmi/Connector.js +0 -272
  55. package/dist/wagmi/Connector.js.map +0 -1
  56. package/src/wagmi/Connector.ts +0 -330
@@ -306,4 +306,149 @@ describe('find', () => {
306
306
  `[Provider.DisconnectedError: No active account.]`,
307
307
  )
308
308
  })
309
+
310
+ test('behavior: unscoped access key is used regardless of calls', async () => {
311
+ const keyPair = await WebCryptoP256.createKeyPair()
312
+ const store = setup(
313
+ [{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
314
+ [
315
+ {
316
+ address: '0x0000000000000000000000000000000000000099',
317
+ access: accounts[0].address,
318
+ keyType: 'webCrypto',
319
+ keyPair,
320
+ },
321
+ ],
322
+ )
323
+
324
+ const result = Account.find({
325
+ signable: true,
326
+ store,
327
+ calls: [{ to: '0x0000000000000000000000000000000000000abc', data: '0xa9059cbb' }],
328
+ })
329
+
330
+ expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
331
+ })
332
+
333
+ test('behavior: scoped access key is used when calls match', async () => {
334
+ const keyPair = await WebCryptoP256.createKeyPair()
335
+ const token = '0x0000000000000000000000000000000000000abc' as const
336
+ const store = setup(
337
+ [{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
338
+ [
339
+ {
340
+ address: '0x0000000000000000000000000000000000000099',
341
+ access: accounts[0].address,
342
+ keyType: 'webCrypto',
343
+ keyPair,
344
+ scopes: [{ address: token, selector: '0xa9059cbb' }],
345
+ },
346
+ ],
347
+ )
348
+
349
+ const result = Account.find({
350
+ signable: true,
351
+ store,
352
+ calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
353
+ })
354
+
355
+ expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
356
+ })
357
+
358
+ test('behavior: scoped access key falls back to root when calls do not match', async () => {
359
+ const keyPair = await WebCryptoP256.createKeyPair()
360
+ const token = '0x0000000000000000000000000000000000000abc' as const
361
+ const store = setup(
362
+ [{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
363
+ [
364
+ {
365
+ address: '0x0000000000000000000000000000000000000099',
366
+ access: accounts[0].address,
367
+ keyType: 'webCrypto',
368
+ keyPair,
369
+ scopes: [{ address: token, selector: '0xa9059cbb' }],
370
+ },
371
+ ],
372
+ )
373
+
374
+ const result = Account.find({
375
+ signable: true,
376
+ store,
377
+ calls: [{ to: '0x0000000000000000000000000000000000000def', data: '0xdeadbeef' }],
378
+ })
379
+
380
+ expect(result.address).toMatchInlineSnapshot(`"${accounts[0].address}"`)
381
+ expect(result.source).not.toBe('accessKey')
382
+ })
383
+
384
+ test('behavior: scoped access key with human-readable selector matches', async () => {
385
+ const keyPair = await WebCryptoP256.createKeyPair()
386
+ const token = '0x0000000000000000000000000000000000000abc' as const
387
+ const store = setup(
388
+ [{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
389
+ [
390
+ {
391
+ address: '0x0000000000000000000000000000000000000099',
392
+ access: accounts[0].address,
393
+ keyType: 'webCrypto',
394
+ keyPair,
395
+ scopes: [{ address: token, selector: 'transfer(address,uint256)' }],
396
+ },
397
+ ],
398
+ )
399
+
400
+ // 0xa9059cbb is the selector for transfer(address,uint256)
401
+ const result = Account.find({
402
+ signable: true,
403
+ store,
404
+ calls: [{ to: token, data: '0xa9059cbb0000000000000000000000000000000000000001' }],
405
+ })
406
+
407
+ expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
408
+ })
409
+
410
+ test('behavior: scoped access key without selector allows any call to that address', async () => {
411
+ const keyPair = await WebCryptoP256.createKeyPair()
412
+ const token = '0x0000000000000000000000000000000000000abc' as const
413
+ const store = setup(
414
+ [{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
415
+ [
416
+ {
417
+ address: '0x0000000000000000000000000000000000000099',
418
+ access: accounts[0].address,
419
+ keyType: 'webCrypto',
420
+ keyPair,
421
+ scopes: [{ address: token }],
422
+ },
423
+ ],
424
+ )
425
+
426
+ const result = Account.find({
427
+ signable: true,
428
+ store,
429
+ calls: [{ to: token, data: '0xdeadbeef' }],
430
+ })
431
+
432
+ expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
433
+ })
434
+
435
+ test('behavior: scoped access key used when no calls provided', async () => {
436
+ const keyPair = await WebCryptoP256.createKeyPair()
437
+ const store = setup(
438
+ [{ address: accounts[0].address, keyType: 'secp256k1', privateKey: privateKeys[0] }],
439
+ [
440
+ {
441
+ address: '0x0000000000000000000000000000000000000099',
442
+ access: accounts[0].address,
443
+ keyType: 'webCrypto',
444
+ keyPair,
445
+ scopes: [{ address: '0x0000000000000000000000000000000000000abc' }],
446
+ },
447
+ ],
448
+ )
449
+
450
+ const result = Account.find({ signable: true, store })
451
+
452
+ expect(result.source).toMatchInlineSnapshot(`"accessKey"`)
453
+ })
309
454
  })
@@ -1,4 +1,4 @@
1
- import { Provider, type WebCryptoP256 } from 'ox'
1
+ import { AbiFunction, Provider, type WebCryptoP256 } from 'ox'
2
2
  import { type KeyAuthorization } from 'ox/tempo'
3
3
  import type { Hex } from 'viem'
4
4
  import type { Address, JsonRpcAccount } from 'viem/accounts'
@@ -42,7 +42,15 @@ export type AccessKey = {
42
42
  /** Key type. */
43
43
  keyType: 'secp256k1' | 'p256' | 'webAuthn' | 'webCrypto'
44
44
  /** TIP-20 spending limits for the access key. */
45
- limits?: { token: Address; limit: bigint }[] | undefined
45
+ limits?: { token: Address; limit: bigint; period?: number | undefined }[] | undefined
46
+ /** Call scopes restricting which contracts/selectors this key can call. */
47
+ scopes?:
48
+ | {
49
+ address: Address
50
+ selector?: Hex | string | undefined
51
+ recipients?: readonly Address[] | undefined
52
+ }[]
53
+ | undefined
46
54
  } & OneOf<
47
55
  | {}
48
56
  | {
@@ -82,7 +90,8 @@ export function find(options: find.Options): TempoAccount.Account | JsonRpcAccou
82
90
  // Remove expired access keys.
83
91
  if (key.expiry && key.expiry < Date.now() / 1000)
84
92
  store.setState({ accessKeys: accessKeys.filter((a) => a !== key) })
85
- else return hydrateAccessKey(key) as never
93
+ // Use access key if unscoped or scopes cover the requested calls; otherwise fall through to root.
94
+ else if (scopesMatch(key, options)) return hydrateAccessKey(key) as never
86
95
  }
87
96
  }
88
97
 
@@ -95,6 +104,8 @@ export declare namespace find {
95
104
  accessKey?: boolean | undefined
96
105
  /** Address to resolve. Defaults to the active account. */
97
106
  address?: Address | undefined
107
+ /** Calls to match against access key scopes. When provided, access keys whose scopes don't cover these calls are skipped. */
108
+ calls?: readonly { to?: Address | undefined; data?: Hex | undefined }[] | undefined
98
109
  /** Whether to hydrate signing capability. @default false */
99
110
  signable?: boolean | undefined
100
111
  /** Reactive state store. */
@@ -168,3 +179,23 @@ export declare namespace hydrate {
168
179
  signable?: boolean | undefined
169
180
  }
170
181
  }
182
+
183
+ /** Returns true if the access key's scopes cover the requested calls (or key is unscoped). */
184
+ function scopesMatch(key: AccessKey, options: find.Options): boolean {
185
+ if (!options.calls || !key.scopes) return true
186
+ return options.calls!.every((call) => {
187
+ if (!call.to) return false
188
+ const callTo = call.to.toLowerCase()
189
+ const callSelector = call.data?.slice(0, 10).toLowerCase()
190
+ return key.scopes!.some((scope) => {
191
+ if (scope.address.toLowerCase() !== callTo) return false
192
+ if (!scope.selector) return true
193
+ const scopeSelector = (
194
+ scope.selector.startsWith('0x') && scope.selector.length === 10
195
+ ? scope.selector
196
+ : AbiFunction.getSelector(scope.selector)
197
+ ).toLowerCase()
198
+ return callSelector === scopeSelector
199
+ })
200
+ })
201
+ }
@@ -206,14 +206,24 @@ export declare namespace authorizeAccessKey {
206
206
  type Parameters = {
207
207
  /** Access key address. Alternative to `publicKey` when the caller already knows the derived address. */
208
208
  address?: Address | undefined
209
+ /** Chain ID the key authorization is scoped to. Defaults to the active chain. */
210
+ chainId?: number | undefined
209
211
  /** Unix timestamp (seconds) when the key expires. */
210
212
  expiry: number
211
213
  /** Key type of the external public key. Required when `publicKey` or `address` is provided. */
212
214
  keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
213
215
  /** TIP-20 spending limits for this key. */
214
- limits?: readonly { token: Address; limit: bigint }[] | undefined
216
+ limits?: readonly { token: Address; limit: bigint; period?: number | undefined }[] | undefined
215
217
  /** External public key to authorize. When provided, no key pair is generated — the caller holds the signing material. */
216
218
  publicKey?: Hex | undefined
219
+ /** Call scopes restricting which contracts/selectors this key can call. */
220
+ scopes?:
221
+ | readonly {
222
+ address: Address
223
+ selector?: Hex | string | undefined
224
+ recipients?: readonly Address[] | undefined
225
+ }[]
226
+ | undefined
217
227
  /** Pre-computed signature over the key authorization digest (skips a second signing ceremony). */
218
228
  signature?: Hex | undefined
219
229
  }
@@ -648,24 +648,32 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
648
648
 
649
649
  const result = await provider.request({ method: 'wallet_getCapabilities' })
650
650
  expect(result).toMatchInlineSnapshot(`
651
- {
652
- "0x1079": {
653
- "accessKeys": {
654
- "status": "supported",
655
- },
656
- "atomic": {
657
- "status": "supported",
658
- },
659
- },
660
- "0xa5bf": {
661
- "accessKeys": {
662
- "status": "supported",
663
- },
664
- "atomic": {
665
- "status": "supported",
666
- },
667
- },
668
- }
651
+ {
652
+ "0x1079": {
653
+ "accessKeys": {
654
+ "status": "supported",
655
+ },
656
+ "atomic": {
657
+ "status": "supported",
658
+ },
659
+ },
660
+ "0x7a56": {
661
+ "accessKeys": {
662
+ "status": "supported",
663
+ },
664
+ "atomic": {
665
+ "status": "supported",
666
+ },
667
+ },
668
+ "0xa5bf": {
669
+ "accessKeys": {
670
+ "status": "supported",
671
+ },
672
+ "atomic": {
673
+ "status": "supported",
674
+ },
675
+ },
676
+ }
669
677
  `)
670
678
  })
671
679
 
@@ -724,7 +732,7 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
724
732
  method: 'wallet_getCapabilities',
725
733
  params: [connected],
726
734
  })
727
- expect(Object.keys(result).length).toMatchInlineSnapshot(`2`)
735
+ expect(Object.keys(result).length).toMatchInlineSnapshot(`3`)
728
736
  expect(result[Hex.fromNumber(tempo.id)]!.atomic.status).toMatchInlineSnapshot(`"supported"`)
729
737
  })
730
738
 
@@ -3,7 +3,7 @@ import { Mppx, tempo as mppx_tempo } from 'mppx/client'
3
3
  import { Hash, Hex, Json, Provider as ox_Provider, RpcResponse } from 'ox'
4
4
  import { KeyAuthorization } from 'ox/tempo'
5
5
  import type { Chain, Client as ViemClient, Transport } from 'viem'
6
- import { tempo, tempoModerato } from 'viem/chains'
6
+ import { tempo, tempoDevnet, tempoModerato } from 'viem/chains'
7
7
  import { Actions } from 'viem/tempo'
8
8
  import * as z from 'zod/mini'
9
9
 
@@ -49,7 +49,7 @@ const announced = new Set<string>()
49
49
  export function create(options: create.Options = {}): create.ReturnType {
50
50
  const {
51
51
  adapter = dialog(),
52
- chains = [tempo, tempoModerato],
52
+ chains = [tempo, tempoModerato, tempoDevnet],
53
53
  persistCredentials,
54
54
  testnet,
55
55
  storage = typeof window !== 'undefined' ? Storage.idb() : Storage.memory(),
@@ -681,7 +681,7 @@ export declare namespace create {
681
681
  authorizeAccessKey?: (() => Adapter.authorizeAccessKey.Parameters) | undefined
682
682
  /**
683
683
  * Supported chains. First chain is the default.
684
- * @default [tempo, tempoModerato]
684
+ * @default [tempo, tempoModerato, tempoDevnet]
685
685
  */
686
686
  chains?: readonly [Chain, ...Chain[]] | undefined
687
687
  /** Fee payer configuration. @see {@link Client.fromChainId.Options.feePayer} */
@@ -34,8 +34,8 @@ export function local(options: local.Options): Adapter.Adapter {
34
34
  * For local keys: generates a P256 key pair via `AccessKey.generate`.
35
35
  */
36
36
  async function prepareKeyAuthorization(options: Adapter.authorizeAccessKey.Parameters) {
37
- const { expiry, limits } = options
38
- const chainId = getClient().chain.id
37
+ const { expiry, limits, scopes } = options
38
+ const chainId = options.chainId ?? getClient().chain.id
39
39
 
40
40
  if (options.publicKey || options.address) {
41
41
  const accessKeyAddress =
@@ -46,6 +46,7 @@ export function local(options: local.Options): Adapter.Adapter {
46
46
  chainId: BigInt(chainId),
47
47
  expiry,
48
48
  limits,
49
+ scopes,
49
50
  type: keyType,
50
51
  })
51
52
  return { keyAuthorization }
@@ -58,6 +59,7 @@ export function local(options: local.Options): Adapter.Adapter {
58
59
  chainId: BigInt(chainId),
59
60
  expiry,
60
61
  limits,
62
+ scopes,
61
63
  type: 'p256',
62
64
  })
63
65
  return { keyAuthorization, keyPair }
@@ -51,7 +51,11 @@ export const keyAuthorization = z.object({
51
51
  expiry: z.union([u.number(), z.null(), z.undefined()]),
52
52
  keyId: u.address(),
53
53
  keyType,
54
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
54
+ limits: z.optional(
55
+ z.readonly(
56
+ z.array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(u.number()) })),
57
+ ),
58
+ ),
55
59
  signature: signatureEnvelope,
56
60
  })
57
61
 
@@ -341,13 +345,31 @@ export namespace wallet_getCapabilities {
341
345
  export namespace wallet_authorizeAccessKey {
342
346
  export const parameters = z.object({
343
347
  address: z.optional(u.address()),
348
+ chainId: z.optional(u.number()),
344
349
  expiry: z.number(),
345
350
  keyType: z.optional(keyType),
346
- limits: z.optional(z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() })))),
351
+ limits: z.optional(
352
+ z.readonly(
353
+ z.array(
354
+ z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) }),
355
+ ),
356
+ ),
357
+ ),
347
358
  publicKey: z.optional(u.hex()),
359
+ scopes: z.optional(
360
+ z.readonly(
361
+ z.array(
362
+ z.object({
363
+ address: u.address(),
364
+ selector: z.optional(z.union([u.hex(), z.string()])),
365
+ recipients: z.optional(z.readonly(z.array(u.address()))),
366
+ }),
367
+ ),
368
+ ),
369
+ ),
348
370
  })
349
371
 
350
- const returns = z.object({
372
+ export const returns = z.object({
351
373
  keyAuthorization,
352
374
  rootAddress: u.address(),
353
375
  })
@@ -366,8 +388,21 @@ export namespace wallet_authorizeAccessKey_strict {
366
388
  address: z.optional(u.address()),
367
389
  expiry: z.number(),
368
390
  keyType: z.optional(keyType),
369
- limits: z.readonly(z.array(z.object({ token: u.address(), limit: u.bigint() }))),
391
+ limits: z.readonly(
392
+ z.array(z.object({ token: u.address(), limit: u.bigint(), period: z.optional(z.number()) })),
393
+ ),
370
394
  publicKey: z.optional(u.hex()),
395
+ scopes: z.optional(
396
+ z.readonly(
397
+ z.array(
398
+ z.object({
399
+ address: u.address(),
400
+ selector: z.optional(z.union([u.hex(), z.string()])),
401
+ recipients: z.optional(z.readonly(z.array(u.address()))),
402
+ }),
403
+ ),
404
+ ),
405
+ ),
371
406
  })
372
407
  }
373
408
 
@@ -0,0 +1,115 @@
1
+ import { Address, Hex, PublicKey } from 'ox'
2
+ import { KeyAuthorization } from 'ox/tempo'
3
+ import { parseUnits, type Address as viem_Address } from 'viem'
4
+ import { Actions, Addresses } from 'viem/tempo'
5
+ import { describe, expect, test } from 'vp/test'
6
+
7
+ import { accounts, chain, getClient } from '../../test/config.js'
8
+ import * as Storage from '../core/Storage.js'
9
+ import * as Provider from './Provider.js'
10
+
11
+ const root = accounts[0]!
12
+ const transferCall = Actions.token.transfer.call({
13
+ to: '0x0000000000000000000000000000000000000001',
14
+ token: Addresses.pathUsd,
15
+ amount: parseUnits('1', 6),
16
+ })
17
+
18
+ async function fund(address: viem_Address) {
19
+ await Actions.token.transferSync(getClient(), {
20
+ account: root,
21
+ feeToken: Addresses.pathUsd,
22
+ to: address,
23
+ token: Addresses.pathUsd,
24
+ amount: parseUnits('10', 6),
25
+ })
26
+ }
27
+
28
+ function createOpen(options: { mismatchFirstCall?: boolean | undefined } = {}) {
29
+ let calls = 0
30
+
31
+ return {
32
+ calls: () => calls,
33
+ open: async (url: string) => {
34
+ calls += 1
35
+
36
+ const authUrl = new URL(url)
37
+ const callback = authUrl.searchParams.get('callback')
38
+ const chainId = authUrl.searchParams.get('chainId')
39
+ const pubKey = authUrl.searchParams.get('pubKey')
40
+ const state = authUrl.searchParams.get('state')
41
+
42
+ if (!callback || !chainId || !pubKey || !state)
43
+ throw new Error('Expected callback, chainId, pubKey, and state in auth URL.')
44
+
45
+ const limits = authUrl.searchParams.get('limits')
46
+ const keyType = authUrl.searchParams.get('keyType')
47
+ if (keyType !== 'p256' && keyType !== 'secp256k1')
48
+ throw new Error('Expected a managed key type in auth URL.')
49
+
50
+ const keyAuthorization = await root.signKeyAuthorization(
51
+ {
52
+ accessKeyAddress:
53
+ options.mismatchFirstCall && calls === 1
54
+ ? accounts[1]!.address
55
+ : Address.fromPublicKey(PublicKey.fromHex(pubKey as Hex.Hex)),
56
+ keyType,
57
+ },
58
+ {
59
+ chainId: BigInt(chainId),
60
+ ...(authUrl.searchParams.get('expiry')
61
+ ? { expiry: Number(authUrl.searchParams.get('expiry')) }
62
+ : {}),
63
+ ...(limits
64
+ ? {
65
+ limits: (JSON.parse(limits) as { token: `0x${string}`; limit: string }[]).map(
66
+ (x) => ({
67
+ limit: BigInt(x.limit),
68
+ token: x.token,
69
+ }),
70
+ ),
71
+ }
72
+ : {}),
73
+ },
74
+ )
75
+
76
+ const callbackUrl = new URL(callback)
77
+ callbackUrl.searchParams.set('accountAddress', root.address)
78
+ callbackUrl.searchParams.set('keyAuthorization', KeyAuthorization.serialize(keyAuthorization))
79
+ callbackUrl.searchParams.set('state', state)
80
+ return callbackUrl.toString()
81
+ },
82
+ }
83
+ }
84
+
85
+ describe('create', () => {
86
+ test('behavior: reauthorizes the managed key when the saved authorization targets the wrong key', async () => {
87
+ const secureStorage = Storage.memory()
88
+ const browser = createOpen({ mismatchFirstCall: true })
89
+ const provider = Provider.create({
90
+ authorizeAccessKey: () => ({
91
+ expiry: Math.floor(Date.now() / 1000) + 3600,
92
+ }),
93
+ chains: [chain],
94
+ host: 'https://wallet.tempo.xyz',
95
+ open: browser.open,
96
+ redirectUri: 'accounts-playground://auth',
97
+ secureStorage,
98
+ })
99
+
100
+ const result = await provider.request({
101
+ method: 'wallet_connect',
102
+ params: [{ capabilities: { method: 'register', name: 'Accounts RN Test' } }],
103
+ })
104
+ expect(result.accounts[0]!.address).toBe(root.address)
105
+
106
+ await fund(root.address)
107
+
108
+ const receipt = await provider.request({
109
+ method: 'eth_sendTransactionSync',
110
+ params: [{ calls: [transferCall], feeToken: Addresses.pathUsd }],
111
+ })
112
+ expect(receipt.status).toMatchInlineSnapshot(`"0x1"`)
113
+ expect(browser.calls()).toMatchInlineSnapshot(`2`)
114
+ })
115
+ })