Skip to main content

canic_core/domain/
icp_refill.rs

1//! Module: domain::icp_refill
2//!
3//! Responsibility: define pure ICP refill operation value enums shared across
4//! storage projections, workflow decisions, and endpoint DTOs.
5//! Does not own: endpoint request/response structs, stable records, or ledger
6//! execution.
7//! Boundary: DTOs re-export these values to preserve the public API path while
8//! internal code imports them from the domain owner.
9
10use candid::CandidType;
11use serde::{Deserialize, Serialize};
12
13///
14/// IcpRefillStatus
15///
16
17#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[remain::sorted]
19pub enum IcpRefillStatus {
20    Completed,
21    Failed,
22    InvalidTransaction,
23    NotifyProcessing,
24    Refunded,
25    Requested,
26    TransactionTooOld,
27    Transferred,
28}
29
30///
31/// IcpRefillErrorCode
32///
33#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
34#[remain::sorted]
35pub enum IcpRefillErrorCode {
36    BadFee,
37    CyclesSentOverflow,
38    Duplicate,
39    InvalidLedgerBlockIndex,
40    InvalidTransaction,
41    LedgerTransferFailed,
42    NotifyFailed,
43    NotifyMaxAttempts,
44    Processing,
45    Refunded,
46    TransactionTooOld,
47    TransferWindowStale,
48}
49
50/// Return whether an ICP refill outcome must retain its operation identity for retry.
51#[must_use]
52pub const fn icp_refill_outcome_is_resumable(
53    status: IcpRefillStatus,
54    error_code: Option<IcpRefillErrorCode>,
55    ledger_block_recorded: bool,
56) -> bool {
57    matches!(
58        status,
59        IcpRefillStatus::Requested
60            | IcpRefillStatus::Transferred
61            | IcpRefillStatus::NotifyProcessing
62    ) || matches!(
63        (status, error_code, ledger_block_recorded),
64        (
65            IcpRefillStatus::Failed,
66            Some(IcpRefillErrorCode::NotifyFailed),
67            true
68        ) | (
69            IcpRefillStatus::Failed,
70            Some(IcpRefillErrorCode::BadFee),
71            false
72        )
73    )
74}
75
76// -----------------------------------------------------------------------------
77// Tests
78// -----------------------------------------------------------------------------
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn resumable_outcome_requires_active_or_retryable_state() {
86        assert!(icp_refill_outcome_is_resumable(
87            IcpRefillStatus::Requested,
88            None,
89            false
90        ));
91        assert!(icp_refill_outcome_is_resumable(
92            IcpRefillStatus::Transferred,
93            None,
94            true
95        ));
96        assert!(icp_refill_outcome_is_resumable(
97            IcpRefillStatus::NotifyProcessing,
98            Some(IcpRefillErrorCode::Processing),
99            true
100        ));
101        assert!(icp_refill_outcome_is_resumable(
102            IcpRefillStatus::Failed,
103            Some(IcpRefillErrorCode::BadFee),
104            false
105        ));
106        assert!(icp_refill_outcome_is_resumable(
107            IcpRefillStatus::Failed,
108            Some(IcpRefillErrorCode::NotifyFailed),
109            true
110        ));
111
112        assert!(!icp_refill_outcome_is_resumable(
113            IcpRefillStatus::Completed,
114            None,
115            true
116        ));
117        assert!(!icp_refill_outcome_is_resumable(
118            IcpRefillStatus::Failed,
119            Some(IcpRefillErrorCode::NotifyMaxAttempts),
120            true
121        ));
122        assert!(!icp_refill_outcome_is_resumable(
123            IcpRefillStatus::Failed,
124            Some(IcpRefillErrorCode::NotifyFailed),
125            false
126        ));
127        assert!(!icp_refill_outcome_is_resumable(
128            IcpRefillStatus::Failed,
129            Some(IcpRefillErrorCode::BadFee),
130            true
131        ));
132    }
133}