ws402
Version:
WebSocket implementation of X402 protocol for pay-as-you-go digital resources with automatic refunds
284 lines (258 loc) • 8.42 kB
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>