@velocity-exchange/sdk 0.4.0 → 0.6.1

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 (325) hide show
  1. package/README.md +3 -3
  2. package/lib/browser/adminClient.d.ts +52 -0
  3. package/lib/browser/adminClient.js +97 -0
  4. package/lib/browser/config.js +6 -7
  5. package/lib/browser/constants/numericConstants.d.ts +1 -1
  6. package/lib/browser/constants/numericConstants.js +1 -1
  7. package/lib/browser/decode/user.js +5 -1
  8. package/lib/browser/idl/velocity.d.ts +191 -15
  9. package/lib/browser/idl/velocity.json +192 -16
  10. package/lib/browser/orderSubscriber/OrderSubscriber.d.ts +1 -0
  11. package/lib/browser/orderSubscriber/OrderSubscriber.js +10 -4
  12. package/lib/browser/pyth/pythLazerSubscriber.js +37 -27
  13. package/lib/browser/types.d.ts +6 -0
  14. package/lib/browser/user.d.ts +20 -3
  15. package/lib/browser/user.js +52 -4
  16. package/lib/browser/velocityClient.d.ts +46 -10
  17. package/lib/browser/velocityClient.js +107 -35
  18. package/lib/node/adminClient.d.ts +52 -0
  19. package/lib/node/adminClient.d.ts.map +1 -1
  20. package/lib/node/adminClient.js +97 -0
  21. package/lib/node/config.d.ts.map +1 -1
  22. package/lib/node/config.js +6 -7
  23. package/lib/node/constants/numericConstants.d.ts +1 -1
  24. package/lib/node/constants/numericConstants.d.ts.map +1 -1
  25. package/lib/node/constants/numericConstants.js +1 -1
  26. package/lib/node/decode/user.d.ts.map +1 -1
  27. package/lib/node/decode/user.js +5 -1
  28. package/lib/node/idl/velocity.d.ts +191 -15
  29. package/lib/node/idl/velocity.d.ts.map +1 -1
  30. package/lib/node/idl/velocity.json +192 -16
  31. package/lib/node/orderSubscriber/OrderSubscriber.d.ts +1 -0
  32. package/lib/node/orderSubscriber/OrderSubscriber.d.ts.map +1 -1
  33. package/lib/node/orderSubscriber/OrderSubscriber.js +10 -4
  34. package/lib/node/pyth/pythLazerSubscriber.d.ts.map +1 -1
  35. package/lib/node/pyth/pythLazerSubscriber.js +37 -27
  36. package/lib/node/types.d.ts +6 -0
  37. package/lib/node/types.d.ts.map +1 -1
  38. package/lib/node/user.d.ts +20 -3
  39. package/lib/node/user.d.ts.map +1 -1
  40. package/lib/node/user.js +52 -4
  41. package/lib/node/velocityClient.d.ts +46 -10
  42. package/lib/node/velocityClient.d.ts.map +1 -1
  43. package/lib/node/velocityClient.js +107 -35
  44. package/package.json +7 -3
  45. package/.prettierignore +0 -1
  46. package/CHANGELOG.md +0 -132
  47. package/build-browser.js +0 -58
  48. package/bunfig.toml +0 -3
  49. package/esbuild-shims.js +0 -12
  50. package/scripts/deposit-isolated-positions.ts +0 -110
  51. package/scripts/find-flagged-users.ts +0 -213
  52. package/scripts/grpc-client-test-comparison.ts +0 -372
  53. package/scripts/grpc-multiuser-client-test-comparison.ts +0 -158
  54. package/scripts/postbuild.js +0 -95
  55. package/scripts/single-grpc-client-test.ts +0 -284
  56. package/scripts/withdraw-isolated-positions.ts +0 -174
  57. package/src/accounts/README_WebSocketAccountSubscriberV2.md +0 -95
  58. package/src/accounts/README_WebSocketProgramAccountSubscriberV2.md +0 -135
  59. package/src/accounts/basicUserAccountSubscriber.ts +0 -79
  60. package/src/accounts/basicUserStatsAccountSubscriber.ts +0 -87
  61. package/src/accounts/bulkAccountLoader.ts +0 -358
  62. package/src/accounts/bulkUserStatsSubscription.ts +0 -40
  63. package/src/accounts/bulkUserSubscription.ts +0 -39
  64. package/src/accounts/customizedCadenceBulkAccountLoader.ts +0 -250
  65. package/src/accounts/fetch.ts +0 -200
  66. package/src/accounts/grpcAccountSubscriber.ts +0 -225
  67. package/src/accounts/grpcInsuranceFundStakeAccountSubscriber.ts +0 -73
  68. package/src/accounts/grpcMultiAccountSubscriber.ts +0 -566
  69. package/src/accounts/grpcMultiUserAccountSubscriber.ts +0 -328
  70. package/src/accounts/grpcProgramAccountSubscriber.ts +0 -264
  71. package/src/accounts/grpcUserAccountSubscriber.ts +0 -63
  72. package/src/accounts/grpcUserStatsAccountSubscriber.ts +0 -65
  73. package/src/accounts/grpcVelocityClientAccountSubscriber.ts +0 -257
  74. package/src/accounts/grpcVelocityClientAccountSubscriberV2.ts +0 -941
  75. package/src/accounts/laserProgramAccountSubscriber.ts +0 -266
  76. package/src/accounts/oneShotUserAccountSubscriber.ts +0 -88
  77. package/src/accounts/oneShotUserStatsAccountSubscriber.ts +0 -94
  78. package/src/accounts/pollingInsuranceFundStakeAccountSubscriber.ts +0 -223
  79. package/src/accounts/pollingOracleAccountSubscriber.ts +0 -151
  80. package/src/accounts/pollingTokenAccountSubscriber.ts +0 -147
  81. package/src/accounts/pollingUserAccountSubscriber.ts +0 -198
  82. package/src/accounts/pollingUserStatsAccountSubscriber.ts +0 -180
  83. package/src/accounts/pollingVelocityClientAccountSubscriber.ts +0 -774
  84. package/src/accounts/testBulkAccountLoader.ts +0 -59
  85. package/src/accounts/types.ts +0 -428
  86. package/src/accounts/utils.ts +0 -90
  87. package/src/accounts/webSocketAccountSubscriber.ts +0 -304
  88. package/src/accounts/webSocketAccountSubscriberV2.ts +0 -536
  89. package/src/accounts/webSocketInsuranceFundStakeAccountSubscriber.ts +0 -171
  90. package/src/accounts/webSocketProgramAccountSubscriber.ts +0 -244
  91. package/src/accounts/webSocketProgramAccountSubscriberV2.ts +0 -1048
  92. package/src/accounts/webSocketProgramAccountsSubscriberV2.ts +0 -1042
  93. package/src/accounts/webSocketUserAccountSubscriber.ts +0 -139
  94. package/src/accounts/webSocketUserStatsAccountSubsriber.ts +0 -131
  95. package/src/accounts/webSocketVelocityClientAccountSubscriber.ts +0 -865
  96. package/src/accounts/webSocketVelocityClientAccountSubscriberV2.ts +0 -897
  97. package/src/accounts/websocketProgramUserAccountSubscriber.ts +0 -127
  98. package/src/addresses/marketAddresses.ts +0 -28
  99. package/src/addresses/pda.ts +0 -620
  100. package/src/adminClient.ts +0 -8136
  101. package/src/assert/assert.ts +0 -5
  102. package/src/auctionSubscriber/auctionSubscriber.ts +0 -86
  103. package/src/auctionSubscriber/auctionSubscriberGrpc.ts +0 -99
  104. package/src/auctionSubscriber/index.ts +0 -9
  105. package/src/auctionSubscriber/types.ts +0 -30
  106. package/src/bankrun/bankrunConnection.ts +0 -630
  107. package/src/blockhashSubscriber/BlockhashSubscriber.ts +0 -160
  108. package/src/blockhashSubscriber/index.ts +0 -6
  109. package/src/blockhashSubscriber/types.ts +0 -13
  110. package/src/clock/clockSubscriber.ts +0 -146
  111. package/src/config.ts +0 -312
  112. package/src/constants/index.ts +0 -9
  113. package/src/constants/numericConstants.ts +0 -194
  114. package/src/constants/perpMarkets.ts +0 -127
  115. package/src/constants/spotMarkets.ts +0 -104
  116. package/src/constants/txConstants.ts +0 -2
  117. package/src/constituentMap/constituentMap.ts +0 -305
  118. package/src/constituentMap/pollingConstituentAccountSubscriber.ts +0 -99
  119. package/src/constituentMap/webSocketConstituentAccountSubscriber.ts +0 -121
  120. package/src/core/VelocityCore.ts +0 -606
  121. package/src/core/index.ts +0 -12
  122. package/src/core/instructions/deposit.ts +0 -64
  123. package/src/core/instructions/fill.ts +0 -51
  124. package/src/core/instructions/funding.ts +0 -33
  125. package/src/core/instructions/liquidation.ts +0 -55
  126. package/src/core/instructions/orders.ts +0 -84
  127. package/src/core/instructions/perpOrders.ts +0 -312
  128. package/src/core/instructions/settlement.ts +0 -40
  129. package/src/core/instructions/trigger.ts +0 -41
  130. package/src/core/instructions/withdraw.ts +0 -65
  131. package/src/core/remainingAccounts.ts +0 -320
  132. package/src/core/signedMsg.ts +0 -109
  133. package/src/decode/customCoder.ts +0 -148
  134. package/src/decode/user.ts +0 -401
  135. package/src/dlob/DLOB.ts +0 -2793
  136. package/src/dlob/DLOBNode.ts +0 -305
  137. package/src/dlob/DLOBSubscriber.ts +0 -269
  138. package/src/dlob/NodeList.ts +0 -244
  139. package/src/dlob/orderBookLevels.ts +0 -695
  140. package/src/dlob/types.ts +0 -37
  141. package/src/events/eventList.ts +0 -114
  142. package/src/events/eventSubscriber.ts +0 -521
  143. package/src/events/eventsServerLogProvider.ts +0 -189
  144. package/src/events/fetchLogs.ts +0 -219
  145. package/src/events/parse.ts +0 -271
  146. package/src/events/pollingLogProvider.ts +0 -113
  147. package/src/events/sort.ts +0 -51
  148. package/src/events/txEventCache.ts +0 -92
  149. package/src/events/types.ts +0 -337
  150. package/src/events/webSocketLogProvider.ts +0 -152
  151. package/src/factory/bigNum.ts +0 -709
  152. package/src/factory/oracleClient.ts +0 -73
  153. package/src/idl/pyth.json +0 -142
  154. package/src/idl/pyth.ts +0 -97
  155. package/src/idl/pyth_solana_receiver.json +0 -628
  156. package/src/idl/token_faucet.json +0 -229
  157. package/src/idl/token_faucet.ts +0 -197
  158. package/src/idl/velocity.json +0 -23974
  159. package/src/idl/velocity.ts +0 -23980
  160. package/src/index.ts +0 -150
  161. package/src/indicative-quotes/indicativeQuotesSender.ts +0 -233
  162. package/src/isomorphic/README.md +0 -19
  163. package/src/isomorphic/anchor.browser.ts +0 -44
  164. package/src/isomorphic/anchor.node.ts +0 -1
  165. package/src/isomorphic/anchor.ts +0 -1
  166. package/src/isomorphic/anchor29.browser.ts +0 -24
  167. package/src/isomorphic/anchor29.node.ts +0 -1
  168. package/src/isomorphic/anchor29.ts +0 -1
  169. package/src/isomorphic/grpc.browser.ts +0 -4
  170. package/src/isomorphic/grpc.node.ts +0 -130
  171. package/src/isomorphic/grpc.ts +0 -1
  172. package/src/jupiter/jupiterClient.ts +0 -502
  173. package/src/keypair.ts +0 -32
  174. package/src/margin/README.md +0 -138
  175. package/src/marginCalculation.ts +0 -407
  176. package/src/marinade/idl/idl.json +0 -1962
  177. package/src/marinade/index.ts +0 -73
  178. package/src/marinade/types.ts +0 -3925
  179. package/src/math/amm.ts +0 -1758
  180. package/src/math/auction.ts +0 -480
  181. package/src/math/bankruptcy.ts +0 -138
  182. package/src/math/builder.ts +0 -130
  183. package/src/math/conversion.ts +0 -45
  184. package/src/math/exchangeStatus.ts +0 -261
  185. package/src/math/funding.ts +0 -498
  186. package/src/math/insurance.ts +0 -171
  187. package/src/math/liquidation.ts +0 -433
  188. package/src/math/margin.ts +0 -327
  189. package/src/math/market.ts +0 -487
  190. package/src/math/oracles.ts +0 -477
  191. package/src/math/orders.ts +0 -500
  192. package/src/math/position.ts +0 -381
  193. package/src/math/repeg.ts +0 -223
  194. package/src/math/spotBalance.ts +0 -996
  195. package/src/math/spotMarket.ts +0 -115
  196. package/src/math/spotPosition.ts +0 -263
  197. package/src/math/state.ts +0 -71
  198. package/src/math/superStake.ts +0 -602
  199. package/src/math/tiers.ts +0 -73
  200. package/src/math/trade.ts +0 -879
  201. package/src/math/utils.ts +0 -183
  202. package/src/memcmp.ts +0 -279
  203. package/src/oracles/oracleClientCache.ts +0 -41
  204. package/src/oracles/oracleId.ts +0 -99
  205. package/src/oracles/prelaunchOracleClient.ts +0 -63
  206. package/src/oracles/pythClient.ts +0 -125
  207. package/src/oracles/pythLazerClient.ts +0 -141
  208. package/src/oracles/quoteAssetOracleClient.ts +0 -43
  209. package/src/oracles/strictOraclePrice.ts +0 -40
  210. package/src/oracles/types.ts +0 -82
  211. package/src/oracles/utils.ts +0 -45
  212. package/src/orderParams.ts +0 -117
  213. package/src/orderSubscriber/OrderSubscriber.ts +0 -378
  214. package/src/orderSubscriber/PollingSubscription.ts +0 -50
  215. package/src/orderSubscriber/WebsocketSubscription.ts +0 -137
  216. package/src/orderSubscriber/grpcSubscription.ts +0 -155
  217. package/src/orderSubscriber/index.ts +0 -7
  218. package/src/orderSubscriber/types.ts +0 -64
  219. package/src/priorityFee/averageOverSlotsStrategy.ts +0 -17
  220. package/src/priorityFee/averageStrategy.ts +0 -13
  221. package/src/priorityFee/ewmaStrategy.ts +0 -51
  222. package/src/priorityFee/heliusPriorityFeeMethod.ts +0 -68
  223. package/src/priorityFee/index.ts +0 -16
  224. package/src/priorityFee/maxOverSlotsStrategy.ts +0 -18
  225. package/src/priorityFee/maxStrategy.ts +0 -8
  226. package/src/priorityFee/priorityFeeSubscriber.ts +0 -328
  227. package/src/priorityFee/priorityFeeSubscriberMap.ts +0 -134
  228. package/src/priorityFee/solanaPriorityFeeMethod.ts +0 -46
  229. package/src/priorityFee/types.ts +0 -82
  230. package/src/priorityFee/velocityPriorityFeeMethod.ts +0 -53
  231. package/src/pyth/constants.ts +0 -9
  232. package/src/pyth/index.ts +0 -15
  233. package/src/pyth/pythLazerSubscriber.ts +0 -365
  234. package/src/pyth/types.ts +0 -4451
  235. package/src/pyth/utils.ts +0 -13
  236. package/src/slot/SlotSubscriber.ts +0 -126
  237. package/src/slot/SlothashSubscriber.ts +0 -163
  238. package/src/swap/UnifiedSwapClient.ts +0 -315
  239. package/src/swift/grpcSignedMsgUserAccountSubscriber.ts +0 -81
  240. package/src/swift/index.ts +0 -10
  241. package/src/swift/signedMsgUserAccountSubscriber.ts +0 -251
  242. package/src/swift/swiftOrderSubscriber.ts +0 -351
  243. package/src/testClient.ts +0 -42
  244. package/src/titan/titanClient.ts +0 -438
  245. package/src/token/index.ts +0 -13
  246. package/src/tokenFaucet.ts +0 -296
  247. package/src/tx/baseTxSender.ts +0 -651
  248. package/src/tx/blockhashFetcher/baseBlockhashFetcher.ts +0 -31
  249. package/src/tx/blockhashFetcher/cachedBlockhashFetcher.ts +0 -105
  250. package/src/tx/blockhashFetcher/types.ts +0 -9
  251. package/src/tx/fastSingleTxSender.ts +0 -200
  252. package/src/tx/forwardOnlyTxSender.ts +0 -193
  253. package/src/tx/priorityFeeCalculator.ts +0 -117
  254. package/src/tx/reportTransactionError.ts +0 -176
  255. package/src/tx/retryTxSender.ts +0 -177
  256. package/src/tx/txHandler.ts +0 -1009
  257. package/src/tx/txParamProcessor.ts +0 -225
  258. package/src/tx/types.ts +0 -117
  259. package/src/tx/utils.ts +0 -133
  260. package/src/tx/whileValidTxSender.ts +0 -377
  261. package/src/types.ts +0 -2575
  262. package/src/user.ts +0 -5316
  263. package/src/userConfig.ts +0 -38
  264. package/src/userMap/PollingSubscription.ts +0 -61
  265. package/src/userMap/WebsocketSubscription.ts +0 -93
  266. package/src/userMap/grpcSubscription.ts +0 -93
  267. package/src/userMap/referrerMap.ts +0 -360
  268. package/src/userMap/revenueShareEscrowMap.ts +0 -342
  269. package/src/userMap/userMap.ts +0 -784
  270. package/src/userMap/userMapConfig.ts +0 -82
  271. package/src/userMap/userStatsMap.ts +0 -393
  272. package/src/userName.ts +0 -37
  273. package/src/userStats.ts +0 -159
  274. package/src/userStatsConfig.ts +0 -35
  275. package/src/util/TransactionConfirmationManager.ts +0 -333
  276. package/src/util/chainClock.ts +0 -65
  277. package/src/util/computeUnits.ts +0 -101
  278. package/src/util/digest.ts +0 -33
  279. package/src/util/ed25519Utils.ts +0 -95
  280. package/src/util/promiseTimeout.ts +0 -23
  281. package/src/velocityClient.ts +0 -14802
  282. package/src/velocityClientConfig.ts +0 -119
  283. package/src/wallet.ts +0 -91
  284. package/tests/VelocityCore/builder_escrow.test.ts +0 -121
  285. package/tests/VelocityCore/decode.test.ts +0 -16
  286. package/tests/VelocityCore/fill_trigger.test.ts +0 -73
  287. package/tests/VelocityCore/instructions.test.ts +0 -46
  288. package/tests/VelocityCore/pdas.test.ts +0 -30
  289. package/tests/VelocityCore/perp_orders.test.ts +0 -205
  290. package/tests/VelocityCore/remainingAccounts.test.ts +0 -72
  291. package/tests/VelocityCore/settlement_liquidation.test.ts +0 -69
  292. package/tests/accounts/customizedCadenceBulkAccountLoader.test.ts +0 -211
  293. package/tests/amm/test.ts +0 -2402
  294. package/tests/auctions/test.ts +0 -81
  295. package/tests/bn/test.ts +0 -355
  296. package/tests/builder/builderFee.test.ts +0 -42
  297. package/tests/ci/verifyConstants.ts +0 -393
  298. package/tests/decode/test.ts +0 -262
  299. package/tests/decode/userAccountBufferStrings.ts +0 -102
  300. package/tests/dlob/helpers.ts +0 -748
  301. package/tests/dlob/test.ts +0 -6969
  302. package/tests/dlob/tickSizeStandardization.ts +0 -545
  303. package/tests/events/parseLogsForCuUsage.ts +0 -139
  304. package/tests/exchangeStatus/test.ts +0 -45
  305. package/tests/insurance/test.ts +0 -40
  306. package/tests/liquidation/test.ts +0 -125
  307. package/tests/oracles/mmOracleGate.test.ts +0 -379
  308. package/tests/oracles/pythPegSnap.test.ts +0 -76
  309. package/tests/sdkParity/enumParity.test.ts +0 -84
  310. package/tests/sdkParity/marginCategoryFill.test.ts +0 -143
  311. package/tests/sdkParity/memcmpOffsets.test.ts +0 -139
  312. package/tests/spot/test.ts +0 -281
  313. package/tests/tx/TransactionConfirmationManager.test.ts +0 -312
  314. package/tests/tx/cachedBlockhashFetcher.test.ts +0 -96
  315. package/tests/tx/priorityFeeCalculator.ts +0 -77
  316. package/tests/tx/priorityFeeStrategy.ts +0 -95
  317. package/tests/user/bankruptcy.ts +0 -165
  318. package/tests/user/feeAndWithdrawLimits.ts +0 -284
  319. package/tests/user/getMarginCalculation.ts +0 -586
  320. package/tests/user/helpers.ts +0 -184
  321. package/tests/user/liquidations.ts +0 -129
  322. package/tests/user/marginCalculations.test.ts +0 -321
  323. package/tests/user/test.ts +0 -833
  324. package/tsconfig.browser.json +0 -18
  325. package/tsconfig.json +0 -36
