chore(security): enhance environment configuration, CI workflows, and wallet daemon with security improvements
- Restructure .env.example with security-focused documentation, service-specific environment file references, and AWS Secrets Manager integration - Update CLI tests workflow to single Python 3.13 version, add pytest-mock dependency, and consolidate test execution with coverage - Add comprehensive security validation to package publishing workflow with manual approval gates, secret scanning, and release
This commit is contained in:
@@ -16,7 +16,7 @@ def admin():
|
||||
@admin.command()
|
||||
@click.pass_context
|
||||
def status(ctx):
|
||||
"""Get system status"""
|
||||
"""Show system status"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
@@ -30,13 +30,77 @@ def status(ctx):
|
||||
status_data = response.json()
|
||||
output(status_data, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to get system status: {response.status_code}")
|
||||
error(f"Failed to get status: {response.status_code}")
|
||||
ctx.exit(1)
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
ctx.exit(1)
|
||||
|
||||
|
||||
@admin.command()
|
||||
@click.option("--output", type=click.Path(), help="Output report to file")
|
||||
@click.pass_context
|
||||
def audit_verify(ctx, output):
|
||||
"""Verify audit log integrity"""
|
||||
audit_logger = AuditLogger()
|
||||
is_valid, issues = audit_logger.verify_integrity()
|
||||
|
||||
if is_valid:
|
||||
success("Audit log integrity verified - no tampering detected")
|
||||
else:
|
||||
error("Audit log integrity compromised!")
|
||||
for issue in issues:
|
||||
error(f" - {issue}")
|
||||
ctx.exit(1)
|
||||
|
||||
# Export detailed report if requested
|
||||
if output:
|
||||
try:
|
||||
report = audit_logger.export_report(Path(output))
|
||||
success(f"Audit report exported to {output}")
|
||||
|
||||
# Show summary
|
||||
stats = report["audit_report"]["statistics"]
|
||||
output({
|
||||
"total_entries": stats["total_entries"],
|
||||
"unique_actions": stats["unique_actions"],
|
||||
"unique_users": stats["unique_users"],
|
||||
"date_range": stats["date_range"]
|
||||
}, ctx.obj['output_format'])
|
||||
except Exception as e:
|
||||
error(f"Failed to export report: {e}")
|
||||
|
||||
|
||||
@admin.command()
|
||||
@click.option("--limit", default=50, help="Number of entries to show")
|
||||
@click.option("--action", help="Filter by action type")
|
||||
@click.option("--search", help="Search query")
|
||||
@click.pass_context
|
||||
def audit_logs(ctx, limit: int, action: str, search: str):
|
||||
"""View audit logs with integrity verification"""
|
||||
audit_logger = AuditLogger()
|
||||
|
||||
try:
|
||||
if search:
|
||||
entries = audit_logger.search_logs(search, limit)
|
||||
else:
|
||||
entries = audit_logger.get_logs(limit, action)
|
||||
|
||||
if not entries:
|
||||
warning("No audit entries found")
|
||||
return
|
||||
|
||||
# Show entries
|
||||
output({
|
||||
"total_entries": len(entries),
|
||||
"entries": entries
|
||||
}, ctx.obj['output_format'])
|
||||
|
||||
except Exception as e:
|
||||
error(f"Failed to read audit logs: {e}")
|
||||
ctx.exit(1)
|
||||
|
||||
|
||||
@admin.command()
|
||||
@click.option("--limit", default=50, help="Number of jobs to show")
|
||||
@click.option("--status", help="Filter by status")
|
||||
|
||||
@@ -546,9 +546,9 @@ def progress(ctx, agent_id: str, metrics: str):
|
||||
@click.argument("agent_id")
|
||||
@click.option("--format", default="onnx", type=click.Choice(["onnx", "pickle", "torch"]),
|
||||
help="Export format")
|
||||
@click.option("--output", type=click.Path(), help="Output file path")
|
||||
@click.option("--output-path", type=click.Path(), help="Output file path")
|
||||
@click.pass_context
|
||||
def export(ctx, agent_id: str, format: str, output: Optional[str]):
|
||||
def export(ctx, agent_id: str, format: str, output_path: Optional[str]):
|
||||
"""Export learned agent model"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
@@ -563,10 +563,10 @@ def export(ctx, agent_id: str, format: str, output: Optional[str]):
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
if output:
|
||||
with open(output, 'wb') as f:
|
||||
if output_path:
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
success(f"Model exported to {output}")
|
||||
success(f"Model exported to {output_path}")
|
||||
else:
|
||||
# Output metadata about the export
|
||||
export_info = response.headers.get('X-Export-Info', '{}')
|
||||
|
||||
@@ -25,7 +25,7 @@ def simulate():
|
||||
@click.pass_context
|
||||
def init(ctx, distribute: str, reset: bool):
|
||||
"""Initialize test economy"""
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
|
||||
|
||||
if reset:
|
||||
success("Resetting simulation...")
|
||||
@@ -115,7 +115,7 @@ def user():
|
||||
@click.pass_context
|
||||
def create(ctx, type: str, name: str, balance: float):
|
||||
"""Create a test user"""
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
|
||||
|
||||
user_id = f"{type}_{name}"
|
||||
wallet_path = home_dir / f"{user_id}_wallet.json"
|
||||
@@ -151,7 +151,7 @@ def create(ctx, type: str, name: str, balance: float):
|
||||
@click.pass_context
|
||||
def list(ctx):
|
||||
"""List all test users"""
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
|
||||
|
||||
users = []
|
||||
for wallet_file in home_dir.glob("*_wallet.json"):
|
||||
@@ -181,7 +181,7 @@ def list(ctx):
|
||||
@click.pass_context
|
||||
def balance(ctx, user: str):
|
||||
"""Check user balance"""
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
|
||||
wallet_path = home_dir / f"{user}_wallet.json"
|
||||
|
||||
if not wallet_path.exists():
|
||||
@@ -203,7 +203,7 @@ def balance(ctx, user: str):
|
||||
@click.pass_context
|
||||
def fund(ctx, user: str, amount: float):
|
||||
"""Fund a test user"""
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/home")
|
||||
home_dir = Path("/home/oib/windsurf/aitbc/tests/e2e/fixtures/home")
|
||||
|
||||
# Load genesis wallet
|
||||
genesis_path = home_dir / "genesis_wallet.json"
|
||||
|
||||
467
cli/aitbc_cli/commands/test_cli.py
Normal file
467
cli/aitbc_cli/commands/test_cli.py
Normal file
@@ -0,0 +1,467 @@
|
||||
"""
|
||||
AITBC CLI Testing Commands
|
||||
Provides testing and debugging utilities for the AITBC CLI
|
||||
"""
|
||||
|
||||
import click
|
||||
import json
|
||||
import time
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from ..utils import output, success, error, warning
|
||||
from ..config import get_config
|
||||
|
||||
|
||||
@click.group()
|
||||
def test():
|
||||
"""Testing and debugging commands for AITBC CLI"""
|
||||
pass
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--format', type=click.Choice(['json', 'table', 'yaml']), default='table', help='Output format')
|
||||
@click.pass_context
|
||||
def environment(ctx, format):
|
||||
"""Test CLI environment and configuration"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
env_info = {
|
||||
'coordinator_url': config.coordinator_url,
|
||||
'api_key': config.api_key,
|
||||
'output_format': ctx.obj['output_format'],
|
||||
'test_mode': ctx.obj['test_mode'],
|
||||
'dry_run': ctx.obj['dry_run'],
|
||||
'timeout': ctx.obj['timeout'],
|
||||
'no_verify': ctx.obj['no_verify'],
|
||||
'log_level': ctx.obj['log_level']
|
||||
}
|
||||
|
||||
if format == 'json':
|
||||
output(json.dumps(env_info, indent=2))
|
||||
else:
|
||||
output("CLI Environment Test Results:")
|
||||
output(f" Coordinator URL: {env_info['coordinator_url']}")
|
||||
output(f" API Key: {env_info['api_key'][:10]}..." if env_info['api_key'] else " API Key: None")
|
||||
output(f" Output Format: {env_info['output_format']}")
|
||||
output(f" Test Mode: {env_info['test_mode']}")
|
||||
output(f" Dry Run: {env_info['dry_run']}")
|
||||
output(f" Timeout: {env_info['timeout']}s")
|
||||
output(f" No Verify: {env_info['no_verify']}")
|
||||
output(f" Log Level: {env_info['log_level']}")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--endpoint', default='health', help='API endpoint to test')
|
||||
@click.option('--method', default='GET', help='HTTP method')
|
||||
@click.option('--data', help='JSON data to send (for POST/PUT)')
|
||||
@click.pass_context
|
||||
def api(ctx, endpoint, method, data):
|
||||
"""Test API connectivity"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
import httpx
|
||||
|
||||
# Prepare request
|
||||
url = f"{config.coordinator_url.rstrip('/')}/api/v1/{endpoint.lstrip('/')}"
|
||||
headers = {}
|
||||
if config.api_key:
|
||||
headers['Authorization'] = f"Bearer {config.api_key}"
|
||||
|
||||
# Prepare data
|
||||
json_data = None
|
||||
if data and method in ['POST', 'PUT']:
|
||||
json_data = json.loads(data)
|
||||
|
||||
# Make request
|
||||
with httpx.Client(verify=not ctx.obj['no_verify'], timeout=ctx.obj['timeout']) as client:
|
||||
if method == 'GET':
|
||||
response = client.get(url, headers=headers)
|
||||
elif method == 'POST':
|
||||
response = client.post(url, headers=headers, json=json_data)
|
||||
elif method == 'PUT':
|
||||
response = client.put(url, headers=headers, json=json_data)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
# Display results
|
||||
output(f"API Test Results:")
|
||||
output(f" URL: {url}")
|
||||
output(f" Method: {method}")
|
||||
output(f" Status Code: {response.status_code}")
|
||||
output(f" Response Time: {response.elapsed.total_seconds():.3f}s")
|
||||
|
||||
if response.status_code == 200:
|
||||
success("✅ API test successful")
|
||||
try:
|
||||
response_data = response.json()
|
||||
output("Response Data:")
|
||||
output(json.dumps(response_data, indent=2))
|
||||
except:
|
||||
output(f"Response: {response.text}")
|
||||
else:
|
||||
error(f"❌ API test failed with status {response.status_code}")
|
||||
output(f"Response: {response.text}")
|
||||
|
||||
except ImportError:
|
||||
error("❌ httpx not installed. Install with: pip install httpx")
|
||||
except Exception as e:
|
||||
error(f"❌ API test failed: {str(e)}")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--wallet-name', default='test-wallet', help='Test wallet name')
|
||||
@click.option('--test-operations', is_flag=True, default=True, help='Test wallet operations')
|
||||
@click.pass_context
|
||||
def wallet(ctx, wallet_name, test_operations):
|
||||
"""Test wallet functionality"""
|
||||
from ..commands.wallet import wallet as wallet_cmd
|
||||
|
||||
output(f"Testing wallet functionality with wallet: {wallet_name}")
|
||||
|
||||
# Test wallet creation
|
||||
try:
|
||||
result = ctx.invoke(wallet_cmd, ['create', wallet_name])
|
||||
if result.exit_code == 0:
|
||||
success(f"✅ Wallet '{wallet_name}' created successfully")
|
||||
else:
|
||||
error(f"❌ Wallet creation failed: {result.output}")
|
||||
return
|
||||
except Exception as e:
|
||||
error(f"❌ Wallet creation error: {str(e)}")
|
||||
return
|
||||
|
||||
if test_operations:
|
||||
# Test wallet balance
|
||||
try:
|
||||
result = ctx.invoke(wallet_cmd, ['balance'])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Wallet balance check successful")
|
||||
output(f"Balance output: {result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Wallet balance check failed: {result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Wallet balance check error: {str(e)}")
|
||||
|
||||
# Test wallet info
|
||||
try:
|
||||
result = ctx.invoke(wallet_cmd, ['info'])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Wallet info check successful")
|
||||
output(f"Info output: {result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Wallet info check failed: {result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Wallet info check error: {str(e)}")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--job-type', default='ml_inference', help='Type of job to test')
|
||||
@click.option('--test-data', default='{"model": "test-model", "input": "test-data"}', help='Test job data')
|
||||
@click.pass_context
|
||||
def job(ctx, job_type, test_data):
|
||||
"""Test job submission and management"""
|
||||
from ..commands.client import client as client_cmd
|
||||
|
||||
output(f"Testing job submission with type: {job_type}")
|
||||
|
||||
try:
|
||||
# Parse test data
|
||||
job_data = json.loads(test_data)
|
||||
job_data['type'] = job_type
|
||||
|
||||
# Test job submission
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(job_data, f)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
result = ctx.invoke(client_cmd, ['submit', '--job-file', temp_file])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Job submission successful")
|
||||
output(f"Submission output: {result.output}")
|
||||
|
||||
# Extract job ID if present
|
||||
if 'job_id' in result.output:
|
||||
import re
|
||||
job_id_match = re.search(r'job[_\s-]?id[:\s]+(\w+)', result.output, re.IGNORECASE)
|
||||
if job_id_match:
|
||||
job_id = job_id_match.group(1)
|
||||
output(f"Extracted job ID: {job_id}")
|
||||
|
||||
# Test job status
|
||||
try:
|
||||
status_result = ctx.invoke(client_cmd, ['status', job_id])
|
||||
if status_result.exit_code == 0:
|
||||
success("✅ Job status check successful")
|
||||
output(f"Status output: {status_result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Job status check failed: {status_result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Job status check error: {str(e)}")
|
||||
else:
|
||||
error(f"❌ Job submission failed: {result.output}")
|
||||
finally:
|
||||
# Clean up temp file
|
||||
Path(temp_file).unlink(missing_ok=True)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
error(f"❌ Invalid test data JSON: {test_data}")
|
||||
except Exception as e:
|
||||
error(f"❌ Job test failed: {str(e)}")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--gpu-type', default='RTX 3080', help='GPU type to test')
|
||||
@click.option('--price', type=float, default=0.1, help='Price to test')
|
||||
@click.pass_context
|
||||
def marketplace(ctx, gpu_type, price):
|
||||
"""Test marketplace functionality"""
|
||||
from ..commands.marketplace import marketplace as marketplace_cmd
|
||||
|
||||
output(f"Testing marketplace functionality for {gpu_type} at {price} AITBC/hour")
|
||||
|
||||
# Test marketplace offers listing
|
||||
try:
|
||||
result = ctx.invoke(marketplace_cmd, ['offers', 'list'])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Marketplace offers list successful")
|
||||
output(f"Offers output: {result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Marketplace offers list failed: {result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Marketplace offers list error: {str(e)}")
|
||||
|
||||
# Test marketplace pricing
|
||||
try:
|
||||
result = ctx.invoke(marketplace_cmd, ['pricing', gpu_type])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Marketplace pricing check successful")
|
||||
output(f"Pricing output: {result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Marketplace pricing check failed: {result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Marketplace pricing check error: {str(e)}")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--test-endpoints', is_flag=True, default=True, help='Test blockchain endpoints')
|
||||
@click.pass_context
|
||||
def blockchain(ctx, test_endpoints):
|
||||
"""Test blockchain functionality"""
|
||||
from ..commands.blockchain import blockchain as blockchain_cmd
|
||||
|
||||
output("Testing blockchain functionality")
|
||||
|
||||
if test_endpoints:
|
||||
# Test blockchain info
|
||||
try:
|
||||
result = ctx.invoke(blockchain_cmd, ['info'])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Blockchain info successful")
|
||||
output(f"Info output: {result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Blockchain info failed: {result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Blockchain info error: {str(e)}")
|
||||
|
||||
# Test chain status
|
||||
try:
|
||||
result = ctx.invoke(blockchain_cmd, ['status'])
|
||||
if result.exit_code == 0:
|
||||
success("✅ Blockchain status successful")
|
||||
output(f"Status output: {result.output}")
|
||||
else:
|
||||
warning(f"⚠️ Blockchain status failed: {result.output}")
|
||||
except Exception as e:
|
||||
warning(f"⚠️ Blockchain status error: {str(e)}")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--component', help='Specific component to test (wallet, job, marketplace, blockchain, api)')
|
||||
@click.option('--verbose', is_flag=True, help='Verbose test output')
|
||||
@click.pass_context
|
||||
def integration(ctx, component, verbose):
|
||||
"""Run integration tests"""
|
||||
|
||||
if component:
|
||||
output(f"Running integration tests for: {component}")
|
||||
|
||||
if component == 'wallet':
|
||||
ctx.invoke(wallet, ['--test-operations'])
|
||||
elif component == 'job':
|
||||
ctx.invoke(job, [])
|
||||
elif component == 'marketplace':
|
||||
ctx.invoke(marketplace, [])
|
||||
elif component == 'blockchain':
|
||||
ctx.invoke(blockchain, [])
|
||||
elif component == 'api':
|
||||
ctx.invoke(api, ['--endpoint', 'health'])
|
||||
else:
|
||||
error(f"Unknown component: {component}")
|
||||
return
|
||||
else:
|
||||
output("Running full integration test suite...")
|
||||
|
||||
# Test API connectivity first
|
||||
output("1. Testing API connectivity...")
|
||||
ctx.invoke(api, ['--endpoint', 'health'])
|
||||
|
||||
# Test wallet functionality
|
||||
output("2. Testing wallet functionality...")
|
||||
ctx.invoke(wallet, ['--wallet-name', 'integration-test-wallet'])
|
||||
|
||||
# Test marketplace functionality
|
||||
output("3. Testing marketplace functionality...")
|
||||
ctx.invoke(marketplace, [])
|
||||
|
||||
# Test blockchain functionality
|
||||
output("4. Testing blockchain functionality...")
|
||||
ctx.invoke(blockchain, [])
|
||||
|
||||
# Test job functionality
|
||||
output("5. Testing job functionality...")
|
||||
ctx.invoke(job, [])
|
||||
|
||||
success("✅ Integration test suite completed")
|
||||
|
||||
|
||||
@test.command()
|
||||
@click.option('--output-file', help='Save test results to file')
|
||||
@click.pass_context
|
||||
def diagnostics(ctx, output_file):
|
||||
"""Run comprehensive diagnostics"""
|
||||
|
||||
diagnostics_data = {
|
||||
'timestamp': time.time(),
|
||||
'test_mode': ctx.obj['test_mode'],
|
||||
'dry_run': ctx.obj['dry_run'],
|
||||
'config': {
|
||||
'coordinator_url': ctx.obj['config'].coordinator_url,
|
||||
'api_key_present': bool(ctx.obj['config'].api_key),
|
||||
'output_format': ctx.obj['output_format']
|
||||
}
|
||||
}
|
||||
|
||||
output("Running comprehensive diagnostics...")
|
||||
|
||||
# Test 1: Environment
|
||||
output("1. Testing environment...")
|
||||
try:
|
||||
ctx.invoke(environment, ['--format', 'json'])
|
||||
diagnostics_data['environment'] = 'PASS'
|
||||
except Exception as e:
|
||||
diagnostics_data['environment'] = f'FAIL: {str(e)}'
|
||||
error(f"Environment test failed: {str(e)}")
|
||||
|
||||
# Test 2: API Connectivity
|
||||
output("2. Testing API connectivity...")
|
||||
try:
|
||||
ctx.invoke(api, ['--endpoint', 'health'])
|
||||
diagnostics_data['api_connectivity'] = 'PASS'
|
||||
except Exception as e:
|
||||
diagnostics_data['api_connectivity'] = f'FAIL: {str(e)}'
|
||||
error(f"API connectivity test failed: {str(e)}")
|
||||
|
||||
# Test 3: Wallet Creation
|
||||
output("3. Testing wallet creation...")
|
||||
try:
|
||||
ctx.invoke(wallet, ['--wallet-name', 'diagnostics-test', '--test-operations'])
|
||||
diagnostics_data['wallet_creation'] = 'PASS'
|
||||
except Exception as e:
|
||||
diagnostics_data['wallet_creation'] = f'FAIL: {str(e)}'
|
||||
error(f"Wallet creation test failed: {str(e)}")
|
||||
|
||||
# Test 4: Marketplace
|
||||
output("4. Testing marketplace...")
|
||||
try:
|
||||
ctx.invoke(marketplace, [])
|
||||
diagnostics_data['marketplace'] = 'PASS'
|
||||
except Exception as e:
|
||||
diagnostics_data['marketplace'] = f'FAIL: {str(e)}'
|
||||
error(f"Marketplace test failed: {str(e)}")
|
||||
|
||||
# Generate summary
|
||||
passed_tests = sum(1 for v in diagnostics_data.values() if isinstance(v, str) and v == 'PASS')
|
||||
total_tests = len([k for k in diagnostics_data.keys() if k in ['environment', 'api_connectivity', 'wallet_creation', 'marketplace']])
|
||||
|
||||
diagnostics_data['summary'] = {
|
||||
'total_tests': total_tests,
|
||||
'passed_tests': passed_tests,
|
||||
'failed_tests': total_tests - passed_tests,
|
||||
'success_rate': (passed_tests / total_tests * 100) if total_tests > 0 else 0
|
||||
}
|
||||
|
||||
# Display results
|
||||
output("\n" + "="*50)
|
||||
output("DIAGNOSTICS SUMMARY")
|
||||
output("="*50)
|
||||
output(f"Total Tests: {diagnostics_data['summary']['total_tests']}")
|
||||
output(f"Passed: {diagnostics_data['summary']['passed_tests']}")
|
||||
output(f"Failed: {diagnostics_data['summary']['failed_tests']}")
|
||||
output(f"Success Rate: {diagnostics_data['summary']['success_rate']:.1f}%")
|
||||
|
||||
if diagnostics_data['summary']['success_rate'] == 100:
|
||||
success("✅ All diagnostics passed!")
|
||||
else:
|
||||
warning(f"⚠️ {diagnostics_data['summary']['failed_tests']} test(s) failed")
|
||||
|
||||
# Save to file if requested
|
||||
if output_file:
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(diagnostics_data, f, indent=2)
|
||||
output(f"Diagnostics saved to: {output_file}")
|
||||
|
||||
|
||||
@test.command()
|
||||
def mock():
|
||||
"""Generate mock data for testing"""
|
||||
|
||||
mock_data = {
|
||||
'wallet': {
|
||||
'name': 'test-wallet',
|
||||
'address': 'aitbc1test123456789abcdef',
|
||||
'balance': 1000.0,
|
||||
'transactions': []
|
||||
},
|
||||
'job': {
|
||||
'id': 'test-job-123',
|
||||
'type': 'ml_inference',
|
||||
'status': 'pending',
|
||||
'requirements': {
|
||||
'gpu_type': 'RTX 3080',
|
||||
'memory_gb': 8,
|
||||
'duration_minutes': 30
|
||||
}
|
||||
},
|
||||
'marketplace': {
|
||||
'offers': [
|
||||
{
|
||||
'id': 'offer-1',
|
||||
'provider': 'test-provider',
|
||||
'gpu_type': 'RTX 3080',
|
||||
'price_per_hour': 0.1,
|
||||
'available': True
|
||||
}
|
||||
]
|
||||
},
|
||||
'blockchain': {
|
||||
'chain_id': 'aitbc-testnet',
|
||||
'block_height': 1000,
|
||||
'network_status': 'active'
|
||||
}
|
||||
}
|
||||
|
||||
output("Mock data for testing:")
|
||||
output(json.dumps(mock_data, indent=2))
|
||||
|
||||
# Save to temp file
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(mock_data, f, indent=2)
|
||||
temp_file = f.name
|
||||
|
||||
output(f"Mock data saved to: {temp_file}")
|
||||
return temp_file
|
||||
@@ -727,8 +727,12 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
|
||||
wallet_data["transactions"].append(transaction)
|
||||
wallet_data["balance"] = balance - amount
|
||||
|
||||
with open(wallet_path, "w") as f:
|
||||
json.dump(wallet_data, f, indent=2)
|
||||
# Use _save_wallet to preserve encryption
|
||||
if wallet_data.get("encrypted"):
|
||||
password = _get_wallet_password(wallet_name)
|
||||
_save_wallet(wallet_path, wallet_data, password)
|
||||
else:
|
||||
_save_wallet(wallet_path, wallet_data)
|
||||
|
||||
success(f"Sent {amount} AITBC to {to_address}")
|
||||
output(
|
||||
@@ -932,8 +936,7 @@ def unstake(ctx, stake_id: str):
|
||||
error(f"Wallet '{wallet_name}' not found")
|
||||
return
|
||||
|
||||
with open(wallet_path, "r") as f:
|
||||
wallet_data = json.load(f)
|
||||
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||
|
||||
staking = wallet_data.get("staking", [])
|
||||
stake_record = next(
|
||||
@@ -1145,13 +1148,85 @@ def multisig_propose(
|
||||
)
|
||||
|
||||
|
||||
@wallet.command(name="multisig-challenge")
|
||||
@click.option("--wallet", "wallet_name", required=True, help="Multisig wallet name")
|
||||
@click.argument("tx_id")
|
||||
@click.pass_context
|
||||
def multisig_challenge(ctx, wallet_name: str, tx_id: str):
|
||||
"""Create a cryptographic challenge for multisig transaction signing"""
|
||||
wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets")
|
||||
multisig_path = wallet_dir / f"{wallet_name}_multisig.json"
|
||||
|
||||
if not multisig_path.exists():
|
||||
error(f"Multisig wallet '{wallet_name}' not found")
|
||||
return
|
||||
|
||||
with open(multisig_path) as f:
|
||||
ms_data = json.load(f)
|
||||
|
||||
# Find pending transaction
|
||||
pending = ms_data.get("pending_transactions", [])
|
||||
tx = next(
|
||||
(t for t in pending if t["tx_id"] == tx_id and t["status"] == "pending"), None
|
||||
)
|
||||
|
||||
if not tx:
|
||||
error(f"Pending transaction '{tx_id}' not found")
|
||||
return
|
||||
|
||||
# Import crypto utilities
|
||||
from ..utils.crypto_utils import multisig_security
|
||||
|
||||
try:
|
||||
# Create signing request
|
||||
signing_request = multisig_security.create_signing_request(tx, wallet_name)
|
||||
|
||||
output({
|
||||
"tx_id": tx_id,
|
||||
"wallet": wallet_name,
|
||||
"challenge": signing_request["challenge"],
|
||||
"nonce": signing_request["nonce"],
|
||||
"message": signing_request["message"],
|
||||
"instructions": [
|
||||
"1. Copy the challenge string above",
|
||||
"2. Sign it with your private key using: aitbc wallet sign-challenge <challenge> <private-key>",
|
||||
"3. Use the returned signature with: aitbc wallet multisig-sign --wallet <wallet> <tx_id> --signer <address> --signature <signature>"
|
||||
]
|
||||
}, ctx.obj.get("output_format", "table"))
|
||||
|
||||
except Exception as e:
|
||||
error(f"Failed to create challenge: {e}")
|
||||
|
||||
|
||||
@wallet.command(name="sign-challenge")
|
||||
@click.argument("challenge")
|
||||
@click.argument("private_key")
|
||||
@click.pass_context
|
||||
def sign_challenge(ctx, challenge: str, private_key: str):
|
||||
"""Sign a cryptographic challenge (for testing multisig)"""
|
||||
from ..utils.crypto_utils import sign_challenge
|
||||
|
||||
try:
|
||||
signature = sign_challenge(challenge, private_key)
|
||||
|
||||
output({
|
||||
"challenge": challenge,
|
||||
"signature": signature,
|
||||
"message": "Use this signature with multisig-sign command"
|
||||
}, ctx.obj.get("output_format", "table"))
|
||||
|
||||
except Exception as e:
|
||||
error(f"Failed to sign challenge: {e}")
|
||||
|
||||
|
||||
@wallet.command(name="multisig-sign")
|
||||
@click.option("--wallet", "wallet_name", required=True, help="Multisig wallet name")
|
||||
@click.argument("tx_id")
|
||||
@click.option("--signer", required=True, help="Signer address")
|
||||
@click.option("--signature", required=True, help="Cryptographic signature (hex)")
|
||||
@click.pass_context
|
||||
def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
|
||||
"""Sign a pending multisig transaction"""
|
||||
def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str, signature: str):
|
||||
"""Sign a pending multisig transaction with cryptographic verification"""
|
||||
wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets")
|
||||
multisig_path = wallet_dir / f"{wallet_name}_multisig.json"
|
||||
|
||||
@@ -1167,6 +1242,16 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
|
||||
ctx.exit(1)
|
||||
return
|
||||
|
||||
# Import crypto utilities
|
||||
from ..utils.crypto_utils import multisig_security
|
||||
|
||||
# Verify signature cryptographically
|
||||
success, message = multisig_security.verify_and_add_signature(tx_id, signature, signer)
|
||||
if not success:
|
||||
error(f"Signature verification failed: {message}")
|
||||
ctx.exit(1)
|
||||
return
|
||||
|
||||
pending = ms_data.get("pending_transactions", [])
|
||||
tx = next(
|
||||
(t for t in pending if t["tx_id"] == tx_id and t["status"] == "pending"), None
|
||||
@@ -1177,11 +1262,21 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
|
||||
ctx.exit(1)
|
||||
return
|
||||
|
||||
if signer in tx["signatures"]:
|
||||
error(f"'{signer}' has already signed this transaction")
|
||||
return
|
||||
# Check if already signed
|
||||
for sig in tx.get("signatures", []):
|
||||
if sig["signer"] == signer:
|
||||
error(f"'{signer}' has already signed this transaction")
|
||||
return
|
||||
|
||||
tx["signatures"].append(signer)
|
||||
# Add cryptographic signature
|
||||
if "signatures" not in tx:
|
||||
tx["signatures"] = []
|
||||
|
||||
tx["signatures"].append({
|
||||
"signer": signer,
|
||||
"signature": signature,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
# Check if threshold met
|
||||
if len(tx["signatures"]) >= ms_data["threshold"]:
|
||||
|
||||
Reference in New Issue
Block a user