UNPKG

ws402

Version:

WebSocket implementation of X402 protocol for pay-as-you-go digital resources with automatic refunds

284 lines (258 loc) 8.42 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>WS402 Client Demo</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', -apple-system, sans-serif; background: linear-gradient(135deg, #0a1628 0%, #152238 100%); color: #e0f2fe; min-height: 100vh; padding: 2rem; } .container { max-width: 800px; margin: 0 auto; background: #152238; border-radius: 1rem; padding: 2rem; box-shadow: 0 8px 32px rgba(0, 212, 255, 0.1); border: 1px solid #1e3a5f; } h1 { color: #00d4ff; margin-bottom: 0.5rem; font-size: 2rem; } .subtitle { color: #94a3b8; margin-bottom: 2rem; } .status-box { background: #0a1628; border: 1px solid #1e3a5f; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; } .status-row { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #1e3a5f; } .status-row:last-child { border-bottom: none; } .label { color: #00d4ff; font-weight: 600; } .value { color: #e0f2fe; } .state { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 0.25rem; font-size: 0.875rem; font-weight: 600; } .state.connecting { background: #f59e0b; color: #0a1628; } .state.connected { background: #10b981; color: #0a1628; } .state.disconnected { background: #ef4444; color: white; } .progress-bar { width: 100%; height: 8px; background: #1e3a5f; border-radius: 4px; overflow: hidden; margin-top: 0.5rem; } .progress-fill { height: 100%; background: linear-gradient(90deg, #00d4ff, #00ffcc); transition: width 0.3s ease; } button { background: linear-gradient(135deg, #00d4ff, #00ffcc); color: #0a1628; border: none; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; cursor: pointer; margin-top: 1rem; width: 100%; font-size: 1rem; } button:hover { opacity: 0.9; } button:disabled { background: #1e3a5f; color: #64748b; cursor: not-allowed; } .input-group { margin: 1rem 0; } input { width: 100%; padding: 0.75rem; background: #0a1628; border: 1px solid #1e3a5f; border-radius: 0.5rem; color: #e0f2fe; font-size: 1rem; } input:focus { outline: none; border-color: #00d4ff; } .info { background: #1e3a5f; padding: 1rem; border-radius: 0.5rem; margin-top: 1rem; font-size: 0.875rem; color: #94a3b8; } </style> </head> <body> <div class="container"> <h1>WS402 Protocol Demo</h1> <p class="subtitle">Pay-as-you-go WebSocket resource access</p> <div class="status-box"> <div class="status-row"> <span class="label">Connection:</span> <span id="status" class="state connecting">Disconnected</span> </div> <div class="status-row"> <span class="label">Session ID:</span> <span id="sessionId" class="value"></span> </div> <div class="status-row"> <span class="label">User ID:</span> <span id="userId" class="value"></span> </div> </div> <div class="status-box"> <div class="status-row"> <span class="label">Time Elapsed:</span> <span id="elapsed" class="value">0s</span> </div> <div class="status-row"> <span class="label">Amount Consumed:</span> <span id="consumed" class="value">0</span> </div> <div class="status-row"> <span class="label">Remaining Balance:</span> <span id="remaining" class="value">0</span> </div> <div class="progress-bar"> <div id="progressBar" class="progress-fill" style="width: 0%"></div> </div> </div> <div class="status-box"> <div class="status-row"> <span class="label">Data Transferred:</span> <span id="bytes" class="value">0 bytes</span> </div> <div class="status-row"> <span class="label">Messages:</span> <span id="messages" class="value">0</span> </div> </div> <div class="input-group"> <input id="userInput" type="text" placeholder="Your User ID" value="alice" /> </div> <button id="connectBtn" onclick="connect()">Connect & Pay</button> <div class="info"> <strong>How it works:</strong> You'll pay upfront for 5 minutes of access (3000 wei). The server tracks your actual usage and automatically refunds unused balance when you disconnect. </div> </div> <script> let ws = null; let sessionData = {}; const $ = (id) => document.getElementById(id); async function connect() { const userId = $('userInput').value || 'guest'; $('userId').textContent = userId; $('status').textContent = 'Connecting...'; $('status').className = 'state connecting'; $('connectBtn').disabled = true; try { // Step 1: Get WS402 schema from server const schemaResponse = await fetch(`/ws402/schema/demo-resource?duration=300`); const schema = await schemaResponse.json(); console.log('WS402 Schema:', schema); // Step 2: Connect to WebSocket ws = new WebSocket(`wss://${window.location.host}?userId=${encodeURIComponent(userId)}`); ws.onopen = () => { $('status').textContent = 'Connected - Awaiting Payment'; $('status').className = 'state connecting'; // Step 3: Send payment proof (mock for demo) const paymentProof = { type: 'payment_proof', proof: { amount: schema.pricing.totalPrice, userId: userId, txId: `mock_tx_${Date.now()}`, timestamp: Date.now(), } }; ws.send(JSON.stringify(paymentProof)); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Message:', data); if (data.type === 'session_started') { $('status').textContent = 'Active'; $('status').className = 'state connected'; $('sessionId').textContent = data.sessionId; $('connectBtn').textContent = 'Disconnect'; $('connectBtn').disabled = false; $('connectBtn').onclick = disconnect; } if (data.type === 'usage_update') { $('elapsed').textContent = `${data.elapsedSeconds}s`; $('consumed').textContent = data.consumedAmount; $('remaining').textContent = data.remainingBalance; $('bytes').textContent = `${data.bytesTransferred} bytes`; $('messages').textContent = data.messageCount; const paidAmount = data.consumedAmount + data.remainingBalance; const percentage = (data.consumedAmount / paidAmount) * 100; $('progressBar').style.width = `${percentage}%`; } if (data.type === 'balance_exhausted' || data.type === 'max_duration_reached') { alert(data.message); disconnect(); } if (data.type === 'payment_rejected') { alert(`Payment rejected: ${data.reason}`); disconnect(); } }; ws.onclose = () => { $('status').textContent = 'Disconnected'; $('status').className = 'state disconnected'; $('connectBtn').textContent = 'Connect & Pay'; $('connectBtn').disabled = false; $('connectBtn').onclick = connect; }; ws.onerror = (error) => { console.error('WebSocket error:', error); $('status').textContent = 'Error'; $('status').className = 'state disconnected'; }; } catch (error) { console.error('Connection error:', error); alert('Failed to connect: ' + error.message); $('connectBtn').disabled = false; } } function disconnect() { if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } } </script> </body> </html>