Skip to main content

hypercall_runtime_api/
lib.rs

1pub mod boundary;
2pub mod db_ports;
3pub mod drain;
4pub mod error;
5pub mod quote_provider_cache;
6pub mod recovery_safety;
7pub mod rpi_monitor;
8pub mod sonic_json;
9pub mod trading_halt;
10pub mod valuation;
11
12pub use db_ports::{ApiAsyncDb, ApiRsmStateDb, ApiSyncDb};
13pub use quote_provider_cache::{QuoteProviderCache, QuoteProviderConfig};
14
15use alloy::primitives::{FixedBytes, U256};
16use alloy::sol_types::SolValue;
17use std::collections::HashMap;
18use std::sync::{
19    atomic::{AtomicI64, Ordering},
20    Arc,
21};
22use std::time::{Duration, Instant};
23
24use hypercall_types::{
25    api_models::TradingLimits,
26    directives::{CreditOption, SYSTEM_ACTION_ID_CREDIT_OPTION, SYSTEM_ACTION_VERSION},
27    MarketActionMessage, MarketUpdateMessage, OrderActionMessage, OrderUpdateMessage, Side,
28    WalletAddress,
29};
30use rust_decimal::Decimal;
31use tokio::sync::broadcast;
32use tokio::sync::{mpsc, oneshot};
33
34pub use hypercall_engine::command::{RfqExecuteCommand, RfqExecuteLeg, RfqExecuteResult};
35pub use hypercall_recovery::{
36    QuiesceAction as EngineQuiesceAction, QuiesceReport as EngineQuiesceReport,
37};
38
39/// Global counter for pending engine requests.
40///
41/// API/runtime callers increment this when a request is sent to the engine
42/// channel, and the engine runtime decrements it when processing starts.
43static PENDING_ENGINE_REQUESTS: AtomicI64 = AtomicI64::new(0);
44
45pub fn increment_pending_requests() {
46    PENDING_ENGINE_REQUESTS.fetch_add(1, Ordering::Relaxed);
47}
48
49pub fn decrement_pending_requests() {
50    PENDING_ENGINE_REQUESTS.fetch_sub(1, Ordering::Relaxed);
51}
52
53pub fn get_pending_requests() -> i64 {
54    PENDING_ENGINE_REQUESTS.load(Ordering::Relaxed)
55}
56
57/// Request to execute an RFQ trade through the engine.
58pub struct RfqExecuteRequest {
59    pub command: RfqExecuteCommand,
60    pub response_tx: oneshot::Sender<RfqExecuteResult>,
61}
62
63/// Journaled PM settlement admin mutation request.
64pub struct PmSettlementAdminRequest {
65    pub command: hypercall_engine::command::EngineCommand,
66    pub applied_tx: oneshot::Sender<Result<(), String>>,
67}
68
69#[async_trait::async_trait]
70pub trait EngineEventPersistence: Send + Sync {
71    async fn handle_event(&self, event: &hypercall_types::EngineMessage) -> anyhow::Result<()>;
72}
73
74/// Per-symbol orderbook quote from an engine snapshot.
75#[derive(Clone, Debug)]
76pub struct SnapshotBookQuote {
77    pub best_bid: Option<f64>,
78    pub best_bid_size: Option<f64>,
79    pub best_ask: Option<f64>,
80    pub best_ask_size: Option<f64>,
81    pub mid: Option<f64>,
82    pub bids: Vec<(f64, f64)>,
83    pub asks: Vec<(f64, f64)>,
84}
85
86impl SnapshotBookQuote {
87    /// Empty book for a known instrument after the engine has published a
88    /// snapshot. This is not an unknown-symbol sentinel.
89    pub fn empty() -> Self {
90        Self {
91            best_bid: None,
92            best_bid_size: None,
93            best_ask: None,
94            best_ask_size: None,
95            mid: None,
96            bids: Vec::new(),
97            asks: Vec::new(),
98        }
99    }
100
101    pub fn is_empty_book(&self) -> bool {
102        self.best_bid.is_none()
103            && self.best_ask.is_none()
104            && self.bids.is_empty()
105            && self.asks.is_empty()
106    }
107}
108
109/// Snapshot readiness for book-based routing.
110///
111/// Unknown instruments are rejected before quote-provider lookup. Once the
112/// engine has published a snapshot, a symbol missing from `quotes` is a ready
113/// empty book and should be returned as `Ready { quote: empty(), .. }`.
114#[derive(Clone, Debug)]
115pub enum BookSnapshotState {
116    NotReady {
117        l2_seq: i64,
118    },
119    Ready {
120        quote: SnapshotBookQuote,
121        l2_seq: i64,
122    },
123}
124
125#[derive(Clone, Debug)]
126pub struct QuoteSnapshot {
127    pub quotes: HashMap<String, SnapshotBookQuote>,
128    pub l2_seq: i64,
129}
130
131/// Read-only interface for accessing orderbook quotes.
132pub trait QuoteProvider: Send + Sync {
133    fn get_quote(&self, symbol: &str) -> Option<SnapshotBookQuote>;
134    fn get_quote_with_seq(&self, symbol: &str) -> (Option<SnapshotBookQuote>, i64);
135    fn book_snapshot_state(&self, symbol: &str) -> BookSnapshotState;
136    fn all_quotes(&self) -> HashMap<String, SnapshotBookQuote>;
137    fn l2_seq(&self) -> i64;
138    fn snapshot(&self) -> QuoteSnapshot;
139    fn staleness(&self) -> Duration;
140}
141
142/// Runtime-owned summary of an open order from an engine snapshot.
143#[derive(Debug, Clone)]
144pub struct RuntimeOrderSummary {
145    pub order_id: u64,
146    pub symbol: String,
147    pub side: Side,
148    pub price: Decimal,
149    /// Original order size in human-readable contract units.
150    pub original_size: Decimal,
151    /// Remaining open size in human-readable contract units.
152    pub remaining_size: Decimal,
153    pub is_perp: bool,
154    pub mmp_enabled: bool,
155    pub client_id: Option<String>,
156    pub created_at: i64,
157}
158
159impl From<hypercall_engine::OrderSummary> for RuntimeOrderSummary {
160    fn from(order: hypercall_engine::OrderSummary) -> Self {
161        Self {
162            order_id: order.order_id,
163            symbol: order.symbol,
164            side: order.side,
165            price: order.price,
166            original_size: order.original_size,
167            remaining_size: order.remaining_size,
168            is_perp: order.is_perp,
169            mmp_enabled: order.mmp_enabled,
170            client_id: order.client_id,
171            created_at: order.created_at,
172        }
173    }
174}
175
176/// Read-only interface for accessing open orders from an engine snapshot.
177pub trait OrderSnapshotProvider: Send + Sync {
178    fn get_open_orders_for_wallet(&self, wallet: &WalletAddress) -> Vec<RuntimeOrderSummary>;
179    fn get_all_orders(&self) -> Vec<(RuntimeOrderSummary, WalletAddress)>;
180}
181
182/// Read-only interface for checking agent authorization from an engine snapshot.
183pub trait AgentAuthProvider: Send + Sync {
184    fn is_agent_authorized(&self, wallet: &WalletAddress, agent: &WalletAddress) -> bool;
185    fn get_authorized_agents(&self, wallet: &WalletAddress) -> Vec<WalletAddress>;
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct SystemCreditOptionDirective {
190    pub underlying: [u8; 32],
191    pub expiry: U256,
192    pub strike: U256,
193    pub is_call: bool,
194    pub amount_wei: U256,
195}
196
197pub fn encode_credit_option_action(action: SystemCreditOptionDirective) -> anyhow::Result<Vec<u8>> {
198    if action.amount_wei.is_zero() {
199        anyhow::bail!("refusing to submit SystemCreditOption with amount_wei=0");
200    }
201    if action.expiry.is_zero() {
202        anyhow::bail!("refusing to submit SystemCreditOption with expiry=0");
203    }
204    if action.strike.is_zero() {
205        anyhow::bail!("refusing to submit SystemCreditOption with strike=0");
206    }
207    if action.underlying == [0u8; 32] {
208        anyhow::bail!("refusing to submit SystemCreditOption with zero underlying");
209    }
210
211    hypercall_signer::encode_action_bytes(
212        SYSTEM_ACTION_VERSION,
213        SYSTEM_ACTION_ID_CREDIT_OPTION,
214        &CreditOption {
215            underlying: FixedBytes::<32>::from(action.underlying),
216            expiry: action.expiry,
217            strike: action.strike,
218            isCall: action.is_call,
219            amountWei: action.amount_wei,
220        }
221        .abi_encode(),
222    )
223    .map_err(|error| anyhow::anyhow!("{}", error))
224}
225
226/// Receiver half for shutdown signals.
227///
228/// Subscribe to shutdown signals via `Shutdown::subscribe()`.
229pub type ShutdownRx = broadcast::Receiver<()>;
230
231/// Unified shutdown signal broadcaster.
232///
233/// Create one `Shutdown` instance per shutdown domain and share it across
234/// all services that should shut down together.
235#[derive(Clone)]
236pub struct Shutdown {
237    tx: broadcast::Sender<()>,
238    triggered: Arc<std::sync::atomic::AtomicBool>,
239}
240
241impl Default for Shutdown {
242    fn default() -> Self {
243        Self::new()
244    }
245}
246
247impl Shutdown {
248    /// Create a new shutdown broadcaster.
249    pub fn new() -> Self {
250        let (tx, _) = broadcast::channel(1);
251        Self {
252            tx,
253            triggered: Arc::new(std::sync::atomic::AtomicBool::new(false)),
254        }
255    }
256
257    /// Get the underlying sender for code that expects `broadcast::Sender<()>`.
258    pub fn tx(&self) -> broadcast::Sender<()> {
259        self.tx.clone()
260    }
261
262    /// Subscribe to shutdown signals.
263    pub fn subscribe(&self) -> ShutdownRx {
264        self.tx.subscribe()
265    }
266
267    /// Trigger shutdown.
268    pub fn trigger(&self) {
269        self.triggered
270            .store(true, std::sync::atomic::Ordering::SeqCst);
271        let _ = self.tx.send(());
272    }
273
274    /// Check if shutdown has been triggered.
275    pub fn is_triggered(&self) -> bool {
276        self.triggered.load(std::sync::atomic::Ordering::SeqCst)
277    }
278}
279
280/// Request to the unified engine order path.
281#[derive(Debug, Clone)]
282pub struct UnifiedEngineRequest {
283    pub message: OrderActionMessage,
284    pub response_tx: mpsc::Sender<OrderUpdateMessage>,
285    /// When this request was enqueued, for measuring queue wait time.
286    pub enqueued_at: Instant,
287    /// Trace context for propagating spans across channel boundary.
288    #[cfg(feature = "otel-tracing")]
289    pub trace_context: Option<opentelemetry::Context>,
290}
291
292/// Market management request sent to the runtime engine task.
293#[derive(Debug, Clone)]
294pub struct MarketRequest {
295    pub message: MarketActionMessage,
296    pub response_tx: mpsc::Sender<MarketUpdateMessage>,
297}
298
299/// Agent authorization request sent by the API to approve or revoke agents
300/// through the engine's command stream.
301#[derive(Debug)]
302pub struct AgentAuthRequest {
303    pub wallet: WalletAddress,
304    pub agent: WalletAddress,
305    pub approve: bool,
306    pub expires_at_ms: Option<u64>,
307    /// Signed nonce from the request, forwarded to engine for replay protection.
308    pub nonce: Option<u64>,
309    pub applied_tx: oneshot::Sender<Result<(), String>>,
310}
311
312/// Nonce-only request sent by QP handshakes to advance the per-wallet nonce
313/// watermark through the engine command stream.
314#[derive(Debug)]
315pub struct NonceCheckRequest {
316    pub wallet: WalletAddress,
317    pub nonce: u64,
318    pub applied_tx: oneshot::Sender<Result<(), String>>,
319}
320
321/// Margin mode update request sent by the API to keep engine-owned admission
322/// state in sync with the tier cache.
323#[derive(Debug)]
324pub struct MarginModeUpdateRequest {
325    pub wallet: WalletAddress,
326    pub margin_mode: hypercall_types::MarginMode,
327    pub timestamp_ms: u64,
328    pub applied_tx: oneshot::Sender<Result<(), String>>,
329}
330
331/// Deposit request sent from faucet/admin handlers to update balance_ledger.
332#[derive(Debug)]
333pub struct DepositRequest {
334    pub wallet: WalletAddress,
335    pub amount: Decimal,
336    pub timestamp_ms: u64,
337    /// Monotonic durable sequence for this balance update.
338    pub sequence: Option<u64>,
339    pub source_event_hash: FixedBytes<32>,
340    /// External durable mutations of engine-owned state must carry a UUID-backed
341    /// journal request ID. Runtime validates this before mutating memory.
342    pub journal_request_id: String,
343    pub outbox_appends: Vec<hypercall_db::DirectiveOutboxAppend>,
344    pub applied_tx: Option<oneshot::Sender<Result<(), String>>>,
345}
346
347/// Option-token deposit request sent from the chain observer to update
348/// engine-owned option inventory.
349#[derive(Debug)]
350pub struct OptionDepositRequest {
351    /// External durable mutations of engine-owned state must carry a UUID-backed
352    /// journal request ID. Runtime validates this before mutating memory.
353    pub request_id: String,
354    pub wallet: WalletAddress,
355    pub symbol: String,
356    pub quantity: Decimal,
357    pub timestamp_ms: u64,
358    pub applied_tx: Option<oneshot::Sender<Result<(), String>>>,
359}
360
361#[derive(Debug)]
362pub struct OptionWithdrawalRequest {
363    /// External durable mutations of engine-owned state must carry a UUID-backed
364    /// journal request ID. Runtime validates this before mutating memory.
365    pub request_id: String,
366    pub wallet: WalletAddress,
367    pub account: WalletAddress,
368    pub signer: WalletAddress,
369    pub rsm_signer: WalletAddress,
370    pub symbol: String,
371    pub quantity: Decimal,
372    pub nonce: u64,
373    pub action: Vec<u8>,
374    pub timestamp_ms: u64,
375    pub applied_tx: Option<oneshot::Sender<Result<OptionWithdrawalApplyReceipt, String>>>,
376}
377
378#[derive(Debug, Clone)]
379pub struct OptionWithdrawalApplyReceipt {
380    pub directive_id: String,
381    pub domain_status: hypercall_db::DirectiveDomainStatus,
382    pub delivery_status: hypercall_db::DirectiveDeliveryStatus,
383}
384
385#[derive(Debug)]
386pub struct CashWithdrawalRequest {
387    /// External durable mutations of engine-owned state must carry a UUID-backed
388    /// journal request ID. Runtime validates this before mutating memory.
389    pub request_id: String,
390    pub wallet: WalletAddress,
391    pub account: WalletAddress,
392    pub destination: WalletAddress,
393    pub signer: WalletAddress,
394    pub rsm_signer: WalletAddress,
395    pub amount: Decimal,
396    pub amount_wei: u64,
397    pub nonce: u64,
398    pub timestamp_ms: u64,
399    pub applied_tx: Option<oneshot::Sender<Result<CashWithdrawalApplyReceipt, String>>>,
400}
401
402#[derive(Debug, Clone)]
403pub struct CashWithdrawalApplyReceipt {
404    pub directive_id: String,
405    pub domain_status: hypercall_db::DirectiveDomainStatus,
406    pub delivery_status: hypercall_db::DirectiveDeliveryStatus,
407    pub balance_after: Decimal,
408}
409
410/// Liquidation bonus request sent from the chain observer to update
411/// balance_ledger.
412#[derive(Debug)]
413pub struct LiquidationBonusRequest {
414    /// External durable mutations of engine-owned state must carry a UUID-backed
415    /// journal request ID. Runtime validates this before mutating memory.
416    pub request_id: String,
417    pub wallet: WalletAddress,
418    pub amount: Decimal,
419    pub timestamp_ms: u64,
420    pub sequence: Option<u64>,
421    pub applied_tx: Option<oneshot::Sender<Result<(), String>>>,
422}
423
424/// Tier update sent from runtime handlers to refresh engine-owned admission
425/// state.
426#[derive(Debug)]
427pub struct TierUpdateRequest {
428    pub wallet: WalletAddress,
429    pub margin_mode: hypercall_types::MarginMode,
430    pub tier: String,
431    pub trading_limits: TradingLimits,
432    pub timestamp_ms: u64,
433    pub applied_tx: Option<oneshot::Sender<Result<(), String>>>,
434}
435
436/// HyperCore equity update from the Hydromancer feed for PM margin.
437#[derive(Debug)]
438pub struct HypercoreEquityRequest {
439    pub wallet: WalletAddress,
440    pub account_value: Decimal,
441    pub timestamp_ms: u64,
442}
443
444/// Outcome of promoting a standby engine to active.
445#[derive(Clone, Debug, PartialEq, Eq)]
446pub enum StandbyPromoteOutcome {
447    Promoted { queued_orders: usize },
448    AlreadyActive,
449}
450
451/// Promotes a standby engine to active.
452#[async_trait::async_trait]
453pub trait StandbyPromoter: Send + Sync {
454    async fn promote(&self) -> StandbyPromoteOutcome;
455}
456
457/// Build/version metadata surfaced by health and recovery endpoints.
458#[derive(Clone, Debug)]
459pub struct BuildInfo {
460    pub version: String,
461    pub commit: String,
462    pub git_ref: String,
463    pub build_time: String,
464}
465
466/// Engine-level quiesce control for restart and blue/green drain barriers.
467#[derive(Debug)]
468pub struct EngineQuiesceRequest {
469    pub action: EngineQuiesceAction,
470    pub response_tx: oneshot::Sender<EngineQuiesceReport>,
471}