@mysten/deepbook-v3 2.5.1 → 2.6.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 (101) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/PREDICT.md +160 -12
  3. package/dist/contracts/account/account.d.mts +23 -23
  4. package/dist/contracts/account/account_events.d.mts +20 -20
  5. package/dist/contracts/account/account_events.d.mts.map +1 -1
  6. package/dist/contracts/account/account_registry.d.mts +13 -13
  7. package/dist/contracts/deepbook/balances.d.mts +4 -4
  8. package/dist/contracts/deepbook/order.d.mts +12 -12
  9. package/dist/contracts/deepbook_predict/admin.d.mts +4 -4
  10. package/dist/contracts/deepbook_predict/builder_code.d.mts +11 -11
  11. package/dist/contracts/deepbook_predict/builder_code.d.mts.map +1 -1
  12. package/dist/contracts/deepbook_predict/builder_code_events.d.mts +10 -10
  13. package/dist/contracts/deepbook_predict/config_events.d.mts +67 -67
  14. package/dist/contracts/deepbook_predict/expiry_market.d.mts +176 -96
  15. package/dist/contracts/deepbook_predict/expiry_market.d.mts.map +1 -1
  16. package/dist/contracts/deepbook_predict/expiry_market.mjs +114 -1
  17. package/dist/contracts/deepbook_predict/expiry_market.mjs.map +1 -1
  18. package/dist/contracts/deepbook_predict/market_lifecycle_cap.d.mts +5 -5
  19. package/dist/contracts/deepbook_predict/market_manager.d.mts +27 -27
  20. package/dist/contracts/deepbook_predict/order_events.d.mts +50 -50
  21. package/dist/contracts/deepbook_predict/pause_cap.d.mts +5 -5
  22. package/dist/contracts/deepbook_predict/plp.d.mts +72 -72
  23. package/dist/contracts/deepbook_predict/pool_valuation_cap.d.mts +5 -5
  24. package/dist/contracts/deepbook_predict/predict_account.d.mts +14 -14
  25. package/dist/contracts/deepbook_predict/pricing.d.mts +53 -53
  26. package/dist/contracts/deepbook_predict/protocol_config.d.mts +64 -64
  27. package/dist/contracts/deepbook_predict/range_codec.d.mts +2 -2
  28. package/dist/contracts/deepbook_predict/registry.d.mts +28 -28
  29. package/dist/contracts/deepbook_predict/strike_exposure.mjs +12 -0
  30. package/dist/contracts/deepbook_predict/strike_exposure.mjs.map +1 -1
  31. package/dist/contracts/deepbook_predict/vault_events.d.mts +105 -105
  32. package/dist/contracts/deepbook_sessions/session_config.d.mts +4 -4
  33. package/dist/contracts/deepbook_sessions/sessions.d.mts +35 -10
  34. package/dist/contracts/deepbook_sessions/sessions.d.mts.map +1 -1
  35. package/dist/contracts/deepbook_sessions/sessions.mjs +44 -1
  36. package/dist/contracts/deepbook_sessions/sessions.mjs.map +1 -1
  37. package/dist/deployments/mainnet.d.mts.map +1 -1
  38. package/dist/deployments/mainnet.mjs +2 -0
  39. package/dist/deployments/mainnet.mjs.map +1 -1
  40. package/dist/deployments/testnet.d.mts.map +1 -1
  41. package/dist/deployments/testnet.mjs +4 -2
  42. package/dist/deployments/testnet.mjs.map +1 -1
  43. package/dist/deployments/types.d.mts +1 -1
  44. package/dist/predict/client.d.mts +12 -3
  45. package/dist/predict/client.d.mts.map +1 -1
  46. package/dist/predict/client.mjs +39 -18
  47. package/dist/predict/client.mjs.map +1 -1
  48. package/dist/predict/config/generated.d.mts +3 -2
  49. package/dist/predict/config/generated.d.mts.map +1 -1
  50. package/dist/predict/config/generated.mjs +1 -0
  51. package/dist/predict/config/generated.mjs.map +1 -1
  52. package/dist/predict/config/types.d.mts +3 -0
  53. package/dist/predict/config/types.d.mts.map +1 -1
  54. package/dist/predict/cost.d.mts +353 -0
  55. package/dist/predict/cost.d.mts.map +1 -0
  56. package/dist/predict/cost.mjs +555 -0
  57. package/dist/predict/cost.mjs.map +1 -0
  58. package/dist/predict/decode.mjs +6 -7
  59. package/dist/predict/decode.mjs.map +1 -1
  60. package/dist/predict/index.d.mts +3 -2
  61. package/dist/predict/index.mjs +2 -1
  62. package/dist/predict/pricing.mjs +1 -1
  63. package/dist/predict/reads/positions.mjs +1 -1
  64. package/dist/predict/reads/positions.mjs.map +1 -1
  65. package/dist/predict/tx/trade.d.mts.map +1 -1
  66. package/dist/predict/tx/trade.mjs +18 -2
  67. package/dist/predict/tx/trade.mjs.map +1 -1
  68. package/dist/sessions.d.mts +17 -1
  69. package/dist/sessions.d.mts.map +1 -1
  70. package/dist/sessions.mjs +22 -2
  71. package/dist/sessions.mjs.map +1 -1
  72. package/dist/transactions/balanceManager.d.mts +12 -12
  73. package/dist/transactions/deepbook.d.mts +20 -20
  74. package/dist/transactions/deepbookAdmin.d.mts +4 -4
  75. package/dist/transactions/deepbookAdmin.d.mts.map +1 -1
  76. package/dist/transactions/marginAdmin.d.mts +7 -7
  77. package/dist/transactions/marginAdmin.d.mts.map +1 -1
  78. package/dist/transactions/marginLiquidations.d.mts +3 -3
  79. package/dist/transactions/marginMaintainer.d.mts +5 -5
  80. package/dist/transactions/marginMaintainer.d.mts.map +1 -1
  81. package/dist/transactions/marginManager.d.mts +32 -32
  82. package/dist/transactions/marginPool.d.mts +18 -18
  83. package/dist/transactions/marginRegistry.d.mts +16 -16
  84. package/dist/transactions/marginTPSL.d.mts +10 -10
  85. package/dist/transactions/poolProxy.d.mts +8 -8
  86. package/package.json +2 -2
  87. package/src/contracts/deepbook_predict/expiry_market.ts +160 -0
  88. package/src/contracts/deepbook_predict/strike_exposure.ts +14 -0
  89. package/src/contracts/deepbook_sessions/sessions.ts +68 -0
  90. package/src/deployments/mainnet.ts +4 -1
  91. package/src/deployments/testnet.ts +6 -3
  92. package/src/deployments/types.ts +4 -2
  93. package/src/predict/client.ts +80 -27
  94. package/src/predict/config/generated.ts +4 -2
  95. package/src/predict/config/types.ts +3 -0
  96. package/src/predict/cost.ts +1022 -0
  97. package/src/predict/decode.ts +8 -8
  98. package/src/predict/index.ts +9 -0
  99. package/src/predict/reads/positions.ts +1 -1
  100. package/src/predict/tx/trade.ts +30 -1
  101. package/src/sessions.ts +37 -2
