@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 +1 -1
- package/lib/browser/constants/numericConstants.d.ts +1 -0
- package/lib/browser/constants/numericConstants.js +2 -1
- package/lib/browser/index.d.ts +2 -0
- package/lib/browser/index.js +2 -0
- package/lib/browser/math/liquidation.d.ts +4 -0
- package/lib/browser/math/liquidation.js +46 -0
- package/lib/browser/swift/swiftOrderSubscriber.d.ts +28 -0
- package/lib/browser/swift/swiftOrderSubscriber.js +129 -0
- package/lib/node/constants/numericConstants.d.ts +1 -0
- package/lib/node/constants/numericConstants.js +2 -1
- package/lib/node/index.d.ts +2 -0
- package/lib/node/index.js +2 -0
- package/lib/node/math/liquidation.d.ts +4 -0
- package/lib/node/math/liquidation.js +46 -0
- package/lib/node/swift/swiftOrderSubscriber.d.ts +28 -0
- package/lib/node/swift/swiftOrderSubscriber.js +129 -0
- package/package.json +2 -1
- package/src/constants/numericConstants.ts +3 -0
- package/src/index.ts +2 -0
- package/src/math/liquidation.ts +79 -0
- package/src/swift/swiftOrderSubscriber.ts +211 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.106.0-beta.
|
|
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);
|
package/lib/browser/index.d.ts
CHANGED
|
@@ -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/browser/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;
|
|
@@ -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);
|
package/lib/node/index.d.ts
CHANGED
|
@@ -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.
|
|
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
|
+
}
|