templar_vault_kernel/state/queue/
mod.rs

1//! Chain-agnostic withdrawal queue types and pure logic functions.
2//!
3//! This module provides data structures for pending withdrawals and pure
4//! functions for queue logic. Storage implementation is left to chain-specific
5//! executors (NEAR, Soroban, etc.).
6
7#[cfg(feature = "borsh-schema")]
8use alloc::string::ToString;
9
10use crate::math::number::Number;
11use crate::math::wad::Wad;
12use crate::types::{Address, EscrowSettlement, TimestampNs};
13
14/// Minimum withdrawal amount in base asset units to prevent dust.
15/// Withdrawals below this threshold should be rejected.
16pub const MIN_WITHDRAWAL_ASSETS: u128 = 1_000;
17
18/// Maximum queue length before rejecting new requests.
19///
20/// This is a legacy alias of [`MAX_PENDING`] to keep queue helpers consistent
21/// with the kernel config limit and avoid ambiguous capacity thresholds.
22pub const MAX_QUEUE_LENGTH: u32 = crate::state::vault::MAX_PENDING as u32;
23
24/// Default cooldown period in nanoseconds (1 hour).
25/// Withdrawals cannot be processed until this time has elapsed.
26pub const DEFAULT_COOLDOWN_NS: u64 = 60 * 60 * 1_000_000_000;
27
28/// A pending withdrawal request in the queue.
29///
30/// Represents a user's request to redeem shares for underlying assets.
31/// The shares are held in escrow until the withdrawal is processed.
32#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
33#[derive(Clone, PartialEq, Eq)]
34pub struct PendingWithdrawal {
35    pub owner: Address,
36    pub receiver: Address,
37    pub escrow_shares: u128,
38    pub expected_assets: u128,
39    pub requested_at_ns: TimestampNs,
40}
41
42impl PendingWithdrawal {
43    #[inline]
44    #[must_use]
45    pub fn new(
46        owner: Address,
47        receiver: Address,
48        escrow_shares: u128,
49        expected_assets: u128,
50        requested_at_ns: TimestampNs,
51    ) -> Self {
52        Self {
53            owner,
54            receiver,
55            escrow_shares,
56            expected_assets,
57            requested_at_ns,
58        }
59    }
60}
61
62/// Result of attempting to satisfy a withdrawal from available assets.
63#[templar_vault_macros::vault_derive(borsh, serde)]
64#[derive(Clone, PartialEq, Eq)]
65pub struct WithdrawalResult {
66    pub assets_out: u128,
67    pub settlement: EscrowSettlement,
68}
69
70#[inline]
71#[must_use]
72pub fn compute_idle_settlement(
73    escrow_shares: u128,
74    expected_assets: u128,
75    available_assets: u128,
76) -> Option<WithdrawalResult> {
77    if expected_assets == 0 {
78        return Some(WithdrawalResult {
79            assets_out: 0,
80            settlement: if available_assets > 0 {
81                EscrowSettlement::burn_all(escrow_shares)
82            } else {
83                EscrowSettlement::refund_all(escrow_shares)
84            },
85        });
86    }
87
88    if available_assets >= expected_assets {
89        return Some(WithdrawalResult {
90            assets_out: expected_assets,
91            settlement: EscrowSettlement::burn_all(escrow_shares),
92        });
93    }
94
95    if available_assets == 0 {
96        return None;
97    }
98
99    let assets_out = available_assets;
100    Some(WithdrawalResult {
101        assets_out,
102        settlement: compute_settlement(escrow_shares, expected_assets, assets_out),
103    })
104}
105
106/// Status information for a single withdrawal request in the queue.
107#[templar_vault_macros::vault_derive(borsh, serde)]
108#[derive(Clone, PartialEq, Eq)]
109pub struct WithdrawalRequestStatus {
110    pub index: u32,
111    pub depth_assets: u128,
112    pub withdrawal: PendingWithdrawal,
113}
114
115/// Aggregate status of the entire withdrawal queue.
116#[templar_vault_macros::vault_derive(borsh, serde)]
117#[derive(Clone, Default, PartialEq, Eq)]
118pub struct QueueStatus {
119    pub length: u32,
120    pub total_expected_assets: u128,
121    pub total_escrow_shares: u128,
122}
123
124#[inline]
125#[must_use]
126pub fn is_valid_withdrawal_amount(assets: u128) -> bool {
127    assets >= MIN_WITHDRAWAL_ASSETS
128}
129
130#[inline]
131#[must_use]
132pub fn can_enqueue(current_length: u32) -> bool {
133    current_length < MAX_QUEUE_LENGTH
134}
135
136#[inline]
137#[must_use]
138pub fn is_past_cooldown(
139    requested_at_ns: TimestampNs,
140    now_ns: TimestampNs,
141    cooldown_ns: u64,
142) -> bool {
143    now_ns >= requested_at_ns.saturating_add_u64(cooldown_ns)
144}
145
146/// Check if a withdrawal can be satisfied given available assets.
147///
148/// A withdrawal can be satisfied if the available assets meet or exceed
149/// the expected asset amount from the withdrawal request.
150///
151/// # Arguments
152/// * `withdrawal` - The pending withdrawal request.
153/// * `available_assets` - Assets currently available for withdrawal.
154#[inline]
155#[must_use]
156pub fn can_satisfy_withdrawal(withdrawal: &PendingWithdrawal, available_assets: u128) -> bool {
157    available_assets >= withdrawal.expected_assets
158}
159
160/// Check if a withdrawal can be partially satisfied.
161///
162/// A partial satisfaction is possible when:
163/// 1. Available assets are non-zero but less than expected.
164/// 2. The available amount meets the minimum withdrawal threshold.
165///
166/// # Arguments
167/// * `withdrawal` - The pending withdrawal request.
168/// * `available_assets` - Assets currently available.
169#[inline]
170#[must_use]
171pub fn can_partially_satisfy(withdrawal: &PendingWithdrawal, available_assets: u128) -> bool {
172    available_assets > 0
173        && available_assets < withdrawal.expected_assets
174        && available_assets >= MIN_WITHDRAWAL_ASSETS
175}
176
177/// Calculate how many withdrawals can be fully satisfied from a queue.
178///
179/// Iterates through withdrawals in order, counting how many can be fully
180/// satisfied before running out of available assets.
181///
182/// # Arguments
183/// * `withdrawals` - Iterator over pending withdrawals (in queue order).
184/// * `available_assets` - Total assets available for withdrawals.
185///
186/// # Returns
187/// Tuple of (count of satisfiable withdrawals, total assets needed for those withdrawals).
188#[must_use]
189pub fn count_satisfiable<'a, I>(withdrawals: I, available_assets: u128) -> (u32, u128)
190where
191    I: IntoIterator<Item = &'a PendingWithdrawal>,
192{
193    let mut count = 0u32;
194    let mut total_assets = 0u128;
195
196    for withdrawal in withdrawals {
197        let new_total = total_assets.saturating_add(withdrawal.expected_assets);
198        if new_total > available_assets {
199            break;
200        }
201        total_assets = new_total;
202        count = count.saturating_add(1);
203    }
204
205    (count, total_assets)
206}
207
208// Pure Functions - Settlement Computation
209
210/// Compute escrow settlement when completing a withdrawal.
211///
212/// Determines how many shares to burn vs refund based on actual redemption
213/// versus the original expected amount.
214///
215/// # Arguments
216/// * `escrow_shares` - Total shares held in escrow.
217/// * `expected_assets` - Assets expected at time of request.
218/// * `actual_assets` - Assets actually being redeemed.
219///
220/// # Returns
221/// `EscrowSettlement` with shares to burn and shares to refund.
222///
223/// # Logic
224/// - If actual >= expected: burn all shares (full redemption).
225/// - If actual < expected: burn proportional shares, refund the rest.
226/// - If actual == 0: refund all shares (cancellation).
227#[inline]
228#[must_use]
229pub fn compute_settlement(
230    escrow_shares: u128,
231    expected_assets: u128,
232    actual_assets: u128,
233) -> EscrowSettlement {
234    if escrow_shares == 0 {
235        return EscrowSettlement {
236            to_burn: 0,
237            refund: 0,
238        };
239    }
240
241    if actual_assets == 0 {
242        // Full cancellation - refund all shares
243        return EscrowSettlement::refund_all(escrow_shares);
244    }
245
246    if expected_assets == 0 {
247        return EscrowSettlement::refund_all(escrow_shares);
248    }
249
250    if actual_assets >= expected_assets {
251        // Full redemption - burn all shares
252        return EscrowSettlement::burn_all(escrow_shares);
253    }
254
255    // Partial redemption - burn proportional shares, refund the rest.
256    // Use ceil to avoid zero-burn partials (assets out without burning shares).
257    // shares_to_burn = ceil(escrow_shares * actual_assets / expected_assets)
258    let shares_to_burn = Number::mul_div_ceil(
259        Number::from(escrow_shares),
260        Number::from(actual_assets),
261        Number::from(expected_assets),
262    )
263    .as_u128_trunc();
264
265    let shares_to_refund = escrow_shares.saturating_sub(shares_to_burn);
266
267    EscrowSettlement::partial(shares_to_burn, shares_to_refund)
268}
269
270/// Compute settlement using share price (WAD-scaled).
271///
272/// Alternative settlement computation using current share price instead of
273/// asset ratios. Useful when share price is already computed.
274///
275/// # Arguments
276/// * `escrow_shares` - Total shares held in escrow.
277/// * `share_price_wad` - Current share price as a WAD (1e18 = 1.0).
278/// * `original_share_price_wad` - Share price at time of request.
279///
280/// # Returns
281/// `EscrowSettlement` based on price ratio.
282#[inline]
283#[must_use]
284pub fn compute_settlement_by_price(
285    escrow_shares: u128,
286    share_price_wad: Wad,
287    original_share_price_wad: Wad,
288) -> EscrowSettlement {
289    if escrow_shares == 0 || original_share_price_wad.is_zero() {
290        return EscrowSettlement {
291            to_burn: 0,
292            refund: 0,
293        };
294    }
295
296    // If current price >= original price, full burn
297    if share_price_wad.0 >= original_share_price_wad.0 {
298        return EscrowSettlement::burn_all(escrow_shares);
299    }
300
301    // Partial burn: ratio of current to original price.
302    // Use ceil to avoid zero-burn partials (consistent with compute_settlement).
303    // shares_to_burn = ceil(escrow_shares * current_price / original_price)
304    let shares_to_burn = Number::mul_div_ceil(
305        Number::from(escrow_shares),
306        share_price_wad.0,
307        original_share_price_wad.0,
308    )
309    .as_u128_trunc();
310
311    let shares_to_refund = escrow_shares.saturating_sub(shares_to_burn);
312
313    EscrowSettlement::partial(shares_to_burn, shares_to_refund)
314}
315
316/// Compute the withdrawal result for a fully satisfied withdrawal.
317///
318/// # Arguments
319/// * `withdrawal` - The pending withdrawal to process.
320/// * `available_assets` - Assets available (must be >= withdrawal.expected_assets).
321///
322/// # Returns
323/// `Some(WithdrawalResult)` if withdrawal can be satisfied, `None` otherwise.
324#[must_use]
325pub fn compute_full_withdrawal(
326    withdrawal: &PendingWithdrawal,
327    available_assets: u128,
328) -> Option<WithdrawalResult> {
329    compute_idle_settlement(
330        withdrawal.escrow_shares,
331        withdrawal.expected_assets,
332        available_assets,
333    )
334    .filter(|result| result.assets_out == withdrawal.expected_assets)
335}
336
337/// Compute the withdrawal result for a partial withdrawal.
338///
339/// # Arguments
340/// * `withdrawal` - The pending withdrawal to process.
341/// * `available_assets` - Assets available (should be < withdrawal.expected_assets).
342///
343/// # Returns
344/// `WithdrawalResult` with proportional shares burned.
345#[must_use]
346pub fn compute_partial_withdrawal(
347    withdrawal: &PendingWithdrawal,
348    available_assets: u128,
349) -> WithdrawalResult {
350    let actual_assets = available_assets.min(withdrawal.expected_assets);
351
352    let settlement = compute_settlement(
353        withdrawal.escrow_shares,
354        withdrawal.expected_assets,
355        actual_assets,
356    );
357
358    WithdrawalResult {
359        assets_out: actual_assets,
360        settlement,
361    }
362}
363
364// Pure Functions - Queue Aggregation
365
366/// Compute aggregate queue status from an iterator of withdrawals.
367///
368/// # Arguments
369/// * `withdrawals` - Iterator over all pending withdrawals.
370///
371/// # Returns
372/// `QueueStatus` with totals across all requests.
373#[must_use]
374pub fn compute_queue_status<'a, I>(withdrawals: I) -> QueueStatus
375where
376    I: IntoIterator<Item = &'a PendingWithdrawal>,
377{
378    let mut status = QueueStatus::default();
379
380    for withdrawal in withdrawals {
381        status.length = status.length.saturating_add(1);
382        status.total_expected_assets = status
383            .total_expected_assets
384            .saturating_add(withdrawal.expected_assets);
385        status.total_escrow_shares = status
386            .total_escrow_shares
387            .saturating_add(withdrawal.escrow_shares);
388    }
389
390    status
391}
392
393/// Find a withdrawal request's status by owner.
394///
395/// # Arguments
396/// * `withdrawals` - Iterator over pending withdrawals in queue order.
397/// * `owner` - The owner to search for.
398///
399/// # Returns
400/// `Some(WithdrawalRequestStatus)` if found, `None` otherwise.
401#[must_use]
402pub fn find_request_status<'a, I>(
403    withdrawals: I,
404    owner: &Address,
405) -> Option<WithdrawalRequestStatus>
406where
407    I: IntoIterator<Item = &'a PendingWithdrawal>,
408{
409    let mut index = 0u32;
410    let mut depth_assets = 0u128;
411
412    for withdrawal in withdrawals {
413        if &withdrawal.owner == owner {
414            return Some(WithdrawalRequestStatus {
415                index,
416                depth_assets,
417                withdrawal: withdrawal.clone(),
418            });
419        }
420        depth_assets = depth_assets.saturating_add(withdrawal.expected_assets);
421        index = index.saturating_add(1);
422    }
423
424    None
425}
426
427// Queue Storage Types
428
429use alloc::vec::Vec;
430
431pub use crate::state::vault::MAX_PENDING;
432
433#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
434#[derive(Clone, PartialEq, Eq, Default)]
435pub struct PendingWithdrawals {
436    entries: Vec<PendingWithdrawalEntry>,
437}
438
439#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
440#[derive(Clone, PartialEq, Eq)]
441struct PendingWithdrawalEntry {
442    id: u64,
443    withdrawal: PendingWithdrawal,
444}
445
446impl PendingWithdrawals {
447    #[inline]
448    #[must_use]
449    pub fn new() -> Self {
450        Self {
451            entries: Vec::new(),
452        }
453    }
454
455    #[inline]
456    #[must_use]
457    pub fn len(&self) -> usize {
458        self.entries.len()
459    }
460
461    #[inline]
462    #[must_use]
463    pub fn is_empty(&self) -> bool {
464        self.entries.is_empty()
465    }
466
467    #[inline]
468    fn locate(&self, id: u64) -> Result<usize, usize> {
469        self.entries.binary_search_by(|entry| entry.id.cmp(&id))
470    }
471
472    pub fn insert(&mut self, id: u64, withdrawal: PendingWithdrawal) -> Option<PendingWithdrawal> {
473        match self.locate(id) {
474            Ok(index) => {
475                let old = core::mem::replace(&mut self.entries[index].withdrawal, withdrawal);
476                Some(old)
477            }
478            Err(index) => {
479                self.entries
480                    .insert(index, PendingWithdrawalEntry { id, withdrawal });
481                None
482            }
483        }
484    }
485
486    pub fn remove(&mut self, id: &u64) -> Option<PendingWithdrawal> {
487        self.locate(*id)
488            .ok()
489            .map(|index| self.entries.remove(index).withdrawal)
490    }
491
492    #[inline]
493    #[must_use]
494    pub fn get(&self, id: &u64) -> Option<&PendingWithdrawal> {
495        self.locate(*id)
496            .ok()
497            .map(|index| &self.entries[index].withdrawal)
498    }
499
500    #[inline]
501    #[must_use]
502    pub fn get_mut(&mut self, id: &u64) -> Option<&mut PendingWithdrawal> {
503        self.locate(*id)
504            .ok()
505            .map(|index| &mut self.entries[index].withdrawal)
506    }
507
508    #[inline]
509    #[must_use]
510    pub fn contains_key(&self, id: &u64) -> bool {
511        self.locate(*id).is_ok()
512    }
513
514    #[inline]
515    pub fn iter(&self) -> impl Iterator<Item = (&u64, &PendingWithdrawal)> {
516        self.entries
517            .iter()
518            .map(|entry| (&entry.id, &entry.withdrawal))
519    }
520
521    #[inline]
522    pub fn values(&self) -> impl Iterator<Item = &PendingWithdrawal> {
523        self.entries.iter().map(|entry| &entry.withdrawal)
524    }
525
526    #[inline]
527    pub fn keys(&self) -> impl Iterator<Item = &u64> {
528        self.entries.iter().map(|entry| &entry.id)
529    }
530
531    #[inline]
532    #[must_use]
533    pub fn from_sorted_entries(entries: Vec<(u64, PendingWithdrawal)>) -> Self {
534        let mut last_id = None;
535        for (id, _) in &entries {
536            if last_id.is_some_and(|last| last >= *id) {
537                crate::abort!("pending withdrawal entries must be sorted by unique id");
538            }
539            last_id = Some(*id);
540        }
541
542        Self {
543            entries: entries
544                .into_iter()
545                .map(|(id, withdrawal)| PendingWithdrawalEntry { id, withdrawal })
546                .collect(),
547        }
548    }
549}
550
551impl FromIterator<(u64, PendingWithdrawal)> for PendingWithdrawals {
552    fn from_iter<T: IntoIterator<Item = (u64, PendingWithdrawal)>>(iter: T) -> Self {
553        let mut pending = Self::new();
554        for (id, withdrawal) in iter {
555            assert!(
556                pending.insert(id, withdrawal).is_none(),
557                "duplicate pending withdrawal id: {id}"
558            );
559        }
560        pending
561    }
562}
563
564/// Withdrawal queue storage with FIFO ordering.
565///
566/// Maintains pending withdrawals keyed by monotonic IDs with escrow parity.
567/// The queue uses a sorted `Vec` for efficient iteration and predictable serialization, with two
568/// pointers to track the FIFO head and next ID to allocate.
569///
570/// # Invariants
571///
572/// - `pending_withdrawals.len() <= max_pending_withdrawals <= MAX_PENDING`
573/// - `next_withdraw_to_execute <= next_pending_withdrawal_id`
574/// - If `pending_withdrawals.len() > 0`, then `pending_withdrawals` contains `next_withdraw_to_execute`
575/// - FIFO withdrawal ordering; no skipping head
576/// - `cached_total_escrow == sum(pending_withdrawals.values().map(|w| w.escrow_shares))`
577/// - `cached_total_expected == sum(pending_withdrawals.values().map(|w| w.expected_assets))`
578#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
579#[derive(Clone, PartialEq, Eq)]
580pub struct WithdrawQueue {
581    /// Pending withdrawals keyed by monotonic ID.
582    pending_withdrawals: PendingWithdrawals,
583    /// ID of the next withdrawal to execute (queue head).
584    pub next_withdraw_to_execute: u64,
585    /// Next ID to allocate for new withdrawals (monotonic, never decremented).
586    pub next_pending_withdrawal_id: u64,
587    /// Cached total of escrow shares across all pending withdrawals.
588    /// Maintained incrementally on enqueue/dequeue for O(1) lookups.
589    cached_total_escrow: u128,
590    /// Cached total of expected assets across all pending withdrawals.
591    /// Maintained incrementally on enqueue/dequeue for O(1) lookups.
592    cached_total_expected: u128,
593}
594
595impl Default for WithdrawQueue {
596    fn default() -> Self {
597        Self::new()
598    }
599}
600
601/// Sum escrow shares and expected assets across an iterator of pending withdrawals.
602fn compute_pending_totals<'a>(iter: impl Iterator<Item = &'a PendingWithdrawal>) -> (u128, u128) {
603    iter.fold((0u128, 0u128), |(esc, exp), w| {
604        (
605            esc.saturating_add(w.escrow_shares),
606            exp.saturating_add(w.expected_assets),
607        )
608    })
609}
610
611impl WithdrawQueue {
612    /// Create a new empty withdrawal queue.
613    #[inline]
614    #[must_use]
615    pub fn new() -> Self {
616        Self {
617            pending_withdrawals: PendingWithdrawals::new(),
618            next_withdraw_to_execute: 0,
619            next_pending_withdrawal_id: 0,
620            cached_total_escrow: 0,
621            cached_total_expected: 0,
622        }
623    }
624
625    /// Create a queue with initial state (for testing or recovery).
626    #[must_use]
627    pub fn with_state<I>(
628        pending_withdrawals: I,
629        next_withdraw_to_execute: u64,
630        next_pending_withdrawal_id: u64,
631    ) -> Self
632    where
633        I: IntoIterator<Item = (u64, PendingWithdrawal)>,
634    {
635        let pending_withdrawals =
636            PendingWithdrawals::from_sorted_entries(pending_withdrawals.into_iter().collect());
637        let (cached_total_escrow, cached_total_expected) =
638            compute_pending_totals(pending_withdrawals.values());
639        Self {
640            pending_withdrawals,
641            next_withdraw_to_execute,
642            next_pending_withdrawal_id,
643            cached_total_escrow,
644            cached_total_expected,
645        }
646    }
647
648    /// Returns the current queue length.
649    #[inline]
650    #[must_use]
651    pub fn len(&self) -> usize {
652        self.pending_withdrawals.len()
653    }
654
655    #[inline]
656    #[must_use]
657    pub fn pending_withdrawals(&self) -> &PendingWithdrawals {
658        &self.pending_withdrawals
659    }
660
661    /// Returns true if the queue is empty.
662    #[inline]
663    #[must_use]
664    pub fn is_empty(&self) -> bool {
665        self.pending_withdrawals.is_empty()
666    }
667
668    /// Check if the queue can accept a new withdrawal given the max limit.
669    ///
670    /// # Arguments
671    /// * `max_pending` - Maximum allowed pending withdrawals.
672    ///
673    /// # Returns
674    /// `true` if the queue has room for another withdrawal.
675    #[inline]
676    #[must_use]
677    pub fn can_enqueue(&self, max_pending: u32) -> bool {
678        self.pending_withdrawals.len() < (max_pending as usize).min(MAX_PENDING)
679    }
680
681    /// Enqueue a new pending withdrawal.
682    ///
683    /// Convenience wrapper that constructs a `PendingWithdrawal` and delegates
684    /// to [`enqueue_withdrawal`](Self::enqueue_withdrawal).
685    pub fn enqueue(
686        &mut self,
687        owner: Address,
688        receiver: Address,
689        escrow_shares: u128,
690        expected_assets: u128,
691        requested_at_ns: TimestampNs,
692        max_pending: u32,
693    ) -> Result<u64, QueueError> {
694        let withdrawal = PendingWithdrawal::new(
695            owner,
696            receiver,
697            escrow_shares,
698            expected_assets,
699            requested_at_ns,
700        );
701        self.enqueue_withdrawal(withdrawal, max_pending)
702    }
703
704    /// Enqueue a pre-constructed pending withdrawal.
705    ///
706    /// Allocates a new monotonic ID and inserts the withdrawal at the tail.
707    ///
708    /// # Returns
709    /// `Ok(id)` with the allocated withdrawal ID, or `Err(QueueError)` if full.
710    pub fn enqueue_withdrawal(
711        &mut self,
712        withdrawal: PendingWithdrawal,
713        max_pending: u32,
714    ) -> Result<u64, QueueError> {
715        if !self.can_enqueue(max_pending) {
716            return Err(QueueError::QueueFull {
717                current: self.pending_withdrawals.len() as u32,
718                max: max_pending,
719            });
720        }
721
722        let id = self.next_pending_withdrawal_id;
723        let next_id = id
724            .checked_add(1)
725            .ok_or_else(|| QueueError::InvariantViolation {
726                message: alloc::string::String::from("next_pending_withdrawal_id overflow"),
727            })?;
728
729        // Compute cache totals first so we can fail without mutating queue state.
730        let new_escrow = self
731            .cached_total_escrow
732            .checked_add(withdrawal.escrow_shares)
733            .ok_or(QueueError::CacheOverflow)?;
734        let new_expected = self
735            .cached_total_expected
736            .checked_add(withdrawal.expected_assets)
737            .ok_or(QueueError::CacheOverflow)?;
738
739        self.pending_withdrawals.insert(id, withdrawal);
740        self.next_pending_withdrawal_id = next_id;
741
742        // Update cached totals (overflow already checked)
743        self.cached_total_escrow = new_escrow;
744        self.cached_total_expected = new_expected;
745
746        Ok(id)
747    }
748
749    /// Get the head of the queue without removing it.
750    ///
751    /// # Returns
752    /// `Some((id, &withdrawal))` if non-empty, `None` if empty.
753    #[inline]
754    #[must_use]
755    pub fn head(&self) -> Option<(u64, &PendingWithdrawal)> {
756        self.pending_withdrawals
757            .get(&self.next_withdraw_to_execute)
758            .map(|w| (self.next_withdraw_to_execute, w))
759    }
760
761    /// Dequeue and return the head of the queue (FIFO).
762    ///
763    /// Removes the head and advances `next_withdraw_to_execute` to the next
764    /// available ID in the queue (or to `next_pending_withdrawal_id` if empty).
765    ///
766    /// # Returns
767    /// `Some((id, withdrawal))` if non-empty, `None` if empty.
768    ///
769    pub fn dequeue(&mut self) -> Option<(u64, PendingWithdrawal)> {
770        if self.is_empty() {
771            return None;
772        }
773
774        let head_id = self.next_withdraw_to_execute;
775        let withdrawal = self.pending_withdrawals.remove(&head_id)?;
776
777        self.cached_total_escrow = crate::unwrap_abort!(
778            self.cached_total_escrow
779                .checked_sub(withdrawal.escrow_shares),
780            crate::abort::OVERFLOW,
781        );
782        self.cached_total_expected = crate::unwrap_abort!(
783            self.cached_total_expected
784                .checked_sub(withdrawal.expected_assets),
785            crate::abort::OVERFLOW,
786        );
787
788        // Advance to the next ID in the queue
789        self.next_withdraw_to_execute = self
790            .pending_withdrawals
791            .keys()
792            .next()
793            .copied()
794            .unwrap_or(self.next_pending_withdrawal_id);
795
796        Some((head_id, withdrawal))
797    }
798
799    /// Get a pending withdrawal by ID.
800    ///
801    /// # Arguments
802    /// * `id` - The withdrawal ID to look up.
803    ///
804    /// # Returns
805    /// `Some(&withdrawal)` if found, `None` otherwise.
806    #[inline]
807    #[must_use]
808    pub fn get(&self, id: u64) -> Option<&PendingWithdrawal> {
809        self.pending_withdrawals.get(&id)
810    }
811
812    /// Check if a withdrawal ID exists in the queue.
813    ///
814    /// # Arguments
815    /// * `id` - The withdrawal ID to check.
816    ///
817    /// # Returns
818    /// `true` if the withdrawal exists.
819    #[inline]
820    #[must_use]
821    pub fn contains(&self, id: u64) -> bool {
822        self.pending_withdrawals.contains_key(&id)
823    }
824
825    /// Iterate over all pending withdrawals in FIFO order.
826    ///
827    /// # Returns
828    /// Iterator yielding `(id, &withdrawal)` pairs in order.
829    pub fn iter(&self) -> impl Iterator<Item = (u64, &PendingWithdrawal)> {
830        self.pending_withdrawals.iter().map(|(k, v)| (*k, v))
831    }
832
833    /// Check invariants for the withdrawal queue.
834    ///
835    /// Validates:
836    /// - `next_withdraw_to_execute <= next_pending_withdrawal_id`
837    /// - If non-empty, head ID exists in the map
838    /// - Cached totals match computed totals
839    ///
840    /// # Returns
841    /// `true` if all invariants hold.
842    #[must_use]
843    pub fn check_invariants(&self) -> bool {
844        // next_withdraw_to_execute <= next_pending_withdrawal_id
845        if self.next_withdraw_to_execute > self.next_pending_withdrawal_id {
846            return false;
847        }
848
849        // If non-empty, the head must exist
850        if !self.is_empty()
851            && !self
852                .pending_withdrawals
853                .contains_key(&self.next_withdraw_to_execute)
854        {
855            return false;
856        }
857
858        // Verify cached totals match computed totals
859        let (computed_escrow, computed_expected) =
860            compute_pending_totals(self.pending_withdrawals.values());
861
862        if self.cached_total_escrow != computed_escrow {
863            return false;
864        }
865        if self.cached_total_expected != computed_expected {
866            return false;
867        }
868
869        true
870    }
871
872    /// Check invariants including the max pending limit.
873    ///
874    /// # Arguments
875    /// * `max_pending` - Maximum allowed pending withdrawals.
876    ///
877    /// # Returns
878    /// `true` if all invariants hold including queue length bounds.
879    #[must_use]
880    pub fn check_invariants_with_max(&self, max_pending: u32) -> bool {
881        // Check basic invariants first
882        if !self.check_invariants() {
883            return false;
884        }
885
886        // pending_withdrawals.len() <= max_pending_withdrawals <= MAX_PENDING
887        let len = self.pending_withdrawals.len();
888        if len > (max_pending as usize) || (max_pending as usize) > MAX_PENDING {
889            return false;
890        }
891
892        true
893    }
894
895    /// Compute aggregate queue statistics.
896    ///
897    /// # Returns
898    /// `QueueStatus` with totals.
899    #[inline]
900    #[must_use]
901    pub fn status(&self) -> QueueStatus {
902        QueueStatus {
903            length: self.pending_withdrawals.len() as u32,
904            total_expected_assets: self.cached_total_expected,
905            total_escrow_shares: self.cached_total_escrow,
906        }
907    }
908
909    /// Get total escrowed shares across all pending withdrawals.
910    ///
911    /// Returns cached value in O(1) time.
912    ///
913    /// # Returns
914    /// Total escrow shares.
915    #[inline]
916    #[must_use]
917    pub fn total_escrow_shares(&self) -> u128 {
918        self.cached_total_escrow
919    }
920
921    /// Get total expected assets across all pending withdrawals.
922    ///
923    /// Returns cached value in O(1) time.
924    ///
925    /// # Returns
926    /// Total expected assets.
927    #[inline]
928    #[must_use]
929    pub fn total_expected_assets(&self) -> u128 {
930        self.cached_total_expected
931    }
932}
933
934/// Errors that can occur during queue operations.
935#[templar_vault_macros::vault_derive(borsh, serde)]
936#[derive(Clone, PartialEq, Eq)]
937pub enum QueueError {
938    /// Queue is at maximum capacity.
939    QueueFull { current: u32, max: u32 },
940    /// Withdrawal ID not found.
941    WithdrawalNotFound { id: u64 },
942    /// Queue is empty.
943    QueueEmpty,
944    /// Invariant violation detected.
945    InvariantViolation { message: alloc::string::String },
946    /// Cached total overflow.
947    CacheOverflow,
948}
949
950#[cfg(test)]
951mod tests;