accounts 0.14.0 → 0.14.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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/AccessKey.d.ts +1 -1
  3. package/dist/core/AccessKey.d.ts.map +1 -1
  4. package/dist/core/Adapter.d.ts +2 -2
  5. package/dist/core/Adapter.d.ts.map +1 -1
  6. package/dist/core/Provider.d.ts.map +1 -1
  7. package/dist/core/Provider.js +3 -0
  8. package/dist/core/Provider.js.map +1 -1
  9. package/dist/core/Schema.d.ts +14 -0
  10. package/dist/core/Schema.d.ts.map +1 -1
  11. package/dist/core/adapters/privy.d.ts.map +1 -1
  12. package/dist/core/adapters/privy.js +112 -155
  13. package/dist/core/adapters/privy.js.map +1 -1
  14. package/dist/core/zod/rpc.d.ts +67 -4
  15. package/dist/core/zod/rpc.d.ts.map +1 -1
  16. package/dist/core/zod/rpc.js +9 -4
  17. package/dist/core/zod/rpc.js.map +1 -1
  18. package/dist/react-native/adapter.d.ts.map +1 -1
  19. package/dist/react-native/adapter.js +3 -0
  20. package/dist/react-native/adapter.js.map +1 -1
  21. package/dist/server/CliAuth.d.ts +11 -0
  22. package/dist/server/CliAuth.d.ts.map +1 -1
  23. package/dist/server/CliAuth.js +1 -0
  24. package/dist/server/CliAuth.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/cli/Provider.localnet.test.ts +46 -2
  27. package/src/core/AccessKey.ts +1 -1
  28. package/src/core/Adapter.ts +2 -2
  29. package/src/core/Provider.ts +3 -0
  30. package/src/core/Schema.test-d.ts +3 -1
  31. package/src/core/adapters/privy.test.ts +150 -10
  32. package/src/core/adapters/privy.ts +148 -192
  33. package/src/core/zod/rpc.test.ts +91 -4
  34. package/src/core/zod/rpc.ts +9 -4
  35. package/src/react-native/Provider.localnet.test.ts +25 -0
  36. package/src/react-native/adapter.ts +3 -0
  37. package/src/server/CliAuth.test-d.ts +2 -0
  38. package/src/server/CliAuth.test.ts +2 -0
  39. package/src/server/CliAuth.ts +1 -0
@@ -56,10 +56,11 @@ function connectRequest(
56
56
  options: {
57
57
  accessKey?: typeof accessKey | undefined
58
58
  expiry?: number | undefined
59
+ method?: 'login' | 'register' | undefined
59
60
  showDeposit?: z.output<typeof CliAuth.createRequest>['showDeposit'] | undefined
60
61
  } = {},
61
62
  ) {
62
- const { accessKey: key = accessKey, expiry: expiry_ = expiry, showDeposit } = options
63
+ const { accessKey: key = accessKey, expiry: expiry_ = expiry, method, showDeposit } = options
63
64
 
64
65
  return {
65
66
  method: 'wallet_connect',
@@ -71,7 +72,8 @@ function connectRequest(
71
72
  keyType: key.keyType,
72
73
  publicKey: key.publicKey,
73
74
  },
74
- ...(showDeposit !== undefined ? { method: 'register' as const, showDeposit } : {}),
75
+ ...(method ? { method } : {}),
76
+ ...(showDeposit !== undefined ? { showDeposit } : {}),
75
77
  },
76
78
  },
77
79
  ],
