@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
@@ -2,25 +2,89 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import type { ClientWithCoreApi, SuiClientRegistration } from '@mysten/sui/client';
4
4
  import { Transaction, coinWithBalance, type TransactionResult } from '@mysten/sui/transactions';
5
- import { isValidSuiObjectId } from '@mysten/sui/utils';
5
+ import { isValidSuiObjectId, normalizeSuiAddress } from '@mysten/sui/utils';
6
6
  import { TESTNET_PREDICT } from '../deployments/testnet.js';
7
7
  import { getConfig, type PredictConfig, type UnderlyingConfig } from './config/index.js';
8
- import { toGeneratedConfig, type GeneratedConfig } from './config/generated.js';
8
+ import {
9
+ toGeneratedConfig,
10
+ toOrdersConfig,
11
+ type GeneratedConfig,
12
+ type OrdersGeneratedConfig,
13
+ } from './config/generated.js';
14
+ import { MIN_PREMIUM, decodeOrderRange, type FeePolicy } from './cost.js';
9
15
  import {
10
16
  decodeAccountsCreated,
11
17
  decodeBuilderCodeSets,
12
18
  decodeClaims,
19
+ decodeCohortCommits,
13
20
  decodeDeposits,
21
+ decodeEnqueues,
22
+ decodeExpiryPnlRealized,
23
+ decodeMarketPayoutsCompleted,
14
24
  decodeMints,
25
+ decodeOpenRecordPayouts,
15
26
  decodePlpCancels,
16
27
  decodePlpRequests,
28
+ decodePolicyUpdates,
29
+ decodeQueueEvents,
30
+ decodeQueueOps,
31
+ decodeQueuedFills,
32
+ decodeQueuedRefunds,
33
+ decodeRecordFunds,
17
34
  decodeRedeems,
18
35
  decodeWithdrawals,
19
36
  exactlyOne,
20
37
  type DecodableTransactionResult,
21
38
  } from './decode.js';
22
- import { PredictInputError } from './errors.js';
39
+ import {
40
+ PredictInputError,
41
+ PredictMoveError,
42
+ PredictPreflightError,
43
+ type PredictPreflightCode,
44
+ } from './errors.js';
45
+ import {
46
+ ORDER_FLOW_PACKAGE_VERSION,
47
+ ORDER_KIND,
48
+ ORDER_STATUS,
49
+ budgetMintLimits,
50
+ cashNeedSell,
51
+ channelTickMs,
52
+ exactMintLimits,
53
+ feeCovered,
54
+ maxMintNow,
55
+ mintBudget,
56
+ mintCashNeed,
57
+ orderKindName,
58
+ orderView,
59
+ previewTiming,
60
+ sellLimits,
61
+ slippageBand,
62
+ type DelayedExecutionPolicy,
63
+ type MaxMintNow,
64
+ type OrderKindName,
65
+ type OrderView,
66
+ type QueuedOrder,
67
+ type TimingPreview,
68
+ } from './queue.js';
69
+ import { deriveQueueId } from './queue-id.js';
23
70
  import { simulateWithEvents } from './reads/inspect.js';
