1use alloc::vec;
30use alloc::vec::Vec;
31
32use crate::effects::{KernelEffect, KernelEvent};
33use crate::state::op_state::{
34 AllocatingState, AllocationPlanEntry, OpState, PayoutState, RefreshingState, TargetId,
35 WithdrawingState,
36};
37use crate::types::Address;
38
39#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
41#[derive(Clone, PartialEq, Eq)]
42pub enum TransitionError {
43 WrongState,
44 OpIdMismatch { expected: u64, actual: u64 },
45 EmptyAllocationPlan,
46 EmptyRefreshPlan,
47 ZeroWithdrawalAmount,
48 ZeroEscrowShares,
49 InvalidIndex { index: u32, max: u32 },
50 CollectionOverflow { collected: u128, remaining: u128 },
51 AllocationOverflow { allocated: u128, remaining: u128 },
52 ZeroAllocationAmount,
53 BurnExceedsEscrow { burn: u128, escrow: u128 },
54 WithdrawalIncomplete { remaining: u128, collected: u128 },
55}
56
57impl TransitionError {
58 #[allow(dead_code)]
60 #[cfg(not(target_arch = "wasm32"))]
61 pub(crate) fn state_name(state: &OpState) -> &'static str {
62 state.kind_name()
63 }
64}
65
66#[inline]
68fn validate_plan_index(index: u32, plan_len: usize) -> Result<(), TransitionError> {
69 let len = plan_len as u32;
70 if index >= len {
71 return Err(TransitionError::InvalidIndex {
72 index,
73 max: len.saturating_sub(1),
74 });
75 }
76 Ok(())
77}
78
79#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
81#[derive(Clone, PartialEq, Eq)]
82pub struct TransitionResult {
83 pub new_state: OpState,
85 pub effects: Vec<KernelEffect>,
87}
88
89impl TransitionResult {
90 pub fn new(new_state: OpState) -> Self {
92 Self {
93 new_state,
94 effects: vec![],
95 }
96 }
97
98 pub fn with_effects(new_state: OpState, effects: Vec<KernelEffect>) -> Self {
100 Self { new_state, effects }
101 }
102}
103
104pub type TransitionRes = Result<TransitionResult, TransitionError>;
106
107macro_rules! require_state {
110 ($state:expr, $variant:ident) => {
111 match $state {
112 OpState::$variant(s) => s,
113 _ => {
114 return Err(TransitionError::WrongState);
115 }
116 }
117 };
118}
119
120macro_rules! require_idle {
122 ($state:expr) => {
123 if !$state.is_idle() {
124 return Err(TransitionError::WrongState);
125 }
126 };
127}
128
129pub fn start_allocation(
143 state: OpState,
144 plan: Vec<AllocationPlanEntry>,
145 op_id: u64,
146) -> TransitionRes {
147 require_idle!(state);
148
149 if plan.is_empty() {
150 return Err(TransitionError::EmptyAllocationPlan);
151 }
152
153 let mut total = 0u128;
154 for step in &plan {
155 total = total.saturating_add(step.amount);
156 }
157
158 let plan_len = plan.len() as u32;
159 let new_state = OpState::Allocating(AllocatingState {
160 op_id,
161 index: 0,
162 remaining: total,
163 plan,
164 });
165
166 Ok(TransitionResult::with_effects(
167 new_state,
168 vec![KernelEffect::EmitEvent {
169 event: KernelEvent::AllocationStarted {
170 op_id,
171 total,
172 plan_len,
173 },
174 }],
175 ))
176}
177
178pub fn allocation_step_callback(
192 state: OpState,
193 success: bool,
194 amount_allocated: u128,
195 op_id: u64,
196) -> TransitionRes {
197 let alloc = match state {
198 OpState::Allocating(alloc) => alloc,
199 other => {
200 let _ = other;
201 return Err(TransitionError::WrongState);
202 }
203 };
204
205 if alloc.op_id != op_id {
206 return Err(TransitionError::OpIdMismatch {
207 expected: alloc.op_id,
208 actual: op_id,
209 });
210 }
211
212 validate_plan_index(alloc.index, alloc.plan.len())?;
213
214 if !success {
215 let mut original_total = 0u128;
218 for step in &alloc.plan {
219 original_total = original_total.saturating_add(step.amount);
220 }
221 let total_allocated = original_total.saturating_sub(alloc.remaining);
222
223 return Ok(TransitionResult::with_effects(
224 OpState::Idle,
225 vec![KernelEffect::EmitEvent {
226 event: KernelEvent::AllocationStepFailed {
227 op_id: alloc.op_id,
228 index: alloc.index,
229 remaining: alloc.remaining,
230 total_allocated,
231 },
232 }],
233 ));
234 }
235
236 if amount_allocated == 0 {
239 return Err(TransitionError::ZeroAllocationAmount);
240 }
241
242 if amount_allocated > alloc.remaining {
243 return Err(TransitionError::AllocationOverflow {
244 allocated: amount_allocated,
245 remaining: alloc.remaining,
246 });
247 }
248
249 Ok(TransitionResult::new(OpState::Allocating(
250 alloc.advance(amount_allocated),
251 )))
252}
253
254pub fn complete_allocation(
264 state: OpState,
265 op_id: u64,
266 pending_withdrawal: Option<WithdrawalRequest>,
267) -> TransitionRes {
268 let alloc = require_state!(state, Allocating);
269
270 if alloc.op_id != op_id {
271 return Err(TransitionError::OpIdMismatch {
272 expected: alloc.op_id,
273 actual: op_id,
274 });
275 }
276
277 let actionable_withdrawal = pending_withdrawal.filter(|req| req.amount > 0);
280
281 match actionable_withdrawal {
282 Some(req) => {
283 if req.escrow_shares == 0 {
284 return Err(TransitionError::ZeroEscrowShares);
285 }
286
287 let new_state = OpState::Withdrawing(WithdrawingState {
289 op_id: req.op_id,
290 index: 0,
291 remaining: req.amount,
292 collected: 0,
293 receiver: req.receiver,
294 owner: req.owner,
295 escrow_shares: req.escrow_shares,
296 });
297 Ok(TransitionResult::with_effects(
298 new_state,
299 vec![KernelEffect::EmitEvent {
300 event: KernelEvent::AllocationCompleted {
301 op_id,
302 has_withdrawal: true,
303 },
304 }],
305 ))
306 }
307 None => {
308 Ok(TransitionResult::with_effects(
310 OpState::Idle,
311 vec![KernelEffect::EmitEvent {
312 event: KernelEvent::AllocationCompleted {
313 op_id,
314 has_withdrawal: false,
315 },
316 }],
317 ))
318 }
319 }
320}
321
322#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
326#[derive(Clone, PartialEq, Eq)]
327pub struct WithdrawalRequest {
328 pub op_id: u64,
330 pub amount: u128,
332 pub receiver: Address,
334 pub owner: Address,
336 pub escrow_shares: u128,
338}
339
340pub fn start_withdrawal(state: OpState, request: WithdrawalRequest) -> TransitionRes {
350 require_idle!(state);
351
352 if request.amount == 0 {
353 return Err(TransitionError::ZeroWithdrawalAmount);
354 }
355
356 if request.escrow_shares == 0 {
357 return Err(TransitionError::ZeroEscrowShares);
358 }
359
360 let new_state = OpState::Withdrawing(WithdrawingState {
361 op_id: request.op_id,
362 index: 0,
363 remaining: request.amount,
364 collected: 0,
365 receiver: request.receiver,
366 owner: request.owner,
367 escrow_shares: request.escrow_shares,
368 });
369
370 Ok(TransitionResult::with_effects(
371 new_state,
372 vec![KernelEffect::EmitEvent {
373 event: KernelEvent::WithdrawalStarted {
374 op_id: request.op_id,
375 amount: request.amount,
376 escrow_shares: request.escrow_shares,
377 owner: request.owner,
378 receiver: request.receiver,
379 },
380 }],
381 ))
382}
383
384pub fn withdrawal_step_callback(
395 state: OpState,
396 op_id: u64,
397 amount_collected: u128,
398) -> TransitionRes {
399 let withdraw = require_state!(state, Withdrawing);
400
401 if withdraw.op_id != op_id {
402 return Err(TransitionError::OpIdMismatch {
403 expected: withdraw.op_id,
404 actual: op_id,
405 });
406 }
407
408 if amount_collected > withdraw.remaining {
409 return Err(TransitionError::CollectionOverflow {
410 collected: amount_collected,
411 remaining: withdraw.remaining,
412 });
413 }
414
415 Ok(TransitionResult::new(OpState::Withdrawing(
416 withdraw.advance(amount_collected),
417 )))
418}
419
420pub fn withdrawal_collected(state: OpState, op_id: u64, burn_shares: u128) -> TransitionRes {
430 let withdraw = require_state!(state, Withdrawing);
431
432 if withdraw.op_id != op_id {
433 return Err(TransitionError::OpIdMismatch {
434 expected: withdraw.op_id,
435 actual: op_id,
436 });
437 }
438
439 if withdraw.remaining > 0 {
440 return Err(TransitionError::WithdrawalIncomplete {
441 remaining: withdraw.remaining,
442 collected: withdraw.collected,
443 });
444 }
445
446 if burn_shares > withdraw.escrow_shares {
447 return Err(TransitionError::BurnExceedsEscrow {
448 burn: burn_shares,
449 escrow: withdraw.escrow_shares,
450 });
451 }
452
453 let new_state = OpState::Payout(PayoutState {
454 op_id: withdraw.op_id,
455 receiver: withdraw.receiver,
456 amount: withdraw.collected,
457 owner: withdraw.owner,
458 escrow_shares: withdraw.escrow_shares,
459 burn_shares,
460 });
461
462 Ok(TransitionResult::with_effects(
463 new_state,
464 vec![KernelEffect::EmitEvent {
465 event: KernelEvent::WithdrawalCollected {
466 op_id,
467 burn_shares,
468 collected: withdraw.collected,
469 },
470 }],
471 ))
472}
473
474pub fn withdrawal_settled(
475 state: OpState,
476 op_id: u64,
477 amount_collected: u128,
478 burn_shares: u128,
479) -> TransitionRes {
480 let stepped = withdrawal_step_callback(state, op_id, amount_collected)?;
481 let withdraw = require_state!(stepped.new_state, Withdrawing);
482
483 if burn_shares > withdraw.escrow_shares {
484 return Err(TransitionError::BurnExceedsEscrow {
485 burn: burn_shares,
486 escrow: withdraw.escrow_shares,
487 });
488 }
489
490 let new_state = OpState::Payout(PayoutState {
491 op_id: withdraw.op_id,
492 receiver: withdraw.receiver,
493 amount: withdraw.collected,
494 owner: withdraw.owner,
495 escrow_shares: withdraw.escrow_shares,
496 burn_shares,
497 });
498
499 Ok(TransitionResult::with_effects(
500 new_state,
501 vec![KernelEffect::EmitEvent {
502 event: KernelEvent::WithdrawalCollected {
503 op_id,
504 burn_shares,
505 collected: withdraw.collected,
506 },
507 }],
508 ))
509}
510
511pub fn stop_withdrawal(state: OpState, op_id: u64, escrow_address: Address) -> TransitionRes {
520 let withdraw = require_state!(state, Withdrawing);
521
522 if withdraw.op_id != op_id {
523 return Err(TransitionError::OpIdMismatch {
524 expected: withdraw.op_id,
525 actual: op_id,
526 });
527 }
528
529 let mut effects = vec![];
531
532 if withdraw.escrow_shares > 0 {
535 let owner_address = withdraw.owner;
536 effects.push(KernelEffect::TransferShares {
537 from: escrow_address,
538 to: owner_address,
539 shares: withdraw.escrow_shares,
540 });
541 }
542
543 effects.push(KernelEffect::EmitEvent {
544 event: KernelEvent::WithdrawalStopped {
545 op_id,
546 escrow_shares: withdraw.escrow_shares,
547 },
548 });
549
550 Ok(TransitionResult::with_effects(OpState::Idle, effects))
551}
552
553pub fn start_refresh(state: OpState, plan: Vec<TargetId>, op_id: u64) -> TransitionRes {
565 require_idle!(state);
566
567 if plan.is_empty() {
568 return Err(TransitionError::EmptyRefreshPlan);
569 }
570
571 let plan_len = plan.len() as u32;
572 let new_state = OpState::Refreshing(RefreshingState {
573 op_id,
574 index: 0,
575 plan,
576 });
577
578 Ok(TransitionResult::with_effects(
579 new_state,
580 vec![KernelEffect::EmitEvent {
581 event: KernelEvent::RefreshStarted { op_id, plan_len },
582 }],
583 ))
584}
585
586pub fn refresh_step_callback(state: OpState, op_id: u64) -> TransitionRes {
595 let refresh = match state {
596 OpState::Refreshing(refresh) => refresh,
597 other => {
598 let _ = other;
599 return Err(TransitionError::WrongState);
600 }
601 };
602
603 if refresh.op_id != op_id {
604 return Err(TransitionError::OpIdMismatch {
605 expected: refresh.op_id,
606 actual: op_id,
607 });
608 }
609
610 validate_plan_index(refresh.index, refresh.plan.len())?;
611
612 Ok(TransitionResult::new(OpState::Refreshing(
613 refresh.advance(),
614 )))
615}
616
617pub fn complete_refresh(state: OpState, op_id: u64) -> TransitionRes {
626 let refresh = require_state!(state, Refreshing);
627
628 if refresh.op_id != op_id {
629 return Err(TransitionError::OpIdMismatch {
630 expected: refresh.op_id,
631 actual: op_id,
632 });
633 }
634
635 Ok(TransitionResult::with_effects(
636 OpState::Idle,
637 vec![KernelEffect::EmitEvent {
638 event: KernelEvent::RefreshCompleted { op_id },
639 }],
640 ))
641}
642
643pub fn payout_complete(
656 state: OpState,
657 success: bool,
658 op_id: u64,
659 escrow_address: Address,
660) -> TransitionRes {
661 let payout = require_state!(state, Payout);
662
663 if payout.op_id != op_id {
664 return Err(TransitionError::OpIdMismatch {
665 expected: payout.op_id,
666 actual: op_id,
667 });
668 }
669
670 let mut effects = vec![];
671
672 let owner_address = payout.owner;
673
674 let mut burn_shares = 0u128;
675 let mut refund_shares = 0u128;
676 let mut amount = 0u128;
677
678 if success {
679 if payout.burn_shares > payout.escrow_shares {
681 return Err(TransitionError::BurnExceedsEscrow {
682 burn: payout.burn_shares,
683 escrow: payout.escrow_shares,
684 });
685 }
686
687 if payout.burn_shares > 0 {
689 burn_shares = payout.burn_shares;
690 effects.push(KernelEffect::BurnShares {
691 owner: escrow_address,
692 shares: payout.burn_shares,
693 });
694 }
695
696 refund_shares = payout.escrow_shares - payout.burn_shares;
698 if refund_shares > 0 {
699 effects.push(KernelEffect::TransferShares {
700 from: escrow_address,
701 to: owner_address,
702 shares: refund_shares,
703 });
704 }
705
706 amount = payout.amount;
707 } else {
708 if payout.escrow_shares > 0 {
710 refund_shares = payout.escrow_shares;
711 effects.push(KernelEffect::TransferShares {
712 from: escrow_address,
713 to: owner_address,
714 shares: payout.escrow_shares,
715 });
716 }
717 }
718
719 effects.push(KernelEffect::EmitEvent {
720 event: KernelEvent::PayoutCompleted {
721 op_id,
722 success,
723 burn_shares,
724 refund_shares,
725 amount,
726 },
727 });
728
729 Ok(TransitionResult::with_effects(OpState::Idle, effects))
730}
731
732#[cfg(test)]
733mod tests;