@@ -1343,3 +1343,178 @@ describe('Regression: mempool + retries + onMessage + cache TTL', () => {
13431343  } ) 
13441344} ) 
13451345
1346+ describe . only ( 'WS onMessage matrix (no re-mocks)' ,  ( )  =>  { 
1347+   beforeAll ( ( )  =>  { 
1348+     process . env . WS_AUTH_KEY  =  'test-auth-key' 
1349+   } ) 
1350+ 
1351+   let  client : any 
1352+   let  fetchAddressesArray : jest . Mock 
1353+ 
1354+   beforeEach ( ( )  =>  { 
1355+     jest . clearAllMocks ( ) 
1356+ 
1357+     // fresh client 
1358+     client  =  new  ChronikBlockchainClient ( 'ecash' ) 
1359+ 
1360+     // avoid any wait-paths that depend on async ctor 
1361+     client . setInitialized ( )  // initializing=false 
1362+ 
1363+     // ensure ws endpoint exists for BLK_FINALIZED logs 
1364+     client . chronikWSEndpoint  =  { 
1365+       subs : {  scripts : [ ]  } , 
1366+       subscribeToBlocks : jest . fn ( ) , 
1367+       waitForOpen : jest . fn ( ) 
1368+     }  as  any 
1369+ 
1370+     // addressService mocks used by getAddressesForTransaction / waitForSyncing 
1371+     ; ( {  fetchAddressesArray }  =  require ( '../../services/addressService' ) ) 
1372+     fetchAddressesArray . mockResolvedValue ( [ 
1373+       { 
1374+         id : 'addr-1' , 
1375+         address : 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' , 
1376+         networkId : 1 , 
1377+         syncing : false , 
1378+         lastSynced : new  Date ( ) . toISOString ( ) 
1379+       } 
1380+     ] ) 
1381+ 
1382+     // never hit real payment layer in these tests 
1383+     jest . spyOn ( client  as  any ,  'handleUpdateClientPaymentStatus' ) . mockResolvedValue ( undefined ) 
1384+   } ) 
1385+ 
1386+   it ( 'handles TX_REMOVED_FROM_MEMPOOL → deletes unconfirmed txs' ,  async  ( )  =>  { 
1387+     const  {  fetchUnconfirmedTransactions,  deleteTransactions }  =  require ( '../../services/transactionService' ) 
1388+     fetchUnconfirmedTransactions . mockResolvedValueOnce ( [ 'tx-to-del' ] ) 
1389+     deleteTransactions . mockResolvedValueOnce ( undefined ) 
1390+ 
1391+     await  client . processWsMessage ( {  type : 'Tx' ,  msgType : 'TX_REMOVED_FROM_MEMPOOL' ,  txid : 'deadbeef'  } ) 
1392+ 
1393+     expect ( fetchUnconfirmedTransactions ) . toHaveBeenCalledWith ( 'deadbeef' ) 
1394+     expect ( deleteTransactions ) . toHaveBeenCalledWith ( [ 'tx-to-del' ] ) 
1395+   } ) 
1396+ 
1397+   it ( 'handles TX_CONFIRMED → uses fetchTxWithRetry and updates payments for related addresses' ,  async  ( )  =>  { 
1398+     const  fetchSpy  =  jest . spyOn ( client ,  'fetchTxWithRetry' ) . mockResolvedValue ( { 
1399+       txid : 'txCONF' , 
1400+       inputs : [ ] , 
1401+       outputs : [ {  sats : 10n ,  outputScript : '76a914c5d2460186f7233c927e7db2dcc703c0e500b65388ac'  } ] 
1402+     } ) 
1403+ 
1404+     // deterministic related addresses 
1405+     jest . spyOn ( client  as  any ,  'getRelatedAddressesForTransaction' ) 
1406+       . mockReturnValue ( [ 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' ] ) 
1407+ 
1408+     // minimal transaction shape for downstream 
1409+     jest . spyOn ( client  as  any ,  'getTransactionFromChronikTransaction' ) 
1410+       . mockResolvedValue ( { 
1411+         hash : 'txCONF' , 
1412+         amount : '0.01' , 
1413+         timestamp : Math . floor ( Date . now ( )  /  1000 ) , 
1414+         addressId : 'addr-1' , 
1415+         confirmed : false , 
1416+         opReturn : JSON . stringify ( {  message : {  type : 'PAY' ,  paymentId : 'pid-1'  }  } ) 
1417+       } ) 
1418+ 
1419+     const  paySpy  =  jest . spyOn ( client  as  any ,  'handleUpdateClientPaymentStatus' ) 
1420+ 
1421+     await  client . processWsMessage ( {  type : 'Tx' ,  msgType : 'TX_CONFIRMED' ,  txid : 'txCONF'  } ) 
1422+ 
1423+     expect ( fetchSpy ) . toHaveBeenCalledWith ( 'txCONF' ) 
1424+     expect ( paySpy ) . toHaveBeenCalled ( ) 
1425+     expect ( client . confirmedTxsHashesFromLastBlock ) . toContain ( 'txCONF' ) 
1426+   } ) 
1427+ 
1428+   it ( 'handles TX_ADDED_TO_MEMPOOL → calls fetchTxWithRetry and upserts, triggers once' ,  async  ( )  =>  { 
1429+     const  {  upsertTransaction }  =  require ( '../../services/transactionService' ) 
1430+     const  {  executeAddressTriggers }  =  require ( '../../services/triggerService' ) 
1431+ 
1432+     jest . spyOn ( client  as  any ,  'isAlreadyBeingProcessed' ) . mockReturnValue ( false ) 
1433+ 
1434+     jest . spyOn ( client ,  'fetchTxWithRetry' ) . mockResolvedValue ( { 
1435+       txid : 'txMEM' , 
1436+       inputs : [ ] , 
1437+       outputs : [ {  sats : 10n ,  outputScript : '76a914c5d2460186f7233c927e7db2dcc703c0e500b65388ac'  } ] 
1438+     } ) 
1439+ 
1440+     jest . spyOn ( client  as  any ,  'getAddressesForTransaction' ) . mockResolvedValue ( [ 
1441+       { 
1442+         address : {  address : 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' ,  networkId : 1  } , 
1443+         transaction : { 
1444+           hash : 'txMEM' , 
1445+           amount : '0.01' , 
1446+           timestamp : Math . floor ( Date . now ( )  /  1000 ) , 
1447+           addressId : 'addr-1' , 
1448+           confirmed : false , 
1449+           opReturn : JSON . stringify ( {  message : {  type : 'PAY' ,  paymentId : 'pid-2'  }  } ) 
1450+         } 
1451+       } 
1452+     ] ) 
1453+ 
1454+     upsertTransaction . mockResolvedValue ( { 
1455+       created : true , 
1456+       tx : {  address : {  networkId : 1 ,  address : 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h'  }  } 
1457+     } ) 
1458+ 
1459+     await  client . processWsMessage ( {  type : 'Tx' ,  msgType : 'TX_ADDED_TO_MEMPOOL' ,  txid : 'txMEM'  } ) 
1460+ 
1461+     expect ( client . fetchTxWithRetry ) . toHaveBeenCalledWith ( 'txMEM' ) 
1462+     expect ( upsertTransaction ) . toHaveBeenCalledTimes ( 1 ) 
1463+     expect ( executeAddressTriggers ) . toHaveBeenCalledTimes ( 1 ) 
1464+     expect ( client . mempoolTxsBeingProcessed ) . toBe ( 0 ) 
1465+   } ) 
1466+ 
1467+   it ( 'TX_ADDED_TO_MEMPOOL → short-circuits when already being processed' ,  async  ( )  =>  { 
1468+     const  fetchSpy  =  jest . spyOn ( client ,  'fetchTxWithRetry' ) 
1469+     jest . spyOn ( client  as  any ,  'isAlreadyBeingProcessed' ) . mockReturnValue ( true ) 
1470+ 
1471+     await  client . processWsMessage ( {  type : 'Tx' ,  msgType : 'TX_ADDED_TO_MEMPOOL' ,  txid : 'dup'  } ) 
1472+ 
1473+     expect ( fetchSpy ) . not . toHaveBeenCalled ( ) 
1474+   } ) 
1475+ 
1476+   it ( 'TX_ADDED_TO_MEMPOOL → retries on 404-ish twice then succeeds (uses fake timers)' ,  async  ( )  =>  { 
1477+     jest . useFakeTimers ( ) 
1478+ 
1479+     let  attempts  =  0 
1480+     // drive underlying chronik.tx via the real fetchTxWithRetry 
1481+     ; ( client . chronik  as  any )  =  {  tx : jest . fn ( async  ( )  =>  { 
1482+       attempts  +=  1 
1483+       if  ( attempts  <  3 )  throw  new  Error ( 'Transaction not found in the index' ) 
1484+       return  {  txid : 'tx404' ,  inputs : [ ] ,  outputs : [ ]  } 
1485+     } ) } 
1486+ 
1487+     jest . spyOn ( client  as  any ,  'isAlreadyBeingProcessed' ) . mockReturnValue ( false ) 
1488+     jest . spyOn ( client  as  any ,  'getAddressesForTransaction' ) . mockResolvedValue ( [ ] ) 
1489+ 
1490+     const  p  =  client . processWsMessage ( {  type : 'Tx' ,  msgType : 'TX_ADDED_TO_MEMPOOL' ,  txid : 'tx404'  } ) 
1491+ 
1492+     // advance 1s + 2s exponential backoffs 
1493+     await  jest . advanceTimersByTimeAsync ( 1000 ) 
1494+     await  jest . advanceTimersByTimeAsync ( 2000 ) 
1495+ 
1496+     await  p 
1497+     expect ( attempts ) . toBe ( 3 ) 
1498+     expect ( client . mempoolTxsBeingProcessed ) . toBe ( 0 ) 
1499+ 
1500+     jest . useRealTimers ( ) 
1501+   } ) 
1502+ 
1503+   it ( 'handles Block → BLK_FINALIZED triggers sync and clears cache' ,  async  ( )  =>  { 
1504+     client . initializing  =  false 
1505+     client . confirmedTxsHashesFromLastBlock  =  [ 'A' ,  'B' ] 
1506+     const  syncSpy  =  jest . spyOn ( client  as  any ,  'syncBlockTransactions' ) . mockResolvedValue ( undefined ) 
1507+ 
1508+     await  client . processWsMessage ( {  type : 'Block' ,  msgType : 'BLK_FINALIZED' ,  blockHash : 'bh' ,  blockHeight : 123  } ) 
1509+ 
1510+     expect ( syncSpy ) . toHaveBeenCalledWith ( 'bh' ) 
1511+     expect ( client . confirmedTxsHashesFromLastBlock ) . toEqual ( [ ] ) 
1512+   } ) 
1513+ 
1514+   it ( 'handles type=Error → logs JSON payload' ,  async  ( )  =>  { 
1515+     const  logSpy  =  jest . spyOn ( console ,  'log' ) . mockImplementation ( ( )  =>  { } ) 
1516+     await  client . processWsMessage ( {  type : 'Error' ,  msg : {  code : 42 ,  reason : 'nope'  }  }  as  any ) 
1517+     expect ( logSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( '[CHRONIK — ecash]: [Error]' ) ) 
1518+     logSpy . mockRestore ( ) 
1519+   } ) 
1520+ } ) 
0 commit comments