@drift-labs/sdk 2.26.0-beta.1 → 2.26.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/src/dlob/DLOB.ts CHANGED
@@ -1,37 +1,48 @@
1
1
  import { getOrderSignature, getVammNodeGenerator, NodeList } from './NodeList';
2
2
  import {
3
- MarketType,
4
3
  BN,
5
4
  calculateAskPrice,
6
5
  calculateBidPrice,
7
- DriftClient,
8
6
  convertToNumber,
9
- isOrderExpired,
7
+ DLOBNode,
8
+ DLOBNodeType,
9
+ DriftClient,
10
+ getLimitPrice,
11
+ getVariant,
12
+ isFallbackAvailableLiquiditySource,
10
13
  isOneOfVariant,
14
+ isOrderExpired,
15
+ isRestingLimitOrder,
16
+ isTakingOrder,
17
+ isTriggered,
11
18
  isVariant,
12
- getVariant,
19
+ MarketType,
20
+ MarketTypeStr,
21
+ mustBeTriggered,
22
+ OraclePriceData,
13
23
  Order,
14
- PRICE_PRECISION,
15
- SpotMarketAccount,
24
+ OrderActionRecord,
25
+ OrderRecord,
16
26
  PerpMarketAccount,
17
- OraclePriceData,
27
+ PRICE_PRECISION,
18
28
  SlotSubscriber,
19
- MarketTypeStr,
29
+ SpotMarketAccount,
20
30
  StateAccount,
21
- mustBeTriggered,
22
- isTriggered,
23
- getLimitPrice,
31
+ TriggerOrderNode,
24
32
  UserMap,
25
- OrderRecord,
26
- OrderActionRecord,
27
- isRestingLimitOrder,
28
- isTakingOrder,
29
- isFallbackAvailableLiquiditySource,
30
33
  } from '..';
31
34
  import { PublicKey } from '@solana/web3.js';
32
- import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..';
33
35
  import { ammPaused, exchangePaused, fillPaused } from '../math/exchangeStatus';
34
36
  import { DLOBOrders } from './DLOBOrders';
37
+ import {
38
+ createL2Levels,
39
+ getL2GeneratorFromDLOBNodes,
40
+ L2OrderBook,
41
+ L2OrderBookGenerator,
42
+ L3Level,
43
+ L3OrderBook,
44
+ mergeL2LevelGenerators,
45
+ } from './orderBookLevels';
35
46
 
