@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.
- package/CHANGELOG.md +153 -0
- package/PREDICT.md +389 -61
- package/dist/contracts/account/account.d.mts +23 -23
- package/dist/contracts/account/account.d.mts.map +1 -1
- package/dist/contracts/account/account_registry.d.mts +13 -13
- package/dist/contracts/deepbook/account.d.mts +18 -18
- package/dist/contracts/deepbook/balances.d.mts +4 -4
- package/dist/contracts/deepbook/deep_price.d.mts +3 -3
- package/dist/contracts/deepbook/order.d.mts +12 -12
- package/dist/contracts/deepbook_predict/admin.d.mts +4 -4
- package/dist/contracts/deepbook_predict/builder_code.d.mts +11 -11
- package/dist/contracts/deepbook_predict/builder_code_events.d.mts +10 -10
- package/dist/contracts/deepbook_predict/config_events.d.mts +93 -68
- package/dist/contracts/deepbook_predict/config_events.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/config_events.mjs +45 -1
- package/dist/contracts/deepbook_predict/config_events.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/deps/std/type_name.mjs +17 -0
- package/dist/contracts/deepbook_predict/deps/std/type_name.mjs.map +1 -0
- package/dist/contracts/deepbook_predict/expiry_market.d.mts +652 -235
- package/dist/contracts/deepbook_predict/expiry_market.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/expiry_market.mjs +652 -186
- package/dist/contracts/deepbook_predict/expiry_market.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/lp_book.mjs +1 -1
- package/dist/contracts/deepbook_predict/market_lifecycle_cap.d.mts +5 -5
- package/dist/contracts/deepbook_predict/market_manager.d.mts +27 -27
- package/dist/contracts/deepbook_predict/order_events.d.mts +50 -50
- package/dist/contracts/deepbook_predict/order_events.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/order_events.mjs +3 -1
- package/dist/contracts/deepbook_predict/order_events.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/pause_cap.d.mts +5 -5
- package/dist/contracts/deepbook_predict/plp.d.mts +184 -82
- package/dist/contracts/deepbook_predict/plp.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/plp.mjs +126 -7
- package/dist/contracts/deepbook_predict/plp.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/pool_accounting.mjs +1 -1
- package/dist/contracts/deepbook_predict/pool_valuation_cap.d.mts +5 -5
- package/dist/contracts/deepbook_predict/predict_account.d.mts +14 -14
- package/dist/contracts/deepbook_predict/pricing.d.mts +91 -58
- package/dist/contracts/deepbook_predict/pricing.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/pricing.mjs +30 -3
- package/dist/contracts/deepbook_predict/pricing.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/pricing_config.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/protocol_config.d.mts +400 -92
- package/dist/contracts/deepbook_predict/protocol_config.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/protocol_config.mjs +451 -61
- package/dist/contracts/deepbook_predict/protocol_config.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/range_codec.d.mts +2 -2
- package/dist/contracts/deepbook_predict/registry.d.mts +28 -28
- package/dist/contracts/deepbook_predict/strike_exposure.mjs +1 -1
- package/dist/contracts/deepbook_predict/strike_payout_tree.mjs +17 -12
- package/dist/contracts/deepbook_predict/strike_payout_tree.mjs.map +1 -1
- package/dist/contracts/deepbook_predict/vault_events.d.mts +154 -106
- package/dist/contracts/deepbook_predict/vault_events.d.mts.map +1 -1
- package/dist/contracts/deepbook_predict/vault_events.mjs +57 -1
- package/dist/contracts/deepbook_predict/vault_events.mjs.map +1 -1
- package/dist/contracts/deepbook_predict_math/config-arguments.d.mts +10 -0
- package/dist/contracts/deepbook_predict_math/config-arguments.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_math/lazer_price.d.mts +106 -0
- package/dist/contracts/deepbook_predict_math/lazer_price.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_math/lazer_price.mjs +125 -0
- package/dist/contracts/deepbook_predict_math/lazer_price.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_math/math.d.mts +284 -0
- package/dist/contracts/deepbook_predict_math/math.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_math/math.mjs +364 -0
- package/dist/contracts/deepbook_predict_math/math.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/config-arguments.d.mts +13 -0
- package/dist/contracts/deepbook_predict_orders/config-arguments.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_orders/delayed_execution_config.d.mts +215 -0
- package/dist/contracts/deepbook_predict_orders/delayed_execution_config.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_orders/delayed_execution_config.mjs +215 -0
- package/dist/contracts/deepbook_predict_orders/delayed_execution_config.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/expiry_market.mjs +87 -0
- package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/expiry_market.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/pricing.mjs +52 -0
- package/dist/contracts/deepbook_predict_orders/deps/deepbook_predict/pricing.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/deps/fixed_math/i64.mjs +25 -0
- package/dist/contracts/deepbook_predict_orders/deps/fixed_math/i64.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/deps/sui/balance.mjs +21 -0
- package/dist/contracts/deepbook_predict_orders/deps/sui/balance.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/deps/sui/table.mjs +39 -0
- package/dist/contracts/deepbook_predict_orders/deps/sui/table.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/desk.d.mts +187 -0
- package/dist/contracts/deepbook_predict_orders/desk.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_orders/desk.mjs +261 -0
- package/dist/contracts/deepbook_predict_orders/desk.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/order_queue.d.mts +1042 -0
- package/dist/contracts/deepbook_predict_orders/order_queue.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_orders/order_queue.mjs +849 -0
- package/dist/contracts/deepbook_predict_orders/order_queue.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/queue.d.mts +683 -0
- package/dist/contracts/deepbook_predict_orders/queue.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_orders/queue.mjs +912 -0
- package/dist/contracts/deepbook_predict_orders/queue.mjs.map +1 -0
- package/dist/contracts/deepbook_predict_orders/queue_events.d.mts +195 -0
- package/dist/contracts/deepbook_predict_orders/queue_events.d.mts.map +1 -0
- package/dist/contracts/deepbook_predict_orders/queue_events.mjs +187 -0
- package/dist/contracts/deepbook_predict_orders/queue_events.mjs.map +1 -0
- package/dist/contracts/deepbook_sessions/session_config.d.mts +8 -8
- package/dist/contracts/deepbook_sessions/sessions.d.mts +163 -28
- package/dist/contracts/deepbook_sessions/sessions.d.mts.map +1 -1
- package/dist/contracts/deepbook_sessions/sessions.mjs +234 -5
- package/dist/contracts/deepbook_sessions/sessions.mjs.map +1 -1
- package/dist/deployments/mainnet.mjs +7 -4
- package/dist/deployments/mainnet.mjs.map +1 -1
- package/dist/deployments/testnet.mjs +7 -4
- package/dist/deployments/testnet.mjs.map +1 -1
- package/dist/predict/bindings/expiry-market.d.mts +77 -0
- package/dist/predict/bindings/expiry-market.d.mts.map +1 -0
- package/dist/predict/bindings/expiry-market.mjs +139 -0
- package/dist/predict/bindings/expiry-market.mjs.map +1 -0
- package/dist/predict/bindings/protocol-config.d.mts +39 -0
- package/dist/predict/bindings/protocol-config.d.mts.map +1 -0
- package/dist/predict/bindings/protocol-config.mjs +90 -0
- package/dist/predict/bindings/protocol-config.mjs.map +1 -0
- package/dist/predict/bindings/rename.mjs +9 -0
- package/dist/predict/bindings/rename.mjs.map +1 -0
- package/dist/predict/client.d.mts +595 -16
- package/dist/predict/client.d.mts.map +1 -1
- package/dist/predict/client.mjs +986 -10
- package/dist/predict/client.mjs.map +1 -1
- package/dist/predict/config/generated.d.mts +22 -1
- package/dist/predict/config/generated.d.mts.map +1 -1
- package/dist/predict/config/generated.mjs +30 -1
- package/dist/predict/config/generated.mjs.map +1 -1
- package/dist/predict/config/mainnet.mjs +11 -3
- package/dist/predict/config/mainnet.mjs.map +1 -1
- package/dist/predict/config/testnet.mjs +11 -3
- package/dist/predict/config/testnet.mjs.map +1 -1
- package/dist/predict/config/types.d.mts +50 -1
- package/dist/predict/config/types.d.mts.map +1 -1
- package/dist/predict/cost.d.mts +38 -11
- package/dist/predict/cost.d.mts.map +1 -1
- package/dist/predict/cost.mjs +37 -11
- package/dist/predict/cost.mjs.map +1 -1
- package/dist/predict/decode.d.mts +251 -1
- package/dist/predict/decode.d.mts.map +1 -1
- package/dist/predict/decode.mjs +342 -4
- package/dist/predict/decode.mjs.map +1 -1
- package/dist/predict/errors.d.mts +69 -6
- package/dist/predict/errors.d.mts.map +1 -1
- package/dist/predict/errors.mjs +277 -4
- package/dist/predict/errors.mjs.map +1 -1
- package/dist/predict/index.d.mts +21 -10
- package/dist/predict/index.mjs +24 -12
- package/dist/predict/queue-id.d.mts +13 -0
- package/dist/predict/queue-id.d.mts.map +1 -0
- package/dist/predict/queue-id.mjs +19 -0
- package/dist/predict/queue-id.mjs.map +1 -0
- package/dist/predict/queue.d.mts +505 -0
- package/dist/predict/queue.d.mts.map +1 -0
- package/dist/predict/queue.mjs +774 -0
- package/dist/predict/queue.mjs.map +1 -0
- package/dist/predict/reads/balances.mjs +1 -1
- package/dist/predict/reads/markets.mjs +46 -5
- package/dist/predict/reads/markets.mjs.map +1 -1
- package/dist/predict/reads/pool.mjs +1 -1
- package/dist/predict/reads/pricing.d.mts +1 -1
- package/dist/predict/reads/queue.d.mts +105 -0
- package/dist/predict/reads/queue.d.mts.map +1 -0
- package/dist/predict/reads/queue.mjs +445 -0
- package/dist/predict/reads/queue.mjs.map +1 -0
- package/dist/predict/ticks.d.mts +9 -1
- package/dist/predict/ticks.d.mts.map +1 -1
- package/dist/predict/ticks.mjs +18 -1
- package/dist/predict/ticks.mjs.map +1 -1
- package/dist/predict/tx/authed.mjs +1 -1
- package/dist/predict/tx/common.d.mts.map +1 -1
- package/dist/predict/tx/common.mjs +12 -1
- package/dist/predict/tx/common.mjs.map +1 -1
- package/dist/predict/tx/queue.d.mts +220 -0
- package/dist/predict/tx/queue.d.mts.map +1 -0
- package/dist/predict/tx/queue.mjs +399 -0
- package/dist/predict/tx/queue.mjs.map +1 -0
- package/dist/predict/tx/trade.d.mts.map +1 -1
- package/dist/predict/tx/trade.mjs +61 -43
- package/dist/predict/tx/trade.mjs.map +1 -1
- package/dist/sessions.d.mts +90 -2
- package/dist/sessions.d.mts.map +1 -1
- package/dist/sessions.mjs +107 -2
- package/dist/sessions.mjs.map +1 -1
- package/dist/transactions/balanceManager.d.mts +12 -12
- package/dist/transactions/balanceManager.d.mts.map +1 -1
- package/dist/transactions/deepbook.d.mts +20 -20
- package/dist/transactions/deepbook.d.mts.map +1 -1
- package/dist/transactions/deepbookAdmin.d.mts +4 -4
- package/dist/transactions/deepbookAdmin.d.mts.map +1 -1
- package/dist/transactions/marginAdmin.d.mts +7 -7
- package/dist/transactions/marginAdmin.d.mts.map +1 -1
- package/dist/transactions/marginLiquidations.d.mts +3 -3
- package/dist/transactions/marginMaintainer.d.mts +5 -5
- package/dist/transactions/marginMaintainer.d.mts.map +1 -1
- package/dist/transactions/marginManager.d.mts +32 -32
- package/dist/transactions/marginManager.d.mts.map +1 -1
- package/dist/transactions/marginPool.d.mts +18 -18
- package/dist/transactions/marginRegistry.d.mts +16 -16
- package/dist/transactions/marginTPSL.d.mts +10 -10
- package/dist/transactions/marginTPSL.d.mts.map +1 -1
- package/dist/transactions/poolProxy.d.mts +8 -8
- package/dist/transactions/poolProxy.d.mts.map +1 -1
- package/package.json +4 -4
- package/src/contracts/deepbook_predict/config_events.ts +39 -0
- package/src/contracts/deepbook_predict/deps/std/type_name.ts +24 -0
- package/src/contracts/deepbook_predict/expiry_market.ts +960 -169
- package/src/contracts/deepbook_predict/order_events.ts +3 -1
- package/src/contracts/deepbook_predict/plp.ts +150 -9
- package/src/contracts/deepbook_predict/pricing.ts +46 -7
- package/src/contracts/deepbook_predict/pricing_config.ts +3 -1
- package/src/contracts/deepbook_predict/protocol_config.ts +648 -33
- package/src/contracts/deepbook_predict/strike_payout_tree.ts +25 -21
- package/src/contracts/deepbook_predict/vault_events.ts +65 -0
- package/src/contracts/deepbook_predict_math/config-arguments.ts +6 -0
- package/src/contracts/deepbook_predict_math/lazer_price.ts +196 -0
- package/src/contracts/deepbook_predict_math/math.ts +553 -0
- package/src/contracts/deepbook_predict_orders/config-arguments.ts +11 -0
- package/src/contracts/deepbook_predict_orders/delayed_execution_config.ts +430 -0
- package/src/contracts/deepbook_predict_orders/deps/deepbook_predict/expiry_market.ts +108 -0
- package/src/contracts/deepbook_predict_orders/deps/deepbook_predict/pricing.ts +58 -0
- package/src/contracts/deepbook_predict_orders/deps/fixed_math/i64.ts +21 -0
- package/src/contracts/deepbook_predict_orders/deps/sui/balance.ts +19 -0
- package/src/contracts/deepbook_predict_orders/deps/sui/table.ts +37 -0
- package/src/contracts/deepbook_predict_orders/desk.ts +419 -0
- package/src/contracts/deepbook_predict_orders/order_flow.ts +19 -0
- package/src/contracts/deepbook_predict_orders/order_queue.ts +1670 -0
- package/src/contracts/deepbook_predict_orders/queue.ts +1394 -0
- package/src/contracts/deepbook_predict_orders/queue_events.ts +189 -0
- package/src/contracts/deepbook_sessions/sessions.ts +355 -4
- package/src/deployments/mainnet.ts +7 -2
- package/src/deployments/testnet.ts +7 -2
- package/src/deployments/types.ts +14 -1
- package/src/predict/bindings/expiry-market.ts +127 -0
- package/src/predict/bindings/protocol-config.ts +59 -0
- package/src/predict/bindings/rename.ts +14 -0
- package/src/predict/client.ts +2076 -13
- package/src/predict/config/generated.ts +50 -0
- package/src/predict/config/mainnet.ts +16 -2
- package/src/predict/config/testnet.ts +16 -2
- package/src/predict/config/types.ts +51 -1
- package/src/predict/cost.ts +62 -14
- package/src/predict/decode.ts +691 -2
- package/src/predict/errors.ts +391 -14
- package/src/predict/index.ts +87 -6
- package/src/predict/queue-id.ts +22 -0
- package/src/predict/queue.ts +1231 -0
- package/src/predict/reads/markets.ts +30 -0
- package/src/predict/reads/queue.ts +606 -0
- package/src/predict/ticks.ts +35 -0
- package/src/predict/tx/common.ts +25 -1
- package/src/predict/tx/queue.ts +525 -0
- package/src/predict/tx/trade.ts +80 -49
- package/src/sessions.ts +206 -1
package/src/predict/client.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
931
|
-
|
|
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
|
+
}
|