@@ -237,9 +239,11 @@ describe('Provider.create', () => {
237
239
 
238
240
  await provider.request(
239
241
  connectRequest({
242
+ method: 'register',
240
243
  showDeposit: {
241
244
  amount: '50',
242
245
  displayName: 'DoorDash',
246
+ on: 'register',
243
247
  token: 'USDC',
244
248
  },
245
249
  }),
@@ -250,6 +254,7 @@ describe('Provider.create', () => {
250
254
  {
251
255
  "amount": "50",
252
256
  "displayName": "DoorDash",
257
+ "on": "register",
253
258
  "token": "USDC",
254
259
  },
255
260
  ]
@@ -259,6 +264,45 @@ describe('Provider.create', () => {
259
264
  }
260
265
  })
261
266
 
267
+ test('behavior: forwards showDeposit through login device-code requests', async () => {
268
+ const handler = createHandler()
269
+ const server = await createServer(handler.listener)
270
+ const pendingShowDeposit: z.output<typeof CliAuth.pendingResponse>['showDeposit'][] = []
271
+
272
+ try {
273
+ const provider = Provider.create({
274
+ chains: [chain],
275
+ open: async (url) => {
276
+ const code = new URL(url).searchParams.get('code')!
277
+ const response = await fetch(`${server.url}/cli-auth/pending/${code}`)
278
+ const pending = z.decode(CliAuth.pendingResponse, (await response.json()) as never)
279
+ pendingShowDeposit.push(pending.showDeposit)
280
+ await fetch(`${server.url}/cli-auth`, {
281
+ body: JSON.stringify(await authorizePending(server.url, code)),
282
+ headers: { 'content-type': 'application/json' },
283
+ method: 'POST',
284
+ })
285
+ },
286
+ host: `${server.url}/cli-auth`,
287
+ })
288
+
289
+ await provider.request(
290
+ connectRequest({
291
+ method: 'login',
292
+ showDeposit: true,
293
+ }),
294
+ )
295
+
296
+ expect(pendingShowDeposit).toMatchInlineSnapshot(`
297
+ [
298
+ true,
299
+ ]
300
+ `)
301
+ } finally {
302
+ await server.closeAsync()
303
+ }
304
+ })
305
+
262
306
  test('behavior: browser-open failures surface the URL and code', async () => {
263
307
  const handler = createHandler()
264
308
  const server = await createServer(handler.listener)
@@ -251,7 +251,7 @@ export declare namespace authorize {
251
251
  /** Options for {@link authorize}. */
252
252
  type Options = {
253
253
  /** Root account that owns this access key and signs its authorization. */
254
- account: TempoAccount.Account
254
+ account: Pick<TempoAccount.Account, 'address' | 'sign'>
255
255
  /** Default chain ID for the authorization when `parameters.chainId` is not set. */
256
256
  chainId: bigint | number
257
257
  /** Access key authorization parameters. */
@@ -245,7 +245,7 @@ export declare namespace loadAccounts {
245
245
  type Capabilities = NonNullable<
246
246
  NonNullable<Rpc.wallet_connect.Decoded['params']>[number]['capabilities']
247
247
  >
248
- type ShowDeposit = Extract<Capabilities, { method: 'register' }>['showDeposit']
248
+ type ShowDeposit = Extract<Capabilities, { method?: 'login' | undefined }>['showDeposit']
249
249
 
250
250
  type Parameters = {
251
251
  /** Grant an access key during the ceremony. */
@@ -263,7 +263,7 @@ export declare namespace loadAccounts {
263
263
  personalSign?: { message: string } | undefined
264
264
  /** When `true`, prompts the user to pick from all available credentials instead of using the last-used one. */
265
265
  selectAccount?: boolean | undefined
266
- /** Show the deposit flow after a register ceremony signs in to an existing account. */
266
+ /** Show the deposit flow after the connect ceremony succeeds. */
267
267
  showDeposit?: ShowDeposit | undefined
268
268
  }
269
269
  type ReturnType = {
@@ -664,6 +664,9 @@ export function create(options: create.Options = {}): create.ReturnType {
664
664
  authorizeAccessKey,
665
665
  selectAccount: capabilities?.selectAccount,
666
666
  ...(personalSign_request ? { personalSign: personalSign_request } : {}),
667
+ ...(capabilities?.showDeposit !== undefined
668
+ ? { showDeposit: capabilities.showDeposit }
669
+ : {}),
667
670
  },
668
671
  request,
669
672
  )
@@ -105,6 +105,7 @@ describe('Encoded', () => {
105
105
  type Register = Extract<Capabilities, { method: 'register' }>
106
106
  type Login = Extract<Capabilities, { method?: 'login' | undefined }>
107
107
  type ShowDeposit = Register['showDeposit']
108
+ type LoginShowDeposit = Login['showDeposit']
108
109
  type ShowDepositObject = Exclude<Exclude<ShowDeposit, boolean | undefined>, undefined>
109
110
 
110
111
  expectTypeOf<ShowDeposit>().toMatchTypeOf<
@@ -112,11 +113,12 @@ describe('Encoded', () => {
112
113
  | {
113
114
  amount?: string | undefined
114
115
  displayName?: string | undefined
116
+ on?: 'login' | 'register' | undefined
115
117
  token?: string | undefined
116
118
  }
117
119
  | undefined
118
120
  >()
119
- expectTypeOf<Login>().not.toHaveProperty('showDeposit')
121
+ expectTypeOf<LoginShowDeposit>().toEqualTypeOf<ShowDeposit>()
120
122
  expectTypeOf<ShowDepositObject>().not.toHaveProperty('address')
121
123
  expectTypeOf<ShowDepositObject>().not.toHaveProperty('chainId')
122
124
  })
@@ -80,10 +80,34 @@ describe('privy', () => {
80
80
  `)
81
81
  })
82
82
 
83
+ test('default: createAccount can select the active embedded wallet', async () => {
84
+ const { adapter, client } = setup({ createAddresses: [other] })
85
+ client.addWallet(other)
86
+
87
+ const result = await adapter.actions.createAccount(
88
+ { digest: '0x1234', name: 'Ada' },
89
+ { method: 'wallet_connect', params: undefined },
90
+ )
91
+
92
+ expect(client.signWith).toMatchInlineSnapshot(`
93
+ [
94
+ "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
95
+ ]
96
+ `)
97
+ expect(result.accounts).toMatchInlineSnapshot(`
98
+ [
99
+ {
100
+ "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
101
+ "label": "Ada",
102
+ },
103
+ ]
104
+ `)
105
+ })
106
+
83
107
  test('default: loadAccounts delegates login and caches embedded wallets for signing', async () => {
84
- const { adapter, client } = setup()
108
+ const { adapter, client, store } = setup()
85
109
 
86
- await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
110
+ await connect({ adapter, store })
87
111
  const result = await adapter.actions.signPersonalMessage(
88
112
  { address, data: '0x68656c6c6f' },
89
113
  { method: 'personal_sign', params: ['0x68656c6c6f', address] },
@@ -100,6 +124,32 @@ describe('privy', () => {
100
124
  )
101
125
  })
102
126
 
127
+ test('default: loadAccounts can select and order embedded wallets', async () => {
128
+ const { adapter, client } = setup({ loadAddresses: [other, address] })
129
+ client.addWallet(other)
130
+
131
+ const result = await adapter.actions.loadAccounts(
132
+ { digest: '0x1234' },
133
+ { method: 'wallet_connect', params: undefined },
134
+ )
135
+
136
+ expect(client.signWith).toMatchInlineSnapshot(`
137
+ [
138
+ "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
139
+ ]
140
+ `)
141
+ expect(result.accounts).toMatchInlineSnapshot(`
142
+ [
143
+ {
144
+ "address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
145
+ },
146
+ {
147
+ "address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
148
+ },
149
+ ]
150
+ `)
151
+ })
152
+
103
153
  test('default: loadAccounts can provision an external access key', async () => {
104
154
  const { adapter, client } = setup()
105
155
 
@@ -144,6 +194,39 @@ describe('privy', () => {
144
194
  `)
145
195
  })
146
196
 
197
+ test('default: signs transactions with a materialized Privy account', async () => {
198
+ const { adapter, client, store } = setup()
199
+ await connect({ adapter, store })
200
+
201
+ const result = await adapter.actions.signTransaction(
202
+ {
203
+ chainId: 1,
204
+ from: address,
205
+ gas: 21_000n,
206
+ maxFeePerGas: 1n,
207
+ maxPriorityFeePerGas: 1n,
208
+ nonce: 0,
209
+ to: other,
210
+ value: 1n,
211
+ },
212
+ { method: 'eth_signTransaction', params: [{ from: address }] },
213
+ )
214
+
215
+ expect(client.signWith).toMatchInlineSnapshot(`
216
+ [
217
+ "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
218
+ ]
219
+ `)
220
+ expect(client.signPayloads).toMatchInlineSnapshot(`
221
+ [
222
+ "0x62f087d34b8a023e0461eb1b9a01267ba5b8400c13d2ffdac615ccec872cc288",
223
+ ]
224
+ `)
225
+ expect(result).toMatchInlineSnapshot(
226
+ `"0x76f86a010101825208d8d7942b5ad5c4795c026514f8317c7a215e218dccd6cf0180c0808080808080c0b8418b0c18077cb78666296a4c0e8149935124f70d7820ec4e4ae81de428659d6c305c098dea43ccb536e813bb1c136eab5e96611de69489be6291169b2bb2318cde1b"`,
227
+ )
228
+ })
229
+
147
230
  test('default: authorizeAccessKey signs with the connected Privy account', async () => {
148
231
  const { adapter, client, store } = setup()
149
232
  store.setState({ accounts: [{ address }], activeAccount: 0 })
@@ -386,6 +469,51 @@ describe('privy', () => {
386
469
  expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
387
470
  })
388
471
 
472
+ test('behavior: failed account selection does not poison silent restore cache', async () => {
473
+ const { adapter, store } = setup({ loadAddresses: [other] })
474
+ store.setState({ accounts: [{ address: other }], activeAccount: 0 })
475
+
476
+ await expect(
477
+ adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined }),
478
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
479
+ `[Provider.UnauthorizedError: Privy callback returned address "${other}" that was not found in the user's embedded wallets.]`,
480
+ )
481
+
482
+ await expect(
483
+ adapter.actions.signPersonalMessage(
484
+ { address: other, data: '0x68656c6c6f' },
485
+ { method: 'personal_sign', params: ['0x68656c6c6f', other] },
486
+ ),
487
+ ).rejects.toMatchInlineSnapshot(
488
+ '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]',
489
+ )
490
+ expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
491
+ })
492
+
493
+ test('behavior: failed empty createAccount selection does not poison silent restore cache', async () => {
494
+ const { adapter, store } = setup({ createAddresses: [] })
495
+ store.setState({ accounts: [{ address: other }], activeAccount: 0 })
496
+
497
+ await expect(
498
+ adapter.actions.createAccount(
499
+ { digest: '0x1234', name: 'Ada' },
500
+ { method: 'wallet_connect', params: undefined },
501
+ ),
502
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
503
+ `[Provider.DisconnectedError: Privy returned no wallet.]`,
504
+ )
505
+
506
+ await expect(
507
+ adapter.actions.signPersonalMessage(
508
+ { address: other, data: '0x68656c6c6f' },
509
+ { method: 'personal_sign', params: ['0x68656c6c6f', other] },
510
+ ),
511
+ ).rejects.toMatchInlineSnapshot(
512
+ '[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]',
513
+ )
514
+ expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
515
+ })
516
+
389
517
  test('error: silent restore rejects non-hex secp256k1_sign results', async () => {
390
518
  const { adapter, store } = setup({ signResult: 'not-hex' })
391
519
  store.setState({ accounts: [{ address }], activeAccount: 0 })
@@ -410,8 +538,8 @@ describe('privy', () => {
410
538
  })
411
539
 
412
540
  test('error: malformed secp256k1_sign result is rejected by signer recovery', async () => {
413
- const { adapter } = setup({ signResult: '0x1234' })
414
- await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
541
+ const { adapter, store } = setup({ signResult: '0x1234' })
542
+ await connect({ adapter, store })
415
543
 
416
544
  await expect(
417
545
  adapter.actions.signPersonalMessage(
@@ -424,8 +552,8 @@ describe('privy', () => {
424
552
  })
425
553
 
426
554
  test('error: signing for an unconnected address while others are connected throws Unauthorized', async () => {
427
- const { adapter } = setup()
428
- await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
555
+ const { adapter, store } = setup()
556
+ await connect({ adapter, store })
429
557
 
430
558
  await expect(
431
559
  adapter.actions.signPersonalMessage(
@@ -478,8 +606,8 @@ describe('privy', () => {
478
606
  })
479
607
 
480
608
  test('error: signature recovered from a different key is rejected as Unauthorized', async () => {
481
- const { adapter } = setup({ signWithPrivateKey: privateKeyB })
482
- await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
609
+ const { adapter, store } = setup({ signWithPrivateKey: privateKeyB })
610
+ await connect({ adapter, store })
483
611
 
484
612
  await expect(
485
613
  adapter.actions.signPersonalMessage(
@@ -503,12 +631,12 @@ function setup(options: setup.Options = {}) {
503
631
  : {
504
632
  createAccount: async () => {
505
633
  client.createCalls++
506
- return undefined
634
+ return options.createAddresses
507
635
  },
508
636
  }),
509
637
  loadAccounts: async () => {
510
638
  client.loadCalls++
511
- return undefined
639
+ return options.loadAddresses
512
640
  },
513
641
  })({
514
642
  getAccount: (() => {
@@ -527,10 +655,22 @@ function setup(options: setup.Options = {}) {
527
655
  return { adapter, client, store }
528
656
  }
529
657
 
658
+ async function connect(options: Pick<ReturnType<typeof setup>, 'adapter' | 'store'>) {
659
+ const { adapter, store } = options
660
+ const loaded = await adapter.actions.loadAccounts(undefined, {
661
+ method: 'wallet_connect',
662
+ params: undefined,
663
+ })
664
+ store.setState({ accounts: loaded.accounts, activeAccount: 0 })
665
+ return loaded
666
+ }
667
+
530
668
  declare namespace setup {
531
669
  type Options = {
532
670
  /** Pass `false` to omit the adapter's `createAccount` callback (tests fallback to `loadAccounts`). */
533
671
  createAccount?: false | undefined
672
+ createAddresses?: readonly Address[] | undefined
673
+ loadAddresses?: readonly Address[] | undefined
534
674
  /** Make the mock client's `user.get` throw, to test restore-side session errors. */
535
675
  restoreError?: unknown
536
676
  token?: string | null | undefined