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