@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
185 lines (184 loc) • 8.19 kB
JavaScript
"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;