@@ -489,3 +489,136 @@ def test_without_api_key(self) -> None:
489489
490490 def test_empty_string_api_key (self ) -> None :
491491 assert _build_auth_headers ("" ) == {}
492+
493+
494+ # ---------------------------------------------------------------------------
495+ # _base.py — _wait_for_transaction retry on transient network errors
496+ # ---------------------------------------------------------------------------
497+
498+
499+ class TestWaitForTransactionRetry :
500+ """Verify _wait_for_transaction retries on ConnectTimeout/ReadTimeout/ConnectError."""
501+
502+ @pytest .fixture
503+ def base_sdk (self ):
504+ """Create a minimal BaseSDK instance for testing."""
505+ from unittest .mock import AsyncMock
506+
507+ from aptos_sdk .account import Account
508+
509+ from decibel ._base import BaseSDK , BaseSDKOptions
510+ from decibel ._constants import TESTNET_CONFIG
511+
512+ account = Account .generate ()
513+ sdk = BaseSDK (
514+ TESTNET_CONFIG ,
515+ account ,
516+ opts = BaseSDKOptions (no_fee_payer = True ),
517+ )
518+ # Patch sleep to avoid real delays
519+ sdk ._async_sleep = AsyncMock () # type: ignore[assignment]
520+ return sdk
521+
522+ async def test_retries_on_connect_timeout_then_succeeds (self , base_sdk ) -> None :
523+ """SHALL retry on ConnectTimeout and succeed when tx confirms."""
524+ from unittest .mock import AsyncMock , patch
525+
526+ call_count = 0
527+
528+ async def mock_get (url , headers = None ):
529+ nonlocal call_count
530+ call_count += 1
531+ if call_count <= 2 :
532+ raise httpx .ConnectTimeout ("connection timed out" )
533+ # Third call succeeds
534+ resp = httpx .Response (
535+ 200 ,
536+ json = {"success" : True , "hash" : "0xabc" , "type" : "user_transaction" },
537+ )
538+ return resp
539+
540+ mock_client = AsyncMock ()
541+ mock_client .get = mock_get
542+ mock_client .__aenter__ = AsyncMock (return_value = mock_client )
543+ mock_client .__aexit__ = AsyncMock (return_value = False )
544+
545+ with patch ("httpx.AsyncClient" , return_value = mock_client ):
546+ result = await base_sdk ._wait_for_transaction ("0xabc" )
547+
548+ assert result ["success" ] is True
549+ assert call_count == 3
550+
551+ async def test_retries_on_read_timeout (self , base_sdk ) -> None :
552+ """SHALL retry on ReadTimeout."""
553+ from unittest .mock import AsyncMock , patch
554+
555+ call_count = 0
556+
557+ async def mock_get (url , headers = None ):
558+ nonlocal call_count
559+ call_count += 1
560+ if call_count == 1 :
561+ raise httpx .ReadTimeout ("read timed out" )
562+ resp = httpx .Response (
563+ 200 ,
564+ json = {"success" : True , "hash" : "0xabc" , "type" : "user_transaction" },
565+ )
566+ return resp
567+
568+ mock_client = AsyncMock ()
569+ mock_client .get = mock_get
570+ mock_client .__aenter__ = AsyncMock (return_value = mock_client )
571+ mock_client .__aexit__ = AsyncMock (return_value = False )
572+
573+ with patch ("httpx.AsyncClient" , return_value = mock_client ):
574+ result = await base_sdk ._wait_for_transaction ("0xabc" )
575+
576+ assert result ["success" ] is True
577+ assert call_count == 2
578+
579+ async def test_retries_on_connect_error (self , base_sdk ) -> None :
580+ """SHALL retry on ConnectError."""
581+ from unittest .mock import AsyncMock , patch
582+
583+ call_count = 0
584+
585+ async def mock_get (url , headers = None ):
586+ nonlocal call_count
587+ call_count += 1
588+ if call_count == 1 :
589+ raise httpx .ConnectError ("connection refused" )
590+ resp = httpx .Response (
591+ 200 ,
592+ json = {"success" : True , "hash" : "0xabc" , "type" : "user_transaction" },
593+ )
594+ return resp
595+
596+ mock_client = AsyncMock ()
597+ mock_client .get = mock_get
598+ mock_client .__aenter__ = AsyncMock (return_value = mock_client )
599+ mock_client .__aexit__ = AsyncMock (return_value = False )
600+
601+ with patch ("httpx.AsyncClient" , return_value = mock_client ):
602+ result = await base_sdk ._wait_for_transaction ("0xabc" )
603+
604+ assert result ["success" ] is True
605+
606+ async def test_timeout_after_retries_exhausted (self , base_sdk ) -> None :
607+ """SHALL raise TxnConfirmError if timeout exceeded during retries."""
608+ from unittest .mock import AsyncMock , patch
609+
610+ from decibel ._exceptions import TxnConfirmError
611+
612+ async def mock_get (url , headers = None ):
613+ raise httpx .ConnectTimeout ("always fails" )
614+
615+ mock_client = AsyncMock ()
616+ mock_client .get = mock_get
617+ mock_client .__aenter__ = AsyncMock (return_value = mock_client )
618+ mock_client .__aexit__ = AsyncMock (return_value = False )
619+
620+ with (
621+ patch ("httpx.AsyncClient" , return_value = mock_client ),
622+ pytest .raises (TxnConfirmError , match = "did not confirm" ),
623+ ):
624+ await base_sdk ._wait_for_transaction ("0xabc" , txn_confirm_timeout = 0.01 )
0 commit comments