@mysten/deepbook-v3 2.6.4 → 2.8.0

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 (250) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/PREDICT.md +389 -61
  3. package/dist/contracts/account/account.d.mts +23 -23
  4. package/dist/contracts/account/account.d.mts.map +1 -1
  5. package/dist/contracts/account/account_registry.d.mts +13 -13
  6. package/dist/contracts/deepbook/account.d.mts +18 -18
  7. package/dist/contracts/deepbook/balances.d.mts +4 -4
  8. package/dist/contracts/deepbook/deep_price.d.mts +3 -3
  9. package/dist/contracts/deepbook/order.d.mts +12 -12
  10. package/dist/contracts/deepbook_predict/admin.d.mts +4 -4
  11. package/dist/contracts/deepbook_predict/builder_code.d.mts +11 -11
  12. package/dist/contracts/deepbook_predict/builder_code_events.d.mts +10 -10
  13. package/dist/contracts/deepbook_predict/config_events.d.mts +93 -68
  14. package/dist/contracts/deepbook_predict/config_events.d.mts.map +1 -1
  15. package/dist/contracts/deepbook_predict/config_events.mjs +45 -1
  16. package/dist/contracts/deepbook_predict/config_events.mjs.map +1 -1
  17. package/dist/contracts/deepbook_predict/deps/std/type_name.mjs +17 -0
  18. package/dist/contracts/deepbook_predict/deps/std/type_name.mjs.map +1 -0
  19. package/dist/contracts/deepbook_predict/expiry_market.d.mts +652 -235
  20. package/dist/contracts/deepbook_predict/expiry_market.d.mts.map +1 -1
  21. package/dist/contracts/deepbook_predict/expiry_market.mjs +652 -186
  22. package/dist/contracts/deepbook_predict/expiry_market.mjs.map +1 -1
  23. package/dist/contracts/deepbook_predict/lp_book.mjs +1 -1
  24. package/dist/contracts/deepbook_predict/market_lifecycle_cap.d.mts +5 -5
  25. package/dist/contracts/deepbook_predict/market_manager.d.mts +27 -27
  26. package/dist/contracts/deepbook_predict/order_events.d.mts +50 -50
  27. package/dist/contracts/deepbook_predict/order_events.d.mts.map +1 -1
  28. package/dist/contracts/deepbook_predict/order_events.mjs +3 -1
  29. package/dist/contracts/deepbook_predict/order_events.mjs.map +1 -1
  30. package/dist/contracts/deepbook_predict/pause_cap.d.mts +5 -5
  31. package/dist/contracts/deepbook_predict/plp.d.mts +184 -82
  32. package/dist/contracts/deepbook_predict/plp.d.mts.map +1 -1
  33. package/dist/contracts/deepbook_predict/plp.mjs +126 -7
  34. package/dist/contracts/deepbook_predict/plp.mjs.map +1 -1
  35. package/dist/contracts/deepbook_predict/pool_accounting.mjs +1 -1
  36. package/dist/contracts/deepbook_predict/pool_valuation_cap.d.mts +5 -5
  37. package/dist/contracts/deepbook_predict/predict_account.d.mts +14 -14
  38. package/dist/contracts/deepbook_predict/pricing.d.mts +91 -58
  39. package/dist/contracts/deepbook_predict/pricing.d.mts.map +1 -1
  40. package/dist/contracts/deepbook_predict/pricing.mjs +30 -3
  41. package/dist/contracts/deepbook_predict/pricing.mjs.map +1 -1
  42. package/dist/contracts/deepbook_predict/pricing_config.mjs.map +1 -1
  43. package/dist/contracts/deepbook_predict/protocol_config.d.mts +400 -92
  44. package/dist/contracts/deepbook_predict/protocol_config.d.mts.map +1 -1
  45. package/dist/contracts/deepbook_predict/protocol_config.mjs +451 -61
  46. package/dist/contracts/deepbook_predict/protocol_config.mjs.map +1 -1
  47. package/dist/contracts/deepbook_predict/range_codec.d.mts +2 -2
  48. package/dist/contracts/deepbook_predict/registry.d.mts +28 -28
  49. package/dist/contracts/deepbook_predict/strike_exposure.mjs +1 -1
  50. package/dist/contracts/deepbook_predict/strike_payout_tree.mjs +17 -12
  51. package/dist/contracts/deepbook_predict/strike_payout_tree.mjs.map +1 -1
  52. package/dist/contracts/deepbook_predict/vault_events.d.mts +154 -106
  53. package/dist/contracts/deepbook_predict/vault_events.d.mts.map +1 -1
  54. package/dist/contracts/deepbook_predict/vault_events.mjs +57 -1
  55. package/dist/contracts/deepbook_predict/vault_events.mjs.map +1 -1
  56. package/dist/contracts/deepbook_predict_math/config-arguments.d.mts +10 -0
  57. package/dist/contracts/deepbook_predict_math/config-arguments.d.mts.map +1 -0
  58. package/dist/contracts/deepbook_predict_math/lazer_price.d.mts +106 -0
  59. package/dist/contracts/deepbook_predict_math/lazer_price.d.mts.map +1 -0
  60. package/dist/contracts/deepbook_predict_math/lazer_price.mjs +125 -0
  61. package/dist/contracts/deepbook_predict_math/lazer_price.mjs.map +1 -0
  62. package/dist/contracts/deepbook_predict_math/math.d.mts +284 -0
  63. package/dist/contracts/deepbook_predict_math/math.d.mts.map +1 -0
  64. package/dist/contracts/deepbook_predict_math/math.mjs +364 -0
  65. package/dist/contracts/deepbook_predict_math/math.mjs.map +1 -0
  66. package/dist/contracts/deepbook_predict_orders/config-arguments.d.mts +13 -0
  67. package/dist/contracts/deepbook_predict_orders/config-arguments.d.mts.map +1 -0
  68. package/dist/contracts/deepbook_predict_orders/delayed_execution_config.d.mts +215 -0
  69. package/dist/contracts/deepbook_predict_orders/delayed_execution_config.d.mts.map +1 -0
  70. package/dist/contracts/deepbook_predict_orders/delayed_execution_config.mjs +215 -0
  71. package/dist/contracts/deepbook_predict_orders/delayed_execution_config.mjs.map +1 -0
  72. package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/expiry_market.mjs +87 -0
  73. package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/expiry_market.mjs.map +1 -0
  74. package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/pricing.mjs +52 -0
  75. package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/pricing.mjs.map +1 -0
  76. package/dist/contracts/deepbook_predict_orders/deps/fixed_math/i64.mjs +25 -0
  77. package/dist/contracts/deepbook_predict_orders/deps/fixed_math/i64.mjs.map +1 -0
  78. package/dist/contracts/deepbook_predict_orders/deps/sui/balance.mjs +21 -0
  79. package/dist/contracts/deepbook_predict_orders/deps/sui/balance.mjs.map +1 -0
  80. package/dist/contracts/deepbook_predict_orders/deps/sui/table.mjs +39 -0
  81. package/dist/contracts/deepbook_predict_orders/deps/sui/table.mjs.map +1 -0
  82. package/dist/contracts/deepbook_predict_orders/desk.d.mts +187 -0
  83. package/dist/contracts/deepbook_predict_orders/desk.d.mts.map +1 -0
  84. package/dist/contracts/deepbook_predict_orders/desk.mjs +261 -0
  85. package/dist/contracts/deepbook_predict_orders/desk.mjs.map +1 -0
  86. package/dist/contracts/deepbook_predict_orders/order_queue.d.mts +1042 -0
  87. package/dist/contracts/deepbook_predict_orders/order_queue.d.mts.map +1 -0
  88. package/dist/contracts/deepbook_predict_orders/order_queue.mjs +849 -0
  89. package/dist/contracts/deepbook_predict_orders/order_queue.mjs.map +1 -0
  90. package/dist/contracts/deepbook_predict_orders/queue.d.mts +683 -0
  91. package/dist/contracts/deepbook_predict_orders/queue.d.mts.map +1 -0
  92. package/dist/contracts/deepbook_predict_orders/queue.mjs +912 -0
  93. package/dist/contracts/deepbook_predict_orders/queue.mjs.map +1 -0
  94. package/dist/contracts/deepbook_predict_orders/queue_events.d.mts +195 -0
  95. package/dist/contracts/deepbook_predict_orders/queue_events.d.mts.map +1 -0
  96. package/dist/contracts/deepbook_predict_orders/queue_events.mjs +187 -0
  97. package/dist/contracts/deepbook_predict_orders/queue_events.mjs.map +1 -0
  98. package/dist/contracts/deepbook_sessions/session_config.d.mts +8 -8
  99. package/dist/contracts/deepbook_sessions/sessions.d.mts +163 -28
  100. package/dist/contracts/deepbook_sessions/sessions.d.mts.map +1 -1
  101. package/dist/contracts/deepbook_sessions/sessions.mjs +234 -5
  102. package/dist/contracts/deepbook_sessions/sessions.mjs.map +1 -1
  103. package/dist/deployments/mainnet.mjs +7 -4
  104. package/dist/deployments/mainnet.mjs.map +1 -1
  105. package/dist/deployments/testnet.mjs +7 -4
  106. package/dist/deployments/testnet.mjs.map +1 -1
  107. package/dist/predict/bindings/expiry-market.d.mts +77 -0
  108. package/dist/predict/bindings/expiry-market.d.mts.map +1 -0
  109. package/dist/predict/bindings/expiry-market.mjs +139 -0
  110. package/dist/predict/bindings/expiry-market.mjs.map +1 -0
  111. package/dist/predict/bindings/protocol-config.d.mts +39 -0
  112. package/dist/predict/bindings/protocol-config.d.mts.map +1 -0
  113. package/dist/predict/bindings/protocol-config.mjs +90 -0
  114. package/dist/predict/bindings/protocol-config.mjs.map +1 -0
  115. package/dist/predict/bindings/rename.mjs +9 -0
  116. package/dist/predict/bindings/rename.mjs.map +1 -0
  117. package/dist/predict/client.d.mts +595 -16
  118. package/dist/predict/client.d.mts.map +1 -1
  119. package/dist/predict/client.mjs +986 -10
  120. package/dist/predict/client.mjs.map +1 -1
  121. package/dist/predict/config/generated.d.mts +22 -1
  122. package/dist/predict/config/generated.d.mts.map +1 -1
  123. package/dist/predict/config/generated.mjs +30 -1
  124. package/dist/predict/config/generated.mjs.map +1 -1
  125. package/dist/predict/config/mainnet.mjs +11 -3
  126. package/dist/predict/config/mainnet.mjs.map +1 -1
  127. package/dist/predict/config/testnet.mjs +11 -3
  128. package/dist/predict/config/testnet.mjs.map +1 -1
  129. package/dist/predict/config/types.d.mts +50 -1
  130. package/dist/predict/config/types.d.mts.map +1 -1
  131. package/dist/predict/cost.d.mts +38 -11
  132. package/dist/predict/cost.d.mts.map +1 -1
  133. package/dist/predict/cost.mjs +37 -11
  134. package/dist/predict/cost.mjs.map +1 -1
  135. package/dist/predict/decode.d.mts +251 -1
  136. package/dist/predict/decode.d.mts.map +1 -1
  137. package/dist/predict/decode.mjs +342 -4
  138. package/dist/predict/decode.mjs.map +1 -1
  139. package/dist/predict/errors.d.mts +69 -6
  140. package/dist/predict/errors.d.mts.map +1 -1
  141. package/dist/predict/errors.mjs +277 -4
  142. package/dist/predict/errors.mjs.map +1 -1
  143. package/dist/predict/index.d.mts +21 -10
  144. package/dist/predict/index.mjs +24 -12
  145. package/dist/predict/queue-id.d.mts +13 -0
  146. package/dist/predict/queue-id.d.mts.map +1 -0
  147. package/dist/predict/queue-id.mjs +19 -0
  148. package/dist/predict/queue-id.mjs.map +1 -0
  149. package/dist/predict/queue.d.mts +505 -0
  150. package/dist/predict/queue.d.mts.map +1 -0
  151. package/dist/predict/queue.mjs +774 -0
  152. package/dist/predict/queue.mjs.map +1 -0
  153. package/dist/predict/reads/balances.mjs +1 -1
  154. package/dist/predict/reads/markets.mjs +46 -5
  155. package/dist/predict/reads/markets.mjs.map +1 -1
  156. package/dist/predict/reads/pool.mjs +1 -1
  157. package/dist/predict/reads/pricing.d.mts +1 -1
  158. package/dist/predict/reads/queue.d.mts +105 -0
  159. package/dist/predict/reads/queue.d.mts.map +1 -0
  160. package/dist/predict/reads/queue.mjs +445 -0
  161. package/dist/predict/reads/queue.mjs.map +1 -0
  162. package/dist/predict/ticks.d.mts +9 -1
  163. package/dist/predict/ticks.d.mts.map +1 -1
  164. package/dist/predict/ticks.mjs +18 -1
  165. package/dist/predict/ticks.mjs.map +1 -1
  166. package/dist/predict/tx/authed.mjs +1 -1
  167. package/dist/predict/tx/common.d.mts.map +1 -1
  168. package/dist/predict/tx/common.mjs +12 -1
  169. package/dist/predict/tx/common.mjs.map +1 -1
  170. package/dist/predict/tx/queue.d.mts +220 -0
  171. package/dist/predict/tx/queue.d.mts.map +1 -0
  172. package/dist/predict/tx/queue.mjs +399 -0
  173. package/dist/predict/tx/queue.mjs.map +1 -0
  174. package/dist/predict/tx/trade.d.mts.map +1 -1
  175. package/dist/predict/tx/trade.mjs +61 -43
  176. package/dist/predict/tx/trade.mjs.map +1 -1
  177. package/dist/sessions.d.mts +90 -2
  178. package/dist/sessions.d.mts.map +1 -1
  179. package/dist/sessions.mjs +107 -2
  180. package/dist/sessions.mjs.map +1 -1
  181. package/dist/transactions/balanceManager.d.mts +12 -12
  182. package/dist/transactions/balanceManager.d.mts.map +1 -1
  183. package/dist/transactions/deepbook.d.mts +20 -20
  184. package/dist/transactions/deepbook.d.mts.map +1 -1
  185. package/dist/transactions/deepbookAdmin.d.mts +4 -4
  186. package/dist/transactions/deepbookAdmin.d.mts.map +1 -1
  187. package/dist/transactions/marginAdmin.d.mts +7 -7
  188. package/dist/transactions/marginAdmin.d.mts.map +1 -1
  189. package/dist/transactions/marginLiquidations.d.mts +3 -3
  190. package/dist/transactions/marginMaintainer.d.mts +5 -5
  191. package/dist/transactions/marginMaintainer.d.mts.map +1 -1
  192. package/dist/transactions/marginManager.d.mts +32 -32
  193. package/dist/transactions/marginManager.d.mts.map +1 -1
  194. package/dist/transactions/marginPool.d.mts +18 -18
  195. package/dist/transactions/marginRegistry.d.mts +16 -16
  196. package/dist/transactions/marginTPSL.d.mts +10 -10
  197. package/dist/transactions/marginTPSL.d.mts.map +1 -1
  198. package/dist/transactions/poolProxy.d.mts +8 -8
  199. package/dist/transactions/poolProxy.d.mts.map +1 -1
  200. package/package.json +4 -4
  201. package/src/contracts/deepbook_predict/config_events.ts +39 -0
  202. package/src/contracts/deepbook_predict/deps/std/type_name.ts +24 -0
  203. package/src/contracts/deepbook_predict/expiry_market.ts +960 -169
  204. package/src/contracts/deepbook_predict/order_events.ts +3 -1
  205. package/src/contracts/deepbook_predict/plp.ts +150 -9
  206. package/src/contracts/deepbook_predict/pricing.ts +46 -7
  207. package/src/contracts/deepbook_predict/pricing_config.ts +3 -1
  208. package/src/contracts/deepbook_predict/protocol_config.ts +648 -33
  209. package/src/contracts/deepbook_predict/strike_payout_tree.ts +25 -21
  210. package/src/contracts/deepbook_predict/vault_events.ts +65 -0
  211. package/src/contracts/deepbook_predict_math/config-arguments.ts +6 -0
  212. package/src/contracts/deepbook_predict_math/lazer_price.ts +196 -0
  213. package/src/contracts/deepbook_predict_math/math.ts +553 -0
  214. package/src/contracts/deepbook_predict_orders/config-arguments.ts +11 -0
  215. package/src/contracts/deepbook_predict_orders/delayed_execution_config.ts +430 -0
  216. package/src/contracts/deepbook_predict_orders/deps/deepbook_predict/expiry_market.ts +108 -0
  217. package/src/contracts/deepbook_predict_orders/deps/deepbook_predict/pricing.ts +58 -0
  218. package/src/contracts/deepbook_predict_orders/deps/fixed_math/i64.ts +21 -0
  219. package/src/contracts/deepbook_predict_orders/deps/sui/balance.ts +19 -0
  220. package/src/contracts/deepbook_predict_orders/deps/sui/table.ts +37 -0
  221. package/src/contracts/deepbook_predict_orders/desk.ts +419 -0
  222. package/src/contracts/deepbook_predict_orders/order_flow.ts +19 -0
  223. package/src/contracts/deepbook_predict_orders/order_queue.ts +1670 -0
  224. package/src/contracts/deepbook_predict_orders/queue.ts +1394 -0
  225. package/src/contracts/deepbook_predict_orders/queue_events.ts +189 -0
  226. package/src/contracts/deepbook_sessions/sessions.ts +355 -4
  227. package/src/deployments/mainnet.ts +7 -2
  228. package/src/deployments/testnet.ts +7 -2
  229. package/src/deployments/types.ts +14 -1
  230. package/src/predict/bindings/expiry-market.ts +127 -0
  231. package/src/predict/bindings/protocol-config.ts +59 -0
  232. package/src/predict/bindings/rename.ts +14 -0
  233. package/src/predict/client.ts +2076 -13
  234. package/src/predict/config/generated.ts +50 -0
  235. package/src/predict/config/mainnet.ts +16 -2
  236. package/src/predict/config/testnet.ts +16 -2
  237. package/src/predict/config/types.ts +51 -1
  238. package/src/predict/cost.ts +62 -14
  239. package/src/predict/decode.ts +691 -2
  240. package/src/predict/errors.ts +391 -14
  241. package/src/predict/index.ts +87 -6
  242. package/src/predict/queue-id.ts +22 -0
  243. package/src/predict/queue.ts +1231 -0
  244. package/src/predict/reads/markets.ts +30 -0
  245. package/src/predict/reads/queue.ts +606 -0
  246. package/src/predict/ticks.ts +35 -0
  247. package/src/predict/tx/common.ts +25 -1
  248. package/src/predict/tx/queue.ts +525 -0
  249. package/src/predict/tx/trade.ts +80 -49
  250. package/src/sessions.ts +206 -1
