Skip to content

Commit 29d80e5

Browse files
Copilot0xrinegade
andcommitted
Fix Solana blockhash issues - improved retry logic and health checks for devnet stability
Co-authored-by: 0xrinegade <[email protected]>
1 parent 8bac770 commit 29d80e5

File tree

2 files changed

+91
-12
lines changed

2 files changed

+91
-12
lines changed

python/solana_ai_registries/client.py

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import asyncio
99
import logging
1010
import struct
11+
from copy import deepcopy
1112
from typing import Any, Dict, List, Optional
1213

1314
from solana.rpc.async_api import AsyncClient
@@ -67,6 +68,35 @@ async def close(self) -> None:
6768
await self._client.close()
6869
self._client = None
6970

71+
async def health_check(self) -> bool:
72+
"""
73+
Check if the Solana RPC connection is healthy and can fetch blockhashes.
74+
75+
Returns:
76+
True if connection is healthy, False otherwise
77+
"""
78+
try:
79+
# Test basic connection by getting epoch info (lightweight check)
80+
epoch_info = await self.client.get_epoch_info(commitment=self.commitment)
81+
if not epoch_info.value:
82+
logger.warning("Failed to fetch epoch info")
83+
return False
84+
85+
# Test blockhash fetching (most common failure point)
86+
blockhash_resp = await self.client.get_latest_blockhash(
87+
commitment=self.commitment
88+
)
89+
if not blockhash_resp.value or not blockhash_resp.value.blockhash:
90+
logger.warning("Failed to fetch latest blockhash")
91+
return False
92+
93+
logger.debug("RPC connection health check passed")
94+
return True
95+
96+
except Exception as e:
97+
logger.warning(f"RPC health check failed: {e}")
98+
return False
99+
70100
async def __aenter__(self) -> "SolanaAIRegistriesClient":
71101
"""Async context manager entry."""
72102
return self
@@ -186,7 +216,7 @@ async def send_transaction(
186216
transaction: Transaction,
187217
signers: List[Keypair],
188218
opts: Optional[TxOpts] = None,
189-
max_retries: int = 3,
219+
max_retries: int = 5,
190220
) -> str:
191221
"""
192222
Send transaction with error handling and retry logic.
@@ -209,28 +239,55 @@ async def send_transaction(
209239
last_error = None
210240
for attempt in range(max_retries):
211241
try:
212-
# Get recent blockhash
242+
# Get fresh blockhash for each attempt
213243
blockhash_resp = await self.client.get_latest_blockhash(
214244
commitment=self.commitment
215245
)
216246

217-
# Sign transaction with recent blockhash
218-
transaction.sign(signers, blockhash_resp.value.blockhash)
219-
220-
# Send transaction
221-
response = await self.client.send_transaction(transaction, opts=opts)
247+
# Wait a bit to ensure blockhash is fully propagated
248+
if attempt > 0:
249+
await asyncio.sleep(0.5)
250+
251+
# Create a new transaction instance to avoid signature conflicts
252+
tx_copy = deepcopy(transaction)
253+
254+
# Sign transaction with fresh blockhash
255+
tx_copy.sign(signers, blockhash_resp.value.blockhash)
256+
257+
# Send transaction with additional retry-friendly options
258+
response = await self.client.send_transaction(
259+
tx_copy,
260+
opts=TxOpts(
261+
skip_confirmation=opts.skip_confirmation,
262+
# Always run preflight to catch blockhash issues early
263+
skip_preflight=False,
264+
# Let our outer retry loop handle retries
265+
max_retries=1,
266+
),
267+
)
222268

223269
signature = str(response.value)
224270
logger.info(f"Transaction sent successfully: {signature}")
225271
return signature
226272

227273
except Exception as e:
228274
last_error = e
229-
logger.warning(
230-
f"Transaction attempt {attempt + 1}/{max_retries} failed: {e}"
231-
)
232-
if attempt < max_retries - 1:
233-
await asyncio.sleep(1.0 * (attempt + 1)) # Exponential backoff
275+
error_msg = str(e).lower()
276+
277+
# Check if it's a blockhash-related error
278+
if "blockhash not found" in error_msg or "blockhash" in error_msg:
279+
logger.warning(
280+
f"Blockhash error on attempt {attempt + 1}/{max_retries}: {e}"
281+
)
282+
# For blockhash errors, wait longer before retry
283+
if attempt < max_retries - 1:
284+
await asyncio.sleep(2.0 + (attempt * 1.0))
285+
else:
286+
logger.warning(
287+
f"Transaction attempt {attempt + 1}/{max_retries} failed: {e}"
288+
)
289+
if attempt < max_retries - 1:
290+
await asyncio.sleep(1.0 * (attempt + 1)) # Exponential backoff
234291

235292
# All retries failed
236293
raise TransactionError(

python/tests/integration/test_devnet.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Integration tests for devnet environment."""
22

33
import asyncio
4+
import logging
45

56
import pytest
67
import pytest_asyncio
@@ -20,6 +21,8 @@
2021
ServiceEndpoint,
2122
)
2223

24+
logger = logging.getLogger(__name__)
25+
2326

2427
@pytest.mark.integration
2528
@pytest.mark.devnet
@@ -30,6 +33,25 @@ class TestDevnetAgentOperations:
3033
async def client(self):
3134
"""Create a client for devnet testing."""
3235
client = SolanaAIRegistriesClient(rpc_url=DEFAULT_DEVNET_RPC)
36+
37+
# Verify connection health before running tests
38+
max_health_attempts = 3
39+
for attempt in range(max_health_attempts):
40+
if await client.health_check():
41+
logger.info("Devnet connection verified as healthy")
42+
break
43+
else:
44+
logger.warning(
45+
"Devnet health check failed, "
46+
f"attempt {attempt + 1}/{max_health_attempts}"
47+
)
48+
if attempt < max_health_attempts - 1:
49+
await asyncio.sleep(2.0)
50+
else:
51+
pytest.skip(
52+
"Devnet connection is unhealthy, skipping integration tests"
53+
)
54+
3355
yield client
3456
await client.close()
3557

0 commit comments

Comments
 (0)