Skip to main content

audit_trails/core/locking/
mod.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Locking configuration APIs for Audit Trails.
5
6use iota_interaction::types::base_types::ObjectID;
7use iota_interaction::{IotaKeySignature, OptionalSync};
8use product_common::core_client::CoreClient;
9use product_common::transaction::transaction_builder::TransactionBuilder;
10use secret_storage::Signer;
11
12use crate::core::trail::{AuditTrailFull, AuditTrailReadOnly};
13use crate::core::types::{LockingConfig, LockingWindow, TimeLock};
14use crate::error::Error;
15
16mod operations;
17mod transactions;
18
19pub use transactions::{UpdateDeleteRecordWindow, UpdateDeleteTrailLock, UpdateLockingConfig, UpdateWriteLock};
20
21use self::operations::LockingOps;
22
23/// Locking API scoped to a specific trail.
24///
25/// This handle updates the trail's locking configuration and queries whether an individual record is currently
26/// locked against deletion.
27#[derive(Debug, Clone)]
28pub struct TrailLocking<'a, C> {
29    pub(crate) client: &'a C,
30    pub(crate) trail_id: ObjectID,
31    pub(crate) selected_capability_id: Option<ObjectID>,
32}
33
34impl<'a, C> TrailLocking<'a, C> {
35    pub(crate) fn new(client: &'a C, trail_id: ObjectID, selected_capability_id: Option<ObjectID>) -> Self {
36        Self {
37            client,
38            trail_id,
39            selected_capability_id,
40        }
41    }
42
43    /// Uses the provided capability as the auth capability for subsequent write operations.
44    pub fn using_capability(mut self, capability_id: ObjectID) -> Self {
45        self.selected_capability_id = Some(capability_id);
46        self
47    }
48
49    /// Replaces the full locking configuration for the trail.
50    ///
51    /// This overwrites all three locking dimensions at once: record delete window, trail delete lock, and
52    /// write lock. The supplied [`LockingConfig`] is validated before the transaction is constructed.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`Error::InvalidArgument`] when `config` contains:
57    /// * `delete_record_window` using [`LockingWindow::CountBased`] with `count == 0` (mirrors the Move
58    ///   `ECountWindowMustBePositive` abort).
59    /// * `delete_trail_lock` using [`TimeLock::UntilDestroyed`] (mirrors the Move
60    ///   `EUntilDestroyedNotSupportedForDeleteTrail` abort).
61    pub fn update<S>(&self, config: LockingConfig) -> Result<TransactionBuilder<UpdateLockingConfig>, Error>
62    where
63        C: AuditTrailFull + CoreClient<S>,
64        S: Signer<IotaKeySignature> + OptionalSync,
65    {
66        config.validate()?;
67        let owner = self.client.sender_address();
68        Ok(TransactionBuilder::new(UpdateLockingConfig::new(
69            self.trail_id,
70            owner,
71            config,
72            self.selected_capability_id,
73        )))
74    }
75
76    /// Updates only the delete-record window.
77    ///
78    /// Count-based windows protect the last N records present in trail order at the start of each call that
79    /// consults the window. `count` must be positive; pass [`LockingWindow::None`] to remove the lock.
80    /// Large count values increase delete gas linearly because the on-chain check walks backward from the tail
81    /// to determine the protected window's lower bound.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`Error::InvalidArgument`] when `window` is [`LockingWindow::CountBased`] with `count == 0`
86    /// (mirrors the Move `ECountWindowMustBePositive` abort).
87    pub fn update_delete_record_window<S>(
88        &self,
89        window: LockingWindow,
90    ) -> Result<TransactionBuilder<UpdateDeleteRecordWindow>, Error>
91    where
92        C: AuditTrailFull + CoreClient<S>,
93        S: Signer<IotaKeySignature> + OptionalSync,
94    {
95        window.validate()?;
96        let owner = self.client.sender_address();
97        Ok(TransactionBuilder::new(UpdateDeleteRecordWindow::new(
98            self.trail_id,
99            owner,
100            window,
101            self.selected_capability_id,
102        )))
103    }
104
105    /// Updates only the delete-trail time lock.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`Error::InvalidArgument`] when `lock` is [`TimeLock::UntilDestroyed`]
110    /// (mirrors the Move `EUntilDestroyedNotSupportedForDeleteTrail` abort).
111    pub fn update_delete_trail_lock<S>(
112        &self,
113        lock: TimeLock,
114    ) -> Result<TransactionBuilder<UpdateDeleteTrailLock>, Error>
115    where
116        C: AuditTrailFull + CoreClient<S>,
117        S: Signer<IotaKeySignature> + OptionalSync,
118    {
119        lock.validate_as_delete_trail_lock()?;
120        let owner = self.client.sender_address();
121        Ok(TransactionBuilder::new(UpdateDeleteTrailLock::new(
122            self.trail_id,
123            owner,
124            lock,
125            self.selected_capability_id,
126        )))
127    }
128
129    /// Updates only the write lock.
130    pub fn update_write_lock<S>(&self, lock: TimeLock) -> TransactionBuilder<UpdateWriteLock>
131    where
132        C: AuditTrailFull + CoreClient<S>,
133        S: Signer<IotaKeySignature> + OptionalSync,
134    {
135        let owner = self.client.sender_address();
136        TransactionBuilder::new(UpdateWriteLock::new(
137            self.trail_id,
138            owner,
139            lock,
140            self.selected_capability_id,
141        ))
142    }
143
144    /// Returns `true` when the given record is currently locked against deletion.
145    ///
146    /// For count-based windows, the check determines the protected window's lower bound by walking back
147    /// from the current tail at call time; time-based locks are evaluated against the clock timestamp at
148    /// call time. The result reflects the trail snapshot observed by this read-only call.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the lock state cannot be computed from the current on-chain state.
153    pub async fn is_record_locked(&self, sequence_number: u64) -> Result<bool, Error>
154    where
155        C: AuditTrailReadOnly,
156    {
157        let tx = LockingOps::is_record_locked(self.client, self.trail_id, sequence_number).await?;
158        self.client.execute_read_only_transaction(tx).await
159    }
160}