remove unused queue.py module and refactor chain_id handling in CLI
Deleted aitbc/queue.py containing TaskQueue, JobScheduler, BackgroundTaskManager, and WorkerPool classes that were not being used in the codebase. Refactored chain_id handling in CLI to use centralized get_chain_id utility function instead of duplicating chain_id detection logic in send_transaction, get_balance, and agent_operations functions.
This commit is contained in:
431
aitbc/queue.py
431
aitbc/queue.py
@@ -1,431 +0,0 @@
|
||||
"""
|
||||
Queue utilities for AITBC
|
||||
Provides task queue helpers, job scheduling, and background task management
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import heapq
|
||||
import time
|
||||
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
import uuid
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class JobStatus(Enum):
|
||||
"""Job status enumeration"""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class JobPriority(Enum):
|
||||
"""Job priority levels"""
|
||||
LOW = 1
|
||||
MEDIUM = 2
|
||||
HIGH = 3
|
||||
CRITICAL = 4
|
||||
|
||||
|
||||
@dataclass(order=True)
|
||||
class Job:
|
||||
"""Background job"""
|
||||
priority: int
|
||||
job_id: str = field(compare=False)
|
||||
func: Callable = field(compare=False)
|
||||
args: tuple = field(default_factory=tuple, compare=False)
|
||||
kwargs: dict = field(default_factory=dict, compare=False)
|
||||
status: JobStatus = field(default=JobStatus.PENDING, compare=False)
|
||||
created_at: datetime = field(default_factory=datetime.utcnow, compare=False)
|
||||
started_at: Optional[datetime] = field(default=None, compare=False)
|
||||
completed_at: Optional[datetime] = field(default=None, compare=False)
|
||||
result: Any = field(default=None, compare=False)
|
||||
error: Optional[str] = field(default=None, compare=False)
|
||||
retry_count: int = field(default=0, compare=False)
|
||||
max_retries: int = field(default=3, compare=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.job_id is None:
|
||||
self.job_id = str(uuid.uuid4())
|
||||
|
||||
|
||||
class TaskQueue:
|
||||
"""Priority-based task queue"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize task queue"""
|
||||
self.queue: List[Job] = []
|
||||
self.jobs: Dict[str, Job] = {}
|
||||
self.lock = asyncio.Lock()
|
||||
|
||||
async def enqueue(
|
||||
self,
|
||||
func: Callable,
|
||||
args: tuple = (),
|
||||
kwargs: dict = None,
|
||||
priority: JobPriority = JobPriority.MEDIUM,
|
||||
max_retries: int = 3
|
||||
) -> str:
|
||||
"""Enqueue a task"""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
job = Job(
|
||||
priority=priority.value,
|
||||
func=func,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
max_retries=max_retries
|
||||
)
|
||||
|
||||
async with self.lock:
|
||||
heapq.heappush(self.queue, job)
|
||||
self.jobs[job.job_id] = job
|
||||
|
||||
return job.job_id
|
||||
|
||||
async def dequeue(self) -> Optional[Job]:
|
||||
"""Dequeue a task"""
|
||||
async with self.lock:
|
||||
if not self.queue:
|
||||
return None
|
||||
|
||||
job = heapq.heappop(self.queue)
|
||||
return job
|
||||
|
||||
async def get_job(self, job_id: str) -> Optional[Job]:
|
||||
"""Get job by ID"""
|
||||
return self.jobs.get(job_id)
|
||||
|
||||
async def cancel_job(self, job_id: str) -> bool:
|
||||
"""Cancel a job"""
|
||||
async with self.lock:
|
||||
job = self.jobs.get(job_id)
|
||||
if job and job.status == JobStatus.PENDING:
|
||||
job.status = JobStatus.CANCELLED
|
||||
# Remove from queue
|
||||
self.queue = [j for j in self.queue if j.job_id != job_id]
|
||||
heapq.heapify(self.queue)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_queue_size(self) -> int:
|
||||
"""Get queue size"""
|
||||
return len(self.queue)
|
||||
|
||||
async def get_jobs_by_status(self, status: JobStatus) -> List[Job]:
|
||||
"""Get jobs by status"""
|
||||
return [job for job in self.jobs.values() if job.status == status]
|
||||
|
||||
|
||||
class JobScheduler:
|
||||
"""Job scheduler for delayed and recurring tasks"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize job scheduler"""
|
||||
self.scheduled_jobs: Dict[str, Dict[str, Any]] = {}
|
||||
self.running = False
|
||||
self.task: Optional[asyncio.Task] = None
|
||||
|
||||
async def schedule(
|
||||
self,
|
||||
func: Callable,
|
||||
delay: float = 0,
|
||||
interval: Optional[float] = None,
|
||||
job_id: Optional[str] = None,
|
||||
args: tuple = (),
|
||||
kwargs: dict = None
|
||||
) -> str:
|
||||
"""Schedule a job"""
|
||||
if job_id is None:
|
||||
job_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
run_at = time.time() + delay
|
||||
|
||||
self.scheduled_jobs[job_id] = {
|
||||
"func": func,
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
"run_at": run_at,
|
||||
"interval": interval,
|
||||
"job_id": job_id
|
||||
}
|
||||
|
||||
return job_id
|
||||
|
||||
async def cancel_scheduled_job(self, job_id: str) -> bool:
|
||||
"""Cancel a scheduled job"""
|
||||
if job_id in self.scheduled_jobs:
|
||||
del self.scheduled_jobs[job_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the scheduler"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.task = asyncio.create_task(self._run_scheduler())
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the scheduler"""
|
||||
self.running = False
|
||||
if self.task:
|
||||
self.task.cancel()
|
||||
try:
|
||||
await self.task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _run_scheduler(self) -> None:
|
||||
"""Run the scheduler loop"""
|
||||
while self.running:
|
||||
now = time.time()
|
||||
to_run = []
|
||||
|
||||
for job_id, job in list(self.scheduled_jobs.items()):
|
||||
if job["run_at"] <= now:
|
||||
to_run.append(job)
|
||||
|
||||
for job in to_run:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(job["func"]):
|
||||
await job["func"](*job["args"], **job["kwargs"])
|
||||
else:
|
||||
job["func"](*job["args"], **job["kwargs"])
|
||||
|
||||
if job["interval"]:
|
||||
job["run_at"] = now + job["interval"]
|
||||
else:
|
||||
del self.scheduled_jobs[job["job_id"]]
|
||||
except Exception as e:
|
||||
print(f"Error running scheduled job {job['job_id']}: {e}")
|
||||
if not job["interval"]:
|
||||
del self.scheduled_jobs[job["job_id"]]
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
class BackgroundTaskManager:
|
||||
"""Manage background tasks"""
|
||||
|
||||
def __init__(self, max_concurrent_tasks: int = 10):
|
||||
"""Initialize background task manager"""
|
||||
self.max_concurrent_tasks = max_concurrent_tasks
|
||||
self.semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
||||
self.tasks: Dict[str, asyncio.Task] = {}
|
||||
self.task_info: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
async def run_task(
|
||||
self,
|
||||
func: Callable,
|
||||
task_id: Optional[str] = None,
|
||||
args: tuple = (),
|
||||
kwargs: dict = None
|
||||
) -> str:
|
||||
"""Run a background task"""
|
||||
if task_id is None:
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
async def wrapped_task():
|
||||
async with self.semaphore:
|
||||
try:
|
||||
self.task_info[task_id]["status"] = "running"
|
||||
self.task_info[task_id]["started_at"] = datetime.utcnow()
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
result = await func(*args, **kwargs)
|
||||
else:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
self.task_info[task_id]["status"] = "completed"
|
||||
self.task_info[task_id]["result"] = result
|
||||
self.task_info[task_id]["completed_at"] = datetime.utcnow()
|
||||
except Exception as e:
|
||||
self.task_info[task_id]["status"] = "failed"
|
||||
self.task_info[task_id]["error"] = str(e)
|
||||
self.task_info[task_id]["completed_at"] = datetime.utcnow()
|
||||
finally:
|
||||
if task_id in self.tasks:
|
||||
del self.tasks[task_id]
|
||||
|
||||
self.task_info[task_id] = {
|
||||
"status": "pending",
|
||||
"created_at": datetime.utcnow(),
|
||||
"started_at": None,
|
||||
"completed_at": None,
|
||||
"result": None,
|
||||
"error": None
|
||||
}
|
||||
|
||||
task = asyncio.create_task(wrapped_task())
|
||||
self.tasks[task_id] = task
|
||||
|
||||
return task_id
|
||||
|
||||
async def cancel_task(self, task_id: str) -> bool:
|
||||
"""Cancel a background task"""
|
||||
if task_id in self.tasks:
|
||||
self.tasks[task_id].cancel()
|
||||
try:
|
||||
await self.tasks[task_id]
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self.task_info[task_id]["status"] = "cancelled"
|
||||
self.task_info[task_id]["completed_at"] = datetime.utcnow()
|
||||
del self.tasks[task_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get task status"""
|
||||
return self.task_info.get(task_id)
|
||||
|
||||
async def get_all_tasks(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get all tasks"""
|
||||
return self.task_info.copy()
|
||||
|
||||
async def wait_for_task(self, task_id: str, timeout: Optional[float] = None) -> Any:
|
||||
"""Wait for task completion"""
|
||||
if task_id not in self.tasks:
|
||||
raise ValueError(f"Task {task_id} not found")
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self.tasks[task_id], timeout)
|
||||
except asyncio.TimeoutError:
|
||||
await self.cancel_task(task_id)
|
||||
raise TimeoutError(f"Task {task_id} timed out")
|
||||
|
||||
info = self.task_info.get(task_id)
|
||||
if info["status"] == "failed":
|
||||
raise Exception(info["error"])
|
||||
|
||||
return info["result"]
|
||||
|
||||
|
||||
class WorkerPool:
|
||||
"""Worker pool for parallel task execution"""
|
||||
|
||||
def __init__(self, num_workers: int = 4):
|
||||
"""Initialize worker pool"""
|
||||
self.num_workers = num_workers
|
||||
self.queue: asyncio.Queue = asyncio.Queue()
|
||||
self.workers: List[asyncio.Task] = []
|
||||
self.running = False
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start worker pool"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
for i in range(self.num_workers):
|
||||
worker = asyncio.create_task(self._worker(i))
|
||||
self.workers.append(worker)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop worker pool"""
|
||||
self.running = False
|
||||
|
||||
# Cancel all workers
|
||||
for worker in self.workers:
|
||||
worker.cancel()
|
||||
|
||||
# Wait for workers to finish
|
||||
await asyncio.gather(*self.workers, return_exceptions=True)
|
||||
self.workers.clear()
|
||||
|
||||
async def submit(self, func: Callable, *args, **kwargs) -> Any:
|
||||
"""Submit task to worker pool"""
|
||||
future = asyncio.Future()
|
||||
await self.queue.put((func, args, kwargs, future))
|
||||
return await future
|
||||
|
||||
async def _worker(self, worker_id: int) -> None:
|
||||
"""Worker coroutine"""
|
||||
while self.running:
|
||||
try:
|
||||
func, args, kwargs, future = await self.queue.get()
|
||||
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
result = await func(*args, **kwargs)
|
||||
else:
|
||||
result = func(*args, **kwargs)
|
||||
future.set_result(result)
|
||||
except Exception as e:
|
||||
future.set_exception(e)
|
||||
finally:
|
||||
self.queue.task_done()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Worker {worker_id} error: {e}")
|
||||
|
||||
async def get_queue_size(self) -> int:
|
||||
"""Get queue size"""
|
||||
return self.queue.qsize()
|
||||
|
||||
|
||||
def debounce(delay: float = 0.5):
|
||||
"""Decorator to debounce function calls"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
last_called = [0]
|
||||
timer = [None]
|
||||
|
||||
async def wrapped(*args, **kwargs):
|
||||
async def call():
|
||||
await asyncio.sleep(delay)
|
||||
if asyncio.get_event_loop().time() - last_called[0] >= delay:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
last_called[0] = asyncio.get_event_loop().time()
|
||||
if timer[0]:
|
||||
timer[0].cancel()
|
||||
|
||||
timer[0] = asyncio.create_task(call())
|
||||
return await timer[0]
|
||||
|
||||
return wrapped
|
||||
return decorator
|
||||
|
||||
|
||||
def throttle(calls_per_second: float = 1.0):
|
||||
"""Decorator to throttle function calls"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
min_interval = 1.0 / calls_per_second
|
||||
last_called = [0]
|
||||
|
||||
async def wrapped(*args, **kwargs):
|
||||
now = asyncio.get_event_loop().time()
|
||||
elapsed = now - last_called[0]
|
||||
|
||||
if elapsed < min_interval:
|
||||
await asyncio.sleep(min_interval - elapsed)
|
||||
|
||||
last_called[0] = asyncio.get_event_loop().time()
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
return decorator
|
||||
@@ -214,18 +214,9 @@ def send_transaction(from_wallet: str, to_address: str, amount: float, fee: floa
|
||||
print(f"Error decrypting wallet: {e}")
|
||||
return None
|
||||
|
||||
# Get chain_id from RPC health endpoint
|
||||
chain_id = "ait-testnet" # Default
|
||||
try:
|
||||
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5)
|
||||
health_data = http_client.get("/health")
|
||||
supported_chains = health_data.get("supported_chains", [])
|
||||
if supported_chains:
|
||||
chain_id = supported_chains[0]
|
||||
except NetworkError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# Get chain_id from RPC health endpoint or use override
|
||||
from aitbc_cli.utils.chain_id import get_chain_id, get_default_chain_id
|
||||
chain_id = get_chain_id(rpc_url, override=None, timeout=5)
|
||||
|
||||
# Get actual nonce from blockchain
|
||||
actual_nonce = 0
|
||||
@@ -747,9 +738,13 @@ def get_transactions(wallet_name: str, keystore_dir: Path = DEFAULT_KEYSTORE_DIR
|
||||
return []
|
||||
|
||||
|
||||
def get_balance(wallet_name: str, rpc_url: str = DEFAULT_RPC_URL) -> Optional[Dict]:
|
||||
def get_balance(wallet_name: str, rpc_url: str = DEFAULT_RPC_URL, chain_id_override: str = None) -> Optional[Dict]:
|
||||
"""Get wallet balance"""
|
||||
try:
|
||||
# Get chain_id from RPC health endpoint or use override
|
||||
from aitbc_cli.utils.chain_id import get_chain_id
|
||||
chain_id = get_chain_id(rpc_url, override=chain_id_override, timeout=5)
|
||||
|
||||
# Get wallet address
|
||||
wallet_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json"
|
||||
if not wallet_path.exists():
|
||||
@@ -763,7 +758,7 @@ def get_balance(wallet_name: str, rpc_url: str = DEFAULT_RPC_URL) -> Optional[Di
|
||||
# Get account info from RPC
|
||||
try:
|
||||
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30)
|
||||
account_info = http_client.get(f"/rpc/account/{address}?chain_id=ait-testnet")
|
||||
account_info = http_client.get(f"/rpc/account/{address}?chain_id={chain_id}")
|
||||
return {
|
||||
"wallet_name": wallet_name,
|
||||
"address": address,
|
||||
@@ -1118,13 +1113,18 @@ def agent_operations(action: str, **kwargs) -> Optional[Dict]:
|
||||
format=serialization.PublicFormat.Raw
|
||||
).hex()
|
||||
|
||||
# Get chain_id from RPC health endpoint
|
||||
chain_id = "ait-testnet" # Default
|
||||
# Get chain_id from RPC health endpoint or use provided chain_id
|
||||
chain_id_from_rpc = kwargs.get('chain_id', 'ait-mainnet')
|
||||
# Auto-detect if not provided
|
||||
if not kwargs.get('chain_id'):
|
||||
from aitbc_cli.utils.chain_id import get_chain_id
|
||||
chain_id_from_rpc = get_chain_id(rpc_url)
|
||||
try:
|
||||
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5)
|
||||
health_data = http_client.get("/health")
|
||||
supported_chains = health_data.get("supported_chains", [])
|
||||
if supported_chains:
|
||||
chain_id_from_rpc = supported_chains[0]
|
||||
chain_id = supported_chains[0]
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1811,6 +1811,7 @@ def simulate_ai_jobs(jobs: int, models: str, duration_range: str) -> Dict:
|
||||
|
||||
def legacy_main():
|
||||
parser = argparse.ArgumentParser(description="AITBC CLI - Comprehensive Blockchain Management Tool")
|
||||
parser.add_argument("--chain-id", default=None, help="Chain ID (auto-detected from blockchain node if not provided)")
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Create wallet command
|
||||
@@ -2016,6 +2017,7 @@ def legacy_main():
|
||||
wallet_balance_parser = wallet_subparsers.add_parser("balance", help="Wallet balance")
|
||||
wallet_balance_parser.add_argument("--name", help="Wallet name")
|
||||
wallet_balance_parser.add_argument("--all", action="store_true", help="Show all balances")
|
||||
wallet_balance_parser.add_argument("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
|
||||
|
||||
# All balances command (keep for backward compatibility)
|
||||
all_balances_parser = subparsers.add_parser("all-balances", help="Show all wallet balances")
|
||||
@@ -2140,6 +2142,10 @@ def legacy_main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle chain_id with auto-detection
|
||||
from aitbc_cli.utils.chain_id import get_chain_id
|
||||
chain_id = get_chain_id(DEFAULT_RPC_URL, override=args.chain_id)
|
||||
|
||||
if args.command == "create":
|
||||
# Get password
|
||||
password = None
|
||||
@@ -2312,6 +2318,7 @@ def legacy_main():
|
||||
kwargs['execution_id'] = args.execution_id
|
||||
if hasattr(args, 'status') and args.status:
|
||||
kwargs['status'] = args.status
|
||||
kwargs['chain_id'] = chain_id
|
||||
|
||||
result = agent_operations(args.agent_action, **kwargs)
|
||||
if result:
|
||||
|
||||
@@ -14,9 +14,17 @@ from ..core.marketplace import (
|
||||
from ..utils import output, error, success
|
||||
|
||||
@click.group()
|
||||
def marketplace():
|
||||
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
|
||||
@click.pass_context
|
||||
def marketplace(ctx, chain_id: Optional[str]):
|
||||
"""Global chain marketplace commands"""
|
||||
pass
|
||||
ctx.ensure_object(dict)
|
||||
|
||||
# Handle chain_id with auto-detection
|
||||
from ..utils.chain_id import get_chain_id
|
||||
config = load_multichain_config()
|
||||
default_rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006'
|
||||
ctx.obj['chain_id'] = get_chain_id(default_rpc_url, override=chain_id)
|
||||
|
||||
@marketplace.command()
|
||||
@click.argument('chain_id')
|
||||
|
||||
@@ -102,8 +102,9 @@ def _load_wallet(wallet_path: Path, wallet_name: str) -> Dict[str, Any]:
|
||||
"--wallet-path", help="Direct path to wallet file (overrides --wallet-name)"
|
||||
)
|
||||
@click.option("--use-daemon", is_flag=True, default=True, help="Use wallet daemon for operations")
|
||||
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
|
||||
@click.pass_context
|
||||
def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daemon: bool):
|
||||
def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daemon: bool, chain_id: Optional[str]):
|
||||
"""Manage your AITBC wallets and transactions"""
|
||||
# Ensure wallet object exists
|
||||
ctx.ensure_object(dict)
|
||||
@@ -111,6 +112,12 @@ def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daem
|
||||
# Set daemon mode
|
||||
ctx.obj["use_daemon"] = use_daemon
|
||||
|
||||
# Handle chain_id with auto-detection
|
||||
from ..utils.chain_id import get_chain_id
|
||||
config = get_config()
|
||||
default_rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006'
|
||||
ctx.obj["chain_id"] = get_chain_id(default_rpc_url, override=chain_id)
|
||||
|
||||
# Initialize dual-mode adapter
|
||||
from ..config import get_config
|
||||
import sys
|
||||
@@ -532,7 +539,8 @@ def balance(ctx):
|
||||
base_url=config.coordinator_url.replace('/api', ''),
|
||||
timeout=5
|
||||
)
|
||||
blockchain_balance = http_client.get(f"/rpc/balance/{wallet_data['address']}")
|
||||
chain_id = ctx.obj.get("chain_id", "ait-mainnet")
|
||||
blockchain_balance = http_client.get(f"/rpc/balance/{wallet_data['address']}?chain_id={chain_id}")
|
||||
output(
|
||||
{
|
||||
"wallet": wallet_name,
|
||||
|
||||
@@ -30,6 +30,9 @@ class CLIConfig(BaseAITBCConfig):
|
||||
wallet_url: str = Field(default="http://localhost:8003", description="Wallet daemon URL (alias for compatibility)")
|
||||
blockchain_rpc_url: str = Field(default=f"http://localhost:{BLOCKCHAIN_RPC_PORT}", description="Blockchain RPC URL")
|
||||
|
||||
# Chain configuration
|
||||
chain_id: str = Field(default="ait-mainnet", description="Default chain ID for multichain operations")
|
||||
|
||||
# Authentication
|
||||
api_key: Optional[str] = Field(default=None, description="API key for authentication")
|
||||
|
||||
|
||||
78
cli/aitbc_cli/utils/chain_id.py
Normal file
78
cli/aitbc_cli/utils/chain_id.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Chain ID utilities for AITBC CLI
|
||||
|
||||
This module provides functions for auto-detecting and validating chain IDs
|
||||
from blockchain nodes, supporting multichain operations.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from aitbc import AITBCHTTPClient, NetworkError
|
||||
|
||||
|
||||
# Known chain IDs
|
||||
KNOWN_CHAINS = ["ait-mainnet", "ait-devnet", "ait-testnet", "ait-healthchain"]
|
||||
|
||||
|
||||
def get_default_chain_id() -> str:
|
||||
"""Return the default chain ID (ait-mainnet for production)."""
|
||||
return "ait-mainnet"
|
||||
|
||||
|
||||
def validate_chain_id(chain_id: str) -> bool:
|
||||
"""Validate a chain ID against known chains.
|
||||
|
||||
Args:
|
||||
chain_id: The chain ID to validate
|
||||
|
||||
Returns:
|
||||
True if the chain ID is known, False otherwise
|
||||
"""
|
||||
return chain_id in KNOWN_CHAINS
|
||||
|
||||
|
||||
def get_chain_id_from_health(rpc_url: str, timeout: int = 5) -> str:
|
||||
"""Auto-detect chain ID from blockchain node's /health endpoint.
|
||||
|
||||
Args:
|
||||
rpc_url: The blockchain node RPC URL (e.g., http://localhost:8006)
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
The detected chain ID, or default if detection fails
|
||||
"""
|
||||
try:
|
||||
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=timeout)
|
||||
health_data = http_client.get("/health")
|
||||
supported_chains = health_data.get("supported_chains", [])
|
||||
|
||||
if supported_chains:
|
||||
# Return the first supported chain (typically the primary chain)
|
||||
return supported_chains[0]
|
||||
except NetworkError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to default if detection fails
|
||||
return get_default_chain_id()
|
||||
|
||||
|
||||
def get_chain_id(rpc_url: str, override: Optional[str] = None, timeout: int = 5) -> str:
|
||||
"""Get chain ID with override support and auto-detection fallback.
|
||||
|
||||
Args:
|
||||
rpc_url: The blockchain node RPC URL
|
||||
override: Optional chain ID override (e.g., from --chain-id flag)
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
The chain ID to use (override takes precedence, then auto-detection, then default)
|
||||
"""
|
||||
# If override is provided, validate and use it
|
||||
if override:
|
||||
if validate_chain_id(override):
|
||||
return override
|
||||
# If unknown, still use it (user may be testing new chains)
|
||||
return override
|
||||
|
||||
# Otherwise, auto-detect from health endpoint
|
||||
return get_chain_id_from_health(rpc_url, timeout)
|
||||
@@ -17,12 +17,19 @@ import os
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
|
||||
@click.pass_context
|
||||
def blockchain(ctx):
|
||||
def blockchain(ctx, chain_id: Optional[str]):
|
||||
"""Query blockchain information and status"""
|
||||
# Set role for blockchain commands
|
||||
ctx.ensure_object(dict)
|
||||
ctx.parent.detected_role = 'blockchain'
|
||||
|
||||
# Handle chain_id with auto-detection
|
||||
from aitbc_cli.utils.chain_id import get_chain_id
|
||||
config = ctx.obj.get('config')
|
||||
default_rpc_url = _get_node_endpoint(ctx)
|
||||
ctx.obj['chain_id'] = get_chain_id(default_rpc_url, override=chain_id)
|
||||
|
||||
|
||||
@blockchain.command()
|
||||
|
||||
@@ -266,8 +266,10 @@ def pay(ctx, booking_id: str, amount: float, from_wallet: str, to_wallet: str, t
|
||||
address = wallet_data["address"]
|
||||
|
||||
# Get wallet balance from blockchain
|
||||
from aitbc_cli.utils.chain_id import get_chain_id
|
||||
rpc_url = config.get('rpc_url', 'http://localhost:8006')
|
||||
balance_response = httpx.Client().get(f"{rpc_url}/rpc/account/{address}?chain_id=ait-testnet", timeout=5)
|
||||
chain_id = get_chain_id(rpc_url)
|
||||
balance_response = httpx.Client().get(f"{rpc_url}/rpc/account/{address}?chain_id={chain_id}", timeout=5)
|
||||
if balance_response.status_code != 200:
|
||||
error(f"Failed to get wallet balance")
|
||||
return
|
||||
@@ -285,7 +287,7 @@ def pay(ctx, booking_id: str, amount: float, from_wallet: str, to_wallet: str, t
|
||||
"value": amount,
|
||||
"fee": 1,
|
||||
"nonce": balance_data["nonce"],
|
||||
"chain_id": "ait-testnet",
|
||||
"chain_id": chain_id,
|
||||
"payload": {
|
||||
"type": "marketplace_payment",
|
||||
"booking_id": booking_id,
|
||||
|
||||
@@ -101,6 +101,11 @@ def version():
|
||||
default=None,
|
||||
help="API key for authentication"
|
||||
)
|
||||
@click.option(
|
||||
"--chain-id",
|
||||
default=None,
|
||||
help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)"
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
default="table",
|
||||
@@ -119,7 +124,7 @@ def version():
|
||||
help="Enable debug mode"
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx, url, api_key, output, verbose, debug):
|
||||
def cli(ctx, url, api_key, chain_id, output, verbose, debug):
|
||||
"""AITBC CLI - Command Line Interface for AITBC Network
|
||||
|
||||
Manage jobs, mining, wallets, blockchain operations, marketplaces, and AI
|
||||
@@ -142,6 +147,11 @@ def cli(ctx, url, api_key, output, verbose, debug):
|
||||
ctx.obj['output'] = output
|
||||
ctx.obj['verbose'] = verbose
|
||||
ctx.obj['debug'] = debug
|
||||
|
||||
# Handle chain_id with auto-detection
|
||||
from aitbc_cli.utils.chain_id import get_chain_id, get_default_chain_id
|
||||
default_rpc_url = url.replace('/api', '') if url else 'http://localhost:8006'
|
||||
ctx.obj['chain_id'] = get_chain_id(default_rpc_url, override=chain_id)
|
||||
|
||||
# Add commands to CLI
|
||||
cli.add_command(system)
|
||||
|
||||
@@ -536,6 +536,7 @@ def run_cli(argv, core):
|
||||
wallet_balance_parser.add_argument("--name", dest="wallet_name_opt", help=argparse.SUPPRESS)
|
||||
wallet_balance_parser.add_argument("--all", action="store_true")
|
||||
wallet_balance_parser.add_argument("--rpc-url", default=default_rpc_url)
|
||||
wallet_balance_parser.add_argument("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
|
||||
wallet_balance_parser.set_defaults(handler=handle_wallet_balance)
|
||||
|
||||
wallet_transactions_parser = wallet_subparsers.add_parser("transactions", help="Show wallet transactions")
|
||||
|
||||
@@ -18,12 +18,19 @@ from utils import error, success, output
|
||||
class DualModeWalletAdapter:
|
||||
"""Adapter supporting both file-based and daemon-based wallet operations"""
|
||||
|
||||
def __init__(self, config: Config, use_daemon: bool = False):
|
||||
def __init__(self, config: Config, use_daemon: bool = False, chain_id: Optional[str] = None):
|
||||
self.config = config
|
||||
self.use_daemon = use_daemon
|
||||
self.chain_id = chain_id
|
||||
self.wallet_dir = Path.home() / ".aitbc" / "wallets"
|
||||
self.wallet_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Auto-detect chain_id if not provided
|
||||
if not self.chain_id:
|
||||
from aitbc_cli.utils.chain_id import get_chain_id
|
||||
default_rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006'
|
||||
self.chain_id = get_chain_id(default_rpc_url)
|
||||
|
||||
if use_daemon:
|
||||
self.daemon_client = WalletDaemonClient(config)
|
||||
else:
|
||||
@@ -311,7 +318,7 @@ class DualModeWalletAdapter:
|
||||
|
||||
rpc_url = self.config.blockchain_rpc_url
|
||||
try:
|
||||
resp = httpx.get(f"{rpc_url}/rpc/account/{from_address}?chain_id=ait-testnet", timeout=5)
|
||||
resp = httpx.get(f"{rpc_url}/rpc/account/{from_address}?chain_id={self.chain_id}", timeout=5)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
chain_balance = data.get("balance", 0)
|
||||
|
||||
Reference in New Issue
Block a user