265 lines
7.7 KiB
Python
265 lines
7.7 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
|
|
|
|
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
|