71
+ import {
72
+ executionModeFor,
73
+ lazerPackages,
74
+ marketQueueState,
75
+ orderFeeAndBalance,
76
+ pendingFunds,
77
+ queuedOrders,
78
+ quoteMintAnonymous,
79
+ quoteMintForAccount,
80
+ quoteRedeemOpen,
81
+ versionWatermark,
82
+ type AnonymousMintQuoteRequest,
83
+ type ExecutionMode,
84
+ type MarketQueueState,
85
+ type MintQuoteRaw,
86
+ type MintQuoteRequest,
87
+ } from './reads/queue.js';
24
88
  import {
25
89
  positionsFromTable,
26
90
  resolvePositionsTable,
@@ -32,10 +96,12 @@ import {
32
96
  activeMarketIds,
33
97
  currentNav,
34
98
  expiryMarketId,
99
+ marketFeePolicy,
35
100
  marketState,
36
101
  marketStates,
37
102
  rangePrices,
38
103
  referenceTick,
104
+ settlementPrice,
39
105
  type MarketState,
40
106
  } from './reads/markets.js';
41
107
  import { poolStats } from './reads/pool.js';
@@ -54,6 +120,19 @@ import {
54
120
  } from './tx/authed.js';
55
121
 
56
122
  import { accountContract, deriveAccountWrapperIdFrom } from './tx/common.js';
123
+ import {
124
+ assertOrderFeeAtMost,
125
+ claimParked,
126
+ enqueueExactAmount,
127
+ enqueueExactCost,
128
+ enqueueExactQuantity,
129
+ enqueueRedeemOpen,
130
+ fill,
131
+ payOpen,
132
+ rebalanceExpiryCash,
133
+ refund,
134
+ type LazerPackages,
135
+ } from './tx/queue.js';
57
136
  import type { MarketFeeds } from './tx/trade.js';
58
137
  import {
59
138
  mintExactAmount,
@@ -79,6 +158,10 @@ import {
79
158
  */
80
159
  export const POSITION_LOT_SIZE = BigInt(TESTNET_PREDICT.units.positionLotSize);
81
160
 
161
+ // An unsubsidized price per contract (1e9 = $1) below which a budget fill's cost can't be above
162
+ // its payout, so an admission refusal there is the entry band, not the payout bound.
163
+ const NEAR_DOLLAR_PER_CONTRACT = 950_000_000n;
164
+
82
165
  // Most `tx.*` builders are one builder's worth of commands in a fresh PTB.
83
166
  function txOf(command: (tx: Transaction) => TransactionResult | void): Transaction {
84
167
  const tx = new Transaction();
@@ -134,7 +217,7 @@ export interface MintAmountOptions {
134
217
  maxCost?: number;
135
218
  }
136
219
 
137
- /** Options for `mintCost`: all-in budget. Requires Predict v2 (currently Testnet only). */
220
+ /** Options for `mintCost`: all-in budget. Requires Predict v2 or later. */
138
221
  export interface MintCostOptions {
139
222
  /** All-in USDC budget; the chain also caps it at the account balance. */
140
223
  spend: number;
@@ -206,6 +289,23 @@ export interface MarketSummary {
206
289
  referencePrice: number | null;
207
290
  }
208
291
 
292
+ /**
293
+ * A market's settlement, from `read.settlement`. Pass `settlementPriceRaw` and `tickSizeRaw` to
294
+ * `cost.settledPayout` to show what a position paid.
295
+ */
296
+ export interface MarketSettlement {
297
+ marketId: string;
298
+ expiryMs: bigint;
299
+ /** Whether Predict has recorded the settlement price. */
300
+ settled: boolean;
301
+ /** The settlement price in USD, or null until the market settles. */
302
+ settlementPrice: number | null;
303
+ /** The settlement price at the 1e9 price scale, or null until the market settles. */
304
+ settlementPriceRaw: bigint | null;
305
+ /** The market's raw tick size, which `cost.settledPayout` rounds the price against. */
306
+ tickSizeRaw: bigint;
307
+ }
308
+
209
309
  /** Aggregate pool figures. Balances in human units (shares raw); the pending fields
210
310
  * are request COUNTS, not amounts — the on-chain getters expose queue lengths, and
211
311
  * the escrowed USDC/PLP behind them is tracked separately. */
@@ -248,6 +348,17 @@ export interface MintQuote {
248
348
  raw: { premium: bigint; cost: bigint; quantity: bigint; entryProbability: bigint };
249
349
  /** True: computed by the real mint code path against real account state. */
250
350
  feesExact: true;
351
+ /**
352
+ * True when this previews a queued (delayed-execution) fill: read from the chain's
353
+ * `quote_*` functions with the congestion penalty removed, since a queued fill pays none.
354
+ * Set when the network's config records delayed execution.
355
+ */
356
+ queued?: boolean;
357
+ /**
358
+ * Queued previews only: the flat order fee, charged once at enqueue on top of `cost`. Show
359
+ * it once per order, not per contract.
360
+ */
361
+ orderFee?: number;
251
362
  }
252
363
 
253
364
  /** Exact pre-close quote: the dry-run receipt of the redeem you are about to send. */
@@ -270,6 +381,325 @@ interface ResolvedMarket {
270
381
  state: MarketState;
271
382
  }
272
383
 
384
+ /** A market by its underlying and expiry, optionally pinned to an exact object. */
385
+ export type MarketCoordinates = Pick<MarketDescriptor, 'underlying' | 'expiryMs' | 'marketId'>;
386
+
387
+ /** Options for `enqueueMint`: a queued mint of an exact payout quantity. */
388
+ export interface EnqueueMintOptions {
389
+ quantity: number;
390
+ /** All-in USDC cap (premium + fees). Required: a queued mint has no "unlimited" cap. */
391
+ maxCost: number;
392
+ /** Entry-probability cap at τ, in (0, 1]. Required. See `queue.slippageBand`. */
393
+ maxProbability: number;
394
+ }
395
+
396
+ /** Options for `enqueueMintAmount`: a queued premium-budget mint. */
397
+ export interface EnqueueMintAmountOptions {
398
+ /** Premium budget in USDC. */
399
+ spend: number;
400
+ /** Floor on the payout received. */
401
+ minQuantity: number;
402
+ /** All-in USDC cap (premium + fees). Required. */
403
+ maxCost: number;
404
+ }
405
+
406
+ /** Options for `enqueueMintCost`: a queued all-in-budget mint. */
407
+ export interface EnqueueMintCostOptions {
408
+ /** All-in USDC budget, fees included. Escrowed at enqueue, less what the fill doesn't use. */
409
+ spend: number;
410
+ /** Floor on the payout received. Zero disables this slippage floor. */
411
+ minQuantity: number;
412
+ }
413
+
414
+ /** Options for `enqueueSell`: a queued early sell of an Open record. */
415
+ export interface EnqueueSellOptions {
416
+ /** The Open record to sell from: its queue record ID, not the position's order ID. */
417
+ recordId: bigint;
418
+ /** Payout quantity to close. A partial sell must leave at least the policy minimum. */
419
+ quantity: number;
420
+ /** Close-side probability floor at τ, in [0, 1]. Pass 0 on purpose to disable it. */
421
+ minProbability: number;
422
+ /** Floor on the proceeds before the order fee, in USDC. Pass 0 on purpose to disable it. */
423
+ minProceeds: number;
424
+ /**
425
+ * Add `rebalance_expiry_cash` after the enqueue so the market is funded for this sell at once.
426
+ * `'auto'` (default) adds it only when the sell's cash need is above spare cash, `'always'`
427
+ * always, `'never'` never. It takes the hot `PoolVault`, so keep it conditional.
428
+ */
429
+ fundMarket?: 'auto' | 'always' | 'never';
430
+ }
431
+
432
+ /** What a queued order builder previews alongside the transaction. Display only. */
433
+ export interface QueuedOrderPreview {
434
+ marketId: string;
435
+ kind: number;
436
+ kindName: OrderKindName;
437
+ /** τ, deadline and cutoff for an order placed now, from the local clock. */
438
+ timing: TimingPreview;
439
+ /** The flat order fee, charged once at enqueue. */
440
+ orderFee: number;
441
+ /** USDC escrowed for the premium and fees; 0 for a sell. Unused budget returns at the fill. */
442
+ budget: number;
443
+ /** What enqueue debits: budget + order fee. */
444
+ totalDebit: number;
445
+ /** The order's worst-case cash need against the market's spare cash. */
446
+ cashNeedRaw: bigint;
447
+ spareCashRaw: bigint;
448
+ /**
449
+ * Sells only: the cash need is above spare cash, so the sell may be refunded in full
450
+ * (reason 8) unless the market is funded before the fill.
451
+ */
452
+ needsFunding: boolean;
453
+ /** Whether the builder added `rebalance_expiry_cash` after the enqueue. */
454
+ fundedInTransaction: boolean;
455
+ raw: { orderFee: bigint; budget: bigint; totalDebit: bigint };
456
+ }
457
+
458
+ /** A queued order's transaction and its preview. */
459
+ export interface QueuedOrderPlan {
460
+ transaction: Transaction;
461
+ preview: QueuedOrderPreview;
462
+ }
463
+
464
+ /**
465
+ * How much the price may move against a queued order between its quote and its fill, in cents per
466
+ * contract. A contract pays $1, so its price in cents is its probability: 10¢ of slippage lets
467
+ * a 42¢ contract fill at up to 52¢, fees included. It is never a percentage.
468
+ */
469
+ export interface SlippageOptions {
470
+ /**
471
+ * Cents per contract: `10` is 10¢. `'auto'` (the default) sizes it with `queue.slippageBand`
472
+ * from the time to expiry and the policy's delay.
473
+ */
474
+ slippageCents?: number | 'auto';
475
+ /** Standard deviations of movement for `'auto'`. Default 2. */
476
+ k?: number;
477
+ }
478
+
479
+ /**
480
+ * Options for `read.planMint`. `amount` plans a purchase form's spend (an all-in budget mint),
481
+ * and `quantity` plans an exact payout.
482
+ */
483
+ export type PlanMintOptions = SlippageOptions &
484
+ (
485
+ | {
486
+ /** The purchase amount in USDC. */
487
+ amount: number;
488
+ /**
489
+ * Whether `amount` is the whole debit, order fee included (`'inclusive'`, the default),
490
+ * or the order fee is charged on top (`'exclusive'`).
491
+ */
492
+ orderFee?: 'inclusive' | 'exclusive';
493
+ }
494
+ | { quantity: number }
495
+ );
496
+
497
+ /** The slippage a plan applied. */
498
+ export interface AppliedSlippage {
499
+ /** Cents per contract. */
500
+ cents: number;
501
+ source: 'input' | 'auto';
502
+ /** The model band when `source` is `'auto'`. */
503
+ band: ReturnType<typeof slippageBand> | null;
504
+ }
505
+
506
+ /**
507
+ * `read.planMint`: everything a purchase form shows, and the enqueue options that carry its
508
+ * slippage. Display only: the fill prices at τ, within the limits in `order`.
509
+ */
510
+ export interface MintPlan {
511
+ shape: 'budget' | 'exact-quantity';
512
+ /** The owner, market and strike range the plan was quoted for. */
513
+ target: PlanTarget;
514
+ /** The quote at the current price. */
515
+ quote: MintQuote;
516
+ /**
517
+ * Whether the quote is the account's own (`quote_mint_*_for_account`, with its builder fee). A
518
+ * visitor without an account, or a balance below the order, gets `quote_mint`'s quote instead,
519
+ * with a budget's quantity found by a short search.
520
+ */
521
+ quoteForAccount: boolean;
522
+ slippage: AppliedSlippage;
523
+ /** The payout if it wins, at the current price ("Potential payout"). */
524
+ potentialPayout: number;
525
+ /** The smallest payout the order accepts, at the worst price. A smaller fill is refunded. */
526
+ minPayout: number;
527
+ /** `potentialPayout / expectedDebit` ("Payout multiple"), order fee included. */
528
+ payoutMultiple: number;
529
+ /** `minPayout / totalDebit`: the multiple at the worst price. */
530
+ minPayoutMultiple: number;
531
+ /** All-in price per contract now in USDC (0.42 is 42¢), after the fee subsidy, order fee excluded. */
532
+ pricePerContract: number;
533
+ /**
534
+ * The worst price the limits admit: the price per contract without the fee subsidy, plus the
535
+ * slippage. Enqueue admission checks the order without the subsidy, and the subsidy can run out
536
+ * before the fill, so the limits never count on it.
537
+ */
538
+ worstPricePerContract: number;
539
+ /** The flat order fee, read from the order desk, charged once per order. */
540
+ orderFee: number;
541
+ /** USDC escrowed for the premium and fees. Unused budget comes back at the fill. */
542
+ budget: number;
543
+ /** `budget + orderFee`: what enqueue debits. */
544
+ totalDebit: number;
545
+ /** `quote.cost + orderFee`: what the order costs if it fills at the current price. */
546
+ expectedDebit: number;
547
+ balance: {
548
+ /** Whether the owner has a Predict account yet. */
549
+ hasAccount: boolean;
550
+ /** The account's USDC, or null without an account. */
551
+ available: number | null;
552
+ /** Whether the balance covers `totalDebit`, or null without an account. */
553
+ covers: boolean | null;
554
+ };
555
+ /**
556
+ * The largest `amount` (budget plans) or `quantity` (exact plans) the market's spare cash and,
557
+ * with an account, the balance allow now. An exact plan's balance bound prices each contract
558
+ * at the plan's `worstPricePerContract`. Null when neither binds.
559
+ */
560
+ maxNow: number | null;
561
+ /** τ, deadline and cutoff for an order placed now, from the local clock. */
562
+ timing: TimingPreview;
563
+ /**
564
+ * Whether `tx.enqueuePlan` would build the order now and admission would take it at the current
565
+ * price: the builder's preflight (the queue gates, the order fee, the minimum premium and the
566
+ * market's cash), the balance against `totalDebit`, the payout bound and, for a budget, the
567
+ * floor admission buys without the fee subsidy. Two admission conditions are checked only on
568
+ * chain, at placement: the market's SVI age under the order desk's `svi_max_age_ms`, and room in
569
+ * the payout tree for a new strike's boundary nodes.
570
+ */
571
+ accepting: boolean;
572
+ /**
573
+ * The `PredictPreflightError` code the order would get now, or null. `'fee'` also covers a
574
+ * missing account or a balance below `totalDebit`.
575
+ */
576
+ refusal: PredictPreflightCode | null;
577
+ /** Pass `order.options` to `tx[order.builder]`. */
578
+ order:
579
+ | { builder: 'enqueueMintCost'; options: EnqueueMintCostOptions }
580
+ | { builder: 'enqueueMint'; options: EnqueueMintOptions };
581
+ raw: {
582
+ budget: bigint;
583
+ orderFee: bigint;
584
+ totalDebit: bigint;
585
+ slippage: bigint;
586
+ minQuantity: bigint;
587
+ maxCost: bigint | null;
588
+ maxProbability: bigint | null;
589
+ pricePerContract: bigint;
590
+ worstPricePerContract: bigint;
591
+ };
592
+ }
593
+
594
+ /**
595
+ * What a plan was quoted for. Its limits are sized for that one order, so `tx.enqueuePlan` refuses
596
+ * the plan for any other owner, market, strike or record.
597
+ */
598
+ export interface PlanTarget {
599
+ owner: string;
600
+ expiryMarketId: string;
601
+ /** A mint's strike range, as the market's admission ticks. */
602
+ lowerTick?: bigint;
603
+ higherTick?: bigint;
604
+ /** A sell's Open record. Record IDs restart in every market. */
605
+ recordId?: bigint;
606
+ }
607
+
608
+ /** Options for `read.planSell`. */
609
+ export type PlanSellOptions = SlippageOptions & {
610
+ /** The Open record to sell from. */
611
+ recordId: bigint;
612
+ /** Payout quantity to close. */
613
+ quantity: number;
614
+ };
615
+
616
+ /** `read.planSell`: what an early sell shows, and the enqueue options that carry its slippage. */
617
+ export interface SellPlan {
618
+ /** The owner, market and record the plan was quoted for. */
619
+ target: PlanTarget;
620
+ /** The quote at the current price. */
621
+ quote: SellQuote;
622
+ slippage: AppliedSlippage;
623
+ /** Proceeds at the current price, before the order fee. */
624
+ proceeds: number;
625
+ /** `proceeds − orderFee`. */
626
+ net: number;
627
+ /** The proceeds floor the order carries, before the order fee. A smaller fill is refunded. */
628
+ minProceeds: number;
629
+ /**
630
+ * `minProceeds − orderFee`: the worst the sell nets if it fills. Negative when the order fee is
631
+ * above the proceeds floor.
632
+ */
633
+ minNet: number;
634
+ orderFee: number;
635
+ timing: TimingPreview;
636
+ /**
637
+ * Whether `tx.enqueuePlan` would build the sell now: the builder's preflight, which checks the
638
+ * queue gates, that the record is Open and the owner's, the order fee and the minimum sell. The
639
+ * market's SVI age under the order desk's `svi_max_age_ms` is checked only on chain, at
640
+ * placement.
641
+ */
642
+ accepting: boolean;
643
+ /** The `PredictPreflightError` code the sell would get now, or null. */
644
+ refusal: PredictPreflightCode | null;
645
+ /** Pass `order.options` to `tx.enqueueSell`. */
646
+ order: { builder: 'enqueueSell'; options: EnqueueSellOptions };
647
+ raw: { slippage: bigint; minProceeds: bigint; minProbability: bigint; orderFee: bigint };
648
+ }
649
+
650
+ /** `read.queue`: a market's queue state plus the derived figures the app shows. */
651
+ export interface MarketQueueView extends MarketQueueState {
652
+ mode: ExecutionMode;
653
+ /** τ, deadline and cutoff for an order placed now, from the local clock. */
654
+ timing: TimingPreview;
655
+ /**
656
+ * Whether a new mint would pass the queue gates now: the same gates `enqueueMint*` checks
657
+ * before its fee and cash checks (the order-flow allowlist, the cutover, pauses, stuck, mint
658
+ * capacity, the cutoff, and the account cap when an owner was given). One side can be full
659
+ * while the other is open.
660
+ */
661
+ acceptingMints: boolean;
662
+ /** Whether a new sell would pass the queue gates now (sells skip the mint pauses). */
663
+ acceptingSells: boolean;
664
+ /**
665
+ * Why each side refuses now, or null: the `PredictPreflightError` code the builder would
666
+ * throw for the queue gates.
667
+ */
668
+ refusal: { mint: PredictPreflightCode | null; sell: PredictPreflightCode | null };
669
+ /** The largest mint the market's spare cash admits now. */
670
+ maxMint: { exactQuantity: MaxMintNow; budget: MaxMintNow };
671
+ }
672
+
673
+ /** A queue record and its display state. */
674
+ export interface QueuedOrderView {
675
+ recordId: bigint;
676
+ record: QueuedOrder;
677
+ view: OrderView;
678
+ }
679
+
680
+ /** How `read.waitForOutcome` ended. */
681
+ export interface QueuedOrderOutcome {
682
+ /** `'gone'`: the record is missing (cleaned up, or not landed). `'timeout'`: still waiting. */
683
+ outcome: 'filled' | 'refunded' | 'closed' | 'gone' | 'timeout';
684
+ order: QueuedOrderView | null;
685
+ }
686
+
687
+ /** A quote for selling an Open record early (`quote_redeem_open`). */
688
+ export interface SellQuote {
689
+ /** The live per-contract probability. */
690
+ probability: number;
691
+ /** Proceeds before the order fee, with no congestion penalty. */
692
+ proceeds: number;
693
+ /**
694
+ * `proceeds − orderFee`: what the account nets. Negative when the order fee is above the
695
+ * proceeds, since the fee is charged at enqueue whatever the sell returns.
696
+ */
697
+ net: number;
698
+ fees: { trading: number; builder: number; inventoryImpactRebate: number; order: number };
699
+ quantityClosed: number;
700
+ raw: { probability: bigint; proceeds: bigint; orderFee: bigint; quantityClosed: bigint };
701
+ }
702
+
273
703
  // The strike-bearing (binary) arm of MarketDescriptor, for read.price and its
274
704
  // seam — anonymous board pricing has no range semantics.
275
705
  type BinaryMarketCoordinates = Pick<MarketDescriptor, 'underlying' | 'expiryMs' | 'marketId'> & {
@@ -308,6 +738,16 @@ export function predict<Name extends string = 'predict'>({
308
738
  * returns a finished `Transaction`; callers composing their own PTBs use the generated
309
739
  * move-call bindings this subpath exports (`plpMoveCalls`, `expiryMarketMoveCalls`, …).
310
740
  */
741
+ // The chain refused a quote or order on its own limits at the current price.
742
+ function isOrderFailsLimits(error: unknown): boolean {
743
+ return error instanceof PredictMoveError && error.abortName === 'EOrderFailsLimits';
744
+ }
745
+
746
+ // The chain refused to price a live trade because the market reached its expiry.
747
+ function isPastExpiry(error: unknown): boolean {
748
+ return error instanceof PredictMoveError && error.abortName === 'ELivePricingExpired';
749
+ }
750
+
311
751
  export class PredictClient {
312
752
  readonly cfg: PredictConfig;
313
753
  // The flat slice every generated call resolves `options.config` against.
@@ -324,6 +764,9 @@ export class PredictClient {
324
764
  // immutable once created, so cache-forever; a missing table (no Predict
325
765
  // data yet) is NOT cached — it appears after the owner's first trade.
326
766
  #positionsCache = new Map<string, PositionsHandle>();
767
+ // Queue IDs confirmed to exist. A `MarketQueue` is shared and never deleted, so a confirmed
768
+ // queue is cached forever; a missing one is not cached, since the keeper creates it later.
769
+ #knownQueues = new Set<string>();
327
770
 
328
771
  constructor(opts: {
329
772
  network: 'testnet' | 'mainnet';
@@ -339,6 +782,15 @@ export class PredictClient {
339
782
  return deriveAccountWrapperIdFrom(this.#config, owner);
340
783
  }
341
784
 
785
+ /**
786
+ * The deterministic id of a market's `MarketQueue` under the config's queue registry, whether
787
+ * or not it exists yet — no chain read. Throws `PredictInputError` while the config doesn't
788
+ * record delayed execution.
789
+ */
790
+ queueIdFor(marketId: string): string {
791
+ return deriveQueueId(this.#requireDelayedExecution().queueRegistry, marketId);
792
+ }
793
+
342
794
  // The deployment's wiring for a symbol; throws a typed error on an unknown symbol.
343
795
  // Per-underlying ids are the one thing the flat config slice does not carry.
344
796
  #underlying(underlying: string): UnderlyingConfig {
@@ -489,6 +941,221 @@ export class PredictClient {
489
941
  }
490
942
  }
491
943
 
944
+ // Whether the owner's account wrapper exists. A visitor without an account still gets a plan.
945
+ async #hasAccount(owner: string): Promise<boolean> {
946
+ const {
947
+ objects: [wrapper],
948
+ } = await this.#client.core.getObjects({ objectIds: [this.wrapperIdFor(owner)] });
949
+ return !(wrapper instanceof Error);
950
+ }
951
+
952
+ // The slippage a plan applies, per $1 of payout, 1e9-scaled: the caller's, or the model band at
953
+ // the quote's probability.
954
+ static #appliedSlippage(
955
+ opts: SlippageOptions,
956
+ probabilityRaw: bigint,
957
+ state: MarketQueueState,
958
+ nowMs: bigint,
959
+ ): { raw: bigint; applied: AppliedSlippage } {
960
+ const cents = opts.slippageCents ?? 'auto';
961
+ if (cents !== 'auto') {
962
+ if (!(Number.isFinite(cents) && cents >= 0 && cents <= 100)) {
963
+ throw new PredictInputError(
964
+ `slippageCents must be in [0, 100] cents per contract, got ${cents}`,
965
+ );
966
+ }
967
+ // A cent is 1e7 in the 1e9-scaled price per $1 of payout.
968
+ const raw = BigInt(Math.round(cents * 1e7));
969
+ return { raw, applied: { cents: Number(raw) / 1e7, source: 'input', band: null } };
970
+ }
971
+ const policy = state.desk.policy;
972
+ const band = slippageBand({
973
+ // The band is undefined at the certain ends, where a queued mint is refused anyway.
974
+ probability: Math.min(Math.max(rawToProbability(probabilityRaw), 1e-6), 1 - 1e-6),
975
+ timeToExpiryMs: Number(state.expiryMs > nowMs ? state.expiryMs - nowMs : 0n),
976
+ delayMs: Number(policy.delayMs),
977
+ tickMs: Number(channelTickMs(policy.pythChannel)),
978
+ k: opts.k,
979
+ });
980
+ const raw = BigInt(Math.ceil(band.deltaProbability * 1e9));
981
+ return { raw, applied: { cents: Number(raw) / 1e7, source: 'auto', band } };
982
+ }
983
+
984
+ // Why the chain refused a plan's quote, as a typed refusal, or the chain's own error when it
985
+ // can't tell. A market past its expiry has no live price (`ELivePricingExpired`). A mint the
986
+ // chain refuses at the current price (`EOrderFailsLimits`) is diagnosed by `#entryBandRefusal`.
987
+ async #quoteRefusal(
988
+ error: unknown,
989
+ mint?: { m: MarketDescriptor; quantityRaw?: bigint },
990
+ ): Promise<unknown> {
991
+ if (isPastExpiry(error)) {
992
+ return new PredictPreflightError(
993
+ 'past-cutoff',
994
+ 'the market reached its expiry, so it has no live price and takes no orders',
995
+ );
996
+ }
997
+ if (mint != null && isOrderFailsLimits(error)) {
998
+ return (await this.#entryBandRefusal(mint.m, mint.quantityRaw)) ?? error;
999
+ }
1000
+ return error;
1001
+ }
1002
+
1003
+ // Why the chain refused to quote a mint at the current price (`EOrderFailsLimits`), as a typed
1004
+ // refusal: a binary strike outside the market's entry band, or an exact quantity whose premium
1005
+ // is below the minimum. Null when neither explains it, or the diagnosis can't be read, so the
1006
+ // caller rethrows the chain's error.
1007
+ async #entryBandRefusal(
1008
+ m: MarketDescriptor,
1009
+ quantityRaw?: bigint,
1010
+ ): Promise<PredictPreflightError | null> {
1011
+ if (m.side === 'range') return null;
1012
+ // Best effort: a failed diagnosis read leaves the chain's own error to the caller.
1013
+ const read = await Promise.all([this.read.price(m), this.read.feePolicy(m)]).catch(() => null);
1014
+ if (read == null) return null;
1015
+ const [prices, policy] = read;
1016
+ const probability = probabilityToRaw(m.side === 'up' ? prices.up : prices.down);
1017
+ if (probability < policy.minEntryProbability || probability > policy.maxEntryProbability) {
1018
+ return new PredictPreflightError(
1019
+ 'entry-band',
1020
+ `the ${m.side} probability ${rawToProbability(probability)} is outside the market's entry band [${rawToProbability(policy.minEntryProbability)}, ${rawToProbability(policy.maxEntryProbability)}]: pick a strike nearer the money`,
1021
+ );
1022
+ }
1023
+ if (quantityRaw != null && (quantityRaw * probability) / 1_000_000_000n < MIN_PREMIUM) {
1024
+ return new PredictPreflightError(
1025
+ 'min-premium',
1026
+ `${rawToUsdc(quantityRaw)} contracts cost a premium below the ${rawToUsdc(MIN_PREMIUM)} USDC minimum`,
1027
+ );
1028
+ }
1029
+ return null;
1030
+ }
1031
+
1032
+ // A payout floor admission buys at least, for an all-in budget: the largest lot multiple up to
1033
+ // `startRaw` whose cost without the fee subsidy, by an exact chain quote, fits the budget.
1034
+ // Admission (`cost_qty_at`, no subsidy) first finds the largest quantity whose cost fits, and
1035
+ // cost rises with quantity, so that is at least this. A probe below the minimum premium
1036
+ // (`EOrderFailsLimits`) is retried once at the smallest quantity that clears it. Admission
1037
+ // searches again only when its fill costs more than its payout, a search rounding makes
1038
+ // non-monotone, so a floor whose payout is below the budget, about $1 a contract, is refused.
1039
+ async #admittedFloor(
1040
+ quoteAt: (quantityRaw: bigint) => Promise<MintQuoteRaw>,
1041
+ budgetRaw: bigint,
1042
+ startRaw: bigint,
1043
+ lot: bigint,
1044
+ entryProbabilityRaw: bigint,
1045
+ unsubsidizedPricePerContractRaw: bigint,
1046
+ ): Promise<
1047
+ { quantity: bigint } | { refusal: 'min-premium' | 'cost-above-payout' | 'entry-band' }
1048
+ > {
1049
+ const ceilLot = (x: bigint) => ((x + lot - 1n) / lot) * lot;
1050
+ const minPremiumQuantity =
1051
+ entryProbabilityRaw === 0n
1052
+ ? 0n
1053
+ : ceilLot((MIN_PREMIUM * 1_000_000_000n + entryProbabilityRaw - 1n) / entryProbabilityRaw);
1054
+ let quantity = startRaw;
1055
+ let raised = false;
1056
+ for (let probe = 0; probe < 5 && quantity > 0n; probe++) {
1057
+ let quote: MintQuoteRaw;
1058
+ try {
1059
+ quote = await quoteAt(quantity);
1060
+ } catch (e) {
1061
+ if (!isOrderFailsLimits(e)) throw e;
1062
+ if (!raised && quantity < minPremiumQuantity) {
1063
+ raised = true;
1064
+ quantity = minPremiumQuantity;
1065
+ continue;
1066
+ }
1067
+ // The raise sized the minimum-premium quantity from the budget quote's entry
1068
+ // probability. When the raised probe still fails, the probability moved down between
1069
+ // the two quotes and the premium is still short, so the minimum premium refused it.
1070
+ if (raised || quantity < minPremiumQuantity) return { refusal: 'min-premium' };
1071
+ // At or above the minimum premium, admission refuses a cost above the payout or an entry
1072
+ // probability outside the band. Well below $1 a contract the cost can't reach the payout,
1073
+ // so the probability left the band between the budget quote and this probe.
1074
+ return {
1075
+ refusal:
1076
+ unsubsidizedPricePerContractRaw < NEAR_DOLLAR_PER_CONTRACT
1077
+ ? 'entry-band'
1078
+ : 'cost-above-payout',
1079
+ };
1080
+ }
1081
+ const unsubsidized = quote.allInCost - quote.penaltyFee + quote.feeIncentiveSubsidy;
1082
+ if (unsubsidized <= budgetRaw) {
1083
+ return quantity >= budgetRaw ? { quantity } : { refusal: 'cost-above-payout' };
1084
+ }
1085
+ // The smallest quantity that clears the minimum premium doesn't fit.
1086
+ if (raised) return { refusal: 'min-premium' };
1087
+ const next = ((quantity * budgetRaw) / unsubsidized / lot) * lot;
1088
+ quantity = next < quantity ? next : quantity - lot;
1089
+ }
1090
+ return { refusal: 'min-premium' };
1091
+ }
1092
+
1093
+ // A budget mint's quote for no particular account. `quote_mint` has no all-in budget mode, so
1094
+ // this searches for the largest lot multiple whose all-in cost fits the budget. The first probe
1095
+ // is its premium-budget mode with the whole budget as the premium, whose premium clears the
1096
+ // 1 USDC minimum whenever the purchase can. Each next probe divides the budget by the last price
1097
+ // per contract. The price barely moves with size, so a few quotes settle it. A probe the chain
1098
+ // refuses (`EOrderFailsLimits`) ends the search with the best fit so far. With no fit, the
1099
+ // refusal is `min-premium`, unless the chain priced no probe and the budget is at least the
1100
+ // minimum premium plus a lot: the first probe's premium is above `budget − lot` (a lot costs
1101
+ // under $1), so the minimum premium didn't refuse it, and the chain's own refusal stands.
1102
+ async #searchBudgetQuote(
1103
+ ticks: { expiryMarketId: string; lowerTick: bigint; higherTick: bigint } & MarketFeeds,
1104
+ budgetRaw: bigint,
1105
+ lot: bigint,
1106
+ ): Promise<MintQuoteRaw> {
1107
+ const floorLot = (x: bigint) => (x / lot) * lot;
1108
+ let refusal: unknown = null;
1109
+ let priced = false;
1110
+ const probeQuote = async (request: AnonymousMintQuoteRequest) => {
1111
+ try {
1112
+ const { quote } = await quoteMintAnonymous(this.#client, this.#config, {
1113
+ ...ticks,
1114
+ request,
1115
+ });
1116
+ priced = true;
1117
+ return quote;
1118
+ } catch (e) {
1119
+ if (!isOrderFailsLimits(e)) throw e;
1120
+ refusal ??= e;
1121
+ return null;
1122
+ }
1123
+ };
1124
+ let best: MintQuoteRaw | null = null;
1125
+ let quote = await probeQuote({
1126
+ shape: 'exact-amount',
1127
+ maxPremiumRaw: budgetRaw,
1128
+ minQuantityRaw: lot,
1129
+ });
1130
+ for (let probe = 1; quote != null; probe++) {
1131
+ const cost = quote.allInCost - quote.penaltyFee;
1132
+ if (cost <= budgetRaw && (best == null || quote.quantity > best.quantity)) best = quote;
1133
+ if (probe === 4) break;
1134
+ const next = cost === 0n ? quote.quantity : floorLot((budgetRaw * quote.quantity) / cost);
1135
+ if (next === 0n || next === quote.quantity) break;
1136
+ quote = await probeQuote({ shape: 'exact-quantity', quantityRaw: next });
1137
+ }
1138
+ if (best == null) {
1139
+ if (!priced && refusal != null && budgetRaw >= MIN_PREMIUM + lot) throw refusal;
1140
+ throw new PredictPreflightError(
1141
+ 'min-premium',
1142
+ `a budget of ${rawToUsdc(budgetRaw)} buys no payout at the current price (a mint's premium must be at least ${rawToUsdc(MIN_PREMIUM)} USDC)`,
1143
+ );
1144
+ }
1145
+ return best;
1146
+ }
1147
+
1148
+ // The PredictPreflightError code a check throws, or null when it passes.
1149
+ static #refusal(check: () => void): PredictPreflightCode | null {
1150
+ try {
1151
+ check();
1152
+ return null;
1153
+ } catch (e) {
1154
+ if (e instanceof PredictPreflightError) return e.code;
1155
+ throw e;
1156
+ }
1157
+ }
1158
+
492
1159
  // Shared construction for tx.mint and read.quoteMint. The quote dry-runs the
493
1160
  // same mint the trade sends; quoteMint omits the caller's cost/probability caps
494
1161
  // (they only gate via abort and don't change the receipt numbers).
@@ -603,6 +1270,386 @@ export class PredictClient {
603
1270
  return tick * state.tickSizeRaw;
604
1271
  }
605
1272
 
1273
+ // === delayed execution (DBU-885) ===
1274
+
1275
+ // The config slice the queue calls take, or a typed refusal: without the Predict upgrade, the
1276
+ // order-flow companion and its desk, this SDK version has no record of delayed execution on the
1277
+ // network, and the queue calls would address the wrong package or decode nothing.
1278
+ #requireDelayedExecution(): OrdersGeneratedConfig {
1279
+ return toOrdersConfig(this.cfg);
1280
+ }
1281
+
1282
+ // Whether the config records delayed execution, without throwing.
1283
+ #recordsDelayedExecution(): boolean {
1284
+ const { predictDelayedExecution, predictOrders } = this.cfg.packages;
1285
+ const { orderDesk, queueRegistry } = this.cfg.objects;
1286
+ return Boolean(predictDelayedExecution && predictOrders && orderDesk && queueRegistry);
1287
+ }
1288
+
1289
+ // The market's queue ID, once its `MarketQueue` is known to exist. A missing queue would abort
1290
+ // every read and order with an opaque error, so it is refused here with a typed one.
1291
+ async #existingQueueId(orders: OrdersGeneratedConfig, marketId: string): Promise<string> {
1292
+ const queueId = deriveQueueId(orders.queueRegistry, marketId);
1293
+ if (this.#knownQueues.has(queueId)) return queueId;
1294
+ const {
1295
+ objects: [queue],
1296
+ } = await this.#client.core.getObjects({ objectIds: [queueId] });
1297
+ if (queue instanceof Error) {
1298
+ throw new PredictPreflightError(
1299
+ 'no-queue',
1300
+ `market ${marketId} has no order queue yet (\`queue::create_and_share\` hasn't run for it)`,
1301
+ );
1302
+ }
1303
+ this.#knownQueues.add(queueId);
1304
+ return queueId;
1305
+ }
1306
+
1307
+ async #queueState(
1308
+ orders: OrdersGeneratedConfig,
1309
+ marketId: string,
1310
+ opts: { owner?: string; recordIds?: bigint[] } = {},
1311
+ ): Promise<MarketQueueState> {
1312
+ const queueId = await this.#existingQueueId(orders, marketId);
1313
+ return marketQueueState(this.#client, orders, marketId, {
1314
+ owner: opts.owner,
1315
+ quoteCoinType: this.cfg.quoteCoinType,
1316
+ recordIds: opts.recordIds,
1317
+ queueId,
1318
+ });
1319
+ }
1320
+
1321
+ // The queue gates every enqueue passes: Predict's order-flow allowlist and cutover, the
1322
+ // pauses, then the queue's own (`begin_enqueue`): the stuck gate, capacity, the per-account
1323
+ // cap, and the cutoff on the previewed τ. Throws the matching PredictPreflightError.
1324
+ #assertQueueOpen(
1325
+ state: MarketQueueState,
1326
+ side: 'mint' | 'sell',
1327
+ nowMs: bigint,
1328
+ ): { policy: DelayedExecutionPolicy; timing: TimingPreview } {
1329
+ const policy = state.desk.policy;
1330
+ if (!state.protocol.orderFlowEnabled) {
1331
+ throw new PredictPreflightError(
1332
+ 'not-live',
1333
+ "queued orders aren't live yet: the protocol hasn't enabled the order-flow package",
1334
+ );
1335
+ }
1336
+ const mode = executionModeFor(state.protocol.versionWatermark, true);
1337
+ if (mode === 'retired') {
1338
+ throw new PredictPreflightError(
1339
+ 'retired',
1340
+ 'the Predict package version this SDK calls is retired: update the SDK',
1341
+ );
1342
+ }
1343
+ if (mode !== 'delayed') {
1344
+ throw new PredictPreflightError(
1345
+ 'not-live',
1346
+ "queued orders aren't live yet: the protocol's version watermark hasn't been raised",
1347
+ );
1348
+ }
1349
+ if (state.desk.versionWatermark > ORDER_FLOW_PACKAGE_VERSION) {
1350
+ throw new PredictPreflightError(
1351
+ 'retired',
1352
+ 'the order-flow package version this SDK calls is retired: update the SDK',
1353
+ );
1354
+ }
1355
+ if (state.protocol.frozen) throw new PredictPreflightError('paused', 'the protocol is frozen');
1356
+ if (side === 'mint' && (state.protocol.tradingPaused || state.mintPaused)) {
1357
+ throw new PredictPreflightError('paused', 'minting is paused on this market');
1358
+ }
1359
+ if (state.stuck) {
1360
+ throw new PredictPreflightError(
1361
+ 'stuck',
1362
+ 'pricing is delayed for this market, try again shortly',
1363
+ );
1364
+ }
1365
+ const [pending, capacity] =
1366
+ side === 'mint'
1367
+ ? [state.pending.mints, policy.mintCapacity]
1368
+ : [state.pending.sells, policy.sellCapacity];
1369
+ if (pending >= capacity) {
1370
+ throw new PredictPreflightError(
1371
+ 'queue-full',
1372
+ `the market has ${pending} waiting ${side}s, its maximum`,
1373
+ );
1374
+ }
1375
+ if (state.account && state.account.waitingOrders >= policy.perAccountCap) {
1376
+ throw new PredictPreflightError(
1377
+ 'account-cap',
1378
+ `the account already has ${state.account.waitingOrders} waiting orders in this market, its maximum`,
1379
+ );
1380
+ }
1381
+ const timing = previewTiming({
1382
+ nowMs,
1383
+ policy,
1384
+ heads: state.heads,
1385
+ expiryMs: state.expiryMs,
1386
+ noTradeWindowMs: state.protocol.noTradeWindowMs,
1387
+ });
1388
+ if (!timing.beforeCutoff) {
1389
+ throw new PredictPreflightError(
1390
+ 'past-cutoff',
1391
+ 'this market no longer takes orders before its expiry',
1392
+ );
1393
+ }
1394
+ return { policy, timing };
1395
+ }
1396
+
1397
+ #preview(
1398
+ marketId: string,
1399
+ kind: number,
1400
+ timing: TimingPreview,
1401
+ policy: DelayedExecutionPolicy,
1402
+ budget: bigint,
1403
+ cashNeedRaw: bigint,
1404
+ spareCashRaw: bigint,
1405
+ funding: { needsFunding: boolean; fundedInTransaction: boolean },
1406
+ ): QueuedOrderPreview {
1407
+ const totalDebit = budget + policy.orderFee;
1408
+ return {
1409
+ marketId,
1410
+ kind,
1411
+ kindName: orderKindName(kind),
1412
+ timing,
1413
+ orderFee: rawToUsdc(policy.orderFee),
1414
+ budget: rawToUsdc(budget),
1415
+ totalDebit: rawToUsdc(totalDebit),
1416
+ cashNeedRaw,
1417
+ spareCashRaw,
1418
+ ...funding,
1419
+ raw: { orderFee: policy.orderFee, budget, totalDebit },
1420
+ };
1421
+ }
1422
+
1423
+ // Shared by the three queued mints: resolve the market and ticks, build the enqueue (its static
1424
+ // checks run first), then one read for the preflight: queue gates, fee, budget, cash need.
1425
+ async #planMint(
1426
+ owner: string,
1427
+ m: MarketDescriptor,
1428
+ order: { kind: number; maxCostRaw: bigint; quantityRaw?: bigint; maxPremiumRaw?: bigint },
1429
+ build: (
1430
+ orders: OrdersGeneratedConfig,
1431
+ target: {
1432
+ expiryMarketId: string;
1433
+ wrapperId: string;
1434
+ lowerTick: bigint;
1435
+ higherTick: bigint;
1436
+ } & MarketFeeds,
1437
+ ) => (tx: Transaction) => TransactionResult,
1438
+ ): Promise<QueuedOrderPlan> {
1439
+ const orders = this.#requireDelayedExecution();
1440
+ const feeds = this.#feeds(m.underlying);
1441
+ const { id, state: market } = await this.#resolveMarket(m);
1442
+ const { lowerTick, higherTick } = await this.#strikeTicks(m, id, market);
1443
+ const thunk = build(orders, {
1444
+ expiryMarketId: id,
1445
+ wrapperId: this.wrapperIdFor(owner),
1446
+ lowerTick,
1447
+ higherTick,
1448
+ ...feeds,
1449
+ });
1450
+ const state = await this.#queueState(orders, id, { owner });
1451
+ const { policy, timing } = this.#assertQueueOpen(state, 'mint', BigInt(Date.now()));
1452
+ const { budget, cashNeed } = PredictClient.#assertMintOrder(state, policy, order);
1453
+ // The enqueue charges the desk's fee at execution, so hold it to the previewed one.
1454
+ const tx = new Transaction();
1455
+ tx.add(assertOrderFeeAtMost(orders, policy.orderFee));
1456
+ tx.add(thunk);
1457
+ return {
1458
+ transaction: tx,
1459
+ preview: this.#preview(id, order.kind, timing, policy, budget, cashNeed, state.spareCash, {
1460
+ needsFunding: false,
1461
+ fundedInTransaction: false,
1462
+ }),
1463
+ };
1464
+ }
1465
+
1466
+ // A mint's own checks after the queue gates, shared by the mint builders and `read.planMint`:
1467
+ // the order fee, the escrowed budget against the minimum premium, and the market's cash.
1468
+ static #assertMintOrder(
1469
+ state: MarketQueueState,
1470
+ policy: DelayedExecutionPolicy,
1471
+ order: { kind: number; maxCostRaw: bigint; quantityRaw?: bigint; maxPremiumRaw?: bigint },
1472
+ ): { budget: bigint; cashNeed: bigint } {
1473
+ const available = state.account?.availableRaw ?? 0n;
1474
+ const budget = mintBudget({
1475
+ kind: order.kind,
1476
+ maxCostRaw: order.maxCostRaw,
1477
+ availableRaw: available,
1478
+ orderFeeRaw: policy.orderFee,
1479
+ quantityRaw: order.quantityRaw,
1480
+ });
1481
+ if (budget == null) {
1482
+ throw new PredictPreflightError(
1483
+ 'fee',
1484
+ `the balance (${rawToUsdc(available)}) doesn't cover the ${rawToUsdc(policy.orderFee)} order fee`,
1485
+ );
1486
+ }
1487
+ if (budget < MIN_PREMIUM) {
1488
+ throw new PredictPreflightError(
1489
+ 'min-premium',
1490
+ `the escrowed budget ${rawToUsdc(budget)} is below the ${rawToUsdc(MIN_PREMIUM)} minimum premium`,
1491
+ );
1492
+ }
1493
+ if (order.kind !== ORDER_KIND.EXACT_QUANTITY && state.minEntryProbability === 0n) {
1494
+ throw new PredictPreflightError('market-cash', 'this market takes no budget mints');
1495
+ }
1496
+ const cashNeed = mintCashNeed({
1497
+ kind: order.kind,
1498
+ budgetRaw: budget,
1499
+ minEntryProbability: state.minEntryProbability,
1500
+ quantityRaw: order.quantityRaw,
1501
+ maxPremiumRaw: order.maxPremiumRaw,
1502
+ });
1503
+ if (cashNeed > state.spareCash) {
1504
+ throw new PredictPreflightError(
1505
+ 'market-cash',
1506
+ "this market can't take an order this size right now",
1507
+ );
1508
+ }
1509
+ return { budget, cashNeed };
1510
+ }
1511
+
1512
+ // The record a sell names is Open. The chain can't even quote one that isn't (`ERecordNotOpen`).
1513
+ static #assertRecordOpen(state: MarketQueueState, recordId: bigint): QueuedOrder {
1514
+ const record = state.records[0];
1515
+ if (!record || record.status !== ORDER_STATUS.OPEN || record.position.order_id === 0n) {
1516
+ throw new PredictPreflightError(
1517
+ 'record-not-open',
1518
+ `record ${recordId} isn't an Open position`,
1519
+ );
1520
+ }
1521
+ return record;
1522
+ }
1523
+
1524
+ // The quantity an Open record holds, refusing a sell of more than that.
1525
+ #assertSellQuantity(record: QueuedOrder, closeQuantityRaw: bigint): bigint {
1526
+ const held = decodeOrderRange(
1527
+ record.position.order_id,
1528
+ BigInt(this.cfg.units.positionLotSize),
1529
+ ).quantity;
1530
+ if (closeQuantityRaw > held) {
1531
+ throw new PredictInputError(
1532
+ `quantity ${rawToUsdc(closeQuantityRaw)} is above the record's ${rawToUsdc(held)}`,
1533
+ );
1534
+ }
1535
+ return held;
1536
+ }
1537
+
1538
+ // A sell's own checks after the queue gates, shared by `tx.enqueueSell` and `read.planSell`: the
1539
+ // record is Open and the owner's, the order fee, the record's quantity and the minimum sell. A
1540
+ // sell is never refused for market cash.
1541
+ #assertSellOrder(
1542
+ state: MarketQueueState,
1543
+ policy: DelayedExecutionPolicy,
1544
+ recordId: bigint,
1545
+ closeQuantityRaw: bigint,
1546
+ ): void {
1547
+ const record = PredictClient.#assertRecordOpen(state, recordId);
1548
+ if (
1549
+ !state.account ||
1550
+ normalizeSuiAddress(record.account_id) !== normalizeSuiAddress(state.account.accountId)
1551
+ ) {
1552
+ throw new PredictPreflightError(
1553
+ 'not-record-owner',
1554
+ `record ${recordId} belongs to another account`,
1555
+ );
1556
+ }
1557
+ if (!feeCovered('sell', state.account.availableRaw, policy.orderFee)) {
1558
+ throw new PredictPreflightError(
1559
+ 'fee',
1560
+ `the balance doesn't cover the ${rawToUsdc(policy.orderFee)} order fee`,
1561
+ );
1562
+ }
1563
+ const held = this.#assertSellQuantity(record, closeQuantityRaw);
1564
+ const min = policy.minSellQuantity;
1565
+ if (closeQuantityRaw < min || (closeQuantityRaw < held && held - closeQuantityRaw < min)) {
1566
+ throw new PredictPreflightError(
1567
+ 'below-min-sell',
1568
+ `a sell must close at least ${rawToUsdc(min)} and leave either nothing or at least that much`,
1569
+ );
1570
+ }
1571
+ }
1572
+
1573
+ async #planSell(
1574
+ owner: string,
1575
+ m: MarketCoordinates,
1576
+ opts: EnqueueSellOptions,
1577
+ ): Promise<QueuedOrderPlan> {
1578
+ const orders = this.#requireDelayedExecution();
1579
+ const feeds = this.#feeds(m.underlying);
1580
+ const { id } = await this.#resolveMarket(m);
1581
+ const closeQuantityRaw = usdcToRaw(opts.quantity);
1582
+ this.#assertLot(closeQuantityRaw);
1583
+ const thunk = enqueueRedeemOpen(orders, {
1584
+ expiryMarketId: id,
1585
+ wrapperId: this.wrapperIdFor(owner),
1586
+ recordId: opts.recordId,
1587
+ closeQuantityRaw,
1588
+ minProbabilityRaw: probabilityToRaw(opts.minProbability),
1589
+ minProceedsRaw: usdcToRaw(opts.minProceeds),
1590
+ ...feeds,
1591
+ });
1592
+ const state = await this.#queueState(orders, id, { owner, recordIds: [opts.recordId] });
1593
+ const { policy, timing } = this.#assertQueueOpen(state, 'sell', BigInt(Date.now()));
1594
+ this.#assertSellOrder(state, policy, opts.recordId, closeQuantityRaw);
1595
+ const cashNeed = cashNeedSell(closeQuantityRaw, state.backingBufferLambda);
1596
+ const needsFunding = cashNeed > state.spareCash;
1597
+ const fund = opts.fundMarket ?? 'auto';
1598
+ const funded = fund === 'always' || (fund === 'auto' && needsFunding);
1599
+ const tx = new Transaction();
1600
+ // The enqueue charges the desk's fee at execution, so hold it to the previewed one.
1601
+ tx.add(assertOrderFeeAtMost(orders, policy.orderFee));
1602
+ tx.add(thunk);
1603
+ // After the enqueue: its cash need is then in `waiting_cash_need`, which the rebalance funds.
1604
+ if (funded) tx.add(rebalanceExpiryCash(this.#config, { expiryMarketId: id }));
1605
+ return {
1606
+ transaction: tx,
1607
+ preview: this.#preview(
1608
+ id,
1609
+ ORDER_KIND.REDEEM_OPEN,
1610
+ timing,
1611
+ policy,
1612
+ 0n,
1613
+ cashNeed,
1614
+ state.spareCash,
1615
+ {
1616
+ needsFunding,
1617
+ fundedInTransaction: funded,
1618
+ },
1619
+ ),
1620
+ };
1621
+ }
1622
+
1623
+ // A chain mint quote as a queued-fill preview: the congestion penalty comes out of the cost
1624
+ // (a queued fill pays none) and the order fee is reported separately.
1625
+ static #queuedMintQuote(q: MintQuoteRaw, policy: DelayedExecutionPolicy | null): MintQuote {
1626
+ const costRaw = q.allInCost - q.penaltyFee;
1627
+ return {
1628
+ entryProbability: rawToProbability(q.entryProbability),
1629
+ premium: rawToUsdc(q.premium),
1630
+ fees: {
1631
+ trading: rawToUsdc(q.tradingFee),
1632
+ subsidy: rawToUsdc(q.feeIncentiveSubsidy),
1633
+ builder: rawToUsdc(q.builderFee),
1634
+ penalty: 0,
1635
+ // The chain quote doesn't split it out. It is part of `trading`, never extra.
1636
+ referral: 0,
1637
+ inventoryImpact: rawToUsdc(q.inventoryImpactCharge),
1638
+ },
1639
+ cost: rawToUsdc(costRaw),
1640
+ quantity: rawToUsdc(q.quantity),
1641
+ raw: {
1642
+ premium: q.premium,
1643
+ cost: costRaw,
1644
+ quantity: q.quantity,
1645
+ entryProbability: q.entryProbability,
1646
+ },
1647
+ feesExact: true,
1648
+ queued: true,
1649
+ orderFee: policy ? rawToUsdc(policy.orderFee) : undefined,
1650
+ };
1651
+ }
1652
+
606
1653
  // === tx builders ===
