@account-kit/signer 4.0.0-alpha.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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/base.d.ts +37 -0
  3. package/dist/cjs/base.js +292 -0
  4. package/dist/cjs/base.js.map +1 -0
  5. package/dist/cjs/client/base.d.ts +230 -0
  6. package/dist/cjs/client/base.js +298 -0
  7. package/dist/cjs/client/base.js.map +1 -0
  8. package/dist/cjs/client/index.d.ts +146 -0
  9. package/dist/cjs/client/index.js +260 -0
  10. package/dist/cjs/client/index.js.map +1 -0
  11. package/dist/cjs/client/types.d.ts +106 -0
  12. package/dist/cjs/client/types.js +3 -0
  13. package/dist/cjs/client/types.js.map +1 -0
  14. package/dist/cjs/errors.d.ts +4 -0
  15. package/dist/cjs/errors.js +16 -0
  16. package/dist/cjs/errors.js.map +1 -0
  17. package/dist/cjs/index.d.ts +8 -0
  18. package/dist/cjs/index.js +14 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/package.json +1 -0
  21. package/dist/cjs/session/manager.d.ts +45 -0
  22. package/dist/cjs/session/manager.js +230 -0
  23. package/dist/cjs/session/manager.js.map +1 -0
  24. package/dist/cjs/session/types.d.ts +16 -0
  25. package/dist/cjs/session/types.js +3 -0
  26. package/dist/cjs/session/types.js.map +1 -0
  27. package/dist/cjs/signer.d.ts +262 -0
  28. package/dist/cjs/signer.js +34 -0
  29. package/dist/cjs/signer.js.map +1 -0
  30. package/dist/cjs/types.d.ts +14 -0
  31. package/dist/cjs/types.js +12 -0
  32. package/dist/cjs/types.js.map +1 -0
  33. package/dist/cjs/utils/base64UrlEncode.d.ts +1 -0
  34. package/dist/cjs/utils/base64UrlEncode.js +12 -0
  35. package/dist/cjs/utils/base64UrlEncode.js.map +1 -0
  36. package/dist/cjs/utils/generateRandomBuffer.d.ts +1 -0
  37. package/dist/cjs/utils/generateRandomBuffer.js +10 -0
  38. package/dist/cjs/utils/generateRandomBuffer.js.map +1 -0
  39. package/dist/cjs/version.d.ts +1 -0
  40. package/dist/cjs/version.js +5 -0
  41. package/dist/cjs/version.js.map +1 -0
  42. package/dist/esm/base.d.ts +37 -0
  43. package/dist/esm/base.js +288 -0
  44. package/dist/esm/base.js.map +1 -0
  45. package/dist/esm/client/base.d.ts +230 -0
  46. package/dist/esm/client/base.js +291 -0
  47. package/dist/esm/client/base.js.map +1 -0
  48. package/dist/esm/client/index.d.ts +146 -0
  49. package/dist/esm/client/index.js +256 -0
  50. package/dist/esm/client/index.js.map +1 -0
  51. package/dist/esm/client/types.d.ts +106 -0
  52. package/dist/esm/client/types.js +2 -0
  53. package/dist/esm/client/types.js.map +1 -0
  54. package/dist/esm/errors.d.ts +4 -0
  55. package/dist/esm/errors.js +12 -0
  56. package/dist/esm/errors.js.map +1 -0
  57. package/dist/esm/index.d.ts +8 -0
  58. package/dist/esm/index.js +6 -0
  59. package/dist/esm/index.js.map +1 -0
  60. package/dist/esm/package.json +1 -0
  61. package/dist/esm/session/manager.d.ts +45 -0
  62. package/dist/esm/session/manager.js +223 -0
  63. package/dist/esm/session/manager.js.map +1 -0
  64. package/dist/esm/session/types.d.ts +16 -0
  65. package/dist/esm/session/types.js +2 -0
  66. package/dist/esm/session/types.js.map +1 -0
  67. package/dist/esm/signer.d.ts +262 -0
  68. package/dist/esm/signer.js +30 -0
  69. package/dist/esm/signer.js.map +1 -0
  70. package/dist/esm/types.d.ts +14 -0
  71. package/dist/esm/types.js +9 -0
  72. package/dist/esm/types.js.map +1 -0
  73. package/dist/esm/utils/base64UrlEncode.d.ts +1 -0
  74. package/dist/esm/utils/base64UrlEncode.js +8 -0
  75. package/dist/esm/utils/base64UrlEncode.js.map +1 -0
  76. package/dist/esm/utils/generateRandomBuffer.d.ts +1 -0
  77. package/dist/esm/utils/generateRandomBuffer.js +6 -0
  78. package/dist/esm/utils/generateRandomBuffer.js.map +1 -0
  79. package/dist/esm/version.d.ts +1 -0
  80. package/dist/esm/version.js +2 -0
  81. package/dist/esm/version.js.map +1 -0
  82. package/dist/types/base.d.ts +89 -0
  83. package/dist/types/base.d.ts.map +1 -0
  84. package/dist/types/client/base.d.ts +246 -0
  85. package/dist/types/client/base.d.ts.map +1 -0
  86. package/dist/types/client/index.d.ts +151 -0
  87. package/dist/types/client/index.d.ts.map +1 -0
  88. package/dist/types/client/types.d.ts +107 -0
  89. package/dist/types/client/types.d.ts.map +1 -0
  90. package/dist/types/errors.d.ts +5 -0
  91. package/dist/types/errors.d.ts.map +1 -0
  92. package/dist/types/index.d.ts +9 -0
  93. package/dist/types/index.d.ts.map +1 -0
  94. package/dist/types/session/manager.d.ts +46 -0
  95. package/dist/types/session/manager.d.ts.map +1 -0
  96. package/dist/types/session/types.d.ts +17 -0
  97. package/dist/types/session/types.d.ts.map +1 -0
  98. package/dist/types/signer.d.ts +269 -0
  99. package/dist/types/signer.d.ts.map +1 -0
  100. package/dist/types/types.d.ts +15 -0
  101. package/dist/types/types.d.ts.map +1 -0
  102. package/dist/types/utils/base64UrlEncode.d.ts +2 -0
  103. package/dist/types/utils/base64UrlEncode.d.ts.map +1 -0
  104. package/dist/types/utils/generateRandomBuffer.d.ts +2 -0
  105. package/dist/types/utils/generateRandomBuffer.d.ts.map +1 -0
  106. package/dist/types/version.d.ts +2 -0
  107. package/dist/types/version.d.ts.map +1 -0
  108. package/package.json +79 -0
  109. package/src/base.ts +386 -0
  110. package/src/client/base.ts +399 -0
  111. package/src/client/index.ts +267 -0
  112. package/src/client/types.ts +121 -0
  113. package/src/errors.ts +15 -0
  114. package/src/index.ts +10 -0
  115. package/src/session/manager.ts +249 -0
  116. package/src/session/types.ts +16 -0
  117. package/src/signer.ts +55 -0
  118. package/src/types.ts +17 -0
  119. package/src/utils/base64UrlEncode.ts +7 -0
  120. package/src/utils/generateRandomBuffer.ts +5 -0
  121. package/src/version.ts +3 -0
