HTML Sample MiniApp

The following represents a sample pure HTML MiniApp:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Simple Trader MiniApp</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #0a0a0a;
            color: #ffffff;
            padding: 16px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 600px;
            margin: 0 auto;
        }
        
        .header {
            text-align: center;
            margin-bottom: 24px;
            padding-bottom: 16px;
            border-bottom: 1px solid #333;
        }
        
        .status {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 16px;
            padding: 12px;
            background: #1a1a1a;
            border-radius: 8px;
            font-size: 14px;
        }
        
        .status-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 8px;
        }
        
        .status-dot.connected { background: #00ff88; }
        .status-dot.disconnected { background: #ff4444; }
        
        .card {
            background: #1a1a1a;
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 16px;
            border: 1px solid #333;
        }
        
        .card h3 {
            margin-bottom: 16px;
            color: #00ff88;
            font-size: 18px;
        }
        
        .form-group {
            margin-bottom: 16px;
        }
        
        .form-group label {
            display: block;
            margin-bottom: 4px;
            font-size: 14px;
            color: #ccc;
        }
        
        .form-group input, .form-group select {
            width: 100%;
            padding: 12px;
            background: #0a0a0a;
            border: 1px solid #444;
            border-radius: 6px;
            color: #fff;
            font-size: 14px;
        }
        
        .form-group input:focus, .form-group select:focus {
            outline: none;
            border-color: #00ff88;
        }
        
        .button {
            background: #00ff88;
            color: #000;
            border: none;
            padding: 12px 24px;
            border-radius: 6px;
            font-weight: 600;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.2s;
        }
        
        .button:hover {
            background: #00dd77;
            transform: translateY(-1px);
        }
        
        .button:disabled {
            background: #444;
            color: #888;
            cursor: not-allowed;
            transform: none;
        }
        
        .button.secondary {
            background: #333;
            color: #fff;
        }
        
        .button.secondary:hover {
            background: #444;
        }
        
        .button.danger {
            background: #ff4444;
            color: #fff;
        }
        
        .button.danger:hover {
            background: #dd3333;
        }
        
        .grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 12px;
        }
        
        .log {
            max-height: 200px;
            overflow-y: auto;
            background: #0a0a0a;
            border: 1px solid #333;
            border-radius: 6px;
            padding: 12px;
            font-family: 'Monaco', 'Menlo', monospace;
            font-size: 12px;
            line-height: 1.4;
        }
        
        .log-entry {
            margin-bottom: 4px;
            padding: 4px 0;
        }
        
        .log-entry.info { color: #00ff88; }
        .log-entry.error { color: #ff4444; }
        .log-entry.warning { color: #ffaa00; }
        .log-entry.debug { color: #888; }
        
        .permissions-list {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 8px;
            margin-top: 12px;
        }
        
        .permission-item {
            background: #0a0a0a;
            border: 1px solid #333;
            border-radius: 6px;
            padding: 8px 12px;
            font-size: 12px;
            text-align: center;
        }
        
        .permission-item.granted {
            border-color: #00ff88;
            color: #00ff88;
        }
        
        .market-data {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 12px;
            margin-top: 12px;
        }
        
        .market-item {
            background: #0a0a0a;
            border: 1px solid #333;
            border-radius: 6px;
            padding: 12px;
            text-align: center;
        }
        
        .market-item .symbol {
            font-size: 16px;
            font-weight: 600;
            margin-bottom: 4px;
        }
        
        .market-item .price {
            font-size: 14px;
            color: #00ff88;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🚀 Simple Trader</h1>
            <p>MiniApp for Based.One Terminal</p>
        </div>
        
        <div class="status">
            <div style="display: flex; align-items: center;">
                <div class="status-dot" id="connectionStatus"></div>
                <span id="connectionText">Disconnected</span>
            </div>
            <div>
                <span>Session: </span>
                <code id="sessionId">-</code>
            </div>
        </div>
        
        <!-- Trading Form -->
        <div class="card">
            <h3>📈 Place Order</h3>
            <div class="form-group">
                <label for="symbol">Symbol</label>
                <select id="symbol">
                    <option value="ETH">ETH</option>
                    <option value="BTC">BTC</option>
                    <option value="SOL">SOL</option>
                </select>
            </div>
            <div class="grid">
                <div class="form-group">
                    <label for="side">Side</label>
                    <select id="side">
                        <option value="buy">Buy</option>
                        <option value="sell">Sell</option>
                    </select>
                </div>
                <div class="form-group">
                    <label for="orderType">Order Type</label>
                    <select id="orderType">
                        <option value="market">Market</option>
                        <option value="limit">Limit</option>
                    </select>
                </div>
            </div>
            <div class="grid">
                <div class="form-group">
                    <label for="size">Size</label>
                    <input type="number" id="size" step="0.001" placeholder="0.1">
                </div>
                <div class="form-group">
                    <label for="price">Price (if limit)</label>
                    <input type="number" id="price" step="0.01" placeholder="2500.00">
                </div>
            </div>
            <div class="grid">
                <button class="button" id="placeOrderBtn" onclick="placeOrder()">
                    Place Order
                </button>
                <button class="button danger" id="cancelAllBtn" onclick="cancelAllOrders()">
                    Cancel All
                </button>
            </div>
        </div>
        
        <!-- Market Data -->
        <div class="card">
            <h3>📊 Market Data</h3>
            <button class="button secondary" onclick="subscribeToMarketData()">
                Subscribe to Market Data
            </button>
            <div class="market-data" id="marketData">
                <!-- Market data will be populated here -->
            </div>
        </div>
        
        <!-- Permissions -->
        <div class="card">
            <h3>🔐 Permissions</h3>
            <button class="button secondary" onclick="requestAllPermissions()">
                Request Permissions
            </button>
            <div class="permissions-list" id="permissionsList">
                <!-- Permissions will be populated here -->
            </div>
        </div>
        
        <!-- Debug Log -->
        <div class="card">
            <h3>🐛 Debug Log</h3>
            <button class="button secondary" onclick="clearLog()" style="margin-bottom: 12px;">
                Clear Log
            </button>
            <div class="log" id="debugLog">
                <!-- Debug messages will appear here -->
            </div>
        </div>
    </div>

    <script>
        // MiniApp SDK Integration
        let isConnected = false;
        let sessionId = null;
        let permissions = new Set();
        
        // Initialize connection when page loads
        window.addEventListener('load', () => {
            initializeMiniApp();
        });
        
        // Initialize the MiniApp connection
        function initializeMiniApp() {
            log('🚀 Initializing Simple Trader MiniApp...', 'info');
            
            // Set as connecting initially
            updateConnectionStatus(false);
            
            // Request connection to the parent terminal
            sendToParent({
                type: 'CONNECT',
                appId: 'simple-trader',
                appName: 'Simple Trader',
                version: '1.0.0'
            });
            
            // Also check if we're already in a connected state by trying to communicate
            // Sometimes the parent connection happens before the iframe fully loads
            setTimeout(() => {
                if (!isConnected) {
                    log('🔄 Retrying connection...', 'info');
                    sendToParent({
                        type: 'CONNECT',
                        appId: 'simple-trader',
                        appName: 'Simple Trader',
                        version: '1.0.0'
                    });
                }
            }, 2000);
        }
        
        // Send message to parent terminal
        function sendToParent(data) {
            if (window.parent && window.parent !== window) {
                const message = {
                    id: generateId(),
                    timestamp: Date.now(),
                    appId: 'simple-trader',
                    sessionId: sessionId,
                    ...data
                };
                
                log(`📤 Sending: ${data.type}`, 'debug');
                window.parent.postMessage(message, '*');
            } else {
                log('❌ No parent window found', 'error');
            }
        }
        
        // Listen for messages from parent terminal
        window.addEventListener('message', (event) => {
            try {
                const message = event.data;
                
                if (!message || typeof message !== 'object') return;
                
                log(`📥 Received: ${message.type || 'unknown'}`, 'debug');
                handleParentMessage(message);
            } catch (error) {
                log(`❌ Error handling message: ${error.message}`, 'error');
            }
        });
        
        // Handle messages from parent terminal
        function handleParentMessage(message) {
            log(`📥 Handling message type: ${message.type}`, 'debug');
            
            switch (message.type) {
                case 'connected':
                case 'CONNECTED':
                    handleConnection(message);
                    break;
                case 'disconnected':
                case 'DISCONNECTED':
                    handleDisconnection();
                    break;
                case 'permissionGranted':
                case 'PERMISSION_GRANTED':
                    handlePermissionGranted(message);
                    break;
                case 'permissionsLoaded':
                case 'PERMISSIONS_LOADED':
                    handlePermissionsLoaded(message);
                    break;
                case 'permissionDenied':
                case 'PERMISSION_DENIED':
                    handlePermissionDenied(message);
                    break;
                case 'marketData':
                case 'MARKET_DATA':
                    handleMarketData(message);
                    break;
                case 'orderResponse':
                case 'ORDER_RESPONSE':
                    handleOrderResponse(message);
                    break;
                case 'error':
                case 'ERROR':
                    handleError(message);
                    break;
                case 'PERMISSION_DENIED':
                    handlePermissionDeniedError(message);
                    break;
                // Handle PostMessage responses
                case undefined:
                    // Check for response structure
                    if (message.success !== undefined && message.correlationId) {
                        log('📨 Received response message', 'debug');
                        // This could be a response to our CONNECT message
                        if (message.success && message.data && message.data.connected) {
                            handleConnection({
                                type: 'connected',
                                sessionId: message.data.sessionId,
                                success: true
                            });
                        }
                    } else {
                        log(`🤔 Unknown message structure:`, 'warning');
                        log(JSON.stringify(message, null, 2), 'debug');
                    }
                    break;
                default:
                    log(`🤔 Unknown message type: ${message.type}`, 'warning');
                    log('Full message:', 'debug');
                    log(JSON.stringify(message, null, 2), 'debug');
            }
        }
        
        // Handle connection establishment
        function handleConnection(message) {
            isConnected = true;
            sessionId = message.sessionId || 'connected';
            
            updateConnectionStatus(true);
            document.getElementById('sessionId').textContent = sessionId;
            
            log('✅ Connected to Based.One Terminal', 'info');
            log(`🔗 Session ID: ${sessionId}`, 'debug');
            
            // Query existing permissions first, then request basic permissions after connection
            setTimeout(() => {
                queryExistingPermissions();
            }, 500);
            
            // Request basic permissions as fallback after a delay
            setTimeout(() => {
                requestBasicPermissions();
            }, 1500);
        }
        
        // Handle disconnection
        function handleDisconnection() {
            isConnected = false;
            sessionId = null;
            permissions.clear();
            
            updateConnectionStatus(false);
            document.getElementById('sessionId').textContent = '-';
            
            log('❌ Disconnected from terminal', 'warning');
        }
        
        // Handle permission granted
        function handlePermissionGranted(message) {
            const permission = message.permission;
            permissions.add(permission);
            
            log(`✅ Permission granted: ${permission}`, 'info');
            updatePermissionsDisplay();
        }
        
        // Handle permissions loaded (bulk permission status)
        function handlePermissionsLoaded(message) {
            const loadedPermissions = message.permissions || [];
            log(`📋 Loaded ${loadedPermissions.length} existing permissions`, 'info');
            
            loadedPermissions.forEach(permission => {
                permissions.add(permission);
                log(`✅ Existing permission: ${permission}`, 'debug');
            });
            
            updatePermissionsDisplay();
        }
        
        // Handle permission denied
        function handlePermissionDenied(message) {
            const permission = message.permission;
            log(`❌ Permission denied: ${permission}`, 'warning');
        }
        
        // Handle market data updates
        function handleMarketData(message) {
            const data = message.data;
            log(`📊 Market data: ${data.symbol} = $${data.price}`, 'info');
            updateMarketDataDisplay(data);
        }
        
        // Handle order responses
        function handleOrderResponse(message) {
            if (message.success) {
                log(`✅ Order placed: ${message.orderId}`, 'info');
            } else {
                log(`❌ Order failed: ${message.error}`, 'error');
            }
        }
        
        // Handle errors
        function handleError(message) {
            const error = message.error || 'Unknown error';
            log(`❌ Error: ${error}`, 'error');
            
            // Check if this is a permission-related error
            if (error.includes('Permission denied') || error.includes('PERMISSION_DENIED')) {
                handlePermissionError(error);
            }
        }
        
        // Handle permission denied errors by automatically requesting permissions
        function handlePermissionDeniedError(message) {
            const error = message.error || 'Permission denied';
            log(`🔐 Permission denied: ${error}`, 'warning');
            handlePermissionError(error);
        }
        
        // Handle permission errors by requesting the missing permissions
        function handlePermissionError(errorMessage) {
            let requiredPermissions = [];
            let actionDescription = 'perform this action';
            
            // Parse error message to determine required permissions
            if (errorMessage.includes('place_orders') || errorMessage.includes('placing orders')) {
                requiredPermissions = ['place_orders', 'read_balance', 'read_positions'];
                actionDescription = 'place orders';
            } else if (errorMessage.includes('cancel_orders') || errorMessage.includes('canceling orders')) {
                requiredPermissions = ['cancel_orders'];
                actionDescription = 'cancel orders';
            } else if (errorMessage.includes('read_market_data') || errorMessage.includes('market data')) {
                requiredPermissions = ['read_market_data'];
                actionDescription = 'access market data';
            } else if (errorMessage.includes('read_positions') || errorMessage.includes('positions')) {
                requiredPermissions = ['read_positions'];
                actionDescription = 'read positions';
            } else if (errorMessage.includes('read_orders') || errorMessage.includes('orders')) {
                requiredPermissions = ['read_orders'];
                actionDescription = 'read orders';
            } else if (errorMessage.includes('read_account') || errorMessage.includes('account')) {
                requiredPermissions = ['read_account', 'read_balance'];
                actionDescription = 'access account information';
            } else {
                // Default: request common trading permissions
                requiredPermissions = ['place_orders', 'cancel_orders', 'read_market_data', 'read_positions'];
                actionDescription = 'perform trading operations';
            }
            
            if (requiredPermissions.length > 0) {
                log(`🔄 Auto-requesting permissions for: ${actionDescription}`, 'info');
                requestSpecificPermissions(requiredPermissions, actionDescription);
            }
        }
        
        // Query existing permissions on first load
        function queryExistingPermissions() {
            log('🔍 Querying existing permissions...', 'info');
            
            sendToParent({
                type: 'GET_PERMISSIONS',
                payload: {}
            });
        }
        
        // Request basic permissions
        function requestBasicPermissions() {
            const basicPermissions = [
                'read_market_data',
                'place_orders',
                'cancel_orders',
                'read_positions'
            ];
            
            basicPermissions.forEach(permission => {
                sendToParent({
                    type: 'REQUEST_PERMISSION',
                    payload: {
                        permissions: [permission]
                    }
                });
            });
        }
        
        // Request specific permissions with context
        function requestSpecificPermissions(requiredPermissions, actionDescription) {
            log(`🔐 Requesting permissions for: ${actionDescription}`, 'info');
            
            sendToParent({
                type: 'REQUEST_PERMISSION',
                payload: {
                    permissions: requiredPermissions
                }
            });
        }
        
        // Request all permissions
        function requestAllPermissions() {
            const allPermissions = [
                'read_market_data',
                'read_orderbook',
                'read_trades',
                'read_candles',
                'read_account',
                'read_positions',
                'read_orders',
                'read_balance',
                'place_orders',
                'cancel_orders',
                'modify_orders',
                'read_user_settings'
            ];
            
            sendToParent({
                type: 'REQUEST_PERMISSION',
                payload: {
                    permissions: allPermissions
                }
            });
        }
        
        // Place order
        function placeOrder() {
            if (!isConnected) {
                log('❌ Not connected to terminal', 'error');
                return;
            }
            
            if (!permissions.has('place_orders')) {
                log('❌ Missing place_orders permission - requesting...', 'warning');
                requestSpecificPermissions(['place_orders'], 'place an order');
                return;
            }
            
            const symbol = document.getElementById('symbol').value;
            const side = document.getElementById('side').value;
            const orderType = document.getElementById('orderType').value;
            const size = parseFloat(document.getElementById('size').value);
            const price = parseFloat(document.getElementById('price').value);
            
            if (!size || size <= 0) {
                log('❌ Invalid order size', 'error');
                return;
            }
            
            if (orderType === 'limit' && (!price || price <= 0)) {
                log('❌ Invalid price for limit order', 'error');
                return;
            }
            
            const orderData = {
                symbol,
                side,
                orderType,
                size,
                ...(orderType === 'limit' && { price })
            };
            
            sendToParent({
                type: 'COMMAND',
                action: 'placeOrder',
                payload: orderData
            });
            
            log(`📝 Placing ${side} order: ${size} ${symbol} @ ${orderType}`, 'info');
        }
        
        // Cancel all orders
        function cancelAllOrders() {
            if (!isConnected) {
                log('❌ Not connected to terminal', 'error');
                return;
            }
            
            if (!permissions.has('cancel_orders')) {
                log('❌ Missing cancel_orders permission - requesting...', 'warning');
                requestSpecificPermissions(['cancel_orders'], 'cancel orders');
                return;
            }
            
            sendToParent({
                type: 'COMMAND',
                action: 'cancelAllOrders',
                payload: {}
            });
            
            log('🗑️ Cancelling all orders...', 'info');
        }
        
        // Subscribe to market data
        function subscribeToMarketData() {
            if (!isConnected) {
                log('❌ Not connected to terminal', 'error');
                return;
            }
            
            if (!permissions.has('read_market_data')) {
                log('❌ Missing read_market_data permission - requesting...', 'warning');
                requestSpecificPermissions(['read_market_data'], 'subscribe to market data');
                return;
            }
            
            const symbols = ['ETH', 'BTC', 'SOL'];
            
            symbols.forEach(symbol => {
                sendToParent({
                    type: 'SUBSCRIBE',
                    payload: {
                        dataType: 'MARKET_DATA',
                        symbol: symbol
                    }
                });
            });
            
            log('📡 Subscribed to market data', 'info');
        }
        
        // Update connection status display
        function updateConnectionStatus(connected) {
            const statusDot = document.getElementById('connectionStatus');
            const statusText = document.getElementById('connectionText');
            
            if (connected) {
                statusDot.className = 'status-dot connected';
                statusText.textContent = 'Connected';
            } else {
                statusDot.className = 'status-dot disconnected';
                statusText.textContent = 'Disconnected';
            }
        }
        
        // Update permissions display
        function updatePermissionsDisplay() {
            const container = document.getElementById('permissionsList');
            container.innerHTML = '';
            
            const allPermissions = [
                'read_market_data',
                'place_orders',
                'cancel_orders',
                'read_positions',
                'read_orders',
                'read_balance'
            ];
            
            allPermissions.forEach(permission => {
                const item = document.createElement('div');
                item.className = `permission-item ${permissions.has(permission) ? 'granted' : ''}`;
                item.textContent = permission.replace(/_/g, ' ');
                container.appendChild(item);
            });
        }
        
        // Update market data display
        function updateMarketDataDisplay(data) {
            const container = document.getElementById('marketData');
            
            // Only update display if we have valid price data
            const price = (data.price != null && !isNaN(data.price) && data.price > 0) ? data.price : null;
            
            if (price === null) {
                log(`⚠️ Ignoring invalid price data for ${data.symbol}: ${data.price}`, 'debug');
                return; // Don't update display with invalid data
            }
            
            let item = container.querySelector(`[data-symbol="${data.symbol}"]`);
            if (!item) {
                item = document.createElement('div');
                item.className = 'market-item';
                item.setAttribute('data-symbol', data.symbol);
                container.appendChild(item);
            }
            
            item.innerHTML = `
                <div class="symbol">${data.symbol}</div>
                <div class="price">$${price.toFixed(2)}</div>
            `;
        }
        
        // Utility functions
        function generateId() {
            return 'msg_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
        }
        
        function log(message, level = 'info') {
            const logContainer = document.getElementById('debugLog');
            const entry = document.createElement('div');
            entry.className = `log-entry ${level}`;
            
            const timestamp = new Date().toLocaleTimeString();
            entry.textContent = `[${timestamp}] ${message}`;
            
            logContainer.appendChild(entry);
            logContainer.scrollTop = logContainer.scrollHeight;
            
            // Limit log entries
            while (logContainer.children.length > 100) {
                logContainer.removeChild(logContainer.firstChild);
            }
        }
        
        function clearLog() {
            document.getElementById('debugLog').innerHTML = '';
        }
        
        // Handle order type change
        document.getElementById('orderType').addEventListener('change', (e) => {
            const priceField = document.getElementById('price');
            if (e.target.value === 'market') {
                priceField.disabled = true;
                priceField.style.opacity = '0.5';
            } else {
                priceField.disabled = false;
                priceField.style.opacity = '1';
            }
        });
        
        // Initialize price field state
        document.getElementById('orderType').dispatchEvent(new Event('change'));
    </script>
</body>
</html>

Last updated