Files
aitbc/apps/wallet/src/app/chain/multichain_ledger.py
oib bb5363bebc refactor: consolidate blockchain explorer into single app and update backup ignore patterns
- Remove standalone explorer-web app (README, HTML, package files)
- Add /web endpoint to blockchain-explorer for web interface access
- Update .gitignore to exclude application backup archives (*.tar.gz, *.zip)
- Add backup documentation files to .gitignore (BACKUP_INDEX.md, README.md)
- Consolidate explorer functionality into main blockchain-explorer application
2026-03-06 18:14:49 +01:00

428 lines
16 KiB
Python

"""
Multi-Chain Ledger Adapter for Wallet Daemon
Chain-specific storage and ledger management for wallet operations
across multiple blockchain networks.
"""
from typing import Dict, List, Optional, Any
from pathlib import Path
import sqlite3
import threading
import json
from datetime import datetime
from dataclasses import dataclass, asdict
import logging
from .manager import ChainManager, ChainConfig
logger = logging.getLogger(__name__)
@dataclass
class ChainLedgerRecord:
"""Chain-specific ledger record"""
chain_id: str
wallet_id: str
event_type: str
timestamp: datetime
data: Dict[str, Any]
success: bool = True
@dataclass
class ChainWalletMetadata:
"""Chain-specific wallet metadata"""
chain_id: str
wallet_id: str
public_key: str
address: Optional[str]
metadata: Dict[str, str]
created_at: datetime
updated_at: datetime
class MultiChainLedgerAdapter:
"""Multi-chain ledger adapter with chain-specific storage"""
def __init__(self, chain_manager: ChainManager, base_data_path: Optional[Path] = None):
self.chain_manager = chain_manager
self.base_data_path = base_data_path or Path("./data")
self.base_data_path.mkdir(parents=True, exist_ok=True)
# Separate database connections per chain
self.chain_connections: Dict[str, sqlite3.Connection] = {}
self.chain_locks: Dict[str, threading.Lock] = {}
# Initialize databases for all chains
self._initialize_chain_databases()
def _initialize_chain_databases(self):
"""Initialize database for each chain"""
for chain in self.chain_manager.list_chains():
self._init_chain_database(chain.chain_id)
def _get_chain_db_path(self, chain_id: str) -> Path:
"""Get database path for a specific chain"""
chain = self.chain_manager.get_chain(chain_id)
if chain and chain.ledger_db_path:
return Path(chain.ledger_db_path)
# Default path based on chain ID
return self.base_data_path / f"wallet_ledger_{chain_id}.db"
def _init_chain_database(self, chain_id: str):
"""Initialize database for a specific chain"""
try:
db_path = self._get_chain_db_path(chain_id)
db_path.parent.mkdir(parents=True, exist_ok=True)
# Create connection and lock for this chain
conn = sqlite3.connect(db_path)
self.chain_connections[chain_id] = conn
self.chain_locks[chain_id] = threading.Lock()
# Initialize schema
with self.chain_locks[chain_id]:
self._create_chain_schema(conn, chain_id)
logger.info(f"Initialized database for chain: {chain_id}")
except Exception as e:
logger.error(f"Failed to initialize database for chain {chain_id}: {e}")
def _create_chain_schema(self, conn: sqlite3.Connection, chain_id: str):
"""Create database schema for a specific chain"""
cursor = conn.cursor()
# Wallet metadata table
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS wallet_metadata_{chain_id} (
wallet_id TEXT PRIMARY KEY,
public_key TEXT NOT NULL,
address TEXT,
metadata TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
# Ledger events table
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS ledger_events_{chain_id} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_id TEXT NOT NULL,
event_type TEXT NOT NULL,
timestamp TEXT NOT NULL,
data TEXT,
success BOOLEAN DEFAULT TRUE,
FOREIGN KEY (wallet_id) REFERENCES wallet_metadata_{chain_id} (wallet_id)
)
""")
# Chain-specific indexes
cursor.execute(f"""
CREATE INDEX IF NOT EXISTS idx_wallet_events_{chain_id}
ON ledger_events_{chain_id} (wallet_id, timestamp)
""")
cursor.execute(f"""
CREATE INDEX IF NOT EXISTS idx_wallet_created_{chain_id}
ON wallet_metadata_{chain_id} (created_at)
""")
conn.commit()
def _get_connection(self, chain_id: str) -> Optional[sqlite3.Connection]:
"""Get database connection for a specific chain"""
if chain_id not in self.chain_connections:
self._init_chain_database(chain_id)
return self.chain_connections.get(chain_id)
def _get_lock(self, chain_id: str) -> threading.Lock:
"""Get lock for a specific chain"""
if chain_id not in self.chain_locks:
self.chain_locks[chain_id] = threading.Lock()
return self.chain_locks[chain_id]
def create_wallet(self, chain_id: str, wallet_id: str, public_key: str,
address: Optional[str] = None, metadata: Optional[Dict[str, str]] = None) -> bool:
"""Create wallet in chain-specific database"""
try:
if not self.chain_manager.validate_chain_id(chain_id):
logger.error(f"Invalid chain: {chain_id}")
return False
conn = self._get_connection(chain_id)
if not conn:
return False
lock = self._get_lock(chain_id)
with lock:
cursor = conn.cursor()
# Check if wallet already exists
cursor.execute(f"""
SELECT wallet_id FROM wallet_metadata_{chain_id} WHERE wallet_id = ?
""", (wallet_id,))
if cursor.fetchone():
logger.warning(f"Wallet {wallet_id} already exists in chain {chain_id}")
return False
# Insert wallet metadata
now = datetime.now().isoformat()
metadata_json = json.dumps(metadata or {})
cursor.execute(f"""
INSERT INTO wallet_metadata_{chain_id}
(wallet_id, public_key, address, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (wallet_id, public_key, address, metadata_json, now, now))
# Record creation event
self.record_event(chain_id, wallet_id, "created", {
"public_key": public_key,
"address": address,
"metadata": metadata or {}
})
conn.commit()
logger.info(f"Created wallet {wallet_id} in chain {chain_id}")
return True
except Exception as e:
logger.error(f"Failed to create wallet {wallet_id} in chain {chain_id}: {e}")
return False
def get_wallet(self, chain_id: str, wallet_id: str) -> Optional[ChainWalletMetadata]:
"""Get wallet metadata from chain-specific database"""
try:
if not self.chain_manager.validate_chain_id(chain_id):
return None
conn = self._get_connection(chain_id)
if not conn:
return None
lock = self._get_lock(chain_id)
with lock:
cursor = conn.cursor()
cursor.execute(f"""
SELECT wallet_id, public_key, address, metadata, created_at, updated_at
FROM wallet_metadata_{chain_id} WHERE wallet_id = ?
""", (wallet_id,))
row = cursor.fetchone()
if not row:
return None
metadata = json.loads(row[3]) if row[3] else {}
return ChainWalletMetadata(
chain_id=chain_id,
wallet_id=row[0],
public_key=row[1],
address=row[2],
metadata=metadata,
created_at=datetime.fromisoformat(row[4]),
updated_at=datetime.fromisoformat(row[5])
)
except Exception as e:
logger.error(f"Failed to get wallet {wallet_id} from chain {chain_id}: {e}")
return None
def list_wallets(self, chain_id: str) -> List[ChainWalletMetadata]:
"""List all wallets in a specific chain"""
try:
if not self.chain_manager.validate_chain_id(chain_id):
return []
conn = self._get_connection(chain_id)
if not conn:
return []
lock = self._get_lock(chain_id)
with lock:
cursor = conn.cursor()
cursor.execute(f"""
SELECT wallet_id, public_key, address, metadata, created_at, updated_at
FROM wallet_metadata_{chain_id} ORDER BY created_at DESC
""")
wallets = []
for row in cursor.fetchall():
metadata = json.loads(row[3]) if row[3] else {}
wallets.append(ChainWalletMetadata(
chain_id=chain_id,
wallet_id=row[0],
public_key=row[1],
address=row[2],
metadata=metadata,
created_at=datetime.fromisoformat(row[4]),
updated_at=datetime.fromisoformat(row[5])
))
return wallets
except Exception as e:
logger.error(f"Failed to list wallets in chain {chain_id}: {e}")
return []
def record_event(self, chain_id: str, wallet_id: str, event_type: str,
data: Dict[str, Any], success: bool = True) -> bool:
"""Record an event for a wallet in a specific chain"""
try:
if not self.chain_manager.validate_chain_id(chain_id):
return False
conn = self._get_connection(chain_id)
if not conn:
return False
lock = self._get_lock(chain_id)
with lock:
cursor = conn.cursor()
# Insert event
cursor.execute(f"""
INSERT INTO ledger_events_{chain_id}
(wallet_id, event_type, timestamp, data, success)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, event_type, datetime.now().isoformat(),
json.dumps(data), success))
conn.commit()
return True
except Exception as e:
logger.error(f"Failed to record event for wallet {wallet_id} in chain {chain_id}: {e}")
return False
def get_wallet_events(self, chain_id: str, wallet_id: str,
event_type: Optional[str] = None, limit: int = 100) -> List[ChainLedgerRecord]:
"""Get events for a wallet in a specific chain"""
try:
if not self.chain_manager.validate_chain_id(chain_id):
return []
conn = self._get_connection(chain_id)
if not conn:
return []
lock = self._get_lock(chain_id)
with lock:
cursor = conn.cursor()
if event_type:
cursor.execute(f"""
SELECT wallet_id, event_type, timestamp, data, success
FROM ledger_events_{chain_id}
WHERE wallet_id = ? AND event_type = ?
ORDER BY timestamp DESC LIMIT ?
""", (wallet_id, event_type, limit))
else:
cursor.execute(f"""
SELECT wallet_id, event_type, timestamp, data, success
FROM ledger_events_{chain_id}
WHERE wallet_id = ?
ORDER BY timestamp DESC LIMIT ?
""", (wallet_id, limit))
events = []
for row in cursor.fetchall():
data = json.loads(row[3]) if row[3] else {}
events.append(ChainLedgerRecord(
chain_id=chain_id,
wallet_id=row[0],
event_type=row[1],
timestamp=datetime.fromisoformat(row[2]),
data=data,
success=row[4]
))
return events
except Exception as e:
logger.error(f"Failed to get events for wallet {wallet_id} in chain {chain_id}: {e}")
return []
def get_chain_stats(self, chain_id: str) -> Dict[str, Any]:
"""Get statistics for a specific chain"""
try:
if not self.chain_manager.validate_chain_id(chain_id):
return {}
conn = self._get_connection(chain_id)
if not conn:
return {}
lock = self._get_lock(chain_id)
with lock:
cursor = conn.cursor()
# Wallet count
cursor.execute(f"SELECT COUNT(*) FROM wallet_metadata_{chain_id}")
wallet_count = cursor.fetchone()[0]
# Event count by type
cursor.execute(f"""
SELECT event_type, COUNT(*) FROM ledger_events_{chain_id}
GROUP BY event_type
""")
event_counts = dict(cursor.fetchall())
# Recent activity
cursor.execute(f"""
SELECT COUNT(*) FROM ledger_events_{chain_id}
WHERE timestamp > datetime('now', '-1 hour')
""")
recent_activity = cursor.fetchone()[0]
return {
"chain_id": chain_id,
"wallet_count": wallet_count,
"event_counts": event_counts,
"recent_activity": recent_activity,
"database_path": str(self._get_chain_db_path(chain_id))
}
except Exception as e:
logger.error(f"Failed to get stats for chain {chain_id}: {e}")
return {}
def get_all_chain_stats(self) -> Dict[str, Any]:
"""Get statistics for all chains"""
stats = {
"total_chains": 0,
"total_wallets": 0,
"chain_stats": {}
}
for chain in self.chain_manager.get_active_chains():
chain_stats = self.get_chain_stats(chain.chain_id)
if chain_stats:
stats["chain_stats"][chain.chain_id] = chain_stats
stats["total_wallets"] += chain_stats.get("wallet_count", 0)
stats["total_chains"] += 1
return stats
def close_all_connections(self):
"""Close all database connections"""
for chain_id, conn in self.chain_connections.items():
try:
conn.close()
logger.info(f"Closed connection for chain: {chain_id}")
except Exception as e:
logger.error(f"Failed to close connection for chain {chain_id}: {e}")
self.chain_connections.clear()
self.chain_locks.clear()