Skip to main content

audit_trails/core/trail/
transactions.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transaction payloads for trail-level metadata, migration, and deletion operations.
5
6use async_trait::async_trait;
7use iota_interaction::OptionalSync;
8use iota_interaction::rpc_types::{IotaTransactionBlockEffects, IotaTransactionBlockEvents};
9use iota_interaction::types::base_types::{IotaAddress, ObjectID};
10use iota_interaction::types::transaction::ProgrammableTransaction;
11use product_common::core_client::CoreClientReadOnly;
12use product_common::transaction::transaction_builder::Transaction;
13use tokio::sync::OnceCell;
14
15use super::operations::TrailOps;
16use crate::core::types::{AuditTrailDeleted, Event};
17use crate::error::Error;
18
19/// Transaction that migrates a trail to the latest package version supported by this crate.
20///
21/// This requires the `Migrate` permission on the supplied capability and succeeds only when the on-chain
22/// package version is *strictly less* than the current supported version. Otherwise the Move package aborts
23/// with `EPackageVersionMismatch`.
24///
25/// On success an `AuditTrailMigrated` event is emitted.
26#[derive(Debug, Clone)]
27pub struct Migrate {
28    trail_id: ObjectID,
29    owner: IotaAddress,
30    selected_capability_id: Option<ObjectID>,
31    cached_ptb: OnceCell<ProgrammableTransaction>,
32}
33
34impl Migrate {
35    /// Creates a `Migrate` transaction builder payload.
36    pub fn new(trail_id: ObjectID, owner: IotaAddress, selected_capability_id: Option<ObjectID>) -> Self {
37        Self {
38            trail_id,
39            owner,
40            selected_capability_id,
41            cached_ptb: OnceCell::new(),
42        }
43    }
44
45    async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
46    where
47        C: CoreClientReadOnly + OptionalSync,
48    {
49        TrailOps::migrate(client, self.trail_id, self.owner, self.selected_capability_id).await
50    }
51}
52
53#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
54#[cfg_attr(feature = "send-sync", async_trait)]
55impl Transaction for Migrate {
56    type Error = Error;
57    type Output = ();
58
59    async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
60    where
61        C: CoreClientReadOnly + OptionalSync,
62    {
63        self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
64    }
65
66    async fn apply<C>(self, _: &mut IotaTransactionBlockEffects, _: &C) -> Result<Self::Output, Self::Error>
67    where
68        C: CoreClientReadOnly + OptionalSync,
69    {
70        Ok(())
71    }
72}
73
74/// Transaction that updates mutable trail metadata.
75///
76/// Requires the `UpdateMetadata` permission on the supplied capability. Passing `None` clears the mutable
77/// metadata field on-chain.
78///
79/// On success a `MetadataUpdated` event is emitted.
80#[derive(Debug, Clone)]
81pub struct UpdateMetadata {
82    trail_id: ObjectID,
83    owner: IotaAddress,
84    metadata: Option<String>,
85    selected_capability_id: Option<ObjectID>,
86    cached_ptb: OnceCell<ProgrammableTransaction>,
87}
88
89impl UpdateMetadata {
90    /// Creates an `UpdateMetadata` transaction builder payload.
91    pub fn new(
92        trail_id: ObjectID,
93        owner: IotaAddress,
94        metadata: Option<String>,
95        selected_capability_id: Option<ObjectID>,
96    ) -> Self {
97        Self {
98            trail_id,
99            owner,
100            metadata,
101            selected_capability_id,
102            cached_ptb: OnceCell::new(),
103        }
104    }
105
106    async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
107    where
108        C: CoreClientReadOnly + OptionalSync,
109    {
110        TrailOps::update_metadata(
111            client,
112            self.trail_id,
113            self.owner,
114            self.metadata.clone(),
115            self.selected_capability_id,
116        )
117        .await
118    }
119}
120
121#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
122#[cfg_attr(feature = "send-sync", async_trait)]
123impl Transaction for UpdateMetadata {
124    type Error = Error;
125    type Output = ();
126
127    async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
128    where
129        C: CoreClientReadOnly + OptionalSync,
130    {
131        self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
132    }
133
134    async fn apply<C>(self, _: &mut IotaTransactionBlockEffects, _: &C) -> Result<Self::Output, Self::Error>
135    where
136        C: CoreClientReadOnly + OptionalSync,
137    {
138        Ok(())
139    }
140}
141
142/// Transaction that deletes an empty trail.
143///
144/// Requires the `DeleteAuditTrail` permission. The Move package additionally aborts with
145/// `ETrailNotEmpty` while any records remain in the trail and with `ETrailDeleteLocked` while the
146/// configured `delete_trail_lock` is still active.
147///
148/// On success an `AuditTrailDeleted` event is emitted.
149#[derive(Debug, Clone)]
150pub struct DeleteAuditTrail {
151    trail_id: ObjectID,
152    owner: IotaAddress,
153    selected_capability_id: Option<ObjectID>,
154    cached_ptb: OnceCell<ProgrammableTransaction>,
155}
156
157impl DeleteAuditTrail {
158    /// Creates a `DeleteAuditTrail` transaction builder payload.
159    pub fn new(trail_id: ObjectID, owner: IotaAddress, selected_capability_id: Option<ObjectID>) -> Self {
160        Self {
161            trail_id,
162            owner,
163            selected_capability_id,
164            cached_ptb: OnceCell::new(),
165        }
166    }
167
168    async fn make_ptb<C>(&self, client: &C) -> Result<ProgrammableTransaction, Error>
169    where
170        C: CoreClientReadOnly + OptionalSync,
171    {
172        TrailOps::delete_audit_trail(client, self.trail_id, self.owner, self.selected_capability_id).await
173    }
174}
175
176#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
177#[cfg_attr(feature = "send-sync", async_trait)]
178impl Transaction for DeleteAuditTrail {
179    type Error = Error;
180    type Output = AuditTrailDeleted;
181
182    async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
183    where
184        C: CoreClientReadOnly + OptionalSync,
185    {
186        self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
187    }
188
189    async fn apply_with_events<C>(
190        self,
191        _: &mut IotaTransactionBlockEffects,
192        events: &mut IotaTransactionBlockEvents,
193        _: &C,
194    ) -> Result<Self::Output, Self::Error>
195    where
196        C: CoreClientReadOnly + OptionalSync,
197    {
198        let event = events
199            .data
200            .iter()
201            .find_map(|data| serde_json::from_value::<Event<AuditTrailDeleted>>(data.parsed_json.clone()).ok())
202            .ok_or_else(|| Error::UnexpectedApiResponse("Expected AuditTrailDeleted event not found".to_string()))?;
203
204        Ok(event.data)
205    }
206
207    async fn apply<C>(self, _: &mut IotaTransactionBlockEffects, _: &C) -> Result<Self::Output, Self::Error>
208    where
209        C: CoreClientReadOnly + OptionalSync,
210    {
211        unreachable!()
212    }
213}