From 787ddcdae397ff8c34a709a7a814046bb2bc29b4 Mon Sep 17 00:00:00 2001 From: aitbc Date: Sat, 25 Apr 2026 09:10:12 +0200 Subject: [PATCH] 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. --- aitbc/queue.py | 431 ---------------------- cli/aitbc_cli.py | 39 +- cli/aitbc_cli/commands/marketplace_cmd.py | 12 +- cli/aitbc_cli/commands/wallet.py | 12 +- cli/aitbc_cli/config.py | 3 + cli/aitbc_cli/utils/chain_id.py | 78 ++++ cli/commands/blockchain.py | 9 +- cli/commands/marketplace.py | 6 +- cli/core/main.py | 12 +- cli/unified_cli.py | 1 + cli/utils/dual_mode_wallet_adapter.py | 11 +- 11 files changed, 157 insertions(+), 457 deletions(-) delete mode 100644 aitbc/queue.py create mode 100644 cli/aitbc_cli/utils/chain_id.py diff --git a/aitbc/queue.py b/aitbc/queue.py deleted file mode 100644 index 267e5acc..00000000 --- a/aitbc/queue.py +++ /dev/null @@ -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 diff --git a/cli/aitbc_cli.py b/cli/aitbc_cli.py index 6dd7c250..7e786270 100755 --- a/cli/aitbc_cli.py +++ b/cli/aitbc_cli.py @@ -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: diff --git a/cli/aitbc_cli/commands/marketplace_cmd.py b/cli/aitbc_cli/commands/marketplace_cmd.py index e3f25266..5fdd07fc 100755 --- a/cli/aitbc_cli/commands/marketplace_cmd.py +++ b/cli/aitbc_cli/commands/marketplace_cmd.py @@ -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') diff --git a/cli/aitbc_cli/commands/wallet.py b/cli/aitbc_cli/commands/wallet.py index 1c1f6ef1..1ffa5f59 100644 --- a/cli/aitbc_cli/commands/wallet.py +++ b/cli/aitbc_cli/commands/wallet.py @@ -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, diff --git a/cli/aitbc_cli/config.py b/cli/aitbc_cli/config.py index 3a538804..016453f6 100644 --- a/cli/aitbc_cli/config.py +++ b/cli/aitbc_cli/config.py @@ -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") diff --git a/cli/aitbc_cli/utils/chain_id.py b/cli/aitbc_cli/utils/chain_id.py new file mode 100644 index 00000000..96febcc8 --- /dev/null +++ b/cli/aitbc_cli/utils/chain_id.py @@ -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) diff --git a/cli/commands/blockchain.py b/cli/commands/blockchain.py index 694cf9c4..1dcbf5eb 100755 --- a/cli/commands/blockchain.py +++ b/cli/commands/blockchain.py @@ -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() diff --git a/cli/commands/marketplace.py b/cli/commands/marketplace.py index 5b73ac30..7f0ae1a3 100755 --- a/cli/commands/marketplace.py +++ b/cli/commands/marketplace.py @@ -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, diff --git a/cli/core/main.py b/cli/core/main.py index 2db64388..60246a7b 100644 --- a/cli/core/main.py +++ b/cli/core/main.py @@ -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) diff --git a/cli/unified_cli.py b/cli/unified_cli.py index ee77acf8..8011151f 100755 --- a/cli/unified_cli.py +++ b/cli/unified_cli.py @@ -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") diff --git a/cli/utils/dual_mode_wallet_adapter.py b/cli/utils/dual_mode_wallet_adapter.py index a33f3768..5ca7d818 100755 --- a/cli/utils/dual_mode_wallet_adapter.py +++ b/cli/utils/dual_mode_wallet_adapter.py @@ -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)