Skip to main content

audit_trails/core/
trail.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! High-level trail handles and trail-scoped transactions.
5
6use iota_interaction::{IotaKeySignature, OptionalSync};
7use iota_sdk_types::{ObjectId, ProgrammableTransaction};
8use product_common::core_client::{CoreClient, CoreClientReadOnly};
9use product_common::transaction::transaction_builder::TransactionBuilder;
10use secret_storage::Signer;
11use serde::de::DeserializeOwned;
12
13use crate::core::access::TrailAccess;
14use crate::core::internal::trail as trail_reader;
15use crate::core::locking::TrailLocking;
16use crate::core::records::TrailRecords;
17use crate::core::tags::TrailTags;
18use crate::core::types::{Data, OnChainAuditTrail};
19use crate::error::Error;
20
21mod operations;
22mod transactions;
23
24pub use transactions::{DeleteAuditTrail, Migrate, UpdateMetadata};
25
26/// Marker trait for read-only audit-trail clients.
27#[doc(hidden)]
28#[cfg_attr(not(feature = "send-sync"), async_trait::async_trait(?Send))]
29#[cfg_attr(feature = "send-sync", async_trait::async_trait)]
30pub trait AuditTrailReadOnly: CoreClientReadOnly + OptionalSync {
31    /// Executes a read-only programmable transaction and decodes the first return value.
32    async fn execute_read_only_transaction<T: DeserializeOwned>(&self, tx: ProgrammableTransaction)
33    -> Result<T, Error>;
34}
35
36/// Marker trait for full audit-trail clients.
37#[doc(hidden)]
38pub trait AuditTrailFull: AuditTrailReadOnly {}
39
40/// A typed handle bound to one trail ID and one client.
41///
42/// This is the main trail-scoped entry point. It keeps the trail identity together with the client so record,
43/// locking, access, tag, migration, and metadata operations all share one typed handle.
44#[derive(Debug, Clone)]
45pub struct AuditTrailHandle<'a, C> {
46    pub(crate) client: &'a C,
47    pub(crate) trail_id: ObjectId,
48    pub(crate) selected_capability_id: Option<ObjectId>,
49}
50
51impl<'a, C> AuditTrailHandle<'a, C> {
52    pub(crate) fn new(client: &'a C, trail_id: ObjectId) -> Self {
53        Self {
54            client,
55            trail_id,
56            selected_capability_id: None,
57        }
58    }
59
60    /// Uses the provided capability as the auth capability for subsequent write operations.
61    pub fn using_capability(mut self, capability_id: ObjectId) -> Self {
62        self.selected_capability_id = Some(capability_id);
63        self
64    }
65
66    /// Loads the full on-chain audit trail object.
67    ///
68    /// Each call fetches a fresh snapshot from chain state rather than reusing cached client-side data.
69    pub async fn get(&self) -> Result<OnChainAuditTrail, Error>
70    where
71        C: AuditTrailReadOnly,
72    {
73        trail_reader::get_audit_trail(self.trail_id, self.client).await
74    }
75
76    /// Updates the trail's mutable metadata field.
77    ///
78    /// Passing `None` clears the field on-chain.
79    pub fn update_metadata<S>(&self, metadata: Option<String>) -> TransactionBuilder<UpdateMetadata>
80    where
81        C: AuditTrailFull + CoreClient<S>,
82        S: Signer<IotaKeySignature> + OptionalSync,
83    {
84        let owner = self.client.sender_address();
85        TransactionBuilder::new(UpdateMetadata::new(
86            self.trail_id,
87            owner,
88            metadata,
89            self.selected_capability_id,
90        ))
91    }
92
93    /// Migrates the trail to the latest package version supported by this crate.
94    pub fn migrate<S>(&self) -> TransactionBuilder<Migrate>
95    where
96        C: AuditTrailFull + CoreClient<S>,
97        S: Signer<IotaKeySignature> + OptionalSync,
98    {
99        let owner = self.client.sender_address();
100        TransactionBuilder::new(Migrate::new(self.trail_id, owner, self.selected_capability_id))
101    }
102
103    /// Deletes the trail object.
104    ///
105    /// Requires the `DeleteAuditTrail` permission. Deletion additionally requires the trail to be
106    /// empty (`ETrailNotEmpty` otherwise) and the configured `delete_trail_lock` to have elapsed
107    /// (`ETrailDeleteLocked` otherwise).
108    pub fn delete_audit_trail<S>(&self) -> TransactionBuilder<DeleteAuditTrail>
109    where
110        C: AuditTrailFull + CoreClient<S>,
111        S: Signer<IotaKeySignature> + OptionalSync,
112    {
113        let owner = self.client.sender_address();
114        TransactionBuilder::new(DeleteAuditTrail::new(self.trail_id, owner, self.selected_capability_id))
115    }
116
117    /// Returns the record API scoped to this trail.
118    ///
119    /// Use this for record reads, appends, and deletions.
120    pub fn records(&self) -> TrailRecords<'a, C, Data> {
121        TrailRecords::new(self.client, self.trail_id, self.selected_capability_id)
122    }
123
124    /// Returns the locking API scoped to this trail.
125    ///
126    /// Use this for inspecting lock state and updating locking rules.
127    pub fn locking(&self) -> TrailLocking<'a, C> {
128        TrailLocking::new(self.client, self.trail_id, self.selected_capability_id)
129    }
130
131    /// Returns the access-control API scoped to this trail.
132    ///
133    /// Use this for roles, capabilities, and access-policy updates.
134    pub fn access(&self) -> TrailAccess<'a, C> {
135        TrailAccess::new(self.client, self.trail_id, self.selected_capability_id)
136    }
137
138    /// Returns the tag-registry API scoped to this trail.
139    ///
140    /// Use this for managing the canonical tag registry that record writes and role tags must reference.
141    pub fn tags(&self) -> TrailTags<'a, C> {
142        TrailTags::new(self.client, self.trail_id, self.selected_capability_id)
143    }
144}