@@ -0,0 +1,1022 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Client-side ALL-IN cost math for Predict trades — the fee layer `pricing.ts` stops short of.
4
+ // `pricing.*` answers "what is this contract worth?" (the probability); this module answers
5
+ // "what does the chain debit me for it?" and "what does closing it credit me?", with NO chain
6
+ // call: no `devInspect`, no dry run, no simulate.
7
+ //
8
+ // It is an EXACT integer port of the deployed cost path, function for function:
9
+ //
10
+ // mint `expiry_market::mint_quote_at` / `compute_mint_quote`
11
+ // -> premium + (trading_fee - fee_incentive_subsidy) + builder_fee + penalty_fee
12
+ // + inventory_impact_charge
13
+ // budget `expiry_market::quote_exact_cost_terms` (the `mint_exact_cost` lot search)
14
+ // redeem `expiry_market::redeem_live`'s payment decomposition
15
+ // -> gross + inventory_impact_rebate - trading_fee - builder_fee - penalty
16
+ // fees `strike_exposure_config::trading_fee` (per-boundary Bernoulli fee + expiry ramp),
17
+ // `expiry_market::builder_fee_amount`, `ewma::penalty_fee`,
18
+ // `strike_exposure::mint_range_inventory_impact` / `live_close_inventory_impact`
19
+ //
20
+ // Every rounding step is the chain's: `mul_down`, `mul_div_down` and a bit-identical
21
+ // `sqrt_down` (same Newton schedule as `fixed_math::math`), all on bigints. So given the same
22
+ // inputs the chain uses, these return the chain's numbers to the raw unit — the fee layer
23
+ // carries no approximation of its own.
24
+ //
25
+ // Local float probabilities (`pricing.upProbability`, ~1e-4) introduce approximation.
26
+ // With raw probabilities AND the same fee, account, book and clock inputs as execution,
27
+ // the arithmetic matches the chain. `exactProbabilities` only identifies the probability
28
+ // input format; it does not certify the source or freshness of any state. A nonzero
29
+ // inventory-impact policy requires book data. Congestion defaults to a zero rate; supply
30
+ // the actual rate when enabled. Use `read.quoteMint` / `read.quoteRedeem` for a simulation
31
+ // against current account and market state, including the execution gates these previews
32
+ // do not check (ownership, pauses, trade window, oracle freshness and cash backing).
33
+ //
34
+ // The fee POLICY is a per-market snapshot taken at creation (`StrikeExposureConfig`), and the
35
+ // chain exposes no getter for `base_fee`/`min_fee` — take it from the market's `MarketCreated`
36
+ // event (an indexer, or `decode`), or use {@link SHIPPED_FEE_POLICY} for the shipped template.
37
+
38
+ import { PredictInputError } from './errors.js';
39
+ import type { PricerInputs } from './pricing.js';
40
+ import { upProbability } from './pricing.js';
41
+ import { POS_INF_TICK } from './ticks.js';
42
+ import { fromRaw, U64_MAX, usdcToRaw } from './units.js';
43
+
44
+ // === Protocol constants (mirrors of `deepbook_predict::constants` / `fixed_math::math`) ===
45
+
46
+ /** `math::float_scaling` — 1e9 is 1.0 for every rate and probability here. */
47
+ export const FLOAT_SCALING = 1_000_000_000n;
48
+ /** `constants::position_lot_size` — quantity is an integer number of these. */
49
+ export const POSITION_LOT_SIZE = 10_000n;
50
+ /** `constants::min_premium` — a mint below this premium aborts `EPremiumBelowMinimum`. */
51
+ export const MIN_PREMIUM = 1_000_000n;
52
+ /** `constants::builder_fee_multiplier` — the builder's cut of the trading fee (10%). */
53
+ export const BUILDER_FEE_MULTIPLIER = 100_000_000n;
54
+ /** `constants::max_builder_fee_rate` — cap on the builder fee as a share of quantity (0.5%). */
55
+ export const MAX_BUILDER_FEE_RATE = 5_000_000n;
56
+ /** `constants::fee_incentive_subsidy_rate` — sponsor share of a trader's mint fee (20%). */
57
+ export const FEE_INCENTIVE_SUBSIDY_RATE = 200_000_000n;
58
+ /** `order::max_quantity_lots` — the order ID's 32-bit lot field. */
59
+ export const MAX_QUANTITY_LOTS = (1n << 32n) - 1n;
60
+
61
+ // === Fixed-point primitives (mirrors of `fixed_math::math`) ===
62
+
63
+ const mulDown = (x: bigint, y: bigint): bigint => (x * y) / FLOAT_SCALING;
64
+ const divDown = (x: bigint, y: bigint): bigint => (x * FLOAT_SCALING) / y;
65
+ const mulDivDown = (x: bigint, y: bigint, denominator: bigint): bigint => (x * y) / denominator;
66
+ const min = (a: bigint, b: bigint): bigint => (a < b ? a : b);
67
+ const max = (a: bigint, b: bigint): bigint => (a > b ? a : b);
68
+
69
+ // Move's unsigned types enforce these domains before arithmetic. JS bigint does not.
70
+ function assertUint(value: bigint, name: string, maximum = U64_MAX, minimum = 0n): void {
71
+ if (typeof value !== 'bigint' || value < minimum || value > maximum) {
72
+ throw new PredictInputError(
73
+ `${name} must be an integer in [${minimum}, ${maximum}], got ${value}`,
74
+ );
75
+ }
76
+ }
77
+
78
+ // `math::sqrt_initial_guess_u128` — power-of-two seed for the Newton iteration.
79
+ function sqrtInitialGuess(x: bigint): bigint {
80
+ let bits = 0n;
81
+ let val = x;
82
+ if (val >= 1n << 64n) {
83
+ val >>= 64n;
84
+ bits += 64n;
85
+ }
86
+ if (val >= 1n << 32n) {
87
+ val >>= 32n;
88
+ bits += 32n;
89
+ }
90
+ if (val >= 1n << 16n) {
91
+ val >>= 16n;
92
+ bits += 16n;
93
+ }
94
+ if (val >= 1n << 8n) {
95
+ val >>= 8n;
96
+ bits += 8n;
97
+ }
98
+ if (val >= 1n << 4n) {
99
+ val >>= 4n;
100
+ bits += 4n;
101
+ }
102
+ if (val >= 1n << 2n) {
103
+ val >>= 2n;
104
+ bits += 2n;
105
+ }
106
+ if (val >= 1n << 1n) bits += 1n;
107
+ return 1n << ((bits + 1n) / 2n);
108
+ }
109
+
110
+ // `math::sqrt_u128_down` — seven Newton steps then one correcting decrement, exactly as the
111
+ // chain runs them, so this agrees with the contract raw unit for raw unit rather than merely
112
+ // to within a rounding step.
113
+ function sqrtU128Down(x: bigint): bigint {
114
+ if (x === 0n) return 0n;
115
+ if (x < 4n) return 1n;
116
+ let g = sqrtInitialGuess(x);
117
+ for (let i = 0; i < 7; i++) g = (g + x / g) / 2n;
118
+ if (g > x / g) g -= 1n;
119
+ return g;
120
+ }
121
+
122
+ /** `math::sqrt_down` — square root of a 1e9-scaled value, 1e9-scaled, rounded down. */
123
+ export function sqrtDown(x: bigint): bigint {
124
+ assertUint(x, 'sqrt input');
125
+ return sqrtU128Down(x * FLOAT_SCALING);
126
+ }
127
+
128
+ // === Policy and state inputs ===
129
+
130
+ /**
131
+ * One market's fee policy: the `StrikeExposureConfig` it snapshotted at creation, in raw 1e9
132
+ * rates. Snapshotted means later admin changes do NOT reprice a market already trading, so
133
+ * read it per market (the `MarketCreated` event carries every field) rather than assuming the
134
+ * template. The last three are the inventory-impact parameters, inert while
135
+ * `inventoryImpactMaxRate` is `0n` (the shipped value).
136
+ */
137
+ export interface FeePolicy {
138
+ /** `base_fee` — multiplies `sqrt(p·(1−p))`. */
139
+ baseFee: bigint;
140
+ /** `min_fee` — per-unit floor, applied per boundary leg before the ramp. */
141
+ minFee: bigint;
142
+ /** `expiry_fee_window_ms` — window before expiry over which the fee ramps. */
143
+ expiryFeeWindowMs: bigint;
144
+ /** `expiry_fee_max_multiplier` — multiplier reached at expiry; 1e9 disables the ramp. */
145
+ expiryFeeMaxMultiplier: bigint;
146
+ /** `min_entry_probability` — mint admission floor on the entry probability. */
147
+ minEntryProbability: bigint;
148
+ /** `max_entry_probability` — mint admission ceiling on the entry probability. */
149
+ maxEntryProbability: bigint;
150
+ /** `inventory_impact_max_rate` — maximum marginal impact rate; `0n` disables impact. */
151
+ inventoryImpactMaxRate: bigint;
152
+ /** `inventory_impact_scale` — the market's `max_expiry_allocation`, the curve's `B`. */
153
+ inventoryImpactScale: bigint;
154
+ /** `backing_buffer_lambda` — the buffer on non-peak payout in the liability formula. */
155
+ backingBufferLambda: bigint;
156
+ }
157
+
158
+ /**
159
+ * The shipped template (`config_constants` defaults): 10% Bernoulli fee, a 2.2% per-leg floor,
160
+ * a one-day ramp window that is inert at a 1.0 multiplier, a 1%–99% entry band, and inventory
161
+ * impact disabled. A market that was created under these values charges exactly this; one
162
+ * created after an admin change does not, which is why the per-market snapshot is the real
163
+ * answer. Verify against the deployment's `MarketCreated` before pricing money on it.
164
+ */
165
+ export const SHIPPED_FEE_POLICY: FeePolicy = Object.freeze({
166
+ baseFee: 100_000_000n,
167
+ minFee: 22_000_000n,
168
+ expiryFeeWindowMs: 86_400_000n,
169
+ expiryFeeMaxMultiplier: FLOAT_SCALING,
170
+ minEntryProbability: 10_000_000n,
171
+ maxEntryProbability: 990_000_000n,
172
+ inventoryImpactMaxRate: 0n,
173
+ inventoryImpactScale: 0n,
174
+ backingBufferLambda: 310_000_000n,
175
+ });
176
+
177
+ /** A range's two boundary UP probabilities, raw 1e9. `null` is an infinite boundary — the
178
+ * −∞ lower of a DOWN order and the +∞ higher of an UP order — which is priced at the digital
179
+ * limit and pays no fee leg. */
180
+ export interface Boundaries {
181
+ /** `P(settle > lower)`, or `null` for the −∞ lower bound. */
182
+ lowerUp: bigint | null;
183
+ /** `P(settle > higher)`, or `null` for the +∞ higher bound. */
184
+ higherUp: bigint | null;
185
+ }
186
+
187
+ /** Where a quote's probabilities come from: boundary probabilities you already hold (raw 1e9,
188
+ * e.g. `probabilityToRaw((await read.price(...)).up)`) or a local pricer snapshot plus
189
+ * the range's strikes in USD (`null` for an infinite side). */
190
+ export type ProbabilitySource =
191
+ Boundaries | { pricer: PricerInputs; lower: number | null; upper: number | null };
192
+
193
+ /** The market's gas-price EWMA (`ewma::EwmaState`), both fields 1e9-scaled. */
194
+ export interface CongestionState {
195
+ mean: bigint;
196
+ variance: bigint;
197
+ }
198
+
199
+ /** The protocol's congestion knobs (`EwmaConfig`). Shipped disabled. */
200
+ export interface CongestionPolicy {
201
+ enabled: boolean;
202
+ /** `penalty_rate` — per-unit surcharge charged when the z-score fires. */
203
+ penaltyRate: bigint;
204
+ /** `z_score_threshold` — how many standard deviations above the mean gas must sit. */
205
+ zScoreThreshold: bigint;
206
+ }
207
+
208
+ /** Pre-trade payout-tree reads a mint's inventory-impact charge is evaluated against
209
+ * (`strike_exposure::quote_mint_range`). All in quote units. */
210
+ export interface MintBookTerms {
211
+ /** `M` — largest summed net payout at any one settlement price. */
212
+ maxPayout: bigint;
213
+ /** `T` — sum of every live order's payout. */
214
+ totalPayout: bigint;
215
+ /** The payout peak inside the mint's own `(lower, higher]`. */
216
+ rangeMaxPayout: bigint;
217
+ }
218
+
219
+ /** The same reads for a live close, which also needs the peak OUTSIDE the order's range
220
+ * (`strike_exposure::live_close_inventory_impact`). */
221
+ export interface CloseBookTerms extends MintBookTerms {
222
+ /** The payout peak in the complement of the order's range. */
223
+ complementMaxPayout: bigint;
224
+ }
225
+
226
+ function assertFeePolicy(policy: FeePolicy): void {
227
+ assertUint(policy.baseFee, 'baseFee', FLOAT_SCALING);
228
+ assertUint(policy.minFee, 'minFee', FLOAT_SCALING);
229
+ assertUint(policy.expiryFeeWindowMs, 'expiryFeeWindowMs', U64_MAX, 1n);
230
+ assertUint(
231
+ policy.expiryFeeMaxMultiplier,
232
+ 'expiryFeeMaxMultiplier',
233
+ 10n * FLOAT_SCALING,
234
+ FLOAT_SCALING,
235
+ );
236
+ assertUint(policy.minEntryProbability, 'minEntryProbability', FLOAT_SCALING);
237
+ assertUint(policy.maxEntryProbability, 'maxEntryProbability', FLOAT_SCALING);
238
+ if (policy.minEntryProbability >= policy.maxEntryProbability) {
239
+ throw new PredictInputError('minEntryProbability must be below maxEntryProbability');
240
+ }
241
+ assertUint(policy.inventoryImpactMaxRate, 'inventoryImpactMaxRate', FLOAT_SCALING);
242
+ assertUint(policy.backingBufferLambda, 'backingBufferLambda', FLOAT_SCALING);
243
+ assertUint(
244
+ policy.inventoryImpactScale,
245
+ 'inventoryImpactScale',
246
+ U64_MAX,
247
+ policy.inventoryImpactMaxRate === 0n ? 0n : 1n,
248
+ );
249
+ }
250
+
251
+ function assertBoundaries(boundaries: Boundaries): void {
252
+ if (boundaries.lowerUp !== null) assertUint(boundaries.lowerUp, 'lowerUp', FLOAT_SCALING);
253
+ if (boundaries.higherUp !== null) assertUint(boundaries.higherUp, 'higherUp', FLOAT_SCALING);
254
+ }
255
+
256
+ function assertBook(book: MintBookTerms): void {
257
+ assertUint(book.totalPayout, 'totalPayout');
258
+ assertUint(book.maxPayout, 'maxPayout', book.totalPayout);
259
+ assertUint(book.rangeMaxPayout, 'rangeMaxPayout', book.maxPayout);
260
+ }
261
+
262
+ function assertCostInputs(
263
+ inputs: Pick<MintInputsBase, 'fees' | 'book' | 'penaltyRate' | 'lotSize'>,
264
+ ): void {
265
+ assertFeePolicy(inputs.fees);
266
+ assertUint(inputs.penaltyRate ?? 0n, 'penaltyRate', FLOAT_SCALING);
267
+ assertUint(inputs.lotSize ?? POSITION_LOT_SIZE, 'lotSize', U64_MAX / MAX_QUANTITY_LOTS, 1n);
268
+ if (inputs.fees.inventoryImpactMaxRate > 0n && !inputs.book) {
269
+ throw new PredictInputError('book is required when inventory impact is enabled');
270
+ }
271
+ if (inputs.book) assertBook(inputs.book);
272
+ }
273
+
274
+ // === Fee components ===
275
+
276
+ /** `strike_exposure_config::raw_bernoulli_fee_rate` — `base_fee · sqrt(p·(1−p))`, the fee rate
277
+ * before the `min_fee` floor and the expiry ramp. Zero at the certain ends. */
278
+ export function bernoulliFeeRate(baseFee: bigint, probability: bigint): bigint {
279
+ assertUint(baseFee, 'baseFee', FLOAT_SCALING);
280
+ assertUint(probability, 'probability (EInvalidFeeProbability)', FLOAT_SCALING);
281
+ if (probability === 0n || probability === FLOAT_SCALING) return 0n;
282
+ return mulDown(baseFee, sqrtDown(mulDown(probability, FLOAT_SCALING - probability)));
283
+ }
284
+
285
+ /** `strike_exposure_config::expiry_fee_multiplier` — 1.0 outside the window, rising linearly
286
+ * to `expiry_fee_max_multiplier` at expiry. */
287
+ export function expiryFeeMultiplier(policy: FeePolicy, timeToExpiryMs: bigint): bigint {
288
+ assertFeePolicy(policy);
289
+ assertUint(timeToExpiryMs, 'timeToExpiryMs');
290
+ if (timeToExpiryMs >= policy.expiryFeeWindowMs) return FLOAT_SCALING;
291
+ return (
292
+ FLOAT_SCALING +
293
+ mulDivDown(
294
+ policy.expiryFeeMaxMultiplier - FLOAT_SCALING,
295
+ policy.expiryFeeWindowMs - timeToExpiryMs,
296
+ policy.expiryFeeWindowMs,
297
+ )
298
+ );
299
+ }
300
+
301
+ // `strike_exposure_config::leg_trading_fee` — one finite boundary's fee, floored then ramped,
302
+ // rounded down at each step.
303
+ function legTradingFee(
304
+ policy: FeePolicy,
305
+ probability: bigint,
306
+ quantity: bigint,
307
+ timeToExpiryMs: bigint,
308
+ ): bigint {
309
+ const base = max(bernoulliFeeRate(policy.baseFee, probability), policy.minFee);
310
+ return mulDown(mulDown(base, expiryFeeMultiplier(policy, timeToExpiryMs)), quantity);
311
+ }
312
+
313
+ /**
314
+ * `strike_exposure_config::trading_fee` — the fee for a whole range, charged PER FINITE
315
+ * BOUNDARY with each leg floored and ramped independently. A binary order has one finite
316
+ * boundary and pays one leg; a two-sided range pays two, so it is not the same fee as the
317
+ * range's own probability would suggest.
318
+ */
319
+ export function tradingFee(
320
+ policy: FeePolicy,
321
+ boundaries: Boundaries,
322
+ quantity: bigint,
323
+ timeToExpiryMs: bigint,
324
+ ): bigint {
325
+ assertFeePolicy(policy);
326
+ assertBoundaries(boundaries);
327
+ assertUint(quantity, 'quantity');
328
+ assertUint(timeToExpiryMs, 'timeToExpiryMs');
329
+ const lower =
330
+ boundaries.lowerUp === null
331
+ ? 0n
332
+ : legTradingFee(policy, boundaries.lowerUp, quantity, timeToExpiryMs);
333
+ const higher =
334
+ boundaries.higherUp === null
335
+ ? 0n
336
+ : legTradingFee(policy, boundaries.higherUp, quantity, timeToExpiryMs);
337
+ return lower + higher;
338
+ }
339
+
340
+ /** `expiry_market::builder_fee_amount` — an account carrying a builder code pays the builder a
341
+ * multiple of its trading fee, capped as a share of quantity. */
342
+ export function builderFee(fee: bigint, quantity: bigint, hasBuilderCode: boolean): bigint {
343
+ assertUint(fee, 'fee');
344
+ assertUint(quantity, 'quantity');
345
+ if (!hasBuilderCode) return 0n;
346
+ return min(mulDown(fee, BUILDER_FEE_MULTIPLIER), mulDown(quantity, MAX_BUILDER_FEE_RATE));
347
+ }
348
+
349
+ /** `expiry_market::fee_incentive_subsidy_amount` — a sponsor pays part of the trader's MINT
350
+ * fee, bounded by the expiry's remaining sponsored balance. Mints only; redeems pay in full. */
351
+ export function feeIncentiveSubsidy(fee: bigint, feeIncentiveBalance: bigint): bigint {
352
+ assertUint(fee, 'fee');
353
+ assertUint(feeIncentiveBalance, 'feeIncentiveBalance');
354
+ return min(mulDown(fee, FEE_INCENTIVE_SUBSIDY_RATE), feeIncentiveBalance);
355
+ }
356
+
357
+ /**
358
+ * `ewma::penalty_fee`'s firing test, as a per-unit RATE: `penalty_rate` when the transaction's
359
+ * gas price is a high outlier against the market's pre-trade EWMA, else zero. It is a rate and
360
+ * not an amount because the surcharge is `rate · quantity`, and a budget search has to reprice
361
+ * it at every candidate quantity. `gasPrice` is the raw reference gas price (not 1e9-scaled),
362
+ * as `tx_context::gas_price` reports it.
363
+ */
364
+ export function congestionPenaltyRate(
365
+ policy: CongestionPolicy,
366
+ state: CongestionState,
367
+ gasPrice: bigint,
368
+ ): bigint {
369
+ assertUint(policy.penaltyRate, 'penaltyRate', FLOAT_SCALING);
370
+ assertUint(policy.zScoreThreshold, 'zScoreThreshold');
371
+ assertUint(state.mean, 'mean');
372
+ assertUint(state.variance, 'variance');
373
+ assertUint(gasPrice, 'gasPrice', U64_MAX / FLOAT_SCALING);
374
+ if (!policy.enabled || state.variance === 0n) return 0n;
375
+ const scaled = gasPrice * FLOAT_SCALING;
376
+ if (scaled <= state.mean) return 0n;
377
+ const zScore = divDown(scaled - state.mean, sqrtDown(state.variance));
378
+ if (zScore <= policy.zScoreThreshold) return 0n;
379
+ return policy.penaltyRate;
380
+ }
381
+
382
+ // `strike_exposure::live_payout_liability_from_terms` — `M + lambda·(T − M)`.
383
+ function payoutLiability(policy: FeePolicy, maxPayout: bigint, totalPayout: bigint): bigint {
384
+ return maxPayout + mulDown(policy.backingBufferLambda, totalPayout - maxPayout);
385
+ }
386
+
387
+ /**
388
+ * `strike_exposure::inventory_impact_potential_for_liability` — the book-level potential
389
+ * `phi(L)`: marginal rate rising linearly to `inventory_impact_max_rate` over the scale `B`,
390
+ * flat at the cap above it. Trades are charged the DIFFERENCE of two evaluations, which is
391
+ * what makes inventory cycles telescope to zero.
392
+ */
393
+ export function inventoryImpactPotential(policy: FeePolicy, liability: bigint): bigint {
394
+ assertFeePolicy(policy);
395
+ assertUint(liability, 'liability');
396
+ if (policy.inventoryImpactMaxRate === 0n || liability === 0n) return 0n;
397
+ const scale = policy.inventoryImpactScale;
398
+ const capped = min(liability, scale);
399
+ const utilization = mulDivDown(capped, FLOAT_SCALING, scale);
400
+ const marginalRate = mulDown(policy.inventoryImpactMaxRate, utilization);
401
+ const potentialAtCapped = mulDown(marginalRate, capped) / 2n;
402
+ if (liability <= scale) return potentialAtCapped;
403
+ return potentialAtCapped + mulDown(policy.inventoryImpactMaxRate, liability - scale);
404
+ }
405
+
406
+ /** `strike_exposure::mint_range_inventory_impact` — the charge a mint of `quantity` over a
407
+ * range pays: the exact rise in the book potential. Zero at the shipped rate of `0n`. */
408
+ export function mintInventoryImpact(
409
+ policy: FeePolicy,
410
+ book: MintBookTerms,
411
+ quantity: bigint,
412
+ ): bigint {
413
+ assertFeePolicy(policy);
414
+ assertBook(book);
415
+ assertUint(quantity, 'quantity', U64_MAX - book.totalPayout);
416
+ if (policy.inventoryImpactMaxRate === 0n || quantity === 0n) return 0n;
417
+ const before = payoutLiability(policy, book.maxPayout, book.totalPayout);
418
+ const after = payoutLiability(
419
+ policy,
420
+ max(book.maxPayout, book.rangeMaxPayout + quantity),
421
+ book.totalPayout + quantity,
422
+ );
423
+ return inventoryImpactPotential(policy, after) - inventoryImpactPotential(policy, before);
424
+ }
425
+
426
+ /** `strike_exposure::live_close_inventory_impact` — the rebate a live close of `payout`
427
+ * receives: the exact fall in the same potential. Zero at the shipped rate of `0n`. */
428
+ export function closeInventoryImpact(
429
+ policy: FeePolicy,
430
+ book: CloseBookTerms,
431
+ payout: bigint,
432
+ ): bigint {
433
+ assertFeePolicy(policy);
434
+ assertBook(book);
435
+ assertUint(book.complementMaxPayout, 'complementMaxPayout', book.maxPayout);
436
+ if (max(book.rangeMaxPayout, book.complementMaxPayout) !== book.maxPayout) {
437
+ throw new PredictInputError('maxPayout must equal the larger range or complement payout');
438
+ }
439
+ assertUint(payout, 'close payout', book.rangeMaxPayout);
440
+ if (policy.inventoryImpactMaxRate === 0n || payout === 0n) return 0n;
441
+ const before = payoutLiability(policy, book.maxPayout, book.totalPayout);
442
+ const after = payoutLiability(
443
+ policy,
444
+ max(book.rangeMaxPayout - payout, book.complementMaxPayout),
445
+ book.totalPayout - payout,
446
+ );
447
+ return inventoryImpactPotential(policy, before) - inventoryImpactPotential(policy, after);
448
+ }
449
+
450
+ // === Probabilities ===
451
+
452
+ /** `pricing::probability` — the range's own probability, `up(lower) − up(higher)` with the
453
+ * chain's saturating subtraction and its infinite-boundary defaults. */
454
+ export function rangeProbability(boundaries: Boundaries): bigint {
455
+ assertBoundaries(boundaries);
456
+ const lower = boundaries.lowerUp ?? FLOAT_SCALING;
457
+ const higher = boundaries.higherUp ?? 0n;
458
+ return lower > higher ? lower - higher : 0n;
459
+ }
460
+
461
+ // Float probability -> the chain's raw 1e9 domain, clamped like `compute_nd2`'s own clamp.
462
+ function toRawProbability(p: number): bigint {
463
+ if (!Number.isFinite(p)) throw new PredictInputError(`probability is not finite: ${p}`);
464
+ const raw = BigInt(Math.round(p * Number(FLOAT_SCALING)));
465
+ return raw < 0n ? 0n : raw > FLOAT_SCALING ? FLOAT_SCALING : raw;
466
+ }
467
+
468
+ /** Price a range's two boundaries with the local float pricer. `lower`/`upper` are strikes in
469
+ * USD; pass `null` for an infinite side (`{ lower: strike, upper: null }` is an UP order,
470
+ * `{ lower: null, upper: strike }` a DOWN order). Carries the pricer's ~1e-4 approximation. */
471
+ export function boundaryProbabilities(
472
+ pricer: PricerInputs,
473
+ lower: number | null,
474
+ upper: number | null,
475
+ ): Boundaries {
476
+ return {
477
+ lowerUp: lower === null ? null : toRawProbability(upProbability(pricer, lower)),
478
+ higherUp: upper === null ? null : toRawProbability(upProbability(pricer, upper)),
479
+ };
480
+ }
481
+
482
+ function resolveBoundaries(source: ProbabilitySource): { boundaries: Boundaries; exact: boolean } {
483
+ let boundaries: Boundaries;
484
+ let exact: boolean;
485
+ if ('pricer' in source) {
486
+ const { lower, upper } = source;
487
+ if (lower !== null && upper !== null && lower >= upper) {
488
+ throw new PredictInputError(`lower strike ${lower} must be below ${upper} (EInvalidRange)`);
489
+ }
490
+ boundaries = boundaryProbabilities(source.pricer, lower, upper);
491
+ exact = false;
492
+ } else {
493
+ boundaries = source;
494
+ exact = true;
495
+ }
496
+ assertBoundaries(boundaries);
497
+ // `(-inf, +inf]` is the whole outcome space; `order::assert_valid_order_shape` rejects it.
498
+ if (boundaries.lowerUp === null && boundaries.higherUp === null) {
499
+ throw new PredictInputError('a range cannot be infinite on both sides (EInvalidRange)');
500
+ }
501
+ return { boundaries, exact };
502
+ }
503
+
504
+ // `number` is a human amount in USDC decimals, `bigint` is already raw — the SDK-wide
505
+ // convention for every financial parameter.
506
+ function rawAmount(value: number | bigint): bigint {
507
+ const raw = typeof value === 'bigint' ? value : usdcToRaw(value);
508
+ assertUint(raw, 'amount');
509
+ return raw;
510
+ }
511
+
512
+ function rawMs(value: number | bigint): bigint {
513
+ if (typeof value === 'number' && !Number.isSafeInteger(value)) {
514
+ throw new PredictInputError(`timestamp must be a safe integer in milliseconds, got ${value}`);
515
+ }
516
+ const raw = typeof value === 'bigint' ? value : BigInt(value);
517
+ assertUint(raw, 'timestamp');
518
+ return raw;
519
+ }
520
+
521
+ // === Mint ===
522
+
523
+ /** Inputs shared by the exact-quantity mint quote and the budget-sized one. */
524
+ interface MintInputsBase {
525
+ /** The market's snapshotted fee policy. */
526
+ fees: FeePolicy;
527
+ /** Market expiry, ms since epoch. */
528
+ expiryMs: number | bigint;
529
+ /** The clock the trade will price against, ms since epoch. Defaults to `Date.now()`. */
530
+ nowMs?: number | bigint;
531
+ /** Boundary probabilities, or a pricer snapshot plus the range's strikes. */
532
+ probabilities: ProbabilitySource;
533
+ /** Whether the minting account carries a builder code (it pays the builder fee). */
534
+ builderCode?: boolean;
535
+ /** The expiry's remaining sponsored fee balance (`fee_incentive_balance`). Default `0n`. */
536
+ feeIncentiveBalance?: number | bigint;
537
+ /** Per-unit congestion surcharge rate — see {@link congestionPenaltyRate}. Default `0n`. */
538
+ penaltyRate?: bigint;
539
+ /** Pre-trade payout-tree terms. Required when inventory impact is enabled. */
540
+ book?: MintBookTerms;
541
+ /** The deployment's `position_lot_size`. Defaults to the Move constant, `10_000n`. */
542
+ lotSize?: bigint;
543
+ }
544
+
545
+ /** Inputs for {@link mintCost}: an exact payout quantity. */
546
+ export interface MintCostInputs extends MintInputsBase {
547
+ /** Maximum payout bought, in quote units — `number` is human USD, `bigint` is raw. */
548
+ quantity: number | bigint;
549
+ }
550
+
551
+ /** Inputs for {@link mintCostForBudget}: an all-in budget, the `mint_exact_cost` shape. */
552
+ export interface MintBudgetInputs extends MintInputsBase {
553
+ /** The total to spend, fees included. */
554
+ budget: number | bigint;
555
+ /** Floor on the fill; a smaller result aborts, as `mint_exact_cost` does. Default one lot. */
556
+ minQuantity?: number | bigint;
557
+ /** The account's settled USDC. The chain caps the budget at it before sizing; pass it to
558
+ * reproduce that cap (and to let `U64_MAX` mean "my whole balance"). */
559
+ accountBalance?: number | bigint;
560
+ }
561
+
562
+ /** One mint's cost decomposition — the fields of the chain's `MintQuote`, in human units with
563
+ * the exact integers alongside. `cost` is the account debit. */
564
+ export interface MintCost {
565
+ /** Maximum payout bought (human quote units). */
566
+ quantity: number;
567
+ /** Fill price, 0..1 per $1 of payout, before fees. */
568
+ entryProbability: number;
569
+ /** Premium into LP backing. */
570
+ premium: number;
571
+ /** `referral` is not here: it is a split of protocol proceeds, not a trader debit. */
572
+ fees: { trading: number; subsidy: number; builder: number; penalty: number; impact: number };
573
+ /** All-in account debit: `premium + (trading − subsidy) + builder + penalty + impact`. */
574
+ cost: number;
575
+ /** All-in price per $1 of payout — `cost / quantity`, the number to compare across venues. */
576
+ costPerContract: number;
577
+ /** Maximum payout divided by the all-in debit, including fees. */
578
+ payoutMultiple: number;
579
+ raw: {
580
+ quantity: bigint;
581
+ entryProbability: bigint;
582
+ premium: bigint;
583
+ tradingFee: bigint;
584
+ subsidy: bigint;
585
+ builderFee: bigint;
586
+ penaltyFee: bigint;
587
+ impactCharge: bigint;
588
+ cost: bigint;
589
+ };
590
+ /** True when boundary probabilities were supplied as raw integers rather than priced
591
+ * locally in float. Does not verify their source, freshness or the other quote inputs. */
592
+ exactProbabilities: boolean;
593
+ }
594
+
595
+ /** Budget-sized mint plus the budget information needed to render an order preview. */
596
+ export interface MintBudgetCost extends Omit<MintCost, 'raw'> {
597
+ /** Requested all-in budget, before the optional account-balance cap. */
598
+ budget: number;
599
+ /** Budget after the optional account-balance cap. */
600
+ effectiveBudget: number;
601
+ /** Requested budget minus actual cost, including any balance-cap shortfall. */
602
+ unspentBudget: number;
603
+ raw: MintCost['raw'] & {
604
+ budget: bigint;
605
+ effectiveBudget: bigint;
606
+ unspentBudget: bigint;
607
+ };
608
+ }
609
+
610
+ // `strike_exposure_config::assert_mint_probability_policy`, applied where
611
+ // `assert_range_mint_probability_policy` applies it: each finite leg and the range itself.
612
+ function assertProbabilityPolicy(policy: FeePolicy, probability: bigint): void {
613
+ if (probability < policy.minEntryProbability || probability > policy.maxEntryProbability) {
614
+ throw new PredictInputError(
615
+ `entry probability ${probability} outside the market's [${policy.minEntryProbability}, ` +
616
+ `${policy.maxEntryProbability}] admission band (EEntryProbabilityOutOfBounds)`,
617
+ );
618
+ }
619
+ }
620
+
621
+ function assertRangeMintPolicy(policy: FeePolicy, boundaries: Boundaries): void {
622
+ if (boundaries.lowerUp !== null) assertProbabilityPolicy(policy, boundaries.lowerUp);
623
+ if (boundaries.higherUp !== null) {
624
+ assertProbabilityPolicy(policy, FLOAT_SCALING - boundaries.higherUp);
625
+ }
626
+ assertProbabilityPolicy(policy, rangeProbability(boundaries));
627
+ }
628
+
629
+ function assertValidQuantity(quantity: bigint, lotSize: bigint): void {
630
+ if (quantity <= 0n || quantity % lotSize !== 0n || quantity / lotSize > MAX_QUANTITY_LOTS) {
631
+ throw new PredictInputError(
632
+ `quantity ${quantity} must be a positive multiple of the ${lotSize} lot size, at most ` +
633
+ `${MAX_QUANTITY_LOTS} lots (EInvalidQuantity)`,
634
+ );
635
+ }
636
+ }
637
+
638
+ function timeToExpiry(expiryMs: bigint, nowMs: bigint): bigint {
639
+ if (nowMs >= expiryMs) {
640
+ throw new PredictInputError(
641
+ `market expired at ${expiryMs}; a live trade at ${nowMs} aborts (ELivePricingExpired)`,
642
+ );
643
+ }
644
+ return expiryMs - nowMs;
645
+ }
646
+
647
+ // The all-in sum, `expiry_market::mint_quote_at`, with no admission checks — the one place the
648
+ // cost is assembled, shared by the quote and the budget search exactly as the contract shares
649
+ // it. Rounding order is the chain's: every component is floored before it is summed.
650
+ function mintQuoteAt(
651
+ inputs: MintInputsBase,
652
+ boundaries: Boundaries,
653
+ quantity: bigint,
654
+ timeToExpiryMs: bigint,
655
+ ): {
656
+ premium: bigint;
657
+ fee: bigint;
658
+ subsidy: bigint;
659
+ builder: bigint;
660
+ penalty: bigint;
661
+ impact: bigint;
662
+ cost: bigint;
663
+ } {
664
+ const premium = mulDown(rangeProbability(boundaries), quantity);
665
+ const fee = tradingFee(inputs.fees, boundaries, quantity, timeToExpiryMs);
666
+ const subsidy = feeIncentiveSubsidy(fee, rawAmount(inputs.feeIncentiveBalance ?? 0n));
667
+ const builder = builderFee(fee, quantity, inputs.builderCode ?? false);
668
+ const penalty = mulDown(inputs.penaltyRate ?? 0n, quantity);
669
+ const impact = inputs.book ? mintInventoryImpact(inputs.fees, inputs.book, quantity) : 0n;
670
+ return {
671
+ premium,
672
+ fee,
673
+ subsidy,
674
+ builder,
675
+ penalty,
676
+ impact,
677
+ cost: premium + (fee - subsidy) + builder + penalty + impact,
678
+ };
679
+ }
680
+
681
+ function mintCostFrom(
682
+ inputs: MintInputsBase,
683
+ boundaries: Boundaries,
684
+ exact: boolean,
685
+ quantity: bigint,
686
+ timeToExpiryMs: bigint,
687
+ ): MintCost {
688
+ const q = mintQuoteAt(inputs, boundaries, quantity, timeToExpiryMs);
689
+ const probability = rangeProbability(boundaries);
690
+ return {
691
+ quantity: fromRaw(quantity, 6),
692
+ entryProbability: fromRaw(probability, 9),
693
+ premium: fromRaw(q.premium, 6),
694
+ fees: {
695
+ trading: fromRaw(q.fee, 6),
696
+ subsidy: fromRaw(q.subsidy, 6),
697
+ builder: fromRaw(q.builder, 6),
698
+ penalty: fromRaw(q.penalty, 6),
699
+ impact: fromRaw(q.impact, 6),
700
+ },
701
+ cost: fromRaw(q.cost, 6),
702
+ costPerContract: quantity === 0n ? 0 : Number(q.cost) / Number(quantity),
703
+ payoutMultiple: Number(quantity) / Number(q.cost),
704
+ raw: {
705
+ quantity,
706
+ entryProbability: probability,
707
+ premium: q.premium,
708
+ tradingFee: q.fee,
709
+ subsidy: q.subsidy,
710
+ builderFee: q.builder,
711
+ penaltyFee: q.penalty,
712
+ impactCharge: q.impact,
713
+ cost: q.cost,
714
+ },
715
+ exactProbabilities: exact,
716
+ };
717
+ }
718
+
719
+ /**
720
+ * All-in cost of minting an exact payout quantity — the `mint_exact_quantity` shape, priced
721
+ * entirely client-side. Mirrors `compute_mint_quote`, including its admission checks: the
722
+ * entry-probability band on each finite leg and on the range, the `min_premium` floor, the lot
723
+ * grid, and the `all_in_cost <= quantity` bound (a contract may never cost more than it can
724
+ * pay out). Each violation throws the `PredictInputError` naming the abort the chain would
725
+ * have raised.
726
+ */
727
+ export function mintCost(inputs: MintCostInputs): MintCost {
728
+ assertCostInputs(inputs);
729
+ const { boundaries, exact } = resolveBoundaries(inputs.probabilities);
730
+ const lotSize = inputs.lotSize ?? POSITION_LOT_SIZE;
731
+ const quantity = rawAmount(inputs.quantity);
732
+ const ttl = timeToExpiry(rawMs(inputs.expiryMs), rawMs(inputs.nowMs ?? Date.now()));
733
+
734
+ assertRangeMintPolicy(inputs.fees, boundaries);
735
+ const premium = mulDown(rangeProbability(boundaries), quantity);
736
+ if (premium < MIN_PREMIUM) {
737
+ throw new PredictInputError(
738
+ `premium ${premium} is below the ${MIN_PREMIUM} minimum (EPremiumBelowMinimum)`,
739
+ );
740
+ }
741
+ assertValidQuantity(quantity, lotSize);
742
+
743
+ const quote = mintCostFrom(inputs, boundaries, exact, quantity, ttl);
744
+ if (quote.raw.cost > quantity) {
745
+ throw new PredictInputError(
746
+ `all-in cost ${quote.raw.cost} exceeds the maximum payout ${quantity} ` +
747
+ `(EMintCostAboveMaxPayout)`,
748
+ );
749
+ }
750
+ return quote;
751
+ }
752
+
753
+ /**
754
+ * A fill whose ALL-IN cost fits a budget — `expiry_market::mint_exact_cost` computed
755
+ * client-side, with the same lot search over the same cost function, so the answer is the
756
+ * quantity that entrypoint would size and the cost it would debit.
757
+ *
758
+ * Without it a "spend exactly $X" flow has to guess: every fee is charged ON TOP of the
759
+ * premium, so the caller subtracts an estimated fee load, pads it against an abort, and
760
+ * systematically underspends. Here the whole search runs locally; submit the all-in budget
761
+ * through `tx.mintCost` on deployments carrying `mint_exact_cost`.
762
+ *
763
+ * Sizing also respects the fill's maximum payout and the 32-bit lot cap; either can leave
764
+ * substantial budget unspent. When only the budget binds, one more lot would exceed it.
765
+ * If the budget fill exceeds its maximum payout, the contract's step-down is best effort:
766
+ * rounding can make it miss a larger admissible fill, including one meeting `minQuantity`.
767
+ * The chain caps the budget at the account balance first ({@link MintBudgetInputs.accountBalance}).
768
+ */
769
+ export function mintCostForBudget(inputs: MintBudgetInputs): MintBudgetCost {
770
+ assertCostInputs(inputs);
771
+ const { boundaries, exact } = resolveBoundaries(inputs.probabilities);
772
+ const lotSize = inputs.lotSize ?? POSITION_LOT_SIZE;
773
+ const ttl = timeToExpiry(rawMs(inputs.expiryMs), rawMs(inputs.nowMs ?? Date.now()));
774
+ const minQuantity = inputs.minQuantity === undefined ? lotSize : rawAmount(inputs.minQuantity);
775
+ const requestedBudget = rawAmount(inputs.budget);
776
+ let budget = requestedBudget;
777
+ if (inputs.accountBalance !== undefined) budget = min(budget, rawAmount(inputs.accountBalance));
778
+
779
+ assertRangeMintPolicy(inputs.fees, boundaries);
780
+
781
+ // `strike_exposure::max_quantity_for_premium` — the premium-only fit bounds the all-in
782
+ // fit from above, because every other term is non-negative.
783
+ const probability = rangeProbability(boundaries);
784
+ let lo = 0n;
785
+ let hi = MAX_QUANTITY_LOTS;
786
+ while (lo < hi) {
787
+ const mid = (lo + hi + 1n) / 2n;
788
+ if (mulDown(probability, mid * lotSize) <= budget) lo = mid;
789
+ else hi = mid - 1n;
790
+ }
791
+
792
+ // `expiry_market::quote_exact_cost_terms`, first half: the budget search proper. Exact,
793
+ // because every all-in term is nondecreasing in quantity for fixed pre-trade state.
794
+ const allInCostAt = (quantity: bigint) => mintQuoteAt(inputs, boundaries, quantity, ttl).cost;
795
+ hi = lo;
796
+ lo = 0n;
797
+ while (lo < hi) {
798
+ const mid = (lo + hi + 1n) / 2n;
799
+ if (allInCostAt(mid * lotSize) <= budget) lo = mid;
800
+ else hi = mid - 1n;
801
+ }
802
+ const budgetLots = lo;
803
+ const budgetQuantity = budgetLots * lotSize;
804
+
805
+ // Second half: the maximum-payout bound (`cost <= quantity`) is deliberately NOT part of
806
+ // that search. It is not monotone — cost and quantity both rise, and the independent floors
807
+ // in each cost term let `cost(q) <= q` flip back to true at a larger lot wherever unit cost
808
+ // sits within rounding of one — so binary-searching it would discard admissible fills. It is
809
+ // consulted only when the budget fill breaches it, and the step-down runs strictly below
810
+ // that fill, so every candidate already fits the budget.
811
+ // This fallback mirrors Move's best-effort search; its nonmonotone predicate means it
812
+ // can miss larger admissible fills, including one satisfying minQuantity.
813
+ let lots = budgetLots;
814
+ if (budgetLots > 0n && allInCostAt(budgetQuantity) > budgetQuantity) {
815
+ let stepLo = 0n;
816
+ let stepHi = budgetLots - 1n;
817
+ while (stepLo < stepHi) {
818
+ const mid = (stepLo + stepHi + 1n) / 2n;
819
+ const candidate = mid * lotSize;
820
+ if (allInCostAt(candidate) <= candidate) stepLo = mid;
821
+ else stepHi = mid - 1n;
822
+ }
823
+ // No admissible smaller fill: fall back to the budget fill, which then fails the
824
+ // maximum-payout bound below exactly as the chain's own quote aborts on it.
825
+ lots = stepLo === 0n ? budgetLots : stepLo;
826
+ }
827
+
828
+ const quantity = lots * lotSize;
829
+ if (quantity < minQuantity) {
830
+ throw new PredictInputError(
831
+ `budget ${budget} sizes ${quantity}, below the ${minQuantity} minimum ` +
832
+ `(EMintQuantityBelowMin)`,
833
+ );
834
+ }
835
+ const premium = mulDown(probability, quantity);
836
+ if (premium < MIN_PREMIUM) {
837
+ throw new PredictInputError(
838
+ `budget ${budget} sizes a premium of ${premium}, below the ${MIN_PREMIUM} minimum ` +
839
+ `(EPremiumBelowMinimum)`,
840
+ );
841
+ }
842
+ assertValidQuantity(quantity, lotSize);
843
+ const quote = mintCostFrom(inputs, boundaries, exact, quantity, ttl);
844
+ if (quote.raw.cost > quantity) {
845
+ throw new PredictInputError(
846
+ `sized fill inside budget ${budget} costs more than it can pay out: ${quote.raw.cost} ` +
847
+ `exceeds ${quantity} (EMintCostAboveMaxPayout)`,
848
+ );
849
+ }
850
+ return {
851
+ ...quote,
852
+ budget: fromRaw(requestedBudget, 6),
853
+ effectiveBudget: fromRaw(budget, 6),
854
+ unspentBudget: fromRaw(requestedBudget - quote.raw.cost, 6),
855
+ raw: {
856
+ ...quote.raw,
857
+ budget: requestedBudget,
858
+ effectiveBudget: budget,
859
+ unspentBudget: requestedBudget - quote.raw.cost,
860
+ },
861
+ };
862
+ }
863
+
864
+ // === Live redeem ===
865
+
866
+ /** Inputs for {@link redeemLiveProceeds}. The range is the ORDER's own range — the same two
867
+ * boundaries it was minted over, which is what `redeem_live` reprices. */
868
+ export interface RedeemLiveInputs {
869
+ fees: FeePolicy;
870
+ expiryMs: number | bigint;
871
+ nowMs?: number | bigint;
872
+ /** The order's boundary probabilities now, or a pricer snapshot plus the order's strikes. */
873
+ probabilities: ProbabilitySource;
874
+ /** Payout being closed — the whole order, or part of it. */
875
+ closeQuantity: number | bigint;
876
+ /** Current position payout quantity, when known. Validates the close and returns the remainder. */
877
+ positionQuantity?: number | bigint;
878
+ builderCode?: boolean;
879
+ penaltyRate?: bigint;
880
+ /** Pre-trade payout-tree terms. Required when inventory impact is enabled. */
881
+ book?: CloseBookTerms;
882
+ lotSize?: bigint;
883
+ }
884
+
885
+ /** What a live close credits the account, decomposed. */
886
+ export interface RedeemLiveProceeds {
887
+ /** NET credited to the account. */
888
+ proceeds: number;
889
+ /** Close value before fees: the range's current probability times the closed payout. */
890
+ gross: number;
891
+ fees: { trading: number; builder: number; penalty: number; impactRebate: number };
892
+ quantityClosed: number;
893
+ /** Unclosed payout quantity, or null when positionQuantity was not supplied. */
894
+ remainingQuantity: number | null;
895
+ /** Net credited per $1 of closed payout; includes fees and the inventory rebate. */
896
+ proceedsPerContract: number;
897
+ /** Current range probability, 0..1, before amount rounding and fees. */
898
+ probability: number;
899
+ raw: {
900
+ proceeds: bigint;
901
+ gross: bigint;
902
+ tradingFee: bigint;
903
+ builderFee: bigint;
904
+ penaltyFee: bigint;
905
+ impactRebate: bigint;
906
+ quantityClosed: bigint;
907
+ remainingQuantity: bigint | null;
908
+ /** 1e9-scaled range probability; locally priced inputs remain approximate. */
909
+ probability: bigint;
910
+ };
911
+ /** Identifies raw probability inputs; does not verify their source or state freshness. */
912
+ exactProbabilities: boolean;
913
+ }
914
+
915
+ /**
916
+ * Net proceeds of closing a live position — `expiry_market::redeem_live`'s payment
917
+ * decomposition, client-side. The mirror of {@link mintCost}: the same per-boundary trading
918
+ * fee and builder fee, the same congestion surcharge, and the inventory-impact term as a
919
+ * REBATE rather than a charge. There is no sponsor subsidy on a close (incentives subsidise
920
+ * mints only), and each deduction is clamped at the payout remaining after the ones before it,
921
+ * exactly as the contract clamps them, so a close can never cost more than it releases.
922
+ *
923
+ * Use this for a local UI preview from a supplied snapshot. `read.quoteRedeem` simulates
924
+ * the actual close and remains the pre-trade check for ownership, remaining position size,
925
+ * live-market gates and current fees. `proceeds` is what `min_proceeds` is compared against
926
+ * on the real call; this preview does not guarantee execution at that amount.
927
+ */
928
+ export function redeemLiveProceeds(inputs: RedeemLiveInputs): RedeemLiveProceeds {
929
+ assertCostInputs(inputs);
930
+ const { boundaries, exact } = resolveBoundaries(inputs.probabilities);
931
+ const lotSize = inputs.lotSize ?? POSITION_LOT_SIZE;
932
+ const quantity = rawAmount(inputs.closeQuantity);
933
+ const ttl = timeToExpiry(rawMs(inputs.expiryMs), rawMs(inputs.nowMs ?? Date.now()));
934
+ assertValidQuantity(quantity, lotSize);
935
+
936
+ const positionQuantity =
937
+ inputs.positionQuantity === undefined ? null : rawAmount(inputs.positionQuantity);
938
+ if (positionQuantity !== null) {
939
+ assertValidQuantity(positionQuantity, lotSize);
940
+ if (quantity > positionQuantity)
941
+ throw new PredictInputError('closeQuantity exceeds positionQuantity');
942
+ }
943
+ const remainingQuantity = positionQuantity === null ? null : positionQuantity - quantity;
944
+ const probability = rangeProbability(boundaries);
945
+ const gross = mulDown(probability, quantity);
946
+ const fee = min(tradingFee(inputs.fees, boundaries, quantity, ttl), gross);
947
+ const builder = min(builderFee(fee, quantity, inputs.builderCode ?? false), gross - fee);
948
+ const penalty = min(mulDown(inputs.penaltyRate ?? 0n, quantity), gross - fee - builder);
949
+ const rebate = inputs.book ? closeInventoryImpact(inputs.fees, inputs.book, quantity) : 0n;
950
+ const proceeds = gross + rebate - fee - builder - penalty;
951
+
952
+ return {
953
+ proceeds: fromRaw(proceeds, 6),
954
+ gross: fromRaw(gross, 6),
955
+ fees: {
956
+ trading: fromRaw(fee, 6),
957
+ builder: fromRaw(builder, 6),
958
+ penalty: fromRaw(penalty, 6),
959
+ impactRebate: fromRaw(rebate, 6),
960
+ },
961
+ quantityClosed: fromRaw(quantity, 6),
962
+ remainingQuantity: remainingQuantity === null ? null : fromRaw(remainingQuantity, 6),
963
+ proceedsPerContract: Number(proceeds) / Number(quantity),
964
+ probability: fromRaw(probability, 9),
965
+ raw: {
966
+ proceeds,
967
+ gross,
968
+ tradingFee: fee,
969
+ builderFee: builder,
970
+ penaltyFee: penalty,
971
+ impactRebate: rebate,
972
+ quantityClosed: quantity,
973
+ remainingQuantity,
974
+ probability,
975
+ },
976
+ exactProbabilities: exact,
977
+ };
978
+ }
979
+
980
+ // === Order IDs ===
981
+
982
+ const QUANTITY_LOTS_OFFSET = 100n;
983
+ const LOWER_TICK_OFFSET = 70n;
984
+ const HIGHER_TICK_OFFSET = 40n;
985
+ const TICK_MASK = POS_INF_TICK; // `constants::pos_inf_tick` is the full 30-bit mask
986
+ /** `constants::pos_inf_tick` — the +∞ sentinel in an order's higher-tick field. */
987
+ export { POS_INF_TICK };
988
+ const U32_MASK = (1n << 32n) - 1n;
989
+
990
+ /** The contract terms packed into an order ID (`deepbook_predict::order`). Tick `0` as the
991
+ * lower bound is −∞ and {@link POS_INF_TICK} as the higher is +∞; multiply a finite tick by
992
+ * the market's `tickSize` for its raw strike. Use it to feed a position from `read.positions`
993
+ * into {@link redeemLiveProceeds}. */
994
+ export interface OrderRange {
995
+ lowerTick: bigint;
996
+ higherTick: bigint;
997
+ /** Minted payout: `quantity_lots · position_lot_size`. */
998
+ quantity: bigint;
999
+ }
1000
+
1001
+ /** Decode an order ID's range and quantity — the TypeScript mirror of `order::lower_tick` /
1002
+ * `higher_tick` / `quantity`. */
1003
+ export function decodeOrderRange(orderId: bigint, lotSize: bigint = POSITION_LOT_SIZE): OrderRange {
1004
+ return {
1005
+ lowerTick: (orderId >> LOWER_TICK_OFFSET) & TICK_MASK,
1006
+ higherTick: (orderId >> HIGHER_TICK_OFFSET) & TICK_MASK,
1007
+ quantity: ((orderId >> QUANTITY_LOTS_OFFSET) & U32_MASK) * lotSize,
1008
+ };
1009
+ }
1010
+
1011
+ /** The strikes an {@link OrderRange} prices against, in USD, with `null` for each infinite
1012
+ * side — the shape {@link boundaryProbabilities} takes. `tickSize` is the market's raw tick
1013
+ * size (`ActiveMarket.tickSize` in USD, or the raw value from the deployment). */
1014
+ export function orderStrikes(
1015
+ range: OrderRange,
1016
+ tickSizeRaw: bigint,
1017
+ ): { lower: number | null; upper: number | null } {
1018
+ return {
1019
+ lower: range.lowerTick === 0n ? null : fromRaw(range.lowerTick * tickSizeRaw, 9),
1020
+ upper: range.higherTick === POS_INF_TICK ? null : fromRaw(range.higherTick * tickSizeRaw, 9),
1021
+ };
1022
+ }