accounts 0.18.4 → 0.18.5

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.
@@ -1,5 +1,5 @@
1
1
  import { Address as core_Address, Hex, Secp256k1, Signature } from 'ox'
2
- import { decodeFunctionData } from 'viem'
2
+ import { BaseError, decodeFunctionData } from 'viem'
3
3
  import type { Address } from 'viem/accounts'
4
4
  import { Abis } from 'viem/tempo'
5
5
  import { describe, expect, test } from 'vp/test'
@@ -323,6 +323,140 @@ describe('privy', () => {
323
323
  expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
324
324
  })
325
325
 
326
+ test('behavior: revokeAccessKey forwards a fee payer URL', async () => {
327
+ const { adapter, client, store } = setup()
328
+ store.setState({ accounts: [{ address }], activeAccount: 0 })
329
+
330
+ await adapter.actions.revokeAccessKey!(
331
+ { accessKeyAddress: other, address, feePayer: 'https://example.com/fee-payer' },
332
+ {
333
+ method: 'wallet_revokeAccessKey',
334
+ params: [{ accessKeyAddress: other, address, feePayer: 'https://example.com/fee-payer' }],
335
+ },
336
+ )
337
+
338
+ expect({
339
+ clientOptions: client.clientOptions,
340
+ transactions: client.transactions,
341
+ }).toMatchInlineSnapshot(`
342
+ {
343
+ "clientOptions": [
344
+ {
345
+ "feePayer": "https://example.com/fee-payer",
346
+ },
347
+ ],
348
+ "transactions": [
349
+ {
350
+ "account": {
351
+ "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
352
+ "sign": [Function],
353
+ "signTransaction": [Function],
354
+ "source": "privy",
355
+ "type": "local",
356
+ },
357
+ "data": "0x5ae7ab320000000000000000000000002b5ad5c4795c026514f8317c7a215e218dccd6cf",
358
+ "feePayer": true,
359
+ "to": "0xaAAAaaAA00000000000000000000000000000000",
360
+ },
361
+ ],
362
+ }
363
+ `)
364
+ })
365
+
366
+ test('error: revokeAccessKey preserves local state on external fee payer errors', async () => {
367
+ const transactionError = new BaseError('revoke failed', {
368
+ cause: Object.assign(new Error('execution reverted'), {
369
+ data: { errorName: 'KeyAlreadyRevoked' },
370
+ }),
371
+ })
372
+ const { adapter, store } = setup({ transactionError })
373
+ store.setState({
374
+ accounts: [{ address }],
375
+ activeAccount: 0,
376
+ accessKeys: [
377
+ {
378
+ access: address,
379
+ address: other,
380
+ chainId: 1,
381
+ keyType: 'secp256k1',
382
+ } as never,
383
+ ],
384
+ })
385
+
386
+ await expect(
387
+ adapter.actions.revokeAccessKey!(
388
+ { accessKeyAddress: other, address, feePayer: 'https://example.com/fee-payer' },
389
+ {
390
+ method: 'wallet_revokeAccessKey',
391
+ params: [{ accessKeyAddress: other, address, feePayer: 'https://example.com/fee-payer' }],
392
+ },
393
+ ),
394
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
395
+ [ContractFunctionExecutionError: revoke failed
396
+
397
+ Contract Call:
398
+ address: 0xaAAAaaAA00000000000000000000000000000000
399
+ function: revokeKey(address keyId)
400
+ args: (0x2b5ad5c4795c026514f8317c7a215e218dccd6cf)
401
+ sender: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
402
+
403
+ Docs: https://viem.sh/docs/contract/writeContract
404
+ Details: execution reverted
405
+ Version: viem@2.56.0]
406
+ `)
407
+
408
+ expect(store.getState().accessKeys).toMatchInlineSnapshot(`
409
+ [
410
+ {
411
+ "access": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
412
+ "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
413
+ "chainId": 1,
414
+ "keyType": "secp256k1",
415
+ },
416
+ ]
417
+ `)
418
+ })
419
+
420
+ test('behavior: revokeAccessKey forwards default fee sponsorship', async () => {
421
+ const { adapter, client, store } = setup()
422
+ store.setState({ accounts: [{ address }], activeAccount: 0 })
423
+
424
+ await adapter.actions.revokeAccessKey!(
425
+ { accessKeyAddress: other, address, feePayer: true },
426
+ {
427
+ method: 'wallet_revokeAccessKey',
428
+ params: [{ accessKeyAddress: other, address, feePayer: true }],
429
+ },
430
+ )
431
+
432
+ expect({
433
+ clientOptions: client.clientOptions,
434
+ transactions: client.transactions,
435
+ }).toMatchInlineSnapshot(`
436
+ {
437
+ "clientOptions": [
438
+ {
439
+ "feePayer": undefined,
440
+ },
441
+ ],
442
+ "transactions": [
443
+ {
444
+ "account": {
445
+ "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
446
+ "sign": [Function],
447
+ "signTransaction": [Function],
448
+ "source": "privy",
449
+ "type": "local",
450
+ },
451
+ "data": "0x5ae7ab320000000000000000000000002b5ad5c4795c026514f8317c7a215e218dccd6cf",
452
+ "feePayer": true,
453
+ "to": "0xaAAAaaAA00000000000000000000000000000000",
454
+ },
455
+ ],
456
+ }
457
+ `)
458
+ })
459
+
326
460
  test('behavior: signing silently restores wallet accounts via the Privy SDK', async () => {
327
461
  const { adapter, client, store } = setup()
328
462
  store.setState({ accounts: [{ address }], activeAccount: 0 })
@@ -642,13 +776,17 @@ function setup(options: setup.Options = {}) {
642
776
  getAccount: (() => {
643
777
  throw new Error('not implemented')
644
778
  }) as never,
645
- getClient: (() => ({
646
- chain: { id: 1 },
647
- sendTransaction: async (parameters: unknown) => {
648
- client.transactions.push(parameters)
649
- return Hex.padLeft('0x1', 32)
650
- },
651
- })) as never,
779
+ getClient: ((clientOptions: unknown) => {
780
+ client.clientOptions.push(clientOptions)
781
+ return {
782
+ chain: { id: 1 },
783
+ sendTransaction: async (parameters: unknown) => {
784
+ client.transactions.push(parameters)
785
+ if (options.transactionError) throw options.transactionError
786
+ return Hex.padLeft('0x1', 32)
787
+ },
788
+ }
789
+ }) as never,
652
790
  storage,
653
791
  store,
654
792
  })
@@ -675,6 +813,8 @@ declare namespace setup {
675
813
  restoreError?: unknown
676
814
  token?: string | null | undefined
677
815
  signError?: unknown
816
+ /** Make the mock wallet client transaction fail. */
817
+ transactionError?: unknown
678
818
  /** Override the value returned by the embedded provider's `secp256k1_sign`. */
679
819
  signResult?: unknown
680
820
  /** Force the test wallet to sign with this private key (for wrong-signer tests). */
@@ -683,6 +823,7 @@ declare namespace setup {
683
823
  }
684
824
 
685
825
  type MockClient = privy.Client & {
826
+ clientOptions: unknown[]
686
827
  createCalls: number
687
828
  initCalls: number
688
829
  loadCalls: number
@@ -699,6 +840,7 @@ type MockClient = privy.Client & {
699
840
 
700
841
  function createClient(options: setup.Options = {}) {
701
842
  const client: MockClient = {
843
+ clientOptions: [] as unknown[],
702
844
  createCalls: 0,
703
845
  initCalls: 0,
704
846
  loadCalls: 0,
@@ -570,13 +570,21 @@ export function privy<const client extends privy.Client>(
570
570
  },
571
571
  async revokeAccessKey(parameters) {
572
572
  const account = await getSigningAccount(parameters.address)
573
+ const { feePayer } = parameters
573
574
  try {
574
- await Actions.accessKey.revoke(getClient(), {
575
- account: account as never,
576
- accessKey: parameters.accessKeyAddress,
577
- })
575
+ await Actions.accessKey.revoke(
576
+ getClient({ feePayer: feePayer === true ? undefined : feePayer }),
577
+ {
578
+ account: account as never,
579
+ accessKey: parameters.accessKeyAddress,
580
+ ...(feePayer ? { feePayer: true } : {}),
581
+ ...(parameters.keyAuthorization
582
+ ? { keyAuthorization: parameters.keyAuthorization }
583
+ : {}),
584
+ },
585
+ )
578
586
  } catch (error) {
579
- if (!AccessKey.isUnavailableError(error)) throw error
587
+ if (typeof feePayer === 'string' || !AccessKey.isUnavailableError(error)) throw error
580
588
  }
581
589
  store.accessKeys.remove({
582
590
  accessKey: parameters.accessKeyAddress,
@@ -69,6 +69,32 @@ describe('validate', () => {
69
69
  `)
70
70
  })
