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:
oib
2026-03-02 15:38:25 +01:00
parent af185cdd8b
commit ccedbace53
271 changed files with 35942 additions and 2359 deletions

288
cli/examples/client.py Executable file
View 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()

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