import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Connection } from '@solana/web3.js'; import { RealWalletWatcher } from '../src/solana/watcher.js'; import { fetchTransactionWithRetry } from '../src/solana/fetcher.js'; import { InMemoryRawWalletTransactionRepository, InMemoryEventJournalRepository, } from './mocks/InMemoryRepositories.js'; import { EventJournalService } from '../src/persistence/eventJournal.js'; import { WalletTransactionObservation } from '../src/domain/interfaces.js'; import { RawWalletTransaction } from '../src/domain/models.js'; import { randomUUID } from 'crypto'; // Mock @solana/web3.js references starting with "mock" to be accessible in hoisted vi.mock const mockOnLogs = vi.fn().mockReturnValue(12345); const mockRemoveOnLogsListener = vi.fn().mockResolvedValue(true); const mockGetTransaction = vi.fn(); vi.mock('@solana/web3.js', () => { class MockConnection { onLogs = mockOnLogs; removeOnLogsListener = mockRemoveOnLogsListener; getTransaction = mockGetTransaction; } class MockPublicKey { constructor(public address: string) {} toBase58() { return this.address; } } return { Connection: MockConnection, PublicKey: MockPublicKey, }; }); describe('Real Solana Observation Mode Unit Tests', () => { let connection: any; beforeEach(() => { vi.clearAllMocks(); connection = new Connection('http://dummy', 'confirmed'); }); // 1. Transaction fetch retry with bounded exponential backoff describe('Transaction Fetch Retry Mechanism', () => { it('should successfully return transaction if it is found on the first attempt', async () => { const mockTx = { slot: 12345, blockTime: 1710000000 }; connection.getTransaction.mockResolvedValueOnce(mockTx); const result = await fetchTransactionWithRetry(connection as any, 'dummy-sig', 3, 10, 20); expect(result).toEqual(mockTx); expect(connection.getTransaction).toHaveBeenCalledTimes(1); }); it('should retry fetching with backoff and succeed if transaction becomes available eventually', async () => { const mockTx = { slot: 12345, blockTime: 1710000000 }; connection.getTransaction .mockResolvedValueOnce(null) // first attempt: temporarily unavailable .mockRejectedValueOnce(new Error('RPC limit reached')) // second attempt: error .mockResolvedValueOnce(mockTx); // third attempt: success const result = await fetchTransactionWithRetry(connection as any, 'dummy-sig', 4, 10, 50); expect(result).toEqual(mockTx); expect(connection.getTransaction).toHaveBeenCalledTimes(3); }); it('should stop retrying and throw error if maxRetries limit is reached', async () => { connection.getTransaction.mockResolvedValue(null); // always returns null await expect( fetchTransactionWithRetry(connection as any, 'dummy-sig', 3, 5, 10) ).rejects.toThrow('Transaction dummy-sig could not be fetched after 3 attempts.'); expect(connection.getTransaction).toHaveBeenCalledTimes(3); }); }); // 2. RealWalletWatcher Subscription and Shutdown Behavior describe('RealWalletWatcher Lifecycle & Reconnect Behavior', () => { it('should subscribe to logs for all watched wallets on startup', async () => { const wallets = ['3yF9asA9B7G3Y1G7as78gHJKa7A', '4yF9asA9B7G3Y1G7as78gHJKa7B']; const watcher = new RealWalletWatcher(wallets, 'http://dummy-rpc', 'wss://dummy-wss'); await watcher.startWatching(); expect(connection.onLogs).toHaveBeenCalledTimes(2); expect(connection.onLogs).toHaveBeenNthCalledWith(1, expect.any(Object), expect.any(Function), 'confirmed'); expect(connection.onLogs).toHaveBeenNthCalledWith(2, expect.any(Object), expect.any(Function), 'confirmed'); }); it('should unsubscribe from all logs listeners on shutdown', async () => { const wallets = ['3yF9asA9B7G3Y1G7as78gHJKa7A']; const watcher = new RealWalletWatcher(wallets, 'http://dummy-rpc', 'wss://dummy-wss'); await watcher.startWatching(); await watcher.stopWatching(); expect(connection.removeOnLogsListener).toHaveBeenCalledTimes(1); expect(connection.removeOnLogsListener).toHaveBeenCalledWith(12345); }); }); // 3. Real Mode Pipeline orchestration, Raw Persistence, Duplicates, and Stopping describe('Real Mode Pipeline Logic', () => { let rawTxRepo: InMemoryRawWalletTransactionRepository; let journalRepo: InMemoryEventJournalRepository; let journalService: EventJournalService; beforeEach(() => { rawTxRepo = new InMemoryRawWalletTransactionRepository(); journalRepo = new InMemoryEventJournalRepository(); journalService = new EventJournalService(journalRepo, '1.0.0', 'commit', 'hash'); }); // Simulated callback registration helper replicating Application real-mode callback const runPipelineCallback = async ( observation: WalletTransactionObservation, watcherInstance: any, ) => { const correlationId = observation.correlationId || randomUUID(); const detectedAt = observation.timestamp || new Date(); // Check duplicate first const existing = await rawTxRepo.getTransactionBySignature(observation.signature); if (existing) { await journalService.log(correlationId, 'duplicate_transaction_ignored', { signature: observation.signature, walletAddress: observation.walletAddress, }); return { stopped: true, reason: 'duplicate' }; } await journalService.log(correlationId, 'wallet_activity_detected', { walletAddress: observation.walletAddress, signature: observation.signature, slot: observation.slot, detectedAt, }); // Fetch transaction await journalService.log(correlationId, 'raw_transaction_fetch_started', { signature: observation.signature, }); let txPayload: any; try { txPayload = await fetchTransactionWithRetry(watcherInstance.connection, observation.signature, 3, 5, 10); } catch (error) { await journalService.log(correlationId, 'raw_transaction_fetch_failed', { signature: observation.signature, error: error instanceof Error ? error.message : String(error), }); return { stopped: true, reason: 'fetch_failed' }; } await journalService.log(correlationId, 'raw_transaction_fetched', { signature: observation.signature, slot: txPayload.slot, blockTime: txPayload.blockTime, }); // Persist raw transaction try { const blockTime = txPayload.blockTime ? new Date(txPayload.blockTime * 1000) : null; const rawTx: RawWalletTransaction = { transactionSignature: observation.signature, sourceWallet: observation.walletAddress, slot: observation.slot, blockTime, detectedAt, rawPayload: txPayload, parserVersion: 'ingestion_v1', }; await rawTxRepo.saveTransaction(rawTx); await journalService.log(correlationId, 'raw_transaction_persisted', { signature: observation.signature, slot: observation.slot, }); } catch { // failed to persist } // STOP: The pipeline must return here and NOT trigger strategy or simulation return { stopped: true, reason: 'pipeline_stopped_normally' }; }; it('should fetch, persist raw transaction, log events to journal, and STOP', async () => { const observation: WalletTransactionObservation = { signature: 'tx-sig-123', slot: 100, walletAddress: '3yF9asA9B7G3Y1G7as78gHJKa7A', timestamp: new Date(), correlationId: 'corr-real-mode', }; const mockTxPayload = { slot: 100, blockTime: Math.floor(Date.now() / 1000), meta: { logMessages: ['program instruction...'] }, }; connection.getTransaction.mockResolvedValueOnce(mockTxPayload); const watcher = new RealWalletWatcher(['3yF9asA9B7G3Y1G7as78gHJKa7A'], 'http://dummy', 'wss://dummy'); (watcher as any).connection = connection; // inject connection mock const res = await runPipelineCallback(observation, watcher); // Verify the pipeline stopped normally expect(res.stopped).toBe(true); expect(res.reason).toBe('pipeline_stopped_normally'); // Verify the raw transaction was saved const savedTx = await rawTxRepo.getTransactionBySignature('tx-sig-123'); expect(savedTx).not.toBeNull(); expect(savedTx?.transactionSignature).toBe('tx-sig-123'); expect(savedTx?.sourceWallet).toBe('3yF9asA9B7G3Y1G7as78gHJKa7A'); expect(savedTx?.rawPayload).toEqual(mockTxPayload); // Verify Event Journal logs const events = await journalRepo.getEventsByCorrelationId('corr-real-mode'); const eventTypes = events.map((e) => e.eventType); expect(eventTypes).toContain('wallet_activity_detected'); expect(eventTypes).toContain('raw_transaction_fetch_started'); expect(eventTypes).toContain('raw_transaction_fetched'); expect(eventTypes).toContain('raw_transaction_persisted'); }); it('should ignore and log duplicate websocket notifications without calling RPC fetch again', async () => { const observation: WalletTransactionObservation = { signature: 'tx-sig-dup', slot: 100, walletAddress: '3yF9asA9B7G3Y1G7as78gHJKa7A', timestamp: new Date(), correlationId: 'corr-real-mode-dup', }; // Pre-populate raw persistence to simulate duplicate await rawTxRepo.saveTransaction({ transactionSignature: 'tx-sig-dup', sourceWallet: '3yF9asA9B7G3Y1G7as78gHJKa7A', slot: 100, blockTime: null, detectedAt: new Date(), rawPayload: {}, parserVersion: 'ingestion_v1', }); const watcher = new RealWalletWatcher(['3yF9asA9B7G3Y1G7as78gHJKa7A'], 'http://dummy', 'wss://dummy'); (watcher as any).connection = connection; const res = await runPipelineCallback(observation, watcher); // Verify pipeline stopped expect(res.stopped).toBe(true); expect(res.reason).toBe('duplicate'); // Verify connection.getTransaction was NEVER called (saving RPC rate limit!) expect(connection.getTransaction).not.toHaveBeenCalled(); // Verify duplicate event is written to journal const events = await journalRepo.getEventsByCorrelationId('corr-real-mode-dup'); expect(events.map((e) => e.eventType)).toContain('duplicate_transaction_ignored'); }); it('should log raw_transaction_fetch_failed if RPC continues to fail and return gracefully', async () => { const observation: WalletTransactionObservation = { signature: 'tx-sig-fail', slot: 100, walletAddress: '3yF9asA9B7G3Y1G7as78gHJKa7A', timestamp: new Date(), correlationId: 'corr-real-mode-fail', }; connection.getTransaction.mockResolvedValue(null); // simulates always failing to fetch const watcher = new RealWalletWatcher(['3yF9asA9B7G3Y1G7as78gHJKa7A'], 'http://dummy', 'wss://dummy'); (watcher as any).connection = connection; const res = await runPipelineCallback(observation, watcher); // Verify pipeline stopped on failure expect(res.stopped).toBe(true); expect(res.reason).toBe('fetch_failed'); // Verify fetch failed event is in journal const events = await journalRepo.getEventsByCorrelationId('corr-real-mode-fail'); expect(events.map((e) => e.eventType)).toContain('raw_transaction_fetch_failed'); }); }); });