package/src/dlob/DLOB.ts DELETED
@@ -1,2793 +0,0 @@
1
- /**
2
- * DLOB — Decentralized Limit Order Book.
3
- *
4
- * Maintains an in-memory order book built from on-chain `User` accounts.
5
- * Used by keeper bots to identify the best maker orders to match against taker fills.
6
- *
7
- * Key types:
8
- * `DLOBNode` — a single order node with price/size/user info (DLOBNode.ts)
9
- * `DLOBSubscriber` — subscribes to on-chain accounts and keeps the DLOB live (DLOBSubscriber.ts)
10
- * `NodeList` — sorted linked list of DLOBNodes per side/market (NodeList.ts)
11
- * `orderBookLevels.ts` — aggregated L2/L3 book level construction for quoting
12
- */
13
- import { getOrderSignature, NodeList } from './NodeList';
14
- import { BN } from '../isomorphic/anchor';
15
- import {
16
- BASE_PRECISION,
17
- BN_MAX,
18
- PRICE_PRECISION,
19
- QUOTE_PRECISION,
20
- ZERO,
21
- } from '../constants/numericConstants';
22
- import { decodeName } from '../userName';
23
- import { DLOBNode, DLOBNodeType, TriggerOrderNode } from './DLOBNode';
24
- import { VelocityClient } from '../velocityClient';
25
- import {
26
- calculateOrderBaseAssetAmount,
27
- getLimitPrice,
28
- isOrderExpired,
29
- isRestingLimitOrder,
30
- isTriggered,
31
- mustBeTriggered,
32
- } from '../math/orders';
33
- import {
34
- getVariant,
35
- isOneOfVariant,
36
- isVariant,
37
- MarketType,
38
- MarketTypeStr,
39
- Order,
40
- PerpMarketAccount,
41
- PositionDirection,
42
- SpotMarketAccount,
43
- StateAccount,
44
- } from '../types';
45
- import { MMOraclePriceData, OraclePriceData } from '../oracles/types';
46
- import { SlotSubscriber } from '../slot/SlotSubscriber';
47
- import { UserMap } from '../userMap/userMap';
48
- import { PublicKey } from '@solana/web3.js';
49
- import { ammPaused, exchangePaused, fillPaused } from '../math/exchangeStatus';
50
- import {
51
- createL2Levels,
52
- getL2GeneratorFromDLOBNodes,
53
- L2OrderBook,
54
- L2OrderBookGenerator,
55
- L3Level,
56
- L3OrderBook,
57
- mergeL2LevelGenerators,
58
- } from './orderBookLevels';
59
- import { isFallbackAvailableLiquiditySource } from '../math/auction';
60
- import { convertToNumber } from '../math/conversion';
61
-
62
- /** An on-chain order paired with the pubkey of its owning `User` account. */
63
- export type DLOBOrder = { user: PublicKey; order: Order };
64
- /** A list of `DLOBOrder`s, e.g. the flattened output of `DLOB.getDLOBOrders`. */
65
- export type DLOBOrders = DLOBOrder[];
66
-
67
- /** The full set of sorted `NodeList`s the DLOB maintains for one market, one per order category and side. */
68
- export type MarketNodeLists = {
69
- restingLimit: {
70
- ask: NodeList<'restingLimit'>;
71
- bid: NodeList<'restingLimit'>;
72
- };
73
- floatingLimit: {
74
- ask: NodeList<'floatingLimit'>;
75
- bid: NodeList<'floatingLimit'>;
76
- };
77
- takingLimit: {
78
- ask: NodeList<'takingLimit'>;
79
- bid: NodeList<'takingLimit'>;
80
- };
81
- market: {
82
- ask: NodeList<'market'>;
83
- bid: NodeList<'market'>;
84
- };
85
- trigger: {
86
- above: NodeList<'trigger'>;
87
- below: NodeList<'trigger'>;
88
- };
89
- signedMsg: {
90
- ask: NodeList<'signedMsg'>;
91
- bid: NodeList<'signedMsg'>;
92
- };
93
- };
94
-
95
- type OrderBookCallback = () => void;
96
-
97
- /**
98
- * Receives a DLOBNode and is expected to return true if the node should
99
- * be taken into account when generating, or false otherwise.
100
- *
101
- * Currently used in functions that rely on getBestNode
102
- */
103
- export type DLOBFilterFcn = (node: DLOBNode) => boolean;
104
-
105
- /** A taker node paired with the maker node(s) it should be filled against. `makerNodes` is empty when the fill is expected to route through fallback (e.g. vAMM) liquidity or is an expiration/cancellation rather than a maker match. */
106
- export type NodeToFill = {
107
- node: DLOBNode;
108
- makerNodes: DLOBNode[];
109
- };
110
-
111
- /** A trigger order whose `triggerPrice` has been crossed and is ready to be triggered on-chain. */
112
- export type NodeToTrigger = {
113
- node: TriggerOrderNode;
114
- };
115
-
116
- const SUPPORTED_ORDER_TYPES = [
117
- 'market',
118
- 'limit',
119
- 'triggerMarket',
120
- 'triggerLimit',
121
- 'oracle',
122
- ];
123
-
124
- function getOrderOrThrow(node: DLOBNode): Order {
125
- if (!node.order) {
126
- throw new Error('DLOBNode must have an order');
127
- }
128
- return node.order;
129
- }
130
-
131
- function getUserAccountOrThrow(node: DLOBNode): string {
132
- if (node.userAccount === undefined) {
133
- throw new Error('DLOBNode must have a userAccount');
134
- }
135
- return node.userAccount;
136
- }
137
-
138
- /**
139
- * In-memory order book. Indexes every open order it is given into per-market, per-side sorted
140
- * `NodeList`s (see `MarketNodeLists`), and provides the crossing/fill-finding logic
141
- * (`findNodesToFill`) and aggregated book views (`getL2`/`getL3`) that keepers and clients use to
142
- * predict and drive on-chain fills. A `DLOB` instance is normally built once per slot (e.g. via
143
- * `initFromUserMap`) rather than mutated indefinitely, since state changes (`insertOrder`,
144
- * `delete`) must be paired with the caller's own bookkeeping of what's already been applied.
145
- */
146
- export class DLOB {
147
- /** Order signatures (`getOrderSignature`) currently open, keyed by market type (`'perp'`/`'spot'`). */
148
- openOrders = new Map<MarketTypeStr, Set<string>>();
149
- /** Every market's `MarketNodeLists`, keyed by market type then market index. */
150
- orderLists = new Map<MarketTypeStr, Map<number, MarketNodeLists>>();
151
- /** The highest slot `updateRestingLimitOrders` has processed; used to skip redundant re-promotion of taking→resting orders when called with a slot that's already been seen. */
152
- maxSlotForRestingLimitOrders = 0;
153
-
154
- /** Set to `true` once `initFromUserMap` has successfully populated this instance; `initFromUserMap` is then a no-op. */
155
- initialized = false;
156
-
157
- /** Constructs an empty, uninitialized `DLOB` with no orders. Call `initFromUserMap` (or `insertOrder`/`insertSignedMsgOrder`) to populate it. */
158
- public constructor() {
159
- this.init();
160
- }
161
-
162
- private init() {
163
- this.openOrders.set('perp', new Set<string>());
164
- this.openOrders.set('spot', new Set<string>());
165
- this.orderLists.set('perp', new Map<number, MarketNodeLists>());
166
- this.orderLists.set('spot', new Map<number, MarketNodeLists>());
167
- }
168
-
169
- private getOpenOrdersForMarketType(
170
- marketTypeStr: MarketTypeStr
171
- ): Set<string> {
172
- const openOrders = this.openOrders.get(marketTypeStr);
173
- if (!openOrders) {
174
- throw new Error(
175
- `DLOB has no open orders set for market type ${marketTypeStr}`
176
- );
177
- }
178
- return openOrders;
179
- }
180
-
181
- private getOrderListsForMarketType(
182
- marketTypeStr: MarketTypeStr
183
- ): Map<number, MarketNodeLists> {
184
- const orderLists = this.orderLists.get(marketTypeStr);
185
- if (!orderLists) {
186
- throw new Error(
187
- `DLOB has no order lists for market type ${marketTypeStr}`
188
- );
189
- }
190
- return orderLists;
191
- }
192
-
193
- private tryGetMarketNodeLists(
194
- marketTypeStr: MarketTypeStr,
195
- marketIndex: number
196
- ): MarketNodeLists | undefined {
197
- return this.orderLists.get(marketTypeStr)?.get(marketIndex);
198
- }
199
-
200
- private getMarketNodeLists(
201
- marketTypeStr: MarketTypeStr,
202
- marketIndex: number
203
- ): MarketNodeLists {
204
- const marketNodeLists = this.tryGetMarketNodeLists(
205
- marketTypeStr,
206
- marketIndex
207
- );
208
- if (!marketNodeLists) {
209
- throw new Error(
210
- `DLOB has no order lists for market type ${marketTypeStr} and market index ${marketIndex}`
211
- );
212
- }
213
- return marketNodeLists;
214
- }
215
-
216
- /** Empties every order list and resets the DLOB to its freshly-constructed (uninitialized) state, including `maxSlotForRestingLimitOrders` and `initialized`. */
217
- public clear() {
218
- for (const openOrders of this.openOrders.values()) {
219
- openOrders.clear();
220
- }
221
- this.openOrders.clear();
222
-
223
- for (const marketNodeListsMap of this.orderLists.values()) {
224
- for (const marketNodeLists of marketNodeListsMap.values()) {
225
- for (const side of Object.values(marketNodeLists)) {
226
- for (const nodeList of Object.values(side)) {
227
- nodeList.clear();
228
- }
229
- }
230
- }
231
- }
232
- this.orderLists.clear();
233
-
234
- this.maxSlotForRestingLimitOrders = 0;
235
-
236
- this.init();
237
- }
238
-
239
- /**
240
- * Populates this DLOB from every open order across every user in `userMap`. For reduce-only
241
- * orders, the fillable amount is capped via `calculateOrderBaseAssetAmount` against the
242
- * user's existing perp position for that market, rather than trusting the order's full
243
- * stated `baseAssetAmount`. No-ops (returns `false` immediately) if this instance has already
244
- * been initialized — call `clear()` first to rebuild from scratch.
245
- *
246
- * @param userMap map of all users' accounts to index orders from
247
- * @param slot slot orders are inserted at, used to classify taking vs. resting limit orders
248
- * @returns `true` if this call performed initialization, `false` if it was already initialized
249
- */
250
- public async initFromUserMap(
251
- userMap: UserMap,
252
- slot: number
253
- ): Promise<boolean> {
254
- if (this.initialized) {
255
- return false;
256
- }
257
-
258
- // initialize the dlob with the user map
259
- for (const user of userMap.values()) {
260
- const userAccount = user.getUserAccountOrThrow();
261
- const userAccountPubkey = user.getUserAccountPublicKey();
262
- const userAccountPubkeyString = userAccountPubkey.toString();
263
-
264
- for (const order of userAccount.orders) {
265
- let baseAssetAmount = order.baseAssetAmount;
266
- if (order.reduceOnly) {
267
- const existingBaseAmount =
268
- userAccount.perpPositions.find(
269
- (pos) =>
270
- pos.marketIndex === order.marketIndex && pos.openOrders > 0
271
- )?.baseAssetAmount || ZERO;
272
- baseAssetAmount = calculateOrderBaseAssetAmount(
273
- order,
274
- existingBaseAmount
275
- );
276
- }
277
-
278
- this.insertOrder(order, userAccountPubkeyString, slot, baseAssetAmount);
279
- }
280
- }
281
-
282
- this.initialized = true;
283
- return true;
284
- }
285
-
286
- /**
287
- * Inserts a single on-chain order into the appropriate `NodeList` for its market/side/type.
288
- * No-ops if the order's status isn't `open`, or if its `orderType` isn't one of the
289
- * DLOB-supported types (`market`, `limit`, `triggerMarket`, `triggerLimit`, `oracle`).
290
- * Lazily creates the market's `MarketNodeLists` (via `addOrderList`) on first insert for that
291
- * market. Which list the order lands in (taking vs. resting limit, floating, market, or
292
- * inactive trigger) is decided by `getListForOnChainOrder`.
293
- *
294
- * @param order the on-chain order to insert
295
- * @param userAccount base58 pubkey string of the order's owner
296
- * @param slot current slot, used to classify taking vs. resting limit orders
297
- * @param baseAssetAmount remaining fillable base amount, BASE_PRECISION (1e9) — for
298
- * reduce-only orders this should be the position-capped amount (see
299
- * `calculateOrderBaseAssetAmount`), not the raw `order.baseAssetAmount`
300
- * @param onInsert optional callback invoked after a successful insert
301
- */
302
- public insertOrder(
303
- order: Order,
304
- userAccount: string,
305
- slot: number,
306
- baseAssetAmount: BN,
307
- onInsert?: OrderBookCallback
308
- ): void {
309
- if (!isVariant(order.status, 'open')) {
310
- return;
311
- }
312
-
313
- if (!isOneOfVariant(order.orderType, SUPPORTED_ORDER_TYPES)) {
314
- return;
315
- }
316
-
317
- const marketType = getVariant(order.marketType) as MarketTypeStr;
318
-
319
- if (!this.getOrderListsForMarketType(marketType).has(order.marketIndex)) {
320
- this.addOrderList(marketType, order.marketIndex);
321
- }
322
-
323
- if (isVariant(order.status, 'open')) {
324
- this.getOpenOrdersForMarketType(marketType).add(
325
- getOrderSignature(order.orderId, userAccount)
326
- );
327
- }
328
-
329
- this.getListForOnChainOrder(order, slot)?.insert(
330
- order,
331
- marketType,
332
- userAccount,
333
- baseAssetAmount
334
- );
335
-
336
- if (onInsert) {
337
- onInsert();
338
- }
339
- }
340
-
341
- /**
342
- * Inserts an off-chain signed-message order (not yet landed on-chain) into the market's
343
- * `signedMsg` bid/ask list, unconditionally (no status/order-type filtering, unlike
344
- * `insertOrder`). Lazily creates the market's `MarketNodeLists` on first insert.
345
- *
346
- * @param order the signed-message order to insert
347
- * @param userAccount base58 pubkey string of the order's owner
348
- * @param baseAssetAmount remaining fillable base amount, BASE_PRECISION (1e9); defaults to `order.baseAssetAmount`
349
- * @param onInsert optional callback invoked after insert
350
- */
351
- public insertSignedMsgOrder(
352
- order: Order,
353
- userAccount: string,
354
- baseAssetAmount?: BN,
355
- onInsert?: OrderBookCallback
356
- ): void {
357
- const marketType = getVariant(order.marketType) as MarketTypeStr;
358
- const marketIndex = order.marketIndex;
359
- const bidOrAsk = isVariant(order.direction, 'long') ? 'bid' : 'ask';
360
- if (!this.getOrderListsForMarketType(marketType).has(order.marketIndex)) {
361
- this.addOrderList(marketType, order.marketIndex);
362
- }
363
- this.getOpenOrdersForMarketType(marketType).add(
364
- getOrderSignature(order.orderId, userAccount)
365
- );
366
- this.getMarketNodeLists(marketType, marketIndex).signedMsg[bidOrAsk].insert(
367
- order,
368
- marketType,
369
- userAccount,
370
- baseAssetAmount
371
- );
372
- if (onInsert) {
373
- onInsert();
374
- }
375
- }
376
-
377
- /** Creates and registers an empty `MarketNodeLists` (all six order categories, both sides) for `marketIndex`, overwriting any existing lists for that market. */
378
- addOrderList(marketType: MarketTypeStr, marketIndex: number): void {
379
- this.getOrderListsForMarketType(marketType).set(marketIndex, {
380
- restingLimit: {
381
- ask: new NodeList('restingLimit', 'asc'),
382
- bid: new NodeList('restingLimit', 'desc'),
383
- },
384
- floatingLimit: {
385
- ask: new NodeList('floatingLimit', 'asc'),
386
- bid: new NodeList('floatingLimit', 'desc'),
387
- },
388
- takingLimit: {
389
- ask: new NodeList('takingLimit', 'asc'),
390
- bid: new NodeList('takingLimit', 'asc'), // always sort ascending for market orders
391
- },
392
- market: {
393
- ask: new NodeList('market', 'asc'),
394
- bid: new NodeList('market', 'asc'), // always sort ascending for market orders
395
- },
396
- trigger: {
397
- above: new NodeList('trigger', 'asc'),
398
- below: new NodeList('trigger', 'desc'),
399
- },
400
- signedMsg: {
401
- ask: new NodeList('signedMsg', 'asc'),
402
- bid: new NodeList('signedMsg', 'asc'),
403
- },
404
- });
405
- }
406
-
407
- /**
408
- * Removes an order from whichever `NodeList` it currently lives in. No-ops if the order's
409
- * status isn't `open`. First calls `updateRestingLimitOrders(slot)` so a taking-limit order
410
- * that has since become a resting-limit order is looked up (and removed from) the correct
411
- * list.
412
- *
413
- * @param order the order to remove
414
- * @param userAccount pubkey of the order's owner
415
- * @param slot current slot, used to resolve which list the order is currently in
416
- * @param onDelete optional callback invoked after a successful delete
417
- */
418
- public delete(
419
- order: Order,
420
- userAccount: PublicKey,
421
- slot: number,
422
- onDelete?: OrderBookCallback
423
- ): void {
424
- if (!isVariant(order.status, 'open')) {
425
- return;
426
- }
427
-
428
- this.updateRestingLimitOrders(slot);
429
-
430
- this.getListForOnChainOrder(order, slot)?.remove(
431
- order,
432
- userAccount.toString()
433
- );
434
-
435
- if (onDelete) {
436
- onDelete();
437
- }
438
- }
439
-
440
- /**
441
- * Determines which `NodeList` an order belongs in, given its current state and the slot:
442
- * a trigger order (`triggerMarket`/`triggerLimit`) that hasn't fired yet goes in
443
- * `trigger.above`/`trigger.below`; a market/oracle-type order goes in `market`; a limit order
444
- * with a non-zero `oraclePriceOffset` goes in `floatingLimit`; otherwise a limit order goes in
445
- * `restingLimit` once its auction is complete or it's post-only (per `isRestingLimitOrder`),
446
- * and in `takingLimit` while still auctioning.
447
- *
448
- * @param order the order to classify
449
- * @param slot current slot, used to evaluate `isRestingLimitOrder`
450
- * @returns the matching `NodeList`, or `undefined` if the order's market has no `MarketNodeLists` registered yet (e.g. `insertOrder`/`addOrderList` hasn't been called for it)
451
- */
452
- public getListForOnChainOrder(
453
- order: Order,
454
- slot: number
455
- ): NodeList<any> | undefined {
456
- const isInactiveTriggerOrder =
457
- mustBeTriggered(order) && !isTriggered(order);
458
-
459
- const marketType = getVariant(order.marketType) as MarketTypeStr;
460
-
461
- const marketNodeLists = this.tryGetMarketNodeLists(
462
- marketType,
463
- order.marketIndex
464
- );
465
- if (!marketNodeLists) {
466
- return undefined;
467
- }
468
-
469
- if (isInactiveTriggerOrder) {
470
- const subType = isVariant(order.triggerCondition, 'above')
471
- ? 'above'
472
- : 'below';
473
- return marketNodeLists.trigger[subType];
474
- }
475
-
476
- const subType = isVariant(order.direction, 'long') ? 'bid' : 'ask';
477
- if (
478
- isOneOfVariant(order.orderType, ['market', 'triggerMarket', 'oracle'])
479
- ) {
480
- return marketNodeLists.market[subType];
481
- } else if (!order.oraclePriceOffset.eq(ZERO)) {
482
- return marketNodeLists.floatingLimit[subType];
483
- } else {
484
- const isResting = isRestingLimitOrder(order, slot);
485
- return isResting
486
- ? marketNodeLists.restingLimit[subType]
487
- : marketNodeLists.takingLimit[subType];
488
- }
489
- }
490
-
491
- private getListForOnChainOrderOrThrow(
492
- order: Order,
493
- slot: number
494
- ): NodeList<any> {
495
- const list = this.getListForOnChainOrder(order, slot);
496
- if (!list) {
497
- throw new Error(
498
- `No order list found for order ${order.orderId} in market ${order.marketIndex}`
499
- );
500
- }
501
- return list;
502
- }
503
-
504
- /**
505
- * Promotes any `takingLimit` orders across all perp and spot markets whose auction has since
506
- * completed (per `isRestingLimitOrder`) into their market's `restingLimit` list. No-ops if
507
- * `slot` is not newer than the last slot this was called with (`maxSlotForRestingLimitOrders`),
508
- * so it is cheap to call defensively before any read that depends on resting-limit state
509
- * being current (most getters here do so internally).
510
- *
511
- * @param slot current slot
512
- */
513
- public updateRestingLimitOrders(slot: number): void {
514
- if (slot <= this.maxSlotForRestingLimitOrders) {
515
- return;
516
- }
517
-
518
- this.maxSlotForRestingLimitOrders = slot;
519
-
520
- this.updateRestingLimitOrdersForMarketType(slot, 'perp');
521
-
522
- this.updateRestingLimitOrdersForMarketType(slot, 'spot');
523
- }
524
-
525
- /** Does the `takingLimit` → `restingLimit` promotion (see `updateRestingLimitOrders`) for every market of one market type. */
526
- updateRestingLimitOrdersForMarketType(
527
- slot: number,
528
- marketTypeStr: MarketTypeStr
529
- ): void {
530
- for (const [_, nodeLists] of this.getOrderListsForMarketType(
531
- marketTypeStr
532
- )) {
533
- const nodesToUpdate: Array<{ side: 'ask' | 'bid'; node: DLOBNode }> = [];
534
- for (const node of nodeLists.takingLimit.ask.getGenerator()) {
535
- if (!isRestingLimitOrder(getOrderOrThrow(node), slot)) {
536
- continue;
537
- }
538
-
539
- nodesToUpdate.push({
540
- side: 'ask',
541
- node,
542
- });
543
- }
544
-
545
- for (const node of nodeLists.takingLimit.bid.getGenerator()) {
546
- if (!isRestingLimitOrder(getOrderOrThrow(node), slot)) {
547
- continue;
548
- }
549
-
550
- nodesToUpdate.push({
551
- side: 'bid',
552
- node,
553
- });
554
- }
555
-
556
- for (const nodeToUpdate of nodesToUpdate) {
557
- const { side, node } = nodeToUpdate;
558
- const order = getOrderOrThrow(node);
559
- const userAccount = getUserAccountOrThrow(node);
560
- nodeLists.takingLimit[side].remove(order, userAccount);
561
- nodeLists.restingLimit[side].insert(order, marketTypeStr, userAccount);
562
- }
563
- }
564
- }
565
-
566
- /**
567
- * Looks up an order by id/owner across every `NodeList` in the DLOB (perp and spot, all
568
- * categories/sides) via `getNodeLists`. O(number of lists); prefer a narrower lookup (e.g.
569
- * `NodeList.get`) if you already know the order's market/type.
570
- *
571
- * @param orderId the order's id (unique per user account)
572
- * @param userAccount pubkey of the order's owner
573
- * @returns the matching `Order`, or `undefined` if not found in any list
574
- */
575
- public getOrder(orderId: number, userAccount: PublicKey): Order | undefined {
576
- const orderSignature = getOrderSignature(orderId, userAccount.toString());
577
- for (const nodeList of this.getNodeLists()) {
578
- const node = nodeList.get(orderSignature);
579
- if (node) {
580
- return node.order;
581
- }
582
- }
583
-
584
- return undefined;
585
- }
586
-
587
- /**
588
- * Top-level entry point for keepers: finds every node in one market that is currently
589
- * fillable, combining four sources — crossing resting-limit orders
590
- * (`findRestingLimitOrderNodesToFill`), taking (still-auctioning) orders that cross a maker or
591
- * fallback price (`findTakingNodesToFill`), expired orders to cancel/settle
592
- * (`findExpiredNodesToFill`), and unfillable reduce-only orders below the step size to cancel
593
- * (`findUnfillableReduceOnlyOrdersToCancel`). Returns `[]` immediately if fills are paused for
594
- * this market (`fillPaused`). The market's `orderTickSize` is read from `marketAccount` and
595
- * threaded through to every price comparison below so all crossing checks agree with on-chain
596
- * price standardization.
597
- *
598
- * @param marketIndex the market to scan
599
- * @param fallbackBid best available non-DLOB bid (e.g. vAMM), PRICE_PRECISION (1e6); `undefined` disables fallback-bid crossing checks
600
- * @param fallbackAsk best available non-DLOB ask (e.g. vAMM), PRICE_PRECISION (1e6); `undefined` disables fallback-ask crossing checks
601
- * @param slot current slot
602
- * @param ts current unix timestamp (seconds), used to find expired orders
603
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`; determines whether `oraclePriceData`/`marketAccount` are typed as spot or perp
604
- * @param oraclePriceData oracle price data for the market (`MMOraclePriceData` for perp, `OraclePriceData` for spot)
605
- * @param stateAccount global protocol state, used for pause checks and fee-tier maker rebates
606
- * @param marketAccount the market's account, used for `orderTickSize`/`orderStepSize` and pause checks
607
- * @returns deduplicated `NodeToFill`s (see `mergeNodesToFill`) merging resting-limit and taking-order matches, plus expired and cancel-eligible nodes appended
608
- */
609
- public findNodesToFill<T extends MarketType>(
610
- marketIndex: number,
611
- fallbackBid: BN | undefined,
612
- fallbackAsk: BN | undefined,
613
- slot: number,
614
- ts: number,
615
- marketType: T,
616
- oraclePriceData: T extends { spot: unknown }
617
- ? OraclePriceData
618
- : MMOraclePriceData,
619
- stateAccount: StateAccount,
620
- marketAccount: T extends { spot: unknown }
621
- ? SpotMarketAccount
622
- : PerpMarketAccount
623
- ): NodeToFill[] {
624
- if (fillPaused(stateAccount, marketAccount)) {
625
- return [];
626
- }
627
-
628
- const isAmmPaused = ammPaused(stateAccount, marketAccount);
629
-
630
- const tickSize = (marketAccount as PerpMarketAccount | SpotMarketAccount)
631
- .orderTickSize;
632
-
633
- const { makerRebateNumerator, makerRebateDenominator } =
634
- this.getMakerRebate(marketType, stateAccount, marketAccount);
635
-
636
- const takingOrderNodesToFill: Array<NodeToFill> =
637
- this.findTakingNodesToFill(
638
- marketIndex,
639
- slot,
640
- marketType,
641
- oraclePriceData,
642
- isAmmPaused,
643
- stateAccount,
644
- marketAccount,
645
- fallbackAsk,
646
- fallbackBid,
647
- tickSize
648
- );
649
-
650
- const restingLimitOrderNodesToFill: Array<NodeToFill> =
651
- this.findRestingLimitOrderNodesToFill(
652
- marketIndex,
653
- slot,
654
- marketType,
655
- oraclePriceData,
656
- isAmmPaused,
657
- stateAccount,
658
- marketAccount,
659
- makerRebateNumerator,
660
- makerRebateDenominator,
661
- fallbackAsk,
662
- fallbackBid,
663
- tickSize
664
- );
665
-
666
- // get expired market nodes
667
- const expiredNodesToFill = this.findExpiredNodesToFill(
668
- marketIndex,
669
- ts,
670
- marketType,
671
- new BN(slot)
672
- );
673
-
674
- const stepSize = isVariant(marketType, 'perp')
675
- ? (marketAccount as PerpMarketAccount).orderStepSize
676
- : (marketAccount as SpotMarketAccount).orderStepSize;
677
-
678
- const cancelReduceOnlyNodesToFill =
679
- this.findUnfillableReduceOnlyOrdersToCancel(
680
- marketIndex,
681
- marketType,
682
- stepSize
683
- );
684
-
685
- return this.mergeNodesToFill(
686
- restingLimitOrderNodesToFill,
687
- takingOrderNodesToFill
688
- )
689
- .concat(expiredNodesToFill)
690
- .concat(cancelReduceOnlyNodesToFill);
691
- }
692
-
693
- /**
694
- * Reads the tier-0 maker rebate fraction (`makerRebateNumerator / makerRebateDenominator`)
695
- * for a market from `stateAccount`'s perp/spot fee structure, then scales the numerator up by
696
- * the market's `feeAdjustment` percentage if one is set. Used by `findRestingLimitOrderNodesToFill`
697
- * to size the buffer added to fallback prices so fallback fills aren't triggered by rebate-sized
698
- * noise.
699
- *
700
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
701
- * @param stateAccount global protocol state holding the perp/spot fee tier tables
702
- * @param marketAccount the specific market, whose optional `feeAdjustment` (percent) scales the rebate
703
- * @returns the rebate as a numerator/denominator pair (unitless fraction, not a fixed-point BN)
704
- */
705
- getMakerRebate(
706
- marketType: MarketType,
707
- stateAccount: StateAccount,
708
- marketAccount: PerpMarketAccount | SpotMarketAccount
709
- ): { makerRebateNumerator: number; makerRebateDenominator: number } {
710
- let makerRebateNumerator: number;
711
- let makerRebateDenominator: number;
712
- if (isVariant(marketType, 'perp')) {
713
- makerRebateNumerator =
714
- stateAccount.perpFeeStructure.feeTiers[0].makerRebateNumerator;
715
- makerRebateDenominator =
716
- stateAccount.perpFeeStructure.feeTiers[0].makerRebateDenominator;
717
- } else {
718
- makerRebateNumerator =
719
- stateAccount.spotFeeStructure.feeTiers[0].makerRebateNumerator;
720
- makerRebateDenominator =
721
- stateAccount.spotFeeStructure.feeTiers[0].makerRebateDenominator;
722
- }
723
-
724
- // @ts-ignore
725
- const feeAdjustment = marketAccount.feeAdjustment || 0;
726
- if (feeAdjustment !== 0) {
727
- makerRebateNumerator += (makerRebateNumerator * feeAdjustment) / 100;
728
- }
729
-
730
- return { makerRebateNumerator, makerRebateDenominator };
731
- }
732
-
733
- /**
734
- * Merges two `NodeToFill` arrays (typically resting-limit crossings and taking-order
735
- * crossings for the same market/pass) by taker order signature, concatenating `makerNodes`
736
- * for any taker that appears in both — e.g. an order that both crosses a resting maker and
737
- * separately crosses fallback liquidity ends up as one `NodeToFill` with both maker sources.
738
- *
739
- * @param restingLimitOrderNodesToFill fills found via resting-limit crossing
740
- * @param takingOrderNodesToFill fills found via taking-order crossing
741
- * @returns one `NodeToFill` per distinct taker order, with all matched maker nodes combined
742
- */
743
- mergeNodesToFill(
744
- restingLimitOrderNodesToFill: NodeToFill[],
745
- takingOrderNodesToFill: NodeToFill[]
746
- ): NodeToFill[] {
747
- const mergedNodesToFill = new Map<string, NodeToFill>();
748
-
749
- const mergeNodesToFillHelper = (nodesToFillArray: NodeToFill[]) => {
750
- nodesToFillArray.forEach((nodeToFill) => {
751
- const nodeSignature = getOrderSignature(
752
- getOrderOrThrow(nodeToFill.node).orderId,
753
- getUserAccountOrThrow(nodeToFill.node)
754
- );
755
-
756
- let mergedNodeToFill = mergedNodesToFill.get(nodeSignature);
757
- if (!mergedNodeToFill) {
758
- mergedNodeToFill = {
759
- node: nodeToFill.node,
760
- makerNodes: [],
761
- };
762
- mergedNodesToFill.set(nodeSignature, mergedNodeToFill);
763
- }
764
-
765
- if (nodeToFill.makerNodes) {
766
- mergedNodeToFill.makerNodes.push(...nodeToFill.makerNodes);
767
- }
768
- });
769
- };
770
-
771
- mergeNodesToFillHelper(restingLimitOrderNodesToFill);
772
- mergeNodesToFillHelper(takingOrderNodesToFill);
773
-
774
- return Array.from(mergedNodesToFill.values());
775
- }
776
-
777
- /**
778
- * Finds resting-limit-order fills for a market: resting bids/asks that cross each other
779
- * (`findCrossingRestingLimitOrders`), plus resting asks that cross the fallback bid and
780
- * resting bids that cross the fallback ask (each skipped entirely if the AMM is paused).
781
- * The fallback price on each side is tightened by the maker rebate before comparing, so a
782
- * maker order priced exactly at the rebate-adjusted fallback isn't spuriously flagged as
783
- * crossing (`fallbackBidWithBuffer = fallbackBid - fallbackBid * makerRebateNumerator / makerRebateDenominator`,
784
- * and symmetrically for the ask).
785
- *
786
- * @param marketIndex the market to scan
787
- * @param slot current slot
788
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
789
- * @param oraclePriceData oracle price data for the market
790
- * @param isAmmPaused if true, skips fallback-crossing checks (only maker-vs-maker crossings are returned)
791
- * @param stateAccount global protocol state, forwarded to fallback-availability checks
792
- * @param marketAccount the market's account, forwarded to fallback-availability checks
793
- * @param makerRebateNumerator numerator of the maker rebate fraction (see `getMakerRebate`)
794
- * @param makerRebateDenominator denominator of the maker rebate fraction (see `getMakerRebate`)
795
- * @param fallbackAsk best available non-DLOB ask, PRICE_PRECISION (1e6); `undefined` skips fallback-ask crossing
796
- * @param fallbackBid best available non-DLOB bid, PRICE_PRECISION (1e6); `undefined` skips fallback-bid crossing
797
- * @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into every price comparison; omit to fall back to no rounding
798
- * @returns `NodeToFill`s for maker-crossing-maker and maker-crossing-fallback matches
799
- */
800
- public findRestingLimitOrderNodesToFill<T extends MarketType>(
801
- marketIndex: number,
802
- slot: number,
803
- marketType: T,
804
- oraclePriceData: T extends { spot: unknown }
805
- ? OraclePriceData
806
- : MMOraclePriceData,
807
- isAmmPaused: boolean,
808
- stateAccount: StateAccount,
809
- marketAccount: T extends { spot: unknown }
810
- ? SpotMarketAccount
811
- : PerpMarketAccount,
812
- makerRebateNumerator: number,
813
- makerRebateDenominator: number,
814
- fallbackAsk: BN | undefined,
815
- fallbackBid: BN | undefined,
816
- tickSize?: BN
817
- ): NodeToFill[] {
818
- const nodesToFill = new Array<NodeToFill>();
819
-
820
- const crossingNodes = this.findCrossingRestingLimitOrders(
821
- marketIndex,
822
- slot,
823
- marketType,
824
- oraclePriceData,
825
- tickSize
826
- );
827
-
828
- for (const crossingNode of crossingNodes) {
829
- nodesToFill.push(crossingNode);
830
- }
831
-
832
- if (fallbackBid && !isAmmPaused) {
833
- const askGenerator = this.getRestingLimitAsks(
834
- marketIndex,
835
- slot,
836
- marketType,
837
- oraclePriceData,
838
- undefined,
839
- tickSize
840
- );
841
-
842
- const fallbackBidWithBuffer = fallbackBid.sub(
843
- fallbackBid.muln(makerRebateNumerator).divn(makerRebateDenominator)
844
- );
845
-
846
- const asksCrossingFallback = this.findNodesCrossingFallbackLiquidity(
847
- marketType,
848
- slot,
849
- oraclePriceData,
850
- askGenerator,
851
- (askPrice) => {
852
- if (askPrice === undefined) {
853
- throw new Error('Resting limit ask must have a limit price');
854
- }
855
- return askPrice.lte(fallbackBidWithBuffer);
856
- },
857
- stateAccount,
858
- marketAccount,
859
- tickSize
860
- );
861
-
862
- for (const askCrossingFallback of asksCrossingFallback) {
863
- nodesToFill.push(askCrossingFallback);
864
- }
865
- }
866
-
867
- if (fallbackAsk && !isAmmPaused) {
868
- const bidGenerator = this.getRestingLimitBids(
869
- marketIndex,
870
- slot,
871
- marketType,
872
- oraclePriceData,
873
- undefined,
874
- tickSize
875
- );
876
-
877
- const fallbackAskWithBuffer = fallbackAsk.add(
878
- fallbackAsk.muln(makerRebateNumerator).divn(makerRebateDenominator)
879
- );
880
-
881
- const bidsCrossingFallback = this.findNodesCrossingFallbackLiquidity(
882
- marketType,
883
- slot,
884
- oraclePriceData,
885
- bidGenerator,
886
- (bidPrice) => {
887
- if (bidPrice === undefined) {
888
- throw new Error('Resting limit bid must have a limit price');
889
- }
890
- return bidPrice.gte(fallbackAskWithBuffer);
891
- },
892
- stateAccount,
893
- marketAccount,
894
- tickSize
895
- );
896
-
897
- for (const bidCrossingFallback of bidsCrossingFallback) {
898
- nodesToFill.push(bidCrossingFallback);
899
- }
900
- }
901
-
902
- return nodesToFill;
903
- }
904
-
905
- /**
906
- * Finds fills for taking (still-auctioning) orders: taking asks crossing resting bids or the
907
- * fallback bid, and taking bids crossing resting asks or the fallback ask
908
- * (`findTakingNodesCrossingMakerNodes` / `findNodesCrossingFallbackLiquidity`). Fallback
909
- * crossing checks are skipped entirely when `isAmmPaused`. For spot markets, a taking order is
910
- * only allowed to cross the opposite fallback price if doing so wouldn't also require crossing
911
- * beyond the *other* fallback price (see the inline `fallbackBid`/`fallbackAsk` guards) —
912
- * this prevents a taking order from routing through DLOB makers priced worse than the AMM.
913
- *
914
- * @param marketIndex the market to scan
915
- * @param slot current slot
916
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
917
- * @param oraclePriceData oracle price data for the market
918
- * @param isAmmPaused if true, skips fallback-crossing checks
919
- * @param state global protocol state, forwarded to fallback-availability checks
920
- * @param marketAccount the market's account, forwarded to fallback-availability checks
921
- * @param fallbackAsk best available non-DLOB ask, PRICE_PRECISION (1e6); `undefined` skips ask-side fallback crossing
922
- * @param fallbackBid best available non-DLOB bid, PRICE_PRECISION (1e6); `undefined` skips bid-side fallback crossing
923
- * @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into every price comparison; omit to fall back to no rounding
924
- * @returns `NodeToFill`s for taking orders that cross a resting maker or fallback liquidity
925
- */
926
- public findTakingNodesToFill<T extends MarketType>(
927
- marketIndex: number,
928
- slot: number,
929
- marketType: T,
930
- oraclePriceData: T extends { spot: unknown }
931
- ? OraclePriceData
932
- : MMOraclePriceData,
933
- isAmmPaused: boolean,
934
- state: StateAccount,
935
- marketAccount: T extends { spot: unknown }
936
- ? SpotMarketAccount
937
- : PerpMarketAccount,
938
- fallbackAsk: BN | undefined,
939
- fallbackBid?: BN | undefined,
940
- tickSize?: BN
941
- ): NodeToFill[] {
942
- const nodesToFill = new Array<NodeToFill>();
943
-
944
- let takingOrderGenerator = this.getTakingAsks(
945
- marketIndex,
946
- marketType,
947
- slot,
948
- oraclePriceData
949
- );
950
-
951
- const takingAsksCrossingBids = this.findTakingNodesCrossingMakerNodes(
952
- marketIndex,
953
- slot,
954
- marketType,
955
- oraclePriceData,
956
- takingOrderGenerator,
957
- this.getRestingLimitBids.bind(this),
958
- (takerPrice, makerPrice) => {
959
- if (isVariant(marketType, 'spot')) {
960
- if (takerPrice === undefined) {
961
- return false;
962
- }
963
-
964
- if (fallbackBid && makerPrice.lt(fallbackBid)) {
965
- return false;
966
- }
967
- }
968
- return takerPrice === undefined || takerPrice.lte(makerPrice);
969
- },
970
- tickSize
971
- );
972
- for (const takingAskCrossingBid of takingAsksCrossingBids) {
973
- nodesToFill.push(takingAskCrossingBid);
974
- }
975
-
976
- if (fallbackBid && !isAmmPaused) {
977
- takingOrderGenerator = this.getTakingAsks(
978
- marketIndex,
979
- marketType,
980
- slot,
981
- oraclePriceData
982
- );
983
- const takingAsksCrossingFallback =
984
- this.findNodesCrossingFallbackLiquidity(
985
- marketType,
986
- slot,
987
- oraclePriceData,
988
- takingOrderGenerator,
989
- (takerPrice) => {
990
- return takerPrice === undefined || takerPrice.lte(fallbackBid);
991
- },
992
- state,
993
- marketAccount,
994
- tickSize
995
- );
996
-
997
- for (const takingAskCrossingFallback of takingAsksCrossingFallback) {
998
- nodesToFill.push(takingAskCrossingFallback);
999
- }
1000
- }
1001
-
1002
- takingOrderGenerator = this.getTakingBids(
1003
- marketIndex,
1004
- marketType,
1005
- slot,
1006
- oraclePriceData
1007
- );
1008
-
1009
- const takingBidsToFill = this.findTakingNodesCrossingMakerNodes(
1010
- marketIndex,
1011
- slot,
1012
- marketType,
1013
- oraclePriceData,
1014
- takingOrderGenerator,
1015
- this.getRestingLimitAsks.bind(this),
1016
- (takerPrice, makerPrice) => {
1017
- if (isVariant(marketType, 'spot')) {
1018
- if (takerPrice === undefined) {
1019
- return false;
1020
- }
1021
-
1022
- if (fallbackAsk && makerPrice.gt(fallbackAsk)) {
1023
- return false;
1024
- }
1025
- }
1026
-
1027
- return takerPrice === undefined || takerPrice.gte(makerPrice);
1028
- },
1029
- tickSize
1030
- );
1031
-
1032
- for (const takingBidToFill of takingBidsToFill) {
1033
- nodesToFill.push(takingBidToFill);
1034
- }
1035
-
1036
- if (fallbackAsk && !isAmmPaused) {
1037
- takingOrderGenerator = this.getTakingBids(
1038
- marketIndex,
1039
- marketType,
1040
- slot,
1041
- oraclePriceData
1042
- );
1043
- const takingBidsCrossingFallback =
1044
- this.findNodesCrossingFallbackLiquidity(
1045
- marketType,
1046
- slot,
1047
- oraclePriceData,
1048
- takingOrderGenerator,
1049
- (takerPrice) => {
1050
- return takerPrice === undefined || takerPrice.gte(fallbackAsk);
1051
- },
1052
- state,
1053
- marketAccount,
1054
- tickSize
1055
- );
1056
- for (const marketBidCrossingFallback of takingBidsCrossingFallback) {
1057
- nodesToFill.push(marketBidCrossingFallback);
1058
- }
1059
- }
1060
-
1061
- return nodesToFill;
1062
- }
1063
-
1064
- /**
1065
- * Walks `takerNodeGenerator` (taking bids or asks, sorted by arrival slot) against a fresh
1066
- * maker-side generator (built per taker via `makerNodeGeneratorFn`, e.g.
1067
- * `getRestingLimitBids`) and records a `NodeToFill` for every taker/maker pair that
1068
- * `doesCross` accepts, skipping same-user matches. For each match this method also **mutates
1069
- * DLOB state**: it applies the simulated fill to both the maker's and taker's order lists
1070
- * (via `NodeList.update`) so subsequent iterations see updated `baseAssetAmountFilled` and a
1071
- * taker stops matching once fully filled. Because maker nodes (sorted by price) are scanned
1072
- * in order, `doesCross` returning false breaks out of the maker loop entirely — this is
1073
- * correct for resting-limit makers but relies on the maker generator being price-sorted, not
1074
- * time-sorted.
1075
- *
1076
- * @param marketIndex the market being scanned (used only to route signed-message taker fills into the right list)
1077
- * @param slot current slot, forwarded to price lookups
1078
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1079
- * @param oraclePriceData oracle price data for the market
1080
- * @param takerNodeGenerator taking orders to check, e.g. from `getTakingAsks`/`getTakingBids`
1081
- * @param makerNodeGeneratorFn factory invoked once per taker to get a fresh maker-side generator, e.g. `getRestingLimitBids`/`getRestingLimitAsks`
1082
- * @param doesCross given the taker's price (`undefined` if it has none, e.g. still mid-auction) and the maker's price, returns whether they cross
1083
- * @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into all price lookups
1084
- * @returns one `NodeToFill` per matched taker/maker pair (a taker matched against multiple makers yields multiple entries, each with one maker)
1085
- */
1086
- public findTakingNodesCrossingMakerNodes<T extends MarketType>(
1087
- marketIndex: number,
1088
- slot: number,
1089
- marketType: T,
1090
- oraclePriceData: T extends { spot: unknown }
1091
- ? OraclePriceData
1092
- : MMOraclePriceData,
1093
- takerNodeGenerator: Generator<DLOBNode>,
1094
- makerNodeGeneratorFn: (
1095
- marketIndex: number,
1096
- slot: number,
1097
- marketType: T,
1098
- oraclePriceData: T extends { spot: unknown }
1099
- ? OraclePriceData
1100
- : MMOraclePriceData,
1101
- filterFcn?: DLOBFilterFcn,
1102
- tickSize?: BN
1103
- ) => Generator<DLOBNode>,
1104
- doesCross: (takerPrice: BN | undefined, makerPrice: BN) => boolean,
1105
- tickSize?: BN
1106
- ): NodeToFill[] {
1107
- const nodesToFill = new Array<NodeToFill>();
1108
-
1109
- for (const takerNode of takerNodeGenerator) {
1110
- const makerNodeGenerator = makerNodeGeneratorFn(
1111
- marketIndex,
1112
- slot,
1113
- marketType,
1114
- oraclePriceData,
1115
- undefined,
1116
- tickSize
1117
- );
1118
-
1119
- for (const makerNode of makerNodeGenerator) {
1120
- // Can't match orders from the same user
1121
- const sameUser = takerNode.userAccount === makerNode.userAccount;
1122
- if (sameUser) {
1123
- continue;
1124
- }
1125
-
1126
- const makerPrice = makerNode.getPriceOrThrow(
1127
- oraclePriceData,
1128
- slot,
1129
- tickSize
1130
- );
1131
- const takerPrice = takerNode.getPrice(oraclePriceData, slot, tickSize);
1132
-
1133
- const ordersCross = doesCross(takerPrice, makerPrice);
1134
- if (!ordersCross) {
1135
- // market orders aren't sorted by price, they are sorted by time, so we need to traverse
1136
- // through all of em
1137
- break;
1138
- }
1139
-
1140
- nodesToFill.push({
1141
- node: takerNode,
1142
- makerNodes: [makerNode],
1143
- });
1144
-
1145
- const makerOrder = getOrderOrThrow(makerNode);
1146
- const takerOrder = getOrderOrThrow(takerNode);
1147
-
1148
- const makerBaseRemaining = makerOrder.baseAssetAmount.sub(
1149
- makerOrder.baseAssetAmountFilled
1150
- );
1151
- const takerBaseRemaining = takerOrder.baseAssetAmount.sub(
1152
- takerOrder.baseAssetAmountFilled
1153
- );
1154
-
1155
- const baseFilled = BN.min(makerBaseRemaining, takerBaseRemaining);
1156
-
1157
- const newMakerOrder = { ...makerOrder };
1158
- newMakerOrder.baseAssetAmountFilled =
1159
- makerOrder.baseAssetAmountFilled.add(baseFilled);
1160
- this.getListForOnChainOrderOrThrow(newMakerOrder, slot).update(
1161
- newMakerOrder,
1162
- getUserAccountOrThrow(makerNode)
1163
- );
1164
-
1165
- const newTakerOrder = { ...takerOrder };
1166
- newTakerOrder.baseAssetAmountFilled =
1167
- takerOrder.baseAssetAmountFilled.add(baseFilled);
1168
-
1169
- if (takerNode.isSignedMsg) {
1170
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1171
- const marketNodeLists = this.getMarketNodeLists(
1172
- marketTypeStr,
1173
- marketIndex
1174
- );
1175
- const orderList = isVariant(takerOrder.direction, 'long')
1176
- ? marketNodeLists.signedMsg.bid
1177
- : marketNodeLists.signedMsg.ask;
1178
- orderList.update(newTakerOrder, getUserAccountOrThrow(takerNode));
1179
- } else {
1180
- this.getListForOnChainOrderOrThrow(newTakerOrder, slot).update(
1181
- newTakerOrder,
1182
- getUserAccountOrThrow(takerNode)
1183
- );
1184
- }
1185
-
1186
- if (
1187
- newTakerOrder.baseAssetAmountFilled.eq(takerOrder.baseAssetAmount)
1188
- ) {
1189
- break;
1190
- }
1191
- }
1192
- }
1193
-
1194
- return nodesToFill;
1195
- }
1196
-
1197
- /**
1198
- * Scans `nodeGenerator` for nodes that both cross the fallback price (`doesCross`, evaluated
1199
- * against each node's `getLimitPrice`, or crossing unconditionally if the node has no limit
1200
- * price) and have fallback liquidity actually available to fill against. For spot markets,
1201
- * post-only orders are skipped (they can never take against the AMM) and fallback liquidity
1202
- * is always considered available; for perp markets, availability additionally requires
1203
- * `isFallbackAvailableLiquiditySource` (broadly: the order's auction is complete and the
1204
- * oracle is valid enough for AMM fills). Does not mutate any order state — unlike
1205
- * `findTakingNodesCrossingMakerNodes`, fallback fills are expected to be sized/settled
1206
- * on-chain rather than simulated here.
1207
- *
1208
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1209
- * @param slot current slot
1210
- * @param oraclePriceData oracle price data for the market
1211
- * @param nodeGenerator candidate nodes to check, e.g. resting-limit or taking orders
1212
- * @param doesCross given a node's limit price (`undefined` if it has none), returns whether it crosses the fallback price
1213
- * @param state global protocol state, used by the perp fallback-availability check
1214
- * @param marketAccount the market's account, used by the perp fallback-availability check
1215
- * @param tickSize market order tick size, PRICE_PRECISION (1e6), used to resolve each node's limit price
1216
- * @returns `NodeToFill`s with an empty `makerNodes` array (the fill is expected to route through fallback liquidity, not a DLOB maker)
1217
- */
1218
- public findNodesCrossingFallbackLiquidity<T extends MarketType>(
1219
- marketType: T,
1220
- slot: number,
1221
- oraclePriceData: T extends { spot: unknown }
1222
- ? OraclePriceData
1223
- : MMOraclePriceData,
1224
- nodeGenerator: Generator<DLOBNode>,
1225
- doesCross: (nodePrice: BN | undefined) => boolean,
1226
- state: StateAccount,
1227
- marketAccount: T extends { spot: unknown }
1228
- ? SpotMarketAccount
1229
- : PerpMarketAccount,
1230
- tickSize?: BN
1231
- ): NodeToFill[] {
1232
- const nodesToFill = new Array<NodeToFill>();
1233
-
1234
- let nextNode = nodeGenerator.next();
1235
- while (!nextNode.done) {
1236
- const node = nextNode.value;
1237
-
1238
- if (isVariant(marketType, 'spot') && node.order?.postOnly) {
1239
- nextNode = nodeGenerator.next();
1240
- continue;
1241
- }
1242
- const nodeOrder = getOrderOrThrow(node);
1243
- const nodePrice = getLimitPrice(
1244
- nodeOrder,
1245
- oraclePriceData,
1246
- slot,
1247
- undefined,
1248
- tickSize
1249
- );
1250
-
1251
- // order crosses if there is no limit price or it crosses fallback price
1252
- const crosses = doesCross(nodePrice);
1253
-
1254
- // fallback is available if auction is complete or it's a spot order
1255
- const fallbackAvailable =
1256
- isVariant(marketType, 'spot') ||
1257
- isFallbackAvailableLiquiditySource(
1258
- nodeOrder,
1259
- oraclePriceData as MMOraclePriceData,
1260
- slot,
1261
- state,
1262
- marketAccount as PerpMarketAccount
1263
- );
1264
-
1265
- if (crosses && fallbackAvailable) {
1266
- nodesToFill.push({
1267
- node: node,
1268
- makerNodes: [], // filled by fallback
1269
- });
1270
- }
1271
-
1272
- nextNode = nodeGenerator.next();
1273
- }
1274
-
1275
- return nodesToFill;
1276
- }
1277
-
1278
- /**
1279
- * Finds orders in a market that are eligible to be expired: any non-trigger, non-TIF-limit
1280
- * order whose `maxTs` (plus a 25-second buffer for limit orders, via `isOrderExpired`) has
1281
- * passed the given timestamp. Also proactively removes (not just reports) signed-message
1282
- * orders whose auction window (`order.slot + order.auctionDuration`) has passed `slot`, since
1283
- * those never landed on-chain and have no on-chain expiration to wait for.
1284
- *
1285
- * @param marketIndex the market to scan
1286
- * @param ts current unix timestamp (seconds)
1287
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1288
- * @param slot current slot; required if the market has any signed-message orders (throws otherwise)
1289
- * @returns `NodeToFill`s (with empty `makerNodes`) for orders ready to expire
1290
- * @throws if a signed-message order is present and `slot` was not provided
1291
- */
1292
- public findExpiredNodesToFill(
1293
- marketIndex: number,
1294
- ts: number,
1295
- marketType: MarketType,
1296
- slot?: BN
1297
- ): NodeToFill[] {
1298
- const nodesToFill = new Array<NodeToFill>();
1299
-
1300
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1301
- const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
1302
-
1303
- if (!nodeLists) {
1304
- return nodesToFill;
1305
- }
1306
-
1307
- // All bids/asks that can expire
1308
- // dont try to expire limit orders with tif as its inefficient use of blockspace
1309
- const bidGenerators = [
1310
- nodeLists.takingLimit.bid.getGenerator(),
1311
- nodeLists.restingLimit.bid.getGenerator(),
1312
- nodeLists.floatingLimit.bid.getGenerator(),
1313
- nodeLists.market.bid.getGenerator(),
1314
- nodeLists.signedMsg.bid.getGenerator(),
1315
- ];
1316
- const askGenerators = [
1317
- nodeLists.takingLimit.ask.getGenerator(),
1318
- nodeLists.restingLimit.ask.getGenerator(),
1319
- nodeLists.floatingLimit.ask.getGenerator(),
1320
- nodeLists.market.ask.getGenerator(),
1321
- nodeLists.signedMsg.ask.getGenerator(),
1322
- ];
1323
-
1324
- for (const bidGenerator of bidGenerators) {
1325
- for (const bid of bidGenerator) {
1326
- const bidOrder = getOrderOrThrow(bid);
1327
- if (bid.isSignedMsg) {
1328
- if (slot === undefined) {
1329
- throw new Error(
1330
- 'Must provide slot to findExpiredNodesToFill to expire signedMsg orders'
1331
- );
1332
- }
1333
- if (slot.gt(bidOrder.slot.addn(bidOrder.auctionDuration))) {
1334
- nodeLists.signedMsg.bid.remove(
1335
- bidOrder,
1336
- getUserAccountOrThrow(bid)
1337
- );
1338
- continue;
1339
- }
1340
- }
1341
- if (isOrderExpired(bidOrder, ts, true, 25)) {
1342
- nodesToFill.push({
1343
- node: bid,
1344
- makerNodes: [],
1345
- });
1346
- }
1347
- }
1348
- }
1349
-
1350
- for (const askGenerator of askGenerators) {
1351
- for (const ask of askGenerator) {
1352
- const askOrder = getOrderOrThrow(ask);
1353
- if (ask.isSignedMsg) {
1354
- if (slot === undefined) {
1355
- throw new Error(
1356
- 'Must provide slot to findExpiredNodesToFill to expire signedMsg orders'
1357
- );
1358
- }
1359
- if (slot.gt(askOrder.slot.addn(askOrder.auctionDuration))) {
1360
- nodeLists.signedMsg.ask.remove(
1361
- askOrder,
1362
- getUserAccountOrThrow(ask)
1363
- );
1364
- continue;
1365
- }
1366
- }
1367
- if (isOrderExpired(askOrder, ts, true, 25)) {
1368
- nodesToFill.push({
1369
- node: ask,
1370
- makerNodes: [],
1371
- });
1372
- }
1373
- }
1374
- }
1375
-
1376
- return nodesToFill;
1377
- }
1378
-
1379
- /**
1380
- * Finds reduce-only orders across every category/side in a market whose remaining
1381
- * `baseAssetAmount` (as tracked on the node, not necessarily the order's original size) has
1382
- * dropped below the market's minimum step size — meaning the order can never be filled again
1383
- * and should be canceled by a keeper rather than left to linger.
1384
- *
1385
- * @param marketIndex the market to scan
1386
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1387
- * @param stepSize market's minimum order step size, BASE_PRECISION (1e9)
1388
- * @returns `NodeToFill`s (with empty `makerNodes`) for reduce-only orders that should be canceled
1389
- */
1390
- public findUnfillableReduceOnlyOrdersToCancel(
1391
- marketIndex: number,
1392
- marketType: MarketType,
1393
- stepSize: BN
1394
- ): NodeToFill[] {
1395
- const nodesToFill = new Array<NodeToFill>();
1396
-
1397
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1398
- const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
1399
-
1400
- if (!nodeLists) {
1401
- return nodesToFill;
1402
- }
1403
-
1404
- const generators = [
1405
- nodeLists.takingLimit.bid.getGenerator(),
1406
- nodeLists.restingLimit.bid.getGenerator(),
1407
- nodeLists.floatingLimit.bid.getGenerator(),
1408
- nodeLists.market.bid.getGenerator(),
1409
- nodeLists.signedMsg.bid.getGenerator(),
1410
- nodeLists.takingLimit.ask.getGenerator(),
1411
- nodeLists.restingLimit.ask.getGenerator(),
1412
- nodeLists.floatingLimit.ask.getGenerator(),
1413
- nodeLists.market.ask.getGenerator(),
1414
- nodeLists.signedMsg.ask.getGenerator(),
1415
- nodeLists.trigger.above.getGenerator(),
1416
- nodeLists.trigger.below.getGenerator(),
1417
- ];
1418
-
1419
- for (const generator of generators) {
1420
- for (const node of generator) {
1421
- if (!getOrderOrThrow(node).reduceOnly) {
1422
- continue;
1423
- }
1424
-
1425
- if (node.baseAssetAmount.lt(stepSize)) {
1426
- nodesToFill.push({
1427
- node,
1428
- makerNodes: [],
1429
- });
1430
- }
1431
- }
1432
- }
1433
-
1434
- return nodesToFill;
1435
- }
1436
- /**
1437
- * Yields taking (still-auctioning) bid nodes for a market — market-bid orders, taking-limit
1438
- * bids, and signed-message bids not yet resting — merged in arrival order (earliest `slot`
1439
- * first, via `getBestNode`). Calls `updateRestingLimitOrders(slot)` first so a signed-message
1440
- * order that has since become a resting-limit order is excluded here.
1441
- *
1442
- * @param marketIndex the market to scan
1443
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1444
- * @param slot current slot
1445
- * @param oraclePriceData oracle price data for the market (unused for the ordering itself, forwarded to `getBestNode`)
1446
- * @param filterFcn optional predicate; nodes it rejects are skipped and not yielded
1447
- * @returns a generator of taking bid nodes, or an empty generator if the market has no `MarketNodeLists`
1448
- */
1449
- *getTakingBids<T extends MarketType>(
1450
- marketIndex: number,
1451
- marketType: T,
1452
- slot: number,
1453
- oraclePriceData: T extends { spot: unknown }
1454
- ? OraclePriceData
1455
- : MMOraclePriceData,
1456
- filterFcn?: DLOBFilterFcn
1457
- ): Generator<DLOBNode> {
1458
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1459
- const orderLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
1460
- if (!orderLists) {
1461
- return;
1462
- }
1463
-
1464
- this.updateRestingLimitOrders(slot);
1465
-
1466
- const generatorList = [
1467
- orderLists.market.bid.getGenerator(),
1468
- orderLists.takingLimit.bid.getGenerator(),
1469
- this.signedMsgGenerator(
1470
- orderLists.signedMsg.bid,
1471
- (x: DLOBNode) => !isRestingLimitOrder(getOrderOrThrow(x), slot)
1472
- ),
1473
- ];
1474
-
1475
- yield* this.getBestNode(
1476
- generatorList,
1477
- oraclePriceData,
1478
- slot,
1479
- (bestNode, currentNode) => {
1480
- return getOrderOrThrow(bestNode).slot.lt(
1481
- getOrderOrThrow(currentNode).slot
1482
- );
1483
- },
1484
- filterFcn
1485
- );
1486
- }
1487
-
1488
- /** Same as `getTakingBids`, but for the ask side. */
1489
- *getTakingAsks<T extends MarketType>(
1490
- marketIndex: number,
1491
- marketType: T,
1492
- slot: number,
1493
- oraclePriceData: T extends { spot: unknown }
1494
- ? OraclePriceData
1495
- : MMOraclePriceData,
1496
- filterFcn?: DLOBFilterFcn
1497
- ): Generator<DLOBNode> {
1498
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1499
- const orderLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
1500
- if (!orderLists) {
1501
- return;
1502
- }
1503
-
1504
- this.updateRestingLimitOrders(slot);
1505
-
1506
- const generatorList = [
1507
- orderLists.market.ask.getGenerator(),
1508
- orderLists.takingLimit.ask.getGenerator(),
1509
- this.signedMsgGenerator(
1510
- orderLists.signedMsg.ask,
1511
- (x: DLOBNode) => !isRestingLimitOrder(getOrderOrThrow(x), slot)
1512
- ),
1513
- ];
1514
-
1515
- yield* this.getBestNode(
1516
- generatorList,
1517
- oraclePriceData,
1518
- slot,
1519
- (bestNode, currentNode) => {
1520
- return getOrderOrThrow(bestNode).slot.lt(
1521
- getOrderOrThrow(currentNode).slot
1522
- );
1523
- },
1524
- filterFcn
1525
- );
1526
- }
1527
-
1528
- /** Filters a `signedMsg` `NodeList`'s nodes by an arbitrary predicate — used to split signed-message orders into "still taking" vs. "now resting" subsets based on `isRestingLimitOrder`. */
1529
- protected *signedMsgGenerator(
1530
- signedMsgOrderList: NodeList<'signedMsg'>,
1531
- filter: (x: DLOBNode) => boolean
1532
- ): Generator<DLOBNode> {
1533
- for (const signedMsgOrder of signedMsgOrderList.getGenerator()) {
1534
- if (filter(signedMsgOrder)) {
1535
- yield signedMsgOrder;
1536
- }
1537
- }
1538
- }
1539
-
1540
- /**
1541
- * K-way-merges multiple node generators (e.g. one per order category feeding one side of the
1542
- * book) into a single generator ordered by `compareFcn`, skipping nodes that are already
1543
- * fully filled (`isBaseFilled`) or rejected by `filterFcn`. This is the shared core behind
1544
- * `getTakingBids`/`getTakingAsks`/`getRestingLimitBids`/`getRestingLimitAsks`/`getBids`/`getAsks`
1545
- * — each just supplies a different `generatorList` and `compareFcn`.
1546
- *
1547
- * @param generatorList the node generators to merge; each must already be sorted per `compareFcn`
1548
- * @param oraclePriceData oracle price data, forwarded to `compareFcn`
1549
- * @param slot current slot, forwarded to `compareFcn`
1550
- * @param compareFcn returns true if `bestDLOBNode` should be preferred over `currentDLOBNode`
1551
- * @param filterFcn optional predicate; nodes it rejects are advanced past and not yielded
1552
- * @returns a single generator yielding the merged, filtered, non-fully-filled nodes in `compareFcn` order
1553
- */
1554
- protected *getBestNode<T extends MarketTypeStr>(
1555
- generatorList: Array<Generator<DLOBNode>>,
1556
- oraclePriceData: T extends 'spot' ? OraclePriceData : MMOraclePriceData,
1557
- slot: number,
1558
- compareFcn: (
1559
- bestDLOBNode: DLOBNode,
1560
- currentDLOBNode: DLOBNode,
1561
- slot: number,
1562
- oraclePriceData: T extends 'spot' ? OraclePriceData : MMOraclePriceData
1563
- ) => boolean,
1564
- filterFcn?: DLOBFilterFcn
1565
- ): Generator<DLOBNode> {
1566
- const generators = generatorList.map((generator) => {
1567
- return {
1568
- next: generator.next(),
1569
- generator,
1570
- };
1571
- });
1572
-
1573
- let sideExhausted = false;
1574
- while (!sideExhausted) {
1575
- const bestGenerator = generators.reduce(
1576
- (bestGenerator, currentGenerator) => {
1577
- if (currentGenerator.next.done) {
1578
- return bestGenerator;
1579
- }
1580
-
1581
- if (bestGenerator.next.done) {
1582
- return currentGenerator;
1583
- }
1584
-
1585
- const bestValue = bestGenerator.next.value as DLOBNode;
1586
- const currentValue = currentGenerator.next.value as DLOBNode;
1587
-
1588
- return compareFcn(bestValue, currentValue, slot, oraclePriceData)
1589
- ? bestGenerator
1590
- : currentGenerator;
1591
- }
1592
- );
1593
-
1594
- if (!bestGenerator.next.done) {
1595
- // skip this node if it's already completely filled
1596
- if (bestGenerator.next.value.isBaseFilled()) {
1597
- bestGenerator.next = bestGenerator.generator.next();
1598
- continue;
1599
- }
1600
-
1601
- if (filterFcn && !filterFcn(bestGenerator.next.value)) {
1602
- bestGenerator.next = bestGenerator.generator.next();
1603
- continue;
1604
- }
1605
-
1606
- yield bestGenerator.next.value;
1607
- bestGenerator.next = bestGenerator.generator.next();
1608
- } else {
1609
- sideExhausted = true;
1610
- }
1611
- }
1612
- }
1613
-
1614
- /**
1615
- * Yields resting-limit ask nodes for a market — `restingLimit`, `floatingLimit`, and any
1616
- * `signedMsg` asks that have become resting — merged best-price-first (lowest ask price
1617
- * first, ties broken by `getBestNode`'s underlying comparator). Calls
1618
- * `updateRestingLimitOrders(slot)` first. `tickSize` is threaded into every price comparison
1619
- * via `DLOBNode.getPriceOrThrow`, so pass the market's `orderTickSize` to match on-chain
1620
- * price standardization — omitting it defaults to no rounding (tick of 1).
1621
- *
1622
- * @param marketIndex the market to scan
1623
- * @param slot current slot
1624
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1625
- * @param oraclePriceData oracle price data for the market; required for spot markets (throws if missing)
1626
- * @param filterFcn optional predicate; nodes it rejects are skipped
1627
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
1628
- * @returns a generator of resting-limit ask nodes, best price first
1629
- * @throws if `marketType` is spot and `oraclePriceData` is not provided; also throws (via `getPriceOrThrow`) if any node has no resolvable limit price
1630
- */
1631
- *getRestingLimitAsks<T extends MarketType>(
1632
- marketIndex: number,
1633
- slot: number,
1634
- marketType: T,
1635
- oraclePriceData: T extends { spot: unknown }
1636
- ? OraclePriceData
1637
- : MMOraclePriceData,
1638
- filterFcn?: DLOBFilterFcn,
1639
- tickSize?: BN
1640
- ): Generator<DLOBNode> {
1641
- if (isVariant(marketType, 'spot') && !oraclePriceData) {
1642
- throw new Error('Must provide OraclePriceData to get spot asks');
1643
- }
1644
-
1645
- this.updateRestingLimitOrders(slot);
1646
-
1647
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1648
- const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
1649
-
1650
- if (!nodeLists) {
1651
- return;
1652
- }
1653
-
1654
- const generatorList = [
1655
- nodeLists.restingLimit.ask.getGenerator(),
1656
- nodeLists.floatingLimit.ask.getGenerator(),
1657
- this.signedMsgGenerator(nodeLists.signedMsg.ask, (x: DLOBNode) =>
1658
- isRestingLimitOrder(getOrderOrThrow(x), slot)
1659
- ),
1660
- ];
1661
-
1662
- yield* this.getBestNode(
1663
- generatorList,
1664
- oraclePriceData,
1665
- slot,
1666
- (bestNode, currentNode, slot, oraclePriceData) => {
1667
- return bestNode
1668
- .getPriceOrThrow(oraclePriceData, slot, tickSize)
1669
- .lt(currentNode.getPriceOrThrow(oraclePriceData, slot, tickSize));
1670
- },
1671
- filterFcn
1672
- );
1673
- }
1674
-
1675
- /** Same as `getRestingLimitAsks`, but for the bid side (merged best-price-first, highest bid first). */
1676
- *getRestingLimitBids<T extends MarketType>(
1677
- marketIndex: number,
1678
- slot: number,
1679
- marketType: T,
1680
- oraclePriceData: T extends { spot: unknown }
1681
- ? OraclePriceData
1682
- : MMOraclePriceData,
1683
- filterFcn?: DLOBFilterFcn,
1684
- tickSize?: BN
1685
- ): Generator<DLOBNode> {
1686
- if (isVariant(marketType, 'spot') && !oraclePriceData) {
1687
- throw new Error('Must provide OraclePriceData to get spot bids');
1688
- }
1689
-
1690
- this.updateRestingLimitOrders(slot);
1691
-
1692
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
1693
- const nodeLists = this.tryGetMarketNodeLists(marketTypeStr, marketIndex);
1694
-
1695
- if (!nodeLists) {
1696
- return;
1697
- }
1698
-
1699
- const generatorList = [
1700
- nodeLists.restingLimit.bid.getGenerator(),
1701
- nodeLists.floatingLimit.bid.getGenerator(),
1702
- this.signedMsgGenerator(nodeLists.signedMsg.bid, (x: DLOBNode) =>
1703
- isRestingLimitOrder(getOrderOrThrow(x), slot)
1704
- ),
1705
- ];
1706
-
1707
- yield* this.getBestNode(
1708
- generatorList,
1709
- oraclePriceData,
1710
- slot,
1711
- (bestNode, currentNode, slot, oraclePriceData) => {
1712
- return bestNode
1713
- .getPriceOrThrow(oraclePriceData, slot, tickSize)
1714
- .gt(currentNode.getPriceOrThrow(oraclePriceData, slot, tickSize));
1715
- },
1716
- filterFcn
1717
- );
1718
- }
1719
-
1720
- /**
1721
- * Merges `getTakingAsks` and `getRestingLimitAsks` into a single best-price-first generator
1722
- * (ties broken by earliest arrival slot). Nodes with no resolvable price (e.g. still
1723
- * mid-auction) sort as price `0` — i.e. best — since `getPrice` (not `getPriceOrThrow`) is
1724
- * used here. Unlike `findTakingNodesToFill`/`findNodesToFill`, this does **not** merge in
1725
- * fallback (e.g. vAMM) liquidity; the `fallbackAsk` parameter is currently unused/reserved.
1726
- *
1727
- * @param marketIndex the market to scan
1728
- * @param fallbackAsk currently unused
1729
- * @param slot current slot
1730
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1731
- * @param oraclePriceData oracle price data for the market; required for spot markets (throws if missing)
1732
- * @param filterFcn optional predicate; nodes it rejects are skipped
1733
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
1734
- * @returns a generator of all ask nodes (taking + resting-limit), best price first
1735
- */
1736
- *getAsks<T extends MarketType>(
1737
- marketIndex: number,
1738
- _fallbackAsk: BN | undefined,
1739
- slot: number,
1740
- marketType: T,
1741
- oraclePriceData: T extends { spot: unknown }
1742
- ? OraclePriceData
1743
- : MMOraclePriceData,
1744
- filterFcn?: DLOBFilterFcn,
1745
- tickSize?: BN
1746
- ): Generator<DLOBNode> {
1747
- if (isVariant(marketType, 'spot') && !oraclePriceData) {
1748
- throw new Error('Must provide OraclePriceData to get spot asks');
1749
- }
1750
-
1751
- const generatorList = [
1752
- this.getTakingAsks(marketIndex, marketType, slot, oraclePriceData),
1753
- this.getRestingLimitAsks(
1754
- marketIndex,
1755
- slot,
1756
- marketType,
1757
- oraclePriceData,
1758
- undefined,
1759
- tickSize
1760
- ),
1761
- ];
1762
-
1763
- yield* this.getBestNode(
1764
- generatorList,
1765
- oraclePriceData,
1766
- slot,
1767
- (bestNode, currentNode, slot, oraclePriceData) => {
1768
- const bestNodePrice =
1769
- bestNode.getPrice(oraclePriceData, slot, tickSize) ?? ZERO;
1770
- const currentNodePrice =
1771
- currentNode.getPrice(oraclePriceData, slot, tickSize) ?? ZERO;
1772
-
1773
- if (bestNodePrice.eq(currentNodePrice)) {
1774
- return getOrderOrThrow(bestNode).slot.lt(
1775
- getOrderOrThrow(currentNode).slot
1776
- );
1777
- }
1778
-
1779
- return bestNodePrice.lt(currentNodePrice);
1780
- },
1781
- filterFcn
1782
- );
1783
- }
1784
-
1785
- /**
1786
- * Merges `getTakingBids` and `getRestingLimitBids` into a single best-price-first generator
1787
- * (ties broken by earliest arrival slot). Nodes with no resolvable price sort as `BN_MAX` —
1788
- * i.e. worst — since a priceless bid shouldn't be preferred over a priced one. Does not merge
1789
- * in fallback (e.g. vAMM) liquidity; the `fallbackBid` parameter is currently unused/reserved.
1790
- *
1791
- * @param marketIndex the market to scan
1792
- * @param fallbackBid currently unused
1793
- * @param slot current slot
1794
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1795
- * @param oraclePriceData oracle price data for the market; required for spot markets (throws if missing)
1796
- * @param filterFcn optional predicate; nodes it rejects are skipped
1797
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
1798
- * @returns a generator of all bid nodes (taking + resting-limit), best price first
1799
- */
1800
- *getBids<T extends MarketType>(
1801
- marketIndex: number,
1802
- _fallbackBid: BN | undefined,
1803
- slot: number,
1804
- marketType: T,
1805
- oraclePriceData: T extends { spot: unknown }
1806
- ? OraclePriceData
1807
- : MMOraclePriceData,
1808
- filterFcn?: DLOBFilterFcn,
1809
- tickSize?: BN
1810
- ): Generator<DLOBNode> {
1811
- if (isVariant(marketType, 'spot') && !oraclePriceData) {
1812
- throw new Error('Must provide OraclePriceData to get spot bids');
1813
- }
1814
-
1815
- const generatorList = [
1816
- this.getTakingBids(marketIndex, marketType, slot, oraclePriceData),
1817
- this.getRestingLimitBids(
1818
- marketIndex,
1819
- slot,
1820
- marketType,
1821
- oraclePriceData,
1822
- undefined,
1823
- tickSize
1824
- ),
1825
- ];
1826
-
1827
- yield* this.getBestNode(
1828
- generatorList,
1829
- oraclePriceData,
1830
- slot,
1831
- (bestNode, currentNode, slot, oraclePriceData) => {
1832
- const bestNodePrice =
1833
- bestNode.getPrice(oraclePriceData, slot, tickSize) ?? BN_MAX;
1834
- const currentNodePrice =
1835
- currentNode.getPrice(oraclePriceData, slot, tickSize) ?? BN_MAX;
1836
-
1837
- if (bestNodePrice.eq(currentNodePrice)) {
1838
- return getOrderOrThrow(bestNode).slot.lt(
1839
- getOrderOrThrow(currentNode).slot
1840
- );
1841
- }
1842
-
1843
- return bestNodePrice.gt(currentNodePrice);
1844
- },
1845
- filterFcn
1846
- );
1847
- }
1848
-
1849
- /**
1850
- * Finds pairs of resting-limit asks and bids that cross each other (`bidPrice >= askPrice`),
1851
- * assigns maker/taker roles via `determineMakerAndTaker` (post-only orders are always makers;
1852
- * otherwise whichever order's auction finished later is the taker), and simulates the fill by
1853
- * updating both orders' `baseAssetAmountFilled` in their `NodeList`s so subsequent iterations
1854
- * see the reduced remaining size. Same-user matches are skipped. Because both ask and bid
1855
- * generators are price-sorted, the inner loop `break`s as soon as `bidPrice < askPrice` for a
1856
- * given ask, since no later (worse) bid can cross either.
1857
- *
1858
- * @param marketIndex the market to scan
1859
- * @param slot current slot
1860
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
1861
- * @param oraclePriceData oracle price data for the market
1862
- * @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into all price lookups
1863
- * @returns `NodeToFill`s (taker node + one maker node each) for every crossing pair found
1864
- */
1865
- findCrossingRestingLimitOrders<T extends MarketType>(
1866
- marketIndex: number,
1867
- slot: number,
1868
- marketType: T,
1869
- oraclePriceData: T extends { spot: unknown }
1870
- ? OraclePriceData
1871
- : MMOraclePriceData,
1872
- tickSize?: BN
1873
- ): NodeToFill[] {
1874
- const nodesToFill = new Array<NodeToFill>();
1875
-
1876
- for (const askNode of this.getRestingLimitAsks(
1877
- marketIndex,
1878
- slot,
1879
- marketType,
1880
- oraclePriceData,
1881
- undefined,
1882
- tickSize
1883
- )) {
1884
- const bidGenerator = this.getRestingLimitBids(
1885
- marketIndex,
1886
- slot,
1887
- marketType,
1888
- oraclePriceData,
1889
- undefined,
1890
- tickSize
1891
- );
1892
-
1893
- for (const bidNode of bidGenerator) {
1894
- const bidPrice = bidNode.getPriceOrThrow(
1895
- oraclePriceData,
1896
- slot,
1897
- tickSize
1898
- );
1899
- const askPrice = askNode.getPriceOrThrow(
1900
- oraclePriceData,
1901
- slot,
1902
- tickSize
1903
- );
1904
-
1905
- // orders don't cross
1906
- if (bidPrice.lt(askPrice)) {
1907
- break;
1908
- }
1909
-
1910
- const bidOrder = getOrderOrThrow(bidNode);
1911
- const askOrder = getOrderOrThrow(askNode);
1912
-
1913
- // Can't match orders from the same user
1914
- const sameUser = bidNode.userAccount === askNode.userAccount;
1915
- if (sameUser) {
1916
- continue;
1917
- }
1918
-
1919
- const makerAndTaker = this.determineMakerAndTaker(askNode, bidNode);
1920
-
1921
- // unable to match maker and taker due to post only or slot
1922
- if (!makerAndTaker) {
1923
- continue;
1924
- }
1925
-
1926
- const { takerNode, makerNode } = makerAndTaker;
1927
-
1928
- const bidBaseRemaining = bidOrder.baseAssetAmount.sub(
1929
- bidOrder.baseAssetAmountFilled
1930
- );
1931
- const askBaseRemaining = askOrder.baseAssetAmount.sub(
1932
- askOrder.baseAssetAmountFilled
1933
- );
1934
-
1935
- const baseFilled = BN.min(bidBaseRemaining, askBaseRemaining);
1936
-
1937
- const newBidOrder = { ...bidOrder };
1938
- newBidOrder.baseAssetAmountFilled =
1939
- bidOrder.baseAssetAmountFilled.add(baseFilled);
1940
- this.getListForOnChainOrderOrThrow(newBidOrder, slot).update(
1941
- newBidOrder,
1942
- getUserAccountOrThrow(bidNode)
1943
- );
1944
-
1945
- // ask completely filled
1946
- const newAskOrder = { ...askOrder };
1947
- newAskOrder.baseAssetAmountFilled =
1948
- askOrder.baseAssetAmountFilled.add(baseFilled);
1949
- this.getListForOnChainOrderOrThrow(newAskOrder, slot).update(
1950
- newAskOrder,
1951
- getUserAccountOrThrow(askNode)
1952
- );
1953
-
1954
- nodesToFill.push({
1955
- node: takerNode,
1956
- makerNodes: [makerNode],
1957
- });
1958
-
1959
- if (newAskOrder.baseAssetAmount.eq(newAskOrder.baseAssetAmountFilled)) {
1960
- break;
1961
- }
1962
- }
1963
- }
1964
-
1965
- return nodesToFill;
1966
- }
1967
-
1968
- /**
1969
- * Decides which of a crossing ask/bid pair is the maker and which is the taker: if both are
1970
- * post-only, they can't be matched (`undefined`); if exactly one is post-only, it's the
1971
- * maker; otherwise whichever order's auction window (`order.slot + order.auctionDuration`)
1972
- * ends later is treated as the taker (it "arrived crossing" the earlier order).
1973
- *
1974
- * @param askNode the crossing ask node
1975
- * @param bidNode the crossing bid node
1976
- * @returns the assigned `{ takerNode, makerNode }`, or `undefined` if both orders are post-only and neither can take
1977
- */
1978
- determineMakerAndTaker(
1979
- askNode: DLOBNode,
1980
- bidNode: DLOBNode
1981
- ): { takerNode: DLOBNode; makerNode: DLOBNode } | undefined {
1982
- const askOrder = getOrderOrThrow(askNode);
1983
- const bidOrder = getOrderOrThrow(bidNode);
1984
- const askSlot = askOrder.slot.add(new BN(askOrder.auctionDuration));
1985
- const bidSlot = bidOrder.slot.add(new BN(bidOrder.auctionDuration));
1986
-
1987
- if (bidOrder.postOnly && askOrder.postOnly) {
1988
- return undefined;
1989
- } else if (bidOrder.postOnly) {
1990
- return {
1991
- takerNode: askNode,
1992
- makerNode: bidNode,
1993
- };
1994
- } else if (askOrder.postOnly) {
1995
- return {
1996
- takerNode: bidNode,
1997
- makerNode: askNode,
1998
- };
1999
- } else if (askSlot.lte(bidSlot)) {
2000
- return {
2001
- takerNode: bidNode,
2002
- makerNode: askNode,
2003
- };
2004
- } else {
2005
- return {
2006
- takerNode: askNode,
2007
- makerNode: bidNode,
2008
- };
2009
- }
2010
- }
2011
-
2012
- /**
2013
- * Gets the best (lowest) resting-limit ask price for a market. Does not consider fallback
2014
- * (e.g. vAMM) liquidity.
2015
- *
2016
- * @param marketIndex the market to query
2017
- * @param slot current slot
2018
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2019
- * @param oraclePriceData oracle price data for the market
2020
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
2021
- * @returns the best ask price, PRICE_PRECISION (1e6), or `undefined` if there are no resting-limit asks
2022
- */
2023
- public getBestAsk<T extends MarketType>(
2024
- marketIndex: number,
2025
- slot: number,
2026
- marketType: T,
2027
- oraclePriceData: T extends { spot: unknown }
2028
- ? OraclePriceData
2029
- : MMOraclePriceData,
2030
- tickSize?: BN
2031
- ): BN | undefined {
2032
- const bestAsk = this.getRestingLimitAsks(
2033
- marketIndex,
2034
- slot,
2035
- marketType,
2036
- oraclePriceData,
2037
- undefined,
2038
- tickSize
2039
- ).next().value;
2040
-
2041
- if (bestAsk) {
2042
- return bestAsk.getPrice(oraclePriceData, slot, tickSize);
2043
- }
2044
- return undefined;
2045
- }
2046
-
2047
- /**
2048
- * Gets the best (highest) resting-limit bid price for a market. Does not consider fallback
2049
- * (e.g. vAMM) liquidity.
2050
- *
2051
- * @param marketIndex the market to query
2052
- * @param slot current slot
2053
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2054
- * @param oraclePriceData oracle price data for the market
2055
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
2056
- * @returns the best bid price, PRICE_PRECISION (1e6), or `undefined` if there are no resting-limit bids
2057
- */
2058
- public getBestBid<T extends MarketType>(
2059
- marketIndex: number,
2060
- slot: number,
2061
- marketType: T,
2062
- oraclePriceData: T extends { spot: unknown }
2063
- ? OraclePriceData
2064
- : MMOraclePriceData,
2065
- tickSize?: BN
2066
- ): BN | undefined {
2067
- const bestBid = this.getRestingLimitBids(
2068
- marketIndex,
2069
- slot,
2070
- marketType,
2071
- oraclePriceData,
2072
- undefined,
2073
- tickSize
2074
- ).next().value;
2075
-
2076
- if (bestBid) {
2077
- return bestBid.getPrice(oraclePriceData, slot, tickSize);
2078
- }
2079
- return undefined;
2080
- }
2081
-
2082
- /**
2083
- * Yields untriggered trigger orders that would close a position in `direction`: for a `long`
2084
- * position, short-direction orders in the `trigger.below` list (stop triggers on a price
2085
- * drop); for a `short` position, long-direction orders in `trigger.above` (stop triggers on a
2086
- * price rise). Includes both `triggerMarket` and `triggerLimit` order types — see
2087
- * `getStopLossMarkets`/`getStopLossLimits` to filter to one.
2088
- *
2089
- * @param marketIndex the market to scan
2090
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2091
- * @param direction the direction of the position being protected (not the order's own direction)
2092
- * @returns a generator of stop-loss trigger order nodes
2093
- */
2094
- public *getStopLosses(
2095
- marketIndex: number,
2096
- marketType: MarketType,
2097
- direction: PositionDirection
2098
- ): Generator<DLOBNode> {
2099
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
2100
- const marketNodeLists = this.getMarketNodeLists(marketTypeStr, marketIndex);
2101
-
2102
- if (isVariant(direction, 'long') && marketNodeLists.trigger.below) {
2103
- for (const node of marketNodeLists.trigger.below.getGenerator()) {
2104
- if (isVariant(getOrderOrThrow(node).direction, 'short')) {
2105
- yield node;
2106
- }
2107
- }
2108
- } else if (isVariant(direction, 'short') && marketNodeLists.trigger.above) {
2109
- for (const node of marketNodeLists.trigger.above.getGenerator()) {
2110
- if (isVariant(getOrderOrThrow(node).direction, 'long')) {
2111
- yield node;
2112
- }
2113
- }
2114
- }
2115
- }
2116
-
2117
- /** Same as `getStopLosses`, filtered to `triggerMarket` orders only. */
2118
- public *getStopLossMarkets(
2119
- marketIndex: number,
2120
- marketType: MarketType,
2121
- direction: PositionDirection
2122
- ): Generator<DLOBNode> {
2123
- for (const node of this.getStopLosses(marketIndex, marketType, direction)) {
2124
- if (isVariant(getOrderOrThrow(node).orderType, 'triggerMarket')) {
2125
- yield node;
2126
- }
2127
- }
2128
- }
2129
-
2130
- /** Same as `getStopLosses`, filtered to `triggerLimit` orders only. */
2131
- public *getStopLossLimits(
2132
- marketIndex: number,
2133
- marketType: MarketType,
2134
- direction: PositionDirection
2135
- ): Generator<DLOBNode> {
2136
- for (const node of this.getStopLosses(marketIndex, marketType, direction)) {
2137
- if (isVariant(getOrderOrThrow(node).orderType, 'triggerLimit')) {
2138
- yield node;
2139
- }
2140
- }
2141
- }
2142
-
2143
- /**
2144
- * Yields untriggered trigger orders that would close a position in `direction` for profit:
2145
- * for a `long` position, short-direction orders in `trigger.above` (take-profit on a price
2146
- * rise); for a `short` position, long-direction orders in `trigger.below` (take-profit on a
2147
- * price drop). Includes both `triggerMarket` and `triggerLimit` order types — see
2148
- * `getTakeProfitMarkets`/`getTakeProfitLimits` to filter to one.
2149
- *
2150
- * @param marketIndex the market to scan
2151
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2152
- * @param direction the direction of the position being protected (not the order's own direction)
2153
- * @returns a generator of take-profit trigger order nodes
2154
- */
2155
- public *getTakeProfits(
2156
- marketIndex: number,
2157
- marketType: MarketType,
2158
- direction: PositionDirection
2159
- ): Generator<DLOBNode> {
2160
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
2161
- const marketNodeLists = this.getMarketNodeLists(marketTypeStr, marketIndex);
2162
-
2163
- if (isVariant(direction, 'long') && marketNodeLists.trigger.above) {
2164
- for (const node of marketNodeLists.trigger.above.getGenerator()) {
2165
- if (isVariant(getOrderOrThrow(node).direction, 'short')) {
2166
- yield node;
2167
- }
2168
- }
2169
- } else if (isVariant(direction, 'short') && marketNodeLists.trigger.below) {
2170
- for (const node of marketNodeLists.trigger.below.getGenerator()) {
2171
- if (isVariant(getOrderOrThrow(node).direction, 'long')) {
2172
- yield node;
2173
- }
2174
- }
2175
- }
2176
- }
2177
-
2178
- /** Same as `getTakeProfits`, filtered to `triggerMarket` orders only. */
2179
- public *getTakeProfitMarkets(
2180
- marketIndex: number,
2181
- marketType: MarketType,
2182
- direction: PositionDirection
2183
- ): Generator<DLOBNode> {
2184
- for (const node of this.getTakeProfits(
2185
- marketIndex,
2186
- marketType,
2187
- direction
2188
- )) {
2189
- if (isVariant(getOrderOrThrow(node).orderType, 'triggerMarket')) {
2190
- yield node;
2191
- }
2192
- }
2193
- }
2194
-
2195
- /** Same as `getTakeProfits`, filtered to `triggerLimit` orders only. */
2196
- public *getTakeProfitLimits(
2197
- marketIndex: number,
2198
- marketType: MarketType,
2199
- direction: PositionDirection
2200
- ): Generator<DLOBNode> {
2201
- for (const node of this.getTakeProfits(
2202
- marketIndex,
2203
- marketType,
2204
- direction
2205
- )) {
2206
- if (isVariant(getOrderOrThrow(node).orderType, 'triggerLimit')) {
2207
- yield node;
2208
- }
2209
- }
2210
- }
2211
-
2212
- /**
2213
- * Finds trigger orders whose condition is now satisfied by `triggerPrice`: `trigger.above`
2214
- * orders with `triggerPrice > order.triggerPrice`, and `trigger.below` orders with
2215
- * `triggerPrice < order.triggerPrice`. Both lists are sorted by trigger price with the
2216
- * nearest-to-triggering order at `head`, so each scan walks from `head` and `break`s at the
2217
- * first order that isn't (yet) triggered. Returns `[]` immediately if the exchange is paused.
2218
- *
2219
- * @param marketIndex the market to scan
2220
- * @param slot current slot (currently unused by the scan itself, reserved for future use)
2221
- * @param triggerPrice the price to check trigger conditions against, PRICE_PRECISION (1e6) — typically the current oracle or mark price
2222
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2223
- * @param stateAccount global protocol state, used for the exchange-pause check
2224
- * @returns `NodeToTrigger`s for every order ready to be triggered on-chain
2225
- */
2226
- public findNodesToTrigger(
2227
- marketIndex: number,
2228
- slot: number,
2229
- triggerPrice: BN,
2230
- marketType: MarketType,
2231
- stateAccount: StateAccount
2232
- ): NodeToTrigger[] {
2233
- if (exchangePaused(stateAccount)) {
2234
- return [];
2235
- }
2236
-
2237
- const nodesToTrigger: NodeToTrigger[] = [];
2238
- const marketTypeStr = getVariant(marketType) as MarketTypeStr;
2239
- const marketNodeLists = this.orderLists
2240
- .get(marketTypeStr)
2241
- ?.get(marketIndex);
2242
-
2243
- const triggerAboveList = marketNodeLists
2244
- ? marketNodeLists.trigger.above
2245
- : undefined;
2246
- if (triggerAboveList) {
2247
- for (
2248
- let node = triggerAboveList.head;
2249
- node !== undefined;
2250
- node = node.next
2251
- ) {
2252
- if (triggerPrice.gt(node.order.triggerPrice)) {
2253
- nodesToTrigger.push({
2254
- node: node,
2255
- });
2256
- } else {
2257
- break;
2258
- }
2259
- }
2260
- }
2261
-
2262
- const triggerBelowList = marketNodeLists
2263
- ? marketNodeLists.trigger.below
2264
- : undefined;
2265
- if (triggerBelowList) {
2266
- for (
2267
- let node = triggerBelowList.head;
2268
- node !== undefined;
2269
- node = node.next
2270
- ) {
2271
- if (triggerPrice.lt(node.order.triggerPrice)) {
2272
- nodesToTrigger.push({
2273
- node: node,
2274
- });
2275
- } else {
2276
- break;
2277
- }
2278
- }
2279
- }
2280
-
2281
- return nodesToTrigger;
2282
- }
2283
-
2284
- /**
2285
- * Debug helper: logs the market's best bid, best ask, and mid price (all resting-limit only,
2286
- * no fallback liquidity), along with each side's spread to the current oracle price, as a
2287
- * percentage.
2288
- *
2289
- * @param velocityClient client used to resolve market accounts and oracle price data
2290
- * @param slotSubscriber source of the current slot
2291
- * @param marketIndex the market to print
2292
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2293
- * @throws if the market currently has no resting-limit bid or ask (best bid/ask is `undefined`)
2294
- */
2295
- public printTop(
2296
- velocityClient: VelocityClient,
2297
- slotSubscriber: SlotSubscriber,
2298
- marketIndex: number,
2299
- marketType: MarketType
2300
- ) {
2301
- if (isVariant(marketType, 'perp')) {
2302
- const slot = slotSubscriber.getSlot();
2303
- const oraclePriceData =
2304
- velocityClient.getMMOracleDataForPerpMarket(marketIndex);
2305
-
2306
- const bestAsk = this.getBestAsk(
2307
- marketIndex,
2308
- slot,
2309
- marketType,
2310
- oraclePriceData
2311
- );
2312
- const bestBid = this.getBestBid(
2313
- marketIndex,
2314
- slot,
2315
- marketType,
2316
- oraclePriceData
2317
- );
2318
- if (bestAsk === undefined || bestBid === undefined) {
2319
- throw new Error(
2320
- `printTop requires both a best ask and best bid for market ${marketIndex}`
2321
- );
2322
- }
2323
- const mid = bestAsk.add(bestBid).div(new BN(2));
2324
-
2325
- const bidSpread =
2326
- (convertToNumber(bestBid, PRICE_PRECISION) /
2327
- convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
2328
- 1) *
2329
- 100.0;
2330
- const askSpread =
2331
- (convertToNumber(bestAsk, PRICE_PRECISION) /
2332
- convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
2333
- 1) *
2334
- 100.0;
2335
-
2336
- const name = decodeName(
2337
- velocityClient.getPerpMarketAccountOrThrow(marketIndex).name
2338
- );
2339
- console.log(`Market ${name} Orders`);
2340
- console.log(
2341
- ` Ask`,
2342
- convertToNumber(bestAsk, PRICE_PRECISION).toFixed(3),
2343
- `(${askSpread.toFixed(4)}%)`
2344
- );
2345
- console.log(` Mid`, convertToNumber(mid, PRICE_PRECISION).toFixed(3));
2346
- console.log(
2347
- ` Bid`,
2348
- convertToNumber(bestBid, PRICE_PRECISION).toFixed(3),
2349
- `(${bidSpread.toFixed(4)}%)`
2350
- );
2351
- } else if (isVariant(marketType, 'spot')) {
2352
- const slot = slotSubscriber.getSlot();
2353
- const oraclePriceData =
2354
- velocityClient.getOracleDataForSpotMarket(marketIndex);
2355
-
2356
- const bestAsk = this.getBestAsk(
2357
- marketIndex,
2358
- slot,
2359
- MarketType.SPOT,
2360
- oraclePriceData
2361
- );
2362
- const bestBid = this.getBestBid(
2363
- marketIndex,
2364
- slot,
2365
- MarketType.SPOT,
2366
- oraclePriceData
2367
- );
2368
- if (bestAsk === undefined || bestBid === undefined) {
2369
- throw new Error(
2370
- `printTop requires both a best ask and best bid for market ${marketIndex}`
2371
- );
2372
- }
2373
- const mid = bestAsk.add(bestBid).div(new BN(2));
2374
-
2375
- const bidSpread =
2376
- (convertToNumber(bestBid, PRICE_PRECISION) /
2377
- convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
2378
- 1) *
2379
- 100.0;
2380
- const askSpread =
2381
- (convertToNumber(bestAsk, PRICE_PRECISION) /
2382
- convertToNumber(oraclePriceData.price, PRICE_PRECISION) -
2383
- 1) *
2384
- 100.0;
2385
-
2386
- const name = decodeName(
2387
- velocityClient.getSpotMarketAccountOrThrow(marketIndex).name
2388
- );
2389
- console.log(`Market ${name} Orders`);
2390
- console.log(
2391
- ` Ask`,
2392
- convertToNumber(bestAsk, PRICE_PRECISION).toFixed(3),
2393
- `(${askSpread.toFixed(4)}%)`
2394
- );
2395
- console.log(` Mid`, convertToNumber(mid, PRICE_PRECISION).toFixed(3));
2396
- console.log(
2397
- ` Bid`,
2398
- convertToNumber(bestBid, PRICE_PRECISION).toFixed(3),
2399
- `(${bidSpread.toFixed(4)}%)`
2400
- );
2401
- }
2402
- }
2403
-
2404
- /** Flattens every order across every `NodeList` (perp and spot, all categories/sides) into a single `DLOBOrders` array of `{ user, order }` pairs, in no particular cross-list order. */
2405
- public getDLOBOrders(): DLOBOrders {
2406
- const dlobOrders: DLOBOrders = [];
2407
-
2408
- for (const nodeList of this.getNodeLists()) {
2409
- for (const node of nodeList.getGenerator()) {
2410
- dlobOrders.push({
2411
- user: new PublicKey(getUserAccountOrThrow(node)),
2412
- order: getOrderOrThrow(node),
2413
- });
2414
- }
2415
- }
2416
-
2417
- return dlobOrders;
2418
- }
2419
-
2420
- /** Yields every `NodeList` (all ten category/side combinations, per market) across every perp market, then every spot market. Used by `getOrder`/`getDLOBOrders` to walk the entire book. */
2421
- *getNodeLists(): Generator<NodeList<DLOBNodeType>> {
2422
- for (const [_, nodeLists] of this.getOrderListsForMarketType('perp')) {
2423
- yield nodeLists.restingLimit.bid;
2424
- yield nodeLists.restingLimit.ask;
2425
- yield nodeLists.takingLimit.bid;
2426
- yield nodeLists.takingLimit.ask;
2427
- yield nodeLists.market.bid;
2428
- yield nodeLists.market.ask;
2429
- yield nodeLists.floatingLimit.bid;
2430
- yield nodeLists.floatingLimit.ask;
2431
- yield nodeLists.trigger.above;
2432
- yield nodeLists.trigger.below;
2433
- }
2434
-
2435
- for (const [_, nodeLists] of this.getOrderListsForMarketType('spot')) {
2436
- yield nodeLists.restingLimit.bid;
2437
- yield nodeLists.restingLimit.ask;
2438
- yield nodeLists.takingLimit.bid;
2439
- yield nodeLists.takingLimit.ask;
2440
- yield nodeLists.market.bid;
2441
- yield nodeLists.market.ask;
2442
- yield nodeLists.floatingLimit.bid;
2443
- yield nodeLists.floatingLimit.ask;
2444
- yield nodeLists.trigger.above;
2445
- yield nodeLists.trigger.below;
2446
- }
2447
- }
2448
-
2449
- /**
2450
- * Get an L2 (aggregated price/size) view of the order book for a given market: resting-limit
2451
- * DLOB liquidity merged with any supplied fallback generators (e.g. the vAMM, via
2452
- * `getVammL2Generator`), then bucketed into up to `depth` levels per side via `createL2Levels`.
2453
- * Does not include taking (still-auctioning) orders — only resting-limit makers and fallback
2454
- * liquidity are represented.
2455
- *
2456
- * @param marketIndex the market to build a book for
2457
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2458
- * @param slot current slot, used to resolve resting-limit order prices
2459
- * @param oraclePriceData oracle price data for the market
2460
- * @param depth how many price levels of the order book to return, per side
2461
- * @param fallbackL2Generators additional non-DLOB liquidity sources to merge in, e.g. `getVammL2Generator`'s output; defaults to `[]`
2462
- * @param tickSize market order tick size, PRICE_PRECISION (1e6), threaded into all DLOB price lookups; defaults to no rounding if omitted
2463
- * @returns the merged `L2OrderBook`, tagged with the given `slot`
2464
- */
2465
- public getL2<T extends MarketType>({
2466
- marketIndex,
2467
- marketType,
2468
- slot,
2469
- oraclePriceData,
2470
- depth,
2471
- fallbackL2Generators = [],
2472
- tickSize,
2473
- }: {
2474
- marketIndex: number;
2475
- marketType: T;
2476
- slot: number;
2477
- oraclePriceData: T extends { spot: unknown }
2478
- ? OraclePriceData
2479
- : MMOraclePriceData;
2480
- depth: number;
2481
- fallbackL2Generators?: L2OrderBookGenerator[];
2482
- tickSize?: BN;
2483
- }): L2OrderBook {
2484
- const makerAskL2LevelGenerator = getL2GeneratorFromDLOBNodes(
2485
- this.getRestingLimitAsks(
2486
- marketIndex,
2487
- slot,
2488
- marketType,
2489
- oraclePriceData,
2490
- undefined,
2491
- tickSize
2492
- ),
2493
- oraclePriceData,
2494
- slot,
2495
- tickSize
2496
- );
2497
-
2498
- const fallbackAskGenerators = fallbackL2Generators.map(
2499
- (fallbackL2Generator) => {
2500
- return fallbackL2Generator.getL2Asks();
2501
- }
2502
- );
2503
-
2504
- const askL2LevelGenerator = mergeL2LevelGenerators(
2505
- [makerAskL2LevelGenerator, ...fallbackAskGenerators],
2506
- (a, b) => {
2507
- return a.price.lt(b.price);
2508
- }
2509
- );
2510
-
2511
- const asks = createL2Levels(askL2LevelGenerator, depth);
2512
-
2513
- const makerBidGenerator = getL2GeneratorFromDLOBNodes(
2514
- this.getRestingLimitBids(
2515
- marketIndex,
2516
- slot,
2517
- marketType,
2518
- oraclePriceData,
2519
- undefined,
2520
- tickSize
2521
- ),
2522
- oraclePriceData,
2523
- slot,
2524
- tickSize
2525
- );
2526
-
2527
- const fallbackBidGenerators = fallbackL2Generators.map((fallbackOrders) => {
2528
- return fallbackOrders.getL2Bids();
2529
- });
2530
-
2531
- const bidL2LevelGenerator = mergeL2LevelGenerators(
2532
- [makerBidGenerator, ...fallbackBidGenerators],
2533
- (a, b) => {
2534
- return a.price.gt(b.price);
2535
- }
2536
- );
2537
-
2538
- const bids = createL2Levels(bidL2LevelGenerator, depth);
2539
-
2540
- return {
2541
- bids,
2542
- asks,
2543
- slot,
2544
- };
2545
- }
2546
-
2547
- /**
2548
- * Get an L3 (individual resting order) view of the order book for a given market. Only
2549
- * resting-limit orders are included — no taking orders and no fallback (e.g. vAMM) liquidity.
2550
- *
2551
- * @param marketIndex the market to build a book for
2552
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2553
- * @param slot current slot, used to resolve resting-limit order prices
2554
- * @param oraclePriceData oracle price data for the market
2555
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
2556
- * @returns the `L3OrderBook`, tagged with the given `slot`
2557
- */
2558
- public getL3<T extends MarketType>({
2559
- marketIndex,
2560
- marketType,
2561
- slot,
2562
- oraclePriceData,
2563
- tickSize,
2564
- }: {
2565
- marketIndex: number;
2566
- marketType: T;
2567
- slot: number;
2568
- oraclePriceData: T extends { spot: unknown }
2569
- ? OraclePriceData
2570
- : MMOraclePriceData;
2571
- tickSize?: BN;
2572
- }): L3OrderBook {
2573
- const bids: L3Level[] = [];
2574
- const asks: L3Level[] = [];
2575
-
2576
- const restingAsks = this.getRestingLimitAsks(
2577
- marketIndex,
2578
- slot,
2579
- marketType,
2580
- oraclePriceData,
2581
- undefined,
2582
- tickSize
2583
- );
2584
-
2585
- for (const ask of restingAsks) {
2586
- const askOrder = getOrderOrThrow(ask);
2587
- asks.push({
2588
- price: ask.getPriceOrThrow(oraclePriceData, slot, tickSize),
2589
- size: askOrder.baseAssetAmount.sub(askOrder.baseAssetAmountFilled),
2590
- maker: new PublicKey(getUserAccountOrThrow(ask)),
2591
- orderId: askOrder.orderId,
2592
- });
2593
- }
2594
-
2595
- const restingBids = this.getRestingLimitBids(
2596
- marketIndex,
2597
- slot,
2598
- marketType,
2599
- oraclePriceData,
2600
- undefined,
2601
- tickSize
2602
- );
2603
-
2604
- for (const bid of restingBids) {
2605
- const bidOrder = getOrderOrThrow(bid);
2606
- bids.push({
2607
- price: bid.getPriceOrThrow(oraclePriceData, slot, tickSize),
2608
- size: bidOrder.baseAssetAmount.sub(bidOrder.baseAssetAmountFilled),
2609
- maker: new PublicKey(getUserAccountOrThrow(bid)),
2610
- orderId: bidOrder.orderId,
2611
- });
2612
- }
2613
-
2614
- return {
2615
- bids,
2616
- asks,
2617
- slot,
2618
- };
2619
- }
2620
-
2621
- private estimateFillExactBaseAmountInForSide(
2622
- baseAmountIn: BN,
2623
- oraclePriceData: OraclePriceData,
2624
- slot: number,
2625
- dlobSide: Generator<DLOBNode>,
2626
- tickSize?: BN
2627
- ): BN {
2628
- let runningSumQuote = ZERO;
2629
- let runningSumBase = ZERO;
2630
- for (const side of dlobSide) {
2631
- const price = side.getPriceOrThrow(oraclePriceData, slot, tickSize); //side.order.quoteAssetAmount.div(side.order.baseAssetAmount);
2632
- const sideOrder = getOrderOrThrow(side);
2633
- const baseAmountRemaining = sideOrder.baseAssetAmount.sub(
2634
- sideOrder.baseAssetAmountFilled
2635
- );
2636
- if (runningSumBase.add(baseAmountRemaining).gt(baseAmountIn)) {
2637
- const remainingBase = baseAmountIn.sub(runningSumBase);
2638
- runningSumBase = runningSumBase.add(remainingBase);
2639
- runningSumQuote = runningSumQuote.add(remainingBase.mul(price));
2640
- break;
2641
- } else {
2642
- runningSumBase = runningSumBase.add(baseAmountRemaining);
2643
- runningSumQuote = runningSumQuote.add(baseAmountRemaining.mul(price));
2644
- }
2645
- }
2646
-
2647
- return runningSumQuote
2648
- .mul(QUOTE_PRECISION)
2649
- .div(BASE_PRECISION.mul(PRICE_PRECISION));
2650
- }
2651
-
2652
- /**
2653
- * Estimates the quote amount that would be filled for a given base amount, walking
2654
- * resting-limit asks (for a `long`/buy) or bids (for a `short`/sell) from best price outward
2655
- * and summing `price * size` until `baseAmount` is consumed. Does not include fallback (e.g.
2656
- * vAMM) liquidity or taking orders, and does not mutate any order state — this is a read-only
2657
- * estimate, not a simulated fill.
2658
- *
2659
- * @param marketIndex the market to estimate against
2660
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2661
- * @param baseAmount the base amount to fill, BASE_PRECISION (1e9)
2662
- * @param orderDirection direction of the hypothetical taker order (`long` walks asks, `short` walks bids)
2663
- * @param slot current slot, used to resolve resting-limit order prices
2664
- * @param oraclePriceData oracle price data for the market
2665
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
2666
- * @returns the estimated quote amount filled, QUOTE_PRECISION (1e6); if resting liquidity is thinner than `baseAmount`, the result silently reflects only the liquidity that was actually walked (no error/flag for partial fill), and is `0` if there is no resting liquidity on that side at all
2667
- * @throws if `orderDirection` is neither `long` nor `short`
2668
- */
2669
- public estimateFillWithExactBaseAmount<T extends MarketType>({
2670
- marketIndex,
2671
- marketType,
2672
- baseAmount,
2673
- orderDirection,
2674
- slot,
2675
- oraclePriceData,
2676
- tickSize,
2677
- }: {
2678
- marketIndex: number;
2679
- marketType: T;
2680
- baseAmount: BN;
2681
- orderDirection: PositionDirection;
2682
- slot: number;
2683
- oraclePriceData: T extends { spot: unknown }
2684
- ? OraclePriceData
2685
- : MMOraclePriceData;
2686
- tickSize?: BN;
2687
- }): BN {
2688
- if (isVariant(orderDirection, 'long')) {
2689
- return this.estimateFillExactBaseAmountInForSide(
2690
- baseAmount,
2691
- oraclePriceData,
2692
- slot,
2693
- this.getRestingLimitAsks(
2694
- marketIndex,
2695
- slot,
2696
- marketType,
2697
- oraclePriceData,
2698
- undefined,
2699
- tickSize
2700
- ),
2701
- tickSize
2702
- );
2703
- } else if (isVariant(orderDirection, 'short')) {
2704
- return this.estimateFillExactBaseAmountInForSide(
2705
- baseAmount,
2706
- oraclePriceData,
2707
- slot,
2708
- this.getRestingLimitBids(
2709
- marketIndex,
2710
- slot,
2711
- marketType,
2712
- oraclePriceData,
2713
- undefined,
2714
- tickSize
2715
- ),
2716
- tickSize
2717
- );
2718
- }
2719
- throw new Error(
2720
- `Invalid order direction ${getVariant(
2721
- orderDirection
2722
- )}: must be long or short`
2723
- );
2724
- }
2725
-
2726
- /**
2727
- * Collects the pubkeys of up to `numMakers` distinct makers currently resting best-priced on
2728
- * one side of the book — bids for a `long` taker, asks for a `short` taker — in best-price
2729
- * order. Used to pick candidate maker accounts to pass as `remaining_accounts` when
2730
- * submitting a fill instruction. A maker with multiple resting orders at different prices
2731
- * only counts once toward `numMakers`.
2732
- *
2733
- * @param marketIndex the market to scan
2734
- * @param marketType `MarketType.PERP` or `MarketType.SPOT`
2735
- * @param direction direction of the taker order being matched (`long` collects bid-side makers, `short` collects ask-side makers)
2736
- * @param slot current slot, used to resolve resting-limit order prices
2737
- * @param oraclePriceData oracle price data for the market
2738
- * @param numMakers maximum number of distinct maker pubkeys to return
2739
- * @param tickSize market order tick size, PRICE_PRECISION (1e6); defaults to no rounding if omitted
2740
- * @returns up to `numMakers` distinct maker `PublicKey`s, best price first
2741
- */
2742
- public getBestMakers<T extends MarketType>({
2743
- marketIndex,
2744
- marketType,
2745
- direction,
2746
- slot,
2747
- oraclePriceData,
2748
- numMakers,
2749
- tickSize,
2750
- }: {
2751
- marketIndex: number;
2752
- marketType: T;
2753
- direction: PositionDirection;
2754
- slot: number;
2755
- oraclePriceData: T extends { spot: unknown }
2756
- ? OraclePriceData
2757
- : MMOraclePriceData;
2758
- numMakers: number;
2759
- tickSize?: BN;
2760
- }): PublicKey[] {
2761
- const makers = new Map<string, PublicKey>();
2762
- const generator = isVariant(direction, 'long')
2763
- ? this.getRestingLimitBids(
2764
- marketIndex,
2765
- slot,
2766
- marketType,
2767
- oraclePriceData,
2768
- undefined,
2769
- tickSize
2770
- )
2771
- : this.getRestingLimitAsks(
2772
- marketIndex,
2773
- slot,
2774
- marketType,
2775
- oraclePriceData,
2776
- undefined,
2777
- tickSize
2778
- );
2779
-
2780
- for (const node of generator) {
2781
- const userAccount = getUserAccountOrThrow(node);
2782
- if (!makers.has(userAccount)) {
2783
- makers.set(userAccount, new PublicKey(userAccount));
2784
- }
2785
-
2786
- if (makers.size === numMakers) {
2787
- break;
2788
- }
2789
- }
2790
-
2791
- return Array.from(makers.values());
2792
- }
2793
- }