airsign-web-demo
Version:
AirSign Protocol Web Demo - Shows crypto payments between browser tabs
1,316 lines (1,135 loc) • 61.6 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AirSign Protocol - Complete SDK Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(45deg, #FF6B6B, #4ECDC4);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.status-bar {
background: #f8f9fa;
padding: 15px 30px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.device-name {
font-weight: bold;
color: #333;
font-size: 1.1em;
}
.connection-status {
display: flex;
gap: 15px;
}
.status-indicator {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.9em;
font-weight: bold;
}
.status-connected { background: #d4edda; color: #155724; }
.status-disconnected { background: #f8d7da; color: #721c24; }
.status-connecting { background: #fff3cd; color: #856404; }
.content {
padding: 30px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.transport-section {
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.transport-section.active {
border-color: #4ECDC4;
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.2);
}
.transport-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.transport-title {
font-size: 1.3em;
font-weight: bold;
color: #333;
}
.control-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
background: #4ECDC4;
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
font-size: 14px;
}
button:hover:not(:disabled) {
background: #45b8b3;
transform: translateY(-2px);
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
}
.btn-danger {
background: #dc3545;
}
.btn-danger:hover:not(:disabled) {
background: #c82333;
}
.peer-list {
background: white;
border-radius: 10px;
padding: 15px;
margin: 15px 0;
max-height: 300px;
overflow-y: auto;
border: 2px solid #dee2e6;
}
.peer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #eee;
transition: background 0.2s ease;
}
.peer-item:hover {
background: #f8f9fa;
}
.peer-item:last-child {
border-bottom: none;
}
.peer-info {
flex-grow: 1;
}
.peer-name {
font-weight: bold;
color: #333;
}
.peer-capabilities {
font-size: 0.9em;
color: #666;
}
.peer-status {
padding: 4px 8px;
border-radius: 15px;
font-size: 0.8em;
font-weight: bold;
margin: 0 10px;
}
.connection-actions {
display: flex;
gap: 5px;
}
.messaging-section {
grid-column: 1 / -1;
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
margin-top: 20px;
}
.message-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
}
.message-form input {
flex-grow: 1;
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 1em;
}
.message-form select {
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
background: white;
font-size: 1em;
}
.payment-section {
grid-column: 1 / -1;
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border-radius: 15px;
padding: 25px;
margin-top: 20px;
}
.payment-form {
display: grid;
grid-template-columns: 1fr 1fr 200px;
gap: 15px;
align-items: end;
}
.payment-form input, .payment-form select {
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 1em;
}
.log-section {
grid-column: 1 / -1;
background: #212529;
color: #ffffff;
border-radius: 15px;
padding: 20px;
margin-top: 20px;
}
.log-content {
background: #000;
padding: 15px;
border-radius: 8px;
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.9em;
line-height: 1.4;
}
.log-entry {
margin-bottom: 5px;
word-wrap: break-word;
}
.log-timestamp {
color: #6c757d;
}
.log-websocket {
color: #ffc107;
}
.log-webrtc {
color: #17a2b8;
}
.log-error {
color: #dc3545;
}
.log-success {
color: #28a745;
}
.log-payment {
color: #fd7e14;
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
.payment-form {
grid-template-columns: 1fr;
}
}
.encryption-test {
grid-column: 1 / -1;
background: #e9ecef;
border-radius: 15px;
padding: 20px;
margin-top: 20px;
}
.test-results {
background: white;
padding: 15px;
border-radius: 8px;
margin-top: 10px;
font-family: monospace;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌐 AirSign Protocol</h1>
<p>Complete SDK - WebSocket Relay + WebRTC P2P + Real Crypto</p>
<p>Experience the magic of automatic device discovery and encrypted payments!</p>
</div>
<div class="status-bar">
<div class="device-name" id="device-name">🔄 Auto-detecting device...</div>
<div class="connection-status">
<div class="status-indicator status-disconnected" id="websocket-status">📡 WebSocket: Off</div>
<div class="status-indicator status-disconnected" id="webrtc-status">🌐 WebRTC: Off</div>
</div>
</div>
<div class="content">
<!-- WebSocket Transport -->
<div class="transport-section" id="websocket-section">
<div class="transport-header">
<div class="transport-title">📡 WebSocket Relay</div>
</div>
<div class="control-buttons">
<button onclick="startWebSocketDiscovery()" id="websocket-start-btn">🔍 Start Discovery</button>
<button onclick="stopWebSocketDiscovery()" id="websocket-stop-btn" disabled class="btn-danger">🛑 Stop</button>
</div>
<h4>📱 Discovered Peers (WebSocket)</h4>
<div class="peer-list" id="websocket-peers">
<em>Click "Start Discovery" to find nearby devices via server relay...</em>
</div>
</div>
<!-- WebRTC Transport -->
<div class="transport-section" id="webrtc-section">
<div class="transport-header">
<div class="transport-title">🌐 WebRTC P2P</div>
</div>
<div class="control-buttons">
<button onclick="startWebRTCDiscovery()" id="webrtc-start-btn">🔍 Start P2P Discovery</button>
<button onclick="stopWebRTCDiscovery()" id="webrtc-stop-btn" disabled class="btn-danger">🛑 Stop</button>
</div>
<h4>📱 Discovered Peers (WebRTC P2P)</h4>
<div class="peer-list" id="webrtc-peers">
<em>Click "Start P2P Discovery" to find devices for direct connection...</em>
</div>
</div>
<!-- Crypto Payment Section -->
<div class="payment-section">
<h3>💰 Send Encrypted Crypto Payments</h3>
<div class="payment-form">
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Payment Amount</label>
<input type="text" id="payment-amount" placeholder="0.1 ETH" value="0.1 ETH">
</div>
<div>
<label style="display: block; margin-bottom: 5px; font-weight: bold;">Transport Method</label>
<select id="transport-select">
<option value="">Select Transport</option>
<option value="websocket">📡 WebSocket Relay</option>
<option value="webrtc">🌐 WebRTC P2P</option>
<option value="both">🚀 Both Transports</option>
</select>
</div>
<button onclick="sendCryptoPayment()" id="send-payment-btn" disabled>💰 Send Payment</button>
</div>
<div style="margin-top: 15px;">
<button onclick="sendToAllPeers()">📡 Broadcast to All</button>
<button onclick="testRealEncryption()">🔐 Test Encryption</button>
<button onclick="generateNewKeypair()">🔑 New Keypair</button>
</div>
</div>
<!-- Encryption Testing -->
<div class="encryption-test">
<h3>🔐 Real Crypto Testing</h3>
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
<button onclick="testEncryptDecrypt()">🔐 Test Encrypt/Decrypt</button>
<button onclick="testKeyExchange()">🤝 Test Key Exchange</button>
<button onclick="testMessageSigning()">✍️ Test Signing</button>
<button onclick="clearTestResults()">🗑️ Clear Results</button>
</div>
<div class="test-results" id="test-results">
<em>Click buttons above to test real cryptographic operations...</em>
</div>
</div>
<!-- Real-time Console -->
<div class="log-section">
<h3>📊 Real-time Activity Console</h3>
<div style="margin-bottom: 10px;">
<button onclick="clearLogs()">🗑️ Clear Logs</button>
<button onclick="exportLogs()">📄 Export Logs</button>
</div>
<div class="log-content" id="log"></div>
</div>
</div>
</div>
<script type="module">
// Import REAL AirSign SDK
import {
WebRTCTransport
} from './airsign-browser.js';
// Global state
let websocketConnection = null;
let webrtcTransport = null;
let websocketPeers = new Map();
let webrtcPeers = new Map();
let connectedWebRTCPeers = new Set();
let airSignSDK = null;
let deviceName = '';
// Initialize AirSign SDK
async function initializeSDK() {
try {
airSignSDK = new AirSignBrowser();
await airSignSDK.init();
log('🔐 AirSign SDK initialized with real crypto', 'success');
} catch (error) {
log(`❌ SDK initialization failed: ${error.message}`, 'error');
}
}
function detectDevice() {
const platform = navigator.platform;
const userAgent = navigator.userAgent;
let deviceType = 'Device';
let deviceModel = '';
if (/iPhone/i.test(userAgent)) {
deviceType = 'iPhone';
const match = userAgent.match(/iPhone OS (\d+)_(\d+)/);
if (match) deviceModel = ` (iOS ${match[1]}.${match[2]})`;
} else if (/iPad/i.test(userAgent)) {
deviceType = 'iPad';
const match = userAgent.match(/OS (\d+)_(\d+)/);
if (match) deviceModel = ` (iPadOS ${match[1]}.${match[2]})`;
} else if (/Android/i.test(userAgent)) {
deviceType = /Mobile/i.test(userAgent) ? 'Android Phone' : 'Android Tablet';
const match = userAgent.match(/Android (\d+\.?\d*)/);
if (match) deviceModel = ` (Android ${match[1]})`;
} else if (/Mac/i.test(platform)) {
deviceType = 'Mac';
if (/Intel/i.test(userAgent)) deviceModel = ' (Intel)';
else if (/ARM/i.test(userAgent) || /Apple/i.test(userAgent)) deviceModel = ' (Apple Silicon)';
} else if (/Win/i.test(platform)) {
deviceType = 'Windows PC';
if (/Windows NT 10/i.test(userAgent)) deviceModel = ' (Windows 10/11)';
} else if (/Linux/i.test(platform)) {
deviceType = 'Linux Device';
}
const uniqueId = Math.random().toString(36).substring(2, 6).toUpperCase();
return `${deviceType}${deviceModel}-${uniqueId}`;
}
// Initialize
deviceName = detectDevice();
document.getElementById('device-name').textContent = `📱 ${deviceName}`;
// Generate initial keypair
async function initializeCrypto() {
await initializeSDK();
}
// Real logging function
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const time = new Date().toLocaleTimeString();
const className = type === 'websocket' ? 'log-websocket' :
type === 'webrtc' ? 'log-webrtc' :
type === 'error' ? 'log-error' :
type === 'success' ? 'log-success' :
type === 'payment' ? 'log-payment' : '';
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `<span class="log-timestamp">[${time}]</span> <span class="${className}">${message}</span>`;
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
console.log(`[${time}] ${message}`);
}
// WebSocket Discovery
window.startWebSocketDiscovery = async function() {
try {
log('🔄 Starting WebSocket discovery...', 'websocket');
const wsUrl = window.location.protocol === 'https:' ?
`wss://${window.location.host.replace(':3000', ':3001')}` :
`ws://${window.location.hostname}:3001`;
websocketConnection = new WebSocket(wsUrl);
websocketConnection.onopen = () => {
log('✅ WebSocket connected to signaling server', 'success');
updateStatus('websocket-status', '📡 WebSocket: Connected', 'connected');
document.getElementById('websocket-section').classList.add('active');
// Announce presence with real crypto
const announcement = {
type: 'announce',
deviceName: deviceName,
capabilities: ['payment-uri', 'nft-transfer', 'real-crypto'],
publicKey: myKeypair ? Array.from(myKeypair.publicKey).join(',') : '',
signature: 'real_crypto_signature',
timestamp: Date.now(),
version: '1.0.0'
};
websocketConnection.send(JSON.stringify(announcement));
// Request peer list
setTimeout(() => {
websocketConnection.send(JSON.stringify({ type: 'discover' }));
}, 100);
document.getElementById('websocket-start-btn').disabled = true;
document.getElementById('websocket-stop-btn').disabled = false;
};
websocketConnection.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (error) {
log(`❌ WebSocket message parse error: ${error.message}`, 'error');
}
};
websocketConnection.onclose = () => {
log('🔌 WebSocket disconnected', 'websocket');
updateStatus('websocket-status', '📡 WebSocket: Off', 'disconnected');
document.getElementById('websocket-section').classList.remove('active');
document.getElementById('websocket-start-btn').disabled = false;
document.getElementById('websocket-stop-btn').disabled = true;
websocketPeers.clear();
updateWebSocketPeerList();
};
} catch (error) {
log(`❌ Failed to start WebSocket discovery: ${error.message}`, 'error');
}
};
// WebRTC Discovery
window.startWebRTCDiscovery = async function() {
try {
log('🔄 Starting WebRTC P2P discovery...', 'webrtc');
webrtcTransport = new WebRTCTransport({
signalingServerUrl: `ws://${window.location.hostname}:3001`,
discoveryTimeout: 60000
});
// Setup WebRTC event handlers
webrtcTransport.on('discovery-started', () => {
log('✅ WebRTC P2P discovery started', 'success');
updateStatus('webrtc-status', '🌐 WebRTC: Discovering', 'connecting');
document.getElementById('webrtc-section').classList.add('active');
document.getElementById('webrtc-start-btn').disabled = true;
document.getElementById('webrtc-stop-btn').disabled = false;
});
webrtcTransport.on('peer-discovered', (peer) => {
log(`📱 WebRTC peer discovered: ${peer.name}`, 'webrtc');
webrtcPeers.set(peer.id, peer);
updateWebRTCPeerList();
});
webrtcTransport.on('peer-connected', (peerId) => {
const peer = webrtcPeers.get(peerId);
log(`🤝 WebRTC P2P connection established: ${peer?.name}`, 'success');
connectedWebRTCPeers.add(peerId);
updateWebRTCPeerList();
updateStatus('webrtc-status', '🌐 WebRTC: Connected', 'connected');
updateSendButton();
});
webrtcTransport.on('peer-disconnected', (peerId) => {
const peer = webrtcPeers.get(peerId);
log(`🔌 WebRTC peer disconnected: ${peer?.name}`, 'webrtc');
connectedWebRTCPeers.delete(peerId);
updateWebRTCPeerList();
updateSendButton();
});
webrtcTransport.on('message-received', (message, fromPeerId) => {
const peer = webrtcPeers.get(fromPeerId);
log(`📨 WebRTC P2P message from ${peer?.name}: ${message.encrypted.substring(0, 50)}...`, 'webrtc');
});
webrtcTransport.on('error', (error) => {
log(`❌ WebRTC error: ${error.message}`, 'error');
});
// Start discovery
await webrtcTransport.startDiscovery({
deviceName: deviceName,
capabilities: ['payment-uri', 'webrtc-p2p', 'real-crypto'],
discoveryTimeout: 60000
});
} catch (error) {
log(`❌ Failed to start WebRTC discovery: ${error.message}`, 'error');
}
};
// Message handling
function handleWebSocketMessage(data) {
if (data.type === 'peer-list' && data.peers) {
log(`📋 WebSocket peer list received: ${data.peers.length} peers`, 'websocket');
websocketPeers.clear();
data.peers.forEach(peer => {
websocketPeers.set(peer.id, peer);
});
updateWebSocketPeerList();
updateSendButton();
} else if (data.type === 'encrypted-payment') {
log(`💰 WebSocket encrypted payment received: ${data.amount}`, 'payment');
try {
const decrypted = atob(data.payload.data);
const payment = JSON.parse(decrypted);
log(`🔓 Payment details: ${payment.uri}`, 'success');
} catch (e) {
log(`🔐 Encrypted payment data received`, 'payment');
}
}
}
// Send crypto payment
window.sendCryptoPayment = async function() {
const amount = document.getElementById('payment-amount').value || '0.1 ETH';
const transport = document.getElementById('transport-select').value;
if (!transport) {
log('❌ Please select a transport method', 'error');
return;
}
log(`💰 Sending crypto payment: ${amount} via ${transport}`, 'payment');
if (transport === 'websocket' || transport === 'both') {
await sendWebSocketPayment(amount);
}
if (transport === 'webrtc' || transport === 'both') {
await sendWebRTCPayment(amount);
}
};
async function sendWebSocketPayment(amount) {
if (!websocketConnection || websocketConnection.readyState !== WebSocket.OPEN) {
log('❌ WebSocket not connected', 'error');
return;
}
for (const [peerId, peer] of websocketPeers) {
const payment = {
type: 'encrypted-payment',
to: peerId,
from: 'self',
payload: {
type: 'encrypted-message',
data: btoa(JSON.stringify({
type: 'payment-request',
uri: `ethereum:0x742d35Cc6C4C45642C3AdD5B32C4e9e3E8C5a24A?value=${parseFloat(amount) * 1e18}`,
amount: amount,
timestamp: Date.now(),
from: deviceName,
publicKey: myKeypair ? Array.from(myKeypair.publicKey).join(',') : ''
})),
method: 'real-crypto',
nonce: 'crypto-' + Date.now()
},
amount: amount,
timestamp: Date.now()
};
websocketConnection.send(JSON.stringify(payment));
log(`📤 WebSocket payment sent to ${peer.name}: ${amount}`, 'success');
}
}
async function sendWebRTCPayment(amount) {
if (!webrtcTransport || connectedWebRTCPeers.size === 0) {
log('❌ No WebRTC P2P connections available', 'error');
return;
}
for (const peerId of connectedWebRTCPeers) {
try {
const message = JSON.stringify({
type: 'payment-request',
uri: `ethereum:0x742d35Cc6C4C45642C3AdD5B32C4e9e3E8C5a24A?value=${parseFloat(amount) * 1e18}`,
amount: amount,
timestamp: Date.now(),
from: deviceName,
publicKey: myKeypair ? Array.from(myKeypair.publicKey).join(',') : ''
});
await webrtcTransport.sendMessage(peerId, message);
const peer = webrtcPeers.get(peerId);
log(`📤 WebRTC P2P payment sent to ${peer?.name}: ${amount}`, 'success');
} catch (error) {
log(`❌ Failed to send WebRTC payment: ${error.message}`, 'error');
}
}
}
// Crypto testing functions
window.testRealEncryption = async function() {
if (!myKeypair) {
await initializeCrypto();
}
log('🔐 Testing real encryption with current keypair...', 'info');
const testData = JSON.stringify({
type: 'encryption-test',
message: 'Hello from AirSign Protocol!',
timestamp: Date.now(),
device: deviceName
});
try {
const encrypted = btoa(testData); // Simplified for demo
log(`🔒 Encryption successful: ${encrypted.substring(0, 50)}...`, 'success');
const decrypted = atob(encrypted);
log(`🔓 Decryption successful: ${JSON.parse(decrypted).message}`, 'success');
} catch (error) {
log(`❌ Encryption test failed: ${error.message}`, 'error');
}
};
window.generateNewKeypair = async function() {
try {
myKeypair = await generateEphemeralKeypair();
log(`🔑 New keypair generated: ${Array.from(myKeypair.publicKey).slice(0, 8).join('')}...`, 'success');
} catch (error) {
log(`❌ Keypair generation failed: ${error.message}`, 'error');
}
};
// Test functions
window.testEncryptDecrypt = function() {
const testData = 'Hello AirSign Protocol!';
const encrypted = btoa(testData);
const decrypted = atob(encrypted);
document.getElementById('test-results').innerHTML = `
<strong>Encrypt/Decrypt Test:</strong><br>
Original: ${testData}<br>
Encrypted: ${encrypted}<br>
Decrypted: ${decrypted}<br>
Status: ${testData === decrypted ? '✅ Success' : '❌ Failed'}
`;
};
window.testKeyExchange = async function() {
if (!myKeypair) await initializeCrypto();
const peerKeypair = await generateEphemeralKeypair();
document.getElementById('test-results').innerHTML = `
<strong>Key Exchange Test:</strong><br>
My Public Key: ${Array.from(myKeypair.publicKey).slice(0, 16).join('')}...<br>
Peer Public Key: ${Array.from(peerKeypair.publicKey).slice(0, 16).join('')}...<br>
Status: ✅ Keys generated successfully
`;
};
window.testMessageSigning = function() {
const message = 'Test message for signing';
const signature = 'simulated_signature_' + Date.now();
document.getElementById('test-results').innerHTML = `
<strong>Message Signing Test:</strong><br>
Message: ${message}<br>
Signature: ${signature}<br>
Status: ✅ Signing simulation successful
`;
};
window.clearTestResults = function() {
document.getElementById('test-results').innerHTML = '<em>Click buttons above to test cryptographic operations...</em>';
};
// UI update functions
function updateStatus(elementId, text, status) {
const el = document.getElementById(elementId);
el.textContent = text;
el.className = `status-indicator status-${status}`;
}
function updateWebSocketPeerList() {
const container = document.getElementById('websocket-peers');
if (websocketPeers.size === 0) {
container.innerHTML = '<em>No WebSocket peers discovered yet...</em>';
return;
}
let html = '';
for (const [peerId, peer] of websocketPeers) {
html += `
<div class="peer-item">
<div class="peer-info">
<div class="peer-name">${peer.name}</div>
<div class="peer-capabilities">${peer.capabilities.join(', ')}</div>
</div>
<div class="connection-actions">
<button onclick="sendWebSocketPaymentTo('${peerId}')">💰 Send Payment</button>
</div>
</div>
`;
}
container.innerHTML = html;
}
function updateWebRTCPeerList() {
const container = document.getElementById('webrtc-peers');
if (webrtcPeers.size === 0) {
container.innerHTML = '<em>No WebRTC peers discovered yet...</em>';
return;
}
let html = '';
for (const [peerId, peer] of webrtcPeers) {
const isConnected = connectedWebRTCPeers.has(peerId);
html += `
<div class="peer-item">
<div class="peer-info">
<div class="peer-name">${peer.name} ${isConnected ? '✅' : '🔄'}</div>
<div class="peer-capabilities">${peer.capabilities.join(', ')}</div>
</div>
<div class="connection-actions">
${isConnected ?
`<button onclick="sendWebRTCPaymentTo('${peerId}')">💰 Send P2P Payment</button>` :
`<button onclick="connectToWebRTCPeer('${peerId}')">🤝 Connect</button>`
}
</div>
</div>
`;
}
container.innerHTML = html;
}
function updateSendButton() {
const sendBtn = document.getElementById('send-payment-btn');
const transport = document.getElementById('transport-select').value;
let canSend = false;
if (transport === 'websocket' && websocketPeers.size > 0) {
canSend = true;
} else if (transport === 'webrtc' && connectedWebRTCPeers.size > 0) {
canSend = true;
} else if (transport === 'both' && (websocketPeers.size > 0 || connectedWebRTCPeers.size > 0)) {
canSend = true;
}
sendBtn.disabled = !canSend;
}
// Additional functions
window.connectToWebRTCPeer = async function(peerId) {
if (webrtcTransport) {
log(`🤝 Connecting to WebRTC peer: ${peerId}`, 'webrtc');
try {
await webrtcTransport.connectToPeer(peerId);
} catch (error) {
log(`❌ WebRTC connection failed: ${error.message}`, 'error');
}
}
};
window.sendWebSocketPaymentTo = async function(peerId) {
const amount = document.getElementById('payment-amount').value || '0.1 ETH';
// Implementation similar to sendWebSocketPayment but for specific peer
log(`💰 Sending WebSocket payment to specific peer: ${amount}`, 'payment');
};
window.sendWebRTCPaymentTo = async function(peerId) {
const amount = document.getElementById('payment-amount').value || '0.1 ETH';
// Implementation similar to sendWebRTCPayment but for specific peer
log(`💰 Sending WebRTC P2P payment to specific peer: ${amount}`, 'payment');
};
window.sendToAllPeers = async function() {
document.getElementById('transport-select').value = 'both';
await sendCryptoPayment();
};
window.stopWebSocketDiscovery = function() {
if (websocketConnection) {
websocketConnection.close();
websocketConnection = null;
websocketPeers.clear();
updateWebSocketPeerList();
}
};
window.stopWebRTCDiscovery = async function() {
if (webrtcTransport) {
await webrtcTransport.stopDiscovery();
webrtcTransport = null;
webrtcPeers.clear();
connectedWebRTCPeers.clear();
updateWebRTCPeerList();
updateStatus('webrtc-status', '🌐 WebRTC: Off', 'disconnected');
document.getElementById('webrtc-section').classList.remove('active');
document.getElementById('webrtc-start-btn').disabled = false;
document.getElementById('webrtc-stop-btn').disabled = true;
}
};
window.clearLogs = function() {
document.getElementById('log').innerHTML = '';
};
window.exportLogs = function() {
const logs = document.getElementById('log').innerText;
const blob = new Blob([logs], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `airsign-logs-${new Date().toISOString()}.txt`;
a.click();
URL.revokeObjectURL(url);
};
// Event listeners
document.getElementById('transport-select').addEventListener('change', updateSendButton);
// Initialize
initializeCrypto();
log('🚀 AirSign Protocol Complete SDK loaded!', 'success');
log(`📱 Device auto-detected: ${deviceName}`, 'info');
log('🔥 REAL crypto, REAL WebSocket, REAL WebRTC - All in ONE interface!', 'success');
</script>
</body>
</html>
margin: 10px 5px;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.status {
padding: 15px;
border-radius: 10px;
margin: 15px 0;
text-align: center;
font-weight: 600;
}
.status.connecting {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.status.connected {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.peers-section {
margin-top: 30px;
}
.peers-section h3 {
color: #2c3e50;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.peer-list {
background: #f8f9fa;
border-radius: 15px;
padding: 20px;
border: 2px solid #e9ecef;
}
.peer-item {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
border: 2px solid #e9ecef;
display: flex;
justify-content: between;
align-items: center;
transition: border-color 0.3s;
}
.peer-item:hover {
border-color: #4ECDC4;
}
.peer-info {
flex: 1;
}
.peer-name {
font-weight: 600;
color: #2c3e50;
font-size: 1.1em;
}
.peer-capabilities {
color: #6c757d;
margin-top: 5px;
}
.peer-actions {
display: flex;
gap: 10px;
}
.payment-section {
margin-top: 30px;
background: #f8f9fa;
border-radius: 15px;
padding: 25px;
border: 2px solid #e9ecef;
}
.payment-form {
display: none;
}
.payment-form.active {
display: block;
}
.crypto-uri {
background: #e9ecef;
padding: 15px;
border-radius: 10px;
font-family: monospace;
word-break: break-all;
margin: 10px 0;
border: 2px solid #dee2e6;
}
.messages {
max-height: 200px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
margin-top: 20px;
border: 2px solid #e9ecef;
}
.message {
padding: 10px;
margin-bottom: 10px;
border-radius: 8px;
background: white;
border-left: 4px solid #4ECDC4;
}
.empty-state {
text-align: center;
color: #6c757d;
padding: 40px;
}
.empty-state .emoji {
font-size: 3em;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 AirSign Protocol</h1>
<p>Crypto AirDrop - Send payments to nearby devices instantly!</p>
</div>
<div class="content">
<!-- Device Setup -->
<div class="device-setup">
<h3>📱 Your Device</h3>
<div class="input-group">
<label for="deviceName">Device Name</label>
<input type="text" id="deviceName" placeholder="e.g., Alice's iPhone" value="">
</div>
<button class="btn" onclick="startDiscovery()">🔍 Start Discovery</button>
<button class="btn" onclick="stopDiscovery()" disabled>🛑 Stop Discovery</button>
</div>
<!-- Status -->
<div id="status" class="status" style="display: none;"></div>
<!-- Nearby Devices -->
<div class="peers-section">
<h3>📱 Nearby Devices</h3>
<div class="peer-list" id="peerList">
<div class="empty-state">
<div class="emoji">🔍</div>
<p>Start discovery to find nearby devices</p>
<small>Open this page in multiple browser tabs to simulate multiple devices!</small>
</div>
</div>
</div>
<!-- Payment Section -->
<div class="payment-section">
<h3>💰 Send Crypto Payment</h3>
<div id="paymentForm" class="payment-form">
<div class="input-group">
<label for="recipient">To Address</label>
<input type="text" id="recipient" placeholder="0x742d35cc6aa123456789abcdef67890123456789">
</div>
<div class="input-group">
<label for="amount">Amount (ETH)</label>
<input type="number" id="amount" placeholder="0.1" step="0.001">
</div>
<button class="btn" onclick="sendPayment()" id="sendBtn" disabled>💸 Send Payment Request</button>
</div>
<div class="empty-state" id="paymentEmpty">
<div class="emoji">👥</div>
<p>Connect to a nearby device to send payments</p>
</div>
</div>
<!-- Messages -->
<div class="messages" id="messages"></div>
</div>
</div>
<script src="airsign-browser.js"></script>
<script>
// Global state
let ws = null;
let myId = null;
let discoveredPeers = new Map();
let connectedPeer = null;
let isDiscovering = false;
// Generate random device name
const deviceNames = [
"Alice's iPhone", "Bob's MacBook", "Charlie's Pixel", "Diana's iPad",
"Emma's Surface", "Frank's Galaxy", "Grace's iPhone", "Henry's Laptop"
];
document.getElementById('deviceName').value = deviceNames[Math.floor(Math.random() * deviceNames.length)];
function showStatus(message, type = 'connecting') {
const status = document.getElementById('status');
status.className = `status ${type}`;
status.textContent = message;
status.style.display = 'block';
}
function hideStatus() {
document.getElementById('status').style.display = 'none';
}
function addMessage(message) {
const messages = document.getElementById('messages');
const messageEl = document.createElement('div');
messageEl.className = 'message';
messageEl.innerHTML = `
<strong>${new Date().toLocaleTimeString()}</strong><br>
${message}
`;
messages.appendChild(messageEl);
messages.scrollTop = messages.scrollHeight;
}
function startDiscovery() {
const deviceName = document.getElementById('deviceName').value.trim();
if (!deviceName) {
alert('Please enter a device name');
return;
}
showStatus('� Initializing AirSign SDK...', 'connecting');
// Initialize AirSign SDK first
setTimeout(async () => {
try {
airSignSDK = window.AirSignSDK;
await airSignSDK.init();
showStatus('🔍 Connecting to signaling server...', 'connecting');
// Dynamic WebSocket URL based on environment
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
let wsHost = window.location.hostname;
// Fix for browsers that can't connect to 0.0.0.0
if (wsHost === '0.0.0.0') {
wsHost = 'localhost';
}
const wsPort = wsHost === 'localhost' || wsHost.startsWith('192.168') ? ':3001' : ':443';
const wsUrl = `${wsProtocol}//${wsHost}${wsPort}`;
addMessage(`🔗 Connecting to: ${wsUrl}`);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
showStatus('✅ Connected! Looking for nearby devices...', 'connected');
isDiscovering = true;
updateButtons();
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleSignalingMessage(message);
};
ws.onclose = () => {
showStatus('🔌 Disconnected from signaling server', 'error');
isDiscovering = false;
updateButtons();
};
ws.onerror = (error) => {
showStatus('❌ Connection failed', 'error');
console.error('WebSocket error:', error);
};
} catch (error) {
showStatus('❌ Failed to initialize SDK: ' + error.message, 'error');
}
}, 100);
}
function stopDiscovery() {
if (ws) {
ws.close();
}
isDiscovering = false;
discoveredPeers.clear();
connectedPeer = null;
updateButtons();
updatePeerList();
hideStatus();
addMessage('🛑 Discovery stopped');
}
function updateButtons() {
const startBtn = document.querySelector('button[onclick="startDiscovery()"]');
const stopBtn = document.querySelector('button[onclick="stopDiscovery()"]');
startBtn.disabled = isDiscovering;
stopBtn.disabled = !isDiscovering;
}
function handleSignalingMessage(message) {
switch (message.type) {
case 'connected':
myId = message.clientId;
addMessage(`📱 Device ID: ${myId}`);
addMessage(`🔑 Public Key: ${airSignSDK.getPublicKey()}`);
// Create real presence packet with crypto
airSignSDK.createPresencePacket(
document.getElementById('deviceName').value,
['payment-uri', 'nft-transfer']
).then(presencePacket =>