Skip to main content

hypercall_api/handlers/
testnet.rs

1#[cfg(feature = "faucet")]
2use alloy::primitives::FixedBytes;
3#[cfg(feature = "faucet")]
4use rust_decimal::prelude::ToPrimitive;
5use utoipa::ToSchema;
6
7use crate::error::ApiError;
8use crate::sonic_json::SonicJson;
9use axum::extract::State;
10#[cfg(any(feature = "test-endpoints", all(test, feature = "faucet")))]
11use axum::http::StatusCode;
12#[cfg(feature = "test-endpoints")]
13use hypercall_runtime_api::MarketRequest;
14#[cfg(feature = "test-endpoints")]
15use hypercall_types::utils::get_timestamp_millis;
16#[cfg(feature = "test-endpoints")]
17use hypercall_types::ParsedOptionSymbol;
18#[cfg(any(feature = "faucet", feature = "test-endpoints"))]
19use hypercall_types::WalletAddress;
20use serde::{Deserialize, Serialize};
21
22use super::AppState;
23
24#[cfg(feature = "faucet")]
25use super::{FAUCET_LIFETIME_CAP_USDC, MARKET_MAKER_TIER};
26#[cfg(feature = "faucet")]
27use hypercall_db::FaucetWriter;
28#[cfg(feature = "faucet")]
29use hypercall_runtime_api::DepositRequest;
30use rust_decimal::Decimal;
31
32#[cfg(feature = "faucet")]
33fn synthetic_faucet_source_event_hash(sequence: u64) -> FixedBytes<32> {
34    let mut bytes = [0u8; 32];
35    bytes[..8].copy_from_slice(&sequence.to_be_bytes());
36    FixedBytes::from(bytes)
37}
38
39// =============================================================================
40// Balance Ledger Endpoint (Testnet Only)
41// =============================================================================
42
43#[cfg(feature = "test-endpoints")]
44#[derive(Debug, Deserialize, ToSchema)]
45pub struct TestBalanceLedgerRequest {
46    #[schema(value_type = String)]
47    pub wallet: hypercall_types::WalletAddress,
48}
49
50#[cfg(feature = "test-endpoints")]
51#[derive(Debug, Serialize, ToSchema)]
52pub struct TestBalanceLedgerResponse {
53    #[schema(value_type = String)]
54    pub wallet: hypercall_types::WalletAddress,
55    pub balance: String,
56}
57
58#[cfg(feature = "test-endpoints")]
59pub async fn get_balance_ledger(
60    State(state): State<AppState>,
61    SonicJson(request): SonicJson<TestBalanceLedgerRequest>,
62) -> Result<SonicJson<TestBalanceLedgerResponse>, ApiError> {
63    if !state.runtime_config.testnet_mode {
64        return Err(ApiError::forbidden(
65            "Balance ledger test endpoint requires testnet mode",
66        ));
67    }
68
69    let balance = state
70        .balance_provider
71        .get_balance(&request.wallet)
72        .await
73        .map_err(|error| {
74            ApiError::internal_error(format!("failed to read engine balance ledger: {error}"))
75        })?;
76    Ok(SonicJson(TestBalanceLedgerResponse {
77        wallet: request.wallet,
78        balance: balance.to_string(),
79    }))
80}
81
82// =============================================================================
83// Option Deposit Endpoint (Testnet Only)
84// =============================================================================
85
86#[cfg(feature = "test-endpoints")]
87#[derive(Debug, Deserialize, ToSchema)]
88pub struct TestOptionDepositRequest {
89    #[schema(value_type = String)]
90    pub wallet: hypercall_types::WalletAddress,
91    pub symbol: String,
92    pub quantity: String,
93}
94
95#[cfg(feature = "test-endpoints")]
96#[derive(Debug, Serialize, ToSchema)]
97pub struct TestOptionDepositResponse {
98    pub request_id: String,
99    #[schema(value_type = String)]
100    pub wallet: hypercall_types::WalletAddress,
101    pub symbol: String,
102    pub quantity: String,
103}
104
105#[cfg(feature = "test-endpoints")]
106pub async fn apply_option_deposit(
107    State(state): State<AppState>,
108    SonicJson(request): SonicJson<TestOptionDepositRequest>,
109) -> Result<SonicJson<TestOptionDepositResponse>, ApiError> {
110    if !state.runtime_config.testnet_mode {
111        return Err(ApiError::forbidden(
112            "Option deposit test endpoint requires testnet mode",
113        ));
114    }
115
116    let quantity = request
117        .quantity
118        .parse::<Decimal>()
119        .map_err(|_| ApiError::bad_request("quantity must be a decimal string"))?;
120    if quantity <= Decimal::ZERO {
121        return Err(ApiError::bad_request("quantity must be positive"));
122    }
123
124    let sender = state.option_deposit_sender.clone().ok_or_else(|| {
125        ApiError::new(
126            StatusCode::SERVICE_UNAVAILABLE,
127            "service_unavailable",
128            "option deposit engine path disabled",
129        )
130    })?;
131    let request_id = uuid::Uuid::now_v7().to_string();
132    let timestamp_ms = get_timestamp_millis();
133    let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
134    sender
135        .send(hypercall_runtime_api::OptionDepositRequest {
136            request_id: request_id.clone(),
137            wallet: request.wallet,
138            symbol: request.symbol.clone(),
139            quantity,
140            timestamp_ms,
141            applied_tx: Some(applied_tx),
142        })
143        .await
144        .map_err(|_| {
145            ApiError::new(
146                StatusCode::SERVICE_UNAVAILABLE,
147                "service_unavailable",
148                "option deposit engine path closed",
149            )
150        })?;
151    applied_rx
152        .await
153        .map_err(|_| {
154            ApiError::new(
155                StatusCode::SERVICE_UNAVAILABLE,
156                "service_unavailable",
157                "option deposit apply dropped",
158            )
159        })?
160        .map_err(ApiError::bad_request)?;
161
162    Ok(SonicJson(TestOptionDepositResponse {
163        request_id,
164        wallet: request.wallet,
165        symbol: request.symbol,
166        quantity: quantity.to_string(),
167    }))
168}
169
170// =============================================================================
171// Faucet Endpoint (compile-time gated by `faucet` feature)
172// =============================================================================
173
174#[cfg(feature = "faucet")]
175/// Request body for faucet endpoint
176#[derive(Debug, Deserialize, ToSchema)]
177pub struct FaucetRequest {
178    /// Wallet address to fund
179    #[schema(value_type = String)]
180    pub wallet: WalletAddress,
181    /// Amount of USDC to credit (default: 10000.0 if not specified)
182    #[serde(default = "default_faucet_amount")]
183    pub amount: f64,
184}
185
186#[cfg(feature = "faucet")]
187fn default_faucet_amount() -> f64 {
188    10000.0
189}
190
191#[cfg(feature = "faucet")]
192fn faucet_cap_exceeded_message() -> String {
193    format!(
194        "faucet lifetime deposit cap exceeded: wallet cap is {} USDC",
195        FAUCET_LIFETIME_CAP_USDC
196    )
197}
198
199#[cfg(feature = "faucet")]
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201enum FaucetCapMode {
202    Enforced,
203    MarketMakerExempt,
204}
205
206#[cfg(feature = "faucet")]
207impl FaucetCapMode {
208    fn enforces_limit(self) -> bool {
209        matches!(self, Self::Enforced)
210    }
211}
212
213#[cfg(feature = "faucet")]
214async fn resolve_faucet_cap_mode(state: &AppState, wallet: &WalletAddress) -> FaucetCapMode {
215    match state.tier_cache.get_tier(wallet).await {
216        Some(tier) if tier.tier == MARKET_MAKER_TIER => FaucetCapMode::MarketMakerExempt,
217        _ => FaucetCapMode::Enforced,
218    }
219}
220
221#[cfg(feature = "faucet")]
222async fn do_persist_faucet_credit(
223    db: &dyn FaucetWriter,
224    wallet: &WalletAddress,
225    amount: Decimal,
226    request_ts_ms: i64,
227    cap_mode: FaucetCapMode,
228) -> Result<u64, ApiError> {
229    // For market-maker-exempt mode, use Decimal::MAX as the limit (effectively unlimited).
230    let limit = match cap_mode {
231        FaucetCapMode::Enforced => Decimal::from(FAUCET_LIFETIME_CAP_USDC),
232        FaucetCapMode::MarketMakerExempt => Decimal::MAX,
233    };
234
235    let result = db
236        .persist_faucet_credit(wallet, amount, limit, request_ts_ms)
237        .await
238        .map_err(|e| {
239            let msg = e.to_string();
240            if msg.contains("exceed") {
241                ApiError::bad_request(faucet_cap_exceeded_message())
242            } else {
243                tracing::error!("Faucet: failed to persist credit for {}: {}", wallet, e);
244                ApiError::internal_error("Failed to persist faucet credit")
245            }
246        })?;
247
248    Ok(result.ledger_event_id as u64)
249}
250
251#[cfg(feature = "faucet")]
252fn engine_deposit_unavailable_error() -> ApiError {
253    ApiError::new(
254        axum::http::StatusCode::SERVICE_UNAVAILABLE,
255        "service_unavailable",
256        "Faucet engine deposit path is unavailable",
257    )
258}
259
260#[cfg(feature = "faucet")]
261async fn reserve_deposit_handoff(
262    deposit_sender: &Option<tokio::sync::mpsc::Sender<DepositRequest>>,
263) -> Result<tokio::sync::mpsc::OwnedPermit<DepositRequest>, ApiError> {
264    let Some(deposit_tx) = deposit_sender else {
265        return Err(engine_deposit_unavailable_error());
266    };
267
268    deposit_tx.clone().reserve_owned().await.map_err(|e| {
269        tracing::error!("Faucet: engine deposit handoff unavailable: {}", e);
270        engine_deposit_unavailable_error()
271    })
272}
273
274#[cfg(feature = "faucet")]
275/// Response from faucet endpoint
276#[derive(Debug, Serialize, ToSchema)]
277pub struct FaucetResponse {
278    #[schema(value_type = String)]
279    pub wallet: WalletAddress,
280    pub amount_credited: f64,
281    pub new_balance: f64,
282}
283
284#[cfg(feature = "faucet")]
285/// Credit test funds to a wallet (testnet only)
286///
287/// This endpoint is only available when the `faucet` feature is enabled
288/// and `modes.testnet_mode=true`.
289#[utoipa::path(
290    post,
291    path = "/faucet",
292    request_body = FaucetRequest,
293    responses(
294        (status = 200, description = "Funds credited successfully", body = ApiResponse<FaucetResponse>),
295        (status = 400, description = "Invalid request"),
296        (status = 403, description = "Faucet not enabled (production mode)"),
297        (status = 500, description = "Internal server error")
298    ),
299    tag = "testnet"
300)]
301pub async fn faucet(
302    State(state): State<AppState>,
303    SonicJson(request): SonicJson<FaucetRequest>,
304) -> Result<SonicJson<crate::models::ApiResponse<FaucetResponse>>, ApiError> {
305    // Check if in testnet mode
306    if !state.runtime_config.testnet_mode {
307        tracing::warn!("Faucet request rejected, testnet mode is disabled in backend config.");
308        return Err(ApiError::forbidden(
309            "Faucet not enabled, set modes.testnet_mode=true",
310        ));
311    }
312
313    // Validate request (WalletAddress is always valid since it's parsed)
314
315    if !request.amount.is_finite() || request.amount <= 0.0 {
316        tracing::error!("Faucet request with invalid amount: {}", request.amount);
317        return Err(ApiError::bad_request(
318            "Amount must be a finite positive number",
319        ));
320    }
321
322    let amount = request.amount;
323    let amount_decimal = Decimal::from_f64_retain(amount)
324        .ok_or_else(|| ApiError::bad_request("Amount must be a finite positive number"))?;
325    let cap_mode = resolve_faucet_cap_mode(&state, &request.wallet).await;
326    if cap_mode.enforces_limit() && amount_decimal > Decimal::from(FAUCET_LIFETIME_CAP_USDC) {
327        return Err(ApiError::bad_request(faucet_cap_exceeded_message()));
328    }
329
330    tracing::info!(
331        "Faucet: Crediting {} USDC to wallet {}",
332        amount,
333        request.wallet
334    );
335
336    let deposit_permit = reserve_deposit_handoff(&state.deposit_sender).await?;
337    let now_ts_ms = chrono::Utc::now().timestamp_millis();
338    let sequence = do_persist_faucet_credit(
339        state.db.as_ref(),
340        &request.wallet,
341        amount_decimal,
342        now_ts_ms,
343        cap_mode,
344    )
345    .await?;
346
347    let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
348    deposit_permit.send(DepositRequest {
349        wallet: request.wallet,
350        amount: amount_decimal,
351        timestamp_ms: now_ts_ms as u64,
352        sequence: Some(sequence),
353        source_event_hash: synthetic_faucet_source_event_hash(sequence),
354        // Journal the deposit so it survives engine restart. Without a
355        // request_id the DepositUpdate is applied to the in-memory ledger but
356        // never journaled; on restart, journal replay does not restore it and
357        // the engine cash diverges from the durable account_balances written
358        // above, tripping the snapshot-cash restore guard.
359        journal_request_id: uuid::Uuid::now_v7().to_string(),
360        outbox_appends: Vec::new(),
361        applied_tx: Some(applied_tx),
362    });
363
364    match applied_rx.await {
365        Ok(Ok(())) => {}
366        Ok(Err(e)) => panic!(
367            "Faucet: DB commit succeeded for {} but engine deposit apply failed: {}",
368            request.wallet, e
369        ),
370        Err(e) => panic!(
371            "Faucet: DB commit succeeded for {} but engine deposit apply ack dropped: {}",
372            request.wallet, e
373        ),
374    }
375    let new_balance = state
376        .balance_provider
377        .get_balance(&request.wallet)
378        .await
379        .map_err(|error| {
380            ApiError::internal_error(format!("failed to read engine balance ledger: {error}"))
381        })?;
382
383    tracing::info!(
384        "Faucet: Credited {} USDC to wallet {} via engine balance ledger (new balance: {})",
385        amount,
386        request.wallet,
387        new_balance
388    );
389
390    Ok(SonicJson(crate::models::ApiResponse::success(
391        FaucetResponse {
392            wallet: request.wallet,
393            amount_credited: amount,
394            new_balance: new_balance
395                .to_f64()
396                .expect("Faucet: new_balance Decimal not representable as f64"),
397        },
398    )))
399}
400
401// =============================================================================
402// Set Spot Price Endpoint (Testnet Only)
403// =============================================================================
404
405#[cfg(feature = "test-endpoints")]
406/// Request body for set_spot_price endpoint
407#[derive(Debug, Deserialize, ToSchema)]
408pub struct SetSpotPriceRequest {
409    /// Underlying asset (e.g., "BTC", "ETH")
410    pub underlying: String,
411    /// Spot price in USD
412    pub price: f64,
413}
414
415#[cfg(feature = "test-endpoints")]
416/// Response for set_spot_price endpoint
417#[derive(Debug, Serialize, ToSchema)]
418pub struct SetSpotPriceResponse {
419    /// Underlying asset
420    pub underlying: String,
421    /// New spot price
422    pub price: f64,
423}
424
425#[cfg(feature = "test-endpoints")]
426/// Request body for set_option_iv endpoint
427#[derive(Debug, Deserialize, ToSchema)]
428pub struct SetOptionIvRequest {
429    /// Option symbol (e.g., "BTC-20260331-100000-C")
430    pub symbol: String,
431    /// Implied volatility as decimal (e.g., 0.8 for 80%)
432    pub implied_vol: f64,
433}
434
435#[cfg(feature = "test-endpoints")]
436/// Response for set_option_iv endpoint
437#[derive(Debug, Serialize, ToSchema)]
438pub struct SetOptionIvResponse {
439    /// Option symbol
440    pub symbol: String,
441    /// Applied implied volatility
442    pub implied_vol: f64,
443}
444
445#[cfg(feature = "test-endpoints")]
446/// Set spot price for an underlying asset (TESTNET ONLY).
447///
448/// This endpoint is only available when `modes.enable_test_endpoints=true`.
449/// It allows tests to set spot prices for margin calculations.
450#[utoipa::path(
451    post,
452    path = "/test/spot-price",
453    request_body = SetSpotPriceRequest,
454    responses(
455        (status = 200, description = "Spot price set successfully", body = ApiResponse<SetSpotPriceResponse>),
456        (status = 400, description = "Invalid request"),
457        (status = 403, description = "Not enabled (production mode)"),
458        (status = 404, description = "Oracle not configured for underlying"),
459        (status = 500, description = "Internal server error")
460    ),
461    tag = "testnet"
462)]
463pub async fn set_spot_price(
464    State(state): State<AppState>,
465    SonicJson(request): SonicJson<SetSpotPriceRequest>,
466) -> Result<SonicJson<crate::models::ApiResponse<SetSpotPriceResponse>>, ApiError> {
467    if !request.price.is_finite() || request.price <= 0.0 {
468        tracing::error!(
469            "set_spot_price request with invalid price: {}",
470            request.price
471        );
472        return Err(ApiError::bad_request(
473            "Price must be a finite positive number",
474        ));
475    }
476
477    tracing::info!(
478        "Setting spot price for {} to ${:.2}",
479        request.underlying,
480        request.price
481    );
482
483    // Update spot price in greeks cache via oracle
484    let success = state
485        .greeks_cache
486        .set_spot_price_for_testing(&request.underlying, request.price)
487        .await;
488
489    if !success {
490        tracing::error!(
491            "Failed to set spot price for {}: oracle not configured",
492            request.underlying
493        );
494        return Err(ApiError::not_found(format!(
495            "Oracle not configured for underlying: {}",
496            request.underlying
497        )));
498    }
499
500    Ok(SonicJson(crate::models::ApiResponse::success(
501        SetSpotPriceResponse {
502            underlying: request.underlying,
503            price: request.price,
504        },
505    )))
506}
507
508#[cfg(feature = "test-endpoints")]
509/// Set theoretical option IV for repricing (TESTNET ONLY).
510///
511/// This endpoint is only available when `modes.enable_test_endpoints=true`.
512#[utoipa::path(
513    post,
514    path = "/test/option-iv",
515    request_body = SetOptionIvRequest,
516    responses(
517        (status = 200, description = "Option IV set successfully", body = ApiResponse<SetOptionIvResponse>),
518        (status = 400, description = "Invalid request"),
519        (status = 403, description = "Not enabled (production mode)")
520    ),
521    tag = "testnet"
522)]
523pub async fn set_option_iv(
524    State(state): State<AppState>,
525    SonicJson(request): SonicJson<SetOptionIvRequest>,
526) -> Result<SonicJson<crate::models::ApiResponse<SetOptionIvResponse>>, ApiError> {
527    if request.implied_vol <= 0.0 || !request.implied_vol.is_finite() {
528        return Err(ApiError::bad_request(
529            "implied_vol must be a finite positive decimal value",
530        ));
531    }
532
533    state
534        .greeks_cache
535        .set_theoretical_iv_for_testing(&request.symbol, request.implied_vol)
536        .await;
537
538    Ok(SonicJson(crate::models::ApiResponse::success(
539        SetOptionIvResponse {
540            symbol: request.symbol,
541            implied_vol: request.implied_vol,
542        },
543    )))
544}
545
546// =============================================================================
547// Expire Instrument Endpoint (Testnet Only)
548// =============================================================================
549
550#[cfg(feature = "test-endpoints")]
551/// Request body for expire_instrument endpoint
552#[derive(Debug, Deserialize, ToSchema)]
553pub struct ExpireInstrumentRequest {
554    /// Option symbol to expire (e.g., "BTC-20260331-100000-C")
555    pub symbol: String,
556}
557
558#[cfg(feature = "test-endpoints")]
559/// Response for expire_instrument endpoint
560#[derive(Debug, Serialize, ToSchema)]
561pub struct ExpireInstrumentResponse {
562    /// Symbol that was expired
563    pub symbol: String,
564    /// Result status from the engine
565    pub status: String,
566    /// Reason if settlement is pending
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub reason: Option<String>,
569}
570
571#[cfg(feature = "test-endpoints")]
572/// Expire an instrument and settle all positions (TESTNET ONLY).
573///
574/// This endpoint triggers expiry for a given symbol: it cancels all open orders,
575/// settles positions at the current settlement price, and removes the orderbook.
576/// Only available when `modes.enable_test_endpoints=true`.
577#[utoipa::path(
578    post,
579    path = "/test/expire-instrument",
580    request_body = ExpireInstrumentRequest,
581    responses(
582        (status = 200, description = "Instrument expired/pending settlement", body = ApiResponse<ExpireInstrumentResponse>),
583        (status = 400, description = "Invalid symbol"),
584        (status = 403, description = "Not enabled (production mode)"),
585        (status = 500, description = "Engine communication failure")
586    ),
587    tag = "testnet"
588)]
589pub async fn expire_instrument(
590    State(state): State<AppState>,
591    SonicJson(request): SonicJson<ExpireInstrumentRequest>,
592) -> Result<SonicJson<crate::models::ApiResponse<ExpireInstrumentResponse>>, ApiError> {
593    // Parse symbol to extract market fields
594    let parsed = ParsedOptionSymbol::from_symbol(&request.symbol)
595        .map_err(|e| ApiError::bad_request(format!("Invalid symbol: {}", e)))?;
596
597    tracing::info!("Expiring instrument {} (testnet)", request.symbol);
598
599    let market = hypercall_types::Market {
600        symbol: request.symbol.clone(),
601        underlying: parsed.underlying,
602        expiry: parsed.expiry,
603        strike: parsed.strike,
604        option_type: parsed.option_type,
605    };
606
607    let message = hypercall_types::MarketActionMessage {
608        market,
609        action: hypercall_types::MarketAction::ExpireMarket,
610        timestamp: get_timestamp_millis(),
611    };
612
613    let (response_tx, mut response_rx) = tokio::sync::mpsc::channel(1);
614
615    let market_request = MarketRequest {
616        message,
617        response_tx,
618    };
619
620    state
621        .market_sender
622        .send(market_request)
623        .await
624        .map_err(|_| ApiError::internal_error("Failed to send expire request to engine"))?;
625
626    // Wait for response with timeout
627    let response = tokio::time::timeout(std::time::Duration::from_secs(30), response_rx.recv())
628        .await
629        .map_err(|_| ApiError::internal_error("Timeout waiting for engine response"))?
630        .ok_or_else(|| ApiError::internal_error("Engine dropped response channel"))?;
631
632    let (status_str, reason) = match response.status {
633        hypercall_types::MarketUpdateStatus::MarketExpired => ("SETTLED".to_string(), None),
634        hypercall_types::MarketUpdateStatus::MarketPendingSettlement => {
635            ("PENDING_SETTLEMENT".to_string(), response.reason)
636        }
637        other => ("UNEXPECTED".to_string(), Some(format!("{:?}", other))),
638    };
639
640    Ok(SonicJson(crate::models::ApiResponse::success(
641        ExpireInstrumentResponse {
642            symbol: request.symbol,
643            status: status_str,
644            reason,
645        },
646    )))
647}
648
649// =============================================================================
650// Cancel All Orders Endpoint (Testnet Only)
651// =============================================================================
652
653/// Response for cancel_all_orders endpoint
654#[derive(Debug, Serialize, ToSchema)]
655pub struct CancelAllOrdersResponse {
656    /// Total MM orders found to cancel
657    pub total_mm_orders: usize,
658    /// Message indicating background processing started
659    pub message: String,
660}
661
662#[cfg(test)]
663mod faucet_tests {
664    use super::*;
665
666    #[cfg(feature = "faucet")]
667    #[tokio::test]
668    async fn test_reserve_deposit_handoff_rejects_missing_sender() {
669        let err = reserve_deposit_handoff(&None)
670            .await
671            .expect_err("missing engine deposit sender should fail before DB persistence");
672        assert_eq!(err.status, StatusCode::SERVICE_UNAVAILABLE);
673    }
674
675    #[cfg(feature = "faucet")]
676    #[tokio::test]
677    async fn test_reserve_deposit_handoff_rejects_closed_sender() {
678        let (deposit_tx, deposit_rx) = tokio::sync::mpsc::channel(1);
679        drop(deposit_rx);
680
681        let err = reserve_deposit_handoff(&Some(deposit_tx))
682            .await
683            .expect_err("closed engine deposit sender should fail before DB persistence");
684        assert_eq!(err.status, StatusCode::SERVICE_UNAVAILABLE);
685    }
686
687    #[cfg(feature = "faucet")]
688    #[tokio::test]
689    async fn test_reserve_deposit_handoff_accepts_open_sender() {
690        let (deposit_tx, _deposit_rx) = tokio::sync::mpsc::channel(1);
691
692        reserve_deposit_handoff(&Some(deposit_tx))
693            .await
694            .expect("open engine deposit sender should reserve handoff capacity");
695    }
696}