refactor: add service layer pattern for blockchain and database interactions
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
- Create BlockchainService abstract base class with RPC implementation - Create DatabaseService abstract base class with SQLite implementation - Add connection pooling for SQLite database service - Implement factory pattern for service creation - Provide high-level abstractions over RPC calls and database operations - Improve testability with interface definitions
This commit is contained in:
311
aitbc/blockchain_service.py
Normal file
311
aitbc/blockchain_service.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
"""
|
||||||
|
Blockchain service layer for AITBC
|
||||||
|
Provides high-level blockchain interaction services with abstraction over RPC calls
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from aitbc.network.http_client import AITBCHTTPClient
|
||||||
|
from aitbc.aitbc_logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Block:
|
||||||
|
"""Block data structure"""
|
||||||
|
height: int
|
||||||
|
hash: str
|
||||||
|
parent_hash: str
|
||||||
|
timestamp: int
|
||||||
|
transactions: List[Dict[str, Any]]
|
||||||
|
miner: Optional[str] = None
|
||||||
|
gas_used: Optional[int] = None
|
||||||
|
gas_limit: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Transaction:
|
||||||
|
"""Transaction data structure"""
|
||||||
|
hash: str
|
||||||
|
from_address: str
|
||||||
|
to_address: str
|
||||||
|
value: str
|
||||||
|
nonce: int
|
||||||
|
gas: int
|
||||||
|
gas_price: Optional[str] = None
|
||||||
|
input_data: Optional[str] = None
|
||||||
|
block_hash: Optional[str] = None
|
||||||
|
block_number: Optional[int] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Account:
|
||||||
|
"""Account data structure"""
|
||||||
|
address: str
|
||||||
|
balance: int
|
||||||
|
nonce: int
|
||||||
|
|
||||||
|
|
||||||
|
class BlockchainService(ABC):
|
||||||
|
"""Abstract base class for blockchain service implementations"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_block(self, block_identifier: int | str) -> Block:
|
||||||
|
"""Get block by height or hash"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_head_block(self) -> Block:
|
||||||
|
"""Get current head block"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_transaction(self, tx_hash: str) -> Transaction:
|
||||||
|
"""Get transaction by hash"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_account_balance(self, address: str) -> Account:
|
||||||
|
"""Get account information including balance"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_transaction(self, tx_data: Dict[str, Any]) -> str:
|
||||||
|
"""Send transaction and return transaction hash"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get blockchain node status"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RPCBlockchainService(BlockchainService):
|
||||||
|
"""RPC-based blockchain service implementation"""
|
||||||
|
|
||||||
|
def __init__(self, rpc_url: str, timeout: int = 30):
|
||||||
|
"""
|
||||||
|
Initialize RPC blockchain service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rpc_url: Blockchain RPC endpoint URL
|
||||||
|
timeout: Request timeout in seconds
|
||||||
|
"""
|
||||||
|
self.rpc_url = rpc_url
|
||||||
|
self.client = AITBCHTTPClient(base_url=rpc_url, timeout=timeout)
|
||||||
|
logger.info(f"Initialized RPC blockchain service for {rpc_url}")
|
||||||
|
|
||||||
|
def get_block(self, block_identifier: int | str) -> Block:
|
||||||
|
"""
|
||||||
|
Get block by height or hash
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block_identifier: Block height (int) or hash (str)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Block object with block data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If block not found
|
||||||
|
NetworkError: If RPC call fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if isinstance(block_identifier, int):
|
||||||
|
endpoint = f"/rpc/blocks/{block_identifier}"
|
||||||
|
else:
|
||||||
|
endpoint = f"/rpc/block/{block_identifier}"
|
||||||
|
|
||||||
|
response = self.client.get(endpoint)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return Block(
|
||||||
|
height=data.get("height", 0),
|
||||||
|
hash=data.get("hash", ""),
|
||||||
|
parent_hash=data.get("parent_hash", ""),
|
||||||
|
timestamp=data.get("timestamp", 0),
|
||||||
|
transactions=data.get("transactions", []),
|
||||||
|
miner=data.get("miner"),
|
||||||
|
gas_used=data.get("gas_used"),
|
||||||
|
gas_limit=data.get("gas_limit")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get block {block_identifier}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_head_block(self) -> Block:
|
||||||
|
"""
|
||||||
|
Get current head block
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Block object with head block data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NetworkError: If RPC call fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.get("/rpc/head")
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return Block(
|
||||||
|
height=data.get("height", 0),
|
||||||
|
hash=data.get("hash", ""),
|
||||||
|
parent_hash=data.get("parent_hash", ""),
|
||||||
|
timestamp=data.get("timestamp", 0),
|
||||||
|
transactions=data.get("transactions", []),
|
||||||
|
miner=data.get("miner"),
|
||||||
|
gas_used=data.get("gas_used"),
|
||||||
|
gas_limit=data.get("gas_limit")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get head block: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_transaction(self, tx_hash: str) -> Transaction:
|
||||||
|
"""
|
||||||
|
Get transaction by hash
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tx_hash: Transaction hash
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Transaction object with transaction data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If transaction not found
|
||||||
|
NetworkError: If RPC call fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.get(f"/rpc/transaction/{tx_hash}")
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return Transaction(
|
||||||
|
hash=data.get("hash", ""),
|
||||||
|
from_address=data.get("from", ""),
|
||||||
|
to_address=data.get("to", ""),
|
||||||
|
value=data.get("value", "0"),
|
||||||
|
nonce=data.get("nonce", 0),
|
||||||
|
gas=data.get("gas", 0),
|
||||||
|
gas_price=data.get("gas_price"),
|
||||||
|
input_data=data.get("input"),
|
||||||
|
block_hash=data.get("block_hash"),
|
||||||
|
block_number=data.get("block_number"),
|
||||||
|
status=data.get("status")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get transaction {tx_hash}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_account_balance(self, address: str) -> Account:
|
||||||
|
"""
|
||||||
|
Get account information including balance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
address: Account address
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Account object with account data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If address is invalid
|
||||||
|
NetworkError: If RPC call fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.get(f"/rpc/account/{address}")
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return Account(
|
||||||
|
address=address,
|
||||||
|
balance=int(data.get("balance", 0)),
|
||||||
|
nonce=data.get("nonce", 0)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get account balance for {address}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send_transaction(self, tx_data: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Send transaction and return transaction hash
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tx_data: Transaction data dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Transaction hash
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If transaction data is invalid
|
||||||
|
NetworkError: If RPC call fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.post("/rpc/sendTx", json=tx_data)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
tx_hash = data.get("hash") or data.get("tx_hash")
|
||||||
|
if not tx_hash:
|
||||||
|
raise ValueError("Transaction hash not found in response")
|
||||||
|
|
||||||
|
logger.info(f"Transaction sent successfully: {tx_hash}")
|
||||||
|
return tx_hash
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send transaction: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get blockchain node status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with node status information
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NetworkError: If RPC call fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.get("/rpc/status")
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get node status: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class BlockchainServiceFactory:
|
||||||
|
"""Factory for creating blockchain service instances"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_rpc_service(rpc_url: str, timeout: int = 30) -> RPCBlockchainService:
|
||||||
|
"""
|
||||||
|
Create RPC blockchain service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rpc_url: Blockchain RPC endpoint URL
|
||||||
|
timeout: Request timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RPCBlockchainService instance
|
||||||
|
"""
|
||||||
|
return RPCBlockchainService(rpc_url, timeout)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_service(service_type: str = "rpc", **kwargs) -> BlockchainService:
|
||||||
|
"""
|
||||||
|
Create blockchain service by type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service_type: Type of service ("rpc")
|
||||||
|
**kwargs: Service-specific configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BlockchainService instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If service type is unknown
|
||||||
|
"""
|
||||||
|
if service_type == "rpc":
|
||||||
|
return BlockchainServiceFactory.create_rpc_service(**kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown service type: {service_type}")
|
||||||
186
aitbc/database_service.py
Normal file
186
aitbc/database_service.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Database service layer for AITBC
|
||||||
|
Provides high-level database interaction services with connection pooling
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from aitbc.aitbc_logging import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseService(ABC):
|
||||||
|
"""Abstract base class for database service implementations"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def execute_query(self, query: str, params: tuple = ()) -> List[Dict[str, Any]]:
|
||||||
|
"""Execute a SELECT query"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def execute_update(self, query: str, params: tuple = ()) -> int:
|
||||||
|
"""Execute an INSERT/UPDATE/DELETE query"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def execute_transaction(self, queries: List[tuple]) -> bool:
|
||||||
|
"""Execute multiple queries in a transaction"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SQLiteDatabaseService(DatabaseService):
|
||||||
|
"""SQLite database service with connection pooling"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: Path, pool_size: int = 5):
|
||||||
|
"""
|
||||||
|
Initialize SQLite database service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: Path to SQLite database file
|
||||||
|
pool_size: Connection pool size
|
||||||
|
"""
|
||||||
|
self.db_path = db_path
|
||||||
|
self.pool_size = pool_size
|
||||||
|
self._connections: List[sqlite3.Connection] = []
|
||||||
|
self._current_connection_index = 0
|
||||||
|
|
||||||
|
# Ensure database exists
|
||||||
|
self._ensure_database()
|
||||||
|
|
||||||
|
logger.info(f"Initialized SQLite database service for {db_path}")
|
||||||
|
|
||||||
|
def _ensure_database(self) -> None:
|
||||||
|
"""Ensure database file and directory exist"""
|
||||||
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not self.db_path.exists():
|
||||||
|
self.db_path.touch()
|
||||||
|
|
||||||
|
def _get_connection(self) -> sqlite3.Connection:
|
||||||
|
"""Get a connection from the pool"""
|
||||||
|
if self._connections:
|
||||||
|
conn = self._connections[self._current_connection_index]
|
||||||
|
self._current_connection_index = (self._current_connection_index + 1) % len(self._connections)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
# Create new connection if pool is empty
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
self._connections.append(conn)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_connection(self):
|
||||||
|
"""Context manager for database connections"""
|
||||||
|
conn = self._get_connection()
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.error(f"Database error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def execute_query(self, query: str, params: tuple = ()) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Execute a SELECT query
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: SQL query string
|
||||||
|
params: Query parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dictionaries with query results
|
||||||
|
"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return [dict(row) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
def execute_update(self, query: str, params: tuple = ()) -> int:
|
||||||
|
"""
|
||||||
|
Execute an INSERT/UPDATE/DELETE query
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: SQL query string
|
||||||
|
params: Query parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of rows affected
|
||||||
|
"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
def execute_transaction(self, queries: List[tuple]) -> bool:
|
||||||
|
"""
|
||||||
|
Execute multiple queries in a transaction
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queries: List of (query, params) tuples
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if transaction succeeded
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If transaction fails
|
||||||
|
"""
|
||||||
|
with self.get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
for query, params in queries:
|
||||||
|
cursor.execute(query, params)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Transaction failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close all database connections"""
|
||||||
|
for conn in self._connections:
|
||||||
|
conn.close()
|
||||||
|
self._connections.clear()
|
||||||
|
logger.info("Closed all database connections")
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseServiceFactory:
|
||||||
|
"""Factory for creating database service instances"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_sqlite_service(db_path: Path, pool_size: int = 5) -> SQLiteDatabaseService:
|
||||||
|
"""
|
||||||
|
Create SQLite database service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: Path to SQLite database file
|
||||||
|
pool_size: Connection pool size
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SQLiteDatabaseService instance
|
||||||
|
"""
|
||||||
|
return SQLiteDatabaseService(db_path, pool_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_service(db_type: str = "sqlite", **kwargs) -> DatabaseService:
|
||||||
|
"""
|
||||||
|
Create database service by type
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_type: Type of database ("sqlite")
|
||||||
|
**kwargs: Database-specific configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DatabaseService instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If database type is unknown
|
||||||
|
"""
|
||||||
|
if db_type == "sqlite":
|
||||||
|
return DatabaseServiceFactory.create_sqlite_service(**kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown database type: {db_type}")
|
||||||
Reference in New Issue
Block a user