UNPKG

@drift-labs/sdk-browser

Version:
185 lines (184 loc) 8.19 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IndicativeQuotesSender = void 0; const tweetnacl_1 = __importDefault(require("tweetnacl")); const tweetnacl_util_1 = require("tweetnacl-util"); const ws_1 = __importDefault(require("ws")); const SEND_INTERVAL = 500; const MAX_BUFFERED_AMOUNT = 20 * 1024; // 20 KB as worst case scenario class IndicativeQuotesSender { constructor(endpoint, keypair) { this.endpoint = endpoint; this.keypair = keypair; this.heartbeatTimeout = null; this.sendQuotesInterval = null; this.heartbeatIntervalMs = 60000; this.reconnectDelay = 1000; this.ws = null; this.connected = false; this.quotes = new Map(); } generateChallengeResponse(nonce) { const messageBytes = (0, tweetnacl_util_1.decodeUTF8)(nonce); const signature = tweetnacl_1.default.sign.detached(messageBytes, this.keypair.secretKey); const signatureBase64 = Buffer.from(signature).toString('base64'); return signatureBase64; } handleAuthMessage(message) { var _a, _b; if (message['channel'] === 'auth' && message['nonce'] != null) { const signatureBase64 = this.generateChallengeResponse(message['nonce']); (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({ pubkey: this.keypair.publicKey.toBase58(), signature: signatureBase64, })); } if (message['channel'] === 'auth' && ((_b = message['message']) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'authenticated') { this.connected = true; } } async connect() { const ws = new ws_1.default(this.endpoint + '?pubkey=' + this.keypair.publicKey.toBase58()); this.ws = ws; ws.on('open', async () => { console.log('Connected to the server'); this.reconnectDelay = 1000; ws.on('message', async (data) => { var _a; let message; try { message = JSON.parse(data.toString()); } catch (e) { console.warn('Failed to parse json message: ', data.toString()); return; } this.startHeartbeatTimer(); if (message['channel'] === 'auth') { this.handleAuthMessage(message); } if (message['channel'] === 'auth' && ((_a = message['message']) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'authenticated') { this.sendQuotesInterval = setInterval(() => { var _a, _b; if (this.connected) { for (const [marketIndex, quotes] of this.quotes.entries()) { const message = { market_index: marketIndex, market_type: 'perp', quotes: quotes.map((quote) => { return { bid_price: quote.bidPrice.toString(), ask_price: quote.askPrice.toString(), bid_size: quote.bidBaseAssetAmount.toString(), ask_size: quote.askBaseAssetAmount.toString(), is_oracle_offset: quote.isOracleOffset, }; }), }; try { if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN && ((_b = this.ws) === null || _b === void 0 ? void 0 : _b.bufferedAmount) < MAX_BUFFERED_AMOUNT) { this.ws.send(JSON.stringify(message)); } } catch (err) { console.error('Error sending quote:', err); } } } }, SEND_INTERVAL); } }); ws.on('close', () => { console.log('Disconnected from the server'); this.reconnect(); }); ws.on('error', (error) => { console.error('WebSocket error:', error); this.reconnect(); }); }); ws.on('unexpected-response', async (request, response) => { console.error('Unexpected response, reconnecting in 5s:', response === null || response === void 0 ? void 0 : response.statusCode); setTimeout(() => { if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout); if (this.sendQuotesInterval) clearInterval(this.sendQuotesInterval); this.reconnect(); }, 5000); }); ws.on('error', async (request, response) => { console.error('WS closed from error, reconnecting in 1s:', response); setTimeout(() => { if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout); if (this.sendQuotesInterval) clearInterval(this.sendQuotesInterval); this.reconnect(); }, 1000); }); } startHeartbeatTimer() { if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); } this.heartbeatTimeout = setTimeout(() => { console.warn('No heartbeat received within 30 seconds, reconnecting...'); this.reconnect(); }, this.heartbeatIntervalMs); } setQuote(newQuotes) { var _a; if (!this.connected) { console.warn('Setting quote before connected to the server, ignoring'); } const quotes = Array.isArray(newQuotes) ? newQuotes : [newQuotes]; const newQuoteMap = new Map(); for (const quote of quotes) { if (quote.marketIndex == null || quote.bidPrice == null || quote.askPrice == null || quote.bidBaseAssetAmount == null || quote.askBaseAssetAmount == null) { console.warn('Received incomplete quote, ignoring and deleting old quote', quote); if (quote.marketIndex != null) { this.quotes.delete(quote.marketIndex); } return; } if (!newQuoteMap.has(quote.marketIndex)) { newQuoteMap.set(quote.marketIndex, []); } (_a = newQuoteMap.get(quote.marketIndex)) === null || _a === void 0 ? void 0 : _a.push(quote); } for (const marketIndex of newQuoteMap.keys()) { this.quotes.set(marketIndex, newQuoteMap.get(marketIndex)); } } reconnect() { if (this.ws) { this.ws.removeAllListeners(); this.ws.terminate(); } if (this.heartbeatTimeout) { clearTimeout(this.heartbeatTimeout); this.heartbeatTimeout = null; } if (this.sendQuotesInterval) { clearInterval(this.sendQuotesInterval); this.sendQuotesInterval = null; } console.log(`Reconnecting to WebSocket in ${this.reconnectDelay / 1000} seconds...`); setTimeout(() => { this.connect(); this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000); }, this.reconnectDelay); } } exports.IndicativeQuotesSender = IndicativeQuotesSender;