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:
363
apps/exchange/admin.html
Normal file
363
apps/exchange/admin.html
Normal 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>
|
||||
175
apps/exchange/bitcoin-wallet.py
Normal file
175
apps/exchange/bitcoin-wallet.py
Normal 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
67
apps/exchange/build.py
Normal 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()
|
||||
651
apps/exchange/complete_cross_chain_exchange.py
Executable file
651
apps/exchange/complete_cross_chain_exchange.py
Executable 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)
|
||||
614
apps/exchange/cross_chain_exchange.py
Executable file
614
apps/exchange/cross_chain_exchange.py
Executable 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
49
apps/exchange/database.py
Normal 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
|
||||
54
apps/exchange/deploy_real_exchange.sh
Executable file
54
apps/exchange/deploy_real_exchange.sh
Executable 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
54
apps/exchange/deploy_simple.sh
Executable 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"
|
||||
356
apps/exchange/exchange_api.py
Normal file
356
apps/exchange/exchange_api.py
Normal 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)
|
||||
6
apps/exchange/exchange_wrapper.sh
Executable file
6
apps/exchange/exchange_wrapper.sh
Executable 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
1250
apps/exchange/index.html
Normal file
File diff suppressed because it is too large
Load Diff
620
apps/exchange/index.prod.html
Normal file
620
apps/exchange/index.prod.html
Normal 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>
|
||||
410
apps/exchange/index.real.html
Normal file
410
apps/exchange/index.real.html
Normal 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>
|
||||
398
apps/exchange/index_fixed.html
Normal file
398
apps/exchange/index_fixed.html
Normal 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>
|
||||
398
apps/exchange/index_inline.html
Normal file
398
apps/exchange/index_inline.html
Normal 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
109
apps/exchange/models.py
Normal 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})>"
|
||||
526
apps/exchange/multichain_exchange_api.py
Executable file
526
apps/exchange/multichain_exchange_api.py
Executable 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)
|
||||
20
apps/exchange/nginx_patch.conf
Normal file
20
apps/exchange/nginx_patch.conf
Normal 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;
|
||||
}
|
||||
8
apps/exchange/requirements.txt
Normal file
8
apps/exchange/requirements.txt
Normal 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
|
||||
250
apps/exchange/scripts/migrate_to_postgresql.py
Normal file
250
apps/exchange/scripts/migrate_to_postgresql.py
Normal 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()
|
||||
54
apps/exchange/scripts/seed_market.py
Normal file
54
apps/exchange/scripts/seed_market.py
Normal 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()
|
||||
37
apps/exchange/scripts/setup_postgresql.sh
Normal file
37
apps/exchange/scripts/setup_postgresql.sh
Normal 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
54
apps/exchange/server.py
Executable 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)
|
||||
608
apps/exchange/simple_exchange_api.py
Executable file
608
apps/exchange/simple_exchange_api.py
Executable 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()
|
||||
369
apps/exchange/simple_exchange_api_pg.py
Normal file
369
apps/exchange/simple_exchange_api_pg.py
Normal 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
388
apps/exchange/styles.css
Normal 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;
|
||||
}
|
||||
58
apps/exchange/update_price_ticker.js
Normal file
58
apps/exchange/update_price_ticker.js
Normal 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
|
||||
Reference in New Issue
Block a user