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:
22
apps/wallet/src/app/chain/__init__.py
Normal file
22
apps/wallet/src/app/chain/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Multi-Chain Support Module for Wallet Daemon
|
||||
|
||||
This module provides multi-chain capabilities for the wallet daemon,
|
||||
including chain management, chain-specific storage, and chain-aware
|
||||
wallet operations.
|
||||
"""
|
||||
|
||||
from .manager import ChainManager, ChainConfig, ChainStatus, chain_manager
|
||||
from .multichain_ledger import MultiChainLedgerAdapter, ChainLedgerRecord, ChainWalletMetadata
|
||||
from .chain_aware_wallet_service import ChainAwareWalletService
|
||||
|
||||
__all__ = [
|
||||
"ChainManager",
|
||||
"ChainConfig",
|
||||
"ChainStatus",
|
||||
"chain_manager",
|
||||
"MultiChainLedgerAdapter",
|
||||
"ChainLedgerRecord",
|
||||
"ChainWalletMetadata",
|
||||
"ChainAwareWalletService"
|
||||
]
|
||||
414
apps/wallet/src/app/chain/chain_aware_wallet_service.py
Normal file
414
apps/wallet/src/app/chain/chain_aware_wallet_service.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
Chain-Aware Wallet Service for Wallet Daemon
|
||||
|
||||
Multi-chain wallet operations with proper chain context,
|
||||
isolation, and management across different blockchain networks.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .manager import ChainManager, ChainConfig, ChainStatus
|
||||
from .multichain_ledger import MultiChainLedgerAdapter, ChainWalletMetadata
|
||||
from ..keystore.persistent_service import PersistentKeystoreService
|
||||
from ..security import wipe_buffer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChainAwareWalletService:
|
||||
"""Chain-aware wallet service with multi-chain support"""
|
||||
|
||||
def __init__(self, chain_manager: ChainManager, multichain_ledger: MultiChainLedgerAdapter):
|
||||
self.chain_manager = chain_manager
|
||||
self.multichain_ledger = multichain_ledger
|
||||
|
||||
# Chain-specific keystores
|
||||
self.chain_keystores: Dict[str, PersistentKeystoreService] = {}
|
||||
self._initialize_chain_keystores()
|
||||
|
||||
def _initialize_chain_keystores(self):
|
||||
"""Initialize keystore for each chain"""
|
||||
for chain in self.chain_manager.list_chains():
|
||||
self._init_chain_keystore(chain.chain_id)
|
||||
|
||||
def _init_chain_keystore(self, chain_id: str):
|
||||
"""Initialize keystore for a specific chain"""
|
||||
try:
|
||||
chain = self.chain_manager.get_chain(chain_id)
|
||||
if not chain:
|
||||
return
|
||||
|
||||
keystore_path = chain.keystore_path or f"./data/keystore_{chain_id}"
|
||||
keystore = PersistentKeystoreService(keystore_path)
|
||||
self.chain_keystores[chain_id] = keystore
|
||||
|
||||
logger.info(f"Initialized keystore for chain: {chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize keystore for chain {chain_id}: {e}")
|
||||
|
||||
def _get_keystore(self, chain_id: str) -> Optional[PersistentKeystoreService]:
|
||||
"""Get keystore for a specific chain"""
|
||||
if chain_id not in self.chain_keystores:
|
||||
self._init_chain_keystore(chain_id)
|
||||
|
||||
return self.chain_keystores.get(chain_id)
|
||||
|
||||
def create_wallet(self, chain_id: str, wallet_id: str, password: str,
|
||||
secret_key: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> Optional[ChainWalletMetadata]:
|
||||
"""Create a wallet in a specific chain"""
|
||||
try:
|
||||
# Validate chain
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
logger.error(f"Invalid or inactive chain: {chain_id}")
|
||||
return None
|
||||
|
||||
# Get keystore for chain
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
logger.error(f"Failed to get keystore for chain: {chain_id}")
|
||||
return None
|
||||
|
||||
# Create wallet in keystore
|
||||
keystore_record = keystore.create_wallet(wallet_id, password, secret_key, metadata or {})
|
||||
|
||||
# Create wallet in ledger
|
||||
success = self.multichain_ledger.create_wallet(
|
||||
chain_id, wallet_id, keystore_record.public_key,
|
||||
metadata=keystore_record.metadata
|
||||
)
|
||||
|
||||
if not success:
|
||||
# Rollback keystore creation
|
||||
try:
|
||||
keystore.delete_wallet(wallet_id, password)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Get wallet metadata
|
||||
wallet_metadata = self.multichain_ledger.get_wallet(chain_id, wallet_id)
|
||||
|
||||
# Record creation event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "created", {
|
||||
"public_key": keystore_record.public_key,
|
||||
"chain_id": chain_id,
|
||||
"metadata": metadata or {}
|
||||
})
|
||||
|
||||
logger.info(f"Created wallet {wallet_id} in chain {chain_id}")
|
||||
return wallet_metadata
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_wallet(self, chain_id: str, wallet_id: str) -> Optional[ChainWalletMetadata]:
|
||||
"""Get wallet metadata from a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return None
|
||||
|
||||
return self.multichain_ledger.get_wallet(chain_id, wallet_id)
|
||||
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: Optional[str] = None) -> List[ChainWalletMetadata]:
|
||||
"""List wallets from a specific chain or all chains"""
|
||||
try:
|
||||
if chain_id:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return []
|
||||
return self.multichain_ledger.list_wallets(chain_id)
|
||||
else:
|
||||
# List from all active chains
|
||||
all_wallets = []
|
||||
for chain in self.chain_manager.get_active_chains():
|
||||
chain_wallets = self.multichain_ledger.list_wallets(chain.chain_id)
|
||||
all_wallets.extend(chain_wallets)
|
||||
return all_wallets
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list wallets: {e}")
|
||||
return []
|
||||
|
||||
def delete_wallet(self, chain_id: str, wallet_id: str, password: str) -> bool:
|
||||
"""Delete a wallet from a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return False
|
||||
|
||||
# Delete from keystore
|
||||
keystore_success = keystore.delete_wallet(wallet_id, password)
|
||||
if not keystore_success:
|
||||
return False
|
||||
|
||||
# Record deletion event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "deleted", {
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
# Note: We keep the wallet metadata in ledger for audit purposes
|
||||
logger.info(f"Deleted wallet {wallet_id} from chain {chain_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete wallet {wallet_id} from chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def sign_message(self, chain_id: str, wallet_id: str, password: str, message: bytes,
|
||||
ip_address: Optional[str] = None) -> Optional[str]:
|
||||
"""Sign a message with wallet private key in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return None
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return None
|
||||
|
||||
# Sign message
|
||||
signature = keystore.sign_message(wallet_id, password, message, ip_address)
|
||||
|
||||
if signature:
|
||||
# Record signing event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "signed", {
|
||||
"message_length": len(message),
|
||||
"ip_address": ip_address,
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
logger.info(f"Signed message for wallet {wallet_id} in chain {chain_id}")
|
||||
|
||||
return signature
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sign message for wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return None
|
||||
|
||||
def unlock_wallet(self, chain_id: str, wallet_id: str, password: str) -> bool:
|
||||
"""Unlock a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return False
|
||||
|
||||
# Unlock wallet
|
||||
success = keystore.unlock_wallet(wallet_id, password)
|
||||
|
||||
if success:
|
||||
# Record unlock event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "unlocked", {
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
logger.info(f"Unlocked wallet {wallet_id} in chain {chain_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to unlock wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def lock_wallet(self, chain_id: str, wallet_id: str) -> bool:
|
||||
"""Lock a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return False
|
||||
|
||||
# Lock wallet
|
||||
success = keystore.lock_wallet(wallet_id)
|
||||
|
||||
if success:
|
||||
# Record lock event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "locked", {
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
logger.info(f"Locked wallet {wallet_id} in chain {chain_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to lock 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[Dict[str, Any]]:
|
||||
"""Get events for a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return []
|
||||
|
||||
events = self.multichain_ledger.get_wallet_events(chain_id, wallet_id, event_type, limit)
|
||||
|
||||
return [
|
||||
{
|
||||
"chain_id": event.chain_id,
|
||||
"wallet_id": event.wallet_id,
|
||||
"event_type": event.event_type,
|
||||
"timestamp": event.timestamp.isoformat(),
|
||||
"data": event.data,
|
||||
"success": event.success
|
||||
}
|
||||
for event in 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_wallet_stats(self, chain_id: str) -> Dict[str, Any]:
|
||||
"""Get wallet statistics for a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return {}
|
||||
|
||||
# Get ledger stats
|
||||
ledger_stats = self.multichain_ledger.get_chain_stats(chain_id)
|
||||
|
||||
# Get keystore stats
|
||||
keystore = self._get_keystore(chain_id)
|
||||
keystore_stats = {}
|
||||
if keystore:
|
||||
keystore_stats = {
|
||||
"total_wallets": len(keystore.list_wallets()),
|
||||
"unlocked_wallets": len([w for w in keystore.list_wallets() if w.get("unlocked", False)])
|
||||
}
|
||||
|
||||
return {
|
||||
"chain_id": chain_id,
|
||||
"ledger_stats": ledger_stats,
|
||||
"keystore_stats": keystore_stats
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get stats for chain {chain_id}: {e}")
|
||||
return {}
|
||||
|
||||
def get_all_chain_wallet_stats(self) -> Dict[str, Any]:
|
||||
"""Get wallet 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_wallet_stats(chain.chain_id)
|
||||
if chain_stats:
|
||||
stats["chain_stats"][chain.chain_id] = chain_stats
|
||||
stats["total_wallets"] += chain_stats.get("ledger_stats", {}).get("wallet_count", 0)
|
||||
stats["total_chains"] += 1
|
||||
|
||||
return stats
|
||||
|
||||
def migrate_wallet_between_chains(self, source_chain_id: str, target_chain_id: str,
|
||||
wallet_id: str, password: str, new_password: Optional[str] = None) -> bool:
|
||||
"""Migrate a wallet from one chain to another"""
|
||||
try:
|
||||
# Validate both chains
|
||||
if not self.chain_manager.validate_chain_id(source_chain_id):
|
||||
logger.error(f"Invalid source chain: {source_chain_id}")
|
||||
return False
|
||||
|
||||
if not self.chain_manager.validate_chain_id(target_chain_id):
|
||||
logger.error(f"Invalid target chain: {target_chain_id}")
|
||||
return False
|
||||
|
||||
# Get source wallet
|
||||
source_wallet = self.get_wallet(source_chain_id, wallet_id)
|
||||
if not source_wallet:
|
||||
logger.error(f"Wallet {wallet_id} not found in source chain {source_chain_id}")
|
||||
return False
|
||||
|
||||
# Check if wallet already exists in target chain
|
||||
target_wallet = self.get_wallet(target_chain_id, wallet_id)
|
||||
if target_wallet:
|
||||
logger.error(f"Wallet {wallet_id} already exists in target chain {target_chain_id}")
|
||||
return False
|
||||
|
||||
# Get source keystore
|
||||
source_keystore = self._get_keystore(source_chain_id)
|
||||
target_keystore = self._get_keystore(target_chain_id)
|
||||
|
||||
if not source_keystore or not target_keystore:
|
||||
logger.error("Failed to get keystores for migration")
|
||||
return False
|
||||
|
||||
# Export wallet from source chain
|
||||
try:
|
||||
# This would require adding export/import methods to keystore
|
||||
# For now, we'll create a new wallet with the same keys
|
||||
source_keystore_record = source_keystore.get_wallet(wallet_id)
|
||||
if not source_keystore_record:
|
||||
logger.error("Failed to get source wallet record")
|
||||
return False
|
||||
|
||||
# Create wallet in target chain with same keys
|
||||
target_wallet = self.create_wallet(
|
||||
target_chain_id, wallet_id, new_password or password,
|
||||
source_keystore_record.get("secret_key"), source_wallet.metadata
|
||||
)
|
||||
|
||||
if target_wallet:
|
||||
# Record migration events
|
||||
self.multichain_ledger.record_event(source_chain_id, wallet_id, "migrated_from", {
|
||||
"target_chain": target_chain_id,
|
||||
"migration_timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
self.multichain_ledger.record_event(target_chain_id, wallet_id, "migrated_to", {
|
||||
"source_chain": source_chain_id,
|
||||
"migration_timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
logger.info(f"Migrated wallet {wallet_id} from {source_chain_id} to {target_chain_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to create wallet in target chain")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to migrate wallet {wallet_id}: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Wallet migration failed: {e}")
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup resources"""
|
||||
try:
|
||||
# Close all keystore connections
|
||||
for chain_id, keystore in self.chain_keystores.items():
|
||||
try:
|
||||
keystore.close()
|
||||
logger.info(f"Closed keystore for chain: {chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close keystore for chain {chain_id}: {e}")
|
||||
|
||||
self.chain_keystores.clear()
|
||||
|
||||
# Close ledger connections
|
||||
self.multichain_ledger.close_all_connections()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup wallet service: {e}")
|
||||
273
apps/wallet/src/app/chain/manager.py
Normal file
273
apps/wallet/src/app/chain/manager.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Multi-Chain Manager for Wallet Daemon
|
||||
|
||||
Central management for multiple blockchain networks, providing
|
||||
chain context, routing, and isolation for wallet operations.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChainStatus(Enum):
|
||||
"""Chain operational status"""
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
MAINTENANCE = "maintenance"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChainConfig:
|
||||
"""Configuration for a specific blockchain network"""
|
||||
chain_id: str
|
||||
name: str
|
||||
coordinator_url: str
|
||||
coordinator_api_key: str
|
||||
status: ChainStatus = ChainStatus.ACTIVE
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Chain-specific settings
|
||||
default_gas_limit: int = 10000000
|
||||
default_gas_price: int = 20000000000
|
||||
transaction_timeout: int = 300
|
||||
max_retries: int = 3
|
||||
|
||||
# Storage configuration
|
||||
ledger_db_path: Optional[str] = None
|
||||
keystore_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization"""
|
||||
return {
|
||||
"chain_id": self.chain_id,
|
||||
"name": self.name,
|
||||
"coordinator_url": self.coordinator_url,
|
||||
"coordinator_api_key": self.coordinator_api_key,
|
||||
"status": self.status.value,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"metadata": self.metadata,
|
||||
"default_gas_limit": self.default_gas_limit,
|
||||
"default_gas_price": self.default_gas_price,
|
||||
"transaction_timeout": self.transaction_timeout,
|
||||
"max_retries": self.max_retries,
|
||||
"ledger_db_path": self.ledger_db_path,
|
||||
"keystore_path": self.keystore_path
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ChainConfig":
|
||||
"""Create from dictionary"""
|
||||
# Ensure data is a dict and make a copy
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Expected dict, got {type(data)}")
|
||||
|
||||
data = data.copy()
|
||||
data["status"] = ChainStatus(data["status"])
|
||||
data["created_at"] = datetime.fromisoformat(data["created_at"])
|
||||
data["updated_at"] = datetime.fromisoformat(data["updated_at"])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class ChainManager:
|
||||
"""Central manager for multi-chain operations"""
|
||||
|
||||
def __init__(self, config_path: Optional[Path] = None):
|
||||
self.config_path = config_path or Path("./data/chains.json")
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.chains: Dict[str, ChainConfig] = {}
|
||||
self.default_chain_id: Optional[str] = None
|
||||
self._load_chains()
|
||||
|
||||
def _load_chains(self):
|
||||
"""Load chain configurations from file"""
|
||||
try:
|
||||
if self.config_path.exists():
|
||||
with open(self.config_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
for chain_data in data.get("chains", []):
|
||||
chain = ChainConfig.from_dict(chain_data)
|
||||
self.chains[chain.chain_id] = chain
|
||||
|
||||
self.default_chain_id = data.get("default_chain_id")
|
||||
logger.info(f"Loaded {len(self.chains)} chain configurations")
|
||||
else:
|
||||
# Create default chain configuration
|
||||
self._create_default_chain()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load chain configurations: {e}")
|
||||
self._create_default_chain()
|
||||
|
||||
def _create_default_chain(self):
|
||||
"""Create default chain configuration"""
|
||||
default_chain = ChainConfig(
|
||||
chain_id="ait-devnet",
|
||||
name="AITBC Development Network",
|
||||
coordinator_url="http://localhost:8011",
|
||||
coordinator_api_key="dev-coordinator-key",
|
||||
ledger_db_path="./data/wallet_ledger_devnet.db",
|
||||
keystore_path="./data/keystore_devnet"
|
||||
)
|
||||
|
||||
self.chains[default_chain.chain_id] = default_chain
|
||||
self.default_chain_id = default_chain.chain_id
|
||||
self._save_chains()
|
||||
logger.info(f"Created default chain: {default_chain.chain_id}")
|
||||
|
||||
def _save_chains(self):
|
||||
"""Save chain configurations to file"""
|
||||
try:
|
||||
data = {
|
||||
"chains": [chain.to_dict() for chain in self.chains.values()],
|
||||
"default_chain_id": self.default_chain_id,
|
||||
"updated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(self.config_path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
logger.info(f"Saved {len(self.chains)} chain configurations")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save chain configurations: {e}")
|
||||
|
||||
def add_chain(self, chain_config: ChainConfig) -> bool:
|
||||
"""Add a new chain configuration"""
|
||||
try:
|
||||
if chain_config.chain_id in self.chains:
|
||||
logger.warning(f"Chain {chain_config.chain_id} already exists")
|
||||
return False
|
||||
|
||||
self.chains[chain_config.chain_id] = chain_config
|
||||
|
||||
# Set as default if no default exists
|
||||
if self.default_chain_id is None:
|
||||
self.default_chain_id = chain_config.chain_id
|
||||
|
||||
self._save_chains()
|
||||
logger.info(f"Added chain: {chain_config.chain_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add chain {chain_config.chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def remove_chain(self, chain_id: str) -> bool:
|
||||
"""Remove a chain configuration"""
|
||||
try:
|
||||
if chain_id not in self.chains:
|
||||
logger.warning(f"Chain {chain_id} not found")
|
||||
return False
|
||||
|
||||
if chain_id == self.default_chain_id:
|
||||
logger.error(f"Cannot remove default chain {chain_id}")
|
||||
return False
|
||||
|
||||
del self.chains[chain_id]
|
||||
self._save_chains()
|
||||
logger.info(f"Removed chain: {chain_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_chain(self, chain_id: str) -> Optional[ChainConfig]:
|
||||
"""Get chain configuration by ID"""
|
||||
return self.chains.get(chain_id)
|
||||
|
||||
def get_default_chain(self) -> Optional[ChainConfig]:
|
||||
"""Get default chain configuration"""
|
||||
if self.default_chain_id:
|
||||
return self.chains.get(self.default_chain_id)
|
||||
return None
|
||||
|
||||
def set_default_chain(self, chain_id: str) -> bool:
|
||||
"""Set default chain"""
|
||||
try:
|
||||
if chain_id not in self.chains:
|
||||
logger.error(f"Chain {chain_id} not found")
|
||||
return False
|
||||
|
||||
self.default_chain_id = chain_id
|
||||
self._save_chains()
|
||||
logger.info(f"Set default chain: {chain_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set default chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def list_chains(self) -> List[ChainConfig]:
|
||||
"""List all chain configurations"""
|
||||
return list(self.chains.values())
|
||||
|
||||
def get_active_chains(self) -> List[ChainConfig]:
|
||||
"""Get only active chains"""
|
||||
return [chain for chain in self.chains.values() if chain.status == ChainStatus.ACTIVE]
|
||||
|
||||
def update_chain_status(self, chain_id: str, status: ChainStatus) -> bool:
|
||||
"""Update chain status"""
|
||||
try:
|
||||
if chain_id not in self.chains:
|
||||
logger.error(f"Chain {chain_id} not found")
|
||||
return False
|
||||
|
||||
self.chains[chain_id].status = status
|
||||
self.chains[chain_id].updated_at = datetime.now()
|
||||
self._save_chains()
|
||||
logger.info(f"Updated chain {chain_id} status to {status.value}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update chain status {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def validate_chain_id(self, chain_id: str) -> bool:
|
||||
"""Validate that a chain ID exists and is active"""
|
||||
chain = self.chains.get(chain_id)
|
||||
return chain is not None and chain.status == ChainStatus.ACTIVE
|
||||
|
||||
def get_chain_config_for_wallet(self, chain_id: str, wallet_id: str) -> Optional[ChainConfig]:
|
||||
"""Get chain configuration for a specific wallet operation"""
|
||||
if not self.validate_chain_id(chain_id):
|
||||
logger.error(f"Invalid or inactive chain: {chain_id}")
|
||||
return None
|
||||
|
||||
chain = self.chains[chain_id]
|
||||
|
||||
# Add wallet-specific context to metadata
|
||||
chain.metadata["last_wallet_access"] = wallet_id
|
||||
chain.metadata["last_access_time"] = datetime.now().isoformat()
|
||||
|
||||
return chain
|
||||
|
||||
def get_chain_stats(self) -> Dict[str, Any]:
|
||||
"""Get statistics about chains"""
|
||||
active_chains = self.get_active_chains()
|
||||
|
||||
return {
|
||||
"total_chains": len(self.chains),
|
||||
"active_chains": len(active_chains),
|
||||
"inactive_chains": len(self.chains) - len(active_chains),
|
||||
"default_chain": self.default_chain_id,
|
||||
"chain_list": [
|
||||
{
|
||||
"chain_id": chain.chain_id,
|
||||
"name": chain.name,
|
||||
"status": chain.status.value,
|
||||
"coordinator_url": chain.coordinator_url
|
||||
}
|
||||
for chain in self.chains.values()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Global chain manager instance
|
||||
chain_manager = ChainManager()
|
||||
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