@drift-labs/sdk 2.52.0-beta.1 → 2.52.0-beta.11

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.
Files changed (81) hide show
  1. package/VERSION +1 -1
  2. package/examples/phoenix.ts +11 -4
  3. package/lib/accounts/webSocketAccountSubscriber.d.ts +1 -1
  4. package/lib/accounts/webSocketAccountSubscriber.js +7 -4
  5. package/lib/accounts/webSocketProgramAccountSubscriber.d.ts +1 -1
  6. package/lib/accounts/webSocketProgramAccountSubscriber.js +7 -4
  7. package/lib/dlob/orderBookLevels.d.ts +22 -0
  8. package/lib/dlob/orderBookLevels.js +115 -1
  9. package/lib/driftClient.d.ts +3 -1
  10. package/lib/driftClient.js +30 -8
  11. package/lib/events/webSocketLogProvider.js +3 -3
  12. package/lib/factory/bigNum.d.ts +1 -1
  13. package/lib/factory/bigNum.js +5 -2
  14. package/lib/idl/drift.json +52 -1
  15. package/lib/index.d.ts +1 -1
  16. package/lib/index.js +1 -1
  17. package/lib/math/amm.d.ts +5 -1
  18. package/lib/math/amm.js +62 -13
  19. package/lib/math/state.js +3 -0
  20. package/lib/orderSubscriber/OrderSubscriber.js +1 -0
  21. package/lib/orderSubscriber/WebsocketSubscription.d.ts +4 -1
  22. package/lib/orderSubscriber/WebsocketSubscription.js +25 -1
  23. package/lib/orderSubscriber/types.d.ts +1 -0
  24. package/lib/phoenix/phoenixSubscriber.js +10 -6
  25. package/lib/priorityFee/averageOverSlotsStrategy.d.ts +12 -0
  26. package/lib/priorityFee/averageOverSlotsStrategy.js +28 -0
  27. package/lib/priorityFee/averageStrategy.d.ts +7 -0
  28. package/lib/priorityFee/averageStrategy.js +11 -0
  29. package/lib/priorityFee/ewmaStrategy.d.ts +13 -0
  30. package/lib/priorityFee/ewmaStrategy.js +33 -0
  31. package/lib/priorityFee/index.d.ts +7 -0
  32. package/lib/priorityFee/index.js +23 -0
  33. package/lib/priorityFee/maxOverSlotsStrategy.d.ts +12 -0
  34. package/lib/priorityFee/maxOverSlotsStrategy.js +29 -0
  35. package/lib/priorityFee/maxStrategy.d.ts +7 -0
  36. package/lib/priorityFee/maxStrategy.js +9 -0
  37. package/lib/priorityFee/priorityFeeSubscriber.d.ts +15 -4
  38. package/lib/priorityFee/priorityFeeSubscriber.js +38 -14
  39. package/lib/priorityFee/types.d.ts +6 -0
  40. package/lib/priorityFee/types.js +2 -0
  41. package/lib/slot/SlotSubscriber.d.ts +11 -3
  42. package/lib/slot/SlotSubscriber.js +40 -4
  43. package/lib/types.d.ts +4 -1
  44. package/lib/types.js +1 -0
  45. package/lib/user.d.ts +1 -1
  46. package/lib/user.js +15 -8
  47. package/lib/userMap/userMap.d.ts +3 -0
  48. package/lib/userMap/userMap.js +9 -0
  49. package/package.json +1 -1
  50. package/src/accounts/webSocketAccountSubscriber.ts +7 -4
  51. package/src/accounts/webSocketProgramAccountSubscriber.ts +7 -4
  52. package/src/dlob/orderBookLevels.ts +136 -0
  53. package/src/driftClient.ts +46 -8
  54. package/src/events/webSocketLogProvider.ts +3 -3
  55. package/src/factory/bigNum.ts +11 -2
  56. package/src/idl/drift.json +52 -1
  57. package/src/index.ts +1 -1
  58. package/src/math/amm.ts +159 -25
  59. package/src/math/state.ts +3 -0
  60. package/src/orderSubscriber/OrderSubscriber.ts +1 -0
  61. package/src/orderSubscriber/WebsocketSubscription.ts +28 -0
  62. package/src/orderSubscriber/types.ts +1 -0
  63. package/src/phoenix/phoenixSubscriber.ts +14 -8
  64. package/src/priorityFee/averageOverSlotsStrategy.ts +30 -0
  65. package/src/priorityFee/averageStrategy.ts +11 -0
  66. package/src/priorityFee/ewmaStrategy.ts +40 -0
  67. package/src/priorityFee/index.ts +7 -0
  68. package/src/priorityFee/maxOverSlotsStrategy.ts +31 -0
  69. package/src/priorityFee/maxStrategy.ts +7 -0
  70. package/src/priorityFee/priorityFeeSubscriber.ts +46 -19
  71. package/src/priorityFee/types.ts +5 -0
  72. package/src/slot/SlotSubscriber.ts +52 -5
  73. package/src/types.ts +2 -1
  74. package/src/user.ts +25 -8
  75. package/src/userMap/userMap.ts +12 -0
  76. package/tests/amm/test.ts +219 -11
  77. package/tests/bn/test.ts +27 -0
  78. package/tests/dlob/helpers.ts +1 -1
  79. package/tests/dlob/test.ts +372 -2
  80. package/tests/tx/priorityFeeStrategy.ts +97 -0
  81. package/tests/user/helpers.ts +1 -0
