@drift-labs/sdk-browser 2.106.0-beta.0 → 2.106.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.106.0-beta.0
1
+ 2.106.0-beta.2
@@ -53,6 +53,7 @@ export declare const MARGIN_PRECISION: BN;
53
53
  export declare const BID_ASK_SPREAD_PRECISION: BN;
54
54
  export declare const LIQUIDATION_PCT_PRECISION: BN;
55
55
  export declare const FUNDING_RATE_OFFSET_DENOMINATOR: BN;
56
+ export declare const PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO: BN;
56
57
  export declare const FIVE_MINUTE: BN;
57
58
  export declare const ONE_HOUR: BN;
58
59
  export declare const ONE_YEAR: BN;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MARGIN_PRECISION = exports.AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO = exports.PRICE_TO_QUOTE_PRECISION = exports.PRICE_DIV_PEG = exports.AMM_TO_QUOTE_PRECISION_RATIO = exports.BASE_PRECISION_EXP = exports.BASE_PRECISION = exports.AMM_RESERVE_PRECISION = exports.PEG_PRECISION = exports.FUNDING_RATE_BUFFER_PRECISION = exports.FUNDING_RATE_PRECISION = exports.PRICE_PRECISION = exports.QUOTE_PRECISION = exports.LIQUIDATION_FEE_PRECISION = exports.SPOT_MARKET_IMF_PRECISION = exports.SPOT_MARKET_IMF_PRECISION_EXP = exports.SPOT_MARKET_BALANCE_PRECISION = exports.SPOT_MARKET_BALANCE_PRECISION_EXP = exports.SPOT_MARKET_WEIGHT_PRECISION = exports.SPOT_MARKET_UTILIZATION_PRECISION = exports.SPOT_MARKET_UTILIZATION_PRECISION_EXP = exports.SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION = exports.SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION_EXP = exports.SPOT_MARKET_RATE_PRECISION = exports.SPOT_MARKET_RATE_PRECISION_EXP = exports.AMM_RESERVE_PRECISION_EXP = exports.PEG_PRECISION_EXP = exports.FUNDING_RATE_PRECISION_EXP = exports.PRICE_PRECISION_EXP = exports.FUNDING_RATE_BUFFER_PRECISION_EXP = exports.QUOTE_PRECISION_EXP = exports.CONCENTRATION_PRECISION = exports.PERCENTAGE_PRECISION = exports.PERCENTAGE_PRECISION_EXP = exports.MAX_LEVERAGE_ORDER_SIZE = exports.MAX_LEVERAGE = exports.TEN_MILLION = exports.BN_MAX = exports.TEN_THOUSAND = exports.TEN = exports.NINE = exports.EIGHT = exports.SEVEN = exports.SIX = exports.FIVE = exports.FOUR = exports.THREE = exports.TWO = exports.ONE = exports.ZERO = void 0;
4
- exports.MAX_PREDICTION_PRICE = exports.FUEL_START_TS = exports.FUEL_WINDOW = exports.DUST_POSITION_SIZE = exports.SLOT_TIME_ESTIMATE_MS = exports.IDLE_TIME_SLOTS = exports.ACCOUNT_AGE_DELETION_CUTOFF_SECONDS = exports.DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT = exports.OPEN_ORDER_MARGIN_REQUIREMENT = exports.LAMPORTS_EXP = exports.LAMPORTS_PRECISION = exports.GOV_SPOT_MARKET_INDEX = exports.QUOTE_SPOT_MARKET_INDEX = exports.ONE_YEAR = exports.ONE_HOUR = exports.FIVE_MINUTE = exports.FUNDING_RATE_OFFSET_DENOMINATOR = exports.LIQUIDATION_PCT_PRECISION = exports.BID_ASK_SPREAD_PRECISION = void 0;
4
+ exports.MAX_PREDICTION_PRICE = exports.FUEL_START_TS = exports.FUEL_WINDOW = exports.DUST_POSITION_SIZE = exports.SLOT_TIME_ESTIMATE_MS = exports.IDLE_TIME_SLOTS = exports.ACCOUNT_AGE_DELETION_CUTOFF_SECONDS = exports.DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT = exports.OPEN_ORDER_MARGIN_REQUIREMENT = exports.LAMPORTS_EXP = exports.LAMPORTS_PRECISION = exports.GOV_SPOT_MARKET_INDEX = exports.QUOTE_SPOT_MARKET_INDEX = exports.ONE_YEAR = exports.ONE_HOUR = exports.FIVE_MINUTE = exports.PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO = exports.FUNDING_RATE_OFFSET_DENOMINATOR = exports.LIQUIDATION_PCT_PRECISION = exports.BID_ASK_SPREAD_PRECISION = void 0;
5
5
  const web3_js_1 = require("@solana/web3.js");
6
6
  const __1 = require("../");
7
7
  exports.ZERO = new __1.BN(0);
@@ -57,6 +57,7 @@ exports.MARGIN_PRECISION = exports.TEN_THOUSAND;
57
57
  exports.BID_ASK_SPREAD_PRECISION = new __1.BN(1000000); // 10^6
58
58
  exports.LIQUIDATION_PCT_PRECISION = exports.TEN_THOUSAND;
59
59
  exports.FUNDING_RATE_OFFSET_DENOMINATOR = new __1.BN(5000);
60
+ exports.PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO = exports.PRICE_PRECISION.mul(exports.AMM_TO_QUOTE_PRECISION_RATIO);
60
61
  exports.FIVE_MINUTE = new __1.BN(60 * 5);
61
62
  exports.ONE_HOUR = new __1.BN(60 * 60);
62
63
  exports.ONE_YEAR = new __1.BN(31536000);
@@ -57,6 +57,7 @@ export * from './math/amm';
57
57
  export * from './math/trade';
58
58
  export * from './math/orders';
59
59
  export * from './math/repeg';
60
+ export * from './math/liquidation';
60
61
  export * from './math/margin';
61
62
  export * from './math/insurance';
62
63
  export * from './math/superStake';
@@ -85,6 +86,7 @@ export * from './oracles/pythClient';
85
86
  export * from './oracles/pythPullClient';
86
87
  export * from './oracles/pythLazerClient';
87
88
  export * from './oracles/switchboardOnDemandClient';
89
+ export * from './swift/swiftOrderSubscriber';
88
90
  export * from './tx/fastSingleTxSender';
89
91
  export * from './tx/retryTxSender';
90
92
  export * from './tx/whileValidTxSender';
@@ -80,6 +80,7 @@ __exportStar(require("./math/amm"), exports);
80
80
  __exportStar(require("./math/trade"), exports);
81
81
  __exportStar(require("./math/orders"), exports);
