Files
aitbc/apps/exchange/multichain_exchange_api.py
oib bb5363bebc refactor: consolidate blockchain explorer into single app and update backup ignore patterns
- Remove standalone explorer-web app (README, HTML, package files)
- Add /web endpoint to blockchain-explorer for web interface access
- Update .gitignore to exclude application backup archives (*.tar.gz, *.zip)
- Add backup documentation files to .gitignore (BACKUP_INDEX.md, README.md)
- Consolidate explorer functionality into main blockchain-explorer application
2026-03-06 18:14:49 +01:00

527 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Multi-Chain AITBC Exchange API
Complete multi-chain trading with chain isolation
"""
import sqlite3
import json
import asyncio
import httpx
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from fastapi import FastAPI, HTTPException, Query, BackgroundTasks
from pydantic import BaseModel, Field
import uvicorn
import os
app = FastAPI(title="AITBC Multi-Chain Exchange", version="2.0.0")
# Database configuration
DB_PATH = os.path.join(os.path.dirname(__file__), "exchange_multichain.db")
# Supported chains
SUPPORTED_CHAINS = {
"ait-devnet": {
"name": "AITBC Development Network",
"status": "active",
"blockchain_url": "http://localhost:8007",
"token_symbol": "AITBC-DEV"
},
"ait-testnet": {
"name": "AITBC Test Network",
"status": "inactive",
"blockchain_url": None,
"token_symbol": "AITBC-TEST"
}
}
# Models
class OrderRequest(BaseModel):
order_type: str = Field(..., regex="^(BUY|SELL)$")
amount: float = Field(..., gt=0)
price: float = Field(..., gt=0)
chain_id: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
user_address: str = Field(..., min_length=1)
class ChainOrderRequest(BaseModel):
chain_id: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
order_type: str = Field(..., regex="^(BUY|SELL)$")
class MultiChainTradeRequest(BaseModel):
buy_order_id: Optional[int] = None
sell_order_id: Optional[int] = None
amount: float = Field(..., gt=0)
chain_id: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
# Database functions
def get_db_connection():
"""Get database connection with proper configuration"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_database():
"""Initialize database with multi-chain schema"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Create chains table if not exists
cursor.execute('''
CREATE TABLE IF NOT EXISTS chains (
chain_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('active', 'inactive', 'maintenance')),
blockchain_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
enabled BOOLEAN DEFAULT 1
)
''')
# Create orders table with chain support
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_type TEXT NOT NULL CHECK(order_type IN ('BUY', 'SELL')),
amount REAL NOT NULL,
price REAL NOT NULL,
total REAL NOT NULL,
filled REAL DEFAULT 0,
remaining REAL NOT NULL,
status TEXT DEFAULT 'open' CHECK(status IN ('open', 'filled', 'cancelled')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_address TEXT,
tx_hash TEXT,
chain_id TEXT NOT NULL DEFAULT 'ait-devnet',
blockchain_tx_hash TEXT,
chain_status TEXT DEFAULT 'pending' CHECK(chain_status IN ('pending', 'confirmed', 'failed'))
)
''')
# Create trades table with chain support
cursor.execute('''
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
buy_order_id INTEGER,
sell_order_id INTEGER,
amount REAL NOT NULL,
price REAL NOT NULL,
total REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
chain_id TEXT NOT NULL DEFAULT 'ait-devnet',
blockchain_tx_hash TEXT,
chain_status TEXT DEFAULT 'pending' CHECK(chain_status IN ('pending', 'confirmed', 'failed'))
)
''')
# Insert default chains
for chain_id, chain_info in SUPPORTED_CHAINS.items():
cursor.execute('''
INSERT OR REPLACE INTO chains (chain_id, name, status, blockchain_url)
VALUES (?, ?, ?, ?)
''', (chain_id, chain_info["name"], chain_info["status"], chain_info["blockchain_url"]))
# Create indexes
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_chain_id ON orders(chain_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_trades_chain_id ON trades(chain_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_chain_status ON orders(chain_id, status)')
conn.commit()
conn.close()
return True
except Exception as e:
print(f"Database initialization error: {e}")
return False
# Chain-specific functions
async def verify_chain_transaction(chain_id: str, tx_hash: str) -> bool:
"""Verify transaction on specific chain"""
if chain_id not in SUPPORTED_CHAINS:
return False
chain_info = SUPPORTED_CHAINS[chain_id]
if chain_info["status"] != "active" or not chain_info["blockchain_url"]:
return False
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{chain_info['blockchain_url']}/api/v1/transactions/{tx_hash}")
return response.status_code == 200
except:
return False
async def submit_chain_transaction(chain_id: str, order_data: Dict) -> Optional[str]:
"""Submit transaction to specific chain"""
if chain_id not in SUPPORTED_CHAINS:
return None
chain_info = SUPPORTED_CHAINS[chain_id]
if chain_info["status"] != "active" or not chain_info["blockchain_url"]:
return None
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{chain_info['blockchain_url']}/api/v1/transactions",
json=order_data
)
if response.status_code == 200:
return response.json().get("tx_hash")
except Exception as e:
print(f"Chain transaction error: {e}")
return None
# API Endpoints
@app.get("/health")
async def health_check():
"""Multi-chain health check"""
chain_status = {}
for chain_id, chain_info in SUPPORTED_CHAINS.items():
chain_status[chain_id] = {
"name": chain_info["name"],
"status": chain_info["status"],
"blockchain_url": chain_info["blockchain_url"],
"connected": False
}
if chain_info["status"] == "active" and chain_info["blockchain_url"]:
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{chain_info['blockchain_url']}/health", timeout=5.0)
chain_status[chain_id]["connected"] = response.status_code == 200
except:
pass
return {
"status": "ok",
"service": "multi-chain-exchange",
"version": "2.0.0",
"supported_chains": list(SUPPORTED_CHAINS.keys()),
"chain_status": chain_status,
"multi_chain": True,
"timestamp": datetime.now().isoformat()
}
@app.get("/api/v1/chains")
async def get_chains():
"""Get all supported chains with their status"""
chains = []
for chain_id, chain_info in SUPPORTED_CHAINS.items():
chains.append({
"chain_id": chain_id,
"name": chain_info["name"],
"status": chain_info["status"],
"blockchain_url": chain_info["blockchain_url"],
"token_symbol": chain_info["token_symbol"]
})
return {
"chains": chains,
"total_chains": len(chains),
"active_chains": len([c for c in chains if c["status"] == "active"])
}
@app.post("/api/v1/orders")
async def create_order(order: OrderRequest, background_tasks: BackgroundTasks):
"""Create chain-specific order"""
if order.chain_id not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
chain_info = SUPPORTED_CHAINS[order.chain_id]
if chain_info["status"] != "active":
raise HTTPException(status_code=400, detail=f"Chain {order.chain_id} is not active")
try:
conn = get_db_connection()
cursor = conn.cursor()
# Create order with chain isolation
cursor.execute('''
INSERT INTO orders (order_type, amount, price, total, remaining, user_address, chain_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (order.order_type, order.amount, order.price, order.total, order.amount, order.user_address, order.chain_id))
order_id = cursor.lastrowid
# Submit to blockchain in background
background_tasks.add_task(submit_order_to_blockchain, order_id, order.chain_id)
conn.commit()
conn.close()
return {
"success": True,
"order_id": order_id,
"chain_id": order.chain_id,
"status": "created",
"message": f"Order created on {chain_info['name']}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Order creation failed: {str(e)}")
async def submit_order_to_blockchain(order_id: int, chain_id: str):
"""Submit order to blockchain in background"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM orders WHERE id = ?", (order_id,))
order = cursor.fetchone()
if order:
order_data = {
"type": "order",
"order_type": order["order_type"],
"amount": order["amount"],
"price": order["price"],
"user_address": order["user_address"]
}
tx_hash = await submit_chain_transaction(chain_id, order_data)
if tx_hash:
cursor.execute('''
UPDATE orders SET blockchain_tx_hash = ?, chain_status = 'pending'
WHERE id = ?
''', (tx_hash, order_id))
conn.commit()
conn.close()
except Exception as e:
print(f"Background blockchain submission error: {e}")
@app.get("/api/v1/orders/{chain_id}")
async def get_chain_orders(chain_id: str, status: Optional[str] = None):
"""Get orders for specific chain"""
if chain_id not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
try:
conn = get_db_connection()
cursor = conn.cursor()
query = "SELECT * FROM orders WHERE chain_id = ?"
params = [chain_id]
if status:
query += " AND status = ?"
params.append(status)
query += " ORDER BY created_at DESC"
cursor.execute(query, params)
orders = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"chain_id": chain_id,
"orders": orders,
"total_orders": len(orders)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get orders: {str(e)}")
@app.get("/api/v1/orderbook/{chain_id}")
async def get_chain_orderbook(chain_id: str):
"""Get order book for specific chain"""
if chain_id not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
try:
conn = get_db_connection()
cursor = conn.cursor()
# Get buy orders (sorted by price descending)
cursor.execute('''
SELECT price, SUM(remaining) as volume, COUNT(*) as count
FROM orders
WHERE chain_id = ? AND order_type = 'BUY' AND status = 'open'
GROUP BY price
ORDER BY price DESC
''', (chain_id,))
buy_orders = [dict(row) for row in cursor.fetchall()]
# Get sell orders (sorted by price ascending)
cursor.execute('''
SELECT price, SUM(remaining) as volume, COUNT(*) as count
FROM orders
WHERE chain_id = ? AND order_type = 'SELL' AND status = 'open'
GROUP BY price
ORDER BY price ASC
''', (chain_id,))
sell_orders = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"chain_id": chain_id,
"buy_orders": buy_orders,
"sell_orders": sell_orders,
"spread": sell_orders[0]["price"] - buy_orders[0]["price"] if buy_orders and sell_orders else None
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get orderbook: {str(e)}")
@app.get("/api/v1/trades/{chain_id}")
async def get_chain_trades(chain_id: str, limit: int = Query(default=50, le=100)):
"""Get trades for specific chain"""
if chain_id not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT t.*, o1.order_type as buy_order_type, o2.order_type as sell_order_type
FROM trades t
LEFT JOIN orders o1 ON t.buy_order_id = o1.id
LEFT JOIN orders o2 ON t.sell_order_id = o2.id
WHERE t.chain_id = ?
ORDER BY t.created_at DESC
LIMIT ?
''', (chain_id, limit))
trades = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"chain_id": chain_id,
"trades": trades,
"total_trades": len(trades)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get trades: {str(e)}")
@app.post("/api/v1/trades")
async def create_trade(trade: MultiChainTradeRequest, background_tasks: BackgroundTasks):
"""Create chain-specific trade"""
if trade.chain_id not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
chain_info = SUPPORTED_CHAINS[trade.chain_id]
if chain_info["status"] != "active":
raise HTTPException(status_code=400, detail=f"Chain is not active")
try:
conn = get_db_connection()
cursor = conn.cursor()
# Create trade with chain isolation
cursor.execute('''
INSERT INTO trades (buy_order_id, sell_order_id, amount, price, total, chain_id)
VALUES (?, ?, ?, ?, ?, ?)
''', (trade.buy_order_id, trade.sell_order_id, trade.amount, trade.price, trade.total, trade.chain_id))
trade_id = cursor.lastrowid
# Submit to blockchain in background
background_tasks.add_task(submit_trade_to_blockchain, trade_id, trade.chain_id)
conn.commit()
conn.close()
return {
"success": True,
"trade_id": trade_id,
"chain_id": trade.chain_id,
"status": "created",
"message": f"Trade created on {chain_info['name']}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Trade creation failed: {str(e)}")
async def submit_trade_to_blockchain(trade_id: int, chain_id: str):
"""Submit trade to blockchain in background"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM trades WHERE id = ?", (trade_id,))
trade = cursor.fetchone()
if trade:
trade_data = {
"type": "trade",
"buy_order_id": trade["buy_order_id"],
"sell_order_id": trade["sell_order_id"],
"amount": trade["amount"],
"price": trade["price"]
}
tx_hash = await submit_chain_transaction(chain_id, trade_data)
if tx_hash:
cursor.execute('''
UPDATE trades SET blockchain_tx_hash = ?, chain_status = 'pending'
WHERE id = ?
''', (tx_hash, trade_id))
conn.commit()
conn.close()
except Exception as e:
print(f"Background trade blockchain submission error: {e}")
@app.get("/api/v1/stats/{chain_id}")
async def get_chain_stats(chain_id: str):
"""Get trading statistics for specific chain"""
if chain_id not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
try:
conn = get_db_connection()
cursor = conn.cursor()
# Get order stats
cursor.execute('''
SELECT
COUNT(*) as total_orders,
SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open_orders,
SUM(CASE WHEN status = 'filled' THEN 1 ELSE 0 END) as filled_orders,
SUM(amount) as total_volume
FROM orders WHERE chain_id = ?
''', (chain_id,))
order_stats = dict(cursor.fetchone())
# Get trade stats
cursor.execute('''
SELECT
COUNT(*) as total_trades,
SUM(amount) as trade_volume,
AVG(price) as avg_price,
MAX(price) as highest_price,
MIN(price) as lowest_price
FROM trades WHERE chain_id = ?
''', (chain_id,))
trade_stats = dict(cursor.fetchone())
conn.close()
return {
"chain_id": chain_id,
"chain_name": SUPPORTED_CHAINS[chain_id]["name"],
"orders": order_stats,
"trades": trade_stats,
"timestamp": datetime.now().isoformat()
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
if __name__ == "__main__":
# Initialize database
if init_database():
print("✅ Multi-chain database initialized successfully")
else:
print("❌ Database initialization failed")
# Run the server
uvicorn.run(app, host="0.0.0.0", port=8001)