Skip to main content

nexus_sdk/scheduler/
receipt.rs

1//! Confirmed transaction receipts for scheduler mutations.
2
3use {
4    crate::{
5        scheduler::{OccurrenceRef, OccurrenceSource, WithdrawalReason},
6        sui,
7    },
8    serde::{Deserialize, Serialize},
9};
10
11/// Reference to one confirmed Sui transaction.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct TransactionReference {
14    digest: sui::types::Digest,
15    checkpoint: u64,
16}
17
18impl TransactionReference {
19    pub(crate) const fn new(digest: sui::types::Digest, checkpoint: u64) -> Self {
20        Self { digest, checkpoint }
21    }
22
23    /// Returns the transaction digest.
24    pub const fn digest(&self) -> &sui::types::Digest {
25        &self.digest
26    }
27
28    /// Returns the checkpoint that contains the transaction.
29    pub const fn checkpoint(&self) -> u64 {
30        self.checkpoint
31    }
32}
33
34/// One occurrence allocated by a scheduler mutation.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct ScheduledOccurrence {
37    reference: OccurrenceRef,
38    start_time_ms: u64,
39    deadline_ms: Option<u64>,
40    priority_fee_percentage: u64,
41    source: OccurrenceSource,
42}
43
44impl ScheduledOccurrence {
45    pub(crate) const fn new(
46        reference: OccurrenceRef,
47        start_time_ms: u64,
48        deadline_ms: Option<u64>,
49        priority_fee_percentage: u64,
50        source: OccurrenceSource,
51    ) -> Self {
52        Self {
53            reference,
54            start_time_ms,
55            deadline_ms,
56            priority_fee_percentage,
57            source,
58        }
59    }
60
61    /// Returns the allocated occurrence identity.
62    pub const fn reference(&self) -> OccurrenceRef {
63        self.reference
64    }
65
66    /// Returns the requested absolute start timestamp.
67    pub const fn start_time_ms(&self) -> u64 {
68        self.start_time_ms
69    }
70
71    /// Returns the optional absolute dispatch deadline.
72    pub const fn deadline_ms(&self) -> Option<u64> {
73        self.deadline_ms
74    }
75
76    /// Returns the dispatch priority fee percentage.
77    pub const fn priority_fee_percentage(&self) -> u64 {
78        self.priority_fee_percentage
79    }
80
81    /// Returns how the occurrence was allocated.
82    pub const fn source(&self) -> OccurrenceSource {
83        self.source
84    }
85}
86
87/// One scheduled occurrence withdrawn before dispatch.
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub struct WithdrawnOccurrence {
90    reference: OccurrenceRef,
91    reason: WithdrawalReason,
92}
93
94impl WithdrawnOccurrence {
95    pub(crate) const fn new(reference: OccurrenceRef, reason: WithdrawalReason) -> Self {
96        Self { reference, reason }
97    }
98
99    /// Returns the withdrawn occurrence identity.
100    pub const fn reference(&self) -> OccurrenceRef {
101        self.reference
102    }
103
104    /// Returns why the occurrence left its Schedule.
105    pub const fn reason(&self) -> WithdrawalReason {
106        self.reason
107    }
108}
109
110/// Net Schedule changes confirmed by one transaction.
111#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
112pub struct ScheduleDelta {
113    scheduled: Vec<ScheduledOccurrence>,
114    withdrawn: Vec<WithdrawnOccurrence>,
115    advertised: Option<OccurrenceRef>,
116}
117
118impl ScheduleDelta {
119    pub(crate) fn new(
120        scheduled: Vec<ScheduledOccurrence>,
121        withdrawn: Vec<WithdrawnOccurrence>,
122        advertised: Option<OccurrenceRef>,
123    ) -> Self {
124        Self {
125            scheduled,
126            withdrawn,
127            advertised,
128        }
129    }
130
131    /// Returns every occurrence allocated by the transaction.
132    pub fn scheduled(&self) -> &[ScheduledOccurrence] {
133        &self.scheduled
134    }
135
136    /// Returns every occurrence withdrawn by the transaction.
137    pub fn withdrawn(&self) -> &[WithdrawnOccurrence] {
138        &self.withdrawn
139    }
140
141    /// Returns the final new advertisement when the transaction changed it.
142    pub const fn advertised(&self) -> Option<OccurrenceRef> {
143        self.advertised
144    }
145
146    /// Returns whether no allocation, withdrawal, or advertisement changed.
147    pub fn is_empty(&self) -> bool {
148        self.scheduled.is_empty() && self.withdrawn.is_empty() && self.advertised.is_none()
149    }
150}
151
152/// Result of one confirmed Task mutation.
153#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
154pub struct TaskMutationReceipt {
155    transaction: TransactionReference,
156    task_id: sui::types::Address,
157    delta: ScheduleDelta,
158}
159
160impl TaskMutationReceipt {
161    pub(crate) const fn new(
162        transaction: TransactionReference,
163        task_id: sui::types::Address,
164        delta: ScheduleDelta,
165    ) -> Self {
166        Self {
167            transaction,
168            task_id,
169            delta,
170        }
171    }
172
173    /// Returns the confirmed transaction reference.
174    pub const fn transaction(&self) -> &TransactionReference {
175        &self.transaction
176    }
177
178    /// Returns the mutated Task identifier.
179    pub const fn task_id(&self) -> sui::types::Address {
180        self.task_id
181    }
182
183    /// Returns the net Schedule changes in the transaction.
184    pub const fn delta(&self) -> &ScheduleDelta {
185        &self.delta
186    }
187}
188
189/// Result of aborting an expired runtime execution for an occurrence.
190#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
191pub struct AbortReceipt {
192    transaction: TransactionReference,
193    occurrence: OccurrenceRef,
194    execution_id: sui::types::Address,
195}
196
197impl AbortReceipt {
198    pub(crate) const fn new(
199        transaction: TransactionReference,
200        occurrence: OccurrenceRef,
201        execution_id: sui::types::Address,
202    ) -> Self {
203        Self {
204            transaction,
205            occurrence,
206            execution_id,
207        }
208    }
209
210    /// Returns the confirmed transaction reference.
211    pub const fn transaction(&self) -> &TransactionReference {
212        &self.transaction
213    }
214
215    /// Returns the occurrence whose execution was aborted.
216    pub const fn occurrence(&self) -> OccurrenceRef {
217        self.occurrence
218    }
219
220    /// Returns the aborted deterministic execution identifier.
221    pub const fn execution_id(&self) -> sui::types::Address {
222        self.execution_id
223    }
224}
225
226/// An expired occurrence can require several resolutions and a final settlement,
227/// or no mutation. `AbortReceipt` represents exactly one abort transaction and
228/// remains unchanged for callers requesting a specific invocation.
229#[cfg(feature = "nexus")]
230#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
231pub struct RecoveryReceipt {
232    pub(crate) occurrence: crate::scheduler::OccurrenceSnapshot,
233    pub(crate) resolutions: Vec<crate::nexus::workflow::ExpiredWalkResolutionResult>,
234    pub(crate) settlement: Option<TaskMutationReceipt>,
235}
236
237#[cfg(feature = "nexus")]
238impl RecoveryReceipt {
239    /// Returns the observed occurrence after recovery, including any unfinished work.
240    pub const fn occurrence(&self) -> &crate::scheduler::OccurrenceSnapshot {
241        &self.occurrence
242    }
243
244    /// Returns the walk resolutions submitted during this pass.
245    pub fn resolutions(&self) -> &[crate::nexus::workflow::ExpiredWalkResolutionResult] {
246        &self.resolutions
247    }
248
249    /// Returns the Task settlement submitted when runtime work finished.
250    pub const fn settlement(&self) -> Option<&TaskMutationReceipt> {
251        self.settlement.as_ref()
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn address(value: &'static str) -> sui::types::Address {
260        sui::types::Address::from_static(value)
261    }
262
263    #[test]
264    fn mutation_receipts_preserve_transaction_and_schedule_changes() {
265        let task_id = address("0x20");
266        let reference = OccurrenceRef::new(task_id, 4);
267        let transaction = TransactionReference::new(sui::types::Digest::new([7; 32]), 12);
268        assert_eq!(transaction.digest(), &sui::types::Digest::new([7; 32]));
269        assert_eq!(transaction.checkpoint(), 12);
270
271        let scheduled = ScheduledOccurrence::new(
272            reference,
273            100,
274            Some(120),
275            30,
276            OccurrenceSource::Recurring { iteration: 2 },
277        );
278        assert_eq!(scheduled.reference(), reference);
279        assert_eq!(scheduled.start_time_ms(), 100);
280        assert_eq!(scheduled.deadline_ms(), Some(120));
281        assert_eq!(scheduled.priority_fee_percentage(), 30);
282        assert_eq!(
283            scheduled.source(),
284            OccurrenceSource::Recurring { iteration: 2 }
285        );
286
287        let withdrawn = WithdrawnOccurrence::new(reference, WithdrawalReason::RecurrenceReplaced);
288        assert_eq!(withdrawn.reference(), reference);
289        assert_eq!(withdrawn.reason(), WithdrawalReason::RecurrenceReplaced);
290
291        let delta = ScheduleDelta::new(
292            vec![scheduled.clone()],
293            vec![withdrawn.clone()],
294            Some(reference),
295        );
296        assert_eq!(delta.scheduled(), &[scheduled]);
297        assert_eq!(delta.withdrawn(), &[withdrawn]);
298        assert_eq!(delta.advertised(), Some(reference));
299        assert!(!delta.is_empty());
300        assert!(ScheduleDelta::default().is_empty());
301
302        let receipt = TaskMutationReceipt::new(transaction.clone(), task_id, delta.clone());
303        assert_eq!(receipt.transaction(), &transaction);
304        assert_eq!(receipt.task_id(), task_id);
305        assert_eq!(receipt.delta(), &delta);
306    }
307
308    #[test]
309    fn abort_receipt_identifies_the_occurrence_and_runtime_object() {
310        let transaction = TransactionReference::new(sui::types::Digest::new([8; 32]), 13);
311        let occurrence = OccurrenceRef::new(address("0x21"), 5);
312        let execution_id = address("0x22");
313        let receipt = AbortReceipt::new(transaction.clone(), occurrence, execution_id);
314
315        assert_eq!(receipt.transaction(), &transaction);
316        assert_eq!(receipt.occurrence(), occurrence);
317        assert_eq!(receipt.execution_id(), execution_id);
318    }
319}