accounts 0.12.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +6 -5
  3. package/dist/cli/adapter.d.ts.map +1 -1
  4. package/dist/cli/adapter.js +76 -69
  5. package/dist/cli/adapter.js.map +1 -1
  6. package/dist/core/AccessKey.d.ts +89 -106
  7. package/dist/core/AccessKey.d.ts.map +1 -1
  8. package/dist/core/AccessKey.js +230 -210
  9. package/dist/core/AccessKey.js.map +1 -1
  10. package/dist/core/Account.d.ts +0 -9
  11. package/dist/core/Account.d.ts.map +1 -1
  12. package/dist/core/Account.js +1 -13
  13. package/dist/core/Account.js.map +1 -1
  14. package/dist/core/Adapter.d.ts +24 -4
  15. package/dist/core/Adapter.d.ts.map +1 -1
  16. package/dist/core/Client.d.ts +1 -1
  17. package/dist/core/Client.d.ts.map +1 -1
  18. package/dist/core/Client.js.map +1 -1
  19. package/dist/core/Provider.d.ts +6 -7
  20. package/dist/core/Provider.d.ts.map +1 -1
  21. package/dist/core/Provider.js +73 -77
  22. package/dist/core/Provider.js.map +1 -1
  23. package/dist/core/Remote.d.ts +2 -0
  24. package/dist/core/Remote.d.ts.map +1 -1
  25. package/dist/core/Remote.js +3 -1
  26. package/dist/core/Remote.js.map +1 -1
  27. package/dist/core/Schema.d.ts +17 -7
  28. package/dist/core/Schema.d.ts.map +1 -1
  29. package/dist/core/adapters/dialog.d.ts.map +1 -1
  30. package/dist/core/adapters/dialog.js +126 -131
  31. package/dist/core/adapters/dialog.js.map +1 -1
  32. package/dist/core/adapters/local.d.ts.map +1 -1
  33. package/dist/core/adapters/local.js +92 -105
  34. package/dist/core/adapters/local.js.map +1 -1
  35. package/dist/core/adapters/privy.d.ts +164 -0
  36. package/dist/core/adapters/privy.d.ts.map +1 -0
  37. package/dist/core/adapters/privy.js +578 -0
  38. package/dist/core/adapters/privy.js.map +1 -0
  39. package/dist/core/adapters/turnkey.d.ts.map +1 -1
  40. package/dist/core/adapters/turnkey.js +136 -133
  41. package/dist/core/adapters/turnkey.js.map +1 -1
  42. package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
  43. package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
  44. package/dist/core/internal/AccessKeyTransaction.js +155 -0
  45. package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
  46. package/dist/core/zod/rpc.d.ts +71 -4
  47. package/dist/core/zod/rpc.d.ts.map +1 -1
  48. package/dist/core/zod/rpc.js +33 -4
  49. package/dist/core/zod/rpc.js.map +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/react-native/adapter.d.ts.map +1 -1
  55. package/dist/react-native/adapter.js +86 -75
  56. package/dist/react-native/adapter.js.map +1 -1
  57. package/dist/server/CliAuth.d.ts +55 -0
  58. package/dist/server/CliAuth.d.ts.map +1 -1
  59. package/dist/server/CliAuth.js +18 -1
  60. package/dist/server/CliAuth.js.map +1 -1
  61. package/dist/server/Kv.d.ts +19 -9
  62. package/dist/server/Kv.d.ts.map +1 -1
  63. package/dist/server/Kv.js +32 -9
  64. package/dist/server/Kv.js.map +1 -1
  65. package/dist/server/internal/handlers/auth.d.ts.map +1 -1
  66. package/dist/server/internal/handlers/auth.js +3 -1
  67. package/dist/server/internal/handlers/auth.js.map +1 -1
  68. package/dist/server/internal/handlers/codeAuth.js +1 -1
  69. package/dist/server/internal/handlers/codeAuth.js.map +1 -1
  70. package/dist/server/internal/handlers/exchange.js +1 -1
  71. package/dist/server/internal/handlers/exchange.js.map +1 -1
  72. package/dist/server/internal/handlers/relay.d.ts +18 -12
  73. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  74. package/dist/server/internal/handlers/relay.js +116 -3
  75. package/dist/server/internal/handlers/relay.js.map +1 -1
  76. package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
  77. package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
  78. package/dist/server/internal/handlers/webAuthn.js +16 -9
  79. package/dist/server/internal/handlers/webAuthn.js.map +1 -1
  80. package/package.json +3 -3
  81. package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +49 -1
  82. package/src/cli/adapter.ts +95 -76
  83. package/src/core/AccessKey.test.ts +563 -560
  84. package/src/core/AccessKey.ts +327 -327
  85. package/src/core/Account.test.ts +6 -51
  86. package/src/core/Account.ts +1 -19
  87. package/src/core/Adapter.ts +22 -4
  88. package/src/core/Client.test.ts +1 -1
  89. package/src/core/Client.ts +1 -1
  90. package/src/core/Provider.connect.browser.test.ts +1 -1
  91. package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +98 -32
  92. package/src/core/Provider.ts +118 -112
  93. package/src/core/Remote.test.ts +28 -0
  94. package/src/core/Remote.ts +4 -1
  95. package/src/core/Schema.test-d.ts +41 -0
  96. package/src/core/adapters/dialog.test.ts +335 -15
  97. package/src/core/adapters/dialog.ts +120 -127
  98. package/src/core/adapters/local.test.ts +1 -1
  99. package/src/core/adapters/local.ts +95 -119
  100. package/src/core/adapters/privy.test-d.ts +128 -0
  101. package/src/core/adapters/privy.test.ts +648 -0
  102. package/src/core/adapters/privy.ts +811 -0
  103. package/src/core/adapters/turnkey.test.ts +117 -13
  104. package/src/core/adapters/turnkey.ts +132 -149
  105. package/src/core/internal/AccessKeyTransaction.ts +243 -0
  106. package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
  107. package/src/core/zod/request.test.ts +36 -0
  108. package/src/core/zod/rpc.test.ts +139 -0
  109. package/src/core/zod/rpc.ts +44 -10
  110. package/src/index.ts +1 -1
  111. package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +65 -0
  112. package/src/react-native/adapter.ts +107 -87
  113. package/src/server/CliAuth.test-d.ts +23 -0
  114. package/src/server/CliAuth.test.ts +47 -0
  115. package/src/server/CliAuth.ts +20 -1
  116. package/src/server/Handler.test-d.ts +1 -1
  117. package/src/server/Kv.test.ts +51 -0
  118. package/src/server/Kv.ts +43 -9
  119. package/src/server/internal/handlers/auth.test.ts +10 -5
  120. package/src/server/internal/handlers/auth.ts +4 -1
  121. package/src/server/internal/handlers/codeAuth.ts +1 -1
  122. package/src/server/internal/handlers/exchange.ts +1 -1
  123. package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +4 -1
  124. package/src/server/internal/handlers/relay.ts +139 -14
  125. package/src/server/internal/handlers/session.test.ts +1 -1
  126. package/src/server/internal/handlers/webAuthn.test.ts +20 -0
  127. package/src/server/internal/handlers/webAuthn.ts +24 -12
  128. /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
