acta_maker_sdk/chain/vault/
crank.rs1use 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 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 pub final_user: Option<Pubkey>,
149}
150
151#[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}