36
47
  export type MarketNodeLists = {
37
48
  restingLimit: {
@@ -1689,4 +1700,153 @@ export class DLOB {
1689
1700
  yield nodeLists.trigger.below;
1690
1701
  }
1691
1702
  }
1703
+
1704
+ /**
1705
+ * Get an L2 view of the order book for a given market.
1706
+ *
1707
+ * @param marketIndex
1708
+ * @param marketType
1709
+ * @param slot
1710
+ * @param oraclePriceData
1711
+ * @param depth how many levels of the order book to return
1712
+ * @param fallbackAsk best ask for fallback liquidity, only relevant for perps
1713
+ * @param fallbackBid best bid for fallback liquidity, only relevant for perps
1714
+ * @param fallbackL2Generators L2 generators for fallback liquidity e.g. vAMM {@link getVammL2Generator}, openbook {@link SerumSubscriber}
1715
+ */
1716
+ public getL2({
1717
+ marketIndex,
1718
+ marketType,
1719
+ slot,
1720
+ oraclePriceData,
1721
+ depth,
1722
+ fallbackAsk,
1723
+ fallbackBid,
1724
+ fallbackL2Generators = [],
1725
+ }: {
1726
+ marketIndex: number;
1727
+ marketType: MarketType;
1728
+ slot: number;
1729
+ oraclePriceData: OraclePriceData;
1730
+ depth: number;
1731
+ fallbackAsk?: BN;
1732
+ fallbackBid?: BN;
1733
+ fallbackL2Generators?: L2OrderBookGenerator[];
1734
+ }): L2OrderBook {
1735
+ const makerAskL2LevelGenerator = getL2GeneratorFromDLOBNodes(
1736
+ this.getMakerLimitAsks(
1737
+ marketIndex,
1738
+ slot,
1739
+ marketType,
1740
+ oraclePriceData,
1741
+ fallbackBid
1742
+ ),
1743
+ oraclePriceData,
1744
+ slot
1745
+ );
1746
+
1747
+ const fallbackAskGenerators = fallbackL2Generators.map(
1748
+ (fallbackL2Generator) => {
1749
+ return fallbackL2Generator.getL2Asks();
1750
+ }
1751
+ );
1752
+
1753
+ const askL2LevelGenerator = mergeL2LevelGenerators(
1754
+ [makerAskL2LevelGenerator, ...fallbackAskGenerators],
1755
+ (a, b) => {
1756
+ return a.price.lt(b.price);
1757
+ }
1758
+ );
1759
+
1760
+ const asks = createL2Levels(askL2LevelGenerator, depth);
1761
+
1762
+ const makerBidGenerator = getL2GeneratorFromDLOBNodes(
1763
+ this.getMakerLimitBids(
1764
+ marketIndex,
1765
+ slot,
1766
+ marketType,
1767
+ oraclePriceData,
1768
+ fallbackAsk
1769
+ ),
1770
+ oraclePriceData,
1771
+ slot
1772
+ );
1773
+
1774
+ const fallbackBidGenerators = fallbackL2Generators.map((fallbackOrders) => {
1775
+ return fallbackOrders.getL2Bids();
1776
+ });
1777
+
1778
+ const bidL2LevelGenerator = mergeL2LevelGenerators(
1779
+ [makerBidGenerator, ...fallbackBidGenerators],
1780
+ (a, b) => {
1781
+ return a.price.gt(b.price);
1782
+ }
1783
+ );
1784
+
1785
+ const bids = createL2Levels(bidL2LevelGenerator, depth);
1786
+
1787
+ return {
1788
+ bids,
1789
+ asks,
1790
+ };
1791
+ }
1792
+
1793
+ /**
1794
+ * Get an L3 view of the order book for a given market. Does not include fallback liquidity sources
1795
+ *
1796
+ * @param marketIndex
1797
+ * @param marketType
1798
+ * @param slot
1799
+ * @param oraclePriceData
1800
+ */
1801
+ public getL3({
1802
+ marketIndex,
1803
+ marketType,
1804
+ slot,
1805
+ oraclePriceData,
1806
+ }: {
1807
+ marketIndex: number;
1808
+ marketType: MarketType;
1809
+ slot: number;
1810
+ oraclePriceData: OraclePriceData;
1811
+ }): L3OrderBook {
1812
+ const bids: L3Level[] = [];
1813
+ const asks: L3Level[] = [];
1814
+
1815
+ const restingAsks = this.getRestingLimitAsks(
1816
+ marketIndex,
1817
+ slot,
1818
+ marketType,
1819
+ oraclePriceData
1820
+ );
1821
+
1822
+ for (const ask of restingAsks) {
1823
+ asks.push({
1824
+ price: ask.getPrice(oraclePriceData, slot),
1825
+ size: ask.order.baseAssetAmount.sub(ask.order.baseAssetAmountFilled),
1826
+ maker: ask.userAccount,
1827
+ orderId: ask.order.orderId,
1828
+ });
1829
+ }
1830
+
1831
+ const restingBids = this.getRestingLimitBids(
1832
+ marketIndex,
1833
+ slot,
1834
+ marketType,
1835
+ oraclePriceData
1836
+ );
1837
+
1838
+ for (const bid of restingBids) {
1839
+ bids.push({
1840
+ price: bid.getPrice(oraclePriceData, slot),
1841
+ size: bid.order.baseAssetAmount.sub(bid.order.baseAssetAmountFilled),
1842
+ maker: bid.userAccount,
1843
+ orderId: bid.order.orderId,
1844
+ });
1845
+ }
1846
+
1847
+ return {
1848
+ bids,
1849
+ asks,
1850
+ };
1851
+ }
1692
1852
  }
