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
This commit is contained in:
oib
2026-03-06 18:14:49 +01:00
parent dc1561d457
commit bb5363bebc
295 changed files with 35501 additions and 3734 deletions

363
apps/exchange/admin.html Normal file
View File

@@ -0,0 +1,363 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITBC Exchange Admin - Live Treasury Dashboard</title>
<link rel="stylesheet" href="/assets/css/aitbc.css">
<script src="/assets/js/axios.min.js"></script>
<script src="/assets/js/lucide.js"></script>
<style>
.stat-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: #f97316;
}
.stat-label {
color: #6b7280;
margin-top: 8px;
}
.wallet-balance {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
padding: 30px;
border-radius: 16px;
margin-bottom: 30px;
}
.wallet-address {
font-family: monospace;
background: rgba(255, 255, 255, 0.2);
padding: 8px 12px;
border-radius: 6px;
display: inline-block;
margin-top: 10px;
}
.payment-list {
max-height: 400px;
overflow-y: auto;
}
.payment-item {
border-left: 4px solid #e5e7eb;
padding: 12px;
margin-bottom: 8px;
background: #f9fafb;
border-radius: 0 8px 8px 0;
}
.payment-item.pending {
border-left-color: #f59e0b;
}
.payment-item.confirmed {
border-left-color: #10b981;
}
.payment-item.expired {
border-left-color: #ef4444;
}
.refresh-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
background: #f97316;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4);
cursor: pointer;
transition: all 0.3s;
}
.refresh-btn:hover {
transform: scale(1.1);
}
.refresh-btn.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-50">
<header class="bg-white shadow-sm border-b">
<div class="bg-green-100 text-green-800 text-center py-2 text-sm">
✅ LIVE MODE - Connected to AITBC Blockchain with Real Treasury Balance
</div>
<div class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<i data-lucide="trending-up" class="w-8 h-8 text-orange-600"></i>
<h1 class="text-2xl font-bold">Exchange Admin Dashboard</h1>
</div>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-600">Bank Director Portal</span>
<button onclick="logout()" class="text-gray-500 hover:text-gray-700">
<i data-lucide="log-out" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<!-- Market Statistics -->
<section class="bg-white rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i data-lucide="bar-chart" class="w-5 h-5 mr-2 text-blue-600"></i>
Market Statistics
</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<div>
<div class="text-2xl font-bold text-gray-900" id="totalAitbcSold">0</div>
<div class="text-sm text-gray-600 mt-1">Total AITBC Sold</div>
</div>
<div>
<div class="text-2xl font-bold text-gray-900" id="totalBtcReceived">0 BTC</div>
<div class="text-sm text-gray-600 mt-1">Total BTC Received</div>
</div>
<div>
<div class="text-2xl font-bold text-gray-900" id="pendingPayments">0</div>
<div class="text-sm text-gray-600 mt-1">Pending Payments</div>
</div>
<div>
<div class="text-2xl font-bold text-green-600" id="marketStatus">Market is open</div>
<div class="text-sm text-gray-600 mt-1">Market Status</div>
</div>
</div>
</section>
<!-- Bitcoin Wallet Balance -->
<section class="wallet-balance">
<h2 class="text-3xl font-bold mb-4">Bitcoin Wallet</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="text-sm opacity-90">Current Balance</div>
<div class="text-4xl font-bold" id="btcBalance">0.00000000 BTC</div>
</div>
<div>
<div class="text-sm opacity-90 mb-2">Wallet Address</div>
<div class="wallet-address text-sm" id="walletAddress">tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh</div>
</div>
</div>
</section>
<!-- Statistics Grid -->
<section class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="stat-card">
<i data-lucide="coins" class="w-8 h-8 text-orange-600 mb-2"></i>
<div class="stat-value" id="totalAitbcSold">0</div>
<div class="stat-label">Total AITBC Sold</div>
</div>
<div class="stat-card">
<i data-lucide="bitcoin" class="w-8 h-8 text-orange-600 mb-2"></i>
<div class="stat-value" id="totalBtcReceived">0 BTC</div>
<div class="stat-label">Total BTC Received</div>
</div>
<div class="stat-card">
<i data-lucide="users" class="w-8 h-8 text-orange-600 mb-2"></i>
<div class="stat-value" id="totalUsers">0</div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-card">
<i data-lucide="clock" class="w-8 h-8 text-orange-600 mb-2"></i>
<div class="stat-value" id="pendingPayments">0</div>
<div class="stat-label">Pending Payments</div>
</div>
</section>
<!-- Available AITBC -->
<section class="bg-white rounded-lg shadow p-6 mb-8">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i data-lucide="package" class="w-5 h-5 mr-2 text-orange-600"></i>
Available AITBC for Sale
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="text-3xl font-bold text-green-600" id="availableAitbc">Loading...</div>
<div class="text-sm text-gray-600 mt-1">AITBC in Treasury (available for sale)</div>
</div>
<div>
<div class="text-2xl font-semibold text-gray-900" id="estimatedValue">100 BTC</div>
<div class="text-sm text-gray-600 mt-1">Estimated value at current rate</div>
</div>
</div>
</section>
<!-- Recent Payments -->
<section class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i data-lucide="activity" class="w-5 h-5 mr-2 text-orange-600"></i>
Recent Payments
</h2>
<div class="payment-list" id="paymentsList">
<div class="text-gray-500 text-center py-8">Loading payments...</div>
</div>
</section>
</main>
<!-- Refresh Button -->
<div class="refresh-btn" onclick="refreshData()" id="refreshBtn">
<i data-lucide="refresh-cw" class="w-6 h-6"></i>
</div>
<script>
const API_BASE = window.location.origin + '/api';
let refreshInterval;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
refreshData();
// Auto-refresh every 30 seconds
refreshInterval = setInterval(refreshData, 30000);
});
// Refresh all data
async function refreshData() {
const btn = document.getElementById('refreshBtn');
btn.classList.add('spinning');
try {
await Promise.all([
loadMarketStats(),
loadPayments(),
loadWalletBalance()
]);
} catch (error) {
console.error('Error refreshing data:', error);
} finally {
setTimeout(() => btn.classList.remove('spinning'), 500);
}
}
// Load market statistics
async function loadMarketStats() {
try {
// Get treasury balance instead of hardcoded amount
const treasuryResponse = await axios.get(`${API_BASE}/treasury-balance`);
const treasury = treasuryResponse.data;
const availableAitbc = parseInt(treasury.available_for_sale) / 1000000; // Convert from smallest units
const stats = { price: 0.00001 }; // Default price
// Update elements with defensive checks
const totalSoldEl = document.getElementById('totalAitbcSold');
if (totalSoldEl) totalSoldEl.textContent = (stats.daily_volume || 0).toLocaleString();
const totalBtcEl = document.getElementById('totalBtcReceived');
if (totalBtcEl) totalBtcEl.textContent = (stats.daily_volume_btc || 0).toFixed(8) + ' BTC';
const pendingEl = document.getElementById('pendingPayments');
if (pendingEl) pendingEl.textContent = stats.pending_payments || 0;
// Update available AITBC from treasury
document.getElementById('availableAitbc').textContent =
availableAitbc.toLocaleString();
document.getElementById('estimatedValue').textContent =
(availableAitbc * (stats.price || 0.00001)).toFixed(2) + ' BTC';
// Add source indicator
const supplyElement = document.getElementById('availableAitbc');
if (treasury.source === 'genesis') {
supplyElement.innerHTML += ' <span class="text-xs text-orange-600">(Genesis)</span>';
}
// Update market status
const marketStatus = stats.market_status;
const marketStatusEl = document.getElementById('marketStatus');
if (marketStatusEl) {
if (marketStatus === 'open') {
marketStatusEl.textContent = 'Market is open';
marketStatusEl.classList.remove('text-red-600');
marketStatusEl.classList.add('text-green-600');
} else if (marketStatus === 'closed') {
marketStatusEl.textContent = 'Market is closed';
marketStatusEl.classList.remove('text-green-600');
marketStatusEl.classList.add('text-red-600');
} else {
marketStatusEl.textContent = 'Market status unknown';
marketStatusEl.classList.remove('text-green-600', 'text-red-600');
}
}
} catch (error) {
console.error('Error loading market stats:', error);
}
}
// Load recent payments
async function loadPayments() {
try {
// Since there's no endpoint to list all payments, we'll show a message
document.getElementById('paymentsList').innerHTML =
'<div class="text-gray-500 text-center py-8">Payment history requires database implementation</div>';
} catch (error) {
console.error('Error loading payments:', error);
}
}
// Load wallet balance from API
async function loadWalletBalance() {
try {
const response = await axios.get(`${API_BASE}/exchange/wallet/info`);
const wallet = response.data;
document.getElementById('btcBalance').textContent =
wallet.balance.toFixed(8) + ' BTC';
document.getElementById('walletAddress').textContent =
wallet.address;
// Show wallet type
const balanceElement = document.getElementById('btcBalance');
if (wallet.testnet) {
balanceElement.innerHTML += ' <span style="font-size: 0.5em; opacity: 0.7;">(TESTNET)</span>';
}
// Update wallet info section
updateWalletInfo(wallet);
} catch (error) {
console.error('Error loading wallet balance:', error);
// Fallback to demo data
document.getElementById('btcBalance').textContent = '0.00000000 BTC';
document.getElementById('walletAddress').textContent = 'Wallet API unavailable';
}
}
// Update wallet information display
function updateWalletInfo(wallet) {
// Create or update wallet info display
let walletInfo = document.getElementById('walletInfoDisplay');
if (!walletInfo) {
walletInfo = document.createElement('div');
walletInfo.id = 'walletInfoDisplay';
walletInfo.className = 'mt-4 p-4 bg-gray-100 rounded-lg';
document.querySelector('.wallet-balance').appendChild(walletInfo);
}
walletInfo.innerHTML = `
<div class="text-sm">
<div class="mb-2"><strong>Wallet Type:</strong> ${wallet.wallet_type}</div>
<div class="mb-2"><strong>Network:</strong> ${wallet.testnet ? 'Testnet' : 'Mainnet'}</div>
<div><strong>Recent Transactions:</strong> ${wallet.transactions.length}</div>
</div>
`;
}
// Logout
function logout() {
window.location.href = '/Exchange/';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
Bitcoin Wallet Integration for AITBC Trade Exchange
"""
import os
import json
import hashlib
import hmac
import time
from typing import Dict, Optional, Tuple
from dataclasses import dataclass
import requests
@dataclass
class BitcoinWallet:
"""Bitcoin wallet configuration"""
address: str
private_key: Optional[str] = None
testnet: bool = True
class BitcoinProcessor:
"""Bitcoin payment processor"""
def __init__(self, config: Dict):
self.config = config
self.testnet = config.get('testnet', True)
self.api_key = config.get('api_key')
self.webhook_secret = config.get('webhook_secret')
def generate_payment_address(self, user_id: str, amount_btc: float) -> str:
"""Generate a unique payment address for each transaction"""
# In production, use HD wallet to generate unique addresses
# For demo, we'll use a fixed address with payment tracking
# Create payment hash
payment_data = f"{user_id}:{amount_btc}:{int(time.time())}"
hash_bytes = hashlib.sha256(payment_data.encode()).hexdigest()
# For demo, return the main wallet address
# In production, generate unique address from HD wallet
return self.config['main_address']
def check_payment(self, address: str, amount_btc: float) -> Tuple[bool, float]:
"""Check if payment has been received"""
# In production, integrate with blockchain API
# For demo, simulate payment check
# Mock API call to check blockchain
if self.testnet:
# Testnet blockchain API
api_url = f"https://blockstream.info/testnet/api/address/{address}"
else:
# Mainnet blockchain API
api_url = f"https://blockstream.info/api/address/{address}"
try:
response = requests.get(api_url, timeout=5)
if response.status_code == 200:
data = response.json()
# Check recent transactions
# In production, implement proper transaction verification
return False, 0.0
except Exception as e:
print(f"Error checking payment: {e}")
return False, 0.0
def verify_webhook(self, payload: str, signature: str) -> bool:
"""Verify webhook signature from payment processor"""
if not self.webhook_secret:
return True # Skip verification if no secret
expected_signature = hmac.new(
self.webhook_secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
class WalletManager:
"""Manages Bitcoin wallet operations"""
def __init__(self):
self.config = self.load_config()
self.processor = BitcoinProcessor(self.config)
def load_config(self) -> Dict:
"""Load wallet configuration"""
return {
'testnet': os.getenv('BITCOIN_TESTNET', 'true').lower() == 'true',
'main_address': os.getenv('BITCOIN_ADDRESS', 'tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'),
'private_key': os.getenv('BITCOIN_PRIVATE_KEY'),
'api_key': os.getenv('BLOCKCHAIN_API_KEY'),
'webhook_secret': os.getenv('WEBHOOK_SECRET'),
'min_confirmations': int(os.getenv('MIN_CONFIRMATIONS', '1')),
'exchange_rate': float(os.getenv('BTC_TO_AITBC_RATE', '100000')) # 1 BTC = 100,000 AITBC
}
def create_payment_request(self, user_id: str, aitbc_amount: float) -> Dict:
"""Create a new payment request"""
btc_amount = aitbc_amount / self.config['exchange_rate']
payment_request = {
'user_id': user_id,
'aitbc_amount': aitbc_amount,
'btc_amount': btc_amount,
'payment_address': self.processor.generate_payment_address(user_id, btc_amount),
'created_at': int(time.time()),
'status': 'pending',
'expires_at': int(time.time()) + 3600 # 1 hour expiry
}
# Save payment request
self.save_payment_request(payment_request)
return payment_request
def save_payment_request(self, request: Dict):
"""Save payment request to storage"""
payments_file = 'payments.json'
payments = []
if os.path.exists(payments_file):
with open(payments_file, 'r') as f:
payments = json.load(f)
payments.append(request)
with open(payments_file, 'w') as f:
json.dump(payments, f, indent=2)
def get_payment_status(self, payment_id: str) -> Optional[Dict]:
"""Get payment status"""
payments_file = 'payments.json'
if not os.path.exists(payments_file):
return None
with open(payments_file, 'r') as f:
payments = json.load(f)
for payment in payments:
if payment.get('payment_id') == payment_id:
return payment
return None
def update_payment_status(self, payment_id: str, status: str, tx_hash: str = None):
"""Update payment status"""
payments_file = 'payments.json'
if not os.path.exists(payments_file):
return False
with open(payments_file, 'r') as f:
payments = json.load(f)
for payment in payments:
if payment.get('payment_id') == payment_id:
payment['status'] = status
payment['updated_at'] = int(time.time())
if tx_hash:
payment['tx_hash'] = tx_hash
with open(payments_file, 'w') as f:
json.dump(payments, f, indent=2)
return True
return False
# Global wallet manager
wallet_manager = WalletManager()

67
apps/exchange/build.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Build script for AITBC Trade Exchange
Combines CSS and HTML for production deployment
"""
import os
import shutil
def build_html():
"""Build production HTML with embedded CSS"""
print("🔨 Building AITBC Exchange for production...")
# Read CSS file
css_path = "styles.css"
html_path = "index.html"
output_path = "index.html"
# Backup original
if os.path.exists(html_path):
shutil.copy(html_path, "index.dev.html")
print("✓ Backed up original index.html to index.dev.html")
# Read the template
with open("index.template.html", "r") as f:
template = f.read()
# Read CSS
with open(css_path, "r") as f:
css_content = f.read()
# Replace placeholder with CSS
html_content = template.replace("<!-- CSS_PLACEHOLDER -->", f"<style>\n{css_content}\n </style>")
# Write production HTML
with open(output_path, "w") as f:
f.write(html_content)
print(f"✓ Built production HTML: {output_path}")
print("✓ CSS is now embedded in HTML")
def create_template():
"""Create a template file for future use"""
template = """<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITBC Trade Exchange - Buy & Sell AITBC</title>
<script src="https://unpkg.com/lucide@latest"></script>
<!-- CSS_PLACEHOLDER -->
</head>
<body>
<!-- Body content will be added here -->
</body>
</html>"""
with open("index.template.html", "w") as f:
f.write(template)
print("✓ Created template file: index.template.html")
if __name__ == "__main__":
if not os.path.exists("index.template.html"):
create_template()
build_html()

View File

@@ -0,0 +1,651 @@
#!/usr/bin/env python3
"""
Complete Cross-Chain AITBC Exchange
Multi-chain trading with cross-chain swaps and bridging
"""
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
import uuid
import hashlib
app = FastAPI(title="AITBC Complete Cross-Chain Exchange", version="3.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",
"bridge_contract": "0x1234567890123456789012345678901234567890"
},
"ait-testnet": {
"name": "AITBC Test Network",
"status": "inactive",
"blockchain_url": None,
"token_symbol": "AITBC-TEST",
"bridge_contract": "0x0987654321098765432109876543210987654321"
}
}
# 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 CrossChainSwapRequest(BaseModel):
from_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
to_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
from_token: str = Field(..., min_length=1)
to_token: str = Field(..., min_length=1)
amount: float = Field(..., gt=0)
min_amount: float = Field(..., gt=0)
user_address: str = Field(..., min_length=1)
slippage_tolerance: float = Field(default=0.01, ge=0, le=0.1)
class BridgeRequest(BaseModel):
source_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
target_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
token: str = Field(..., min_length=1)
amount: float = Field(..., gt=0)
recipient_address: str = Field(..., min_length=1)
# Database functions
def get_db_connection():
"""Get database connection"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_database():
"""Initialize complete cross-chain database"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Chains table
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,
token_symbol TEXT,
bridge_contract TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 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'))
)
''')
# 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'))
)
''')
# Cross-chain swaps table
cursor.execute('''
CREATE TABLE IF NOT EXISTS cross_chain_swaps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
swap_id TEXT UNIQUE NOT NULL,
from_chain TEXT NOT NULL,
to_chain TEXT NOT NULL,
from_token TEXT NOT NULL,
to_token TEXT NOT NULL,
amount REAL NOT NULL,
min_amount REAL NOT NULL,
expected_amount REAL NOT NULL,
actual_amount REAL DEFAULT NULL,
user_address TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'executing', 'completed', 'failed', 'refunded')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
from_tx_hash TEXT NULL,
to_tx_hash TEXT NULL,
bridge_fee REAL DEFAULT 0,
slippage REAL DEFAULT 0,
error_message TEXT NULL
)
''')
# Bridge transactions table
cursor.execute('''
CREATE TABLE IF NOT EXISTS bridge_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bridge_id TEXT UNIQUE NOT NULL,
source_chain TEXT NOT NULL,
target_chain TEXT NOT NULL,
token TEXT NOT NULL,
amount REAL NOT NULL,
recipient_address TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'locked', 'transferred', 'completed', 'failed')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
source_tx_hash TEXT NULL,
target_tx_hash TEXT NULL,
bridge_fee REAL DEFAULT 0,
lock_address TEXT NULL,
error_message TEXT NULL
)
''')
# Cross-chain liquidity pools
cursor.execute('''
CREATE TABLE IF NOT EXISTS cross_chain_pools (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pool_id TEXT UNIQUE NOT NULL,
token_a TEXT NOT NULL,
token_b TEXT NOT NULL,
chain_a TEXT NOT NULL,
chain_b TEXT NOT NULL,
reserve_a REAL DEFAULT 0,
reserve_b REAL DEFAULT 0,
total_liquidity REAL DEFAULT 0,
apr REAL DEFAULT 0,
fee_rate REAL DEFAULT 0.003,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 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, token_symbol, bridge_contract)
VALUES (?, ?, ?, ?, ?, ?)
''', (chain_id, chain_info["name"], chain_info["status"],
chain_info["blockchain_url"], chain_info["token_symbol"],
chain_info.get("bridge_contract")))
# Create sample liquidity pool
cursor.execute('''
INSERT OR IGNORE INTO cross_chain_pools
(pool_id, token_a, token_b, chain_a, chain_b, reserve_a, reserve_b, total_liquidity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', ("ait-devnet-ait-testnet-AITBC", "AITBC", "AITBC", "ait-devnet", "ait-testnet", 1000, 1000, 2000))
# 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_swaps_user ON cross_chain_swaps(user_address)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_swaps_status ON cross_chain_swaps(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bridge_status ON bridge_transactions(status)')
conn.commit()
conn.close()
return True
except Exception as e:
print(f"Database initialization error: {e}")
return False
# Cross-chain rate calculation
def get_cross_chain_rate(from_chain: str, to_chain: str, from_token: str, to_token: str) -> Optional[float]:
"""Get cross-chain exchange rate"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Check liquidity pool
cursor.execute('''
SELECT reserve_a, reserve_b FROM cross_chain_pools
WHERE ((chain_a = ? AND chain_b = ? AND token_a = ? AND token_b = ?) OR
(chain_a = ? AND chain_b = ? AND token_a = ? AND token_b = ?))
''', (from_chain, to_chain, from_token, to_token, to_chain, from_chain, to_token, from_token))
pool = cursor.fetchone()
if pool and pool["reserve_a"] > 0 and pool["reserve_b"] > 0:
return pool["reserve_b"] / pool["reserve_a"]
# Fallback to 1:1 for same tokens
if from_token == to_token:
return 1.0
return 1.0 # Default fallback rate
except Exception as e:
print(f"Rate calculation error: {e}")
return None
# Cross-chain swap execution
async def execute_cross_chain_swap(swap_request: CrossChainSwapRequest) -> Dict[str, Any]:
"""Execute cross-chain swap"""
try:
# Validate chains
if swap_request.from_chain == swap_request.to_chain:
raise HTTPException(status_code=400, detail="Cannot swap within same chain")
# Get exchange rate
rate = get_cross_chain_rate(swap_request.from_chain, swap_request.to_chain,
swap_request.from_token, swap_request.to_token)
if not rate:
raise HTTPException(status_code=400, detail="No exchange rate available")
# Calculate expected amount (including fees)
bridge_fee = swap_request.amount * 0.003 # 0.3% bridge fee
swap_fee = swap_request.amount * 0.001 # 0.1% swap fee
total_fees = bridge_fee + swap_fee
net_amount = swap_request.amount - total_fees
expected_amount = net_amount * rate
# Check slippage
if expected_amount < swap_request.min_amount:
raise HTTPException(status_code=400, detail="Insufficient output due to slippage")
# Create swap record
swap_id = str(uuid.uuid4())
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO cross_chain_swaps
(swap_id, from_chain, to_chain, from_token, to_token, amount, min_amount,
expected_amount, user_address, bridge_fee, slippage)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (swap_id, swap_request.from_chain, swap_request.to_chain, swap_request.from_token,
swap_request.to_token, swap_request.amount, swap_request.min_amount, expected_amount,
swap_request.user_address, bridge_fee, swap_request.slippage_tolerance))
conn.commit()
conn.close()
# Process swap in background
asyncio.create_task(process_cross_chain_swap(swap_id))
return {
"success": True,
"swap_id": swap_id,
"from_chain": swap_request.from_chain,
"to_chain": swap_request.to_chain,
"from_token": swap_request.from_token,
"to_token": swap_request.to_token,
"amount": swap_request.amount,
"expected_amount": expected_amount,
"rate": rate,
"total_fees": total_fees,
"bridge_fee": bridge_fee,
"swap_fee": swap_fee,
"status": "pending"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Swap execution failed: {str(e)}")
async def process_cross_chain_swap(swap_id: str):
"""Process cross-chain swap"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM cross_chain_swaps WHERE swap_id = ?", (swap_id,))
swap = cursor.fetchone()
if not swap:
return
# Update status
cursor.execute("UPDATE cross_chain_swaps SET status = 'executing' WHERE swap_id = ?", (swap_id,))
conn.commit()
# Simulate cross-chain execution
await asyncio.sleep(3) # Simulate blockchain processing
# Generate mock transaction hashes
from_tx_hash = f"0x{uuid.uuid4().hex[:64]}"
to_tx_hash = f"0x{uuid.uuid4().hex[:64]}"
# Complete swap
actual_amount = swap["expected_amount"] * 0.98 # Small slippage
cursor.execute('''
UPDATE cross_chain_swaps SET status = 'completed', actual_amount = ?,
from_tx_hash = ?, to_tx_hash = ?, completed_at = CURRENT_TIMESTAMP
WHERE swap_id = ?
''', (actual_amount, from_tx_hash, to_tx_hash, swap_id))
conn.commit()
conn.close()
except Exception as e:
print(f"Cross-chain swap processing error: {e}")
# API Endpoints
@app.get("/health")
async def health_check():
"""Complete cross-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,
"bridge_contract": chain_info.get("bridge_contract")
}
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": "complete-cross-chain-exchange",
"version": "3.0.0",
"supported_chains": list(SUPPORTED_CHAINS.keys()),
"chain_status": chain_status,
"cross_chain": True,
"features": ["trading", "swaps", "bridging", "liquidity_pools"],
"timestamp": datetime.now().isoformat()
}
@app.get("/api/v1/chains")
async def get_chains():
"""Get all supported chains"""
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"],
"bridge_contract": chain_info.get("bridge_contract")
})
return {
"chains": chains,
"total_chains": len(chains),
"active_chains": len([c for c in chains if c["status"] == "active"])
}
@app.post("/api/v1/cross-chain/swap")
async def create_cross_chain_swap(swap_request: CrossChainSwapRequest):
"""Create cross-chain swap"""
return await execute_cross_chain_swap(swap_request)
@app.get("/api/v1/cross-chain/swap/{swap_id}")
async def get_cross_chain_swap(swap_id: str):
"""Get cross-chain swap details"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM cross_chain_swaps WHERE swap_id = ?", (swap_id,))
swap = cursor.fetchone()
conn.close()
if not swap:
raise HTTPException(status_code=404, detail="Swap not found")
return dict(swap)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get swap: {str(e)}")
@app.get("/api/v1/cross-chain/swaps")
async def get_cross_chain_swaps(user_address: Optional[str] = None, status: Optional[str] = None):
"""Get cross-chain swaps"""
try:
conn = get_db_connection()
cursor = conn.cursor()
query = "SELECT * FROM cross_chain_swaps"
params = []
if user_address:
query += " WHERE user_address = ?"
params.append(user_address)
if status:
if user_address:
query += " AND status = ?"
else:
query += " WHERE status = ?"
params.append(status)
query += " ORDER BY created_at DESC"
cursor.execute(query, params)
swaps = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"swaps": swaps,
"total_swaps": len(swaps)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get swaps: {str(e)}")
@app.post("/api/v1/cross-chain/bridge")
async def create_bridge_transaction(bridge_request: BridgeRequest):
"""Create bridge transaction"""
try:
if bridge_request.source_chain == bridge_request.target_chain:
raise HTTPException(status_code=400, detail="Cannot bridge to same chain")
bridge_id = str(uuid.uuid4())
bridge_fee = bridge_request.amount * 0.001 # 0.1% bridge fee
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO bridge_transactions
(bridge_id, source_chain, target_chain, token, amount, recipient_address, bridge_fee)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (bridge_id, bridge_request.source_chain, bridge_request.target_chain,
bridge_request.token, bridge_request.amount, bridge_request.recipient_address, bridge_fee))
conn.commit()
conn.close()
# Process bridge in background
asyncio.create_task(process_bridge_transaction(bridge_id))
return {
"success": True,
"bridge_id": bridge_id,
"source_chain": bridge_request.source_chain,
"target_chain": bridge_request.target_chain,
"token": bridge_request.token,
"amount": bridge_request.amount,
"bridge_fee": bridge_fee,
"status": "pending"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Bridge creation failed: {str(e)}")
async def process_bridge_transaction(bridge_id: str):
"""Process bridge transaction"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM bridge_transactions WHERE bridge_id = ?", (bridge_id,))
bridge = cursor.fetchone()
if not bridge:
return
# Update status
cursor.execute("UPDATE bridge_transactions SET status = 'locked' WHERE bridge_id = ?", (bridge_id,))
conn.commit()
# Simulate bridge processing
await asyncio.sleep(2)
# Generate mock transaction hashes
source_tx_hash = f"0x{uuid.uuid4().hex[:64]}"
target_tx_hash = f"0x{uuid.uuid4().hex[:64]}"
# Complete bridge
cursor.execute('''
UPDATE bridge_transactions SET status = 'completed',
source_tx_hash = ?, target_tx_hash = ?, completed_at = CURRENT_TIMESTAMP
WHERE bridge_id = ?
''', (source_tx_hash, target_tx_hash, bridge_id))
conn.commit()
conn.close()
except Exception as e:
print(f"Bridge processing error: {e}")
@app.get("/api/v1/cross-chain/bridge/{bridge_id}")
async def get_bridge_transaction(bridge_id: str):
"""Get bridge transaction details"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM bridge_transactions WHERE bridge_id = ?", (bridge_id,))
bridge = cursor.fetchone()
conn.close()
if not bridge:
raise HTTPException(status_code=404, detail="Bridge transaction not found")
return dict(bridge)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get bridge: {str(e)}")
@app.get("/api/v1/cross-chain/rates")
async def get_cross_chain_rates():
"""Get cross-chain exchange rates"""
rates = {}
for from_chain in SUPPORTED_CHAINS:
for to_chain in SUPPORTED_CHAINS:
if from_chain != to_chain:
pair_key = f"{from_chain}-{to_chain}"
rate = get_cross_chain_rate(from_chain, to_chain, "AITBC", "AITBC")
if rate:
rates[pair_key] = rate
return {
"rates": rates,
"timestamp": datetime.now().isoformat()
}
@app.get("/api/v1/cross-chain/pools")
async def get_cross_chain_pools():
"""Get cross-chain liquidity pools"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM cross_chain_pools ORDER BY total_liquidity DESC")
pools = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"pools": pools,
"total_pools": len(pools)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get pools: {str(e)}")
@app.get("/api/v1/cross-chain/stats")
async def get_cross_chain_stats():
"""Get cross-chain trading statistics"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Swap stats
cursor.execute('''
SELECT status, COUNT(*) as count, SUM(amount) as volume
FROM cross_chain_swaps
GROUP BY status
''')
swap_stats = [dict(row) for row in cursor.fetchall()]
# Bridge stats
cursor.execute('''
SELECT status, COUNT(*) as count, SUM(amount) as volume
FROM bridge_transactions
GROUP BY status
''')
bridge_stats = [dict(row) for row in cursor.fetchall()]
# Total volume
cursor.execute("SELECT SUM(amount) FROM cross_chain_swaps WHERE status = 'completed'")
total_volume = cursor.fetchone()[0] or 0
conn.close()
return {
"swap_stats": swap_stats,
"bridge_stats": bridge_stats,
"total_volume": total_volume,
"supported_chains": list(SUPPORTED_CHAINS.keys()),
"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("✅ Complete cross-chain database initialized")
else:
print("❌ Database initialization failed")
# Run the server
uvicorn.run(app, host="0.0.0.0", port=8001)

View File

@@ -0,0 +1,614 @@
#!/usr/bin/env python3
"""
Cross-Chain Trading Extension for Multi-Chain Exchange
Adds cross-chain trading, bridging, and swap functionality
"""
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 uuid
import hashlib
# Import the base multi-chain exchange
from multichain_exchange_api import app, get_db_connection, SUPPORTED_CHAINS
# Cross-Chain Models
class CrossChainSwapRequest(BaseModel):
from_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
to_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
from_token: str = Field(..., min_length=1)
to_token: str = Field(..., min_length=1)
amount: float = Field(..., gt=0)
min_amount: float = Field(..., gt=0)
user_address: str = Field(..., min_length=1)
slippage_tolerance: float = Field(default=0.01, ge=0, le=0.1)
class BridgeRequest(BaseModel):
source_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
target_chain: str = Field(..., regex="^(ait-devnet|ait-testnet)$")
token: str = Field(..., min_length=1)
amount: float = Field(..., gt=0)
recipient_address: str = Field(..., min_length=1)
class CrossChainOrder(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)$")
cross_chain: bool = Field(default=True)
target_chain: Optional[str] = None
user_address: str = Field(..., min_length=1)
# Cross-Chain Database Functions
def init_cross_chain_tables():
"""Initialize cross-chain trading tables"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Cross-chain swaps table
cursor.execute('''
CREATE TABLE IF NOT EXISTS cross_chain_swaps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
swap_id TEXT UNIQUE NOT NULL,
from_chain TEXT NOT NULL,
to_chain TEXT NOT NULL,
from_token TEXT NOT NULL,
to_token TEXT NOT NULL,
amount REAL NOT NULL,
min_amount REAL NOT NULL,
expected_amount REAL NOT NULL,
actual_amount REAL DEFAULT NULL,
user_address TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'executing', 'completed', 'failed', 'refunded')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
from_tx_hash TEXT NULL,
to_tx_hash TEXT NULL,
bridge_fee REAL DEFAULT 0,
slippage REAL DEFAULT 0,
error_message TEXT NULL
)
''')
# Bridge transactions table
cursor.execute('''
CREATE TABLE IF NOT EXISTS bridge_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bridge_id TEXT UNIQUE NOT NULL,
source_chain TEXT NOT NULL,
target_chain TEXT NOT NULL,
token TEXT NOT NULL,
amount REAL NOT NULL,
recipient_address TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'locked', 'transferred', 'completed', 'failed')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
source_tx_hash TEXT NULL,
target_tx_hash TEXT NULL,
bridge_fee REAL DEFAULT 0,
lock_address TEXT NULL,
error_message TEXT NULL
)
''')
# Cross-chain liquidity pools
cursor.execute('''
CREATE TABLE IF NOT EXISTS cross_chain_pools (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pool_id TEXT UNIQUE NOT NULL,
token_a TEXT NOT NULL,
token_b TEXT NOT NULL,
chain_a TEXT NOT NULL,
chain_b TEXT NOT NULL,
reserve_a REAL DEFAULT 0,
reserve_b REAL DEFAULT 0,
total_liquidity REAL DEFAULT 0,
apr REAL DEFAULT 0,
fee_rate REAL DEFAULT 0.003,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Create indexes
cursor.execute('CREATE INDEX IF NOT EXISTS idx_swaps_user ON cross_chain_swaps(user_address)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_swaps_status ON cross_chain_swaps(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_swaps_chains ON cross_chain_swaps(from_chain, to_chain)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bridge_status ON bridge_transactions(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_bridge_chains ON bridge_transactions(source_chain, target_chain)')
conn.commit()
conn.close()
return True
except Exception as e:
print(f"Cross-chain database initialization error: {e}")
return False
# Cross-Chain Liquidity Management
def get_cross_chain_rate(from_chain: str, to_chain: str, from_token: str, to_token: str) -> Optional[float]:
"""Get cross-chain exchange rate"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Check if there's a liquidity pool for this pair
cursor.execute('''
SELECT reserve_a, reserve_b FROM cross_chain_pools
WHERE ((chain_a = ? AND chain_b = ? AND token_a = ? AND token_b = ?) OR
(chain_a = ? AND chain_b = ? AND token_a = ? AND token_b = ?))
''', (from_chain, to_chain, from_token, to_token, to_chain, from_chain, to_token, from_token))
pool = cursor.fetchone()
if pool:
reserve_a, reserve_b = pool
if from_chain == SUPPORTED_CHAINS[from_chain] and reserve_a > 0 and reserve_b > 0:
return reserve_b / reserve_a
# Fallback to 1:1 rate for same tokens
if from_token == to_token:
return 1.0
# Get rates from individual chains
rate_a = get_chain_token_price(from_chain, from_token)
rate_b = get_chain_token_price(to_chain, to_token)
if rate_a and rate_b:
return rate_b / rate_a
return None
except Exception as e:
print(f"Rate calculation error: {e}")
return None
def get_chain_token_price(chain_id: str, token: str) -> Optional[float]:
"""Get token price on specific chain"""
try:
chain_info = SUPPORTED_CHAINS.get(chain_id)
if not chain_info or chain_info["status"] != "active":
return None
# Mock price for now - in production, this would call the chain's price oracle
if token == "AITBC":
return 1.0
elif token == "USDC":
return 1.0
else:
return 0.5 # Default fallback
except:
return None
# Cross-Chain Swap Functions
async def execute_cross_chain_swap(swap_request: CrossChainSwapRequest) -> Dict[str, Any]:
"""Execute cross-chain swap"""
try:
# Validate chains
if swap_request.from_chain == swap_request.to_chain:
raise HTTPException(status_code=400, detail="Cannot swap within same chain")
if swap_request.from_chain not in SUPPORTED_CHAINS or swap_request.to_chain not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail="Unsupported chain")
# Get exchange rate
rate = get_cross_chain_rate(swap_request.from_chain, swap_request.to_chain,
swap_request.from_token, swap_request.to_token)
if not rate:
raise HTTPException(status_code=400, detail="No exchange rate available")
# Calculate expected amount
expected_amount = swap_request.amount * rate * (1 - 0.003) # 0.3% fee
# Check slippage
if expected_amount < swap_request.min_amount:
raise HTTPException(status_code=400, detail="Insufficient output due to slippage")
# Create swap record
swap_id = str(uuid.uuid4())
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO cross_chain_swaps
(swap_id, from_chain, to_chain, from_token, to_token, amount, min_amount, expected_amount, user_address)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (swap_id, swap_request.from_chain, swap_request.to_chain, swap_request.from_token,
swap_request.to_token, swap_request.amount, swap_request.min_amount, expected_amount,
swap_request.user_address))
conn.commit()
conn.close()
# Execute swap in background
asyncio.create_task(process_cross_chain_swap(swap_id))
return {
"success": True,
"swap_id": swap_id,
"from_chain": swap_request.from_chain,
"to_chain": swap_request.to_chain,
"amount": swap_request.amount,
"expected_amount": expected_amount,
"rate": rate,
"status": "pending"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Swap execution failed: {str(e)}")
async def process_cross_chain_swap(swap_id: str):
"""Process cross-chain swap in background"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Get swap details
cursor.execute("SELECT * FROM cross_chain_swaps WHERE swap_id = ?", (swap_id,))
swap = cursor.fetchone()
if not swap:
return
# Update status to executing
cursor.execute("UPDATE cross_chain_swaps SET status = 'executing' WHERE swap_id = ?", (swap_id,))
conn.commit()
# Step 1: Lock funds on source chain
from_tx_hash = await lock_funds_on_chain(swap["from_chain"], swap["from_token"],
swap["amount"], swap["user_address"])
if not from_tx_hash:
cursor.execute('''
UPDATE cross_chain_swaps SET status = 'failed', error_message = ?
WHERE swap_id = ?
''', ("Failed to lock source funds", swap_id))
conn.commit()
return
# Step 2: Transfer to target chain
to_tx_hash = await transfer_to_target_chain(swap["to_chain"], swap["to_token"],
swap["expected_amount"], swap["user_address"])
if not to_tx_hash:
# Refund source chain
await refund_source_chain(swap["from_chain"], from_tx_hash, swap["user_address"])
cursor.execute('''
UPDATE cross_chain_swaps SET status = 'refunded', error_message = ?,
from_tx_hash = ? WHERE swap_id = ?
''', ("Target transfer failed, refunded", from_tx_hash, swap_id))
conn.commit()
return
# Step 3: Complete swap
actual_amount = await verify_target_transfer(swap["to_chain"], to_tx_hash)
cursor.execute('''
UPDATE cross_chain_swaps SET status = 'completed', actual_amount = ?,
from_tx_hash = ?, to_tx_hash = ?, completed_at = CURRENT_TIMESTAMP
WHERE swap_id = ?
''', (actual_amount, from_tx_hash, to_tx_hash, swap_id))
conn.commit()
conn.close()
except Exception as e:
print(f"Cross-chain swap processing error: {e}")
async def lock_funds_on_chain(chain_id: str, token: str, amount: float, user_address: str) -> Optional[str]:
"""Lock funds on source chain"""
try:
chain_info = SUPPORTED_CHAINS[chain_id]
if chain_info["status"] != "active":
return None
# Mock implementation - in production, this would call the chain's lock function
lock_tx_hash = f"lock_{uuid.uuid4().hex[:8]}"
# Simulate blockchain call
await asyncio.sleep(1)
return lock_tx_hash
except:
return None
async def transfer_to_target_chain(chain_id: str, token: str, amount: float, user_address: str) -> Optional[str]:
"""Transfer tokens to target chain"""
try:
chain_info = SUPPORTED_CHAINS[chain_id]
if chain_info["status"] != "active":
return None
# Mock implementation - in production, this would call the chain's mint/transfer function
transfer_tx_hash = f"transfer_{uuid.uuid4().hex[:8]}"
# Simulate blockchain call
await asyncio.sleep(2)
return transfer_tx_hash
except:
return None
async def refund_source_chain(chain_id: str, lock_tx_hash: str, user_address: str) -> bool:
"""Refund locked funds on source chain"""
try:
chain_info = SUPPORTED_CHAINS[chain_id]
if chain_info["status"] != "active":
return False
# Mock implementation - in production, this would call the chain's refund function
await asyncio.sleep(1)
return True
except:
return False
async def verify_target_transfer(chain_id: str, tx_hash: str) -> Optional[float]:
"""Verify transfer on target chain"""
try:
chain_info = SUPPORTED_CHAINS[chain_id]
if chain_info["status"] != "active":
return None
# Mock implementation - in production, this would verify the actual transaction
await asyncio.sleep(1)
return 100.0 # Mock amount
except:
return None
# Cross-Chain API Endpoints
@app.post("/api/v1/cross-chain/swap")
async def create_cross_chain_swap(swap_request: CrossChainSwapRequest, background_tasks: BackgroundTasks):
"""Create cross-chain swap"""
return await execute_cross_chain_swap(swap_request)
@app.get("/api/v1/cross-chain/swap/{swap_id}")
async def get_cross_chain_swap(swap_id: str):
"""Get cross-chain swap details"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM cross_chain_swaps WHERE swap_id = ?", (swap_id,))
swap = cursor.fetchone()
conn.close()
if not swap:
raise HTTPException(status_code=404, detail="Swap not found")
return dict(swap)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get swap: {str(e)}")
@app.get("/api/v1/cross-chain/swaps")
async def get_cross_chain_swaps(user_address: Optional[str] = None, status: Optional[str] = None):
"""Get cross-chain swaps"""
try:
conn = get_db_connection()
cursor = conn.cursor()
query = "SELECT * FROM cross_chain_swaps"
params = []
if user_address:
query += " WHERE user_address = ?"
params.append(user_address)
if status:
if user_address:
query += " AND status = ?"
else:
query += " WHERE status = ?"
params.append(status)
query += " ORDER BY created_at DESC"
cursor.execute(query, params)
swaps = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"swaps": swaps,
"total_swaps": len(swaps)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get swaps: {str(e)}")
@app.post("/api/v1/cross-chain/bridge")
async def create_bridge_transaction(bridge_request: BridgeRequest, background_tasks: BackgroundTasks):
"""Create bridge transaction"""
try:
if bridge_request.source_chain == bridge_request.target_chain:
raise HTTPException(status_code=400, detail="Cannot bridge to same chain")
bridge_id = str(uuid.uuid4())
bridge_fee = bridge_request.amount * 0.001 # 0.1% bridge fee
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO bridge_transactions
(bridge_id, source_chain, target_chain, token, amount, recipient_address, bridge_fee)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (bridge_id, bridge_request.source_chain, bridge_request.target_chain,
bridge_request.token, bridge_request.amount, bridge_request.recipient_address, bridge_fee))
conn.commit()
conn.close()
# Process bridge in background
asyncio.create_task(process_bridge_transaction(bridge_id))
return {
"success": True,
"bridge_id": bridge_id,
"source_chain": bridge_request.source_chain,
"target_chain": bridge_request.target_chain,
"amount": bridge_request.amount,
"bridge_fee": bridge_fee,
"status": "pending"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Bridge creation failed: {str(e)}")
async def process_bridge_transaction(bridge_id: str):
"""Process bridge transaction in background"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM bridge_transactions WHERE bridge_id = ?", (bridge_id,))
bridge = cursor.fetchone()
if not bridge:
return
# Update status
cursor.execute("UPDATE bridge_transactions SET status = 'locked' WHERE bridge_id = ?", (bridge_id,))
conn.commit()
# Lock on source chain
source_tx_hash = await lock_funds_on_chain(bridge["source_chain"], bridge["token"],
bridge["amount"], bridge["recipient_address"])
if source_tx_hash:
# Transfer to target chain
target_tx_hash = await transfer_to_target_chain(bridge["target_chain"], bridge["token"],
bridge["amount"], bridge["recipient_address"])
if target_tx_hash:
cursor.execute('''
UPDATE bridge_transactions SET status = 'completed',
source_tx_hash = ?, target_tx_hash = ?, completed_at = CURRENT_TIMESTAMP
WHERE bridge_id = ?
''', (source_tx_hash, target_tx_hash, bridge_id))
else:
cursor.execute('''
UPDATE bridge_transactions SET status = 'failed', error_message = ?
WHERE bridge_id = ?
''', ("Target transfer failed", bridge_id))
else:
cursor.execute('''
UPDATE bridge_transactions SET status = 'failed', error_message = ?
WHERE bridge_id = ?
''', ("Source lock failed", bridge_id))
conn.commit()
conn.close()
except Exception as e:
print(f"Bridge processing error: {e}")
@app.get("/api/v1/cross-chain/bridge/{bridge_id}")
async def get_bridge_transaction(bridge_id: str):
"""Get bridge transaction details"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM bridge_transactions WHERE bridge_id = ?", (bridge_id,))
bridge = cursor.fetchone()
conn.close()
if not bridge:
raise HTTPException(status_code=404, detail="Bridge transaction not found")
return dict(bridge)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get bridge: {str(e)}")
@app.get("/api/v1/cross-chain/rates")
async def get_cross_chain_rates():
"""Get cross-chain exchange rates"""
rates = {}
for from_chain in SUPPORTED_CHAINS:
for to_chain in SUPPORTED_CHAINS:
if from_chain != to_chain:
pair_key = f"{from_chain}-{to_chain}"
rate = get_cross_chain_rate(from_chain, to_chain, "AITBC", "AITBC")
if rate:
rates[pair_key] = rate
return {
"rates": rates,
"timestamp": datetime.now().isoformat()
}
@app.get("/api/v1/cross-chain/pools")
async def get_cross_chain_pools():
"""Get cross-chain liquidity pools"""
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM cross_chain_pools ORDER BY total_liquidity DESC")
pools = [dict(row) for row in cursor.fetchall()]
conn.close()
return {
"pools": pools,
"total_pools": len(pools)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get pools: {str(e)}")
@app.get("/api/v1/cross-chain/stats")
async def get_cross_chain_stats():
"""Get cross-chain trading statistics"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Swap stats
cursor.execute('''
SELECT status, COUNT(*) as count, SUM(amount) as volume
FROM cross_chain_swaps
GROUP BY status
''')
swap_stats = [dict(row) for row in cursor.fetchall()]
# Bridge stats
cursor.execute('''
SELECT status, COUNT(*) as count, SUM(amount) as volume
FROM bridge_transactions
GROUP BY status
''')
bridge_stats = [dict(row) for row in cursor.fetchall()]
# Total volume
cursor.execute("SELECT SUM(amount) FROM cross_chain_swaps WHERE status = 'completed'")
total_volume = cursor.fetchone()[0] or 0
conn.close()
return {
"swap_stats": swap_stats,
"bridge_stats": bridge_stats,
"total_volume": total_volume,
"supported_chains": list(SUPPORTED_CHAINS.keys()),
"timestamp": datetime.now().isoformat()
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
# Initialize cross-chain tables
if __name__ == "__main__":
init_cross_chain_tables()
print("✅ Cross-chain trading extensions initialized")

49
apps/exchange/database.py Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""
Database configuration for the AITBC Trade Exchange
"""
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import StaticPool
from models import Base
# Database configuration
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./exchange.db")
# Create engine
if DATABASE_URL.startswith("sqlite"):
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
echo=False # Set to True for SQL logging
)
else:
engine = create_engine(DATABASE_URL, echo=False)
# Create session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create tables
def init_db():
"""Initialize database tables"""
Base.metadata.create_all(bind=engine)
def get_db() -> Session:
"""Get database session"""
db = SessionLocal()
try:
yield db
finally:
db.close()
# Dependency for FastAPI
def get_db_session():
"""Get database session for FastAPI dependency"""
db = SessionLocal()
try:
return db
finally:
pass # Don't close here, let the caller handle it

View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Deploy Real AITBC Trade Exchange
echo "🚀 Deploying Real AITBC Trade Exchange..."
# Install Python dependencies
echo "📦 Installing Python dependencies..."
pip3 install -r requirements.txt
# Kill existing services
echo "🔄 Stopping existing services..."
pkill -f "server.py --port 3002" || true
pkill -f "exchange_api.py" || true
# Start the Exchange API server
echo "🔥 Starting Exchange API server on port 3003..."
nohup python3 exchange_api.py > exchange_api.log 2>&1 &
sleep 2
# Start the frontend with real trading
echo "🌐 Starting Exchange frontend with real trading..."
nohup python3 server.py --port 3002 > exchange_frontend.log 2>&1 &
sleep 2
# Check if services are running
echo "✅ Checking services..."
if pgrep -f "exchange_api.py" > /dev/null; then
echo "✓ Exchange API is running on port 3003"
else
echo "✗ Exchange API failed to start"
fi
if pgrep -f "server.py --port 3002" > /dev/null; then
echo "✓ Exchange frontend is running on port 3002"
else
echo "✗ Exchange frontend failed to start"
fi
echo ""
echo "🎉 Real Exchange Deployment Complete!"
echo ""
echo "📍 Access the exchange at:"
echo " Frontend: https://aitbc.bubuit.net/Exchange"
echo " API: http://localhost:3003"
echo ""
echo "📊 API Endpoints:"
echo " GET /api/trades/recent - Get recent trades"
echo " GET /api/orders/orderbook - Get order book"
echo " POST /api/orders - Place new order"
echo " GET /api/health - Health check"
echo ""
echo "📝 Logs:"
echo " API: tail -f exchange_api.log"
echo " Frontend: tail -f exchange_frontend.log"

54
apps/exchange/deploy_simple.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Deploy Simple Real AITBC Trade Exchange
echo "🚀 Deploying Simple Real AITBC Trade Exchange..."
# Kill existing services
echo "🔄 Stopping existing services..."
pkill -f "server.py --port 3002" || true
pkill -f "exchange_api.py" || true
pkill -f "simple_exchange_api.py" || true
# Start the Simple Exchange API server
echo "🔥 Starting Simple Exchange API server on port 3003..."
nohup python3 simple_exchange_api.py > simple_exchange_api.log 2>&1 &
sleep 2
# Replace the frontend with real trading version
echo "🌐 Updating frontend to use real trading..."
cp index.real.html index.html
# Start the frontend
echo "🌐 Starting Exchange frontend..."
nohup python3 server.py --port 3002 > exchange_frontend.log 2>&1 &
sleep 2
# Check if services are running
echo "✅ Checking services..."
if pgrep -f "simple_exchange_api.py" > /dev/null; then
echo "✓ Simple Exchange API is running on port 3003"
else
echo "✗ Simple Exchange API failed to start"
echo " Check log: tail -f simple_exchange_api.log"
fi
if pgrep -f "server.py --port 3002" > /dev/null; then
echo "✓ Exchange frontend is running on port 3002"
else
echo "✗ Exchange frontend failed to start"
fi
echo ""
echo "🎉 Simple Real Exchange Deployment Complete!"
echo ""
echo "📍 Access the exchange at:"
echo " https://aitbc.bubuit.net/Exchange"
echo ""
echo "📊 The exchange now shows REAL trades from the database!"
echo " - Recent trades are loaded from the database"
echo " - Order book shows live orders"
echo " - You can place real buy/sell orders"
echo ""
echo "📝 Logs:"
echo " API: tail -f simple_exchange_api.log"
echo " Frontend: tail -f exchange_frontend.log"

View File

@@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""
FastAPI backend for the AITBC Trade Exchange
"""
from datetime import datetime, timedelta
from typing import List, Optional
from fastapi import FastAPI, Depends, HTTPException, status, Header
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from sqlalchemy import desc, func, and_
from sqlalchemy.orm import Session
import hashlib
import time
from typing import Annotated
from database import init_db, get_db_session
from models import User, Order, Trade, Balance
# Initialize FastAPI app
app = FastAPI(title="AITBC Trade Exchange API", version="1.0.0")
# In-memory session storage (use Redis in production)
user_sessions = {}
def verify_session_token(token: str = Header(..., alias="Authorization")) -> int:
"""Verify session token and return user_id"""
# Remove "Bearer " prefix if present
if token.startswith("Bearer "):
token = token[7:]
if token not in user_sessions:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
session = user_sessions[token]
# Check if expired
if int(time.time()) > session["expires_at"]:
del user_sessions[token]
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired"
)
return session["user_id"]
def optional_auth(token: Optional[str] = Header(None, alias="Authorization")) -> Optional[int]:
"""Optional authentication - returns user_id if token is valid, None otherwise"""
if not token:
return None
try:
return verify_session_token(token)
except HTTPException:
return None
# Type annotations for dependencies
UserDep = Annotated[int, Depends(verify_session_token)]
OptionalUserDep = Annotated[Optional[int], Depends(optional_auth)]
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:8080",
"http://localhost:8000",
"http://localhost:3003"
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"], # Allow all headers for auth tokens
)
# Pydantic models
class OrderCreate(BaseModel):
order_type: str # 'BUY' or 'SELL'
amount: float
price: float
class OrderResponse(BaseModel):
id: int
order_type: str
amount: float
price: float
total: float
filled: float
remaining: float
status: str
created_at: datetime
class Config:
from_attributes = True
class TradeResponse(BaseModel):
id: int
amount: float
price: float
total: float
created_at: datetime
class Config:
from_attributes = True
class OrderBookResponse(BaseModel):
buys: List[OrderResponse]
sells: List[OrderResponse]
# Initialize database on startup
@app.on_event("startup")
async def startup_event():
init_db()
# Create mock data if database is empty
db = get_db_session()
try:
# Check if we have any trades
if db.query(Trade).count() == 0:
create_mock_trades(db)
finally:
db.close()
def create_mock_trades(db: Session):
"""Create some mock trades for demonstration"""
import random
# Create mock trades over the last hour
now = datetime.utcnow()
trades = []
for i in range(20):
# Generate random trade data
amount = random.uniform(10, 500)
price = random.uniform(0.000009, 0.000012)
total = amount * price
trade = Trade(
buyer_id=1, # Mock user ID
seller_id=2, # Mock user ID
order_id=1, # Mock order ID
amount=amount,
price=price,
total=total,
trade_hash=f"mock_tx_{i:04d}",
created_at=now - timedelta(minutes=random.randint(0, 60))
)
trades.append(trade)
db.add_all(trades)
db.commit()
print(f"Created {len(trades)} mock trades")
@app.get("/api/trades/recent", response_model=List[TradeResponse])
def get_recent_trades(limit: int = 20, db: Session = Depends(get_db_session)):
"""Get recent trades"""
trades = db.query(Trade).order_by(desc(Trade.created_at)).limit(limit).all()
return trades
@app.get("/api/orders", response_model=List[OrderResponse])
def get_orders(
status_filter: Optional[str] = None,
user_only: bool = False,
db: Session = Depends(get_db_session),
user_id: OptionalUserDep = None
):
"""Get all orders with optional status filter"""
query = db.query(Order)
# Filter by user if requested and authenticated
if user_only and user_id:
query = query.filter(Order.user_id == user_id)
if status_filter:
query = query.filter(Order.status == status_filter.upper())
orders = query.order_by(Order.created_at.desc()).all()
return orders
@app.get("/api/my/orders", response_model=List[OrderResponse])
def get_my_orders(
user_id: UserDep,
status_filter: Optional[str] = None,
db: Session = Depends(get_db_session)
):
"""Get current user's orders"""
query = db.query(Order).filter(Order.user_id == user_id)
if status_filter:
query = query.filter(Order.status == status_filter.upper())
orders = query.order_by(Order.created_at.desc()).all()
return orders
@app.get("/api/orders/orderbook", response_model=OrderBookResponse)
def get_orderbook(db: Session = Depends(get_db_session)):
"""Get current order book"""
# Get open buy orders (sorted by price descending)
buys = db.query(Order).filter(
and_(Order.order_type == 'BUY', Order.status == 'OPEN')
).order_by(desc(Order.price)).limit(20).all()
# Get open sell orders (sorted by price ascending)
sells = db.query(Order).filter(
and_(Order.order_type == 'SELL', Order.status == 'OPEN')
).order_by(Order.price).limit(20).all()
return OrderBookResponse(buys=buys, sells=sells)
@app.post("/api/orders", response_model=OrderResponse)
def create_order(
order: OrderCreate,
db: Session = Depends(get_db_session),
user_id: UserDep
):
"""Create a new order"""
# Validate order type
if order.order_type not in ['BUY', 'SELL']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Order type must be 'BUY' or 'SELL'"
)
# Create order
total = order.amount * order.price
db_order = Order(
user_id=user_id, # Use authenticated user_id
order_type=order.order_type,
amount=order.amount,
price=order.price,
total=total,
remaining=order.amount
)
db.add(db_order)
db.commit()
db.refresh(db_order)
# Try to match the order
try_match_order(db_order, db)
return db_order
def try_match_order(order: Order, db: Session):
"""Try to match an order with existing orders"""
if order.order_type == 'BUY':
# Match with sell orders at same or lower price
matching_orders = db.query(Order).filter(
and_(
Order.order_type == 'SELL',
Order.status == 'OPEN',
Order.price <= order.price
)
).order_by(Order.price).all()
else:
# Match with buy orders at same or higher price
matching_orders = db.query(Order).filter(
and_(
Order.order_type == 'BUY',
Order.status == 'OPEN',
Order.price >= order.price
)
).order_by(desc(Order.price)).all()
for match in matching_orders:
if order.remaining <= 0:
break
# Calculate trade amount
trade_amount = min(order.remaining, match.remaining)
trade_total = trade_amount * match.price
# Create trade record
trade = Trade(
buyer_id=order.user_id if order.order_type == 'BUY' else match.user_id,
seller_id=match.user_id if order.order_type == 'BUY' else order.user_id,
order_id=order.id,
amount=trade_amount,
price=match.price,
total=trade_total,
trade_hash=f"trade_{datetime.utcnow().timestamp()}"
)
db.add(trade)
# Update orders
order.filled += trade_amount
order.remaining -= trade_amount
match.filled += trade_amount
match.remaining -= trade_amount
# Update order statuses
if order.remaining <= 0:
order.status = 'FILLED'
else:
order.status = 'PARTIALLY_FILLED'
if match.remaining <= 0:
match.status = 'FILLED'
else:
match.status = 'PARTIALLY_FILLED'
db.commit()
@app.post("/api/auth/login")
def login_user(wallet_address: str, db: Session = Depends(get_db_session)):
"""Login with wallet address"""
# Find or create user
user = db.query(User).filter(User.wallet_address == wallet_address).first()
if not user:
user = User(
wallet_address=wallet_address,
email=f"{wallet_address}@aitbc.local",
is_active=True
)
db.add(user)
db.commit()
db.refresh(user)
# Create session token
token_data = f"{user.id}:{int(time.time())}"
token = hashlib.sha256(token_data.encode()).hexdigest()
# Store session
user_sessions[token] = {
"user_id": user.id,
"created_at": int(time.time()),
"expires_at": int(time.time()) + 86400 # 24 hours
}
return {"token": token, "user_id": user.id}
@app.post("/api/auth/logout")
def logout_user(token: str = Header(..., alias="Authorization")):
"""Logout user"""
if token.startswith("Bearer "):
token = token[7:]
if token in user_sessions:
del user_sessions[token]
return {"message": "Logged out successfully"}
@app.get("/api/health")
def health_check():
"""Health check endpoint"""
return {"status": "ok", "timestamp": datetime.utcnow()}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3003)

