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:
@@ -1,363 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,175 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,356 +0,0 @@
|
||||
#!/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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,620 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,410 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,398 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,398 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/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})>"
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# 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
|
||||
@@ -1,250 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/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."
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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)
|
||||
@@ -1,608 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,369 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,388 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// 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