82
82
  __exportStar(require("./math/repeg"), exports);
83
+ __exportStar(require("./math/liquidation"), exports);
83
84
  __exportStar(require("./math/margin"), exports);
84
85
  __exportStar(require("./math/insurance"), exports);
85
86
  __exportStar(require("./math/superStake"), exports);
@@ -108,6 +109,7 @@ __exportStar(require("./oracles/pythClient"), exports);
108
109
  __exportStar(require("./oracles/pythPullClient"), exports);
109
110
  __exportStar(require("./oracles/pythLazerClient"), exports);
110
111
  __exportStar(require("./oracles/switchboardOnDemandClient"), exports);
112
+ __exportStar(require("./swift/swiftOrderSubscriber"), exports);
111
113
  __exportStar(require("./tx/fastSingleTxSender"), exports);
112
114
  __exportStar(require("./tx/retryTxSender"), exports);
113
115
  __exportStar(require("./tx/whileValidTxSender"), exports);
@@ -0,0 +1,4 @@
1
+ /// <reference types="bn.js" />
2
+ import { BN } from '@coral-xyz/anchor';
3
+ export declare function calculateBaseAssetAmountToCoverMarginShortage(marginShortage: BN, marginRatio: number, liquidationFee: number, ifLiquidationFee: number, oraclePrice: BN, quoteOraclePrice: BN): BN | undefined;
4
+ export declare function calculateMaxPctToLiquidate(userLastActiveSlot: BN, userLiquidationMarginFreed: BN, marginShortage: BN, slot: BN, initialPctToLiquidate: BN, liquidationDuration: BN): BN;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateMaxPctToLiquidate = exports.calculateBaseAssetAmountToCoverMarginShortage = void 0;
4
+ const anchor_1 = require("@coral-xyz/anchor");
5
+ const numericConstants_1 = require("../constants/numericConstants");
6
+ function calculateBaseAssetAmountToCoverMarginShortage(marginShortage, marginRatio, liquidationFee, ifLiquidationFee, oraclePrice, quoteOraclePrice) {
7
+ const marginRatioBN = new anchor_1.BN(marginRatio)
8
+ .mul(numericConstants_1.LIQUIDATION_FEE_PRECISION)
9
+ .div(numericConstants_1.MARGIN_PRECISION);
10
+ const liquidationFeeBN = new anchor_1.BN(liquidationFee);
11
+ if (oraclePrice.eq(new anchor_1.BN(0)) || marginRatioBN.lte(liquidationFeeBN)) {
12
+ // undefined is max
13
+ return undefined;
14
+ }
15
+ return marginShortage.mul(numericConstants_1.PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO).div(oraclePrice
16
+ .mul(quoteOraclePrice)
17
+ .div(numericConstants_1.PRICE_PRECISION)
18
+ .mul(marginRatioBN.sub(liquidationFeeBN))
19
+ .div(numericConstants_1.LIQUIDATION_FEE_PRECISION)
20
+ .sub(oraclePrice.mul(new anchor_1.BN(ifLiquidationFee)).div(numericConstants_1.LIQUIDATION_FEE_PRECISION)));
21
+ }
22
+ exports.calculateBaseAssetAmountToCoverMarginShortage = calculateBaseAssetAmountToCoverMarginShortage;
23
+ function calculateMaxPctToLiquidate(userLastActiveSlot, userLiquidationMarginFreed, marginShortage, slot, initialPctToLiquidate, liquidationDuration) {
24
+ // if margin shortage is tiny, accelerate liquidation
25
+ if (marginShortage.lt(new anchor_1.BN(50).mul(numericConstants_1.QUOTE_PRECISION))) {
26
+ return numericConstants_1.LIQUIDATION_PCT_PRECISION;
27
+ }
28
+ let slotsElapsed;
29
+ if (userLiquidationMarginFreed.gt(new anchor_1.BN(0))) {
30
+ slotsElapsed = anchor_1.BN.max(slot.sub(userLastActiveSlot), new anchor_1.BN(0));
31
+ }
32
+ else {
33
+ slotsElapsed = new anchor_1.BN(0);
34
+ }
35
+ const pctFreeable = anchor_1.BN.min(slotsElapsed
36
+ .mul(numericConstants_1.LIQUIDATION_PCT_PRECISION)
37
+ .div(liquidationDuration) // ~ 1 minute if per slot is 400ms
38
+ .add(initialPctToLiquidate), numericConstants_1.LIQUIDATION_PCT_PRECISION);
39
+ const totalMarginShortage = marginShortage.add(userLiquidationMarginFreed);
40
+ const maxMarginFreed = totalMarginShortage
41
+ .mul(pctFreeable)
42
+ .div(numericConstants_1.LIQUIDATION_PCT_PRECISION);
43
+ const marginFreeable = anchor_1.BN.max(maxMarginFreed.sub(userLiquidationMarginFreed), new anchor_1.BN(0));
44
+ return marginFreeable.mul(numericConstants_1.LIQUIDATION_PCT_PRECISION).div(marginShortage);
45
+ }
46
+ exports.calculateMaxPctToLiquidate = calculateMaxPctToLiquidate;
@@ -0,0 +1,28 @@
1
+ import { DriftClient, DriftEnv, OptionalOrderParams, SwiftOrderParamsMessage, UserMap } from '..';
2
+ import { Keypair, TransactionInstruction } from '@solana/web3.js';
3
+ export type SwiftOrderSubscriberConfig = {
4
+ driftClient: DriftClient;
5
+ userMap: UserMap;
6
+ driftEnv: DriftEnv;
7
+ endpoint?: string;
8
+ marketIndexes: number[];
9
+ keypair: Keypair;
10
+ };
11
+ export declare class SwiftOrderSubscriber {
12
+ private config;
13
+ private onOrder;
14
+ private heartbeatTimeout;
15
+ private readonly heartbeatIntervalMs;
16
+ private ws;
17
+ private driftClient;
18
+ private userMap;
19
+ subscribed: boolean;
20
+ constructor(config: SwiftOrderSubscriberConfig, onOrder: (orderMessageRaw: any, swiftOrderParamsMessage: SwiftOrderParamsMessage) => Promise<void>);
21
+ getSymbolForMarketIndex(marketIndex: number): string;
22
+ generateChallengeResponse(nonce: string): string;
23
+ handleAuthMessage(message: any): void;
24
+ subscribe(): Promise<void>;
25
+ getPlaceAndMakeSwiftOrderIxs(orderMessageRaw: any, swiftOrderParamsMessage: SwiftOrderParamsMessage, makerOrderParams: OptionalOrderParams): Promise<TransactionInstruction[]>;
26
+ private startHeartbeatTimer;
27
+ private reconnect;
28
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SwiftOrderSubscriber = void 0;
7
+ const __1 = require("..");
8
+ const web3_js_1 = require("@solana/web3.js");
9
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
10
+ const tweetnacl_util_1 = require("tweetnacl-util");
11
+ const ws_1 = __importDefault(require("ws"));
12
+ class SwiftOrderSubscriber {
13
+ constructor(config, onOrder) {
14
+ this.config = config;
15
+ this.onOrder = onOrder;
16
+ this.heartbeatTimeout = null;
17
+ this.heartbeatIntervalMs = 60000;
18
+ this.ws = null;
19
+ this.subscribed = false;
20
+ this.driftClient = config.driftClient;
21
+ this.userMap = config.userMap;
22
+ }
23
+ getSymbolForMarketIndex(marketIndex) {
24
+ const markets = this.config.driftEnv === 'devnet'
25
+ ? __1.DevnetPerpMarkets
26
+ : __1.MainnetPerpMarkets;
27
+ return markets[marketIndex].symbol;
28
+ }
29
+ generateChallengeResponse(nonce) {
30
+ const messageBytes = (0, tweetnacl_util_1.decodeUTF8)(nonce);
31
+ const signature = tweetnacl_1.default.sign.detached(messageBytes, this.config.keypair.secretKey);
32
+ const signatureBase64 = Buffer.from(signature).toString('base64');
33
+ return signatureBase64;
34
+ }
35
+ handleAuthMessage(message) {
36
+ var _a, _b;
37
+ if (message['channel'] === 'auth' && message['nonce'] != null) {
38
+ const signatureBase64 = this.generateChallengeResponse(message['nonce']);
39
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({
40
+ pubkey: this.config.keypair.publicKey.toBase58(),
41
+ signature: signatureBase64,
42
+ }));
43
+ }
44
+ if (message['channel'] === 'auth' &&
45
+ ((_b = message['message']) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'authenticated') {
46
+ this.subscribed = true;
47
+ this.config.marketIndexes.forEach(async (marketIndex) => {
48
+ var _a;
49
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({
50
+ action: 'subscribe',
51
+ market_type: 'perp',
52
+ market_name: this.getSymbolForMarketIndex(marketIndex),
53
+ }));
54
+ await new Promise((resolve) => setTimeout(resolve, 100));
55
+ });
56
+ }
57
+ }
58
+ async subscribe() {
59
+ const endpoint = this.config.endpoint || this.config.driftEnv === 'devnet'
60
+ ? 'wss://master.swift.drift.trade/ws'
61
+ : 'wss://swift.drift.trade/ws';
62
+ const ws = new ws_1.default(endpoint + '?pubkey=' + this.config.keypair.publicKey.toBase58());
63
+ this.ws = ws;
64
+ ws.on('open', async () => {
65
+ console.log('Connected to the server');
66
+ ws.on('message', async (data) => {
67
+ const message = JSON.parse(data.toString());
68
+ this.startHeartbeatTimer();
69
+ if (message['channel'] === 'auth') {
70
+ this.handleAuthMessage(message);
71
+ }
72
+ if (message['order']) {
73
+ const order = JSON.parse(message['order']);
74
+ const swiftOrderParamsBuf = Buffer.from(order['order_message'], 'base64');
75
+ const swiftOrderParamsMessage = this.driftClient.program.coder.types.decode('SwiftOrderParamsMessage', swiftOrderParamsBuf);
76
+ if (!swiftOrderParamsMessage.swiftOrderParams.price) {
77
+ console.error(`order has no price: ${JSON.stringify(swiftOrderParamsMessage.swiftOrderParams)}`);
78
+ return;
79
+ }
80
+ this.onOrder(order, swiftOrderParamsMessage);
81
+ }
82
+ });
83
+ ws.on('close', () => {
84
+ console.log('Disconnected from the server');
85
+ this.reconnect();
86
+ });
87
+ ws.on('error', (error) => {
88
+ console.error('WebSocket error:', error);
89
+ this.reconnect();
90
+ });
91
+ });
92
+ }
93
+ async getPlaceAndMakeSwiftOrderIxs(orderMessageRaw, swiftOrderParamsMessage, makerOrderParams) {
94
+ const swiftOrderParamsBuf = Buffer.from(orderMessageRaw['order_message'], 'base64');
95
+ const takerAuthority = new web3_js_1.PublicKey(orderMessageRaw['taker_authority']);
96
+ const takerUserPubkey = await (0, __1.getUserAccountPublicKey)(this.driftClient.program.programId, takerAuthority, swiftOrderParamsMessage.subAccountId);
97
+ const takerUserAccount = (await this.userMap.mustGet(takerUserPubkey.toString())).getUserAccount();
98
+ const ixs = await this.driftClient.getPlaceAndMakeSwiftPerpOrderIxs(swiftOrderParamsBuf, Buffer.from(orderMessageRaw['order_signature'], 'base64'), (0, tweetnacl_util_1.decodeUTF8)(orderMessageRaw['uuid']), {
99
+ taker: takerUserPubkey,
100
+ takerUserAccount,
101
+ takerStats: (0, __1.getUserStatsAccountPublicKey)(this.driftClient.program.programId, takerUserAccount.authority),
102
+ }, Object.assign({}, makerOrderParams, {
103
+ postOnly: __1.PostOnlyParams.MUST_POST_ONLY,
104
+ immediateOrCancel: true,
105
+ marketType: __1.MarketType.PERP,
106
+ }));
107
+ return ixs;
108
+ }
109
+ startHeartbeatTimer() {
110
+ if (this.heartbeatTimeout) {
111
+ clearTimeout(this.heartbeatTimeout);
112
+ }
113
+ this.heartbeatTimeout = setTimeout(() => {
114
+ console.warn('No heartbeat received within 30 seconds, reconnecting...');
115
+ this.reconnect();
116
+ }, this.heartbeatIntervalMs);
117
+ }
118
+ reconnect() {
119
+ if (this.ws) {
120
+ this.ws.removeAllListeners();
121
+ this.ws.terminate();
122
+ }
123
+ console.log('Reconnecting to WebSocket...');
124
+ setTimeout(() => {
125
+ this.subscribe();
126
+ }, 1000);
127
+ }
128
+ }
129
+ exports.SwiftOrderSubscriber = SwiftOrderSubscriber;
@@ -53,6 +53,7 @@ export declare const MARGIN_PRECISION: BN;
53
53
  export declare const BID_ASK_SPREAD_PRECISION: BN;
