Skip to main content

acta_maker_sdk/chain/vault/
crank.rs

1use std::time::{Duration, Instant};
2
3use solana_sdk::pubkey::Pubkey;
4
5use super::{VaultAccount, VaultPhase, VaultSafety, VaultStagingState};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CapitalStep {
9    CloseDeposits,
10    OpenDeposits,
11    ComputeEquity,
12    ProcessWithdrawals,
13    ProcessDeposits,
14    FinalizeCycle,
15}
16
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct WindowClock {
19    pub open_for: Option<Duration>,
20    pub closed_for: Option<Duration>,
21}
22
23#[derive(Debug, Clone, Copy, Default)]
24pub struct WindowTracker {
25    state: Option<(bool, Instant)>,
26}
27
28impl WindowTracker {
29    pub fn observe(&mut self, open: bool, now: Instant) -> WindowClock {
30        let since = match self.state {
31            Some((was_open, since)) if was_open == open => since,
32            _ => {
33                self.state = Some((open, now));
34                now
35            }
36        };
37        let held = now.saturating_duration_since(since);
38        if open {
39            WindowClock {
40                open_for: Some(held),
41                closed_for: None,
42            }
43        } else {
44            WindowClock {
45                open_for: None,
46                closed_for: Some(held),
47            }
48        }
49    }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct WindowPolicy {
54    pub reopen: bool,
55    pub deposit_window: Duration,
56    pub trading_window: Duration,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct VaultView {
61    pub phase: VaultPhase,
62    pub safety: VaultSafety,
63    pub equity_computed: bool,
64    pub accepting_deposits: bool,
65    pub pending_deposits: u32,
66    pub pending_withdrawals: u32,
67    pub staging: VaultStagingState,
68}
69
70impl From<&VaultAccount> for VaultView {
71    fn from(vault: &VaultAccount) -> Self {
72        Self {
73            phase: vault.phase,
74            safety: vault.safety,
75            equity_computed: vault.equity_computed,
76            accepting_deposits: vault.accepting_deposits,
77            pending_deposits: vault.pending_deposit_count,
78            pending_withdrawals: vault.pending_withdraw_count,
79            staging: vault.staging,
80        }
81    }
82}
83
84#[must_use]
85pub fn next_step(
86    view: VaultView,
87    window: WindowClock,
88    policy: WindowPolicy,
89) -> Option<CapitalStep> {
90    use VaultPhase::{Idle, ProcessingCapital};
91    use VaultSafety::{Closing, Frozen, Normal, WithdrawOnly};
92
93    let pending = view.pending_deposits > 0 || view.pending_withdrawals > 0;
94    let step = match (view.safety, view.phase) {
95        (WithdrawOnly, Idle) if view.pending_withdrawals > 0 => {
96            Some(CapitalStep::ProcessWithdrawals)
97        }
98        (Frozen | Closing | WithdrawOnly, _) if view.pending_deposits > 0 => {
99            Some(CapitalStep::ProcessDeposits)
100        }
101        (Normal | Frozen, ProcessingCapital) => Some(if !view.equity_computed {
102            CapitalStep::ComputeEquity
103        } else if view.safety == Normal && view.pending_deposits > 0 {
104            CapitalStep::ProcessDeposits
105        } else if view.pending_withdrawals > 0 {
106            CapitalStep::ProcessWithdrawals
107        } else {
108            CapitalStep::FinalizeCycle
109        }),
110        (Frozen, Idle) if view.pending_withdrawals > 0 => Some(if view.equity_computed {
111            CapitalStep::ProcessWithdrawals
112        } else {
113            CapitalStep::ComputeEquity
114        }),
115        (Normal, Idle) if !pending && view.accepting_deposits => window
116            .open_for
117            .is_some_and(|open| open >= policy.deposit_window)
118            .then_some(CapitalStep::CloseDeposits),
119        (Normal, Idle) if !pending => (policy.reopen
120            && !view.equity_computed
121            && window
122                .closed_for
123                .is_some_and(|closed| closed >= policy.trading_window))
124        .then_some(CapitalStep::OpenDeposits),
125        (Normal, Idle) if view.accepting_deposits => Some(CapitalStep::CloseDeposits),
126        (Normal, Idle) if !view.equity_computed => Some(CapitalStep::ComputeEquity),
127        (Normal, Idle) if view.pending_deposits > 0 => Some(CapitalStep::ProcessDeposits),
128        (Normal, Idle) => Some(CapitalStep::ProcessWithdrawals),
129        _ => None,
130    };
131    // Equity and finalization require the operator to unstage first.
132    if view.staging.open.is_some()
133        && matches!(
134            step,
135            Some(CapitalStep::ComputeEquity | CapitalStep::FinalizeCycle)
136        )
137    {
138        None
139    } else {
140        step
141    }
142}
143
144#[derive(Debug, Default, Clone, PartialEq, Eq)]
145pub struct WithdrawalPlan {
146    pub partial: Vec<Pubkey>,
147    /// Sole remaining holder, redeemed through `ProcessFinalWithdrawal`.
148    pub final_user: Option<Pubkey>,
149}
150
151/// A queue that would drain the share supply holds its largest request back for the final redemption.
152#[must_use]
153pub fn plan_withdrawals(requests: &[(Pubkey, u64)], total_shares: u64) -> WithdrawalPlan {
154    let mut plan = WithdrawalPlan::default();
155    if let [(user, shares)] = requests
156        && *shares == total_shares
157    {
158        plan.final_user = Some(*user);
159        return plan;
160    }
161    let queued = requests
162        .iter()
163        .fold(0u64, |sum, (_, shares)| sum.saturating_add(*shares));
164    let mut ordered = requests.to_vec();
165    if !ordered.is_empty() && queued >= total_shares {
166        ordered.sort_by_key(|(_, shares)| std::cmp::Reverse(*shares));
167        ordered.remove(0);
168    }
169    plan.partial = ordered.into_iter().map(|(user, _)| user).collect();
170    plan
171}