@drift-labs/sdk 2.0.13 → 2.0.15

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
@@ -21,10 +21,9 @@ import {
21
21
  MarketTypeStr,
22
22
  StateAccount,
23
23
  isMarketOrder,
24
- isLimitOrder,
25
- getOptionalLimitPrice,
26
24
  mustBeTriggered,
27
25
  isTriggered,
26
+ getLimitPrice,
28
27
  } from '..';
29
28
  import { PublicKey } from '@solana/web3.js';
30
29
  import { DLOBNode, DLOBNodeType, TriggerOrderNode } from '..';
@@ -60,8 +59,6 @@ export type NodeToTrigger = {
60
59
  node: TriggerOrderNode;
61
60
  };
62
61
 
63
- type Side = 'ask' | 'bid' | 'both' | 'nocross';
64
-
65
62
  export class DLOB {
66
63
  openOrders = new Map<MarketTypeStr, Set<string>>();
67
64
  orderLists = new Map<MarketTypeStr, Map<number, MarketNodeLists>>();
@@ -302,25 +299,29 @@ export class DLOB {
302
299
  return [];
303
300
  }
304
301
 
305
- // Find all the crossing nodes
306
- const crossingNodesToFill: Array<NodeToFill> = this.findCrossingNodesToFill(
307
- marketIndex,
308
- slot,
309
- marketType,
310
- oraclePriceData
311
- );
302
+ const isAmmPaused = ammPaused(this.stateAccount, marketAccount);
312
303
 
313
- let fallbackCrossingNodesToFill = new Array<NodeToFill>();
314
- if (!ammPaused(this.stateAccount, marketAccount)) {
315
- fallbackCrossingNodesToFill = this.findFallbackCrossingNodesToFill(
304
+ const marketOrderNodesToFill: Array<NodeToFill> =
305
+ this.findMarketNodesToFill(
316
306
  marketIndex,
317
- fallbackBid,
307
+ slot,
308
+ marketType,
309
+ oraclePriceData,
310
+ isAmmPaused,
318
311
  fallbackAsk,
312
+ fallbackBid
313
+ );
314
+
315
+ const limitOrderNodesToFill: Array<NodeToFill> =
316
+ this.findLimitOrderNodesToFill(
317
+ marketIndex,
319
318
  slot,
320
319
  marketType,
321
- oraclePriceData
320
+ oraclePriceData,
321
+ isAmmPaused,
322
+ fallbackAsk,
323
+ fallbackBid
322
324
  );
323
- }
324
325
 
325
326
  // get expired market nodes
326
327
  const expiredNodesToFill = this.findExpiredNodesToFill(
@@ -328,160 +329,298 @@ export class DLOB {
328
329
  ts,
329
330
  marketType
330
331
  );
331
- return crossingNodesToFill.concat(
332
- fallbackCrossingNodesToFill,
332
+ return marketOrderNodesToFill.concat(
333
+ limitOrderNodesToFill,
333
334
  expiredNodesToFill
334
335
  );
335
336
  }
336
337
 
337
- public findCrossingNodesToFill(
338
+ public findLimitOrderNodesToFill(
338
339
  marketIndex: number,
339
340
  slot: number,
340
341
  marketType: MarketType,
341
- oraclePriceData: OraclePriceData
342
+ oraclePriceData: OraclePriceData,
343
+ isAmmPaused: boolean,
344
+ fallbackAsk: BN | undefined,
345
+ fallbackBid: BN | undefined
342
346
  ): NodeToFill[] {
343
347
  const nodesToFill = new Array<NodeToFill>();
344
348
 
345
- const askGenerator = this.getAsks(
349
+ const crossingNodes = this.findCrossingLimitOrders(
346
350
  marketIndex,
347
- undefined, // dont include vask
348
- slot,
349
- marketType,
350
- oraclePriceData
351
- );
352
- const bidGenerator = this.getBids(
353
- marketIndex,
354
- undefined, // dont include vbid
355
351
  slot,
356
352
  marketType,
357
353
  oraclePriceData
358
354
  );
359
355
 
360
- let nextAsk = askGenerator.next();
361
- let nextBid = bidGenerator.next();
356
+ for (const crossingNode of crossingNodes) {
357
+ nodesToFill.push(crossingNode);
358
+ }
362
359
 
363
- // First try to find orders that cross
364
- while (!nextAsk.done && !nextBid.done) {
365
- const { crossingNodes, exhaustedSide } = this.findCrossingOrders(
366
- nextAsk.value,
367
- nextBid.value,
360
+ if (fallbackBid && !isAmmPaused) {
361
+ const askGenerator = this.getLimitAsks(
362
+ marketIndex,
363
+ slot,
364
+ marketType,
365
+ oraclePriceData
366
+ );
367
+ const asksCrossingFallback = this.findNodesCrossingFallbackLiquidity(
368
+ marketType,
369
+ slot,
368
370
  oraclePriceData,
369
- slot
371
+ askGenerator,
372
+ fallbackBid,
373
+ (askPrice, fallbackPrice) => {
374
+ return askPrice.lte(fallbackPrice);
375
+ }
370
376
  );
371
377
 
372
- if (exhaustedSide === 'bid') {
373
- nextBid = bidGenerator.next();
374
- } else if (exhaustedSide === 'ask') {
375
- nextAsk = askGenerator.next();
376
- } else if (exhaustedSide === 'both') {
377
- nextBid = bidGenerator.next();
378
- nextAsk = askGenerator.next();
379
- } else if (exhaustedSide === 'nocross') {
380
- break;
381
- } else {
382
- console.error(`invalid exhaustedSide: ${exhaustedSide}`);
383
- break;
378
+ for (const askCrossingFallback of asksCrossingFallback) {
379
+ nodesToFill.push(askCrossingFallback);
384
380
  }
381
+ }
382
+
383
+ if (fallbackAsk && !isAmmPaused) {
384
+ const bidGenerator = this.getLimitBids(
385
+ marketIndex,
386
+ slot,
387
+ marketType,
388
+ oraclePriceData
389
+ );
390
+ const bidsCrossingFallback = this.findNodesCrossingFallbackLiquidity(
391
+ marketType,
392
+ slot,
393
+ oraclePriceData,
394
+ bidGenerator,
395
+ fallbackAsk,
396
+ (bidPrice, fallbackPrice) => {
397
+ return bidPrice.gte(fallbackPrice);
398
+ }
399
+ );
385
400
 
386
- for (const crossingNode of crossingNodes) {
387
- nodesToFill.push(crossingNode);
401
+ for (const bidCrossingFallback of bidsCrossingFallback) {
402
+ nodesToFill.push(bidCrossingFallback);
388
403
  }
389
404
  }
405
+
390
406
  return nodesToFill;
391
407
  }
392
408
 
393
- public findFallbackCrossingNodesToFill(
409
+ public findMarketNodesToFill(
394
410
  marketIndex: number,
395
- fallbackBid: BN,
396
- fallbackAsk: BN,
397
411
  slot: number,
398
412
  marketType: MarketType,
399
- oraclePriceData: OraclePriceData
413
+ oraclePriceData: OraclePriceData,
414
+ isAmmPaused: boolean,
415
+ fallbackAsk: BN | undefined,
416
+ fallbackBid?: BN | undefined
400
417
  ): NodeToFill[] {
401
418
  const nodesToFill = new Array<NodeToFill>();
402
419
 
403
- const askGenerator = this.getAsks(
420
+ let marketOrderGenerator = this.getMarketAsks(marketIndex, marketType);
421
+
422
+ const marketAsksCrossingBids = this.findMarketNodesCrossingLimitNodes(
404
423
  marketIndex,
405
- undefined, // dont include vask
406
424
  slot,
407
425
  marketType,
408
- oraclePriceData
426
+ oraclePriceData,
427
+ marketOrderGenerator,
428
+ this.getLimitBids.bind(this),
429
+ (takerPrice, makerPrice) => {
430
+ return takerPrice === undefined || takerPrice.lte(makerPrice);
431
+ }
409
432
  );
410
- const bidGenerator = this.getBids(
433
+ for (const marketAskCrossingBid of marketAsksCrossingBids) {
434
+ nodesToFill.push(marketAskCrossingBid);
435
+ }
436
+
437
+ if (fallbackBid && !isAmmPaused) {
438
+ marketOrderGenerator = this.getMarketAsks(marketIndex, marketType);
439
+ const marketAsksCrossingFallback =
440
+ this.findNodesCrossingFallbackLiquidity(
441
+ marketType,
442
+ slot,
443
+ oraclePriceData,
444
+ marketOrderGenerator,
445
+ fallbackBid,
446
+ (takerPrice, fallbackPrice) => {
447
+ return takerPrice === undefined || takerPrice.lte(fallbackPrice);
448
+ }
449
+ );
450
+
451
+ for (const marketAskCrossingFallback of marketAsksCrossingFallback) {
452
+ nodesToFill.push(marketAskCrossingFallback);
453
+ }
454
+ }
455
+
456
+ marketOrderGenerator = this.getMarketBids(marketIndex, marketType);
457
+
458
+ const marketBidsToFill = this.findMarketNodesCrossingLimitNodes(
411
459
  marketIndex,
412
- undefined, // dont include vbid
413
460
  slot,
414
461
  marketType,
415
- oraclePriceData
462
+ oraclePriceData,
463
+ marketOrderGenerator,
464
+ this.getLimitAsks.bind(this),
465
+ (takerPrice, fallbackPrice) => {
466
+ return takerPrice === undefined || takerPrice.gte(fallbackPrice);
467
+ }
416
468
  );
417
469
 
418
- let nextAsk = askGenerator.next();
419
- let nextBid = bidGenerator.next();
420
-
421
- // check for asks that cross fallbackBid
422
- while (!nextAsk.done && fallbackBid !== undefined) {
423
- const askNode = nextAsk.value;
470
+ for (const marketBidToFill of marketBidsToFill) {
471
+ nodesToFill.push(marketBidToFill);
472
+ }
424
473
 
425
- if (isVariant(marketType, 'spot') && askNode.order?.postOnly) {
426
- nextAsk = askGenerator.next();
427
- continue;
474
+ if (fallbackAsk && !isAmmPaused) {
475
+ marketOrderGenerator = this.getMarketBids(marketIndex, marketType);
476
+ const marketBidsCrossingFallback =
477
+ this.findNodesCrossingFallbackLiquidity(
478
+ marketType,
479
+ slot,
480
+ oraclePriceData,
481
+ marketOrderGenerator,
482
+ fallbackAsk,
483
+ (takerPrice, fallbackPrice) => {
484
+ return takerPrice === undefined || takerPrice.gte(fallbackPrice);
485
+ }
486
+ );
487
+ for (const marketBidCrossingFallback of marketBidsCrossingFallback) {
488
+ nodesToFill.push(marketBidCrossingFallback);
428
489
  }
490
+ }
429
491
 
430
- const askLimitPrice = getOptionalLimitPrice(
431
- askNode.order,
432
- oraclePriceData,
433
- slot
492
+ return nodesToFill;
493
+ }
494
+
495
+ public findMarketNodesCrossingLimitNodes(
496
+ marketIndex: number,
497
+ slot: number,
498
+ marketType: MarketType,
499
+ oraclePriceData: OraclePriceData,
500
+ takerNodeGenerator: Generator<DLOBNode>,
501
+ makerNodeGeneratorFn: (
502
+ marketIndex: number,
503
+ slot: number,
504
+ marketType: MarketType,
505
+ oraclePriceData: OraclePriceData
506
+ ) => Generator<DLOBNode>,
507
+ doesCross: (takerPrice: BN | undefined, makerPrice: BN) => boolean
508
+ ): NodeToFill[] {
509
+ const nodesToFill = new Array<NodeToFill>();
510
+
511
+ for (const takerNode of takerNodeGenerator) {
512
+ const makerNodeGenerator = makerNodeGeneratorFn(
513
+ marketIndex,
514
+ slot,
515
+ marketType,
516
+ oraclePriceData
434
517
  );
435
518
 
436
- // order crosses if there is no limit price or it crosses fallback price
437
- const crosses =
438
- askLimitPrice === undefined || askLimitPrice.lte(fallbackBid);
519
+ for (const makerNode of makerNodeGenerator) {
520
+ const bidUserAuthority = this.userMap.getUserAuthority(
521
+ makerNode.userAccount.toString()
522
+ );
523
+ const askUserAuthority = this.userMap.getUserAuthority(
524
+ takerNode.userAccount.toString()
525
+ );
439
526
 
440
- // fallback is available if auction is complete or it's a spot order
441
- const fallbackAvailable =
442
- isVariant(marketType, 'spot') || isAuctionComplete(askNode.order, slot);
527
+ // Can't match orders from the same authority
528
+ const sameAuthority = bidUserAuthority.equals(askUserAuthority);
529
+
530
+ if (sameAuthority) {
531
+ continue;
532
+ }
533
+
534
+ const makerPrice = makerNode.getPrice(oraclePriceData, slot);
535
+ const takerPrice = takerNode.getPrice(oraclePriceData, slot);
536
+
537
+ const ordersCross = doesCross(takerPrice, makerPrice);
538
+ if (!ordersCross) {
539
+ // market orders aren't sorted by price, they are sorted by time, so we need to traverse
540
+ // through all of em
541
+ break;
542
+ }
443
543
 
444
- if (crosses && fallbackAvailable) {
445
544
  nodesToFill.push({
446
- node: askNode,
447
- makerNode: undefined, // filled by fallback
545
+ node: takerNode,
546
+ makerNode: makerNode,
448
547
  });
449
- }
450
548
 
451
- nextAsk = askGenerator.next();
549
+ const makerOrder = makerNode.order;
550
+ const takerOrder = takerNode.order;
551
+
552
+ const makerBaseRemaining = makerOrder.baseAssetAmount.sub(
553
+ makerOrder.baseAssetAmountFilled
554
+ );
555
+ const takerBaseRemaining = takerOrder.baseAssetAmount.sub(
556
+ takerOrder.baseAssetAmountFilled
557
+ );
558
+
559
+ const baseFilled = BN.min(makerBaseRemaining, takerBaseRemaining);
560
+
561
+ const newMakerOrder = { ...makerOrder };
562
+ newMakerOrder.baseAssetAmountFilled =
563
+ makerOrder.baseAssetAmountFilled.add(baseFilled);
564
+ this.getListForOrder(newMakerOrder).update(
565
+ newMakerOrder,
566
+ makerNode.userAccount
567
+ );
568
+
569
+ const newTakerOrder = { ...takerOrder };
570
+ newTakerOrder.baseAssetAmountFilled =
571
+ takerOrder.baseAssetAmountFilled.add(baseFilled);
572
+ this.getListForOrder(newTakerOrder).update(
573
+ newTakerOrder,
574
+ takerNode.userAccount
575
+ );
576
+
577
+ if (
578
+ newTakerOrder.baseAssetAmountFilled.eq(takerOrder.baseAssetAmount)
579
+ ) {
580
+ break;
581
+ }
582
+ }
452
583
  }
453
584
 
454
- // check for bids that cross fallbackAsk
455
- while (!nextBid.done && fallbackAsk !== undefined) {
456
- const bidNode = nextBid.value;
585
+ return nodesToFill;
586
+ }
587
+
588
+ public findNodesCrossingFallbackLiquidity(
589
+ marketType: MarketType,
590
+ slot: number,
591
+ oraclePriceData: OraclePriceData,
592
+ nodeGenerator: Generator<DLOBNode>,
593
+ fallbackPrice: BN,
594
+ doesCross: (nodePrice: BN | undefined, fallbackPrice: BN) => boolean
595
+ ): NodeToFill[] {
596
+ const nodesToFill = new Array<NodeToFill>();
597
+
598
+ let nextNode = nodeGenerator.next();
599
+ while (!nextNode.done) {
600
+ const node = nextNode.value;
457
601
 
458
- if (isVariant(marketType, 'spot') && bidNode.order?.postOnly) {
459
- nextBid = bidGenerator.next();
602
+ if (isVariant(marketType, 'spot') && node.order?.postOnly) {
603
+ nextNode = nodeGenerator.next();
460
604
  continue;
461
605
  }
462
606
 
463
- const bidLimitPrice = getOptionalLimitPrice(
464
- bidNode.order,
465
- oraclePriceData,
466
- slot
467
- );
607
+ const nodePrice = getLimitPrice(node.order, oraclePriceData, slot);
468
608
 
469
609
  // order crosses if there is no limit price or it crosses fallback price
470
- const crosses =
471
- bidLimitPrice === undefined || bidLimitPrice.gte(fallbackAsk);
610
+ const crosses = doesCross(nodePrice, fallbackPrice);
472
611
 
473
612
  // fallback is available if auction is complete or it's a spot order
474
613
  const fallbackAvailable =
475
- isVariant(marketType, 'spot') || isAuctionComplete(bidNode.order, slot);
614
+ isVariant(marketType, 'spot') || isAuctionComplete(node.order, slot);
476
615
 
477
616
  if (crosses && fallbackAvailable) {
478
617
  nodesToFill.push({
479
- node: bidNode,
618
+ node: node,
480
619
  makerNode: undefined, // filled by fallback
481
620
  });
482
621
  }
483
622
 
484
- nextBid = bidGenerator.next();
623
+ nextNode = nodeGenerator.next();
485
624
  }
486
625
 
487
626
  return nodesToFill;
@@ -557,64 +696,56 @@ export class DLOB {
557
696
  return nodesToFill;
558
697
  }
559
698
 
560
- public getMarketBids(
699
+ *getMarketBids(
561
700
  marketIndex: number,
562
701
  marketType: MarketType
563
702
  ): Generator<DLOBNode> {
564
703
  const marketTypeStr = getVariant(marketType) as MarketTypeStr;
565
- return this.orderLists
704
+ const generator = this.orderLists
566
705
  .get(marketTypeStr)
567
706
  .get(marketIndex)
568
707
  .market.bid.getGenerator();
708
+ for (const marketBidNode of generator) {
709
+ if (marketBidNode.isBaseFilled()) {
710
+ continue;
711
+ }
712
+ yield marketBidNode;
713
+ }
569
714
  }
570
715
 
571
- public getMarketAsks(
716
+ *getMarketAsks(
572
717
  marketIndex: number,
573
718
  marketType: MarketType
574
719
  ): Generator<DLOBNode> {
575
720
  const marketTypeStr = getVariant(marketType) as MarketTypeStr;
576
- return this.orderLists
721
+ const generator = this.orderLists
577
722
  .get(marketTypeStr)
578
723
  .get(marketIndex)
579
724
  .market.ask.getGenerator();
725
+ for (const marketAskNode of generator) {
726
+ if (marketAskNode.isBaseFilled()) {
727
+ continue;
728
+ }
729
+ yield marketAskNode;
730
+ }
580
731
  }
581
732
 
582
- *getAsks(
583
- marketIndex: number,
584
- fallbackAsk: BN | undefined,
733
+ private *getBestNode(
734
+ generatorList: Array<Generator<DLOBNode>>,
735
+ oraclePriceData: OraclePriceData,
585
736
  slot: number,
586
- marketType: MarketType,
587
- oraclePriceData: OraclePriceData
737
+ compareFcn: (bestPrice: BN, currentPrice: BN) => boolean
588
738
  ): Generator<DLOBNode> {
589
- if (isVariant(marketType, 'spot') && !oraclePriceData) {
590
- throw new Error('Must provide OraclePriceData to get spot asks');
591
- }
592
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
593
- const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex);
594
-
595
- const generatorList = [
596
- nodeLists.limit.ask.getGenerator(),
597
- nodeLists.floatingLimit.ask.getGenerator(),
598
- nodeLists.market.ask.getGenerator(),
599
- ];
600
-
601
- if (marketTypeStr === 'perp' && fallbackAsk) {
602
- generatorList.push(getVammNodeGenerator(fallbackAsk));
603
- }
604
- if (generatorList.length === 0) {
605
- throw new Error('No ask generators found');
606
- }
607
-
608
- const askGenerators = generatorList.map((generator) => {
739
+ const generators = generatorList.map((generator) => {
609
740
  return {
610
741
  next: generator.next(),
611
742
  generator,
612
743
  };
613
744
  });
614
745
 
615
- let asksExhausted = false;
616
- while (!asksExhausted) {
617
- const bestGenerator = askGenerators.reduce(
746
+ let sideExhausted = false;
747
+ while (!sideExhausted) {
748
+ const bestGenerator = generators.reduce(
618
749
  (bestGenerator, currentGenerator) => {
619
750
  if (currentGenerator.next.done) {
620
751
  return bestGenerator;
@@ -624,16 +755,21 @@ export class DLOB {
624
755
  return currentGenerator;
625
756
  }
626
757
 
627
- const bestAskPrice = bestGenerator.next.value.getPrice(
628
- oraclePriceData,
629
- slot
630
- );
631
- const currentAskPrice = currentGenerator.next.value.getPrice(
632
- oraclePriceData,
633
- slot
634
- );
758
+ const bestValue = bestGenerator.next.value as DLOBNode;
759
+ const currentValue = currentGenerator.next.value as DLOBNode;
635
760
 
636
- return bestAskPrice.lt(currentAskPrice)
761
+ // always return the market orders first
762
+ if (bestValue.order && isMarketOrder(bestValue.order)) {
763
+ return bestGenerator;
764
+ }
765
+ if (currentValue.order && isMarketOrder(currentValue.order)) {
766
+ return currentGenerator;
767
+ }
768
+
769
+ const bestPrice = bestValue.getPrice(oraclePriceData, slot);
770
+ const currentPrice = currentValue.getPrice(oraclePriceData, slot);
771
+
772
+ return compareFcn(bestPrice, currentPrice)
637
773
  ? bestGenerator
638
774
  : currentGenerator;
639
775
  }
@@ -660,14 +796,40 @@ export class DLOB {
660
796
  yield bestGenerator.next.value;
661
797
  bestGenerator.next = bestGenerator.generator.next();
662
798
  } else {
663
- asksExhausted = true;
799
+ sideExhausted = true;
664
800
  }
665
801
  }
666
802
  }
667
803
 
668
- *getBids(
804
+ *getLimitAsks(
805
+ marketIndex: number,
806
+ slot: number,
807
+ marketType: MarketType,
808
+ oraclePriceData: OraclePriceData
809
+ ): Generator<DLOBNode> {
810
+ if (isVariant(marketType, 'spot') && !oraclePriceData) {
811
+ throw new Error('Must provide OraclePriceData to get spot asks');
812
+ }
813
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
814
+ const nodeLists = this.orderLists.get(marketTypeStr).get(marketIndex);
815
+
816
+ const generatorList = [
817
+ nodeLists.limit.ask.getGenerator(),
818
+ nodeLists.floatingLimit.ask.getGenerator(),
819
+ ];
820
+
821
+ yield* this.getBestNode(
822
+ generatorList,
823
+ oraclePriceData,
824
+ slot,
825
+ (bestPrice, currentPrice) => {
826
+ return bestPrice.lt(currentPrice);
827
+ }
828
+ );
829
+ }
830
+
831
+ *getLimitBids(
669
832
  marketIndex: number,
670
- fallbackBid: BN | undefined,
671
833
  slot: number,
672
834
  marketType: MarketType,
673
835
  oraclePriceData: OraclePriceData
@@ -682,255 +844,192 @@ export class DLOB {
682
844
  const generatorList = [
683
845
  nodeLists.limit.bid.getGenerator(),
684
846
  nodeLists.floatingLimit.bid.getGenerator(),
685
- nodeLists.market.bid.getGenerator(),
686
847
  ];
687
- if (marketTypeStr === 'perp' && fallbackBid) {
688
- generatorList.push(getVammNodeGenerator(fallbackBid));
689
- }
690
- if (generatorList.length === 0) {
691
- throw new Error('No bid generators found');
692
- }
693
-
694
- const bidGenerators = generatorList.map((generator) => {
695
- return {
696
- next: generator.next(),
697
- generator,
698
- };
699
- });
700
-
701
- let bidsExhausted = false; // there will always be the fallbackBid
702
- while (!bidsExhausted) {
703
- const bestGenerator = bidGenerators.reduce(
704
- (bestGenerator, currentGenerator) => {
705
- if (currentGenerator.next.done) {
706
- return bestGenerator;
707
- }
708
-
709
- if (bestGenerator.next.done) {
710
- return currentGenerator;
711
- }
712
848
 
713
- const bestBidPrice = bestGenerator.next.value.getPrice(
714
- oraclePriceData,
715
- slot
716
- );
717
- const currentBidPrice = currentGenerator.next.value.getPrice(
718
- oraclePriceData,
719
- slot
720
- );
849
+ yield* this.getBestNode(
850
+ generatorList,
851
+ oraclePriceData,
852
+ slot,
853
+ (bestPrice, currentPrice) => {
854
+ return bestPrice.gt(currentPrice);
855
+ }
856
+ );
857
+ }
721
858
 
722
- return bestBidPrice.gt(currentBidPrice)
723
- ? bestGenerator
724
- : currentGenerator;
725
- }
726
- );
859
+ *getAsks(
860
+ marketIndex: number,
861
+ fallbackAsk: BN | undefined,
862
+ slot: number,
863
+ marketType: MarketType,
864
+ oraclePriceData: OraclePriceData
865
+ ): Generator<DLOBNode> {
866
+ if (isVariant(marketType, 'spot') && !oraclePriceData) {
867
+ throw new Error('Must provide OraclePriceData to get spot asks');
868
+ }
727
869
 
728
- if (!bestGenerator.next.done) {
729
- // skip this node if it's already completely filled
730
- if (bestGenerator.next.value.isBaseFilled()) {
731
- bestGenerator.next = bestGenerator.generator.next();
732
- continue;
733
- }
870
+ const generatorList = [
871
+ this.getMarketAsks(marketIndex, marketType),
872
+ this.getLimitAsks(marketIndex, slot, marketType, oraclePriceData),
873
+ ];
734
874
 
735
- // skip order if user is being liquidated/bankrupt
736
- if (bestGenerator.next.value.userAccount !== undefined) {
737
- const user = this.userMap.get(
738
- bestGenerator.next.value.userAccount.toString()
739
- );
740
- if (user?.isBeingLiquidated()) {
741
- bestGenerator.next = bestGenerator.generator.next();
742
- continue;
743
- }
744
- }
875
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
876
+ if (marketTypeStr === 'perp' && fallbackAsk) {
877
+ generatorList.push(getVammNodeGenerator(fallbackAsk));
878
+ }
745
879
 
746
- yield bestGenerator.next.value;
747
- bestGenerator.next = bestGenerator.generator.next();
748
- } else {
749
- bidsExhausted = true;
880
+ yield* this.getBestNode(
881
+ generatorList,
882
+ oraclePriceData,
883
+ slot,
884
+ (bestPrice, currentPrice) => {
885
+ return bestPrice.lt(currentPrice);
750
886
  }
751
- }
887
+ );
752
888
  }
753
889
 
754
- findCrossingOrders(
755
- askNode: DLOBNode,
756
- bidNode: DLOBNode,
757
- oraclePriceData: OraclePriceData,
758
- slot: number
759
- ): {
760
- crossingNodes: NodeToFill[];
761
- exhaustedSide: Side;
762
- } {
763
- const bidPrice = bidNode.getPrice(oraclePriceData, slot);
764
- const askPrice = askNode.getPrice(oraclePriceData, slot);
765
-
766
- // orders don't cross - we're done walkin gup the book
767
- if (bidPrice.lt(askPrice)) {
768
- return {
769
- crossingNodes: [],
770
- exhaustedSide: 'nocross',
771
- };
890
+ *getBids(
891
+ marketIndex: number,
892
+ fallbackBid: BN | undefined,
893
+ slot: number,
894
+ marketType: MarketType,
895
+ oraclePriceData: OraclePriceData
896
+ ): Generator<DLOBNode> {
897
+ if (isVariant(marketType, 'spot') && !oraclePriceData) {
898
+ throw new Error('Must provide OraclePriceData to get spot bids');
772
899
  }
773
900
 
774
- const bidOrder = bidNode.order;
775
- const askOrder = askNode.order;
776
-
777
- const bidUserAuthority = this.userMap.getUserAuthority(
778
- bidNode.userAccount.toString()
779
- );
780
- const askUserAuthority = this.userMap.getUserAuthority(
781
- askNode.userAccount.toString()
782
- );
901
+ const generatorList = [
902
+ this.getMarketBids(marketIndex, marketType),
903
+ this.getLimitBids(marketIndex, slot, marketType, oraclePriceData),
904
+ ];
783
905
 
784
- // Can't match orders from the same authority
785
- const sameAuthority = bidUserAuthority.equals(askUserAuthority);
786
- if (sameAuthority || (bidOrder.postOnly && askOrder.postOnly)) {
787
- // don't have a principle way to pick which one to exhaust,
788
- // exhaust each one 50% of the time so we can try each one against other orders
789
- const exhaustedSide = Math.random() < 0.5 ? 'bid' : 'ask';
790
- return {
791
- crossingNodes: [],
792
- exhaustedSide,
793
- };
906
+ const marketTypeStr = getVariant(marketType) as MarketTypeStr;
907
+ if (marketTypeStr === 'perp' && fallbackBid) {
908
+ generatorList.push(getVammNodeGenerator(fallbackBid));
794
909
  }
795
910
 
796
- const { takerNode, makerNode, makerSide } = this.determineMakerAndTaker(
797
- askNode,
798
- bidNode
911
+ yield* this.getBestNode(
912
+ generatorList,
913
+ oraclePriceData,
914
+ slot,
915
+ (bestPrice, currentPrice) => {
916
+ return bestPrice.gt(currentPrice);
917
+ }
799
918
  );
919
+ }
800
920
 
801
- // If maker is market order and auction is complete, cant be maker
802
- if (
803
- isMarketOrder(makerNode.order) &&
804
- isAuctionComplete(makerNode.order, slot)
805
- ) {
806
- return {
807
- crossingNodes: [],
808
- exhaustedSide: makerSide,
809
- };
810
- }
921
+ findCrossingLimitOrders(
922
+ marketIndex: number,
923
+ slot: number,
924
+ marketType: MarketType,
925
+ oraclePriceData: OraclePriceData
926
+ ): NodeToFill[] {
927
+ const nodesToFill = new Array<NodeToFill>();
811
928
 
812
- const bidBaseRemaining = bidOrder.baseAssetAmount.sub(
813
- bidOrder.baseAssetAmountFilled
814
- );
815
- const askBaseRemaining = askOrder.baseAssetAmount.sub(
816
- askOrder.baseAssetAmountFilled
817
- );
929
+ for (const askNode of this.getLimitAsks(
930
+ marketIndex,
931
+ slot,
932
+ marketType,
933
+ oraclePriceData
934
+ )) {
935
+ for (const bidNode of this.getLimitBids(
936
+ marketIndex,
937
+ slot,
938
+ marketType,
939
+ oraclePriceData
940
+ )) {
941
+ const bidPrice = bidNode.getPrice(oraclePriceData, slot);
942
+ const askPrice = askNode.getPrice(oraclePriceData, slot);
818
943
 
819
- let exhaustedSide: Side;
820
- if (bidBaseRemaining.eq(askBaseRemaining)) {
821
- exhaustedSide = 'both';
822
- } else if (bidBaseRemaining.gt(askBaseRemaining)) {
823
- exhaustedSide = 'ask';
824
- } else {
825
- exhaustedSide = 'bid';
826
- }
944
+ // orders don't cross - we're done walking the book
945
+ if (bidPrice.lt(askPrice)) {
946
+ return nodesToFill;
947
+ }
827
948
 
828
- // update the orders on DLOB as if they were fill - so we don't try to match them on the next iteration
829
- // NOTE: if something goes wrong during the actual fill (transaction fails, i.e. due to orders already being filled)
830
- // then we risk having a mismatch between this local DLOB and the actual DLOB state on the blockchain. This isn't
831
- // a problem in the current implementation because we construct a new DLOB from the blockchain state every time, rather
832
- // than updating the existing DLOB based on events.
833
- if (exhaustedSide === 'ask') {
834
- // bid partially filled
835
- const newBidOrder = { ...bidOrder };
836
- newBidOrder.baseAssetAmountFilled =
837
- bidOrder.baseAssetAmountFilled.add(askBaseRemaining);
838
- this.getListForOrder(newBidOrder).update(
839
- newBidOrder,
840
- bidNode.userAccount
841
- );
949
+ const bidOrder = bidNode.order;
950
+ const askOrder = askNode.order;
842
951
 
843
- // ask completely filled
844
- const newAskOrder = { ...askOrder };
845
- newAskOrder.baseAssetAmountFilled = askOrder.baseAssetAmount;
846
- this.getListForOrder(newAskOrder).update(
847
- newAskOrder,
848
- askNode.userAccount
849
- );
850
- } else if (exhaustedSide === 'bid') {
851
- // ask partially filled
852
- const newAskOrder = { ...askOrder };
853
- newAskOrder.baseAssetAmountFilled =
854
- askOrder.baseAssetAmountFilled.add(bidBaseRemaining);
855
- this.getListForOrder(newAskOrder).update(
856
- newAskOrder,
857
- askNode.userAccount
858
- );
952
+ const bidUserAuthority = this.userMap.getUserAuthority(
953
+ bidNode.userAccount.toString()
954
+ );
955
+ const askUserAuthority = this.userMap.getUserAuthority(
956
+ askNode.userAccount.toString()
957
+ );
859
958
 
860
- // bid completely filled
861
- const newBidOrder = { ...bidOrder };
862
- newBidOrder.baseAssetAmountFilled = bidOrder.baseAssetAmount;
863
- this.getListForOrder(newBidOrder).update(
864
- newBidOrder,
865
- bidNode.userAccount
866
- );
867
- } else {
868
- // both completely filled
869
- const newBidOrder = { ...bidOrder };
870
- newBidOrder.baseAssetAmountFilled = bidOrder.baseAssetAmount;
871
- this.getListForOrder(newBidOrder).update(
872
- newBidOrder,
873
- bidNode.userAccount
874
- );
959
+ // Can't match orders from the same authority
960
+ const sameAuthority = bidUserAuthority.equals(askUserAuthority);
961
+ if (sameAuthority || (bidOrder.postOnly && askOrder.postOnly)) {
962
+ continue;
963
+ }
875
964
 
876
- const newAskOrder = { ...askOrder };
877
- newAskOrder.baseAssetAmountFilled = askOrder.baseAssetAmount;
878
- this.getListForOrder(newAskOrder).update(
879
- newAskOrder,
880
- askNode.userAccount
881
- );
882
- }
965
+ const { takerNode, makerNode } = this.determineMakerAndTaker(
966
+ askNode,
967
+ bidNode
968
+ );
969
+
970
+ const bidBaseRemaining = bidOrder.baseAssetAmount.sub(
971
+ bidOrder.baseAssetAmountFilled
972
+ );
973
+ const askBaseRemaining = askOrder.baseAssetAmount.sub(
974
+ askOrder.baseAssetAmountFilled
975
+ );
976
+
977
+ const baseFilled = BN.min(bidBaseRemaining, askBaseRemaining);
978
+
979
+ const newBidOrder = { ...bidOrder };
980
+ newBidOrder.baseAssetAmountFilled =
981
+ bidOrder.baseAssetAmountFilled.add(baseFilled);
982
+ this.getListForOrder(newBidOrder).update(
983
+ newBidOrder,
984
+ bidNode.userAccount
985
+ );
986
+
987
+ // ask completely filled
988
+ const newAskOrder = { ...askOrder };
989
+ newAskOrder.baseAssetAmountFilled =
990
+ askOrder.baseAssetAmountFilled.add(baseFilled);
991
+ this.getListForOrder(newAskOrder).update(
992
+ newAskOrder,
993
+ askNode.userAccount
994
+ );
883
995
 
884
- return {
885
- crossingNodes: [
886
- {
996
+ nodesToFill.push({
887
997
  node: takerNode,
888
998
  makerNode: makerNode,
889
- },
890
- ],
891
- exhaustedSide,
892
- };
999
+ });
1000
+
1001
+ if (newAskOrder.baseAssetAmount.eq(newAskOrder.baseAssetAmountFilled)) {
1002
+ break;
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ return nodesToFill;
893
1008
  }
894
1009
 
895
1010
  determineMakerAndTaker(
896
1011
  askNode: DLOBNode,
897
1012
  bidNode: DLOBNode
898
- ): { takerNode: DLOBNode; makerNode: DLOBNode; makerSide: Side } {
1013
+ ): { takerNode: DLOBNode; makerNode: DLOBNode } {
899
1014
  if (bidNode.order.postOnly) {
900
1015
  return {
901
1016
  takerNode: askNode,
902
1017
  makerNode: bidNode,
903
- makerSide: 'bid',
904
1018
  };
905
1019
  } else if (askNode.order.postOnly) {
906
1020
  return {
907
1021
  takerNode: bidNode,
908
1022
  makerNode: askNode,
909
- makerSide: 'ask',
910
- };
911
- } else if (isMarketOrder(bidNode.order) && isLimitOrder(askNode.order)) {
912
- return {
913
- takerNode: bidNode,
914
- makerNode: askNode,
915
- makerSide: 'ask',
916
- };
917
- } else if (isMarketOrder(askNode.order) && isLimitOrder(bidNode.order)) {
918
- return {
919
- takerNode: askNode,
920
- makerNode: bidNode,
921
- makerSide: 'bid',
922
1023
  };
923
1024
  } else if (askNode.order.slot.lt(bidNode.order.slot)) {
924
1025
  return {
925
1026
  takerNode: bidNode,
926
1027
  makerNode: askNode,
927
- makerSide: 'ask',
928
1028
  };
929
1029
  } else {
930
1030
  return {
931
1031
  takerNode: askNode,
932
1032
  makerNode: bidNode,
933
- makerSide: 'bid',
934
1033
  };
935
1034
  }
936
1035
  }