@drift-labs/sdk
Version:
SDK for Drift Protocol
181 lines (180 loc) • 8.51 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.SwiftOrderSubscriber = void 0;
const perpMarkets_1 = require("../constants/perpMarkets");
const pda_1 = require("../addresses/pda");
const types_1 = require("../types");
const web3_js_1 = require("@solana/web3.js");
const tweetnacl_1 = __importDefault(require("tweetnacl"));
const tweetnacl_util_1 = require("tweetnacl-util");
const ws_1 = __importDefault(require("ws"));
const sha256_1 = require("@noble/hashes/sha256");
class SwiftOrderSubscriber {
constructor(config) {
this.config = config;
this.heartbeatTimeout = null;
this.heartbeatIntervalMs = 60000;
this.ws = null;
this.subscribed = false;
this.driftClient = config.driftClient;
this.userAccountGetter = config.userAccountGetter;
}
unsubscribe() {
if (this.subscribed) {
this.ws.removeAllListeners();
this.ws.terminate();
this.ws = null;
this.subscribed = false;
}
}
getSymbolForMarketIndex(marketIndex) {
const markets = this.config.driftEnv === 'devnet'
? perpMarkets_1.DevnetPerpMarkets
: perpMarkets_1.MainnetPerpMarkets;
return markets[marketIndex].symbol;
}
generateChallengeResponse(nonce) {
const messageBytes = (0, tweetnacl_util_1.decodeUTF8)(nonce);
const signature = tweetnacl_1.default.sign.detached(messageBytes, this.config.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.config.keypair.publicKey.toBase58(),
signature: signatureBase64,
}));
}
if (message['channel'] === 'auth' &&
((_b = message['message']) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'authenticated') {
this.subscribed = true;
this.config.marketIndexes.forEach(async (marketIndex) => {
var _a;
(_a = this.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({
action: 'subscribe',
market_type: 'perp',
market_name: this.getSymbolForMarketIndex(marketIndex),
}));
await new Promise((resolve) => setTimeout(resolve, 100));
});
}
}
async subscribe(onOrder, acceptSanitized = false) {
this.onOrder = onOrder;
const endpoint = this.config.endpoint || this.config.driftEnv === 'devnet'
? 'wss://master.swift.drift.trade/ws'
: 'wss://swift.drift.trade/ws';
const ws = new ws_1.default(endpoint + '?pubkey=' + this.config.keypair.publicKey.toBase58());
this.ws = ws;
ws.on('open', async () => {
console.log('Connected to the server');
ws.on('message', async (data) => {
const message = JSON.parse(data.toString());
this.startHeartbeatTimer();
if (message['channel'] === 'auth') {
this.handleAuthMessage(message);
}
if (message['order']) {
const order = message['order'];
// ignore likely sanitized orders by default
if (order['will_sanitize'] === true && !acceptSanitized) {
return;
}
const signedMsgOrderParamsBuf = Buffer.from(order['order_message'], 'hex');
const isDelegateSigner = signedMsgOrderParamsBuf
.slice(0, 8)
.equals(Uint8Array.from(Buffer.from((0, sha256_1.sha256)('global' + ':' + 'SignedMsgOrderParamsDelegateMessage')).slice(0, 8)));
const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage(signedMsgOrderParamsBuf, isDelegateSigner);
if (!signedMessage.signedMsgOrderParams.price) {
console.error(`order has no price: ${JSON.stringify(signedMessage.signedMsgOrderParams)}`);
return;
}
onOrder(order, signedMessage, isDelegateSigner);
}
});
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.statusCode);
setTimeout(() => {
if (this.heartbeatTimeout)
clearTimeout(this.heartbeatTimeout);
this.reconnect();
}, 5000);
});
ws.on('error', async (request, response) => {
console.error('WS closed from error, reconnecting in 1s:', response.statusCode);
setTimeout(() => {
if (this.heartbeatTimeout)
clearTimeout(this.heartbeatTimeout);
this.reconnect();
}, 1000);
});
}
async getPlaceAndMakeSignedMsgOrderIxs(orderMessageRaw, signedMsgOrderParamsMessage, makerOrderParams) {
if (!this.userAccountGetter) {
throw new Error('userAccountGetter must be set to use this function');
}
const signedMsgOrderParamsBuf = Buffer.from(orderMessageRaw['order_message'], 'hex');
const isDelegateSigner = signedMsgOrderParamsBuf
.slice(0, 8)
.equals(Uint8Array.from(Buffer.from((0, sha256_1.sha256)('global' + ':' + 'SignedMsgOrderParamsDelegateMessage')).slice(0, 8)));
const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage(signedMsgOrderParamsBuf, isDelegateSigner);
const takerAuthority = new web3_js_1.PublicKey(orderMessageRaw['taker_authority']);
const signingAuthority = new web3_js_1.PublicKey(orderMessageRaw['signing_authority']);
const takerUserPubkey = isDelegateSigner
? signedMessage.takerPubkey
: await (0, pda_1.getUserAccountPublicKey)(this.driftClient.program.programId, takerAuthority, signedMessage.subAccountId);
const takerUserAccount = await this.userAccountGetter.mustGetUserAccount(takerUserPubkey.toString());
const ixs = await this.driftClient.getPlaceAndMakeSignedMsgPerpOrderIxs({
orderParams: signedMsgOrderParamsBuf,
signature: Buffer.from(orderMessageRaw['order_signature'], 'base64'),
}, (0, tweetnacl_util_1.decodeUTF8)(orderMessageRaw['uuid']), {
taker: takerUserPubkey,
takerUserAccount,
takerStats: (0, pda_1.getUserStatsAccountPublicKey)(this.driftClient.program.programId, takerUserAccount.authority),
signingAuthority: signingAuthority,
}, Object.assign({}, makerOrderParams, {
postOnly: types_1.PostOnlyParams.MUST_POST_ONLY,
immediateOrCancel: true,
marketType: types_1.MarketType.PERP,
}));
return ixs;
}
startHeartbeatTimer() {
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
}
if (!this.onOrder) {
throw new Error('onOrder callback function must be set');
}
this.heartbeatTimeout = setTimeout(() => {
console.warn('No heartbeat received within 30 seconds, reconnecting...');
this.reconnect();
}, this.heartbeatIntervalMs);
}
reconnect() {
if (this.ws) {
this.ws.removeAllListeners();
this.ws.terminate();
}
console.log('Reconnecting to WebSocket...');
setTimeout(() => {
this.subscribe(this.onOrder);
}, 1000);
}
}
exports.SwiftOrderSubscriber = SwiftOrderSubscriber;