@naculus/connect-appkit-react 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/api-extractor.json +21 -0
  3. package/dist/client.cjs +2 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.js +2 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/index.cjs +2 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +39 -0
  12. package/src/chain-abstraction/index.ts +8 -0
  13. package/src/chain-abstraction/useCompareCosts.ts +137 -0
  14. package/src/chain-abstraction/useExecuteRoute.ts +129 -0
  15. package/src/chain-abstraction/useRouteQuote.ts +168 -0
  16. package/src/client.test.ts +242 -0
  17. package/src/client.ts +265 -0
  18. package/src/hooks/useAccount.test.ts +67 -0
  19. package/src/hooks/useAccount.ts +37 -0
  20. package/src/hooks/useBalance.test.ts +459 -0
  21. package/src/hooks/useBalance.ts +170 -0
  22. package/src/hooks/useChain.test.ts +160 -0
  23. package/src/hooks/useChain.ts +53 -0
  24. package/src/hooks/useConnect.test.ts +112 -0
  25. package/src/hooks/useConnect.ts +29 -0
  26. package/src/hooks/useDisconnect.test.ts +75 -0
  27. package/src/hooks/useDisconnect.ts +32 -0
  28. package/src/hooks/useERC20Allowance.ts +93 -0
  29. package/src/hooks/useERC20Approve.ts +220 -0
  30. package/src/hooks/useERC20Transfer.ts +134 -0
  31. package/src/hooks/useERC20TransferSimulation.ts +147 -0
  32. package/src/hooks/useEmbeddedWallet.test.ts +266 -0
  33. package/src/hooks/useEmbeddedWallet.ts +181 -0
  34. package/src/hooks/useLastTx.test.ts +117 -0
  35. package/src/hooks/useLastTx.ts +73 -0
  36. package/src/hooks/useLookupAddress.ts +131 -0
  37. package/src/hooks/useNotification.ts +308 -0
  38. package/src/hooks/useResolveName.ts +128 -0
  39. package/src/hooks/useSIWxLogin.test.ts +307 -0
  40. package/src/hooks/useSIWxLogin.ts +163 -0
  41. package/src/hooks/useSIWxSession.test.ts +481 -0
  42. package/src/hooks/useSIWxSession.ts +290 -0
  43. package/src/hooks/useSendCalls.ts +103 -0
  44. package/src/hooks/useSendTransaction.test.ts +175 -0
  45. package/src/hooks/useSendTransaction.ts +80 -0
  46. package/src/hooks/useSendUserOperation.ts +313 -0
  47. package/src/hooks/useSession.ts +96 -0
  48. package/src/hooks/useSessionKeys.ts +356 -0
  49. package/src/hooks/useSignInWithEthereum.test.ts +224 -0
  50. package/src/hooks/useSignInWithEthereum.ts +46 -0
  51. package/src/hooks/useSignInWithX.test.ts +210 -0
  52. package/src/hooks/useSignInWithX.ts +141 -0
  53. package/src/hooks/useSignMessage.test.ts +151 -0
  54. package/src/hooks/useSignMessage.ts +65 -0
  55. package/src/hooks/useSimulateTransfer.ts +165 -0
  56. package/src/hooks/useSiwxAuthSession.test.ts +456 -0
  57. package/src/hooks/useSiwxAuthSession.ts +163 -0
  58. package/src/hooks/useSmartAccount.ts +194 -0
  59. package/src/hooks/useSwitchChain.ts +68 -0
  60. package/src/hooks/useTokenBalance.test.ts +337 -0
  61. package/src/hooks/useTokenBalance.ts +210 -0
  62. package/src/hooks/useTransactionSimulation.ts +401 -0
  63. package/src/hooks/useTxHistory.test.ts +108 -0
  64. package/src/hooks/useTxHistory.ts +83 -0
  65. package/src/hooks/useTxMonitor.test.ts +127 -0
  66. package/src/hooks/useTxMonitor.ts +178 -0
  67. package/src/hooks/useUserOpStatus.ts +245 -0
  68. package/src/hooks/useValidateDestination.test.ts +96 -0
  69. package/src/hooks/useValidateDestination.ts +72 -0
  70. package/src/hooks/useViemClient.test.ts +155 -0
  71. package/src/hooks/useViemClient.ts +65 -0
  72. package/src/hooks/useWallet.test.ts +133 -0
  73. package/src/hooks/useWallet.ts +39 -0
  74. package/src/hooks/useWeb3ErrorHandler.ts +102 -0
  75. package/src/index.ts +186 -0
  76. package/src/provider/Web3ConnectProvider.test.tsx +178 -0
  77. package/src/provider/Web3ConnectProvider.tsx +530 -0
  78. package/src/token-list/TokenSelector.tsx +351 -0
  79. package/src/token-list/defaults.ts +39 -0
  80. package/src/token-list/index.ts +14 -0
  81. package/src/token-list/useTokenList.ts +119 -0
  82. package/src/token-list/useTokenSearch.ts +99 -0
  83. package/src/types/index.ts +99 -0
  84. package/src/utils/chains.ts +115 -0
  85. package/src/utils/errorMessages.ts +142 -0
  86. package/src/utils/i18n.ts +90 -0
  87. package/src/wc-generated/components.ts +377 -0
  88. package/test/setup.ts +17 -0
  89. package/test/test-utils.tsx +78 -0
  90. package/tsup.config.ts +18 -0
