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
@@ -0,0 +1,811 @@
1
+ import {
2
+ Address as core_Address,
3
+ Hex,
4
+ Provider as ox_Provider,
5
+ PublicKey,
6
+ RpcResponse,
7
+ Secp256k1,
8
+ Signature,
9
+ WebCryptoP256,
10
+ } from 'ox'
11
+ import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
12
+ import { hashMessage, hashTypedData, isAddressEqual, keccak256 } from 'viem'
13
+ import type { Address, LocalAccount } from 'viem/accounts'
14
+ import { prepareTransactionRequest } from 'viem/actions'
15
+ import { Actions, Transaction as TempoTransaction } from 'viem/tempo'
16
+
17
+ import * as AccessKey from '../AccessKey.js'
18
+ import * as Adapter from '../Adapter.js'
19
+ import * as AccessKeyTransaction from '../internal/AccessKeyTransaction.js'
20
+ import * as Store from '../Store.js'
21
+
22
+ const privySessionErrorCodes = new Set([
23
+ 'attempted_rpc_call_before_logged_in',
24
+ 'attempted_to_read_storage_before_client_initialized',
25
+ 'embedded_wallet_before_logged_in',
26
+ 'embedded_wallet_does_not_exist',
27
+ 'embedded_wallet_request_error',
28
+ 'missing_auth_token',
29
+ 'missing_privy_token',
30
+ 'oauth_session_failed',
31
+ 'oauth_session_timeout',
32
+ 'session_expired',
33
+ 'unauthenticated',
34
+ 'unauthorized',
35
+ ])
36
+
37
+ /**
38
+ * Creates a Privy adapter backed by `@privy-io/js-sdk-core` Privy sessions and embedded
39
+ * Ethereum wallets.
40
+ *
41
+ * The adapter owns silent reconnect, session-expiry cleanup, and signing. Apps supply
42
+ * the UI-bearing login flow via `loadAccounts` (and optionally a distinct `createAccount`
43
+ * for registration). Callbacks fire only on user-initiated `wallet_connect`/registration —
44
+ * never during silent restore on page reload.
45
+ *
46
+ * Silent restore on page reload pulls wallets directly from the Privy SDK
47
+ * (`client.user.get` + `client.embeddedWallet.getEthereumProvider`), so apps don't
48
+ * need to re-run the login UI when the user returns with a still-valid Privy session.
49
+ *
50
+ * Callbacks only run the Privy auth UI. They may optionally return a subset of
51
+ * embedded wallet addresses to expose; if omitted, the adapter exposes every
52
+ * embedded wallet on the resulting Privy user.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import Privy from '@privy-io/js-sdk-core'
57
+ *
58
+ * const client = new Privy({ appId: import.meta.env.VITE_PRIVY_APP_ID })
59
+ *
60
+ * const provider = Provider.create({
61
+ * adapter: privy({
62
+ * client,
63
+ * // Optional: omit to route registration through `loadAccounts`.
64
+ * createAccount: async ({ client }) => {
65
+ * await myPrivyRegisterUI(client)
66
+ * },
67
+ * loadAccounts: async ({ client }) => {
68
+ * await myPrivyLoginUI(client)
69
+ * },
70
+ * }),
71
+ * })
72
+ * ```
73
+ */
74
+ export function privy<const client extends privy.Client>(
75
+ options: privy.Options<client>,
76
+ ): Adapter.Adapter {
77
+ const { icon, name = 'Privy', rdns = 'io.privy' } = options
78
+
79
+ return Adapter.define({ icon, name, rdns }, ({ getClient, store }) => {
80
+ let privyClient_promise: Promise<client> | undefined
81
+ let restore_promise: Promise<void> | undefined
82
+ let walletAccounts: readonly privy.EmbeddedWallet[] = []
83
+
84
+ async function getPrivyClient(): Promise<client> {
85
+ privyClient_promise ??= (async () => {
86
+ await options.client.initialize?.()
87
+ return options.client
88
+ })()
89
+ return await privyClient_promise
90
+ }
91
+
92
+ function toStoreAccount(account: privy.EmbeddedWallet, label?: string | undefined) {
93
+ return {
94
+ address: core_Address.from(account.address),
95
+ ...(label ? { label } : {}),
96
+ }
97
+ }
98
+
99
+ function clear() {
100
+ restore_promise = undefined
101
+ walletAccounts = []
102
+ store.setState({ accessKeys: [], accounts: [], activeAccount: 0 })
103
+ }
104
+
105
+ async function hasValidSession() {
106
+ const token = await (await getPrivyClient()).getAccessToken().catch((error) => {
107
+ if (isSessionError(error)) return null
108
+ throw error
109
+ })
110
+ return !!token
111
+ }
112
+
113
+ /**
114
+ * Loads the user's Privy embedded Ethereum wallets and constructs their
115
+ * EIP-1193 providers. Mirrors `getAllUserEmbeddedEthereumWallets` +
116
+ * `getEntropyDetailsFromUser` from `@privy-io/js-sdk-core`: per the SDK,
117
+ * `entropyId` is the **primary** embedded wallet's address (wallet_index === 0)
118
+ * shared across all wallets of the same user, and `entropyIdVerifier` is
119
+ * hardcoded to `'ethereum-address-verifier'` for Ethereum wallets.
120
+ */
121
+ async function loadEthereumWallets(
122
+ privyClient: privy.Client,
123
+ ): Promise<readonly privy.EmbeddedWallet[]> {
124
+ const { user } = await privyClient.user.get()
125
+ const wallets = (user?.linked_accounts ?? [])
126
+ .filter(
127
+ (account) =>
128
+ account.type === 'wallet' &&
129
+ account.wallet_client_type === 'privy' &&
130
+ account.connector_type === 'embedded' &&
131
+ account.chain_type === 'ethereum' &&
132
+ typeof account.address === 'string',
133
+ )
134
+ .slice()
135
+ .sort((a, b) => {
136
+ // Wallets without a `wallet_index` are sorted to the end so they
137
+ // never accidentally become primary when a sibling has an index.
138
+ const a_index = a.wallet_index ?? Number.POSITIVE_INFINITY
139
+ const b_index = b.wallet_index ?? Number.POSITIVE_INFINITY
140
+ return a_index - b_index
141
+ })
142
+
143
+ // Primary is the wallet with `wallet_index === 0`. Fall back to the
144
+ // lowest-indexed wallet only when no wallet declares index 0.
145
+ const primary = wallets.find((wallet) => wallet.wallet_index === 0) ?? wallets[0]
146
+ if (!primary) return []
147
+ const entropyId = primary.address as string
148
+
149
+ return await Promise.all(
150
+ wallets.map(async (wallet) => ({
151
+ address: core_Address.from(wallet.address as string),
152
+ provider: await privyClient.embeddedWallet.getEthereumProvider({
153
+ wallet,
154
+ entropyId,
155
+ entropyIdVerifier: 'ethereum-address-verifier',
156
+ }),
157
+ })),
158
+ )
159
+ }
160
+
161
+ function selectWalletAccounts(
162
+ accounts: readonly privy.EmbeddedWallet[],
163
+ addresses: privy.AccountSelection,
164
+ ): readonly privy.EmbeddedWallet[] {
165
+ if (!addresses) return accounts
166
+
167
+ return addresses.map((address) => {
168
+ const address_ = core_Address.from(address)
169
+ const account = accounts.find((account) =>
170
+ isAddressEqual(core_Address.from(account.address), address_),
171
+ )
172
+ if (account) return account
173
+
174
+ throw new ox_Provider.UnauthorizedError({
175
+ message: `Privy callback returned address "${address_}" that was not found in the user's embedded wallets.`,
176
+ })
177
+ })
178
+ }
179
+
180
+ async function restore() {
181
+ await Store.waitForHydration(store)
182
+ if (walletAccounts.length > 0) return
183
+ if (restore_promise) return await restore_promise
184
+
185
+ restore_promise = (async () => {
186
+ const state = store.getState()
187
+ const persisted = state.accounts
188
+ if (persisted.length === 0) return
189
+
190
+ if (!(await hasValidSession())) {
191
+ clear()
192
+ throw new ox_Provider.DisconnectedError({ message: 'Privy session expired.' })
193
+ }
194
+
195
+ const restored = await loadEthereumWallets(await getPrivyClient()).catch((error) => {
196
+ if (!isSessionError(error)) throw error
197
+ clear()
198
+ throw new ox_Provider.DisconnectedError({ message: 'Privy session expired.' })
199
+ })
200
+ walletAccounts = persisted
201
+ .map((account) =>
202
+ restored.find((walletAccount) =>
203
+ isAddressEqual(core_Address.from(walletAccount.address), account.address),
204
+ ),
205
+ )
206
+ .filter((account): account is privy.EmbeddedWallet => !!account)
207
+
208
+ // If the persisted accounts no longer exist in Privy (different user
209
+ // signed in, wallets removed), wipe the stale state so callers see a
210
+ // clean disconnected state instead of ghost accounts without providers.
211
+ if (walletAccounts.length === 0) {
212
+ clear()
213
+ throw new ox_Provider.DisconnectedError({
214
+ message: 'Privy session no longer matches persisted accounts.',
215
+ })
216
+ }
217
+
218
+ store.setState({
219
+ accounts: walletAccounts.map((account) => toStoreAccount(account)),
220
+ activeAccount: Math.min(state.activeAccount, walletAccounts.length - 1),
221
+ })
222
+ })()
223
+
224
+ try {
225
+ await restore_promise
226
+ } finally {
227
+ restore_promise = undefined
228
+ }
229
+ }
230
+
231
+ async function requireSession() {
232
+ if (await hasValidSession()) return
233
+ clear()
234
+ throw new ox_Provider.DisconnectedError({ message: 'Privy session expired.' })
235
+ }
236
+
237
+ async function accountForSigning(address: Address | undefined) {
238
+ await restore()
239
+ await requireSession()
240
+
241
+ const address_ = address ?? store.getState().accounts[store.getState().activeAccount]?.address
242
+ if (!address_) throw new ox_Provider.DisconnectedError({ message: 'No accounts connected.' })
243
+
244
+ const account = walletAccounts.find((account) =>
245
+ isAddressEqual(core_Address.from(account.address), address_),
246
+ )
247
+ if (account) return account
248
+
249
+ if (walletAccounts.length === 0)
250
+ throw new ox_Provider.DisconnectedError({
251
+ message: 'No Privy account connected.',
252
+ })
253
+
254
+ throw new ox_Provider.UnauthorizedError({ message: `Account "${address_}" not found.` })
255
+ }
256
+
257
+ async function signPayload(parameters: {
258
+ payload: Hex.Hex
259
+ walletAccount: privy.EmbeddedWallet
260
+ }) {
261
+ const { payload, walletAccount } = parameters
262
+ const result = await walletAccount.provider
263
+ .request({ method: 'secp256k1_sign', params: [payload] })
264
+ .catch((error) => {
265
+ const code = getPrivyErrorCode(error)
266
+ const message = getPrivyErrorMessage(error).toLowerCase()
267
+ const unsupported =
268
+ (typeof code === 'number' && (code === 4200 || code === -32601)) ||
269
+ (typeof code === 'string' && code.toLowerCase().includes('unsupported')) ||
270
+ message.includes('unsupported') ||
271
+ message.includes('method not found')
272
+ if (unsupported)
273
+ throw new ox_Provider.UnsupportedMethodError({
274
+ message:
275
+ 'Privy adapter requires raw secp256k1 hash signing via `secp256k1_sign` for Tempo transactions and access keys.',
276
+ })
277
+ if (isSessionError(error)) {
278
+ clear()
279
+ throw new ox_Provider.DisconnectedError({ message: 'Privy session expired.' })
280
+ }
281
+ throw error
282
+ })
283
+ if (typeof result !== 'string' || !Hex.validate(result))
284
+ throw new ox_Provider.ProviderRpcError(
285
+ -32603,
286
+ 'Privy provider returned a non-hex secp256k1_sign result.',
287
+ )
288
+ const signature: Hex.Hex = result
289
+
290
+ // Verify Privy returned a signature for the wallet we asked.
291
+ const expected = core_Address.from(walletAccount.address)
292
+ const recovered = (() => {
293
+ try {
294
+ return Secp256k1.recoverAddress({ payload, signature: Signature.fromHex(signature) })
295
+ } catch {
296
+ return undefined
297
+ }
298
+ })()
299
+ if (!recovered || !isAddressEqual(recovered, expected))
300
+ throw new ox_Provider.UnauthorizedError({
301
+ message: `Privy provider returned a signature for "${recovered ?? 'unknown'}" that does not match the requested wallet "${expected}".`,
302
+ })
303
+ return signature
304
+ }
305
+
306
+ /**
307
+ * Builds, signs, and saves an access key authorization for the given Privy
308
+ * wallet. Generates a local P256 key pair when no external key is provided.
309
+ */
310
+ async function authorizeAccessKeyFor(
311
+ account: privy.EmbeddedWallet,
312
+ options: Adapter.authorizeAccessKey.Parameters,
313
+ ) {
314
+ const { expiry, limits, scopes } = options
315
+ const chainId = options.chainId ?? getClient().chain.id
316
+
317
+ const prepared = await (async () => {
318
+ if (options.publicKey || options.address) {
319
+ const address =
320
+ options.address ?? core_Address.fromPublicKey(PublicKey.from(options.publicKey!))
321
+ return {
322
+ keyAuthorization: KeyAuthorization.from({
323
+ address,
324
+ chainId: BigInt(chainId),
325
+ expiry,
326
+ limits,
327
+ scopes,
328
+ type: options.keyType ?? 'secp256k1',
329
+ }),
330
+ }
331
+ }
332
+
333
+ if (options.keyType && options.keyType !== 'p256')
334
+ throw new RpcResponse.InvalidParamsError({
335
+ message: `\`keyType: "${options.keyType}"\` requires externally generated key material; provide \`publicKey\` or \`address\`.`,
336
+ })
337
+
338
+ const keyPair = await WebCryptoP256.createKeyPair()
339
+ const address = core_Address.fromPublicKey(PublicKey.from(keyPair.publicKey))
340
+ return {
341
+ keyAuthorization: KeyAuthorization.from({
342
+ address,
343
+ chainId: BigInt(chainId),
344
+ expiry,
345
+ limits,
346
+ scopes,
347
+ type: 'p256',
348
+ }),
349
+ keyPair,
350
+ }
351
+ })()
352
+
353
+ const signature = await signPayload({
354
+ payload: KeyAuthorization.getSignPayload(prepared.keyAuthorization),
355
+ walletAccount: account,
356
+ })
357
+ const keyAuthorization = KeyAuthorization.from(prepared.keyAuthorization, {
358
+ signature: SignatureEnvelope.from(signature),
359
+ })
360
+
361
+ AccessKey.add({
362
+ account: core_Address.from(account.address),
363
+ authorization: keyAuthorization,
364
+ ...(prepared.keyPair ? { keyPair: prepared.keyPair } : {}),
365
+ store,
366
+ })
367
+
368
+ return KeyAuthorization.toRpc(keyAuthorization)
369
+ }
370
+
371
+ async function signTransaction(parameters: Adapter.signTransaction.Parameters) {
372
+ const account = await accountForSigning(parameters.from)
373
+ const { feePayer, ...rest } = parameters
374
+ const viemClient = getClient({
375
+ chainId: parameters.chainId,
376
+ feePayer: feePayer === true ? undefined : feePayer,
377
+ })
378
+ const prepared = await prepareTransactionRequest(viemClient, {
379
+ account: core_Address.from(account.address),
380
+ ...rest,
381
+ ...(feePayer ? { feePayer: true } : {}),
382
+ type: 'tempo',
383
+ } as never)
384
+ return await signPreparedTransaction(account, prepared)
385
+ }
386
+
387
+ async function signPreparedTransaction(account: privy.EmbeddedWallet, prepared: unknown) {
388
+ const presign = (() => {
389
+ if (
390
+ prepared &&
391
+ typeof prepared === 'object' &&
392
+ 'feePayerSignature' in prepared &&
393
+ prepared.feePayerSignature
394
+ )
395
+ return { ...prepared, feePayerSignature: null }
396
+ return prepared
397
+ })()
398
+ const unsignedTransaction = await TempoTransaction.serialize(presign as never)
399
+
400
+ const signature = await signPayload({
401
+ payload: keccak256(unsignedTransaction),
402
+ walletAccount: account,
403
+ })
404
+ return await TempoTransaction.serialize(
405
+ prepared as never,
406
+ SignatureEnvelope.from(Signature.fromHex(signature)) as never,
407
+ )
408
+ }
409
+
410
+ async function prepareTransaction(parameters: Adapter.signTransaction.Parameters) {
411
+ const viemClient = getClient({
412
+ chainId: parameters.chainId,
413
+ feePayer: parameters.feePayer === true ? undefined : parameters.feePayer,
414
+ })
415
+
416
+ const state = store.getState()
417
+ const address = parameters.from ?? state.accounts[state.activeAccount]?.address
418
+ const transaction = address
419
+ ? await AccessKeyTransaction.create({
420
+ address,
421
+ calls: parameters.calls,
422
+ chainId: parameters.chainId ?? state.chainId,
423
+ client: viemClient,
424
+ store,
425
+ })
426
+ : undefined
427
+ if (transaction) {
428
+ const { feePayer, ...rest } = parameters
429
+ try {
430
+ return await transaction.prepare({
431
+ ...rest,
432
+ ...(feePayer ? { feePayer: true } : {}),
433
+ })
434
+ } catch {}
435
+ }
436
+
437
+ async function sign() {
438
+ return await signTransaction(parameters)
439
+ }
440
+
441
+ return {
442
+ request: undefined as never,
443
+ sign,
444
+ async send() {
445
+ const signed = await sign()
446
+ return await viemClient.request({
447
+ method: 'eth_sendRawTransaction' as never,
448
+ params: [signed],
449
+ })
450
+ },
451
+ async sendSync() {
452
+ const signed = await sign()
453
+ return await viemClient.request({
454
+ method: 'eth_sendRawTransactionSync' as never,
455
+ params: [signed],
456
+ })
457
+ },
458
+ }
459
+ }
460
+
461
+ function isSessionError(error: unknown) {
462
+ const code = getPrivyErrorCode(error)
463
+ if (typeof code === 'string') {
464
+ const normalized = code.toLowerCase()
465
+ if (privySessionErrorCodes.has(normalized)) return true
466
+ if (normalized.includes('session')) return true
467
+ if (normalized.includes('before_logged_in')) return true
468
+ }
469
+
470
+ const message = getPrivyErrorMessage(error).toLowerCase()
471
+ return (
472
+ message.includes('missing privy token') ||
473
+ message.includes('must be logged in') ||
474
+ message.includes('not authenticated') ||
475
+ message.includes('not logged in') ||
476
+ message.includes('session expired')
477
+ )
478
+ }
479
+
480
+ function getPrivyErrorCode(error: unknown): string | number | undefined {
481
+ if (!isObject(error)) return undefined
482
+
483
+ if (typeof error.code === 'string' || typeof error.code === 'number') return error.code
484
+ if (typeof error.error_code === 'string' || typeof error.error_code === 'number')
485
+ return error.error_code
486
+ if (typeof error.errorCode === 'string' || typeof error.errorCode === 'number')
487
+ return error.errorCode
488
+
489
+ return getPrivyErrorCode(error.cause)
490
+ }
491
+
492
+ function getPrivyErrorMessage(error: unknown): string {
493
+ if (error instanceof Error) {
494
+ const caused = getPrivyErrorMessage(error.cause)
495
+ return caused ? `${error.message} ${caused}` : error.message
496
+ }
497
+ if (!isObject(error)) return ''
498
+ const own =
499
+ (typeof error.message === 'string' && error.message) ||
500
+ (typeof error.error === 'string' && error.error) ||
501
+ ''
502
+ const caused = getPrivyErrorMessage(error.cause)
503
+ if (own && caused) return `${own} ${caused}`
504
+ return own || caused
505
+ }
506
+
507
+ function isObject(value: unknown): value is Record<string, unknown> {
508
+ return typeof value === 'object' && value !== null
509
+ }
510
+
511
+ return {
512
+ cleanup() {},
513
+ actions: {
514
+ async createAccount(parameters) {
515
+ const { authorizeAccessKey, personalSign } = parameters
516
+ if (personalSign && parameters.digest)
517
+ throw new ox_Provider.ProviderRpcError(
518
+ -32602,
519
+ '`digest` and `personalSign` cannot both be set on `wallet_connect`.',
520
+ )
521
+
522
+ const privyClient = await getPrivyClient()
523
+ const addresses = options.createAccount
524
+ ? await options.createAccount({ client: privyClient, parameters })
525
+ : await options.loadAccounts({
526
+ client: privyClient,
527
+ parameters: {
528
+ ...(authorizeAccessKey ? { authorizeAccessKey } : {}),
529
+ ...(parameters.digest ? { digest: parameters.digest } : {}),
530
+ ...(personalSign ? { personalSign } : {}),
531
+ },
532
+ })
533
+ await requireSession()
534
+ walletAccounts = selectWalletAccounts(await loadEthereumWallets(privyClient), addresses)
535
+ // Drop any in-flight `restore()` (from a concurrent `accountForSigning`)
536
+ // so re-entrant `restore()` calls don't `await` a stale IIFE that would
537
+ // later overwrite `walletAccounts` with the intersection against
538
+ // now-replaced persisted accounts.
539
+ restore_promise = undefined
540
+
541
+ const account = walletAccounts[0]
542
+ if (!account)
543
+ throw new ox_Provider.DisconnectedError({
544
+ message: 'Privy returned no wallet.',
545
+ })
546
+
547
+ const digest = personalSign ? hashMessage(personalSign.message) : parameters.digest
548
+ const keyAuthorization = authorizeAccessKey
549
+ ? await authorizeAccessKeyFor(account, authorizeAccessKey)
550
+ : undefined
551
+
552
+ return {
553
+ accounts: walletAccounts.map((wallet, index) =>
554
+ toStoreAccount(wallet, index === 0 ? parameters.name : undefined),
555
+ ),
556
+ ...(personalSign ? { personalSign: { message: personalSign.message } } : {}),
557
+ ...(keyAuthorization ? { keyAuthorization } : {}),
558
+ signature: digest
559
+ ? await signPayload({
560
+ payload: digest,
561
+ walletAccount: account,
562
+ })
563
+ : undefined,
564
+ }
565
+ },
566
+ async loadAccounts(parameters) {
567
+ const { authorizeAccessKey, personalSign } =
568
+ parameters ?? ({} as Adapter.loadAccounts.Parameters)
569
+ if (personalSign && parameters?.digest)
570
+ throw new ox_Provider.ProviderRpcError(
571
+ -32602,
572
+ '`digest` and `personalSign` cannot both be set on `wallet_connect`.',
573
+ )
574
+
575
+ const privyClient = await getPrivyClient()
576
+ const addresses = await options.loadAccounts({ client: privyClient, parameters })
577
+ await requireSession()
578
+ walletAccounts = selectWalletAccounts(await loadEthereumWallets(privyClient), addresses)
579
+ // Drop any in-flight `restore()` (from a concurrent `accountForSigning`)
580
+ // so re-entrant `restore()` calls don't `await` a stale IIFE that would
581
+ // later overwrite `walletAccounts` with the intersection against
582
+ // now-replaced persisted accounts.
583
+ restore_promise = undefined
584
+
585
+ const digest = personalSign ? hashMessage(personalSign.message) : parameters?.digest
586
+ const account = walletAccounts[0]
587
+ const keyAuthorization =
588
+ authorizeAccessKey && account
589
+ ? await authorizeAccessKeyFor(account, authorizeAccessKey)
590
+ : undefined
591
+
592
+ return {
593
+ accounts: walletAccounts.map((account) => toStoreAccount(account)),
594
+ ...(personalSign ? { personalSign: { message: personalSign.message } } : {}),
595
+ ...(keyAuthorization ? { keyAuthorization } : {}),
596
+ signature:
597
+ digest && account
598
+ ? await signPayload({
599
+ payload: digest,
600
+ walletAccount: account,
601
+ })
602
+ : undefined,
603
+ }
604
+ },
605
+ async authorizeAccessKey(parameters) {
606
+ const account = await accountForSigning(undefined)
607
+ const keyAuthorization = await authorizeAccessKeyFor(account, parameters)
608
+ return { keyAuthorization, rootAddress: core_Address.from(account.address) }
609
+ },
610
+ async revokeAccessKey(parameters) {
611
+ const account = await accountForSigning(parameters.address)
612
+ const account_tempo = {
613
+ address: core_Address.from(account.address),
614
+ source: 'privy',
615
+ signTransaction: async (request: unknown) =>
616
+ await signPreparedTransaction(account, request),
617
+ type: 'local',
618
+ } satisfies {
619
+ address: Address
620
+ signTransaction: (request: unknown) => Promise<Hex.Hex>
621
+ source: 'privy'
622
+ type: 'local'
623
+ }
624
+ try {
625
+ await Actions.accessKey.revoke(getClient(), {
626
+ account: account_tempo as LocalAccount<'privy'>,
627
+ accessKey: parameters.accessKeyAddress,
628
+ })
629
+ } catch (error) {
630
+ if (!AccessKey.isUnavailableError(error)) throw error
631
+ }
632
+ AccessKey.remove({
633
+ accessKey: parameters.accessKeyAddress,
634
+ account: core_Address.from(account.address),
635
+ chainId: store.getState().chainId,
636
+ store,
637
+ })
638
+ },
639
+ async signPersonalMessage(parameters) {
640
+ const account = await accountForSigning(parameters.address)
641
+ return await signPayload({
642
+ payload: hashMessage({ raw: parameters.data }),
643
+ walletAccount: account,
644
+ })
645
+ },
646
+ async signTransaction(parameters) {
647
+ return await (await prepareTransaction(parameters)).sign()
648
+ },
649
+ async signTypedData(parameters) {
650
+ const account = await accountForSigning(parameters.address)
651
+ const typedData = JSON.parse(parameters.data) as {
652
+ domain: Record<string, unknown>
653
+ message: Record<string, unknown>
654
+ primaryType: string
655
+ types: Record<string, unknown>
656
+ }
657
+ return await signPayload({
658
+ payload: hashTypedData(typedData as never),
659
+ walletAccount: account,
660
+ })
661
+ },
662
+ async sendTransaction(parameters) {
663
+ return await (await prepareTransaction(parameters)).send()
664
+ },
665
+ async sendTransactionSync(parameters) {
666
+ return await (await prepareTransaction(parameters)).sendSync()
667
+ },
668
+ async disconnect() {
669
+ try {
670
+ const privyClient = await getPrivyClient()
671
+ const userId = await privyClient.user
672
+ .get()
673
+ .then(({ user }) => user.id)
674
+ .catch(() => undefined)
675
+ await privyClient.auth.logout(userId ? { userId } : undefined)
676
+ } finally {
677
+ clear()
678
+ }
679
+ },
680
+ },
681
+ }
682
+ })
683
+ }
684
+
685
+ export declare namespace privy {
686
+ /** Options for {@link privy}. */
687
+ type Options<client extends Client = Client> = {
688
+ /** Existing Privy client, such as `Privy` from `@privy-io/js-sdk-core`. */
689
+ client: client
690
+ /**
691
+ * Runs the Privy registration UI. May optionally return a subset of the user's
692
+ * embedded wallet addresses to expose to the provider; if omitted, the adapter
693
+ * exposes every embedded wallet on the resulting Privy user.
694
+ *
695
+ * The adapter materializes EIP-1193 providers internally via
696
+ * `client.embeddedWallet.getEthereumProvider` — callbacks should not.
697
+ *
698
+ * Defaults to `loadAccounts` — apps that don't distinguish register vs login
699
+ * can omit this.
700
+ */
701
+ createAccount?:
702
+ | ((parameters: {
703
+ /** Initialized Privy client. */
704
+ client: client
705
+ /** Provider create-account parameters. */
706
+ parameters: Adapter.createAccount.Parameters
707
+ }) => Promise<AccountSelection>)
708
+ | undefined
709
+ /** Data URI of the provider icon. @default Black 1×1 SVG. */
710
+ icon?: `data:image/${string}` | undefined
711
+ /**
712
+ * Runs the Privy login UI in response to a user-initiated `wallet_connect`.
713
+ * May optionally return a subset of the user's embedded wallet addresses to
714
+ * expose to the provider; if omitted, the adapter exposes every embedded
715
+ * wallet on the Privy user.
716
+ *
717
+ * Silent restore on page reload pulls wallets directly from the Privy SDK
718
+ * (`client.user.get` + `client.embeddedWallet.getEthereumProvider`) and does
719
+ * NOT call this function.
720
+ */
721
+ loadAccounts: (parameters: {
722
+ /** Initialized Privy client. */
723
+ client: client
724
+ /** Provider load-accounts parameters. */
725
+ parameters?: Adapter.loadAccounts.Parameters | undefined
726
+ }) => Promise<AccountSelection>
727
+ /** Display name of the provider. @default "Privy" */
728
+ name?: string | undefined
729
+ /** Reverse DNS identifier. @default "io.privy" */
730
+ rdns?: string | undefined
731
+ }
732
+
733
+ /**
734
+ * Optional subset of embedded wallet addresses returned from `createAccount` /
735
+ * `loadAccounts`. `void`/`undefined` means "expose every embedded wallet".
736
+ */
737
+ type AccountSelection = readonly Address[] | void
738
+
739
+ /**
740
+ * Minimal structural Privy client surface used by the adapter for session checks,
741
+ * silent restore, and disconnect. User-initiated `wallet_connect`/registration
742
+ * is delegated to the app's `loadAccounts` / `createAccount` callbacks.
743
+ *
744
+ * Satisfied by `Privy` from `@privy-io/js-sdk-core` — apps pass the SDK instance
745
+ * directly. The adapter never imports `@privy-io/js-sdk-core` itself; the structural
746
+ * shape keeps the dependency one-way.
747
+ */
748
+ type Client = {
749
+ /** Auth API; the adapter only needs `logout`. */
750
+ auth: {
751
+ /**
752
+ * Clears the current Privy session. The adapter passes the current user id
753
+ * (when available) so multi-tab/multi-user setups scope the logout correctly.
754
+ */
755
+ logout: (parameters?: { userId: string } | undefined) => Promise<void> | void
756
+ }
757
+ /** Embedded wallet API used by the adapter to materialize EIP-1193 providers. */
758
+ embeddedWallet: {
759
+ /** Returns an EIP-1193 provider for a Privy embedded Ethereum wallet. */
760
+ getEthereumProvider(parameters: {
761
+ wallet: LinkedAccount
762
+ entropyId: string
763
+ entropyIdVerifier: string
764
+ }): Promise<EthereumProvider> | EthereumProvider
765
+ }
766
+ /** Returns the current Privy access token, or `null` if no session. */
767
+ getAccessToken: () => Promise<string | null>
768
+ /** Initializes the client. Called once by the adapter, before any other method. */
769
+ initialize?: (() => Promise<void> | void) | undefined
770
+ /** User API used by the adapter to scope `auth.logout` and to silently restore wallets. */
771
+ user: {
772
+ /** Returns the currently authenticated Privy user. */
773
+ get: () => Promise<{ user: User }>
774
+ }
775
+ }
776
+
777
+ /** Minimal Privy user shape used by the adapter for silent restore. */
778
+ type User = {
779
+ id: string
780
+ linked_accounts?: readonly LinkedAccount[] | undefined
781
+ }
782
+
783
+ /** Minimal Privy linked account shape used by the adapter for silent restore. */
784
+ type LinkedAccount = {
785
+ address?: string | undefined
786
+ chain_type?: string | undefined
787
+ connector_type?: string | undefined
788
+ type?: string | undefined
789
+ wallet_client_type?: string | undefined
790
+ wallet_index?: number | undefined
791
+ }
792
+
793
+ /** Minimal EIP-1193 provider surface used by the adapter for `secp256k1_sign`. */
794
+ type EthereumProvider = {
795
+ request(parameters: {
796
+ method: string
797
+ params?: readonly unknown[] | undefined
798
+ }): Promise<unknown>
799
+ }
800
+
801
+ /**
802
+ * Materialized Privy embedded wallet — the `{ address, provider }` shape the
803
+ * adapter caches internally after calling
804
+ * `client.embeddedWallet.getEthereumProvider`. The adapter calls
805
+ * `provider.request({ method: 'secp256k1_sign', params: [hash] })` for signing.
806
+ */
807
+ type EmbeddedWallet = {
808
+ address: string
809
+ provider: EthereumProvider
810
+ }
811
+ }