54
54
  export declare const LIQUIDATION_PCT_PRECISION: BN;
55
55
  export declare const FUNDING_RATE_OFFSET_DENOMINATOR: BN;
56
+ export declare const PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO: BN;
56
57
  export declare const FIVE_MINUTE: BN;
57
58
  export declare const ONE_HOUR: BN;
58
59
  export declare const ONE_YEAR: BN;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MARGIN_PRECISION = exports.AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO = exports.PRICE_TO_QUOTE_PRECISION = exports.PRICE_DIV_PEG = exports.AMM_TO_QUOTE_PRECISION_RATIO = exports.BASE_PRECISION_EXP = exports.BASE_PRECISION = exports.AMM_RESERVE_PRECISION = exports.PEG_PRECISION = exports.FUNDING_RATE_BUFFER_PRECISION = exports.FUNDING_RATE_PRECISION = exports.PRICE_PRECISION = exports.QUOTE_PRECISION = exports.LIQUIDATION_FEE_PRECISION = exports.SPOT_MARKET_IMF_PRECISION = exports.SPOT_MARKET_IMF_PRECISION_EXP = exports.SPOT_MARKET_BALANCE_PRECISION = exports.SPOT_MARKET_BALANCE_PRECISION_EXP = exports.SPOT_MARKET_WEIGHT_PRECISION = exports.SPOT_MARKET_UTILIZATION_PRECISION = exports.SPOT_MARKET_UTILIZATION_PRECISION_EXP = exports.SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION = exports.SPOT_MARKET_CUMULATIVE_INTEREST_PRECISION_EXP = exports.SPOT_MARKET_RATE_PRECISION = exports.SPOT_MARKET_RATE_PRECISION_EXP = exports.AMM_RESERVE_PRECISION_EXP = exports.PEG_PRECISION_EXP = exports.FUNDING_RATE_PRECISION_EXP = exports.PRICE_PRECISION_EXP = exports.FUNDING_RATE_BUFFER_PRECISION_EXP = exports.QUOTE_PRECISION_EXP = exports.CONCENTRATION_PRECISION = exports.PERCENTAGE_PRECISION = exports.PERCENTAGE_PRECISION_EXP = exports.MAX_LEVERAGE_ORDER_SIZE = exports.MAX_LEVERAGE = exports.TEN_MILLION = exports.BN_MAX = exports.TEN_THOUSAND = exports.TEN = exports.NINE = exports.EIGHT = exports.SEVEN = exports.SIX = exports.FIVE = exports.FOUR = exports.THREE = exports.TWO = exports.ONE = exports.ZERO = void 0;
