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 request_id: req.request_id,
291 index: 0,
292 remaining: req.amount,
293 collected: 0,
294 receiver: req.receiver,
295 owner: req.owner,
296 escrow_shares: req.escrow_shares,
297 });
298 Ok(TransitionResult::with_effects(
299 new_state,
300 vec![KernelEffect::EmitEvent {
301 event: KernelEvent::AllocationCompleted {
302 op_id,
303 has_withdrawal: true,
304 },
305 }],
306 ))
307 }
308 None => {
309 Ok(TransitionResult::with_effects(
311 OpState::Idle,
312 vec![KernelEffect::EmitEvent {
313 event: KernelEvent::AllocationCompleted {
314 op_id,
315 has_withdrawal: false,
316 },
317 }],
318 ))
319 }
320 }
321}
322
323#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
327#[derive(Clone, PartialEq, Eq)]
328pub struct WithdrawalRequest {
329 pub op_id: u64,
331 pub request_id: u64,
333 pub amount: u128,
335 pub receiver: Address,
337 pub owner: Address,
339 pub escrow_shares: u128,
341}
342
343pub fn start_withdrawal(state: OpState, request: WithdrawalRequest) -> TransitionRes {
353 require_idle!(state);
354
355 if request.amount == 0 {
356 return Err(TransitionError::ZeroWithdrawalAmount);
357 }
358
359 if request.escrow_shares == 0 {
360 return Err(TransitionError::ZeroEscrowShares);
361 }
362
363 let new_state = OpState::Withdrawing(WithdrawingState {
364 op_id: request.op_id,
365 request_id: request.request_id,
366 index: 0,
367 remaining: request.amount,
368 collected: 0,
369 receiver: request.receiver,
370 owner: request.owner,
371 escrow_shares: request.escrow_shares,
372 });
373
374 Ok(TransitionResult::with_effects(
375 new_state,
376 vec![KernelEffect::EmitEvent {
377 event: KernelEvent::WithdrawalStarted {
378 op_id: request.op_id,
379 amount: request.amount,
380 escrow_shares: request.escrow_shares,
381 owner: request.owner,
382 receiver: request.receiver,
383 },
384 }],
385 ))
386}
387
388pub fn withdrawal_step_callback(
399 state: OpState,
400 op_id: u64,
401 amount_collected: u128,
402) -> TransitionRes {
403 let withdraw = require_state!(state, Withdrawing);
404
405 if withdraw.op_id != op_id {
406 return Err(TransitionError::OpIdMismatch {
407 expected: withdraw.op_id,
408 actual: op_id,
409 });
410 }
411
412 if amount_collected > withdraw.remaining {
413 return Err(TransitionError::CollectionOverflow {
414 collected: amount_collected,
415 remaining: withdraw.remaining,
416 });
417 }
418
419 Ok(TransitionResult::new(OpState::Withdrawing(
420 withdraw.advance(amount_collected),
421 )))
422}
423
424pub fn withdrawal_collected(state: OpState, op_id: u64, burn_shares: u128) -> TransitionRes {
434 let withdraw = require_state!(state, Withdrawing);
435
436 if withdraw.op_id != op_id {
437 return Err(TransitionError::OpIdMismatch {
438 expected: withdraw.op_id,
439 actual: op_id,
440 });
441 }
442
443 if withdraw.remaining > 0 {
444 return Err(TransitionError::WithdrawalIncomplete {
445 remaining: withdraw.remaining,
446 collected: withdraw.collected,
447 });
448 }
449
450 if burn_shares > withdraw.escrow_shares {
451 return Err(TransitionError::BurnExceedsEscrow {
452 burn: burn_shares,
453 escrow: withdraw.escrow_shares,
454 });
455 }
456
457 let new_state = OpState::Payout(PayoutState {
458 op_id: withdraw.op_id,
459 request_id: withdraw.request_id,
460 receiver: withdraw.receiver,
461 amount: withdraw.collected,
462 owner: withdraw.owner,
463 escrow_shares: withdraw.escrow_shares,
464 burn_shares,
465 });
466
467 Ok(TransitionResult::with_effects(
468 new_state,
469 vec![KernelEffect::EmitEvent {
470 event: KernelEvent::WithdrawalCollected {
471 op_id,
472 burn_shares,
473 collected: withdraw.collected,
474 },
475 }],
476 ))
477}
478
479pub fn withdrawal_settled(
480 state: OpState,
481 op_id: u64,
482 amount_collected: u128,
483 burn_shares: u128,
484) -> TransitionRes {
485 let stepped = withdrawal_step_callback(state, op_id, amount_collected)?;
486 let withdraw = require_state!(stepped.new_state, Withdrawing);
487
488 if burn_shares > withdraw.escrow_shares {
489 return Err(TransitionError::BurnExceedsEscrow {
490 burn: burn_shares,
491 escrow: withdraw.escrow_shares,
492 });
493 }
494
495 let new_state = OpState::Payout(PayoutState {
496 op_id: withdraw.op_id,
497 request_id: withdraw.request_id,
498 receiver: withdraw.receiver,
499 amount: withdraw.collected,
500 owner: withdraw.owner,
501 escrow_shares: withdraw.escrow_shares,
502 burn_shares,
503 });
504
505 Ok(TransitionResult::with_effects(
506 new_state,
507 vec![KernelEffect::EmitEvent {
508 event: KernelEvent::WithdrawalCollected {
509 op_id,
510 burn_shares,
511 collected: withdraw.collected,
512 },
513 }],
514 ))
515}
516
517pub fn stop_withdrawal(state: OpState, op_id: u64, escrow_address: Address) -> TransitionRes {
526 let withdraw = require_state!(state, Withdrawing);
527
528 if withdraw.op_id != op_id {
529 return Err(TransitionError::OpIdMismatch {
530 expected: withdraw.op_id,
531 actual: op_id,
532 });
533 }
534
535 let mut effects = vec![];
537
538 if withdraw.escrow_shares > 0 {
541 let owner_address = withdraw.owner;
542 effects.push(KernelEffect::TransferShares {
543 from: escrow_address,
544 to: owner_address,
545 shares: withdraw.escrow_shares,
546 });
547 }
548
549 effects.push(KernelEffect::EmitEvent {
550 event: KernelEvent::WithdrawalStopped {
551 op_id,
552 escrow_shares: withdraw.escrow_shares,
553 },
554 });
555
556 Ok(TransitionResult::with_effects(OpState::Idle, effects))
557}
558
559pub fn start_refresh(state: OpState, plan: Vec<TargetId>, op_id: u64) -> TransitionRes {
571 require_idle!(state);
572
573 if plan.is_empty() {
574 return Err(TransitionError::EmptyRefreshPlan);
575 }
576
577 let plan_len = plan.len() as u32;
578 let new_state = OpState::Refreshing(RefreshingState {
579 op_id,
580 index: 0,
581 plan,
582 });
583
584 Ok(TransitionResult::with_effects(
585 new_state,
586 vec![KernelEffect::EmitEvent {
587 event: KernelEvent::RefreshStarted { op_id, plan_len },
588 }],
589 ))
590}
591
592pub fn refresh_step_callback(state: OpState, op_id: u64) -> TransitionRes {
601 let refresh = match state {
602 OpState::Refreshing(refresh) => refresh,
603 other => {
604 let _ = other;
605 return Err(TransitionError::WrongState);
606 }
607 };
608
609 if refresh.op_id != op_id {
610 return Err(TransitionError::OpIdMismatch {
611 expected: refresh.op_id,
612 actual: op_id,
613 });
614 }
615
616 validate_plan_index(refresh.index, refresh.plan.len())?;
617
618 Ok(TransitionResult::new(OpState::Refreshing(
619 refresh.advance(),
620 )))
621}
622
623pub fn complete_refresh(state: OpState, op_id: u64) -> TransitionRes {
632 let refresh = require_state!(state, Refreshing);
633
634 if refresh.op_id != op_id {
635 return Err(TransitionError::OpIdMismatch {
636 expected: refresh.op_id,
637 actual: op_id,
638 });
639 }
640
641 Ok(TransitionResult::with_effects(
642 OpState::Idle,
643 vec![KernelEffect::EmitEvent {
644 event: KernelEvent::RefreshCompleted { op_id },
645 }],
646 ))
647}
648
649pub fn payout_complete(
662 state: OpState,
663 success: bool,
664 op_id: u64,
665 escrow_address: Address,
666) -> TransitionRes {
667 let payout = require_state!(state, Payout);
668
669 if payout.op_id != op_id {
670 return Err(TransitionError::OpIdMismatch {
671 expected: payout.op_id,
672 actual: op_id,
673 });
674 }
675
676 let mut effects = vec![];
677
678 let owner_address = payout.owner;
679
680 let mut burn_shares = 0u128;
681 let mut refund_shares = 0u128;
682 let mut amount = 0u128;
683
684 if success {
685 if payout.burn_shares > payout.escrow_shares {
687 return Err(TransitionError::BurnExceedsEscrow {
688 burn: payout.burn_shares,
689 escrow: payout.escrow_shares,
690 });
691 }
692
693 if payout.burn_shares > 0 {
695 burn_shares = payout.burn_shares;
696 effects.push(KernelEffect::BurnShares {
697 owner: escrow_address,
698 shares: payout.burn_shares,
699 });
700 }
701
702 refund_shares = payout.escrow_shares - payout.burn_shares;
704 if refund_shares > 0 {
705 effects.push(KernelEffect::TransferShares {
706 from: escrow_address,
707 to: owner_address,
708 shares: refund_shares,
709 });
710 }
711
712 amount = payout.amount;
713 } else {
714 if payout.escrow_shares > 0 {
716 refund_shares = payout.escrow_shares;
717 effects.push(KernelEffect::TransferShares {
718 from: escrow_address,
719 to: owner_address,
720 shares: payout.escrow_shares,
721 });
722 }
723 }
724
725 effects.push(KernelEffect::EmitEvent {
726 event: KernelEvent::PayoutCompleted {
727 op_id,
728 success,
729 burn_shares,
730 refund_shares,
731 amount,
732 },
733 });
734
735 Ok(TransitionResult::with_effects(OpState::Idle, effects))
736}
737
738#[cfg(test)]
739mod tests;