chore: remove configuration files and enhance blockchain explorer with advanced search, analytics, and export features
- Delete .aitbc.yaml.example CLI configuration template - Delete .lycheeignore link checker exclusion rules - Delete .nvmrc Node.js version specification - Add advanced search panel with filters for address, amount range, transaction type, time range, and validator - Add analytics dashboard with transaction volume, active addresses, and block time metrics - Add Chart.js integration
This commit is contained in:
288
cli/examples/client.py
Executable file
288
cli/examples/client.py
Executable file
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AITBC Client CLI Tool - Submit jobs and check status
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import httpx
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# Configuration
|
||||
DEFAULT_COORDINATOR = "http://localhost:8000"
|
||||
DEFAULT_API_KEY = "${CLIENT_API_KEY}"
|
||||
|
||||
class AITBCClient:
|
||||
def __init__(self, coordinator_url: str, api_key: str):
|
||||
self.coordinator_url = coordinator_url
|
||||
self.api_key = api_key
|
||||
self.client = httpx.Client()
|
||||
|
||||
def submit_job(self, job_type: str, task_data: dict, ttl: int = 900) -> Optional[str]:
|
||||
"""Submit a job to the coordinator"""
|
||||
job_payload = {
|
||||
"payload": {
|
||||
"type": job_type,
|
||||
**task_data
|
||||
},
|
||||
"ttl_seconds": ttl
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/jobs",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": self.api_key
|
||||
},
|
||||
json=job_payload
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
job = response.json()
|
||||
return job['job_id']
|
||||
else:
|
||||
print(f"❌ Error submitting job: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def list_transactions(self, limit: int = 10) -> Optional[list]:
|
||||
"""List recent transactions"""
|
||||
try:
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/explorer/transactions",
|
||||
params={"limit": limit}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
transactions = response.json()
|
||||
return transactions.get('items', [])[:limit]
|
||||
else:
|
||||
print(f"❌ Error listing transactions: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def list_receipts(self, limit: int = 10, job_id: Optional[str] = None) -> Optional[list]:
|
||||
"""List recent receipts"""
|
||||
params = {"limit": limit}
|
||||
if job_id:
|
||||
params["job_id"] = job_id
|
||||
try:
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/explorer/receipts",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
receipts = response.json()
|
||||
return receipts.get('items', [])[:limit]
|
||||
else:
|
||||
print(f"❌ Error listing receipts: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def get_job_status(self, job_id: str) -> Optional[dict]:
|
||||
"""Get job status"""
|
||||
try:
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/jobs/{job_id}",
|
||||
headers={"X-Api-Key": self.api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"❌ Error getting status: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def list_blocks(self, limit: int = 10) -> Optional[list]:
|
||||
"""List recent blocks"""
|
||||
try:
|
||||
response = self.client.get(f"{self.coordinator_url}/v1/explorer/blocks")
|
||||
|
||||
if response.status_code == 200:
|
||||
blocks = response.json()
|
||||
return blocks['items'][:limit]
|
||||
else:
|
||||
print(f"❌ Error listing blocks: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AITBC Client CLI")
|
||||
parser.add_argument("--url", default=DEFAULT_COORDINATOR, help="Coordinator URL")
|
||||
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# Submit job command
|
||||
submit_parser = subparsers.add_parser("submit", help="Submit a job")
|
||||
submit_parser.add_argument("type", help="Job type (e.g., inference, training)")
|
||||
submit_parser.add_argument("--task", help="Task description")
|
||||
submit_parser.add_argument("--model", help="Model to use")
|
||||
submit_parser.add_argument("--prompt", help="Prompt for inference")
|
||||
submit_parser.add_argument("--ttl", type=int, default=900, help="TTL in seconds")
|
||||
|
||||
# Status command
|
||||
status_parser = subparsers.add_parser("status", help="Check job status")
|
||||
status_parser.add_argument("job_id", help="Job ID to check")
|
||||
|
||||
# Blocks command
|
||||
blocks_parser = subparsers.add_parser("blocks", help="List recent blocks")
|
||||
blocks_parser.add_argument("--limit", type=int, default=10, help="Number of blocks")
|
||||
|
||||
# Browser command
|
||||
browser_parser = subparsers.add_parser("browser", help="Show latest blocks, transactions, and receipt metrics")
|
||||
browser_parser.add_argument("--block-limit", type=int, default=1, help="Number of blocks")
|
||||
browser_parser.add_argument("--tx-limit", type=int, default=5, help="Number of transactions")
|
||||
browser_parser.add_argument("--receipt-limit", type=int, default=10, help="Number of receipts")
|
||||
browser_parser.add_argument("--job-id", help="Filter receipts by job ID")
|
||||
|
||||
# Quick demo command
|
||||
demo_parser = subparsers.add_parser("demo", help="Submit a demo job")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
client = AITBCClient(args.url, args.api_key)
|
||||
|
||||
if args.command == "submit":
|
||||
task_data = {}
|
||||
if args.task:
|
||||
task_data["task"] = args.task
|
||||
if args.model:
|
||||
task_data["model"] = args.model
|
||||
if args.prompt:
|
||||
task_data["prompt"] = args.prompt
|
||||
task_data["parameters"] = {"prompt": args.prompt}
|
||||
|
||||
print(f"📤 Submitting {args.type} job...")
|
||||
job_id = client.submit_job(args.type, task_data, args.ttl)
|
||||
|
||||
if job_id:
|
||||
print(f"✅ Job submitted successfully!")
|
||||
print(f" Job ID: {job_id}")
|
||||
print(f" Track with: python3 cli/client.py status {job_id}")
|
||||
|
||||
elif args.command == "status":
|
||||
print(f"🔍 Checking status for job {args.job_id}...")
|
||||
status = client.get_job_status(args.job_id)
|
||||
|
||||
if status:
|
||||
print(f"📊 Job Status:")
|
||||
print(f" ID: {status['job_id']}")
|
||||
print(f" State: {status['state']}")
|
||||
print(f" Miner: {status.get('assigned_miner_id', 'None')}")
|
||||
print(f" Created: {status['requested_at']}")
|
||||
if status.get('expires_at'):
|
||||
print(f" Expires: {status['expires_at']}")
|
||||
|
||||
elif args.command == "blocks":
|
||||
print(f"📦 Recent blocks (last {args.limit}):")
|
||||
blocks = client.list_blocks(args.limit)
|
||||
|
||||
if blocks:
|
||||
for i, block in enumerate(blocks, 1):
|
||||
print(f"\n{i}. Height: {block['height']}")
|
||||
print(f" Hash: {block['hash']}")
|
||||
print(f" Time: {block['timestamp']}")
|
||||
print(f" Proposer: {block['proposer']}")
|
||||
|
||||
elif args.command == "browser":
|
||||
blocks = client.list_blocks(args.block_limit) or []
|
||||
transactions = client.list_transactions(args.tx_limit) or []
|
||||
receipts = client.list_receipts(args.receipt_limit, job_id=args.job_id) or []
|
||||
|
||||
print("🧭 Blockchain Browser Snapshot")
|
||||
if blocks:
|
||||
block = blocks[0]
|
||||
tx_count = block.get("txCount", block.get("tx_count"))
|
||||
print("\n🧱 Latest Block")
|
||||
print(f" Height: {block.get('height')}")
|
||||
print(f" Hash: {block.get('hash')}")
|
||||
print(f" Time: {block.get('timestamp')}")
|
||||
print(f" Tx Count: {tx_count}")
|
||||
print(f" Proposer: {block.get('proposer')}")
|
||||
else:
|
||||
print("\n🧱 Latest Block: none found")
|
||||
|
||||
print("\n🧾 Latest Transactions")
|
||||
if not transactions:
|
||||
print(" No transactions found")
|
||||
for tx in transactions:
|
||||
tx_hash = tx.get("hash") or tx.get("tx_hash")
|
||||
from_addr = tx.get("from") or tx.get("from_address")
|
||||
to_addr = tx.get("to") or tx.get("to_address")
|
||||
value = tx.get("value")
|
||||
status = tx.get("status")
|
||||
block_ref = tx.get("block")
|
||||
print(f" - {tx_hash} | block {block_ref} | {status}")
|
||||
print(f" from: {from_addr} -> to: {to_addr} | value: {value}")
|
||||
|
||||
print("\n📈 Receipt Metrics (recent)")
|
||||
if not receipts:
|
||||
print(" No receipts found")
|
||||
else:
|
||||
status_counts = {}
|
||||
total_units = 0.0
|
||||
unit_type = None
|
||||
for receipt in receipts:
|
||||
status_label = receipt.get("status") or receipt.get("state") or "Unknown"
|
||||
status_counts[status_label] = status_counts.get(status_label, 0) + 1
|
||||
payload = receipt.get("payload") or {}
|
||||
units = payload.get("units")
|
||||
if isinstance(units, (int, float)):
|
||||
total_units += float(units)
|
||||
if unit_type is None:
|
||||
unit_type = payload.get("unit_type")
|
||||
|
||||
print(f" Receipts: {len(receipts)}")
|
||||
for status_label, count in status_counts.items():
|
||||
print(f" {status_label}: {count}")
|
||||
if total_units:
|
||||
unit_suffix = f" {unit_type}" if unit_type else ""
|
||||
print(f" Total Units: {total_units}{unit_suffix}")
|
||||
|
||||
elif args.command == "demo":
|
||||
print("🎭 Submitting demo inference job...")
|
||||
job_id = client.submit_job("inference", {
|
||||
"task": "text-generation",
|
||||
"model": "llama-2-7b",
|
||||
"prompt": "What is AITBC?",
|
||||
"parameters": {"max_tokens": 100}
|
||||
})
|
||||
|
||||
if job_id:
|
||||
print(f"✅ Demo job submitted!")
|
||||
print(f" Job ID: {job_id}")
|
||||
|
||||
# Check status after a moment
|
||||
import time
|
||||
time.sleep(2)
|
||||
status = client.get_job_status(job_id)
|
||||
if status:
|
||||
print(f"\n📊 Status: {status['state']}")
|
||||
print(f" Miner: {status.get('assigned_miner_id', 'unassigned')}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
328
cli/examples/client_enhanced.py
Normal file
328
cli/examples/client_enhanced.py
Normal file
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AITBC Client CLI Tool - Enhanced version with output formatting
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import httpx
|
||||
import json
|
||||
import sys
|
||||
import yaml
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from tabulate import tabulate
|
||||
|
||||
# Configuration
|
||||
DEFAULT_COORDINATOR = "http://127.0.0.1:18000"
|
||||
DEFAULT_API_KEY = "${CLIENT_API_KEY}"
|
||||
|
||||
|
||||
class OutputFormatter:
|
||||
"""Handle different output formats"""
|
||||
|
||||
@staticmethod
|
||||
def format(data: Any, format_type: str = "table") -> str:
|
||||
"""Format data according to specified type"""
|
||||
if format_type == "json":
|
||||
return json.dumps(data, indent=2, default=str)
|
||||
elif format_type == "yaml":
|
||||
return yaml.dump(data, default_flow_style=False, sort_keys=False)
|
||||
elif format_type == "table":
|
||||
return OutputFormatter._format_table(data)
|
||||
else:
|
||||
return str(data)
|
||||
|
||||
@staticmethod
|
||||
def _format_table(data: Any) -> str:
|
||||
"""Format data as table"""
|
||||
if isinstance(data, dict):
|
||||
# Simple key-value table
|
||||
rows = [[k, v] for k, v in data.items()]
|
||||
return tabulate(rows, headers=["Key", "Value"], tablefmt="grid")
|
||||
elif isinstance(data, list) and data:
|
||||
if all(isinstance(item, dict) for item in data):
|
||||
# Table from list of dicts
|
||||
headers = list(data[0].keys())
|
||||
rows = [[item.get(h, "") for h in headers] for item in data]
|
||||
return tabulate(rows, headers=headers, tablefmt="grid")
|
||||
else:
|
||||
# Simple list
|
||||
return "\n".join(f"• {item}" for item in data)
|
||||
else:
|
||||
return str(data)
|
||||
|
||||
|
||||
class AITBCClient:
|
||||
def __init__(self, coordinator_url: str, api_key: str):
|
||||
self.coordinator_url = coordinator_url
|
||||
self.api_key = api_key
|
||||
self.client = httpx.Client()
|
||||
|
||||
def submit_job(self, job_type: str, task_data: dict, ttl: int = 900) -> Optional[str]:
|
||||
"""Submit a job to the coordinator"""
|
||||
job_payload = {
|
||||
"payload": {
|
||||
"type": job_type,
|
||||
**task_data
|
||||
},
|
||||
"ttl_seconds": ttl
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/jobs",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": self.api_key
|
||||
},
|
||||
json=job_payload
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
job = response.json()
|
||||
return job['job_id']
|
||||
else:
|
||||
print(f"❌ Error submitting job: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def get_job_status(self, job_id: str) -> Optional[Dict]:
|
||||
"""Get job status"""
|
||||
try:
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/jobs/{job_id}",
|
||||
headers={"X-Api-Key": self.api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"❌ Error getting status: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def list_blocks(self, limit: int = 10) -> Optional[list]:
|
||||
"""List recent blocks"""
|
||||
try:
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/explorer/blocks",
|
||||
params={"limit": limit},
|
||||
headers={"X-Api-Key": self.api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"❌ Error getting blocks: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def list_transactions(self, limit: int = 10) -> Optional[list]:
|
||||
"""List recent transactions"""
|
||||
try:
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/explorer/transactions",
|
||||
params={"limit": limit},
|
||||
headers={"X-Api-Key": self.api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"❌ Error getting transactions: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def list_receipts(self, limit: int = 10, job_id: str = None) -> Optional[list]:
|
||||
"""List job receipts"""
|
||||
try:
|
||||
params = {"limit": limit}
|
||||
if job_id:
|
||||
params["job_id"] = job_id
|
||||
|
||||
response = self.client.get(
|
||||
f"{self.coordinator_url}/v1/explorer/receipts",
|
||||
params=params,
|
||||
headers={"X-Api-Key": self.api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"❌ Error getting receipts: {response.status_code}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def cancel_job(self, job_id: str) -> bool:
|
||||
"""Cancel a job"""
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/jobs/{job_id}/cancel",
|
||||
headers={"X-Api-Key": self.api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Error cancelling job: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AITBC Client CLI Tool")
|
||||
parser.add_argument("--url", default=DEFAULT_COORDINATOR, help="Coordinator URL")
|
||||
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key")
|
||||
parser.add_argument("--output", choices=["table", "json", "yaml"],
|
||||
default="table", help="Output format")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Submit command
|
||||
submit_parser = subparsers.add_parser("submit", help="Submit a job")
|
||||
submit_parser.add_argument("type", help="Job type (e.g., inference, training)")
|
||||
submit_parser.add_argument("--prompt", help="Prompt for inference jobs")
|
||||
submit_parser.add_argument("--model", help="Model name")
|
||||
submit_parser.add_argument("--ttl", type=int, default=900, help="Time to live (seconds)")
|
||||
submit_parser.add_argument("--file", type=argparse.FileType('r'),
|
||||
help="Submit job from JSON file")
|
||||
|
||||
# Status command
|
||||
status_parser = subparsers.add_parser("status", help="Check job status")
|
||||
status_parser.add_argument("job_id", help="Job ID")
|
||||
|
||||
# Blocks command
|
||||
blocks_parser = subparsers.add_parser("blocks", help="List recent blocks")
|
||||
blocks_parser.add_argument("--limit", type=int, default=10, help="Number of blocks")
|
||||
|
||||
# Browser command
|
||||
browser_parser = subparsers.add_parser("browser", help="Browse blockchain")
|
||||
browser_parser.add_argument("--block-limit", type=int, default=5, help="Block limit")
|
||||
browser_parser.add_argument("--tx-limit", type=int, default=10, help="Transaction limit")
|
||||
browser_parser.add_argument("--receipt-limit", type=int, default=10, help="Receipt limit")
|
||||
browser_parser.add_argument("--job-id", help="Filter by job ID")
|
||||
|
||||
# Cancel command
|
||||
cancel_parser = subparsers.add_parser("cancel", help="Cancel a job")
|
||||
cancel_parser.add_argument("job_id", help="Job ID")
|
||||
|
||||
# Receipts command
|
||||
receipts_parser = subparsers.add_parser("receipts", help="List receipts")
|
||||
receipts_parser.add_argument("--limit", type=int, default=10, help="Number of receipts")
|
||||
receipts_parser.add_argument("--job-id", help="Filter by job ID")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Create client
|
||||
client = AITBCClient(args.url, args.api_key)
|
||||
|
||||
# Execute command
|
||||
if args.command == "submit":
|
||||
# Build job data
|
||||
if args.file:
|
||||
try:
|
||||
task_data = json.load(args.file)
|
||||
except Exception as e:
|
||||
print(f"❌ Error reading job file: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
task_data = {"type": args.type}
|
||||
if args.prompt:
|
||||
task_data["prompt"] = args.prompt
|
||||
if args.model:
|
||||
task_data["model"] = args.model
|
||||
|
||||
# Submit job
|
||||
job_id = client.submit_job(args.type, task_data, args.ttl)
|
||||
|
||||
if job_id:
|
||||
result = {
|
||||
"status": "success",
|
||||
"job_id": job_id,
|
||||
"message": "Job submitted successfully",
|
||||
"track_command": f"python3 cli/client_enhanced.py status {job_id}"
|
||||
}
|
||||
print(OutputFormatter.format(result, args.output))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "status":
|
||||
status = client.get_job_status(args.job_id)
|
||||
|
||||
if status:
|
||||
print(OutputFormatter.format(status, args.output))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "blocks":
|
||||
blocks = client.list_blocks(args.limit)
|
||||
|
||||
if blocks:
|
||||
print(OutputFormatter.format(blocks, args.output))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "browser":
|
||||
blocks = client.list_blocks(args.block_limit) or []
|
||||
transactions = client.list_transactions(args.tx_limit) or []
|
||||
receipts = client.list_receipts(args.receipt_limit, job_id=args.job_id) or []
|
||||
|
||||
result = {
|
||||
"latest_block": blocks[0] if blocks else None,
|
||||
"recent_transactions": transactions,
|
||||
"recent_receipts": receipts
|
||||
}
|
||||
|
||||
print(OutputFormatter.format(result, args.output))
|
||||
sys.exit(0)
|
||||
|
||||
elif args.command == "cancel":
|
||||
if client.cancel_job(args.job_id):
|
||||
result = {
|
||||
"status": "success",
|
||||
"job_id": args.job_id,
|
||||
"message": "Job cancelled successfully"
|
||||
}
|
||||
print(OutputFormatter.format(result, args.output))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "receipts":
|
||||
receipts = client.list_receipts(args.limit, args.job_id)
|
||||
|
||||
if receipts:
|
||||
print(OutputFormatter.format(receipts, args.output))
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
259
cli/examples/miner.py
Executable file
259
cli/examples/miner.py
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AITBC Miner CLI Tool - Register, poll for jobs, and submit results
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import httpx
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# Configuration
|
||||
DEFAULT_COORDINATOR = "http://localhost:8000"
|
||||
DEFAULT_API_KEY = "${MINER_API_KEY}"
|
||||
DEFAULT_MINER_ID = "cli-miner"
|
||||
|
||||
class AITBCMiner:
|
||||
def __init__(self, coordinator_url: str, api_key: str, miner_id: str):
|
||||
self.coordinator_url = coordinator_url
|
||||
self.api_key = api_key
|
||||
self.miner_id = miner_id
|
||||
self.client = httpx.Client()
|
||||
|
||||
def register(self, capabilities: dict) -> bool:
|
||||
"""Register miner with coordinator"""
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/miners/register?miner_id={self.miner_id}",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": self.api_key
|
||||
},
|
||||
json={"capabilities": capabilities}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Miner {self.miner_id} registered successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Registration failed: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def poll_job(self, max_wait: int = 5) -> Optional[dict]:
|
||||
"""Poll for available jobs"""
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/miners/poll",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": self.api_key
|
||||
},
|
||||
json={"max_wait_seconds": max_wait}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
elif response.status_code == 204:
|
||||
return None
|
||||
else:
|
||||
print(f"❌ Poll failed: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return None
|
||||
|
||||
def submit_result(self, job_id: str, result: dict, metrics: dict = None) -> bool:
|
||||
"""Submit job result"""
|
||||
payload = {
|
||||
"result": result
|
||||
}
|
||||
if metrics:
|
||||
payload["metrics"] = metrics
|
||||
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/miners/{job_id}/result",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": self.api_key
|
||||
},
|
||||
json=payload
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Result submitted for job {job_id}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Submit failed: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def send_heartbeat(self) -> bool:
|
||||
"""Send heartbeat to coordinator"""
|
||||
heartbeat_data = {
|
||||
"status": "ONLINE",
|
||||
"inflight": 0,
|
||||
"metadata": {
|
||||
"last_seen": datetime.utcnow().isoformat(),
|
||||
"gpu_utilization": 75,
|
||||
"gpu_memory_used": 8000,
|
||||
"gpu_temperature": 65
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.coordinator_url}/v1/miners/heartbeat?miner_id={self.miner_id}",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": self.api_key
|
||||
},
|
||||
json=heartbeat_data
|
||||
)
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Heartbeat error: {e}")
|
||||
return False
|
||||
|
||||
def mine_continuous(self, max_jobs: int = None, simulate_work: bool = True):
|
||||
"""Continuously mine jobs"""
|
||||
print(f"⛏️ Starting continuous mining...")
|
||||
print(f" Miner ID: {self.miner_id}")
|
||||
print(f" Max jobs: {max_jobs or 'unlimited'}")
|
||||
print()
|
||||
|
||||
jobs_completed = 0
|
||||
|
||||
try:
|
||||
while max_jobs is None or jobs_completed < max_jobs:
|
||||
# Poll for job
|
||||
print("🔍 Polling for jobs...")
|
||||
job = self.poll_job()
|
||||
|
||||
if job:
|
||||
print(f"✅ Got job: {job['job_id']}")
|
||||
print(f" Type: {job['payload'].get('type', 'unknown')}")
|
||||
|
||||
if simulate_work:
|
||||
print("⚙️ Processing job...")
|
||||
time.sleep(2) # Simulate work
|
||||
|
||||
# Submit result
|
||||
result = {
|
||||
"status": "completed",
|
||||
"output": f"Job {job['job_id']} processed by {self.miner_id}",
|
||||
"execution_time_ms": 2000,
|
||||
"miner_id": self.miner_id
|
||||
}
|
||||
|
||||
metrics = {
|
||||
"compute_time": 2.0,
|
||||
"energy_used": 0.1,
|
||||
"aitbc_earned": 10.0
|
||||
}
|
||||
|
||||
if self.submit_result(job['job_id'], result, metrics):
|
||||
jobs_completed += 1
|
||||
print(f"💰 Earned 10 AITBC!")
|
||||
print(f" Total jobs completed: {jobs_completed}")
|
||||
|
||||
# Check if this job is now a block with proposer
|
||||
print("🔍 Checking block status...")
|
||||
time.sleep(1)
|
||||
else:
|
||||
print("💤 No jobs available, sending heartbeat...")
|
||||
self.send_heartbeat()
|
||||
|
||||
print("-" * 50)
|
||||
time.sleep(3) # Wait before next poll
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⏹️ Mining stopped by user")
|
||||
print(f" Total jobs completed: {jobs_completed}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AITBC Miner CLI")
|
||||
parser.add_argument("--url", default=DEFAULT_COORDINATOR, help="Coordinator URL")
|
||||
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key")
|
||||
parser.add_argument("--miner-id", default=DEFAULT_MINER_ID, help="Miner ID")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# Register command
|
||||
register_parser = subparsers.add_parser("register", help="Register miner")
|
||||
register_parser.add_argument("--gpu", default="RTX 4060 Ti", help="GPU model")
|
||||
register_parser.add_argument("--memory", type=int, default=16, help="GPU memory GB")
|
||||
|
||||
# Poll command
|
||||
poll_parser = subparsers.add_parser("poll", help="Poll for a job")
|
||||
poll_parser.add_argument("--wait", type=int, default=5, help="Max wait seconds")
|
||||
|
||||
# Mine command
|
||||
mine_parser = subparsers.add_parser("mine", help="Mine continuously")
|
||||
mine_parser.add_argument("--jobs", type=int, help="Max jobs to complete")
|
||||
mine_parser.add_argument("--no-simulate", action="store_true", help="Don't simulate work")
|
||||
|
||||
# Heartbeat command
|
||||
heartbeat_parser = subparsers.add_parser("heartbeat", help="Send heartbeat")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
miner = AITBCMiner(args.url, args.api_key, args.miner_id)
|
||||
|
||||
if args.command == "register":
|
||||
capabilities = {
|
||||
"gpu": {
|
||||
"model": args.gpu,
|
||||
"memory_gb": args.memory,
|
||||
"cuda_version": "12.4"
|
||||
},
|
||||
"compute": {
|
||||
"type": "GPU",
|
||||
"platform": "CUDA",
|
||||
"supported_tasks": ["inference", "training"],
|
||||
"max_concurrent_jobs": 1
|
||||
}
|
||||
}
|
||||
miner.register(capabilities)
|
||||
|
||||
elif args.command == "poll":
|
||||
print(f"🔍 Polling for jobs (max wait: {args.wait}s)...")
|
||||
job = miner.poll_job(args.wait)
|
||||
|
||||
if job:
|
||||
print(f"✅ Received job:")
|
||||
print(json.dumps(job, indent=2))
|
||||
else:
|
||||
print("💤 No jobs available")
|
||||
|
||||
elif args.command == "mine":
|
||||
miner.mine_continuous(args.jobs, not args.no_simulate)
|
||||
|
||||
elif args.command == "heartbeat":
|
||||
if miner.send_heartbeat():
|
||||
print("💓 Heartbeat sent successfully")
|
||||
else:
|
||||
print("❌ Heartbeat failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
158
cli/examples/wallet.py
Executable file
158
cli/examples/wallet.py
Executable file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AITBC Wallet CLI Tool - Track earnings and manage wallet
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
class AITBCWallet:
|
||||
def __init__(self, wallet_file: str = None):
|
||||
if wallet_file is None:
|
||||
wallet_file = os.path.expanduser("~/.aitbc_wallet.json")
|
||||
|
||||
self.wallet_file = wallet_file
|
||||
self.data = self._load_wallet()
|
||||
|
||||
def _load_wallet(self) -> dict:
|
||||
"""Load wallet data from file"""
|
||||
if os.path.exists(self.wallet_file):
|
||||
try:
|
||||
with open(self.wallet_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Create new wallet
|
||||
return {
|
||||
"address": "aitbc1" + os.urandom(10).hex(),
|
||||
"balance": 0.0,
|
||||
"transactions": [],
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def save(self):
|
||||
"""Save wallet to file"""
|
||||
with open(self.wallet_file, 'w') as f:
|
||||
json.dump(self.data, f, indent=2)
|
||||
|
||||
def add_earnings(self, amount: float, job_id: str, description: str = ""):
|
||||
"""Add earnings from completed job"""
|
||||
transaction = {
|
||||
"type": "earn",
|
||||
"amount": amount,
|
||||
"job_id": job_id,
|
||||
"description": description or f"Job {job_id}",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.data["transactions"].append(transaction)
|
||||
self.data["balance"] += amount
|
||||
self.save()
|
||||
|
||||
print(f"💰 Added {amount} AITBC to wallet")
|
||||
print(f" New balance: {self.data['balance']} AITBC")
|
||||
|
||||
def spend(self, amount: float, description: str):
|
||||
"""Spend AITBC"""
|
||||
if self.data["balance"] < amount:
|
||||
print(f"❌ Insufficient balance!")
|
||||
print(f" Balance: {self.data['balance']} AITBC")
|
||||
print(f" Needed: {amount} AITBC")
|
||||
return False
|
||||
|
||||
transaction = {
|
||||
"type": "spend",
|
||||
"amount": -amount,
|
||||
"description": description,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.data["transactions"].append(transaction)
|
||||
self.data["balance"] -= amount
|
||||
self.save()
|
||||
|
||||
print(f"💸 Spent {amount} AITBC")
|
||||
print(f" Remaining: {self.data['balance']} AITBC")
|
||||
return True
|
||||
|
||||
def show_balance(self):
|
||||
"""Show wallet balance"""
|
||||
print(f"💳 Wallet Address: {self.data['address']}")
|
||||
print(f"💰 Balance: {self.data['balance']} AITBC")
|
||||
print(f"📊 Total Transactions: {len(self.data['transactions'])}")
|
||||
|
||||
def show_history(self, limit: int = 10):
|
||||
"""Show transaction history"""
|
||||
transactions = self.data["transactions"][-limit:]
|
||||
|
||||
if not transactions:
|
||||
print("📭 No transactions yet")
|
||||
return
|
||||
|
||||
print(f"📜 Recent Transactions (last {limit}):")
|
||||
print("-" * 60)
|
||||
|
||||
for tx in reversed(transactions):
|
||||
symbol = "💰" if tx["type"] == "earn" else "💸"
|
||||
print(f"{symbol} {tx['amount']:+8.2f} AITBC | {tx.get('description', 'N/A')}")
|
||||
print(f" 📅 {tx['timestamp']}")
|
||||
if "job_id" in tx:
|
||||
print(f" 🆔 Job: {tx['job_id']}")
|
||||
print()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AITBC Wallet CLI")
|
||||
parser.add_argument("--wallet", help="Wallet file path")
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# Balance command
|
||||
balance_parser = subparsers.add_parser("balance", help="Show balance")
|
||||
|
||||
# History command
|
||||
history_parser = subparsers.add_parser("history", help="Show transaction history")
|
||||
history_parser.add_argument("--limit", type=int, default=10, help="Number of transactions")
|
||||
|
||||
# Earn command
|
||||
earn_parser = subparsers.add_parser("earn", help="Add earnings")
|
||||
earn_parser.add_argument("amount", type=float, help="Amount earned")
|
||||
earn_parser.add_argument("--job", help="Job ID")
|
||||
earn_parser.add_argument("--desc", help="Description")
|
||||
|
||||
# Spend command
|
||||
spend_parser = subparsers.add_parser("spend", help="Spend AITBC")
|
||||
spend_parser.add_argument("amount", type=float, help="Amount to spend")
|
||||
spend_parser.add_argument("description", help="What you're spending on")
|
||||
|
||||
# Address command
|
||||
address_parser = subparsers.add_parser("address", help="Show wallet address")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
wallet = AITBCWallet(args.wallet)
|
||||
|
||||
if args.command == "balance":
|
||||
wallet.show_balance()
|
||||
|
||||
elif args.command == "history":
|
||||
wallet.show_history(args.limit)
|
||||
|
||||
elif args.command == "earn":
|
||||
wallet.add_earnings(args.amount, args.job or "unknown", args.desc or "")
|
||||
|
||||
elif args.command == "spend":
|
||||
wallet.spend(args.amount, args.description)
|
||||
|
||||
elif args.command == "address":
|
||||
print(f"💳 Wallet Address: {wallet.data['address']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user