templar_vault_kernel/state/op_state/
mod.rs

1//! Operation state machine for asynchronous vault operations.
2//!
3//! This module provides a chain-agnostic state machine for managing the lifecycle
4//! of allocation, withdrawal, refresh, and payout operations in a vault.
5//!
6//! # State Machine
7//!
8//! ```text
9//!                    +-------+
10//!                    | Idle  |<-----------------------+
11//!                    +-------+                        |
12//!                        |                            |
13//!          +-------------+-------------+              |
14//!          |                           |              |
15//!          v                           v              |
16//!    +------------+            +-------------+        |
17//!    | Allocating |            | Refreshing  |--------+
18//!    +------------+            +-------------+        |
19//!          |                                          |
20//!          | (on completion or stop)                  |
21//!          v                                          |
22//!    +-------------+                                  |
23//!    | Withdrawing |----------------------------------+
24//!    +-------------+                                  |
25//!          |                                          |
26//!          | (when enough collected)                  |
27//!          v                                          |
28//!    +--------+                                       |
29//!    | Payout |---------------------------------------+
30//!    +--------+
31//! ```
32//!
33#[cfg(feature = "borsh-schema")]
34use alloc::string::ToString;
35use alloc::vec::Vec;
36
37use derive_more::{From, IsVariant};
38
39use crate::types::Address;
40
41pub type TargetId = u32;
42
43#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
44#[derive(Clone, Copy, PartialEq, Eq)]
45pub struct AllocationPlanEntry {
46    pub target_id: TargetId,
47    pub amount: u128,
48}
49
50impl AllocationPlanEntry {
51    #[inline]
52    #[must_use]
53    pub const fn new(target_id: TargetId, amount: u128) -> Self {
54        Self { target_id, amount }
55    }
56}
57
58/// No operation in-flight. The vault is ready to start a new allocation or withdrawal.
59#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
60#[derive(Clone, PartialEq, Eq)]
61pub struct IdleState;
62
63/// Supplying idle underlying to targets according to a plan or queue.
64///
65/// # Transitions
66/// - On completion of allocation: `Withdrawing` (to satisfy pending user requests) or `Idle` (if stopped).
67/// - On stop/failure: `Idle`.
68#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
69#[derive(Clone, PartialEq, Eq)]
70pub struct AllocatingState {
71    pub op_id: u64,
72    pub index: u32,
73    pub remaining: u128,
74    pub plan: Vec<AllocationPlanEntry>,
75}
76
77/// Collecting liquidity from targets to satisfy a user withdrawal/redeem request.
78///
79/// # Transitions
80/// - Advance within queue: `Withdrawing` (index increments) while collecting funds.
81/// - When enough is collected to satisfy the request: `Payout`.
82/// - If the op is stopped or cannot proceed and needs to refund: `Idle` (escrow_shares refunded).
83#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
84#[derive(Clone, PartialEq, Eq)]
85pub struct WithdrawingState {
86    pub op_id: u64,
87    pub request_id: u64,
88    pub index: u32,
89    pub remaining: u128,
90    pub collected: u128,
91    pub receiver: Address,
92    pub owner: Address,
93    pub escrow_shares: u128,
94}
95
96/// Read-only refresh of target principals to update stored AUM.
97///
98/// # Transitions
99/// - On completion: `Idle`.
100/// - On failure: `Idle` (with potentially stale AUM data).
101#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
102#[derive(Clone, PartialEq, Eq)]
103pub struct RefreshingState {
104    pub op_id: u64,
105    pub index: u32,
106    pub plan: Vec<TargetId>,
107}
108
109/// Final step that transfers assets to the receiver and settles the share escrow.
110///
111/// # Transitions
112/// - On success or failure: `Idle`.
113///
114/// # Invariant hooks
115/// - `idle_balance` decreases only on payout success by `amount`.
116/// - On success, `burn_shares` are burned from `escrow_shares`; any remainder is refunded.
117/// - On failure, all `escrow_shares` are refunded.
118#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
119#[derive(Clone, PartialEq, Eq)]
120pub struct PayoutState {
121    pub op_id: u64,
122    pub request_id: u64,
123    pub receiver: Address,
124    pub amount: u128,
125    pub owner: Address,
126    pub escrow_shares: u128,
127    pub burn_shares: u128,
128}
129
130impl AllocatingState {
131    /// Advance to the next allocation step after `amount_allocated` was supplied.
132    #[inline]
133    #[must_use]
134    pub fn advance(self, amount_allocated: u128) -> Self {
135        Self {
136            op_id: self.op_id,
137            index: self.index.saturating_add(1),
138            remaining: self.remaining.saturating_sub(amount_allocated),
139            plan: self.plan,
140        }
141    }
142}
143
144impl WithdrawingState {
145    /// Advance to the next withdrawal step after `amount_collected` was received.
146    #[inline]
147    #[must_use]
148    pub fn advance(self, amount_collected: u128) -> Self {
149        Self {
150            op_id: self.op_id,
151            request_id: self.request_id,
152            index: self.index.saturating_add(1),
153            remaining: self.remaining.saturating_sub(amount_collected),
154            collected: self.collected.saturating_add(amount_collected),
155            receiver: self.receiver,
156            owner: self.owner,
157            escrow_shares: self.escrow_shares,
158        }
159    }
160}
161
162impl RefreshingState {
163    /// Advance to the next refresh step.
164    #[inline]
165    #[must_use]
166    pub fn advance(self) -> Self {
167        Self {
168            op_id: self.op_id,
169            index: self.index.saturating_add(1),
170            plan: self.plan,
171        }
172    }
173}
174
175/// Operation state machine for asynchronous allocation, withdrawal, and payout flows.
176///
177/// # State Machine
178/// - `Allocating` -> `Withdrawing` (or `Idle` via stop)
179/// - `Withdrawing` -> `Withdrawing` (advance) | `Payout` | `Idle` (refund)
180/// - `Refreshing` -> `Idle`
181/// - `Payout` -> `Idle` (success or failure)
182///
183/// # Invariants
184/// - `idle_balance` increases only when funds are received and decreases only on payout success.
185/// - `escrow_shares` are refunded on stop/failure or partially burned/refunded on payout success.
186#[templar_vault_macros::vault_derive(borsh, borsh_schema, serde)]
187#[derive(Clone, Default, PartialEq, Eq, From, IsVariant)]
188pub enum OpState {
189    /// No operation in-flight. The vault is ready to start a new allocation or withdrawal.
190    #[default]
191    Idle,
192
193    /// Supplying idle underlying to targets according to a plan or queue.
194    ///
195    /// # Transitions
196    /// - On completion of allocation: `Withdrawing` (to satisfy pending user requests) or `Idle` (if stopped).
197    /// - On stop/failure: `Idle`.
198    Allocating(AllocatingState),
199
200    /// Collecting liquidity from targets to satisfy a user withdrawal/redeem request.
201    ///
202    /// # Transitions
203    /// - Advance within queue: `Withdrawing` (index increments) while collecting funds.
204    /// - When enough is collected to satisfy the request: `Payout`.
205    /// - If the op is stopped or cannot proceed and needs to refund: `Idle` (escrow_shares refunded).
206    Withdrawing(WithdrawingState),
207
208    /// Read-only refresh of target principals to update stored AUM.
209    Refreshing(RefreshingState),
210
211    /// Final step that transfers assets to the receiver and settles the share escrow.
212    ///
213    /// # Transitions
214    /// - On success or failure: `Idle`.
215    ///
216    /// # Invariant hooks
217    /// - `idle_balance` decreases only on payout success by `amount`.
218    /// - On success, `burn_shares` are burned from `escrow_shares`; any remainder is refunded.
219    /// - On failure, all `escrow_shares` are refunded.
220    Payout(PayoutState),
221}
222
223// Note: From<AllocatingState>, From<WithdrawingState>, From<RefreshingState>,
224// From<PayoutState> are auto-generated by derive_more::From
225
226impl From<IdleState> for OpState {
227    fn from(_: IdleState) -> Self {
228        OpState::Idle
229    }
230}
231
232// --- Accessor methods ---
233
234impl OpState {
235    /// Returns a numeric code for the current op state.
236    #[inline]
237    #[must_use]
238    pub const fn kind_code(&self) -> u32 {
239        match self {
240            OpState::Idle => 0,
241            OpState::Allocating(_) => 1,
242            OpState::Withdrawing(_) => 2,
243            OpState::Refreshing(_) => 3,
244            OpState::Payout(_) => 4,
245        }
246    }
247
248    /// Returns a human-readable name for the current op state.
249    #[cfg(not(target_arch = "wasm32"))]
250    #[inline]
251    #[must_use]
252    pub const fn kind_name(&self) -> &'static str {
253        match self {
254            OpState::Idle => "Idle",
255            OpState::Allocating(_) => "Allocating",
256            OpState::Withdrawing(_) => "Withdrawing",
257            OpState::Refreshing(_) => "Refreshing",
258            OpState::Payout(_) => "Payout",
259        }
260    }
261
262    /// Returns a reference to the idle state if this is `Idle`, otherwise `None`.
263    #[inline]
264    #[must_use]
265    pub const fn as_idle(&self) -> Option<&IdleState> {
266        match self {
267            OpState::Idle => Some(&IdleState),
268            _ => None,
269        }
270    }
271
272    /// Returns a reference to the allocating state if this is `Allocating`, otherwise `None`.
273    #[inline]
274    #[must_use]
275    pub const fn as_allocating(&self) -> Option<&AllocatingState> {
276        match self {
277            OpState::Allocating(s) => Some(s),
278            _ => None,
279        }
280    }
281
282    /// Returns a reference to the withdrawing state if this is `Withdrawing`, otherwise `None`.
283    #[inline]
284    #[must_use]
285    pub const fn as_withdrawing(&self) -> Option<&WithdrawingState> {
286        match self {
287            OpState::Withdrawing(s) => Some(s),
288            _ => None,
289        }
290    }
291
292    /// Returns a reference to the refreshing state if this is `Refreshing`, otherwise `None`.
293    #[inline]
294    #[must_use]
295    pub const fn as_refreshing(&self) -> Option<&RefreshingState> {
296        match self {
297            OpState::Refreshing(s) => Some(s),
298            _ => None,
299        }
300    }
301
302    /// Returns a reference to the payout state if this is `Payout`, otherwise `None`.
303    #[inline]
304    #[must_use]
305    pub const fn as_payout(&self) -> Option<&PayoutState> {
306        match self {
307            OpState::Payout(s) => Some(s),
308            _ => None,
309        }
310    }
311
312    // Note: is_idle(), is_allocating(), is_withdrawing(), is_refreshing(), is_payout()
313    // are auto-generated by derive_more::IsVariant
314
315    /// Returns the operation ID if this state has one, otherwise `None`.
316    #[inline]
317    #[must_use]
318    pub const fn op_id(&self) -> Option<u64> {
319        match self {
320            OpState::Idle => None,
321            OpState::Allocating(s) => Some(s.op_id),
322            OpState::Withdrawing(s) => Some(s.op_id),
323            OpState::Refreshing(s) => Some(s.op_id),
324            OpState::Payout(s) => Some(s.op_id),
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests;