rmi-backend/app/solana_connector.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

265 lines
7.8 KiB
Python

"""
Solana Integration - Using solana-py Client directly
=====================================================
Uses solana-py (the "solana" python package) for reliable Solana interactions.
This package provides a high-level synchronous API.
"""
import logging
from dataclasses import dataclass
from datetime import datetime
from typing import Any
logger = logging.getLogger(__name__)
try:
from solana.rpc.api import Client, Pubkey, Signature, Transaction # noqa: F401
SOLANA_AVAILABLE = True
logger.info("solana-py SDK imported successfully")
except ImportError as e:
logger.warning(f"solana-py SDK not available: {e}")
SOLANA_AVAILABLE = False
# ─── RPC CONFIGURATION ────────────────────────────────────────────
SOLANA_RPC_URLS = {
"mainnet": "https://solana-rpc.publicnode.com",
"devnet": "https://api.devnet.solana.com",
"testnet": "https://api.testnet.solana.com",
"localnet": "http://localhost:8899",
}
# ─── DATA MODELS ──────────────────────────────────────────────────
@dataclass
class SolanaAccount:
"""Solana account data."""
address: str
lamports: int
owner: str
executable: bool
rent_epoch: int
data: str | None = None
@dataclass
class SolanaTransaction:
"""Solana transaction data."""
signature: str
slot: int
block_time: int | None
status: dict[str, Any] | None
fee: int
pre_balances: list[int]
post_balances: list[int]
# ─── SOLANA CLIENT ────────────────────────────────────────────────
class SolanaClient:
"""High-level Solana client wrapper."""
def __init__(self, network: str = "mainnet", rpc_url: str | None = None):
self.network = network.lower()
self.rpc_url = rpc_url or SOLANA_RPC_URLS.get(self.network, SOLANA_RPC_URLS["mainnet"])
self._client: Any = None
if not SOLANA_AVAILABLE:
logger.warning("solana-py SDK not installed, using stub mode")
@property
def client(self) -> Any:
"""Get or create Client instance."""
if not SOLANA_AVAILABLE:
raise RuntimeError("solana-py SDK not installed")
if self._client is None:
self._client = Client(self.rpc_url)
return self._client
def get_account_info(self, address: str) -> SolanaAccount | None:
"""
Get account information for an address.
Args:
address: Solana address (base58)
Returns:
SolanaAccount data or None if account doesn't exist
"""
if not SOLANA_AVAILABLE:
return self._stub_account(address)
try:
pubkey = Pubkey(address)
response = self.client.get_account_info(pubkey)
if response.value is None:
return None
info = response.value
return SolanaAccount(
address=address,
lamports=info.lamports,
owner=str(info.owner),
executable=info.executable,
rent_epoch=info.rent_epoch,
data=info.data,
)
except Exception as e:
logger.error(f"Error fetching account {address}: {e}")
return self._stub_account(address)
def get_balance(self, address: str) -> int:
"""Get account balance in lamports."""
if not SOLANA_AVAILABLE:
return 0
try:
pubkey = Pubkey(address)
response = self.client.get_balance(pubkey)
return response.value
except Exception as e:
logger.error(f"Error fetching balance for {address}: {e}")
return 0
def send_transaction(self, transaction, payer) -> str:
"""
Send a Solana transaction.
Args:
transaction: Built transaction
payer: Payer public key
Returns:
Transaction signature (base58)
"""
if not SOLANA_AVAILABLE:
return "stub_sig_" + datetime.utcnow().isoformat()
try:
response = self.client.send_transaction(transaction, payer)
return str(response.value)
except Exception as e:
logger.error(f"Error sending transaction: {e}")
raise
def get_transaction(self, signature: str) -> SolanaTransaction | None:
"""
Get transaction details by signature.
Args:
signature: Transaction signature (base58)
Returns:
SolanaTransaction data or None
"""
if not SOLANA_AVAILABLE:
return self._stub_transaction(signature)
try:
sig = Signature.from_string(signature)
response = self.client.get_transaction(sig, encoding="jsonParsed")
if response.value is None:
return None
tx = response.value
meta = tx.meta
return SolanaTransaction(
signature=signature,
slot=tx.slot,
block_time=tx.block_time,
status=meta,
fee=meta.fee if meta else 0,
pre_balances=meta.pre_balances if meta else [],
post_balances=meta.post_balances if meta else [],
)
except Exception as e:
logger.error(f"Error fetching transaction {signature}: {e}")
return None
def get_block_height(self) -> int:
"""Get current block height."""
if not SOLANA_AVAILABLE:
return 0
try:
return self.client.get_block_height()
except Exception as e:
logger.error(f"Error fetching block height: {e}")
return 0
def get_slot(self) -> int:
"""Get current slot."""
if not SOLANA_AVAILABLE:
return 0
try:
return self.client.get_slot()
except Exception as e:
logger.error(f"Error fetching slot: {e}")
return 0
def get_health(self) -> bool:
"""Check RPC health."""
if not SOLANA_AVAILABLE:
return False
try:
return self.client.get_health()
except Exception:
return False
def _stub_account(self, address: str) -> SolanaAccount:
"""Stub account info when SDK unavailable."""
return SolanaAccount(
address=address,
lamports=0,
owner="11111111111111111111111111111111",
executable=False,
rent_epoch=0,
data="",
)
def _stub_transaction(self, signature: str) -> SolanaTransaction:
"""Stub transaction data when SDK unavailable."""
return SolanaTransaction(
signature=signature,
slot=0,
block_time=None,
status=None,
fee=0,
pre_balances=[],
post_balances=[],
)
# ─── GLOBAL SINGLETON ─────────────────────────────────────────────
_default_client: SolanaClient | None = None
def get_solana_client(network: str = "mainnet") -> SolanaClient:
"""Get or create Solana client instance."""
global _default_client
if _default_client is None:
_default_client = SolanaClient(network=network)
return _default_client
def check_solana_connection() -> bool:
"""Quick check if Solana RPC is accessible."""
if not SOLANA_AVAILABLE:
return False
try:
client = get_solana_client()
return client.get_health()
except Exception:
return False