Skip to main content

nexus_sdk/nexus/
transaction.rs

1//! Client scoped programmable transaction composition.
2
3use {
4    crate::{
5        move_boundary::NexusPtbBuilder,
6        nexus::{
7            client::NexusClient,
8            error::NexusError,
9            scheduler::resolve,
10            signer::ExecutedTransaction,
11        },
12        scheduler::{DispatchOffer, OccurrenceRef, Schedule, SchedulerError, TaskSpec},
13        sui,
14        transactions::scheduler::{
15            compile_append_dispatch_occurrence,
16            compile_append_expire_occurrence,
17            TaskDraftCompiler,
18        },
19        types::{NexusContext, PackageRole},
20    },
21    std::sync::Arc,
22};
23
24#[derive(Clone, Copy, Debug)]
25enum TransactionSource {
26    Object(sui::types::Address),
27    Creator {
28        package: sui::types::Address,
29        role: PackageRole,
30    },
31}
32
33/// One programmable transaction scoped to a [`NexusClient`].
34///
35/// Dropping an unfinished value has no external effect.
36#[must_use = "dropping a Nexus transaction discards its commands"]
37pub struct NexusTransaction {
38    client: NexusClient,
39    source: TransactionSource,
40    transaction: NexusPtbBuilder,
41}
42
43impl NexusTransaction {
44    pub(super) fn for_object(
45        client: NexusClient,
46        context: Arc<NexusContext>,
47        object_id: sui::types::Address,
48    ) -> Self {
49        Self {
50            client,
51            source: TransactionSource::Object(object_id),
52            transaction: NexusPtbBuilder::new(context),
53        }
54    }
55
56    pub(super) fn for_creator(
57        client: NexusClient,
58        context: Arc<NexusContext>,
59        package: sui::types::Address,
60        role: PackageRole,
61    ) -> Self {
62        Self {
63            client,
64            source: TransactionSource::Creator { package, role },
65            transaction: NexusPtbBuilder::new(context),
66        }
67    }
68
69    /// Borrows the scheduler command composer.
70    pub fn scheduler(&mut self) -> SchedulerTransaction<'_> {
71        SchedulerTransaction {
72            client: &self.client,
73            source: self.source,
74            transaction: &mut self.transaction,
75        }
76    }
77
78    /// Finishes the programmable transaction without submitting it.
79    #[must_use]
80    pub fn finish(self) -> sui::types::ProgrammableTransaction {
81        self.transaction.finish()
82    }
83
84    /// Finishes and submits the transaction with this client's signer and gas.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`NexusError`] when signing, submission, or confirmation fails.
89    pub async fn submit(self) -> Result<ExecutedTransaction, NexusError> {
90        let sender = self.client.owner()?;
91        let transaction = self.transaction.finish();
92        self.client.submit_transaction(transaction, sender).await
93    }
94}
95
96/// Scheduler commands appended to one [`NexusTransaction`].
97pub struct SchedulerTransaction<'transaction> {
98    client: &'transaction NexusClient,
99    source: TransactionSource,
100    transaction: &'transaction mut NexusPtbBuilder,
101}
102
103impl SchedulerTransaction<'_> {
104    fn require_task_source(
105        &self,
106        task: &sui::types::ObjectReference,
107    ) -> Result<(), SchedulerError> {
108        match self.source {
109            TransactionSource::Object(object_id) if object_id == *task.object_id() => Ok(()),
110            TransactionSource::Object(object_id) => Err(SchedulerError::InvalidRequest {
111                message: format!(
112                    "transaction source object '{object_id}' differs from Task '{}'",
113                    task.object_id()
114                ),
115            }),
116            TransactionSource::Creator { package, role } => Err(SchedulerError::InvalidRequest {
117                message: format!(
118                    "Task commands require an object source, but package '{package}' selected \
119                         the '{}' creator role",
120                    role.as_str()
121                ),
122            }),
123        }
124    }
125
126    /// Creates an unshared Task draft.
127    ///
128    /// Call [`TaskDraft::share`] after adding any desired Schedule.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`SchedulerError`] when Task preparation or command
133    /// construction fails.
134    pub async fn create_task<'draft>(
135        &'draft mut self,
136        task: &TaskSpec,
137    ) -> Result<TaskDraft<'draft>, SchedulerError> {
138        if !matches!(
139            self.source,
140            TransactionSource::Creator {
141                role: PackageRole::Scheduler,
142                ..
143            }
144        ) {
145            return Err(SchedulerError::InvalidRequest {
146                message: "Task creation requires an explicit Scheduler creator package".to_owned(),
147            });
148        }
149        let prepared = resolve::prepare_task(self.client, self.transaction.context(), task).await?;
150        let compiler = TaskDraftCompiler::create(self.transaction, &prepared)?;
151        Ok(TaskDraft {
152            client: self.client,
153            compiler,
154        })
155    }
156
157    /// Appends dispatch of one advertised occurrence with an explicit leader submission gas charge.
158    /// Pass zero when the leader waives reimbursement.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`SchedulerError`] when the offer and Task disagree or command
163    /// construction fails.
164    pub fn dispatch_occurrence(
165        &mut self,
166        offer: &DispatchOffer,
167        task: &sui::types::ObjectReference,
168        dag: &sui::types::ObjectReference,
169        leader_cap: &sui::types::ObjectReference,
170        gas_charge: u64,
171    ) -> Result<(), SchedulerError> {
172        self.require_task_source(task)?;
173        if offer.occurrence().task_id() != *task.object_id() {
174            return Err(SchedulerError::InvalidRequest {
175                message: format!(
176                    "dispatch offer Task '{}' differs from object '{}'",
177                    offer.occurrence().task_id(),
178                    task.object_id()
179                ),
180            });
181        }
182        compile_append_dispatch_occurrence(
183            self.transaction,
184            task,
185            dag,
186            leader_cap,
187            offer.occurrence().occurrence_id(),
188            gas_charge,
189        )
190    }
191
192    /// Appends expiration of one advertised occurrence.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`SchedulerError`] when the occurrence and Task disagree or
197    /// command construction fails.
198    pub fn expire_occurrence(
199        &mut self,
200        occurrence: OccurrenceRef,
201        task: &sui::types::ObjectReference,
202    ) -> Result<(), SchedulerError> {
203        self.require_task_source(task)?;
204        if occurrence.task_id() != *task.object_id() {
205            return Err(SchedulerError::InvalidRequest {
206                message: format!(
207                    "occurrence Task '{}' differs from object '{}'",
208                    occurrence.task_id(),
209                    task.object_id()
210                ),
211            });
212        }
213        compile_append_expire_occurrence(self.transaction, task, occurrence.occurrence_id())
214    }
215}
216
217/// An unshared Task being composed in one transaction.
218#[must_use = "a Task draft must be shared before its transaction is finished"]
219pub struct TaskDraft<'builder> {
220    client: &'builder NexusClient,
221    compiler: TaskDraftCompiler<'builder>,
222}
223
224impl TaskDraft<'_> {
225    /// Appends a complete Schedule before the Task is shared.
226    ///
227    /// Empty Schedules are valid and append no commands.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`SchedulerError`] when time preparation or command
232    /// construction fails.
233    pub async fn schedule(self, schedule: &Schedule) -> Result<Self, SchedulerError> {
234        let prepared = resolve::prepare_schedule(self.client, schedule).await?;
235        let compiler = self.compiler.schedule(&prepared)?;
236        Ok(Self {
237            client: self.client,
238            compiler,
239        })
240    }
241
242    /// Shares the composed Task and consumes the draft.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`SchedulerError`] when the share command cannot be built.
247    pub fn share(self) -> Result<(), SchedulerError> {
248        let owner = self.client.owner().map_err(SchedulerError::transport)?;
249        self.compiler.share(owner)
250    }
251}