Remove backup and temporary files from git

Deleted .bak, .backup, and .orig files:
- 2 .orig files from blockchain-node
- 9 .bak files from cli commands
- 1 .bak file from dev scripts
- 1 .backup file from docs
- 1 .bak file from scripts

These files add noise and should not be tracked in git.
This commit is contained in:
aitbc
2026-04-30 10:28:03 +02:00
parent 09b0edc212
commit 2777fdb987
14 changed files with 0 additions and 5607 deletions

View File

@@ -1,229 +0,0 @@
import asyncio
import hashlib
import re
from datetime import datetime
from typing import Callable, ContextManager, Optional
from sqlmodel import Session, select
from ..logger import get_logger
from ..metrics import metrics_registry
from ..config import ProposerConfig
from ..models import Block
from ..gossip import gossip_broker
_METRIC_KEY_SANITIZE = re.compile(r"[^a-zA-Z0-9_]")
def _sanitize_metric_suffix(value: str) -> str:
sanitized = _METRIC_KEY_SANITIZE.sub("_", value).strip("_")
return sanitized or "unknown"
import time
class CircuitBreaker:
def __init__(self, threshold: int, timeout: int):
self._threshold = threshold
self._timeout = timeout
self._failures = 0
self._last_failure_time = 0.0
self._state = "closed"
@property
def state(self) -> str:
if self._state == "open":
if time.time() - self._last_failure_time > self._timeout:
self._state = "half-open"
return self._state
def allow_request(self) -> bool:
state = self.state
if state == "closed":
return True
if state == "half-open":
return True
return False
def record_failure(self) -> None:
self._failures += 1
self._last_failure_time = time.time()
if self._failures >= self._threshold:
self._state = "open"
def record_success(self) -> None:
self._failures = 0
self._state = "closed"
class PoAProposer:
"""Proof-of-Authority block proposer.
Responsible for periodically proposing blocks if this node is configured as a proposer.
In the real implementation, this would involve checking the mempool, validating transactions,
and signing the block.
"""
def __init__(
self,
*,
config: ProposerConfig,
session_factory: Callable[[], ContextManager[Session]],
) -> None:
self._config = config
self._session_factory = session_factory
self._logger = get_logger(__name__)
self._stop_event = asyncio.Event()
self._task: Optional[asyncio.Task[None]] = None
self._last_proposer_id: Optional[str] = None
async def start(self) -> None:
if self._task is not None:
return
self._logger.info("Starting PoA proposer loop", extra={"interval": self._config.interval_seconds})
self._ensure_genesis_block()
self._stop_event.clear()
self._task = asyncio.create_task(self._run_loop())
async def stop(self) -> None:
if self._task is None:
return
self._logger.info("Stopping PoA proposer loop")
self._stop_event.set()
await self._task
self._task = None
async def _run_loop(self) -> None:
while not self._stop_event.is_set():
await self._wait_until_next_slot()
if self._stop_event.is_set():
break
try:
self._propose_block()
except Exception as exc: # pragma: no cover - defensive logging
self._logger.exception("Failed to propose block", extra={"error": str(exc)})
async def _wait_until_next_slot(self) -> None:
head = self._fetch_chain_head()
if head is None:
return
now = datetime.utcnow()
elapsed = (now - head.timestamp).total_seconds()
sleep_for = max(self._config.interval_seconds - elapsed, 0.1)
if sleep_for <= 0:
sleep_for = 0.1
try:
await asyncio.wait_for(self._stop_event.wait(), timeout=sleep_for)
except asyncio.TimeoutError:
return
async def _propose_block(self) -> None:
# Check internal mempool
from ..mempool import get_mempool
if get_mempool().size(self._config.chain_id) == 0:
return
with self._session_factory() as session:
head = session.exec(select(Block).where(Block.chain_id == self._config.chain_id).order_by(Block.height.desc()).limit(1)).first()
next_height = 0
parent_hash = "0x00"
interval_seconds: Optional[float] = None
if head is not None:
next_height = head.height + 1
parent_hash = head.hash
interval_seconds = (datetime.utcnow() - head.timestamp).total_seconds()
timestamp = datetime.utcnow()
block_hash = self._compute_block_hash(next_height, parent_hash, timestamp)
block = Block(
chain_id=self._config.chain_id,
height=next_height,
hash=block_hash,
parent_hash=parent_hash,
proposer=self._config.proposer_id,
timestamp=timestamp,
tx_count=0,
state_root=None,
)
session.add(block)
session.commit()
metrics_registry.increment("blocks_proposed_total")
metrics_registry.set_gauge("chain_head_height", float(next_height))
if interval_seconds is not None and interval_seconds >= 0:
metrics_registry.observe("block_interval_seconds", interval_seconds)
metrics_registry.set_gauge("poa_last_block_interval_seconds", float(interval_seconds))
proposer_suffix = _sanitize_metric_suffix(self._config.proposer_id)
metrics_registry.increment(f"poa_blocks_proposed_total_{proposer_suffix}")
if self._last_proposer_id is not None and self._last_proposer_id != self._config.proposer_id:
metrics_registry.increment("poa_proposer_switches_total")
self._last_proposer_id = self._config.proposer_id
self._logger.info(
"Proposed block",
extra={
"height": block.height,
"hash": block.hash,
"proposer": block.proposer,
},
)
# Broadcast the new block
await gossip_broker.publish(
"blocks",
{
"height": block.height,
"hash": block.hash,
"parent_hash": block.parent_hash,
"proposer": block.proposer,
"timestamp": block.timestamp.isoformat(),
"tx_count": block.tx_count,
"state_root": block.state_root,
}
)
async def _ensure_genesis_block(self) -> None:
with self._session_factory() as session:
head = session.exec(select(Block).where(Block.chain_id == self._config.chain_id).order_by(Block.height.desc()).limit(1)).first()
if head is not None:
return
# Use a deterministic genesis timestamp so all nodes agree on the genesis block hash
timestamp = datetime(2025, 1, 1, 0, 0, 0)
block_hash = self._compute_block_hash(0, "0x00", timestamp)
genesis = Block(
chain_id=self._config.chain_id,
height=0,
hash=block_hash,
parent_hash="0x00",
proposer="genesis",
timestamp=timestamp,
tx_count=0,
state_root=None,
)
session.add(genesis)
session.commit()
# Broadcast genesis block for initial sync
await gossip_broker.publish(
"blocks",
{
"height": genesis.height,
"hash": genesis.hash,
"parent_hash": genesis.parent_hash,
"proposer": genesis.proposer,
"timestamp": genesis.timestamp.isoformat(),
"tx_count": genesis.tx_count,
"state_root": genesis.state_root,
}
)
def _fetch_chain_head(self) -> Optional[Block]:
with self._session_factory() as session:
return session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first()
def _compute_block_hash(self, height: int, parent_hash: str, timestamp: datetime) -> str:
payload = f"{self._config.chain_id}|{height}|{parent_hash}|{timestamp.isoformat()}".encode()
return "0x" + hashlib.sha256(payload).hexdigest()

View File

@@ -1,307 +0,0 @@
from __future__ import annotations
import asyncio
import json
import warnings
from collections import defaultdict
from contextlib import suppress
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Set
warnings.filterwarnings("ignore", message="coroutine.* was never awaited", category=RuntimeWarning)
try:
from broadcaster import Broadcast
except ImportError: # pragma: no cover
Broadcast = None # type: ignore[assignment]
from ..metrics import metrics_registry
def _increment_publication(metric_prefix: str, topic: str) -> None:
metrics_registry.increment(f"{metric_prefix}_total")
metrics_registry.increment(f"{metric_prefix}_topic_{topic}")
def _set_queue_gauge(topic: str, size: int) -> None:
metrics_registry.set_gauge(f"gossip_queue_size_{topic}", float(size))
def _update_subscriber_metrics(topics: Dict[str, List["asyncio.Queue[Any]"]]) -> None:
for topic, queues in topics.items():
metrics_registry.set_gauge(f"gossip_subscribers_topic_{topic}", float(len(queues)))
total = sum(len(queues) for queues in topics.values())
metrics_registry.set_gauge("gossip_subscribers_total", float(total))
def _clear_topic_metrics(topic: str) -> None:
metrics_registry.set_gauge(f"gossip_subscribers_topic_{topic}", 0.0)
_set_queue_gauge(topic, 0)
@dataclass
class TopicSubscription:
topic: str
queue: "asyncio.Queue[Any]"
_unsubscribe: Callable[[], None]
def close(self) -> None:
self._unsubscribe()
async def get(self) -> Any:
return await self.queue.get()
async def __aiter__(self): # type: ignore[override]
try:
while True:
yield await self.queue.get()
finally:
self.close()
class GossipBackend:
async def start(self) -> None: # pragma: no cover - overridden as needed
return None
async def publish(self, topic: str, message: Any) -> None:
raise NotImplementedError
async def subscribe(self, topic: str, max_queue_size: int = 100) -> TopicSubscription:
raise NotImplementedError
async def shutdown(self) -> None:
return None
class InMemoryGossipBackend(GossipBackend):
def __init__(self) -> None:
self._topics: Dict[str, List["asyncio.Queue[Any]"]] = defaultdict(list)
self._lock = asyncio.Lock()
async def publish(self, topic: str, message: Any) -> None:
async with self._lock:
queues = list(self._topics.get(topic, []))
for queue in queues:
await queue.put(message)
_set_queue_gauge(topic, queue.qsize())
_increment_publication("gossip_publications", topic)
async def subscribe(self, topic: str, max_queue_size: int = 100) -> TopicSubscription:
queue: "asyncio.Queue[Any]" = asyncio.Queue(maxsize=max_queue_size)
async with self._lock:
self._topics[topic].append(queue)
_update_subscriber_metrics(self._topics)
_set_queue_gauge(topic, queue.qsize())
def _unsubscribe() -> None:
async def _remove() -> None:
async with self._lock:
queues = self._topics.get(topic)
if queues is None:
return
if queue in queues:
queues.remove(queue)
if not queues:
self._topics.pop(topic, None)
_clear_topic_metrics(topic)
_update_subscriber_metrics(self._topics)
asyncio.create_task(_remove())
return TopicSubscription(topic=topic, queue=queue, _unsubscribe=_unsubscribe)
async def shutdown(self) -> None:
async with self._lock:
topics = list(self._topics.keys())
self._topics.clear()
for topic in topics:
_clear_topic_metrics(topic)
_update_subscriber_metrics(self._topics)
class BroadcastGossipBackend(GossipBackend):
def __init__(self, url: str) -> None:
if Broadcast is None: # provide in-process fallback when Broadcast is missing
self._broadcast = _InProcessBroadcast()
else:
self._broadcast = Broadcast(url) # type: ignore[arg-type]
self._tasks: Set[asyncio.Task[None]] = set()
self._lock = asyncio.Lock()
self._running = False
async def start(self) -> None:
if not self._running:
await self._broadcast.connect() # type: ignore[union-attr]
self._running = True
async def publish(self, topic: str, message: Any) -> None:
if not self._running:
raise RuntimeError("Broadcast backend not started")
payload = _encode_message(message)
await self._broadcast.publish(topic, payload) # type: ignore[union-attr]
_increment_publication("gossip_broadcast_publications", topic)
async def subscribe(self, topic: str, max_queue_size: int = 100) -> TopicSubscription:
if not self._running:
raise RuntimeError("Broadcast backend not started")
queue: "asyncio.Queue[Any]" = asyncio.Queue(maxsize=max_queue_size)
stop_event = asyncio.Event()
async def _run_subscription() -> None:
async with self._broadcast.subscribe(topic) as subscriber: # type: ignore[attr-defined,union-attr]
async for event in subscriber: # type: ignore[union-attr]
if stop_event.is_set():
break
data = _decode_message(getattr(event, "message", event))
try:
await queue.put(data)
_set_queue_gauge(topic, queue.qsize())
except asyncio.CancelledError:
break
task = asyncio.create_task(_run_subscription(), name=f"broadcast-sub:{topic}")
async with self._lock:
self._tasks.add(task)
metrics_registry.set_gauge("gossip_broadcast_subscribers_total", float(len(self._tasks)))
def _unsubscribe() -> None:
async def _stop() -> None:
stop_event.set()
task.cancel()
with suppress(asyncio.CancelledError):
await task
async with self._lock:
self._tasks.discard(task)
metrics_registry.set_gauge("gossip_broadcast_subscribers_total", float(len(self._tasks)))
asyncio.create_task(_stop())
return TopicSubscription(topic=topic, queue=queue, _unsubscribe=_unsubscribe)
async def shutdown(self) -> None:
async with self._lock:
tasks = list(self._tasks)
self._tasks.clear()
metrics_registry.set_gauge("gossip_broadcast_subscribers_total", 0.0)
for task in tasks:
task.cancel()
with suppress(asyncio.CancelledError):
await task
if self._running:
await self._broadcast.disconnect() # type: ignore[union-attr]
self._running = False
class GossipBroker:
def __init__(self, backend: GossipBackend) -> None:
self._backend = backend
self._lock = asyncio.Lock()
self._started = False
async def publish(self, topic: str, message: Any) -> None:
if not self._started:
await self._backend.start()
self._started = True
await self._backend.publish(topic, message)
async def subscribe(self, topic: str, max_queue_size: int = 100) -> TopicSubscription:
if not self._started:
await self._backend.start()
self._started = True
return await self._backend.subscribe(topic, max_queue_size=max_queue_size)
async def set_backend(self, backend: GossipBackend) -> None:
await backend.start()
async with self._lock:
previous = self._backend
self._backend = backend
self._started = True
await previous.shutdown()
async def shutdown(self) -> None:
await self._backend.shutdown()
class _InProcessSubscriber:
def __init__(self, queue: "asyncio.Queue[Any]"):
self._queue = queue
def __aiter__(self): # type: ignore[override]
return self._iterator()
async def _iterator(self):
while True:
yield await self._queue.get()
class _InProcessBroadcast:
"""Minimal in-memory broadcast substitute for tests when Starlette Broadcast is absent."""
def __init__(self) -> None:
self._topics: Dict[str, List["asyncio.Queue[Any]"]] = defaultdict(list)
self._lock = asyncio.Lock()
self._running = False
async def connect(self) -> None:
self._running = True
async def disconnect(self) -> None:
async with self._lock:
self._topics.clear()
self._running = False
@asynccontextmanager
async def subscribe(self, topic: str):
queue: "asyncio.Queue[Any]" = asyncio.Queue()
async with self._lock:
self._topics[topic].append(queue)
try:
yield _InProcessSubscriber(queue)
finally:
async with self._lock:
queues = self._topics.get(topic)
if queues and queue in queues:
queues.remove(queue)
async def publish(self, topic: str, message: Any) -> None:
if not self._running:
raise RuntimeError("Broadcast backend not started")
async with self._lock:
queues = list(self._topics.get(topic, []))
for queue in queues:
await queue.put(message)
def create_backend(backend_type: str, *, broadcast_url: Optional[str] = None) -> GossipBackend:
backend = backend_type.lower()
if backend in {"memory", "inmemory", "local"}:
return InMemoryGossipBackend()
if backend in {"broadcast", "starlette", "redis"}:
if not broadcast_url:
raise ValueError("Broadcast backend requires a gossip_broadcast_url setting")
return BroadcastGossipBackend(broadcast_url)
raise ValueError(f"Unsupported gossip backend '{backend_type}'")
def _encode_message(message: Any) -> Any:
if isinstance(message, (str, bytes, bytearray)):
return message
return json.dumps(message, separators=(",", ":"))
def _decode_message(message: Any) -> Any:
if isinstance(message, (bytes, bytearray)):
message = message.decode("utf-8")
if isinstance(message, str):
try:
return json.loads(message)
except json.JSONDecodeError:
return message
return message
gossip_broker = GossipBroker(InMemoryGossipBackend())

View File

