class BridgeRealtimeClient {
constructor(userAddress) {
this.userAddress = userAddress;
this.socket = null;
this.reconnectInterval = 5000; // 5 seconds
this.maxReconnectAttempts = 10;
this.reconnectAttempts = 0;
this.isConnecting = false;
this.eventHandlers = {};
// Bind methods to preserve 'this' context
this.connect = this.connect.bind(this);
this.disconnect = this.disconnect.bind(this);
this.handleMessage = this.handleMessage.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleError = this.handleError.bind(this);
}
connect() {
if (this.isConnecting || (this.socket && this.socket.readyState === WebSocket.CONNECTING)) {
return;
}
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
return;
}
this.isConnecting = true;
try {
// Use ws:// for development, wss:// for production
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws/${encodeURIComponent(this.userAddress)}`;
console.log('Connecting to WebSocket:', wsUrl);
this.socket = new WebSocket(wsUrl);
this.socket.onopen = () => {
console.log('WebSocket connected');
this.isConnecting = false;
this.reconnectAttempts = 0;
this.triggerEvent('connected');
// Send a ping to confirm connection
this.send({type: 'ping'});
};
this.socket.onmessage = this.handleMessage;
this.socket.onclose = this.handleClose;
this.socket.onerror = this.handleError;
} catch (error) {
console.error('Error creating WebSocket connection:', error);
this.isConnecting = false;
this.scheduleReconnect();
}
}
disconnect() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent auto-reconnect
}
handleMessage(event) {
try {
const data = JSON.parse(event.data);
console.log('Received WebSocket message:', data);
switch (data.type) {
case 'pong':
// Handle ping/pong for keepalive
break;
case 'cas_deposit_update':
this.triggerEvent('casDepositUpdate', data.data);
break;
case 'wcas_return_intention_update':
this.triggerEvent('wcasReturnIntentionUpdate', data.data);
break;
case 'polygon_transaction_update':
this.triggerEvent('polygonTransactionUpdate', data.data);
break;
case 'error':
console.error('WebSocket error:', data.message);
this.triggerEvent('error', data.message);
break;
default:
console.log('Unknown message type:', data.type);
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
}
handleClose(event) {
console.log('WebSocket closed:', event.code, event.reason);
this.socket = null;
this.isConnecting = false;
this.triggerEvent('disconnected');
// Attempt to reconnect unless it was a clean close
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect();
}
}
handleError(error) {
console.error('WebSocket error:', error);
this.isConnecting = false;
this.triggerEvent('error', error);
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('Max reconnect attempts reached');
this.triggerEvent('maxReconnectAttemptsReached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectInterval * this.reconnectAttempts;
console.log(`Reconnecting in ${delay/1000} seconds... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.triggerEvent('reconnecting', {attempt: this.reconnectAttempts, delay: delay});
setTimeout(this.connect, delay);
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
console.warn('Cannot send message: WebSocket not connected');
}
}
requestStatusUpdate() {
this.send({type: 'request_status_update'});
}
// Event handling methods
on(event, handler) {
if (!this.eventHandlers[event]) {
this.eventHandlers[event] = [];
}
this.eventHandlers[event].push(handler);
}
off(event, handler) {
if (this.eventHandlers[event]) {
const index = this.eventHandlers[event].indexOf(handler);
if (index > -1) {
this.eventHandlers[event].splice(index, 1);
}
}
}
triggerEvent(event, data) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
}
}
// Utility functions for updating the UI
class BridgeUIUpdater {
constructor() {
this.statusColors = {
'pending': '#ffc107',
'pending_deposit': '#ffc107',
'pending_confirmation': '#17a2b8',
'confirmed': '#28a745',
'completed': '#28a745',
'failed': '#dc3545',
'expired': '#6c757d'
};
this.statusLabels = {
'pending': 'Pending',
'pending_deposit': 'Pending Deposit',
'pending_confirmation': 'Confirming',
'deposit_detected': 'Deposit Detected',
'confirmed': 'Confirmed',
'completed': 'Completed',
'processed': 'Processed',
'failed': 'Failed',
'expired': 'Expired'
};
}
updateCasDepositStatus(deposit) {
const responseArea = document.getElementById('responseArea');
const responseText = document.getElementById('responseText');
if (!responseArea || !responseText) return;
const statusColor = this.statusColors[deposit.status] || '#6c757d';
const statusLabel = this.statusLabels[deposit.status] || deposit.status;
let html = `
🔄 Bridge Status: ${statusLabel}
Deposit ID: ${deposit.id}
Cascoin Deposit Address:
${deposit.cascoin_deposit_address}
Your Polygon Address:
${deposit.polygon_address}
`;
if (deposit.received_amount) {
html += `
Received Amount: ${deposit.received_amount} CAS
`;
}
if (deposit.mint_tx_hash) {
html += `
Mint Transaction:
${deposit.mint_tx_hash}
`;
}
// Add confirmation progress if transaction is being confirmed
if (deposit.current_confirmations !== undefined && deposit.required_confirmations !== undefined) {
const confirmationsPercent = Math.min((deposit.current_confirmations / deposit.required_confirmations) * 100, 100);
html += `
Confirmations:
${deposit.current_confirmations}/${deposit.required_confirmations}
`;
}
if (deposit.deposit_tx_hash) {
html += `
Deposit Transaction:
${deposit.deposit_tx_hash}
`;
}
html += `
Created: ${new Date(deposit.created_at).toLocaleString()}
Last Updated: ${new Date(deposit.updated_at).toLocaleString()}
`;
// Add instructions based on status
if (deposit.status === 'pending') {
html += `
💡 Next Step: Send CAS to the deposit address above to start the bridge process.
`;
} else if (deposit.status === 'pending_confirmation') {
html += `
⏳ Processing: Your deposit has been detected and is being confirmed on the Cascoin network.
`;
} else if (deposit.status === 'completed') {
html += `
✅ Complete! Your wCAS tokens have been minted and sent to your Polygon address.
`;
}
responseText.innerHTML = html;
responseArea.style.display = 'block';
}
updateWcasReturnIntentionStatus(intention) {
const responseArea = document.getElementById('responseArea');
const responseText = document.getElementById('responseText');
if (!responseArea || !responseText) return;
const statusColor = this.statusColors[intention.status] || '#6c757d';
const statusLabel = this.statusLabels[intention.status] || intention.status;
let html = `
🔄 Bridge Status: ${statusLabel}
Return Intention ID: ${intention.id}
From Polygon Address:
${intention.user_polygon_address}
To Cascoin Address:
${intention.target_cascoin_address}
Bridge Amount: ${intention.bridge_amount} wCAS
Fee Model: ${intention.fee_model}
`;
// Add confirmation progress if transaction is being confirmed
if (intention.current_confirmations !== undefined && intention.required_confirmations !== undefined) {
const confirmationsPercent = Math.min((intention.current_confirmations / intention.required_confirmations) * 100, 100);
html += `
Confirmations:
${intention.current_confirmations}/${intention.required_confirmations}
`;
}
html += `
Created: ${new Date(intention.created_at).toLocaleString()}
Last Updated: ${new Date(intention.updated_at).toLocaleString()}
`;
// Add instructions based on status
if (intention.status === 'pending_deposit') {
html += `
💡 Next Step: Send ${intention.bridge_amount} wCAS from your Polygon address to the bridge contract to complete the return process.
`;
} else if (intention.status === 'deposit_detected') {
html += `
⏳ Processing: Your wCAS deposit has been detected and CAS is being sent to your Cascoin address.
`;
} else if (intention.status === 'processed') {
html += `
✅ Complete! Your CAS has been sent to your Cascoin address.
`;
}
responseText.innerHTML = html;
responseArea.style.display = 'block';
}
updatePolygonTransactionStatus(polygonTx) {
const responseArea = document.getElementById('responseArea');
const responseText = document.getElementById('responseText');
if (!responseArea || !responseText) return;
const statusColor = this.statusColors[polygonTx.status] || '#6c757d';
const statusLabel = this.statusLabels[polygonTx.status] || polygonTx.status;
let html = `
🔄 Polygon Transaction Status: ${statusLabel}
Transaction ID: ${polygonTx.id}
From Address:
${polygonTx.from_address}
Target CAS Address:
${polygonTx.user_cascoin_address_request}
Amount: ${polygonTx.amount} wCAS
Polygon TX Hash:
${polygonTx.polygon_tx_hash}
`;
// Add confirmation progress if transaction is being confirmed
if (polygonTx.current_confirmations !== undefined && polygonTx.required_confirmations !== undefined) {
const confirmationsPercent = Math.min((polygonTx.current_confirmations / polygonTx.required_confirmations) * 100, 100);
html += `
Confirmations:
${polygonTx.current_confirmations}/${polygonTx.required_confirmations}
`;
}
if (polygonTx.cas_release_tx_hash) {
html += `
CAS Release TX Hash:
${polygonTx.cas_release_tx_hash}
`;
}
html += `
Created: ${new Date(polygonTx.created_at).toLocaleString()}
Last Updated: ${new Date(polygonTx.updated_at).toLocaleString()}
`;
// Add instructions based on status
if (polygonTx.status === 'pending_confirmation') {
html += `
⏳ Processing: Your wCAS transaction is being confirmed on the Polygon network.
`;
} else if (polygonTx.status === 'completed') {
html += `
✅ Complete! Your CAS tokens have been released to your Cascoin address.
`;
}
responseText.innerHTML = html;
responseArea.style.display = 'block';
}
showConnectionStatus(isConnected, message = '') {
let statusElement = document.getElementById('connectionStatus');
if (!statusElement) {
statusElement = document.createElement('div');
statusElement.id = 'connectionStatus';
statusElement.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
z-index: 1000;
transition: all 0.3s ease;
`;
document.body.appendChild(statusElement);
}
if (isConnected) {
statusElement.style.backgroundColor = '#d4edda';
statusElement.style.color = '#155724';
statusElement.style.border = '1px solid #c3e6cb';
statusElement.innerHTML = '🟢 Real-time updates active';
} else {
statusElement.style.backgroundColor = '#f8d7da';
statusElement.style.color = '#721c24';
statusElement.style.border = '1px solid #f5c6cb';
statusElement.innerHTML = `🔴 ${message || 'Real-time updates disconnected'}`;
}
}
}
// Global utility function to initialize real-time updates
window.initializeBridgeRealtime = function(userAddress) {
if (!userAddress || userAddress.length < 10) {
console.warn('Invalid user address provided for real-time updates');
return null;
}
const client = new BridgeRealtimeClient(userAddress);
const uiUpdater = new BridgeUIUpdater();
// Set up event handlers
client.on('connected', () => {
uiUpdater.showConnectionStatus(true);
});
client.on('disconnected', () => {
uiUpdater.showConnectionStatus(false, 'Disconnected');
});
client.on('reconnecting', (data) => {
uiUpdater.showConnectionStatus(false, `Reconnecting... (${data.attempt}/${client.maxReconnectAttempts})`);
});
client.on('casDepositUpdate', (deposit) => {
uiUpdater.updateCasDepositStatus(deposit);
});
client.on('wcasReturnIntentionUpdate', (intention) => {
uiUpdater.updateWcasReturnIntentionStatus(intention);
});
client.on('polygonTransactionUpdate', (polygonTx) => {
uiUpdater.updatePolygonTransactionStatus(polygonTx);
});
client.on('error', (error) => {
console.error('Real-time client error:', error);
});
// Connect to WebSocket
client.connect();
return client;
};