@drift-labs/jit-proxy 0.6.0 → 0.7.0

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/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types/jit_proxy';
2
2
  export * from './jitProxyClient';
3
- export * from './jitter';
3
+ export * from './jitter/jitterSniper';
4
+ export * from './jitter/jitterShotgun';
package/lib/index.js CHANGED
@@ -12,4 +12,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  __exportStar(require("./types/jit_proxy"), exports);
14
14
  __exportStar(require("./jitProxyClient"), exports);
15
- __exportStar(require("./jitter"), exports);
15
+ __exportStar(require("./jitter/jitterSniper"), exports);
16
+ __exportStar(require("./jitter/jitterShotgun"), exports);
@@ -32,5 +32,5 @@ export declare class JitProxyClient {
32
32
  programId: PublicKey;
33
33
  });
34
34
  jit(params: JitIxParams, txParams?: TxParams): Promise<TxSigAndSlot>;
35
- getJitIx({ takerKey, takerStatsKey, taker, takerOrderId, maxPosition, minPosition, bid, ask, postOnly, priceType, referrerInfo, subAccountId }: JitIxParams): Promise<TransactionInstruction>;
35
+ getJitIx({ takerKey, takerStatsKey, taker, takerOrderId, maxPosition, minPosition, bid, ask, postOnly, priceType, referrerInfo, subAccountId, }: JitIxParams): Promise<TransactionInstruction>;
36
36
  }
@@ -19,8 +19,11 @@ class JitProxyClient {
19
19
  const tx = await this.driftClient.buildTransaction([ix], txParams);
20
20
  return await this.driftClient.sendTransaction(tx);
21
21
  }
