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