1use crate::{cdk::types::Principal, ids::IntentResourceKey, model::replay::OperationId};
8use serde::{Deserialize, Serialize};
9
10pub const PAYLOAD_BINDING_SCHEMA_VERSION: u32 = 1;
11pub const RECEIPT_BACKED_INTENT_SCHEMA_VERSION: u32 = 1;
12pub const TERMINAL_EVIDENCE_SCHEMA_VERSION: u32 = 1;
13pub const CANIC_INTENT_RESOURCE_PREFIX: &str = "canic:";
14pub const MAX_RECEIPT_BACKED_INTENT_REPLAY_WINDOW_NS: u64 = 24 * 60 * 60 * 1_000_000_000;
15pub const RECEIPT_TERMINAL_OBSERVATION_GRACE_NS: u64 = MAX_RECEIPT_BACKED_INTENT_REPLAY_WINDOW_NS;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum ReceiptReplayWindowDecision {
20 Open,
21 Closed,
22 TooLong { remaining_ns: u64 },
23}
24
25#[must_use]
27pub const fn receipt_terminal_eligible_at(
28 replay_deadline_ns: u64,
29 terminal_timestamp_ns: u64,
30) -> Option<u64> {
31 let Some(observation_deadline_ns) =
32 terminal_timestamp_ns.checked_add(RECEIPT_TERMINAL_OBSERVATION_GRACE_NS)
33 else {
34 return None;
35 };
36 Some(if replay_deadline_ns > observation_deadline_ns {
37 replay_deadline_ns
38 } else {
39 observation_deadline_ns
40 })
41}
42
43#[must_use]
44pub fn is_canic_owned_intent_resource_key(resource_key: &IntentResourceKey) -> bool {
45 resource_key.starts_with(CANIC_INTENT_RESOURCE_PREFIX)
46}
47
48#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
50pub struct PayloadBinding {
51 pub schema_version: u32,
52 pub digest: [u8; 32],
53}
54
55impl PayloadBinding {
56 #[must_use]
57 pub const fn new(digest: [u8; 32]) -> Self {
58 Self {
59 schema_version: PAYLOAD_BINDING_SCHEMA_VERSION,
60 digest,
61 }
62 }
63}
64
65#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub enum TerminalEvidenceDecision {
68 Committed,
69 RolledBack,
70}
71
72#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct TerminalEvidence {
75 pub source_canister: Principal,
76 pub schema_version: u32,
77 pub decision: TerminalEvidenceDecision,
78 pub fingerprint: [u8; 32],
79}
80
81impl TerminalEvidence {
82 #[must_use]
83 pub const fn new(
84 source_canister: Principal,
85 decision: TerminalEvidenceDecision,
86 fingerprint: [u8; 32],
87 ) -> Self {
88 Self {
89 source_canister,
90 schema_version: TERMINAL_EVIDENCE_SCHEMA_VERSION,
91 decision,
92 fingerprint,
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
99pub enum ReceiptBackedIntentState {
100 Pending,
101 Committed { evidence: TerminalEvidence },
102 RolledBack { evidence: TerminalEvidence },
103}
104
105#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub struct ReceiptBackedIntent {
108 pub schema_version: u32,
109 pub operation_id: OperationId,
110 pub payload_binding: PayloadBinding,
111 pub resource_key: IntentResourceKey,
112 pub quantity: u64,
113 pub state: ReceiptBackedIntentState,
114 pub revision: u64,
115 pub created_at_ns: u64,
116 pub updated_at_ns: u64,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct BeginLocalIntentInput {
122 pub resource_key: IntentResourceKey,
123 pub quantity: u64,
124 pub ttl_secs: Option<u64>,
125 pub reservation_limit: Option<u64>,
126}
127
128#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct BeginReceiptBackedIntentInput {
131 pub operation_id: OperationId,
132 pub payload_binding: PayloadBinding,
133 pub resource_key: IntentResourceKey,
134 pub quantity: u64,
135 pub reservation_limit: u64,
136 pub replay_deadline_ns: u64,
137}
138
139#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct BeginPlacementReceiptBackedIntentInput {
142 pub operation_id: OperationId,
143 pub payload_binding: PayloadBinding,
144 pub resource_key: IntentResourceKey,
145 pub quantity: u64,
146 pub reservation_limit: u64,
147}
148
149#[derive(Clone, Debug, Eq, PartialEq)]
151pub enum BeginReceiptBackedIntentResult {
152 Created {
153 revision: u64,
154 },
155 ExistingPending {
156 revision: u64,
157 },
158 ExistingCommitted {
159 revision: u64,
160 evidence: TerminalEvidence,
161 },
162 ExistingRolledBack {
163 revision: u64,
164 evidence: TerminalEvidence,
165 },
166 BindingConflict,
167 ReplayWindowClosed {
168 replay_deadline_ns: u64,
169 },
170 ReplayWindowTooLong {
171 remaining_ns: u64,
172 maximum_ns: u64,
173 },
174 CapacityExceeded {
175 current_quantity: u64,
176 requested_quantity: u64,
177 limit: u64,
178 },
179 StoreCapacityReached {
180 current_records: u64,
181 limit: u64,
182 },
183}
184
185#[derive(Clone, Debug, Eq, PartialEq)]
187pub struct SettleReceiptBackedIntentInput {
188 pub operation_id: OperationId,
189 pub expected_revision: u64,
190 pub expected_payload_binding: PayloadBinding,
191 pub evidence: TerminalEvidence,
192}
193
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub enum SettleReceiptBackedIntentResult {
197 Settled {
198 revision: u64,
199 state: ReceiptBackedIntentState,
200 },
201 AlreadySettled {
202 revision: u64,
203 state: ReceiptBackedIntentState,
204 },
205 NotFound,
206 RevisionConflict {
207 actual_revision: u64,
208 },
209 BindingConflict,
210}
211
212#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct RemoveTerminalReceiptBackedIntentInput {
215 pub operation_id: OperationId,
216 pub expected_revision: u64,
217 pub expected_payload_binding: PayloadBinding,
218}
219
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum RemoveTerminalReceiptBackedIntentResult {
223 Removed,
224 NotFound,
225 NotTerminal,
226 RevisionConflict { actual_revision: u64 },
227 BindingConflict,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn terminal_eligibility_preserves_both_retention_deadlines() {
236 assert_eq!(
237 receipt_terminal_eligible_at(100, 50),
238 Some(50 + RECEIPT_TERMINAL_OBSERVATION_GRACE_NS)
239 );
240 assert_eq!(
241 receipt_terminal_eligible_at(u64::MAX - 1, 50),
242 Some(u64::MAX - 1)
243 );
244 assert_eq!(receipt_terminal_eligible_at(u64::MAX, u64::MAX), None);
245 }
246}