import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PrexClient } from './prex-client'; import { PrexApiService } from './api'; import { PublicActions } from 'viem'; import { ProviderInterface } from './providers/base-provider'; import * as WebAuthnAuth from './core/web-authn'; import { MockStorage } from './storage/MockStorage'; vi.mock('../src/api'); vi.mock('../src/storage/MockStorage', () => { return { MockStorage: vi.fn().mockImplementation(() => ({ getItem: vi.fn(), setItem: vi.fn(), removeItem: vi.fn(), })), }; }); const mockExecuteOperation = vi.fn(); vi.mock('./actions/execute-operation', () => { return { ExecuteOperationAction: vi.fn().mockImplementation(() => ({ executeOperation: mockExecuteOperation, })), }; }); const MOCK_ADDRESS = '0x1234000000000000000000000000000000000000'; const MOCK_TOKEN = '0x1234567890123456789012345678901234567890'; describe('PrexClient', () => { let prexClient: PrexClient; let mockApiService: PrexApiService; let mockEvmChainClient: PublicActions; let mockProvider: ProviderInterface; let mockStorage: MockStorage; beforeEach(() => { mockApiService = { chainId: 421614, setApiKey: vi.fn(), register: vi.fn(), createAccount: vi.fn(), setEthAddress: vi.fn(), updateNickName: vi.fn(), transfer: vi.fn(), submitLinkTransfer: vi.fn(), signPaymasterAndData: vi.fn(), getConfig: vi.fn().mockResolvedValue({ fee: [], max_fee_per_gas: '0.1', max_priority_fee_per_gas: '0.1', }), } as unknown as PrexApiService; mockEvmChainClient = { multicall: vi.fn(), readContract: vi.fn(() => 100n), estimateGas: vi.fn(() => 100n), } as unknown as PublicActions; mockProvider = {} as ProviderInterface; mockStorage = new MockStorage(); prexClient = new PrexClient(421614, 'testPolicyId', { apiKey: 'testApiKey', debugMode: true, provider: mockProvider, storage: mockStorage, }); prexClient['apiService'] = mockApiService; prexClient['evmChainClient'] = mockEvmChainClient; vi.mocked(mockStorage.setItem).mockReset(); vi.mocked(mockStorage.removeItem).mockReset(); }); describe('constructor', () => { it('should initialize with correct values', () => { expect(prexClient['apiService']).toBeDefined(); expect(prexClient['evmChainClient']).toBeDefined(); expect(prexClient['logger']).toBeDefined(); expect(prexClient['provider']).toBe(mockProvider); }); }); describe('setApiKey', () => { it('should set the API key', () => { prexClient.setApiKey('newApiKey'); expect(mockApiService.setApiKey).toHaveBeenCalledWith('newApiKey'); }); }); describe('isPasskeyAvailable', () => { it('should check if passkey is available', async () => { // Mock the necessary functions vi.stubGlobal('navigator', { credentials: { create: vi.fn(), get: vi.fn(), }, }); vi.stubGlobal('PublicKeyCredential', { isUserVerifyingPlatformAuthenticatorAvailable: vi .fn() .mockResolvedValue(true), }); const result = await prexClient.isPasskeyAvailable(); expect(result).toBe(false); }); }); describe('createWallet', () => { it('should create a new wallet', async () => { const mockCredential = { walletId: 'testWalletId', userHandle: 'testUserHandle', displayName: 'Test User', credential: {} as any, }; vi.spyOn(prexClient as any, 'createOrGetCredential').mockResolvedValue( mockCredential ); vi.mocked(mockApiService.register).mockResolvedValue({ eth_address: MOCK_ADDRESS, }); vi.mocked(mockApiService.createAccount).mockResolvedValue({} as any); vi.mocked(mockStorage.getItem).mockResolvedValue(null); vi.mocked(mockStorage.setItem).mockResolvedValue(undefined); const result = await prexClient.createWallet({ withDeploy: true }); expect(result).toBeDefined(); expect(result?.wallet).toBeDefined(); expect(result?.wallet.address).toBe(MOCK_ADDRESS); expect(mockApiService.register).toHaveBeenCalledWith( mockCredential.walletId, mockCredential.credential, expect.any(String) ); expect(mockApiService.createAccount).toHaveBeenCalledWith( mockCredential.walletId ); expect(mockStorage.setItem).toHaveBeenCalledWith('address', MOCK_ADDRESS); }); it('should revert if wallet already exists', async () => { vi.mocked(mockStorage.getItem).mockResolvedValue('0x456'); await expect(prexClient.createWallet()).rejects.toThrow( 'Wallet already exists' ); }); }); describe('restoreWallet', () => { it('should restore an existing wallet', async () => { const mockPasskeyId = 'testPasskeyId'; vi.spyOn(prexClient as any, 'restoreWalletInner').mockResolvedValue( undefined ); vi.spyOn(WebAuthnAuth, 'getCredentialID').mockResolvedValue( mockPasskeyId ); await prexClient.restoreWallet(); expect(prexClient['restoreWalletInner']).toHaveBeenCalledWith( mockPasskeyId ); }); it('should throw an error if getUserHandleWithError fails', async () => { vi.spyOn(WebAuthnAuth, 'getCredentialID').mockRejectedValue( new Error('Failed to get user handle') ); await expect(prexClient.restoreWallet()).rejects.toThrow( 'Failed to get user handle' ); }); }); describe('updateNickName', () => { it('should update the nickname', async () => { prexClient.user = { address: MOCK_ADDRESS } as any; prexClient.signer = { signReplaySafeHash: vi.fn().mockResolvedValue('0xsignature'), signHash: vi.fn().mockResolvedValue('0xsignature'), } as any; await prexClient.updateNickName({ nickName: 'New Nickname' }); expect(mockExecuteOperation).toHaveBeenCalledWith( expect.any(String), expect.any(String), { from: MOCK_ADDRESS, } ); }); it('should throw an error if user is not initialized', async () => { prexClient.user = undefined; await expect( prexClient.updateNickName({ nickName: 'New Nickname' }) ).rejects.toThrow('User not initialized'); }); }); describe('transfer', () => { it('should initiate a token transfer', async () => { prexClient.setAccountAddress(MOCK_ADDRESS); prexClient.user = { address: MOCK_ADDRESS } as any; prexClient.signer = { signTypedData: vi.fn().mockResolvedValue('0xsignature'), } as any; vi.mocked(mockApiService.transfer).mockResolvedValue({} as any); await prexClient.transfer({ token: '0x1234567890123456789012345678901234567890', recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', amount: BigInt(100), metadata: { memo: 'Test transfer' }, }); expect(mockApiService.transfer).toHaveBeenCalledWith( expect.any(String), '0xsignature' ); }); it('should throw an error if user is not initialized', async () => { prexClient.user = undefined; await expect( prexClient.transfer({ token: '0x1234567890123456789012345678901234567890', recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', amount: BigInt(100), metadata: { memo: 'Test transfer' }, }) ).rejects.toThrow('User not initialized'); }); }); describe('fetchBalance', () => { it('should fetch the balance for a given token', async () => { prexClient.user = { address: MOCK_ADDRESS } as any; mockEvmChainClient.multicall = vi.fn().mockResolvedValue([ { result: '1000', status: 'success' }, { result: '500', status: 'success' }, ]); const balance = await prexClient.fetchBalance(MOCK_TOKEN); expect(balance).toStrictEqual({ balance: BigInt(1000), allowance: BigInt(500), }); expect(prexClient.balances[MOCK_TOKEN]).toBe(BigInt(1000)); expect(prexClient.allowances[MOCK_TOKEN]).toBe(BigInt(500)); }); it('should throw an error if user is not initialized', async () => { prexClient.user = undefined; await expect(prexClient.fetchBalance(MOCK_TOKEN)).rejects.toThrow( 'User not initialized' ); }); }); describe('transferByLink', () => { it('should initiate a transfer by link', async () => { prexClient.setAccountAddress(MOCK_ADDRESS); prexClient.user = { address: MOCK_ADDRESS } as any; prexClient.signer = { chainId: 421614, signTypedData: vi.fn().mockResolvedValue('0xsignature'), } as any; vi.mocked(mockApiService.submitLinkTransfer).mockResolvedValue({ hash: '0x123', }); // mock for fetchBalance mockEvmChainClient.readContract = vi .fn() .mockResolvedValue([{ result: BigInt(0) }]); mockEvmChainClient.multicall = vi .fn() .mockResolvedValue([{ result: BigInt(1000) }, { result: BigInt(500) }]); const expiration = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now const result = await prexClient.transferByLink({ token: MOCK_TOKEN, amount: BigInt(100), expiration, metadata: { memo: 'Test transfer by link' }, }); expect(mockApiService.submitLinkTransfer).toHaveBeenCalledWith( expect.any(String), '0xsignature' ); expect(result).toStrictEqual({ id: expect.any(String), secret: expect.any(String), hash: expect.any(String), }); }); it('should throw an error if user is not initialized', async () => { prexClient.user = undefined; const expiration = Math.floor(Date.now() / 1000) + 3600; await expect( prexClient.transferByLink({ token: MOCK_TOKEN, amount: BigInt(100), expiration, }) ).rejects.toThrow('User not initialized'); }); it('should throw an error if the API response does not contain a link', async () => { prexClient.setAccountAddress(MOCK_ADDRESS); prexClient.user = { address: MOCK_ADDRESS } as any; prexClient.signer = { chainId: 421614, signTypedData: vi.fn().mockResolvedValue('0xsignature'), } as any; vi.mocked(mockApiService.submitLinkTransfer).mockRejectedValue( new Error('Failed to submit') ); mockEvmChainClient.readContract = vi .fn() .mockResolvedValue([{ result: BigInt(0) }]); const expiration = Math.floor(Date.now() / 1000) + 3600; await expect( prexClient.transferByLink({ token: MOCK_TOKEN, amount: BigInt(100), expiration, }) ).rejects.toThrow('Failed to submit'); }); }); });