@@ -0,0 +1,1231 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Delayed execution (DBU-885): the codes, cash-need math, timing previews and order-state views
4
+ // for queued orders. Pure functions over raw bigints, no network. Exported as the `queue`
5
+ // namespace, like `cost` and `pricing`.
6
+ //
7
+ // A queued order is placed now and priced later at Pyth's signed price for its τ, a channel
8
+ // tick shortly after placement. A keeper (or anyone, see `fill` in `tx/queue.ts`) commits the
9
+ // price and resolves the order: a fill, or a refund with a reason code. A filled mint stays in
10
+ // its market's queue as an Open record until `enqueue_redeem_open` sells it or the queue's
11
+ // settlement walk (`settle_step`) pays it.
12
+ //
13
+ // The queue lives in the order-flow companion, `deepbook_predict_orders`. The code values mirror
14
+ // its `order_queue` getters, and the cash-need formulas are 1:1 ports (rounding included) of
15
+ // `deepbook_predict_math::math::need_*`, which Predict's admission charges. Probabilities and λ
16
+ // are 1e9-scaled.
17
+
18
+ import type { DelayedExecutionPolicy as DelayedExecutionPolicyBcs } from '../contracts/deepbook_predict_orders/delayed_execution_config.js';
19
+ import type { OrderView as OrderViewBcs } from '../contracts/deepbook_predict_orders/order_queue.js';
20
+ import { FLOAT_SCALING, MAX_QUANTITY_LOTS, POSITION_LOT_SIZE, decodeOrderRange } from './cost.js';
21
+ import type { QueueEvent } from './decode.js';
22
+ import { PredictInputError } from './errors.js';
23
+ import { U64_MAX } from './units.js';
24
+
25
+ // === Codes ===
26
+
27
+ /**
28
+ * Predict's `constants::current_version` from the upgrade that added delayed execution. Enqueue
29
+ * works once `ProtocolConfig.version_watermark` reaches it (Predict's admission aborts
30
+ * `ECutoverNotReached` until then). The retired immediate trades (`mint_exact_*`, `redeem_live`)
31
+ * always abort in that package, and the older packages they still run in are retired by the same
32
+ * bump. The same on both networks: it is the code's version constant, not the on-chain package
33
+ * version (Mainnet v4, Testnet v5).
34
+ */
35
+ export const DELAYED_EXECUTION_VERSION = 4n;
36
+
37
+ /**
38
+ * Predict's `constants::current_version` of the package these bindings were generated from. Every
39
+ * version-gated Predict call checks `chk_version`, which aborts `EPackageVersionDisabled` once
40
+ * `ProtocolConfig.version_watermark` is above it, so a higher watermark retires the Predict code
41
+ * this SDK calls. Bump it with Predict when regenerating.
42
+ */
43
+ export const PREDICT_PACKAGE_VERSION = 4n;
44
+
45
+ /**
46
+ * `desk::current_version` of the order-flow package these bindings were generated from. The desk's
47
+ * version floor retires older companion code, so a floor above it means the package this SDK calls
48
+ * aborts `EPackageVersionDisabled`. Bump it with the companion when regenerating.
49
+ */
50
+ export const ORDER_FLOW_PACKAGE_VERSION = 1n;
51
+
52
+ /** `queue::phase_*`: what `settle_step` returns, the phase the next call runs. */
53
+ export const SETTLE_PHASE = Object.freeze({
54
+ /** Refunding the waiting orders (reason 5). */
55
+ DRAIN: 0,
56
+ /** Paying the Open records, once Predict has settled the market. */
57
+ PAY: 1,
58
+ /**
59
+ * The payout walk reached the last record and `MarketPayoutsCompleted` was emitted. A record
60
+ * the market couldn't pay (`OpenRecordPayoutSkipped`) stays Open and unpaid.
61
+ */
62
+ DONE: 2,
63
+ } as const);
64
+
65
+ /** `order_queue::status_*`. Never renumbered on chain; unknown codes still decode. */
66
+ export const ORDER_STATUS = Object.freeze({
67
+ PENDING: 0,
68
+ COMMITTED: 1,
69
+ OPEN: 2,
70
+ REFUNDED: 3,
71
+ CLOSED: 4,
72
+ /** Reserved: nothing sets it at launch. Counts as unfinished. */
73
+ REFUND_DUE: 5,
74
+ } as const);
75
+
76
+ /** `order_queue::kind_*`. `REDEEM_LIVE` is reserved and never used by v4. */
77
+ export const ORDER_KIND = Object.freeze({
78
+ EXACT_QUANTITY: 0,
79
+ EXACT_AMOUNT: 1,
80
+ EXACT_COST: 2,
81
+ REDEEM_LIVE: 3,
82
+ REDEEM_OPEN: 4,
83
+ } as const);
84
+
85
+ /** `order_queue::reason_*`. `0` in a record's result means filled. */
86
+ export const REFUND_REASON = Object.freeze({
87
+ LIMITS: 1,
88
+ ADMISSION: 2,
89
+ NO_PRICE: 3,
90
+ MISSING_NODE: 4,
91
+ DEADLINE: 5,
92
+ FREEZE: 6,
93
+ ADMIN: 7,
94
+ NO_CASH: 8,
95
+ RECIPIENT_DENIED: 9,
96
+ } as const);
97
+
98
+ /** The Pyth Lazer channels the queue prices on (`delayed_execution_config`). */
99
+ export const PYTH_CHANNEL = Object.freeze({
100
+ FIXED_RATE_50MS: 2,
101
+ FIXED_RATE_200MS: 3,
102
+ } as const);
103
+
104
+ /** Predict's `constants::deadline_expiry_margin_ms`: every deadline is at least this long before expiry. */
105
+ export const DEADLINE_EXPIRY_MARGIN_MS = 5_000n;
106
+
107
+ /**
108
+ * How long after an order's deadline the app waits before offering "Refund my order". Keepers
109
+ * refund overdue orders on their own, so the button only matters when they are all down.
110
+ */
111
+ export const REFUND_REQUEST_GRACE_MS = 5_000n;
112
+
113
+ export type OrderStatusName =
114
+ 'pending' | 'committed' | 'open' | 'refunded' | 'closed' | 'refund-due' | 'unknown';
115
+
116
+ export type OrderKindName =
117
+ 'exact-quantity' | 'exact-amount' | 'exact-cost' | 'redeem-live' | 'redeem-open' | 'unknown';
118
+
119
+ const STATUS_NAMES: readonly OrderStatusName[] = [
120
+ 'pending',
121
+ 'committed',
122
+ 'open',
123
+ 'refunded',
124
+ 'closed',
125
+ 'refund-due',
126
+ ];
127
+ const KIND_NAMES: readonly OrderKindName[] = [
128
+ 'exact-quantity',
129
+ 'exact-amount',
130
+ 'exact-cost',
131
+ 'redeem-live',
132
+ 'redeem-open',
133
+ ];
134
+
135
+ /** A status code's name, or `'unknown'` for a code this SDK version predates. */
136
+ export function orderStatusName(code: number): OrderStatusName {
137
+ return STATUS_NAMES[code] ?? 'unknown';
138
+ }
139
+
140
+ /** A kind code's name, or `'unknown'` for a code this SDK version predates. */
141
+ export function orderKindName(code: number): OrderKindName {
142
+ return KIND_NAMES[code] ?? 'unknown';
143
+ }
144
+
145
+ /** Whether a kind code is a queued mint (exact quantity, exact amount or exact cost). */
146
+ export function isMintKind(kind: number): boolean {
147
+ return (
148
+ kind === ORDER_KIND.EXACT_QUANTITY ||
149
+ kind === ORDER_KIND.EXACT_AMOUNT ||
150
+ kind === ORDER_KIND.EXACT_COST
151
+ );
152
+ }
153
+
154
+ /** Whether a kind code is a queued sell. */
155
+ export function isSellKind(kind: number): boolean {
156
+ return kind === ORDER_KIND.REDEEM_OPEN || kind === ORDER_KIND.REDEEM_LIVE;
157
+ }
158
+
159
+ /** What a refund reason means for the trader. */
160
+ export interface RefundReasonInfo {
161
+ code: number;
162
+ key:
163
+ | 'limits'
164
+ | 'admission'
165
+ | 'no-price'
166
+ | 'missing-node'
167
+ | 'deadline'
168
+ | 'freeze'
169
+ | 'admin'
170
+ | 'no-cash'
171
+ | 'recipient-denied'
172
+ | 'unknown';
173
+ /** Display text for the refund. */
174
+ text: string;
175
+ /** Whether the order fee was kept (reasons 1 and 2). `null` for an unknown code. */
176
+ feeKept: boolean | null;
177
+ /** False for codes v4 reserves but never sets (3 and 6), and for unknown codes. */
178
+ live: boolean;
179
+ }
180
+
181
+ /** Every reason code v4 defines, with its text and fee treatment. */
182
+ export const REFUND_REASONS: Readonly<Record<number, RefundReasonInfo>> = Object.freeze({
183
+ 1: {
184
+ code: 1,
185
+ key: 'limits',
186
+ text: "Your price limits weren't met at the fill price. Refunded, the order fee is kept.",
187
+ feeKept: true,
188
+ live: true,
189
+ },
190
+ 2: {
191
+ code: 2,
192
+ key: 'admission',
193
+ text: "The market couldn't accept the order at the fill price. Refunded, the order fee is kept.",
194
+ feeKept: true,
195
+ live: true,
196
+ },
197
+ 3: {
198
+ code: 3,
199
+ key: 'no-price',
200
+ text: 'No price arrived for the order. Refunded in full.',
201
+ feeKept: false,
202
+ live: false,
203
+ },
204
+ 4: {
205
+ code: 4,
206
+ key: 'missing-node',
207
+ text: "The market couldn't place the order. Refunded in full.",
208
+ feeKept: false,
209
+ live: true,
210
+ },
211
+ 5: {
212
+ code: 5,
213
+ key: 'deadline',
214
+ text: "The order wasn't priced before its deadline. Refunded in full.",
215
+ feeKept: false,
216
+ live: true,
217
+ },
218
+ 6: {
219
+ code: 6,
220
+ key: 'freeze',
221
+ text: 'The protocol was frozen. Refunded in full.',
222
+ feeKept: false,
223
+ live: false,
224
+ },
225
+ 7: { code: 7, key: 'admin', text: 'Refunded in full by an admin.', feeKept: false, live: true },
226
+ 8: {
227
+ code: 8,
228
+ key: 'no-cash',
229
+ text: "The market couldn't pay right now. Refunded in full.",
230
+ feeKept: false,
231
+ live: true,
232
+ },
233
+ 9: {
234
+ code: 9,
235
+ key: 'recipient-denied',
236
+ text: "The account's receive address can't take USDC right now (it is on USDC's deny list, or USDC is paused). Refunded in full, and the refund waits in the order until it can be claimed.",
237
+ feeKept: false,
238
+ live: true,
239
+ },
240
+ });
241
+
242
+ /** A reason code's meaning. Unknown codes map to `key: 'unknown'` rather than throwing. */
243
+ export function refundReason(code: number): RefundReasonInfo {
244
+ return (
245
+ REFUND_REASONS[code] ?? {
246
+ code,
247
+ key: 'unknown',
248
+ text: `Refunded (code ${code}).`,
249
+ feeKept: null,
250
+ live: false,
251
+ }
252
+ );
253
+ }
254
+
255
+ // === Policy ===
256
+
257
+ /**
258
+ * `delayed_execution_config::DelayedExecutionPolicy` with camelCase keys, as the companion's
259
+ * `OrderDesk` holds it (`desk::policy`). Times in ms, USDC raw.
260
+ */
261
+ export interface DelayedExecutionPolicy {
262
+ delayMs: bigint;
263
+ stallTimeoutMs: bigint;
264
+ stuckThresholdMs: bigint;
265
+ gapWaitMs: bigint;
266
+ pythPriceBufferMs: bigint;
267
+ /** {@link PYTH_CHANNEL}: `2` is 50 ms, `3` is 200 ms. */
268
+ pythChannel: number;
269
+ sviMaxAgeMs: bigint;
270
+ mintCapacity: bigint;
271
+ sellCapacity: bigint;
272
+ perAccountCap: bigint;
273
+ /** Flat USDC fee charged at enqueue, raw. */
274
+ orderFee: bigint;
275
+ minSellQuantity: bigint;
276
+ settleRefundBatch: bigint;
277
+ settlePayoutBatch: bigint;
278
+ }
279
+
280
+ /** Map the generated policy layout to {@link DelayedExecutionPolicy}. */
281
+ export function policyFromBcs(
282
+ p: (typeof DelayedExecutionPolicyBcs)['$inferType'],
283
+ ): DelayedExecutionPolicy {
284
+ return {
285
+ delayMs: p.delay_ms,
286
+ stallTimeoutMs: p.stall_timeout_ms,
287
+ stuckThresholdMs: p.stuck_threshold_ms,
288
+ gapWaitMs: p.gap_wait_ms,
289
+ pythPriceBufferMs: p.pyth_price_buffer_ms,
290
+ pythChannel: p.pyth_channel,
291
+ sviMaxAgeMs: p.svi_max_age_ms,
292
+ mintCapacity: p.mint_capacity,
293
+ sellCapacity: p.sell_capacity,
294
+ perAccountCap: p.per_account_cap,
295
+ orderFee: p.order_fee,
296
+ minSellQuantity: p.min_sell_quantity,
297
+ settleRefundBatch: p.settle_refund_batch,
298
+ settlePayoutBatch: p.settle_payout_batch,
299
+ };
300
+ }
301
+
302
+ /** `delayed_execution_config::channel_tick_ms`: 50 ms for channel 2, 200 ms for channel 3. */
303
+ export function channelTickMs(channel: number): bigint {
304
+ if (channel === PYTH_CHANNEL.FIXED_RATE_50MS) return 50n;
305
+ if (channel === PYTH_CHANNEL.FIXED_RATE_200MS) return 200n;
306
+ throw new PredictInputError(`unsupported Pyth Lazer channel ${channel}`);
307
+ }
308
+
309
+ // === Cash need (`deepbook_predict_math::math::need_*`) ===
310
+
311
+ function assertUint(value: bigint, name: string, max: bigint = U64_MAX): void {
312
+ if (typeof value !== 'bigint' || value < 0n || value > max) {
313
+ throw new PredictInputError(`${name} must be a bigint in [0, ${max}], got ${String(value)}`);
314
+ }
315
+ }
316
+
317
+ // `math::mul_div_up`, including its abort on a zero denominator and a u64 overflow.
318
+ function mulDivUp(x: bigint, y: bigint, denominator: bigint): bigint {
319
+ if (denominator === 0n) throw new PredictInputError('division by zero (EInputZero)');
320
+ return (x * y + denominator - 1n) / denominator;
321
+ }
322
+
323
+ function plusOne(need: bigint): bigint {
324
+ if (need >= U64_MAX) throw new PredictInputError('cash need overflows u64');
325
+ return need + 1n;
326
+ }
327
+
328
+ /** Exact-quantity mint cash need: `⌈quantity · (1 − p_min)⌉ + 1`. */
329
+ export function cashNeedExactQuantity(quantity: bigint, minEntryProbability: bigint): bigint {
330
+ assertUint(quantity, 'quantity');
331
+ assertUint(minEntryProbability, 'minEntryProbability', FLOAT_SCALING);
332
+ return plusOne(mulDivUp(quantity, FLOAT_SCALING - minEntryProbability, FLOAT_SCALING));
333
+ }
334
+
335
+ /**
336
+ * Budget mint cash need: `⌈(budget + 1) · (1 / p_min − 1)⌉ + 1`, rounded as Move rounds it (one
337
+ * `mul_div_up` of `(budget + 1) · (1e9 − p_min) / p_min`). Exact-amount mints pass
338
+ * `min(max_premium, budget)`. `p_min = 0` throws, as the Move division aborts.
339
+ */
340
+ export function cashNeedBudget(budget: bigint, minEntryProbability: bigint): bigint {
341
+ assertUint(budget, 'budget', U64_MAX - 1n);
342
+ assertUint(minEntryProbability, 'minEntryProbability', FLOAT_SCALING);
343
+ return plusOne(mulDivUp(budget + 1n, FLOAT_SCALING - minEntryProbability, minEntryProbability));
344
+ }
345
+
346
+ /** Sell cash need: `⌈close_quantity · (1 − λ)⌉ + 1`. */
347
+ export function cashNeedSell(closeQuantity: bigint, backingBufferLambda: bigint): bigint {
348
+ assertUint(closeQuantity, 'closeQuantity');
349
+ assertUint(backingBufferLambda, 'backingBufferLambda', FLOAT_SCALING);
350
+ return plusOne(mulDivUp(closeQuantity, FLOAT_SCALING - backingBufferLambda, FLOAT_SCALING));
351
+ }
352
+
353
+ /**
354
+ * Whether the account can pay the order fee: a mint needs `available > fee` (it escrows a budget
355
+ * on top), a sell needs `available ≥ fee` (it escrows only the fee). `available` is
356
+ * `account::balance<USDC>`, unsettled accumulator funds included.
357
+ */
358
+ export function feeCovered(
359
+ side: 'mint' | 'sell',
360
+ availableRaw: bigint,
361
+ orderFeeRaw: bigint,
362
+ ): boolean {
363
+ return side === 'mint' ? availableRaw > orderFeeRaw : availableRaw >= orderFeeRaw;
364
+ }
365
+
366
+ /** Inputs for {@link mintBudget}. */
367
+ export interface MintBudgetInputs {
368
+ /** {@link ORDER_KIND}: a mint kind. */
369
+ kind: number;
370
+ maxCostRaw: bigint;
371
+ availableRaw: bigint;
372
+ orderFeeRaw: bigint;
373
+ /** Required for exact quantity: a fill never costs more than its quantity. */
374
+ quantityRaw?: bigint;
375
+ }
376
+
377
+ /**
378
+ * The budget a queued mint escrows: `min(max_cost, available − fee)`, and for exact quantity also
379
+ * at most the quantity. `null` when the balance doesn't cover the fee. Enqueue debits the budget
380
+ * plus the order fee, and the unspent budget comes back at the fill.
381
+ */
382
+ export function mintBudget(inputs: MintBudgetInputs): bigint | null {
383
+ if (!feeCovered('mint', inputs.availableRaw, inputs.orderFeeRaw)) return null;
384
+ const budget = min(inputs.maxCostRaw, inputs.availableRaw - inputs.orderFeeRaw);
385
+ if (inputs.kind !== ORDER_KIND.EXACT_QUANTITY) return budget;
386
+ if (inputs.quantityRaw == null) {
387
+ throw new PredictInputError('quantityRaw is required for an exact-quantity budget');
388
+ }
389
+ return min(budget, inputs.quantityRaw);
390
+ }
391
+
392
+ /** Inputs for {@link mintCashNeed}. */
393
+ export interface MintCashNeedInputs {
394
+ kind: number;
395
+ /** The escrowed budget, from {@link mintBudget}. */
396
+ budgetRaw: bigint;
397
+ minEntryProbability: bigint;
398
+ /** Exact quantity: the quantity ordered. */
399
+ quantityRaw?: bigint;
400
+ /** Exact amount: the premium cap. */
401
+ maxPremiumRaw?: bigint;
402
+ }
403
+
404
+ /** A queued mint's cash need, picking the formula the contract picks for its kind. */
405
+ export function mintCashNeed(inputs: MintCashNeedInputs): bigint {
406
+ const p = inputs.minEntryProbability;
407
+ switch (inputs.kind) {
408
+ case ORDER_KIND.EXACT_QUANTITY:
409
+ if (inputs.quantityRaw == null) throw new PredictInputError('quantityRaw is required');
410
+ return cashNeedExactQuantity(inputs.quantityRaw, p);
411
+ case ORDER_KIND.EXACT_AMOUNT:
412
+ if (inputs.maxPremiumRaw == null) throw new PredictInputError('maxPremiumRaw is required');
413
+ return cashNeedBudget(min(inputs.maxPremiumRaw, inputs.budgetRaw), p);
414
+ case ORDER_KIND.EXACT_COST:
415
+ return cashNeedBudget(inputs.budgetRaw, p);
416
+ default:
417
+ throw new PredictInputError(`kind ${inputs.kind} is not a mint`);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * A sell's cash need against the market's spare cash. Sells are never refused for market cash at
423
+ * placement: the keeper funds the market before τ, and a sell the market still can't cover at the
424
+ * fill is refunded in full (reason 8). `short` flags that risk, and the sell builder can add
425
+ * `rebalance_expiry_cash` after the enqueue to fund the market at once.
426
+ */
427
+ export function sellCashCheck(
428
+ closeQuantityRaw: bigint,
429
+ spareCashRaw: bigint,
430
+ backingBufferLambda: bigint,
431
+ ): { needRaw: bigint; short: boolean } {
432
+ const needRaw = cashNeedSell(closeQuantityRaw, backingBufferLambda);
433
+ return { needRaw, short: needRaw > spareCashRaw };
434
+ }
435
+
436
+ // === Largest mint the market takes now ===
437
+
438
+ /** Inputs for {@link maxMintNow}. */
439
+ export interface MaxMintInputs {
440
+ /** `'exact-quantity'` sizes a quantity; `'budget'` sizes an exact-cost or exact-amount spend. */
441
+ shape: 'exact-quantity' | 'budget';
442
+ /** Market cash above required cash (`MarketQueueState.spareCash`). */
443
+ spareCashRaw: bigint;
444
+ /** The minimum entry probability from `expiry_market::order_flow_state`, 1e9-scaled. */
445
+ minEntryProbability: bigint;
446
+ /** `account::balance<USDC>`. Bounds a budget spend at `available − fee`. */
447
+ availableRaw?: bigint;
448
+ orderFeeRaw?: bigint;
449
+ /** The deployment's `position_lot_size`. Defaults to `10_000n`. */
450
+ lotSize?: bigint;
451
+ /** Margin left below spare cash, in basis points. Default 100 (1%). */
452
+ headroomBps?: bigint;
453
+ /** When the reads were taken, echoed back as `asOfMs`. */
454
+ asOfMs?: bigint;
455
+ }
456
+
457
+ /** The largest mint a market takes right now, and what binds it. A snapshot: see {@link maxMintNow}. */
458
+ export interface MaxMintNow {
459
+ /**
460
+ * The cash-limited maximum: a quantity for `'exact-quantity'`, a spend for `'budget'`. `null`
461
+ * when cash doesn't bind (`p_min ≥ 1`).
462
+ */
463
+ cashLimitRaw: bigint | null;
464
+ /** `available − fee` for `'budget'`, when a balance was given. `null` otherwise. */
465
+ balanceLimitRaw: bigint | null;
466
+ /** The smaller of the two limits, or `null` when neither binds. */
467
+ maxRaw: bigint | null;
468
+ limitedBy: 'cash' | 'balance' | 'none';
469
+ asOfMs: bigint | null;
470
+ }
471
+
472
+ /**
473
+ * The largest queued mint a market's spare cash admits now ("Max right now"). Predict's admission
474
+ * refuses a mint whose cash need is above spare cash (`EInsufficientMarketCash`). For exact quantity this
475
+ * is the largest lot multiple `q` with `cashNeedExactQuantity(q) ≤ S`; for a budget it is the
476
+ * largest `b` with `cashNeedBudget(b) ≤ S`, where `S` is spare cash less the headroom.
477
+ *
478
+ * It is a snapshot: fills lower spare cash, and the keeper's rebalance after each enqueue raises
479
+ * it. Other waiting orders don't count against a mint. For exact quantity this caps only the
480
+ * cash side: size the balance side with `cost.mintCostForBudget` at `available − fee`. A budget
481
+ * mint with `p_min = 0` can't be placed at all (the cash need divides by `p_min`), so it reads 0.
482
+ */
483
+ export function maxMintNow(inputs: MaxMintInputs): MaxMintNow {
484
+ const p = inputs.minEntryProbability;
485
+ assertUint(p, 'minEntryProbability', FLOAT_SCALING);
486
+ assertUint(inputs.spareCashRaw, 'spareCashRaw');
487
+ const headroom = inputs.headroomBps ?? 100n;
488
+ if (headroom < 0n || headroom > 10_000n) {
489
+ throw new PredictInputError(`headroomBps must be in [0, 10000], got ${headroom}`);
490
+ }
491
+ const spare = inputs.spareCashRaw - (inputs.spareCashRaw * headroom) / 10_000n;
492
+ const asOfMs = inputs.asOfMs ?? null;
493
+
494
+ let cashLimitRaw: bigint | null;
495
+ let balanceLimitRaw: bigint | null = null;
496
+ if (inputs.shape === 'exact-quantity') {
497
+ const lot = inputs.lotSize ?? POSITION_LOT_SIZE;
498
+ if (p >= FLOAT_SCALING) {
499
+ cashLimitRaw = null;
500
+ } else if (spare < 2n) {
501
+ cashLimitRaw = 0n;
502
+ } else {
503
+ const cap = MAX_QUANTITY_LOTS * lot;
504
+ let q = min(((spare - 1n) * FLOAT_SCALING) / (FLOAT_SCALING - p), cap);
505
+ q -= q % lot;
506
+ while (q > 0n && cashNeedExactQuantity(q, p) > spare) q -= lot;
507
+ cashLimitRaw = q;
508
+ }
509
+ } else {
510
+ if (p === 0n) {
511
+ cashLimitRaw = 0n;
512
+ } else if (p >= FLOAT_SCALING) {
513
+ cashLimitRaw = null;
514
+ } else if (spare < 2n) {
515
+ cashLimitRaw = 0n;
516
+ } else {
517
+ let b = ((spare - 1n) * p) / (FLOAT_SCALING - p) - 1n;
518
+ while (b > 0n && cashNeedBudget(b, p) > spare) b -= 1n;
519
+ cashLimitRaw = b > 0n ? b : 0n;
520
+ }
521
+ if (inputs.availableRaw != null) {
522
+ const fee = inputs.orderFeeRaw ?? 0n;
523
+ balanceLimitRaw = inputs.availableRaw > fee ? inputs.availableRaw - fee : 0n;
524
+ }
525
+ }
526
+
527
+ if (cashLimitRaw == null && balanceLimitRaw == null) {
528
+ return { cashLimitRaw, balanceLimitRaw, maxRaw: null, limitedBy: 'none', asOfMs };
529
+ }
530
+ if (balanceLimitRaw == null || (cashLimitRaw != null && cashLimitRaw <= balanceLimitRaw)) {
531
+ return { cashLimitRaw, balanceLimitRaw, maxRaw: cashLimitRaw, limitedBy: 'cash', asOfMs };
532
+ }
533
+ return { cashLimitRaw, balanceLimitRaw, maxRaw: balanceLimitRaw, limitedBy: 'balance', asOfMs };
534
+ }
535
+
536
+ // === Timing ===
537
+
538
+ /** `(resolve_head, next_id, last_tau_ms, last_committed_tau_ms)` from `queue::queue_heads`. */
539
+ export interface QueueHeads {
540
+ resolveHead: bigint;
541
+ nextId: bigint;
542
+ lastTauMs: bigint;
543
+ lastCommittedTauMs: bigint;
544
+ }
545
+
546
+ /**
547
+ * `expiry − max(no_trade_window_ms, stall_timeout_ms + 5_000)`, saturating at 0: enqueue requires
548
+ * the order's τ below it.
549
+ */
550
+ export function orderCutoffMs(
551
+ expiryMs: bigint,
552
+ policy: Pick<DelayedExecutionPolicy, 'stallTimeoutMs'>,
553
+ noTradeWindowMs: bigint,
554
+ ): bigint {
555
+ const margin = max(noTradeWindowMs, policy.stallTimeoutMs + DEADLINE_EXPIRY_MARGIN_MS);
556
+ return expiryMs > margin ? expiryMs - margin : 0n;
557
+ }
558
+
559
+ /** Inputs for {@link previewTiming}. */
560
+ export interface TimingPreviewInputs {
561
+ nowMs: bigint;
562
+ policy: Pick<DelayedExecutionPolicy, 'delayMs' | 'stallTimeoutMs' | 'pythChannel'>;
563
+ heads: QueueHeads;
564
+ expiryMs: bigint;
565
+ noTradeWindowMs: bigint;
566
+ /**
567
+ * The newest order's channel, when known. A channel change pushes τ past the last τ, so one
568
+ * cohort never mixes channels. The public reads don't expose it, so it is optional.
569
+ */
570
+ lastChannel?: number;
571
+ }
572
+
573
+ /** A display preview of the τ, deadline and cutoff an order placed now would get. */
574
+ export interface TimingPreview {
575
+ tauMs: bigint;
576
+ /** `min(τ + stall, expiry)`. The chain also keeps deadlines non-decreasing. */
577
+ deadlineMs: bigint;
578
+ cutoffMs: bigint;
579
+ tickMs: bigint;
580
+ /** Whether τ is before the cutoff, so enqueue would accept the timing. */
581
+ beforeCutoff: boolean;
582
+ }
583
+
584
+ /**
585
+ * Preview `order_queue::plan_timing`: τ is the policy channel's last tick at or before
586
+ * `now + delay`, never below the last τ, and pushed past the last committed τ. Display only:
587
+ * the contract uses the consensus timestamp, which can differ from the local clock.
588
+ */
589
+ export function previewTiming(inputs: TimingPreviewInputs): TimingPreview {
590
+ const { policy, heads } = inputs;
591
+ const tickMs = channelTickMs(policy.pythChannel);
592
+ const rounded = ((inputs.nowMs + policy.delayMs) / tickMs) * tickMs;
593
+ let tauMs = max(rounded, heads.lastTauMs);
594
+ if (
595
+ inputs.lastChannel != null &&
596
+ heads.nextId > 0n &&
597
+ inputs.lastChannel !== policy.pythChannel
598
+ ) {
599
+ tauMs = max(rounded, nextTickAfter(heads.lastTauMs, tickMs));
600
+ }
601
+ if (tauMs <= heads.lastCommittedTauMs) tauMs = nextTickAfter(heads.lastCommittedTauMs, tickMs);
602
+ const deadlineMs = min(tauMs + policy.stallTimeoutMs, inputs.expiryMs);
603
+ const cutoffMs = orderCutoffMs(inputs.expiryMs, policy, inputs.noTradeWindowMs);
604
+ return { tauMs, deadlineMs, cutoffMs, tickMs, beforeCutoff: tauMs < cutoffMs };
605
+ }
606
+
607
+ function nextTickAfter(timeMs: bigint, tickMs: bigint): bigint {
608
+ return (timeMs / tickMs + 1n) * tickMs;
609
+ }
610
+
611
+ // === Slippage ===
612
+
613
+ /** Inputs for {@link slippageBand}. All times in ms. */
614
+ export interface SlippageBandInputs {
615
+ /** The quoted probability, in (0, 1). */
616
+ probability: number;
617
+ timeToExpiryMs: number;
618
+ /** The policy's delay. */
619
+ delayMs: number;
620
+ /** The channel tick: 50 or 200. */
621
+ tickMs: number;
622
+ /** Time between the quote and the enqueue landing. Default 500. */
623
+ signingMs?: number;
624
+ /** Time between τ and the fill. Default 500. */
625
+ resolveMs?: number;
626
+ /** Standard deviations of movement to allow. Default 2. */
627
+ k?: number;
628
+ }
629
+
630
+ /**
631
+ * A heuristic band for a queued order's price limits: `Δp ≈ k · φ(Φ⁻¹(p)) · √(h / T)`, the
632
+ * probability move of a digital under Brownian motion over the horizon `h` (delay + tick +
633
+ * resolve + signing), scaled by the time `T` left. Use `maxProbability = p + Δp` on exact-quantity
634
+ * mints and `minProbability = p − Δp` on sells, and size cost caps at the shifted probability.
635
+ * A sizing aid pending product sign-off, not a contract rule.
636
+ */
637
+ export function slippageBand(inputs: SlippageBandInputs): {
638
+ horizonMs: number;
639
+ deltaProbability: number;
640
+ maxProbability: number;
641
+ minProbability: number;
642
+ } {
643
+ const p = inputs.probability;
644
+ if (!(p > 0 && p < 1)) {
645
+ throw new PredictInputError(`probability must be in (0, 1), got ${p}`);
646
+ }
647
+ const horizonMs =
648
+ inputs.delayMs + inputs.tickMs + (inputs.resolveMs ?? 500) + (inputs.signingMs ?? 500);
649
+ const k = inputs.k ?? 2;
650
+ const delta =
651
+ inputs.timeToExpiryMs <= horizonMs
652
+ ? 1
653
+ : Math.min(
654
+ 1,
655
+ k * normalPdf(inverseNormalCdf(p)) * Math.sqrt(horizonMs / inputs.timeToExpiryMs),
656
+ );
657
+ return {
658
+ horizonMs,
659
+ deltaProbability: delta,
660
+ maxProbability: Math.min(1, p + delta),
661
+ minProbability: Math.max(0, p - delta),
662
+ };
663
+ }
664
+
665
+ // === Order limits from a quote ===
666
+ //
667
+ // A queued order prices at its τ, after the quote. Its limits bound how far the price may move
668
+ // against it in the meantime. `slippageRaw` is that move in price per contract, an absolute amount
669
+ // and never a percentage: a contract pays $1, so 10¢ is `100_000_000n` (1e9-scaled). Each helper
670
+ // turns a quote at the current price into the limits the enqueue takes.
671
+
672
+ /** The quote a set of mint limits is sized from: the chain quote at the current price. */
673
+ export interface MintLimitsInputs {
674
+ /** The quote's all-in cost after its fee subsidy, order fee excluded. */
675
+ quoteCostRaw: bigint;
676
+ /** The quote's payout quantity. */
677
+ quoteQuantityRaw: bigint;
678
+ /** The quote's entry probability, 1e9-scaled. */
679
+ entryProbabilityRaw: bigint;
680
+ /** The price move to allow per contract, 1e9-scaled: 10¢ is `100_000_000n`. */
681
+ slippageRaw: bigint;
682
+ /**
683
+ * The quote's fee subsidy (`fee_incentive_subsidy`). Defaults to `0n`. Enqueue admission checks
684
+ * the order's limits without the subsidy, and the subsidy can run out before the fill, so the
685
+ * limits add it back: they are sized from the unsubsidized cost.
686
+ */
687
+ feeIncentiveSubsidyRaw?: bigint;
688
+ /** The deployment's `position_lot_size`. Defaults to `10_000n`. */
689
+ lotSize?: bigint;
690
+ }
691
+
692
+ // Raw USDC a budget floor leaves for the per-component rounding of the fill's cost: the premium,
693
+ // the trading, builder and referral fees and the inventory impact each round on their own.
694
+ const COST_ROUNDING_SLACK = 10n;
695
+
696
+ /** All-in price per $1 of payout, 1e9-scaled and rounded up. */
697
+ export interface PricePerContract {
698
+ /** The quote's price, after its fee subsidy. */
699
+ nowRaw: bigint;
700
+ /** The worst price the limits admit: the price without the fee subsidy, plus the slippage. */
701
+ worstRaw: bigint;
702
+ }
703
+
704
+ function pricePerContract(inputs: MintLimitsInputs): PricePerContract {
705
+ const subsidy = inputs.feeIncentiveSubsidyRaw ?? 0n;
706
+ assertUint(inputs.quoteCostRaw, 'quoteCostRaw');
707
+ assertUint(inputs.quoteQuantityRaw, 'quoteQuantityRaw');
708
+ assertUint(inputs.slippageRaw, 'slippageRaw', FLOAT_SCALING);
709
+ assertUint(subsidy, 'feeIncentiveSubsidyRaw');
710
+ if (inputs.quoteQuantityRaw === 0n) {
711
+ throw new PredictInputError('the quote buys no payout, so it sizes no limits');
712
+ }
713
+ const nowRaw = mulDivUp(inputs.quoteCostRaw, FLOAT_SCALING, inputs.quoteQuantityRaw);
714
+ const unsubsidizedRaw = mulDivUp(
715
+ inputs.quoteCostRaw + subsidy,
716
+ FLOAT_SCALING,
717
+ inputs.quoteQuantityRaw,
718
+ );
719
+ return { nowRaw, worstRaw: unsubsidizedRaw + inputs.slippageRaw };
720
+ }
721
+
722
+ /**
723
+ * Limits for a queued all-in budget mint (`enqueueMintCost`): the payout floor at the worst price.
724
+ * The fill buys what `budgetRaw` buys at τ and refunds the order when that is below
725
+ * `minQuantityRaw`, so the order fills while the all-in price per $1 of payout stays within
726
+ * `slippageRaw` of the quote.
727
+ */
728
+ export function budgetMintLimits(inputs: MintLimitsInputs & { budgetRaw: bigint }): {
729
+ minQuantityRaw: bigint;
730
+ pricePerContract: PricePerContract;
731
+ } {
732
+ assertUint(inputs.budgetRaw, 'budgetRaw');
733
+ const price = pricePerContract(inputs);
734
+ const lot = inputs.lotSize ?? POSITION_LOT_SIZE;
735
+ // The fill rounds the premium and each fee on its own, so a quantity's cost can sit above
736
+ // `quantity × price`. The floor leaves a few raw units of the budget for that. Inventory impact
737
+ // can round by more, so `read.planMint` also checks the floor with exact quotes.
738
+ const spendable =
739
+ inputs.budgetRaw > COST_ROUNDING_SLACK ? inputs.budgetRaw - COST_ROUNDING_SLACK : 0n;
740
+ const quantity = (spendable * FLOAT_SCALING) / price.worstRaw;
741
+ return { minQuantityRaw: (quantity / lot) * lot, pricePerContract: price };
742
+ }
743
+
744
+ /**
745
+ * Limits for a queued exact-quantity mint (`enqueueMint`): the entry-probability cap and the
746
+ * all-in cost cap at the worst price. The cost cap never exceeds the quantity, since a contract
747
+ * never costs more than it pays.
748
+ */
749
+ export function exactMintLimits(inputs: MintLimitsInputs): {
750
+ maxCostRaw: bigint;
751
+ maxProbabilityRaw: bigint;
752
+ pricePerContract: PricePerContract;
753
+ } {
754
+ assertUint(inputs.entryProbabilityRaw, 'entryProbabilityRaw', FLOAT_SCALING);
755
+ const price = pricePerContract(inputs);
756
+ const maxCostRaw = mulDivUp(inputs.quoteQuantityRaw, price.worstRaw, FLOAT_SCALING);
757
+ const maxProbabilityRaw = inputs.entryProbabilityRaw + inputs.slippageRaw;
758
+ return {
759
+ maxCostRaw: min(maxCostRaw, inputs.quoteQuantityRaw),
760
+ maxProbabilityRaw: min(maxProbabilityRaw, FLOAT_SCALING),
761
+ pricePerContract: price,
762
+ };
763
+ }
764
+
765
+ /**
766
+ * Limits for a queued early sell (`enqueueSell`): the close-probability floor and the proceeds
767
+ * floor at the worst price. `proceedsRaw` is the quote's proceeds before the order fee, as
768
+ * `minProceeds` is.
769
+ */
770
+ export function sellLimits(inputs: {
771
+ proceedsRaw: bigint;
772
+ closeQuantityRaw: bigint;
773
+ probabilityRaw: bigint;
774
+ slippageRaw: bigint;
775
+ }): { minProceedsRaw: bigint; minProbabilityRaw: bigint } {
776
+ assertUint(inputs.proceedsRaw, 'proceedsRaw');
777
+ assertUint(inputs.closeQuantityRaw, 'closeQuantityRaw');
778
+ assertUint(inputs.probabilityRaw, 'probabilityRaw', FLOAT_SCALING);
779
+ assertUint(inputs.slippageRaw, 'slippageRaw', FLOAT_SCALING);
780
+ const give = mulDivUp(inputs.closeQuantityRaw, inputs.slippageRaw, FLOAT_SCALING);
781
+ return {
782
+ minProceedsRaw: inputs.proceedsRaw > give ? inputs.proceedsRaw - give : 0n,
783
+ minProbabilityRaw:
784
+ inputs.probabilityRaw > inputs.slippageRaw ? inputs.probabilityRaw - inputs.slippageRaw : 0n,
785
+ };
786
+ }
787
+
788
+ function normalPdf(x: number): number {
789
+ return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
790
+ }
791
+
792
+ // Acklam's rational approximation of Φ⁻¹, relative error below 1.15e-9.
793
+ function inverseNormalCdf(p: number): number {
794
+ const a = [
795
+ -3.969683028665376e1, 2.209460984245205e2, -2.759285104469687e2, 1.38357751867269e2,
796
+ -3.066479806614716e1, 2.506628277459239,
797
+ ];
798
+ const b = [
799
+ -5.447609879822406e1, 1.615858368580409e2, -1.556989798598866e2, 6.680131188771972e1,
800
+ -1.328068155288572e1,
801
+ ];
802
+ const c = [
803
+ -7.784894002430293e-3, -3.223964580411365e-1, -2.400758277161838, -2.549732539343734,
804
+ 4.374664141464968, 2.938163982698783,
805
+ ];
806
+ const d = [7.784695709041462e-3, 3.224671290700398e-1, 2.445134137142996, 3.754408661907416];
807
+ const low = 0.02425;
808
+ if (p < low) {
809
+ const q = Math.sqrt(-2 * Math.log(p));
810
+ return (
811
+ (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
812
+ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1)
813
+ );
814
+ }
815
+ if (p > 1 - low) {
816
+ const q = Math.sqrt(-2 * Math.log(1 - p));
817
+ return -(
818
+ (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
819
+ ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1)
820
+ );
821
+ }
822
+ const q = p - 0.5;
823
+ const r = q * q;
824
+ return (
825
+ ((((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q) /
826
+ (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)
827
+ );
828
+ }
829
+
830
+ // === Order state ===
831
+
832
+ /**
833
+ * A queue record as `queue::order` returns it: the generated `order_queue::OrderView` layout, a
834
+ * copy of the record without Predict's receipt and the escrow balance (`funds` is its value).
835
+ */
836
+ export type QueuedOrder = (typeof OrderViewBcs)['$inferType'];
837
+
838
+ /** The position a record holds: a filled mint's, a sell's, or a partial sell's remainder. */
839
+ export interface HeldPosition {
840
+ orderId: bigint;
841
+ rootId: bigint;
842
+ openedAtMs: bigint;
843
+ /**
844
+ * The position's payout quantity, decoded from `orderId` at the Move lot size: a filled
845
+ * mint's quantity, or what a partial sell left Open.
846
+ */
847
+ quantityRaw: bigint;
848
+ }
849
+
850
+ interface OrderViewBase {
851
+ status: number;
852
+ kind: number;
853
+ kindName: OrderKindName;
854
+ side: 'mint' | 'sell' | 'unknown';
855
+ }
856
+
857
+ /**
858
+ * What the app shows for one queued order. `placed` → `priced` → one of `filled` / `refunded`.
859
+ * Report `filled` only from the record or the fill event, never from a resolve simulation.
860
+ */
861
+ export type OrderView = OrderViewBase &
862
+ (
863
+ | {
864
+ state: 'placed';
865
+ tauMs: bigint;
866
+ deadlineMs: bigint;
867
+ /** τ has passed and the price hasn't been committed yet. */
868
+ awaitingPrice: boolean;
869
+ /** Keepers refund an unfinished order at its deadline. */
870
+ autoRefundByMs: bigint;
871
+ /** Offer "Refund my order": unfinished and {@link REFUND_REQUEST_GRACE_MS} past the deadline. */
872
+ canRequestRefund: boolean;
873
+ }
874
+ | {
875
+ state: 'priced';
876
+ tauMs: bigint;
877
+ /** The committed update's tick: τ, or a backup tick. */
878
+ tickMs: bigint;
879
+ /** The committed Pyth price, 1e9-scaled. */
880
+ priceRaw: bigint;
881
+ deadlineMs: bigint;
882
+ msToDeadline: bigint;
883
+ autoRefundByMs: bigint;
884
+ canRequestRefund: boolean;
885
+ }
886
+ | {
887
+ state: 'refunding';
888
+ reason: RefundReasonInfo;
889
+ deadlineMs: bigint;
890
+ canRequestRefund: boolean;
891
+ }
892
+ | {
893
+ state: 'filled';
894
+ /**
895
+ * Filled quantity (mint) or closed quantity (sell). A partial sell's remainder is
896
+ * `position.quantityRaw`.
897
+ */
898
+ quantityRaw: bigint;
899
+ /** Cost paid (mint) or proceeds before the order fee (sell). */
900
+ amountRaw: bigint;
901
+ finishedAtMs: bigint;
902
+ /** The Open record's position: a filled mint, or a partial sell's remainder. */
903
+ position: HeldPosition | null;
904
+ /** Whether `enqueue_redeem_open` can sell this record now. */
905
+ sellable: boolean;
906
+ /** See {@link ParkedFunds}. */
907
+ parkedRaw: bigint;
908
+ }
909
+ | {
910
+ state: 'refunded';
911
+ reason: RefundReasonInfo;
912
+ finishedAtMs: bigint;
913
+ /** A refunded sell keeps its position as an Open record, sellable again. */
914
+ positionBackAsOpenRecord: boolean;
915
+ position: HeldPosition | null;
916
+ sellable: boolean;
917
+ /** See {@link ParkedFunds}. */
918
+ parkedRaw: bigint;
919
+ }
920
+ | {
921
+ /** A filled mint's record whose position was later sold or settled. */
922
+ state: 'closed';
923
+ quantityRaw: bigint;
924
+ amountRaw: bigint;
925
+ finishedAtMs: bigint;
926
+ /** See {@link ParkedFunds}. */
927
+ parkedRaw: bigint;
928
+ }
929
+ | { state: 'unknown' }
930
+ );
931
+
932
+ /**
933
+ * `parkedRaw` on a finished record: USDC the record kept because its receive address couldn't take
934
+ * it (on USDC's deny list, or USDC paused) when its change or refund was sent. It is the
935
+ * record's `funds`, which holds only parked USDC once the order finishes. Anyone can send it with
936
+ * `claim_parked` once the address is clear. `0n` when nothing is parked.
937
+ */
938
+ export type ParkedFunds = bigint;
939
+
940
+ function heldPosition(p: QueuedOrder['position'], lotSize?: bigint): HeldPosition | null {
941
+ return p.order_id === 0n
942
+ ? null
943
+ : {
944
+ orderId: p.order_id,
945
+ rootId: p.root_id,
946
+ openedAtMs: p.opened_at_ms,
947
+ quantityRaw: decodeOrderRange(p.order_id, lotSize).quantity,
948
+ };
949
+ }
950
+
951
+ /**
952
+ * Map a queue record to its display state. `nowMs` drives the refund flags. A record is sellable
953
+ * while it is Open, holds a position, and `nowMs` is before the cutoff (`opts.cutoffMs`, or the
954
+ * record's own placement cutoff when omitted). Unknown status or kind codes map to `unknown`.
955
+ * `opts.lotSize` is the config's position lot (default 10,000, Predict's), which decodes a held
956
+ * position's quantity.
957
+ */
958
+ export function orderView(
959
+ record: QueuedOrder,
960
+ nowMs: bigint,
961
+ opts: { cutoffMs?: bigint; lotSize?: bigint } = {},
962
+ ): OrderView {
963
+ const kind = record.kind;
964
+ const base: OrderViewBase = {
965
+ status: record.status,
966
+ kind,
967
+ kindName: orderKindName(kind),
968
+ side: isMintKind(kind) ? 'mint' : isSellKind(kind) ? 'sell' : 'unknown',
969
+ };
970
+ if (base.side === 'unknown') return { ...base, state: 'unknown' };
971
+ const { tau_ms: tauMs, deadline_ms: deadlineMs } = record.timing;
972
+ const canRequestRefund = nowMs >= deadlineMs + REFUND_REQUEST_GRACE_MS;
973
+ const position = heldPosition(record.position, opts.lotSize);
974
+ const cutoffMs = opts.cutoffMs ?? record.timing.cutoff_ms;
975
+ const sellable = position != null && nowMs < cutoffMs;
976
+ const result = record.result;
977
+ // Only read on the finished statuses below, where `funds` holds parked USDC, not escrow.
978
+ const parkedRaw = record.funds;
979
+ switch (record.status) {
980
+ case ORDER_STATUS.PENDING:
981
+ return {
982
+ ...base,
983
+ state: 'placed',
984
+ tauMs,
985
+ deadlineMs,
986
+ awaitingPrice: nowMs >= tauMs,
987
+ autoRefundByMs: deadlineMs,
988
+ canRequestRefund,
989
+ };
990
+ case ORDER_STATUS.COMMITTED:
991
+ return {
992
+ ...base,
993
+ state: 'priced',
994
+ tauMs,
995
+ tickMs: record.price.tick_ms,
996
+ priceRaw: record.price.spot,
997
+ deadlineMs,
998
+ msToDeadline: deadlineMs > nowMs ? deadlineMs - nowMs : 0n,
999
+ autoRefundByMs: deadlineMs,
1000
+ canRequestRefund,
1001
+ };
1002
+ case ORDER_STATUS.REFUND_DUE:
1003
+ return {
1004
+ ...base,
1005
+ state: 'refunding',
1006
+ reason: refundReason(result.reason),
1007
+ deadlineMs,
1008
+ canRequestRefund,
1009
+ };
1010
+ case ORDER_STATUS.OPEN:
1011
+ if (base.side === 'sell' && result.reason > 0) {
1012
+ return {
1013
+ ...base,
1014
+ state: 'refunded',
1015
+ reason: refundReason(result.reason),
1016
+ finishedAtMs: result.finished_at_ms,
1017
+ positionBackAsOpenRecord: true,
1018
+ position,
1019
+ sellable,
1020
+ parkedRaw,
1021
+ };
1022
+ }
1023
+ return {
1024
+ ...base,
1025
+ state: 'filled',
1026
+ quantityRaw: result.quantity,
1027
+ amountRaw: result.amount,
1028
+ finishedAtMs: result.finished_at_ms,
1029
+ position,
1030
+ sellable,
1031
+ parkedRaw,
1032
+ };
1033
+ case ORDER_STATUS.REFUNDED:
1034
+ return {
1035
+ ...base,
1036
+ state: 'refunded',
1037
+ reason: refundReason(result.reason),
1038
+ finishedAtMs: result.finished_at_ms,
1039
+ positionBackAsOpenRecord: false,
1040
+ position: null,
1041
+ sellable: false,
1042
+ parkedRaw,
1043
+ };
1044
+ case ORDER_STATUS.CLOSED:
1045
+ // A sell record that filled stays `filled` after its remainder moves on; a mint record
1046
+ // (or a refunded sell sold again) is `closed` once its position left.
1047
+ if (base.side === 'sell' && result.reason === 0 && result.quantity > 0n) {
1048
+ return {
1049
+ ...base,
1050
+ state: 'filled',
1051
+ quantityRaw: result.quantity,
1052
+ amountRaw: result.amount,
1053
+ finishedAtMs: result.finished_at_ms,
1054
+ position: null,
1055
+ sellable: false,
1056
+ parkedRaw,
1057
+ };
1058
+ }
1059
+ return {
1060
+ ...base,
1061
+ state: 'closed',
1062
+ quantityRaw: result.quantity,
1063
+ amountRaw: result.amount,
1064
+ finishedAtMs: result.finished_at_ms,
1065
+ parkedRaw,
1066
+ };
1067
+ default:
1068
+ return { ...base, state: 'unknown' };
1069
+ }
1070
+ }
1071
+
1072
+ /** One record's state rebuilt from events, for the app's event path (indexer or own effects). */
1073
+ export interface OrderEventState {
1074
+ marketId: string;
1075
+ recordId: bigint;
1076
+ /**
1077
+ * `closed`: the record's position moved into a later sell's record (`enqueue_redeem_open` closes
1078
+ * the source record), so this record holds nothing any more.
1079
+ */
1080
+ state: 'placed' | 'priced' | 'filled' | 'refunded' | 'settled' | 'closed';
1081
+ kind: number | null;
1082
+ tauMs: bigint | null;
1083
+ deadlineMs: bigint | null;
1084
+ /** Set once priced: the committed Pyth price as a float (`spot / 1e9`) and its tick. */
1085
+ price: number | null;
1086
+ tickMs: bigint | null;
1087
+ /**
1088
+ * Set on a refund. The settlement drain (`settle_step`) refunds the orders still waiting at
1089
+ * expiry with reason 5 (deadline), like a keeper's deadline refund.
1090
+ */
1091
+ reason: RefundReasonInfo | null;
1092
+ /**
1093
+ * The position the record holds now, or null. A sell's record takes its source's position at
1094
+ * enqueue and keeps it through a refund, a fill leaves a mint's new position or a partial
1095
+ * sell's remainder, and a later sell, a full close or settlement clears it.
1096
+ */
1097
+ position: HeldPosition | null;
1098
+ /** For a sell: the record its position came from. */
1099
+ sourceRecordId: bigint | null;
1100
+ /** Set when `settle_step` or `pay_open` paid the record (0 for a loser). */
1101
+ payoutRaw: bigint | null;
1102
+ /** True after `OpenRecordPayoutSkipped`: the record stays Open, unpaid for now. */
1103
+ payoutSkipped: boolean;
1104
+ /**
1105
+ * USDC the record holds parked (`RecordFundsParked`, summed) until `RecordFundsClaimed`. See
1106
+ * {@link ParkedFunds}.
1107
+ */
1108
+ parkedRaw: bigint;
1109
+ }
1110
+
1111
+ /**
1112
+ * Fold decoded queue events (in chain order) into per-record states, keyed `marketId:recordId`.
1113
+ * The app shows Placed from the enqueue's own effects, Priced at the cohort commit, and switches to
1114
+ * Filled or Refunded on `QueuedOrderFilled` / `QueuedOrderRefunded`. Events for records that never
1115
+ * appeared enqueued still create an entry, so a partial event window works.
1116
+ */
1117
+ export function reduceOrderEvents(
1118
+ events: readonly QueueEvent[],
1119
+ into: Map<string, OrderEventState> = new Map(),
1120
+ ): Map<string, OrderEventState> {
1121
+ const entry = (marketId: string, recordId: bigint): OrderEventState => {
1122
+ const key = `${marketId}:${recordId}`;
1123
+ let s = into.get(key);
1124
+ if (!s) {
1125
+ s = {
1126
+ marketId,
1127
+ recordId,
1128
+ state: 'placed',
1129
+ kind: null,
1130
+ tauMs: null,
1131
+ deadlineMs: null,
1132
+ price: null,
1133
+ tickMs: null,
1134
+ reason: null,
1135
+ position: null,
1136
+ sourceRecordId: null,
1137
+ payoutRaw: null,
1138
+ payoutSkipped: false,
1139
+ parkedRaw: 0n,
1140
+ };
1141
+ into.set(key, s);
1142
+ }
1143
+ return s;
1144
+ };
1145
+ for (const e of events) {
1146
+ switch (e.type) {
1147
+ case 'enqueued': {
1148
+ const s = entry(e.marketId, e.recordId);
1149
+ s.kind = e.kind;
1150
+ s.tauMs = e.timing.tauMs;
1151
+ s.deadlineMs = e.timing.deadlineMs;
1152
+ // A sell moves the whole position out of its source record, which the chain marks
1153
+ // Closed, and into the new record, which holds it until the sell fills or refunds.
1154
+ s.position = e.position;
1155
+ s.sourceRecordId = e.sourceRecordId;
1156
+ if (e.sourceRecordId != null) {
1157
+ const source = entry(e.marketId, e.sourceRecordId);
1158
+ source.state = 'closed';
1159
+ source.position = null;
1160
+ }
1161
+ break;
1162
+ }
1163
+ case 'cohort-committed':
1164
+ for (const s of into.values()) {
1165
+ if (
1166
+ s.marketId === e.marketId &&
1167
+ s.state === 'placed' &&
1168
+ s.recordId >= e.firstRecordId &&
1169
+ s.recordId <= e.lastRecordId
1170
+ ) {
1171
+ s.state = 'priced';
1172
+ s.price = e.price;
1173
+ s.tickMs = e.tickMs;
1174
+ }
1175
+ }
1176
+ break;
1177
+ case 'filled': {
1178
+ const s = entry(e.marketId, e.recordId);
1179
+ s.state = 'filled';
1180
+ s.kind = e.kind;
1181
+ s.tickMs = e.tickMs;
1182
+ s.position = e.position;
1183
+ break;
1184
+ }
1185
+ case 'refunded': {
1186
+ const s = entry(e.marketId, e.recordId);
1187
+ s.state = 'refunded';
1188
+ s.kind = e.kind;
1189
+ s.reason = e.reason;
1190
+ // A refunded sell keeps its position, Open and sellable again from this record.
1191
+ if (!e.positionReturned) s.position = null;
1192
+ break;
1193
+ }
1194
+ case 'open-record-settled': {
1195
+ const s = entry(e.marketId, e.recordId);
1196
+ s.state = 'settled';
1197
+ s.payoutRaw = e.raw.payout;
1198
+ s.payoutSkipped = false;
1199
+ s.position = null;
1200
+ break;
1201
+ }
1202
+ case 'open-record-payout-skipped': {
1203
+ const s = entry(e.marketId, e.recordId);
1204
+ s.payoutSkipped = true;
1205
+ break;
1206
+ }
1207
+ case 'record-funds-parked': {
1208
+ const s = entry(e.marketId, e.recordId);
1209
+ s.parkedRaw += e.raw.amount;
1210
+ break;
1211
+ }
1212
+ case 'record-funds-claimed': {
1213
+ // `claim_parked` sends everything the record had parked.
1214
+ const s = entry(e.marketId, e.recordId);
1215
+ s.parkedRaw = 0n;
1216
+ break;
1217
+ }
1218
+ default:
1219
+ break;
1220
+ }
1221
+ }
1222
+ return into;
1223
+ }
1224
+
1225
+ function min(a: bigint, b: bigint): bigint {
1226
+ return a < b ? a : b;
1227
+ }
1228
+
1229
+ function max(a: bigint, b: bigint): bigint {
1230
+ return a > b ? a : b;
1231
+ }