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:
@@ -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()
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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'])
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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%)
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user