// 前端 CLOB WebSocket SDK // 该 SDK 提供了连接到 CLOB WebSocket 服务的 TypeScript 接口。 // 它支持针对不同消息类型的特定回调: // - Welcome (连接建立) // - PriceSizeAll (全量订单簿快照) // - PriceSizeDelta (增量订单簿更新) // - Trade (成交记录) export interface ClobSocketOptions { // WebSocket URL (例如: "ws://localhost:8888/clob/ws" 或 "/api/clob/ws") url?: string; // 断开连接时是否自动重连 autoReconnect?: boolean; // 重连间隔时间(毫秒) reconnectInterval?: number; // 最大重连尝试次数 maxReconnectAttempts?: number; } // 基于 Go 结构体的消息接口定义 export interface WelcomeMsg { type: 'welcome'; t: number; } export interface PriceSizePolyMsg { i: number; // 索引 e: 'price_size_all' | 'price_size_delta'; // 事件类型 m: string; // 市场 ID t: number; // 时间戳 b?: Record; // 买单: 价格 -> 数量 s?: Record; // 卖单: 价格 -> 数量 } export interface TradePolyMsg { e: 'trade'; // 事件类型 m: string; // 市场 ID p: string; // 价格 s: string; // 数量 side: string; // "buy" 或 "sell" } export type ClobMsg = WelcomeMsg | PriceSizePolyMsg | TradePolyMsg; // 回调函数类型定义 export type WelcomeCallback = (data: WelcomeMsg) => void; export type PriceSizeAllCallback = (data: PriceSizePolyMsg) => void; export type PriceSizeDeltaCallback = (data: PriceSizePolyMsg) => void; export type TradeCallback = (data: TradePolyMsg) => void; export type ConnectCallback = (event: Event) => void; export type DisconnectCallback = (event: CloseEvent) => void; export type ErrorCallback = (event: Event) => void; export class ClobSdk { private url: string; private tokenIds: string[]; private ws: WebSocket | null = null; private autoReconnect: boolean; private reconnectInterval: number; private maxReconnectAttempts: number; private reconnectAttempts = 0; private isExplicitClose = false; private listeners: { welcome: WelcomeCallback[]; priceSizeAll: PriceSizeAllCallback[]; priceSizeDelta: PriceSizeDeltaCallback[]; trade: TradeCallback[]; connect: ConnectCallback[]; disconnect: DisconnectCallback[]; error: ErrorCallback[]; } = { welcome: [], priceSizeAll: [], priceSizeDelta: [], trade: [], connect: [], disconnect: [], error: [] }; /** * 初始化 CLOB SDK * @param tokenIds Yes/No 的 token ID,可传单个或数组,如 [yesTokenId, noTokenId] * @param options 配置选项 */ constructor(tokenIds: string | string[], options: ClobSocketOptions = {}) { this.tokenIds = Array.isArray(tokenIds) ? tokenIds : [tokenIds]; // 如果未提供 URL,默认为相对路径 // 修改为 Node.js 测试环境适配:如果 window 未定义,回退到 localhost let protocol = 'ws:'; let host = '10.117.63.212:8888'; if (typeof window !== 'undefined' && window.location) { protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; host = window.location.host; } this.url = options.url || `${protocol}//${host}/clob/ws`; this.autoReconnect = options.autoReconnect ?? true; this.reconnectInterval = options.reconnectInterval || 3000; this.maxReconnectAttempts = options.maxReconnectAttempts || 10; console.log('[ClobSdk] 构造完成,最终参数:', { tokenIds: this.tokenIds, url: this.url, autoReconnect: this.autoReconnect, reconnectInterval: this.reconnectInterval, maxReconnectAttempts: this.maxReconnectAttempts, }); } /** * 连接到 WebSocket 服务 */ public connect(): void { this.isExplicitClose = false; try { this.ws = new WebSocket(this.url); this.ws.onopen = (event: any) => { console.log(`[ClobSdk] 已连接到 ${this.url}`); this.reconnectAttempts = 0; this.notifyConnect(event); setTimeout(() => { this.subscribe(); }, 1000); // this.subscribe(); }; this.ws.onmessage = (event: any) => { try { const rawData = JSON.parse(event.data); // console.log('[ClobSdk] 收到消息:', rawData); this.handleMessage(rawData); } catch (e) { console.error('[ClobSdk] 消息解析失败:', e); } }; this.ws.onclose = (event: any) => { console.log('[ClobSdk] 连接关闭', { code: event.code, reason: event.reason, explicit: this.isExplicitClose }); if (!this.isExplicitClose) { this.notifyDisconnect(event); if (this.autoReconnect) { this.attemptReconnect(); } } else { this.notifyDisconnect(event); } }; this.ws.onerror = (event: any) => { console.error('[ClobSdk] 错误:', event); this.notifyError(event); }; } catch (e) { console.error('[ClobSdk] 连接错误:', e); if (this.autoReconnect) { this.attemptReconnect(); } } } /** * 断开 WebSocket 服务连接 */ public disconnect(): void { this.isExplicitClose = true; if (this.ws) { this.ws.close(); this.ws = null; } } // --- 事件监听器 --- public onWelcome(callback: WelcomeCallback): () => void { this.listeners.welcome.push(callback); return () => this.removeListener('welcome', callback); } public onPriceSizeAll(callback: PriceSizeAllCallback): () => void { this.listeners.priceSizeAll.push(callback); return () => this.removeListener('priceSizeAll', callback); } public onPriceSizeDelta(callback: PriceSizeDeltaCallback): () => void { this.listeners.priceSizeDelta.push(callback); return () => this.removeListener('priceSizeDelta', callback); } public onTrade(callback: TradeCallback): () => void { this.listeners.trade.push(callback); return () => this.removeListener('trade', callback); } public onConnect(callback: ConnectCallback): () => void { this.listeners.connect.push(callback); return () => this.removeListener('connect', callback); } public onDisconnect(callback: DisconnectCallback): () => void { this.listeners.disconnect.push(callback); return () => this.removeListener('disconnect', callback); } public onError(callback: ErrorCallback): () => void { this.listeners.error.push(callback); return () => this.removeListener('error', callback); } private removeListener(type: keyof typeof this.listeners, callback: any): void { this.listeners[type] = (this.listeners[type] as any[]).filter((cb: any) => cb !== callback); } // --- 内部方法 --- private subscribe(): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { const msg = { type: 'market', assets_ids: this.tokenIds }; this.ws.send(JSON.stringify(msg)); console.log('[ClobSdk] 已订阅 tokenIds:', this.tokenIds); } } private handleMessage(data: any): void { // Polymarket 消息有时是数组,有时是对象 (welcome) if (Array.isArray(data)) { data.forEach(msg => this.dispatchMessage(msg)); } else { this.dispatchMessage(data); } } private dispatchMessage(msg: any): void { // 1. Welcome 消息 if (msg.type === 'welcome') { console.log('[ClobSdk] 回调: welcome', msg); this.listeners.welcome.forEach(cb => cb(msg as WelcomeMsg)); return; } // 2. 基于 'e' 字段的事件 switch (msg.e) { case 'price_size_all': console.log('[ClobSdk] 回调: price_size_all', { i: msg.i, m: msg.m, t: msg.t, bids: msg.b, asks: msg.s }); this.listeners.priceSizeAll.forEach(cb => cb(msg as PriceSizePolyMsg)); break; case 'price_size_delta': console.log('[ClobSdk] 回调: price_size_delta', { i: msg.i, m: msg.m, t: msg.t, bids: msg.b, asks: msg.s }); this.listeners.priceSizeDelta.forEach(cb => cb(msg as PriceSizePolyMsg)); break; case 'trade': // console.log('[ClobSdk] 回调: trade', { m: msg.m, p: msg.p, s: msg.s, side: msg.side }); this.listeners.trade.forEach(cb => cb(msg as TradePolyMsg)); break; default: console.warn('[ClobSdk] 未知消息类型:', msg); } } private notifyConnect(event: Event): void { console.log('[ClobSdk] 回调: onConnect'); this.listeners.connect.forEach(cb => cb(event)); } private notifyDisconnect(event: CloseEvent): void { console.log('[ClobSdk] 回调: onDisconnect', { code: event.code, reason: event.reason }); this.listeners.disconnect.forEach(cb => cb(event)); } private notifyError(event: Event): void { console.log('[ClobSdk] 回调: onError', event); this.listeners.error.forEach(cb => cb(event)); } private attemptReconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('[ClobSdk] 达到最大重连次数。停止重连。'); return; } this.reconnectAttempts++; console.log(`[ClobSdk] ${this.reconnectInterval}ms 后尝试重连 (尝试次数 ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`); setTimeout(() => { this.connect(); }, this.reconnectInterval); } }