71
71
 
72
+ test('behavior: preserves access-key revocation sponsorship', () => {
73
+ const result = RpcRequest.validate(Schema.Request, {
74
+ method: 'wallet_revokeAccessKey',
75
+ params: [
76
+ {
77
+ accessKeyAddress: '0x2222222222222222222222222222222222222222',
78
+ address: '0x1111111111111111111111111111111111111111',
79
+ feePayer: 'https://app.example.com/fee-payer',
80
+ },
81
+ ],
82
+ })
83
+
84
+ expect(result._decoded).toMatchInlineSnapshot(`
85
+ {
86
+ "method": "wallet_revokeAccessKey",
87
+ "params": [
88
+ {
89
+ "accessKeyAddress": "0x2222222222222222222222222222222222222222",
90
+ "address": "0x1111111111111111111111111111111111111111",
91
+ "feePayer": "https://app.example.com/fee-payer",
92
+ },
93
+ ],
94
+ }
95
+ `)
96
+ })
97
+
72
98
  test('default: validates wallet_switchEthereumChain with hex chainId', () => {
73
99
  const result = RpcRequest.validate(Schema.Request, {
74
100
  method: 'wallet_switchEthereumChain',
@@ -540,11 +540,21 @@ export namespace wallet_authorizeAccessKey_strict {
540
540
  }
541
541
 
542
542
  export namespace wallet_revokeAccessKey {
543
+ /** Parameters for revoking an access key. */
544
+ export const parameters = z.object({
545
+ /** Root account address. */
546
+ address: u.address(),
547
+ /** Address of the access key to revoke. */
548
+ accessKeyAddress: u.address(),
549
+ /** App-provided fee sponsorship for the revocation transaction. */
550
+ feePayer: z.optional(z.union([z.boolean(), z.string()])),
551
+ /** Signed authorization for an access key that has not been published on-chain. */
552
+ keyAuthorization: z.optional(keyAuthorization),
553
+ })
554
+
543
555
  export const schema = Schema.defineItem({
544
556
  method: z.literal('wallet_revokeAccessKey'),
545
- params: z.readonly(
546
- z.tuple([z.object({ address: u.address(), accessKeyAddress: u.address() })]),
547
- ),
557
+ params: z.readonly(z.tuple([parameters])),
548
558
  returns: undefined,
549
559
  })
550
560
  export type Encoded = Schema.Encoded<typeof schema>
@@ -0,0 +1,153 @@
1
+ import { vi } from 'vitest'
2
+ import { afterEach, describe, expect, test } from 'vp/test'
3
+
4
+ const hooks = vi.hoisted(() => ({ cleanups: [] as (() => void)[] }))
5
+
6
+ vi.mock('react', () => ({
7
+ useCallback: (fn: unknown) => fn,
8
+ useEffect: (effect: () => void | (() => void)) => {
9
+ const cleanup = effect()
10
+ if (cleanup) hooks.cleanups.push(cleanup)
11
+ },
12
+ useMemo: (fn: () => unknown) => fn(),
13
+ useRef: (current: unknown) => ({ current }),
14
+ useState: (initial: unknown) => [initial, () => {}],
15
+ }))
16
+
17
+ import { useTheme } from './Remote.js'
18
+
19
+ afterEach(() => {
20
+ hooks.cleanups.length = 0
21
+ vi.unstubAllGlobals()
22
+ })
23
+
24
+ describe('useTheme', () => {
25
+ test('behavior: applies URL theme values to the document', () => {
26
+ const root = createRoot()
27
+ stubBrowser(root, '?accent=%23b9a3ff&radius=medium&scheme=dark')
28
+
29
+ useTheme()
30
+
31
+ expect(readRoot(root)).toMatchInlineSnapshot(`
32
+ {
33
+ "attributes": {
34
+ "data-theme": "dark",
35
+ "data-theme-accent": "custom",
36
+ "data-theme-radius": "medium",
37
+ },
38
+ "colorScheme": "dark",
39
+ "properties": {
40
+ "--theme-accent": "#b9a3ff",
41
+ },
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('behavior: replaces URL overrides with live theme updates', () => {
47
+ const root = createRoot()
48
+ stubBrowser(root, '?accent=purple&radius=full&scheme=dark')
49
+ let update: ((theme: { scheme?: string }) => void) | undefined
50
+ const remote = {
51
+ messenger: {
52
+ on(_topic: string, listener: typeof update) {
53
+ update = listener
54
+ return () => {}
55
+ },
56
+ },
57
+ }
58
+ useTheme(remote as never)
59
+
60
+ update?.({ scheme: 'light' })
61
+
62
+ expect(readRoot(root)).toMatchInlineSnapshot(`
63
+ {
64
+ "attributes": {
65
+ "data-theme": "light",
66
+ },
67
+ "colorScheme": "light",
68
+ "properties": {},
69
+ }
70
+ `)
71
+ })
72
+
73
+ test('behavior: restores the document theme when unmounted', () => {
74
+ const root = createRoot(
75
+ {
76
+ 'data-theme': 'light',
77
+ 'data-theme-accent': 'custom',
78
+ 'data-theme-radius': 'small',
79
+ },
80
+ 'light',
81
+ { '--theme-accent': '#ff007a' },
82
+ )
83
+ stubBrowser(root, '?accent=blue&radius=full&scheme=dark')
84
+ useTheme()
85
+
86
+ hooks.cleanups[0]?.()
87
+
88
+ expect(readRoot(root)).toMatchInlineSnapshot(`
89
+ {
90
+ "attributes": {
91
+ "data-theme": "light",
92
+ "data-theme-accent": "custom",
93
+ "data-theme-radius": "small",
94
+ },
95
+ "colorScheme": "light",
96
+ "properties": {
97
+ "--theme-accent": "#ff007a",
98
+ },
99
+ }
100
+ `)
101
+ })
102
+ })
103
+
104
+ // Helpers
105
+
106
+ function stubBrowser(root: ReturnType<typeof createRoot>, search: string) {
107
+ vi.stubGlobal('document', { documentElement: root })
108
+ vi.stubGlobal('window', { location: { search } })
109
+ }
110
+
111
+ function createRoot(
112
+ initialAttributes: Record<string, string> = {},
113
+ colorScheme = '',
114
+ initialProperties: Record<string, string> = {},
115
+ ) {
116
+ const attributes = new Map(Object.entries(initialAttributes))
117
+ const properties = new Map(Object.entries(initialProperties))
118
+ return {
119
+ attributes,
120
+ getAttribute(name: string) {
121
+ return attributes.get(name) ?? null
122
+ },
123
+ removeAttribute(name: string) {
124
+ attributes.delete(name)
125
+ },
126
+ setAttribute(name: string, value: string) {
127
+ attributes.set(name, value)
128
+ },
129
+ style: {
130
+ colorScheme,
131
+ getPropertyValue(name: string) {
132
+ return properties.get(name) ?? ''
133
+ },
134
+ removeProperty(name: string) {
135
+ const value = properties.get(name) ?? ''
136
+ properties.delete(name)
137
+ return value
138
+ },
139
+ setProperty(name: string, value: string) {
140
+ properties.set(name, value)
141
+ },
142
+ },
143
+ properties,
144
+ }
145
+ }
146
+
147
+ function readRoot(root: ReturnType<typeof createRoot>) {
148
+ return {
149
+ attributes: Object.fromEntries(root.attributes),
150
+ colorScheme: root.style.colorScheme,
151
+ properties: Object.fromEntries(root.properties),
152
+ }
153
+ }
@@ -126,7 +126,10 @@ function applyTheme(theme: {
126
126
  root.setAttribute('data-theme-accent', getAccentName(accent))
127
127
  if (!isAccentPreset(accent)) root.style.setProperty('--theme-accent', accent)
128
128
  }
129
- if (scheme) root.style.colorScheme = scheme
129
+ if (scheme) {
130
+ root.style.colorScheme = scheme
131
+ root.setAttribute('data-theme', scheme)
132
+ }
130
133
  if (radius) root.setAttribute('data-theme-radius', radius)
131
134
  }
132
135
 
@@ -137,11 +140,13 @@ function captureTheme(): ThemeSnapshot {
137
140
  accentValue: root.style.getPropertyValue('--theme-accent'),
138
141
  colorScheme: root.style.colorScheme,
139
142
  radius: root.getAttribute('data-theme-radius'),
143
+ scheme: root.getAttribute('data-theme'),
140
144
  }
141
145
  }
142
146
 
143
147
  function restoreTheme(snapshot: ThemeSnapshot) {
144
148
  const root = document.documentElement
149
+ restoreAttribute(root, 'data-theme', snapshot.scheme)
145
150
  restoreAttribute(root, 'data-theme-accent', snapshot.accentPreset)
146
151
  restoreAttribute(root, 'data-theme-radius', snapshot.radius)
147
152
  if (snapshot.accentValue) root.style.setProperty('--theme-accent', snapshot.accentValue)
@@ -175,6 +180,7 @@ type ThemeSnapshot = {
175
180
  accentValue: string
176
181
  colorScheme: string
177
182
  radius: string | null
183
+ scheme: string | null
178
184
  }
179
185
 
180
186
  export declare namespace useEnsureVisibility {
@@ -3,6 +3,7 @@
3
3
  "localhost",
4
4
  "*.localhost",
5
5
  "*.tempo.xyz",
6
+ "mercator.sh",
6
7
  "paradigm.xyz",
7
8
  "promptgolf.sh",
8
9
  "app.polyhedge.capital",