Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 107 additions & 62 deletions __tests__/hooks/useSecureSign-wagmi.test.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
import { renderHook, act } from '@testing-library/react';
import { useSecureSign } from '@/hooks/useSecureSign';
import { useAppKitAccount } from '@reown/appkit/react';
import { useSignMessage } from 'wagmi';
import { UserRejectedRequestError } from 'viem';
import { renderHook, act } from "@testing-library/react";
import { useSecureSign } from "@/hooks/useSecureSign";
import { useAppKitAccount } from "@reown/appkit/react";
import { useSignMessage } from "wagmi";
import { UserRejectedRequestError } from "viem";

// Mock the hooks
jest.mock('@reown/appkit/react', () => ({
jest.mock("@reown/appkit/react", () => ({
useAppKitAccount: jest.fn(),
}));

jest.mock('wagmi', () => ({
jest.mock("wagmi", () => ({
useSignMessage: jest.fn(),
}));

const mockUseAppKitAccount = useAppKitAccount as jest.MockedFunction<typeof useAppKitAccount>;
const mockUseSignMessage = useSignMessage as jest.MockedFunction<typeof useSignMessage>;
const mockUseAppKitAccount = useAppKitAccount as jest.MockedFunction<
typeof useAppKitAccount
>;
const mockUseSignMessage = useSignMessage as jest.MockedFunction<
typeof useSignMessage
>;

describe('useSecureSign with Wagmi', () => {
const validAddress = '0x1234567890123456789012345678901234567890';
const validSignature = '0x1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890';
describe("useSecureSign with Wagmi", () => {
const validAddress = "0x1234567890123456789012345678901234567890";
const validSignature =
"0x1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";

beforeEach(() => {
jest.clearAllMocks();

// Setup default successful mocks
mockUseAppKitAccount.mockReturnValue({
address: validAddress,
isConnected: true,
caipAddress: '',
status: 'connected'
caipAddress: "",
status: "connected",
});

mockUseSignMessage.mockReturnValue({
signMessage: jest.fn(),
signMessageAsync: jest.fn().mockResolvedValue(validSignature),
Expand All @@ -41,13 +46,13 @@ describe('useSecureSign with Wagmi', () => {
isPending: false,
isSuccess: false,
reset: jest.fn(),
status: 'idle',
status: "idle",
variables: undefined,
} as any);
});

describe('Successful signing with Wagmi', () => {
it('successfully signs message using Wagmi signMessageAsync', async () => {
describe("Successful signing with Wagmi", () => {
it("successfully signs message using Wagmi signMessageAsync", async () => {
const mockSignMessageAsync = jest.fn().mockResolvedValue(validSignature);
mockUseSignMessage.mockReturnValue({
signMessageAsync: mockSignMessageAsync,
Expand All @@ -56,137 +61,177 @@ describe('useSecureSign with Wagmi', () => {
const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('test message');
const signResult = await result.current.signMessage("test message");
expect(signResult.signature).toBe(validSignature);
expect(signResult.userRejected).toBe(false);
expect(signResult.error).toBeUndefined();
});

expect(mockSignMessageAsync).toHaveBeenCalledWith({ message: 'test message' });
expect(mockSignMessageAsync).toHaveBeenCalledWith({
message: "test message",
});
});

it("accepts contract signatures when configured", async () => {
const contractSignature = `0x${"a".repeat(200)}`;
const mockSignMessageAsync = jest
.fn()
.mockResolvedValue(contractSignature);
mockUseSignMessage.mockReturnValue({
signMessageAsync: mockSignMessageAsync,
} as any);

const { result } = renderHook(() =>
useSecureSign({ signatureType: "contract" })
);

await act(async () => {
const signResult = await result.current.signMessage("test message");
expect(signResult.signature).toBe(contractSignature);
expect(signResult.userRejected).toBe(false);
expect(signResult.error).toBeUndefined();
});
});

it('handles user rejection correctly', async () => {
const mockSignMessageAsync = jest.fn().mockRejectedValue(new UserRejectedRequestError(new Error()));
it("handles user rejection correctly", async () => {
const mockSignMessageAsync = jest
.fn()
.mockRejectedValue(new UserRejectedRequestError(new Error()));
mockUseSignMessage.mockReturnValue({
signMessageAsync: mockSignMessageAsync,
} as any);

const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('test message');
const signResult = await result.current.signMessage("test message");
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(true);
expect(signResult.error).toBeUndefined();
});
});
});

describe('Connection validation', () => {
it('fails when wallet not connected', async () => {
describe("Connection validation", () => {
it("fails when wallet not connected", async () => {
mockUseAppKitAccount.mockReturnValue({
address: undefined,
isConnected: false,
caipAddress: '',
status: 'disconnected'
caipAddress: "",
status: "disconnected",
});

const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('test message');
expect(signResult.error?.name).toBe('MobileSigningError');
expect(signResult.error?.message).toBe('Wallet not connected. Please connect your wallet and try again.');
const signResult = await result.current.signMessage("test message");
expect(signResult.error?.name).toBe("MobileSigningError");
expect(signResult.error?.message).toBe(
"Wallet not connected. Please connect your wallet and try again."
);
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(false);
});
});

it('fails when address is missing', async () => {
it("fails when address is missing", async () => {
mockUseAppKitAccount.mockReturnValue({
address: undefined,
isConnected: true,
caipAddress: '',
status: 'connected'
caipAddress: "",
status: "connected",
});

const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('test message');
expect(signResult.error?.name).toBe('MobileSigningError');
expect(signResult.error?.message).toBe('No wallet address detected. Please reconnect your wallet.');
const signResult = await result.current.signMessage("test message");
expect(signResult.error?.name).toBe("MobileSigningError");
expect(signResult.error?.message).toBe(
"No wallet address detected. Please reconnect your wallet."
);
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(false);
});
});
});

describe('Input validation', () => {
it('validates empty message', async () => {
describe("Input validation", () => {
it("validates empty message", async () => {
const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('');
expect(signResult.error?.name).toBe('ProviderValidationError');
expect(signResult.error?.message).toBe('Message cannot be empty');
const signResult = await result.current.signMessage("");
expect(signResult.error?.name).toBe("ProviderValidationError");
expect(signResult.error?.message).toBe("Message cannot be empty");
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(false);
});
});

it('validates message length', async () => {
it("validates message length", async () => {
const { result } = renderHook(() => useSecureSign());
const longMessage = 'a'.repeat(10001);
const longMessage = "a".repeat(10001);

await act(async () => {
const signResult = await result.current.signMessage(longMessage);
expect(signResult.error?.name).toBe('ProviderValidationError');
expect(signResult.error?.message).toBe('Message too long (max 10000 characters)');
expect(signResult.error?.name).toBe("ProviderValidationError");
expect(signResult.error?.message).toBe(
"Message too long (max 10000 characters)"
);
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(false);
});
});

it('validates signature format', async () => {
const mockSignMessageAsync = jest.fn().mockResolvedValue('invalid-signature');
it("validates signature format", async () => {
const mockSignMessageAsync = jest
.fn()
.mockResolvedValue("invalid-signature");
mockUseSignMessage.mockReturnValue({
signMessageAsync: mockSignMessageAsync,
} as any);

const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('test message');
expect(signResult.error?.name).toBe('ProviderValidationError');
expect(signResult.error?.message).toBe('Invalid signature format');
const signResult = await result.current.signMessage("test message");
expect(signResult.error?.name).toBe("ProviderValidationError");
expect(signResult.error?.message).toBe("Invalid signature format");
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(false);
});
});
});

describe('Error handling', () => {
it('handles generic Wagmi errors', async () => {
const mockSignMessageAsync = jest.fn().mockRejectedValue(new Error('Wagmi signing failed'));
describe("Error handling", () => {
it("handles generic Wagmi errors", async () => {
const mockSignMessageAsync = jest
.fn()
.mockRejectedValue(new Error("Wagmi signing failed"));
mockUseSignMessage.mockReturnValue({
signMessageAsync: mockSignMessageAsync,
} as any);

const { result } = renderHook(() => useSecureSign());

await act(async () => {
const signResult = await result.current.signMessage('test message');
expect(signResult.error?.name).toBe('MobileSigningError');
const signResult = await result.current.signMessage("test message");
expect(signResult.error?.name).toBe("MobileSigningError");
expect(signResult.signature).toBeNull();
expect(signResult.userRejected).toBe(false);
});
});

it('manages signing pending state correctly', async () => {
const mockSignMessageAsync = jest.fn().mockImplementation(() =>
new Promise(resolve => setTimeout(() => resolve(validSignature), 50))
);
it("manages signing pending state correctly", async () => {
const mockSignMessageAsync = jest
.fn()
.mockImplementation(
() =>
new Promise((resolve) =>
setTimeout(() => resolve(validSignature), 50)
)
);
mockUseSignMessage.mockReturnValue({
signMessageAsync: mockSignMessageAsync,
} as any);
Expand All @@ -196,11 +241,11 @@ describe('useSecureSign with Wagmi', () => {
expect(result.current.isSigningPending).toBe(false);

await act(async () => {
const signResult = await result.current.signMessage('test message');
const signResult = await result.current.signMessage("test message");
expect(signResult.signature).toBe(validSignature);
});

expect(result.current.isSigningPending).toBe(false);
});
});
});
});
6 changes: 4 additions & 2 deletions components/auth/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
useCallback,
useRef,
} from "react";
import type { TypeOptions} from "react-toastify";
import type { TypeOptions } from "react-toastify";
import { Slide, ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import {
Expand Down Expand Up @@ -138,7 +138,9 @@ export default function Auth({
signMessage,
isSigningPending,
reset: resetSigning,
} = useSecureSign();
} = useSecureSign({
signatureType: isSafeWallet ? "contract" : "eoa",
});
const [showSignModal, setShowSignModal] = useState(false);

const { profile: connectedProfile, isLoading: fetchingProfile } = useIdentity(
Expand Down
12 changes: 9 additions & 3 deletions components/auth/SeizeConnectContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import {
useAppKitAccount,
useAppKitState,
useDisconnect,
useWalletInfo,
} from "@reown/appkit/react";
import { getAddress, isAddress } from "viem";
import { WalletErrorBoundary } from "./error-boundary";
import { isSafeWalletInfo } from "@/utils/wallet-detection";

// Custom error types for better error handling
class WalletConnectionError extends Error {
Expand Down Expand Up @@ -345,6 +347,7 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const account = useAppKitAccount();
const { walletInfo } = useWalletInfo();
const { disconnect } = useDisconnect();
const { open } = useAppKit();
const state = useAppKitState();
Expand Down Expand Up @@ -582,9 +585,9 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({
const contextValue = useMemo(
(): SeizeConnectContextType => ({
address: connectedAddress,
walletName: undefined,
walletIcon: undefined,
isSafeWallet: false,
walletName: walletInfo?.name,
walletIcon: walletInfo?.icon,
isSafeWallet: isSafeWalletInfo(walletInfo),
seizeConnect,
seizeDisconnect,
seizeDisconnectAndLogout,
Expand All @@ -599,6 +602,9 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({
}),
[
connectedAddress,
walletInfo?.name,
walletInfo?.icon,
walletInfo?.type,
seizeConnect,
seizeDisconnect,
seizeDisconnectAndLogout,
Expand Down
Loading