607
1654
  // Each returns a ready-to-sign Transaction. Market-resolving builders are async.
608
1655
  readonly tx = {
@@ -690,13 +1737,29 @@ export class PredictClient {
690
1737
  return tx;
691
1738
  },
692
1739
 
1740
+ /**
1741
+ * Immediate exact-quantity mint (`mint_exact_quantity`).
1742
+ * @deprecated Retired by delayed execution (DBU-885): Predict v4 always aborts it
1743
+ * (`EDelayedExecutionRequired`), so it works only while the config calls a pre-v4 package.
1744
+ * Use {@link PredictClient.tx.enqueueMint}, and `read.executionMode()` to know which path a
1745
+ * network is on.
1746
+ */
693
1747
  mint: (owner: string, m: MarketDescriptor, opts: MintOptions): Promise<Transaction> =>
694
1748
  this.#buildMint(owner, m, opts),
695
1749
 
696
- /** V2 all-in budget mint; use minQuantity to protect the fill against slippage. */
1750
+ /**
1751
+ * V2 all-in budget mint; use minQuantity to protect the fill against slippage.
1752
+ * @deprecated Retired by delayed execution (DBU-885): Predict v4 always aborts it
1753
+ * (`EDelayedExecutionRequired`). Use `tx.enqueueMintCost`.
1754
+ */
697
1755
  mintCost: (owner: string, m: MarketDescriptor, opts: MintCostOptions): Promise<Transaction> =>
698
1756
  this.#buildMintCost(owner, m, opts),
699
1757
 
1758
+ /**
1759
+ * Immediate premium-budget mint (`mint_exact_amount`).
1760
+ * @deprecated Retired by delayed execution (DBU-885): Predict v4 always aborts it
1761
+ * (`EDelayedExecutionRequired`). Use `tx.enqueueMintAmount`.
1762
+ */
700
1763
  mintAmount: async (
701
1764
  owner: string,
702
1765
  m: MarketDescriptor,
@@ -727,9 +1790,21 @@ export class PredictClient {
727
1790
  );
728
1791
  },
729
1792
 
1793
+ /**
1794
+ * Immediate close of an account position (`redeem_live`).
1795
+ * @deprecated Retired by delayed execution (DBU-885): Predict v4 always aborts it
1796
+ * (`EDelayedExecutionRequired`). A position held in the account then has no early exit and
1797
+ * is paid at settlement with `claimSettled`. Queued fills are Open records in the market's
1798
+ * queue, sold with `tx.enqueueSell`.
1799
+ */
730
1800
  redeem: (owner: string, m: MarketDescriptor, opts: CloseOptions): Promise<Transaction> =>
731
1801
  this.#buildRedeem(owner, m, opts),
732
1802
 
1803
+ /**
1804
+ * Pay a settled position held in the ACCOUNT (`redeem_settled`), in full. Unchanged by delayed
1805
+ * execution, for positions minted before it. It doesn't pay Open queue records: the queue's
1806
+ * settlement walk (`settle_step`) pays those to the account's wrapper address.
1807
+ */
733
1808
  claimSettled: async (
734
1809
  owner: string,
735
1810
  m: Pick<MarketDescriptor, 'underlying' | 'expiryMs' | 'marketId'>,
@@ -745,6 +1820,272 @@ export class PredictClient {
745
1820
  );
746
1821
  },
747
1822
 
1823
+ // --- delayed execution (DBU-885) ---
1824
+ // Each queued-order builder reads the market once and refuses, with a typed
1825
+ // PredictPreflightError, an order the queue or protocol gates would abort, so a refused order
1826
+ // never fails a larger transaction. The order's own limits at the current price are checked
1827
+ // only on chain. It returns the transaction and a display preview. Every builder throws
1828
+ // PredictInputError while the network's config doesn't record delayed execution.
1829
+
1830
+ /**
1831
+ * Queue a mint of an exact payout quantity, filled at Pyth's price for its τ. Escrows
1832
+ * `min(maxCost, quantity, available − fee)` plus the order fee. `maxCost` and
1833
+ * `maxProbability` are required caps (see `queue.slippageBand`). Size against
1834
+ * `read.queue(...).maxMint`. The record ID is in the enqueue's `OrderEnqueued`
1835
+ * (`decode.enqueue`).
1836
+ */
1837
+ enqueueMint: async (
1838
+ owner: string,
1839
+ m: MarketDescriptor,
1840
+ opts: EnqueueMintOptions,
1841
+ ): Promise<QueuedOrderPlan> => {
1842
+ const quantityRaw = usdcToRaw(opts.quantity);
1843
+ this.#assertLot(quantityRaw);
1844
+ const maxCostRaw = usdcToRaw(opts.maxCost);
1845
+ const maxProbabilityRaw = probabilityToRaw(opts.maxProbability);
1846
+ return this.#planMint(
1847
+ owner,
1848
+ m,
1849
+ { kind: ORDER_KIND.EXACT_QUANTITY, maxCostRaw, quantityRaw },
1850
+ (orders, target) =>
1851
+ enqueueExactQuantity(orders, {
1852
+ ...target,
1853
+ quantityRaw,
1854
+ maxCostRaw,
1855
+ maxProbabilityRaw,
1856
+ }),
1857
+ );
1858
+ },
1859
+
1860
+ /**
1861
+ * Queue a premium-budget mint: sized at τ under `spend`, at least `minQuantity`, with the
1862
+ * all-in debit capped by the required `maxCost`.
1863
+ */
1864
+ enqueueMintAmount: async (
1865
+ owner: string,
1866
+ m: MarketDescriptor,
1867
+ opts: EnqueueMintAmountOptions,
1868
+ ): Promise<QueuedOrderPlan> => {
1869
+ const maxPremiumRaw = usdcToRaw(opts.spend);
1870
+ const minQuantityRaw = usdcToRaw(opts.minQuantity);
1871
+ const maxCostRaw = usdcToRaw(opts.maxCost);
1872
+ // The fill buys at most `spend` of premium, so a spend below the minimum premium fails
1873
+ // the chain's placement dry run whatever the budget. Refused before any read.
1874
+ if (maxPremiumRaw < MIN_PREMIUM) {
1875
+ throw new PredictPreflightError(
1876
+ 'min-premium',
1877
+ `spend ${rawToUsdc(maxPremiumRaw)} is below the ${rawToUsdc(MIN_PREMIUM)} minimum premium`,
1878
+ );
1879
+ }
1880
+ return this.#planMint(
1881
+ owner,
1882
+ m,
1883
+ { kind: ORDER_KIND.EXACT_AMOUNT, maxCostRaw, maxPremiumRaw },
1884
+ (orders, target) =>
1885
+ enqueueExactAmount(orders, {
1886
+ ...target,
1887
+ maxPremiumRaw,
1888
+ minQuantityRaw,
1889
+ maxCostRaw,
1890
+ }),
1891
+ );
1892
+ },
1893
+
1894
+ /**
1895
+ * Queue an all-in-budget mint (the default mint): sized at τ so the all-in cost fits
1896
+ * `spend`, at least `minQuantity`. Escrows `min(spend, available − fee)` plus the order fee.
1897
+ */
1898
+ enqueueMintCost: async (
1899
+ owner: string,
1900
+ m: MarketDescriptor,
1901
+ opts: EnqueueMintCostOptions,
1902
+ ): Promise<QueuedOrderPlan> => {
1903
+ const maxCostRaw = usdcToRaw(opts.spend);
1904
+ const minQuantityRaw = usdcToRaw(opts.minQuantity);
1905
+ return this.#planMint(
1906
+ owner,
1907
+ m,
1908
+ { kind: ORDER_KIND.EXACT_COST, maxCostRaw },
1909
+ (orders, target) => enqueueExactCost(orders, { ...target, maxCostRaw, minQuantityRaw }),
1910
+ );
1911
+ },
1912
+
1913
+ /**
1914
+ * Queue an early sell of an Open record (`enqueue_redeem_open`), the only early sell in v4.
1915
+ * The record must be Open and the owner's. Sells are never refused for market cash: when the
1916
+ * sell's cash need is above spare cash the preview sets `needsFunding`, and by default the
1917
+ * builder adds `rebalance_expiry_cash` after the enqueue (see `fundMarket`). A sell the market
1918
+ * still can't cover at the fill is refunded in full (reason 8) and the position returns to an
1919
+ * Open record.
1920
+ */
1921
+ enqueueSell: (
1922
+ owner: string,
1923
+ m: MarketCoordinates,
1924
+ opts: EnqueueSellOptions,
1925
+ ): Promise<QueuedOrderPlan> => this.#planSell(owner, m, opts),
1926
+
1927
+ /**
1928
+ * Queue the order a `read.planMint` or `read.planSell` plan describes, with its limits:
1929
+ * `enqueueMintCost`, `enqueueMint` or `enqueueSell`, by `plan.order.builder`. Pass the
1930
+ * market the plan was made for: the descriptor a mint plan was planned with, or the
1931
+ * coordinates `read.planSell` took for a sell plan. Throws the plan's `refusal` as a
1932
+ * `PredictPreflightError`, and `'fee'` when the balance now escrows less than the plan's
1933
+ * budget.
1934
+ */
1935
+ enqueuePlan: async <P extends MintPlan | SellPlan>(
1936
+ owner: string,
1937
+ m: P extends SellPlan ? MarketCoordinates : MarketDescriptor,
1938
+ plan: P,
1939
+ ): Promise<QueuedOrderPlan> => {
1940
+ // A plan that reported a refusal is refused the same way, so the two never disagree.
1941
+ // Plan again once the reason clears.
1942
+ if (plan.refusal != null) {
1943
+ throw new PredictPreflightError(plan.refusal, `the plan was refused (${plan.refusal})`);
1944
+ }
1945
+ // A plan's limits fit one order. Queued for another owner, market, strike or record, they
1946
+ // would buy or sell a different contract, so plan that order instead.
1947
+ const target = plan.target;
1948
+ const { id, state: marketState } = await this.#resolveMarket(m);
1949
+ if (normalizeSuiAddress(owner) !== normalizeSuiAddress(target.owner)) {
1950
+ throw new PredictInputError(`the plan was made for owner ${target.owner}, not ${owner}`);
1951
+ }
1952
+ if (normalizeSuiAddress(id) !== normalizeSuiAddress(target.expiryMarketId)) {
1953
+ throw new PredictInputError(
1954
+ `the plan was made for market ${target.expiryMarketId}, not ${id}`,
1955
+ );
1956
+ }
1957
+ const order = plan.order;
1958
+ // A mint needs the descriptor's strike and side. A plan typed only as
1959
+ // `MintPlan | SellPlan` can pass bare coordinates, so check.
1960
+ const descriptor = 'side' in m ? (m as MarketDescriptor) : null;
1961
+ let placed: QueuedOrderPlan;
1962
+ if (order.builder === 'enqueueSell') {
1963
+ if (order.options.recordId !== target.recordId) {
1964
+ throw new PredictInputError(
1965
+ `the plan was made for record ${target.recordId}, not ${order.options.recordId}`,
1966
+ );
1967
+ }
1968
+ placed = await this.tx.enqueueSell(owner, m, order.options);
1969
+ } else {
1970
+ if (descriptor == null) {
1971
+ throw new PredictInputError(
1972
+ 'a mint plan needs the market descriptor it was planned with (its strike and side)',
1973
+ );
1974
+ }
1975
+ const { lowerTick, higherTick } = await this.#strikeTicks(descriptor, id, marketState);
1976
+ if (lowerTick !== target.lowerTick || higherTick !== target.higherTick) {
1977
+ throw new PredictInputError(
1978
+ 'the market descriptor names a different strike or side than the plan, or its reference strike moved: plan again',
1979
+ );
1980
+ }
1981
+ placed =
1982
+ order.builder === 'enqueueMintCost'
1983
+ ? await this.tx.enqueueMintCost(owner, descriptor, order.options)
1984
+ : await this.tx.enqueueMint(owner, descriptor, order.options);
1985
+ }
1986
+ // The plan's debit, and a sell's net, assume the order fee it was quoted with. The builders
1987
+ // read the desk's fee again, and no order argument bounds it on chain, so refuse a fee
1988
+ // that rose since the plan.
1989
+ if (placed.preview.raw.orderFee > plan.raw.orderFee) {
1990
+ throw new PredictInputError(
1991
+ `the order fee rose from ${rawToUsdc(plan.raw.orderFee)} to ${placed.preview.orderFee} since the plan: plan again`,
1992
+ );
1993
+ }
1994
+ if (order.builder === 'enqueueSell') return placed;
1995
+ // The limits assume the plan's whole budget. The builders escrow less when the balance
1996
+ // fell since the plan, so refuse that instead.
1997
+ const planned = (plan as MintPlan).raw.budget;
1998
+ if (placed.preview.raw.budget < planned) {
1999
+ throw new PredictPreflightError(
2000
+ 'fee',
2001
+ `the balance now escrows ${placed.preview.budget}, below the plan's ${rawToUsdc(planned)}`,
2002
+ );
2003
+ }
2004
+ return placed;
2005
+ },
2006
+
2007
+ /**
2008
+ * Refund this market's orders past their deadline. For the app's "Refund my order" button
2009
+ * only (`OrderView.canRequestRefund`), never prepended to other transactions: keepers
2010
+ * refund on their own. Needs no account, session or Pyth key, and works during a freeze.
2011
+ * `maxOrders` (default 100) bounds the records visited.
2012
+ */
2013
+ refund: async (
2014
+ m: MarketCoordinates,
2015
+ opts: { maxOrders?: number } = {},
2016
+ ): Promise<Transaction> => {
2017
+ const orders = this.#requireDelayedExecution();
2018
+ const { id } = await this.#resolveMarket(m);
2019
+ const queueId = await this.#existingQueueId(orders, id);
2020
+ return txOf(
2021
+ refund(orders, {
2022
+ expiryMarketId: id,
2023
+ queueId,
2024
+ maxOrders: BigInt(opts.maxOrders ?? 100),
2025
+ }),
2026
+ );
2027
+ },
2028
+
2029
+ /**
2030
+ * Send a finished record's parked funds to its receive address (`queue::claim_parked`): a
2031
+ * refund or change the address couldn't take while it was on USDC's deny list or USDC was
2032
+ * paused (`OrderView` `parkedRaw`). A skipped settlement payout isn't parked: pay it with
2033
+ * `payOpen`. Permissionless, and a no-op returning 0 while
2034
+ * the address is still denied. Needs no account, session or Pyth key.
2035
+ */
2036
+ claimParked: async (m: MarketCoordinates, recordId: bigint): Promise<Transaction> => {
2037
+ const orders = this.#requireDelayedExecution();
2038
+ const { id } = await this.#resolveMarket(m);
2039
+ const queueId = await this.#existingQueueId(orders, id);
2040
+ return txOf(claimParked(orders, { expiryMarketId: id, queueId, recordId }));
2041
+ },
2042
+
2043
+ /**
2044
+ * Pay a settled market's Open record that the payout walk skipped (`queue::pay_open`), for
2045
+ * example while its receive address was denied. Permissionless: the payout goes only to the
2046
+ * record's own receive address. Aborts `EMarketNotSettled` before settlement.
2047
+ */
2048
+ payOpen: async (m: MarketCoordinates, recordId: bigint): Promise<Transaction> => {
2049
+ const orders = this.#requireDelayedExecution();
2050
+ const { id } = await this.#resolveMarket(m);
2051
+ const queueId = await this.#existingQueueId(orders, id);
2052
+ return txOf(payOpen(orders, { expiryMarketId: id, queueId, recordId }));
2053
+ },
2054
+
2055
+ /**
2056
+ * The open filler: verify signed Pyth Lazer payloads, commit them to the market's waiting
2057
+ * cohorts, then resolve up to `maxOrders` (default 15) records. Reads the current Lazer
2058
+ * package from Lazer's `State` first (`lazerStateId`, or `config.oracle.pythLazerState`).
2059
+ */
2060
+ fill: async (
2061
+ m: MarketCoordinates,
2062
+ opts: {
2063
+ payloads: readonly Uint8Array[];
2064
+ maxOrders?: number;
2065
+ refundOverdue?: boolean;
2066
+ lazerStateId?: string;
2067
+ },
2068
+ ): Promise<Transaction> => {
2069
+ const orders = this.#requireDelayedExecution();
2070
+ const stateId = opts.lazerStateId ?? this.cfg.oracle?.pythLazerState;
2071
+ if (!stateId) {
2072
+ throw new PredictInputError('fill needs the Pyth Lazer State: pass `lazerStateId`');
2073
+ }
2074
+ const { id } = await this.#resolveMarket(m);
2075
+ const queueId = await this.#existingQueueId(orders, id);
2076
+ const lazer = await lazerPackages(this.#client, stateId);
2077
+ return txOf(
2078
+ fill(orders, {
2079
+ expiryMarketId: id,
2080
+ queueId,
2081
+ payloads: opts.payloads,
2082
+ lazer,
2083
+ maxOrders: BigInt(opts.maxOrders ?? 15),
2084
+ refundOverdue: opts.refundOverdue,
2085
+ }),
2086
+ );
2087
+ },
2088
+
748
2089
  // Queue a supply request pulling `amountUsdc` from the account's existing custody
749
2090
  // balance. `request_supply` auto-settles USDC then `account.withdraw`s the payment
750
2091
  // into queue escrow; the PLP fill is delivered at the next flush, not returned here.
@@ -907,28 +2248,674 @@ export class PredictClient {
907
2248
  return { ...boardPricer(snap), asOf: snap.sources };
908
2249
  },
909
2250
 
910
- // Exact pre-trade quote: dry-runs the caller's own mint (same tx as
911
- // tx.mint) and decodes the receipt. Requires a funded account; throws
912
- // the same typed errors the real trade would — quote doubles as preflight.
2251
+ /**
2252
+ * Exact pre-trade quote for an exact-quantity mint. Requires a funded account.
2253
+ *
2254
+ * Where the config records the delayed-execution upgrade (`packages.predictDelayedExecution`),
2255
+ * this reads Predict's own `quote_mint_for_account` (the immediate mint is retired there) and
2256
+ * the order desk's fee, and previews a QUEUED fill at the clock: `queued: true`, no
2257
+ * congestion penalty in `cost`, and the order fee in `orderFee`. The fill itself prices at
2258
+ * its committed Pyth tick. It throws `PredictInputError` when that config lacks the
2259
+ * order-flow package or its desk. Elsewhere it dry-runs the immediate mint and decodes its
2260
+ * receipt, as before.
2261
+ */
913
2262
  quoteMint: async (
914
2263
  owner: string,
915
2264
  m: MarketDescriptor,
916
2265
  opts: Pick<MintOptions, 'quantity'>,
917
2266
  ): Promise<MintQuote> => {
2267
+ if (this.cfg.packages.predictDelayedExecution) {
2268
+ const orders = this.#requireDelayedExecution();
2269
+ const feeds = this.#feeds(m.underlying);
2270
+ const { id, state } = await this.#resolveMarket(m);
2271
+ const quantityRaw = usdcToRaw(opts.quantity);
2272
+ this.#assertLot(quantityRaw);
2273
+ const { lowerTick, higherTick } = await this.#strikeTicks(m, id, state);
2274
+ const { quote, policy } = await quoteMintForAccount(this.#client, this.#config, {
2275
+ expiryMarketId: id,
2276
+ wrapperId: this.wrapperIdFor(owner),
2277
+ lowerTick,
2278
+ higherTick,
2279
+ request: { shape: 'exact-quantity', quantityRaw },
2280
+ ordersConfig: orders,
2281
+ ...feeds,
2282
+ });
2283
+ return PredictClient.#queuedMintQuote(quote, policy);
2284
+ }
918
2285
  const tx = await this.#buildMint(owner, m, opts);
919
2286
  return this.#quoteMintTransaction(owner, tx);
920
2287
  },