22
- async getJitIx({ takerKey, takerStatsKey, taker, takerOrderId, maxPosition, minPosition, bid, ask, postOnly = null, priceType = PriceType.LIMIT, referrerInfo, subAccountId }) {
23
- subAccountId = subAccountId !== undefined ? subAccountId : this.driftClient.activeSubAccountId;
22
+ async getJitIx({ takerKey, takerStatsKey, taker, takerOrderId, maxPosition, minPosition, bid, ask, postOnly = null, priceType = PriceType.LIMIT, referrerInfo, subAccountId, }) {
23
+ subAccountId =
24
+ subAccountId !== undefined
25
+ ? subAccountId
26
+ : this.driftClient.activeSubAccountId;
24
27
  const order = taker.orders.find((order) => order.orderId === takerOrderId);
25
28
  const remainingAccounts = this.driftClient.getRemainingAccounts({
26
29
  userAccounts: [taker, this.driftClient.getUserAccount(subAccountId)],
@@ -0,0 +1,35 @@
1
+ /// <reference types="bn.js" />
2
+ import { JitProxyClient, PriceType } from '../jitProxyClient';
3
+ import { PublicKey } from '@solana/web3.js';
4
+ import { AuctionSubscriber, BN, DriftClient, Order, UserAccount, UserStatsMap } from '@drift-labs/sdk';
5
+ export declare type UserFilter = (userAccount: UserAccount, userKey: string, order: Order) => boolean;
6
+ export declare type JitParams = {
7
+ bid: BN;
8
+ ask: BN;
9
+ minPosition: BN;
10
+ maxPosition: any;
11
+ priceType: PriceType;
12
+ subAccountId?: number;
13
+ };
14
+ export declare abstract class BaseJitter {
15
+ auctionSubscriber: AuctionSubscriber;
16
+ driftClient: DriftClient;
17
+ jitProxyClient: JitProxyClient;
18
+ userStatsMap: UserStatsMap;
19
+ perpParams: Map<number, JitParams>;
20
+ spotParams: Map<number, JitParams>;
21
+ onGoingAuctions: Map<string, Promise<void>>;
22
+ userFilter: UserFilter;
23
+ constructor({ auctionSubscriber, jitProxyClient, driftClient, userStatsMap, }: {
24
+ driftClient: DriftClient;
25
+ auctionSubscriber: AuctionSubscriber;
26
+ jitProxyClient: JitProxyClient;
27
+ userStatsMap?: UserStatsMap;
28
+ });
29
+ subscribe(): Promise<void>;
30
+ createTryFill(taker: UserAccount, takerKey: PublicKey, takerStatsKey: PublicKey, order: Order, orderSignature: string): () => Promise<void>;
31
+ getOrderSignatures(takerKey: string, orderId: number): string;
32
+ updatePerpParams(marketIndex: number, params: JitParams): void;
33
+ updateSpotParams(marketIndex: number, params: JitParams): void;
34
+ setUserFilter(userFilter: UserFilter | undefined): void;
35
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseJitter = void 0;
4
+ const sdk_1 = require("@drift-labs/sdk");
5
+ class BaseJitter {
6
+ constructor({ auctionSubscriber, jitProxyClient, driftClient, userStatsMap, }) {
7
+ this.perpParams = new Map();
8
+ this.spotParams = new Map();
9
+ this.onGoingAuctions = new Map();
10
+ this.auctionSubscriber = auctionSubscriber;
11
+ this.driftClient = driftClient;
12
+ this.jitProxyClient = jitProxyClient;
13
+ this.userStatsMap =
14
+ userStatsMap ||
15
+ new sdk_1.UserStatsMap(this.driftClient, {
16
+ type: 'polling',
17
+ accountLoader: new sdk_1.BulkAccountLoader(this.driftClient.connection, 'confirmed', 0),
18
+ });
19
+ }
20
+ async subscribe() {
21
+ await this.driftClient.subscribe();
22
+ await this.userStatsMap.subscribe();
23
+ await this.auctionSubscriber.subscribe();
24
+ this.auctionSubscriber.eventEmitter.on('onAccountUpdate', async (taker, takerKey, slot) => {
25
+ const takerKeyString = takerKey.toBase58();
26
+ const takerStatsKey = sdk_1.getUserStatsAccountPublicKey(this.driftClient.program.programId, taker.authority);
27
+ for (const order of taker.orders) {
28
+ if (!sdk_1.isVariant(order.status, 'open')) {
29
+ continue;
30
+ }
31
+ if (!sdk_1.hasAuctionPrice(order, slot)) {
32
+ continue;
33
+ }
34
+ if (this.userFilter) {
35
+ if (this.userFilter(taker, takerKeyString, order)) {
36
+ return;
37
+ }
38
+ }
39
+ const orderSignature = this.getOrderSignatures(takerKeyString, order.orderId);
40
+ if (this.onGoingAuctions.has(orderSignature)) {
41
+ continue;
42
+ }
43
+ if (sdk_1.isVariant(order.marketType, 'perp')) {
44
+ if (!this.perpParams.has(order.marketIndex)) {
45
+ return;
46
+ }
47
+ const perpMarketAccount = this.driftClient.getPerpMarketAccount(order.marketIndex);
48
+ if (order.baseAssetAmount
49
+ .sub(order.baseAssetAmountFilled)
50
+ .lte(perpMarketAccount.amm.minOrderSize)) {
51
+ return;
52
+ }
53
+ const promise = this.createTryFill(taker, takerKey, takerStatsKey, order, orderSignature).bind(this)();
54
+ this.onGoingAuctions.set(orderSignature, promise);
55
+ }
56
+ else {
57
+ if (!this.spotParams.has(order.marketIndex)) {
58
+ return;
59
+ }
60
+ const spotMarketAccount = this.driftClient.getSpotMarketAccount(order.marketIndex);
61
+ if (order.baseAssetAmount
62
+ .sub(order.baseAssetAmountFilled)
63
+ .lte(spotMarketAccount.minOrderSize)) {
64
+ return;
65
+ }
66
+ const promise = this.createTryFill(taker, takerKey, takerStatsKey, order, orderSignature).bind(this)();
67
+ this.onGoingAuctions.set(orderSignature, promise);
68
+ }
69
+ }
70
+ });
71
+ }
72
+ createTryFill(taker, takerKey, takerStatsKey, order, orderSignature) {
73
+ throw new Error('Not implemented');
74
+ }
75
+ getOrderSignatures(takerKey, orderId) {
76
+ return `${takerKey}-${orderId}`;
77
+ }
78
+ updatePerpParams(marketIndex, params) {
79
+ this.perpParams.set(marketIndex, params);
80
+ }
81
+ updateSpotParams(marketIndex, params) {
82
+ this.spotParams.set(marketIndex, params);
83
+ }
84
+ setUserFilter(userFilter) {
85
+ this.userFilter = userFilter;
86
+ }
87
+ }
88
+ exports.BaseJitter = BaseJitter;
@@ -0,0 +1,23 @@
1
+ /// <reference types="bn.js" />
2
+ import { JitProxyClient, PriceType } from '../jitProxyClient';
3
+ import { PublicKey } from '@solana/web3.js';
4
+ import { AuctionSubscriber, BN, DriftClient, Order, UserAccount, UserStatsMap } from '@drift-labs/sdk';
5
+ import { BaseJitter } from './baseJitter';
6
+ export declare type UserFilter = (userAccount: UserAccount, userKey: string, order: Order) => boolean;
7
+ export declare type JitParams = {
8
+ bid: BN;
9
+ ask: BN;
10
+ minPosition: BN;
11
+ maxPosition: any;
12
+ priceType: PriceType;
13
+ subAccountId?: number;
14
+ };
15
+ export declare class JitterShotgun extends BaseJitter {
16
+ constructor({ auctionSubscriber, jitProxyClient, driftClient, userStatsMap, }: {
17
+ driftClient: DriftClient;
18
+ auctionSubscriber: AuctionSubscriber;
19
+ jitProxyClient: JitProxyClient;
20
+ userStatsMap?: UserStatsMap;
21
+ });
22
+ createTryFill(taker: UserAccount, takerKey: PublicKey, takerStatsKey: PublicKey, order: Order, orderSignature: string): () => Promise<void>;
23
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JitterShotgun = void 0;
4
+ const baseJitter_1 = require("./baseJitter");
5
+ class JitterShotgun extends baseJitter_1.BaseJitter {
6
+ constructor({ auctionSubscriber, jitProxyClient, driftClient, userStatsMap, }) {
7
+ super({
8
+ auctionSubscriber,
9
+ jitProxyClient,
10
+ driftClient,
11
+ userStatsMap,
12
+ });
13
+ }
14
+ createTryFill(taker, takerKey, takerStatsKey, order, orderSignature) {
15
+ return async () => {
16
+ let i = 0;
17
+ while (i < 10) {
18
+ const params = this.perpParams.get(order.marketIndex);
19
+ if (!params) {
20
+ this.onGoingAuctions.delete(orderSignature);
21
+ return;
22
+ }
23
+ const takerStats = await this.userStatsMap.mustGet(taker.authority.toString());
24
+ const referrerInfo = takerStats.getReferrerInfo();
25
+ console.log(`Trying to fill ${orderSignature}`);
26
+ try {
27
+ const { txSig } = await this.jitProxyClient.jit({
28
+ takerKey,
29
+ takerStatsKey,
30
+ taker,
31
+ takerOrderId: order.orderId,
32
+ maxPosition: params.maxPosition,
33
+ minPosition: params.minPosition,
34
+ bid: params.bid,
35
+ ask: params.ask,
36
+ postOnly: null,
37
+ priceType: params.priceType,
38
+ referrerInfo,
39
+ subAccountId: params.subAccountId,
40
+ });
41
+ console.log(`Filled ${orderSignature} txSig ${txSig}`);
42
+ await sleep(10000);
43
+ this.onGoingAuctions.delete(orderSignature);
44
+ return;
45
+ }
46
+ catch (e) {
47
+ console.error(`Failed to fill ${orderSignature}`);
48
+ if (e.message.includes('0x1770') || e.message.includes('0x1771')) {
49
+ console.log('Order does not cross params yet, retrying');
50
+ }
51
+ else if (e.message.includes('0x1793')) {
52
+ console.log('Oracle invalid, retrying');
53
+ }
54
+ else {
55
+ await sleep(10000);
56
+ this.onGoingAuctions.delete(orderSignature);
57
+ return;
58
+ }
59
+ }
60
+ i++;
61
+ }
62
+ this.onGoingAuctions.delete(orderSignature);
63
+ };
64
+ }
65
+ }
66
+ exports.JitterShotgun = JitterShotgun;
67
+ function sleep(ms) {
68
+ return new Promise((resolve) => setTimeout(resolve, ms));
69
+ }
@@ -0,0 +1,32 @@
1
+ import { JitProxyClient } from '../jitProxyClient';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import { AuctionSubscriber, DriftClient, OraclePriceData, Order, SlotSubscriber, UserAccount, UserStatsMap } from '@drift-labs/sdk';
4
+ import { BaseJitter } from './baseJitter';
5
+ declare type AuctionAndOrderDetails = {
6
+ slotsTilCross: number;
7
+ willCross: boolean;
8
+ bid: number;
9
+ ask: number;
10
+ auctionStartPrice: number;
11
+ auctionEndPrice: number;
12
+ stepSize: number;
13
+ oraclePrice: OraclePriceData;
14
+ };
15
+ export declare class JitterSniper extends BaseJitter {
16
+ slotSubscriber: SlotSubscriber;
17
+ userStatsMap: UserStatsMap;
18
+ constructor({ auctionSubscriber, slotSubscriber, jitProxyClient, driftClient, userStatsMap, }: {
19
+ driftClient: DriftClient;
20
+ slotSubscriber: SlotSubscriber;
21
+ auctionSubscriber: AuctionSubscriber;
22
+ jitProxyClient: JitProxyClient;
23
+ userStatsMap?: UserStatsMap;
24
+ });
25
+ createTryFill(taker: UserAccount, takerKey: PublicKey, takerStatsKey: PublicKey, order: Order, orderSignature: string): () => Promise<void>;
26
+ getAuctionAndOrderDetails(order: Order): AuctionAndOrderDetails;
27
+ waitForSlotOrCrossOrExpiry(targetSlot: number, order: Order, initialDetails: AuctionAndOrderDetails): Promise<{
28
+ slot: number;
29
+ updatedDetails: AuctionAndOrderDetails;
30
+ }>;
31
+ }
32
+ export {};
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JitterSniper = void 0;
4
+ const sdk_1 = require("@drift-labs/sdk");
5
+ const baseJitter_1 = require("./baseJitter");
6
+ class JitterSniper extends baseJitter_1.BaseJitter {
7
+ constructor({ auctionSubscriber, slotSubscriber, jitProxyClient, driftClient, userStatsMap, }) {
8
+ super({
9
+ auctionSubscriber,
10
+ jitProxyClient,
11
+ driftClient,
12
+ userStatsMap,
13
+ });
14
+ this.slotSubscriber = slotSubscriber;
15
+ }
16
+ createTryFill(taker, takerKey, takerStatsKey, order, orderSignature) {
17
+ return async () => {
18
+ const params = this.perpParams.get(order.marketIndex);
19
+ if (!params) {
20
+ this.onGoingAuctions.delete(orderSignature);
21
+ return;
22
+ }
23
+ const takerStats = await this.userStatsMap.mustGet(taker.authority.toString());
24
+ const referrerInfo = takerStats.getReferrerInfo();
25
+ const { slotsTilCross, willCross, bid, ask, auctionStartPrice, auctionEndPrice, stepSize, oraclePrice, } = this.getAuctionAndOrderDetails(order);
26
+ console.log(`
27
+ Taker wants to ${JSON.stringify(order.direction)}, order slot is ${order.slot.toNumber()},
28
+ My market: ${bid}@${ask},
29
+ Auction: ${auctionStartPrice} -> ${auctionEndPrice}, step size ${stepSize}
30
+ Current slot: ${this.slotSubscriber.currentSlot}, Order slot: ${order.slot.toNumber()},
31
+ Will cross?: ${willCross}
32
+ Slots to wait: ${slotsTilCross}. Target slot = ${order.slot.toNumber() + slotsTilCross}
33
+ `);
34
+ this.waitForSlotOrCrossOrExpiry(willCross
35
+ ? order.slot.toNumber() + slotsTilCross
36
+ : order.slot.toNumber() + order.auctionDuration + 1, order, {
37
+ slotsTilCross,
38
+ willCross,
39
+ bid,
40
+ ask,
41
+ auctionStartPrice,
42
+ auctionEndPrice,
43
+ stepSize,
44
+ oraclePrice,
45
+ }).then(async ({ slot, updatedDetails }) => {
46
+ if (slot === -1) {
47
+ console.log('Auction expired without crossing');
48
+ this.onGoingAuctions.delete(orderSignature);
49
+ return;
50
+ }
51
+ const params = sdk_1.isVariant(order.marketType, 'perp')
52
+ ? this.perpParams.get(order.marketIndex)
53
+ : this.spotParams.get(order.marketIndex);
54
+ const bid = sdk_1.isVariant(params.priceType, 'oracle')
55
+ ? sdk_1.convertToNumber(oraclePrice.price.add(params.bid), sdk_1.PRICE_PRECISION)
56
+ : sdk_1.convertToNumber(params.bid, sdk_1.PRICE_PRECISION);
57
+ const ask = sdk_1.isVariant(params.priceType, 'oracle')
58
+ ? sdk_1.convertToNumber(oraclePrice.price.add(params.ask), sdk_1.PRICE_PRECISION)
59
+ : sdk_1.convertToNumber(params.ask, sdk_1.PRICE_PRECISION);
60
+ const auctionPrice = sdk_1.convertToNumber(sdk_1.getAuctionPrice(order, slot, updatedDetails.oraclePrice.price), sdk_1.PRICE_PRECISION);
61
+ console.log(`
62
+ Expected auction price: ${auctionStartPrice + slotsTilCross * stepSize}
63
+ Actual auction price: ${auctionPrice}
64
+ -----------------
65
+ Looking for slot ${order.slot.toNumber() + slotsTilCross}
66
+ Got slot ${slot}
67
+ `);
68
+ console.log(`Trying to fill ${orderSignature} with:
69
+ market: ${bid}@${ask}
70
+ auction price: ${auctionPrice}
71
+ submitting" ${sdk_1.convertToNumber(params.bid, sdk_1.PRICE_PRECISION)}@${sdk_1.convertToNumber(params.ask, sdk_1.PRICE_PRECISION)}
72
+ `);
73
+ let i = 0;
74
+ while (i < 3) {
75
+ try {
76
+ const { txSig } = await this.jitProxyClient.jit({
77
+ takerKey,
78
+ takerStatsKey,
79
+ taker,
80
+ takerOrderId: order.orderId,
81
+ maxPosition: params.maxPosition,
82
+ minPosition: params.minPosition,
83
+ bid: params.bid,
84
+ ask: params.ask,
85
+ postOnly: null,
86
+ priceType: params.priceType,
87
+ referrerInfo,
88
+ subAccountId: params.subAccountId,
89
+ });
90
+ console.log(`Filled ${orderSignature} txSig ${txSig}`);
91
+ await sleep(3000);
92
+ this.onGoingAuctions.delete(orderSignature);
93
+ return;
94
+ }
95
+ catch (e) {
96
+ console.error(`Failed to fill ${orderSignature}`);
97
+ if (e.message.includes('0x1770') || e.message.includes('0x1771')) {
98
+ console.log('Order does not cross params yet');
99
+ }
100
+ else if (e.message.includes('0x1793')) {
101
+ console.log('Oracle invalid');
102
+ }
103
+ else {
104
+ await sleep(3000);
105
+ this.onGoingAuctions.delete(orderSignature);
106
+ return;
107
+ }
108
+ }
109
+ await sleep(50);
110
+ i++;
111
+ }
112
+ });
113
+ this.onGoingAuctions.delete(orderSignature);
114
+ };
115
+ }
116
+ getAuctionAndOrderDetails(order) {
117
+ // Find number of slots until the order is expected to be in cross
118
+ const params = sdk_1.isVariant(order.marketType, 'perp')
119
+ ? this.perpParams.get(order.marketIndex)
120
+ : this.spotParams.get(order.marketIndex);
121
+ const oraclePrice = sdk_1.isVariant(order.marketType, 'perp')
122
+ ? this.driftClient.getOracleDataForPerpMarket(order.marketIndex)
123
+ : this.driftClient.getOracleDataForSpotMarket(order.marketIndex);
124
+ const makerOrderDir = sdk_1.isVariant(order.direction, 'long') ? 'sell' : 'buy';
125
+ const auctionStartPrice = sdk_1.convertToNumber(sdk_1.isVariant(order.orderType, 'oracle')
126
+ ? sdk_1.getAuctionPriceForOracleOffsetAuction(order, order.slot.toNumber(), oraclePrice.price)
127
+ : order.auctionStartPrice, sdk_1.PRICE_PRECISION);
128
+ const auctionEndPrice = sdk_1.convertToNumber(sdk_1.isVariant(order.orderType, 'oracle')
129
+ ? sdk_1.getAuctionPriceForOracleOffsetAuction(order, order.slot.toNumber() + order.auctionDuration - 1, oraclePrice.price)
130
+ : order.auctionEndPrice, sdk_1.PRICE_PRECISION);
131
+ const bid = sdk_1.isVariant(params.priceType, 'oracle')
132
+ ? sdk_1.convertToNumber(oraclePrice.price.add(params.bid), sdk_1.PRICE_PRECISION)
133
+ : sdk_1.convertToNumber(params.bid, sdk_1.PRICE_PRECISION);
134
+ const ask = sdk_1.isVariant(params.priceType, 'oracle')
135
+ ? sdk_1.convertToNumber(oraclePrice.price.add(params.ask), sdk_1.PRICE_PRECISION)
136
+ : sdk_1.convertToNumber(params.ask, sdk_1.PRICE_PRECISION);
137
+ let slotsTilCross = 0;
138
+ let willCross = false;
139
+ const stepSize = (auctionEndPrice - auctionStartPrice) / (order.auctionDuration - 1);
140
+ while (slotsTilCross < order.auctionDuration) {
141
+ if (makerOrderDir === 'buy') {
142
+ if (sdk_1.convertToNumber(sdk_1.getAuctionPrice(order, order.slot.toNumber() + slotsTilCross, oraclePrice.price), sdk_1.PRICE_PRECISION) <= bid) {
143
+ willCross = true;
144
+ break;
145
+ }
146
+ }
147
+ else {
148
+ if (sdk_1.convertToNumber(sdk_1.getAuctionPrice(order, order.slot.toNumber() + slotsTilCross, oraclePrice.price), sdk_1.PRICE_PRECISION) >= ask) {
149
+ willCross = true;
150
+ break;
151
+ }
152
+ }
153
+ slotsTilCross++;
154
+ }
155
+ return {
156
+ slotsTilCross,
157
+ willCross,
158
+ bid,
159
+ ask,
160
+ auctionStartPrice,
161
+ auctionEndPrice,
162
+ stepSize,
163
+ oraclePrice,
164
+ };
165
+ }
166
+ async waitForSlotOrCrossOrExpiry(targetSlot, order, initialDetails) {
167
+ const auctionEndSlot = order.auctionDuration + order.slot.toNumber();
168
+ let currentDetails = initialDetails;
169
+ let willCross = initialDetails.willCross;
170
+ if (this.slotSubscriber.currentSlot > auctionEndSlot) {
171
+ return new Promise((resolve) => resolve({ slot: -1, updatedDetails: currentDetails }));
172
+ }
173
+ return new Promise((resolve) => {
174
+ // Immediately return if we are past target slot
175
+ const slotListener = (slot) => {
176
+ if (slot >= targetSlot && willCross) {
177
+ resolve({ slot, updatedDetails: currentDetails });
178
+ }
179
+ };
180
+ // Otherwise listen for new slots in case we hit the target slot and we're gonna cross
181
+ this.slotSubscriber.eventEmitter.once('newSlot', slotListener);
182
+ // Update target slot as the bid/ask and the oracle changes
183
+ const intervalId = setInterval(async () => {
184
+ if (this.slotSubscriber.currentSlot >= auctionEndSlot) {
185
+ this.slotSubscriber.eventEmitter.removeListener('newSlot', slotListener);
186
+ clearInterval(intervalId);
187
+ resolve({ slot: -1, updatedDetails: currentDetails });
188
+ }
189
+ currentDetails = this.getAuctionAndOrderDetails(order);
190
+ willCross = currentDetails.willCross;
191
+ if (willCross) {
192
+ targetSlot = order.slot.toNumber() + currentDetails.slotsTilCross;
193
+ }
194
+ }, 50);
195
+ });
196
+ }
197
+ }
198
+ exports.JitterSniper = JitterSniper;
199
+ function sleep(ms) {
200
+ return new Promise((resolve) => setTimeout(resolve, ms));
201
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@drift-labs/jit-proxy",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "dependencies": {
5
5
  "@coral-xyz/anchor": "^0.26.0",
6
6
  "@solana/web3.js": "1.73.2",
7
- "@drift-labs/sdk": "2.33.0"
7
+ "@drift-labs/sdk": "^2.37.1-beta.2"
8
8
  },
9
9
  "engines": {
10
10
  "node": ">=16"
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types/jit_proxy';
2
2
  export * from './jitProxyClient';
3
- export * from './jitter';
3
+ export * from './jitter/jitterSniper';
4
+ export * from './jitter/jitterShotgun';
@@ -69,9 +69,12 @@ export class JitProxyClient {
69
69
  postOnly = null,
70
70
  priceType = PriceType.LIMIT,
71
71
  referrerInfo,
72
- subAccountId
72
+ subAccountId,
73
73
  }: JitIxParams): Promise<TransactionInstruction> {
74
- subAccountId = subAccountId !== undefined ? subAccountId : this.driftClient.activeSubAccountId;
74
+ subAccountId =
75
+ subAccountId !== undefined
76
+ ? subAccountId
77
+ : this.driftClient.activeSubAccountId;
75
78
  const order = taker.orders.find((order) => order.orderId === takerOrderId);
76
79
  const remainingAccounts = this.driftClient.getRemainingAccounts({
77
80
  userAccounts: [taker, this.driftClient.getUserAccount(subAccountId)],
@@ -1,14 +1,23 @@
1
- import { JitProxyClient, PriceType } from './jitProxyClient';
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { JitProxyClient, PriceType } from '../jitProxyClient';
2
3
  import { PublicKey } from '@solana/web3.js';
3
4
  import {
4
5
  AuctionSubscriber,
5
- BN, BulkAccountLoader,
6
+ BN,
7
+ BulkAccountLoader,
8
+ convertToNumber,
6
9
  DriftClient,
10
+ getAuctionPrice,
11
+ getAuctionPriceForOracleOffsetAuction,
7
12
  getUserStatsAccountPublicKey,
8
13
  hasAuctionPrice,
9
14
  isVariant,
15
+ OraclePriceData,
10
16
  Order,
11
- UserAccount, UserMap, UserStatsMap,
17
+ PRICE_PRECISION,
18
+ SlotSubscriber,
19
+ UserAccount,
20
+ UserStatsMap,
12
21
  } from '@drift-labs/sdk';
13
22
 
14
23
  export type UserFilter = (
@@ -26,7 +35,7 @@ export type JitParams = {
26
35
  subAccountId?: number;
27
36
  };
28
37
 
29
- export class Jitter {
38
+ export abstract class BaseJitter {
30
39
  auctionSubscriber: AuctionSubscriber;
31
40
  driftClient: DriftClient;
32
41
  jitProxyClient: JitProxyClient;
@@ -53,10 +62,16 @@ export class Jitter {
53
62
  this.auctionSubscriber = auctionSubscriber;
54
63
  this.driftClient = driftClient;
55
64
  this.jitProxyClient = jitProxyClient;
56
- this.userStatsMap = userStatsMap || new UserStatsMap(this.driftClient, {
57
- type: 'polling',
58
- accountLoader: new BulkAccountLoader(this.driftClient.connection, 'confirmed', 0),
59
- });
65
+ this.userStatsMap =
66
+ userStatsMap ||
67
+ new UserStatsMap(this.driftClient, {
68
+ type: 'polling',
69
+ accountLoader: new BulkAccountLoader(
70
+ this.driftClient.connection,
71
+ 'confirmed',
72
+ 0
73
+ ),
74
+ });
60
75
  }
61
76
 
62
77
  async subscribe(): Promise<void> {
@@ -92,6 +107,7 @@ export class Jitter {
92
107
  takerKeyString,
93
108
  order.orderId
94
109
  );
110
+
95
111
  if (this.onGoingAuctions.has(orderSignature)) {
96
112
  continue;
97
113
  }
@@ -101,6 +117,17 @@ export class Jitter {
101
117
  return;
102
118
  }
103
119
 
120
+ const perpMarketAccount = this.driftClient.getPerpMarketAccount(
121
+ order.marketIndex
122
+ );
123
+ if (
124
+ order.baseAssetAmount
125
+ .sub(order.baseAssetAmountFilled)
126
+ .lte(perpMarketAccount.amm.minOrderSize)
127
+ ) {
128
+ return;
129
+ }
130
+
104
131
  const promise = this.createTryFill(
105
132
  taker,
106
133
  takerKey,
@@ -114,6 +141,17 @@ export class Jitter {
114
141
  return;
115
142
  }
116
143
 
144
+ const spotMarketAccount = this.driftClient.getSpotMarketAccount(
145
+ order.marketIndex
146
+ );
147
+ if (
148
+ order.baseAssetAmount
149
+ .sub(order.baseAssetAmountFilled)
150
+ .lte(spotMarketAccount.minOrderSize)
151
+ ) {
152
+ return;
153
+ }
154
+
117
155
  const promise = this.createTryFill(
118
156
  taker,
119
157
  takerKey,
@@ -135,56 +173,7 @@ export class Jitter {
135
173
  order: Order,
136
174
  orderSignature: string
137
175
  ): () => Promise<void> {
138
- return async () => {
139
- let i = 0;
140
- while (i < 10) {
141
- const params = this.perpParams.get(order.marketIndex);
142
- if (!params) {
143
- this.onGoingAuctions.delete(orderSignature);
144
- return;
145
- }
146
-
147
- const takerStats = await this.userStatsMap.mustGet(taker.authority.toString());
148
- const referrerInfo = takerStats.getReferrerInfo();
149
-
150
- console.log(`Trying to fill ${orderSignature}`);
151
- try {
152
- const { txSig } = await this.jitProxyClient.jit({
153
- takerKey,
154
- takerStatsKey,
155
- taker,
156
- takerOrderId: order.orderId,
157
- maxPosition: params.maxPosition,
158
- minPosition: params.minPosition,
159
- bid: params.bid,
160
- ask: params.ask,
161
- postOnly: null,
162
- priceType: params.priceType,
163
- referrerInfo,
164
- subAccountId: params.subAccountId,
165
- });
166
-
167
- console.log(`Filled ${orderSignature} txSig ${txSig}`);
168
- await sleep(10000);
169
- this.onGoingAuctions.delete(orderSignature);
170
- return;
171
- } catch (e) {
172
- console.error(`Failed to fill ${orderSignature}`);
173
- if (e.message.includes('0x1770') || e.message.includes('0x1771')) {
174
- console.log('Order does not cross params yet, retrying');
175
- } else if (e.message.includes('0x1793')) {
176
- console.log('Oracle invalid, retrying');
177
- } else {
178
- await sleep(10000);
179
- this.onGoingAuctions.delete(orderSignature);
180
- return;
181
- }
182
- }
183
- i++;
184
- }
185
-
186
- this.onGoingAuctions.delete(orderSignature);
187
- };
176
+ throw new Error('Not implemented');
188
177
  }
189
178
 
190
179
  getOrderSignatures(takerKey: string, orderId: number): string {
@@ -203,7 +192,3 @@ export class Jitter {
203
192
  this.userFilter = userFilter;
204
193
  }
205
194
  }
206
-
207
- function sleep(ms: number): Promise<void> {
208
- return new Promise((resolve) => setTimeout(resolve, ms));
209
- }
@@ -0,0 +1,112 @@
1
+ import { JitProxyClient, PriceType } from '../jitProxyClient';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import {
4
+ AuctionSubscriber,
5
+ BN,
6
+ DriftClient,
7
+ Order,
8
+ UserAccount,
9
+ UserStatsMap,
10
+ } from '@drift-labs/sdk';
11
+ import { BaseJitter } from './baseJitter';
12
+
13
+ export type UserFilter = (
14
+ userAccount: UserAccount,
15
+ userKey: string,
16
+ order: Order
17
+ ) => boolean;
18
+
19
+ export type JitParams = {
20
+ bid: BN;
21
+ ask: BN;
22
+ minPosition: BN;
23
+ maxPosition;
24
+ priceType: PriceType;
25
+ subAccountId?: number;
26
+ };
27
+
28
+ export class JitterShotgun extends BaseJitter {
29
+ constructor({
30
+ auctionSubscriber,
31
+ jitProxyClient,
32
+ driftClient,
33
+ userStatsMap,
34
+ }: {
35
+ driftClient: DriftClient;
36
+ auctionSubscriber: AuctionSubscriber;
37
+ jitProxyClient: JitProxyClient;
38
+ userStatsMap?: UserStatsMap;
39
+ }) {
40
+ super({
41
+ auctionSubscriber,
42
+ jitProxyClient,
43
+ driftClient,
44
+ userStatsMap,
45
+ });
46
+ }
47
+
48
+ createTryFill(
49
+ taker: UserAccount,
50
+ takerKey: PublicKey,
51
+ takerStatsKey: PublicKey,
52
+ order: Order,
53
+ orderSignature: string
54
+ ): () => Promise<void> {
55
+ return async () => {
56
+ let i = 0;
57
+ while (i < 10) {
58
+ const params = this.perpParams.get(order.marketIndex);
59
+ if (!params) {
60
+ this.onGoingAuctions.delete(orderSignature);
61
+ return;
62
+ }
63
+
64
+ const takerStats = await this.userStatsMap.mustGet(
65
+ taker.authority.toString()
66
+ );
67
+ const referrerInfo = takerStats.getReferrerInfo();
68
+
69
+ console.log(`Trying to fill ${orderSignature}`);
70
+ try {
71
+ const { txSig } = await this.jitProxyClient.jit({
72
+ takerKey,
73
+ takerStatsKey,
74
+ taker,
75
+ takerOrderId: order.orderId,
76
+ maxPosition: params.maxPosition,
77
+ minPosition: params.minPosition,
78
+ bid: params.bid,
79
+ ask: params.ask,
80
+ postOnly: null,
81
+ priceType: params.priceType,
82
+ referrerInfo,
83
+ subAccountId: params.subAccountId,
84
+ });
85
+
86
+ console.log(`Filled ${orderSignature} txSig ${txSig}`);
87
+ await sleep(10000);
88
+ this.onGoingAuctions.delete(orderSignature);
89
+ return;
90
+ } catch (e) {
91
+ console.error(`Failed to fill ${orderSignature}`);
92
+ if (e.message.includes('0x1770') || e.message.includes('0x1771')) {
93
+ console.log('Order does not cross params yet, retrying');
94
+ } else if (e.message.includes('0x1793')) {
95
+ console.log('Oracle invalid, retrying');
96
+ } else {
97
+ await sleep(10000);
98
+ this.onGoingAuctions.delete(orderSignature);
99
+ return;
100
+ }
101
+ }
102
+ i++;
103
+ }
104
+
105
+ this.onGoingAuctions.delete(orderSignature);
106
+ };
107
+ }
108
+ }
109
+
110
+ function sleep(ms: number): Promise<void> {
111
+ return new Promise((resolve) => setTimeout(resolve, ms));
112
+ }
@@ -0,0 +1,330 @@
1
+ import { JitProxyClient } from '../jitProxyClient';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import {
4
+ AuctionSubscriber,
5
+ convertToNumber,
6
+ DriftClient,
7
+ getAuctionPrice,
8
+ getAuctionPriceForOracleOffsetAuction,
9
+ isVariant,
10
+ OraclePriceData,
11
+ Order,
12
+ PRICE_PRECISION,
13
+ SlotSubscriber,
14
+ UserAccount,
15
+ UserStatsMap,
16
+ } from '@drift-labs/sdk';
17
+ import { BaseJitter } from './baseJitter';
18
+
19
+ type AuctionAndOrderDetails = {
20
+ slotsTilCross: number;
21
+ willCross: boolean;
22
+ bid: number;
23
+ ask: number;
24
+ auctionStartPrice: number;
25
+ auctionEndPrice: number;
26
+ stepSize: number;
27
+ oraclePrice: OraclePriceData;
28
+ };
29
+
30
+ export class JitterSniper extends BaseJitter {
31
+ slotSubscriber: SlotSubscriber;
32
+ userStatsMap: UserStatsMap;
33
+
34
+ constructor({
35
+ auctionSubscriber,
36
+ slotSubscriber,
37
+ jitProxyClient,
38
+ driftClient,
39
+ userStatsMap,
40
+ }: {
41
+ driftClient: DriftClient;
42
+ slotSubscriber: SlotSubscriber;
43
+ auctionSubscriber: AuctionSubscriber;
44
+ jitProxyClient: JitProxyClient;
45
+ userStatsMap?: UserStatsMap;
46
+ }) {
47
+ super({
48
+ auctionSubscriber,
49
+ jitProxyClient,
50
+ driftClient,
51
+ userStatsMap,
52
+ });
53
+ this.slotSubscriber = slotSubscriber;
54
+ }
55
+
56
+ createTryFill(
57
+ taker: UserAccount,
58
+ takerKey: PublicKey,
59
+ takerStatsKey: PublicKey,
60
+ order: Order,
61
+ orderSignature: string
62
+ ): () => Promise<void> {
63
+ return async () => {
64
+ const params = this.perpParams.get(order.marketIndex);
65
+ if (!params) {
66
+ this.onGoingAuctions.delete(orderSignature);
67
+ return;
68
+ }
69
+
70
+ const takerStats = await this.userStatsMap.mustGet(
71
+ taker.authority.toString()
72
+ );
73
+ const referrerInfo = takerStats.getReferrerInfo();
74
+
75
+ const {
76
+ slotsTilCross,
77
+ willCross,
78
+ bid,
79
+ ask,
80
+ auctionStartPrice,
81
+ auctionEndPrice,
82
+ stepSize,
83
+ oraclePrice,
84
+ } = this.getAuctionAndOrderDetails(order);
85
+
86
+ console.log(`
87
+ Taker wants to ${JSON.stringify(
88
+ order.direction
89
+ )}, order slot is ${order.slot.toNumber()},
90
+ My market: ${bid}@${ask},
91
+ Auction: ${auctionStartPrice} -> ${auctionEndPrice}, step size ${stepSize}
92
+ Current slot: ${
93
+ this.slotSubscriber.currentSlot
94
+ }, Order slot: ${order.slot.toNumber()},
95
+ Will cross?: ${willCross}
96
+ Slots to wait: ${slotsTilCross}. Target slot = ${
97
+ order.slot.toNumber() + slotsTilCross
98
+ }
99
+ `);
100
+
101
+ this.waitForSlotOrCrossOrExpiry(
102
+ willCross
103
+ ? order.slot.toNumber() + slotsTilCross
104
+ : order.slot.toNumber() + order.auctionDuration + 1,
105
+ order,
106
+ {
107
+ slotsTilCross,
108
+ willCross,
109
+ bid,
110
+ ask,
111
+ auctionStartPrice,
112
+ auctionEndPrice,
113
+ stepSize,
114
+ oraclePrice,
115
+ }
116
+ ).then(async ({ slot, updatedDetails }) => {
117
+ if (slot === -1) {
118
+ console.log('Auction expired without crossing');
119
+ this.onGoingAuctions.delete(orderSignature);
120
+ return;
121
+ }
122
+
123
+ const params = isVariant(order.marketType, 'perp')
124
+ ? this.perpParams.get(order.marketIndex)
125
+ : this.spotParams.get(order.marketIndex);
126
+ const bid = isVariant(params.priceType, 'oracle')
127
+ ? convertToNumber(oraclePrice.price.add(params.bid), PRICE_PRECISION)
128
+ : convertToNumber(params.bid, PRICE_PRECISION);
129
+ const ask = isVariant(params.priceType, 'oracle')
130
+ ? convertToNumber(oraclePrice.price.add(params.ask), PRICE_PRECISION)
131
+ : convertToNumber(params.ask, PRICE_PRECISION);
132
+ const auctionPrice = convertToNumber(
133
+ getAuctionPrice(order, slot, updatedDetails.oraclePrice.price),
134
+ PRICE_PRECISION
135
+ );
136
+ console.log(`
137
+ Expected auction price: ${auctionStartPrice + slotsTilCross * stepSize}
138
+ Actual auction price: ${auctionPrice}
139
+ -----------------
140
+ Looking for slot ${order.slot.toNumber() + slotsTilCross}
141
+ Got slot ${slot}
142
+ `);
143
+
144
+ console.log(`Trying to fill ${orderSignature} with:
145
+ market: ${bid}@${ask}
146
+ auction price: ${auctionPrice}
147
+ submitting" ${convertToNumber(params.bid, PRICE_PRECISION)}@${convertToNumber(
148
+ params.ask,
149
+ PRICE_PRECISION
150
+ )}
151
+ `);
152
+ let i = 0;
153
+ while (i < 3) {
154
+ try {
155
+ const { txSig } = await this.jitProxyClient.jit({
156
+ takerKey,
157
+ takerStatsKey,
158
+ taker,
159
+ takerOrderId: order.orderId,
160
+ maxPosition: params.maxPosition,
161
+ minPosition: params.minPosition,
162
+ bid: params.bid,
163
+ ask: params.ask,
164
+ postOnly: null,
165
+ priceType: params.priceType,
166
+ referrerInfo,
167
+ subAccountId: params.subAccountId,
168
+ });
169
+
170
+ console.log(`Filled ${orderSignature} txSig ${txSig}`);
171
+ await sleep(3000);
172
+ this.onGoingAuctions.delete(orderSignature);
173
+ return;
174
+ } catch (e) {
175
+ console.error(`Failed to fill ${orderSignature}`);
176
+ if (e.message.includes('0x1770') || e.message.includes('0x1771')) {
177
+ console.log('Order does not cross params yet');
178
+ } else if (e.message.includes('0x1793')) {
179
+ console.log('Oracle invalid');
180
+ } else {
181
+ await sleep(3000);
182
+ this.onGoingAuctions.delete(orderSignature);
183
+ return;
184
+ }
185
+ }
186
+ await sleep(50);
187
+ i++;
188
+ }
189
+ });
190
+ this.onGoingAuctions.delete(orderSignature);
191
+ };
192
+ }
193
+
194
+ getAuctionAndOrderDetails(order: Order): AuctionAndOrderDetails {
195
+ // Find number of slots until the order is expected to be in cross
196
+ const params = isVariant(order.marketType, 'perp')
197
+ ? this.perpParams.get(order.marketIndex)
198
+ : this.spotParams.get(order.marketIndex);
199
+ const oraclePrice = isVariant(order.marketType, 'perp')
200
+ ? this.driftClient.getOracleDataForPerpMarket(order.marketIndex)
201
+ : this.driftClient.getOracleDataForSpotMarket(order.marketIndex);
202
+
203
+ const makerOrderDir = isVariant(order.direction, 'long') ? 'sell' : 'buy';
204
+ const auctionStartPrice = convertToNumber(
205
+ isVariant(order.orderType, 'oracle')
206
+ ? getAuctionPriceForOracleOffsetAuction(
207
+ order,
208
+ order.slot.toNumber(),
209
+ oraclePrice.price
210
+ )
211
+ : order.auctionStartPrice,
212
+ PRICE_PRECISION
213
+ );
214
+ const auctionEndPrice = convertToNumber(
215
+ isVariant(order.orderType, 'oracle')
216
+ ? getAuctionPriceForOracleOffsetAuction(
217
+ order,
218
+ order.slot.toNumber() + order.auctionDuration - 1,
219
+ oraclePrice.price
220
+ )
221
+ : order.auctionEndPrice,
222
+ PRICE_PRECISION
223
+ );
224
+
225
+ const bid = isVariant(params.priceType, 'oracle')
226
+ ? convertToNumber(oraclePrice.price.add(params.bid), PRICE_PRECISION)
227
+ : convertToNumber(params.bid, PRICE_PRECISION);
228
+ const ask = isVariant(params.priceType, 'oracle')
229
+ ? convertToNumber(oraclePrice.price.add(params.ask), PRICE_PRECISION)
230
+ : convertToNumber(params.ask, PRICE_PRECISION);
231
+
232
+ let slotsTilCross = 0;
233
+ let willCross = false;
234
+ const stepSize =
235
+ (auctionEndPrice - auctionStartPrice) / (order.auctionDuration - 1);
236
+ while (slotsTilCross < order.auctionDuration) {
237
+ if (makerOrderDir === 'buy') {
238
+ if (
239
+ convertToNumber(
240
+ getAuctionPrice(
241
+ order,
242
+ order.slot.toNumber() + slotsTilCross,
243
+ oraclePrice.price
244
+ ),
245
+ PRICE_PRECISION
246
+ ) <= bid
247
+ ) {
248
+ willCross = true;
249
+ break;
250
+ }
251
+ } else {
252
+ if (
253
+ convertToNumber(
254
+ getAuctionPrice(
255
+ order,
256
+ order.slot.toNumber() + slotsTilCross,
257
+ oraclePrice.price
258
+ ),
259
+ PRICE_PRECISION
260
+ ) >= ask
261
+ ) {
262
+ willCross = true;
263
+ break;
264
+ }
265
+ }
266
+ slotsTilCross++;
267
+ }
268
+
269
+ return {
270
+ slotsTilCross,
271
+ willCross,
272
+ bid,
273
+ ask,
274
+ auctionStartPrice,
275
+ auctionEndPrice,
276
+ stepSize,
277
+ oraclePrice,
278
+ };
279
+ }
280
+
281
+ async waitForSlotOrCrossOrExpiry(
282
+ targetSlot: number,
283
+ order: Order,
284
+ initialDetails: AuctionAndOrderDetails
285
+ ): Promise<{ slot: number; updatedDetails: AuctionAndOrderDetails }> {
286
+ const auctionEndSlot = order.auctionDuration + order.slot.toNumber();
287
+ let currentDetails: AuctionAndOrderDetails = initialDetails;
288
+ let willCross = initialDetails.willCross;
289
+ if (this.slotSubscriber.currentSlot > auctionEndSlot) {
290
+ return new Promise((resolve) =>
291
+ resolve({ slot: -1, updatedDetails: currentDetails })
292
+ );
293
+ }
294
+
295
+ return new Promise((resolve) => {
296
+ // Immediately return if we are past target slot
297
+
298
+ const slotListener = (slot: number) => {
299
+ if (slot >= targetSlot && willCross) {
300
+ resolve({ slot, updatedDetails: currentDetails });
301
+ }
302
+ };
303
+
304
+ // Otherwise listen for new slots in case we hit the target slot and we're gonna cross
305
+ this.slotSubscriber.eventEmitter.once('newSlot', slotListener);
306
+
307
+ // Update target slot as the bid/ask and the oracle changes
308
+ const intervalId = setInterval(async () => {
309
+ if (this.slotSubscriber.currentSlot >= auctionEndSlot) {
310
+ this.slotSubscriber.eventEmitter.removeListener(
311
+ 'newSlot',
312
+ slotListener
313
+ );
314
+ clearInterval(intervalId);
315
+ resolve({ slot: -1, updatedDetails: currentDetails });
316
+ }
317
+
318
+ currentDetails = this.getAuctionAndOrderDetails(order);
319
+ willCross = currentDetails.willCross;
320
+ if (willCross) {
321
+ targetSlot = order.slot.toNumber() + currentDetails.slotsTilCross;
322
+ }
323
+ }, 50);
324
+ });
325
+ }
326
+ }
327
+
328
+ function sleep(ms: number): Promise<void> {
329
+ return new Promise((resolve) => setTimeout(resolve, ms));
330
+ }