A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
275 lines
7.5 KiB
TypeScript
275 lines
7.5 KiB
TypeScript
/**
|
|
* WebSocket service for real-time blockchain updates.
|
|
* Handles connection management, reconnection, and event subscriptions.
|
|
*/
|
|
|
|
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
|
|
|
export interface BlockEvent {
|
|
type: 'new_block';
|
|
hash: string;
|
|
blueScore: number;
|
|
timestamp: number;
|
|
txCount: number;
|
|
isChainBlock: boolean;
|
|
}
|
|
|
|
export interface StatsEvent {
|
|
type: 'stats_update';
|
|
blockCount: number;
|
|
virtualDaaScore: number;
|
|
difficulty: number;
|
|
mempoolSize: number;
|
|
hashrate: number;
|
|
hashrateHuman: string;
|
|
}
|
|
|
|
export interface TipEvent {
|
|
type: 'tip_update';
|
|
tips: string[];
|
|
tipCount: number;
|
|
}
|
|
|
|
export interface MempoolEvent {
|
|
type: 'mempool_tx';
|
|
txId: string;
|
|
fee: number;
|
|
mass: number;
|
|
}
|
|
|
|
export type WebSocketEvent = BlockEvent | StatsEvent | TipEvent | MempoolEvent;
|
|
|
|
type EventCallback<T extends WebSocketEvent = WebSocketEvent> = (event: T) => void;
|
|
type StatusCallback = (status: WebSocketStatus) => void;
|
|
|
|
class WebSocketService {
|
|
private ws: WebSocket | null = null;
|
|
private url: string;
|
|
private subscriptions: Map<string, Set<EventCallback>> = new Map();
|
|
private statusListeners: Set<StatusCallback> = new Set();
|
|
private status: WebSocketStatus = 'disconnected';
|
|
private reconnectAttempts = 0;
|
|
private maxReconnectAttempts = 10;
|
|
private reconnectDelay = 1000;
|
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
private mockMode = false;
|
|
private mockInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
constructor() {
|
|
// Default WebSocket URL - can be overridden
|
|
const wsProtocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsHost = typeof window !== 'undefined' ? window.location.host : 'localhost:3000';
|
|
this.url = `${wsProtocol}//${wsHost}/ws`;
|
|
|
|
// Check if mock mode should be enabled
|
|
this.mockMode = this.shouldUseMock();
|
|
}
|
|
|
|
private shouldUseMock(): boolean {
|
|
if (typeof window !== 'undefined') {
|
|
if (localStorage.getItem('useMockApi') === 'true') return true;
|
|
}
|
|
return import.meta.env.VITE_USE_MOCK === 'true';
|
|
}
|
|
|
|
setUrl(url: string) {
|
|
this.url = url;
|
|
}
|
|
|
|
enableMock(enable: boolean) {
|
|
this.mockMode = enable;
|
|
if (enable && this.status === 'connected') {
|
|
this.startMockUpdates();
|
|
} else if (!enable) {
|
|
this.stopMockUpdates();
|
|
}
|
|
}
|
|
|
|
connect(): void {
|
|
if (this.mockMode) {
|
|
this.setStatus('connected');
|
|
this.startMockUpdates();
|
|
return;
|
|
}
|
|
|
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
return;
|
|
}
|
|
|
|
this.setStatus('connecting');
|
|
|
|
try {
|
|
this.ws = new WebSocket(this.url);
|
|
|
|
this.ws.onopen = () => {
|
|
this.setStatus('connected');
|
|
this.reconnectAttempts = 0;
|
|
this.reconnectDelay = 1000;
|
|
console.log('[WS] Connected to', this.url);
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
try {
|
|
const data = JSON.parse(event.data) as WebSocketEvent;
|
|
this.emit(data);
|
|
} catch (e) {
|
|
console.error('[WS] Failed to parse message:', e);
|
|
}
|
|
};
|
|
|
|
this.ws.onerror = (error) => {
|
|
console.error('[WS] Error:', error);
|
|
};
|
|
|
|
this.ws.onclose = (event) => {
|
|
console.log('[WS] Disconnected:', event.code, event.reason);
|
|
this.setStatus('disconnected');
|
|
this.scheduleReconnect();
|
|
};
|
|
} catch (error) {
|
|
console.error('[WS] Failed to connect:', error);
|
|
this.setStatus('disconnected');
|
|
this.scheduleReconnect();
|
|
}
|
|
}
|
|
|
|
disconnect(): void {
|
|
this.stopMockUpdates();
|
|
if (this.reconnectTimer) {
|
|
clearTimeout(this.reconnectTimer);
|
|
this.reconnectTimer = null;
|
|
}
|
|
if (this.ws) {
|
|
this.ws.close(1000, 'Client disconnect');
|
|
this.ws = null;
|
|
}
|
|
this.setStatus('disconnected');
|
|
}
|
|
|
|
private scheduleReconnect(): void {
|
|
if (this.mockMode) return;
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
console.log('[WS] Max reconnect attempts reached');
|
|
return;
|
|
}
|
|
|
|
this.reconnectAttempts++;
|
|
this.setStatus('reconnecting');
|
|
|
|
const delay = Math.min(this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
|
|
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
|
|
this.reconnectTimer = setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
}
|
|
|
|
private setStatus(status: WebSocketStatus): void {
|
|
this.status = status;
|
|
this.statusListeners.forEach(callback => callback(status));
|
|
}
|
|
|
|
getStatus(): WebSocketStatus {
|
|
return this.status;
|
|
}
|
|
|
|
onStatusChange(callback: StatusCallback): () => void {
|
|
this.statusListeners.add(callback);
|
|
return () => this.statusListeners.delete(callback);
|
|
}
|
|
|
|
subscribe<T extends WebSocketEvent>(eventType: T['type'], callback: EventCallback<T>): () => void {
|
|
if (!this.subscriptions.has(eventType)) {
|
|
this.subscriptions.set(eventType, new Set());
|
|
}
|
|
this.subscriptions.get(eventType)!.add(callback as EventCallback);
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
this.subscriptions.get(eventType)?.delete(callback as EventCallback);
|
|
};
|
|
}
|
|
|
|
private emit(event: WebSocketEvent): void {
|
|
const callbacks = this.subscriptions.get(event.type);
|
|
if (callbacks) {
|
|
callbacks.forEach(callback => callback(event));
|
|
}
|
|
|
|
// Also emit to wildcard subscribers
|
|
const wildcardCallbacks = this.subscriptions.get('*');
|
|
if (wildcardCallbacks) {
|
|
wildcardCallbacks.forEach(callback => callback(event));
|
|
}
|
|
}
|
|
|
|
// Mock data simulation for development
|
|
private startMockUpdates(): void {
|
|
if (this.mockInterval) return;
|
|
|
|
let blockCounter = 125847;
|
|
let mempoolSize = 42;
|
|
|
|
this.mockInterval = setInterval(() => {
|
|
// Simulate new block every 1 second
|
|
blockCounter++;
|
|
const blockEvent: BlockEvent = {
|
|
type: 'new_block',
|
|
hash: this.generateMockHash(blockCounter),
|
|
blueScore: blockCounter,
|
|
timestamp: Date.now(),
|
|
txCount: Math.floor(Math.random() * 50) + 1,
|
|
isChainBlock: true,
|
|
};
|
|
this.emit(blockEvent);
|
|
|
|
// Stats update with each block
|
|
mempoolSize = Math.max(0, mempoolSize + Math.floor(Math.random() * 10) - 5);
|
|
const statsEvent: StatsEvent = {
|
|
type: 'stats_update',
|
|
blockCount: blockCounter,
|
|
virtualDaaScore: blockCounter,
|
|
difficulty: 1234567890 + Math.random() * 100000000,
|
|
mempoolSize,
|
|
hashrate: 45.6e9 + Math.random() * 5e9,
|
|
hashrateHuman: `${(45.6 + Math.random() * 5).toFixed(2)} GH/s`,
|
|
};
|
|
this.emit(statsEvent);
|
|
|
|
// Occasional tip updates
|
|
if (Math.random() > 0.7) {
|
|
const tipEvent: TipEvent = {
|
|
type: 'tip_update',
|
|
tips: [
|
|
this.generateMockHash(blockCounter),
|
|
this.generateMockHash(blockCounter - 1),
|
|
this.generateMockHash(blockCounter - 2),
|
|
],
|
|
tipCount: 3,
|
|
};
|
|
this.emit(tipEvent);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
private stopMockUpdates(): void {
|
|
if (this.mockInterval) {
|
|
clearInterval(this.mockInterval);
|
|
this.mockInterval = null;
|
|
}
|
|
}
|
|
|
|
private generateMockHash(seed: number): string {
|
|
const chars = '0123456789abcdef';
|
|
let hash = '';
|
|
for (let i = 0; i < 64; i++) {
|
|
const charIndex = (seed * 31 + i * 7 + Math.floor(i / 8) * seed) % 16;
|
|
hash += chars[Math.abs(charIndex)];
|
|
}
|
|
return hash;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
export const wsService = new WebSocketService();
|
|
export default wsService;
|