CEX Trading Platform

Build a centralized exchange trading platform with Solana integration for deposits and withdrawals.

Project Overview

A centralized exchange (CEX) combines traditional fintech infrastructure with blockchain settlement. This project teaches you how to bridge Web2 and Web3 architectures.

Text
┌─────────────────────────────────────────────────────────────────┐
│                    CEX Architecture                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────┐    ┌─────────────────┐    ┌───────────────┐   │
│  │   Frontend  │───▶│   Backend API   │───▶│   Database    │   │
│  │   (React)   │    │   (Express)     │    │  (PostgreSQL) │   │
│  └─────────────┘    └────────┬────────┘    └───────────────┘   │
│                              │                                  │
│                              ▼                                  │
│                    ┌─────────────────┐                         │
│                    │  Solana RPC     │                         │
│                    │  (Deposits/     │                         │
│                    │   Withdrawals)  │                         │
│                    └─────────────────┘                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Core Features

1. User Authentication & KYC

  • Email/password authentication
  • Two-factor authentication (2FA)
  • KYC verification flow

2. Wallet Management

  • Unique deposit addresses per user
  • Hot/cold wallet architecture
  • Multi-signature security

3. Order Book Trading

  • Limit and market orders
  • Real-time order matching
  • Trade history and charts

4. Solana Integration

  • SOL and SPL token deposits
  • On-chain withdrawal processing
  • Transaction confirmation tracking

Database Schema

SQL
-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    kyc_status VARCHAR(50) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Wallets table
CREATE TABLE wallets (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id),
    currency VARCHAR(10) NOT NULL,
    balance DECIMAL(20, 8) DEFAULT 0,
    locked_balance DECIMAL(20, 8) DEFAULT 0,
    deposit_address VARCHAR(255),
    UNIQUE(user_id, currency)
);

-- Orders table
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id),
    pair VARCHAR(20) NOT NULL,
    side VARCHAR(4) NOT NULL, -- 'buy' or 'sell'
    type VARCHAR(10) NOT NULL, -- 'limit' or 'market'
    price DECIMAL(20, 8),
    quantity DECIMAL(20, 8) NOT NULL,
    filled_quantity DECIMAL(20, 8) DEFAULT 0,
    status VARCHAR(20) DEFAULT 'open',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Deposits/Withdrawals
CREATE TABLE transactions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id),
    type VARCHAR(20) NOT NULL, -- 'deposit' or 'withdrawal'
    currency VARCHAR(10) NOT NULL,
    amount DECIMAL(20, 8) NOT NULL,
    tx_hash VARCHAR(128),
    status VARCHAR(20) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Deposit Flow

TypeScript
import { Connection, PublicKey } from "@solana/web3.js";

interface DepositMonitor {
  connection: Connection;
  depositAddresses: Map<string, string>; // address -> userId
}

async function monitorDeposits(monitor: DepositMonitor) {
  const { connection, depositAddresses } = monitor;
  
  // Subscribe to account changes for all deposit addresses
  for (const [address, userId] of depositAddresses) {
    const pubkey = new PublicKey(address);
    
    connection.onAccountChange(pubkey, async (accountInfo) => {
      const newBalance = accountInfo.lamports;
      
      // Check for new deposits
      const previousBalance = await getPreviousBalance(address);
      if (newBalance > previousBalance) {
        const depositAmount = newBalance - previousBalance;
        
        await processDeposit({
          userId,
          amount: depositAmount / 1e9, // Convert lamports to SOL
          address,
        });
      }
    });
  }
}

async function processDeposit(deposit: {
  userId: string;
  amount: number;
  address: string;
}) {
  // 1. Create transaction record
  await db.transactions.create({
    userId: deposit.userId,
    type: 'deposit',
    currency: 'SOL',
    amount: deposit.amount,
    status: 'confirmed',
  });
  
  // 2. Credit user's balance
  await db.wallets.increment({
    userId: deposit.userId,
    currency: 'SOL',
    amount: deposit.amount,
  });
  
  // 3. Notify user
  await sendNotification(deposit.userId, 'Deposit confirmed');
}

Withdrawal Flow