4
- exports.MAX_PREDICTION_PRICE = exports.FUEL_START_TS = exports.FUEL_WINDOW = exports.DUST_POSITION_SIZE = exports.SLOT_TIME_ESTIMATE_MS = exports.IDLE_TIME_SLOTS = exports.ACCOUNT_AGE_DELETION_CUTOFF_SECONDS = exports.DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT = exports.OPEN_ORDER_MARGIN_REQUIREMENT = exports.LAMPORTS_EXP = exports.LAMPORTS_PRECISION = exports.GOV_SPOT_MARKET_INDEX = exports.QUOTE_SPOT_MARKET_INDEX = exports.ONE_YEAR = exports.ONE_HOUR = exports.FIVE_MINUTE = exports.FUNDING_RATE_OFFSET_DENOMINATOR = exports.LIQUIDATION_PCT_PRECISION = exports.BID_ASK_SPREAD_PRECISION = void 0;
4
+ exports.MAX_PREDICTION_PRICE = exports.FUEL_START_TS = exports.FUEL_WINDOW = exports.DUST_POSITION_SIZE = exports.SLOT_TIME_ESTIMATE_MS = exports.IDLE_TIME_SLOTS = exports.ACCOUNT_AGE_DELETION_CUTOFF_SECONDS = exports.DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT = exports.OPEN_ORDER_MARGIN_REQUIREMENT = exports.LAMPORTS_EXP = exports.LAMPORTS_PRECISION = exports.GOV_SPOT_MARKET_INDEX = exports.QUOTE_SPOT_MARKET_INDEX = exports.ONE_YEAR = exports.ONE_HOUR = exports.FIVE_MINUTE = exports.PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO = exports.FUNDING_RATE_OFFSET_DENOMINATOR = exports.LIQUIDATION_PCT_PRECISION = exports.BID_ASK_SPREAD_PRECISION = void 0;
5
5
  const web3_js_1 = require("@solana/web3.js");
6
6
  const __1 = require("../");
7
7
  exports.ZERO = new __1.BN(0);
@@ -57,6 +57,7 @@ exports.MARGIN_PRECISION = exports.TEN_THOUSAND;
57
57
  exports.BID_ASK_SPREAD_PRECISION = new __1.BN(1000000); // 10^6
58
58
  exports.LIQUIDATION_PCT_PRECISION = exports.TEN_THOUSAND;
59
59
  exports.FUNDING_RATE_OFFSET_DENOMINATOR = new __1.BN(5000);
60
+ exports.PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO = exports.PRICE_PRECISION.mul(exports.AMM_TO_QUOTE_PRECISION_RATIO);
60
61
  exports.FIVE_MINUTE = new __1.BN(60 * 5);
61
62
  exports.ONE_HOUR = new __1.BN(60 * 60);
62
63
  exports.ONE_YEAR = new __1.BN(31536000);
@@ -57,6 +57,7 @@ export * from './math/amm';
57
57
  export * from './math/trade';
58
58
  export * from './math/orders';
59
59
  export * from './math/repeg';
60
+ export * from './math/liquidation';
60
61
  export * from './math/margin';
61
62
  export * from './math/insurance';
62
63
  export * from './math/superStake';
@@ -85,6 +86,7 @@ export * from './oracles/pythClient';
85
86
  export * from './oracles/pythPullClient';
86
87
  export * from './oracles/pythLazerClient';
87
88
  export * from './oracles/switchboardOnDemandClient';
89
+ export * from './swift/swiftOrderSubscriber';
88
90
  export * from './tx/fastSingleTxSender';
89
91
  export * from './tx/retryTxSender';
90
92
  export * from './tx/whileValidTxSender';
package/lib/node/index.js CHANGED
@@ -80,6 +80,7 @@ __exportStar(require("./math/amm"), exports);
80
80
  __exportStar(require("./math/trade"), exports);
81
81
  __exportStar(require("./math/orders"), exports);
82
82
  __exportStar(require("./math/repeg"), exports);
83
+ __exportStar(require("./math/liquidation"), exports);
83
84
  __exportStar(require("./math/margin"), exports);
84
85
  __exportStar(require("./math/insurance"), exports);
