UNPKG

@gemini-wallet/core

Version:

Core SDK for Gemini Wallet integration with popup communication

350 lines (292 loc) 11.2 kB
import { beforeEach, describe, expect, it, mock } from "bun:test"; import { type Address } from "viem"; import { DEFAULT_CHAIN_ID, getDefaultRpcUrl, SUPPORTED_CHAIN_IDS } from "../constants"; import { GeminiStorage, STORAGE_ETH_ACCOUNTS_KEY, STORAGE_ETH_ACTIVE_CHAIN_KEY } from "../storage"; import { type Chain, type ConnectResponse, GeminiSdkEvent, type SendTransactionResponse, type SignMessageResponse, type SwitchChainResponse, } from "../types"; import { isChainSupportedByGeminiSw } from "./wallet"; // Test constants const mockAddress = "0xAfEDA61dB9e162293b2eF2C2bC5A800b37Bb5E4a" as Address; const mockTxHash = "0x5de3752c591ecc35d1046f3aca2eba1ba5bdcfb786639a8661e9ecb823675743"; const mockSigHash = "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045b"; // Set up global window mock for this test file only (global as any).window = { crypto: { randomUUID: () => "test-uuid", }, location: { origin: "http://localhost:3000", }, }; // Mock localStorage for this test file only (global as any).localStorage = { clear: () => {}, getItem: () => null, removeItem: () => {}, setItem: () => {}, }; describe("GeminiWallet - Unit Tests", () => { // Note: These are unit tests that test the wallet behavior with mocked dependencies. // Since we can't easily mock the Communicator module without affecting other tests, // we'll focus on testing the parts that don't require the full wallet instance. describe("isChainSupportedByGeminiSw", () => { it("should return true for all supported chains", () => { SUPPORTED_CHAIN_IDS.forEach(chainId => { expect(isChainSupportedByGeminiSw(chainId)).toBe(true); }); }); it("should return false for unsupported chains", () => { expect(isChainSupportedByGeminiSw(999999)).toBe(false); expect(isChainSupportedByGeminiSw(0)).toBe(false); expect(isChainSupportedByGeminiSw(-1)).toBe(false); }); it("should handle Ethereum mainnet", () => { expect(isChainSupportedByGeminiSw(1)).toBe(true); }); it("should handle Arbitrum", () => { expect(isChainSupportedByGeminiSw(42161)).toBe(true); }); it("should handle Base", () => { expect(isChainSupportedByGeminiSw(8453)).toBe(true); }); it("should handle Polygon", () => { expect(isChainSupportedByGeminiSw(137)).toBe(true); }); it("should handle Optimism", () => { expect(isChainSupportedByGeminiSw(10)).toBe(true); }); }); describe("Storage Integration", () => { let mockStorage: GeminiStorage; beforeEach(() => { mockStorage = new GeminiStorage(); mockStorage.loadObject = mock((key: string, defaultValue: any) => { if (key === STORAGE_ETH_ACTIVE_CHAIN_KEY) { return Promise.resolve({ id: DEFAULT_CHAIN_ID, rpcUrl: getDefaultRpcUrl(DEFAULT_CHAIN_ID), }); } if (key === STORAGE_ETH_ACCOUNTS_KEY) { return Promise.resolve([]); } return Promise.resolve(defaultValue); }); mockStorage.storeObject = mock(() => Promise.resolve()); mockStorage.removeItem = mock(async () => {}); }); it("should store accounts to storage", async () => { const accounts = [mockAddress]; await mockStorage.storeObject(STORAGE_ETH_ACCOUNTS_KEY, accounts); expect(mockStorage.storeObject).toHaveBeenCalledWith(STORAGE_ETH_ACCOUNTS_KEY, accounts); }); it("should store chain to storage", async () => { const chain = { id: 42161, rpcUrl: getDefaultRpcUrl(42161) }; await mockStorage.storeObject(STORAGE_ETH_ACTIVE_CHAIN_KEY, chain); expect(mockStorage.storeObject).toHaveBeenCalledWith(STORAGE_ETH_ACTIVE_CHAIN_KEY, chain); }); it("should load accounts from storage", async () => { const accounts = await mockStorage.loadObject(STORAGE_ETH_ACCOUNTS_KEY, []); expect(mockStorage.loadObject).toHaveBeenCalledWith(STORAGE_ETH_ACCOUNTS_KEY, []); expect(accounts).toEqual([]); }); it("should load chain from storage", async () => { const chain = await mockStorage.loadObject(STORAGE_ETH_ACTIVE_CHAIN_KEY, { id: DEFAULT_CHAIN_ID, }); expect(mockStorage.loadObject).toHaveBeenCalledWith(STORAGE_ETH_ACTIVE_CHAIN_KEY, { id: DEFAULT_CHAIN_ID }); expect(chain.id).toBe(DEFAULT_CHAIN_ID); }); }); describe("Chain Management", () => { it("should get default RPC URL for supported chains", () => { expect(getDefaultRpcUrl(1)).toBeDefined(); expect(getDefaultRpcUrl(42161)).toBeDefined(); expect(getDefaultRpcUrl(8453)).toBeDefined(); expect(getDefaultRpcUrl(137)).toBeDefined(); expect(getDefaultRpcUrl(10)).toBeDefined(); }); it("should return undefined for unsupported chain RPC", () => { expect(getDefaultRpcUrl(999999)).toBeUndefined(); }); it("should validate chain has required properties", () => { const validChain: Chain = { id: 1, rpcUrl: "https://eth.merkle.io", }; expect(validChain.id).toBeDefined(); expect(validChain.rpcUrl).toBeDefined(); }); }); describe("Message Formatting", () => { it("should format connect message correctly", () => { const message = { chainId: DEFAULT_CHAIN_ID, event: GeminiSdkEvent.SDK_CONNECT, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(message.event).toBe(GeminiSdkEvent.SDK_CONNECT); expect(message.chainId).toBe(DEFAULT_CHAIN_ID); expect(message.origin).toBeDefined(); expect(message.requestId).toBeDefined(); }); it("should format transaction message correctly", () => { const txRequest = { data: "0x" as const, from: mockAddress, to: mockAddress, value: 100n, }; const message = { chainId: DEFAULT_CHAIN_ID, data: txRequest, event: GeminiSdkEvent.SDK_SEND_TRANSACTION, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(message.event).toBe(GeminiSdkEvent.SDK_SEND_TRANSACTION); expect(message.data).toEqual(txRequest); expect(message.data.value).toBe(100n); }); it("should format sign message correctly", () => { const signMessage = "0x48656c6c6f20576f726c64"; const message = { chainId: DEFAULT_CHAIN_ID, data: { message: signMessage }, event: GeminiSdkEvent.SDK_SIGN_DATA, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(message.event).toBe(GeminiSdkEvent.SDK_SIGN_DATA); expect(message.data.message).toBe(signMessage); }); it("should format typed data message correctly", () => { const typedData = { domain: { chainId: 1, name: "Test Domain", verifyingContract: mockAddress, version: "1", }, message: { message: "Hello", value: 123, }, primaryType: "Test" as const, types: { Test: [ { name: "value", type: "uint256" }, { name: "message", type: "string" }, ], }, }; const message = { chainId: DEFAULT_CHAIN_ID, data: typedData, event: GeminiSdkEvent.SDK_SIGN_TYPED_DATA, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(message.event).toBe(GeminiSdkEvent.SDK_SIGN_TYPED_DATA); expect(message.data).toEqual(typedData); expect(message.data.primaryType).toBe("Test"); }); it("should format switch chain message correctly", () => { const chainId = 42161; const message = { chainId: DEFAULT_CHAIN_ID, data: chainId, event: GeminiSdkEvent.SDK_SWITCH_CHAIN, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(message.event).toBe(GeminiSdkEvent.SDK_SWITCH_CHAIN); expect(message.data).toBe(chainId); }); }); describe("Response Handling", () => { it("should handle successful connect response", () => { const response: ConnectResponse = { chainId: DEFAULT_CHAIN_ID, data: { address: mockAddress }, event: GeminiSdkEvent.SDK_CONNECT_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.address).toBe(mockAddress); expect(response.data.error).toBeUndefined(); }); it("should handle error connect response", () => { const response: ConnectResponse = { chainId: DEFAULT_CHAIN_ID, data: { error: "User rejected" }, event: GeminiSdkEvent.SDK_CONNECT_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.error).toBe("User rejected"); expect(response.data.address).toBeUndefined(); }); it("should handle successful transaction response", () => { const response: SendTransactionResponse = { chainId: DEFAULT_CHAIN_ID, data: { hash: mockTxHash }, event: GeminiSdkEvent.SDK_SEND_TRANSACTION_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.hash).toBe(mockTxHash); expect(response.data.error).toBeUndefined(); }); it("should handle error transaction response", () => { const response: SendTransactionResponse = { chainId: DEFAULT_CHAIN_ID, data: { error: "Insufficient funds" }, event: GeminiSdkEvent.SDK_SEND_TRANSACTION_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.error).toBe("Insufficient funds"); expect(response.data.hash).toBeUndefined(); }); it("should handle successful signature response", () => { const response: SignMessageResponse = { chainId: DEFAULT_CHAIN_ID, data: { signature: mockSigHash }, event: GeminiSdkEvent.SDK_SIGN_DATA_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.signature).toBe(mockSigHash); expect(response.data.error).toBeUndefined(); }); it("should handle error signature response", () => { const response: SignMessageResponse = { chainId: DEFAULT_CHAIN_ID, data: { error: "User rejected signature" }, event: GeminiSdkEvent.SDK_SIGN_DATA_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.error).toBe("User rejected signature"); expect(response.data.signature).toBeUndefined(); }); it("should handle switch chain response", () => { const response: SwitchChainResponse = { chainId: DEFAULT_CHAIN_ID, data: { error: "Chain not supported" }, event: GeminiSdkEvent.SDK_SWITCH_CHAIN_RESPONSE, origin: "http://localhost:3000", requestId: "test-uuid", }; expect(response.data.error).toBe("Chain not supported"); }); }); });