@drift-labs/jit-proxy 0.6.0 → 0.8.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/Readme.md +58 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/jitProxyClient.d.ts +1 -1
- package/lib/jitProxyClient.js +5 -2
- package/lib/jitter/baseJitter.d.ts +35 -0
- package/lib/jitter/baseJitter.js +88 -0
- package/lib/jitter/jitterShotgun.d.ts +23 -0
- package/lib/jitter/jitterShotgun.js +69 -0
- package/lib/jitter/jitterSniper.d.ts +32 -0
- package/lib/jitter/jitterSniper.js +201 -0
- package/package.json +2 -2
- package/src/index.ts +2 -1
- package/src/jitProxyClient.ts +5 -2
- package/src/{jitter.ts → jitter/baseJitter.ts} +47 -62
- package/src/jitter/jitterShotgun.ts +112 -0
- package/src/jitter/jitterSniper.ts +330 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Jit Proxy SDK
|
|
2
|
+
|
|
3
|
+
## Jit Proxy Client
|
|
4
|
+
|
|
5
|
+
```JitProxyClient``` will create and send transactions to the jit proxy program. Instantiate a jit proxy client with a drift client and a program Id. The current public key for the jit proxy program is
|
|
6
|
+
|
|
7
|
+
```J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP```. Example instantiation:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
const jitProxyClient = new JitProxyClient({
|
|
11
|
+
driftClient,
|
|
12
|
+
programId: new PublicKey('J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP'),
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Jitter
|
|
17
|
+
|
|
18
|
+
A jitter takes ```JitParams``` and uses them to determine when and how to use the ```JitProxyClient``` to send a transaction. For bots to make use of the jitter, they should create a jitter instance, and then set the ```JitParams``` according to their strategy for each market they are market making. ```JitParams``` are set on the Jitter object using ```updatePerpParams()``` and ```updateSpotParams()```. There are two kinds of Jitters that can be insantiated: ```JitterShotgun``` and ```JitterSniper```. The difference between the two is how orders are sent.
|
|
19
|
+
|
|
20
|
+
For ```JitterShotgun```, orders are sent immediately when detecting a new eligible auction. The JitterShotgun will try up to 10 times to fill the order, retrying every time the it receives back an error due to the order not crossing the bid/ask in the ```JitParams``` or the oracle price being invalid.
|
|
21
|
+
|
|
22
|
+
For ```JitterSniper```, orders are sent only when it detects that an order might cross the bid/ask of the ```JitParams```, waiting until the right slot, before sending up to 3 orders (retrying on errors). It will not send orders if the price of the order does not cross the bid/ask during the auction, unlike the JitterShotgun, which will immediately attempt.
|
|
23
|
+
|
|
24
|
+
## Jit Params
|
|
25
|
+
|
|
26
|
+
Type definition for the JitParmas is below:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
export type JitParams = {
|
|
30
|
+
bid: BN;
|
|
31
|
+
ask: BN;
|
|
32
|
+
minPosition: BN;
|
|
33
|
+
maxPosition: BN;
|
|
34
|
+
priceType: PriceType;
|
|
35
|
+
subAccountId?: number;
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
PriceType options are ```ORACLE``` and ```LIMIT```. Limit price type is the BN representaiton of an absolute price; i.e., price type of LIMIT and a market of bid: 10, ask: 11 means your market is 10@11. Oracle price types are offsets relative to the oralce price for a given slot. They are always added to the oracle price, so if the oracle price is 10.5, to get a market of 10@11 when price type is oracle, bid and ask are -0.5 and 0.5 respectively (Remember that bid and ask are of BN type, this example is for illustration purposes only. Remember to use BN math operations).
|
|
40
|
+
|
|
41
|
+
## Example set up
|
|
42
|
+
|
|
43
|
+
Example set up for the JitterSniper (assuming parameters are already initialized/subscribed to). JitterShotgun is instantiated and initialized in a similar manner.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
const jitProxyClient = new JitProxyClient({
|
|
47
|
+
driftClient,
|
|
48
|
+
programId: new PublicKey('J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP'),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const jitter = new JitterSniper({
|
|
52
|
+
auctionSubscriber,
|
|
53
|
+
driftClient,
|
|
54
|
+
slotSubscriber,
|
|
55
|
+
jitProxyClient,
|
|
56
|
+
});
|
|
57
|
+
await jitter.subscribe();
|
|
58
|
+
```
|
package/lib/index.d.ts
CHANGED
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);
|
package/lib/jitProxyClient.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/jitProxyClient.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
3
|
+
"version": "0.8.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.
|
|
7
|
+
"@drift-labs/sdk": "^2.38.0"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=16"
|
package/src/index.ts
CHANGED
package/src/jitProxyClient.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
+
}
|