85
86
  __exportStar(require("./math/superStake"), exports);
@@ -108,6 +109,7 @@ __exportStar(require("./oracles/pythClient"), exports);
108
109
  __exportStar(require("./oracles/pythPullClient"), exports);
109
110
  __exportStar(require("./oracles/pythLazerClient"), exports);
110
111
  __exportStar(require("./oracles/switchboardOnDemandClient"), exports);
112
+ __exportStar(require("./swift/swiftOrderSubscriber"), exports);
111
113
  __exportStar(require("./tx/fastSingleTxSender"), exports);
112
114
  __exportStar(require("./tx/retryTxSender"), exports);
113
115
  __exportStar(require("./tx/whileValidTxSender"), exports);
@@ -0,0 +1,4 @@
1
+ /// <reference types="bn.js" />
2
+ import { BN } from '@coral-xyz/anchor';
3
+ export declare function calculateBaseAssetAmountToCoverMarginShortage(marginShortage: BN, marginRatio: number, liquidationFee: number, ifLiquidationFee: number, oraclePrice: BN, quoteOraclePrice: BN): BN | undefined;
4
+ export declare function calculateMaxPctToLiquidate(userLastActiveSlot: BN, userLiquidationMarginFreed: BN, marginShortage: BN, slot: BN, initialPctToLiquidate: BN, liquidationDuration: BN): BN;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateMaxPctToLiquidate = exports.calculateBaseAssetAmountToCoverMarginShortage = void 0;
4
+ const anchor_1 = require("@coral-xyz/anchor");
5
+ const numericConstants_1 = require("../constants/numericConstants");
6
+ function calculateBaseAssetAmountToCoverMarginShortage(marginShortage, marginRatio, liquidationFee, ifLiquidationFee, oraclePrice, quoteOraclePrice) {
7
+ const marginRatioBN = new anchor_1.BN(marginRatio)
8
+ .mul(numericConstants_1.LIQUIDATION_FEE_PRECISION)
9
+ .div(numericConstants_1.MARGIN_PRECISION);
10
+ const liquidationFeeBN = new anchor_1.BN(liquidationFee);
11
+ if (oraclePrice.eq(new anchor_1.BN(0)) || marginRatioBN.lte(liquidationFeeBN)) {
12
+ // undefined is max
13
+ return undefined;
14
+ }
15
+ return marginShortage.mul(numericConstants_1.PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO).div(oraclePrice
16
+ .mul(quoteOraclePrice)
17
+ .div(numericConstants_1.PRICE_PRECISION)
18
+ .mul(marginRatioBN.sub(liquidationFeeBN))
19
+ .div(numericConstants_1.LIQUIDATION_FEE_PRECISION)
20
+ .sub(oraclePrice.mul(new anchor_1.BN(ifLiquidationFee)).div(numericConstants_1.LIQUIDATION_FEE_PRECISION)));
21
+ }
22
+ exports.calculateBaseAssetAmountToCoverMarginShortage = calculateBaseAssetAmountToCoverMarginShortage;
23
+ function calculateMaxPctToLiquidate(userLastActiveSlot, userLiquidationMarginFreed, marginShortage, slot, initialPctToLiquidate, liquidationDuration) {
24
+ // if margin shortage is tiny, accelerate liquidation
25
+ if (marginShortage.lt(new anchor_1.BN(50).mul(numericConstants_1.QUOTE_PRECISION))) {
26
+ return numericConstants_1.LIQUIDATION_PCT_PRECISION;
27
+ }
28
+ let slotsElapsed;
29
+ if (userLiquidationMarginFreed.gt(new anchor_1.BN(0))) {
30
+ slotsElapsed = anchor_1.BN.max(slot.sub(userLastActiveSlot), new anchor_1.BN(0));
31
+ }
32
+ else {
33
+ slotsElapsed = new anchor_1.BN(0);
34
+ }
35
+ const pctFreeable = anchor_1.BN.min(slotsElapsed
36
+ .mul(numericConstants_1.LIQUIDATION_PCT_PRECISION)
37
+ .div(liquidationDuration) // ~ 1 minute if per slot is 400ms
38
+ .add(initialPctToLiquidate), numericConstants_1.LIQUIDATION_PCT_PRECISION);
39
+ const totalMarginShortage = marginShortage.add(userLiquidationMarginFreed);
40
+ const maxMarginFreed = totalMarginShortage
41
+ .mul(pctFreeable)
42
+ .div(numericConstants_1.LIQUIDATION_PCT_PRECISION);
43
+ const marginFreeable = anchor_1.BN.max(maxMarginFreed.sub(userLiquidationMarginFreed), new anchor_1.BN(0));
44
+ return marginFreeable.mul(numericConstants_1.LIQUIDATION_PCT_PRECISION).div(marginShortage);
45
+ }
46
+ exports.calculateMaxPctToLiquidate = calculateMaxPctToLiquidate;
@@ -0,0 +1,28 @@
1
+ import { DriftClient, DriftEnv, OptionalOrderParams, SwiftOrderParamsMessage, UserMap } from '..';
2
+ import { Keypair, TransactionInstruction } from '@solana/web3.js';
3
+ export type SwiftOrderSubscriberConfig = {
4
+ driftClient: DriftClient;
5
+ userMap: UserMap;
6
+ driftEnv: DriftEnv;
7
+ endpoint?: string;
8
+ marketIndexes: number[];
9
+ keypair: Keypair;
10
+ };
11
+ export declare class SwiftOrderSubscriber {
12
+ private config;
13
+ private onOrder;
14
+ private heartbeatTimeout;
15
+ private readonly heartbeatIntervalMs;
16
+ private ws;
17
+ private driftClient;
18
+ private userMap;
19
+ subscribed: boolean;
20
+ constructor(config: SwiftOrderSubscriberConfig, onOrder: (orderMessageRaw: any, swiftOrderParamsMessage: SwiftOrderParamsMessage) => Promise<void>);
21
+ getSymbolForMarketIndex(marketIndex: number): string;
22
+ generateChallengeResponse(nonce: string): string;
23
+ handleAuthMessage(message: any): void;
24
+ subscribe(): Promise<void>;
25
+ getPlaceAndMakeSwiftOrderIxs(orderMessageRaw: any, swiftOrderParamsMessage: SwiftOrderParamsMessage, makerOrderParams: OptionalOrderParams): Promise<TransactionInstruction[]>;
26
+ private startHeartbeatTimer;
27
+ private reconnect;
28
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SwiftOrderSubscriber = void 0;
7
+ const __1 = require("..");
8
+ const web3_js_1 = require("@solana/web3.js");
9
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
10
+ const tweetnacl_util_1 = require("tweetnacl-util");
11
+ const ws_1 = __importDefault(require("ws"));
12
+ class SwiftOrderSubscriber {
13
+ constructor(config, onOrder) {
14
+ this.config = config;
15
+ this.onOrder = onOrder;
16
+ this.heartbeatTimeout = null;
17
+ this.heartbeatIntervalMs = 60000;
18
+ this.ws = null;
19
+ this.subscribed = false;
20
+ this.driftClient = config.driftClient;
21
+ this.userMap = config.userMap;
22
+ }
23
+ getSymbolForMarketIndex(marketIndex) {
24
+ const markets = this.config.driftEnv === 'devnet'
25
+ ? __1.DevnetPerpMarkets
26
+ : __1.MainnetPerpMarkets;
27
+ return markets[marketIndex].symbol;
28
+ }
29
+ generateChallengeResponse(nonce) {
30
+ const messageBytes = (0, tweetnacl_util_1.decodeUTF8)(nonce);
31
+ const signature = tweetnacl_1.default.sign.detached(messageBytes, this.config.keypair.secretKey);
32
+ const signatureBase64 = Buffer.from(signature).toString('base64');
33
+ return signatureBase64;
34
+ }
35
+ handleAuthMessage(message) {
36
+ var _a, _b;
37
+ if (message['channel'] === 'auth' && message['nonce'] != null) {
38
+ const signatureBase64 = this.generateChallengeResponse(message['nonce']);
39
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({
40
+ pubkey: this.config.keypair.publicKey.toBase58(),
41
+ signature: signatureBase64,
42
+ }));
43
+ }
44
+ if (message['channel'] === 'auth' &&
45
+ ((_b = message['message']) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'authenticated') {
46
+ this.subscribed = true;
47
+ this.config.marketIndexes.forEach(async (marketIndex) => {
48
+ var _a;
49
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify({
50
+ action: 'subscribe',
51
+ market_type: 'perp',
52
+ market_name: this.getSymbolForMarketIndex(marketIndex),
53
+ }));
54
+ await new Promise((resolve) => setTimeout(resolve, 100));
55
+ });
56
+ }
57
+ }
58
+ async subscribe() {
59
+ const endpoint = this.config.endpoint || this.config.driftEnv === 'devnet'
60
+ ? 'wss://master.swift.drift.trade/ws'
61
+ : 'wss://swift.drift.trade/ws';
62
+ const ws = new ws_1.default(endpoint + '?pubkey=' + this.config.keypair.publicKey.toBase58());
63
+ this.ws = ws;
64
+ ws.on('open', async () => {
65
+ console.log('Connected to the server');
66
+ ws.on('message', async (data) => {
67
+ const message = JSON.parse(data.toString());
68
+ this.startHeartbeatTimer();
69
+ if (message['channel'] === 'auth') {
70
+ this.handleAuthMessage(message);
71
+ }
72
+ if (message['order']) {
73
+ const order = JSON.parse(message['order']);
74
+ const swiftOrderParamsBuf = Buffer.from(order['order_message'], 'base64');
75
+ const swiftOrderParamsMessage = this.driftClient.program.coder.types.decode('SwiftOrderParamsMessage', swiftOrderParamsBuf);
76
+ if (!swiftOrderParamsMessage.swiftOrderParams.price) {
77
+ console.error(`order has no price: ${JSON.stringify(swiftOrderParamsMessage.swiftOrderParams)}`);
78
+ return;
79
+ }
80
+ this.onOrder(order, swiftOrderParamsMessage);
81
+ }
82
+ });
83
+ ws.on('close', () => {
84
+ console.log('Disconnected from the server');
85
+ this.reconnect();
86
+ });
87
+ ws.on('error', (error) => {
88
+ console.error('WebSocket error:', error);
89
+ this.reconnect();
90
+ });
91
+ });
92
+ }
93
+ async getPlaceAndMakeSwiftOrderIxs(orderMessageRaw, swiftOrderParamsMessage, makerOrderParams) {
94
+ const swiftOrderParamsBuf = Buffer.from(orderMessageRaw['order_message'], 'base64');
95
+ const takerAuthority = new web3_js_1.PublicKey(orderMessageRaw['taker_authority']);
96
+ const takerUserPubkey = await (0, __1.getUserAccountPublicKey)(this.driftClient.program.programId, takerAuthority, swiftOrderParamsMessage.subAccountId);
97
+ const takerUserAccount = (await this.userMap.mustGet(takerUserPubkey.toString())).getUserAccount();
98
+ const ixs = await this.driftClient.getPlaceAndMakeSwiftPerpOrderIxs(swiftOrderParamsBuf, Buffer.from(orderMessageRaw['order_signature'], 'base64'), (0, tweetnacl_util_1.decodeUTF8)(orderMessageRaw['uuid']), {
99
+ taker: takerUserPubkey,
100
+ takerUserAccount,
101
+ takerStats: (0, __1.getUserStatsAccountPublicKey)(this.driftClient.program.programId, takerUserAccount.authority),
102
+ }, Object.assign({}, makerOrderParams, {
103
+ postOnly: __1.PostOnlyParams.MUST_POST_ONLY,
104
+ immediateOrCancel: true,
105
+ marketType: __1.MarketType.PERP,
106
+ }));
107
+ return ixs;
108
+ }
109
+ startHeartbeatTimer() {
110
+ if (this.heartbeatTimeout) {
111
+ clearTimeout(this.heartbeatTimeout);
112
+ }
113
+ this.heartbeatTimeout = setTimeout(() => {
114
+ console.warn('No heartbeat received within 30 seconds, reconnecting...');
115
+ this.reconnect();
116
+ }, this.heartbeatIntervalMs);
117
+ }
118
+ reconnect() {
119
+ if (this.ws) {
120
+ this.ws.removeAllListeners();
121
+ this.ws.terminate();
122
+ }
123
+ console.log('Reconnecting to WebSocket...');
124
+ setTimeout(() => {
125
+ this.subscribe();
126
+ }, 1000);
127
+ }
128
+ }
129
+ exports.SwiftOrderSubscriber = SwiftOrderSubscriber;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drift-labs/sdk-browser",
3
- "version": "2.106.0-beta.0",
3
+ "version": "2.106.0-beta.2",
4
4
  "main": "lib/node/index.js",
