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
This commit is contained in:
427
apps/wallet/src/app/chain/multichain_ledger.py
Normal file
427
apps/wallet/src/app/chain/multichain_ledger.py
Normal file
@@ -0,0 +1,427 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user