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:
oib
2026-03-06 18:14:49 +01:00
parent dc1561d457
commit bb5363bebc
295 changed files with 35501 additions and 3734 deletions

View File

@@ -0,0 +1,5 @@
"""Wallet daemon FastAPI application package."""
from .main import create_app
__all__ = ["create_app"]

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Wallet Daemon Entry Point
This module provides the entry point for running the AITBC wallet daemon
with multi-chain support.
"""
import uvicorn
import logging
from pathlib import Path
from app.main import app
from app.settings import settings
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
def main():
"""Main entry point for the wallet daemon"""
logger.info("Starting AITBC Wallet Daemon with Multi-Chain Support")
logger.info(f"Debug mode: {settings.debug}")
logger.info(f"Coordinator URL: {settings.coordinator_base_url}")
logger.info(f"Ledger DB Path: {settings.ledger_db_path}")
# Create data directory if it doesn't exist
data_dir = settings.ledger_db_path.parent
data_dir.mkdir(parents=True, exist_ok=True)
# Initialize chain manager
try:
from app.chain.manager import chain_manager
logger.info("Initializing chain manager...")
# Load chains from configuration
chain_manager.load_chains()
# Log chain information
chains = chain_manager.list_chains()
logger.info(f"Loaded {len(chains)} chains:")
for chain in chains:
logger.info(f" - {chain.chain_id}: {chain.name} ({chain.status.value})")
logger.info(f"Default chain: {chain_manager.default_chain_id}")
except Exception as e:
logger.error(f"Failed to initialize chain manager: {e}")
logger.info("Continuing without multi-chain support...")
# Start the server
logger.info(f"Starting server on {settings.host}:{settings.port}")
uvicorn.run(
app,
host=settings.host,
port=settings.port,
reload=settings.debug,
log_level="info" if not settings.debug else "debug"
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,118 @@
from __future__ import annotations
import base64
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends
from .deps import get_receipt_service, get_keystore, get_ledger
from .models import ReceiptVerificationModel, from_validation_result
from .keystore.persistent_service import PersistentKeystoreService
from .ledger_mock import SQLiteLedgerAdapter
from .receipts.service import ReceiptVerifierService
router = APIRouter(tags=["jsonrpc"])
def _response(result: Optional[Dict[str, Any]] = None, error: Optional[Dict[str, Any]] = None, *, request_id: Any = None) -> Dict[str, Any]:
payload: Dict[str, Any] = {"jsonrpc": "2.0", "id": request_id}
if error is not None:
payload["error"] = error
else:
payload["result"] = result
return payload
@router.post("/rpc", summary="JSON-RPC endpoint")
def handle_jsonrpc(
request: Dict[str, Any],
service: ReceiptVerifierService = Depends(get_receipt_service),
keystore: KeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> Dict[str, Any]:
method = request.get("method")
params = request.get("params") or {}
request_id = request.get("id")
if method == "receipts.verify_latest":
job_id = params.get("job_id")
if not job_id:
return _response(error={"code": -32602, "message": "job_id required"}, request_id=request_id)
result = service.verify_latest(str(job_id))
if result is None:
return _response(error={"code": -32004, "message": "receipt not found"}, request_id=request_id)
model = from_validation_result(result)
return _response(result=model.model_dump(), request_id=request_id)
if method == "receipts.verify_history":
job_id = params.get("job_id")
if not job_id:
return _response(error={"code": -32602, "message": "job_id required"}, request_id=request_id)
results = [from_validation_result(item).model_dump() for item in service.verify_history(str(job_id))]
return _response(result={"items": results}, request_id=request_id)
if method == "wallet.list":
items = []
for record in keystore.list_records():
ledger_record = ledger.get_wallet(record.wallet_id)
metadata = ledger_record.metadata if ledger_record else record.metadata
items.append({"wallet_id": record.wallet_id, "public_key": record.public_key, "metadata": metadata})
return _response(result={"items": items}, request_id=request_id)
if method == "wallet.create":
wallet_id = params.get("wallet_id")
password = params.get("password")
metadata = params.get("metadata") or {}
secret_b64 = params.get("secret_key")
if not wallet_id or not password:
return _response(error={"code": -32602, "message": "wallet_id and password required"}, request_id=request_id)
secret = base64.b64decode(secret_b64) if secret_b64 else None
record = keystore.create_wallet(wallet_id=wallet_id, password=password, secret=secret, metadata=metadata)
ledger.upsert_wallet(record.wallet_id, record.public_key, record.metadata)
ledger.record_event(record.wallet_id, "created", {"metadata": record.metadata})
return _response(
result={
"wallet": {
"wallet_id": record.wallet_id,
"public_key": record.public_key,
"metadata": record.metadata,
}
},
request_id=request_id,
)
if method == "wallet.unlock":
wallet_id = params.get("wallet_id")
password = params.get("password")
if not wallet_id or not password:
return _response(error={"code": -32602, "message": "wallet_id and password required"}, request_id=request_id)
try:
keystore.unlock_wallet(wallet_id, password)
ledger.record_event(wallet_id, "unlocked", {"success": True})
return _response(result={"wallet_id": wallet_id, "unlocked": True}, request_id=request_id)
except (KeyError, ValueError):
ledger.record_event(wallet_id, "unlocked", {"success": False})
return _response(error={"code": -32001, "message": "invalid credentials"}, request_id=request_id)
if method == "wallet.sign":
wallet_id = params.get("wallet_id")
password = params.get("password")
message_b64 = params.get("message")
if not wallet_id or not password or not message_b64:
return _response(error={"code": -32602, "message": "wallet_id, password, message required"}, request_id=request_id)
try:
message = base64.b64decode(message_b64)
except Exception:
return _response(error={"code": -32602, "message": "invalid base64 message"}, request_id=request_id)
try:
signature = keystore.sign_message(wallet_id, password, message)
ledger.record_event(wallet_id, "sign", {"success": True})
except (KeyError, ValueError):
ledger.record_event(wallet_id, "sign", {"success": False})
return _response(error={"code": -32001, "message": "invalid credentials"}, request_id=request_id)
signature_b64 = base64.b64encode(signature).decode()
return _response(result={"wallet_id": wallet_id, "signature": signature_b64}, request_id=request_id)
return _response(error={"code": -32601, "message": "Method not found"}, request_id=request_id)

View File

@@ -0,0 +1,439 @@
from __future__ import annotations
import base64
from datetime import datetime
from aitbc.logging import get_logger
from fastapi import APIRouter, Depends, HTTPException, status, Request
from .deps import get_receipt_service, get_keystore, get_ledger
# Temporarily disable multi-chain imports
# from .chain.manager import ChainManager, chain_manager
# from .chain.multichain_ledger import MultiChainLedgerAdapter
# from .chain.chain_aware_wallet_service import ChainAwareWalletService
from .models import (
ReceiptVerificationListResponse,
ReceiptVerificationModel,
ReceiptVerifyResponse,
SignatureValidationModel,
WalletCreateRequest,
WalletCreateResponse,
WalletListResponse,
WalletUnlockRequest,
WalletUnlockResponse,
WalletSignRequest,
WalletSignResponse,
WalletDescriptor,
ChainInfo,
ChainListResponse,
ChainCreateRequest,
ChainCreateResponse,
WalletMigrationRequest,
WalletMigrationResponse,
from_validation_result,
)
from .keystore.persistent_service import PersistentKeystoreService
from .ledger_mock import SQLiteLedgerAdapter
from .receipts.service import ReceiptValidationResult, ReceiptVerifierService
from .chain.manager import ChainManager, chain_manager
from .chain.multichain_ledger import MultiChainLedgerAdapter
from .chain.chain_aware_wallet_service import ChainAwareWalletService
from .security import RateLimiter, wipe_buffer
logger = get_logger(__name__)
_rate_limiter = RateLimiter(max_requests=30, window_seconds=60)
def _rate_key(action: str, request: Request, wallet_id: Optional[str] = None) -> str:
host = request.client.host if request.client else "unknown"
parts = [action, host]
if wallet_id:
parts.append(wallet_id)
return ":".join(parts)
def _enforce_limit(action: str, request: Request, wallet_id: Optional[str] = None) -> None:
key = _rate_key(action, request, wallet_id)
if not _rate_limiter.allow(key):
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="rate limit exceeded")
router = APIRouter(prefix="/v1", tags=["wallets", "receipts"])
def _result_to_response(result: ReceiptValidationResult) -> ReceiptVerifyResponse:
payload = from_validation_result(result)
return ReceiptVerifyResponse(result=payload)
@router.get(
"/receipts/{job_id}",
response_model=ReceiptVerifyResponse,
summary="Verify latest receipt for a job",
)
def verify_latest_receipt(
job_id: str,
service: ReceiptVerifierService = Depends(get_receipt_service),
) -> ReceiptVerifyResponse:
result = service.verify_latest(job_id)
if result is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="receipt not found")
return _result_to_response(result)
@router.get(
"/receipts/{job_id}/history",
response_model=ReceiptVerificationListResponse,
summary="Verify all historical receipts for a job",
)
def verify_receipt_history(
job_id: str,
service: ReceiptVerifierService = Depends(get_receipt_service),
) -> ReceiptVerificationListResponse:
results = service.verify_history(job_id)
items = [from_validation_result(result) for result in results]
return ReceiptVerificationListResponse(items=items)
@router.get("/wallets", response_model=WalletListResponse, summary="List wallets")
def list_wallets(
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletListResponse:
descriptors = []
for record in keystore.list_records():
ledger_record = ledger.get_wallet(record.wallet_id)
metadata = ledger_record.metadata if ledger_record else record.metadata
descriptors.append(
WalletDescriptor(wallet_id=record.wallet_id, public_key=record.public_key, metadata=metadata)
)
return WalletListResponse(items=descriptors)
@router.post("/wallets", response_model=WalletCreateResponse, status_code=status.HTTP_201_CREATED, summary="Create wallet")
def create_wallet(
request: WalletCreateRequest,
http_request: Request,
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletCreateResponse:
_enforce_limit("wallet-create", http_request)
try:
secret = base64.b64decode(request.secret_key) if request.secret_key else None
except Exception as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid base64 secret") from exc
try:
ip_address = http_request.client.host if http_request.client else "unknown"
record = keystore.create_wallet(
wallet_id=request.wallet_id,
password=request.password,
secret=secret,
metadata=request.metadata,
ip_address=ip_address
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"reason": "password_too_weak", "min_length": 10, "message": str(exc)},
) from exc
ledger.upsert_wallet(record.wallet_id, record.public_key, record.metadata)
ledger.record_event(record.wallet_id, "created", {"metadata": record.metadata})
logger.info("Created wallet", extra={"wallet_id": record.wallet_id})
wallet = WalletDescriptor(wallet_id=record.wallet_id, public_key=record.public_key, metadata=record.metadata)
return WalletCreateResponse(wallet=wallet)
@router.post("/wallets/{wallet_id}/unlock", response_model=WalletUnlockResponse, summary="Unlock wallet")
def unlock_wallet(
wallet_id: str,
request: WalletUnlockRequest,
http_request: Request,
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletUnlockResponse:
_enforce_limit("wallet-unlock", http_request, wallet_id)
try:
ip_address = http_request.client.host if http_request.client else "unknown"
secret = bytearray(keystore.unlock_wallet(wallet_id, request.password, ip_address))
ledger.record_event(wallet_id, "unlocked", {"success": True, "ip_address": ip_address})
logger.info("Unlocked wallet", extra={"wallet_id": wallet_id})
except (KeyError, ValueError):
ip_address = http_request.client.host if http_request.client else "unknown"
ledger.record_event(wallet_id, "unlocked", {"success": False, "ip_address": ip_address})
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
finally:
if "secret" in locals():
wipe_buffer(secret)
# We don't expose the secret in response
return WalletUnlockResponse(wallet_id=wallet_id, unlocked=True)
@router.post("/wallets/{wallet_id}/sign", response_model=WalletSignResponse, summary="Sign payload")
def sign_payload(
wallet_id: str,
request: WalletSignRequest,
http_request: Request,
keystore: PersistentKeystoreService = Depends(get_keystore),
ledger: SQLiteLedgerAdapter = Depends(get_ledger),
) -> WalletSignResponse:
_enforce_limit("wallet-sign", http_request, wallet_id)
try:
message = base64.b64decode(request.message_base64)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid base64 message") from exc
try:
ip_address = http_request.client.host if http_request.client else "unknown"
signature = keystore.sign_message(wallet_id, request.password, message, ip_address)
ledger.record_event(wallet_id, "sign", {"success": True, "ip_address": ip_address})
logger.debug("Signed payload", extra={"wallet_id": wallet_id})
except (KeyError, ValueError):
ip_address = http_request.client.host if http_request.client else "unknown"
ledger.record_event(wallet_id, "sign", {"success": False, "ip_address": ip_address})
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
signature_b64 = base64.b64encode(signature).decode()
return WalletSignResponse(wallet_id=wallet_id, signature_base64=signature_b64)
# Multi-Chain Endpoints
@router.get("/chains", response_model=ChainListResponse, summary="List all chains")
def list_chains(
chain_manager: ChainManager = Depends(get_chain_manager),
multichain_ledger: MultiChainLedgerAdapter = Depends(get_multichain_ledger)
) -> ChainListResponse:
"""List all blockchain chains with their statistics"""
chains = []
active_chains = chain_manager.get_active_chains()
for chain in chain_manager.list_chains():
stats = multichain_ledger.get_chain_stats(chain.chain_id)
chain_info = ChainInfo(
chain_id=chain.chain_id,
name=chain.name,
status=chain.status.value,
coordinator_url=chain.coordinator_url,
created_at=chain.created_at.isoformat(),
updated_at=chain.updated_at.isoformat(),
wallet_count=stats.get("wallet_count", 0),
recent_activity=stats.get("recent_activity", 0)
)
chains.append(chain_info)
return ChainListResponse(
chains=chains,
total_chains=len(chains),
active_chains=len(active_chains)
)
@router.post("/chains", response_model=ChainCreateResponse, status_code=status.HTTP_201_CREATED, summary="Create a new chain")
def create_chain(
request: ChainCreateRequest,
http_request: Request,
chain_manager: ChainManager = Depends(get_chain_manager)
) -> ChainCreateResponse:
"""Create a new blockchain chain configuration"""
_enforce_limit("chain-create", http_request)
from .chain.manager import ChainConfig
chain_config = ChainConfig(
chain_id=request.chain_id,
name=request.name,
coordinator_url=request.coordinator_url,
coordinator_api_key=request.coordinator_api_key,
metadata=request.metadata
)
success = chain_manager.add_chain(chain_config)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Chain {request.chain_id} already exists"
)
chain_info = ChainInfo(
chain_id=chain_config.chain_id,
name=chain_config.name,
status=chain_config.status.value,
coordinator_url=chain_config.coordinator_url,
created_at=chain_config.created_at.isoformat(),
updated_at=chain_config.updated_at.isoformat(),
wallet_count=0,
recent_activity=0
)
return ChainCreateResponse(chain=chain_info)
@router.get("/chains/{chain_id}/wallets", response_model=WalletListResponse, summary="List wallets in a specific chain")
def list_chain_wallets(
chain_id: str,
wallet_service: ChainAwareWalletService = Depends(get_chain_aware_wallet_service)
) -> WalletListResponse:
"""List wallets in a specific blockchain chain"""
wallets = wallet_service.list_wallets(chain_id)
descriptors = []
for wallet in wallets:
descriptor = WalletDescriptor(
wallet_id=wallet.wallet_id,
chain_id=wallet.chain_id,
public_key=wallet.public_key,
address=wallet.address,
metadata=wallet.metadata
)
descriptors.append(descriptor)
return WalletListResponse(items=descriptors)
@router.post("/chains/{chain_id}/wallets", response_model=WalletCreateResponse, status_code=status.HTTP_201_CREATED, summary="Create wallet in a specific chain")
def create_chain_wallet(
chain_id: str,
request: WalletCreateRequest,
http_request: Request,
wallet_service: ChainAwareWalletService = Depends(get_chain_aware_wallet_service)
) -> WalletCreateResponse:
"""Create a wallet in a specific blockchain chain"""
_enforce_limit("wallet-create", http_request)
try:
secret = base64.b64decode(request.secret_key) if request.secret_key else None
except Exception as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid base64 secret") from exc
wallet_metadata = wallet_service.create_wallet(
chain_id=chain_id,
wallet_id=request.wallet_id,
password=request.password,
secret_key=secret,
metadata=request.metadata
)
if not wallet_metadata:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to create wallet in chain"
)
wallet = WalletDescriptor(
wallet_id=wallet_metadata.wallet_id,
chain_id=wallet_metadata.chain_id,
public_key=wallet_metadata.public_key,
address=wallet_metadata.address,
metadata=wallet_metadata.metadata
)
return WalletCreateResponse(wallet=wallet)
@router.post("/chains/{chain_id}/wallets/{wallet_id}/unlock", response_model=WalletUnlockResponse, summary="Unlock wallet in a specific chain")
def unlock_chain_wallet(
chain_id: str,
wallet_id: str,
request: WalletUnlockRequest,
http_request: Request,
wallet_service: ChainAwareWalletService = Depends(get_chain_aware_wallet_service)
) -> WalletUnlockResponse:
"""Unlock a wallet in a specific blockchain chain"""
_enforce_limit("wallet-unlock", http_request, wallet_id)
success = wallet_service.unlock_wallet(chain_id, wallet_id, request.password)
if not success:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
return WalletUnlockResponse(wallet_id=wallet_id, chain_id=chain_id, unlocked=True)
@router.post("/chains/{chain_id}/wallets/{wallet_id}/sign", response_model=WalletSignResponse, summary="Sign payload with wallet in a specific chain")
def sign_chain_payload(
chain_id: str,
wallet_id: str,
request: WalletSignRequest,
http_request: Request,
wallet_service: ChainAwareWalletService = Depends(get_chain_aware_wallet_service)
) -> WalletSignResponse:
"""Sign a payload with a wallet in a specific blockchain chain"""
_enforce_limit("wallet-sign", http_request, wallet_id)
try:
message = base64.b64decode(request.message_base64)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid base64 message") from exc
ip_address = http_request.client.host if http_request.client else "unknown"
signature = wallet_service.sign_message(chain_id, wallet_id, request.password, message, ip_address)
if not signature:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid credentials")
return WalletSignResponse(
wallet_id=wallet_id,
chain_id=chain_id,
signature_base64=base64.b64encode(signature).decode()
)
@router.post("/wallets/migrate", response_model=WalletMigrationResponse, summary="Migrate wallet between chains")
def migrate_wallet(
request: WalletMigrationRequest,
http_request: Request,
wallet_service: ChainAwareWalletService = Depends(get_chain_aware_wallet_service)
) -> WalletMigrationResponse:
"""Migrate a wallet from one chain to another"""
_enforce_limit("wallet-migrate", http_request)
success = wallet_service.migrate_wallet_between_chains(
source_chain_id=request.source_chain_id,
target_chain_id=request.target_chain_id,
wallet_id=request.wallet_id,
password=request.password,
new_password=request.new_password
)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Failed to migrate wallet"
)
# Get both wallet descriptors
source_wallet = wallet_service.get_wallet(request.source_chain_id, request.wallet_id)
target_wallet = wallet_service.get_wallet(request.target_chain_id, request.wallet_id)
if not source_wallet or not target_wallet:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Migration completed but wallet retrieval failed"
)
source_descriptor = WalletDescriptor(
wallet_id=source_wallet.wallet_id,
chain_id=source_wallet.chain_id,
public_key=source_wallet.public_key,
address=source_wallet.address,
metadata=source_wallet.metadata
)
target_descriptor = WalletDescriptor(
wallet_id=target_wallet.wallet_id,
chain_id=target_wallet.chain_id,
public_key=target_wallet.public_key,
address=target_wallet.address,
metadata=target_wallet.metadata
)
return WalletMigrationResponse(
success=True,
source_wallet=source_descriptor,
target_wallet=target_descriptor,
migration_timestamp=datetime.now().isoformat()
)

View 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"
]

View 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}")

View 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()

View 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()

View File

@@ -0,0 +1,66 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from argon2.low_level import Type as Argon2Type, hash_secret_raw
from nacl.bindings import (
crypto_aead_xchacha20poly1305_ietf_decrypt,
crypto_aead_xchacha20poly1305_ietf_encrypt,
)
from ..security import wipe_buffer
class EncryptionError(Exception):
"""Raised when encryption or decryption fails."""
@dataclass
class EncryptionSuite:
"""Argon2id + XChaCha20-Poly1305 helper functions."""
salt_bytes: int = 16
nonce_bytes: int = 24
key_bytes: int = 32
argon_time_cost: int = 3
argon_memory_cost: int = 64 * 1024 # kibibytes
argon_parallelism: int = 2
def _derive_key(self, *, password: str, salt: bytes) -> bytes:
password_bytes = password.encode("utf-8")
return hash_secret_raw(
secret=password_bytes,
salt=salt,
time_cost=self.argon_time_cost,
memory_cost=self.argon_memory_cost,
parallelism=self.argon_parallelism,
hash_len=self.key_bytes,
type=Argon2Type.ID,
)
def encrypt(self, *, password: str, plaintext: bytes, salt: bytes, nonce: bytes) -> bytes:
key = self._derive_key(password=password, salt=salt)
try:
return crypto_aead_xchacha20poly1305_ietf_encrypt(
message=plaintext,
aad=b"",
nonce=nonce,
key=key,
)
except Exception as exc: # pragma: no cover
raise EncryptionError("encryption failed") from exc
def decrypt(self, *, password: str, ciphertext: bytes, salt: bytes, nonce: bytes) -> bytes:
key_bytes = bytearray(self._derive_key(password=password, salt=salt))
try:
return crypto_aead_xchacha20poly1305_ietf_decrypt(
ciphertext=ciphertext,
aad=b"",
nonce=nonce,
key=bytes(key_bytes),
)
except Exception as exc:
raise EncryptionError("decryption failed") from exc
finally:
wipe_buffer(key_bytes)

View File

@@ -0,0 +1,51 @@
from __future__ import annotations
from functools import lru_cache
from fastapi import Depends
from .keystore.service import KeystoreService
from .ledger_mock import SQLiteLedgerAdapter
from .keystore.persistent_service import PersistentKeystoreService
from .receipts.service import ReceiptVerifierService
from .settings import Settings, settings
# Temporarily disable multi-chain imports to test basic functionality
# from .chain.manager import ChainManager, chain_manager
# from .chain.multichain_ledger import MultiChainLedgerAdapter
# from .chain.chain_aware_wallet_service import ChainAwareWalletService
def get_settings() -> Settings:
return settings
def get_receipt_service(config: Settings = Depends(get_settings)) -> ReceiptVerifierService:
return ReceiptVerifierService(
coordinator_url=config.coordinator_base_url,
api_key=config.coordinator_api_key,
)
@lru_cache
def get_keystore(config: Settings = Depends(get_settings)) -> PersistentKeystoreService:
return PersistentKeystoreService(db_path=config.ledger_db_path.parent / "keystore.db")
def get_ledger(config: Settings = Depends(get_settings)) -> SQLiteLedgerAdapter:
return SQLiteLedgerAdapter(config.ledger_db_path)
# Temporarily disable multi-chain dependency functions
# @lru_cache
# def get_chain_manager() -> ChainManager:
# return chain_manager
# @lru_cache
# def get_multichain_ledger(chain_mgr: ChainManager = Depends(get_chain_manager)) -> MultiChainLedgerAdapter:
# return MultiChainLedgerAdapter(chain_mgr)
# @lru_cache
# def get_chain_aware_wallet_service(
# chain_mgr: ChainManager = Depends(get_chain_manager),
# multichain_ledger: MultiChainLedgerAdapter = Depends(get_multichain_ledger)
# ) -> ChainAwareWalletService:
# return ChainAwareWalletService(chain_mgr, multichain_ledger)

View File

@@ -0,0 +1,396 @@
"""
Persistent Keystore Service - Fixes data loss on restart
Replaces the in-memory-only keystore with database persistence
"""
from __future__ import annotations
import json
import sqlite3
import threading
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, Iterable, List, Optional
from secrets import token_bytes
from nacl.signing import SigningKey
from ..crypto.encryption import EncryptionSuite, EncryptionError
from ..security import validate_password_rules, wipe_buffer
@dataclass
class WalletRecord:
"""Wallet record with database persistence"""
wallet_id: str
public_key: str
salt: bytes
nonce: bytes
ciphertext: bytes
metadata: Dict[str, str]
created_at: str
updated_at: str
class PersistentKeystoreService:
"""Persistent keystore with database storage and proper encryption"""
def __init__(self, db_path: Optional[Path] = None, encryption: Optional[EncryptionSuite] = None) -> None:
self.db_path = db_path or Path("./data/keystore.db")
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._encryption = encryption or EncryptionSuite()
self._lock = threading.Lock()
self._init_database()
def _init_database(self):
"""Initialize database schema"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
conn.execute("""
CREATE TABLE IF NOT EXISTS wallets (
wallet_id TEXT PRIMARY KEY,
public_key TEXT NOT NULL,
salt BLOB NOT NULL,
nonce BLOB NOT NULL,
ciphertext BLOB NOT NULL,
metadata TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS wallet_access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_id TEXT NOT NULL,
action TEXT NOT NULL,
timestamp TEXT NOT NULL,
success INTEGER NOT NULL,
ip_address TEXT,
FOREIGN KEY (wallet_id) REFERENCES wallets (wallet_id)
)
""")
# Indexes for performance
conn.execute("CREATE INDEX IF NOT EXISTS idx_wallets_created_at ON wallets(created_at)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_access_log_wallet_id ON wallet_access_log(wallet_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_access_log_timestamp ON wallet_access_log(timestamp)")
conn.commit()
finally:
conn.close()
def list_wallets(self) -> List[str]:
"""List all wallet IDs"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("SELECT wallet_id FROM wallets ORDER BY created_at DESC")
return [row[0] for row in cursor.fetchall()]
finally:
conn.close()
def list_records(self) -> Iterable[WalletRecord]:
"""List all wallet records"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, salt, nonce, ciphertext, metadata, created_at, updated_at
FROM wallets
ORDER BY created_at DESC
""")
for row in cursor.fetchall():
metadata = json.loads(row[5])
yield WalletRecord(
wallet_id=row[0],
public_key=row[1],
salt=row[2],
nonce=row[3],
ciphertext=row[4],
metadata=metadata,
created_at=row[6],
updated_at=row[7]
)
finally:
conn.close()
def get_wallet(self, wallet_id: str) -> Optional[WalletRecord]:
"""Get wallet record by ID"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, salt, nonce, ciphertext, metadata, created_at, updated_at
FROM wallets
WHERE wallet_id = ?
""", (wallet_id,))
row = cursor.fetchone()
if row:
metadata = json.loads(row[5])
return WalletRecord(
wallet_id=row[0],
public_key=row[1],
salt=row[2],
nonce=row[3],
ciphertext=row[4],
metadata=metadata,
created_at=row[6],
updated_at=row[7]
)
return None
finally:
conn.close()
def create_wallet(
self,
wallet_id: str,
password: str,
secret: Optional[bytes] = None,
metadata: Optional[Dict[str, str]] = None,
ip_address: Optional[str] = None
) -> WalletRecord:
"""Create a new wallet with database persistence"""
with self._lock:
# Check if wallet already exists
if self.get_wallet(wallet_id):
raise ValueError("wallet already exists")
validate_password_rules(password)
metadata_map = {str(k): str(v) for k, v in (metadata or {}).items()}
if secret is None:
signing_key = SigningKey.generate()
secret_bytes = signing_key.encode()
else:
if len(secret) != SigningKey.seed_size:
raise ValueError("secret key must be 32 bytes")
secret_bytes = secret
signing_key = SigningKey(secret_bytes)
salt = token_bytes(self._encryption.salt_bytes)
nonce = token_bytes(self._encryption.nonce_bytes)
ciphertext = self._encryption.encrypt(password=password, plaintext=secret_bytes, salt=salt, nonce=nonce)
now = datetime.utcnow().isoformat()
conn = sqlite3.connect(self.db_path)
try:
conn.execute("""
INSERT INTO wallets (wallet_id, public_key, salt, nonce, ciphertext, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
wallet_id,
signing_key.verify_key.encode().hex(),
salt,
nonce,
ciphertext,
json.dumps(metadata_map),
now,
now
))
# Log creation
conn.execute("""
INSERT INTO wallet_access_log (wallet_id, action, timestamp, success, ip_address)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, "created", now, 1, ip_address))
conn.commit()
finally:
conn.close()
record = WalletRecord(
wallet_id=wallet_id,
public_key=signing_key.verify_key.encode().hex(),
salt=salt,
nonce=nonce,
ciphertext=ciphertext,
metadata=metadata_map,
created_at=now,
updated_at=now
)
return record
def unlock_wallet(self, wallet_id: str, password: str, ip_address: Optional[str] = None) -> bytes:
"""Unlock wallet and return secret key"""
record = self.get_wallet(wallet_id)
if record is None:
self._log_access(wallet_id, "unlock_failed", False, ip_address)
raise KeyError("wallet not found")
try:
secret = self._encryption.decrypt(password=password, ciphertext=record.ciphertext, salt=record.salt, nonce=record.nonce)
self._log_access(wallet_id, "unlock_success", True, ip_address)
return secret
except EncryptionError as exc:
self._log_access(wallet_id, "unlock_failed", False, ip_address)
raise ValueError("failed to decrypt wallet") from exc
def delete_wallet(self, wallet_id: str) -> bool:
"""Delete a wallet and all its access logs"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Delete access logs first
conn.execute("DELETE FROM wallet_access_log WHERE wallet_id = ?", (wallet_id,))
# Delete wallet
cursor = conn.execute("DELETE FROM wallets WHERE wallet_id = ?", (wallet_id,))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def sign_message(self, wallet_id: str, password: str, message: bytes, ip_address: Optional[str] = None) -> bytes:
"""Sign a message with wallet's private key"""
try:
secret_bytes = bytearray(self.unlock_wallet(wallet_id, password, ip_address))
try:
signing_key = SigningKey(bytes(secret_bytes))
signed = signing_key.sign(message)
self._log_access(wallet_id, "sign_success", True, ip_address)
return signed.signature
finally:
wipe_buffer(secret_bytes)
except (KeyError, ValueError) as exc:
self._log_access(wallet_id, "sign_failed", False, ip_address)
raise
def update_metadata(self, wallet_id: str, metadata: Dict[str, str]) -> bool:
"""Update wallet metadata"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
metadata_json = json.dumps(metadata)
cursor = conn.execute("""
UPDATE wallets
SET metadata = ?, updated_at = ?
WHERE wallet_id = ?
""", (metadata_json, now, wallet_id))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def _log_access(self, wallet_id: str, action: str, success: bool, ip_address: Optional[str] = None):
"""Log wallet access for audit trail"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
conn.execute("""
INSERT INTO wallet_access_log (wallet_id, action, timestamp, success, ip_address)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, action, now, int(success), ip_address))
conn.commit()
except Exception:
# Don't fail the main operation if logging fails
pass
finally:
conn.close()
def get_access_log(self, wallet_id: str, limit: int = 50) -> List[Dict]:
"""Get access log for a wallet"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT action, timestamp, success, ip_address
FROM wallet_access_log
WHERE wallet_id = ?
ORDER BY timestamp DESC
LIMIT ?
""", (wallet_id, limit))
return [
{
"action": row[0],
"timestamp": row[1],
"success": bool(row[2]),
"ip_address": row[3]
}
for row in cursor.fetchall()
]
finally:
conn.close()
def get_statistics(self) -> Dict[str, Any]:
"""Get keystore statistics"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Wallet count
wallet_count = conn.execute("SELECT COUNT(*) FROM wallets").fetchone()[0]
# Recent activity
recent_creations = conn.execute("""
SELECT COUNT(*) FROM wallets
WHERE created_at > datetime('now', '-24 hours')
""").fetchone()[0]
recent_access = conn.execute("""
SELECT COUNT(*) FROM wallet_access_log
WHERE timestamp > datetime('now', '-24 hours')
""").fetchone()[0]
# Access success rate
total_access = conn.execute("SELECT COUNT(*) FROM wallet_access_log").fetchone()[0]
successful_access = conn.execute("SELECT COUNT(*) FROM wallet_access_log WHERE success = 1").fetchone()[0]
success_rate = (successful_access / total_access * 100) if total_access > 0 else 0
return {
"total_wallets": wallet_count,
"created_last_24h": recent_creations,
"access_last_24h": recent_access,
"access_success_rate": round(success_rate, 2),
"database_path": str(self.db_path)
}
finally:
conn.close()
def backup_keystore(self, backup_path: Path) -> bool:
"""Create a backup of the keystore database"""
try:
with self._lock:
conn = sqlite3.connect(self.db_path)
backup_conn = sqlite3.connect(backup_path)
conn.backup(backup_conn)
conn.close()
backup_conn.close()
return True
except Exception:
return False
def verify_integrity(self) -> Dict[str, Any]:
"""Verify database integrity"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Run integrity check
result = conn.execute("PRAGMA integrity_check").fetchall()
# Check foreign key constraints
fk_check = conn.execute("PRAGMA foreign_key_check").fetchall()
return {
"integrity_check": result,
"foreign_key_check": fk_check,
"is_valid": len(result) == 1 and result[0][0] == "ok"
}
finally:
conn.close()
# Import datetime for the module
from datetime import datetime

View File

@@ -0,0 +1,96 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional
from secrets import token_bytes
from nacl.signing import SigningKey
from ..crypto.encryption import EncryptionSuite, EncryptionError
from ..security import validate_password_rules, wipe_buffer
@dataclass
class WalletRecord:
wallet_id: str
public_key: str
salt: bytes
nonce: bytes
ciphertext: bytes
metadata: Dict[str, str]
class KeystoreService:
"""In-memory keystore with Argon2id + XChaCha20-Poly1305 encryption."""
def __init__(self, encryption: Optional[EncryptionSuite] = None) -> None:
self._wallets: Dict[str, WalletRecord] = {}
self._encryption = encryption or EncryptionSuite()
def list_wallets(self) -> List[str]:
return list(self._wallets.keys())
def list_records(self) -> Iterable[WalletRecord]:
return list(self._wallets.values())
def get_wallet(self, wallet_id: str) -> Optional[WalletRecord]:
return self._wallets.get(wallet_id)
def create_wallet(
self,
wallet_id: str,
password: str,
secret: Optional[bytes] = None,
metadata: Optional[Dict[str, str]] = None,
) -> WalletRecord:
if wallet_id in self._wallets:
raise ValueError("wallet already exists")
validate_password_rules(password)
metadata_map = {str(k): str(v) for k, v in (metadata or {}).items()}
if secret is None:
signing_key = SigningKey.generate()
secret_bytes = signing_key.encode()
else:
if len(secret) != SigningKey.seed_size:
raise ValueError("secret key must be 32 bytes")
secret_bytes = secret
signing_key = SigningKey(secret_bytes)
salt = token_bytes(self._encryption.salt_bytes)
nonce = token_bytes(self._encryption.nonce_bytes)
ciphertext = self._encryption.encrypt(password=password, plaintext=secret_bytes, salt=salt, nonce=nonce)
record = WalletRecord(
wallet_id=wallet_id,
public_key=signing_key.verify_key.encode().hex(),
salt=salt,
nonce=nonce,
ciphertext=ciphertext,
metadata=metadata_map,
)
self._wallets[wallet_id] = record
return record
def unlock_wallet(self, wallet_id: str, password: str) -> bytes:
record = self._wallets.get(wallet_id)
if record is None:
raise KeyError("wallet not found")
try:
return self._encryption.decrypt(password=password, ciphertext=record.ciphertext, salt=record.salt, nonce=record.nonce)
except EncryptionError as exc:
raise ValueError("failed to decrypt wallet") from exc
def delete_wallet(self, wallet_id: str) -> bool:
return self._wallets.pop(wallet_id, None) is not None
def sign_message(self, wallet_id: str, password: str, message: bytes) -> bytes:
secret_bytes = bytearray(self.unlock_wallet(wallet_id, password))
try:
signing_key = SigningKey(bytes(secret_bytes))
signed = signing_key.sign(message)
return signed.signature
finally:
wipe_buffer(secret_bytes)

View File

@@ -0,0 +1,283 @@
"""
SQLite Ledger Adapter for Wallet Daemon
Production-ready ledger implementation (replacing missing mock)
"""
from __future__ import annotations
import json
import sqlite3
import threading
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
@dataclass
class LedgerRecord:
"""Ledger record for wallet events"""
wallet_id: str
event_type: str
timestamp: datetime
data: Dict[str, Any]
success: bool = True
@dataclass
class WalletMetadata:
"""Wallet metadata stored in ledger"""
wallet_id: str
public_key: str
metadata: Dict[str, str]
created_at: datetime
updated_at: datetime
class SQLiteLedgerAdapter:
"""Production-ready SQLite ledger adapter"""
def __init__(self, db_path: Optional[Path] = None):
self.db_path = db_path or Path("./data/wallet_ledger.db")
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
self._init_database()
def _init_database(self):
"""Initialize database schema"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Create wallet metadata table
conn.execute("""
CREATE TABLE IF NOT EXISTS wallet_metadata (
wallet_id TEXT PRIMARY KEY,
public_key TEXT NOT NULL,
metadata TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
# Create events table
conn.execute("""
CREATE TABLE IF NOT EXISTS wallet_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_id TEXT NOT NULL,
event_type TEXT NOT NULL,
timestamp TEXT NOT NULL,
data TEXT NOT NULL,
success INTEGER NOT NULL,
FOREIGN KEY (wallet_id) REFERENCES wallet_metadata (wallet_id)
)
""")
# Create indexes for performance
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_wallet_id ON wallet_events(wallet_id)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_timestamp ON wallet_events(timestamp)")
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_type ON wallet_events(event_type)")
conn.commit()
finally:
conn.close()
def upsert_wallet(self, wallet_id: str, public_key: str, metadata: Dict[str, str]) -> None:
"""Insert or update wallet metadata"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
metadata_json = json.dumps(metadata)
# Try update first
cursor = conn.execute("""
UPDATE wallet_metadata
SET public_key = ?, metadata = ?, updated_at = ?
WHERE wallet_id = ?
""", (public_key, metadata_json, now, wallet_id))
# If no rows updated, insert new
if cursor.rowcount == 0:
conn.execute("""
INSERT INTO wallet_metadata (wallet_id, public_key, metadata, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, public_key, metadata_json, now, now))
conn.commit()
finally:
conn.close()
def get_wallet(self, wallet_id: str) -> Optional[WalletMetadata]:
"""Get wallet metadata"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, metadata, created_at, updated_at
FROM wallet_metadata
WHERE wallet_id = ?
""", (wallet_id,))
row = cursor.fetchone()
if row:
metadata = json.loads(row[2])
return WalletMetadata(
wallet_id=row[0],
public_key=row[1],
metadata=metadata,
created_at=datetime.fromisoformat(row[3]),
updated_at=datetime.fromisoformat(row[4])
)
return None
finally:
conn.close()
def record_event(self, wallet_id: str, event_type: str, data: Dict[str, Any]) -> None:
"""Record a wallet event"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
now = datetime.utcnow().isoformat()
data_json = json.dumps(data)
success = data.get("success", True)
conn.execute("""
INSERT INTO wallet_events (wallet_id, event_type, timestamp, data, success)
VALUES (?, ?, ?, ?, ?)
""", (wallet_id, event_type, now, data_json, int(success)))
conn.commit()
finally:
conn.close()
def get_wallet_events(self, wallet_id: str, limit: int = 50) -> List[LedgerRecord]:
"""Get events for a wallet"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, event_type, timestamp, data, success
FROM wallet_events
WHERE wallet_id = ?
ORDER BY timestamp DESC
LIMIT ?
""", (wallet_id, limit))
events = []
for row in cursor.fetchall():
data = json.loads(row[3])
events.append(LedgerRecord(
wallet_id=row[0],
event_type=row[1],
timestamp=datetime.fromisoformat(row[2]),
data=data,
success=bool(row[4])
))
return events
finally:
conn.close()
def get_all_wallets(self) -> List[WalletMetadata]:
"""Get all wallets"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
cursor = conn.execute("""
SELECT wallet_id, public_key, metadata, created_at, updated_at
FROM wallet_metadata
ORDER BY created_at DESC
""")
wallets = []
for row in cursor.fetchall():
metadata = json.loads(row[2])
wallets.append(WalletMetadata(
wallet_id=row[0],
public_key=row[1],
metadata=metadata,
created_at=datetime.fromisoformat(row[3]),
updated_at=datetime.fromisoformat(row[4])
))
return wallets
finally:
conn.close()
def get_statistics(self) -> Dict[str, Any]:
"""Get ledger statistics"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Wallet count
wallet_count = conn.execute("SELECT COUNT(*) FROM wallet_metadata").fetchone()[0]
# Event counts by type
event_stats = conn.execute("""
SELECT event_type, COUNT(*) as count
FROM wallet_events
GROUP BY event_type
""").fetchall()
# Recent activity
recent_events = conn.execute("""
SELECT COUNT(*) FROM wallet_events
WHERE timestamp > datetime('now', '-24 hours')
""").fetchone()[0]
return {
"total_wallets": wallet_count,
"event_breakdown": dict(event_stats),
"events_last_24h": recent_events,
"database_path": str(self.db_path)
}
finally:
conn.close()
def delete_wallet(self, wallet_id: str) -> bool:
"""Delete a wallet and all its events"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Delete events first (foreign key constraint)
conn.execute("DELETE FROM wallet_events WHERE wallet_id = ?", (wallet_id,))
# Delete wallet metadata
cursor = conn.execute("DELETE FROM wallet_metadata WHERE wallet_id = ?", (wallet_id,))
conn.commit()
return cursor.rowcount > 0
finally:
conn.close()
def backup_ledger(self, backup_path: Path) -> bool:
"""Create a backup of the ledger database"""
try:
with self._lock:
conn = sqlite3.connect(self.db_path)
backup_conn = sqlite3.connect(backup_path)
conn.backup(backup_conn)
conn.close()
backup_conn.close()
return True
except Exception:
return False
def verify_integrity(self) -> Dict[str, Any]:
"""Verify database integrity"""
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
# Run integrity check
result = conn.execute("PRAGMA integrity_check").fetchall()
# Check foreign key constraints
fk_check = conn.execute("PRAGMA foreign_key_check").fetchall()
return {
"integrity_check": result,
"foreign_key_check": fk_check,
"is_valid": len(result) == 1 and result[0][0] == "ok"
}
finally:
conn.close()

View File

@@ -0,0 +1,3 @@
from .sqlite_adapter import SQLiteLedgerAdapter, WalletRecord, WalletEvent
__all__ = ["SQLiteLedgerAdapter", "WalletRecord", "WalletEvent"]

View File

@@ -0,0 +1,197 @@
"""PostgreSQL adapter for Wallet Daemon"""
import psycopg2
from psycopg2.extras import RealDictCursor
from typing import Optional, Dict, Any, List
from datetime import datetime
import json
from aitbc.logging import get_logger
logger = get_logger(__name__)
class PostgreSQLLedgerAdapter:
"""PostgreSQL implementation of the wallet ledger"""
def __init__(self, db_config: Dict[str, Any]):
self.db_config = db_config
self.connection = None
self._connect()
def _connect(self):
"""Establish database connection"""
try:
self.connection = psycopg2.connect(
cursor_factory=RealDictCursor,
**self.db_config
)
logger.info("Connected to PostgreSQL wallet ledger")
except Exception as e:
logger.error(f"Failed to connect to PostgreSQL: {e}")
raise
def create_wallet(self, wallet_id: str, public_key: str, metadata: Dict[str, Any] = None) -> bool:
"""Create a new wallet"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
INSERT INTO wallets (wallet_id, public_key, metadata)
VALUES (%s, %s, %s)
ON CONFLICT (wallet_id) DO UPDATE
SET public_key = EXCLUDED.public_key,
metadata = EXCLUDED.metadata,
updated_at = NOW()
""", (wallet_id, public_key, json.dumps(metadata or {})))
self.connection.commit()
logger.info(f"Created wallet: {wallet_id}")
return True
except Exception as e:
logger.error(f"Failed to create wallet {wallet_id}: {e}")
self.connection.rollback()
return False
def get_wallet(self, wallet_id: str) -> Optional[Dict[str, Any]]:
"""Get wallet information"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
SELECT wallet_id, public_key, metadata, created_at, updated_at
FROM wallets
WHERE wallet_id = %s
""", (wallet_id,))
result = cursor.fetchone()
if result:
return dict(result)
return None
except Exception as e:
logger.error(f"Failed to get wallet {wallet_id}: {e}")
return None
def list_wallets(self, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
"""List all wallets"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
SELECT wallet_id, public_key, metadata, created_at, updated_at
FROM wallets
ORDER BY created_at DESC
LIMIT %s OFFSET %s
""", (limit, offset))
return [dict(row) for row in cursor.fetchall()]
except Exception as e:
logger.error(f"Failed to list wallets: {e}")
return []
def add_wallet_event(self, wallet_id: str, event_type: str, payload: Dict[str, Any]) -> bool:
"""Add an event to the wallet"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
INSERT INTO wallet_events (wallet_id, event_type, payload)
VALUES (%s, %s, %s)
""", (wallet_id, event_type, json.dumps(payload)))
self.connection.commit()
logger.debug(f"Added event {event_type} to wallet {wallet_id}")
return True
except Exception as e:
logger.error(f"Failed to add event to wallet {wallet_id}: {e}")
self.connection.rollback()
return False
def get_wallet_events(self, wallet_id: str, limit: int = 100) -> List[Dict[str, Any]]:
"""Get events for a wallet"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
SELECT id, event_type, payload, created_at
FROM wallet_events
WHERE wallet_id = %s
ORDER BY created_at DESC
LIMIT %s
""", (wallet_id, limit))
return [dict(row) for row in cursor.fetchall()]
except Exception as e:
logger.error(f"Failed to get events for wallet {wallet_id}: {e}")
return []
def update_wallet_metadata(self, wallet_id: str, metadata: Dict[str, Any]) -> bool:
"""Update wallet metadata"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
UPDATE wallets
SET metadata = %s, updated_at = NOW()
WHERE wallet_id = %s
""", (json.dumps(metadata), wallet_id))
self.connection.commit()
return cursor.rowcount > 0
except Exception as e:
logger.error(f"Failed to update metadata for wallet {wallet_id}: {e}")
self.connection.rollback()
return False
def delete_wallet(self, wallet_id: str) -> bool:
"""Delete a wallet and all its events"""
try:
with self.connection.cursor() as cursor:
cursor.execute("""
DELETE FROM wallets
WHERE wallet_id = %s
""", (wallet_id,))
self.connection.commit()
return cursor.rowcount > 0
except Exception as e:
logger.error(f"Failed to delete wallet {wallet_id}: {e}")
self.connection.rollback()
return False
def get_wallet_stats(self) -> Dict[str, Any]:
"""Get wallet statistics"""
try:
with self.connection.cursor() as cursor:
cursor.execute("SELECT COUNT(*) as total_wallets FROM wallets")
total_wallets = cursor.fetchone()['total_wallets']
cursor.execute("SELECT COUNT(*) as total_events FROM wallet_events")
total_events = cursor.fetchone()['total_events']
cursor.execute("""
SELECT event_type, COUNT(*) as count
FROM wallet_events
GROUP BY event_type
ORDER BY count DESC
""")
event_types = {row['event_type']: row['count'] for row in cursor.fetchall()}
return {
"total_wallets": total_wallets,
"total_events": total_events,
"event_types": event_types
}
except Exception as e:
logger.error(f"Failed to get wallet stats: {e}")
return {}
def close(self):
"""Close the database connection"""
if self.connection:
self.connection.close()
logger.info("PostgreSQL connection closed")
# Factory function
def create_postgresql_adapter() -> PostgreSQLLedgerAdapter:
"""Create a PostgreSQL ledger adapter"""
config = {
"host": "localhost",
"database": "aitbc_wallet",
"user": "aitbc_user",
"password": "aitbc_password",
"port": 5432
}
return PostgreSQLLedgerAdapter(config)

View File

@@ -0,0 +1,106 @@
from __future__ import annotations
import json
import sqlite3
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional
@dataclass
class WalletRecord:
wallet_id: str
public_key: str
metadata: dict
@dataclass
class WalletEvent:
wallet_id: str
event_type: str
payload: dict
class SQLiteLedgerAdapter:
def __init__(self, db_path: Path) -> None:
self._db_path = db_path
self._ensure_schema()
def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row
return conn
def _ensure_schema(self) -> None:
self._db_path.parent.mkdir(parents=True, exist_ok=True)
with self._connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS wallets (
wallet_id TEXT PRIMARY KEY,
public_key TEXT NOT NULL,
metadata TEXT NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS wallet_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(wallet_id) REFERENCES wallets(wallet_id)
)
"""
)
def upsert_wallet(self, wallet_id: str, public_key: str, metadata: dict) -> None:
payload = json.dumps(metadata)
with self._connect() as conn:
conn.execute(
"""
INSERT INTO wallets(wallet_id, public_key, metadata)
VALUES (?, ?, ?)
ON CONFLICT(wallet_id) DO UPDATE SET public_key=excluded.public_key, metadata=excluded.metadata
""",
(wallet_id, public_key, payload),
)
def get_wallet(self, wallet_id: str) -> Optional[WalletRecord]:
with self._connect() as conn:
row = conn.execute(
"SELECT wallet_id, public_key, metadata FROM wallets WHERE wallet_id = ?",
(wallet_id,),
).fetchone()
if row is None:
return None
return WalletRecord(wallet_id=row["wallet_id"], public_key=row["public_key"], metadata=json.loads(row["metadata"]))
def list_wallets(self) -> Iterable[WalletRecord]:
with self._connect() as conn:
rows = conn.execute("SELECT wallet_id, public_key, metadata FROM wallets").fetchall()
for row in rows:
yield WalletRecord(wallet_id=row["wallet_id"], public_key=row["public_key"], metadata=json.loads(row["metadata"]))
def record_event(self, wallet_id: str, event_type: str, payload: dict) -> None:
data = json.dumps(payload)
with self._connect() as conn:
conn.execute(
"INSERT INTO wallet_events(wallet_id, event_type, payload) VALUES (?, ?, ?)",
(wallet_id, event_type, data),
)
def list_events(self, wallet_id: str) -> Iterable[WalletEvent]:
with self._connect() as conn:
rows = conn.execute(
"SELECT wallet_id, event_type, payload FROM wallet_events WHERE wallet_id = ? ORDER BY id",
(wallet_id,),
).fetchall()
for row in rows:
yield WalletEvent(
wallet_id=row["wallet_id"],
event_type=row["event_type"],
payload=json.loads(row["payload"]),
)

View File

@@ -0,0 +1,27 @@
from __future__ import annotations
from fastapi import FastAPI
from .api_jsonrpc import router as jsonrpc_router
from .api_rest import router as receipts_router
from .settings import settings
def create_app() -> FastAPI:
app = FastAPI(title=settings.app_name, debug=settings.debug)
app.include_router(receipts_router)
app.include_router(jsonrpc_router)
# Add health check endpoint
@app.get("/health")
async def health_check():
return {
"status": "ok",
"env": "dev",
"python_version": "3.13.5"
}
return app
app = create_app()

View File

@@ -0,0 +1,134 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional
from aitbc_sdk import SignatureValidation
from pydantic import BaseModel
class SignatureValidationModel(BaseModel):
key_id: str
alg: str = "Ed25519"
valid: bool
class ReceiptVerificationModel(BaseModel):
job_id: str
receipt_id: str
miner_signature: SignatureValidationModel
coordinator_attestations: List[SignatureValidationModel]
all_valid: bool
class ReceiptVerifyResponse(BaseModel):
result: ReceiptVerificationModel
def _signature_to_model(sig: SignatureValidation | SignatureValidationModel) -> SignatureValidationModel:
if isinstance(sig, SignatureValidationModel):
return sig
return SignatureValidationModel(key_id=sig.key_id, alg=sig.algorithm, valid=sig.valid)
def from_validation_result(result) -> ReceiptVerificationModel:
return ReceiptVerificationModel(
job_id=result.job_id,
receipt_id=result.receipt_id,
miner_signature=_signature_to_model(result.miner_signature),
coordinator_attestations=[_signature_to_model(att) for att in result.coordinator_attestations],
all_valid=result.all_valid,
)
class ReceiptVerificationListResponse(BaseModel):
items: List[ReceiptVerificationModel]
class WalletDescriptor(BaseModel):
wallet_id: str
chain_id: str
public_key: str
address: Optional[str]
metadata: Dict[str, Any]
class WalletListResponse(BaseModel):
items: List[WalletDescriptor]
class WalletCreateRequest(BaseModel):
chain_id: str
wallet_id: str
password: str
metadata: Dict[str, Any] = {}
secret_key: Optional[str] = None
class WalletCreateResponse(BaseModel):
wallet: WalletDescriptor
class WalletUnlockRequest(BaseModel):
password: str
class WalletUnlockResponse(BaseModel):
wallet_id: str
chain_id: str
unlocked: bool
class WalletSignRequest(BaseModel):
password: str
message_base64: str
class WalletSignResponse(BaseModel):
wallet_id: str
chain_id: str
signature_base64: str
class ChainInfo(BaseModel):
chain_id: str
name: str
status: str
coordinator_url: str
created_at: str
updated_at: str
wallet_count: int
recent_activity: int
class ChainListResponse(BaseModel):
chains: List[ChainInfo]
total_chains: int
active_chains: int
class ChainCreateRequest(BaseModel):
chain_id: str
name: str
coordinator_url: str
coordinator_api_key: str
metadata: Dict[str, Any] = {}
class ChainCreateResponse(BaseModel):
chain: ChainInfo
class WalletMigrationRequest(BaseModel):
source_chain_id: str
target_chain_id: str
wallet_id: str
password: str
new_password: Optional[str] = None
class WalletMigrationResponse(BaseModel):
success: bool
source_wallet: WalletDescriptor
target_wallet: WalletDescriptor
migration_timestamp: str

View File

@@ -0,0 +1,5 @@
"""Receipt verification helpers for the wallet daemon."""
from .service import ReceiptValidationResult, ReceiptVerifierService
__all__ = ["ReceiptValidationResult", "ReceiptVerifierService"]

View File

@@ -0,0 +1,58 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
from aitbc_sdk import (
CoordinatorReceiptClient,
ReceiptVerification,
SignatureValidation,
verify_receipt,
verify_receipts,
)
@dataclass
class ReceiptValidationResult:
job_id: str
receipt_id: str
receipt: dict
miner_signature: SignatureValidation
coordinator_attestations: List[SignatureValidation]
@property
def miner_valid(self) -> bool:
return self.miner_signature.valid
@property
def all_valid(self) -> bool:
return self.miner_signature.valid and all(att.valid for att in self.coordinator_attestations)
class ReceiptVerifierService:
"""Wraps `aitbc_sdk` receipt verification for wallet daemon workflows."""
def __init__(self, coordinator_url: str, api_key: str, timeout: float = 10.0) -> None:
self.client = CoordinatorReceiptClient(coordinator_url, api_key, timeout=timeout)
def verify_latest(self, job_id: str) -> Optional[ReceiptValidationResult]:
receipt = self.client.fetch_latest(job_id)
if receipt is None:
return None
verification = verify_receipt(receipt)
return self._to_result(verification)
def verify_history(self, job_id: str) -> List[ReceiptValidationResult]:
receipts = self.client.fetch_history(job_id)
verifications = verify_receipts(receipts)
return [self._to_result(item) for item in verifications]
@staticmethod
def _to_result(verification: ReceiptVerification) -> ReceiptValidationResult:
return ReceiptValidationResult(
job_id=str(verification.receipt.get("job_id")),
receipt_id=str(verification.receipt.get("receipt_id")),
receipt=verification.receipt,
miner_signature=verification.miner_signature,
coordinator_attestations=list(verification.coordinator_attestations),
)

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
import re
import threading
import time
from collections import defaultdict, deque
class RateLimiter:
def __init__(self, max_requests: int = 30, window_seconds: int = 60) -> None:
self._max_requests = max_requests
self._window_seconds = window_seconds
self._lock = threading.Lock()
self._records: dict[str, deque[float]] = defaultdict(deque)
def allow(self, key: str) -> bool:
now = time.monotonic()
with self._lock:
entries = self._records[key]
while entries and now - entries[0] > self._window_seconds:
entries.popleft()
if len(entries) >= self._max_requests:
return False
entries.append(now)
return True
def validate_password_rules(password: str) -> None:
if len(password) < 12:
raise ValueError("password must be at least 12 characters long")
if not re.search(r"[A-Z]", password):
raise ValueError("password must include at least one uppercase letter")
if not re.search(r"[a-z]", password):
raise ValueError("password must include at least one lowercase letter")
if not re.search(r"\d", password):
raise ValueError("password must include at least one digit")
if not re.search(r"[^A-Za-z0-9]", password):
raise ValueError("password must include at least one symbol")
def wipe_buffer(buffer: bytearray) -> None:
for index in range(len(buffer)):
buffer[index] = 0

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from pathlib import Path
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Runtime configuration for the wallet daemon service."""
app_name: str = Field(default="AITBC Wallet Daemon")
debug: bool = Field(default=False)
coordinator_base_url: str = Field(default="http://localhost:8011", alias="COORDINATOR_BASE_URL")
coordinator_api_key: str = Field(..., alias="COORDINATOR_API_KEY")
rest_prefix: str = Field(default="/v1", alias="REST_PREFIX")
ledger_db_path: Path = Field(default=Path("./data/wallet_ledger.db"), alias="LEDGER_DB_PATH")
@field_validator('coordinator_api_key')
@classmethod
def validate_api_key(cls, v: str) -> str:
if v.startswith('$') or not v or v == 'your_api_key_here':
raise ValueError('COORDINATOR_API_KEY must be set to a valid value and cannot be a template placeholder')
return v
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()