300 lines
9.3 KiB
TypeScript
300 lines
9.3 KiB
TypeScript
|
||
// 前端 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<string, number>; // 买单: 价格 -> 数量
|
||
s?: Record<string, number>; // 卖单: 价格 -> 数量
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|