5
5
  "types": "lib/node/index.d.ts",
6
6
  "browser": "./lib/browser/index.js",
@@ -56,6 +56,7 @@
56
56
  "solana-bankrun": "0.3.1",
57
57
  "strict-event-emitter-types": "2.0.0",
58
58
  "tweetnacl": "1.0.3",
59
+ "tweetnacl-util": "0.15.1",
59
60
  "uuid": "8.3.2",
60
61
  "yargs": "17.7.2",
61
62
  "zstddec": "0.1.0"
@@ -84,6 +84,9 @@ export const MARGIN_PRECISION = TEN_THOUSAND;
84
84
  export const BID_ASK_SPREAD_PRECISION = new BN(1000000); // 10^6
85
85
  export const LIQUIDATION_PCT_PRECISION = TEN_THOUSAND;
86
86
  export const FUNDING_RATE_OFFSET_DENOMINATOR = new BN(5000);
87
+ export const PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO = PRICE_PRECISION.mul(
88
+ AMM_TO_QUOTE_PRECISION_RATIO
89
+ );
87
90
 
88
91
  export const FIVE_MINUTE = new BN(60 * 5);
89
92
  export const ONE_HOUR = new BN(60 * 60);
package/src/index.ts CHANGED
@@ -58,6 +58,7 @@ export * from './math/amm';
58
58
  export * from './math/trade';
