@@ -377,7 +377,61 @@ def test_send_signed_transaction_reverted(blockchain_client: BlockchainClient):
377
377
blockchain_client ._send_signed_transaction (mock_signed_tx )
378
378
379
379
380
- # 3. Orchestration Tests
380
+ # 3. Orchestration and Edge Case Tests
381
+
382
+
383
+ def test_execute_rpc_call_with_failover (blockchain_client : BlockchainClient ):
384
+ """
385
+ Tests that _execute_rpc_call fails over to the next provider if the first one
386
+ is unreachable, and sends a Slack notification.
387
+ """
388
+ # 1. Setup
389
+ # Simulate the first provider failing, then the second succeeding.
390
+ # The inner retry will try 3 times, then the outer failover logic will switch provider.
391
+ mock_func = MagicMock ()
392
+ mock_func .side_effect = [
393
+ requests .exceptions .ConnectionError ("RPC down 1" ),
394
+ requests .exceptions .ConnectionError ("RPC down 2" ),
395
+ requests .exceptions .ConnectionError ("RPC down 3" ),
396
+ "Success" ,
397
+ ]
398
+ blockchain_client .slack_notifier .reset_mock () # Clear prior calls
399
+
400
+ # 2. Action
401
+ # The _execute_rpc_call decorator will handle the retry/failover
402
+ result = blockchain_client ._execute_rpc_call (mock_func )
403
+
404
+ # 3. Assertions
405
+ assert result == "Success"
406
+ assert mock_func .call_count == 4
407
+ assert blockchain_client .current_rpc_index == 1 # Should have failed over to the 2nd provider
408
+
409
+ # Verify a slack message was sent about the failover
410
+ blockchain_client .slack_notifier .send_info_notification .assert_called_once ()
411
+ call_kwargs = blockchain_client .slack_notifier .send_info_notification .call_args .kwargs
412
+ assert "Switching from previous RPC" in call_kwargs ["message" ]
413
+
414
+
415
+ def test_determine_transaction_nonce_replace_handles_errors (blockchain_client : BlockchainClient ):
416
+ """
417
+ Tests that nonce determination falls back gracefully if checking for pending
418
+ transactions or nonce gaps fails.
419
+ """
420
+ # 1. Setup
421
+ # Simulate errors when trying to get pending blocks and checking for gaps
422
+ w3_instance = blockchain_client .mock_w3_instance
423
+ w3_instance .eth .get_block .side_effect = ValueError ("Cannot get pending block" )
424
+ # After all errors, the final fallback is to get the latest transaction count.
425
+ w3_instance .eth .get_transaction_count .side_effect = [
426
+ ValueError ("Cannot get pending nonce" ),
427
+ ValueError ("Cannot get latest nonce" ),
428
+ 9 , # This will be the final fallback value
429
+ ]
430
+
431
+ # 2. Action & Assertions
432
+ # The function should swallow the errors and then raise the final one
433
+ with pytest .raises (ValueError , match = "Cannot get latest nonce" ):
434
+ blockchain_client ._determine_transaction_nonce (MOCK_SENDER_ADDRESS , replace = True )
381
435
382
436
383
437
def test_send_transaction_to_allow_indexers_orchestration (blockchain_client : BlockchainClient ):
@@ -428,6 +482,7 @@ def test_batch_processing_splits_correctly(blockchain_client: BlockchainClient):
428
482
# Assertions
429
483
assert len (tx_hashes ) == 3
430
484
assert blockchain_client .send_transaction_to_allow_indexers .call_count == 3
485
+
431
486
# Check the contents of each call
432
487
assert blockchain_client .send_transaction_to_allow_indexers .call_args_list [0 ][0 ][0 ] == addresses [0 :2 ]
433
488
assert blockchain_client .send_transaction_to_allow_indexers .call_args_list [1 ][0 ][0 ] == addresses [2 :4 ]
@@ -440,6 +495,7 @@ def test_batch_processing_halts_on_failure(blockchain_client: BlockchainClient):
440
495
"""
441
496
# Setup
442
497
addresses = [f"0x{ i } " * 40 for i in range (5 )]
498
+
443
499
# Simulate failure on the second call
444
500
blockchain_client .send_transaction_to_allow_indexers = MagicMock (
445
501
side_effect = ["tx_hash_1" , Exception ("RPC Error" ), "tx_hash_3" ]
0 commit comments