@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/README.md +3 -3
- package/lib/accounts/pollingDriftClientAccountSubscriber.js +0 -3
- package/lib/accounts/pollingUserStatsAccountSubscriber.js +0 -3
- package/lib/adminClient.d.ts +2 -0
- package/lib/adminClient.js +16 -0
- package/lib/constants/numericConstants.d.ts +2 -0
- package/lib/constants/numericConstants.js +3 -1
- package/lib/dlob/DLOB.d.ts +8 -8
- package/lib/dlob/DLOB.js +227 -246
- package/lib/driftClient.d.ts +19 -0
- package/lib/driftClient.js +32 -0
- package/lib/idl/drift.json +65 -7
- package/lib/math/amm.d.ts +3 -2
- package/lib/math/amm.js +61 -27
- package/lib/math/oracles.d.ts +2 -0
- package/lib/math/oracles.js +23 -1
- package/lib/math/orders.d.ts +1 -2
- package/lib/math/orders.js +4 -20
- package/lib/math/utils.d.ts +1 -0
- package/lib/math/utils.js +5 -1
- package/lib/types.d.ts +6 -2
- package/lib/user.js +10 -6
- package/package.json +1 -1
- package/src/accounts/pollingDriftClientAccountSubscriber.ts +0 -4
- package/src/accounts/pollingUserStatsAccountSubscriber.ts +0 -4
- package/src/adminClient.ts +28 -0
- package/src/constants/numericConstants.ts +3 -0
- package/src/dlob/DLOB.ts +444 -345
- package/src/driftClient.ts +41 -0
- package/src/idl/drift.json +65 -7
- package/src/math/amm.ts +126 -42
- package/src/math/oracles.ts +52 -0
- package/src/math/orders.ts +5 -22
- package/src/math/utils.ts +4 -0
- package/src/types.ts +6 -2
- package/src/user.ts +17 -6
- package/tests/dlob/helpers.ts +4 -2
- package/tests/dlob/test.ts +547 -63
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
fallbackCrossingNodesToFill = this.findFallbackCrossingNodesToFill(
|
|
304
|
+
const marketOrderNodesToFill: Array<NodeToFill> =
|
|
305
|
+
this.findMarketNodesToFill(
|
|
316
306
|
marketIndex,
|
|
317
|
-
|
|
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
|
|
332
|
-
|
|
332
|
+
return marketOrderNodesToFill.concat(
|
|
333
|
+
limitOrderNodesToFill,
|
|
333
334
|
expiredNodesToFill
|
|
334
335
|
);
|
|
335
336
|
}
|
|
336
337
|
|
|
337
|
-
public
|
|
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
|
|
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
|
-
|
|
361
|
-
|
|
356
|
+
for (const crossingNode of crossingNodes) {
|
|
357
|
+
nodesToFill.push(crossingNode);
|
|
358
|
+
}
|
|
362
359
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
+
askGenerator,
|
|
372
|
+
fallbackBid,
|
|
373
|
+
(askPrice, fallbackPrice) => {
|
|
374
|
+
return askPrice.lte(fallbackPrice);
|
|
375
|
+
}
|
|
370
376
|
);
|
|
371
377
|
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
387
|
-
nodesToFill.push(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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:
|
|
447
|
-
makerNode:
|
|
545
|
+
node: takerNode,
|
|
546
|
+
makerNode: makerNode,
|
|
448
547
|
});
|
|
449
|
-
}
|
|
450
548
|
|
|
451
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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') &&
|
|
459
|
-
|
|
602
|
+
if (isVariant(marketType, 'spot') && node.order?.postOnly) {
|
|
603
|
+
nextNode = nodeGenerator.next();
|
|
460
604
|
continue;
|
|
461
605
|
}
|
|
462
606
|
|
|
463
|
-
const
|
|
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(
|
|
614
|
+
isVariant(marketType, 'spot') || isAuctionComplete(node.order, slot);
|
|
476
615
|
|
|
477
616
|
if (crosses && fallbackAvailable) {
|
|
478
617
|
nodesToFill.push({
|
|
479
|
-
node:
|
|
618
|
+
node: node,
|
|
480
619
|
makerNode: undefined, // filled by fallback
|
|
481
620
|
});
|
|
482
621
|
}
|
|
483
622
|
|
|
484
|
-
|
|
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
|
-
|
|
699
|
+
*getMarketBids(
|
|
561
700
|
marketIndex: number,
|
|
562
701
|
marketType: MarketType
|
|
563
702
|
): Generator<DLOBNode> {
|
|
564
703
|
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
565
|
-
|
|
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
|
-
|
|
716
|
+
*getMarketAsks(
|
|
572
717
|
marketIndex: number,
|
|
573
718
|
marketType: MarketType
|
|
574
719
|
): Generator<DLOBNode> {
|
|
575
720
|
const marketTypeStr = getVariant(marketType) as MarketTypeStr;
|
|
576
|
-
|
|
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
|
-
*
|
|
583
|
-
|
|
584
|
-
|
|
733
|
+
private *getBestNode(
|
|
734
|
+
generatorList: Array<Generator<DLOBNode>>,
|
|
735
|
+
oraclePriceData: OraclePriceData,
|
|
585
736
|
slot: number,
|
|
586
|
-
|
|
587
|
-
oraclePriceData: OraclePriceData
|
|
737
|
+
compareFcn: (bestPrice: BN, currentPrice: BN) => boolean
|
|
588
738
|
): Generator<DLOBNode> {
|
|
589
|
-
|
|
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
|
|
616
|
-
while (!
|
|
617
|
-
const bestGenerator =
|
|
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
|
|
628
|
-
|
|
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
|
|
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
|
-
|
|
799
|
+
sideExhausted = true;
|
|
664
800
|
}
|
|
665
801
|
}
|
|
666
802
|
}
|
|
667
803
|
|
|
668
|
-
*
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
870
|
+
const generatorList = [
|
|
871
|
+
this.getMarketAsks(marketIndex, marketType),
|
|
872
|
+
this.getLimitAsks(marketIndex, slot, marketType, oraclePriceData),
|
|
873
|
+
];
|
|
734
874
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
829
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
885
|
-
crossingNodes: [
|
|
886
|
-
{
|
|
996
|
+
nodesToFill.push({
|
|
887
997
|
node: takerNode,
|
|
888
998
|
makerNode: makerNode,
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
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
|
}
|