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