TypeScript
import {
  Connection,
  Keypair,
  PublicKey,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";

async function processWithdrawal(withdrawal: {
  userId: string;
  amount: number;
  destinationAddress: string;
}) {
  const connection = new Connection(process.env.SOLANA_RPC_URL!);
  const hotWallet = Keypair.fromSecretKey(
    Buffer.from(process.env.HOT_WALLET_SECRET!, 'base64')
  );
  
  // 1. Validate user has sufficient balance
  const wallet = await db.wallets.findOne({
    userId: withdrawal.userId,
    currency: 'SOL',
  });
  
  if (wallet.balance < withdrawal.amount) {
    throw new Error('Insufficient balance');
  }
  
  // 2. Lock the funds
  await db.wallets.update({
    userId: withdrawal.userId,
    currency: 'SOL',
    balance: wallet.balance - withdrawal.amount,
    lockedBalance: wallet.lockedBalance + withdrawal.amount,
  });
  
  try {
    // 3. Send on-chain transaction
    const transaction = new Transaction().add(
      SystemProgram.transfer({
        fromPubkey: hotWallet.publicKey,
        toPubkey: new PublicKey(withdrawal.destinationAddress),
        lamports: withdrawal.amount * 1e9,
      })
    );
    
    const signature = await sendAndConfirmTransaction(
      connection,
      transaction,
      [hotWallet]
    );
    
    // 4. Update transaction record
    await db.transactions.update({
      userId: withdrawal.userId,
      status: 'confirmed',
      txHash: signature,
    });
    
    // 5. Clear locked balance
    await db.wallets.update({
      userId: withdrawal.userId,
      currency: 'SOL',
      lockedBalance: wallet.lockedBalance - withdrawal.amount,
    });
    
  } catch (error) {
    // Rollback on failure
    await db.wallets.update({
      userId: withdrawal.userId,
      currency: 'SOL',
      balance: wallet.balance,
      lockedBalance: wallet.lockedBalance - withdrawal.amount,
    });
    throw error;
  }
}

Order Matching Engine

TypeScript
interface Order {
  id: string;
  userId: string;
  side: 'buy' | 'sell';
  price: number;
  quantity: number;
  filledQuantity: number;
}

class OrderBook {
  private bids: Order[] = []; // Buy orders, sorted high to low
  private asks: Order[] = []; // Sell orders, sorted low to high
  
  addOrder(order: Order): Order[] {
    const trades: Order[] = [];
    
    if (order.side === 'buy') {
      // Match against asks
      while (
        this.asks.length > 0 &&
        order.quantity > order.filledQuantity &&
        this.asks[0].price <= order.price
      ) {
        const match = this.asks[0];
        const fillQuantity = Math.min(
          order.quantity - order.filledQuantity,
          match.quantity - match.filledQuantity
        );
        
        order.filledQuantity += fillQuantity;
        match.filledQuantity += fillQuantity;
        
        trades.push({ ...match, quantity: fillQuantity });
        
        if (match.filledQuantity >= match.quantity) {
          this.asks.shift();
        }
      }
      
      // Add remaining to order book
      if (order.filledQuantity < order.quantity) {
        this.bids.push(order);
        this.bids.sort((a, b) => b.price - a.price);
      }
    } else {
      // Similar logic for sell orders...
    }
    
    return trades;
  }
}

Security Considerations

Hot/Cold Wallet Split

  • Keep only ~5% of assets in hot wallet
  • Cold wallet requires multi-signature approval
  • Regular audits and rebalancing

Rate Limiting

TypeScript
const rateLimit = require('express-rate-limit');

const withdrawalLimiter = rateLimit({
  windowMs: 24 * 60 * 60 * 1000, // 24 hours
  max: 5, // 5 withdrawals per day
  message: 'Too many withdrawal requests',
});

app.post('/api/withdraw', withdrawalLimiter, handleWithdrawal);

Transaction Signing Security

  • Hardware security modules (HSM) for production
  • Multi-party computation for key management
  • Regular key rotation

Learning Outcomes

After completing this project, you will understand:

  1. Hybrid Web2/Web3 architecture - Combining databases with blockchain
  2. Deposit monitoring - Tracking on-chain transactions
  3. Withdrawal security - Safe transaction signing and processing
  4. Order book mechanics - How exchanges match orders
  5. Financial system design - Balance management and reconciliation

Try It Yourself

  1. Set up PostgreSQL database with the schema above
  2. Create deposit address generation script
  3. Implement deposit monitoring service
  4. Build order matching engine
  5. Add withdrawal processing with proper security

Next: Prediction Market