@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,210 @@
|
|
|
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 { useSignInWithX } from "./useSignInWithX";
|
|
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
|
+
function createSolanaSession() {
|
|
34
|
+
return {
|
|
35
|
+
topic: "test-topic",
|
|
36
|
+
namespaces: {
|
|
37
|
+
solana: {
|
|
38
|
+
chains: ["solana:4sGjMW1s"],
|
|
39
|
+
accounts: ["solana:4sGjMW1s:5JG7DPRQAxJVCLx3GxsHTxP1KZbSyLmPQBLkLhFZPGrH"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("useSignInWithX", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should have initial idle state", () => {
|
|
51
|
+
mockUseWeb3.mockReturnValue({ session: null, chainId: null });
|
|
52
|
+
mockGetClient.mockReturnValue(null);
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
55
|
+
|
|
56
|
+
expect(result.current.isSigningIn).toBe(false);
|
|
57
|
+
expect(result.current.result).toBeNull();
|
|
58
|
+
expect(result.current.error).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should throw when no session", async () => {
|
|
62
|
+
mockUseWeb3.mockReturnValue({ session: null, chainId: null });
|
|
63
|
+
mockGetClient.mockReturnValue({
|
|
64
|
+
signMessage: mockSignMessage,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
68
|
+
|
|
69
|
+
await expect(result.current.signIn()).rejects.toThrow("No active session");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should throw when client not initialized", async () => {
|
|
73
|
+
mockUseWeb3.mockReturnValue({ session: createEvmSession(), chainId: "eip155:1" });
|
|
74
|
+
mockGetClient.mockReturnValue(null);
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
77
|
+
|
|
78
|
+
await expect(result.current.signIn()).rejects.toThrow("Client not initialized");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should sign in with the first available account (EVM)", async () => {
|
|
82
|
+
const mockSignature = "0xgeneric_sig";
|
|
83
|
+
mockSignMessage.mockResolvedValue(mockSignature);
|
|
84
|
+
mockUseWeb3.mockReturnValue({
|
|
85
|
+
session: createEvmSession(),
|
|
86
|
+
chainId: "eip155:1",
|
|
87
|
+
});
|
|
88
|
+
mockGetClient.mockReturnValue({
|
|
89
|
+
signMessage: mockSignMessage,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
93
|
+
|
|
94
|
+
let siwxResult: Awaited<ReturnType<typeof result.current.signIn>> | undefined;
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
siwxResult = await result.current.signIn({
|
|
98
|
+
statement: "Generic SIWx sign in",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(siwxResult!).toBeDefined();
|
|
103
|
+
expect(siwxResult!.signature).toBe(mockSignature);
|
|
104
|
+
expect(siwxResult!.message.address).toBe(
|
|
105
|
+
"eip155:1:0x1234567890123456789012345678901234567890"
|
|
106
|
+
);
|
|
107
|
+
expect(mockSignMessage).toHaveBeenCalledTimes(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should sign in with the first available account (Solana)", async () => {
|
|
111
|
+
const mockSignature = "sol_gen_sig";
|
|
112
|
+
mockSignMessage.mockResolvedValue(mockSignature);
|
|
113
|
+
mockUseWeb3.mockReturnValue({
|
|
114
|
+
session: createSolanaSession(),
|
|
115
|
+
chainId: "solana:4sGjMW1s",
|
|
116
|
+
});
|
|
117
|
+
mockGetClient.mockReturnValue({
|
|
118
|
+
signMessage: mockSignMessage,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
122
|
+
|
|
123
|
+
let siwxResult: Awaited<ReturnType<typeof result.current.signIn>> | undefined;
|
|
124
|
+
|
|
125
|
+
await act(async () => {
|
|
126
|
+
siwxResult = await result.current.signIn();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(siwxResult!.message.address).toBe(
|
|
130
|
+
"solana:4sGjMW1s:5JG7DPRQAxJVCLx3GxsHTxP1KZbSyLmPQBLkLhFZPGrH"
|
|
131
|
+
);
|
|
132
|
+
expect(siwxResult!.message.chainId).toBe("solana:4sGjMW1s");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should set loading state during signing", async () => {
|
|
136
|
+
let resolvePromise: (v: string) => void;
|
|
137
|
+
const signPromise = new Promise<string>((resolve) => {
|
|
138
|
+
resolvePromise = resolve;
|
|
139
|
+
});
|
|
140
|
+
mockSignMessage.mockReturnValue(signPromise);
|
|
141
|
+
mockUseWeb3.mockReturnValue({
|
|
142
|
+
session: createEvmSession(),
|
|
143
|
+
chainId: "eip155:1",
|
|
144
|
+
});
|
|
145
|
+
mockGetClient.mockReturnValue({
|
|
146
|
+
signMessage: mockSignMessage,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
150
|
+
|
|
151
|
+
let callPromise: Promise<unknown>;
|
|
152
|
+
act(() => {
|
|
153
|
+
callPromise = result.current.signIn();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(result.current.isSigningIn).toBe(true);
|
|
157
|
+
|
|
158
|
+
await act(async () => {
|
|
159
|
+
resolvePromise!("0xsig");
|
|
160
|
+
await callPromise!;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result.current.isSigningIn).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should capture error on failed signing", async () => {
|
|
167
|
+
mockSignMessage.mockRejectedValue(new Error("Signature rejected"));
|
|
168
|
+
mockUseWeb3.mockReturnValue({
|
|
169
|
+
session: createEvmSession(),
|
|
170
|
+
chainId: "eip155:1",
|
|
171
|
+
});
|
|
172
|
+
mockGetClient.mockReturnValue({
|
|
173
|
+
signMessage: mockSignMessage,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
177
|
+
|
|
178
|
+
await act(async () => {
|
|
179
|
+
try {
|
|
180
|
+
await result.current.signIn();
|
|
181
|
+
} catch {
|
|
182
|
+
// Expected
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.current.error).not.toBeNull();
|
|
187
|
+
expect(result.current.error?.message).toBe("Signature rejected");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should set result after successful sign-in", async () => {
|
|
191
|
+
mockSignMessage.mockResolvedValue("0xsig");
|
|
192
|
+
mockUseWeb3.mockReturnValue({
|
|
193
|
+
session: createEvmSession(),
|
|
194
|
+
chainId: "eip155:1",
|
|
195
|
+
});
|
|
196
|
+
mockGetClient.mockReturnValue({
|
|
197
|
+
signMessage: mockSignMessage,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const { result } = renderHook(() => useSignInWithX());
|
|
201
|
+
|
|
202
|
+
let siwxResult: Awaited<ReturnType<typeof result.current.signIn>> | undefined;
|
|
203
|
+
|
|
204
|
+
await act(async () => {
|
|
205
|
+
siwxResult = await result.current.signIn();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(result.current.result).toBe(siwxResult);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { useWeb3 } from "../provider/Web3ConnectProvider";
|
|
3
|
+
import { getClient } from "../client";
|
|
4
|
+
import { WalletError } from "@naculus/connect-core";
|
|
5
|
+
import {
|
|
6
|
+
createSiwxMessage,
|
|
7
|
+
generateNonce,
|
|
8
|
+
nowISO,
|
|
9
|
+
getBlockchainName,
|
|
10
|
+
type SiwxResult,
|
|
11
|
+
} from "@naculus/siwx";
|
|
12
|
+
|
|
13
|
+
export interface UseSignInWithXOptions {
|
|
14
|
+
domain?: string;
|
|
15
|
+
statement?: string;
|
|
16
|
+
uri?: string;
|
|
17
|
+
chainId?: string;
|
|
18
|
+
expirySeconds?: number;
|
|
19
|
+
resources?: string[];
|
|
20
|
+
requestId?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseSignInWithXReturn {
|
|
24
|
+
signIn: (options?: UseSignInWithXOptions) => Promise<SiwxResult>;
|
|
25
|
+
isSigningIn: boolean;
|
|
26
|
+
result: SiwxResult | null;
|
|
27
|
+
error: Error | null;
|
|
28
|
+
clearError: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getDefaultDomain(): string {
|
|
32
|
+
if (typeof window !== "undefined") return window.location.host;
|
|
33
|
+
return "localhost";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getDefaultUri(): string {
|
|
37
|
+
if (typeof window !== "undefined") return window.location.origin;
|
|
38
|
+
return "http://localhost";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useSignInWithX(): UseSignInWithXReturn {
|
|
42
|
+
const { session, chainId: currentChainId } = useWeb3();
|
|
43
|
+
const [isSigningIn, setIsSigningIn] = useState(false);
|
|
44
|
+
const [result, setResult] = useState<SiwxResult | null>(null);
|
|
45
|
+
const [error, setError] = useState<Error | null>(null);
|
|
46
|
+
|
|
47
|
+
const clearError = useCallback(() => setError(null), []);
|
|
48
|
+
|
|
49
|
+
const signIn = useCallback(
|
|
50
|
+
async (options?: UseSignInWithXOptions): Promise<SiwxResult> => {
|
|
51
|
+
if (!session) {
|
|
52
|
+
throw new WalletError("wallet_unavailable", "No active session");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const client = getClient();
|
|
56
|
+
if (!client) {
|
|
57
|
+
throw new WalletError("wallet_unavailable", "Client not initialized");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const allAccounts = Object.entries(session.namespaces).flatMap(
|
|
61
|
+
([ns, info]) =>
|
|
62
|
+
info.accounts.map((acc: string) => ({ ns, acc }))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (allAccounts.length === 0) {
|
|
66
|
+
throw new WalletError("wallet_unavailable", "No account found");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { acc: address } = allAccounts[0];
|
|
70
|
+
const chainId = options?.chainId ?? currentChainId ?? "eip155:1";
|
|
71
|
+
const domain = options?.domain ?? getDefaultDomain();
|
|
72
|
+
const uri = options?.uri ?? getDefaultUri();
|
|
73
|
+
const nonce = generateNonce();
|
|
74
|
+
const issuedAt = nowISO();
|
|
75
|
+
|
|
76
|
+
const message = createSiwxMessage({
|
|
77
|
+
domain,
|
|
78
|
+
address,
|
|
79
|
+
uri,
|
|
80
|
+
version: 1,
|
|
81
|
+
chainId,
|
|
82
|
+
nonce,
|
|
83
|
+
issuedAt,
|
|
84
|
+
statement: options?.statement,
|
|
85
|
+
expirationTime: options?.expirySeconds
|
|
86
|
+
? new Date(Date.now() + options.expirySeconds * 1000).toISOString()
|
|
87
|
+
: undefined,
|
|
88
|
+
resources: options?.resources,
|
|
89
|
+
requestId: options?.requestId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
setIsSigningIn(true);
|
|
93
|
+
setError(null);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const signature = (await client.signMessage(session, {
|
|
97
|
+
message,
|
|
98
|
+
address,
|
|
99
|
+
chainId: chainId ?? undefined,
|
|
100
|
+
})) as string;
|
|
101
|
+
|
|
102
|
+
const expirationTime = options?.expirySeconds
|
|
103
|
+
? new Date(Date.now() + options.expirySeconds * 1000).toISOString()
|
|
104
|
+
: null;
|
|
105
|
+
|
|
106
|
+
const siwxResult: SiwxResult = {
|
|
107
|
+
message: {
|
|
108
|
+
raw: message,
|
|
109
|
+
domain,
|
|
110
|
+
address,
|
|
111
|
+
statement: options?.statement ?? null,
|
|
112
|
+
uri,
|
|
113
|
+
version: 1,
|
|
114
|
+
chainId,
|
|
115
|
+
nonce,
|
|
116
|
+
issuedAt,
|
|
117
|
+
blockchain: getBlockchainName(chainId),
|
|
118
|
+
expirationTime,
|
|
119
|
+
notBefore: null,
|
|
120
|
+
resources: options?.resources ?? [],
|
|
121
|
+
requestId: options?.requestId ?? null,
|
|
122
|
+
},
|
|
123
|
+
signature,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
setResult(siwxResult);
|
|
127
|
+
return siwxResult;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const wrappedErr =
|
|
130
|
+
err instanceof Error ? err : new Error("SIWx sign-in failed");
|
|
131
|
+
setError(wrappedErr);
|
|
132
|
+
throw wrappedErr;
|
|
133
|
+
} finally {
|
|
134
|
+
setIsSigningIn(false);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
[session, currentChainId]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return { signIn, isSigningIn, result, error, clearError };
|
|
141
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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 { useSignMessage } from "./useSignMessage";
|
|
20
|
+
|
|
21
|
+
function createMockSession() {
|
|
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("useSignMessage", () => {
|
|
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(() => useSignMessage());
|
|
43
|
+
|
|
44
|
+
expect(result.current.isSigning).toBe(false);
|
|
45
|
+
expect(result.current.error).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should throw when no session", async () => {
|
|
49
|
+
mockUseWeb3.mockReturnValue({ session: null, chainId: null });
|
|
50
|
+
mockGetClient.mockReturnValue({
|
|
51
|
+
signMessage: mockSignMessage,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(() => useSignMessage());
|
|
55
|
+
|
|
56
|
+
await expect(async () => {
|
|
57
|
+
await act(async () => {
|
|
58
|
+
await result.current.signMessage("hello");
|
|
59
|
+
});
|
|
60
|
+
}).rejects.toThrow("No active session");
|
|
61
|
+
|
|
62
|
+
// Pre-condition checks throw directly without setting state
|
|
63
|
+
expect(result.current.error).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should throw when client not initialized", async () => {
|
|
67
|
+
mockUseWeb3.mockReturnValue({ session: createMockSession(), chainId: "eip155:1" });
|
|
68
|
+
mockGetClient.mockReturnValue(null);
|
|
69
|
+
|
|
70
|
+
const { result } = renderHook(() => useSignMessage());
|
|
71
|
+
|
|
72
|
+
await expect(async () => {
|
|
73
|
+
await act(async () => {
|
|
74
|
+
await result.current.signMessage("hello");
|
|
75
|
+
});
|
|
76
|
+
}).rejects.toThrow("Client not initialized");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should sign a message successfully", async () => {
|
|
80
|
+
mockSignMessage.mockResolvedValue("0xsignature123");
|
|
81
|
+
mockUseWeb3.mockReturnValue({ session: createMockSession(), chainId: "eip155:1" });
|
|
82
|
+
mockGetClient.mockReturnValue({
|
|
83
|
+
signMessage: mockSignMessage,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const { result } = renderHook(() => useSignMessage());
|
|
87
|
+
|
|
88
|
+
let signature: string;
|
|
89
|
+
await act(async () => {
|
|
90
|
+
signature = await result.current.signMessage("hello");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(signature!).toBe("0xsignature123");
|
|
94
|
+
expect(mockSignMessage).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(mockSignMessage).toHaveBeenCalledWith(createMockSession(), {
|
|
96
|
+
message: "hello",
|
|
97
|
+
address: "eip155:1:0x1234567890123456789012345678901234567890",
|
|
98
|
+
chainId: "eip155:1",
|
|
99
|
+
});
|
|
100
|
+
expect(result.current.isSigning).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should set loading state during signing", async () => {
|
|
104
|
+
let resolvePromise: (v: string) => void;
|
|
105
|
+
const signPromise = new Promise<string>((resolve) => {
|
|
106
|
+
resolvePromise = resolve;
|
|
107
|
+
});
|
|
108
|
+
mockSignMessage.mockReturnValue(signPromise);
|
|
109
|
+
mockUseWeb3.mockReturnValue({ session: createMockSession(), chainId: "eip155:1" });
|
|
110
|
+
mockGetClient.mockReturnValue({
|
|
111
|
+
signMessage: mockSignMessage,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const { result } = renderHook(() => useSignMessage());
|
|
115
|
+
|
|
116
|
+
let callPromise: Promise<string>;
|
|
117
|
+
act(() => {
|
|
118
|
+
callPromise = result.current.signMessage("hello");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.current.isSigning).toBe(true);
|
|
122
|
+
|
|
123
|
+
await act(async () => {
|
|
124
|
+
resolvePromise!("0xsig");
|
|
125
|
+
await callPromise!;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result.current.isSigning).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should capture error on failed signing", async () => {
|
|
132
|
+
mockSignMessage.mockRejectedValue(new Error("User rejected"));
|
|
133
|
+
mockUseWeb3.mockReturnValue({ session: createMockSession(), chainId: "eip155:1" });
|
|
134
|
+
mockGetClient.mockReturnValue({
|
|
135
|
+
signMessage: mockSignMessage,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const { result } = renderHook(() => useSignMessage());
|
|
139
|
+
|
|
140
|
+
await act(async () => {
|
|
141
|
+
try {
|
|
142
|
+
await result.current.signMessage("hello");
|
|
143
|
+
} catch {
|
|
144
|
+
// Expected
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.current.error).not.toBeNull();
|
|
149
|
+
expect(result.current.error?.message).toBe("User rejected");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { useWeb3 } from "../provider/Web3ConnectProvider";
|
|
3
|
+
import { getClient } from "../client";
|
|
4
|
+
import { WalletError } from "@naculus/connect-core";
|
|
5
|
+
|
|
6
|
+
export function useSignMessage() {
|
|
7
|
+
const { session, chainId } = useWeb3();
|
|
8
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState<Error | null>(null);
|
|
10
|
+
|
|
11
|
+
const signMessage = useCallback(
|
|
12
|
+
async (message: string): Promise<string> => {
|
|
13
|
+
// Precondition checks — set error state before throwing (consistent with useSendTransaction pattern)
|
|
14
|
+
if (!session) {
|
|
15
|
+
const err = new WalletError("wallet_unavailable", "No active session");
|
|
16
|
+
setError(err);
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const client = getClient();
|
|
21
|
+
if (!client) {
|
|
22
|
+
const err = new WalletError("wallet_unavailable", "Client not initialized");
|
|
23
|
+
setError(err);
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const address = Object.values(session.namespaces)
|
|
28
|
+
.flatMap((ns) => ns.accounts)
|
|
29
|
+
.find((acc) => acc.includes("0x")) ?? Object.values(session.namespaces)[0]?.accounts[0];
|
|
30
|
+
|
|
31
|
+
if (!address) {
|
|
32
|
+
const err = new WalletError("wallet_unavailable", "No account found");
|
|
33
|
+
setError(err);
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setIsLoading(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = (await client.signMessage(session, {
|
|
42
|
+
message,
|
|
43
|
+
address,
|
|
44
|
+
chainId: chainId ?? undefined
|
|
45
|
+
})) as string;
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const errorMessage = err instanceof Error ? err.message : "Signing failed";
|
|
50
|
+
const error = err instanceof Error ? err : new Error(errorMessage);
|
|
51
|
+
setError(error);
|
|
52
|
+
throw error;
|
|
53
|
+
} finally {
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[session, chainId]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
signMessage,
|
|
62
|
+
isSigning: isLoading,
|
|
63
|
+
error
|
|
64
|
+
};
|
|
65
|
+
}
|