Skip to main content

audit_trails/core/create/
transactions.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use async_trait::async_trait;
5use iota_interaction::OptionalSync;
6use iota_interaction::rpc_types::{IotaTransactionBlockEffects, IotaTransactionBlockEvents};
7use iota_interaction::types::base_types::{IotaAddress, ObjectID};
8use iota_interaction::types::transaction::ProgrammableTransaction;
9use product_common::core_client::CoreClientReadOnly;
10use product_common::transaction::transaction_builder::Transaction;
11use tokio::sync::OnceCell;
12
13use super::operations::{CreateOps, CreateTrailArgs};
14use crate::core::builder::AuditTrailBuilder;
15use crate::core::internal::trail as trail_reader;
16use crate::core::types::{AuditTrailCreated, Event, OnChainAuditTrail};
17use crate::error::Error;
18
19/// Output of a successful trail-creation transaction.
20#[derive(Debug, Clone)]
21pub struct TrailCreated {
22    /// Newly created trail object ID.
23    pub trail_id: ObjectID,
24    /// Address that created the trail.
25    pub creator: IotaAddress,
26    /// Millisecond timestamp emitted by the creation event.
27    pub timestamp: u64,
28}
29
30impl TrailCreated {
31    /// Loads the newly created trail object from the ledger.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the trail cannot be fetched or deserialized.
36    pub async fn fetch_audit_trail<C>(&self, client: &C) -> Result<OnChainAuditTrail, Error>
37    where
38        C: CoreClientReadOnly + OptionalSync,
39    {
40        trail_reader::get_audit_trail(self.trail_id, client).await
41    }
42}
43
44/// A transaction that creates a new audit trail.
45///
46/// The builder state is normalized into the exact Move `create` call shape, including tag-registry setup,
47/// optional initial-record creation, and initial-admin capability assignment.
48///
49/// On execution the Move package: shares the trail object, seeds the reserved `Admin` role with the
50/// permissions returned by `permission::admin_permissions`, transfers a freshly minted initial-admin
51/// capability to the admin address, stores the optional initial record at sequence number `0`, and emits
52/// an `AuditTrailCreated` event. If an initial record carries a tag, the tag must already be in the
53/// configured record-tag registry or the call aborts with `ERecordTagNotDefined`.
54#[derive(Debug, Clone)]
55pub struct CreateTrail {
56    builder: AuditTrailBuilder,
57    cached_ptb: OnceCell<ProgrammableTransaction>,
58}
59
60impl CreateTrail {
61    /// Creates a new [`CreateTrail`] instance.
62    pub fn new(builder: AuditTrailBuilder) -> Self {
63        Self {
64            builder,
65            cached_ptb: OnceCell::new(),
66        }
67    }
68
69    async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
70    where
71        C: CoreClientReadOnly + OptionalSync,
72    {
73        let AuditTrailBuilder {
74            admin,
75            initial_record,
76            locking_config,
77            trail_metadata,
78            updatable_metadata,
79            record_tags,
80        } = self.builder.clone();
81
82        let admin = admin.ok_or_else(|| {
83            Error::InvalidArgument(
84                "admin address is required; use `client.create_trail()` with signer or call `with_admin(...)`"
85                    .to_string(),
86            )
87        })?;
88        let tf_package_id = client
89            .tf_components_package_id()
90            .expect("TfComponents package ID should be present for Audit Trail clients");
91
92        CreateOps::create_trail(CreateTrailArgs {
93            audit_trail_package_id: client.package_id(),
94            tf_components_package_id: tf_package_id,
95            admin,
96            initial_record,
97            locking_config,
98            trail_metadata,
99            updatable_metadata,
100            record_tags,
101        })
102    }
103}
104
105#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
106#[cfg_attr(feature = "send-sync", async_trait)]
107impl Transaction for CreateTrail {
108    type Error = Error;
109    type Output = TrailCreated;
110
111    async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
112    where
113        C: CoreClientReadOnly + OptionalSync,
114    {
115        self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
116    }
117
118    async fn apply_with_events<C>(
119        mut self,
120        _: &mut IotaTransactionBlockEffects,
121        events: &mut IotaTransactionBlockEvents,
122        _: &C,
123    ) -> Result<Self::Output, Self::Error>
124    where
125        C: CoreClientReadOnly + OptionalSync,
126    {
127        let event = events
128            .data
129            .iter()
130            .find_map(|data| serde_json::from_value::<Event<AuditTrailCreated>>(data.parsed_json.clone()).ok())
131            .ok_or_else(|| Error::UnexpectedApiResponse("AuditTrailCreated event not found".to_string()))?;
132
133        Ok(TrailCreated {
134            trail_id: event.data.trail_id,
135            creator: event.data.creator,
136            timestamp: event.data.timestamp,
137        })
138    }
139
140    async fn apply<C>(mut self, _: &mut IotaTransactionBlockEffects, _: &C) -> Result<Self::Output, Self::Error>
141    where
142        C: CoreClientReadOnly + OptionalSync,
143    {
144        unreachable!()
145    }
146}