package/src/math/amm.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  PRICE_DIV_PEG,
13
13
  PERCENTAGE_PRECISION,
14
14
  DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT,
15
+ FUNDING_RATE_BUFFER_PRECISION,
15
16
  TWO,
16
17
  } from '../constants/numericConstants';
17
18
  import {
@@ -22,7 +23,7 @@ import {
22
23
  isVariant,
23
24
  } from '../types';
24
25
  import { assert } from '../assert/assert';
25
- import { squareRootBN, clampBN, standardizeBaseAssetAmount } from '..';
26
+ import { squareRootBN, sigNum, clampBN, standardizeBaseAssetAmount } from '..';
26
27
 
27
28
  import { OraclePriceData } from '../oracles/types';
28
29
  import {
@@ -352,21 +353,12 @@ export function calculateMarketOpenBidAsk(
352
353
  return [openBids, openAsks];
353
354
  }
354
355
 
355
- export function calculateInventoryScale(
356
+ export function calculateInventoryLiquidityRatio(
356
357
  baseAssetAmountWithAmm: BN,
357
358
  baseAssetReserve: BN,
358
359
  minBaseAssetReserve: BN,
359
- maxBaseAssetReserve: BN,
360
- directionalSpread: number,
361
- maxSpread: number
362
- ): number {
363
- if (baseAssetAmountWithAmm.eq(ZERO)) {
364
- return 1;
365
- }
366
-
367
- const MAX_BID_ASK_INVENTORY_SKEW_FACTOR = BID_ASK_SPREAD_PRECISION.mul(
368
- new BN(10)
369
- );
360
+ maxBaseAssetReserve: BN
361
+ ): BN {
370
362
  // inventory skew
371
363
  const [openBids, openAsks] = calculateMarketOpenBidAsk(
372
364
  baseAssetReserve,
@@ -383,6 +375,31 @@ export function calculateInventoryScale(
383
375
  .abs(),
384
376
  PERCENTAGE_PRECISION
385
377
  );
378
+ return inventoryScaleBN;
379
+ }
380
+
381
+ export function calculateInventoryScale(
382
+ baseAssetAmountWithAmm: BN,
383
+ baseAssetReserve: BN,
384
+ minBaseAssetReserve: BN,
385
+ maxBaseAssetReserve: BN,
386
+ directionalSpread: number,
387
+ maxSpread: number
388
+ ): number {
389
+ if (baseAssetAmountWithAmm.eq(ZERO)) {
390
+ return 1;
391
+ }
392
+
393
+ const MAX_BID_ASK_INVENTORY_SKEW_FACTOR = BID_ASK_SPREAD_PRECISION.mul(
394
+ new BN(10)
395
+ );
396
+
397
+ const inventoryScaleBN = calculateInventoryLiquidityRatio(
398
+ baseAssetAmountWithAmm,
399
+ baseAssetReserve,
400
+ minBaseAssetReserve,
401
+ maxBaseAssetReserve
402
+ );
386
403
 
387
404
  const inventoryScaleMaxBN = BN.max(
388
405
  MAX_BID_ASK_INVENTORY_SKEW_FACTOR,
@@ -402,6 +419,76 @@ export function calculateInventoryScale(
402
419
  return inventoryScaleCapped;
403
420
  }
404
421
 
422
+ export function calculateReferencePriceOffset(
423
+ reservePrice: BN,
424
+ last24hAvgFundingRate: BN,
425
+ liquidityFraction: BN,
426
+ oracleTwapFast: BN,
427
+ markTwapFast: BN,
428
+ oracleTwapSlow: BN,
429
+ markTwapSlow: BN,
430
+ maxOffsetPct: number
431
+ ): BN {
432
+ if (last24hAvgFundingRate.eq(ZERO)) {
433
+ return ZERO;
434
+ }
435
+
436
+ const maxOffsetInPrice = new BN(maxOffsetPct)
437
+ .mul(reservePrice)
438
+ .div(PERCENTAGE_PRECISION);
439
+
440
+ // Calculate quote denominated market premium
441
+ const markPremiumMinute = clampBN(
442
+ markTwapFast.sub(oracleTwapFast),
443
+ maxOffsetInPrice.mul(new BN(-1)),
444
+ maxOffsetInPrice
445
+ );
446
+
447
+ const markPremiumHour = clampBN(
448
+ markTwapSlow.sub(oracleTwapSlow),
449
+ maxOffsetInPrice.mul(new BN(-1)),
450
+ maxOffsetInPrice
451
+ );
452
+
453
+ // Convert last24hAvgFundingRate to quote denominated premium
454
+ const markPremiumDay = clampBN(
455
+ last24hAvgFundingRate.div(FUNDING_RATE_BUFFER_PRECISION).mul(new BN(24)),
456
+ maxOffsetInPrice.mul(new BN(-1)),
457
+ maxOffsetInPrice
458
+ );
459
+
460
+ // Take average clamped premium as the price-based offset
461
+ const markPremiumAvg = markPremiumMinute
462
+ .add(markPremiumHour)
463
+ .add(markPremiumDay)
464
+ .div(new BN(3));
465
+
466
+ const markPremiumAvgPct = markPremiumAvg
467
+ .mul(PRICE_PRECISION)
468
+ .div(reservePrice);
469
+
470
+ const inventoryPct = clampBN(
471
+ liquidityFraction.mul(new BN(maxOffsetPct)).div(PERCENTAGE_PRECISION),
472
+ maxOffsetInPrice.mul(new BN(-1)),
473
+ maxOffsetInPrice
474
+ );
475
+
476
+ // Only apply when inventory is consistent with recent and 24h market premium
477
+ let offsetPct = markPremiumAvgPct.add(inventoryPct);
478
+
479
+ if (!sigNum(inventoryPct).eq(sigNum(markPremiumAvgPct))) {
480
+ offsetPct = ZERO;
481
+ }
482
+
483
+ const clampedOffsetPct = clampBN(
484
+ offsetPct,
485
+ new BN(-maxOffsetPct),
486
+ new BN(maxOffsetPct)
487
+ );
488
+
489
+ return clampedOffsetPct;
490
+ }
491
+
405
492
  export function calculateEffectiveLeverage(
406
493
  baseSpread: number,
407
494
  quoteAssetReserve: BN,
@@ -532,6 +619,8 @@ export function calculateSpreadBN(
532
619
  halfRevenueRetreatAmount: 0,
533
620
  longSpreadwRevRetreat: 0,
534
621
  shortSpreadwRevRetreat: 0,
622
+ longSpreadwOffsetShrink: 0,
623
+ shortSpreadwOffsetShrink: 0,
535
624
  totalSpread: 0,
536
625
  longSpread: 0,
537
626
  shortSpread: 0,
@@ -678,7 +767,6 @@ export function calculateSpreadBN(
678
767
  spreadTerms.totalSpread = totalSpread;
679
768
  spreadTerms.longSpread = longSpread;
680
769
  spreadTerms.shortSpread = shortSpread;
681
-
682
770
  if (returnTerms) {
683
771
  return spreadTerms;
684
772
  }
@@ -688,17 +776,20 @@ export function calculateSpreadBN(
688
776
  export function calculateSpread(
689
777
  amm: AMM,
690
778
  oraclePriceData: OraclePriceData,
691
- now?: BN
779
+ now?: BN,
780
+ reservePrice?: BN
692
781
  ): [number, number] {
693
782
  if (amm.baseSpread == 0 || amm.curveUpdateIntensity == 0) {
694
783
  return [amm.baseSpread / 2, amm.baseSpread / 2];
695
784
  }
696
785
 
697
- const reservePrice = calculatePrice(
698
- amm.baseAssetReserve,
699
- amm.quoteAssetReserve,
700
- amm.pegMultiplier
701
- );
786
+ if (!reservePrice) {
787
+ reservePrice = calculatePrice(
788
+ amm.baseAssetReserve,
789
+ amm.quoteAssetReserve,
790
+ amm.pegMultiplier
791
+ );
792
+ }
702
793
 
703
794
  const targetPrice = oraclePriceData?.price || reservePrice;
704
795
  const confInterval = oraclePriceData.confidence || ZERO;
@@ -760,13 +851,22 @@ export function calculateSpreadReserves(
760
851
  quoteAssetReserve: amm.quoteAssetReserve,
761
852
  };
762
853
  }
763
- const spreadFraction = BN.max(new BN(spread / 2), ONE);
854
+ let spreadFraction = new BN(spread / 2);
855
+
856
+ // make non-zero
857
+ if (spreadFraction.eq(ZERO)) {
858
+ spreadFraction = spread >= 0 ? new BN(1) : new BN(-1);
859
+ }
860
+
764
861
  const quoteAssetReserveDelta = amm.quoteAssetReserve.div(
765
862
  BID_ASK_SPREAD_PRECISION.div(spreadFraction)
766
863
  );
767
864
 
768
865
  let quoteAssetReserve;
769
- if (isVariant(direction, 'long')) {
866
+ if (
867
+ (spread >= 0 && isVariant(direction, 'long')) ||
868
+ (spread <= 0 && isVariant(direction, 'short'))
869
+ ) {
770
870
  quoteAssetReserve = amm.quoteAssetReserve.add(quoteAssetReserveDelta);
771
871
  } else {
772
872
  quoteAssetReserve = amm.quoteAssetReserve.sub(quoteAssetReserveDelta);
@@ -779,14 +879,48 @@ export function calculateSpreadReserves(
779
879
  };
780
880
  }
781
881
 
782
- const [longSpread, shortSpread] = calculateSpread(amm, oraclePriceData, now);
882
+ const reservePrice = calculatePrice(
883
+ amm.baseAssetReserve,
884
+ amm.quoteAssetReserve,
885
+ amm.pegMultiplier
886
+ );
887
+
888
+ // always allow 10 bps of price offset, up to a fifth of the market's max_spread
889
+ const maxOffset = Math.max(
890
+ amm.maxSpread / 5,
891
+ PERCENTAGE_PRECISION.toNumber() / 1000
892
+ );
893
+ const liquidityFraction = calculateInventoryLiquidityRatio(
894
+ amm.baseAssetAmountWithAmm,
895
+ amm.baseAssetReserve,
896
+ amm.minBaseAssetReserve,
897
+ amm.maxBaseAssetReserve
898
+ );
899
+ const referencePriceOffset = calculateReferencePriceOffset(
900
+ reservePrice,
901
+ amm.last24HAvgFundingRate,
902
+ liquidityFraction,
903
+ amm.historicalOracleData.lastOraclePriceTwap5Min,
904
+ amm.lastMarkPriceTwap5Min,
905
+ amm.historicalOracleData.lastOraclePriceTwap,
906
+ amm.lastMarkPriceTwap,
907
+ maxOffset
908
+ );
909
+
910
+ const [longSpread, shortSpread] = calculateSpread(
911
+ amm,
912
+ oraclePriceData,
913
+ now,
914
+ reservePrice
915
+ );
916
+
783
917
  const askReserves = calculateSpreadReserve(
784
- longSpread,
918
+ longSpread + referencePriceOffset.toNumber(),
785
919
  PositionDirection.LONG,
786
920
  amm
787
921
  );
788
922
  const bidReserves = calculateSpreadReserve(
789
- shortSpread,
923
+ shortSpread + referencePriceOffset.toNumber(),
790
924
  PositionDirection.SHORT,
791
925
  amm
792
926
  );
package/src/math/state.ts CHANGED
@@ -22,5 +22,8 @@ export function calculateInitUserFee(stateAccount: StateAccount): BN {
22
22
  }
23
23
 
24
24
  export function getMaxNumberOfSubAccounts(stateAccount: StateAccount): BN {
25
+ if (stateAccount.maxNumberOfSubAccounts <= 5) {
26
+ return new BN(stateAccount.maxNumberOfSubAccounts);
27
+ }
25
28
  return new BN(stateAccount.maxNumberOfSubAccounts).muln(100);
26
29
  }
@@ -39,6 +39,7 @@ export class OrderSubscriber {
39
39
  commitment: this.commitment,
40
40
  skipInitialLoad: config.subscriptionConfig.skipInitialLoad,
41
41
  resubTimeoutMs: config.subscriptionConfig.resubTimeoutMs,
42
+ resyncIntervalMs: config.subscriptionConfig.resyncIntervalMs,
42
43
  });
43
44
  }
44
45
  if (config.fastDecode ?? true) {
@@ -9,24 +9,29 @@ export class WebsocketSubscription {
9
9
  private commitment: Commitment;
10
10
  private skipInitialLoad: boolean;
11
11
  private resubTimeoutMs?: number;
12
+ private resyncIntervalMs?: number;
12
13
 
13
14
  private subscriber?: WebSocketProgramAccountSubscriber<UserAccount>;
15
+ private resyncTimeoutId?: NodeJS.Timeout;
14
16
 
15
17
  constructor({
16
18
  orderSubscriber,
17
19
  commitment,
18
20
  skipInitialLoad = false,
19
21
  resubTimeoutMs,
22
+ resyncIntervalMs,
20
23
  }: {
21
24
  orderSubscriber: OrderSubscriber;
22
25
  commitment: Commitment;
23
26
  skipInitialLoad?: boolean;
24
27
  resubTimeoutMs?: number;
28
+ resyncIntervalMs?: number;
25
29
  }) {
26
30
  this.orderSubscriber = orderSubscriber;
27
31
  this.commitment = commitment;
28
32
  this.skipInitialLoad = skipInitialLoad;
29
33
  this.resubTimeoutMs = resubTimeoutMs;
34
+ this.resyncIntervalMs = resyncIntervalMs;
30
35
  }
31
36
 
32
37
  public async subscribe(): Promise<void> {
@@ -61,11 +66,34 @@ export class WebsocketSubscription {
61
66
  if (!this.skipInitialLoad) {
62
67
  await this.orderSubscriber.fetch();
63
68
  }
69
+
70
+ if (this.resyncIntervalMs) {
71
+ const recursiveResync = () => {
72
+ this.resyncTimeoutId = setTimeout(() => {
73
+ this.orderSubscriber
74
+ .fetch()
75
+ .catch((e) => {
76
+ console.error('Failed to resync in OrderSubscriber');
77
+ console.log(e);
78
+ })
79
+ .finally(() => {
80
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
81
+ if (!this.resyncTimeoutId) return;
82
+ recursiveResync();
83
+ });
84
+ }, this.resyncIntervalMs);
85
+ };
86
+ recursiveResync();
87
+ }
64
88
  }
65
89
 
66
90
  public async unsubscribe(): Promise<void> {
67
91
  if (!this.subscriber) return;
68
92
  await this.subscriber.unsubscribe();
69
93
  this.subscriber = undefined;
94
+ if (this.resyncTimeoutId !== undefined) {
95
+ clearTimeout(this.resyncTimeoutId);
96
+ this.resyncTimeoutId = undefined;
97
+ }
70
98
  }
71
99
  }
@@ -14,6 +14,7 @@ export type OrderSubscriberConfig = {
14
14
  type: 'websocket';
15
15
  skipInitialLoad?: boolean;
16
16
  resubTimeoutMs?: number;
17
+ resyncIntervalMs?: number;
17
18
  commitment?: Commitment;
18
19
  };
19
20
  fastDecode?: boolean;
@@ -6,6 +6,7 @@ import {
6
6
  toNum,
7
7
  getMarketUiLadder,
8
8
  Market,
9
+ getMarketLadder,
9
10
  } from '@ellipsis-labs/phoenix-sdk';
10
11
  import { PRICE_PRECISION } from '../constants/numericConstants';
11
12
  import { BN } from '@coral-xyz/anchor';
@@ -163,13 +164,18 @@ export class PhoenixSubscriber implements L2OrderBookGenerator {
163
164
  }
164
165
 
165
166
  *getL2Levels(side: 'bids' | 'asks'): Generator<L2Level> {
166
- const basePrecision = Math.pow(
167
- 10,
168
- this.market.data.header.baseParams.decimals
167
+ const tickSize = this.market.data.header
168
+ .tickSizeInQuoteAtomsPerBaseUnit as BN;
169
+ const baseLotsToRawBaseUnits = this.market.baseLotsToRawBaseUnits(1);
170
+
171
+ const basePrecision = new BN(
172
+ Math.pow(10, this.market.data.header.baseParams.decimals) *
173
+ baseLotsToRawBaseUnits
169
174
  );
170
- const pricePrecision = PRICE_PRECISION.toNumber();
171
175
 
172
- const ladder = getMarketUiLadder(
176
+ const pricePrecision = PRICE_PRECISION.div(tickSize as BN);
177
+
178
+ const ladder = getMarketLadder(
173
179
  this.market,
174
180
  this.lastSlot,
175
181
  this.lastUnixTimestamp,
@@ -177,10 +183,10 @@ export class PhoenixSubscriber implements L2OrderBookGenerator {
177
183
  );
178
184
 
179
185
  for (let i = 0; i < ladder[side].length; i++) {
180
- const { price, quantity } = ladder[side][i];
181
- const size = new BN(Math.floor(quantity * basePrecision));
186
+ const { priceInTicks, sizeInBaseLots } = ladder[side][i];
187
+ const size = sizeInBaseLots.mul(basePrecision);
182
188
  yield {
183
- price: new BN(Math.floor(price * pricePrecision)),
189
+ price: priceInTicks.mul(new BN(pricePrecision)),
184
190
  size,
185
191
  sources: {
186
192
  phoenix: size,
@@ -0,0 +1,30 @@
1
+ import { PriorityFeeStrategy } from './types';
2
+
3
+ export class AverageOverSlotsStrategy implements PriorityFeeStrategy {
4
+ private lookbackSlots: number;
5
+
6
+ /**
7
+ * @param lookbackSlots The number of slots to look back from the max slot in the sample
8
+ */
9
+ constructor(lookbackSlots = 10) {
10
+ this.lookbackSlots = lookbackSlots;
11
+ }
12
+
13
+ calculate(samples: { slot: number; prioritizationFee: number }[]): number {
14
+ if (samples.length === 0) {
15
+ return 0;
16
+ }
17
+ const stopSlot = samples[0].slot - this.lookbackSlots;
18
+ let runningSumFees = 0;
19
+ let countFees = 0;
20
+
21
+ for (let i = 0; i < samples.length; i++) {
22
+ if (samples[i].slot <= stopSlot) {
23
+ return runningSumFees / countFees;
24
+ }
25
+ runningSumFees += samples[i].prioritizationFee;
26
+ countFees++;
27
+ }
28
+ return runningSumFees / countFees;
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ import { PriorityFeeStrategy } from './types';
2
+
3
+ export class AverageStrategy implements PriorityFeeStrategy {
4
+ calculate(samples: { slot: number; prioritizationFee: number }[]): number {
5
+ return (
6
+ samples.reduce((a, b) => {
7
+ return a + b.prioritizationFee;
8
+ }, 0) / samples.length
9
+ );
10
+ }
11
+ }
@@ -0,0 +1,40 @@
1
+ import { PriorityFeeStrategy } from './types';
2
+
3
+ class EwmaStrategy implements PriorityFeeStrategy {
4
+ private halfLife: number;
5
+
6
+ /**
7
+ * @param halfLife The half life of the EWMA in slots. Default is 25 slots, approx 10 seconds.
8
+ */
9
+ constructor(halfLife = 25) {
10
+ this.halfLife = halfLife;
11
+ }
12
+
13
+ // samples provided in desc slot order
14
+ calculate(samples: { slot: number; prioritizationFee: number }[]): number {
15
+ if (samples.length === 0) {
16
+ return 0;
17
+ }
18
+ if (samples.length === 1) {
19
+ return samples[0].prioritizationFee;
20
+ }
21
+
22
+ let ewma = 0;
23
+
24
+ const samplesReversed = samples.slice().reverse();
25
+ for (let i = 0; i < samplesReversed.length; i++) {
26
+ if (i === 0) {
27
+ ewma = samplesReversed[i].prioritizationFee;
28
+ continue;
29
+ }
30
+ const gap = samplesReversed[i].slot - samplesReversed[i - 1].slot;
31
+ const alpha = 1 - Math.exp((Math.log(0.5) / this.halfLife) * gap);
32
+
33
+ ewma = alpha * samplesReversed[i].prioritizationFee + (1 - alpha) * ewma;
34
+ }
35
+
36
+ return ewma;
37
+ }
38
+ }
39
+
40
+ export { EwmaStrategy };
@@ -0,0 +1,7 @@
1
+ export * from './averageOverSlotsStrategy';
2
+ export * from './averageStrategy';
3
+ export * from './ewmaStrategy';
4
+ export * from './maxOverSlotsStrategy';
5
+ export * from './maxStrategy';
6
+ export * from './priorityFeeSubscriber';
7
+ export * from './types';
@@ -0,0 +1,31 @@
1
+ import { PriorityFeeStrategy } from './types';
2
+
3
+ export class MaxOverSlotsStrategy implements PriorityFeeStrategy {
4
+ private lookbackSlots: number;
5
+
6
+ /**
7
+ * @param lookbackSlots The number of slots to look back from the max slot in the sample
8
+ */
9
+ constructor(lookbackSlots = 10) {
10
+ this.lookbackSlots = lookbackSlots;
11
+ }
12
+
13
+ calculate(samples: { slot: number; prioritizationFee: number }[]): number {
14
+ if (samples.length === 0) {
15
+ return 0;
16
+ }
17
+ // Assuming samples are sorted in descending order of slot.
18
+ const stopSlot = samples[0].slot - this.lookbackSlots;
19
+ let currMaxFee = samples[0].prioritizationFee;
20
+
21
+ for (let i = 0; i < samples.length; i++) {
22
+ if (samples[i].slot <= stopSlot) {
23
+ return currMaxFee;
24
+ }
25
+ if (samples[i].prioritizationFee > currMaxFee) {
26
+ currMaxFee = samples[i].prioritizationFee;
27
+ }
28
+ }
29
+ return currMaxFee;
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ import { PriorityFeeStrategy } from './types';
2
+
3
+ export class MaxStrategy implements PriorityFeeStrategy {
4
+ calculate(samples: { slot: number; prioritizationFee: number }[]): number {
5
+ return Math.max(...samples.map((result) => result.prioritizationFee));
6
+ }
7
+ }
@@ -1,35 +1,63 @@
1
1
  import { Connection, PublicKey } from '@solana/web3.js';
2
+ import { PriorityFeeStrategy } from './types';
3
+ import { AverageOverSlotsStrategy } from './averageOverSlotsStrategy';
4
+ import { MaxOverSlotsStrategy } from './maxOverSlotsStrategy';
2
5
 
3
6
  export class PriorityFeeSubscriber {
4
7
  connection: Connection;
5
8
  frequencyMs: number;
6
9
  addresses: PublicKey[];
7
- slotsToCheck: number;
10
+ customStrategy?: PriorityFeeStrategy;
11
+ averageStrategy = new AverageOverSlotsStrategy();
12
+ maxStrategy = new MaxOverSlotsStrategy();
8
13
 
9
14
  intervalId?: ReturnType<typeof setTimeout>;
10
15
 
11
16
  latestPriorityFee = 0;
12
- // avg of last {slotsToCheck} slots
13
- avgPriorityFee = 0;
14
- // max of last {slotsToCheck} slots
15
- maxPriorityFee = 0;
17
+ lastStrategyResult = 0;
18
+ lastCustomStrategyResult = 0;
19
+ lastAvgStrategyResult = 0;
20
+ lastMaxStrategyResult = 0;
16
21
  lastSlotSeen = 0;
17
22
 
18
23
  public constructor({
19
24
  connection,
20
25
  frequencyMs,
21
26
  addresses,
27
+ customStrategy,
22
28
  slotsToCheck = 10,
23
29
  }: {
24
30
  connection: Connection;
25
31
  frequencyMs: number;
26
32
  addresses: PublicKey[];
33
+ customStrategy?: PriorityFeeStrategy;
27
34
  slotsToCheck?: number;
28
35
  }) {
29
36
  this.connection = connection;
30
37
  this.frequencyMs = frequencyMs;
31
38
  this.addresses = addresses;
32
- this.slotsToCheck = slotsToCheck;
39
+ if (slotsToCheck) {
40
+ this.averageStrategy = new AverageOverSlotsStrategy(slotsToCheck);
41
+ this.maxStrategy = new MaxOverSlotsStrategy(slotsToCheck);
42
+ }
43
+ if (customStrategy) {
44
+ this.customStrategy = customStrategy;
45
+ }
46
+ }
47
+
48
+ public get avgPriorityFee(): number {
49
+ return Math.floor(this.lastAvgStrategyResult);
50
+ }
51
+
52
+ public get maxPriorityFee(): number {
53
+ return Math.floor(this.lastMaxStrategyResult);
54
+ }
55
+
56
+ public get customPriorityFee(): number {
57
+ if (!this.customStrategy) {
58
+ console.error('Custom strategy not set');
59
+ }
60
+ return Math.floor(this.lastCustomStrategyResult);
33
61
  }
34
62
 
35
63
  public async subscribe(): Promise<void> {
@@ -47,23 +75,22 @@ export class PriorityFeeSubscriber {
47
75
  [this.addresses]
48
76
  );
49
77
 
50
- const descResults: { slot: number; prioritizationFee: number }[] =
51
- rpcJSONResponse?.result
52
- ?.sort((a, b) => b.slot - a.slot)
53
- ?.slice(0, this.slotsToCheck) ?? [];
54
-
55
- if (!descResults?.length) return;
78
+ // getRecentPrioritizationFees returns results unsorted
79
+ const results: { slot: number; prioritizationFee: number }[] =
80
+ rpcJSONResponse?.result;
81
+ if (!results.length) return;
82
+ const descResults = results.sort((a, b) => b.slot - a.slot);
56
83
 
57
84
  const mostRecentResult = descResults[0];
58
85
  this.latestPriorityFee = mostRecentResult.prioritizationFee;
59
86
  this.lastSlotSeen = mostRecentResult.slot;
60
- this.avgPriorityFee =
61
- descResults.reduce((a, b) => {
62
- return a + b.prioritizationFee;
63
- }, 0) / descResults.length;
64
- this.maxPriorityFee = Math.max(
65
- ...descResults.map((result) => result.prioritizationFee)
66
- );
87
+
88
+ this.lastAvgStrategyResult = this.averageStrategy.calculate(descResults);
89
+ this.lastMaxStrategyResult = this.maxStrategy.calculate(descResults);
90
+ if (this.customStrategy) {
91
+ this.lastCustomStrategyResult =
92
+ this.customStrategy.calculate(descResults);
93
+ }
67
94
  }
68
95
 
69
96
  public async unsubscribe(): Promise<void> {
@@ -0,0 +1,5 @@
1
+ export interface PriorityFeeStrategy {
2
+ // calculate the priority fee for a given set of samples.
3
+ // expect samples to be sorted in descending order (by slot)
4
+ calculate(samples: { slot: number; prioritizationFee: number }[]): number;
5
+ }