@@ -1,496 +0,0 @@
"""Cross-chain agent communication commands for AITBC CLI"""
import click
import asyncio
import json
from datetime import datetime, timedelta
from typing import Optional
from ..core.config import load_multichain_config
from ..core.agent_communication import (
CrossChainAgentCommunication, AgentInfo, AgentMessage,
MessageType, AgentStatus
)
from ..utils import output, error, success
@click.group()
def agent_comm():
"""Cross-chain agent communication commands"""
pass
@agent_comm.command()
@click.argument('agent_id')
@click.argument('name')
@click.argument('chain_id')
@click.argument('endpoint')
@click.option('--capabilities', help='Comma-separated list of capabilities')
@click.option('--reputation', default=0.5, help='Initial reputation score')
@click.option('--version', default='1.0.0', help='Agent version')
@click.pass_context
def register(ctx, agent_id, name, chain_id, endpoint, capabilities, reputation, version):
"""Register an agent in the cross-chain network"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Parse capabilities
cap_list = capabilities.split(',') if capabilities else []
# Create agent info
agent_info = AgentInfo(
agent_id=agent_id,
name=name,
chain_id=chain_id,
node_id="default-node", # Would be determined dynamically
status=AgentStatus.ACTIVE,
capabilities=cap_list,
reputation_score=reputation,
last_seen=datetime.now(),
endpoint=endpoint,
version=version
)
# Register agent
success = asyncio.run(comm.register_agent(agent_info))
if success:
success(f"Agent {agent_id} registered successfully!")
agent_data = {
"Agent ID": agent_id,
"Name": name,
"Chain ID": chain_id,
"Status": "active",
"Capabilities": ", ".join(cap_list),
"Reputation": f"{reputation:.2f}",
"Endpoint": endpoint,
"Version": version
}
output(agent_data, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to register agent {agent_id}")
raise click.Abort()
except Exception as e:
error(f"Error registering agent: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.option('--chain-id', help='Filter by chain ID')
@click.option('--status', type=click.Choice(['active', 'inactive', 'busy', 'offline']), help='Filter by status')
@click.option('--capabilities', help='Filter by capabilities (comma-separated)')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def list(ctx, chain_id, status, capabilities, format):
"""List registered agents"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Get all agents
agents = list(comm.agents.values())
# Apply filters
if chain_id:
agents = [a for a in agents if a.chain_id == chain_id]
if status:
agents = [a for a in agents if a.status.value == status]
if capabilities:
required_caps = [cap.strip() for cap in capabilities.split(',')]
agents = [a for a in agents if any(cap in a.capabilities for cap in required_caps)]
if not agents:
output("No agents found", ctx.obj.get('output_format', 'table'))
return
# Format output
agent_data = [
{
"Agent ID": agent.agent_id,
"Name": agent.name,
"Chain ID": agent.chain_id,
"Status": agent.status.value,
"Reputation": f"{agent.reputation_score:.2f}",
"Capabilities": ", ".join(agent.capabilities[:3]), # Show first 3
"Last Seen": agent.last_seen.strftime("%Y-%m-%d %H:%M:%S")
}
for agent in agents
]
output(agent_data, ctx.obj.get('output_format', format), title="Registered Agents")
except Exception as e:
error(f"Error listing agents: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.argument('chain_id')
@click.option('--capabilities', help='Required capabilities (comma-separated)')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def discover(ctx, chain_id, capabilities, format):
"""Discover agents on a specific chain"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Parse capabilities
cap_list = capabilities.split(',') if capabilities else None
# Discover agents
agents = asyncio.run(comm.discover_agents(chain_id, cap_list))
if not agents:
output(f"No agents found on chain {chain_id}", ctx.obj.get('output_format', 'table'))
return
# Format output
agent_data = [
{
"Agent ID": agent.agent_id,
"Name": agent.name,
"Status": agent.status.value,
"Reputation": f"{agent.reputation_score:.2f}",
"Capabilities": ", ".join(agent.capabilities),
"Endpoint": agent.endpoint,
"Version": agent.version
}
for agent in agents
]
output(agent_data, ctx.obj.get('output_format', format), title=f"Agents on Chain {chain_id}")
except Exception as e:
error(f"Error discovering agents: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.argument('sender_id')
@click.argument('receiver_id')
@click.argument('message_type')
@click.argument('chain_id')
@click.option('--payload', help='Message payload (JSON string)')
@click.option('--target-chain', help='Target chain for cross-chain messages')
@click.option('--priority', default=5, help='Message priority (1-10)')
@click.option('--ttl', default=3600, help='Time to live in seconds')
@click.pass_context
def send(ctx, sender_id, receiver_id, message_type, chain_id, payload, target_chain, priority, ttl):
"""Send a message to an agent"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Parse message type
try:
msg_type = MessageType(message_type)
except ValueError:
error(f"Invalid message type: {message_type}")
error(f"Valid types: {[t.value for t in MessageType]}")
raise click.Abort()
# Parse payload
payload_dict = {}
if payload:
try:
payload_dict = json.loads(payload)
except json.JSONDecodeError:
error("Invalid JSON payload")
raise click.Abort()
# Create message
message = AgentMessage(
message_id=f"msg_{datetime.now().strftime('%Y%m%d%H%M%S')}_{sender_id}",
sender_id=sender_id,
receiver_id=receiver_id,
message_type=msg_type,
chain_id=chain_id,
target_chain_id=target_chain,
payload=payload_dict,
timestamp=datetime.now(),
signature="auto_generated", # Would be cryptographically signed
priority=priority,
ttl_seconds=ttl
)
# Send message
success = asyncio.run(comm.send_message(message))
if success:
success(f"Message sent successfully to {receiver_id}")
message_data = {
"Message ID": message.message_id,
"Sender": sender_id,
"Receiver": receiver_id,
"Type": message_type,
"Chain": chain_id,
"Target Chain": target_chain or "Same",
"Priority": priority,
"TTL": f"{ttl}s",
"Sent": message.timestamp.strftime("%Y-%m-%d %H:%M:%S")
}
output(message_data, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to send message to {receiver_id}")
raise click.Abort()
except Exception as e:
error(f"Error sending message: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.argument('agent_ids', nargs=-1, required=True)
@click.argument('collaboration_type')
@click.option('--governance', help='Governance rules (JSON string)')
@click.pass_context
def collaborate(ctx, agent_ids, collaboration_type, governance):
"""Create a multi-agent collaboration"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Parse governance rules
governance_dict = {}
if governance:
try:
governance_dict = json.loads(governance)
except json.JSONDecodeError:
error("Invalid JSON governance rules")
raise click.Abort()
# Create collaboration
collaboration_id = asyncio.run(comm.create_collaboration(
list(agent_ids), collaboration_type, governance_dict
))
if collaboration_id:
success(f"Collaboration created: {collaboration_id}")
collab_data = {
"Collaboration ID": collaboration_id,
"Type": collaboration_type,
"Participants": ", ".join(agent_ids),
"Status": "active",
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(collab_data, ctx.obj.get('output_format', 'table'))
else:
error("Failed to create collaboration")
raise click.Abort()
except Exception as e:
error(f"Error creating collaboration: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.argument('agent_id')
@click.argument('interaction_result', type=click.Choice(['success', 'failure']))
@click.option('--feedback', type=float, help='Feedback score (0.0-1.0)')
@click.pass_context
def reputation(ctx, agent_id, interaction_result, feedback):
"""Update agent reputation"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Update reputation
success = asyncio.run(comm.update_reputation(
agent_id, interaction_result == 'success', feedback
))
if success:
# Get updated reputation
agent_status = asyncio.run(comm.get_agent_status(agent_id))
if agent_status and agent_status.get('reputation'):
rep = agent_status['reputation']
success(f"Reputation updated for {agent_id}")
rep_data = {
"Agent ID": agent_id,
"Reputation Score": f"{rep['reputation_score']:.3f}",
"Total Interactions": rep['total_interactions'],
"Successful": rep['successful_interactions'],
"Failed": rep['failed_interactions'],
"Success Rate": f"{(rep['successful_interactions'] / rep['total_interactions'] * 100):.1f}%" if rep['total_interactions'] > 0 else "N/A",
"Last Updated": rep['last_updated']
}
output(rep_data, ctx.obj.get('output_format', 'table'))
else:
success(f"Reputation updated for {agent_id}")
else:
error(f"Failed to update reputation for {agent_id}")
raise click.Abort()
except Exception as e:
error(f"Error updating reputation: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.argument('agent_id')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def status(ctx, agent_id, format):
"""Get detailed agent status"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Get agent status
agent_status = asyncio.run(comm.get_agent_status(agent_id))
if not agent_status:
error(f"Agent {agent_id} not found")
raise click.Abort()
# Format output
status_data = [
{"Metric": "Agent ID", "Value": agent_status["agent_info"]["agent_id"]},
{"Metric": "Name", "Value": agent_status["agent_info"]["name"]},
{"Metric": "Chain ID", "Value": agent_status["agent_info"]["chain_id"]},
{"Metric": "Status", "Value": agent_status["status"]},
{"Metric": "Reputation", "Value": f"{agent_status['agent_info']['reputation_score']:.3f}" if agent_status.get('reputation') else "N/A"},
{"Metric": "Capabilities", "Value": ", ".join(agent_status["agent_info"]["capabilities"])},
{"Metric": "Message Queue Size", "Value": agent_status["message_queue_size"]},
{"Metric": "Active Collaborations", "Value": agent_status["active_collaborations"]},
{"Metric": "Last Seen", "Value": agent_status["last_seen"]},
{"Metric": "Endpoint", "Value": agent_status["agent_info"]["endpoint"]},
{"Metric": "Version", "Value": agent_status["agent_info"]["version"]}
]
output(status_data, ctx.obj.get('output_format', format), title=f"Agent Status: {agent_id}")
except Exception as e:
error(f"Error getting agent status: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def network(ctx, format):
"""Get cross-chain network overview"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
# Get network overview
overview = asyncio.run(comm.get_network_overview())
if not overview:
error("No network data available")
raise click.Abort()
# Overview data
overview_data = [
{"Metric": "Total Agents", "Value": overview["total_agents"]},
{"Metric": "Active Agents", "Value": overview["active_agents"]},
{"Metric": "Total Collaborations", "Value": overview["total_collaborations"]},
{"Metric": "Active Collaborations", "Value": overview["active_collaborations"]},
{"Metric": "Total Messages", "Value": overview["total_messages"]},
{"Metric": "Queued Messages", "Value": overview["queued_messages"]},
{"Metric": "Average Reputation", "Value": f"{overview['average_reputation']:.3f}"},
{"Metric": "Routing Table Size", "Value": overview["routing_table_size"]},
{"Metric": "Discovery Cache Size", "Value": overview["discovery_cache_size"]}
]
output(overview_data, ctx.obj.get('output_format', format), title="Network Overview")
# Agents by chain
if overview["agents_by_chain"]:
chain_data = [
{"Chain ID": chain_id, "Total Agents": count, "Active Agents": overview["active_agents_by_chain"].get(chain_id, 0)}
for chain_id, count in overview["agents_by_chain"].items()
]
output(chain_data, ctx.obj.get('output_format', format), title="Agents by Chain")
# Collaborations by type
if overview["collaborations_by_type"]:
collab_data = [
{"Type": collab_type, "Count": count}
for collab_type, count in overview["collaborations_by_type"].items()
]
output(collab_data, ctx.obj.get('output_format', format), title="Collaborations by Type")
except Exception as e:
error(f"Error getting network overview: {str(e)}")
raise click.Abort()
@agent_comm.command()
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
@click.option('--interval', default=10, help='Update interval in seconds')
@click.pass_context
def monitor(ctx, realtime, interval):
"""Monitor cross-chain agent communication"""
try:
config = load_multichain_config()
comm = CrossChainAgentCommunication(config)
if realtime:
# Real-time monitoring
from rich.console import Console
from rich.live import Live
from rich.table import Table
import time
console = Console()
def generate_monitor_table():
try:
overview = asyncio.run(comm.get_network_overview())
table = Table(title=f"Agent Network Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Total Agents", str(overview["total_agents"]))
table.add_row("Active Agents", str(overview["active_agents"]))
table.add_row("Active Collaborations", str(overview["active_collaborations"]))
table.add_row("Queued Messages", str(overview["queued_messages"]))
table.add_row("Avg Reputation", f"{overview['average_reputation']:.3f}")
# Add top chains by agent count
if overview["agents_by_chain"]:
table.add_row("", "")
table.add_row("Top Chains by Agents", "")
for chain_id, count in sorted(overview["agents_by_chain"].items(), key=lambda x: x[1], reverse=True)[:3]:
active = overview["active_agents_by_chain"].get(chain_id, 0)
table.add_row(f" {chain_id}", f"{count} total, {active} active")
return table
except Exception as e:
return f"Error getting network data: {e}"
with Live(generate_monitor_table(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_table())
time.sleep(interval)
except KeyboardInterrupt:
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
else:
# Single snapshot
overview = asyncio.run(comm.get_network_overview())
monitor_data = [
{"Metric": "Total Agents", "Value": overview["total_agents"]},
{"Metric": "Active Agents", "Value": overview["active_agents"]},
{"Metric": "Total Collaborations", "Value": overview["total_collaborations"]},
{"Metric": "Active Collaborations", "Value": overview["active_collaborations"]},
{"Metric": "Total Messages", "Value": overview["total_messages"]},
{"Metric": "Queued Messages", "Value": overview["queued_messages"]},
{"Metric": "Average Reputation", "Value": f"{overview['average_reputation']:.3f}"},
{"Metric": "Routing Table Size", "Value": overview["routing_table_size"]}
]
output(monitor_data, ctx.obj.get('output_format', 'table'), title="Agent Network Monitor")
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()

View File

@@ -1,402 +0,0 @@
"""Analytics and monitoring commands for AITBC CLI"""
import click
import asyncio
from datetime import datetime, timedelta
from typing import Optional
from ..core.config import load_multichain_config
from ..core.analytics import ChainAnalytics
from ..utils import output, error, success
@click.group()
def analytics():
"""Chain analytics and monitoring commands"""
pass
@analytics.command()
@click.option('--chain-id', help='Specific chain ID to analyze')
@click.option('--hours', default=24, help='Time range in hours')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def summary(ctx, chain_id, hours, format):
"""Get performance summary for chains"""
try:
config = load_multichain_config()
analytics = ChainAnalytics(config)
if chain_id:
# Single chain summary
summary = analytics.get_chain_performance_summary(chain_id, hours)
if not summary:
error(f"No data available for chain {chain_id}")
raise click.Abort()
# Format summary for display
summary_data = [
{"Metric": "Chain ID", "Value": summary["chain_id"]},
{"Metric": "Time Range", "Value": f"{summary['time_range_hours']} hours"},
{"Metric": "Data Points", "Value": summary["data_points"]},
{"Metric": "Health Score", "Value": f"{summary['health_score']:.1f}/100"},
{"Metric": "Active Alerts", "Value": summary["active_alerts"]},
{"Metric": "Avg TPS", "Value": f"{summary['statistics']['tps']['avg']:.2f}"},
{"Metric": "Avg Block Time", "Value": f"{summary['statistics']['block_time']['avg']:.2f}s"},
{"Metric": "Avg Gas Price", "Value": f"{summary['statistics']['gas_price']['avg']:,} wei"}
]
output(summary_data, ctx.obj.get('output_format', format), title=f"Chain Summary: {chain_id}")
else:
# Cross-chain analysis
analysis = analytics.get_cross_chain_analysis()
if not analysis:
error("No analytics data available")
raise click.Abort()
# Overview data
overview_data = [
{"Metric": "Total Chains", "Value": analysis["total_chains"]},
{"Metric": "Active Chains", "Value": analysis["active_chains"]},
{"Metric": "Total Alerts", "Value": analysis["alerts_summary"]["total_alerts"]},
{"Metric": "Critical Alerts", "Value": analysis["alerts_summary"]["critical_alerts"]},
{"Metric": "Total Memory Usage", "Value": f"{analysis['resource_usage']['total_memory_mb']:.1f}MB"},
{"Metric": "Total Disk Usage", "Value": f"{analysis['resource_usage']['total_disk_mb']:.1f}MB"},
{"Metric": "Total Clients", "Value": analysis["resource_usage"]["total_clients"]},
{"Metric": "Total Agents", "Value": analysis["resource_usage"]["total_agents"]}
]
output(overview_data, ctx.obj.get('output_format', format), title="Cross-Chain Analysis Overview")
# Performance comparison
if analysis["performance_comparison"]:
comparison_data = [
{
"Chain ID": chain_id,
"TPS": f"{data['tps']:.2f}",
"Block Time": f"{data['block_time']:.2f}s",
"Health Score": f"{data['health_score']:.1f}/100"
}
for chain_id, data in analysis["performance_comparison"].items()
]
output(comparison_data, ctx.obj.get('output_format', format), title="Chain Performance Comparison")
except Exception as e:
error(f"Error getting analytics summary: {str(e)}")
raise click.Abort()
@analytics.command()
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
@click.option('--interval', default=30, help='Update interval in seconds')
@click.option('--chain-id', help='Monitor specific chain')
@click.pass_context
def monitor(ctx, realtime, interval, chain_id):
"""Monitor chain performance in real-time"""
try:
config = load_multichain_config()
analytics = ChainAnalytics(config)
if realtime:
# Real-time monitoring
from rich.console import Console
from rich.live import Live
from rich.table import Table
import time
console = Console()
def generate_monitor_table():
try:
# Collect latest metrics
asyncio.run(analytics.collect_all_metrics())
table = Table(title=f"Chain Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
table.add_column("Chain ID", style="cyan")
table.add_column("TPS", style="green")
table.add_column("Block Time", style="yellow")
table.add_column("Health", style="red")
table.add_column("Alerts", style="magenta")
if chain_id:
# Single chain monitoring
summary = analytics.get_chain_performance_summary(chain_id, 1)
if summary:
health_color = "green" if summary["health_score"] > 70 else "yellow" if summary["health_score"] > 40 else "red"
table.add_row(
chain_id,
f"{summary['statistics']['tps']['avg']:.2f}",
f"{summary['statistics']['block_time']['avg']:.2f}s",
f"[{health_color}]{summary['health_score']:.1f}[/{health_color}]",
str(summary["active_alerts"])
)
else:
# All chains monitoring
analysis = analytics.get_cross_chain_analysis()
for chain_id, data in analysis["performance_comparison"].items():
health_color = "green" if data["health_score"] > 70 else "yellow" if data["health_score"] > 40 else "red"
table.add_row(
chain_id,
f"{data['tps']:.2f}",
f"{data['block_time']:.2f}s",
f"[{health_color}]{data['health_score']:.1f}[/{health_color}]",
str(len([a for a in analytics.alerts if a.chain_id == chain_id]))
)
return table
except Exception as e:
return f"Error collecting metrics: {e}"
with Live(generate_monitor_table(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_table())
time.sleep(interval)
except KeyboardInterrupt:
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
else:
# Single snapshot
asyncio.run(analytics.collect_all_metrics())
if chain_id:
summary = analytics.get_chain_performance_summary(chain_id, 1)
if not summary:
error(f"No data available for chain {chain_id}")
raise click.Abort()
monitor_data = [
{"Metric": "Chain ID", "Value": summary["chain_id"]},
{"Metric": "Current TPS", "Value": f"{summary['statistics']['tps']['avg']:.2f}"},
{"Metric": "Current Block Time", "Value": f"{summary['statistics']['block_time']['avg']:.2f}s"},
{"Metric": "Health Score", "Value": f"{summary['health_score']:.1f}/100"},
{"Metric": "Active Alerts", "Value": summary["active_alerts"]},
{"Metric": "Memory Usage", "Value": f"{summary['latest_metrics']['memory_usage_mb']:.1f}MB"},
{"Metric": "Disk Usage", "Value": f"{summary['latest_metrics']['disk_usage_mb']:.1f}MB"},
{"Metric": "Active Nodes", "Value": summary["latest_metrics"]["active_nodes"]},
{"Metric": "Client Count", "Value": summary["latest_metrics"]["client_count"]},
{"Metric": "Agent Count", "Value": summary["latest_metrics"]["agent_count"]}
]
output(monitor_data, ctx.obj.get('output_format', 'table'), title=f"Chain Monitor: {chain_id}")
else:
analysis = analytics.get_cross_chain_analysis()
monitor_data = [
{"Metric": "Total Chains", "Value": analysis["total_chains"]},
{"Metric": "Active Chains", "Value": analysis["active_chains"]},
{"Metric": "Total Memory Usage", "Value": f"{analysis['resource_usage']['total_memory_mb']:.1f}MB"},
{"Metric": "Total Disk Usage", "Value": f"{analysis['resource_usage']['total_disk_mb']:.1f}MB"},
{"Metric": "Total Clients", "Value": analysis["resource_usage"]["total_clients"]},
{"Metric": "Total Agents", "Value": analysis["resource_usage"]["total_agents"]},
{"Metric": "Total Alerts", "Value": analysis["alerts_summary"]["total_alerts"]},
{"Metric": "Critical Alerts", "Value": analysis["alerts_summary"]["critical_alerts"]}
]
output(monitor_data, ctx.obj.get('output_format', 'table'), title="System Monitor")
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()
@analytics.command()
@click.option('--chain-id', help='Specific chain ID for predictions')
@click.option('--hours', default=24, help='Prediction time horizon in hours')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def predict(ctx, chain_id, hours, format):
"""Predict chain performance"""
try:
config = load_multichain_config()
analytics = ChainAnalytics(config)
# Collect current metrics first
asyncio.run(analytics.collect_all_metrics())
if chain_id:
# Single chain prediction
predictions = asyncio.run(analytics.predict_chain_performance(chain_id, hours))
if not predictions:
error(f"No prediction data available for chain {chain_id}")
raise click.Abort()
prediction_data = [
{
"Metric": pred.metric,
"Predicted Value": f"{pred.predicted_value:.2f}",
"Confidence": f"{pred.confidence:.1%}",
"Time Horizon": f"{pred.time_horizon_hours}h"
}
for pred in predictions
]
output(prediction_data, ctx.obj.get('output_format', format), title=f"Performance Predictions: {chain_id}")
else:
# All chains prediction
analysis = analytics.get_cross_chain_analysis()
all_predictions = {}
for chain_id in analysis["performance_comparison"].keys():
predictions = asyncio.run(analytics.predict_chain_performance(chain_id, hours))
if predictions:
all_predictions[chain_id] = predictions
if not all_predictions:
error("No prediction data available")
raise click.Abort()
# Format predictions for display
prediction_data = []
for chain_id, predictions in all_predictions.items():
for pred in predictions:
prediction_data.append({
"Chain ID": chain_id,
"Metric": pred.metric,
"Predicted Value": f"{pred.predicted_value:.2f}",
"Confidence": f"{pred.confidence:.1%}",
"Time Horizon": f"{pred.time_horizon_hours}h"
})
output(prediction_data, ctx.obj.get('output_format', format), title="Chain Performance Predictions")
except Exception as e:
error(f"Error generating predictions: {str(e)}")
raise click.Abort()
@analytics.command()
@click.option('--chain-id', help='Specific chain ID for recommendations')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def optimize(ctx, chain_id, format):
"""Get optimization recommendations"""
try:
config = load_multichain_config()
analytics = ChainAnalytics(config)
# Collect current metrics first
asyncio.run(analytics.collect_all_metrics())
if chain_id:
# Single chain recommendations
recommendations = analytics.get_optimization_recommendations(chain_id)
if not recommendations:
success(f"No optimization recommendations for chain {chain_id}")
return
recommendation_data = [
{
"Type": rec["type"],
"Priority": rec["priority"],
"Issue": rec["issue"],
"Current Value": rec["current_value"],
"Recommended Action": rec["recommended_action"],
"Expected Improvement": rec["expected_improvement"]
}
for rec in recommendations
]
output(recommendation_data, ctx.obj.get('output_format', format), title=f"Optimization Recommendations: {chain_id}")
else:
# All chains recommendations
analysis = analytics.get_cross_chain_analysis()
all_recommendations = {}
for chain_id in analysis["performance_comparison"].keys():
recommendations = analytics.get_optimization_recommendations(chain_id)
if recommendations:
all_recommendations[chain_id] = recommendations
if not all_recommendations:
success("No optimization recommendations available")
return
# Format recommendations for display
recommendation_data = []
for chain_id, recommendations in all_recommendations.items():
for rec in recommendations:
recommendation_data.append({
"Chain ID": chain_id,
"Type": rec["type"],
"Priority": rec["priority"],
"Issue": rec["issue"],
"Current Value": rec["current_value"],
"Recommended Action": rec["recommended_action"]
})
output(recommendation_data, ctx.obj.get('output_format', format), title="Chain Optimization Recommendations")
except Exception as e:
error(f"Error getting optimization recommendations: {str(e)}")
raise click.Abort()
@analytics.command()
@click.option('--severity', type=click.Choice(['all', 'critical', 'warning']), default='all', help='Alert severity filter')
@click.option('--hours', default=24, help='Time range in hours')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def alerts(ctx, severity, hours, format):
"""View performance alerts"""
try:
config = load_multichain_config()
analytics = ChainAnalytics(config)
# Collect current metrics first
asyncio.run(analytics.collect_all_metrics())
# Filter alerts
cutoff_time = datetime.now() - timedelta(hours=hours)
filtered_alerts = [
alert for alert in analytics.alerts
if alert.timestamp >= cutoff_time
]
if severity != 'all':
filtered_alerts = [a for a in filtered_alerts if a.severity == severity]
if not filtered_alerts:
success("No alerts found")
return
alert_data = [
{
"Chain ID": alert.chain_id,
"Type": alert.alert_type,
"Severity": alert.severity,
"Message": alert.message,
"Current Value": f"{alert.current_value:.2f}",
"Threshold": f"{alert.threshold:.2f}",
"Time": alert.timestamp.strftime("%Y-%m-%d %H:%M:%S")
}
for alert in filtered_alerts
]
output(alert_data, ctx.obj.get('output_format', format), title=f"Performance Alerts (Last {hours}h)")
except Exception as e:
error(f"Error getting alerts: {str(e)}")
raise click.Abort()
@analytics.command()
@click.option('--format', type=click.Choice(['json']), default='json', help='Output format')
@click.pass_context
def dashboard(ctx, format):
"""Get complete dashboard data"""
try:
config = load_multichain_config()
analytics = ChainAnalytics(config)
# Collect current metrics
asyncio.run(analytics.collect_all_metrics())
# Get dashboard data
dashboard_data = analytics.get_dashboard_data()
if format == 'json':
import json
click.echo(json.dumps(dashboard_data, indent=2, default=str))
else:
error("Dashboard data only available in JSON format")
raise click.Abort()
except Exception as e:
error(f"Error getting dashboard data: {str(e)}")
raise click.Abort()

View File

@@ -1,562 +0,0 @@
"""Chain management commands for AITBC CLI"""
import click
from typing import Optional
from ..core.chain_manager import ChainManager, ChainNotFoundError, NodeNotAvailableError
from ..core.config import MultiChainConfig, load_multichain_config
from ..models.chain import ChainType
from ..utils import output, error, success
@click.group()
def chain():
"""Multi-chain management commands"""
pass
@chain.command()
@click.option('--type', 'chain_type', type=click.Choice(['main', 'topic', 'private', 'all']),
default='all', help='Filter by chain type')
@click.option('--show-private', is_flag=True, help='Show private chains')
@click.option('--sort', type=click.Choice(['id', 'size', 'nodes', 'created']),
default='id', help='Sort by field')
@click.pass_context
def list(ctx, chain_type, show_private, sort):
"""List all available chains"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
# Get chains
import asyncio
chains = asyncio.run(chain_manager.list_chains(
chain_type=ChainType(chain_type) if chain_type != 'all' else None,
include_private=show_private,
sort_by=sort
))
if not chains:
output("No chains found", ctx.obj.get('output_format', 'table'))
return
# Format output
chains_data = [
{
"Chain ID": chain.id,
"Type": chain.type.value,
"Purpose": chain.purpose,
"Name": chain.name,
"Size": f"{chain.size_mb:.1f}MB",
"Nodes": chain.node_count,
"Contracts": chain.contract_count,
"Clients": chain.client_count,
"Miners": chain.miner_count,
"Status": chain.status.value
}
for chain in chains
]
output(chains_data, ctx.obj.get('output_format', 'table'), title="Available Chains")
except Exception as e:
error(f"Error listing chains: {str(e)}")
raise click.Abort()
@chain.command()
@click.option('--chain-id', help='Specific chain ID to check status (shows all if not specified)')
@click.option('--detailed', is_flag=True, help='Show detailed status information')
@click.pass_context
def status(ctx, chain_id, detailed):
"""Check status of chains"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
import asyncio
if chain_id:
# Get specific chain status
chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=detailed))
status_data = {
"Chain ID": chain_info.id,
"Name": chain_info.name,
"Type": chain_info.type.value,
"Status": chain_info.status.value,
"Block Height": chain_info.block_height,
"Active Nodes": chain_info.active_nodes,
"Total Nodes": chain_info.node_count
}
if detailed:
status_data.update({
"Consensus": chain_info.consensus_algorithm.value,
"TPS": f"{chain_info.tps:.1f}",
"Gas Price": f"{chain_info.gas_price / 1e9:.1f} gwei",
"Memory Usage": f"{chain_info.memory_usage_mb:.1f}MB"
})
output(status_data, ctx.obj.get('output_format', 'table'), title=f"Chain Status: {chain_id}")
else:
# Get all chains status
chains = asyncio.run(chain_manager.list_chains())
if not chains:
output({"message": "No chains found"}, ctx.obj.get('output_format', 'table'))
return
status_list = []
for chain in chains:
status_info = {
"Chain ID": chain.id,
"Name": chain.name,
"Type": chain.type.value,
"Status": chain.status.value,
"Block Height": chain.block_height,
"Active Nodes": chain.active_nodes
}
status_list.append(status_info)
output(status_list, ctx.obj.get('output_format', 'table'), title="Chain Status Overview")
except ChainNotFoundError:
error(f"Chain {chain_id} not found")
raise click.Abort()
except Exception as e:
error(f"Error getting chain status: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.option('--detailed', is_flag=True, help='Show detailed information')
@click.option('--metrics', is_flag=True, help='Show performance metrics')
@click.pass_context
def info(ctx, chain_id, detailed, metrics):
"""Get detailed information about a chain"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
import asyncio
chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed, metrics))
# Basic information
basic_info = {
"Chain ID": chain_info.id,
"Type": chain_info.type.value,
"Purpose": chain_info.purpose,
"Name": chain_info.name,
"Description": chain_info.description or "No description",
"Status": chain_info.status.value,
"Created": chain_info.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"Block Height": chain_info.block_height,
"Size": f"{chain_info.size_mb:.1f}MB"
}
output(basic_info, ctx.obj.get('output_format', 'table'), title=f"Chain Information: {chain_id}")
if detailed:
# Network details
network_info = {
"Total Nodes": chain_info.node_count,
"Active Nodes": chain_info.active_nodes,
"Consensus": chain_info.consensus_algorithm.value,
"Block Time": f"{chain_info.block_time}s",
"Clients": chain_info.client_count,
"Miners": chain_info.miner_count,
"Contracts": chain_info.contract_count,
"Agents": chain_info.agent_count,
"Privacy": chain_info.privacy.visibility,
"Access Control": chain_info.privacy.access_control
}
output(network_info, ctx.obj.get('output_format', 'table'), title="Network Details")
if metrics:
# Performance metrics
performance_info = {
"TPS": f"{chain_info.tps:.1f}",
"Avg Block Time": f"{chain_info.avg_block_time:.1f}s",
"Avg Gas Used": f"{chain_info.avg_gas_used:,}",
"Gas Price": f"{chain_info.gas_price / 1e9:.1f} gwei",
"Growth Rate": f"{chain_info.growth_rate_mb_per_day:.1f}MB/day",
"Memory Usage": f"{chain_info.memory_usage_mb:.1f}MB",
"Disk Usage": f"{chain_info.disk_usage_mb:.1f}MB"
}
output(performance_info, ctx.obj.get('output_format', 'table'), title="Performance Metrics")
except ChainNotFoundError:
error(f"Chain {chain_id} not found")
raise click.Abort()
except Exception as e:
error(f"Error getting chain info: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('config_file', type=click.Path(exists=True))
@click.option('--node', help='Target node for chain creation')
@click.option('--dry-run', is_flag=True, help='Show what would be created without actually creating')
@click.pass_context
def create(ctx, config_file, node, dry_run):
"""Create a new chain from configuration file"""
try:
import yaml
from ..models.chain import ChainConfig
config = load_multichain_config()
chain_manager = ChainManager(config)
# Load and validate configuration
with open(config_file, 'r') as f:
config_data = yaml.safe_load(f)
chain_config = ChainConfig(**config_data['chain'])
if dry_run:
dry_run_info = {
"Chain Type": chain_config.type.value,
"Purpose": chain_config.purpose,
"Name": chain_config.name,
"Description": chain_config.description or "No description",
"Consensus": chain_config.consensus.algorithm.value,
"Privacy": chain_config.privacy.visibility,
"Target Node": node or "Auto-selected"
}
output(dry_run_info, ctx.obj.get('output_format', 'table'), title="Dry Run - Chain Creation")
return
# Create chain
chain_id = chain_manager.create_chain(chain_config, node)
success(f"Chain created successfully!")
result = {
"Chain ID": chain_id,
"Type": chain_config.type.value,
"Purpose": chain_config.purpose,
"Name": chain_config.name,
"Node": node or "Auto-selected"
}
output(result, ctx.obj.get('output_format', 'table'))
if chain_config.privacy.visibility == "private":
success("Private chain created! Use access codes to invite participants.")
except Exception as e:
error(f"Error creating chain: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.option('--force', is_flag=True, help='Force deletion without confirmation')
@click.option('--confirm', is_flag=True, help='Confirm deletion')
@click.pass_context
def delete(ctx, chain_id, force, confirm):
"""Delete a chain permanently"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
# Get chain information for confirmation
import asyncio
chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=True))
if not force:
# Show warning and confirmation
warning_info = {
"Chain ID": chain_id,
"Type": chain_info.type.value,
"Purpose": chain_info.purpose,
"Name": chain_info.name,
"Status": chain_info.status.value,
"Participants": chain_info.client_count,
"Transactions": "Multiple" # Would get actual count
}
output(warning_info, ctx.obj.get('output_format', 'table'), title="Chain Deletion Warning")
if not confirm:
error("To confirm deletion, use --confirm flag")
raise click.Abort()
# Delete chain
import asyncio
is_success = asyncio.run(chain_manager.delete_chain(chain_id, force))
if is_success:
success(f"Chain {chain_id} deleted successfully!")
else:
error(f"Failed to delete chain {chain_id}")
raise click.Abort()
except ChainNotFoundError:
error(f"Chain {chain_id} not found")
raise click.Abort()
except Exception as e:
error(f"Error deleting chain: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.argument('node_id')
@click.pass_context
def add(ctx, chain_id, node_id):
"""Add a chain to a specific node"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
import asyncio
is_success = asyncio.run(chain_manager.add_chain_to_node(chain_id, node_id))
if is_success:
success(f"Chain {chain_id} added to node {node_id} successfully!")
else:
error(f"Failed to add chain {chain_id} to node {node_id}")
raise click.Abort()
except Exception as e:
error(f"Error adding chain to node: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.argument('node_id')
@click.option('--migrate', is_flag=True, help='Migrate to another node before removal')
@click.pass_context
def remove(ctx, chain_id, node_id, migrate):
"""Remove a chain from a specific node"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
is_success = chain_manager.remove_chain_from_node(chain_id, node_id, migrate)
if is_success:
success(f"Chain {chain_id} removed from node {node_id} successfully!")
else:
error(f"Failed to remove chain {chain_id} from node {node_id}")
raise click.Abort()
except Exception as e:
error(f"Error removing chain from node: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.argument('from_node')
@click.argument('to_node')
@click.option('--dry-run', is_flag=True, help='Show migration plan without executing')
@click.option('--verify', is_flag=True, help='Verify migration after completion')
@click.pass_context
def migrate(ctx, chain_id, from_node, to_node, dry_run, verify):
"""Migrate a chain between nodes"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
migration_result = chain_manager.migrate_chain(chain_id, from_node, to_node, dry_run)
if dry_run:
plan_info = {
"Chain ID": chain_id,
"Source Node": from_node,
"Target Node": to_node,
"Feasible": "Yes" if migration_result.success else "No",
"Estimated Time": f"{migration_result.transfer_time_seconds}s",
"Error": migration_result.error or "None"
}
output(plan_info, ctx.obj.get('output_format', 'table'), title="Migration Plan")
return
if migration_result.success:
success(f"Chain migration completed successfully!")
result = {
"Chain ID": chain_id,
"Source Node": from_node,
"Target Node": to_node,
"Blocks Transferred": migration_result.blocks_transferred,
"Transfer Time": f"{migration_result.transfer_time_seconds}s",
"Verification": "Passed" if migration_result.verification_passed else "Failed"
}
output(result, ctx.obj.get('output_format', 'table'))
else:
error(f"Migration failed: {migration_result.error}")
raise click.Abort()
except Exception as e:
error(f"Error during migration: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.option('--path', help='Backup directory path')
@click.option('--compress', is_flag=True, help='Compress backup')
@click.option('--verify', is_flag=True, help='Verify backup integrity')
@click.pass_context
def backup(ctx, chain_id, path, compress, verify):
"""Backup chain data"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
import asyncio
backup_result = asyncio.run(chain_manager.backup_chain(chain_id, path, compress, verify))
success(f"Chain backup completed successfully!")
result = {
"Chain ID": chain_id,
"Backup File": backup_result.backup_file,
"Original Size": f"{backup_result.original_size_mb:.1f}MB",
"Backup Size": f"{backup_result.backup_size_mb:.1f}MB",
"Compression": f"{backup_result.compression_ratio:.1f}x" if compress else "None",
"Checksum": backup_result.checksum,
"Verification": "Passed" if backup_result.verification_passed else "Failed"
}
output(result, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error during backup: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('backup_file', type=click.Path(exists=True))
@click.option('--node', help='Target node for restoration')
@click.option('--verify', is_flag=True, help='Verify restoration')
@click.pass_context
def restore(ctx, backup_file, node, verify):
"""Restore chain from backup"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
import asyncio
restore_result = asyncio.run(chain_manager.restore_chain(backup_file, node, verify))
success(f"Chain restoration completed successfully!")
result = {
"Chain ID": restore_result.chain_id,
"Node": restore_result.node_id,
"Blocks Restored": restore_result.blocks_restored,
"Verification": "Passed" if restore_result.verification_passed else "Failed"
}
output(result, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error during restoration: {str(e)}")
raise click.Abort()
@chain.command()
@click.argument('chain_id')
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
@click.option('--export', help='Export monitoring data to file')
@click.option('--interval', default=5, help='Update interval in seconds')
@click.pass_context
def monitor(ctx, chain_id, realtime, export, interval):
"""Monitor chain activity"""
try:
config = load_multichain_config()
chain_manager = ChainManager(config)
if realtime:
# Real-time monitoring (placeholder implementation)
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
import time
console = Console()
def generate_monitor_layout():
try:
import asyncio
chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=True, metrics=True))
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="stats"),
Layout(name="activity", size=10)
)
# Header
layout["header"].update(
f"Chain Monitor: {chain_id} - {chain_info.status.value.upper()}"
)
# Stats table
stats_data = [
["Block Height", str(chain_info.block_height)],
["TPS", f"{chain_info.tps:.1f}"],
["Active Nodes", str(chain_info.active_nodes)],
["Gas Price", f"{chain_info.gas_price / 1e9:.1f} gwei"],
["Memory Usage", f"{chain_info.memory_usage_mb:.1f}MB"],
["Disk Usage", f"{chain_info.disk_usage_mb:.1f}MB"]
]
layout["stats"].update(str(stats_data))
# Recent activity (placeholder)
layout["activity"].update("Recent activity would be displayed here")
return layout
except Exception as e:
return f"Error getting chain info: {e}"
with Live(generate_monitor_layout(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_layout())
time.sleep(interval)
except KeyboardInterrupt:
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
else:
# Single snapshot
import asyncio
chain_info = asyncio.run(chain_manager.get_chain_info(chain_id, detailed=True, metrics=True))
stats_data = [
{
"Metric": "Block Height",
"Value": str(chain_info.block_height)
},
{
"Metric": "TPS",
"Value": f"{chain_info.tps:.1f}"
},
{
"Metric": "Active Nodes",
"Value": str(chain_info.active_nodes)
},
{
"Metric": "Gas Price",
"Value": f"{chain_info.gas_price / 1e9:.1f} gwei"
},
{
"Metric": "Memory Usage",
"Value": f"{chain_info.memory_usage_mb:.1f}MB"
},
{
"Metric": "Disk Usage",
"Value": f"{chain_info.disk_usage_mb:.1f}MB"
}
]
output(stats_data, ctx.obj.get('output_format', 'table'), title=f"Chain Statistics: {chain_id}")
if export:
import json
with open(export, 'w') as f:
json.dump(chain_info.dict(), f, indent=2, default=str)
success(f"Statistics exported to {export}")
except ChainNotFoundError:
error(f"Chain {chain_id} not found")
raise click.Abort()
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()

View File

@@ -1,476 +0,0 @@
"""Cross-chain trading commands for AITBC CLI"""
import click
import httpx
import json
from typing import Optional
from tabulate import tabulate
from ..config import get_config
from ..utils import success, error, output
@click.group()
def cross_chain():
"""Cross-chain trading operations"""
pass
@cross_chain.command()
@click.option("--from-chain", help="Source chain ID")
@click.option("--to-chain", help="Target chain ID")
@click.option("--from-token", help="Source token symbol")
@click.option("--to-token", help="Target token symbol")
@click.pass_context
def rates(ctx, from_chain: Optional[str], to_chain: Optional[str],
from_token: Optional[str], to_token: Optional[str]):
"""Get cross-chain exchange rates"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
# Get rates from cross-chain exchange
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/rates",
timeout=10
)
if response.status_code == 200:
rates_data = response.json()
rates = rates_data.get('rates', {})
if from_chain and to_chain:
# Get specific rate
pair_key = f"{from_chain}-{to_chain}"
if pair_key in rates:
success(f"Exchange rate {from_chain}{to_chain}: {rates[pair_key]}")
else:
error(f"No rate available for {from_chain}{to_chain}")
else:
# Show all rates
success("Cross-chain exchange rates:")
rate_table = []
for pair, rate in rates.items():
chains = pair.split('-')
rate_table.append([chains[0], chains[1], f"{rate:.6f}"])
if rate_table:
headers = ["From Chain", "To Chain", "Rate"]
print(tabulate(rate_table, headers=headers, tablefmt="grid"))
else:
output("No cross-chain rates available")
else:
error(f"Failed to get cross-chain rates: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.option("--from-chain", required=True, help="Source chain ID")
@click.option("--to-chain", required=True, help="Target chain ID")
@click.option("--from-token", required=True, help="Source token symbol")
@click.option("--to-token", required=True, help="Target token symbol")
@click.option("--amount", type=float, required=True, help="Amount to swap")
@click.option("--min-amount", type=float, help="Minimum amount to receive")
@click.option("--slippage", type=float, default=0.01, help="Slippage tolerance (0-0.1)")
@click.option("--address", help="User wallet address")
@click.pass_context
def swap(ctx, from_chain: str, to_chain: str, from_token: str, to_token: str,
amount: float, min_amount: Optional[float], slippage: float, address: Optional[str]):
"""Create cross-chain swap"""
config = ctx.obj['config']
# Validate inputs
if from_chain == to_chain:
error("Source and target chains must be different")
return
if amount <= 0:
error("Amount must be greater than 0")
return
# Use default address if not provided
if not address:
address = config.get('default_address', '0x1234567890123456789012345678901234567890')
# Calculate minimum amount if not provided
if not min_amount:
# Get rate first
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/rates",
timeout=10
)
if response.status_code == 200:
rates_data = response.json()
pair_key = f"{from_chain}-{to_chain}"
rate = rates_data.get('rates', {}).get(pair_key, 1.0)
min_amount = amount * rate * (1 - slippage) * 0.97 # Account for fees
else:
min_amount = amount * 0.95 # Conservative fallback
except:
min_amount = amount * 0.95
swap_data = {
"from_chain": from_chain,
"to_chain": to_chain,
"from_token": from_token,
"to_token": to_token,
"amount": amount,
"min_amount": min_amount,
"user_address": address,
"slippage_tolerance": slippage
}
try:
with httpx.Client() as client:
response = client.post(
f"http://localhost:8001/api/v1/cross-chain/swap",
json=swap_data,
timeout=30
)
if response.status_code == 200:
swap_result = response.json()
success("Cross-chain swap created successfully!")
output({
"Swap ID": swap_result.get('swap_id'),
"From Chain": swap_result.get('from_chain'),
"To Chain": swap_result.get('to_chain'),
"Amount": swap_result.get('amount'),
"Expected Amount": swap_result.get('expected_amount'),
"Rate": swap_result.get('rate'),
"Total Fees": swap_result.get('total_fees'),
"Status": swap_result.get('status')
}, ctx.obj['output_format'])
# Show swap ID for tracking
success(f"Track swap with: aitbc cross-chain status {swap_result.get('swap_id')}")
else:
error(f"Failed to create swap: {response.status_code}")
if response.text:
error(f"Details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.argument("swap_id")
@click.pass_context
def status(ctx, swap_id: str):
"""Check cross-chain swap status"""
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/swap/{swap_id}",
timeout=10
)
if response.status_code == 200:
swap_data = response.json()
success(f"Swap Status: {swap_data.get('status', 'unknown')}")
# Display swap details
details = {
"Swap ID": swap_data.get('swap_id'),
"From Chain": swap_data.get('from_chain'),
"To Chain": swap_data.get('to_chain'),
"From Token": swap_data.get('from_token'),
"To Token": swap_data.get('to_token'),
"Amount": swap_data.get('amount'),
"Expected Amount": swap_data.get('expected_amount'),
"Actual Amount": swap_data.get('actual_amount'),
"Status": swap_data.get('status'),
"Created At": swap_data.get('created_at'),
"Completed At": swap_data.get('completed_at'),
"Bridge Fee": swap_data.get('bridge_fee'),
"From Tx Hash": swap_data.get('from_tx_hash'),
"To Tx Hash": swap_data.get('to_tx_hash')
}
output(details, ctx.obj['output_format'])
# Show additional status info
if swap_data.get('status') == 'completed':
success("✅ Swap completed successfully!")
elif swap_data.get('status') == 'failed':
error("❌ Swap failed")
if swap_data.get('error_message'):
error(f"Error: {swap_data['error_message']}")
elif swap_data.get('status') == 'pending':
success("⏳ Swap is pending...")
elif swap_data.get('status') == 'executing':
success("🔄 Swap is executing...")
elif swap_data.get('status') == 'refunded':
success("💰 Swap was refunded")
else:
error(f"Failed to get swap status: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.option("--user-address", help="Filter by user address")
@click.option("--status", help="Filter by status")
@click.option("--limit", type=int, default=10, help="Number of swaps to show")
@click.pass_context
def swaps(ctx, user_address: Optional[str], status: Optional[str], limit: int):
"""List cross-chain swaps"""
params = {}
if user_address:
params['user_address'] = user_address
if status:
params['status'] = status
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/swaps",
params=params,
timeout=10
)
if response.status_code == 200:
swaps_data = response.json()
swaps = swaps_data.get('swaps', [])
if swaps:
success(f"Found {len(swaps)} cross-chain swaps:")
# Create table
swap_table = []
for swap in swaps[:limit]:
swap_table.append([
swap.get('swap_id', '')[:8] + '...',
swap.get('from_chain', ''),
swap.get('to_chain', ''),
swap.get('amount', 0),
swap.get('status', ''),
swap.get('created_at', '')[:19]
])
table(["ID", "From", "To", "Amount", "Status", "Created"], swap_table)
if len(swaps) > limit:
success(f"Showing {limit} of {len(swaps)} total swaps")
else:
success("No cross-chain swaps found")
else:
error(f"Failed to get swaps: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.option("--source-chain", required=True, help="Source chain ID")
@click.option("--target-chain", required=True, help="Target chain ID")
@click.option("--token", required=True, help="Token to bridge")
@click.option("--amount", type=float, required=True, help="Amount to bridge")
@click.option("--recipient", help="Recipient address")
@click.pass_context
def bridge(ctx, source_chain: str, target_chain: str, token: str,
amount: float, recipient: Optional[str]):
"""Create cross-chain bridge transaction"""
config = ctx.obj['config']
# Validate inputs
if source_chain == target_chain:
error("Source and target chains must be different")
return
if amount <= 0:
error("Amount must be greater than 0")
return
# Use default recipient if not provided
if not recipient:
recipient = config.get('default_address', '0x1234567890123456789012345678901234567890')
bridge_data = {
"source_chain": source_chain,
"target_chain": target_chain,
"token": token,
"amount": amount,
"recipient_address": recipient
}
try:
with httpx.Client() as client:
response = client.post(
f"http://localhost:8001/api/v1/cross-chain/bridge",
json=bridge_data,
timeout=30
)
if response.status_code == 200:
bridge_result = response.json()
success("Cross-chain bridge created successfully!")
output({
"Bridge ID": bridge_result.get('bridge_id'),
"Source Chain": bridge_result.get('source_chain'),
"Target Chain": bridge_result.get('target_chain'),
"Token": bridge_result.get('token'),
"Amount": bridge_result.get('amount'),
"Bridge Fee": bridge_result.get('bridge_fee'),
"Status": bridge_result.get('status')
}, ctx.obj['output_format'])
# Show bridge ID for tracking
success(f"Track bridge with: aitbc cross-chain bridge-status {bridge_result.get('bridge_id')}")
else:
error(f"Failed to create bridge: {response.status_code}")
if response.text:
error(f"Details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.argument("bridge_id")
@click.pass_context
def bridge_status(ctx, bridge_id: str):
"""Check cross-chain bridge status"""
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/bridge/{bridge_id}",
timeout=10
)
if response.status_code == 200:
bridge_data = response.json()
success(f"Bridge Status: {bridge_data.get('status', 'unknown')}")
# Display bridge details
details = {
"Bridge ID": bridge_data.get('bridge_id'),
"Source Chain": bridge_data.get('source_chain'),
"Target Chain": bridge_data.get('target_chain'),
"Token": bridge_data.get('token'),
"Amount": bridge_data.get('amount'),
"Recipient Address": bridge_data.get('recipient_address'),
"Status": bridge_data.get('status'),
"Created At": bridge_data.get('created_at'),
"Completed At": bridge_data.get('completed_at'),
"Bridge Fee": bridge_data.get('bridge_fee'),
"Source Tx Hash": bridge_data.get('source_tx_hash'),
"Target Tx Hash": bridge_data.get('target_tx_hash')
}
output(details, ctx.obj['output_format'])
# Show additional status info
if bridge_data.get('status') == 'completed':
success("✅ Bridge completed successfully!")
elif bridge_data.get('status') == 'failed':
error("❌ Bridge failed")
if bridge_data.get('error_message'):
error(f"Error: {bridge_data['error_message']}")
elif bridge_data.get('status') == 'pending':
success("⏳ Bridge is pending...")
elif bridge_data.get('status') == 'locked':
success("🔒 Bridge is locked...")
elif bridge_data.get('status') == 'transferred':
success("🔄 Bridge is transferring...")
else:
error(f"Failed to get bridge status: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.pass_context
def pools(ctx):
"""Show cross-chain liquidity pools"""
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/pools",
timeout=10
)
if response.status_code == 200:
pools_data = response.json()
pools = pools_data.get('pools', [])
if pools:
success(f"Found {len(pools)} cross-chain liquidity pools:")
# Create table
pool_table = []
for pool in pools:
pool_table.append([
pool.get('pool_id', ''),
pool.get('token_a', ''),
pool.get('token_b', ''),
pool.get('chain_a', ''),
pool.get('chain_b', ''),
f"{pool.get('reserve_a', 0):.2f}",
f"{pool.get('reserve_b', 0):.2f}",
f"{pool.get('total_liquidity', 0):.2f}",
f"{pool.get('apr', 0):.2%}"
])
table(["Pool ID", "Token A", "Token B", "Chain A", "Chain B",
"Reserve A", "Reserve B", "Liquidity", "APR"], pool_table)
else:
success("No cross-chain liquidity pools found")
else:
error(f"Failed to get pools: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@cross_chain.command()
@click.pass_context
def stats(ctx):
"""Show cross-chain trading statistics"""
try:
with httpx.Client() as client:
response = client.get(
f"http://localhost:8001/api/v1/cross-chain/stats",
timeout=10
)
if response.status_code == 200:
stats_data = response.json()
success("Cross-Chain Trading Statistics:")
# Show swap stats
swap_stats = stats_data.get('swap_stats', [])
if swap_stats:
success("Swap Statistics:")
swap_table = []
for stat in swap_stats:
swap_table.append([
stat.get('status', ''),
stat.get('count', 0),
f"{stat.get('volume', 0):.2f}"
])
table(["Status", "Count", "Volume"], swap_table)
# Show bridge stats
bridge_stats = stats_data.get('bridge_stats', [])
if bridge_stats:
success("Bridge Statistics:")
bridge_table = []
for stat in bridge_stats:
bridge_table.append([
stat.get('status', ''),
stat.get('count', 0),
f"{stat.get('volume', 0):.2f}"
])
table(["Status", "Count", "Volume"], bridge_table)
# Show overall stats
success("Overall Statistics:")
output({
"Total Volume": f"{stats_data.get('total_volume', 0):.2f}",
"Supported Chains": ", ".join(stats_data.get('supported_chains', [])),
"Last Updated": stats_data.get('timestamp', '')
}, ctx.obj['output_format'])
else:
error(f"Failed to get stats: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")

View File

@@ -1,378 +0,0 @@
"""Production deployment and scaling commands for AITBC CLI"""
import click
import asyncio
import json
from datetime import datetime
from typing import Optional
from ..core.deployment import (
ProductionDeployment, ScalingPolicy, DeploymentStatus
)
from ..utils import output, error, success
@click.group()
def deploy():
"""Production deployment and scaling commands"""
pass
@deploy.command()
@click.argument('name')
@click.argument('environment')
@click.argument('region')
@click.argument('instance_type')
@click.argument('min_instances', type=int)
@click.argument('max_instances', type=int)
@click.argument('desired_instances', type=int)
@click.argument('port', type=int)
@click.argument('domain')
@click.option('--db-host', default='localhost', help='Database host')
@click.option('--db-port', default=5432, help='Database port')
@click.option('--db-name', default='aitbc', help='Database name')
@click.pass_context
def create(ctx, name, environment, region, instance_type, min_instances, max_instances, desired_instances, port, domain, db_host, db_port, db_name):
"""Create a new deployment configuration"""
try:
deployment = ProductionDeployment()
# Database configuration
database_config = {
"host": db_host,
"port": db_port,
"name": db_name,
"ssl_enabled": True if environment == "production" else False
}
# Create deployment
deployment_id = asyncio.run(deployment.create_deployment(
name=name,
environment=environment,
region=region,
instance_type=instance_type,
min_instances=min_instances,
max_instances=max_instances,
desired_instances=desired_instances,
port=port,
domain=domain,
database_config=database_config
))
if deployment_id:
success(f"Deployment configuration created! ID: {deployment_id}")
deployment_data = {
"Deployment ID": deployment_id,
"Name": name,
"Environment": environment,
"Region": region,
"Instance Type": instance_type,
"Min Instances": min_instances,
"Max Instances": max_instances,
"Desired Instances": desired_instances,
"Port": port,
"Domain": domain,
"Status": "pending",
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(deployment_data, ctx.obj.get('output_format', 'table'))
else:
error("Failed to create deployment configuration")
raise click.Abort()
except Exception as e:
error(f"Error creating deployment: {str(e)}")
raise click.Abort()
@deploy.command()
@click.argument('deployment_id')
@click.pass_context
def start(ctx, deployment_id):
"""Deploy the application to production"""
try:
deployment = ProductionDeployment()
# Deploy application
success_deploy = asyncio.run(deployment.deploy_application(deployment_id))
if success_deploy:
success(f"Deployment {deployment_id} started successfully!")
deployment_data = {
"Deployment ID": deployment_id,
"Status": "running",
"Started": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(deployment_data, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to start deployment {deployment_id}")
raise click.Abort()
except Exception as e:
error(f"Error starting deployment: {str(e)}")
raise click.Abort()
@deploy.command()
@click.argument('deployment_id')
@click.argument('target_instances', type=int)
@click.option('--reason', default='manual', help='Scaling reason')
@click.pass_context
def scale(ctx, deployment_id, target_instances, reason):
"""Scale a deployment to target instance count"""
try:
deployment = ProductionDeployment()
# Scale deployment
success_scale = asyncio.run(deployment.scale_deployment(deployment_id, target_instances, reason))
if success_scale:
success(f"Deployment {deployment_id} scaled to {target_instances} instances!")
scaling_data = {
"Deployment ID": deployment_id,
"Target Instances": target_instances,
"Reason": reason,
"Status": "completed",
"Scaled": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(scaling_data, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to scale deployment {deployment_id}")
raise click.Abort()
except Exception as e:
error(f"Error scaling deployment: {str(e)}")
raise click.Abort()
@deploy.command()
@click.argument('deployment_id')
@click.pass_context
def status(ctx, deployment_id):
"""Get comprehensive deployment status"""
try:
deployment = ProductionDeployment()
# Get deployment status
status_data = asyncio.run(deployment.get_deployment_status(deployment_id))
if not status_data:
error(f"Deployment {deployment_id} not found")
raise click.Abort()
# Format deployment info
deployment_info = status_data["deployment"]
info_data = [
{"Metric": "Deployment ID", "Value": deployment_info["deployment_id"]},
{"Metric": "Name", "Value": deployment_info["name"]},
{"Metric": "Environment", "Value": deployment_info["environment"]},
{"Metric": "Region", "Value": deployment_info["region"]},
{"Metric": "Instance Type", "Value": deployment_info["instance_type"]},
{"Metric": "Min Instances", "Value": deployment_info["min_instances"]},
{"Metric": "Max Instances", "Value": deployment_info["max_instances"]},
{"Metric": "Desired Instances", "Value": deployment_info["desired_instances"]},
{"Metric": "Port", "Value": deployment_info["port"]},
{"Metric": "Domain", "Value": deployment_info["domain"]},
{"Metric": "Health Status", "Value": "Healthy" if status_data["health_status"] else "Unhealthy"},
{"Metric": "Uptime", "Value": f"{status_data['uptime_percentage']:.2f}%"}
]
output(info_data, ctx.obj.get('output_format', 'table'), title=f"Deployment Status: {deployment_id}")
# Show metrics if available
if status_data["metrics"]:
metrics = status_data["metrics"]
metrics_data = [
{"Metric": "CPU Usage", "Value": f"{metrics['cpu_usage']:.1f}%"},
{"Metric": "Memory Usage", "Value": f"{metrics['memory_usage']:.1f}%"},
{"Metric": "Disk Usage", "Value": f"{metrics['disk_usage']:.1f}%"},
{"Metric": "Request Count", "Value": metrics['request_count']},
{"Metric": "Error Rate", "Value": f"{metrics['error_rate']:.2f}%"},
{"Metric": "Response Time", "Value": f"{metrics['response_time']:.1f}ms"},
{"Metric": "Active Instances", "Value": metrics['active_instances']}
]
output(metrics_data, ctx.obj.get('output_format', 'table'), title="Performance Metrics")
# Show recent scaling events
if status_data["recent_scaling_events"]:
events = status_data["recent_scaling_events"]
events_data = [
{
"Event ID": event["event_id"][:8],
"Type": event["scaling_type"],
"From": event["old_instances"],
"To": event["new_instances"],
"Reason": event["trigger_reason"],
"Success": "Yes" if event["success"] else "No",
"Time": event["triggered_at"]
}
for event in events
]
output(events_data, ctx.obj.get('output_format', 'table'), title="Recent Scaling Events")
except Exception as e:
error(f"Error getting deployment status: {str(e)}")
raise click.Abort()
@deploy.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def overview(ctx, format):
"""Get overview of all deployments"""
try:
deployment = ProductionDeployment()
# Get cluster overview
overview_data = asyncio.run(deployment.get_cluster_overview())
if not overview_data:
error("No deployment data available")
raise click.Abort()
# Cluster metrics
cluster_data = [
{"Metric": "Total Deployments", "Value": overview_data["total_deployments"]},
{"Metric": "Running Deployments", "Value": overview_data["running_deployments"]},
{"Metric": "Total Instances", "Value": overview_data["total_instances"]},
{"Metric": "Health Check Coverage", "Value": f"{overview_data['health_check_coverage']:.1%}"},
{"Metric": "Recent Scaling Events", "Value": overview_data["recent_scaling_events"]},
{"Metric": "Scaling Success Rate", "Value": f"{overview_data['successful_scaling_rate']:.1%}"}
]
output(cluster_data, ctx.obj.get('output_format', format), title="Cluster Overview")
# Aggregate metrics
if "aggregate_metrics" in overview_data:
metrics = overview_data["aggregate_metrics"]
metrics_data = [
{"Metric": "Average CPU Usage", "Value": f"{metrics['total_cpu_usage']:.1f}%"},
{"Metric": "Average Memory Usage", "Value": f"{metrics['total_memory_usage']:.1f}%"},
{"Metric": "Average Disk Usage", "Value": f"{metrics['total_disk_usage']:.1f}%"},
{"Metric": "Average Response Time", "Value": f"{metrics['average_response_time']:.1f}ms"},
{"Metric": "Average Error Rate", "Value": f"{metrics['average_error_rate']:.2f}%"},
{"Metric": "Average Uptime", "Value": f"{metrics['average_uptime']:.1f}%"}
]
output(metrics_data, ctx.obj.get('output_format', format), title="Aggregate Performance Metrics")
except Exception as e:
error(f"Error getting cluster overview: {str(e)}")
raise click.Abort()
@deploy.command()
@click.argument('deployment_id')
@click.option('--interval', default=60, help='Update interval in seconds')
@click.pass_context
def monitor(ctx, deployment_id, interval):
"""Monitor deployment performance in real-time"""
try:
deployment = ProductionDeployment()
# Real-time monitoring
from rich.console import Console
from rich.live import Live
from rich.table import Table
import time
console = Console()
def generate_monitor_table():
try:
status_data = asyncio.run(deployment.get_deployment_status(deployment_id))
if not status_data:
return f"Deployment {deployment_id} not found"
deployment_info = status_data["deployment"]
metrics = status_data.get("metrics")
table = Table(title=f"Deployment Monitor - {deployment_info['name']} ({deployment_id[:8]}) - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
table.add_row("Environment", deployment_info["environment"])
table.add_row("Desired Instances", str(deployment_info["desired_instances"]))
table.add_row("Health Status", "✅ Healthy" if status_data["health_status"] else "❌ Unhealthy")
table.add_row("Uptime", f"{status_data['uptime_percentage']:.2f}%")
if metrics:
table.add_row("CPU Usage", f"{metrics['cpu_usage']:.1f}%")
table.add_row("Memory Usage", f"{metrics['memory_usage']:.1f}%")
table.add_row("Disk Usage", f"{metrics['disk_usage']:.1f}%")
table.add_row("Request Count", str(metrics['request_count']))
table.add_row("Error Rate", f"{metrics['error_rate']:.2f}%")
table.add_row("Response Time", f"{metrics['response_time']:.1f}ms")
table.add_row("Active Instances", str(metrics['active_instances']))
return table
except Exception as e:
return f"Error getting deployment data: {e}"
with Live(generate_monitor_table(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_table())
time.sleep(interval)
except KeyboardInterrupt:
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()
@deploy.command()
@click.argument('deployment_id')
@click.pass_context
def auto_scale(ctx, deployment_id):
"""Trigger auto-scaling evaluation for a deployment"""
try:
deployment = ProductionDeployment()
# Trigger auto-scaling
success_auto = asyncio.run(deployment.auto_scale_deployment(deployment_id))
if success_auto:
success(f"Auto-scaling evaluation completed for deployment {deployment_id}")
else:
error(f"Auto-scaling evaluation failed for deployment {deployment_id}")
raise click.Abort()
except Exception as e:
error(f"Error in auto-scaling: {str(e)}")
raise click.Abort()
@deploy.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def list_deployments(ctx, format):
"""List all deployments"""
try:
deployment = ProductionDeployment()
# Get all deployment statuses
deployments = []
for deployment_id in deployment.deployments.keys():
status_data = asyncio.run(deployment.get_deployment_status(deployment_id))
if status_data:
deployment_info = status_data["deployment"]
deployments.append({
"Deployment ID": deployment_info["deployment_id"][:8],
"Name": deployment_info["name"],
"Environment": deployment_info["environment"],
"Instances": f"{deployment_info['desired_instances']}/{deployment_info['max_instances']}",
"Status": "Running" if status_data["health_status"] else "Stopped",
"Uptime": f"{status_data['uptime_percentage']:.1f}%",
"Created": deployment_info["created_at"]
})
if not deployments:
output("No deployments found", ctx.obj.get('output_format', 'table'))
return
output(deployments, ctx.obj.get('output_format', format), title="All Deployments")
except Exception as e:
error(f"Error listing deployments: {str(e)}")
raise click.Abort()

View File

@@ -1,981 +0,0 @@
"""Exchange integration commands for AITBC CLI"""
import click
import httpx
import json
import os
from pathlib import Path
from typing import Optional, Dict, Any, List
from datetime import datetime
from ..utils import output, error, success, warning
from ..config import get_config
@click.group()
def exchange():
"""Exchange integration and trading management commands"""
pass
@exchange.command()
@click.option("--name", required=True, help="Exchange name (e.g., Binance, Coinbase, Kraken)")
@click.option("--api-key", required=True, help="Exchange API key")
@click.option("--secret-key", help="Exchange API secret key")
@click.option("--sandbox", is_flag=True, help="Use sandbox/testnet environment")
@click.option("--description", help="Exchange description")
@click.pass_context
def register(ctx, name: str, api_key: str, secret_key: Optional[str], sandbox: bool, description: Optional[str]):
"""Register a new exchange integration"""
config = get_config()
# Create exchange configuration
exchange_config = {
"name": name,
"api_key": api_key,
"secret_key": secret_key or "NOT_SET",
"sandbox": sandbox,
"description": description or f"{name} exchange integration",
"created_at": datetime.utcnow().isoformat(),
"status": "active",
"trading_pairs": [],
"last_sync": None
}
# Store exchange configuration
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
exchanges_file.parent.mkdir(parents=True, exist_ok=True)
# Load existing exchanges
exchanges = {}
if exchanges_file.exists():
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
# Add new exchange
exchanges[name.lower()] = exchange_config
# Save exchanges
with open(exchanges_file, 'w') as f:
json.dump(exchanges, f, indent=2)
success(f"Exchange '{name}' registered successfully")
output({
"exchange": name,
"status": "registered",
"sandbox": sandbox,
"created_at": exchange_config["created_at"]
})
@exchange.command()
@click.option("--base-asset", required=True, help="Base asset symbol (e.g., AITBC)")
@click.option("--quote-asset", required=True, help="Quote asset symbol (e.g., BTC)")
@click.option("--exchange", required=True, help="Exchange name")
@click.option("--min-order-size", type=float, default=0.001, help="Minimum order size")
@click.option("--price-precision", type=int, default=8, help="Price precision")
@click.option("--quantity-precision", type=int, default=8, help="Quantity precision")
@click.pass_context
def create_pair(ctx, base_asset: str, quote_asset: str, exchange: str, min_order_size: float, price_precision: int, quantity_precision: int):
"""Create a new trading pair"""
pair_symbol = f"{base_asset}/{quote_asset}"
# Load exchanges
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
if not exchanges_file.exists():
error("No exchanges registered. Use 'aitbc exchange register' first.")
return
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
if exchange.lower() not in exchanges:
error(f"Exchange '{exchange}' not registered.")
return
# Create trading pair configuration
pair_config = {
"symbol": pair_symbol,
"base_asset": base_asset,
"quote_asset": quote_asset,
"exchange": exchange,
"min_order_size": min_order_size,
"price_precision": price_precision,
"quantity_precision": quantity_precision,
"status": "active",
"created_at": datetime.utcnow().isoformat(),
"trading_enabled": False
}
# Update exchange with new pair
exchanges[exchange.lower()]["trading_pairs"].append(pair_config)
# Save exchanges
with open(exchanges_file, 'w') as f:
json.dump(exchanges, f, indent=2)
success(f"Trading pair '{pair_symbol}' created on {exchange}")
output({
"pair": pair_symbol,
"exchange": exchange,
"status": "created",
"min_order_size": min_order_size,
"created_at": pair_config["created_at"]
})
@exchange.command()
@click.option("--pair", required=True, help="Trading pair symbol (e.g., AITBC/BTC)")
@click.option("--price", type=float, help="Initial price for the pair")
@click.option("--base-liquidity", type=float, default=10000, help="Base asset liquidity amount")
@click.option("--quote-liquidity", type=float, default=10000, help="Quote asset liquidity amount")
@click.option("--exchange", help="Exchange name (if not specified, uses first available)")
@click.pass_context
def start_trading(ctx, pair: str, price: Optional[float], base_liquidity: float, quote_liquidity: float, exchange: Optional[str]):
"""Start trading for a specific pair"""
# Load exchanges
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
if not exchanges_file.exists():
error("No exchanges registered. Use 'aitbc exchange register' first.")
return
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
# Find the pair
target_exchange = None
target_pair = None
for exchange_name, exchange_data in exchanges.items():
for pair_config in exchange_data.get("trading_pairs", []):
if pair_config["symbol"] == pair:
target_exchange = exchange_name
target_pair = pair_config
break
if target_pair:
break
if not target_pair:
error(f"Trading pair '{pair}' not found. Create it first with 'aitbc exchange create-pair'.")
return
# Update pair to enable trading
target_pair["trading_enabled"] = True
target_pair["started_at"] = datetime.utcnow().isoformat()
target_pair["initial_price"] = price or 0.00001 # Default price for AITBC
target_pair["base_liquidity"] = base_liquidity
target_pair["quote_liquidity"] = quote_liquidity
# Save exchanges
with open(exchanges_file, 'w') as f:
json.dump(exchanges, f, indent=2)
success(f"Trading started for pair '{pair}' on {target_exchange}")
output({
"pair": pair,
"exchange": target_exchange,
"status": "trading_active",
"initial_price": target_pair["initial_price"],
"base_liquidity": base_liquidity,
"quote_liquidity": quote_liquidity,
"started_at": target_pair["started_at"]
})
@exchange.command()
@click.option("--pair", help="Trading pair symbol (e.g., AITBC/BTC)")
@click.option("--exchange", help="Exchange name")
@click.option("--real-time", is_flag=True, help="Enable real-time monitoring")
@click.option("--interval", type=int, default=60, help="Update interval in seconds")
@click.pass_context
def monitor(ctx, pair: Optional[str], exchange: Optional[str], real_time: bool, interval: int):
"""Monitor exchange trading activity"""
# Load exchanges
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
if not exchanges_file.exists():
error("No exchanges registered. Use 'aitbc exchange register' first.")
return
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
# Filter exchanges and pairs
monitoring_data = []
for exchange_name, exchange_data in exchanges.items():
if exchange and exchange_name != exchange.lower():
continue
for pair_config in exchange_data.get("trading_pairs", []):
if pair and pair_config["symbol"] != pair:
continue
monitoring_data.append({
"exchange": exchange_name,
"pair": pair_config["symbol"],
"status": "active" if pair_config.get("trading_enabled") else "inactive",
"created_at": pair_config.get("created_at"),
"started_at": pair_config.get("started_at"),
"initial_price": pair_config.get("initial_price"),
"base_liquidity": pair_config.get("base_liquidity"),
"quote_liquidity": pair_config.get("quote_liquidity")
})
if not monitoring_data:
error("No trading pairs found for monitoring.")
return
# Display monitoring data
output({
"monitoring_active": True,
"real_time": real_time,
"interval": interval,
"pairs": monitoring_data,
"total_pairs": len(monitoring_data)
})
if real_time:
warning(f"Real-time monitoring enabled. Updates every {interval} seconds.")
# Note: In a real implementation, this would start a background monitoring process
@exchange.command()
@click.option("--pair", required=True, help="Trading pair symbol (e.g., AITBC/BTC)")
@click.option("--amount", type=float, required=True, help="Liquidity amount")
@click.option("--side", type=click.Choice(['buy', 'sell']), default='both', help="Side to provide liquidity")
@click.option("--exchange", help="Exchange name")
@click.pass_context
def add_liquidity(ctx, pair: str, amount: float, side: str, exchange: Optional[str]):
"""Add liquidity to a trading pair"""
# Load exchanges
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
if not exchanges_file.exists():
error("No exchanges registered. Use 'aitbc exchange register' first.")
return
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
# Find the pair
target_exchange = None
target_pair = None
for exchange_name, exchange_data in exchanges.items():
if exchange and exchange_name != exchange.lower():
continue
for pair_config in exchange_data.get("trading_pairs", []):
if pair_config["symbol"] == pair:
target_exchange = exchange_name
target_pair = pair_config
break
if target_pair:
break
if not target_pair:
error(f"Trading pair '{pair}' not found.")
return
# Add liquidity
if side == 'buy' or side == 'both':
target_pair["quote_liquidity"] = target_pair.get("quote_liquidity", 0) + amount
if side == 'sell' or side == 'both':
target_pair["base_liquidity"] = target_pair.get("base_liquidity", 0) + amount
target_pair["liquidity_updated_at"] = datetime.utcnow().isoformat()
# Save exchanges
with open(exchanges_file, 'w') as f:
json.dump(exchanges, f, indent=2)
success(f"Added {amount} liquidity to {pair} on {target_exchange} ({side} side)")
output({
"pair": pair,
"exchange": target_exchange,
"amount": amount,
"side": side,
"base_liquidity": target_pair.get("base_liquidity"),
"quote_liquidity": target_pair.get("quote_liquidity"),
"updated_at": target_pair["liquidity_updated_at"]
})
@exchange.command()
@click.pass_context
def list(ctx):
"""List all registered exchanges and trading pairs"""
# Load exchanges
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
if not exchanges_file.exists():
warning("No exchanges registered.")
return
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
# Format output
exchange_list = []
for exchange_name, exchange_data in exchanges.items():
exchange_info = {
"name": exchange_data["name"],
"status": exchange_data["status"],
"sandbox": exchange_data.get("sandbox", False),
"trading_pairs": len(exchange_data.get("trading_pairs", [])),
"created_at": exchange_data["created_at"]
}
exchange_list.append(exchange_info)
output({
"exchanges": exchange_list,
"total_exchanges": len(exchange_list),
"total_pairs": sum(ex["trading_pairs"] for ex in exchange_list)
})
@exchange.command()
@click.argument("exchange_name")
@click.pass_context
def status(ctx, exchange_name: str):
"""Get detailed status of a specific exchange"""
# Load exchanges
exchanges_file = Path.home() / ".aitbc" / "exchanges.json"
if not exchanges_file.exists():
error("No exchanges registered.")
return
with open(exchanges_file, 'r') as f:
exchanges = json.load(f)
if exchange_name.lower() not in exchanges:
error(f"Exchange '{exchange_name}' not found.")
return
exchange_data = exchanges[exchange_name.lower()]
output({
"exchange": exchange_data["name"],
"status": exchange_data["status"],
"sandbox": exchange_data.get("sandbox", False),
"description": exchange_data.get("description"),
"created_at": exchange_data["created_at"],
"trading_pairs": exchange_data.get("trading_pairs", []),
"last_sync": exchange_data.get("last_sync")
})
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/v1/exchange/rates",
timeout=10
)
if response.status_code == 200:
rates_data = response.json()
success("Current exchange rates:")
output(rates_data, ctx.obj['output_format'])
else:
error(f"Failed to get exchange rates: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--aitbc-amount", type=float, help="Amount of AITBC to buy")
@click.option("--btc-amount", type=float, help="Amount of BTC to spend")
@click.option("--user-id", help="User ID for the payment")
@click.option("--notes", help="Additional notes for the payment")
@click.pass_context
def create_payment(ctx, aitbc_amount: Optional[float], btc_amount: Optional[float],
user_id: Optional[str], notes: Optional[str]):
"""Create a Bitcoin payment request for AITBC purchase"""
config = ctx.obj['config']
# Validate input
if aitbc_amount is not None and aitbc_amount <= 0:
error("AITBC amount must be greater than 0")
return
if btc_amount is not None and btc_amount <= 0:
error("BTC amount must be greater than 0")
return
if not aitbc_amount and not btc_amount:
error("Either --aitbc-amount or --btc-amount must be specified")
return
# Get exchange rates to calculate missing amount
try:
with httpx.Client() as client:
rates_response = client.get(
f"{config.coordinator_url}/v1/exchange/rates",
timeout=10
)
if rates_response.status_code != 200:
error("Failed to get exchange rates")
return
rates = rates_response.json()
btc_to_aitbc = rates.get('btc_to_aitbc', 100000)
# Calculate missing amount
if aitbc_amount and not btc_amount:
btc_amount = aitbc_amount / btc_to_aitbc
elif btc_amount and not aitbc_amount:
aitbc_amount = btc_amount * btc_to_aitbc
# Prepare payment request
payment_data = {
"user_id": user_id or "cli_user",
"aitbc_amount": aitbc_amount,
"btc_amount": btc_amount
}
if notes:
payment_data["notes"] = notes
# Create payment
response = client.post(
f"{config.coordinator_url}/v1/exchange/create-payment",
json=payment_data,
timeout=10
)
if response.status_code == 200:
payment = response.json()
success(f"Payment created: {payment.get('payment_id')}")
success(f"Send {btc_amount:.8f} BTC to: {payment.get('payment_address')}")
success(f"Expires at: {payment.get('expires_at')}")
output(payment, ctx.obj['output_format'])
else:
error(f"Failed to create payment: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--payment-id", required=True, help="Payment ID to check")
@click.pass_context
def payment_status(ctx, payment_id: str):
"""Check payment confirmation status"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/v1/exchange/payment-status/{payment_id}",
timeout=10
)
if response.status_code == 200:
status_data = response.json()
status = status_data.get('status', 'unknown')
if status == 'confirmed':
success(f"Payment {payment_id} is confirmed!")
success(f"AITBC amount: {status_data.get('aitbc_amount', 0)}")
elif status == 'pending':
success(f"Payment {payment_id} is pending confirmation")
elif status == 'expired':
error(f"Payment {payment_id} has expired")
else:
success(f"Payment {payment_id} status: {status}")
output(status_data, ctx.obj['output_format'])
else:
error(f"Failed to get payment status: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.pass_context
def market_stats(ctx):
"""Get exchange market statistics"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/v1/exchange/market-stats",
timeout=10
)
if response.status_code == 200:
stats = response.json()
success("Exchange market statistics:")
output(stats, ctx.obj['output_format'])
else:
error(f"Failed to get market stats: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@exchange.group()
def wallet():
"""Bitcoin wallet operations"""
pass
@wallet.command()
@click.pass_context
def balance(ctx):
"""Get Bitcoin wallet balance"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/exchange/wallet/balance",
timeout=10
)
if response.status_code == 200:
balance_data = response.json()
success("Bitcoin wallet balance:")
output(balance_data, ctx.obj['output_format'])
else:
error(f"Failed to get wallet balance: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@wallet.command()
@click.pass_context
def info(ctx):
"""Get comprehensive Bitcoin wallet information"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/exchange/wallet/info",
timeout=10
)
if response.status_code == 200:
wallet_info = response.json()
success("Bitcoin wallet information:")
output(wallet_info, ctx.obj['output_format'])
else:
error(f"Failed to get wallet info: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--name", required=True, help="Exchange name (e.g., Binance, Coinbase)")
@click.option("--api-key", required=True, help="API key for exchange integration")
@click.option("--api-secret", help="API secret for exchange integration")
@click.option("--sandbox", is_flag=True, default=False, help="Use sandbox/testnet environment")
@click.pass_context
def register(ctx, name: str, api_key: str, api_secret: Optional[str], sandbox: bool):
"""Register a new exchange integration"""
config = ctx.obj['config']
exchange_data = {
"name": name,
"api_key": api_key,
"sandbox": sandbox
}
if api_secret:
exchange_data["api_secret"] = api_secret
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/v1/exchange/register",
json=exchange_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Exchange '{name}' registered successfully!")
success(f"Exchange ID: {result.get('exchange_id')}")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to register exchange: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--pair", required=True, help="Trading pair (e.g., AITBC/BTC, AITBC/ETH)")
@click.option("--base-asset", required=True, help="Base asset symbol")
@click.option("--quote-asset", required=True, help="Quote asset symbol")
@click.option("--min-order-size", type=float, help="Minimum order size")
@click.option("--max-order-size", type=float, help="Maximum order size")
@click.option("--price-precision", type=int, default=8, help="Price decimal precision")
@click.option("--size-precision", type=int, default=8, help="Size decimal precision")
@click.pass_context
def create_pair(ctx, pair: str, base_asset: str, quote_asset: str,
min_order_size: Optional[float], max_order_size: Optional[float],
price_precision: int, size_precision: int):
"""Create a new trading pair"""
config = ctx.obj['config']
pair_data = {
"pair": pair,
"base_asset": base_asset,
"quote_asset": quote_asset,
"price_precision": price_precision,
"size_precision": size_precision
}
if min_order_size is not None:
pair_data["min_order_size"] = min_order_size
if max_order_size is not None:
pair_data["max_order_size"] = max_order_size
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/v1/exchange/create-pair",
json=pair_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Trading pair '{pair}' created successfully!")
success(f"Pair ID: {result.get('pair_id')}")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to create trading pair: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--pair", required=True, help="Trading pair to start trading")
@click.option("--exchange", help="Specific exchange to enable")
@click.option("--order-type", multiple=True, default=["limit", "market"],
help="Order types to enable (limit, market, stop_limit)")
@click.pass_context
def start_trading(ctx, pair: str, exchange: Optional[str], order_type: tuple):
"""Start trading for a specific pair"""
config = ctx.obj['config']
trading_data = {
"pair": pair,
"order_types": list(order_type)
}
if exchange:
trading_data["exchange"] = exchange
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/v1/exchange/start-trading",
json=trading_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"Trading started for pair '{pair}'!")
success(f"Order types: {', '.join(order_type)}")
output(result, ctx.obj['output_format'])
else:
error(f"Failed to start trading: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--pair", help="Filter by trading pair")
@click.option("--exchange", help="Filter by exchange")
@click.option("--status", help="Filter by status (active, inactive, suspended)")
@click.pass_context
def list_pairs(ctx, pair: Optional[str], exchange: Optional[str], status: Optional[str]):
"""List all trading pairs"""
config = ctx.obj['config']
params = {}
if pair:
params["pair"] = pair
if exchange:
params["exchange"] = exchange
if status:
params["status"] = status
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/v1/exchange/pairs",
params=params,
timeout=10
)
if response.status_code == 200:
pairs = response.json()
success("Trading pairs:")
output(pairs, ctx.obj['output_format'])
else:
error(f"Failed to list trading pairs: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
@exchange.command()
@click.option("--exchange", required=True, help="Exchange name (binance, coinbasepro, kraken)")
@click.option("--api-key", required=True, help="API key for exchange")
@click.option("--secret", required=True, help="API secret for exchange")
@click.option("--sandbox", is_flag=True, default=True, help="Use sandbox/testnet environment")
@click.option("--passphrase", help="API passphrase (for Coinbase)")
@click.pass_context
def connect(ctx, exchange: str, api_key: str, secret: str, sandbox: bool, passphrase: Optional[str]):
"""Connect to a real exchange API"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import connect_to_exchange
# Run async connection
import asyncio
success = asyncio.run(connect_to_exchange(exchange, api_key, secret, sandbox, passphrase))
if success:
success(f"✅ Successfully connected to {exchange}")
if sandbox:
success("🧪 Using sandbox/testnet environment")
else:
error(f"❌ Failed to connect to {exchange}")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Connection error: {e}")
@exchange.command()
@click.option("--exchange", help="Check specific exchange (default: all)")
@click.pass_context
def status(ctx, exchange: Optional[str]):
"""Check exchange connection status"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import get_exchange_status
# Run async status check
import asyncio
status_data = asyncio.run(get_exchange_status(exchange))
# Display status
for exchange_name, health in status_data.items():
status_icon = "🟢" if health.status.value == "connected" else "🔴" if health.status.value == "error" else "🟡"
success(f"{status_icon} {exchange_name.upper()}")
success(f" Status: {health.status.value}")
success(f" Latency: {health.latency_ms:.2f}ms")
success(f" Last Check: {health.last_check.strftime('%H:%M:%S')}")
if health.error_message:
error(f" Error: {health.error_message}")
print()
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Status check error: {e}")
@exchange.command()
@click.option("--exchange", required=True, help="Exchange name to disconnect")
@click.pass_context
def disconnect(ctx, exchange: str):
"""Disconnect from an exchange"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import disconnect_from_exchange
# Run async disconnection
import asyncio
success = asyncio.run(disconnect_from_exchange(exchange))
if success:
success(f"🔌 Disconnected from {exchange}")
else:
error(f"❌ Failed to disconnect from {exchange}")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Disconnection error: {e}")
@exchange.command()
@click.option("--exchange", required=True, help="Exchange name")
@click.option("--symbol", required=True, help="Trading symbol (e.g., BTC/USDT)")
@click.option("--limit", type=int, default=20, help="Order book depth")
@click.pass_context
def orderbook(ctx, exchange: str, symbol: str, limit: int):
"""Get order book from exchange"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import exchange_manager
# Run async order book fetch
import asyncio
orderbook = asyncio.run(exchange_manager.get_order_book(exchange, symbol, limit))
# Display order book
success(f"📊 Order Book for {symbol} on {exchange.upper()}")
# Display bids (buy orders)
if 'bids' in orderbook and orderbook['bids']:
success("\n🟢 Bids (Buy Orders):")
for i, bid in enumerate(orderbook['bids'][:10]):
price, amount = bid
success(f" {i+1}. ${price:.8f} x {amount:.6f}")
# Display asks (sell orders)
if 'asks' in orderbook and orderbook['asks']:
success("\n🔴 Asks (Sell Orders):")
for i, ask in enumerate(orderbook['asks'][:10]):
price, amount = ask
success(f" {i+1}. ${price:.8f} x {amount:.6f}")
# Spread
if 'bids' in orderbook and 'asks' in orderbook and orderbook['bids'] and orderbook['asks']:
best_bid = orderbook['bids'][0][0]
best_ask = orderbook['asks'][0][0]
spread = best_ask - best_bid
spread_pct = (spread / best_bid) * 100
success(f"\n📈 Spread: ${spread:.8f} ({spread_pct:.4f}%)")
success(f"🎯 Best Bid: ${best_bid:.8f}")
success(f"🎯 Best Ask: ${best_ask:.8f}")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Order book error: {e}")
@exchange.command()
@click.option("--exchange", required=True, help="Exchange name")
@click.pass_context
def balance(ctx, exchange: str):
"""Get account balance from exchange"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import exchange_manager
# Run async balance fetch
import asyncio
balance_data = asyncio.run(exchange_manager.get_balance(exchange))
# Display balance
success(f"💰 Account Balance on {exchange.upper()}")
if 'total' in balance_data:
for asset, amount in balance_data['total'].items():
if amount > 0:
available = balance_data.get('free', {}).get(asset, 0)
used = balance_data.get('used', {}).get(asset, 0)
success(f"\n{asset}:")
success(f" Total: {amount:.8f}")
success(f" Available: {available:.8f}")
success(f" In Orders: {used:.8f}")
else:
warning("No balance data available")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Balance error: {e}")
@exchange.command()
@click.option("--exchange", required=True, help="Exchange name")
@click.pass_context
def pairs(ctx, exchange: str):
"""List supported trading pairs"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import exchange_manager
# Run async pairs fetch
import asyncio
pairs = asyncio.run(exchange_manager.get_supported_pairs(exchange))
# Display pairs
success(f"📋 Supported Trading Pairs on {exchange.upper()}")
success(f"Found {len(pairs)} trading pairs:\n")
# Group by base currency
base_currencies = {}
for pair in pairs:
base = pair.split('/')[0] if '/' in pair else pair.split('-')[0]
if base not in base_currencies:
base_currencies[base] = []
base_currencies[base].append(pair)
# Display organized pairs
for base in sorted(base_currencies.keys()):
success(f"\n🔹 {base}:")
for pair in sorted(base_currencies[base][:10]): # Show first 10 per base
success(f"{pair}")
if len(base_currencies[base]) > 10:
success(f" ... and {len(base_currencies[base]) - 10} more")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Pairs error: {e}")
@exchange.command()
@click.pass_context
def list_exchanges(ctx):
"""List all supported exchanges"""
try:
# Import the real exchange integration
import sys
sys.path.append('/home/oib/windsurf/aitbc/apps/exchange')
from real_exchange_integration import exchange_manager
success("🏢 Supported Exchanges:")
for exchange in exchange_manager.supported_exchanges:
success(f"{exchange.title()}")
success("\n📝 Usage:")
success(" aitbc exchange connect --exchange binance --api-key <key> --secret <secret>")
success(" aitbc exchange status --exchange binance")
success(" aitbc exchange orderbook --exchange binance --symbol BTC/USDT")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
except Exception as e:
error(f"❌ Error: {e}")

View File

@@ -1,494 +0,0 @@
"""Global chain marketplace commands for AITBC CLI"""
import click
import asyncio
import json
from decimal import Decimal
from datetime import datetime
from typing import Optional
from ..core.config import load_multichain_config
from ..core.marketplace import (
GlobalChainMarketplace, ChainType, MarketplaceStatus,
TransactionStatus
)
from ..utils import output, error, success
@click.group()
def marketplace():
"""Global chain marketplace commands"""
pass
@marketplace.command()
@click.argument('chain_id')
@click.argument('chain_name')
@click.argument('chain_type')
@click.argument('description')
@click.argument('seller_id')
@click.argument('price')
@click.option('--currency', default='ETH', help='Currency for pricing')
@click.option('--specs', help='Chain specifications (JSON string)')
@click.option('--metadata', help='Additional metadata (JSON string)')
@click.pass_context
def list(ctx, chain_id, chain_name, chain_type, description, seller_id, price, currency, specs, metadata):
"""List a chain for sale in the marketplace"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Parse chain type
try:
chain_type_enum = ChainType(chain_type)
except ValueError:
error(f"Invalid chain type: {chain_type}")
error(f"Valid types: {[t.value for t in ChainType]}")
raise click.Abort()
# Parse price
try:
price_decimal = Decimal(price)
except:
error("Invalid price format")
raise click.Abort()
# Parse specifications
chain_specs = {}
if specs:
try:
chain_specs = json.loads(specs)
except json.JSONDecodeError:
error("Invalid JSON specifications")
raise click.Abort()
# Parse metadata
metadata_dict = {}
if metadata:
try:
metadata_dict = json.loads(metadata)
except json.JSONDecodeError:
error("Invalid JSON metadata")
raise click.Abort()
# Create listing
listing_id = asyncio.run(marketplace.create_listing(
chain_id, chain_name, chain_type_enum, description,
seller_id, price_decimal, currency, chain_specs, metadata_dict
))
if listing_id:
success(f"Chain listed successfully! Listing ID: {listing_id}")
listing_data = {
"Listing ID": listing_id,
"Chain ID": chain_id,
"Chain Name": chain_name,
"Type": chain_type,
"Price": f"{price} {currency}",
"Seller": seller_id,
"Status": "active",
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(listing_data, ctx.obj.get('output_format', 'table'))
else:
error("Failed to create listing")
raise click.Abort()
except Exception as e:
error(f"Error creating listing: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('listing_id')
@click.argument('buyer_id')
@click.option('--payment', default='crypto', help='Payment method')
@click.pass_context
def buy(ctx, listing_id, buyer_id, payment):
"""Purchase a chain from the marketplace"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Purchase chain
transaction_id = asyncio.run(marketplace.purchase_chain(listing_id, buyer_id, payment))
if transaction_id:
success(f"Purchase initiated! Transaction ID: {transaction_id}")
transaction_data = {
"Transaction ID": transaction_id,
"Listing ID": listing_id,
"Buyer": buyer_id,
"Payment Method": payment,
"Status": "pending",
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(transaction_data, ctx.obj.get('output_format', 'table'))
else:
error("Failed to purchase chain")
raise click.Abort()
except Exception as e:
error(f"Error purchasing chain: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('transaction_id')
@click.argument('transaction_hash')
@click.pass_context
def complete(ctx, transaction_id, transaction_hash):
"""Complete a marketplace transaction"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Complete transaction
success = asyncio.run(marketplace.complete_transaction(transaction_id, transaction_hash))
if success:
success(f"Transaction {transaction_id} completed successfully!")
transaction_data = {
"Transaction ID": transaction_id,
"Transaction Hash": transaction_hash,
"Status": "completed",
"Completed": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(transaction_data, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to complete transaction {transaction_id}")
raise click.Abort()
except Exception as e:
error(f"Error completing transaction: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.option('--type', help='Filter by chain type')
@click.option('--min-price', help='Minimum price')
@click.option('--max-price', help='Maximum price')
@click.option('--seller', help='Filter by seller ID')
@click.option('--status', help='Filter by listing status')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def search(ctx, type, min_price, max_price, seller, status, format):
"""Search chain listings in the marketplace"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Parse filters
chain_type = None
if type:
try:
chain_type = ChainType(type)
except ValueError:
error(f"Invalid chain type: {type}")
raise click.Abort()
min_price_dec = None
if min_price:
try:
min_price_dec = Decimal(min_price)
except:
error("Invalid minimum price format")
raise click.Abort()
max_price_dec = None
if max_price:
try:
max_price_dec = Decimal(max_price)
except:
error("Invalid maximum price format")
raise click.Abort()
listing_status = None
if status:
try:
listing_status = MarketplaceStatus(status)
except ValueError:
error(f"Invalid status: {status}")
raise click.Abort()
# Search listings
listings = asyncio.run(marketplace.search_listings(
chain_type, min_price_dec, max_price_dec, seller, listing_status
))
if not listings:
output("No listings found matching your criteria", ctx.obj.get('output_format', 'table'))
return
# Format output
listing_data = [
{
"Listing ID": listing.listing_id,
"Chain ID": listing.chain_id,
"Chain Name": listing.chain_name,
"Type": listing.chain_type.value,
"Price": f"{listing.price} {listing.currency}",
"Seller": listing.seller_id,
"Status": listing.status.value,
"Created": listing.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"Expires": listing.expires_at.strftime("%Y-%m-%d %H:%M:%S")
}
for listing in listings
]
output(listing_data, ctx.obj.get('output_format', format), title="Marketplace Listings")
except Exception as e:
error(f"Error searching listings: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('chain_id')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def economy(ctx, chain_id, format):
"""Get economic metrics for a specific chain"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Get chain economy
economy = asyncio.run(marketplace.get_chain_economy(chain_id))
if not economy:
error(f"No economic data available for chain {chain_id}")
raise click.Abort()
# Format output
economy_data = [
{"Metric": "Chain ID", "Value": economy.chain_id},
{"Metric": "Total Value Locked", "Value": f"{economy.total_value_locked} ETH"},
{"Metric": "Daily Volume", "Value": f"{economy.daily_volume} ETH"},
{"Metric": "Market Cap", "Value": f"{economy.market_cap} ETH"},
{"Metric": "Transaction Count", "Value": economy.transaction_count},
{"Metric": "Active Users", "Value": economy.active_users},
{"Metric": "Agent Count", "Value": economy.agent_count},
{"Metric": "Governance Tokens", "Value": f"{economy.governance_tokens}"},
{"Metric": "Staking Rewards", "Value": f"{economy.staking_rewards}"},
{"Metric": "Last Updated", "Value": economy.last_updated.strftime("%Y-%m-%d %H:%M:%S")}
]
output(economy_data, ctx.obj.get('output_format', format), title=f"Chain Economy: {chain_id}")
except Exception as e:
error(f"Error getting chain economy: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('user_id')
@click.option('--role', type=click.Choice(['buyer', 'seller', 'both']), default='both', help='User role')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def transactions(ctx, user_id, role, format):
"""Get transactions for a specific user"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Get user transactions
transactions = asyncio.run(marketplace.get_user_transactions(user_id, role))
if not transactions:
output(f"No transactions found for user {user_id}", ctx.obj.get('output_format', 'table'))
return
# Format output
transaction_data = [
{
"Transaction ID": transaction.transaction_id,
"Listing ID": transaction.listing_id,
"Chain ID": transaction.chain_id,
"Price": f"{transaction.price} {transaction.currency}",
"Role": "buyer" if transaction.buyer_id == user_id else "seller",
"Counterparty": transaction.seller_id if transaction.buyer_id == user_id else transaction.buyer_id,
"Status": transaction.status.value,
"Created": transaction.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"Completed": transaction.completed_at.strftime("%Y-%m-%d %H:%M:%S") if transaction.completed_at else "N/A"
}
for transaction in transactions
]
output(transaction_data, ctx.obj.get('output_format', format), title=f"Transactions for {user_id}")
except Exception as e:
error(f"Error getting user transactions: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def overview(ctx, format):
"""Get comprehensive marketplace overview"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Get marketplace overview
overview = asyncio.run(marketplace.get_marketplace_overview())
if not overview:
error("No marketplace data available")
raise click.Abort()
# Marketplace metrics
if "marketplace_metrics" in overview:
metrics = overview["marketplace_metrics"]
metrics_data = [
{"Metric": "Total Listings", "Value": metrics["total_listings"]},
{"Metric": "Active Listings", "Value": metrics["active_listings"]},
{"Metric": "Total Transactions", "Value": metrics["total_transactions"]},
{"Metric": "Total Volume", "Value": f"{metrics['total_volume']} ETH"},
{"Metric": "Average Price", "Value": f"{metrics['average_price']} ETH"},
{"Metric": "Market Sentiment", "Value": f"{metrics['market_sentiment']:.2f}"}
]
output(metrics_data, ctx.obj.get('output_format', format), title="Marketplace Metrics")
# Volume 24h
if "volume_24h" in overview:
volume_data = [
{"Metric": "24h Volume", "Value": f"{overview['volume_24h']} ETH"}
]
output(volume_data, ctx.obj.get('output_format', format), title="24-Hour Volume")
# Top performing chains
if "top_performing_chains" in overview:
chains = overview["top_performing_chains"]
if chains:
chain_data = [
{
"Chain ID": chain["chain_id"],
"Volume": f"{chain['volume']} ETH",
"Transactions": chain["transactions"]
}
for chain in chains[:5] # Top 5
]
output(chain_data, ctx.obj.get('output_format', format), title="Top Performing Chains")
# Chain types distribution
if "chain_types_distribution" in overview:
distribution = overview["chain_types_distribution"]
if distribution:
dist_data = [
{"Chain Type": chain_type, "Count": count}
for chain_type, count in distribution.items()
]
output(dist_data, ctx.obj.get('output_format', format), title="Chain Types Distribution")
# User activity
if "user_activity" in overview:
activity = overview["user_activity"]
activity_data = [
{"Metric": "Active Buyers (7d)", "Value": activity["active_buyers_7d"]},
{"Metric": "Active Sellers (7d)", "Value": activity["active_sellers_7d"]},
{"Metric": "Total Unique Users", "Value": activity["total_unique_users"]},
{"Metric": "Average Reputation", "Value": f"{activity['average_reputation']:.3f}"}
]
output(activity_data, ctx.obj.get('output_format', format), title="User Activity")
# Escrow summary
if "escrow_summary" in overview:
escrow = overview["escrow_summary"]
escrow_data = [
{"Metric": "Active Escrows", "Value": escrow["active_escrows"]},
{"Metric": "Released Escrows", "Value": escrow["released_escrows"]},
{"Metric": "Total Escrow Value", "Value": f"{escrow['total_escrow_value']} ETH"},
{"Metric": "Escrow Fees Collected", "Value": f"{escrow['escrow_fee_collected']} ETH"}
]
output(escrow_data, ctx.obj.get('output_format', format), title="Escrow Summary")
except Exception as e:
error(f"Error getting marketplace overview: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
@click.option('--interval', default=30, help='Update interval in seconds')
@click.pass_context
def monitor(ctx, realtime, interval):
"""Monitor marketplace activity"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
if realtime:
# Real-time monitoring
from rich.console import Console
from rich.live import Live
from rich.table import Table
import time
console = Console()
def generate_monitor_table():
try:
overview = asyncio.run(marketplace.get_marketplace_overview())
table = Table(title=f"Marketplace Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
if "marketplace_metrics" in overview:
metrics = overview["marketplace_metrics"]
table.add_row("Total Listings", str(metrics["total_listings"]))
table.add_row("Active Listings", str(metrics["active_listings"]))
table.add_row("Total Transactions", str(metrics["total_transactions"]))
table.add_row("Total Volume", f"{metrics['total_volume']} ETH")
table.add_row("Market Sentiment", f"{metrics['market_sentiment']:.2f}")
if "volume_24h" in overview:
table.add_row("24h Volume", f"{overview['volume_24h']} ETH")
if "user_activity" in overview:
activity = overview["user_activity"]
table.add_row("Active Users (7d)", str(activity["active_buyers_7d"] + activity["active_sellers_7d"]))
return table
except Exception as e:
return f"Error getting marketplace data: {e}"
with Live(generate_monitor_table(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_table())
time.sleep(interval)
except KeyboardInterrupt:
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
else:
# Single snapshot
overview = asyncio.run(marketplace.get_marketplace_overview())
monitor_data = []
if "marketplace_metrics" in overview:
metrics = overview["marketplace_metrics"]
monitor_data.extend([
{"Metric": "Total Listings", "Value": metrics["total_listings"]},
{"Metric": "Active Listings", "Value": metrics["active_listings"]},
{"Metric": "Total Transactions", "Value": metrics["total_transactions"]},
{"Metric": "Total Volume", "Value": f"{metrics['total_volume']} ETH"},
{"Metric": "Market Sentiment", "Value": f"{metrics['market_sentiment']:.2f}"}
])
if "volume_24h" in overview:
monitor_data.append({"Metric": "24h Volume", "Value": f"{overview['volume_24h']} ETH"})
if "user_activity" in overview:
activity = overview["user_activity"]
monitor_data.append({"Metric": "Active Users (7d)", "Value": activity["active_buyers_7d"] + activity["active_sellers_7d"]})
output(monitor_data, ctx.obj.get('output_format', 'table'), title="Marketplace Monitor")
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()

View File

@@ -1,485 +0,0 @@
"""Monitoring and dashboard commands for AITBC CLI"""
import click
import httpx
import json
import time
from pathlib import Path
from typing import Optional
from datetime import datetime, timedelta
from ..utils import output, error, success, console
@click.group()
def monitor():
"""Monitoring, metrics, and alerting commands"""
pass
@monitor.command()
@click.option("--refresh", type=int, default=5, help="Refresh interval in seconds")
@click.option("--duration", type=int, default=0, help="Duration in seconds (0 = indefinite)")
@click.pass_context
def dashboard(ctx, refresh: int, duration: int):
"""Real-time system dashboard"""
config = ctx.obj['config']
start_time = time.time()
try:
while True:
elapsed = time.time() - start_time
if duration > 0 and elapsed >= duration:
break
console.clear()
console.rule("[bold blue]AITBC Dashboard[/bold blue]")
console.print(f"[dim]Refreshing every {refresh}s | Elapsed: {int(elapsed)}s[/dim]\n")
# Fetch system dashboard
try:
with httpx.Client(timeout=5) as client:
# Get dashboard data
try:
url = f"{config.coordinator_url}/api/v1/dashboard"
resp = client.get(
url,
headers={"X-Api-Key": config.api_key or ""}
)
if resp.status_code == 200:
dashboard = resp.json()
console.print("[bold green]Dashboard Status:[/bold green] Online")
# Overall status
overall_status = dashboard.get("overall_status", "unknown")
console.print(f" Overall Status: {overall_status}")
# Services summary
services = dashboard.get("services", {})
console.print(f" Services: {len(services)}")
for service_name, service_data in services.items():
status = service_data.get("status", "unknown")
console.print(f" {service_name}: {status}")
# Metrics summary
metrics = dashboard.get("metrics", {})
if metrics:
health_pct = metrics.get("health_percentage", 0)
console.print(f" Health: {health_pct:.1f}%")
else:
console.print(f"[bold yellow]Dashboard:[/bold yellow] HTTP {resp.status_code}")
except Exception as e:
console.print(f"[bold red]Dashboard:[/bold red] Error - {e}")
except Exception as e:
console.print(f"[red]Error fetching data: {e}[/red]")
console.print(f"\n[dim]Press Ctrl+C to exit[/dim]")
time.sleep(refresh)
except KeyboardInterrupt:
console.print("\n[bold]Dashboard stopped[/bold]")
@monitor.command()
@click.option("--period", default="24h", help="Time period (1h, 24h, 7d, 30d)")
@click.option("--export", "export_path", type=click.Path(), help="Export metrics to file")
@click.pass_context
def metrics(ctx, period: str, export_path: Optional[str]):
"""Collect and display system metrics"""
config = ctx.obj['config']
# Parse period
multipliers = {"h": 3600, "d": 86400}
unit = period[-1]
value = int(period[:-1])
seconds = value * multipliers.get(unit, 3600)
since = datetime.now() - timedelta(seconds=seconds)
metrics_data = {
"period": period,
"since": since.isoformat(),
"collected_at": datetime.now().isoformat(),
"coordinator": {},
"jobs": {},
"miners": {}
}
try:
with httpx.Client(timeout=10) as client:
# Coordinator metrics
try:
resp = client.get(
f"{config.coordinator_url}/status",
headers={"X-Api-Key": config.api_key or ""}
)
if resp.status_code == 200:
metrics_data["coordinator"] = resp.json()
metrics_data["coordinator"]["status"] = "online"
else:
metrics_data["coordinator"]["status"] = f"error_{resp.status_code}"
except Exception:
metrics_data["coordinator"]["status"] = "offline"
# Job metrics
try:
resp = client.get(
f"{config.coordinator_url}/jobs",
headers={"X-Api-Key": config.api_key or ""},
params={"limit": 100}
)
if resp.status_code == 200:
jobs = resp.json()
if isinstance(jobs, list):
metrics_data["jobs"] = {
"total": len(jobs),
"completed": sum(1 for j in jobs if j.get("status") == "completed"),
"pending": sum(1 for j in jobs if j.get("status") == "pending"),
"failed": sum(1 for j in jobs if j.get("status") == "failed"),
}
except Exception:
metrics_data["jobs"] = {"error": "unavailable"}
# Miner metrics
try:
resp = client.get(
f"{config.coordinator_url}/miners",
headers={"X-Api-Key": config.api_key or ""}
)
if resp.status_code == 200:
miners = resp.json()
if isinstance(miners, list):
metrics_data["miners"] = {
"total": len(miners),
"online": sum(1 for m in miners if m.get("status") == "ONLINE"),
"offline": sum(1 for m in miners if m.get("status") != "ONLINE"),
}
except Exception:
metrics_data["miners"] = {"error": "unavailable"}
except Exception as e:
error(f"Failed to collect metrics: {e}")
if export_path:
with open(export_path, "w") as f:
json.dump(metrics_data, f, indent=2)
success(f"Metrics exported to {export_path}")
output(metrics_data, ctx.obj['output_format'])
@monitor.command()
@click.argument("action", type=click.Choice(["add", "list", "remove", "test"]))
@click.option("--name", help="Alert name")
@click.option("--type", "alert_type", type=click.Choice(["coordinator_down", "miner_offline", "job_failed", "low_balance"]), help="Alert type")
@click.option("--threshold", type=float, help="Alert threshold value")
@click.option("--webhook", help="Webhook URL for notifications")
@click.pass_context
def alerts(ctx, action: str, name: Optional[str], alert_type: Optional[str],
threshold: Optional[float], webhook: Optional[str]):
"""Configure monitoring alerts"""
alerts_dir = Path.home() / ".aitbc" / "alerts"
alerts_dir.mkdir(parents=True, exist_ok=True)
alerts_file = alerts_dir / "alerts.json"
# Load existing alerts
existing = []
if alerts_file.exists():
with open(alerts_file) as f:
existing = json.load(f)
if action == "add":
if not name or not alert_type:
error("Alert name and type required (--name, --type)")
return
alert = {
"name": name,
"type": alert_type,
"threshold": threshold,
"webhook": webhook,
"created_at": datetime.now().isoformat(),
"enabled": True
}
existing.append(alert)
with open(alerts_file, "w") as f:
json.dump(existing, f, indent=2)
success(f"Alert '{name}' added")
output(alert, ctx.obj['output_format'])
elif action == "list":
if not existing:
output({"message": "No alerts configured"}, ctx.obj['output_format'])
else:
output(existing, ctx.obj['output_format'])
elif action == "remove":
if not name:
error("Alert name required (--name)")
return
existing = [a for a in existing if a["name"] != name]
with open(alerts_file, "w") as f:
json.dump(existing, f, indent=2)
success(f"Alert '{name}' removed")
elif action == "test":
if not name:
error("Alert name required (--name)")
return
alert = next((a for a in existing if a["name"] == name), None)
if not alert:
error(f"Alert '{name}' not found")
return
if alert.get("webhook"):
try:
with httpx.Client(timeout=10) as client:
resp = client.post(alert["webhook"], json={
"alert": name,
"type": alert["type"],
"message": f"Test alert from AITBC CLI",
"timestamp": datetime.now().isoformat()
})
output({"status": "sent", "response_code": resp.status_code}, ctx.obj['output_format'])
except Exception as e:
error(f"Webhook test failed: {e}")
else:
output({"status": "no_webhook", "alert": alert}, ctx.obj['output_format'])
@monitor.command()
@click.option("--period", default="7d", help="Analysis period (1d, 7d, 30d)")
@click.pass_context
def history(ctx, period: str):
"""Historical data analysis"""
config = ctx.obj['config']
multipliers = {"h": 3600, "d": 86400}
unit = period[-1]
value = int(period[:-1])
seconds = value * multipliers.get(unit, 3600)
since = datetime.now() - timedelta(seconds=seconds)
analysis = {
"period": period,
"since": since.isoformat(),
"analyzed_at": datetime.now().isoformat(),
"summary": {}
}
try:
with httpx.Client(timeout=10) as client:
try:
resp = client.get(
f"{config.coordinator_url}/jobs",
headers={"X-Api-Key": config.api_key or ""},
params={"limit": 500}
)
if resp.status_code == 200:
jobs = resp.json()
if isinstance(jobs, list):
completed = [j for j in jobs if j.get("status") == "completed"]
failed = [j for j in jobs if j.get("status") == "failed"]
analysis["summary"] = {
"total_jobs": len(jobs),
"completed": len(completed),
"failed": len(failed),
"success_rate": f"{len(completed) / max(1, len(jobs)) * 100:.1f}%",
}
except Exception:
analysis["summary"] = {"error": "Could not fetch job data"}
except Exception as e:
error(f"Analysis failed: {e}")
output(analysis, ctx.obj['output_format'])
@monitor.command()
@click.argument("action", type=click.Choice(["add", "list", "remove", "test"]))
@click.option("--name", help="Webhook name")
@click.option("--url", help="Webhook URL")
@click.option("--events", help="Comma-separated event types (job_completed,miner_offline,alert)")
@click.pass_context
def webhooks(ctx, action: str, name: Optional[str], url: Optional[str], events: Optional[str]):
"""Manage webhook notifications"""
webhooks_dir = Path.home() / ".aitbc" / "webhooks"
webhooks_dir.mkdir(parents=True, exist_ok=True)
webhooks_file = webhooks_dir / "webhooks.json"
existing = []
if webhooks_file.exists():
with open(webhooks_file) as f:
existing = json.load(f)
if action == "add":
if not name or not url:
error("Webhook name and URL required (--name, --url)")
return
webhook = {
"name": name,
"url": url,
"events": events.split(",") if events else ["all"],
"created_at": datetime.now().isoformat(),
"enabled": True
}
existing.append(webhook)
with open(webhooks_file, "w") as f:
json.dump(existing, f, indent=2)
success(f"Webhook '{name}' added")
output(webhook, ctx.obj['output_format'])
elif action == "list":
if not existing:
output({"message": "No webhooks configured"}, ctx.obj['output_format'])
else:
output(existing, ctx.obj['output_format'])
elif action == "remove":
if not name:
error("Webhook name required (--name)")
return
existing = [w for w in existing if w["name"] != name]
with open(webhooks_file, "w") as f:
json.dump(existing, f, indent=2)
success(f"Webhook '{name}' removed")
elif action == "test":
if not name:
error("Webhook name required (--name)")
return
wh = next((w for w in existing if w["name"] == name), None)
if not wh:
error(f"Webhook '{name}' not found")
return
try:
with httpx.Client(timeout=10) as client:
resp = client.post(wh["url"], json={
"event": "test",
"source": "aitbc-cli",
"message": "Test webhook notification",
"timestamp": datetime.now().isoformat()
})
output({"status": "sent", "response_code": resp.status_code}, ctx.obj['output_format'])
except Exception as e:
error(f"Webhook test failed: {e}")
CAMPAIGNS_DIR = Path.home() / ".aitbc" / "campaigns"
def _ensure_campaigns():
CAMPAIGNS_DIR.mkdir(parents=True, exist_ok=True)
campaigns_file = CAMPAIGNS_DIR / "campaigns.json"
if not campaigns_file.exists():
# Seed with default campaigns
default = {"campaigns": [
{
"id": "staking_launch",
"name": "Staking Launch Campaign",
"type": "staking",
"apy_boost": 2.0,
"start_date": "2026-02-01T00:00:00",
"end_date": "2026-04-01T00:00:00",
"status": "active",
"total_staked": 0,
"participants": 0,
"rewards_distributed": 0
},
{
"id": "liquidity_mining_q1",
"name": "Q1 Liquidity Mining",
"type": "liquidity",
"apy_boost": 3.0,
"start_date": "2026-01-15T00:00:00",
"end_date": "2026-03-15T00:00:00",
"status": "active",
"total_staked": 0,
"participants": 0,
"rewards_distributed": 0
}
]}
with open(campaigns_file, "w") as f:
json.dump(default, f, indent=2)
return campaigns_file
@monitor.command()
@click.option("--status", type=click.Choice(["active", "ended", "all"]), default="all", help="Filter by status")
@click.pass_context
def campaigns(ctx, status: str):
"""List active incentive campaigns"""
campaigns_file = _ensure_campaigns()
with open(campaigns_file) as f:
data = json.load(f)
campaign_list = data.get("campaigns", [])
# Auto-update status
now = datetime.now()
for c in campaign_list:
end = datetime.fromisoformat(c["end_date"])
if now > end and c["status"] == "active":
c["status"] = "ended"
with open(campaigns_file, "w") as f:
json.dump(data, f, indent=2)
if status != "all":
campaign_list = [c for c in campaign_list if c["status"] == status]
if not campaign_list:
output({"message": "No campaigns found"}, ctx.obj['output_format'])
return
output(campaign_list, ctx.obj['output_format'])
@monitor.command(name="campaign-stats")
@click.argument("campaign_id", required=False)
@click.pass_context
def campaign_stats(ctx, campaign_id: Optional[str]):
"""Campaign performance metrics (TVL, participants, rewards)"""
campaigns_file = _ensure_campaigns()
with open(campaigns_file) as f:
data = json.load(f)
campaign_list = data.get("campaigns", [])
if campaign_id:
campaign = next((c for c in campaign_list if c["id"] == campaign_id), None)
if not campaign:
error(f"Campaign '{campaign_id}' not found")
ctx.exit(1)
return
targets = [campaign]
else:
targets = campaign_list
stats = []
for c in targets:
start = datetime.fromisoformat(c["start_date"])
end = datetime.fromisoformat(c["end_date"])
now = datetime.now()
duration_days = (end - start).days
elapsed_days = min((now - start).days, duration_days)
progress_pct = round(elapsed_days / max(duration_days, 1) * 100, 1)
stats.append({
"campaign_id": c["id"],
"name": c["name"],
"type": c["type"],
"status": c["status"],
"apy_boost": c.get("apy_boost", 0),
"tvl": c.get("total_staked", 0),
"participants": c.get("participants", 0),
"rewards_distributed": c.get("rewards_distributed", 0),
"duration_days": duration_days,
"elapsed_days": elapsed_days,
"progress_pct": progress_pct,
"start_date": c["start_date"],
"end_date": c["end_date"]
})
if len(stats) == 1:
output(stats[0], ctx.obj['output_format'])
else:
output(stats, ctx.obj['output_format'])

View File

@@ -1,439 +0,0 @@
"""Node management commands for AITBC CLI"""
import click
from typing import Optional
from ..core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config
from ..core.node_client import NodeClient
from ..utils import output, error, success
@click.group()
def node():
"""Node management commands"""
pass
@node.command()
@click.argument('node_id')
@click.pass_context
def info(ctx, node_id):
"""Get detailed node information"""
try:
config = load_multichain_config()
if node_id not in config.nodes:
error(f"Node {node_id} not found in configuration")
raise click.Abort()
node_config = config.nodes[node_id]
import asyncio
async def get_node_info():
async with NodeClient(node_config) as client:
return await client.get_node_info()
node_info = asyncio.run(get_node_info())
# Basic node information
basic_info = {
"Node ID": node_info["node_id"],
"Node Type": node_info["type"],
"Status": node_info["status"],
"Version": node_info["version"],
"Uptime": f"{node_info['uptime_days']} days, {node_info['uptime_hours']} hours",
"Endpoint": node_config.endpoint
}
output(basic_info, ctx.obj.get('output_format', 'table'), title=f"Node Information: {node_id}")
# Performance metrics
metrics = {
"CPU Usage": f"{node_info['cpu_usage']}%",
"Memory Usage": f"{node_info['memory_usage_mb']:.1f}MB",
"Disk Usage": f"{node_info['disk_usage_mb']:.1f}MB",
"Network In": f"{node_info['network_in_mb']:.1f}MB/s",
"Network Out": f"{node_info['network_out_mb']:.1f}MB/s"
}
output(metrics, ctx.obj.get('output_format', 'table'), title="Performance Metrics")
# Hosted chains
if node_info.get("hosted_chains"):
chains_data = [
{
"Chain ID": chain_id,
"Type": chain.get("type", "unknown"),
"Status": chain.get("status", "unknown")
}
for chain_id, chain in node_info["hosted_chains"].items()
]
output(chains_data, ctx.obj.get('output_format', 'table'), title="Hosted Chains")
except Exception as e:
error(f"Error getting node info: {str(e)}")
raise click.Abort()
@node.command()
@click.option('--show-private', is_flag=True, help='Show private chains')
@click.option('--node-id', help='Specific node ID to query')
@click.pass_context
def chains(ctx, show_private, node_id):
"""List chains hosted on all nodes"""
try:
config = load_multichain_config()
all_chains = []
import asyncio
async def get_all_chains():
tasks = []
for nid, node_config in config.nodes.items():
if node_id and nid != node_id:
continue
async def get_chains_for_node(nid, nconfig):
try:
async with NodeClient(nconfig) as client:
chains = await client.get_hosted_chains()
return [(nid, chain) for chain in chains]
except Exception as e:
print(f"Error getting chains from node {nid}: {e}")
return []
tasks.append(get_chains_for_node(node_id, node_config))
results = await asyncio.gather(*tasks)
for result in results:
all_chains.extend(result)
asyncio.run(get_all_chains())
if not all_chains:
output("No chains found on any node", ctx.obj.get('output_format', 'table'))
return
# Filter private chains if not requested
if not show_private:
all_chains = [(node_id, chain) for node_id, chain in all_chains
if chain.privacy.visibility != "private"]
# Format output
chains_data = [
{
"Node ID": node_id,
"Chain ID": chain.id,
"Type": chain.type.value,
"Purpose": chain.purpose,
"Name": chain.name,
"Status": chain.status.value,
"Block Height": chain.block_height,
"Size": f"{chain.size_mb:.1f}MB"
}
for node_id, chain in all_chains
]
output(chains_data, ctx.obj.get('output_format', 'table'), title="Chains by Node")
except Exception as e:
error(f"Error listing chains: {str(e)}")
raise click.Abort()
@node.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def list(ctx, format):
"""List all configured nodes"""
try:
config = load_multichain_config()
if not config.nodes:
output("No nodes configured", ctx.obj.get('output_format', 'table'))
return
nodes_data = [
{
"Node ID": node_id,
"Endpoint": node_config.endpoint,
"Timeout": f"{node_config.timeout}s",
"Max Connections": node_config.max_connections,
"Retry Count": node_config.retry_count
}
for node_id, node_config in config.nodes.items()
]
output(nodes_data, ctx.obj.get('output_format', 'table'), title="Configured Nodes")
except Exception as e:
error(f"Error listing nodes: {str(e)}")
raise click.Abort()
@node.command()
@click.argument('node_id')
@click.argument('endpoint')
@click.option('--timeout', default=30, help='Request timeout in seconds')
@click.option('--max-connections', default=10, help='Maximum concurrent connections')
@click.option('--retry-count', default=3, help='Number of retry attempts')
@click.pass_context
def add(ctx, node_id, endpoint, timeout, max_connections, retry_count):
"""Add a new node to configuration"""
try:
config = load_multichain_config()
if node_id in config.nodes:
error(f"Node {node_id} already exists")
raise click.Abort()
node_config = get_default_node_config()
node_config.id = node_id
node_config.endpoint = endpoint
node_config.timeout = timeout
node_config.max_connections = max_connections
node_config.retry_count = retry_count
config = add_node_config(config, node_config)
from ..core.config import save_multichain_config
save_multichain_config(config)
success(f"Node {node_id} added successfully!")
result = {
"Node ID": node_id,
"Endpoint": endpoint,
"Timeout": f"{timeout}s",
"Max Connections": max_connections,
"Retry Count": retry_count
}
output(result, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error adding node: {str(e)}")
raise click.Abort()
@node.command()
@click.argument('node_id')
@click.option('--force', is_flag=True, help='Force removal without confirmation')
@click.pass_context
def remove(ctx, node_id, force):
"""Remove a node from configuration"""
try:
config = load_multichain_config()
if node_id not in config.nodes:
error(f"Node {node_id} not found")
raise click.Abort()
if not force:
# Show node information before removal
node_config = config.nodes[node_id]
node_info = {
"Node ID": node_id,
"Endpoint": node_config.endpoint,
"Timeout": f"{node_config.timeout}s",
"Max Connections": node_config.max_connections
}
output(node_info, ctx.obj.get('output_format', 'table'), title="Node to Remove")
if not click.confirm(f"Are you sure you want to remove node {node_id}?"):
raise click.Abort()
config = remove_node_config(config, node_id)
from ..core.config import save_multichain_config
save_multichain_config(config)
success(f"Node {node_id} removed successfully!")
except Exception as e:
error(f"Error removing node: {str(e)}")
raise click.Abort()
@node.command()
@click.argument('node_id')
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
@click.option('--interval', default=5, help='Update interval in seconds')
@click.pass_context
def monitor(ctx, node_id, realtime, interval):
"""Monitor node activity"""
try:
config = load_multichain_config()
if node_id not in config.nodes:
error(f"Node {node_id} not found")
raise click.Abort()
node_config = config.nodes[node_id]
import asyncio
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
import time
console = Console()
async def get_node_stats():
async with NodeClient(node_config) as client:
node_info = await client.get_node_info()
return node_info
if realtime:
# Real-time monitoring
def generate_monitor_layout():
try:
node_info = asyncio.run(get_node_stats())
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="metrics"),
Layout(name="chains", size=10)
)
# Header
layout["header"].update(
f"Node Monitor: {node_id} - {node_info['status'].upper()}"
)
# Metrics table
metrics_data = [
["CPU Usage", f"{node_info['cpu_usage']}%"],
["Memory Usage", f"{node_info['memory_usage_mb']:.1f}MB"],
["Disk Usage", f"{node_info['disk_usage_mb']:.1f}MB"],
["Network In", f"{node_info['network_in_mb']:.1f}MB/s"],
["Network Out", f"{node_info['network_out_mb']:.1f}MB/s"],
["Uptime", f"{node_info['uptime_days']}d {node_info['uptime_hours']}h"]
]
layout["metrics"].update(str(metrics_data))
# Chains info
if node_info.get("hosted_chains"):
chains_text = f"Hosted Chains: {len(node_info['hosted_chains'])}\n"
for chain_id, chain in list(node_info["hosted_chains"].items())[:5]:
chains_text += f"{chain_id} ({chain.get('status', 'unknown')})\n"
layout["chains"].update(chains_text)
else:
layout["chains"].update("No chains hosted")
return layout
except Exception as e:
return f"Error getting node stats: {e}"
with Live(generate_monitor_layout(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_layout())
time.sleep(interval)
except KeyboardInterrupt:
console.print("\n[yellow]Monitoring stopped by user[/yellow]")
else:
# Single snapshot
node_info = asyncio.run(get_node_stats())
stats_data = [
{
"Metric": "CPU Usage",
"Value": f"{node_info['cpu_usage']}%"
},
{
"Metric": "Memory Usage",
"Value": f"{node_info['memory_usage_mb']:.1f}MB"
},
{
"Metric": "Disk Usage",
"Value": f"{node_info['disk_usage_mb']:.1f}MB"
},
{
"Metric": "Network In",
"Value": f"{node_info['network_in_mb']:.1f}MB/s"
},
{
"Metric": "Network Out",
"Value": f"{node_info['network_out_mb']:.1f}MB/s"
},
{
"Metric": "Uptime",
"Value": f"{node_info['uptime_days']}d {node_info['uptime_hours']}h"
}
]
output(stats_data, ctx.obj.get('output_format', 'table'), title=f"Node Statistics: {node_id}")
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()
@node.command()
@click.argument('node_id')
@click.pass_context
def test(ctx, node_id):
"""Test connectivity to a node"""
try:
config = load_multichain_config()
if node_id not in config.nodes:
error(f"Node {node_id} not found")
raise click.Abort()
node_config = config.nodes[node_id]
import asyncio
async def test_node():
try:
async with NodeClient(node_config) as client:
node_info = await client.get_node_info()
chains = await client.get_hosted_chains()
return {
"connected": True,
"node_id": node_info["node_id"],
"status": node_info["status"],
"version": node_info["version"],
"chains_count": len(chains)
}
except Exception as e:
return {
"connected": False,
"error": str(e)
}
result = asyncio.run(test_node())
if result["connected"]:
success(f"Successfully connected to node {node_id}!")
test_data = [
{
"Test": "Connection",
"Status": "✓ Pass"
},
{
"Test": "Node ID",
"Status": result["node_id"]
},
{
"Test": "Status",
"Status": result["status"]
},
{
"Test": "Version",
"Status": result["version"]
},
{
"Test": "Chains",
"Status": f"{result['chains_count']} hosted"
}
]
output(test_data, ctx.obj.get('output_format', 'table'), title=f"Node Test Results: {node_id}")
else:
error(f"Failed to connect to node {node_id}: {result['error']}")
raise click.Abort()
except Exception as e:
error(f"Error testing node: {str(e)}")
raise click.Abort()

View File

@@ -1,54 +0,0 @@
from __future__ import annotations
import asyncio
import json
from contextlib import asynccontextmanager
from typing import List
import websockets
DEFAULT_WS_URL = "ws://127.0.0.1:8000/rpc/ws"
BLOCK_TOPIC = "/blocks"
TRANSACTION_TOPIC = "/transactions"
async def producer(ws_url: str, interval: float = 0.1, total: int = 100) -> None:
async with websockets.connect(f"{ws_url}{BLOCK_TOPIC}") as websocket:
for index in range(total):
payload = {
"height": index,
"hash": f"0x{index:064x}",
"parent_hash": f"0x{index-1:064x}",
"timestamp": "2025-01-01T00:00:00Z",
"tx_count": 0,
}
await websocket.send(json.dumps(payload))
await asyncio.sleep(interval)
async def consumer(name: str, ws_url: str, path: str, duration: float = 5.0) -> None:
async with websockets.connect(f"{ws_url}{path}") as websocket:
end = asyncio.get_event_loop().time() + duration
received = 0
while asyncio.get_event_loop().time() < end:
try:
message = await asyncio.wait_for(websocket.recv(), timeout=1.0)
except asyncio.TimeoutError:
continue
received += 1
if received % 10 == 0:
print(f"[{name}] received {received} messages")
print(f"[{name}] total received: {received}")
async def main() -> None:
ws_url = DEFAULT_WS_URL
consumers = [
consumer("blocks-consumer", ws_url, BLOCK_TOPIC),
consumer("tx-consumer", ws_url, TRANSACTION_TOPIC),
]
await asyncio.gather(producer(ws_url), *consumers)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,250 +0,0 @@
# AITBC CLI Getting Started Guide
**Complete Command Line Interface Setup and Usage**
## 🚀 **Quick Start**
### Prerequisites
- Linux system (Debian 13+ recommended)
- Python 3.13+ installed
- System access (sudo for initial setup)
### Installation
```bash
# 1. Load development environment
source /opt/aitbc/.env.dev
# 2. Test CLI installation
aitbc --help
aitbc version
# 3. Verify services are running
aitbc-services status
```
## 🔧 **Development Environment Setup**
### Permission Configuration
```bash
# Fix permissions (one-time setup)
sudo /opt/aitbc/scripts/clean-sudoers-fix.sh
# Test permissions
/opt/aitbc/scripts/test-permissions.sh
```
### Environment Variables
```bash
# Load development environment
source /opt/aitbc/.env.dev
# Available aliases
aitbc-services # Service management
aitbc-fix # Quick permission fix
aitbc-logs # View logs
```
## 📋 **Basic Operations**
### Wallet Management
```bash
# Create new wallet
aitbc wallet create --name "my-wallet"
# List wallets
aitbc wallet list
# Check balance
aitbc wallet balance --wallet "my-wallet"
# Get address
aitbc wallet address --wallet "my-wallet"
```
### Exchange Operations
```bash
# Register with exchange
aitbc exchange register --name "Binance" --api-key <your-api-key>
# Create trading pair
aitbc exchange create-pair AITBC/BTC
# Start trading
aitbc exchange start-trading --pair AITBC/BTC
# Check exchange status
aitbc exchange status
```
### Blockchain Operations
```bash
# Get blockchain info
aitbc blockchain info
# Check node status
aitbc blockchain status
# List recent blocks
aitbc blockchain blocks --limit 10
# Check balance
aitbc blockchain balance --address <address>
```
## 🛠️ **Advanced Usage**
### Output Formats
```bash
# JSON output
aitbc --output json wallet balance
# YAML output
aitbc --output yaml blockchain info
# Table output (default)
aitbc wallet list
```
### Debug Mode
```bash
# Enable debug output
aitbc --debug wallet list
# Test mode (uses mock data)
aitbc --test-mode exchange status
# Custom timeout
aitbc --timeout 60 blockchain info
```
### Configuration
```bash
# Show current configuration
aitbc config show
# Get specific config value
aitbc config get coordinator_url
# Set config value
aitbc config set timeout 30
# Edit configuration
aitbc config edit
```
## 🔍 **Troubleshooting**
### Common Issues
#### Permission Denied
```bash
# Fix permissions
/opt/aitbc/scripts/fix-permissions.sh
# Test permissions
/opt/aitbc/scripts/test-permissions.sh
```
#### Service Not Running
```bash
# Check service status
aitbc-services status
# Restart services
aitbc-services restart
# View logs
aitbc-logs
```
#### Command Not Found
```bash
# Check CLI installation
which aitbc
# Load environment
source /opt/aitbc/.env.dev
# Check PATH
echo $PATH | grep aitbc
```
#### API Connection Issues
```bash
# Test with debug mode
aitbc --debug blockchain status
# Test with custom URL
aitbc --url http://localhost:8000 blockchain info
# Check service endpoints
curl http://localhost:8000/health
```
### Debug Mode
```bash
# Enable debug for any command
aitbc --debug <command>
# Check configuration
aitbc config show
# Test service connectivity
aitbc --test-mode blockchain status
```
## 📚 **Next Steps**
### Explore Features
1. **Wallet Operations**: Try creating and managing wallets
2. **Exchange Integration**: Register with exchanges and start trading
3. **Blockchain Operations**: Explore blockchain features
4. **Compliance**: Set up KYC/AML verification
### Advanced Topics
1. **Market Making**: Configure automated trading
2. **Oracle Integration**: Set up price feeds
3. **Security**: Implement multi-sig and time-lock
4. **Development**: Build custom tools and integrations
### Documentation
- [Complete CLI Reference](../23_cli/README.md)
- [Testing Procedures](../23_cli/testing.md)
- [Permission Setup](../23_cli/permission-setup.md)
- [Exchange Integration](../19_marketplace/exchange_integration.md)
## 🎯 **Tips and Best Practices**
### Development Workflow
```bash
# 1. Load environment
source /opt/aitbc/.env.dev
# 2. Check services
aitbc-services status
# 3. Test CLI
aitbc version
# 4. Start development
aitbc wallet create
```
### Security Best Practices
- Use strong passwords for wallet encryption
- Enable multi-sig for large amounts
- Keep API keys secure
- Regular backup of wallets
- Monitor compliance requirements
### Performance Tips
- Use appropriate output formats for automation
- Leverage test mode for development
- Cache frequently used data
- Monitor service health
---
**Last Updated**: March 8, 2026
**CLI Version**: 0.1.0
**Test Coverage**: 67/67 tests passing (100%)

View File

@@ -1,54 +0,0 @@
from __future__ import annotations
import asyncio
import json
from contextlib import asynccontextmanager
from typing import List
import websockets
DEFAULT_WS_URL = "ws://127.0.0.1:8000/rpc/ws"
BLOCK_TOPIC = "/blocks"
TRANSACTION_TOPIC = "/transactions"
async def producer(ws_url: str, interval: float = 0.1, total: int = 100) -> None:
async with websockets.connect(f"{ws_url}{BLOCK_TOPIC}") as websocket:
for index in range(total):
payload = {
"height": index,
"hash": f"0x{index:064x}",
"parent_hash": f"0x{index-1:064x}",
"timestamp": "2025-01-01T00:00:00Z",
"tx_count": 0,
}
await websocket.send(json.dumps(payload))
await asyncio.sleep(interval)
async def consumer(name: str, ws_url: str, path: str, duration: float = 5.0) -> None:
async with websockets.connect(f"{ws_url}{path}") as websocket:
end = asyncio.get_event_loop().time() + duration
received = 0
while asyncio.get_event_loop().time() < end:
try:
message = await asyncio.wait_for(websocket.recv(), timeout=1.0)
except asyncio.TimeoutError:
continue
received += 1
if received % 10 == 0:
print(f"[{name}] received {received} messages")
print(f"[{name}] total received: {received}")
async def main() -> None:
ws_url = DEFAULT_WS_URL
consumers = [
consumer("blocks-consumer", ws_url, BLOCK_TOPIC),
consumer("tx-consumer", ws_url, TRANSACTION_TOPIC),
]
await asyncio.gather(producer(ws_url), *consumers)
if __name__ == "__main__":
asyncio.run(main())