ws402
Version:
WebSocket implementation of X402 protocol for pay-as-you-go digital resources with automatic refunds
411 lines (352 loc) • 10.8 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WS402 - HTTP Resource Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0a1628 0%, #152238 100%);
color: #e0f2fe;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: #00d4ff;
margin-bottom: 10px;
font-size: 2.5rem;
text-align: center;
}
.subtitle {
text-align: center;
color: #94a3b8;
margin-bottom: 30px;
font-size: 1.1rem;
}
.status-box {
background: #152238;
border: 1px solid #1e3a5f;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.status-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #1e3a5f;
}
.status-row:last-child {
border-bottom: none;
}
.label {
color: #00d4ff;
font-weight: 600;
}
.value {
color: #e0f2fe;
font-weight: 500;
}
.state {
display: inline-block;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
}
.state.connecting {
background: #f59e0b;
color: #0a1628;
}
.state.connected {
background: #10b981;
color: #0a1628;
}
.state.disconnected {
background: #ef4444;
color: white;
}
.resources {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.resource-card {
background: #152238;
border: 1px solid #1e3a5f;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
}
.resource-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.2);
border-color: #00d4ff;
}
.resource-card h3 {
color: #00d4ff;
margin-bottom: 10px;
font-size: 1.3rem;
}
.resource-info {
color: #94a3b8;
font-size: 0.9rem;
margin-bottom: 15px;
line-height: 1.6;
}
button {
background: linear-gradient(135deg, #00d4ff, #00ffcc);
color: #0a1628;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
width: 100%;
font-size: 1rem;
transition: all 0.3s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.4);
}
button:disabled {
background: #1e3a5f;
color: #64748b;
cursor: not-allowed;
transform: none;
}
.progress-bar {
width: 100%;
height: 8px;
background: #1e3a5f;
border-radius: 4px;
overflow: hidden;
margin-top: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00d4ff, #00ffcc);
transition: width 0.3s ease;
}
#pdfViewer {
width: 100%;
height: 700px;
border: 2px solid #1e3a5f;
border-radius: 12px;
background: white;
display: none;
margin-top: 20px;
}
#pdfViewer.active {
display: block;
}
.info-box {
background: #1e3a5f;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
}
.info-box strong {
color: #00d4ff;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}
</style>
</head>
<body>
<div class="container">
<h1>📄 WS402 HTTP Resource Demo</h1>
<p class="subtitle">Resources served via HTTP • Time tracking via WebSocket</p>
<div class="status-box">
<div class="status-row">
<span class="label">Connection:</span>
<span id="status" class="state disconnected">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">alice</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 wei</span>
</div>
<div class="status-row">
<span class="label">Remaining Balance:</span>
<span id="remaining" class="value">0 wei</span>
</div>
<div class="progress-bar">
<div id="progressBar" class="progress-fill" style="width: 0%"></div>
</div>
</div>
<div class="resources">
<div class="resource-card">
<h3>📋 Annual Report 2024</h3>
<div class="resource-info">
<strong>Type:</strong> PDF<br>
<strong>Price:</strong> 5 wei/second<br>
<strong>Estimated:</strong> 10 minutes<br>
<strong>Total:</strong> 3000 wei
</div>
<button onclick="accessResource('pdf-report-2024')">Access PDF</button>
</div>
<div class="resource-card">
<h3>📖 Complete Guide</h3>
<div class="resource-info">
<strong>Type:</strong> PDF<br>
<strong>Price:</strong> 3 wei/second<br>
<strong>Estimated:</strong> 30 minutes<br>
<strong>Total:</strong> 5400 wei
</div>
<button onclick="accessResource('ebook-guide')">Access eBook</button>
</div>
<div class="resource-card">
<h3>🖼️ Premium Image</h3>
<div class="resource-info">
<strong>Type:</strong> Image<br>
<strong>Price:</strong> 2 wei/second<br>
<strong>Estimated:</strong> 5 minutes<br>
<strong>Total:</strong> 600 wei
</div>
<button onclick="accessResource('image-premium')">Access Image</button>
</div>
</div>
<iframe id="pdfViewer"></iframe>
<div class="info-box">
<strong>💡 How it works:</strong><br>
1. You pay upfront for the estimated time<br>
2. The resource downloads via HTTP (complete PDF/image)<br>
3. WebSocket tracks time in the background<br>
4. When you close, automatic refund for unused time<br>
<br>
<strong>You only pay for the time you actually use!</strong> ✅
</div>
</div>
<script>
let ws = null;
let currentSession = null;
const $ = (id) => document.getElementById(id);
function setStatus(text, cssClass = 'disconnected') {
$('status').textContent = text;
$('status').className = `state ${cssClass}`;
}
async function accessResource(resourceId) {
try {
setStatus('Loading...', 'connecting');
console.log('Requesting schema for:', resourceId);
// Step 1: Get WS402 schema
const schemaRes = await fetch(`/api/resource/${resourceId}/schema`);
const { resource, ws402Schema } = await schemaRes.json();
console.log('Resource:', resource);
console.log('Schema:', ws402Schema);
setStatus('Connecting...', 'connecting');
// Step 2: Connect WebSocket
ws = new WebSocket(ws402Schema.websocketEndpoint + '&userId=alice');
ws.onopen = () => {
setStatus('Sending Payment...', 'connecting');
console.log('WebSocket connected');
// Step 3: Send payment proof
ws.send(JSON.stringify({
type: 'payment_proof',
proof: {
amount: ws402Schema.pricing.totalPrice,
userId: 'alice',
txId: 'mock_tx_' + Date.now(),
}
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('Message:', msg);
if (msg.type === 'session_started') {
setStatus('Session Active', 'connected');
currentSession = msg.sessionId;
$('sessionId').textContent = msg.sessionId.substring(0, 20) + '...';
$('remaining').textContent = msg.balance + ' wei';
}
if (msg.type === 'http_access_granted') {
console.log('HTTP access granted!');
setStatus('✅ Loading Resource...', 'connected');
// Step 4: Load resource via HTTP
const viewer = $('pdfViewer');
viewer.src = msg.resourceUrl;
viewer.classList.add('active');
setTimeout(() => {
setStatus('✅ Active - Tracking Time', 'connected');
}, 1000);
}
if (msg.type === 'usage_update') {
$('elapsed').textContent = msg.elapsedSeconds + 's';
$('consumed').textContent = msg.consumedAmount + ' wei';
$('remaining').textContent = msg.remainingBalance + ' wei';
const paidAmount = msg.consumedAmount + msg.remainingBalance;
const percentage = (msg.consumedAmount / paidAmount) * 100;
$('progressBar').style.width = percentage + '%';
}
if (msg.type === 'balance_exhausted') {
setStatus('Balance Exhausted', 'disconnected');
alert('Your balance has been exhausted. Closing session...');
setTimeout(() => ws.close(), 2000);
}
if (msg.type === 'payment_rejected') {
setStatus('Payment Rejected', 'disconnected');
alert('Payment was rejected: ' + msg.reason);
}
};
ws.onclose = () => {
setStatus('Disconnected - Refund Processed', 'disconnected');
$('pdfViewer').classList.remove('active');
console.log('WebSocket closed, refund processed automatically');
};
ws.onerror = (error) => {
setStatus('Error', 'disconnected');
console.error('WebSocket error:', error);
};
} catch (error) {
setStatus('Error', 'disconnected');
console.error('Error:', error);
alert('Error: ' + error.message);
}
}
// Close WebSocket when leaving page
window.addEventListener('beforeunload', () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
</script>
</body>
</html>