@@ -0,0 +1,356 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useEffect, useRef } from "react";
4
+ import { useWeb3 } from "../provider/Web3ConnectProvider";
5
+ import { getClient } from "../client";
6
+ import { WalletError, SessionKeyManager, MemoryStorageAdapter } from "@naculus/connect-core";
7
+ import type {
8
+ SessionKeyScope,
9
+ SessionKeyInfo,
10
+ SessionKeyManagerConfig,
11
+ } from "@naculus/connect-core";
12
+
13
+ // ─── Internal singleton for session key management ─────────────────────
14
+ // In a full production setup, this would be injected via context or client config.
15
+
16
+ let globalSessionKeyManager: SessionKeyManager | null = null;
17
+
18
+ function getSessionKeyManager(config?: SessionKeyManagerConfig): SessionKeyManager {
19
+ if (!globalSessionKeyManager) {
20
+ globalSessionKeyManager = new SessionKeyManager(
21
+ config,
22
+ // Use MemoryStorageAdapter for SSR safety; browser integration will use LocalStorageAdapter
23
+ typeof window !== "undefined" ? undefined : new MemoryStorageAdapter(),
24
+ );
25
+ }
26
+ return globalSessionKeyManager;
27
+ }
28
+
29
+ // ─── useSessionKeys ────────────────────────────────────────────────────
30
+
31
+ export interface UseSessionKeysReturn {
32
+ /** All session keys (active, revoked, expired) */
33
+ sessions: SessionKeyInfo[];
34
+ /** Only active session keys */
35
+ activeSessions: SessionKeyInfo[];
36
+ /** Whether data is loading */
37
+ loading: boolean;
38
+ /** Error from last operation */
39
+ error: Error | null;
40
+ /** Refresh the session list from storage */
41
+ refresh: () => Promise<void>;
42
+ /** Clear any error */
43
+ clearError: () => void;
44
+ /** Whether the storage backend is available */
45
+ storageAvailable: boolean;
46
+ }
47
+
48
+ /**
49
+ * Hook to list and manage session keys.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * const { sessions, activeSessions, refresh } = useSessionKeys();
54
+ * ```
55
+ */
56
+ export function useSessionKeys(): UseSessionKeysReturn {
57
+ const { session } = useWeb3();
58
+ const [sessions, setSessions] = useState<SessionKeyInfo[]>([]);
59
+ const [loading, setLoading] = useState(false);
60
+ const [error, setError] = useState<Error | null>(null);
61
+ const mountedRef = useRef(true);
62
+
63
+ const clearError = useCallback(() => setError(null), []);
64
+
65
+ const manager = getSessionKeyManager();
66
+ const storageAvailable = manager.isStorageAvailable();
67
+
68
+ const refresh = useCallback(async () => {
69
+ if (!session) {
70
+ setSessions([]);
71
+ return;
72
+ }
73
+
74
+ setLoading(true);
75
+ setError(null);
76
+ try {
77
+ const keys = await manager.listSessions();
78
+ if (mountedRef.current) {
79
+ setSessions(keys);
80
+ }
81
+ } catch (err) {
82
+ if (mountedRef.current) {
83
+ setError(err instanceof Error ? err : new Error("Failed to load session keys"));
84
+ }
85
+ } finally {
86
+ if (mountedRef.current) {
87
+ setLoading(false);
88
+ }
89
+ }
90
+ }, [session, manager]);
91
+
92
+ // Load sessions on mount and on session change
93
+ useEffect(() => {
94
+ mountedRef.current = true;
95
+ refresh();
96
+ return () => {
97
+ mountedRef.current = false;
98
+ };
99
+ }, [refresh]);
100
+
101
+ const activeSessions = sessions.filter((s) => s.status === "active");
102
+
103
+ return {
104
+ sessions,
105
+ activeSessions,
106
+ loading,
107
+ error,
108
+ refresh,
109
+ clearError,
110
+ storageAvailable,
111
+ };
112
+ }
113
+
114
+ // ─── useCreateSessionKey ───────────────────────────────────────────────
115
+
116
+ export interface UseCreateSessionKeyReturn {
117
+ /** Create a new session key */
118
+ createSessionKey: (
119
+ scope?: Partial<SessionKeyScope>,
120
+ signerAddress?: `0x${string}`,
121
+ ) => Promise<SessionKeyInfo>;
122
+ /** The last created session key info */
123
+ lastCreated: SessionKeyInfo | null;
124
+ /** Whether creation is in progress */
125
+ isCreating: boolean;
126
+ /** Error from last creation */
127
+ error: Error | null;
128
+ /** Clear error */
129
+ clearError: () => void;
130
+ }
131
+
132
+ /**
133
+ * Hook to create new session keys.
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * const { createSessionKey, lastCreated, isCreating } = useCreateSessionKey();
138
+ *
139
+ * const handleCreate = async () => {
140
+ * const session = await createSessionKey({
141
+ * expiry: Math.floor(Date.now() / 1000) + 1800, // 30 min
142
+ * maxTotalValue: parseEther("0.1"),
143
+ * });
144
+ * };
145
+ * ```
146
+ */
147
+ export function useCreateSessionKey(): UseCreateSessionKeyReturn {
148
+ const { session } = useWeb3();
149
+ const [lastCreated, setLastCreated] = useState<SessionKeyInfo | null>(null);
150
+ const [isCreating, setIsCreating] = useState(false);
151
+ const [error, setError] = useState<Error | null>(null);
152
+
153
+ const clearError = useCallback(() => setError(null), []);
154
+
155
+ const manager = getSessionKeyManager();
156
+
157
+ const createSessionKey = useCallback(
158
+ async (
159
+ scope?: Partial<SessionKeyScope>,
160
+ signerAddress?: `0x${string}`,
161
+ ): Promise<SessionKeyInfo> => {
162
+ if (!session) {
163
+ throw new WalletError("wallet_unavailable", "No active wallet session");
164
+ }
165
+
166
+ setIsCreating(true);
167
+ setError(null);
168
+ try {
169
+ const resolvedSigner = signerAddress ?? undefined;
170
+ const info = await manager.createSessionKey(scope, resolvedSigner);
171
+ setLastCreated(info);
172
+ return info;
173
+ } catch (err) {
174
+ const errorObj = err instanceof Error ? err : new Error("Failed to create session key");
175
+ setError(errorObj);
176
+ throw errorObj;
177
+ } finally {
178
+ setIsCreating(false);
179
+ }
180
+ },
181
+ [session, manager],
182
+ );
183
+
184
+ return {
185
+ createSessionKey,
186
+ lastCreated,
187
+ isCreating,
188
+ error,
189
+ clearError,
190
+ };
191
+ }
192
+
193
+ // ─── useRevokeSession ──────────────────────────────────────────────────
194
+
195
+ export interface UseRevokeSessionReturn {
196
+ /** Revoke a session key by ID */
197
+ revokeSession: (sessionId: string) => Promise<void>;
198
+ /** Whether revoking is in progress */
199
+ isRevoking: boolean;
200
+ /** Error from last operation */
201
+ error: Error | null;
202
+ /** Clear error */
203
+ clearError: () => void;
204
+ }
205
+
206
+ /**
207
+ * Hook to revoke a session key.
208
+ *
209
+ * @example
210
+ * ```tsx
211
+ * const { revokeSession, isRevoking } = useRevokeSession();
212
+ * await revokeSession(sessionId);
213
+ * ```
214
+ */
215
+ export function useRevokeSession(): UseRevokeSessionReturn {
216
+ const [isRevoking, setIsRevoking] = useState(false);
217
+ const [error, setError] = useState<Error | null>(null);
218
+
219
+ const clearError = useCallback(() => setError(null), []);
220
+
221
+ const manager = getSessionKeyManager();
222
+
223
+ const revokeSession = useCallback(
224
+ async (sessionId: string): Promise<void> => {
225
+ setIsRevoking(true);
226
+ setError(null);
227
+ try {
228
+ await manager.revokeSession(sessionId);
229
+ } catch (err) {
230
+ const errorObj = err instanceof Error ? err : new Error("Failed to revoke session key");
231
+ setError(errorObj);
232
+ throw errorObj;
233
+ } finally {
234
+ setIsRevoking(false);
235
+ }
236
+ },
237
+ [manager],
238
+ );
239
+
240
+ return {
241
+ revokeSession,
242
+ isRevoking,
243
+ error,
244
+ clearError,
245
+ };
246
+ }
247
+
248
+ // ─── useSendWithSession ────────────────────────────────────────────────
249
+
250
+ export interface UseSendWithSessionReturn {
251
+ /** Sign a message hash using a session key */
252
+ signWithSession: (sessionId: string, messageHash: `0x${string}`) => Promise<`0x${string}`>;
253
+ /** Check if a transaction is within a session's scope */
254
+ checkScope: (
255
+ sessionId: string,
256
+ tx: {
257
+ to?: string;
258
+ value?: string;
259
+ data?: string;
260
+ chainId?: number;
261
+ gas?: string;
262
+ },
263
+ ) => Promise<{
264
+ valid: boolean;
265
+ reason?: string;
266
+ remainingGas?: bigint;
267
+ remainingValue?: bigint;
268
+ remainingTxCount?: number;
269
+ }>;
270
+ /** Whether an operation is in progress */
271
+ isBusy: boolean;
272
+ /** Error from last operation */
273
+ error: Error | null;
274
+ /** Clear error */
275
+ clearError: () => void;
276
+ }
277
+
278
+ /**
279
+ * Hook to sign transactions using session keys and check scope.
280
+ *
281
+ * @example
282
+ * ```tsx
283
+ * const { signWithSession, checkScope } = useSendWithSession();
284
+ *
285
+ * const check = await checkScope(sessionId, tx);
286
+ * if (check.valid) {
287
+ * const sig = await signWithSession(sessionId, messageHash);
288
+ * }
289
+ * ```
290
+ */
291
+ export function useSendWithSession(): UseSendWithSessionReturn {
292
+ const [isBusy, setIsBusy] = useState(false);
293
+ const [error, setError] = useState<Error | null>(null);
294
+
295
+ const clearError = useCallback(() => setError(null), []);
296
+
297
+ const manager = getSessionKeyManager();
298
+
299
+ const signWithSession = useCallback(
300
+ async (sessionId: string, messageHash: `0x${string}`): Promise<`0x${string}`> => {
301
+ setIsBusy(true);
302
+ setError(null);
303
+ try {
304
+ return await manager.signWithSessionKey(sessionId, messageHash);
305
+ } catch (err) {
306
+ const errorObj = err instanceof Error ? err : new Error("Failed to sign with session key");
307
+ setError(errorObj);
308
+ throw errorObj;
309
+ } finally {
310
+ setIsBusy(false);
311
+ }
312
+ },
313
+ [manager],
314
+ );
315
+
316
+ const checkScope = useCallback(
317
+ async (
318
+ sessionId: string,
319
+ tx: {
320
+ to?: string;
321
+ value?: string;
322
+ data?: string;
323
+ chainId?: number;
324
+ gas?: string;
325
+ },
326
+ ) => {
327
+ setIsBusy(true);
328
+ setError(null);
329
+ try {
330
+ return await manager.checkSessionScope(sessionId, tx);
331
+ } catch (err) {
332
+ const errorObj = err instanceof Error ? err : new Error("Failed to check session scope");
333
+ setError(errorObj);
334
+ throw errorObj;
335
+ } finally {
336
+ setIsBusy(false);
337
+ }
338
+ },
339
+ [manager],
340
+ );
341
+
342
+ return {
343
+ signWithSession,
344
+ checkScope,
345
+ isBusy,
346
+ error,
347
+ clearError,
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Reset the global session key manager (useful for testing / cleanup).
353
+ */
354
+ export function resetSessionKeyManager(): void {
355
+ globalSessionKeyManager = null;
356
+ }
@@ -0,0 +1,224 @@
1
+ /// <reference types="vitest" />
2
+ /// @vitest-environment jsdom
3
+
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { renderHook, act } from "@testing-library/react";
6
+
7
+ const mockUseWeb3 = vi.fn();
8
+ vi.mock("../provider/Web3ConnectProvider", () => ({
9
+ useWeb3: () => mockUseWeb3(),
10
+ Web3ConnectProvider: ({ children }: { children: React.ReactNode }) => children,
11
+ }));
12
+
13
+ const mockSignMessage = vi.fn();
14
+ const mockGetClient = vi.fn();
15
+ vi.mock("../client", () => ({
16
+ getClient: () => mockGetClient(),
17
+ }));
18
+
19
+ import { useSignInWithEthereum } from "./useSignInWithEthereum";
20
+
21
+ function createEvmSession() {
22
+ return {
23
+ topic: "test-topic",
24
+ namespaces: {
25
+ eip155: {
26
+ chains: ["eip155:1"],
27
+ accounts: ["eip155:1:0x1234567890123456789012345678901234567890"],
28
+ },
29
+ },
30
+ };
31
+ }
32
+
33
+ describe("useSignInWithEthereum", () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ it("should have initial idle state", () => {
39
+ mockUseWeb3.mockReturnValue({ session: null, chainId: null });
40
+ mockGetClient.mockReturnValue(null);
41
+
42
+ const { result } = renderHook(() => useSignInWithEthereum());
43
+
44
+ expect(result.current.isSigningIn).toBe(false);
45
+ expect(result.current.result).toBeNull();
46
+ expect(result.current.error).toBeNull();
47
+ });
48
+
49
+ it("should throw when no session", async () => {
50
+ mockUseWeb3.mockReturnValue({ session: null, chainId: null });
51
+ mockGetClient.mockReturnValue({
52
+ signMessage: mockSignMessage,
53
+ });
54
+
55
+ const { result } = renderHook(() => useSignInWithEthereum());
56
+
57
+ await expect(result.current.signIn()).rejects.toThrow("No active session");
58
+ });
59
+
60
+ it("should throw when client not initialized", async () => {
61
+ mockUseWeb3.mockReturnValue({ session: createEvmSession(), chainId: "eip155:1" });
62
+ mockGetClient.mockReturnValue(null);
63
+
64
+ const { result } = renderHook(() => useSignInWithEthereum());
65
+
66
+ await expect(result.current.signIn()).rejects.toThrow("Client not initialized");
67
+ });
68
+
69
+ it("should throw when no EVM accounts in session", async () => {
70
+ const solanaSession = {
71
+ topic: "test-topic",
72
+ namespaces: {
73
+ solana: {
74
+ chains: ["solana:4sGjMW1s"],
75
+ accounts: ["solana:4sGjMW1s:5JG7DPRQAxJVCLx3GxsHTxP1KZbSyLmPQBLkLhFZPGrH"],
76
+ },
77
+ },
78
+ };
79
+
80
+ mockUseWeb3.mockReturnValue({ session: solanaSession, chainId: null });
81
+ mockGetClient.mockReturnValue({
82
+ signMessage: mockSignMessage,
83
+ });
84
+
85
+ const { result } = renderHook(() => useSignInWithEthereum());
86
+
87
+ await expect(result.current.signIn()).rejects.toThrow(
88
+ "No EVM (eip155) accounts found in session"
89
+ );
90
+ });
91
+
92
+ it("should sign in with Ethereum successfully", async () => {
93
+ const mockSignature = "0xsiwsignature1234567890abcdef";
94
+ mockSignMessage.mockResolvedValue(mockSignature);
95
+ mockUseWeb3.mockReturnValue({
96
+ session: createEvmSession(),
97
+ chainId: "eip155:1",
98
+ });
99
+ mockGetClient.mockReturnValue({
100
+ signMessage: mockSignMessage,
101
+ });
102
+
103
+ const { result } = renderHook(() => useSignInWithEthereum());
104
+
105
+ let siwxResult: Awaited<ReturnType<typeof result.current.signIn>> | undefined;
106
+
107
+ await act(async () => {
108
+ siwxResult = await result.current.signIn({
109
+ statement: "Sign in to MyApp",
110
+ });
111
+ });
112
+
113
+ expect(siwxResult!).toBeDefined();
114
+ expect(siwxResult!.signature).toBe(mockSignature);
115
+ expect(siwxResult!.message.domain).toBe("localhost:3000");
116
+ expect(siwxResult!.message.address).toBe("eip155:1:0x1234567890123456789012345678901234567890");
117
+ expect(siwxResult!.message.statement).toBe("Sign in to MyApp");
118
+ expect(siwxResult!.message.version).toBe(1);
119
+ expect(siwxResult!.message.chainId).toBe("eip155:1");
120
+ expect(siwxResult!.message.nonce).toHaveLength(16);
121
+ expect(siwxResult!.message.issuedAt).toBeTruthy();
122
+ expect(mockSignMessage).toHaveBeenCalledTimes(1);
123
+ expect(result.current.isSigningIn).toBe(false);
124
+ expect(result.current.result).toBe(siwxResult);
125
+ });
126
+
127
+ it("should set loading state during signing", async () => {
128
+ let resolvePromise: (v: string) => void;
129
+ const signPromise = new Promise<string>((resolve) => {
130
+ resolvePromise = resolve;
131
+ });
132
+ mockSignMessage.mockReturnValue(signPromise);
133
+ mockUseWeb3.mockReturnValue({
134
+ session: createEvmSession(),
135
+ chainId: "eip155:1",
136
+ });
137
+ mockGetClient.mockReturnValue({
138
+ signMessage: mockSignMessage,
139
+ });
140
+
141
+ const { result } = renderHook(() => useSignInWithEthereum());
142
+
143
+ let callPromise: Promise<unknown>;
144
+ act(() => {
145
+ callPromise = result.current.signIn();
146
+ });
147
+
148
+ expect(result.current.isSigningIn).toBe(true);
149
+
150
+ await act(async () => {
151
+ resolvePromise!("0xsig");
152
+ await callPromise!;
153
+ });
154
+
155
+ expect(result.current.isSigningIn).toBe(false);
156
+ });
157
+
158
+ it("should capture error on failed signing", async () => {
159
+ mockSignMessage.mockRejectedValue(new Error("User rejected"));
160
+ mockUseWeb3.mockReturnValue({
161
+ session: createEvmSession(),
162
+ chainId: "eip155:1",
163
+ });
164
+ mockGetClient.mockReturnValue({
165
+ signMessage: mockSignMessage,
166
+ });
167
+
168
+ const { result } = renderHook(() => useSignInWithEthereum());
169
+
170
+ await act(async () => {
171
+ try {
172
+ await result.current.signIn();
173
+ } catch {
174
+ // Expected
175
+ }
176
+ });
177
+
178
+ expect(result.current.error).not.toBeNull();
179
+ expect(result.current.error?.message).toBe("User rejected");
180
+ });
181
+
182
+ it("should clear error", () => {
183
+ mockUseWeb3.mockReturnValue({ session: null, chainId: null });
184
+ mockGetClient.mockReturnValue(null);
185
+
186
+ const { result } = renderHook(() => useSignInWithEthereum());
187
+
188
+ act(() => {
189
+ result.current.clearError();
190
+ });
191
+
192
+ expect(result.current.error).toBeNull();
193
+ });
194
+
195
+ it("should accept custom domain and URI", async () => {
196
+ const mockSignature = "0xsig";
197
+ mockSignMessage.mockResolvedValue(mockSignature);
198
+ mockUseWeb3.mockReturnValue({
199
+ session: createEvmSession(),
200
+ chainId: "eip155:1",
201
+ });
202
+ mockGetClient.mockReturnValue({
203
+ signMessage: mockSignMessage,
204
+ });
205
+
206
+ const { result } = renderHook(() => useSignInWithEthereum());
207
+
208
+ let siwxResult: Awaited<ReturnType<typeof result.current.signIn>> | undefined;
209
+
210
+ await act(async () => {
211
+ siwxResult = await result.current.signIn({
212
+ domain: "app.example.com",
213
+ uri: "https://app.example.com/login",
214
+ expirySeconds: 3600,
215
+ resources: ["https://app.example.com/privacy"],
216
+ });
217
+ });
218
+
219
+ expect(siwxResult!.message.domain).toBe("app.example.com");
220
+ expect(siwxResult!.message.uri).toBe("https://app.example.com/login");
221
+ expect(siwxResult!.message.expirationTime).toBeTruthy();
222
+ expect(siwxResult!.message.resources).toContain("https://app.example.com/privacy");
223
+ });
224
+ });
@@ -0,0 +1,46 @@
1
+ import { useSignInWithX } from "./useSignInWithX";
2
+ import type { UseSignInWithXOptions, UseSignInWithXReturn } from "./useSignInWithX";
3
+ import { WalletError } from "@naculus/connect-core";
4
+ import { useWeb3 } from "../provider/Web3ConnectProvider";
5
+ import { useCallback } from "react";
6
+
7
+ export interface UseSignInWithEthereumOptions extends UseSignInWithXOptions {}
8
+
9
+ export interface UseSignInWithEthereumReturn extends UseSignInWithXReturn {}
10
+
11
+ function useEip155SessionGuard(): () => void {
12
+ const { session } = useWeb3();
13
+ return useCallback(() => {
14
+ if (!session) {
15
+ throw new WalletError("wallet_unavailable", "No active session");
16
+ }
17
+ const evmNamespace = session.namespaces["eip155"];
18
+ if (!evmNamespace || evmNamespace.accounts.length === 0) {
19
+ throw new WalletError(
20
+ "namespace_mismatch",
21
+ "No EVM (eip155) accounts found in session"
22
+ );
23
+ }
24
+ }, [session]);
25
+ }
26
+
27
+ export function useSignInWithEthereum(): UseSignInWithEthereumReturn {
28
+ const guard = useEip155SessionGuard();
29
+ const siwx = useSignInWithX();
30
+
31
+ const signIn = useCallback(
32
+ async (options?: UseSignInWithEthereumOptions) => {
33
+ guard();
34
+ return siwx.signIn(options);
35
+ },
36
+ [guard, siwx.signIn]
37
+ );
38
+
39
+ return {
40
+ signIn,
41
+ isSigningIn: siwx.isSigningIn,
42
+ result: siwx.result,
43
+ error: siwx.error,
44
+ clearError: siwx.clearError,
45
+ };
46
+ }