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:
5
apps/wallet/src/app/__init__.py
Normal file
5
apps/wallet/src/app/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Wallet daemon FastAPI application package."""
|
||||
|
||||
from .main import create_app
|
||||
|
||||
__all__ = ["create_app"]
|
||||
67
apps/wallet/src/app/__main__.py
Normal file
67
apps/wallet/src/app/__main__.py
Normal 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()
|
||||
118
apps/wallet/src/app/api_jsonrpc.py
Normal file
118
apps/wallet/src/app/api_jsonrpc.py
Normal 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)
|
||||
439
apps/wallet/src/app/api_rest.py
Normal file
439
apps/wallet/src/app/api_rest.py
Normal 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()
|
||||
)
|
||||
22
apps/wallet/src/app/chain/__init__.py
Normal file
22
apps/wallet/src/app/chain/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Multi-Chain Support Module for Wallet Daemon
|
||||
|
||||
This module provides multi-chain capabilities for the wallet daemon,
|
||||
including chain management, chain-specific storage, and chain-aware
|
||||
wallet operations.
|
||||
"""
|
||||
|
||||
from .manager import ChainManager, ChainConfig, ChainStatus, chain_manager
|
||||
from .multichain_ledger import MultiChainLedgerAdapter, ChainLedgerRecord, ChainWalletMetadata
|
||||
from .chain_aware_wallet_service import ChainAwareWalletService
|
||||
|
||||
__all__ = [
|
||||
"ChainManager",
|
||||
"ChainConfig",
|
||||
"ChainStatus",
|
||||
"chain_manager",
|
||||
"MultiChainLedgerAdapter",
|
||||
"ChainLedgerRecord",
|
||||
"ChainWalletMetadata",
|
||||
"ChainAwareWalletService"
|
||||
]
|
||||
414
apps/wallet/src/app/chain/chain_aware_wallet_service.py
Normal file
414
apps/wallet/src/app/chain/chain_aware_wallet_service.py
Normal file
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
Chain-Aware Wallet Service for Wallet Daemon
|
||||
|
||||
Multi-chain wallet operations with proper chain context,
|
||||
isolation, and management across different blockchain networks.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .manager import ChainManager, ChainConfig, ChainStatus
|
||||
from .multichain_ledger import MultiChainLedgerAdapter, ChainWalletMetadata
|
||||
from ..keystore.persistent_service import PersistentKeystoreService
|
||||
from ..security import wipe_buffer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChainAwareWalletService:
|
||||
"""Chain-aware wallet service with multi-chain support"""
|
||||
|
||||
def __init__(self, chain_manager: ChainManager, multichain_ledger: MultiChainLedgerAdapter):
|
||||
self.chain_manager = chain_manager
|
||||
self.multichain_ledger = multichain_ledger
|
||||
|
||||
# Chain-specific keystores
|
||||
self.chain_keystores: Dict[str, PersistentKeystoreService] = {}
|
||||
self._initialize_chain_keystores()
|
||||
|
||||
def _initialize_chain_keystores(self):
|
||||
"""Initialize keystore for each chain"""
|
||||
for chain in self.chain_manager.list_chains():
|
||||
self._init_chain_keystore(chain.chain_id)
|
||||
|
||||
def _init_chain_keystore(self, chain_id: str):
|
||||
"""Initialize keystore for a specific chain"""
|
||||
try:
|
||||
chain = self.chain_manager.get_chain(chain_id)
|
||||
if not chain:
|
||||
return
|
||||
|
||||
keystore_path = chain.keystore_path or f"./data/keystore_{chain_id}"
|
||||
keystore = PersistentKeystoreService(keystore_path)
|
||||
self.chain_keystores[chain_id] = keystore
|
||||
|
||||
logger.info(f"Initialized keystore for chain: {chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize keystore for chain {chain_id}: {e}")
|
||||
|
||||
def _get_keystore(self, chain_id: str) -> Optional[PersistentKeystoreService]:
|
||||
"""Get keystore for a specific chain"""
|
||||
if chain_id not in self.chain_keystores:
|
||||
self._init_chain_keystore(chain_id)
|
||||
|
||||
return self.chain_keystores.get(chain_id)
|
||||
|
||||
def create_wallet(self, chain_id: str, wallet_id: str, password: str,
|
||||
secret_key: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> Optional[ChainWalletMetadata]:
|
||||
"""Create a wallet in a specific chain"""
|
||||
try:
|
||||
# Validate chain
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
logger.error(f"Invalid or inactive chain: {chain_id}")
|
||||
return None
|
||||
|
||||
# Get keystore for chain
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
logger.error(f"Failed to get keystore for chain: {chain_id}")
|
||||
return None
|
||||
|
||||
# Create wallet in keystore
|
||||
keystore_record = keystore.create_wallet(wallet_id, password, secret_key, metadata or {})
|
||||
|
||||
# Create wallet in ledger
|
||||
success = self.multichain_ledger.create_wallet(
|
||||
chain_id, wallet_id, keystore_record.public_key,
|
||||
metadata=keystore_record.metadata
|
||||
)
|
||||
|
||||
if not success:
|
||||
# Rollback keystore creation
|
||||
try:
|
||||
keystore.delete_wallet(wallet_id, password)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Get wallet metadata
|
||||
wallet_metadata = self.multichain_ledger.get_wallet(chain_id, wallet_id)
|
||||
|
||||
# Record creation event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "created", {
|
||||
"public_key": keystore_record.public_key,
|
||||
"chain_id": chain_id,
|
||||
"metadata": metadata or {}
|
||||
})
|
||||
|
||||
logger.info(f"Created wallet {wallet_id} in chain {chain_id}")
|
||||
return wallet_metadata
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_wallet(self, chain_id: str, wallet_id: str) -> Optional[ChainWalletMetadata]:
|
||||
"""Get wallet metadata from a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return None
|
||||
|
||||
return self.multichain_ledger.get_wallet(chain_id, wallet_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get wallet {wallet_id} from chain {chain_id}: {e}")
|
||||
return None
|
||||
|
||||
def list_wallets(self, chain_id: Optional[str] = None) -> List[ChainWalletMetadata]:
|
||||
"""List wallets from a specific chain or all chains"""
|
||||
try:
|
||||
if chain_id:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return []
|
||||
return self.multichain_ledger.list_wallets(chain_id)
|
||||
else:
|
||||
# List from all active chains
|
||||
all_wallets = []
|
||||
for chain in self.chain_manager.get_active_chains():
|
||||
chain_wallets = self.multichain_ledger.list_wallets(chain.chain_id)
|
||||
all_wallets.extend(chain_wallets)
|
||||
return all_wallets
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list wallets: {e}")
|
||||
return []
|
||||
|
||||
def delete_wallet(self, chain_id: str, wallet_id: str, password: str) -> bool:
|
||||
"""Delete a wallet from a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return False
|
||||
|
||||
# Delete from keystore
|
||||
keystore_success = keystore.delete_wallet(wallet_id, password)
|
||||
if not keystore_success:
|
||||
return False
|
||||
|
||||
# Record deletion event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "deleted", {
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
# Note: We keep the wallet metadata in ledger for audit purposes
|
||||
logger.info(f"Deleted wallet {wallet_id} from chain {chain_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete wallet {wallet_id} from chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def sign_message(self, chain_id: str, wallet_id: str, password: str, message: bytes,
|
||||
ip_address: Optional[str] = None) -> Optional[str]:
|
||||
"""Sign a message with wallet private key in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return None
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return None
|
||||
|
||||
# Sign message
|
||||
signature = keystore.sign_message(wallet_id, password, message, ip_address)
|
||||
|
||||
if signature:
|
||||
# Record signing event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "signed", {
|
||||
"message_length": len(message),
|
||||
"ip_address": ip_address,
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
logger.info(f"Signed message for wallet {wallet_id} in chain {chain_id}")
|
||||
|
||||
return signature
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sign message for wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return None
|
||||
|
||||
def unlock_wallet(self, chain_id: str, wallet_id: str, password: str) -> bool:
|
||||
"""Unlock a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return False
|
||||
|
||||
# Unlock wallet
|
||||
success = keystore.unlock_wallet(wallet_id, password)
|
||||
|
||||
if success:
|
||||
# Record unlock event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "unlocked", {
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
logger.info(f"Unlocked wallet {wallet_id} in chain {chain_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to unlock wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def lock_wallet(self, chain_id: str, wallet_id: str) -> bool:
|
||||
"""Lock a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
# Get keystore
|
||||
keystore = self._get_keystore(chain_id)
|
||||
if not keystore:
|
||||
return False
|
||||
|
||||
# Lock wallet
|
||||
success = keystore.lock_wallet(wallet_id)
|
||||
|
||||
if success:
|
||||
# Record lock event
|
||||
self.multichain_ledger.record_event(chain_id, wallet_id, "locked", {
|
||||
"chain_id": chain_id
|
||||
})
|
||||
|
||||
logger.info(f"Locked wallet {wallet_id} in chain {chain_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to lock wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_wallet_events(self, chain_id: str, wallet_id: str,
|
||||
event_type: Optional[str] = None, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""Get events for a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return []
|
||||
|
||||
events = self.multichain_ledger.get_wallet_events(chain_id, wallet_id, event_type, limit)
|
||||
|
||||
return [
|
||||
{
|
||||
"chain_id": event.chain_id,
|
||||
"wallet_id": event.wallet_id,
|
||||
"event_type": event.event_type,
|
||||
"timestamp": event.timestamp.isoformat(),
|
||||
"data": event.data,
|
||||
"success": event.success
|
||||
}
|
||||
for event in events
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get events for wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_chain_wallet_stats(self, chain_id: str) -> Dict[str, Any]:
|
||||
"""Get wallet statistics for a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return {}
|
||||
|
||||
# Get ledger stats
|
||||
ledger_stats = self.multichain_ledger.get_chain_stats(chain_id)
|
||||
|
||||
# Get keystore stats
|
||||
keystore = self._get_keystore(chain_id)
|
||||
keystore_stats = {}
|
||||
if keystore:
|
||||
keystore_stats = {
|
||||
"total_wallets": len(keystore.list_wallets()),
|
||||
"unlocked_wallets": len([w for w in keystore.list_wallets() if w.get("unlocked", False)])
|
||||
}
|
||||
|
||||
return {
|
||||
"chain_id": chain_id,
|
||||
"ledger_stats": ledger_stats,
|
||||
"keystore_stats": keystore_stats
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get stats for chain {chain_id}: {e}")
|
||||
return {}
|
||||
|
||||
def get_all_chain_wallet_stats(self) -> Dict[str, Any]:
|
||||
"""Get wallet statistics for all chains"""
|
||||
stats = {
|
||||
"total_chains": 0,
|
||||
"total_wallets": 0,
|
||||
"chain_stats": {}
|
||||
}
|
||||
|
||||
for chain in self.chain_manager.get_active_chains():
|
||||
chain_stats = self.get_chain_wallet_stats(chain.chain_id)
|
||||
if chain_stats:
|
||||
stats["chain_stats"][chain.chain_id] = chain_stats
|
||||
stats["total_wallets"] += chain_stats.get("ledger_stats", {}).get("wallet_count", 0)
|
||||
stats["total_chains"] += 1
|
||||
|
||||
return stats
|
||||
|
||||
def migrate_wallet_between_chains(self, source_chain_id: str, target_chain_id: str,
|
||||
wallet_id: str, password: str, new_password: Optional[str] = None) -> bool:
|
||||
"""Migrate a wallet from one chain to another"""
|
||||
try:
|
||||
# Validate both chains
|
||||
if not self.chain_manager.validate_chain_id(source_chain_id):
|
||||
logger.error(f"Invalid source chain: {source_chain_id}")
|
||||
return False
|
||||
|
||||
if not self.chain_manager.validate_chain_id(target_chain_id):
|
||||
logger.error(f"Invalid target chain: {target_chain_id}")
|
||||
return False
|
||||
|
||||
# Get source wallet
|
||||
source_wallet = self.get_wallet(source_chain_id, wallet_id)
|
||||
if not source_wallet:
|
||||
logger.error(f"Wallet {wallet_id} not found in source chain {source_chain_id}")
|
||||
return False
|
||||
|
||||
# Check if wallet already exists in target chain
|
||||
target_wallet = self.get_wallet(target_chain_id, wallet_id)
|
||||
if target_wallet:
|
||||
logger.error(f"Wallet {wallet_id} already exists in target chain {target_chain_id}")
|
||||
return False
|
||||
|
||||
# Get source keystore
|
||||
source_keystore = self._get_keystore(source_chain_id)
|
||||
target_keystore = self._get_keystore(target_chain_id)
|
||||
|
||||
if not source_keystore or not target_keystore:
|
||||
logger.error("Failed to get keystores for migration")
|
||||
return False
|
||||
|
||||
# Export wallet from source chain
|
||||
try:
|
||||
# This would require adding export/import methods to keystore
|
||||
# For now, we'll create a new wallet with the same keys
|
||||
source_keystore_record = source_keystore.get_wallet(wallet_id)
|
||||
if not source_keystore_record:
|
||||
logger.error("Failed to get source wallet record")
|
||||
return False
|
||||
|
||||
# Create wallet in target chain with same keys
|
||||
target_wallet = self.create_wallet(
|
||||
target_chain_id, wallet_id, new_password or password,
|
||||
source_keystore_record.get("secret_key"), source_wallet.metadata
|
||||
)
|
||||
|
||||
if target_wallet:
|
||||
# Record migration events
|
||||
self.multichain_ledger.record_event(source_chain_id, wallet_id, "migrated_from", {
|
||||
"target_chain": target_chain_id,
|
||||
"migration_timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
self.multichain_ledger.record_event(target_chain_id, wallet_id, "migrated_to", {
|
||||
"source_chain": source_chain_id,
|
||||
"migration_timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
logger.info(f"Migrated wallet {wallet_id} from {source_chain_id} to {target_chain_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to create wallet in target chain")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to migrate wallet {wallet_id}: {e}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Wallet migration failed: {e}")
|
||||
return False
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup resources"""
|
||||
try:
|
||||
# Close all keystore connections
|
||||
for chain_id, keystore in self.chain_keystores.items():
|
||||
try:
|
||||
keystore.close()
|
||||
logger.info(f"Closed keystore for chain: {chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close keystore for chain {chain_id}: {e}")
|
||||
|
||||
self.chain_keystores.clear()
|
||||
|
||||
# Close ledger connections
|
||||
self.multichain_ledger.close_all_connections()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup wallet service: {e}")
|
||||
273
apps/wallet/src/app/chain/manager.py
Normal file
273
apps/wallet/src/app/chain/manager.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
Multi-Chain Manager for Wallet Daemon
|
||||
|
||||
Central management for multiple blockchain networks, providing
|
||||
chain context, routing, and isolation for wallet operations.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChainStatus(Enum):
|
||||
"""Chain operational status"""
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
MAINTENANCE = "maintenance"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChainConfig:
|
||||
"""Configuration for a specific blockchain network"""
|
||||
chain_id: str
|
||||
name: str
|
||||
coordinator_url: str
|
||||
coordinator_api_key: str
|
||||
status: ChainStatus = ChainStatus.ACTIVE
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Chain-specific settings
|
||||
default_gas_limit: int = 10000000
|
||||
default_gas_price: int = 20000000000
|
||||
transaction_timeout: int = 300
|
||||
max_retries: int = 3
|
||||
|
||||
# Storage configuration
|
||||
ledger_db_path: Optional[str] = None
|
||||
keystore_path: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for serialization"""
|
||||
return {
|
||||
"chain_id": self.chain_id,
|
||||
"name": self.name,
|
||||
"coordinator_url": self.coordinator_url,
|
||||
"coordinator_api_key": self.coordinator_api_key,
|
||||
"status": self.status.value,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"metadata": self.metadata,
|
||||
"default_gas_limit": self.default_gas_limit,
|
||||
"default_gas_price": self.default_gas_price,
|
||||
"transaction_timeout": self.transaction_timeout,
|
||||
"max_retries": self.max_retries,
|
||||
"ledger_db_path": self.ledger_db_path,
|
||||
"keystore_path": self.keystore_path
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ChainConfig":
|
||||
"""Create from dictionary"""
|
||||
# Ensure data is a dict and make a copy
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Expected dict, got {type(data)}")
|
||||
|
||||
data = data.copy()
|
||||
data["status"] = ChainStatus(data["status"])
|
||||
data["created_at"] = datetime.fromisoformat(data["created_at"])
|
||||
data["updated_at"] = datetime.fromisoformat(data["updated_at"])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class ChainManager:
|
||||
"""Central manager for multi-chain operations"""
|
||||
|
||||
def __init__(self, config_path: Optional[Path] = None):
|
||||
self.config_path = config_path or Path("./data/chains.json")
|
||||
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.chains: Dict[str, ChainConfig] = {}
|
||||
self.default_chain_id: Optional[str] = None
|
||||
self._load_chains()
|
||||
|
||||
def _load_chains(self):
|
||||
"""Load chain configurations from file"""
|
||||
try:
|
||||
if self.config_path.exists():
|
||||
with open(self.config_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
for chain_data in data.get("chains", []):
|
||||
chain = ChainConfig.from_dict(chain_data)
|
||||
self.chains[chain.chain_id] = chain
|
||||
|
||||
self.default_chain_id = data.get("default_chain_id")
|
||||
logger.info(f"Loaded {len(self.chains)} chain configurations")
|
||||
else:
|
||||
# Create default chain configuration
|
||||
self._create_default_chain()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load chain configurations: {e}")
|
||||
self._create_default_chain()
|
||||
|
||||
def _create_default_chain(self):
|
||||
"""Create default chain configuration"""
|
||||
default_chain = ChainConfig(
|
||||
chain_id="ait-devnet",
|
||||
name="AITBC Development Network",
|
||||
coordinator_url="http://localhost:8011",
|
||||
coordinator_api_key="dev-coordinator-key",
|
||||
ledger_db_path="./data/wallet_ledger_devnet.db",
|
||||
keystore_path="./data/keystore_devnet"
|
||||
)
|
||||
|
||||
self.chains[default_chain.chain_id] = default_chain
|
||||
self.default_chain_id = default_chain.chain_id
|
||||
self._save_chains()
|
||||
logger.info(f"Created default chain: {default_chain.chain_id}")
|
||||
|
||||
def _save_chains(self):
|
||||
"""Save chain configurations to file"""
|
||||
try:
|
||||
data = {
|
||||
"chains": [chain.to_dict() for chain in self.chains.values()],
|
||||
"default_chain_id": self.default_chain_id,
|
||||
"updated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(self.config_path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
logger.info(f"Saved {len(self.chains)} chain configurations")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save chain configurations: {e}")
|
||||
|
||||
def add_chain(self, chain_config: ChainConfig) -> bool:
|
||||
"""Add a new chain configuration"""
|
||||
try:
|
||||
if chain_config.chain_id in self.chains:
|
||||
logger.warning(f"Chain {chain_config.chain_id} already exists")
|
||||
return False
|
||||
|
||||
self.chains[chain_config.chain_id] = chain_config
|
||||
|
||||
# Set as default if no default exists
|
||||
if self.default_chain_id is None:
|
||||
self.default_chain_id = chain_config.chain_id
|
||||
|
||||
self._save_chains()
|
||||
logger.info(f"Added chain: {chain_config.chain_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add chain {chain_config.chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def remove_chain(self, chain_id: str) -> bool:
|
||||
"""Remove a chain configuration"""
|
||||
try:
|
||||
if chain_id not in self.chains:
|
||||
logger.warning(f"Chain {chain_id} not found")
|
||||
return False
|
||||
|
||||
if chain_id == self.default_chain_id:
|
||||
logger.error(f"Cannot remove default chain {chain_id}")
|
||||
return False
|
||||
|
||||
del self.chains[chain_id]
|
||||
self._save_chains()
|
||||
logger.info(f"Removed chain: {chain_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_chain(self, chain_id: str) -> Optional[ChainConfig]:
|
||||
"""Get chain configuration by ID"""
|
||||
return self.chains.get(chain_id)
|
||||
|
||||
def get_default_chain(self) -> Optional[ChainConfig]:
|
||||
"""Get default chain configuration"""
|
||||
if self.default_chain_id:
|
||||
return self.chains.get(self.default_chain_id)
|
||||
return None
|
||||
|
||||
def set_default_chain(self, chain_id: str) -> bool:
|
||||
"""Set default chain"""
|
||||
try:
|
||||
if chain_id not in self.chains:
|
||||
logger.error(f"Chain {chain_id} not found")
|
||||
return False
|
||||
|
||||
self.default_chain_id = chain_id
|
||||
self._save_chains()
|
||||
logger.info(f"Set default chain: {chain_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set default chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def list_chains(self) -> List[ChainConfig]:
|
||||
"""List all chain configurations"""
|
||||
return list(self.chains.values())
|
||||
|
||||
def get_active_chains(self) -> List[ChainConfig]:
|
||||
"""Get only active chains"""
|
||||
return [chain for chain in self.chains.values() if chain.status == ChainStatus.ACTIVE]
|
||||
|
||||
def update_chain_status(self, chain_id: str, status: ChainStatus) -> bool:
|
||||
"""Update chain status"""
|
||||
try:
|
||||
if chain_id not in self.chains:
|
||||
logger.error(f"Chain {chain_id} not found")
|
||||
return False
|
||||
|
||||
self.chains[chain_id].status = status
|
||||
self.chains[chain_id].updated_at = datetime.now()
|
||||
self._save_chains()
|
||||
logger.info(f"Updated chain {chain_id} status to {status.value}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update chain status {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def validate_chain_id(self, chain_id: str) -> bool:
|
||||
"""Validate that a chain ID exists and is active"""
|
||||
chain = self.chains.get(chain_id)
|
||||
return chain is not None and chain.status == ChainStatus.ACTIVE
|
||||
|
||||
def get_chain_config_for_wallet(self, chain_id: str, wallet_id: str) -> Optional[ChainConfig]:
|
||||
"""Get chain configuration for a specific wallet operation"""
|
||||
if not self.validate_chain_id(chain_id):
|
||||
logger.error(f"Invalid or inactive chain: {chain_id}")
|
||||
return None
|
||||
|
||||
chain = self.chains[chain_id]
|
||||
|
||||
# Add wallet-specific context to metadata
|
||||
chain.metadata["last_wallet_access"] = wallet_id
|
||||
chain.metadata["last_access_time"] = datetime.now().isoformat()
|
||||
|
||||
return chain
|
||||
|
||||
def get_chain_stats(self) -> Dict[str, Any]:
|
||||
"""Get statistics about chains"""
|
||||
active_chains = self.get_active_chains()
|
||||
|
||||
return {
|
||||
"total_chains": len(self.chains),
|
||||
"active_chains": len(active_chains),
|
||||
"inactive_chains": len(self.chains) - len(active_chains),
|
||||
"default_chain": self.default_chain_id,
|
||||
"chain_list": [
|
||||
{
|
||||
"chain_id": chain.chain_id,
|
||||
"name": chain.name,
|
||||
"status": chain.status.value,
|
||||
"coordinator_url": chain.coordinator_url
|
||||
}
|
||||
for chain in self.chains.values()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Global chain manager instance
|
||||
chain_manager = ChainManager()
|
||||
427
apps/wallet/src/app/chain/multichain_ledger.py
Normal file
427
apps/wallet/src/app/chain/multichain_ledger.py
Normal file
@@ -0,0 +1,427 @@
|
||||
"""
|
||||
Multi-Chain Ledger Adapter for Wallet Daemon
|
||||
|
||||
Chain-specific storage and ledger management for wallet operations
|
||||
across multiple blockchain networks.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import threading
|
||||
import json
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, asdict
|
||||
import logging
|
||||
|
||||
from .manager import ChainManager, ChainConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChainLedgerRecord:
|
||||
"""Chain-specific ledger record"""
|
||||
chain_id: str
|
||||
wallet_id: str
|
||||
event_type: str
|
||||
timestamp: datetime
|
||||
data: Dict[str, Any]
|
||||
success: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChainWalletMetadata:
|
||||
"""Chain-specific wallet metadata"""
|
||||
chain_id: str
|
||||
wallet_id: str
|
||||
public_key: str
|
||||
address: Optional[str]
|
||||
metadata: Dict[str, str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class MultiChainLedgerAdapter:
|
||||
"""Multi-chain ledger adapter with chain-specific storage"""
|
||||
|
||||
def __init__(self, chain_manager: ChainManager, base_data_path: Optional[Path] = None):
|
||||
self.chain_manager = chain_manager
|
||||
self.base_data_path = base_data_path or Path("./data")
|
||||
self.base_data_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Separate database connections per chain
|
||||
self.chain_connections: Dict[str, sqlite3.Connection] = {}
|
||||
self.chain_locks: Dict[str, threading.Lock] = {}
|
||||
|
||||
# Initialize databases for all chains
|
||||
self._initialize_chain_databases()
|
||||
|
||||
def _initialize_chain_databases(self):
|
||||
"""Initialize database for each chain"""
|
||||
for chain in self.chain_manager.list_chains():
|
||||
self._init_chain_database(chain.chain_id)
|
||||
|
||||
def _get_chain_db_path(self, chain_id: str) -> Path:
|
||||
"""Get database path for a specific chain"""
|
||||
chain = self.chain_manager.get_chain(chain_id)
|
||||
if chain and chain.ledger_db_path:
|
||||
return Path(chain.ledger_db_path)
|
||||
|
||||
# Default path based on chain ID
|
||||
return self.base_data_path / f"wallet_ledger_{chain_id}.db"
|
||||
|
||||
def _init_chain_database(self, chain_id: str):
|
||||
"""Initialize database for a specific chain"""
|
||||
try:
|
||||
db_path = self._get_chain_db_path(chain_id)
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create connection and lock for this chain
|
||||
conn = sqlite3.connect(db_path)
|
||||
self.chain_connections[chain_id] = conn
|
||||
self.chain_locks[chain_id] = threading.Lock()
|
||||
|
||||
# Initialize schema
|
||||
with self.chain_locks[chain_id]:
|
||||
self._create_chain_schema(conn, chain_id)
|
||||
|
||||
logger.info(f"Initialized database for chain: {chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database for chain {chain_id}: {e}")
|
||||
|
||||
def _create_chain_schema(self, conn: sqlite3.Connection, chain_id: str):
|
||||
"""Create database schema for a specific chain"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Wallet metadata table
|
||||
cursor.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS wallet_metadata_{chain_id} (
|
||||
wallet_id TEXT PRIMARY KEY,
|
||||
public_key TEXT NOT NULL,
|
||||
address TEXT,
|
||||
metadata TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Ledger events table
|
||||
cursor.execute(f"""
|
||||
CREATE TABLE IF NOT EXISTS ledger_events_{chain_id} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
wallet_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
data TEXT,
|
||||
success BOOLEAN DEFAULT TRUE,
|
||||
FOREIGN KEY (wallet_id) REFERENCES wallet_metadata_{chain_id} (wallet_id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Chain-specific indexes
|
||||
cursor.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_wallet_events_{chain_id}
|
||||
ON ledger_events_{chain_id} (wallet_id, timestamp)
|
||||
""")
|
||||
|
||||
cursor.execute(f"""
|
||||
CREATE INDEX IF NOT EXISTS idx_wallet_created_{chain_id}
|
||||
ON wallet_metadata_{chain_id} (created_at)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
def _get_connection(self, chain_id: str) -> Optional[sqlite3.Connection]:
|
||||
"""Get database connection for a specific chain"""
|
||||
if chain_id not in self.chain_connections:
|
||||
self._init_chain_database(chain_id)
|
||||
|
||||
return self.chain_connections.get(chain_id)
|
||||
|
||||
def _get_lock(self, chain_id: str) -> threading.Lock:
|
||||
"""Get lock for a specific chain"""
|
||||
if chain_id not in self.chain_locks:
|
||||
self.chain_locks[chain_id] = threading.Lock()
|
||||
|
||||
return self.chain_locks[chain_id]
|
||||
|
||||
def create_wallet(self, chain_id: str, wallet_id: str, public_key: str,
|
||||
address: Optional[str] = None, metadata: Optional[Dict[str, str]] = None) -> bool:
|
||||
"""Create wallet in chain-specific database"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
logger.error(f"Invalid chain: {chain_id}")
|
||||
return False
|
||||
|
||||
conn = self._get_connection(chain_id)
|
||||
if not conn:
|
||||
return False
|
||||
|
||||
lock = self._get_lock(chain_id)
|
||||
with lock:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if wallet already exists
|
||||
cursor.execute(f"""
|
||||
SELECT wallet_id FROM wallet_metadata_{chain_id} WHERE wallet_id = ?
|
||||
""", (wallet_id,))
|
||||
|
||||
if cursor.fetchone():
|
||||
logger.warning(f"Wallet {wallet_id} already exists in chain {chain_id}")
|
||||
return False
|
||||
|
||||
# Insert wallet metadata
|
||||
now = datetime.now().isoformat()
|
||||
metadata_json = json.dumps(metadata or {})
|
||||
|
||||
cursor.execute(f"""
|
||||
INSERT INTO wallet_metadata_{chain_id}
|
||||
(wallet_id, public_key, address, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (wallet_id, public_key, address, metadata_json, now, now))
|
||||
|
||||
# Record creation event
|
||||
self.record_event(chain_id, wallet_id, "created", {
|
||||
"public_key": public_key,
|
||||
"address": address,
|
||||
"metadata": metadata or {}
|
||||
})
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Created wallet {wallet_id} in chain {chain_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_wallet(self, chain_id: str, wallet_id: str) -> Optional[ChainWalletMetadata]:
|
||||
"""Get wallet metadata from chain-specific database"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return None
|
||||
|
||||
conn = self._get_connection(chain_id)
|
||||
if not conn:
|
||||
return None
|
||||
|
||||
lock = self._get_lock(chain_id)
|
||||
with lock:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT wallet_id, public_key, address, metadata, created_at, updated_at
|
||||
FROM wallet_metadata_{chain_id} WHERE wallet_id = ?
|
||||
""", (wallet_id,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
metadata = json.loads(row[3]) if row[3] else {}
|
||||
|
||||
return ChainWalletMetadata(
|
||||
chain_id=chain_id,
|
||||
wallet_id=row[0],
|
||||
public_key=row[1],
|
||||
address=row[2],
|
||||
metadata=metadata,
|
||||
created_at=datetime.fromisoformat(row[4]),
|
||||
updated_at=datetime.fromisoformat(row[5])
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get wallet {wallet_id} from chain {chain_id}: {e}")
|
||||
return None
|
||||
|
||||
def list_wallets(self, chain_id: str) -> List[ChainWalletMetadata]:
|
||||
"""List all wallets in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return []
|
||||
|
||||
conn = self._get_connection(chain_id)
|
||||
if not conn:
|
||||
return []
|
||||
|
||||
lock = self._get_lock(chain_id)
|
||||
with lock:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT wallet_id, public_key, address, metadata, created_at, updated_at
|
||||
FROM wallet_metadata_{chain_id} ORDER BY created_at DESC
|
||||
""")
|
||||
|
||||
wallets = []
|
||||
for row in cursor.fetchall():
|
||||
metadata = json.loads(row[3]) if row[3] else {}
|
||||
|
||||
wallets.append(ChainWalletMetadata(
|
||||
chain_id=chain_id,
|
||||
wallet_id=row[0],
|
||||
public_key=row[1],
|
||||
address=row[2],
|
||||
metadata=metadata,
|
||||
created_at=datetime.fromisoformat(row[4]),
|
||||
updated_at=datetime.fromisoformat(row[5])
|
||||
))
|
||||
|
||||
return wallets
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list wallets in chain {chain_id}: {e}")
|
||||
return []
|
||||
|
||||
def record_event(self, chain_id: str, wallet_id: str, event_type: str,
|
||||
data: Dict[str, Any], success: bool = True) -> bool:
|
||||
"""Record an event for a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return False
|
||||
|
||||
conn = self._get_connection(chain_id)
|
||||
if not conn:
|
||||
return False
|
||||
|
||||
lock = self._get_lock(chain_id)
|
||||
with lock:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Insert event
|
||||
cursor.execute(f"""
|
||||
INSERT INTO ledger_events_{chain_id}
|
||||
(wallet_id, event_type, timestamp, data, success)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (wallet_id, event_type, datetime.now().isoformat(),
|
||||
json.dumps(data), success))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to record event for wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_wallet_events(self, chain_id: str, wallet_id: str,
|
||||
event_type: Optional[str] = None, limit: int = 100) -> List[ChainLedgerRecord]:
|
||||
"""Get events for a wallet in a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return []
|
||||
|
||||
conn = self._get_connection(chain_id)
|
||||
if not conn:
|
||||
return []
|
||||
|
||||
lock = self._get_lock(chain_id)
|
||||
with lock:
|
||||
cursor = conn.cursor()
|
||||
|
||||
if event_type:
|
||||
cursor.execute(f"""
|
||||
SELECT wallet_id, event_type, timestamp, data, success
|
||||
FROM ledger_events_{chain_id}
|
||||
WHERE wallet_id = ? AND event_type = ?
|
||||
ORDER BY timestamp DESC LIMIT ?
|
||||
""", (wallet_id, event_type, limit))
|
||||
else:
|
||||
cursor.execute(f"""
|
||||
SELECT wallet_id, event_type, timestamp, data, success
|
||||
FROM ledger_events_{chain_id}
|
||||
WHERE wallet_id = ?
|
||||
ORDER BY timestamp DESC LIMIT ?
|
||||
""", (wallet_id, limit))
|
||||
|
||||
events = []
|
||||
for row in cursor.fetchall():
|
||||
data = json.loads(row[3]) if row[3] else {}
|
||||
|
||||
events.append(ChainLedgerRecord(
|
||||
chain_id=chain_id,
|
||||
wallet_id=row[0],
|
||||
event_type=row[1],
|
||||
timestamp=datetime.fromisoformat(row[2]),
|
||||
data=data,
|
||||
success=row[4]
|
||||
))
|
||||
|
||||
return events
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get events for wallet {wallet_id} in chain {chain_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_chain_stats(self, chain_id: str) -> Dict[str, Any]:
|
||||
"""Get statistics for a specific chain"""
|
||||
try:
|
||||
if not self.chain_manager.validate_chain_id(chain_id):
|
||||
return {}
|
||||
|
||||
conn = self._get_connection(chain_id)
|
||||
if not conn:
|
||||
return {}
|
||||
|
||||
lock = self._get_lock(chain_id)
|
||||
with lock:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Wallet count
|
||||
cursor.execute(f"SELECT COUNT(*) FROM wallet_metadata_{chain_id}")
|
||||
wallet_count = cursor.fetchone()[0]
|
||||
|
||||
# Event count by type
|
||||
cursor.execute(f"""
|
||||
SELECT event_type, COUNT(*) FROM ledger_events_{chain_id}
|
||||
GROUP BY event_type
|
||||
""")
|
||||
event_counts = dict(cursor.fetchall())
|
||||
|
||||
# Recent activity
|
||||
cursor.execute(f"""
|
||||
SELECT COUNT(*) FROM ledger_events_{chain_id}
|
||||
WHERE timestamp > datetime('now', '-1 hour')
|
||||
""")
|
||||
recent_activity = cursor.fetchone()[0]
|
||||
|
||||
return {
|
||||
"chain_id": chain_id,
|
||||
"wallet_count": wallet_count,
|
||||
"event_counts": event_counts,
|
||||
"recent_activity": recent_activity,
|
||||
"database_path": str(self._get_chain_db_path(chain_id))
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get stats for chain {chain_id}: {e}")
|
||||
return {}
|
||||
|
||||
def get_all_chain_stats(self) -> Dict[str, Any]:
|
||||
"""Get statistics for all chains"""
|
||||
stats = {
|
||||
"total_chains": 0,
|
||||
"total_wallets": 0,
|
||||
"chain_stats": {}
|
||||
}
|
||||
|
||||
for chain in self.chain_manager.get_active_chains():
|
||||
chain_stats = self.get_chain_stats(chain.chain_id)
|
||||
if chain_stats:
|
||||
stats["chain_stats"][chain.chain_id] = chain_stats
|
||||
stats["total_wallets"] += chain_stats.get("wallet_count", 0)
|
||||
stats["total_chains"] += 1
|
||||
|
||||
return stats
|
||||
|
||||
def close_all_connections(self):
|
||||
"""Close all database connections"""
|
||||
for chain_id, conn in self.chain_connections.items():
|
||||
try:
|
||||
conn.close()
|
||||
logger.info(f"Closed connection for chain: {chain_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to close connection for chain {chain_id}: {e}")
|
||||
|
||||
self.chain_connections.clear()
|
||||
self.chain_locks.clear()
|
||||
66
apps/wallet/src/app/crypto/encryption.py
Normal file
66
apps/wallet/src/app/crypto/encryption.py
Normal 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)
|
||||
51
apps/wallet/src/app/deps.py
Normal file
51
apps/wallet/src/app/deps.py
Normal 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)
|
||||
396
apps/wallet/src/app/keystore/persistent_service.py
Normal file
396
apps/wallet/src/app/keystore/persistent_service.py
Normal 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
|
||||
96
apps/wallet/src/app/keystore/service.py
Normal file
96
apps/wallet/src/app/keystore/service.py
Normal 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)
|
||||
283
apps/wallet/src/app/ledger_mock.py
Normal file
283
apps/wallet/src/app/ledger_mock.py
Normal 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()
|
||||
3
apps/wallet/src/app/ledger_mock/__init__.py
Normal file
3
apps/wallet/src/app/ledger_mock/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .sqlite_adapter import SQLiteLedgerAdapter, WalletRecord, WalletEvent
|
||||
|
||||
__all__ = ["SQLiteLedgerAdapter", "WalletRecord", "WalletEvent"]
|
||||
197
apps/wallet/src/app/ledger_mock/postgresql_adapter.py
Normal file
197
apps/wallet/src/app/ledger_mock/postgresql_adapter.py
Normal 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)
|
||||
106
apps/wallet/src/app/ledger_mock/sqlite_adapter.py
Normal file
106
apps/wallet/src/app/ledger_mock/sqlite_adapter.py
Normal 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"]),
|
||||
)
|
||||
27
apps/wallet/src/app/main.py
Normal file
27
apps/wallet/src/app/main.py
Normal 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()
|
||||
134
apps/wallet/src/app/models/__init__.py
Normal file
134
apps/wallet/src/app/models/__init__.py
Normal 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
|
||||
5
apps/wallet/src/app/receipts/__init__.py
Normal file
5
apps/wallet/src/app/receipts/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Receipt verification helpers for the wallet daemon."""
|
||||
|
||||
from .service import ReceiptValidationResult, ReceiptVerifierService
|
||||
|
||||
__all__ = ["ReceiptValidationResult", "ReceiptVerifierService"]
|
||||
58
apps/wallet/src/app/receipts/service.py
Normal file
58
apps/wallet/src/app/receipts/service.py
Normal 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),
|
||||
)
|
||||
43
apps/wallet/src/app/security.py
Normal file
43
apps/wallet/src/app/security.py
Normal 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
|
||||
33
apps/wallet/src/app/settings.py
Normal file
33
apps/wallet/src/app/settings.py
Normal 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()
|
||||
Reference in New Issue
Block a user