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}