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