Skip to main content

audit_trails/core/
builder.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Builder for trail-creation transactions.
5
6use std::collections::HashSet;
7
8use iota_interaction::types::base_types::IotaAddress;
9use product_common::transaction::transaction_builder::TransactionBuilder;
10
11use super::types::{Data, ImmutableMetadata, InitialRecord, LockingConfig};
12use crate::core::create::CreateTrail;
13use crate::error::Error;
14
15/// Builder for creating an audit trail.
16///
17/// The builder collects the full create-time configuration before it is normalized into the Move `create`
18/// call. Any tag list configured here becomes the trail-owned registry that later role-tag and record-tag
19/// checks refer to.
20///
21/// Creation has three additional on-chain effects worth noting:
22///
23/// - The trail object is published as a *shared* object.
24/// - A reserved `Admin` role is seeded with the permissions returned by
25///   [`PermissionSet::admin_permissions`](super::types::PermissionSet::admin_permissions), and an *initial-admin*
26///   capability is minted and transferred to the configured admin address.
27/// - When [`Self::with_initial_record`] is set, that record is stored as sequence number `0`. Its tag (if any) must
28///   already appear in the configured record tags; otherwise the on-chain create call aborts with
29///   `ERecordTagNotDefined`.
30/// - An `AuditTrailCreated` event is emitted.
31#[derive(Debug, Clone, Default)]
32pub struct AuditTrailBuilder {
33    /// Initial admin address that should receive the initial admin capability.
34    pub admin: Option<IotaAddress>,
35    /// Optional initial record created together with the trail.
36    pub initial_record: Option<InitialRecord>,
37    /// Locking rules to apply at creation time.
38    pub locking_config: LockingConfig,
39    /// Immutable metadata stored once at creation time.
40    pub trail_metadata: Option<ImmutableMetadata>,
41    /// Mutable metadata stored on the trail object.
42    pub updatable_metadata: Option<String>,
43    /// Canonical list of record tags owned by the trail.
44    pub record_tags: HashSet<String>,
45}
46
47impl AuditTrailBuilder {
48    /// Sets the full initial record input used during trail creation.
49    ///
50    /// When present, the initial record is created as sequence number `0`.
51    pub fn with_initial_record(mut self, initial_record: InitialRecord) -> Self {
52        self.initial_record = Some(initial_record);
53        self
54    }
55
56    /// Convenience helper for constructing the initial record inline.
57    pub fn with_initial_record_parts(
58        mut self,
59        data: impl Into<Data>,
60        metadata: Option<String>,
61        tag: Option<String>,
62    ) -> Self {
63        self.initial_record = Some(InitialRecord::new(data, metadata, tag));
64        self
65    }
66
67    /// Sets the locking configuration for the trail.
68    ///
69    /// This replaces the entire create-time locking configuration.
70    pub fn with_locking_config(mut self, config: LockingConfig) -> Self {
71        self.locking_config = config;
72        self
73    }
74
75    /// Sets immutable metadata for the trail.
76    ///
77    /// Immutable metadata is stored once during creation and cannot be updated later.
78    pub fn with_trail_metadata(mut self, metadata: ImmutableMetadata) -> Self {
79        self.trail_metadata = Some(metadata);
80        self
81    }
82
83    /// Sets immutable metadata by parts.
84    pub fn with_trail_metadata_parts(mut self, name: impl Into<String>, description: Option<String>) -> Self {
85        self.trail_metadata = Some(ImmutableMetadata {
86            name: name.into(),
87            description,
88        });
89        self
90    }
91
92    /// Sets updatable metadata for the trail.
93    ///
94    /// This seeds the mutable metadata field that later `update_metadata` calls can replace or clear.
95    pub fn with_updatable_metadata(mut self, metadata: impl Into<String>) -> Self {
96        self.updatable_metadata = Some(metadata.into());
97        self
98    }
99
100    /// Sets the canonical list of tags that may be used on records in this trail.
101    ///
102    /// The list is deduplicated into the trail-owned tag registry during creation.
103    pub fn with_record_tags<I, S>(mut self, tags: I) -> Self
104    where
105        I: IntoIterator<Item = S>,
106        S: Into<String>,
107    {
108        self.record_tags = tags.into_iter().map(Into::into).collect();
109        self
110    }
111
112    /// Sets the admin address that receives the initial-admin capability.
113    pub fn with_admin(mut self, admin: IotaAddress) -> Self {
114        self.admin = Some(admin);
115        self
116    }
117
118    /// Finalizes the builder and creates the trail-creation transaction builder.
119    ///
120    /// Validates the configured [`LockingConfig`] before returning the transaction. Currently this rejects:
121    /// - [`LockingWindow::CountBased`](super::types::LockingWindow::CountBased) with `count == 0` (mirrors the Move
122    ///   `ECountWindowMustBePositive` abort).
123    /// - [`TimeLock::UntilDestroyed`](super::types::TimeLock::UntilDestroyed) used as `delete_trail_lock` (mirrors the
124    ///   Move `EUntilDestroyedNotSupportedForDeleteTrail` abort). `write_lock` may still be `UntilDestroyed`.
125    ///
126    /// # Errors
127    ///
128    /// Returns [`Error::InvalidArgument`] when the locking configuration is invalid.
129    pub fn finish(self) -> Result<TransactionBuilder<CreateTrail>, Error> {
130        self.locking_config.validate()?;
131        Ok(TransactionBuilder::new(CreateTrail::new(self)))
132    }
133}