templar_curator_primitives/recovery/
mod.rs1use alloc::string::String;
20use templar_vault_kernel::{
21 settle_proportional, AllocatingState, EscrowEntry, EscrowSettlement, KernelAction, OpState,
22 PayoutOutcome, PayoutState, RefreshingState, WithdrawingState,
23};
24use typed_builder::TypedBuilder;
25
26#[templar_vault_macros::vault_derive]
28#[derive(Clone, TypedBuilder)]
29#[builder(field_defaults(setter(into)))]
30pub struct RecoveryContext {
31 pub current_ns: u64,
33 #[builder(default)]
36 pub stuck_threshold_ns: u64,
37 #[builder(default)]
39 pub force_recovery: bool,
40}
41
42impl RecoveryContext {
43 pub fn new(current_ns: u64) -> Self {
44 Self {
45 current_ns,
46 stuck_threshold_ns: 0,
47 force_recovery: false,
48 }
49 }
50
51 pub fn with_stuck_threshold(current_ns: u64, stuck_threshold_ns: u64) -> Self {
52 Self {
53 current_ns,
54 stuck_threshold_ns,
55 force_recovery: false,
56 }
57 }
58
59 pub fn forced(current_ns: u64) -> Self {
60 Self {
61 current_ns,
62 stuck_threshold_ns: 0,
63 force_recovery: true,
64 }
65 }
66}
67
68impl Default for RecoveryContext {
69 fn default() -> Self {
70 Self::new(0)
71 }
72}
73
74#[templar_vault_macros::vault_derive]
76#[derive(Clone, Copy, PartialEq, Eq, TypedBuilder)]
77#[builder(field_defaults(setter(into)))]
78pub struct RecoveryProgress {
79 pub started_at_ns: u64,
81 pub last_progress_ns: u64,
83}
84
85impl RecoveryProgress {
86 pub const fn new(started_at_ns: u64) -> Self {
87 Self {
88 started_at_ns,
89 last_progress_ns: started_at_ns,
90 }
91 }
92
93 pub const fn with_last_progress(started_at_ns: u64, last_progress_ns: u64) -> Self {
94 Self {
95 started_at_ns,
96 last_progress_ns,
97 }
98 }
99}
100
101#[templar_vault_macros::vault_derive(borsh, serde)]
103#[derive(Clone, PartialEq, Eq)]
104pub struct RecoveryOutcome {
105 pub action: KernelAction,
106 pub success: bool,
107 pub message: Option<String>,
108}
109
110impl RecoveryOutcome {
111 pub fn success(action: KernelAction) -> Self {
112 Self {
113 action,
114 success: true,
115 message: None,
116 }
117 }
118
119 pub fn success_with_message(action: KernelAction, message: impl Into<String>) -> Self {
120 Self {
121 action,
122 success: true,
123 message: Some(message.into()),
124 }
125 }
126
127 pub fn failure(action: KernelAction, message: impl Into<String>) -> Self {
128 Self {
129 action,
130 success: false,
131 message: Some(message.into()),
132 }
133 }
134}
135
136pub fn determine_recovery_action(
138 state: &OpState,
139 context: &RecoveryContext,
140 progress: &RecoveryProgress,
141) -> Option<KernelAction> {
142 if matches!(state, OpState::Idle) {
143 return None;
144 }
145
146 if !is_recovery_eligible(context, progress) {
147 return None;
148 }
149
150 match state {
151 OpState::Allocating(alloc) => Some(abort_allocating_action(alloc)),
152 OpState::Withdrawing(withdraw) => Some(abort_withdrawing_action(withdraw)),
153 OpState::Refreshing(refresh) => Some(abort_refreshing_action(refresh)),
154 OpState::Payout(payout) => Some(settle_payout_failure_action(payout, payout.amount)),
155 OpState::Idle => None,
156 }
157}
158
159pub fn handle_allocation_failure(
161 state: &AllocatingState,
162 failure_reason: impl Into<String>,
163) -> RecoveryOutcome {
164 recovery_success_with_message(abort_allocating_action(state), failure_reason)
165}
166
167pub fn handle_withdrawal_failure(
169 state: &WithdrawingState,
170 failure_reason: impl Into<String>,
171) -> RecoveryOutcome {
172 recovery_success_with_message(abort_withdrawing_action(state), failure_reason)
173}
174
175pub fn handle_refresh_failure(
177 state: &RefreshingState,
178 failure_reason: impl Into<String>,
179) -> RecoveryOutcome {
180 recovery_success_with_message(abort_refreshing_action(state), failure_reason)
181}
182
183pub fn handle_payout_failure(
185 state: &PayoutState,
186 restore_idle: u128,
187 failure_reason: impl Into<String>,
188) -> RecoveryOutcome {
189 recovery_success_with_message(
190 settle_payout_failure_action(state, restore_idle),
191 failure_reason,
192 )
193}
194
195pub fn handle_payout_failure_default(
197 state: &PayoutState,
198 failure_reason: impl Into<String>,
199) -> RecoveryOutcome {
200 handle_payout_failure(state, state.amount, failure_reason)
201}
202
203pub fn compute_settlement_shares(
208 escrow_shares: u128,
209 expected_amount: u128,
210 collected_amount: u128,
211) -> EscrowSettlement {
212 if expected_amount == 0 || escrow_shares == 0 {
213 return EscrowSettlement::refund_all(escrow_shares);
214 }
215
216 if collected_amount >= expected_amount {
217 return EscrowSettlement::burn_all(escrow_shares);
218 }
219
220 settle_proportional(
221 &EscrowEntry::new(
222 templar_vault_kernel::Address([0u8; 32]),
223 escrow_shares,
224 templar_vault_kernel::TimestampNs(0),
225 expected_amount,
226 ),
227 collected_amount,
228 )
229}
230
231#[must_use]
235pub fn compute_payout_success_outcome(
236 escrow_shares: u128,
237 expected_amount: u128,
238 collected_amount: u128,
239) -> PayoutOutcome {
240 let EscrowSettlement {
241 to_burn: burn_shares,
242 refund: refund_shares,
243 } = compute_settlement_shares(escrow_shares, expected_amount, collected_amount);
244
245 PayoutOutcome::Success {
246 burn_shares,
247 refund_shares,
248 }
249}
250
251#[must_use]
253pub fn compute_payout_failure_outcome(escrow_shares: u128, restore_idle: u128) -> PayoutOutcome {
254 PayoutOutcome::Failure {
255 restore_idle,
256 refund_shares: escrow_shares,
257 }
258}
259
260#[templar_vault_macros::vault_derive]
264#[derive(Clone, Copy, Default)]
265pub struct RecoveryStats {
266 pub completed_targets: usize,
268 pub remaining_targets: usize,
270 pub collected_amount: u128,
272 pub remaining_amount: u128,
274 pub escrow_shares: u128,
276}
277
278pub fn compute_recovery_stats(state: &OpState) -> RecoveryStats {
280 match state {
281 OpState::Idle => RecoveryStats::default(),
282
283 OpState::Allocating(alloc) => {
284 let completed_targets = (alloc.index as usize).min(alloc.plan.len());
285 RecoveryStats {
286 completed_targets,
287 remaining_targets: alloc.plan.len().saturating_sub(completed_targets),
288 remaining_amount: alloc.remaining,
289 ..RecoveryStats::default()
290 }
291 }
292
293 OpState::Withdrawing(withdraw) => RecoveryStats {
294 completed_targets: withdraw.index as usize,
295 collected_amount: withdraw.collected,
296 remaining_amount: withdraw.remaining,
297 escrow_shares: withdraw.escrow_shares,
298 ..RecoveryStats::default()
299 },
300
301 OpState::Refreshing(refresh) => {
302 let completed_targets = (refresh.index as usize).min(refresh.plan.len());
303 RecoveryStats {
304 completed_targets,
305 remaining_targets: refresh.plan.len().saturating_sub(completed_targets),
306 ..RecoveryStats::default()
307 }
308 }
309
310 OpState::Payout(payout) => RecoveryStats {
311 collected_amount: payout.amount,
312 escrow_shares: payout.escrow_shares,
313 ..RecoveryStats::default()
314 },
315 }
316}
317
318fn recovery_success_with_message(
319 action: KernelAction,
320 message: impl Into<String>,
321) -> RecoveryOutcome {
322 RecoveryOutcome::success_with_message(action, message)
323}
324
325fn is_recovery_eligible(context: &RecoveryContext, progress: &RecoveryProgress) -> bool {
326 if context.force_recovery {
327 return true;
328 }
329
330 let threshold = context.stuck_threshold_ns;
331 if threshold == 0 {
332 return true;
333 }
334
335 context.current_ns.saturating_sub(progress.last_progress_ns) >= threshold
336}
337
338fn abort_allocating_action(state: &AllocatingState) -> KernelAction {
339 KernelAction::AbortAllocating {
340 op_id: state.op_id,
341 restore_idle: state.remaining,
342 }
343}
344
345fn abort_withdrawing_action(state: &WithdrawingState) -> KernelAction {
346 KernelAction::AbortWithdrawing {
347 op_id: state.op_id,
348 refund_shares: state.escrow_shares,
349 }
350}
351
352fn abort_refreshing_action(state: &RefreshingState) -> KernelAction {
353 KernelAction::AbortRefreshing { op_id: state.op_id }
354}
355
356fn settle_payout_failure_action(state: &PayoutState, restore_idle: u128) -> KernelAction {
357 KernelAction::SettlePayout {
358 op_id: state.op_id,
359 outcome: compute_payout_failure_outcome(state.escrow_shares, restore_idle),
360 }
361}