package/src/base.ts ADDED
@@ -0,0 +1,386 @@
1
+ import { takeBytes, type SmartAccountAuthenticator } from "@aa-sdk/core";
2
+ import {
3
+ hashMessage,
4
+ hashTypedData,
5
+ keccak256,
6
+ serializeTransaction,
7
+ type CustomSource,
8
+ type Hex,
9
+ type LocalAccount,
10
+ type SignableMessage,
11
+ type TypedData,
12
+ type TypedDataDefinition,
13
+ } from "viem";
14
+ import { toAccount } from "viem/accounts";
15
+ import type { Mutate, StoreApi } from "zustand";
16
+ import { subscribeWithSelector } from "zustand/middleware";
17
+ import { createStore } from "zustand/vanilla";
18
+ import type { BaseSignerClient } from "./client/base";
19
+ import type { User } from "./client/types";
20
+ import { NotAuthenticatedError } from "./errors.js";
21
+ import {
22
+ SessionManager,
23
+ type SessionManagerParams,
24
+ } from "./session/manager.js";
25
+ import type { AuthParams } from "./signer";
26
+ import {
27
+ AlchemySignerStatus,
28
+ type AlchemySignerEvent,
29
+ type AlchemySignerEvents,
30
+ } from "./types.js";
31
+
32
+ export interface BaseAlchemySignerParams<TClient extends BaseSignerClient> {
33
+ client: TClient;
34
+ sessionConfig?: Omit<SessionManagerParams, "client">;
35
+ }
36
+
37
+ type AlchemySignerStore = {
38
+ user: User | null;
39
+ status: AlchemySignerStatus;
40
+ };
41
+
42
+ type InternalStore = Mutate<
43
+ StoreApi<AlchemySignerStore>,
44
+ [["zustand/subscribeWithSelector", never]]
45
+ >;
46
+
47
+ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
48
+ implements SmartAccountAuthenticator<AuthParams, User, TClient>
49
+ {
50
+ signerType: string = "alchemy-signer";
51
+ inner: TClient;
52
+ private sessionManager: SessionManager;
53
+ private store: InternalStore;
54
+
55
+ constructor({ client, sessionConfig }: BaseAlchemySignerParams<TClient>) {
56
+ this.inner = client;
57
+ this.store = createStore(
58
+ subscribeWithSelector(
59
+ () =>
60
+ ({
61
+ user: null,
62
+ status: AlchemySignerStatus.INITIALIZING,
63
+ } satisfies AlchemySignerStore)
64
+ )
65
+ );
66
+ // NOTE: it's important that the session manager share a client
67
+ // with the signer. The SessionManager leverages the Signer's client
68
+ // to manage session state.
69
+ this.sessionManager = new SessionManager({
70
+ ...sessionConfig,
71
+ client: this.inner,
72
+ });
73
+ this.store = createStore(
74
+ subscribeWithSelector(
75
+ () =>
76
+ ({
77
+ user: null,
78
+ status: AlchemySignerStatus.INITIALIZING,
79
+ } satisfies AlchemySignerStore)
80
+ )
81
+ );
82
+ // register listeners first
83
+ this.registerListeners();
84
+ // then initialize so that we can catch those events
85
+ this.sessionManager.initialize();
86
+ }
87
+
88
+ /**
89
+ * Allows you to subscribe to events emitted by the signer
90
+ *
91
+ * @param event the event to subscribe to
92
+ * @param listener the function to run when the event is emitted
93
+ * @returns a function to remove the listener
94
+ */
95
+ on = <E extends AlchemySignerEvent>(
96
+ event: E,
97
+ listener: AlchemySignerEvents[E]
98
+ ) => {
99
+ // NOTE: we're using zustand here to handle this because we are able to use the fireImmediately
100
+ // option which deals with a possible race condition where the listener is added after the event
101
+ // is fired. In the Client and SessionManager we use EventEmitter because it's easier to handle internally
102
+ switch (event) {
103
+ case "connected":
104
+ return this.store.subscribe(
105
+ ({ status }) => status,
106
+ (status) =>
107
+ status === AlchemySignerStatus.CONNECTED &&
108
+ (listener as AlchemySignerEvents["connected"])(
109
+ this.store.getState().user!
110
+ ),
111
+ { fireImmediately: true }
112
+ );
113
+ case "disconnected":
114
+ return this.store.subscribe(
115
+ ({ status }) => status,
116
+ (status) =>
117
+ status === AlchemySignerStatus.DISCONNECTED &&
118
+ (listener as AlchemySignerEvents["disconnected"])(),
119
+ { fireImmediately: true }
120
+ );
121
+ case "statusChanged":
122
+ return this.store.subscribe(
123
+ ({ status }) => status,
124
+ listener as AlchemySignerEvents["statusChanged"],
125
+ { fireImmediately: true }
126
+ );
127
+ default:
128
+ throw new Error(`Uknown event type ${event}`);
129
+ }
130
+ };
131
+
132
+ /**
133
+ * Authenticate a user with either an email or a passkey and create a session for that user
134
+ *
135
+ * @param params - undefined if passkey login, otherwise an object with email and bundle to resolve
136
+ * @returns the user that was authenticated
137
+ */
138
+ authenticate: (params: AuthParams) => Promise<User> = async (params) => {
139
+ if (params.type === "email") {
140
+ return this.authenticateWithEmail(params);
141
+ }
142
+
143
+ return this.authenticateWithPasskey(params);
144
+ };
145
+
146
+ /**
147
+ * NOTE: right now this only clears the session locally.
148
+ */
149
+ disconnect: () => Promise<void> = async () => {
150
+ await this.inner.disconnect();
151
+ };
152
+
153
+ /**
154
+ * Gets the current logged in user
155
+ * If a user has an ongoing session, it will use that session and
156
+ * try to authenticate
157
+ *
158
+ * @throws if there is no user logged in
159
+ * @returns the current user
160
+ */
161
+ getAuthDetails: () => Promise<User> = async () => {
162
+ const sessionUser = await this.sessionManager.getSessionUser();
163
+ if (sessionUser != null) {
164
+ return sessionUser;
165
+ }
166
+
167
+ return this.inner.whoami();
168
+ };
169
+
170
+ getAddress: () => Promise<`0x${string}`> = async () => {
171
+ const { address } = await this.inner.whoami();
172
+
173
+ return address;
174
+ };
175
+
176
+ signMessage: (msg: SignableMessage) => Promise<`0x${string}`> = async (
177
+ msg
178
+ ) => {
179
+ const messageHash = hashMessage(msg);
180
+
181
+ return this.inner.signRawMessage(messageHash);
182
+ };
183
+
184
+ signTypedData: <
185
+ const TTypedData extends TypedData | { [key: string]: unknown },
186
+ TPrimaryType extends keyof TTypedData | "EIP712Domain" = keyof TTypedData
187
+ >(
188
+ params: TypedDataDefinition<TTypedData, TPrimaryType>
189
+ ) => Promise<Hex> = async (params) => {
190
+ const messageHash = hashTypedData(params);
191
+
192
+ return this.inner.signRawMessage(messageHash);
193
+ };
194
+
195
+ signTransaction: CustomSource["signTransaction"] = async (tx, args) => {
196
+ const serializeFn = args?.serializer ?? serializeTransaction;
197
+ const serializedTx = serializeFn(tx);
198
+ const signatureHex = await this.inner.signRawMessage(
199
+ keccak256(serializedTx)
200
+ );
201
+
202
+ const signature = {
203
+ r: takeBytes(signatureHex, { count: 32 }),
204
+ s: takeBytes(signatureHex, { count: 32, offset: 32 }),
205
+ v: BigInt(takeBytes(signatureHex, { count: 1, offset: 64 })),
206
+ };
207
+
208
+ return serializeFn(tx, signature);
209
+ };
210
+
211
+ /**
212
+ * Unauthenticated call to look up a user's organizationId by email
213
+ *
214
+ * @param email the email to lookup
215
+ * @returns the organization id for the user if they exist
216
+ */
217
+ getUser: (email: string) => Promise<{ orgId: string } | null> = async (
218
+ email
219
+ ) => {
220
+ const result = await this.inner.lookupUserByEmail(email);
221
+
222
+ if (result.orgId == null) {
223
+ return null;
224
+ }
225
+
226
+ return {
227
+ orgId: result.orgId,
228
+ };
229
+ };
230
+
231
+ /**
232
+ * Adds a passkey to the user's account
233
+ *
234
+ * @param params optional parameters for the passkey creation
235
+ * @returns an array of the authenticator ids added to the user
236
+ */
237
+ addPasskey: (params?: CredentialCreationOptions) => Promise<string[]> =
238
+ async (params) => {
239
+ return this.inner.addPasskey(params ?? {});
240
+ };
241
+
242
+ /**
243
+ * Used to export the wallet for a given user
244
+ * If the user is authenticated with an Email, this will return a seed phrase
245
+ * If the user is authenticated with a Passkey, this will return a private key
246
+ *
247
+ * @param params export wallet parameters
248
+ * @returns true if the wallet was exported successfully
249
+ */
250
+ exportWallet: (
251
+ params: Parameters<(typeof this.inner)["exportWallet"]>[0]
252
+ ) => Promise<boolean> = async (params) => {
253
+ return this.inner.exportWallet(params);
254
+ };
255
+
256
+ /**
257
+ * This method lets you adapt your AlchemySigner to a viem LocalAccount, which
258
+ * will let you use the signer as an EOA directly.
259
+ *
260
+ * @throws if your signer is not authenticated
261
+ * @returns a LocalAccount object that can be used with viem's wallet client
262
+ */
263
+ toViemAccount: () => LocalAccount = () => {
264
+ // if we want this method to be synchronous, then we need to do this check here
265
+ // otherwise we can use the sessionManager to get the user
266
+ if (!this.inner.getUser()) {
267
+ throw new NotAuthenticatedError();
268
+ }
269
+
270
+ return toAccount({
271
+ address: this.inner.getUser()!.address,
272
+ signMessage: (msg) => this.signMessage(msg.message),
273
+ signTypedData: <
274
+ const typedData extends TypedData | Record<string, unknown>,
275
+ primaryType extends keyof typedData | "EIP712Domain" = keyof typedData
276
+ >(
277
+ typedDataDefinition: TypedDataDefinition<typedData, primaryType>
278
+ ) => this.signTypedData<typedData, primaryType>(typedDataDefinition),
279
+ signTransaction: this.signTransaction,
280
+ });
281
+ };
282
+
283
+ private authenticateWithEmail = async (
284
+ params: Extract<AuthParams, { type: "email" }>
285
+ ): Promise<User> => {
286
+ if ("email" in params) {
287
+ const existingUser = await this.getUser(params.email);
288
+
289
+ const { orgId } = existingUser
290
+ ? await this.inner.initEmailAuth({
291
+ email: params.email,
292
+ expirationSeconds: this.sessionManager.expirationTimeMs,
293
+ redirectParams: params.redirectParams,
294
+ })
295
+ : await this.inner.createAccount({
296
+ type: "email",
297
+ email: params.email,
298
+ expirationSeconds: this.sessionManager.expirationTimeMs,
299
+ redirectParams: params.redirectParams,
300
+ });
301
+
302
+ this.sessionManager.setTemporarySession({ orgId });
303
+ this.store.setState({ status: AlchemySignerStatus.AWAITING_EMAIL_AUTH });
304
+
305
+ // We wait for the session manager to emit a connected event if
306
+ // cross tab sessions are permitted
307
+ return new Promise<User>((resolve) => {
308
+ const removeListener = this.sessionManager.on(
309
+ "connected",
310
+ (session) => {
311
+ resolve(session.user);
312
+ removeListener();
313
+ }
314
+ );
315
+ });
316
+ } else {
317
+ const temporarySession = params.orgId
318
+ ? { orgId: params.orgId }
319
+ : this.sessionManager.getTemporarySession();
320
+
321
+ if (!temporarySession) {
322
+ this.store.setState({ status: AlchemySignerStatus.DISCONNECTED });
323
+ throw new Error("Could not find email auth init session!");
324
+ }
325
+
326
+ const user = await this.inner.completeEmailAuth({
327
+ bundle: params.bundle,
328
+ orgId: temporarySession.orgId,
329
+ });
330
+
331
+ return user;
332
+ }
333
+ };
334
+
335
+ private authenticateWithPasskey = async (
336
+ args: Extract<AuthParams, { type: "passkey" }>
337
+ ) => {
338
+ let user: User;
339
+ if (args.createNew) {
340
+ const result = await this.inner.createAccount(args);
341
+ // account creation for passkeys returns the whoami response so we don't have to
342
+ // call it again after signup
343
+ user = {
344
+ address: result.address!,
345
+ userId: result.userId!,
346
+ orgId: result.orgId,
347
+ };
348
+ } else {
349
+ user = await this.inner.lookupUserWithPasskey();
350
+ if (!user) {
351
+ this.store.setState({ status: AlchemySignerStatus.DISCONNECTED });
352
+ throw new Error("No user found");
353
+ }
354
+ }
355
+
356
+ return user;
357
+ };
358
+
359
+ private registerListeners = () => {
360
+ this.sessionManager.on("connected", (session) => {
361
+ this.store.setState({
362
+ user: session.user,
363
+ status: AlchemySignerStatus.CONNECTED,
364
+ });
365
+ });
366
+
367
+ this.sessionManager.on("disconnected", () => {
368
+ this.store.setState({
369
+ user: null,
370
+ status: AlchemySignerStatus.DISCONNECTED,
371
+ });
372
+ });
373
+
374
+ this.sessionManager.on("initialized", () => {
375
+ this.store.setState((state) => ({
376
+ status: state.user
377
+ ? AlchemySignerStatus.CONNECTED
378
+ : AlchemySignerStatus.DISCONNECTED,
379
+ }));
380
+ });
381
+
382
+ this.inner.on("authenticating", () => {
383
+ this.store.setState({ status: AlchemySignerStatus.AUTHENTICATING });
384
+ });
385
+ };
386
+ }