@@ -19,7 +19,9 @@ export class DLOBApiClient {
19
19
  public async getDLOB(slot: number): Promise<DLOB> {
20
20
  const r = await fetch(this.url);
21
21
  if (!r.ok) {
22
- throw new Error(`Failed to fetch DLOB from ${this.url}`);
22
+ throw new Error(
23
+ `Failed to fetch DLOB from ${this.url}. Status: ${r.status}, ${r.statusText}`
24
+ );
23
25
  }
24
26
 
25
27
  const resp = await r.json();
@@ -7,8 +7,18 @@ import {
7
7
  DLOBSubscriptionConfig,
8
8
  SlotSource,
9
9
  } from './types';
10
+ import { DriftClient } from '../driftClient';
11
+ import { isVariant, MarketType } from '../types';
12
+ import {
13
+ getVammL2Generator,
14
+ L2OrderBook,
15
+ L2OrderBookGenerator,
16
+ L3OrderBook,
17
+ } from './orderBookLevels';
18
+ import { calculateAskPrice, calculateBidPrice } from '../math/market';
10
19
 
11
20
  export class DLOBSubscriber {
21
+ driftClient: DriftClient;
12
22
  dlobSource: DLOBSource;
13
23
  slotSource: SlotSource;
14
24
  updateFrequency: number;
@@ -17,6 +27,7 @@ export class DLOBSubscriber {
17
27
  public eventEmitter: StrictEventEmitter<EventEmitter, DLOBSubscriberEvents>;
18
28
 
19
29
  constructor(config: DLOBSubscriptionConfig) {
30
+ this.driftClient = config.driftClient;
20
31
  this.dlobSource = config.dlobSource;
21
32
  this.slotSource = config.slotSource;
22
33
  this.updateFrequency = config.updateFrequency;
@@ -48,6 +59,136 @@ export class DLOBSubscriber {
48
59
  return this.dlob;
49
60
  }
50
61
 
62
+ /**
63
+ * Get the L2 order book for a given market.
64
+ *
65
+ * @param marketName e.g. "SOL-PERP" or "SOL". If not provided, marketIndex and marketType must be provided.
66
+ * @param marketIndex
67
+ * @param marketType
68
+ * @param depth Number of orders to include in the order book. Defaults to 10.
69
+ * @param includeVamm Whether to include the VAMM orders in the order book. Defaults to false. If true, creates vAMM generator {@link getVammL2Generator} and adds it to fallbackL2Generators.
70
+ * @param fallbackL2Generators L2 generators for fallback liquidity e.g. vAMM {@link getVammL2Generator}, openbook {@link SerumSubscriber}
71
+ */
72
+ public getL2({
73
+ marketName,
74
+ marketIndex,
75
+ marketType,
76
+ depth = 10,
77
+ includeVamm = false,
78
+ fallbackL2Generators = [],
79
+ }: {
80
+ marketName?: string;
81
+ marketIndex?: number;
82
+ marketType?: MarketType;
83
+ depth?: number;
84
+ includeVamm?: boolean;
85
+ fallbackL2Generators?: L2OrderBookGenerator[];
86
+ }): L2OrderBook {
87
+ if (marketName) {
88
+ const derivedMarketInfo =
89
+ this.driftClient.getMarketIndexAndType(marketName);
90
+ if (!derivedMarketInfo) {
91
+ throw new Error(`Market ${marketName} not found`);
92
+ }
93
+ marketIndex = derivedMarketInfo.marketIndex;
94
+ marketType = derivedMarketInfo.marketType;
95
+ } else {
96
+ if (marketIndex === undefined || marketType === undefined) {
97
+ throw new Error(
98
+ 'Either marketName or marketIndex and marketType must be provided'
99
+ );
100
+ }
101
+ }
102
+
103
+ let oraclePriceData;
104
+ let fallbackBid;
105
+ let fallbackAsk;
106
+ const isPerp = isVariant(marketType, 'perp');
107
+ if (isPerp) {
108
+ const perpMarketAccount =
109
+ this.driftClient.getPerpMarketAccount(marketIndex);
110
+ oraclePriceData = this.driftClient.getOraclePriceDataAndSlot(
111
+ perpMarketAccount.amm.oracle
112
+ );
113
+ fallbackBid = calculateBidPrice(perpMarketAccount, oraclePriceData);
114
+ fallbackAsk = calculateAskPrice(perpMarketAccount, oraclePriceData);
115
+ } else {
116
+ oraclePriceData =
117
+ this.driftClient.getOracleDataForSpotMarket(marketIndex);
118
+ }
119
+
120
+ if (isPerp && includeVamm) {
121
+ fallbackL2Generators = [
122
+ getVammL2Generator({
123
+ marketAccount: this.driftClient.getPerpMarketAccount(marketIndex),
124
+ oraclePriceData,
125
+ numOrders: depth,
126
+ }),
127
+ ];
128
+ }
129
+
130
+ return this.dlob.getL2({
131
+ marketIndex,
132
+ marketType,
133
+ depth,
134
+ oraclePriceData,
135
+ slot: this.slotSource.getSlot(),
136
+ fallbackBid,
137
+ fallbackAsk,
138
+ fallbackL2Generators: fallbackL2Generators,
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Get the L3 order book for a given market.
144
+ *
145
+ * @param marketName e.g. "SOL-PERP" or "SOL". If not provided, marketIndex and marketType must be provided.
146
+ * @param marketIndex
147
+ * @param marketType
148
+ */
149
+ public getL3({
150
+ marketName,
151
+ marketIndex,
152
+ marketType,
153
+ }: {
154
+ marketName?: string;
155
+ marketIndex?: number;
156
+ marketType?: MarketType;
157
+ }): L3OrderBook {
158
+ if (marketName) {
159
+ const derivedMarketInfo =
160
+ this.driftClient.getMarketIndexAndType(marketName);
161
+ if (!derivedMarketInfo) {
162
+ throw new Error(`Market ${marketName} not found`);
163
+ }
164
+ marketIndex = derivedMarketInfo.marketIndex;
165
+ marketType = derivedMarketInfo.marketType;
166
+ } else {
167
+ if (marketIndex === undefined || marketType === undefined) {
168
+ throw new Error(
169
+ 'Either marketName or marketIndex and marketType must be provided'
170
+ );
171
+ }
172
+ }
173
+
174
+ let oraclePriceData;
175
+ const isPerp = isVariant(marketType, 'perp');
176
+ if (isPerp) {
177
+ oraclePriceData =
178
+ this.driftClient.getOracleDataForPerpMarket(marketIndex);
179
+ } else {
180
+ oraclePriceData =
181
+ this.driftClient.getOracleDataForSpotMarket(marketIndex);
182
+ }
183
+
184
+ return this.dlob.getL3({
185
+ marketIndex,
186
+ marketType,
187
+ oraclePriceData,
188
+ slot: this.slotSource.getSlot(),
189
+ });
190
+ }
191
+
51
192
  public async unsubscribe(): Promise<void> {
52
193
  if (this.intervalId) {
53
194
  clearInterval(this.intervalId);
@@ -0,0 +1,243 @@
1
+ import {
2
+ BASE_PRECISION,
3
+ BN,
4
+ calculateAmmReservesAfterSwap,
5
+ calculateMarketOpenBidAsk,
6
+ calculateQuoteAssetAmountSwapped,
7
+ calculateSpreadReserves,
8
+ calculateUpdatedAMM,
9
+ DLOBNode,
10
+ OraclePriceData,
11
+ PerpMarketAccount,
12
+ SwapDirection,
13
+ } from '..';
14
+ import { PublicKey } from '@solana/web3.js';
15
+
16
+ type liquiditySource = 'serum' | 'vamm' | 'dlob';
17
+
18
+ export type L2Level = {
19
+ price: BN;
20
+ size: BN;
21
+ sources: { [key in liquiditySource]?: BN };
22
+ };
23
+
24
+ export type L2OrderBook = {
25
+ asks: L2Level[];
26
+ bids: L2Level[];
27
+ };
28
+
29
+ export interface L2OrderBookGenerator {
30
+ getL2Asks(): Generator<L2Level>;
31
+ getL2Bids(): Generator<L2Level>;
32
+ }
33
+
34
+ export type L3Level = {
35
+ price: BN;
36
+ size: BN;
37
+ maker: PublicKey;
38
+ orderId: number;
39
+ };
40
+
41
+ export type L3OrderBook = {
42
+ asks: L3Level[];
43
+ bids: L3Level[];
44
+ };
45
+
46
+ /**
47
+ * Get an {@link Generator<L2Level>} generator from a {@link Generator<DLOBNode>}
48
+ * @param dlobNodes e.g. {@link DLOB#getMakerLimitAsks} or {@link DLOB#getMakerLimitBids}
49
+ * @param oraclePriceData
50
+ * @param slot
51
+ */
52
+ export function* getL2GeneratorFromDLOBNodes(
53
+ dlobNodes: Generator<DLOBNode>,
54
+ oraclePriceData: OraclePriceData,
55
+ slot: number
56
+ ): Generator<L2Level> {
57
+ for (const dlobNode of dlobNodes) {
58
+ const size = dlobNode.order.baseAssetAmount.sub(
59
+ dlobNode.order.baseAssetAmountFilled
60
+ ) as BN;
61
+ yield {
62
+ size,
63
+ price: dlobNode.getPrice(oraclePriceData, slot),
64
+ sources: {
65
+ dlob: size,
66
+ },
67
+ };
68
+ }
69
+ }
70
+
71
+ export function* mergeL2LevelGenerators(
72
+ l2LevelGenerators: Generator<L2Level>[],
73
+ compare: (a: L2Level, b: L2Level) => boolean
74
+ ): Generator<L2Level> {
75
+ const generators = l2LevelGenerators.map((generator) => {
76
+ return {
77
+ generator,
78
+ next: generator.next(),
79
+ };
80
+ });
81
+
82
+ let next;
83
+ do {
84
+ next = generators.reduce((best, next) => {
85
+ if (next.next.done) {
86
+ return best;
87
+ }
88
+
89
+ if (!best) {
90
+ return next;
91
+ }
92
+
93
+ if (compare(next.next.value, best.next.value)) {
94
+ return next;
95
+ } else {
96
+ return best;
97
+ }
98
+ }, undefined);
99
+
100
+ if (next) {
101
+ yield next.next.value;
102
+ next.next = next.generator.next();
103
+ }
104
+ } while (next !== undefined);
105
+ }
106
+
107
+ export function createL2Levels(
108
+ generator: Generator<L2Level>,
109
+ depth: number
110
+ ): L2Level[] {
111
+ const levels = [];
112
+ for (const level of generator) {
113
+ const price = level.price;
114
+ const size = level.size;
115
+ if (levels.length > 0 && levels[levels.length - 1].price.eq(price)) {
116
+ const currentLevel = levels[levels.length - 1];
117
+ currentLevel.size.add(size);
118
+ for (const [source, size] of Object.entries(level.sources)) {
119
+ if (currentLevel.sources[source]) {
120
+ currentLevel.sources[source] = currentLevel.sources[source].add(size);
121
+ } else {
122
+ currentLevel.sources[source] = size;
123
+ }
124
+ }
125
+ } else if (levels.length === depth) {
126
+ break;
127
+ } else {
128
+ levels.push(level);
129
+ }
130
+ }
131
+ return levels;
132
+ }
133
+
134
+ export function getVammL2Generator({
135
+ marketAccount,
136
+ oraclePriceData,
137
+ numOrders,
138
+ now,
139
+ }: {
140
+ marketAccount: PerpMarketAccount;
141
+ oraclePriceData: OraclePriceData;
142
+ numOrders: number;
143
+ now?: BN;
144
+ }): L2OrderBookGenerator {
145
+ const updatedAmm = calculateUpdatedAMM(marketAccount.amm, oraclePriceData);
146
+
147
+ const [openBids, openAsks] = calculateMarketOpenBidAsk(
148
+ updatedAmm.baseAssetReserve,
149
+ updatedAmm.minBaseAssetReserve,
150
+ updatedAmm.maxBaseAssetReserve,
151
+ updatedAmm.orderStepSize
152
+ );
153
+
154
+ now = now ?? new BN(Date.now() / 1000);
155
+ const [bidReserves, askReserves] = calculateSpreadReserves(
156
+ updatedAmm,
157
+ oraclePriceData,
158
+ now
159
+ );
160
+
161
+ let numBids = 0;
162
+ const baseSize = openBids.div(new BN(numOrders));
163
+ const bidAmm = {
164
+ baseAssetReserve: bidReserves.baseAssetReserve,
165
+ quoteAssetReserve: bidReserves.quoteAssetReserve,
166
+ sqrtK: updatedAmm.sqrtK,
167
+ pegMultiplier: updatedAmm.pegMultiplier,
168
+ };
169
+ const getL2Bids = function* () {
170
+ while (numBids < numOrders) {
171
+ const [afterSwapQuoteReserves, afterSwapBaseReserves] =
172
+ calculateAmmReservesAfterSwap(
173
+ bidAmm,
174
+ 'base',
175
+ baseSize,
176
+ SwapDirection.ADD
177
+ );
178
+
179
+ const quoteSwapped = calculateQuoteAssetAmountSwapped(
180
+ bidAmm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(),
181
+ bidAmm.pegMultiplier,
182
+ SwapDirection.ADD
183
+ );
184
+
185
+ const price = quoteSwapped.mul(BASE_PRECISION).div(baseSize);
186
+
187
+ bidAmm.baseAssetReserve = afterSwapBaseReserves;
188
+ bidAmm.quoteAssetReserve = afterSwapQuoteReserves;
189
+
190
+ yield {
191
+ price,
192
+ size: baseSize,
193
+ sources: { vamm: baseSize },
194
+ };
195
+
196
+ numBids++;
197
+ }
198
+ };
199
+
200
+ let numAsks = 0;
201
+ const askSize = openAsks.abs().div(new BN(numOrders));
202
+ const askAmm = {
203
+ baseAssetReserve: askReserves.baseAssetReserve,
204
+ quoteAssetReserve: askReserves.quoteAssetReserve,
205
+ sqrtK: updatedAmm.sqrtK,
206
+ pegMultiplier: updatedAmm.pegMultiplier,
207
+ };
208
+ const getL2Asks = function* () {
209
+ while (numAsks < numOrders) {
210
+ const [afterSwapQuoteReserves, afterSwapBaseReserves] =
211
+ calculateAmmReservesAfterSwap(
212
+ askAmm,
213
+ 'base',
214
+ askSize,
215
+ SwapDirection.REMOVE
216
+ );
217
+
218
+ const quoteSwapped = calculateQuoteAssetAmountSwapped(
219
+ askAmm.quoteAssetReserve.sub(afterSwapQuoteReserves).abs(),
220
+ askAmm.pegMultiplier,
221
+ SwapDirection.REMOVE
222
+ );
223
+
224
+ const price = quoteSwapped.mul(BASE_PRECISION).div(askSize);
225
+
226
+ askAmm.baseAssetReserve = afterSwapBaseReserves;
227
+ askAmm.quoteAssetReserve = afterSwapQuoteReserves;
228
+
229
+ yield {
230
+ price,
231
+ size: askSize,
232
+ sources: { vamm: baseSize },
233
+ };
234
+
235
+ numAsks++;
236
+ }
237
+ };
238
+
239
+ return {
240
+ getL2Bids,
241
+ getL2Asks,
242
+ };
243
+ }
package/src/dlob/types.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { DLOB } from './DLOB';
2
+ import { DriftClient } from '../driftClient';
2
3
 
3
4
  export type DLOBSubscriptionConfig = {
5
+ driftClient: DriftClient;
4
6
  dlobSource: DLOBSource;
5
7
  slotSource: SlotSource;
6
8
  updateFrequency: number;