remove unused queue.py module and refactor chain_id handling in CLI
Some checks failed
CLI Tests / test-cli (push) Failing after 3s
Security Scanning / security-scan (push) Successful in 1m23s
Multi-Node Blockchain Health Monitoring / health-check (push) Has been cancelled
P2P Network Verification / p2p-verification (push) Has been cancelled

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:
aitbc
2026-04-25 09:10:12 +02:00
parent ac0d4b3f45
commit 787ddcdae3
11 changed files with 157 additions and 457 deletions

View File

@@ -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

View File

@@ -214,18 +214,9 @@ def send_transaction(from_wallet: str, to_address: str, amount: float, fee: floa
print(f"Error decrypting wallet: {e}") print(f"Error decrypting wallet: {e}")
return None return None
# Get chain_id from RPC health endpoint # Get chain_id from RPC health endpoint or use override
chain_id = "ait-testnet" # Default from aitbc_cli.utils.chain_id import get_chain_id, get_default_chain_id
try: chain_id = get_chain_id(rpc_url, override=None, timeout=5)
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 actual nonce from blockchain # Get actual nonce from blockchain
actual_nonce = 0 actual_nonce = 0
@@ -747,9 +738,13 @@ def get_transactions(wallet_name: str, keystore_dir: Path = DEFAULT_KEYSTORE_DIR
return [] 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""" """Get wallet balance"""
try: 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 # Get wallet address
wallet_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json" wallet_path = DEFAULT_KEYSTORE_DIR / f"{wallet_name}.json"
if not wallet_path.exists(): 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 # Get account info from RPC
try: try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=30) 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 { return {
"wallet_name": wallet_name, "wallet_name": wallet_name,
"address": address, "address": address,
@@ -1118,13 +1113,18 @@ def agent_operations(action: str, **kwargs) -> Optional[Dict]:
format=serialization.PublicFormat.Raw format=serialization.PublicFormat.Raw
).hex() ).hex()
# Get chain_id from RPC health endpoint # Get chain_id from RPC health endpoint or use provided chain_id
chain_id = "ait-testnet" # Default 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: try:
http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5) http_client = AITBCHTTPClient(base_url=rpc_url, timeout=5)
health_data = http_client.get("/health") health_data = http_client.get("/health")
supported_chains = health_data.get("supported_chains", []) supported_chains = health_data.get("supported_chains", [])
if supported_chains: if supported_chains:
chain_id_from_rpc = supported_chains[0]
chain_id = supported_chains[0] chain_id = supported_chains[0]
except Exception: except Exception:
pass pass
@@ -1811,6 +1811,7 @@ def simulate_ai_jobs(jobs: int, models: str, duration_range: str) -> Dict:
def legacy_main(): def legacy_main():
parser = argparse.ArgumentParser(description="AITBC CLI - Comprehensive Blockchain Management Tool") 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") subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Create wallet command # Create wallet command
@@ -2016,6 +2017,7 @@ def legacy_main():
wallet_balance_parser = wallet_subparsers.add_parser("balance", help="Wallet balance") 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("--name", help="Wallet name")
wallet_balance_parser.add_argument("--all", action="store_true", help="Show all balances") 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 command (keep for backward compatibility)
all_balances_parser = subparsers.add_parser("all-balances", help="Show all wallet balances") all_balances_parser = subparsers.add_parser("all-balances", help="Show all wallet balances")
@@ -2140,6 +2142,10 @@ def legacy_main():
args = parser.parse_args() 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": if args.command == "create":
# Get password # Get password
password = None password = None
@@ -2312,6 +2318,7 @@ def legacy_main():
kwargs['execution_id'] = args.execution_id kwargs['execution_id'] = args.execution_id
if hasattr(args, 'status') and args.status: if hasattr(args, 'status') and args.status:
kwargs['status'] = args.status kwargs['status'] = args.status
kwargs['chain_id'] = chain_id
result = agent_operations(args.agent_action, **kwargs) result = agent_operations(args.agent_action, **kwargs)
if result: if result:

View File

@@ -14,9 +14,17 @@ from ..core.marketplace import (
from ..utils import output, error, success from ..utils import output, error, success
@click.group() @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""" """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() @marketplace.command()
@click.argument('chain_id') @click.argument('chain_id')

View File

@@ -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)" "--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("--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 @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""" """Manage your AITBC wallets and transactions"""
# Ensure wallet object exists # Ensure wallet object exists
ctx.ensure_object(dict) ctx.ensure_object(dict)
@@ -111,6 +112,12 @@ def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str], use_daem
# Set daemon mode # Set daemon mode
ctx.obj["use_daemon"] = use_daemon 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 # Initialize dual-mode adapter
from ..config import get_config from ..config import get_config
import sys import sys
@@ -532,7 +539,8 @@ def balance(ctx):
base_url=config.coordinator_url.replace('/api', ''), base_url=config.coordinator_url.replace('/api', ''),
timeout=5 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( output(
{ {
"wallet": wallet_name, "wallet": wallet_name,

View File

@@ -30,6 +30,9 @@ class CLIConfig(BaseAITBCConfig):
wallet_url: str = Field(default="http://localhost:8003", description="Wallet daemon URL (alias for compatibility)") 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") 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 # Authentication
api_key: Optional[str] = Field(default=None, description="API key for authentication") api_key: Optional[str] = Field(default=None, description="API key for authentication")

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

View File

@@ -17,13 +17,20 @@ import os
@click.group() @click.group()
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
@click.pass_context @click.pass_context
def blockchain(ctx): def blockchain(ctx, chain_id: Optional[str]):
"""Query blockchain information and status""" """Query blockchain information and status"""
# Set role for blockchain commands # Set role for blockchain commands
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.parent.detected_role = 'blockchain' 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() @blockchain.command()
@click.option("--limit", type=int, default=10, help="Number of blocks to show") @click.option("--limit", type=int, default=10, help="Number of blocks to show")

View File

@@ -266,8 +266,10 @@ def pay(ctx, booking_id: str, amount: float, from_wallet: str, to_wallet: str, t
address = wallet_data["address"] address = wallet_data["address"]
# Get wallet balance from blockchain # Get wallet balance from blockchain
from aitbc_cli.utils.chain_id import get_chain_id
rpc_url = config.get('rpc_url', 'http://localhost:8006') 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: if balance_response.status_code != 200:
error(f"Failed to get wallet balance") error(f"Failed to get wallet balance")
return return
@@ -285,7 +287,7 @@ def pay(ctx, booking_id: str, amount: float, from_wallet: str, to_wallet: str, t
"value": amount, "value": amount,
"fee": 1, "fee": 1,
"nonce": balance_data["nonce"], "nonce": balance_data["nonce"],
"chain_id": "ait-testnet", "chain_id": chain_id,
"payload": { "payload": {
"type": "marketplace_payment", "type": "marketplace_payment",
"booking_id": booking_id, "booking_id": booking_id,

View File

@@ -101,6 +101,11 @@ def version():
default=None, default=None,
help="API key for authentication" 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( @click.option(
"--output", "--output",
default="table", default="table",
@@ -119,7 +124,7 @@ def version():
help="Enable debug mode" help="Enable debug mode"
) )
@click.pass_context @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 """AITBC CLI - Command Line Interface for AITBC Network
Manage jobs, mining, wallets, blockchain operations, marketplaces, and AI Manage jobs, mining, wallets, blockchain operations, marketplaces, and AI
@@ -143,6 +148,11 @@ def cli(ctx, url, api_key, output, verbose, debug):
ctx.obj['verbose'] = verbose ctx.obj['verbose'] = verbose
ctx.obj['debug'] = debug 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 # Add commands to CLI
cli.add_command(system) cli.add_command(system)
cli.add_command(version) cli.add_command(version)

View File

@@ -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("--name", dest="wallet_name_opt", help=argparse.SUPPRESS)
wallet_balance_parser.add_argument("--all", action="store_true") 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("--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_balance_parser.set_defaults(handler=handle_wallet_balance)
wallet_transactions_parser = wallet_subparsers.add_parser("transactions", help="Show wallet transactions") wallet_transactions_parser = wallet_subparsers.add_parser("transactions", help="Show wallet transactions")

View File

@@ -18,12 +18,19 @@ from utils import error, success, output
class DualModeWalletAdapter: class DualModeWalletAdapter:
"""Adapter supporting both file-based and daemon-based wallet operations""" """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.config = config
self.use_daemon = use_daemon self.use_daemon = use_daemon
self.chain_id = chain_id
self.wallet_dir = Path.home() / ".aitbc" / "wallets" self.wallet_dir = Path.home() / ".aitbc" / "wallets"
self.wallet_dir.mkdir(parents=True, exist_ok=True) 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: if use_daemon:
self.daemon_client = WalletDaemonClient(config) self.daemon_client = WalletDaemonClient(config)
else: else:
@@ -311,7 +318,7 @@ class DualModeWalletAdapter:
rpc_url = self.config.blockchain_rpc_url rpc_url = self.config.blockchain_rpc_url
try: 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: if resp.status_code == 200:
data = resp.json() data = resp.json()
chain_balance = data.get("balance", 0) chain_balance = data.get("balance", 0)