@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.
- package/LICENSE +21 -0
- package/api-extractor.json +21 -0
- package/dist/client.cjs +2 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/chain-abstraction/index.ts +8 -0
- package/src/chain-abstraction/useCompareCosts.ts +137 -0
- package/src/chain-abstraction/useExecuteRoute.ts +129 -0
- package/src/chain-abstraction/useRouteQuote.ts +168 -0
- package/src/client.test.ts +242 -0
- package/src/client.ts +265 -0
- package/src/hooks/useAccount.test.ts +67 -0
- package/src/hooks/useAccount.ts +37 -0
- package/src/hooks/useBalance.test.ts +459 -0
- package/src/hooks/useBalance.ts +170 -0
- package/src/hooks/useChain.test.ts +160 -0
- package/src/hooks/useChain.ts +53 -0
- package/src/hooks/useConnect.test.ts +112 -0
- package/src/hooks/useConnect.ts +29 -0
- package/src/hooks/useDisconnect.test.ts +75 -0
- package/src/hooks/useDisconnect.ts +32 -0
- package/src/hooks/useERC20Allowance.ts +93 -0
- package/src/hooks/useERC20Approve.ts +220 -0
- package/src/hooks/useERC20Transfer.ts +134 -0
- package/src/hooks/useERC20TransferSimulation.ts +147 -0
- package/src/hooks/useEmbeddedWallet.test.ts +266 -0
- package/src/hooks/useEmbeddedWallet.ts +181 -0
- package/src/hooks/useLastTx.test.ts +117 -0
- package/src/hooks/useLastTx.ts +73 -0
- package/src/hooks/useLookupAddress.ts +131 -0
- package/src/hooks/useNotification.ts +308 -0
- package/src/hooks/useResolveName.ts +128 -0
- package/src/hooks/useSIWxLogin.test.ts +307 -0
- package/src/hooks/useSIWxLogin.ts +163 -0
- package/src/hooks/useSIWxSession.test.ts +481 -0
- package/src/hooks/useSIWxSession.ts +290 -0
- package/src/hooks/useSendCalls.ts +103 -0
- package/src/hooks/useSendTransaction.test.ts +175 -0
- package/src/hooks/useSendTransaction.ts +80 -0
- package/src/hooks/useSendUserOperation.ts +313 -0
- package/src/hooks/useSession.ts +96 -0
- package/src/hooks/useSessionKeys.ts +356 -0
- package/src/hooks/useSignInWithEthereum.test.ts +224 -0
- package/src/hooks/useSignInWithEthereum.ts +46 -0
- package/src/hooks/useSignInWithX.test.ts +210 -0
- package/src/hooks/useSignInWithX.ts +141 -0
- package/src/hooks/useSignMessage.test.ts +151 -0
- package/src/hooks/useSignMessage.ts +65 -0
- package/src/hooks/useSimulateTransfer.ts +165 -0
- package/src/hooks/useSiwxAuthSession.test.ts +456 -0
- package/src/hooks/useSiwxAuthSession.ts +163 -0
- package/src/hooks/useSmartAccount.ts +194 -0
- package/src/hooks/useSwitchChain.ts +68 -0
- package/src/hooks/useTokenBalance.test.ts +337 -0
- package/src/hooks/useTokenBalance.ts +210 -0
- package/src/hooks/useTransactionSimulation.ts +401 -0
- package/src/hooks/useTxHistory.test.ts +108 -0
- package/src/hooks/useTxHistory.ts +83 -0
- package/src/hooks/useTxMonitor.test.ts +127 -0
- package/src/hooks/useTxMonitor.ts +178 -0
- package/src/hooks/useUserOpStatus.ts +245 -0
- package/src/hooks/useValidateDestination.test.ts +96 -0
- package/src/hooks/useValidateDestination.ts +72 -0
- package/src/hooks/useViemClient.test.ts +155 -0
- package/src/hooks/useViemClient.ts +65 -0
- package/src/hooks/useWallet.test.ts +133 -0
- package/src/hooks/useWallet.ts +39 -0
- package/src/hooks/useWeb3ErrorHandler.ts +102 -0
- package/src/index.ts +186 -0
- package/src/provider/Web3ConnectProvider.test.tsx +178 -0
- package/src/provider/Web3ConnectProvider.tsx +530 -0
- package/src/token-list/TokenSelector.tsx +351 -0
- package/src/token-list/defaults.ts +39 -0
- package/src/token-list/index.ts +14 -0
- package/src/token-list/useTokenList.ts +119 -0
- package/src/token-list/useTokenSearch.ts +99 -0
- package/src/types/index.ts +99 -0
- package/src/utils/chains.ts +115 -0
- package/src/utils/errorMessages.ts +142 -0
- package/src/utils/i18n.ts +90 -0
- package/src/wc-generated/components.ts +377 -0
- package/test/setup.ts +17 -0
- package/test/test-utils.tsx +78 -0
- package/tsup.config.ts +18 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSimulateTransfer — React hook for simulating ERC-20 token transfers.
|
|
3
|
+
*
|
|
4
|
+
* Wraps SimulationManager.simulateERC20Transfer in a React hook with
|
|
5
|
+
* reactive state for loading, result, and error.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { useSimulateTransfer } from "@naculus/connect-appkit-react";
|
|
10
|
+
*
|
|
11
|
+
* function SendForm({ token, to, amount }) {
|
|
12
|
+
* const { result, simulate, loading } = useSimulateTransfer({
|
|
13
|
+
* rpcUrl: "https://eth.llamarpc.com",
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <div>
|
|
18
|
+
* <button onClick={() => simulate(token, to, amount)} disabled={loading}>
|
|
19
|
+
* Simulate Transfer
|
|
20
|
+
* </button>
|
|
21
|
+
* {result && <p>Result: {result.status}</p>}
|
|
22
|
+
* </div>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useState, useCallback, useRef } from "react";
|
|
29
|
+
import { SimulationManager } from "@naculus/wallet-engine";
|
|
30
|
+
import type { SimulationResult } from "@naculus/wallet-engine";
|
|
31
|
+
|
|
32
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface UseSimulateTransferOptions {
|
|
35
|
+
/** RPC URL for eth_call simulation */
|
|
36
|
+
rpcUrl?: string;
|
|
37
|
+
/** Default chain ID */
|
|
38
|
+
chainId?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UseSimulateTransferReturn {
|
|
42
|
+
/** Current simulation result */
|
|
43
|
+
result: SimulationResult | null;
|
|
44
|
+
/** Whether a simulation is in progress */
|
|
45
|
+
loading: boolean;
|
|
46
|
+
/** Last error (null if no error) */
|
|
47
|
+
error: Error | null;
|
|
48
|
+
/**
|
|
49
|
+
* Simulate an ERC-20 transfer.
|
|
50
|
+
*
|
|
51
|
+
* @param tokenAddress - ERC-20 token contract address
|
|
52
|
+
* @param from - Sender address
|
|
53
|
+
* @param to - Recipient address
|
|
54
|
+
* @param amount - Human-readable amount (e.g. "1.50")
|
|
55
|
+
* @param options - Optional chainId, rpcUrl, decimals override
|
|
56
|
+
*/
|
|
57
|
+
simulate: (
|
|
58
|
+
tokenAddress: `0x${string}`,
|
|
59
|
+
from: `0x${string}`,
|
|
60
|
+
to: `0x${string}`,
|
|
61
|
+
amount: string,
|
|
62
|
+
options?: {
|
|
63
|
+
chainId?: number;
|
|
64
|
+
rpcUrl?: string;
|
|
65
|
+
decimals?: number;
|
|
66
|
+
},
|
|
67
|
+
) => Promise<SimulationResult>;
|
|
68
|
+
/** Reset result and error state */
|
|
69
|
+
reset: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Hook ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Hook for simulating an ERC-20 token transfer.
|
|
76
|
+
*
|
|
77
|
+
* Creates a SimulationManager internally and provides a `simulate`
|
|
78
|
+
* function that builds transfer calldata automatically.
|
|
79
|
+
*
|
|
80
|
+
* @param options - Configuration for the underlying SimulationManager
|
|
81
|
+
*/
|
|
82
|
+
export function useSimulateTransfer(
|
|
83
|
+
options?: UseSimulateTransferOptions,
|
|
84
|
+
): UseSimulateTransferReturn {
|
|
85
|
+
const rpcUrl = options?.rpcUrl;
|
|
86
|
+
const chainId = options?.chainId;
|
|
87
|
+
|
|
88
|
+
// ── SimulationManager ref (lazy init) ──────────────────────────
|
|
89
|
+
|
|
90
|
+
const managerRef = useRef<SimulationManager | null>(null);
|
|
91
|
+
|
|
92
|
+
function getManager(): SimulationManager {
|
|
93
|
+
if (!managerRef.current) {
|
|
94
|
+
managerRef.current = new SimulationManager({
|
|
95
|
+
enabled: true,
|
|
96
|
+
rpcUrl,
|
|
97
|
+
autoSimulate: false,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return managerRef.current;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── State ──────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const [result, setResult] = useState<SimulationResult | null>(null);
|
|
106
|
+
const [loading, setLoading] = useState(false);
|
|
107
|
+
const [error, setError] = useState<Error | null>(null);
|
|
108
|
+
|
|
109
|
+
// ── Simulate ───────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const simulate = useCallback(
|
|
112
|
+
async (
|
|
113
|
+
tokenAddress: `0x${string}`,
|
|
114
|
+
from: `0x${string}`,
|
|
115
|
+
to: `0x${string}`,
|
|
116
|
+
amount: string,
|
|
117
|
+
simOptions?: {
|
|
118
|
+
chainId?: number;
|
|
119
|
+
rpcUrl?: string;
|
|
120
|
+
decimals?: number;
|
|
121
|
+
},
|
|
122
|
+
): Promise<SimulationResult> => {
|
|
123
|
+
setLoading(true);
|
|
124
|
+
setError(null);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const manager = getManager();
|
|
128
|
+
const actualChainId = simOptions?.chainId ?? chainId ?? 1;
|
|
129
|
+
const actualRpcUrl = simOptions?.rpcUrl ?? rpcUrl;
|
|
130
|
+
|
|
131
|
+
const simResult = await manager.simulateERC20Transfer(
|
|
132
|
+
tokenAddress,
|
|
133
|
+
from,
|
|
134
|
+
to,
|
|
135
|
+
amount,
|
|
136
|
+
actualChainId,
|
|
137
|
+
simOptions?.decimals,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
setResult(simResult);
|
|
141
|
+
return simResult;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const errorObj =
|
|
144
|
+
err instanceof Error ? err : new Error(String(err));
|
|
145
|
+
setError(errorObj);
|
|
146
|
+
throw errorObj;
|
|
147
|
+
} finally {
|
|
148
|
+
setLoading(false);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[rpcUrl, chainId],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ── Reset ──────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
const reset = useCallback(() => {
|
|
157
|
+
setResult(null);
|
|
158
|
+
setError(null);
|
|
159
|
+
setLoading(false);
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// ── Return ─────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
return { result, loading, error, simulate, reset };
|
|
165
|
+
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
/// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
6
|
+
|
|
7
|
+
// ── localStorage polyfill for Node 26 ────────────────────────────
|
|
8
|
+
|
|
9
|
+
if (typeof globalThis.localStorage === 'undefined') {
|
|
10
|
+
const store: Record<string, string> = {};
|
|
11
|
+
const lsMock = {
|
|
12
|
+
getItem: (key: string) => store[key] ?? null,
|
|
13
|
+
setItem: (key: string, value: string) => { store[key] = value; },
|
|
14
|
+
removeItem: (key: string) => { delete store[key]; },
|
|
15
|
+
clear: () => { Object.keys(store).forEach(k => delete store[k]); },
|
|
16
|
+
get length() { return Object.keys(store).length; },
|
|
17
|
+
key: (index: number) => Object.keys(store)[index] ?? null,
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
20
|
+
value: new Proxy(lsMock, {
|
|
21
|
+
ownKeys: () => Object.keys(store),
|
|
22
|
+
getOwnPropertyDescriptor: () => ({ configurable: true, enumerable: true }),
|
|
23
|
+
}),
|
|
24
|
+
writable: true, configurable: true,
|
|
25
|
+
});
|
|
26
|
+
if (typeof window !== 'undefined') {
|
|
27
|
+
Object.defineProperty(window, 'localStorage', {
|
|
28
|
+
value: new Proxy(lsMock, {
|
|
29
|
+
ownKeys: () => Object.keys(store),
|
|
30
|
+
getOwnPropertyDescriptor: () => ({ configurable: true, enumerable: true }),
|
|
31
|
+
}),
|
|
32
|
+
writable: true, configurable: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Mocks ────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const mockUseWeb3 = vi.fn();
|
|
40
|
+
vi.mock("../provider/Web3ConnectProvider", () => ({
|
|
41
|
+
useWeb3: () => mockUseWeb3(),
|
|
42
|
+
Web3ConnectProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const mockSignMessage = vi.fn();
|
|
46
|
+
const mockGetClient = vi.fn();
|
|
47
|
+
vi.mock("../client", () => ({
|
|
48
|
+
getClient: () => mockGetClient(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// We need to mock useSignInWithX since useSiwxAuthSession composes it
|
|
52
|
+
const mockUseSignInWithX = vi.fn();
|
|
53
|
+
vi.mock("./useSignInWithX", () => ({
|
|
54
|
+
useSignInWithX: () => mockUseSignInWithX(),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
import { useSiwxAuthSession } from "./useSiwxAuthSession";
|
|
58
|
+
|
|
59
|
+
// ── Test Fixtures ────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function createSiwxResult(overrides: Record<string, unknown> = {}) {
|
|
62
|
+
const defaultExpiry = new Date(Date.now() + 3600000).toISOString(); // 1 hour from now
|
|
63
|
+
return {
|
|
64
|
+
message: {
|
|
65
|
+
raw: "localhost wants you to sign in",
|
|
66
|
+
domain: "localhost",
|
|
67
|
+
address: "eip155:1:0x1234567890123456789012345678901234567890",
|
|
68
|
+
statement: "Sign in to test",
|
|
69
|
+
uri: "http://localhost",
|
|
70
|
+
version: 1,
|
|
71
|
+
chainId: "eip155:1",
|
|
72
|
+
nonce: "abc123xyz",
|
|
73
|
+
issuedAt: new Date().toISOString(),
|
|
74
|
+
expirationTime: defaultExpiry,
|
|
75
|
+
notBefore: null,
|
|
76
|
+
resources: [],
|
|
77
|
+
requestId: null,
|
|
78
|
+
...(overrides.message ?? {}),
|
|
79
|
+
},
|
|
80
|
+
signature: "0xdeadbeef",
|
|
81
|
+
...overrides,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Storage Helpers ──────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function setLocalStorage(key: string, value: unknown) {
|
|
88
|
+
localStorage.setItem(`naculus_${key}`, JSON.stringify(value));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getLocalStorage(key: string) {
|
|
92
|
+
const v = localStorage.getItem(`naculus_${key}`);
|
|
93
|
+
return v ? JSON.parse(v) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clearLocalStorage() {
|
|
97
|
+
const keys = Object.keys(localStorage).filter((k) => k.startsWith("naculus_"));
|
|
98
|
+
keys.forEach((k) => localStorage.removeItem(k));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Tests ───────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("useSiwxAuthSession", () => {
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
vi.clearAllMocks();
|
|
106
|
+
clearLocalStorage();
|
|
107
|
+
|
|
108
|
+
// Default mock for useSignInWithX
|
|
109
|
+
mockUseSignInWithX.mockReturnValue({
|
|
110
|
+
signIn: vi.fn(),
|
|
111
|
+
isSigningIn: false,
|
|
112
|
+
result: null,
|
|
113
|
+
error: null,
|
|
114
|
+
clearError: vi.fn(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Default mock for Web3 context
|
|
118
|
+
mockUseWeb3.mockReturnValue({
|
|
119
|
+
session: {
|
|
120
|
+
topic: "test-topic",
|
|
121
|
+
namespaces: {
|
|
122
|
+
eip155: {
|
|
123
|
+
chains: ["eip155:1"],
|
|
124
|
+
accounts: ["eip155:1:0x1234567890123456789012345678901234567890"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
chainId: "eip155:1",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
clearLocalStorage();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── Initial State ──────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
it("should have initial unsigned state", () => {
|
|
139
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
140
|
+
|
|
141
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
142
|
+
expect(result.current.siwxResult).toBeNull();
|
|
143
|
+
expect(result.current.isSigningIn).toBe(false);
|
|
144
|
+
expect(result.current.error).toBeNull();
|
|
145
|
+
expect(result.current.isRestoring).toBe(true); // still restoring
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should finish restoring when no saved auth exists", async () => {
|
|
149
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(result.current.isRestoring).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
156
|
+
expect(result.current.siwxResult).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── Persisted Auth Restore ─────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
it("should restore a valid persisted auth session", async () => {
|
|
162
|
+
const siwxResult = createSiwxResult();
|
|
163
|
+
setLocalStorage("siwx_auth", siwxResult);
|
|
164
|
+
|
|
165
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
166
|
+
|
|
167
|
+
await waitFor(() => {
|
|
168
|
+
expect(result.current.isRestoring).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.current.isSignedIn).toBe(true);
|
|
172
|
+
expect(result.current.siwxResult).not.toBeNull();
|
|
173
|
+
expect(result.current.siwxResult!.signature).toBe("0xdeadbeef");
|
|
174
|
+
expect(result.current.error).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should auto-restore off when autoRestore=false", async () => {
|
|
178
|
+
setLocalStorage("siwx_auth", createSiwxResult());
|
|
179
|
+
|
|
180
|
+
const { result } = renderHook(() => useSiwxAuthSession({ autoRestore: false }));
|
|
181
|
+
|
|
182
|
+
// Auto-restore set to false, so isRestoring should quickly be false
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(result.current.isRestoring).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Should not have restored the auth
|
|
188
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
189
|
+
expect(result.current.siwxResult).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should discard expired persisted auth", async () => {
|
|
193
|
+
const expired = createSiwxResult({
|
|
194
|
+
message: {
|
|
195
|
+
expirationTime: new Date(Date.now() - 10000).toISOString(), // 10s ago
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
setLocalStorage("siwx_auth", expired);
|
|
199
|
+
|
|
200
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(result.current.isRestoring).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
207
|
+
expect(result.current.siwxResult).toBeNull();
|
|
208
|
+
|
|
209
|
+
// Verify it was cleaned from storage
|
|
210
|
+
expect(getLocalStorage("siwx_auth")).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should discard notBefore-violated persisted auth", async () => {
|
|
214
|
+
const futureNotBefore = createSiwxResult({
|
|
215
|
+
message: {
|
|
216
|
+
notBefore: new Date(Date.now() + 3600000).toISOString(), // 1h in future
|
|
217
|
+
expirationTime: new Date(Date.now() + 7200000).toISOString(),
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
setLocalStorage("siwx_auth", futureNotBefore);
|
|
221
|
+
|
|
222
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
223
|
+
|
|
224
|
+
await waitFor(() => {
|
|
225
|
+
expect(result.current.isRestoring).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
229
|
+
expect(result.current.siwxResult).toBeNull();
|
|
230
|
+
expect(getLocalStorage("siwx_auth")).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should restore auth without expirationTime", async () => {
|
|
234
|
+
const noExpiry = createSiwxResult({
|
|
235
|
+
message: { expirationTime: null },
|
|
236
|
+
});
|
|
237
|
+
setLocalStorage("siwx_auth", noExpiry);
|
|
238
|
+
|
|
239
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
240
|
+
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(result.current.isRestoring).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result.current.isSignedIn).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ── Sign In ────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
it("should persist SIWx result on successful sign-in", async () => {
|
|
251
|
+
const mockResult = createSiwxResult({ signature: "0xpersisted_sig" });
|
|
252
|
+
const mockSignIn = vi.fn().mockResolvedValue(mockResult);
|
|
253
|
+
mockUseSignInWithX.mockReturnValue({
|
|
254
|
+
signIn: mockSignIn,
|
|
255
|
+
isSigningIn: false,
|
|
256
|
+
result: null,
|
|
257
|
+
error: null,
|
|
258
|
+
clearError: vi.fn(),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
262
|
+
|
|
263
|
+
// Wait for restore check to complete
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(result.current.isRestoring).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
let returned: Awaited<ReturnType<typeof result.current.signIn>> | undefined;
|
|
269
|
+
|
|
270
|
+
await act(async () => {
|
|
271
|
+
returned = await result.current.signIn({
|
|
272
|
+
statement: "Test sign-in",
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(returned!.signature).toBe("0xpersisted_sig");
|
|
277
|
+
expect(result.current.isSignedIn).toBe(true);
|
|
278
|
+
expect(result.current.siwxResult!.signature).toBe("0xpersisted_sig");
|
|
279
|
+
|
|
280
|
+
// Verify it was persisted to storage
|
|
281
|
+
const stored = getLocalStorage("siwx_auth");
|
|
282
|
+
expect(stored).not.toBeNull();
|
|
283
|
+
expect(stored.signature).toBe("0xpersisted_sig");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should handle sign-in failure without persisting", async () => {
|
|
287
|
+
const mockSignIn = vi.fn().mockRejectedValue(new Error("User rejected"));
|
|
288
|
+
mockUseSignInWithX.mockReturnValue({
|
|
289
|
+
signIn: mockSignIn,
|
|
290
|
+
isSigningIn: false,
|
|
291
|
+
result: null,
|
|
292
|
+
error: null,
|
|
293
|
+
clearError: vi.fn(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(result.current.isRestoring).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await act(async () => {
|
|
303
|
+
try {
|
|
304
|
+
await result.current.signIn();
|
|
305
|
+
} catch {
|
|
306
|
+
// expected
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
311
|
+
expect(result.current.error).not.toBeNull();
|
|
312
|
+
expect(result.current.error!.message).toBe("User rejected");
|
|
313
|
+
|
|
314
|
+
// Should NOT have persisted anything
|
|
315
|
+
expect(getLocalStorage("siwx_auth")).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── Sign Out ───────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
it("should clear persisted auth on sign-out", async () => {
|
|
321
|
+
const mockResult = createSiwxResult({ signature: "0xwill_clear" });
|
|
322
|
+
const mockSignIn = vi.fn().mockResolvedValue(mockResult);
|
|
323
|
+
mockUseSignInWithX.mockReturnValue({
|
|
324
|
+
signIn: mockSignIn,
|
|
325
|
+
isSigningIn: false,
|
|
326
|
+
result: null,
|
|
327
|
+
error: null,
|
|
328
|
+
clearError: vi.fn(),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const { result } = renderHook(() => useSiwxAuthSession());
|
|
332
|
+
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(result.current.isRestoring).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Sign in first
|
|
338
|
+
await act(async () => {
|
|
339
|
+
await result.current.signIn();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result.current.isSignedIn).toBe(true);
|
|
343
|
+
expect(getLocalStorage("siwx_auth")).not.toBeNull();
|
|
344
|
+
|
|
345
|
+
// Sign out
|
|
346
|
+
await act(async () => {
|
|
347
|
+
result.current.signOut();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
351
|
+
expect(result.current.siwxResult).toBeNull();
|
|
352
|
+
expect(getLocalStorage("siwx_auth")).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── Edge Cases ─────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
it("should use custom storage prefix", async () => {
|
|
358
|
+
const mockResult = createSiwxResult({ signature: "0xprefix_test" });
|
|
359
|
+
const mockSignIn = vi.fn().mockResolvedValue(mockResult);
|
|
360
|
+
mockUseSignInWithX.mockReturnValue({
|
|
361
|
+
signIn: mockSignIn,
|
|
362
|
+
isSigningIn: false,
|
|
363
|
+
result: null,
|
|
364
|
+
error: null,
|
|
365
|
+
clearError: vi.fn(),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const { result } = renderHook(() =>
|
|
369
|
+
useSiwxAuthSession({ storagePrefix: "myapp_", autoRestore: false })
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
await act(async () => {
|
|
373
|
+
await result.current.signIn();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Should use custom prefix
|
|
377
|
+
expect(localStorage.getItem("myapp_siwx_auth")).not.toBeNull();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should allow sign-in after sign-out", async () => {
|
|
381
|
+
const mockResult1 = createSiwxResult({ signature: "0xfirst" });
|
|
382
|
+
const mockResult2 = createSiwxResult({ signature: "0xsecond" });
|
|
383
|
+
const mockSignIn = vi.fn()
|
|
384
|
+
.mockResolvedValueOnce(mockResult1)
|
|
385
|
+
.mockResolvedValueOnce(mockResult2);
|
|
386
|
+
mockUseSignInWithX.mockReturnValue({
|
|
387
|
+
signIn: mockSignIn,
|
|
388
|
+
isSigningIn: false,
|
|
389
|
+
result: null,
|
|
390
|
+
error: null,
|
|
391
|
+
clearError: vi.fn(),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const { result } = renderHook(() => useSiwxAuthSession({ autoRestore: false }));
|
|
395
|
+
|
|
396
|
+
// Sign in first time
|
|
397
|
+
await act(async () => {
|
|
398
|
+
await result.current.signIn({ statement: "First" });
|
|
399
|
+
});
|
|
400
|
+
expect(result.current.siwxResult!.signature).toBe("0xfirst");
|
|
401
|
+
|
|
402
|
+
// Sign out
|
|
403
|
+
await act(async () => {
|
|
404
|
+
result.current.signOut();
|
|
405
|
+
});
|
|
406
|
+
expect(result.current.isSignedIn).toBe(false);
|
|
407
|
+
|
|
408
|
+
// Sign in again
|
|
409
|
+
await act(async () => {
|
|
410
|
+
await result.current.signIn({ statement: "Second" });
|
|
411
|
+
});
|
|
412
|
+
expect(result.current.siwxResult!.signature).toBe("0xsecond");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should restore auth with custom prefix", async () => {
|
|
416
|
+
const siwxResult = createSiwxResult();
|
|
417
|
+
localStorage.setItem("custom_siwx_auth", JSON.stringify(siwxResult));
|
|
418
|
+
|
|
419
|
+
const { result } = renderHook(() =>
|
|
420
|
+
useSiwxAuthSession({ storagePrefix: "custom_" })
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
await waitFor(() => {
|
|
424
|
+
expect(result.current.isSignedIn).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("should clear error via clearError", async () => {
|
|
429
|
+
const mockSignIn = vi.fn().mockRejectedValue(new Error("Something went wrong"));
|
|
430
|
+
mockUseSignInWithX.mockReturnValue({
|
|
431
|
+
signIn: mockSignIn,
|
|
432
|
+
isSigningIn: false,
|
|
433
|
+
result: null,
|
|
434
|
+
error: null,
|
|
435
|
+
clearError: vi.fn(),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const { result } = renderHook(() => useSiwxAuthSession({ autoRestore: false }));
|
|
439
|
+
|
|
440
|
+
await act(async () => {
|
|
441
|
+
try {
|
|
442
|
+
await result.current.signIn();
|
|
443
|
+
} catch {
|
|
444
|
+
// expected
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(result.current.error).not.toBeNull();
|
|
449
|
+
|
|
450
|
+
act(() => {
|
|
451
|
+
result.current.clearError();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(result.current.error).toBeNull();
|
|
455
|
+
});
|
|
456
|
+
});
|