921
2288
 
922
- /** Simulate the v2 all-in budget mint against current account and market state. */
2289
+ /**
2290
+ * Quote the all-in budget mint against current account and market state. Where the config
2291
+ * records the delayed-execution upgrade, it reads `quote_mint_exact_cost_for_account` at
2292
+ * `min(spend, available − fee)`, the budget enqueue escrows, and previews a queued fill as
2293
+ * `quoteMint` does (and throws as it does on a partial config). Elsewhere it simulates the
2294
+ * immediate v2 mint.
2295
+ */
923
2296
  quoteMintCost: async (
924
2297
  owner: string,
925
2298
  m: MarketDescriptor,
926
2299
  opts: MintCostOptions,
927
- ): Promise<MintQuote> =>
928
- this.#quoteMintTransaction(owner, await this.#buildMintCost(owner, m, opts)),
2300
+ ): Promise<MintQuote> => {
2301
+ if (this.cfg.packages.predictDelayedExecution) {
2302
+ const orders = this.#requireDelayedExecution();
2303
+ const feeds = this.#feeds(m.underlying);
2304
+ const { id, state } = await this.#resolveMarket(m);
2305
+ const { lowerTick, higherTick } = await this.#strikeTicks(m, id, state);
2306
+ const { policy, availableRaw } = await orderFeeAndBalance(
2307
+ this.#client,
2308
+ orders,
2309
+ owner,
2310
+ this.cfg.quoteCoinType,
2311
+ );
2312
+ const fee = policy.orderFee;
2313
+ const spendRaw = usdcToRaw(opts.spend);
2314
+ const escrowable = availableRaw > fee ? availableRaw - fee : 0n;
2315
+ const { quote } = await quoteMintForAccount(this.#client, this.#config, {
2316
+ expiryMarketId: id,
2317
+ wrapperId: this.wrapperIdFor(owner),
2318
+ lowerTick,
2319
+ higherTick,
2320
+ request: {
2321
+ shape: 'exact-cost',
2322
+ maxCostRaw: spendRaw < escrowable ? spendRaw : escrowable,
2323
+ minQuantityRaw: usdcToRaw(opts.minQuantity),
2324
+ },
2325
+ ...feeds,
2326
+ });
2327
+ return PredictClient.#queuedMintQuote(quote, policy);
2328
+ }
2329
+ return this.#quoteMintTransaction(owner, await this.#buildMintCost(owner, m, opts));
2330
+ },
2331
+
2332
+ /**
2333
+ * Quote an early sell of an Open record (the queue's `quote_redeem_open`) at a fresh live
2334
+ * pricer. `proceeds` is before the order fee, with no congestion penalty, and `net` takes the
2335
+ * fee off. Throws `ERecordNotOpen` when the record isn't Open.
2336
+ */
2337
+ quoteSell: async (
2338
+ owner: string,
2339
+ m: MarketCoordinates,
2340
+ opts: { recordId: bigint; quantity: number },
2341
+ ): Promise<SellQuote> => {
2342
+ const orders = this.#requireDelayedExecution();
2343
+ const feeds = this.#feeds(m.underlying);
2344
+ const { id } = await this.#resolveMarket(m);
2345
+ const closeQuantityRaw = usdcToRaw(opts.quantity);
2346
+ this.#assertLot(closeQuantityRaw);
2347
+ const queueId = await this.#existingQueueId(orders, id);
2348
+ const { quote, policy } = await quoteRedeemOpen(this.#client, orders, {
2349
+ expiryMarketId: id,
2350
+ queueId,
2351
+ wrapperId: this.wrapperIdFor(owner),
2352
+ recordId: opts.recordId,
2353
+ closeQuantityRaw,
2354
+ ...feeds,
2355
+ });
2356
+ const orderFee = policy.orderFee;
2357
+ return {
2358
+ probability: rawToProbability(quote.probability),
2359
+ proceeds: rawToUsdc(quote.proceeds),
2360
+ net: rawToUsdc(quote.proceeds - orderFee),
2361
+ fees: {
2362
+ trading: rawToUsdc(quote.tradingFee),
2363
+ builder: rawToUsdc(quote.builderFee),
2364
+ inventoryImpactRebate: rawToUsdc(quote.inventoryImpactRebate),
2365
+ order: rawToUsdc(orderFee),
2366
+ },
2367
+ quantityClosed: rawToUsdc(quote.closeQuantity),
2368
+ raw: {
2369
+ probability: quote.probability,
2370
+ proceeds: quote.proceeds,
2371
+ orderFee,
2372
+ quantityClosed: quote.closeQuantity,
2373
+ },
2374
+ };
2375
+ },
2376
+
2377
+ /**
2378
+ * Which trade path the network is on, from `ProtocolConfig.version_watermark` and whether
2379
+ * this config records delayed execution (the Predict upgrade, the order-flow companion and
2380
+ * its desk). Flip the app between `mint*` and `enqueue*` on it, without a redeploy at the
2381
+ * watermark bump.
2382
+ */
2383
+ executionMode: async (): Promise<ExecutionMode> =>
2384
+ executionModeFor(
2385
+ await versionWatermark(this.#client, this.cfg.objects.protocolConfig),
2386
+ this.#recordsDelayedExecution(),
2387
+ this.cfg.packages.predictDelayedExecution != null,
2388
+ ),
2389
+
2390
+ /**
2391
+ * A market's queue in one read: cash, the stuck gate ("pricing delayed"), cohorts, counters
2392
+ * against capacity, the desk's policy, a τ/deadline/cutoff preview, and the largest mint
2393
+ * spare cash admits now. With `owner` (who must have an account), also the account's
2394
+ * waiting orders and balance. Throws `PredictPreflightError` `'no-queue'` while the market
2395
+ * has no `MarketQueue`.
2396
+ */
2397
+ queue: async (m: MarketCoordinates, owner?: string): Promise<MarketQueueView> => {
2398
+ const orders = this.#requireDelayedExecution();
2399
+ const { id } = await this.#resolveMarket(m);
2400
+ const state = await this.#queueState(orders, id, { owner });
2401
+ const nowMs = BigInt(Date.now());
2402
+ const policy = state.desk.policy;
2403
+ const mode = executionModeFor(state.protocol.versionWatermark, true);
2404
+ const timing = previewTiming({
2405
+ nowMs,
2406
+ policy,
2407
+ heads: state.heads,
2408
+ expiryMs: state.expiryMs,
2409
+ noTradeWindowMs: state.protocol.noTradeWindowMs,
2410
+ });
2411
+ // The builders' own gate, so the flags can never disagree with their preflight.
2412
+ const refusalFor = (side: 'mint' | 'sell'): PredictPreflightCode | null => {
2413
+ try {
2414
+ this.#assertQueueOpen(state, side, nowMs);
2415
+ return null;
2416
+ } catch (e) {
2417
+ if (e instanceof PredictPreflightError) return e.code;
2418
+ throw e;
2419
+ }
2420
+ };
2421
+ const refusal = { mint: refusalFor('mint'), sell: refusalFor('sell') };
2422
+ return {
2423
+ ...state,
2424
+ mode,
2425
+ timing,
2426
+ acceptingMints: refusal.mint == null,
2427
+ acceptingSells: refusal.sell == null,
2428
+ refusal,
2429
+ maxMint: {
2430
+ exactQuantity: maxMintNow({
2431
+ shape: 'exact-quantity',
2432
+ spareCashRaw: state.spareCash,
2433
+ minEntryProbability: state.minEntryProbability,
2434
+ lotSize: BigInt(this.cfg.units.positionLotSize),
2435
+ asOfMs: nowMs,
2436
+ }),
2437
+ budget: maxMintNow({
2438
+ shape: 'budget',
2439
+ spareCashRaw: state.spareCash,
2440
+ minEntryProbability: state.minEntryProbability,
2441
+ availableRaw: state.account?.availableRaw,
2442
+ orderFeeRaw: policy.orderFee,
2443
+ asOfMs: nowMs,
2444
+ }),
2445
+ },
2446
+ };
2447
+ },
2448
+
2449
+ /**
2450
+ * Plan a queued mint for a purchase form: the quote at the current price, the payout and
2451
+ * multiple, the order fee read from the desk, the escrow and debit, the balance, the most
2452
+ * the market takes now, the timing and the queue gates, plus the enqueue options that carry
2453
+ * the slippage. `amount` plans an all-in spend (`enqueueMintCost`, refunded below the payout
2454
+ * floor), and `quantity` an exact payout (`enqueueMint`, capped by probability and cost).
2455
+ * Works for a visitor without an account: the quote then comes from `quote_mint`.
2456
+ *
2457
+ * `slippageCents` is cents per contract, never a percentage: a 10¢ limit fills while the
2458
+ * all-in price per contract stays within 10¢ of the quote's price without its fee subsidy.
2459
+ */
2460
+ planMint: async (
2461
+ owner: string,
2462
+ m: MarketDescriptor,
2463
+ opts: PlanMintOptions,
2464
+ ): Promise<MintPlan> => {
2465
+ const orders = this.#requireDelayedExecution();
2466
+ const feeds = this.#feeds(m.underlying);
2467
+ const { id, state: market } = await this.#resolveMarket(m);
2468
+ const { lowerTick, higherTick } = await this.#strikeTicks(m, id, market);
2469
+ const hasAccount = await this.#hasAccount(owner);
2470
+ const state = await this.#queueState(orders, id, hasAccount ? { owner } : {});
2471
+ const nowMs = BigInt(Date.now());
2472
+ const policy = state.desk.policy;
2473
+ const fee = policy.orderFee;
2474
+ const availableRaw = state.account?.availableRaw ?? null;
2475
+ const lot = BigInt(this.cfg.units.positionLotSize);
2476
+ const ticks = { expiryMarketId: id, lowerTick, higherTick, ...feeds };
2477
+ const forAccount = (request: MintQuoteRequest) =>
2478
+ quoteMintForAccount(this.#client, this.#config, {
2479
+ ...ticks,
2480
+ wrapperId: this.wrapperIdFor(owner),
2481
+ request,
2482
+ }).then((r) => r.quote);
2483
+
2484
+ let quote: MintQuoteRaw;
2485
+ let quoteForAccount: boolean;
2486
+ let budgetRequested: bigint | null = null;
2487
+ if ('amount' in opts) {
2488
+ const amountRaw = usdcToRaw(opts.amount);
2489
+ budgetRequested =
2490
+ (opts.orderFee ?? 'inclusive') === 'inclusive'
2491
+ ? amountRaw > fee
2492
+ ? amountRaw - fee
2493
+ : 0n
2494
+ : amountRaw;
2495
+ // No budget below the minimum premium can be admitted, and the account's quote
2496
+ // aborts `EOrderFailsLimits` on one, so refuse it as a visitor's search does. An
2497
+ // amount the order fee takes whole buys nothing either.
2498
+ if (budgetRequested === 0n) {
2499
+ throw new PredictPreflightError(
2500
+ 'min-premium',
2501
+ `${opts.amount} doesn't cover the ${rawToUsdc(fee)} order fee, so it buys no premium`,
2502
+ );
2503
+ }
2504
+ if (budgetRequested < MIN_PREMIUM) {
2505
+ throw new PredictPreflightError(
2506
+ 'min-premium',
2507
+ `a budget of ${rawToUsdc(budgetRequested)} is below the minimum premium (a mint's premium must be at least ${rawToUsdc(MIN_PREMIUM)} USDC)`,
2508
+ );
2509
+ }
2510
+ // The account's own quote caps the budget at its balance, so it only previews an
2511
+ // order the balance covers.
2512
+ quoteForAccount =
2513
+ availableRaw != null && availableRaw > fee && availableRaw - fee >= budgetRequested;
2514
+ const budget = budgetRequested;
2515
+ const search = () =>
2516
+ this.#searchBudgetQuote(ticks, budget, lot).catch(async (error: unknown) => {
2517
+ // Every probe refused ends the search in 'min-premium', which a strike outside
2518
+ // the entry band also explains.
2519
+ if (error instanceof PredictPreflightError && error.code === 'min-premium') {
2520
+ throw (await this.#entryBandRefusal(m)) ?? error;
2521
+ }
2522
+ throw await this.#quoteRefusal(error, { m });
2523
+ });
2524
+ if (quoteForAccount) {
2525
+ try {
2526
+ quote = await forAccount({
2527
+ shape: 'exact-cost',
2528
+ maxCostRaw: budget,
2529
+ minQuantityRaw: 0n,
2530
+ });
2531
+ } catch (error) {
2532
+ if (!isOrderFailsLimits(error)) throw await this.#quoteRefusal(error);
2533
+ // The chain refused the budget at the account's own pricing, its builder fee
2534
+ // included. The account-free search only diagnoses the refusal: a strike outside
2535
+ // the entry band, or a budget too small for the minimum premium, throws its typed
2536
+ // refusal. It leaves the builder fee out, so a budget only it can size is never
2537
+ // planned, and keeps the chain's own error. (Near $1 a contract, admission's
2538
+ // payout-bound step-down isn't monotone, so a slightly smaller budget can pass
2539
+ // where this one is refused. That is a false refusal, never a bad order.)
2540
+ await search();
2541
+ throw error;
2542
+ }
2543
+ } else {
2544
+ quote = await search();
2545
+ }
2546
+ } else {
2547
+ const quantityRaw = usdcToRaw(opts.quantity);
2548
+ this.#assertLot(quantityRaw);
2549
+ quoteForAccount = hasAccount;
2550
+ try {
2551
+ quote = hasAccount
2552
+ ? await forAccount({ shape: 'exact-quantity', quantityRaw })
2553
+ : (
2554
+ await quoteMintAnonymous(this.#client, this.#config, {
2555
+ ...ticks,
2556
+ request: { shape: 'exact-quantity', quantityRaw },
2557
+ })
2558
+ ).quote;
2559
+ } catch (error) {
2560
+ throw await this.#quoteRefusal(error, { m, quantityRaw });
2561
+ }
2562
+ }
2563
+
2564
+ // The expected cost, after the fee subsidy. The limits add the subsidy back, because
2565
+ // admission checks the order without it (`EOrderFailsLimits`).
2566
+ const costRaw = quote.allInCost - quote.penaltyFee;
2567
+ const slippage = PredictClient.#appliedSlippage(opts, quote.entryProbability, state, nowMs);
2568
+ const limitsIn = {
2569
+ quoteCostRaw: costRaw,
2570
+ quoteQuantityRaw: quote.quantity,
2571
+ entryProbabilityRaw: quote.entryProbability,
2572
+ slippageRaw: slippage.raw,
2573
+ feeIncentiveSubsidyRaw: quote.feeIncentiveSubsidy,
2574
+ lotSize: lot,
2575
+ };
2576
+ let budgetRaw: bigint;
2577
+ let minQuantityRaw: bigint;
2578
+ // What admission buys at least, for a budget plan, or why it buys nothing the plan can
2579
+ // preview.
2580
+ let admitted:
2581
+ | { quantity: bigint }
2582
+ | { refusal: 'min-premium' | 'cost-above-payout' | 'entry-band' }
2583
+ | null = null;
2584
+ let maxCostRaw: bigint | null = null;
2585
+ let maxProbabilityRaw: bigint | null = null;
2586
+ let price: { nowRaw: bigint; worstRaw: bigint };
2587
+ let order: MintPlan['order'];
2588
+ if (budgetRequested != null) {
2589
+ const limits = budgetMintLimits({ ...limitsIn, budgetRaw: budgetRequested });
2590
+ budgetRaw = budgetRequested;
2591
+ // Admission sizes the fill without the subsidy by Move's exact arithmetic, whose
2592
+ // per-component rounding (inventory impact included) an average price can't bound.
2593
+ // So the floor's ceiling is checked with exact quotes. It also never exceeds the
2594
+ // quote's own quantity, since a searched quote can leave part of the budget unspent.
2595
+ // The first probe divides the budget by the quote's unsubsidized price, with no rounding
2596
+ // allowance, so a minimum-sized purchase isn't probed one lot short.
2597
+ const unsubsidizedCost = costRaw + quote.feeIncentiveSubsidy;
2598
+ const start =
2599
+ unsubsidizedCost === 0n
2600
+ ? quote.quantity
2601
+ : ((budgetRaw * quote.quantity) / unsubsidizedCost / lot) * lot;
2602
+ admitted = await this.#admittedFloor(
2603
+ (quantityRaw) =>
2604
+ quoteForAccount
2605
+ ? forAccount({ shape: 'exact-quantity', quantityRaw })
2606
+ : quoteMintAnonymous(this.#client, this.#config, {
2607
+ ...ticks,
2608
+ request: { shape: 'exact-quantity', quantityRaw },
2609
+ }).then((r) => r.quote),
2610
+ budgetRaw,
2611
+ start < quote.quantity ? start : quote.quantity,
2612
+ lot,
2613
+ quote.entryProbability,
2614
+ quote.quantity === 0n ? 0n : (unsubsidizedCost * 1_000_000_000n) / quote.quantity,
2615
+ );
2616
+ const ceiling = 'quantity' in admitted ? admitted.quantity : 0n;
2617
+ minQuantityRaw = limits.minQuantityRaw < ceiling ? limits.minQuantityRaw : ceiling;
2618
+ price = limits.pricePerContract;
2619
+ order = {
2620
+ builder: 'enqueueMintCost',
2621
+ options: { spend: rawToUsdc(budgetRaw), minQuantity: rawToUsdc(minQuantityRaw) },
2622
+ };
2623
+ } else {
2624
+ const limits = exactMintLimits(limitsIn);
2625
+ maxCostRaw = limits.maxCostRaw;
2626
+ maxProbabilityRaw = limits.maxProbabilityRaw;
2627
+ // Enqueue escrows `min(max_cost, quantity, available − fee)`.
2628
+ budgetRaw = maxCostRaw < quote.quantity ? maxCostRaw : quote.quantity;
2629
+ minQuantityRaw = quote.quantity;
2630
+ price = limits.pricePerContract;
2631
+ order = {
2632
+ builder: 'enqueueMint',
2633
+ options: {
2634
+ quantity: rawToUsdc(quote.quantity),
2635
+ maxCost: rawToUsdc(maxCostRaw),
2636
+ maxProbability: rawToProbability(maxProbabilityRaw),
2637
+ },
2638
+ };
2639
+ }
2640
+
2641
+ const totalDebitRaw = budgetRaw + fee;
2642
+ const expectedDebitRaw = costRaw + fee;
2643
+ const inclusive = 'amount' in opts && (opts.orderFee ?? 'inclusive') === 'inclusive';
2644
+ // The builder's own preflight on the plan's order, plus what only the quote shows: the
2645
+ // balance against the whole debit the limits assume, the payout bound, and the premium a
2646
+ // budget buys at admission, which prices without the fee subsidy.
2647
+ const refusal = PredictClient.#refusal(() => {
2648
+ this.#assertQueueOpen(state, 'mint', nowMs);
2649
+ if (availableRaw == null || availableRaw < totalDebitRaw) {
2650
+ throw new PredictPreflightError(
2651
+ 'fee',
2652
+ `the balance doesn't cover the plan's ${rawToUsdc(totalDebitRaw)} debit`,
2653
+ );
2654
+ }
2655
+ PredictClient.#assertMintOrder(
2656
+ state,
2657
+ policy,
2658
+ budgetRequested != null
2659
+ ? { kind: ORDER_KIND.EXACT_COST, maxCostRaw: budgetRaw }
2660
+ : {
2661
+ kind: ORDER_KIND.EXACT_QUANTITY,
2662
+ maxCostRaw: maxCostRaw ?? budgetRaw,
2663
+ quantityRaw: quote.quantity,
2664
+ },
2665
+ );
2666
+ if (costRaw + quote.feeIncentiveSubsidy > quote.quantity) {
2667
+ throw new PredictPreflightError(
2668
+ 'cost-above-payout',
2669
+ 'the all-in cost without the fee subsidy is above the payout',
2670
+ );
2671
+ }
2672
+ if (admitted != null && 'refusal' in admitted) {
2673
+ throw new PredictPreflightError(
2674
+ admitted.refusal,
2675
+ admitted.refusal === 'min-premium'
2676
+ ? `without the fee subsidy, ${rawToUsdc(budgetRaw)} buys no fill that clears the ${rawToUsdc(MIN_PREMIUM)} minimum premium`
2677
+ : admitted.refusal === 'entry-band'
2678
+ ? "the entry probability left the market's entry band while the order was sized: plan again, or pick a strike nearer the money"
2679
+ : `without the fee subsidy, ${rawToUsdc(budgetRaw)} buys a fill near $1 a contract, whose admission sizing can't be previewed`,
2680
+ );
2681
+ }
2682
+ });
2683
+ const cashOrBudgetMax = maxMintNow(
2684
+ budgetRequested != null
2685
+ ? {
2686
+ shape: 'budget',
2687
+ spareCashRaw: state.spareCash,
2688
+ minEntryProbability: state.minEntryProbability,
2689
+ availableRaw: availableRaw ?? undefined,
2690
+ orderFeeRaw: fee,
2691
+ asOfMs: nowMs,
2692
+ }
2693
+ : {
2694
+ shape: 'exact-quantity',
2695
+ spareCashRaw: state.spareCash,
2696
+ minEntryProbability: state.minEntryProbability,
2697
+ lotSize: lot,
2698
+ asOfMs: nowMs,
2699
+ },
2700
+ ).maxRaw;
2701
+ // `maxMintNow` bounds an exact quantity by spare cash only. Enqueue escrows the quantity
2702
+ // at the plan's worst price per contract, capped at the $1 payout, so the balance bounds
2703
+ // it too.
2704
+ let max = cashOrBudgetMax;
2705
+ if (budgetRequested == null && availableRaw != null && price.worstRaw > 0n) {
2706
+ const spendable = availableRaw > fee ? availableRaw - fee : 0n;
2707
+ const perContract = price.worstRaw < 1_000_000_000n ? price.worstRaw : 1_000_000_000n;
2708
+ const byBalance = ((spendable * 1_000_000_000n) / perContract / lot) * lot;
2709
+ if (max == null || byBalance < max) max = byBalance;
2710
+ }
2711
+ return {
2712
+ shape: budgetRequested != null ? 'budget' : 'exact-quantity',
2713
+ target: { owner: normalizeSuiAddress(owner), expiryMarketId: id, lowerTick, higherTick },
2714
+ quote: PredictClient.#queuedMintQuote(quote, policy),
2715
+ quoteForAccount,
2716
+ slippage: slippage.applied,
2717
+ potentialPayout: rawToUsdc(quote.quantity),
2718
+ minPayout: rawToUsdc(minQuantityRaw),
2719
+ payoutMultiple: Number(quote.quantity) / Number(expectedDebitRaw),
2720
+ minPayoutMultiple: Number(minQuantityRaw) / Number(totalDebitRaw),
2721
+ pricePerContract: rawToProbability(price.nowRaw),
2722
+ worstPricePerContract: rawToProbability(price.worstRaw),
2723
+ orderFee: rawToUsdc(fee),
2724
+ budget: rawToUsdc(budgetRaw),
2725
+ totalDebit: rawToUsdc(totalDebitRaw),
2726
+ expectedDebit: rawToUsdc(expectedDebitRaw),
2727
+ balance: {
2728
+ hasAccount,
2729
+ available: availableRaw == null ? null : rawToUsdc(availableRaw),
2730
+ covers: availableRaw == null ? null : availableRaw >= totalDebitRaw,
2731
+ },
2732
+ maxNow: max == null ? null : rawToUsdc(inclusive ? max + fee : max),
2733
+ timing: previewTiming({
2734
+ nowMs,
2735
+ policy,
2736
+ heads: state.heads,
2737
+ expiryMs: state.expiryMs,
2738
+ noTradeWindowMs: state.protocol.noTradeWindowMs,
2739
+ }),
2740
+ accepting: refusal == null,
2741
+ refusal,
2742
+ order,
2743
+ raw: {
2744
+ budget: budgetRaw,
2745
+ orderFee: fee,
2746
+ totalDebit: totalDebitRaw,
2747
+ slippage: slippage.raw,
2748
+ minQuantity: minQuantityRaw,
2749
+ maxCost: maxCostRaw,
2750
+ maxProbability: maxProbabilityRaw,
2751
+ pricePerContract: price.nowRaw,
2752
+ worstPricePerContract: price.worstRaw,
2753
+ },
2754
+ };
2755
+ },
2756
+
2757
+ /**
2758
+ * Plan a queued early sell of an Open record: the quote at the current price, net of the
2759
+ * order fee, the floors at the worst price the slippage allows, the timing and the
2760
+ * builder's preflight, plus the `enqueueSell` options that carry the floors. A record that
2761
+ * isn't Open has no quote, so it throws `PredictPreflightError` `'record-not-open'` instead.
2762
+ */
2763
+ planSell: async (
2764
+ owner: string,
2765
+ m: MarketCoordinates,
2766
+ opts: PlanSellOptions,
2767
+ ): Promise<SellPlan> => {
2768
+ const orders = this.#requireDelayedExecution();
2769
+ const { id } = await this.#resolveMarket(m);
2770
+ const state = await this.#queueState(orders, id, { owner, recordIds: [opts.recordId] });
2771
+ // No quote exists for a record that isn't Open, or for more than it holds, so those
2772
+ // refusals throw.
2773
+ const record = PredictClient.#assertRecordOpen(state, opts.recordId);
2774
+ this.#assertSellQuantity(record, usdcToRaw(opts.quantity));
2775
+ const quote = await this.read
2776
+ .quoteSell(owner, m, { recordId: opts.recordId, quantity: opts.quantity })
2777
+ .catch(async (error: unknown) => {
2778
+ throw await this.#quoteRefusal(error);
2779
+ });
2780
+ const nowMs = BigInt(Date.now());
2781
+ const slippage = PredictClient.#appliedSlippage(opts, quote.raw.probability, state, nowMs);
2782
+ const limits = sellLimits({
2783
+ proceedsRaw: quote.raw.proceeds,
2784
+ closeQuantityRaw: quote.raw.quantityClosed,
2785
+ probabilityRaw: quote.raw.probability,
2786
+ slippageRaw: slippage.raw,
2787
+ });
2788
+ const fee = quote.raw.orderFee;
2789
+ // The builder's own preflight on the sell.
2790
+ const refusal = PredictClient.#refusal(() => {
2791
+ const { policy } = this.#assertQueueOpen(state, 'sell', nowMs);
2792
+ this.#assertSellOrder(state, policy, opts.recordId, usdcToRaw(opts.quantity));
2793
+ });
2794
+ return {
2795
+ target: { owner: normalizeSuiAddress(owner), expiryMarketId: id, recordId: opts.recordId },
2796
+ quote,
2797
+ slippage: slippage.applied,
2798
+ proceeds: quote.proceeds,
2799
+ net: quote.net,
2800
+ minProceeds: rawToUsdc(limits.minProceedsRaw),
2801
+ minNet: rawToUsdc(limits.minProceedsRaw - fee),
2802
+ orderFee: rawToUsdc(fee),
2803
+ timing: previewTiming({
2804
+ nowMs,
2805
+ policy: state.desk.policy,
2806
+ heads: state.heads,
2807
+ expiryMs: state.expiryMs,
2808
+ noTradeWindowMs: state.protocol.noTradeWindowMs,
2809
+ }),
2810
+ accepting: refusal == null,
2811
+ refusal,
2812
+ order: {
2813
+ builder: 'enqueueSell',
2814
+ options: {
2815
+ recordId: opts.recordId,
2816
+ quantity: opts.quantity,
2817
+ minProbability: rawToProbability(limits.minProbabilityRaw),
2818
+ minProceeds: rawToUsdc(limits.minProceedsRaw),
2819
+ },
2820
+ },
2821
+ raw: {
2822
+ slippage: slippage.raw,
2823
+ minProceeds: limits.minProceedsRaw,
2824
+ minProbability: limits.minProbabilityRaw,
2825
+ orderFee: fee,
2826
+ },
2827
+ };
2828
+ },
2829
+
2830
+ /** One queue record and its display state, or null when missing or cleaned up. */
2831
+ order: async (m: MarketCoordinates, recordId: bigint): Promise<QueuedOrderView | null> =>
2832
+ (await this.read.orders(m, [recordId]))[0],
929
2833
 