View File

@@ -0,0 +1,6 @@
#!/bin/bash
# AITBC Exchange Service Wrapper Script
# This script handles the systemd service startup properly
cd /opt/aitbc/apps/exchange
exec /usr/bin/python3 simple_exchange_api.py

1250
apps/exchange/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,620 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITBC Trade Exchange - Buy AITBC with Bitcoin</title>
<base href="/Exchange/">
<!-- Production: Use local assets -->
<script src="/assets/js/tailwind.js"></script>
<script>
tailwind.config = {
darkMode: 'class'
}
</script>
<script src="/assets/js/axios.min.js"></script>
<script src="/assets/js/lucide.js"></script>
<style>
.gradient-bg {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.pulse-animation {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
</head>
<body class="bg-gray-50 dark:bg-gray-900 transition-colors duration-300">
<!-- Header -->
<header class="gradient-bg text-white shadow-lg">
<div class="container mx-auto px-4 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<i data-lucide="trending-up" class="w-8 h-8"></i>
<h1 class="text-2xl font-bold">AITBC Trade Exchange</h1>
</div>
<nav class="flex items-center space-x-6">
<button onclick="showSection('trade')" class="hover:text-orange-200 transition">Trade</button>
<button onclick="showSection('marketplace')" class="hover:text-orange-200 transition">Marketplace</button>
<button onclick="showSection('wallet')" class="hover:text-orange-200 transition">Wallet</button>
<button onclick="toggleDarkMode()" class="hover:text-orange-200 transition" title="Toggle dark mode">
<i data-lucide="moon" class="w-5 h-5" id="darkModeIcon"></i>
</button>
<button id="navConnectBtn" onclick="connectWallet()" class="bg-white text-orange-600 px-4 py-2 rounded-lg hover:bg-orange-100 transition">
<i data-lucide="wallet" class="w-4 h-4 inline mr-2"></i>Connect Wallet
</button>
<div id="navUserInfo" class="hidden flex items-center space-x-3">
<span class="text-sm" id="navUsername">-</span>
<button onclick="showSection('wallet')" class="text-white hover:text-orange-200 transition">
<i data-lucide="user" class="w-5 h-5"></i>
</button>
<button onclick="logout()" class="text-white hover:text-orange-200 transition">
<i data-lucide="log-out" class="w-5 h-5"></i>
</button>
</div>
</nav>
</div>
</div>
</header>
<!-- Price Ticker -->
<section class="bg-white dark:bg-gray-800 border-b dark:border-gray-700">
<div class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-6">
<div class="flex items-center space-x-2">
<span class="text-gray-600 dark:text-gray-400">AITBC/BTC:</span>
<span class="text-2xl font-bold text-green-600 dark:text-green-400" id="aitbcBtcPrice">0.00001</span>
<span class="text-sm text-green-500 dark:text-green-400">+5.2%</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-gray-600 dark:text-gray-400">24h Volume:</span>
<span class="font-semibold text-gray-900 dark:text-white">1,234 AITBC</span>
</div>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
Last updated: <span id="lastUpdated">Just now</span>
</div>
</div>
</div>
</section>
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<!-- Trade Section -->
<section id="tradeSection" class="section">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Buy AITBC -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 flex items-center text-gray-900 dark:text-white">
<i data-lucide="arrow-down-left" class="w-5 h-5 mr-2 text-green-600 dark:text-green-400"></i>
Buy AITBC with Bitcoin
</h2>
<div id="tradeConnectPrompt" class="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-4">
<p class="text-sm text-orange-800 dark:text-orange-200 mb-3">
<i data-lucide="wallet" class="w-4 h-4 inline mr-1"></i>
Connect your wallet to start trading
</p>
<button onclick="connectWallet()" class="bg-orange-600 text-white px-4 py-2 rounded-lg hover:bg-orange-700 transition">
<i data-lucide="wallet" class="w-4 h-4 inline mr-2"></i>Connect Wallet
</button>
</div>
<div id="tradeForm" class="hidden">
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-800 dark:text-blue-200">
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
Send Bitcoin to the generated address. Your AITBC will be credited after 1 confirmation.
</p>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Pay with Bitcoin</label>
<div class="relative">
<input type="number" id="btcAmount" class="w-full border dark:border-gray-600 rounded-lg px-4 py-3 pr-12 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" placeholder="0.001" step="0.00001">
<span class="absolute right-3 top-3 text-gray-500 dark:text-gray-400">BTC</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Available: 0.12345 BTC</p>
</div>
<div class="flex justify-center">
<button onclick="swapCurrencies()" class="p-2 bg-gray-100 dark:bg-gray-700 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition">
<i data-lucide="arrow-up-down" class="w-5 h-5 text-gray-700 dark:text-gray-300"></i>
</button>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">You will receive</label>
<div class="relative">
<input type="number" id="aitbcAmount" class="w-full border dark:border-gray-600 rounded-lg px-4 py-3 pr-16 bg-white dark:bg-gray-700 text-gray-900 dark:text-white" placeholder="100" step="0.01">
<span class="absolute right-3 top-3 text-gray-500 dark:text-gray-400">AITBC</span>
</div>
</div>
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex justify-between text-sm mb-2 text-gray-700 dark:text-gray-300">
<span>Price</span>
<span>0.00001 BTC/AITBC</span>
</div>
<div class="flex justify-between text-sm mb-2 text-gray-700 dark:text-gray-300">
<span>Fee (0.5%)</span>
<span>0.000005 BTC</span>
</div>
<div class="flex justify-between text-sm font-semibold text-gray-900 dark:text-white">
<span>Total</span>
<span>0.001005 BTC</span>
</div>
</div>
<button onclick="createPaymentRequest()" class="w-full bg-green-600 text-white py-3 rounded-lg hover:bg-green-700 transition font-semibold">
Create Payment Request
</button>
</div>
</div>
<!-- Order Book -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 class="text-xl font-bold mb-6 flex items-center text-gray-900 dark:text-white">
<i data-lucide="book-open" class="w-5 h-5 mr-2 text-blue-600 dark:text-blue-400"></i>
Order Book
</h2>
<div class="space-y-4">
<div class="border-b dark:border-gray-700 pb-2">
<h3 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Buy Orders</h3>
<div class="space-y-1">
<div class="flex justify-between text-sm">
<span class="text-green-600 dark:text-green-400">100 AITBC</span>
<span class="text-gray-700 dark:text-gray-300">0.001 BTC</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-green-600 dark:text-green-400">50 AITBC</span>
<span class="text-gray-700 dark:text-gray-300">0.0005 BTC</span>
</div>
</div>
</div>
<div>
<h3 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Sell Orders</h3>
<div class="space-y-1">
<div class="flex justify-between text-sm">
<span class="text-red-600 dark:text-red-400">200 AITBC</span>
<span class="text-gray-700 dark:text-gray-300">0.002 BTC</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-red-600 dark:text-red-400">150 AITBC</span>
<span class="text-gray-700 dark:text-gray-300">0.0015 BTC</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Marketplace Section -->
<section id="marketplaceSection" class="section hidden">
<h2 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">GPU Marketplace</h2>
<div id="gpuOffers" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- GPU offers will be loaded here -->
</div>
</section>
<!-- Wallet Section -->
<section id="walletSection" class="section hidden">
<h2 class="text-2xl font-bold mb-6 text-gray-900 dark:text-white">My Wallet</h2>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Wallet Information</h3>
<div class="space-y-3">
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">Address:</span>
<p class="font-mono text-sm bg-gray-100 dark:bg-gray-700 p-2 rounded" id="walletAddress">-</p>
</div>
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">Username:</span>
<p id="walletUsername">-</p>
</div>
</div>
</div>
<div>
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Balances</h3>
<div class="space-y-3">
<div>
<span class="text-sm text-gray-600 dark:text-gray-400">AITBC Balance:</span>
<p class="text-2xl font-bold text-green-600 dark:text-green-400" id="aitbcBalance">0 AITBC</p>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- QR Code Modal -->
<div id="qrModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
<h3 class="text-lg font-bold mb-4 text-gray-900 dark:text-white">Send Bitcoin to this Address</h3>
<div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-4">
<div id="qrCode" class="w-64 h-64 mx-auto mb-4 bg-white rounded"></div>
<p class="font-mono text-sm break-all" id="paymentAddress">-</p>
</div>
<div class="grid grid-cols-2 gap-4 mb-4 text-sm">
<div>
<p class="text-gray-600 dark:text-gray-400">Amount to Send:</p>
<p class="font-semibold" id="paymentAmount">0 BTC</p>
</div>
<div>
<p class="text-gray-600 dark:text-gray-400">You'll Receive:</p>
<p class="font-semibold text-green-600" id="receiveAmount">0 AITBC</p>
</div>
</div>
<div class="flex items-center justify-center mb-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600" id="paymentSpinner"></div>
<span class="ml-2 text-sm text-gray-600">Waiting for payment...</span>
</div>
<div class="flex space-x-3">
<button onclick="closeQRModal()" class="flex-1 bg-gray-200 text-gray-800 py-2 rounded-lg hover:bg-gray-300 transition">
Cancel
</button>
<button onclick="checkPaymentStatus()" class="flex-1 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">
Check Payment
</button>
</div>
</div>
</div>
<script>
// API Configuration
const API_BASE = window.location.origin + '/api';
const BLOCKCHAIN_API = window.location.origin + '/rpc';
const EXCHANGE_RATE = 0.00001; // 1 AITBC = 0.00001 BTC
let walletAddress = null;
let currentUser = null;
let sessionToken = null;
let aitbcBalance = 0;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
updatePrices();
loadGPUOffers();
// Auto-refresh prices every 30 seconds
setInterval(updatePrices, 30000);
// Input handlers
document.getElementById('btcAmount').addEventListener('input', updateAITBCAmount);
document.getElementById('aitbcAmount').addEventListener('input', updateBTCAmount);
// Check for saved dark mode preference
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
updateDarkModeIcon(true);
}
});
// Dark mode toggle
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', isDark);
updateDarkModeIcon(isDark);
}
function updateDarkModeIcon(isDark) {
const icon = document.getElementById('darkModeIcon');
if (isDark) {
icon.setAttribute('data-lucide', 'sun');
} else {
icon.setAttribute('data-lucide', 'moon');
}
lucide.createIcons();
}
// Section Navigation
function showSection(section) {
document.querySelectorAll('.section').forEach(s => s.classList.add('hidden'));
document.getElementById(section + 'Section').classList.remove('hidden');
if (section === 'marketplace') {
loadGPUOffers();
}
}
// Connect Wallet
async function connectWallet() {
try {
// Generate a random wallet address for demo
walletAddress = 'aitbc1' + Array(39).fill(0).map(() => Math.random().toString(36).substr(2, 1)).join('');
// Login or register via API
const response = await axios.post(`${API_BASE}/users/login`, {
wallet_address: walletAddress
});
currentUser = response.data;
sessionToken = response.data.session_token;
// Update UI
document.getElementById('tradeConnectPrompt').classList.add('hidden');
document.getElementById('tradeForm').classList.remove('hidden');
document.getElementById('navConnectBtn').classList.add('hidden');
document.getElementById('navUserInfo').classList.remove('hidden');
document.getElementById('navUsername').textContent = currentUser.username;
document.getElementById('walletAddress').textContent = walletAddress;
document.getElementById('walletUsername').textContent = currentUser.username;
// Load wallet balance
await loadWalletBalance();
showToast('Wallet connected successfully!', 'success');
} catch (error) {
console.error('Failed to connect wallet:', error);
showToast('Failed to connect wallet', 'error');
}
}
// Load Wallet Balance
async function loadWalletBalance() {
try {
const response = await axios.get(
`${API_BASE}/users/${currentUser.user_id}/balance`,
{ headers: { 'Authorization': `Bearer ${sessionToken}` } }
);
aitbcBalance = response.data.balance || 0;
document.getElementById('aitbcBalance').textContent = aitbcBalance + ' AITBC';
} catch (error) {
console.error('Failed to load balance:', error);
}
}
// Logout
async function logout() {
try {
if (sessionToken) {
await axios.post(`${API_BASE}/users/logout`, {}, {
headers: { 'Authorization': `Bearer ${sessionToken}` }
});
}
} catch (error) {
console.error('Logout error:', error);
}
// Reset state
walletAddress = null;
currentUser = null;
sessionToken = null;
aitbcBalance = 0;
// Update UI
document.getElementById('tradeConnectPrompt').classList.remove('hidden');
document.getElementById('tradeForm').classList.add('hidden');
document.getElementById('navConnectBtn').classList.remove('hidden');
document.getElementById('navUserInfo').classList.add('hidden');
showToast('Logged out successfully', 'success');
}
// Update Prices
function updatePrices() {
// Simulate price updates
const variation = (Math.random() - 0.5) * 0.000001;
const newPrice = EXCHANGE_RATE + variation;
document.getElementById('aitbcBtcPrice').textContent = newPrice.toFixed(5);
document.getElementById('lastUpdated').textContent = 'Just now';
}
// Currency Conversion
function updateAITBCAmount() {
const btcAmount = parseFloat(document.getElementById('btcAmount').value) || 0;
const aitbcAmount = btcAmount / EXCHANGE_RATE;
document.getElementById('aitbcAmount').value = aitbcAmount.toFixed(2);
}
function updateBTCAmount() {
const aitbcAmount = parseFloat(document.getElementById('aitbcAmount').value) || 0;
const btcAmount = aitbcAmount * EXCHANGE_RATE;
document.getElementById('btcAmount').value = btcAmount.toFixed(5);
}
function swapCurrencies() {
const btcInput = document.getElementById('btcAmount');
const aitbcInput = document.getElementById('aitbcAmount');
const temp = btcInput.value;
btcInput.value = aitbcInput.value;
aitbcInput.value = temp;
}
// Create Payment Request
async function createPaymentRequest() {
const btcAmount = document.getElementById('btcAmount').value;
if (!btcAmount || btcAmount <= 0) {
showToast('Please enter a valid amount', 'error');
return;
}
try {
const response = await axios.post(`${API_BASE}/exchange/create-payment`, {
amount: parseFloat(btcAmount),
currency: 'BTC'
}, {
headers: { 'Authorization': `Bearer ${sessionToken}` }
});
const payment = response.data;
showQRModal(payment);
} catch (error) {
console.error('Failed to create payment:', error);
showToast('Failed to create payment request', 'error');
}
}
// Show QR Modal
function showQRModal(payment) {
const modal = document.getElementById('qrModal');
const addressEl = document.getElementById('paymentAddress');
const amountEl = document.getElementById('paymentAmount');
const receiveEl = document.getElementById('receiveAmount');
addressEl.textContent = payment.address;
amountEl.textContent = payment.amount + ' BTC';
receiveEl.textContent = (payment.amount / EXCHANGE_RATE).toFixed(2) + ' AITBC';
// Generate QR code (simplified - in production use a proper QR library)
const qrDiv = document.getElementById('qrCode');
qrDiv.innerHTML = `
<div class="w-full h-full flex items-center justify-center border-2 border-gray-300 rounded">
<div class="text-center">
<i data-lucide="qr-code" class="w-32 h-32 mx-auto mb-2"></i>
<p class="text-sm">QR Code for ${payment.address}</p>
</div>
</div>
`;
lucide.createIcons();
modal.classList.remove('hidden');
window.currentPaymentId = payment.payment_id;
}
// Close QR Modal
function closeQRModal() {
document.getElementById('qrModal').classList.add('hidden');
window.currentPaymentId = null;
}
// Check Payment Status
async function checkPaymentStatus() {
if (!window.currentPaymentId) return;
try {
const response = await axios.get(
`${API_BASE}/exchange/payment-status/${window.currentPaymentId}`,
{ headers: { 'Authorization': `Bearer ${sessionToken}` } }
);
const status = response.data.status;
if (status === 'completed') {
showToast('Payment received! AITBC credited to your wallet.', 'success');
closeQRModal();
await loadWalletBalance();
} else if (status === 'pending') {
showToast('Payment still pending...', 'info');
} else {
showToast('Payment not found', 'error');
}
} catch (error) {
console.error('Failed to check payment:', error);
showToast('Failed to check payment status', 'error');
}
}
// Load GPU Offers
async function loadGPUOffers() {
try {
const response = await axios.get(`${API_BASE}/marketplace/offers`);
displayGPUOffers(response.data);
} catch (error) {
console.error('Failed to load GPU offers:', error);
// Display demo offers
displayGPUOffers([
{
id: 'demo-1',
provider: 'Demo Provider 1',
capacity: 'RTX 4090',
price: 0.01,
status: 'available'
},
{
id: 'demo-2',
provider: 'Demo Provider 2',
capacity: 'A100 80GB',
price: 0.05,
status: 'available'
}
]);
}
}
// Display GPU Offers
function displayGPUOffers(offers) {
const container = document.getElementById('gpuOffers');
if (offers.length === 0) {
container.innerHTML = '<p class="text-gray-500 dark:text-gray-400">No GPU offers available at the moment.</p>';
return;
}
container.innerHTML = offers.map(offer => `
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 card-hover">
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-semibold text-blue-600 dark:text-blue-400">${offer.capacity}</span>
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
${offer.status || 'Available'}
</span>
</div>
<h3 class="font-semibold mb-2 text-gray-900 dark:text-white">${offer.provider}</h3>
<p class="text-2xl font-bold text-gray-900 dark:text-white mb-4">${offer.price} BTC/hour</p>
<button onclick="rentGPU('${offer.id}')" class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">
Rent Now
</button>
</div>
`).join('');
}
// Rent GPU
function rentGPU(gpuId) {
if (!currentUser) {
showToast('Please connect your wallet first', 'error');
showSection('trade');
return;
}
showToast(`Renting GPU ${gpuId}...`, 'info');
}
// Toast Notification
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg transform transition-all duration-300 z-50`;
if (type === 'success') {
toast.classList.add('bg-green-500', 'text-white');
} else if (type === 'error') {
toast.classList.add('bg-red-500', 'text-white');
} else {
toast.classList.add('bg-blue-500', 'text-white');
}
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('translate-y-full', 'opacity-0');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,410 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITBC Trade Exchange - Buy & Sell AITBC</title>
<link rel="stylesheet" href="./styles.css">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="h-full bg-gray-50 dark:bg-gray-900">
<div class="min-h-full">
<!-- Navigation -->
<nav class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">AITBC Exchange</h1>
</div>
<div class="hidden sm:ml-8 sm:flex sm:space-x-8">
<a href="#" class="border-primary-500 text-gray-900 dark:text-white inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
Trade
</a>
<a href="#" class="border-transparent text-gray-500 dark:text-gray-300 hover:border-gray-300 hover:text-gray-700 dark:hover:text-gray-200 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
Orders
</a>
<a href="#" class="border-transparent text-gray-500 dark:text-gray-300 hover:border-gray-300 hover:text-gray-700 dark:hover:text-gray-200 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
History
</a>
</div>
</div>
<div class="flex items-center space-x-4">
<button onclick="toggleDarkMode()" class="p-2 rounded-md text-gray-500 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-200">
<i data-lucide="moon" id="darkModeIcon" class="h-5 w-5"></i>
</button>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-700 dark:text-gray-300">Balance:</span>
<span class="text-sm font-medium text-gray-900 dark:text-white" id="userBalance">0 BTC | 0 AITBC</span>
</div>
<button class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Connect Wallet
</button>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Price Ticker -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Current Price</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="currentPrice">0.000010 BTC</p>
<p class="text-sm text-green-600" id="priceChange">+5.2%</p>
</div>
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">24h Volume</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="volume24h">12,345 AITBC</p>
<p class="text-sm text-gray-500 dark:text-gray-400">≈ 0.12345 BTC</p>
</div>
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">24h High / Low</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white" id="highLow">0.000011 / 0.000009</p>
<p class="text-sm text-gray-500 dark:text-gray-400">BTC</p>
</div>
</div>
</div>
<!-- Trading Interface -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Order Book -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="p-4 border-b dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Order Book</h2>
</div>
<div class="p-4">
<div class="space-y-2">
<div class="grid grid-cols-3 text-xs font-medium text-gray-500 dark:text-gray-400 pb-2">
<span>Price (BTC)</span>
<span class="text-right">Amount (AITBC)</span>
<span class="text-right">Total (BTC)</span>
</div>
<!-- Sell Orders -->
<div id="sellOrders" class="space-y-1">
<!-- Dynamically populated -->
</div>
<div class="border-t dark:border-gray-700 my-2"></div>
<!-- Buy Orders -->
<div id="buyOrders" class="space-y-1">
<!-- Dynamically populated -->
</div>
</div>
</div>
</div>
<!-- Trade Form -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="p-4 border-b dark:border-gray-700">
<div class="flex space-x-4">
<button onclick="setTradeType('BUY')" id="buyTab" class="flex-1 py-2 px-4 text-center font-medium text-white bg-green-600 rounded-md">
Buy AITBC
</button>
<button onclick="setTradeType('SELL')" id="sellTab" class="flex-1 py-2 px-4 text-center font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-md">
Sell AITBC
</button>
</div>
</div>
<div class="p-4">
<form id="tradeForm" onsubmit="placeOrder(event)">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Price (BTC)</label>
<input type="number" id="orderPrice" step="0.000001" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500" value="0.000010">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Amount (AITBC)</label>
<input type="number" id="orderAmount" step="0.01" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm focus:border-primary-500 focus:ring-primary-500" placeholder="0.00">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Total (BTC)</label>
<input type="number" id="orderTotal" step="0.000001" readonly class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white bg-gray-50 dark:bg-gray-600" placeholder="0.00">
</div>
<button type="submit" id="submitOrder" class="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md">
Place Buy Order
</button>
</div>
</form>
</div>
</div>
<!-- Recent Trades -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="p-4 border-b dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Trades</h2>
</div>
<div class="p-4">
<div class="space-y-2">
<div class="grid grid-cols-3 text-xs font-medium text-gray-500 dark:text-gray-400 pb-2">
<span>Price (BTC)</span>
<span class="text-right">Amount</span>
<span class="text-right">Time</span>
</div>
<div id="recentTrades" class="space-y-1">
<!-- Dynamically populated -->
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
// API Configuration
const API_BASE = window.location.origin + '/api';
const EXCHANGE_API_BASE = window.location.origin; // Use same domain with nginx routing
let tradeType = 'BUY';
// Initialize
document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
// Load initial data
loadRecentTrades();
loadOrderBook();
updatePriceTicker();
// Auto-refresh every 5 seconds
setInterval(() => {
loadRecentTrades();
loadOrderBook();
updatePriceTicker();
}, 5000);
// Input handlers
document.getElementById('orderAmount').addEventListener('input', updateOrderTotal);
document.getElementById('orderPrice').addEventListener('input', updateOrderTotal);
// Check for saved dark mode preference
if (localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
updateDarkModeIcon(true);
}
});
// Dark mode toggle
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', isDark);
updateDarkModeIcon(isDark);
}
function updateDarkModeIcon(isDark) {
const icon = document.getElementById('darkModeIcon');
if (isDark) {
icon.setAttribute('data-lucide', 'sun');
} else {
icon.setAttribute('data-lucide', 'moon');
}
lucide.createIcons();
}
// Update price ticker with real data
async function updatePriceTicker() {
try {
// Get recent trades to calculate price statistics
const response = await fetch(`${EXCHANGE_API_BASE}/api/trades/recent?limit=100`);
if (!response.ok) return;
const trades = await response.json();
if (trades.length === 0) {
console.log('No trades to calculate price from');
return;
}
// Calculate 24h volume (sum of all trades in last 24h)
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const recentTrades = trades.filter(trade =>
new Date(trade.created_at) > yesterday
);
const totalVolume = recentTrades.reduce((sum, trade) => sum + trade.amount, 0);
const totalBTC = recentTrades.reduce((sum, trade) => sum + trade.total, 0);
// Calculate current price (price of last trade)
const currentPrice = trades[0].price;
// Calculate 24h high/low
const prices = recentTrades.map(t => t.price);
const high24h = Math.max(...prices);
const low24h = Math.min(...prices);
// Calculate price change (compare with price 24h ago)
const price24hAgo = trades[trades.length - 1]?.price || currentPrice;
const priceChange = ((currentPrice - price24hAgo) / price24hAgo) * 100;
// Update UI
document.getElementById('currentPrice').textContent = `${currentPrice.toFixed(6)} BTC`;
document.getElementById('volume24h').textContent = `${totalVolume.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")} AITBC`;
document.getElementById('volume24h').nextElementSibling.textContent = `${totalBTC.toFixed(5)} BTC`;
document.getElementById('highLow').textContent = `${high24h.toFixed(6)} / ${low24h.toFixed(6)}`;
// Update price change with color
const changeElement = document.getElementById('priceChange');
changeElement.textContent = `${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%`;
changeElement.className = `text-sm ${priceChange >= 0 ? 'text-green-600' : 'text-red-600'}`;
} catch (error) {
console.error('Failed to update price ticker:', error);
}
}
// Trade type
function setTradeType(type) {
tradeType = type;
const buyTab = document.getElementById('buyTab');
const sellTab = document.getElementById('sellTab');
const submitBtn = document.getElementById('submitOrder');
if (type === 'BUY') {
buyTab.className = 'flex-1 py-2 px-4 text-center font-medium text-white bg-green-600 rounded-md';
sellTab.className = 'flex-1 py-2 px-4 text-center font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-md';
submitBtn.className = 'w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md';
submitBtn.textContent = 'Place Buy Order';
} else {
sellTab.className = 'flex-1 py-2 px-4 text-center font-medium text-white bg-red-600 rounded-md';
buyTab.className = 'flex-1 py-2 px-4 text-center font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-md';
submitBtn.className = 'w-full bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-md';
submitBtn.textContent = 'Place Sell Order';
}
}
// Order calculations
function updateOrderTotal() {
const price = parseFloat(document.getElementById('orderPrice').value) || 0;
const amount = parseFloat(document.getElementById('orderAmount').value) || 0;
const total = price * amount;
document.getElementById('orderTotal').value = total.toFixed(8);
}
// Load recent trades
async function loadRecentTrades() {
try {
const response = await fetch(`${EXCHANGE_API_BASE}/api/trades/recent?limit=15`);
if (response.ok) {
const trades = await response.json();
displayRecentTrades(trades);
}
} catch (error) {
console.error('Failed to load recent trades:', error);
}
}
function displayRecentTrades(trades) {
const container = document.getElementById('recentTrades');
container.innerHTML = '';
trades.forEach(trade => {
const div = document.createElement('div');
div.className = 'flex justify-between text-sm';
const time = new Date(trade.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
const priceClass = trade.id % 2 === 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400';
div.innerHTML = `
<span class="${priceClass}">${trade.price.toFixed(6)}</span>
<span class="text-gray-600 dark:text-gray-400 text-right">${trade.amount.toFixed(2)}</span>
<span class="text-gray-500 dark:text-gray-400 text-right">${time}</span>
`;
container.appendChild(div);
});
}
// Load order book
async function loadOrderBook() {
try {
const response = await fetch(`${EXCHANGE_API_BASE}/api/orders/orderbook`);
if (response.ok) {
const orderbook = await response.json();
displayOrderBook(orderbook);
}
} catch (error) {
console.error('Failed to load order book:', error);
}
}
function displayOrderBook(orderbook) {
const sellContainer = document.getElementById('sellOrders');
const buyContainer = document.getElementById('buyOrders');
// Display sell orders (highest to lowest)
sellContainer.innerHTML = '';
orderbook.sells.slice(0, 8).reverse().forEach(order => {
const div = document.createElement('div');
div.className = 'flex justify-between text-sm';
div.innerHTML = `
<span class="text-red-600 dark:text-red-400">${order.price.toFixed(6)}</span>
<span class="text-gray-600 dark:text-gray-400 text-right">${order.remaining.toFixed(2)}</span>
<span class="text-gray-500 dark:text-gray-400 text-right">${(order.remaining * order.price).toFixed(4)}</span>
`;
sellContainer.appendChild(div);
});
// Display buy orders (highest to lowest)
buyContainer.innerHTML = '';
orderbook.buys.slice(0, 8).forEach(order => {
const div = document.createElement('div');
div.className = 'flex justify-between text-sm';
div.innerHTML = `
<span class="text-green-600 dark:text-green-400">${order.price.toFixed(6)}</span>
<span class="text-gray-600 dark:text-gray-400 text-right">${order.remaining.toFixed(2)}</span>
<span class="text-gray-500 dark:text-gray-400 text-right">${(order.remaining * order.price).toFixed(4)}</span>
`;
buyContainer.appendChild(div);
});
}
// Place order
async function placeOrder(event) {
event.preventDefault();
const price = parseFloat(document.getElementById('orderPrice').value);
const amount = parseFloat(document.getElementById('orderAmount').value);
if (!price || !amount) {
alert('Please enter valid price and amount');
return;
}
try {
const response = await fetch(`${EXCHANGE_API_BASE}/api/orders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
order_type: tradeType,
price: price,
amount: amount
})
});
if (response.ok) {
const order = await response.json();
alert(`${tradeType} order placed successfully! Order ID: ${order.id}`);
// Clear form
document.getElementById('orderAmount').value = '';
document.getElementById('orderTotal').value = '';
// Reload order book
loadOrderBook();
} else {
const error = await response.json();
alert(`Failed to place order: ${error.detail || 'Unknown error'}`);
}
} catch (error) {
console.error('Failed to place order:', error);
alert('Failed to place order. Please try again.');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,398 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITBC Trade Exchange - Buy & Sell AITBC</title>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* Production CSS for AITBC Trade Exchange */
/* Dark mode variables */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-tertiary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border-color: #e5e7eb;
--primary-50: #eff6ff;
--primary-500: #3b82f6;
--primary-600: #2563eb;
--primary-700: #1d4ed8;
}
.dark {
--bg-primary: #1f2937;
--bg-secondary: #111827;
--bg-tertiary: #374151;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-tertiary: #9ca3af;
--border-color: #4b5563;
}
/* Base styles */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--bg-secondary);
color: var(--text-primary);
margin: 0;
padding: 0;
}
/* Layout */
.h-full {
height: 100%;
}
.min-h-full {
min-height: 100%;
}
.max-w-7xl {
max-width: 1280px;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
/* Navigation */
nav {
background-color: var(--bg-primary);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
nav > div {
display: flex;
justify-content: space-between;
height: 4rem;
align-items: center;
}
nav .flex {
display: flex;
}
nav .items-center {
align-items: center;
}
nav .space-x-8 > * + * {
margin-left: 2rem;
}
nav .space-x-4 > * + * {
margin-left: 1rem;
}
nav .text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
nav .font-bold {
font-weight: 700;
}
nav .text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
nav .font-medium {
font-weight: 500;
}
/* Links */
a {
color: inherit;
text-decoration: none;
}
a:hover {
color: var(--primary-600);
}
/* Cards */
.bg-white {
background-color: var(--bg-primary);
}
.dark .bg-white {
background-color: var(--bg-primary);
}
.rounded-lg {
border-radius: 0.5rem;
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
/* Grid */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gap-6 {
gap: 1.5rem;
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* Typography */
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.text-gray-600 {
color: var(--text-secondary);
}
.text-gray-900 {
color: var(--text-primary);
}
.text-gray-500 {
color: var(--text-tertiary);
}
.dark .text-gray-300 {
color: #d1d5db;
}
.dark .text-gray-400 {
color: #9ca3af;
}
.dark .text-white {
color: #ffffff;
}
/* Buttons */
button {
cursor: pointer;
border: none;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.15s ease-in-out;
}
.bg-primary-600 {
background-color: var(--primary-600);
}
.bg-primary-600:hover {
background-color: var(--primary-700);
}
.text-white {
color: #ffffff;
}
.bg-green-600 {
background-color: #059669;
}
.bg-green-600:hover {
background-color: #047857;
}
.bg-red-600 {
background-color: #dc2626;
}
.bg-red-600:hover {
background-color: #b91c1c;
}
.bg-gray-100 {
background-color: var(--bg-tertiary);
}
/* Forms */
input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background-color: var(--bg-primary);
color: var(--text-primary);
}
input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.dark input {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
}
.dark input:focus {
border-color: var(--primary-500);
}
/* Tables */
.space-y-2 > * + * {
margin-top: 0.5rem;
}
.space-y-1 > * + * {
margin-top: 0.25rem;
}
.justify-between {
justify-content: space-between;
}
.text-right {
text-align: right;
}
.text-green-600 {
color: #059669;
}
.text-red-600 {
color: #dc2626;
}
/* Borders */
.border-b {
border-bottom: 1px solid var(--border-color);
}
.border-t {
border-top: 1px solid var(--border-color);
}
/* Width */
.w-full {
width: 100%;
}
/* Flex */
.flex {
display: flex;
}
.flex-1 {
flex: 1 1 0%;
}
/* Colors */
.bg-gray-50 {
background-color: var(--bg-secondary);
}
.dark .bg-gray-600 {
background-color: #4b5563;
}
.dark .bg-gray-700 {
background-color: #374151;
}
/* Dark mode toggle */
.p-2 {
padding: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
/* Hover states */
.hover\:text-gray-700:hover {
color: var(--text-primary);
}
.dark .hover\:text-gray-200:hover {
color: #e5e7eb;
}
/* Order book colors */
.text-red-600 {
color: #dc2626;
}
.dark .text-red-400 {
color: #f87171;
}
.text-green-600 {
color: #059669;
}
.dark .text-green-400 {
color: #4ade80;
}
</style>
</head>

View File

@@ -0,0 +1,398 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AITBC Trade Exchange - Buy & Sell AITBC</title>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* Production CSS for AITBC Trade Exchange */
/* Dark mode variables */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-tertiary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border-color: #e5e7eb;
--primary-50: #eff6ff;
--primary-500: #3b82f6;
--primary-600: #2563eb;
--primary-700: #1d4ed8;
}
.dark {
--bg-primary: #1f2937;
--bg-secondary: #111827;
--bg-tertiary: #374151;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-tertiary: #9ca3af;
--border-color: #4b5563;
}
/* Base styles */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--bg-secondary);
color: var(--text-primary);
margin: 0;
padding: 0;
}
/* Layout */
.h-full {
height: 100%;
}
.min-h-full {
min-height: 100%;
}
.max-w-7xl {
max-width: 1280px;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
/* Navigation */
nav {
background-color: var(--bg-primary);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
nav > div {
display: flex;
justify-content: space-between;
height: 4rem;
align-items: center;
}
nav .flex {
display: flex;
}
nav .items-center {
align-items: center;
}
nav .space-x-8 > * + * {
margin-left: 2rem;
}
nav .space-x-4 > * + * {
margin-left: 1rem;
}
nav .text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
nav .font-bold {
font-weight: 700;
}
nav .text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
nav .font-medium {
font-weight: 500;
}
/* Links */
a {
color: inherit;
text-decoration: none;
}
a:hover {
color: var(--primary-600);
}
/* Cards */
.bg-white {
background-color: var(--bg-primary);
}
.dark .bg-white {
background-color: var(--bg-primary);
}
.rounded-lg {
border-radius: 0.5rem;
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
/* Grid */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gap-6 {
gap: 1.5rem;
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* Typography */
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.text-gray-600 {
color: var(--text-secondary);
}
.text-gray-900 {
color: var(--text-primary);
}
.text-gray-500 {
color: var(--text-tertiary);
}
.dark .text-gray-300 {
color: #d1d5db;
}
.dark .text-gray-400 {
color: #9ca3af;
}
.dark .text-white {
color: #ffffff;
}
/* Buttons */
button {
cursor: pointer;
border: none;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.15s ease-in-out;
}
.bg-primary-600 {
background-color: var(--primary-600);
}
.bg-primary-600:hover {
background-color: var(--primary-700);
}
.text-white {
color: #ffffff;
}
.bg-green-600 {
background-color: #059669;
}
.bg-green-600:hover {
background-color: #047857;
}
.bg-red-600 {
background-color: #dc2626;
}
.bg-red-600:hover {
background-color: #b91c1c;
}
.bg-gray-100 {
background-color: var(--bg-tertiary);
}
/* Forms */
input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background-color: var(--bg-primary);
color: var(--text-primary);
}
input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.dark input {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
}
.dark input:focus {
border-color: var(--primary-500);
}
/* Tables */
.space-y-2 > * + * {
margin-top: 0.5rem;
}
.space-y-1 > * + * {
margin-top: 0.25rem;
}
.justify-between {
justify-content: space-between;
}
.text-right {
text-align: right;
}
.text-green-600 {
color: #059669;
}
.text-red-600 {
color: #dc2626;
}
/* Borders */
.border-b {
border-bottom: 1px solid var(--border-color);
}
.border-t {
border-top: 1px solid var(--border-color);
}
/* Width */
.w-full {
width: 100%;
}
/* Flex */
.flex {
display: flex;
}
.flex-1 {
flex: 1 1 0%;
}
/* Colors */
.bg-gray-50 {
background-color: var(--bg-secondary);
}
.dark .bg-gray-600 {
background-color: #4b5563;
}
.dark .bg-gray-700 {
background-color: #374151;
}
/* Dark mode toggle */
.p-2 {
padding: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
/* Hover states */
.hover\:text-gray-700:hover {
color: var(--text-primary);
}
.dark .hover\:text-gray-200:hover {
color: #e5e7eb;
}
/* Order book colors */
.text-red-600 {
color: #dc2626;
}
.dark .text-red-400 {
color: #f87171;
}
.text-green-600 {
color: #059669;
}
.dark .text-green-400 {
color: #4ade80;
}
</style>
</head>

109
apps/exchange/models.py Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
Database models for the AITBC Trade Exchange
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, Index
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class User(Base):
"""User account for trading"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True, nullable=False)
password_hash = Column(String(255), nullable=False)
bitcoin_address = Column(String(100), unique=True, nullable=False)
aitbc_address = Column(String(100), unique=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
is_active = Column(Boolean, default=True)
# Relationships
orders = relationship("Order", back_populates="user")
trades = relationship("Trade", back_populates="buyer")
def __repr__(self):
return f"<User(username='{self.username}')>"
class Order(Base):
"""Trading order (buy or sell)"""
__tablename__ = "orders"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
order_type = Column(String(4), nullable=False) # 'BUY' or 'SELL'
amount = Column(Float, nullable=False) # Amount of AITBC
price = Column(Float, nullable=False) # Price in BTC
total = Column(Float, nullable=False) # Total in BTC (amount * price)
filled = Column(Float, default=0.0) # Amount filled
remaining = Column(Float, nullable=False) # Amount remaining to fill
status = Column(String(20), default='OPEN') # OPEN, PARTIALLY_FILLED, FILLED, CANCELLED
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="orders")
trades = relationship("Trade", back_populates="order")
__table_args__ = (
Index('idx_order_type_status', 'order_type', 'status'),
Index('idx_price_status', 'price', 'status'),
)
def __repr__(self):
return f"<Order(type='{self.order_type}', amount={self.amount}, price={self.price})>"
class Trade(Base):
"""Completed trade record"""
__tablename__ = "trades"
id = Column(Integer, primary_key=True, index=True)
buyer_id = Column(Integer, ForeignKey("users.id"), nullable=False)
seller_id = Column(Integer, ForeignKey("users.id"), nullable=False)
order_id = Column(Integer, ForeignKey("orders.id"), nullable=False)
amount = Column(Float, nullable=False) # Amount of AITBC traded
price = Column(Float, nullable=False) # Trade price in BTC
total = Column(Float, nullable=False) # Total value in BTC
trade_hash = Column(String(100), unique=True, nullable=False) # Blockchain transaction hash
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
buyer = relationship("User", back_populates="trades", foreign_keys=[buyer_id])
seller = relationship("User", foreign_keys=[seller_id])
order = relationship("Order", back_populates="trades")
__table_args__ = (
Index('idx_created_at', 'created_at'),
Index('idx_price', 'price'),
)
def __repr__(self):
return f"<Trade(amount={self.amount}, price={self.price}, total={self.total})>"
class Balance(Base):
"""User balance tracking"""
__tablename__ = "balances"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
btc_balance = Column(Float, default=0.0)
aitbc_balance = Column(Float, default=0.0)
btc_locked = Column(Float, default=0.0) # Locked in open orders
aitbc_locked = Column(Float, default=0.0) # Locked in open orders
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship
user = relationship("User")
def __repr__(self):
return f"<Balance(btc={self.btc_balance}, aitbc={self.aitbc_balance})>"

View File

@@ -0,0 +1,526 @@
#!/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)

View File

@@ -0,0 +1,20 @@
# Exchange API Routes - Add this to the existing nginx config
# Exchange API Routes
location /api/trades/ {
proxy_pass http://127.0.0.1:3003/api/trades/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
location /api/orders {
proxy_pass http://127.0.0.1:3003/api/orders;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}

View File

@@ -0,0 +1,8 @@
# AITBC Trade Exchange Requirements
# Compatible with Python 3.13+
fastapi>=0.111.0
uvicorn[standard]>=0.30.0
sqlalchemy>=2.0.30
pydantic>=2.7.0
python-multipart>=0.0.6

View File

@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""Migration script from SQLite to PostgreSQL for AITBC Exchange"""
import os
import sys
from pathlib import Path
# Add the src directory to the path
sys.path.insert(0, str(Path(__file__).parent / "src"))
import sqlite3
import psycopg2
from psycopg2.extras import RealDictCursor
from datetime import datetime
from decimal import Decimal
# Database configurations
SQLITE_DB = "exchange.db"
PG_CONFIG = {
"host": "localhost",
"database": "aitbc_exchange",
"user": "aitbc_user",
"password": "aitbc_password",
"port": 5432
}
def create_pg_schema():
"""Create PostgreSQL schema with optimized types"""
conn = psycopg2.connect(**PG_CONFIG)
cursor = conn.cursor()
print("Creating PostgreSQL schema...")
# Drop existing tables
cursor.execute("DROP TABLE IF EXISTS trades CASCADE")
cursor.execute("DROP TABLE IF EXISTS orders CASCADE")
# Create trades table with proper types
cursor.execute("""
CREATE TABLE trades (
id SERIAL PRIMARY KEY,
amount NUMERIC(20, 8) NOT NULL,
price NUMERIC(20, 8) NOT NULL,
total NUMERIC(20, 8) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
tx_hash VARCHAR(66),
maker_address VARCHAR(66),
taker_address VARCHAR(66)
)
""")
# Create orders table with proper types
cursor.execute("""
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_type VARCHAR(4) NOT NULL CHECK (order_type IN ('BUY', 'SELL')),
amount NUMERIC(20, 8) NOT NULL,
price NUMERIC(20, 8) NOT NULL,
total NUMERIC(20, 8) NOT NULL,
remaining NUMERIC(20, 8) NOT NULL,
filled NUMERIC(20, 8) DEFAULT 0,
status VARCHAR(20) DEFAULT 'OPEN' CHECK (status IN ('OPEN', 'FILLED', 'CANCELLED')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user_address VARCHAR(66),
tx_hash VARCHAR(66)
)
""")
# Create indexes for performance
print("Creating indexes...")
cursor.execute("CREATE INDEX idx_trades_created_at ON trades(created_at DESC)")
cursor.execute("CREATE INDEX idx_trades_price ON trades(price)")
cursor.execute("CREATE INDEX idx_orders_type ON orders(order_type)")
cursor.execute("CREATE INDEX idx_orders_price ON orders(price)")
cursor.execute("CREATE INDEX idx_orders_status ON orders(status)")
cursor.execute("CREATE INDEX idx_orders_created_at ON orders(created_at DESC)")
cursor.execute("CREATE INDEX idx_orders_user ON orders(user_address)")
conn.commit()
conn.close()
print("✅ PostgreSQL schema created successfully!")
def migrate_data():
"""Migrate data from SQLite to PostgreSQL"""
print("\nStarting data migration...")
# Connect to SQLite
sqlite_conn = sqlite3.connect(SQLITE_DB)
sqlite_conn.row_factory = sqlite3.Row
sqlite_cursor = sqlite_conn.cursor()
# Connect to PostgreSQL
pg_conn = psycopg2.connect(**PG_CONFIG)
pg_cursor = pg_conn.cursor()
# Migrate trades
print("Migrating trades...")
sqlite_cursor.execute("SELECT * FROM trades")
trades = sqlite_cursor.fetchall()
trades_count = 0
for trade in trades:
pg_cursor.execute("""
INSERT INTO trades (amount, price, total, created_at, tx_hash, maker_address, taker_address)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (
Decimal(str(trade['amount'])),
Decimal(str(trade['price'])),
Decimal(str(trade['total'])),
trade['created_at'],
trade.get('tx_hash'),
trade.get('maker_address'),
trade.get('taker_address')
))
trades_count += 1
# Migrate orders
print("Migrating orders...")
sqlite_cursor.execute("SELECT * FROM orders")
orders = sqlite_cursor.fetchall()
orders_count = 0
for order in orders:
pg_cursor.execute("""
INSERT INTO orders (order_type, amount, price, total, remaining, filled, status,
created_at, updated_at, user_address, tx_hash)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
order['order_type'],
Decimal(str(order['amount'])),
Decimal(str(order['price'])),
Decimal(str(order['total'])),
Decimal(str(order['remaining'])),
Decimal(str(order['filled'])),
order['status'],
order['created_at'],
order['updated_at'],
order.get('user_address'),
order.get('tx_hash')
))
orders_count += 1
pg_conn.commit()
print(f"\n✅ Migration complete!")
print(f" - Migrated {trades_count} trades")
print(f" - Migrated {orders_count} orders")
sqlite_conn.close()
pg_conn.close()
def update_exchange_config():
"""Update exchange configuration to use PostgreSQL"""
config_file = Path("simple_exchange_api.py")
if not config_file.exists():
print("❌ simple_exchange_api.py not found!")
return
print("\nUpdating exchange configuration...")
# Read the current file
content = config_file.read_text()
# Add PostgreSQL configuration
pg_config = """
# PostgreSQL Configuration
PG_CONFIG = {
"host": "localhost",
"database": "aitbc_exchange",
"user": "aitbc_user",
"password": "aitbc_password",
"port": 5432
}
def get_pg_connection():
\"\"\"Get PostgreSQL connection\"\"\"
return psycopg2.connect(**PG_CONFIG)
"""
# Replace SQLite init with PostgreSQL
new_init = """
def init_db():
\"\"\"Initialize PostgreSQL database\"\"\"
try:
conn = get_pg_connection()
cursor = conn.cursor()
# Check if tables exist
cursor.execute(\"\"\"
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name IN ('trades', 'orders')
)
\"\"\")
if not cursor.fetchone()[0]:
print("Creating PostgreSQL tables...")
create_pg_schema()
conn.close()
except Exception as e:
print(f"Database initialization error: {e}")
"""
# Update the file
content = content.replace("import sqlite3", "import sqlite3\nimport psycopg2\nfrom psycopg2.extras import RealDictCursor")
content = content.replace("def init_db():", new_init)
content = content.replace("conn = sqlite3.connect('exchange.db')", "conn = get_pg_connection()")
content = content.replace("cursor = conn.cursor()", "cursor = conn.cursor(cursor_factory=RealDictCursor)")
# Write back
config_file.write_text(content)
print("✅ Configuration updated to use PostgreSQL!")
def main():
"""Main migration process"""
print("=" * 60)
print("AITBC Exchange SQLite to PostgreSQL Migration")
print("=" * 60)
# Check if SQLite DB exists
if not Path(SQLITE_DB).exists():
print(f"❌ SQLite database '{SQLITE_DB}' not found!")
return
# Create PostgreSQL schema
create_pg_schema()
# Migrate data
migrate_data()
# Update configuration
update_exchange_config()
print("\n" + "=" * 60)
print("Migration completed successfully!")
print("=" * 60)
print("\nNext steps:")
print("1. Install PostgreSQL dependencies: pip install psycopg2-binary")
print("2. Restart the exchange service")
print("3. Verify data integrity")
print("4. Backup and remove SQLite database")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Seed initial market price for the exchange"""
import sqlite3
from datetime import datetime
def seed_initial_price():
"""Create initial trades to establish market price"""
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
# Create some initial trades at different price points
initial_trades = [
(1000, 0.00001), # 1000 AITBC at 0.00001 BTC each
(500, 0.0000105), # 500 AITBC at slightly higher
(750, 0.0000095), # 750 AITBC at slightly lower
(2000, 0.00001), # 2000 AITBC at base price
(1500, 0.000011), # 1500 AITBC at higher price
]
for amount, price in initial_trades:
total = amount * price
cursor.execute('''
INSERT INTO trades (amount, price, total, created_at)
VALUES (?, ?, ?, ?)
''', (amount, price, total, datetime.utcnow()))
# Also create some initial orders for liquidity
initial_orders = [
('BUY', 5000, 0.0000095), # Buy order
('BUY', 3000, 0.00001), # Buy order
('SELL', 2000, 0.0000105), # Sell order
('SELL', 4000, 0.000011), # Sell order
]
for order_type, amount, price in initial_orders:
total = amount * price
cursor.execute('''
INSERT INTO orders (order_type, amount, price, total, remaining, user_address)
VALUES (?, ?, ?, ?, ?, ?)
''', (order_type, amount, price, total, amount, 'aitbcexchange00000000000000000000000000000000'))
conn.commit()
conn.close()
print("✅ Seeded initial market data:")
print(f" - Created {len(initial_trades)} historical trades")
print(f" - Created {len(initial_orders)} liquidity orders")
print(f" - Initial price range: 0.0000095 - 0.000011 BTC")
print(" The exchange should now show real prices!")
if __name__ == "__main__":
seed_initial_price()

View File

@@ -0,0 +1,37 @@
#!/bin/bash
echo "=== PostgreSQL Setup for AITBC Exchange ==="
echo ""
# Install PostgreSQL if not already installed
if ! command -v psql &> /dev/null; then
echo "Installing PostgreSQL..."
sudo apt-get update
sudo apt-get install -y postgresql postgresql-contrib
fi
# Start PostgreSQL service
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create database and user
echo "Creating database and user..."
sudo -u postgres psql -c "CREATE DATABASE aitbc_exchange;"
sudo -u postgres psql -c "CREATE USER aitbc_user WITH PASSWORD 'aitbc_password';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE aitbc_exchange TO aitbc_user;"
# Test connection
echo "Testing connection..."
sudo -u postgres psql -c "\l" | grep aitbc_exchange
echo ""
echo "✅ PostgreSQL setup complete!"
echo ""
echo "Connection details:"
echo " Host: localhost"
echo " Port: 5432"
echo " Database: aitbc_exchange"
echo " User: aitbc_user"
echo " Password: aitbc_password"
echo ""
echo "You can now run the migration script."

54
apps/exchange/server.py Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Simple HTTP server for the AITBC Trade Exchange
"""
import os
import sys
from http.server import HTTPServer, SimpleHTTPRequestHandler
import argparse
class CORSHTTPRequestHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key')
super().end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.end_headers()
def run_server(port=3002, directory=None):
"""Run the HTTP server"""
if directory:
os.chdir(directory)
server_address = ('', port)
httpd = HTTPServer(server_address, CORSHTTPRequestHandler)
print(f"""
╔═══════════════════════════════════════╗
║ AITBC Trade Exchange Server ║
╠═══════════════════════════════════════╣
║ Server running at: ║
║ http://localhost:{port}
║ ║
║ Buy AITBC with Bitcoin! ║
║ Press Ctrl+C to stop ║
╚═══════════════════════════════════════╝
""")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server...")
httpd.server_close()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run the AITBC Trade Exchange server')
parser.add_argument('--port', type=int, default=3002, help='Port to run the server on')
parser.add_argument('--dir', type=str, default='.', help='Directory to serve from')
args = parser.parse_args()
run_server(port=args.port, directory=args.dir)

View File

@@ -0,0 +1,608 @@
#!/usr/bin/env python3
"""
Simple FastAPI backend for the AITBC Trade Exchange (Python 3.13 compatible)
"""
import sqlite3
import json
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse
import random
# Database setup
def init_db():
"""Initialize SQLite database"""
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
# Create tables
cursor.execute('''
CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount REAL NOT NULL,
price REAL NOT NULL,
total REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
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
)
''')
# Add columns if they don't exist (for existing databases)
try:
cursor.execute('ALTER TABLE orders ADD COLUMN user_address TEXT')
except:
pass
try:
cursor.execute('ALTER TABLE orders ADD COLUMN tx_hash TEXT')
except:
pass
conn.commit()
conn.close()
def create_mock_trades():
"""Create some mock trades"""
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
# Check if we have trades
cursor.execute('SELECT COUNT(*) FROM trades')
if cursor.fetchone()[0] > 0:
conn.close()
return
# Create mock trades
now = datetime.utcnow()
for i in range(20):
amount = random.uniform(10, 500)
price = random.uniform(0.000009, 0.000012)
total = amount * price
created_at = now - timedelta(minutes=random.randint(0, 60))
cursor.execute('''
INSERT INTO trades (amount, price, total, created_at)
VALUES (?, ?, ?, ?)
''', (amount, price, total, created_at))
conn.commit()
conn.close()
class ExchangeAPIHandler(BaseHTTPRequestHandler):
def do_GET(self):
"""Handle GET requests"""
if self.path == '/api/health':
self.health_check()
elif self.path.startswith('/api/trades/recent'):
parsed = urllib.parse.urlparse(self.path)
self.get_recent_trades(parsed)
elif self.path.startswith('/api/orders/orderbook'):
self.get_orderbook()
elif self.path.startswith('/api/wallet/balance'):
self.handle_wallet_balance()
elif self.path == '/api/total-supply':
self.handle_total_supply()
elif self.path == '/api/treasury-balance':
self.handle_treasury_balance()
else:
self.send_error(404, "Not Found")
def do_POST(self):
"""Handle POST requests"""
if self.path == '/api/orders':
self.handle_place_order()
elif self.path == '/api/wallet/connect':
self.handle_wallet_connect()
else:
self.send_error(404, "Not Found")
def get_recent_trades(self, parsed):
"""Get recent trades"""
query = urllib.parse.parse_qs(parsed.query)
limit = int(query.get('limit', [20])[0])
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
cursor.execute('''
SELECT id, amount, price, total, created_at
FROM trades
ORDER BY created_at DESC
LIMIT ?
''', (limit,))
trades = []
for row in cursor.fetchall():
trades.append({
'id': row[0],
'amount': row[1],
'price': row[2],
'total': row[3],
'created_at': row[4]
})
conn.close()
self.send_json_response(trades)
def get_orderbook(self):
"""Get order book"""
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
# Get sell orders
cursor.execute('''
SELECT id, order_type, amount, price, total, filled, remaining, status, created_at
FROM orders
WHERE order_type = 'SELL' AND status = 'OPEN'
ORDER BY price ASC
LIMIT 20
''')
sells = []
for row in cursor.fetchall():
sells.append({
'id': row[0],
'order_type': row[1],
'amount': row[2],
'price': row[3],
'total': row[4],
'filled': row[5],
'remaining': row[6],
'status': row[7],
'created_at': row[8]
})
# Get buy orders
cursor.execute('''
SELECT id, order_type, amount, price, total, filled, remaining, status, created_at
FROM orders
WHERE order_type = 'BUY' AND status = 'OPEN'
ORDER BY price DESC
LIMIT 20
''')
buys = []
for row in cursor.fetchall():
buys.append({
'id': row[0],
'order_type': row[1],
'amount': row[2],
'price': row[3],
'total': row[4],
'filled': row[5],
'remaining': row[6],
'status': row[7],
'created_at': row[8]
})
conn.close()
self.send_json_response({'buys': buys, 'sells': sells})
def handle_place_order(self):
"""Place a new order on the blockchain"""
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
try:
data = json.loads(post_data.decode('utf-8'))
order_type = data.get('order_type')
amount = data.get('amount')
price = data.get('price')
user_address = data.get('user_address')
if not all([order_type, amount, price, user_address]):
self.send_error(400, "Missing required fields")
return
if order_type not in ['BUY', 'SELL']:
self.send_error(400, "Invalid order type")
return
# Create order transaction on blockchain
try:
import urllib.request
import urllib.parse
# Prepare transaction data
tx_data = {
"from": user_address,
"type": "ORDER",
"order_type": order_type,
"amount": str(amount),
"price": str(price),
"nonce": 0 # Would get actual nonce from wallet
}
# Send transaction to blockchain
tx_url = "http://localhost:9080/rpc/sendTx"
encoded_data = urllib.parse.urlencode(tx_data).encode('utf-8')
req = urllib.request.Request(
tx_url,
data=encoded_data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
with urllib.request.urlopen(req) as response:
tx_result = json.loads(response.read().decode())
# Store order in local database for orderbook
total = amount * price
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
cursor.execute('''
INSERT INTO orders (order_type, amount, price, total, remaining, user_address, tx_hash)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (order_type, amount, price, total, amount, user_address, tx_result.get('tx_hash', '')))
order_id = cursor.lastrowid
conn.commit()
# Get the created order
cursor.execute('SELECT * FROM orders WHERE id = ?', (order_id,))
row = cursor.fetchone()
order = {
'id': row[0],
'order_type': row[1],
'amount': row[2],
'price': row[3],
'total': row[4],
'filled': row[5],
'remaining': row[6],
'status': row[7],
'created_at': row[8],
'user_address': row[9],
'tx_hash': row[10]
}
conn.close()
# Try to match orders
self.match_orders(order)
self.send_json_response(order)
except Exception as e:
# Fallback to database-only if blockchain is down
total = amount * price
conn = sqlite3.connect('exchange.db')
cursor = conn.cursor()
cursor.execute('''
INSERT INTO orders (order_type, amount, price, total, remaining, user_address)
VALUES (?, ?, ?, ?, ?, ?)
''', (order_type, amount, price, total, amount, user_address))
order_id = cursor.lastrowid
conn.commit()
# Get the created order
cursor.execute('SELECT * FROM orders WHERE id = ?', (order_id,))
row = cursor.fetchone()
order = {
'id': row[0],
'order_type': row[1],
'amount': row[2],
'price': row[3],
'total': row[4],
'filled': row[5],
'remaining': row[6],
'status': row[7],
'created_at': row[8],
'user_address': row[9] if len(row) > 9 else None
}
conn.close()
# Try to match orders
self.match_orders(order)
self.send_json_response(order)
except Exception as e:
# Fallback to hardcoded values if blockchain is down
self.send_json_response({
"total_supply": "21000000",
"circulating_supply": "1000000",
"treasury_balance": "0",
"source": "fallback",
"error": str(e)
})
# Match with sell orders
cursor.execute('''
SELECT * FROM orders
WHERE order_type = 'SELL' AND status = 'OPEN' AND price <= ?
ORDER BY price ASC, created_at ASC
''', (new_order['price'],))
else:
# Match with buy orders
cursor.execute('''
SELECT * FROM orders
WHERE order_type = 'BUY' AND status = 'OPEN' AND price >= ?
ORDER BY price DESC, created_at ASC
''', (new_order['price'],))
matching_orders = cursor.fetchall()
for order_row in matching_orders:
if new_order['remaining'] <= 0:
break
# Calculate trade amount
trade_amount = min(new_order['remaining'], order_row[6]) # remaining
if trade_amount > 0:
# Create trade on blockchain
try:
import urllib.request
import urllib.parse
trade_price = order_row[3] # Use the existing order's price
trade_data = {
"type": "TRADE",
"buy_order": new_order['id'] if new_order['order_type'] == 'BUY' else order_row[0],
"sell_order": order_row[0] if new_order['order_type'] == 'BUY' else new_order['id'],
"amount": str(trade_amount),
"price": str(trade_price)
}
# Record trade in database
cursor.execute('''
INSERT INTO trades (amount, price, total)
VALUES (?, ?, ?)
''', (trade_amount, trade_price, trade_amount * trade_price))
# Update orders
new_order['remaining'] -= trade_amount
new_order['filled'] = new_order.get('filled', 0) + trade_amount
# Update matching order
new_remaining = order_row[6] - trade_amount
cursor.execute('''
UPDATE orders SET remaining = ?, filled = filled + ?
WHERE id = ?
''', (new_remaining, trade_amount, order_row[0]))
# Close order if fully filled
if new_remaining <= 0:
cursor.execute('''
UPDATE orders SET status = 'FILLED' WHERE id = ?
''', (order_row[0],))
except Exception as e:
print(f"Failed to create trade on blockchain: {e}")
# Still record the trade in database
cursor.execute('''
INSERT INTO trades (amount, price, total)
VALUES (?, ?, ?)
''', (trade_amount, order_row[3], trade_amount * order_row[3]))
# Update new order in database
if new_order['remaining'] <= 0:
cursor.execute('''
UPDATE orders SET status = 'FILLED', remaining = 0, filled = ?
WHERE id = ?
''', (new_order['filled'], new_order['id']))
else:
cursor.execute('''
UPDATE orders SET remaining = ?, filled = ?
WHERE id = ?
''', (new_order['remaining'], new_order['filled'], new_order['id']))
conn.commit()
conn.close()
def handle_treasury_balance(self):
"""Get exchange treasury balance from blockchain"""
try:
import urllib.request
import json
# Treasury address from genesis
treasury_address = "aitbcexchange00000000000000000000000000000000"
blockchain_url = f"http://localhost:9080/rpc/getBalance/{treasury_address}"
try:
with urllib.request.urlopen(blockchain_url) as response:
balance_data = json.loads(response.read().decode())
treasury_balance = balance_data.get('balance', 0)
self.send_json_response({
"address": treasury_address,
"balance": str(treasury_balance),
"available_for_sale": str(treasury_balance), # All treasury tokens available
"source": "blockchain"
})
except Exception as e:
# If blockchain query fails, show the genesis amount
self.send_json_response({
"address": treasury_address,
"balance": "10000000000000", # 10 million in smallest units
"available_for_sale": "10000000000000",
"source": "genesis",
"note": "Genesis amount - blockchain may need restart"
})
except Exception as e:
self.send_error(500, str(e))
def health_check(self):
"""Health check"""
self.send_json_response({
'status': 'ok',
'timestamp': datetime.utcnow().isoformat()
})
def handle_wallet_balance(self):
"""Handle wallet balance request"""
from urllib.parse import urlparse, parse_qs
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
address = params.get('address', [''])[0]
if not address:
self.send_json_response({
"btc": "0.00000000",
"aitbc": "0.00",
"address": "unknown"
})
return
try:
# Query real blockchain for balance
import urllib.request
import json
# Get AITBC balance from blockchain
blockchain_url = f"http://localhost:9080/rpc/getBalance/{address}"
with urllib.request.urlopen(blockchain_url) as response:
balance_data = json.loads(response.read().decode())
# For BTC, we'll query a Bitcoin API (simplified for now)
# In production, you'd integrate with a real Bitcoin node API
btc_balance = "0.00000000" # Placeholder - would query real Bitcoin network
self.send_json_response({
"btc": btc_balance,
"aitbc": str(balance_data.get('balance', 0)),
"address": address,
"nonce": balance_data.get('nonce', 0)
})
except Exception as e:
# Fallback to error if blockchain is down
self.send_json_response({
"btc": "0.00000000",
"aitbc": "0.00",
"address": address,
"error": "Failed to fetch balance from blockchain"
})
def handle_wallet_connect(self):
"""Handle wallet connection request"""
import secrets
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
mock_address = "aitbc" + secrets.token_hex(20)
self.send_json_response({
"address": mock_address,
"status": "connected"
})
def send_json_response(self, data, status=200):
"""Send JSON response"""
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(json.dumps(data, default=str).encode())
def do_OPTIONS(self):
"""Handle OPTIONS requests for CORS"""
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
class WalletAPIHandler(BaseHTTPRequestHandler):
"""Handle wallet API requests"""
def do_GET(self):
"""Handle GET requests"""
if self.path.startswith('/api/wallet/balance'):
# Parse address from query params
from urllib.parse import urlparse, parse_qs
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
address = params.get('address', [''])[0]
# Return mock balance for now
self.send_json_response({
"btc": "0.12345678",
"aitbc": "1000.50",
"address": address or "unknown"
})
else:
self.send_error(404)
def do_POST(self):
"""Handle POST requests"""
if self.path == '/wallet/connect':
import secrets
mock_address = "aitbc" + secrets.token_hex(20)
self.send_json_response({
"address": mock_address,
"status": "connected"
})
else:
self.send_error(404)
def send_json_response(self, data, status=200):
"""Send JSON response"""
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
self.wfile.write(json.dumps(data, default=str).encode())
def do_OPTIONS(self):
"""Handle OPTIONS requests for CORS"""
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def run_server(port=8001):
"""Run the server"""
init_db()
# Removed mock trades - now using only real blockchain data
server = HTTPServer(('localhost', port), ExchangeAPIHandler)
print(f"""
╔═══════════════════════════════════════╗
║ AITBC Exchange API Server ║
╠═══════════════════════════════════════╣
║ Server running at: ║
║ http://localhost:{port}
║ ║
║ Real trading API active! ║
║ Press Ctrl+C to stop ║
╚═══════════════════════════════════════╝
""")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server...")
server.server_close()
if __name__ == "__main__":
run_server()

View File

@@ -0,0 +1,369 @@
#!/usr/bin/env python3
"""AITBC Exchange API with PostgreSQL Support"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import json
import urllib.request
import psycopg2
from psycopg2.extras import RealDictCursor
from datetime import datetime
from decimal import Decimal
import random
# PostgreSQL Configuration
PG_CONFIG = {
"host": "localhost",
"database": "aitbc_exchange",
"user": "aitbc_user",
"password": "aitbc_password",
"port": 5432
}
def get_pg_connection():
"""Get PostgreSQL connection"""
return psycopg2.connect(**PG_CONFIG)
def init_db():
"""Initialize PostgreSQL database"""
try:
conn = get_pg_connection()
cursor = conn.cursor()
# Check if tables exist
cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name IN ('trades', 'orders')
)
""")
if not cursor.fetchone()[0]:
print("Creating PostgreSQL tables...")
create_pg_schema()
conn.close()
except Exception as e:
print(f"Database initialization error: {e}")
def create_pg_schema():
"""Create PostgreSQL schema"""
conn = get_pg_connection()
cursor = conn.cursor()
# Create trades table
cursor.execute("""
CREATE TABLE trades (
id SERIAL PRIMARY KEY,
amount NUMERIC(20, 8) NOT NULL,
price NUMERIC(20, 8) NOT NULL,
total NUMERIC(20, 8) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
tx_hash VARCHAR(66),
maker_address VARCHAR(66),
taker_address VARCHAR(66)
)
""")
# Create orders table
cursor.execute("""
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_type VARCHAR(4) NOT NULL CHECK (order_type IN ('BUY', 'SELL')),
amount NUMERIC(20, 8) NOT NULL,
price NUMERIC(20, 8) NOT NULL,
total NUMERIC(20, 8) NOT NULL,
remaining NUMERIC(20, 8) NOT NULL,
filled NUMERIC(20, 8) DEFAULT 0,
status VARCHAR(20) DEFAULT 'OPEN' CHECK (status IN ('OPEN', 'FILLED', 'CANCELLED')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user_address VARCHAR(66),
tx_hash VARCHAR(66)
)
""")
# Create indexes
cursor.execute("CREATE INDEX idx_trades_created_at ON trades(created_at DESC)")
cursor.execute("CREATE INDEX idx_orders_type ON orders(order_type)")
cursor.execute("CREATE INDEX idx_orders_price ON orders(price)")
cursor.execute("CREATE INDEX idx_orders_status ON orders(status)")
conn.commit()
conn.close()
class ExchangeAPIHandler(BaseHTTPRequestHandler):
def send_json_response(self, data, status=200):
"""Send JSON response"""
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(data, default=str).encode())
def do_OPTIONS(self):
"""Handle OPTIONS requests for CORS"""
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.end_headers()
def do_GET(self):
"""Handle GET requests"""
if self.path == '/api/health':
self.health_check()
elif self.path.startswith('/api/trades/recent'):
parsed = urlparse(self.path)
self.get_recent_trades(parsed)
elif self.path.startswith('/api/orders/orderbook'):
self.get_orderbook()
elif self.path.startswith('/api/wallet/balance'):
self.handle_wallet_balance()
elif self.path == '/api/treasury-balance':
self.handle_treasury_balance()
else:
self.send_error(404)
def do_POST(self):
"""Handle POST requests"""
if self.path == '/api/orders':
self.handle_place_order()
elif self.path == '/api/wallet/connect':
self.handle_wallet_connect()
else:
self.send_error(404)
def health_check(self):
"""Health check"""
try:
conn = get_pg_connection()
cursor = conn.cursor()
cursor.execute("SELECT 1")
cursor.close()
conn.close()
self.send_json_response({
'status': 'ok',
'database': 'postgresql',
'timestamp': datetime.utcnow().isoformat()
})
except Exception as e:
self.send_json_response({
'status': 'error',
'error': str(e)
}, 500)
def get_recent_trades(self, parsed):
"""Get recent trades from PostgreSQL"""
try:
conn = get_pg_connection()
cursor = conn.cursor(cursor_factory=RealDictCursor)
# Get limit from query params
params = parse_qs(parsed.query)
limit = int(params.get('limit', [10])[0])
cursor.execute("""
SELECT * FROM trades
ORDER BY created_at DESC
LIMIT %s
""", (limit,))
trades = []
for row in cursor.fetchall():
trades.append({
'id': row['id'],
'amount': float(row['amount']),
'price': float(row['price']),
'total': float(row['total']),
'created_at': row['created_at'].isoformat(),
'tx_hash': row['tx_hash']
})
cursor.close()
conn.close()
self.send_json_response(trades)
except Exception as e:
self.send_error(500, str(e))
def get_orderbook(self):
"""Get order book from PostgreSQL"""
try:
conn = get_pg_connection()
cursor = conn.cursor(cursor_factory=RealDictCursor)
# Get sell orders (asks)
cursor.execute("""
SELECT * FROM orders
WHERE order_type = 'SELL' AND status = 'OPEN' AND remaining > 0
ORDER BY price ASC, created_at ASC
LIMIT 20
""")
sells = []
for row in cursor.fetchall():
sells.append({
'id': row['id'],
'amount': float(row['remaining']),
'price': float(row['price']),
'total': float(row['remaining'] * row['price'])
})
# Get buy orders (bids)
cursor.execute("""
SELECT * FROM orders
WHERE order_type = 'BUY' AND status = 'OPEN' AND remaining > 0
ORDER BY price DESC, created_at ASC
LIMIT 20
""")
buys = []
for row in cursor.fetchall():
buys.append({
'id': row['id'],
'amount': float(row['remaining']),
'price': float(row['price']),
'total': float(row['remaining'] * row['price'])
})
cursor.close()
conn.close()
self.send_json_response({
'buys': buys,
'sells': sells
})
except Exception as e:
self.send_error(500, str(e))
def handle_wallet_connect(self):
"""Handle wallet connection"""
# Generate a mock wallet address for demo
address = f"aitbc{''.join(random.choices('0123456789abcdef', k=64))}"
self.send_json_response({
"address": address,
"status": "connected"
})
def handle_wallet_balance(self):
"""Handle wallet balance request"""
from urllib.parse import urlparse, parse_qs
parsed = urlparse(self.path)
params = parse_qs(parsed.query)
address = params.get('address', [''])[0]
try:
# Query blockchain for balance
blockchain_url = f"http://localhost:9080/rpc/getBalance/{address}"
with urllib.request.urlopen(blockchain_url) as response:
balance_data = json.loads(response.read().decode())
aitbc_balance = balance_data.get('balance', 0)
nonce = balance_data.get('nonce', 0)
except:
aitbc_balance = 0
nonce = 0
self.send_json_response({
"btc": "0.00000000",
"aitbc": str(aitbc_balance),
"address": address or "unknown",
"nonce": nonce
})
def handle_treasury_balance(self):
"""Get exchange treasury balance"""
try:
treasury_address = "aitbcexchange00000000000000000000000000000000"
blockchain_url = f"http://localhost:9080/rpc/getBalance/{treasury_address}"
with urllib.request.urlopen(blockchain_url) as response:
balance_data = json.loads(response.read().decode())
treasury_balance = balance_data.get('balance', 0)
self.send_json_response({
"address": treasury_address,
"balance": str(treasury_balance),
"available_for_sale": str(treasury_balance),
"source": "blockchain"
})
except Exception as e:
self.send_error(500, str(e))
def handle_place_order(self):
"""Handle placing an order"""
try:
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
order_data = json.loads(post_data.decode())
# Validate order data
required_fields = ['order_type', 'amount', 'price']
for field in required_fields:
if field not in order_data:
self.send_json_response({
"error": f"Missing required field: {field}"
}, 400)
return
# Insert order into PostgreSQL
conn = get_pg_connection()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO orders (order_type, amount, price, total, remaining, user_address)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id, created_at
""", (
order_data['order_type'],
Decimal(str(order_data['amount'])),
Decimal(str(order_data['price'])),
Decimal(str(order_data['amount'] * order_data['price'])),
Decimal(str(order_data['amount'])),
order_data.get('user_address', 'aitbcexchange00000000000000000000000000000000')
))
result = cursor.fetchone()
order_id = result[0]
created_at = result[1]
conn.commit()
cursor.close()
conn.close()
self.send_json_response({
"id": order_id,
"order_type": order_data['order_type'],
"amount": order_data['amount'],
"price": order_data['price'],
"status": "OPEN",
"created_at": created_at.isoformat()
})
except Exception as e:
self.send_json_response({
"error": str(e)
}, 500)
def run_server(port=3003):
"""Run the server"""
init_db()
server = HTTPServer(('localhost', port), ExchangeAPIHandler)
print(f"""
╔═══════════════════════════════════════╗
║ AITBC Exchange API Server ║
╠═══════════════════════════════════════╣
║ Server running at: ║
║ http://localhost:{port}
║ ║
║ Database: PostgreSQL ║
║ Real trading API active! ║
╚═══════════════════════════════════════╝
""")
server.serve_forever()
if __name__ == "__main__":
run_server()

388
apps/exchange/styles.css Normal file
View File

@@ -0,0 +1,388 @@
/* Production CSS for AITBC Trade Exchange */
/* Dark mode variables */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--bg-tertiary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-tertiary: #9ca3af;
--border-color: #e5e7eb;
--primary-50: #eff6ff;
--primary-500: #3b82f6;
--primary-600: #2563eb;
--primary-700: #1d4ed8;
}
.dark {
--bg-primary: #1f2937;
--bg-secondary: #111827;
--bg-tertiary: #374151;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-tertiary: #9ca3af;
--border-color: #4b5563;
}
/* Base styles */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--bg-secondary);
color: var(--text-primary);
margin: 0;
padding: 0;
}
/* Layout */
.h-full {
height: 100%;
}
.min-h-full {
min-height: 100%;
}
.max-w-7xl {
max-width: 1280px;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
/* Navigation */
nav {
background-color: var(--bg-primary);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
nav > div {
display: flex;
justify-content: space-between;
height: 4rem;
align-items: center;
}
nav .flex {
display: flex;
}
nav .items-center {
align-items: center;
}
nav .space-x-8 > * + * {
margin-left: 2rem;
}
nav .space-x-4 > * + * {
margin-left: 1rem;
}
nav .text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
nav .font-bold {
font-weight: 700;
}
nav .text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
nav .font-medium {
font-weight: 500;
}
/* Links */
a {
color: inherit;
text-decoration: none;
}
a:hover {
color: var(--primary-600);
}
/* Cards */
.bg-white {
background-color: var(--bg-primary);
}
.dark .bg-white {
background-color: var(--bg-primary);
}
.rounded-lg {
border-radius: 0.5rem;
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.p-4 {
padding: 1rem;
}
.p-6 {
padding: 1.5rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
/* Grid */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.gap-6 {
gap: 1.5rem;
}
@media (min-width: 1024px) {
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* Typography */
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.text-gray-600 {
color: var(--text-secondary);
}
.text-gray-900 {
color: var(--text-primary);
}
.text-gray-500 {
color: var(--text-tertiary);
}
.dark .text-gray-300 {
color: #d1d5db;
}
.dark .text-gray-400 {
color: #9ca3af;
}
.dark .text-white {
color: #ffffff;
}
/* Buttons */
button {
cursor: pointer;
border: none;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.15s ease-in-out;
}
.bg-primary-600 {
background-color: var(--primary-600);
}
.bg-primary-600:hover {
background-color: var(--primary-700);
}
.text-white {
color: #ffffff;
}
.bg-green-600 {
background-color: #059669;
}
.bg-green-600:hover {
background-color: #047857;
}
.bg-red-600 {
background-color: #dc2626;
}
.bg-red-600:hover {
background-color: #b91c1c;
}
.bg-gray-100 {
background-color: var(--bg-tertiary);
}
/* Forms */
input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 0.375rem;
background-color: var(--bg-primary);
color: var(--text-primary);
}
input:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.dark input {
background-color: var(--bg-tertiary);
border-color: var(--border-color);
}
.dark input:focus {
border-color: var(--primary-500);
}
/* Tables */
.space-y-2 > * + * {
margin-top: 0.5rem;
}
.space-y-1 > * + * {
margin-top: 0.25rem;
}
.justify-between {
justify-content: space-between;
}
.text-right {
text-align: right;
}
.text-green-600 {
color: #059669;
}
.text-red-600 {
color: #dc2626;
}
/* Borders */
.border-b {
border-bottom: 1px solid var(--border-color);
}
.border-t {
border-top: 1px solid var(--border-color);
}
/* Width */
.w-full {
width: 100%;
}
/* Flex */
.flex {
display: flex;
}
.flex-1 {
flex: 1 1 0%;
}
/* Colors */
.bg-gray-50 {
background-color: var(--bg-secondary);
}
.dark .bg-gray-600 {
background-color: #4b5563;
}
.dark .bg-gray-700 {
background-color: #374151;
}
/* Dark mode toggle */
.p-2 {
padding: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
/* Hover states */
.hover\:text-gray-700:hover {
color: var(--text-primary);
}
.dark .hover\:text-gray-200:hover {
color: #e5e7eb;
}
/* Order book colors */
.text-red-600 {
color: #dc2626;
}
.dark .text-red-400 {
color: #f87171;
}
.text-green-600 {
color: #059669;
}
.dark .text-green-400 {
color: #4ade80;
}

View File

@@ -0,0 +1,58 @@
// Add this function to index.real.html to update price ticker with real data
async function updatePriceTicker() {
try {
// Get recent trades to calculate price statistics
const response = await fetch(`${EXCHANGE_API_BASE}/api/trades/recent?limit=100`);
if (!response.ok) return;
const trades = await response.json();
if (trades.length === 0) {
console.log('No trades to calculate price from');
return;
}
// Calculate 24h volume (sum of all trades in last 24h)
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const recentTrades = trades.filter(trade =>
new Date(trade.created_at) > yesterday
);
const totalVolume = recentTrades.reduce((sum, trade) => sum + trade.amount, 0);
const totalBTC = recentTrades.reduce((sum, trade) => sum + trade.total, 0);
// Calculate current price (price of last trade)
const currentPrice = trades[0].price;
// Calculate 24h high/low
const prices = recentTrades.map(t => t.price);
const high24h = Math.max(...prices);
const low24h = Math.min(...prices);
// Calculate price change (compare with price 24h ago)
const price24hAgo = trades[trades.length - 1]?.price || currentPrice;
const priceChange = ((currentPrice - price24hAgo) / price24hAgo) * 100;
// Update UI
document.getElementById('currentPrice').textContent = `${currentPrice.toFixed(6)} BTC`;
document.getElementById('volume24h').textContent = `${totalVolume.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")} AITBC`;
document.getElementById('volume24h').nextElementSibling.textContent = `${totalBTC.toFixed(5)} BTC`;
document.getElementById('highLow').textContent = `${high24h.toFixed(6)} / ${low24h.toFixed(6)}`;
// Update price change with color
const changeElement = document.getElementById('priceChange');
changeElement.textContent = `${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%`;
changeElement.className = `text-sm ${priceChange >= 0 ? 'text-green-600' : 'text-red-600'}`;
} catch (error) {
console.error('Failed to update price ticker:', error);
}
}
// Call this function in the DOMContentLoaded event
// Add to existing initialization:
// updatePriceTicker();
// setInterval(updatePriceTicker, 30000); // Update every 30 seconds