const mockTransaction = { execute: jest.fn(), getReceipt: jest.fn(), transactionId: { toString: jest.fn().mockReturnValue('0.0.123@1234567890.000000000') } }; const mockTransactionResponse = { transactionId: { toString: jest.fn().mockReturnValue('0.0.123@1234567890.000000000') }, getReceipt: jest.fn().mockResolvedValue({ status: { toString: jest.fn().mockReturnValue('SUCCESS') }, transactionId: { toString: jest.fn().mockReturnValue('0.0.123@1234567890.000000000') } }) }; const mockTransactionReceipt = { status: { toString: jest.fn().mockReturnValue('SUCCESS') }, transactionId: { toString: jest.fn().mockReturnValue('0.0.123@1234567890.000000000') } }; jest.mock('@hashgraph/sdk', () => ({ Client: { forMainnet: jest.fn().mockReturnThis(), forTestnet: jest.fn().mockReturnThis(), setOperator: jest.fn().mockReturnThis(), close: jest.fn() }, Transaction: { fromBytes: jest.fn() }, PrivateKey: { fromString: jest.fn().mockReturnValue({ publicKey: {} }) }, AccountId: { fromString: jest.fn().mockReturnValue({ toString: jest.fn().mockReturnValue('0.0.12345') }) }, TransactionResponse: jest.fn(), TransactionReceipt: jest.fn(), ScheduleId: jest.fn(), ScheduleSignTransaction: jest.fn(), ScheduleDeleteTransaction: jest.fn() })); jest.mock('../../../src/main/utils/logger'); import { HederaService, type TransactionExecutionResult } from '../../../src/main/services/hedera-service'; describe('HederaService', () => { let hederaService: HederaService; let mockLogger: any; let mockClient: any; const mockExecutedTransaction = { transactionId: '0.0.123@1234567890.000000000', timestamp: new Date('2024-01-01T00:00:00Z'), transactionBytes: 'mock-transaction-bytes' }; beforeEach(() => { jest.clearAllMocks(); mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }; mockClient = { setOperator: jest.fn().mockReturnThis(), close: jest.fn() }; mockTransaction.execute.mockResolvedValue(mockTransactionResponse); mockTransaction.getReceipt.mockResolvedValue(mockTransactionResponse); mockTransaction.freezeWith = jest.fn().mockResolvedValue(mockTransaction); mockTransaction.sign = jest.fn().mockResolvedValue(mockTransaction); mockTransactionResponse.getReceipt.mockResolvedValue(mockTransactionReceipt); mockTransactionReceipt.status.toString.mockReturnValue('SUCCESS'); const { Transaction, Client } = require('@hashgraph/sdk'); Transaction.fromBytes.mockReturnValue(mockTransaction); Client.forMainnet.mockReturnValue(mockClient); Client.forTestnet.mockReturnValue(mockClient); const { Logger } = require('../../../src/main/utils/logger'); Logger.mockImplementation(() => mockLogger); HederaService['instance'] = null; hederaService = HederaService.getInstance(); }); afterEach(() => { HederaService['instance'] = null; }); describe('Singleton Pattern', () => { test('should return the same instance', () => { const instance1 = HederaService.getInstance(); const instance2 = HederaService.getInstance(); expect(instance1).toBe(instance2); }); test('should create new instance when singleton is reset', () => { const instance1 = HederaService.getInstance(); HederaService['instance'] = null; const instance2 = HederaService.getInstance(); expect(instance1).not.toBe(instance2); }); }); describe('Transaction Execution Status', () => { test('should check if transaction is executed', () => { expect(hederaService['isTransactionExecuted']('non-existent')).toBe(false); (hederaService as any).executedTransactions.set('existing-transaction', mockExecutedTransaction); expect(hederaService['isTransactionExecuted']('existing-transaction')).toBe(true); }); test('should retrieve executed transaction', () => { expect(hederaService['getExecutedTransaction']('non-existent')).toBeUndefined(); (hederaService as any).executedTransactions.set('existing-transaction', mockExecutedTransaction); const result = hederaService['getExecutedTransaction']('existing-transaction'); expect(result).toEqual(mockExecutedTransaction); }); }); describe('executeTransactionBytes', () => { beforeEach(() => { mockTransactionResponse.transactionId.toString.mockReturnValue('0.0.123@1234567890.000000000'); }); test('should execute transaction successfully on testnet', async () => { const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07', 'testnet' ); expect(result.success).toBe(true); expect(result.transactionId).toBe('0.0.123@1234567890.000000000'); expect(result.status).toBe('SUCCESS'); expect(mockLogger.info).toHaveBeenCalledWith( 'Starting transaction execution via Hedera SDK', expect.objectContaining({ accountId: '0.0.12345', network: 'testnet' }) ); }); test('should execute transaction successfully on mainnet', async () => { const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07', 'mainnet' ); expect(result.success).toBe(true); expect(require('@hashgraph/sdk').Client.forMainnet).toHaveBeenCalled(); }); test('should reject already executed transaction', async () => { (hederaService as any).executedTransactions.set('already-executed', mockExecutedTransaction); const result = await hederaService['executeTransactionBytes']( 'already-executed', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Transaction already executed'); }); test('should handle missing required parameters', async () => { const testCases = [ ['', '0.0.12345', 'private-key'], ['transaction-bytes', '', 'private-key'], ['transaction-bytes', '0.0.12345', ''] ]; for (const [bytes, accountId, privateKey] of testCases) { const result = await hederaService['executeTransactionBytes']( bytes, accountId, privateKey ); expect(result.success).toBe(false); expect(result.error).toContain('required'); } }); test('should handle transaction parsing errors', async () => { const parseError = new Error('Invalid transaction bytes'); require('@hashgraph/sdk').Transaction.fromBytes.mockImplementation(() => { throw parseError; }); const result = await hederaService['executeTransactionBytes']( 'invalid-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Invalid transaction bytes format'); }); test('should handle transaction execution errors', async () => { const executionError = new Error('Transaction execution failed'); mockTransaction.execute.mockRejectedValue(executionError); const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Transaction execution failed'); }); test('should handle receipt retrieval errors', async () => { const receiptError = new Error('Receipt retrieval failed'); mockTransactionResponse.getReceipt.mockRejectedValue(receiptError); const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Receipt retrieval failed'); }); test('should extract entity information from receipt', async () => { mockTransactionReceipt.status.toString.mockReturnValue('SUCCESS'); mockTransactionReceipt.accountId = { toString: () => '0.0.12345' }; mockTransactionReceipt.tokenId = { toString: () => '0.0.67890' }; const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(true); expect(result.entityId).toBe('0.0.67890'); expect(result.entityType).toBe('token'); }); test('should handle different transaction types', async () => { mockTransactionReceipt.status.toString.mockReturnValue('SUCCESS'); mockTransactionReceipt.tokenId = { toString: () => '0.0.67890' }; const result = await hederaService['executeTransactionBytes']( 'token-creation-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.entityId).toBe('0.0.67890'); expect(result.entityType).toBe('token'); }); }); describe('Error Handling', () => { test('should handle client initialization errors', async () => { require('@hashgraph/sdk').Client.forTestnet.mockImplementation(() => { throw new Error('Client initialization failed'); }); const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Client initialization failed'); }); test('should handle operator setting errors', async () => { mockClient.setOperator.mockImplementation(() => { throw new Error('Invalid operator credentials'); }); const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Invalid operator credentials'); }); test('should handle receipt parsing errors', async () => { mockTransactionReceipt.status.toString.mockImplementation(() => { throw new Error('Invalid status'); }); const result = await hederaService['executeTransactionBytes']( 'valid-transaction-bytes', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(result.success).toBe(false); expect(result.error).toContain('Invalid status'); }); }); describe('Integration Scenarios', () => { beforeEach(() => { mockTransactionResponse.transactionId.toString.mockReturnValue('0.0.123@1234567890.000000000'); }); test('should handle complete transaction lifecycle', async () => { expect(hederaService['isTransactionExecuted']('lifecycle-test')).toBe(false); const executeResult = await hederaService['executeTransactionBytes']( 'lifecycle-test', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(executeResult.success).toBe(true); expect(hederaService['isTransactionExecuted']('lifecycle-test')).toBe(true); const executedTx = hederaService['getExecutedTransaction']('lifecycle-test'); expect(executedTx).toBeDefined(); expect(executedTx?.transactionId).toBe('0.0.123@1234567890.000000000'); const duplicateResult = await hederaService['executeTransactionBytes']( 'lifecycle-test', '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ); expect(duplicateResult.success).toBe(false); expect(duplicateResult.error).toContain('Transaction already executed'); }); test('should handle multiple concurrent transactions', async () => { const transactions = [ { id: 'tx-1', bytes: 'bytes-1' }, { id: 'tx-2', bytes: 'bytes-2' }, { id: 'tx-3', bytes: 'bytes-3' } ]; const promises = transactions.map(tx => hederaService['executeTransactionBytes']( tx.bytes, '0.0.12345', '302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c61e54b5e07' ) ); const results = await Promise.all(promises); results.forEach(result => { expect(result.success).toBe(true); }); transactions.forEach(tx => { expect(hederaService['isTransactionExecuted'](tx.bytes)).toBe(true); }); }); }); });