Files
aitbc/cli/aitbc_cli/commands/cross_chain.py
oib 15427c96c0 chore: update file permissions to executable across repository
- Change file mode from 644 to 755 for all project files
- Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet"
- Rename Miner.extra_meta_data to extra_metadata for consistency
2026-03-06 22:17:54 +01:00

477 lines
19 KiB
Python
Executable File

"""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}")