@drift-labs/sdk 2.106.0-beta.1 → 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/index.d.ts +1 -0
- package/lib/browser/index.js +1 -0
- package/lib/browser/swift/swiftOrderSubscriber.d.ts +28 -0
- package/lib/browser/swift/swiftOrderSubscriber.js +129 -0
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.js +1 -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/index.ts +1 -0
- package/src/swift/swiftOrderSubscriber.ts +211 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.106.0-beta.
|
|
1
|
+
2.106.0-beta.2
|
package/lib/browser/index.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export * from './oracles/pythClient';
|
|
|
86
86
|
export * from './oracles/pythPullClient';
|
|
87
87
|
export * from './oracles/pythLazerClient';
|
|
88
88
|
export * from './oracles/switchboardOnDemandClient';
|
|
89
|
+
export * from './swift/swiftOrderSubscriber';
|
|
89
90
|
export * from './tx/fastSingleTxSender';
|
|
90
91
|
export * from './tx/retryTxSender';
|
|
91
92
|
export * from './tx/whileValidTxSender';
|
package/lib/browser/index.js
CHANGED
|
@@ -109,6 +109,7 @@ __exportStar(require("./oracles/pythClient"), exports);
|
|
|
109
109
|
__exportStar(require("./oracles/pythPullClient"), exports);
|
|
110
110
|
__exportStar(require("./oracles/pythLazerClient"), exports);
|
|
111
111
|
__exportStar(require("./oracles/switchboardOnDemandClient"), exports);
|
|
112
|
+
__exportStar(require("./swift/swiftOrderSubscriber"), exports);
|
|
112
113
|
__exportStar(require("./tx/fastSingleTxSender"), exports);
|
|
113
114
|
__exportStar(require("./tx/retryTxSender"), exports);
|
|
114
115
|
__exportStar(require("./tx/whileValidTxSender"), exports);
|
|
@@ -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/lib/node/index.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export * from './oracles/pythClient';
|
|
|
86
86
|
export * from './oracles/pythPullClient';
|
|
87
87
|
export * from './oracles/pythLazerClient';
|
|
88
88
|
export * from './oracles/switchboardOnDemandClient';
|
|
89
|
+
export * from './swift/swiftOrderSubscriber';
|
|
89
90
|
export * from './tx/fastSingleTxSender';
|
|
90
91
|
export * from './tx/retryTxSender';
|
|
91
92
|
export * from './tx/whileValidTxSender';
|
package/lib/node/index.js
CHANGED
|
@@ -109,6 +109,7 @@ __exportStar(require("./oracles/pythClient"), exports);
|
|
|
109
109
|
__exportStar(require("./oracles/pythPullClient"), exports);
|
|
110
110
|
__exportStar(require("./oracles/pythLazerClient"), exports);
|
|
111
111
|
__exportStar(require("./oracles/switchboardOnDemandClient"), exports);
|
|
112
|
+
__exportStar(require("./swift/swiftOrderSubscriber"), exports);
|
|
112
113
|
__exportStar(require("./tx/fastSingleTxSender"), exports);
|
|
113
114
|
__exportStar(require("./tx/retryTxSender"), exports);
|
|
114
115
|
__exportStar(require("./tx/whileValidTxSender"), exports);
|
|
@@ -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",
|
|
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"
|
package/src/index.ts
CHANGED
|
@@ -87,6 +87,7 @@ export * from './oracles/pythClient';
|
|
|
87
87
|
export * from './oracles/pythPullClient';
|
|
88
88
|
export * from './oracles/pythLazerClient';
|
|
89
89
|
export * from './oracles/switchboardOnDemandClient';
|
|
90
|
+
export * from './swift/swiftOrderSubscriber';
|
|
90
91
|
export * from './tx/fastSingleTxSender';
|
|
91
92
|
export * from './tx/retryTxSender';
|
|
92
93
|
export * from './tx/whileValidTxSender';
|
|
@@ -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
|
+
}
|