930
- // Exact pre-close quote: dry-runs the caller's own redeem and decodes
931
- // the receipt — the informed close against the floor-less deployed redeem.
2834
+ /**
2835
+ * Queue records by record ID, with their display states. The SDK has no indexer: the app
2836
+ * supplies record IDs from its enqueue receipts or its indexer.
2837
+ */
2838
+ orders: async (
2839
+ m: MarketCoordinates,
2840
+ recordIds: readonly bigint[],
2841
+ ): Promise<(QueuedOrderView | null)[]> => {
2842
+ const orders = this.#requireDelayedExecution();
2843
+ const { id } = await this.#resolveMarket(m);
2844
+ const queueId = await this.#existingQueueId(orders, id);
2845
+ const records = await queuedOrders(
2846
+ this.#client,
2847
+ orders,
2848
+ { expiryMarketId: id, queueId },
2849
+ recordIds,
2850
+ );
2851
+ const nowMs = BigInt(Date.now());
2852
+ return records.map((record, i) =>
2853
+ record
2854
+ ? {
2855
+ recordId: recordIds[i],
2856
+ record,
2857
+ view: orderView(record, nowMs, {
2858
+ lotSize: BigInt(this.cfg.units.positionLotSize),
2859
+ }),
2860
+ }
2861
+ : null,
2862
+ );
2863
+ },
2864
+
2865
+ /**
2866
+ * Poll a record until it is filled, refunded or closed (`pollMs`, default 250), or until
2867
+ * `timeoutMs` (default 30 s). The fullnode a client reads can trail the enqueue's
2868
+ * execution, so a record not seen yet is polled again until the timeout, and ends as
2869
+ * `'gone'` only then. A record that disappears after it was seen ends as `'gone'` at once.
2870
+ * The record's result is enough for the UI; the fee breakdown is in the
2871
+ * `QueuedOrderFilled` event.
2872
+ */
2873
+ waitForOutcome: async (
2874
+ m: MarketCoordinates,
2875
+ recordId: bigint,
2876
+ opts: { pollMs?: number; timeoutMs?: number; signal?: AbortSignal } = {},
2877
+ ): Promise<QueuedOrderOutcome> => {
2878
+ const pollMs = opts.pollMs ?? 250;
2879
+ const deadline = Date.now() + (opts.timeoutMs ?? 30_000);
2880
+ let seen = false;
2881
+ for (;;) {
2882
+ const order = await this.read.order(m, recordId);
2883
+ const timedOut = Date.now() + pollMs > deadline;
2884
+ if (!order) {
2885
+ if (seen || timedOut) return { outcome: 'gone', order: null };
2886
+ } else {
2887
+ seen = true;
2888
+ const state = order.view.state;
2889
+ if (state === 'filled' || state === 'refunded' || state === 'closed') {
2890
+ return { outcome: state, order };
2891
+ }
2892
+ if (timedOut) return { outcome: 'timeout', order };
2893
+ }
2894
+ await sleep(pollMs, opts.signal);
2895
+ }
2896
+ },
2897
+
2898
+ /**
2899
+ * USDC sent to the owner's account wrapper that the account hasn't settled yet: refunds,
2900
+ * sell proceeds and settled payouts of Open records. Already counted in `read.balance`; for
2901
+ * display.
2902
+ */
2903
+ pendingFunds: async (owner: string): Promise<number> =>
2904
+ rawToUsdc(await pendingFunds(this.#client, this.wrapperIdFor(owner), this.cfg.quoteCoinType)),
2905
+
2906
+ /** The current Pyth Lazer package, for a filler. Read it per batch and never cache it. */
2907
+ lazerPackages: (stateId?: string): Promise<LazerPackages> => {
2908
+ const id = stateId ?? this.cfg.oracle?.pythLazerState;
2909
+ if (!id) throw new PredictInputError('pass the Pyth Lazer State id');
2910
+ return lazerPackages(this.#client, id);
2911
+ },
2912
+
2913
+ /**
2914
+ * Exact pre-close quote for an ACCOUNT position: dry-runs the immediate redeem and decodes
2915
+ * the receipt.
2916
+ * @deprecated `redeem_live` is retired by delayed execution (DBU-885): Predict v4 always
2917
+ * aborts it. Queued fills are Open records: quote them with `read.quoteSell`.
2918
+ */
932
2919
  quoteRedeem: async (
933
2920
  owner: string,
934
2921
  m: MarketDescriptor,
@@ -976,6 +2963,36 @@ export class PredictClient {
976
2963
  };
977
2964
  },
978
2965
 
2966
+ /**
2967
+ * The market's settlement price, or `null` until a keeper settles it. Unlike `market`,
2968
+ * `price` and the quotes, which abort `ELivePricingExpired` once the market expires, it
2969
+ * keeps working after expiry, when an app shows results. With delayed execution the
2970
+ * keeper settles a market, pays its Open records and cleans them up within about a second
2971
+ * of expiry, so `read.order` returns `null` by then. Keep each filled record's
2972
+ * `position.orderId` and pass it to `cost.settledPayout` with this price.
2973
+ */
2974
+ settlement: async (m: MarketCoordinates): Promise<MarketSettlement> => {
2975
+ const { id, state } = await this.#resolveMarket(m);
2976
+ const raw = await settlementPrice(this.#client, this.#config, id);
2977
+ return {
2978
+ marketId: id,
2979
+ expiryMs: state.expiryMs,
2980
+ settled: raw != null,
2981
+ settlementPrice: raw == null ? null : fromRaw(raw, 9),
2982
+ settlementPriceRaw: raw,
2983
+ tickSizeRaw: state.tickSizeRaw,
2984
+ };
2985
+ },
2986
+
2987
+ /**
2988
+ * The fee and exposure policy the market snapshotted at creation, ready to pass to the
2989
+ * `cost` functions as `fees`. A market keeps it for its whole life, so it can differ from
2990
+ * the protocol's current template, and neither recorded deployment's template matches
2991
+ * `cost.SHIPPED_FEE_POLICY` any more. One object read.
2992
+ */
2993
+ feePolicy: async (m: MarketCoordinates): Promise<FeePolicy> =>
2994
+ marketFeePolicy(this.#client, (await this.#resolveMarket(m)).id),
2995
+
979
2996
  balance: async (owner: string): Promise<number> =>
980
2997
  rawToUsdc(await accountBalance(this.#client, this.#config, owner, this.cfg.quoteCoinType)),
981
2998
 
@@ -1021,5 +3038,51 @@ export class PredictClient {
1021
3038
  exactlyOne(decodePlpCancels(this.cfg, r), 'RequestCancelled'),
1022
3039
  builderCode: (r: DecodableTransactionResult) =>
1023
3040
  exactlyOne(decodeBuilderCodeSets(this.cfg, r), 'BuilderCodeSet'),
3041
+
3042
+ // --- delayed execution (DBU-885) --- the queue events are matched against the order-flow
3043
+ // companion's original ID (`packages.predictOrdersV1 ?? predictOrders`), the Predict events
3044
+ // the upgrade added against `packages.predictDelayedExecution`. Each throws
3045
+ // PredictInputError while the config doesn't record the package it needs.
3046
+
3047
+ /** The trader's `OrderEnqueued` receipt: the record ID, τ, deadline, escrow and fee. */
3048
+ enqueue: (r: DecodableTransactionResult) =>
3049
+ exactlyOne(decodeEnqueues(this.cfg, r), 'OrderEnqueued'),
3050
+ enqueues: (r: DecodableTransactionResult) => decodeEnqueues(this.cfg, r),
3051
+ /** Every queue event, tagged, in chain order: feed it to `queue.reduceOrderEvents`. */
3052
+ queueEvents: (r: DecodableTransactionResult) => decodeQueueEvents(this.cfg, r),
3053
+ cohortCommits: (r: DecodableTransactionResult) => decodeCohortCommits(this.cfg, r),
3054
+ queuedFills: (r: DecodableTransactionResult) => decodeQueuedFills(this.cfg, r),
3055
+ queuedRefunds: (r: DecodableTransactionResult) => decodeQueuedRefunds(this.cfg, r),
3056
+ openRecordPayouts: (r: DecodableTransactionResult) => decodeOpenRecordPayouts(this.cfg, r),
3057
+ marketPayoutsCompleted: (r: DecodableTransactionResult) =>
3058
+ decodeMarketPayoutsCompleted(this.cfg, r),
3059
+ queueOps: (r: DecodableTransactionResult) => decodeQueueOps(this.cfg, r),
3060
+ /** `RecordFundsParked` and `RecordFundsClaimed`: USDC a record keeps until it can be sent. */
3061
+ recordFunds: (r: DecodableTransactionResult) => decodeRecordFunds(this.cfg, r),
3062
+ policyUpdates: (r: DecodableTransactionResult) => decodePolicyUpdates(this.cfg, r),
3063
+ /**
3064
+ * `ExpiryPnlRealized` changes in the pool's gross realized result, for P&L reporting. Sum
3065
+ * them with `realizedPnlRaw`.
3066
+ */
3067
+ expiryPnlRealized: (r: DecodableTransactionResult) => decodeExpiryPnlRealized(this.cfg, r),
1024
3068
  };
1025
3069
  }
3070
+
3071
+ // Resolve after `ms`, or reject with the signal's reason once it aborts.
3072
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
3073
+ return new Promise((resolve, reject) => {
3074
+ if (signal?.aborted) {
3075
+ reject(signal.reason);
3076
+ return;
3077
+ }
3078
+ const onAbort = () => {
3079
+ clearTimeout(timer);
3080
+ reject(signal?.reason);
3081
+ };
3082
+ const timer = setTimeout(() => {
3083
+ signal?.removeEventListener('abort', onAbort);
3084
+ resolve();
3085
+ }, ms);
3086
+ signal?.addEventListener('abort', onAbort, { once: true });
3087
+ });
3088
+ }