@@ -1,9 +1,10 @@
1
- import { Provider as ox_Provider } from 'ox'
2
- import { tempoLocalnet } from 'viem/chains'
1
+ import { Address, Hex, Provider as ox_Provider, PublicKey } from 'ox'
2
+ import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
+ import { tempoLocalnet } from 'viem/tempo/chains'
3
4
  import { afterEach, describe, expect, test, vi } from 'vp/test'
4
5
 
5
- import * as AccessKey from '../AccessKey.js'
6
6
  import * as Dialog from '../Dialog.js'
7
+ import * as AccessKeyTransaction from '../internal/AccessKeyTransaction.js'
7
8
  import * as Storage from '../Storage.js'
8
9
  import * as Store from '../Store.js'
9
10
  import { dialog } from './dialog.js'
@@ -11,6 +12,47 @@ import { dialog } from './dialog.js'
11
12
  const address = '0x0000000000000000000000000000000000000001'
12
13
  const recipient = '0x0000000000000000000000000000000000000002'
13
14
 
15
+ function createKeyAuthorization(options: {
16
+ expiry: number
17
+ keyType: 'secp256k1' | 'p256'
18
+ publicKey: Hex.Hex
19
+ }) {
20
+ const { expiry, keyType, publicKey } = options
21
+ return KeyAuthorization.toRpc(
22
+ KeyAuthorization.from(
23
+ {
24
+ address: Address.fromPublicKey(PublicKey.from(publicKey)),
25
+ chainId: BigInt(tempoLocalnet.id),
26
+ expiry,
27
+ type: keyType,
28
+ },
29
+ { signature: SignatureEnvelope.from(`0x${'00'.repeat(65)}`) },
30
+ ),
31
+ )
32
+ }
33
+
34
+ function setup() {
35
+ const storage = Storage.memory()
36
+ const store = Store.create({ chainId: tempoLocalnet.id, storage })
37
+ const adapter = dialog({ dialog: Dialog.noop() })({
38
+ getAccount: (options) => {
39
+ if (options?.signable) throw new ox_Provider.UnauthorizedError({ message: 'No signer.' })
40
+ return { address, type: 'json-rpc' } as never
41
+ },
42
+ getClient: () => ({}) as never,
43
+ storage,
44
+ store,
45
+ })
46
+ return { adapter, store }
47
+ }
48
+
49
+ async function takeRequest(store: Store.Store) {
50
+ await vi.waitFor(() => {
51
+ if (!store.getState().requestQueue[0]) throw new Error('request not queued')
52
+ })
53
+ return store.getState().requestQueue[0]!
54
+ }
55
+
14
56
  describe('dialog', () => {
15
57
  afterEach(() => {
16
58
  vi.restoreAllMocks()
@@ -21,16 +63,24 @@ describe('dialog', () => {
21
63
  const store = Store.create({ chainId: tempoLocalnet.id, storage })
22
64
  const clientRequests: unknown[] = []
23
65
  const signRequests: unknown[] = []
24
- vi.spyOn(AccessKey, 'selectAccount').mockReturnValue({
25
- accessKeyAddress: '0x0000000000000000000000000000000000000099',
26
- address: '0x0000000000000000000000000000000000000099',
27
- signTransaction: async (request: unknown) => {
28
- signRequests.push(request)
29
- return '0xsigned'
30
- },
31
- source: 'accessKey',
32
- type: 'local',
33
- } as never)
66
+ vi.spyOn(AccessKeyTransaction, 'create').mockResolvedValue({
67
+ fill: async () => ({ capabilities: { sponsored: false }, tx: {} }),
68
+ prepare: async (request) => ({
69
+ request: request as never,
70
+ send: async () => {
71
+ signRequests.push(request)
72
+ clientRequests.push({ method: 'eth_sendRawTransaction', params: ['0xsigned'] })
73
+ return '0xtransaction'
74
+ },
75
+ sendSync: async () => {
76
+ throw new Error('unexpected sendSync')
77
+ },
78
+ sign: async () => {
79
+ signRequests.push(request)
80
+ return '0xsigned'
81
+ },
82
+ }),
83
+ })
34
84
  const adapter = dialog({ dialog: Dialog.noop() })({
35
85
  getAccount: () => {
36
86
  throw new ox_Provider.UnauthorizedError({ message: 'No local signer.' })
@@ -84,10 +134,76 @@ describe('dialog', () => {
84
134
  expect(store.getState().requestQueue).toMatchInlineSnapshot(`[]`)
85
135
  })
86
136
 
137
+ test('behavior: loadAccounts forwards auth capabilities returned by the dialog', async () => {
138
+ const storage = Storage.memory()
139
+ const store = Store.create({ chainId: tempoLocalnet.id, storage })
140
+ const adapter = dialog({ dialog: Dialog.noop() })({
141
+ getAccount: () => {
142
+ throw new ox_Provider.UnauthorizedError({ message: 'No local signer.' })
143
+ },
144
+ getClient: () => ({}) as never,
145
+ storage,
146
+ store,
147
+ })
148
+ const request = {
149
+ method: 'wallet_connect' as const,
150
+ params: [
151
+ {
152
+ capabilities: {
153
+ auth: {
154
+ url: 'https://app.example/auth',
155
+ returnToken: true,
156
+ },
157
+ },
158
+ chainId: '0x1079' as const,
159
+ },
160
+ ] as const,
161
+ }
162
+
163
+ const promise = adapter.actions.loadAccounts(undefined, request)
164
+
165
+ await vi.waitFor(() => {
166
+ if (!store.getState().requestQueue[0]) throw new Error('request not queued')
167
+ })
168
+
169
+ const queued = store.getState().requestQueue[0]!
170
+ store.setState({
171
+ requestQueue: [
172
+ {
173
+ request: queued.request,
174
+ result: {
175
+ accounts: [
176
+ {
177
+ address,
178
+ capabilities: {
179
+ auth: { token: 'test-token' },
180
+ },
181
+ },
182
+ ],
183
+ },
184
+ status: 'success',
185
+ },
186
+ ],
187
+ })
188
+
189
+ await expect(promise).resolves.toMatchInlineSnapshot(`
190
+ {
191
+ "accounts": [
192
+ {
193
+ "address": "0x0000000000000000000000000000000000000001",
194
+ },
195
+ ],
196
+ "auth": {
197
+ "token": "test-token",
198
+ },
199
+ }
200
+ `)
201
+ })
202
+
87
203
  test('behavior: sendTransaction falls through when no access key is selected', async () => {
88
204
  const storage = Storage.memory()
89
205
  const store = Store.create({ chainId: tempoLocalnet.id, storage })
90
- vi.spyOn(AccessKey, 'selectAccount').mockReturnValue(undefined)
206
+ vi.spyOn(AccessKeyTransaction, 'create').mockResolvedValue(undefined)
91
207
  const lookups: unknown[] = []
92
208
  const adapter = dialog({ dialog: Dialog.noop() })({
93
209
  getAccount: (options) => {
@@ -137,10 +253,214 @@ describe('dialog', () => {
137
253
  expect(lookups).toMatchInlineSnapshot(`[]`)
138
254
  })
139
255
 
256
+ test('behavior: revokeAccessKey clears the forwarded key from local state', async () => {
257
+ const storage = Storage.memory()
258
+ const store = Store.create({ chainId: tempoLocalnet.id, storage })
259
+ store.setState({
260
+ accessKeys: [
261
+ {
262
+ access: address,
263
+ address: recipient,
264
+ chainId: tempoLocalnet.id,
265
+ keyType: 'p256',
266
+ } as never,
267
+ ],
268
+ })
269
+ const adapter = dialog({ dialog: Dialog.noop() })({
270
+ getAccount: () => {
271
+ throw new ox_Provider.UnauthorizedError({ message: 'No local signer.' })
272
+ },
273
+ getClient: () => ({}) as never,
274
+ storage,
275
+ store,
276
+ })
277
+ const request = {
278
+ method: 'wallet_revokeAccessKey' as const,
279
+ params: [{ accessKeyAddress: recipient, address }] as const,
280
+ }
281
+
282
+ const promise = adapter.actions.revokeAccessKey!(
283
+ { accessKeyAddress: recipient, address },
284
+ request,
285
+ )
286
+
287
+ await vi.waitFor(() => {
288
+ if (!store.getState().requestQueue[0]) throw new Error('request not queued')
289
+ })
290
+
291
+ const queued = store.getState().requestQueue[0]!
292
+ store.setState({
293
+ requestQueue: [
294
+ {
295
+ request: queued.request,
296
+ result: undefined,
297
+ status: 'success',
298
+ },
299
+ ],
300
+ })
301
+
302
+ await expect(promise).resolves.toMatchInlineSnapshot(`undefined`)
303
+ expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
304
+ })
305
+
306
+ test('behavior: authorizeAccessKey forwards an external secp256k1 key', async () => {
307
+ const { adapter, store } = setup()
308
+ const expiry = 123
309
+ const promise = adapter.actions.authorizeAccessKey!(
310
+ { address: recipient, expiry, keyType: 'secp256k1' },
311
+ {
312
+ method: 'wallet_authorizeAccessKey',
313
+ params: [{ address: recipient, expiry, keyType: 'secp256k1' }],
314
+ },
315
+ )
316
+
317
+ const queued = await takeRequest(store)
318
+ const request = queued.request as {
319
+ params: [{ address: typeof recipient; expiry: number; keyType: 'secp256k1' }]
320
+ }
321
+ const params = request.params[0]
322
+ const keyAuthorization = KeyAuthorization.toRpc(
323
+ KeyAuthorization.from(
324
+ {
325
+ address: recipient,
326
+ chainId: BigInt(tempoLocalnet.id),
327
+ expiry,
328
+ type: 'secp256k1',
329
+ },
330
+ { signature: SignatureEnvelope.from(`0x${'00'.repeat(65)}`) },
331
+ ),
332
+ )
333
+ store.setState({
334
+ requestQueue: [
335
+ {
336
+ request: queued.request,
337
+ result: {
338
+ keyAuthorization,
339
+ rootAddress: address,
340
+ },
341
+ status: 'success',
342
+ },
343
+ ],
344
+ })
345
+
346
+ await expect(promise).resolves.toMatchObject({ rootAddress: address })
347
+ expect(params.keyType).toMatchInlineSnapshot(`"secp256k1"`)
348
+ expect(params.address).toMatchInlineSnapshot(`"0x0000000000000000000000000000000000000002"`)
349
+ expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
350
+ })
351
+
352
+ test('behavior: authorizeAccessKey generates a p256 key by default', async () => {
353
+ const { adapter, store } = setup()
354
+ const expiry = 123
355
+ const promise = adapter.actions.authorizeAccessKey!(
356
+ { expiry },
357
+ { method: 'wallet_authorizeAccessKey', params: [{ expiry }] },
358
+ )
359
+
360
+ const queued = await takeRequest(store)
361
+ const request = queued.request as {
362
+ params: [{ expiry: number; keyType: 'p256'; publicKey: Hex.Hex }]
363
+ }
364
+ const params = request.params[0]
365
+ store.setState({
366
+ requestQueue: [
367
+ {
368
+ request: queued.request,
369
+ result: {
370
+ keyAuthorization: createKeyAuthorization(params),
371
+ rootAddress: address,
372
+ },
373
+ status: 'success',
374
+ },
375
+ ],
376
+ })
377
+
378
+ await expect(promise).resolves.toMatchObject({ rootAddress: address })
379
+ expect(params.keyType).toMatchInlineSnapshot(`"p256"`)
380
+ expect(store.getState().accessKeys).toMatchObject([
381
+ {
382
+ access: address,
383
+ keyType: 'p256',
384
+ },
385
+ ])
386
+ expect('keyPair' in store.getState().accessKeys[0]!).toMatchInlineSnapshot(`true`)
387
+ })
388
+
389
+ test('behavior: authorizeAccessKey generates a p256 key when requested', async () => {
390
+ const { adapter, store } = setup()
391
+ const expiry = 123
392
+ const promise = adapter.actions.authorizeAccessKey!(
393
+ { expiry, keyType: 'p256' },
394
+ { method: 'wallet_authorizeAccessKey', params: [{ expiry, keyType: 'p256' }] },
395
+ )
396
+
397
+ const queued = await takeRequest(store)
398
+ const request = queued.request as {
399
+ params: [{ expiry: number; keyType: 'p256'; publicKey: Hex.Hex }]
400
+ }
401
+ const params = request.params[0]
402
+ store.setState({
403
+ requestQueue: [
404
+ {
405
+ request: queued.request,
406
+ result: {
407
+ keyAuthorization: createKeyAuthorization(params),
408
+ rootAddress: address,
409
+ },
410
+ status: 'success',
411
+ },
412
+ ],
413
+ })
414
+
415
+ await expect(promise).resolves.toMatchObject({ rootAddress: address })
416
+ expect(params.keyType).toMatchInlineSnapshot(`"p256"`)
417
+ expect(store.getState().accessKeys).toMatchObject([
418
+ {
419
+ access: address,
420
+ keyType: 'p256',
421
+ },
422
+ ])
423
+ expect('keyPair' in store.getState().accessKeys[0]!).toMatchInlineSnapshot(`true`)
424
+ })
425
+
426
+ test('error: secp256k1 access key requires external key material', async () => {
427
+ const { adapter, store } = setup()
428
+
429
+ await expect(
430
+ adapter.actions.authorizeAccessKey!(
431
+ { expiry: 123, keyType: 'secp256k1' },
432
+ {
433
+ method: 'wallet_authorizeAccessKey',
434
+ params: [{ expiry: 123, keyType: 'secp256k1' }],
435
+ },
436
+ ),
437
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
438
+ `[RpcResponse.InvalidParamsError: \`keyType: "secp256k1"\` requires externally generated key material; provide \`publicKey\` or \`address\`.]`,
439
+ )
440
+ expect(store.getState().requestQueue).toMatchInlineSnapshot(`[]`)
441
+ })
442
+
443
+ test('error: webAuthn access key requires external key material', async () => {
444
+ const { adapter, store } = setup()
445
+
446
+ await expect(
447
+ adapter.actions.authorizeAccessKey!(
448
+ { expiry: 123, keyType: 'webAuthn' },
449
+ {
450
+ method: 'wallet_authorizeAccessKey',
451
+ params: [{ expiry: 123, keyType: 'webAuthn' }],
452
+ },
453
+ ),
454
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
455
+ `[RpcResponse.InvalidParamsError: \`keyType: "webAuthn"\` requires externally generated key material; provide \`publicKey\` or \`address\`.]`,
456
+ )
457
+ expect(store.getState().requestQueue).toMatchInlineSnapshot(`[]`)
458
+ })
459
+
140
460
  test('error: wallet validation errors keep their RPC code', async () => {
141
461
  const storage = Storage.memory()
142
462
  const store = Store.create({ chainId: tempoLocalnet.id, storage })
143
- vi.spyOn(AccessKey, 'selectAccount').mockReturnValue(undefined)
463
+ vi.spyOn(AccessKeyTransaction, 'create').mockResolvedValue(undefined)
144
464
  const adapter = dialog({ dialog: Dialog.noop() })({
145
465
  getAccount: () => {
146
466
  throw new ox_Provider.UnauthorizedError({ message: 'No local signer.' })