59
59
  export * from './math/orders';
60
60
  export * from './math/repeg';
61
+ export * from './math/liquidation';
61
62
  export * from './math/margin';
62
63
  export * from './math/insurance';
63
64
  export * from './math/superStake';
@@ -86,6 +87,7 @@ export * from './oracles/pythClient';
86
87
  export * from './oracles/pythPullClient';
87
88
  export * from './oracles/pythLazerClient';
88
89
  export * from './oracles/switchboardOnDemandClient';
90
+ export * from './swift/swiftOrderSubscriber';
89
91
  export * from './tx/fastSingleTxSender';
90
92
  export * from './tx/retryTxSender';
91
93
  export * from './tx/whileValidTxSender';
@@ -0,0 +1,79 @@
1
+ import { BN } from '@coral-xyz/anchor';
2
+ import {
3
+ PRICE_PRECISION,
4
+ LIQUIDATION_FEE_PRECISION,
5
+ MARGIN_PRECISION,
6
+ PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO,
7
+ QUOTE_PRECISION,
8
+ LIQUIDATION_PCT_PRECISION,
9
+ } from '../constants/numericConstants';
10
+
11
+ export function calculateBaseAssetAmountToCoverMarginShortage(
12
+ marginShortage: BN,
13
+ marginRatio: number,
14
+ liquidationFee: number,
15
+ ifLiquidationFee: number,
16
+ oraclePrice: BN,
17
+ quoteOraclePrice: BN
18
+ ): BN | undefined {
19
+ const marginRatioBN = new BN(marginRatio)
20
+ .mul(LIQUIDATION_FEE_PRECISION)
21
+ .div(MARGIN_PRECISION);
22
+ const liquidationFeeBN = new BN(liquidationFee);
23
+
24
+ if (oraclePrice.eq(new BN(0)) || marginRatioBN.lte(liquidationFeeBN)) {
25
+ // undefined is max
26
+ return undefined;
27
+ }
28
+
29
+ return marginShortage.mul(PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO).div(
30
+ oraclePrice
31
+ .mul(quoteOraclePrice)
32
+ .div(PRICE_PRECISION)
33
+ .mul(marginRatioBN.sub(liquidationFeeBN))
34
+ .div(LIQUIDATION_FEE_PRECISION)
35
+ .sub(
36
+ oraclePrice.mul(new BN(ifLiquidationFee)).div(LIQUIDATION_FEE_PRECISION)
37
+ )
38
+ );
39
+ }
40
+
41
+ export function calculateMaxPctToLiquidate(
42
+ userLastActiveSlot: BN,
43
+ userLiquidationMarginFreed: BN,
44
+ marginShortage: BN,
45
+ slot: BN,
46
+ initialPctToLiquidate: BN,
47
+ liquidationDuration: BN
48
+ ): BN {
49
+ // if margin shortage is tiny, accelerate liquidation
50
+ if (marginShortage.lt(new BN(50).mul(QUOTE_PRECISION))) {
51
+ return LIQUIDATION_PCT_PRECISION;
52
+ }
53
+
54
+ let slotsElapsed;
55
+ if (userLiquidationMarginFreed.gt(new BN(0))) {
56
+ slotsElapsed = BN.max(slot.sub(userLastActiveSlot), new BN(0));
57
+ } else {
58
+ slotsElapsed = new BN(0);
59
+ }
60
+
61
+ const pctFreeable = BN.min(
62
+ slotsElapsed
63
+ .mul(LIQUIDATION_PCT_PRECISION)
64
+ .div(liquidationDuration) // ~ 1 minute if per slot is 400ms
65
+ .add(initialPctToLiquidate),
66
+ LIQUIDATION_PCT_PRECISION
67
+ );
68
+
69
+ const totalMarginShortage = marginShortage.add(userLiquidationMarginFreed);
70
+ const maxMarginFreed = totalMarginShortage
71
+ .mul(pctFreeable)
72
+ .div(LIQUIDATION_PCT_PRECISION);
73
+ const marginFreeable = BN.max(
74
+ maxMarginFreed.sub(userLiquidationMarginFreed),
75
+ new BN(0)
76
+ );
77
+
78
+ return marginFreeable.mul(LIQUIDATION_PCT_PRECISION).div(marginShortage);
79
+ }
@@ -0,0 +1,211 @@
1
+ import {
2
+ DevnetPerpMarkets,
3
+ DriftClient,
4
+ DriftEnv,
5
+ getUserAccountPublicKey,
6
+ getUserStatsAccountPublicKey,
7
+ MainnetPerpMarkets,
8
+ MarketType,
9
+ OptionalOrderParams,
10
+ PostOnlyParams,
11
+ SwiftOrderParamsMessage,
12
+ UserMap,
13
+ } from '..';
14
+ import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js';
15
+ import nacl from 'tweetnacl';
16
+ import { decodeUTF8 } from 'tweetnacl-util';
17
+ import WebSocket from 'ws';
18
+
19
+ export type SwiftOrderSubscriberConfig = {
20
+ driftClient: DriftClient;
21
+ userMap: UserMap;
22
+ driftEnv: DriftEnv;
23
+ endpoint?: string;
24
+ marketIndexes: number[];
25
+ keypair: Keypair;
26
+ };
27
+
28
+ export class SwiftOrderSubscriber {
29
+ private heartbeatTimeout: NodeJS.Timeout | null = null;
30
+ private readonly heartbeatIntervalMs = 60000;
31
+ private ws: WebSocket | null = null;
32
+ private driftClient: DriftClient;
33
+ private userMap: UserMap;
34
+ subscribed = false;
35
+
36
+ constructor(
37
+ private config: SwiftOrderSubscriberConfig,
38
+ private onOrder: (
39
+ orderMessageRaw: any,
40
+ swiftOrderParamsMessage: SwiftOrderParamsMessage
41
+ ) => Promise<void>
42
+ ) {
43
+ this.driftClient = config.driftClient;
44
+ this.userMap = config.userMap;
45
+ }
46
+
47
+ getSymbolForMarketIndex(marketIndex: number): string {
48
+ const markets =
49
+ this.config.driftEnv === 'devnet'
50
+ ? DevnetPerpMarkets
51
+ : MainnetPerpMarkets;
52
+ return markets[marketIndex].symbol;
53
+ }
54
+
55
+ generateChallengeResponse(nonce: string): string {
56
+ const messageBytes = decodeUTF8(nonce);
57
+ const signature = nacl.sign.detached(
58
+ messageBytes,
59
+ this.config.keypair.secretKey
60
+ );
61
+ const signatureBase64 = Buffer.from(signature).toString('base64');
62
+ return signatureBase64;
63
+ }
64
+
65
+ handleAuthMessage(message: any): void {
66
+ if (message['channel'] === 'auth' && message['nonce'] != null) {
67
+ const signatureBase64 = this.generateChallengeResponse(message['nonce']);
68
+ this.ws?.send(
69
+ JSON.stringify({
70
+ pubkey: this.config.keypair.publicKey.toBase58(),
71
+ signature: signatureBase64,
72
+ })
73
+ );
74
+ }
75
+
76
+ if (
77
+ message['channel'] === 'auth' &&
78
+ message['message']?.toLowerCase() === 'authenticated'
79
+ ) {
80
+ this.subscribed = true;
81
+ this.config.marketIndexes.forEach(async (marketIndex) => {
82
+ this.ws?.send(
83
+ JSON.stringify({
84
+ action: 'subscribe',
85
+ market_type: 'perp',
86
+ market_name: this.getSymbolForMarketIndex(marketIndex),
87
+ })
88
+ );
89
+ await new Promise((resolve) => setTimeout(resolve, 100));
90
+ });
91
+ }
92
+ }
93
+
94
+ async subscribe(): Promise<void> {
95
+ const endpoint =
96
+ this.config.endpoint || this.config.driftEnv === 'devnet'
97
+ ? 'wss://master.swift.drift.trade/ws'
98
+ : 'wss://swift.drift.trade/ws';
99
+ const ws = new WebSocket(
100
+ endpoint + '?pubkey=' + this.config.keypair.publicKey.toBase58()
101
+ );
102
+ this.ws = ws;
103
+ ws.on('open', async () => {
104
+ console.log('Connected to the server');
105
+
106
+ ws.on('message', async (data: WebSocket.Data) => {
107
+ const message = JSON.parse(data.toString());
108
+ this.startHeartbeatTimer();
109
+
110
+ if (message['channel'] === 'auth') {
111
+ this.handleAuthMessage(message);
112
+ }
113
+
114
+ if (message['order']) {
115
+ const order = JSON.parse(message['order']);
116
+ const swiftOrderParamsBuf = Buffer.from(
117
+ order['order_message'],
118
+ 'base64'
119
+ );
120
+ const swiftOrderParamsMessage: SwiftOrderParamsMessage =
121
+ this.driftClient.program.coder.types.decode(
122
+ 'SwiftOrderParamsMessage',
123
+ swiftOrderParamsBuf
124
+ );
125
+
126
+ if (!swiftOrderParamsMessage.swiftOrderParams.price) {
127
+ console.error(
128
+ `order has no price: ${JSON.stringify(
129
+ swiftOrderParamsMessage.swiftOrderParams
130
+ )}`
131
+ );
132
+ return;
133
+ }
134
+
135
+ this.onOrder(order, swiftOrderParamsMessage);
136
+ }
137
+ });
138
+
139
+ ws.on('close', () => {
140
+ console.log('Disconnected from the server');
141
+ this.reconnect();
142
+ });
143
+
144
+ ws.on('error', (error: Error) => {
145
+ console.error('WebSocket error:', error);
146
+ this.reconnect();
147
+ });
148
+ });
149
+ }
150
+
151
+ async getPlaceAndMakeSwiftOrderIxs(
152
+ orderMessageRaw: any,
153
+ swiftOrderParamsMessage: SwiftOrderParamsMessage,
154
+ makerOrderParams: OptionalOrderParams
155
+ ): Promise<TransactionInstruction[]> {
156
+ const swiftOrderParamsBuf = Buffer.from(
157
+ orderMessageRaw['order_message'],
158
+ 'base64'
159
+ );
160
+ const takerAuthority = new PublicKey(orderMessageRaw['taker_authority']);
161
+ const takerUserPubkey = await getUserAccountPublicKey(
162
+ this.driftClient.program.programId,
163
+ takerAuthority,
164
+ swiftOrderParamsMessage.subAccountId
165
+ );
166
+ const takerUserAccount = (
167
+ await this.userMap.mustGet(takerUserPubkey.toString())
168
+ ).getUserAccount();
169
+ const ixs = await this.driftClient.getPlaceAndMakeSwiftPerpOrderIxs(
170
+ swiftOrderParamsBuf,
171
+ Buffer.from(orderMessageRaw['order_signature'], 'base64'),
172
+ decodeUTF8(orderMessageRaw['uuid']),
173
+ {
174
+ taker: takerUserPubkey,
175
+ takerUserAccount,
176
+ takerStats: getUserStatsAccountPublicKey(
177
+ this.driftClient.program.programId,
178
+ takerUserAccount.authority
179
+ ),
180
+ },
181
+ Object.assign({}, makerOrderParams, {
182
+ postOnly: PostOnlyParams.MUST_POST_ONLY,
183
+ immediateOrCancel: true,
184
+ marketType: MarketType.PERP,
185
+ })
186
+ );
187
+ return ixs;
188
+ }
189
+
190
+ private startHeartbeatTimer() {
191
+ if (this.heartbeatTimeout) {
192
+ clearTimeout(this.heartbeatTimeout);
193
+ }
194
+ this.heartbeatTimeout = setTimeout(() => {
195
+ console.warn('No heartbeat received within 30 seconds, reconnecting...');
196
+ this.reconnect();
197
+ }, this.heartbeatIntervalMs);
198
+ }
199
+
200
+ private reconnect() {
201
+ if (this.ws) {
202
+ this.ws.removeAllListeners();
203
+ this.ws.terminate();
204
+ }
205
+
206
+ console.log('Reconnecting to WebSocket...');
207
+ setTimeout(() => {
208
+ this.subscribe();
209
+ }, 1000);
210
+ }
211
+ }