Skip to main content

audit_trails/client/
full_client.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! # Audit Trails Client
5//!
6//! The full client extends [`AuditTrailClientReadOnly`] with signing support and write
7//! transaction builders.
8//!
9//! ## Transaction Flow
10//!
11//! Write APIs return a [`TransactionBuilder`](product_common::transaction::transaction_builder::TransactionBuilder)
12//! that you can configure before signing and submitting:
13//!
14//! ```rust,no_run
15//! # use audit_trails::AuditTrailClient;
16//! # use audit_trails::core::types::Data;
17//! # async fn example(
18//! #     client: &AuditTrailClient<
19//! #         impl secret_storage::Signer<iota_interaction::IotaKeySignature> + iota_interaction::OptionalSync,
20//! #     >,
21//! # ) -> Result<(), Box<dyn std::error::Error>> {
22//! let created = client
23//!     .create_trail()
24//!     .with_initial_record_parts(Data::text("Initial record"), None, None)
25//!     .finish()?
26//!     .with_gas_budget(1_000_000)
27//!     .build_and_execute(client)
28//!     .await?;
29//!
30//! let trail_id = created.output.trail_id;
31//!
32//! client
33//!     .trail(trail_id)
34//!     .records()
35//!     .add(Data::text("Follow-up record"), None, None)
36//!     .build_and_execute(client)
37//!     .await?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ## Example Workflow
43//!
44//! ```rust,no_run
45//! # use audit_trails::AuditTrailClient;
46//! # use audit_trails::core::types::{Data, PermissionSet, RoleTags};
47//! # async fn example(
48//! #     client: &AuditTrailClient<
49//! #         impl secret_storage::Signer<iota_interaction::IotaKeySignature> + iota_interaction::OptionalSync,
50//! #     >,
51//! # ) -> Result<(), Box<dyn std::error::Error>> {
52//! let created = client
53//!     .create_trail()
54//!     .with_initial_record_parts(Data::text("Initial record"), None, None)
55//!     .with_record_tags(["finance"])
56//!     .finish()?
57//!     .build_and_execute(client)
58//!     .await?;
59//!
60//! let trail_id = created.output.trail_id;
61//!
62//! client
63//!     .trail(trail_id)
64//!     .access()
65//!     .for_role("TaggedWriter")
66//!     .create(PermissionSet::record_admin_permissions(), Some(RoleTags::new(["finance"])))
67//!     .build_and_execute(client)
68//!     .await?;
69//!
70//! client
71//!     .trail(trail_id)
72//!     .records()
73//!     .add(Data::text("Budget approved"), None, Some("finance".to_string()))
74//!     .build_and_execute(client)
75//!     .await?;
76//! # Ok(())
77//! # }
78//! ```
79
80use std::ops::Deref;
81
82use async_trait::async_trait;
83#[cfg(not(target_arch = "wasm32"))]
84use iota_interaction::IotaClient;
85use iota_interaction::types::base_types::{IotaAddress, ObjectID};
86use iota_interaction::types::crypto::PublicKey;
87use iota_interaction::types::transaction::ProgrammableTransaction;
88use iota_interaction::{IotaKeySignature, OptionalSync};
89#[cfg(target_arch = "wasm32")]
90use iota_interaction_ts::bindings::WasmIotaClient as IotaClient;
91use product_common::core_client::{CoreClient, CoreClientReadOnly};
92use product_common::network_name::NetworkName;
93use secret_storage::Signer;
94use serde::de::DeserializeOwned;
95
96use crate::client::read_only::{AuditTrailClientReadOnly, PackageOverrides};
97use crate::core::builder::AuditTrailBuilder;
98use crate::core::trail::{AuditTrailFull, AuditTrailHandle, AuditTrailReadOnly};
99use crate::error::Error;
100use crate::iota_interaction_adapter::IotaClientAdapter;
101
102/// A marker type indicating the absence of a signer.
103#[derive(Debug, Clone, Copy)]
104#[non_exhaustive]
105pub struct NoSigner;
106
107/// The error that results from a failed attempt at creating an [`AuditTrailClient`]
108/// from a given [IotaClient].
109#[derive(Debug, thiserror::Error)]
110#[error("failed to create an 'AuditTrailClient' from the given 'IotaClient'")]
111#[non_exhaustive]
112pub struct FromIotaClientError {
113    /// Type of failure for this error.
114    #[source]
115    pub kind: FromIotaClientErrorKind,
116}
117
118/// Categories of failure for [`FromIotaClientError`].
119#[derive(Debug, thiserror::Error)]
120#[non_exhaustive]
121pub enum FromIotaClientErrorKind {
122    /// A package ID is required, but was not supplied.
123    #[error("an audit-trail package ID must be supplied when connecting to an unofficial IOTA network")]
124    MissingPackageId,
125    /// Network ID resolution through an RPC call failed.
126    #[error("failed to resolve the network the given client is connected to")]
127    NetworkResolution(#[source] Box<dyn std::error::Error + Send + Sync>),
128}
129
130/// A client for creating and managing audit trails on the IOTA blockchain.
131///
132/// This client combines read-only capabilities with transaction signing,
133/// enabling full interaction with audit trails.
134///
135/// ## Type Parameter
136///
137/// - `S`: The signer type that implements [`Signer<IotaKeySignature>`]
138#[derive(Clone)]
139pub struct AuditTrailClient<S> {
140    /// The underlying read-only client used for executing read-only operations.
141    pub(super) read_client: AuditTrailClientReadOnly,
142    /// The public key associated with the signer, if any.
143    pub(super) public_key: Option<PublicKey>,
144    /// The signer used for signing transactions, or `NoSigner` if the client is read-only.
145    pub(super) signer: S,
146}
147
148impl<S> Deref for AuditTrailClient<S> {
149    type Target = AuditTrailClientReadOnly;
150    fn deref(&self) -> &Self::Target {
151        &self.read_client
152    }
153}
154
155impl AuditTrailClient<NoSigner> {
156    /// Creates a new client with no signing capabilities from an IOTA client.
157    ///
158    /// # Warning
159    ///
160    /// Passing `package_overrides` is only needed when connecting to a custom IOTA network or
161    /// when testing against explicitly deployed package pairs.
162    ///
163    /// Relying on a custom audit-trail package while connected to an official IOTA network is
164    /// strongly discouraged and can lead to compatibility problems with other official IOTA Trust
165    /// Framework products.
166    ///
167    /// # Examples
168    /// ```rust,ignore
169    /// # use audit_trails::client::AuditTrailClient;
170    ///
171    /// # #[tokio::main]
172    /// # async fn main() -> anyhow::Result<()> {
173    /// let iota_client = iota_sdk::IotaClientBuilder::default()
174    ///     .build_testnet()
175    ///     .await?;
176    /// // No package ID is required since we are connecting to an official IOTA network.
177    /// let audit_trail_client = AuditTrailClient::from_iota_client(iota_client, None).await?;
178    /// # Ok(())
179    /// # }
180    /// ```
181    pub async fn from_iota_client(
182        iota_client: IotaClient,
183        package_overrides: impl Into<Option<PackageOverrides>>,
184    ) -> Result<Self, FromIotaClientError> {
185        let read_only_client = if let Some(package_overrides) = package_overrides.into() {
186            AuditTrailClientReadOnly::new_with_package_overrides(iota_client, package_overrides).await
187        } else {
188            AuditTrailClientReadOnly::new(iota_client).await
189        }
190        .map_err(|e| match e {
191            Error::InvalidConfig(_) => FromIotaClientErrorKind::MissingPackageId,
192            Error::RpcError(msg) => FromIotaClientErrorKind::NetworkResolution(msg.into()),
193            _ => unreachable!(
194                "'AuditTrailClientReadOnly::new' has been changed without updating error handling in 'AuditTrailClient::from_iota_client'"
195            ),
196        })
197        .map_err(|kind| FromIotaClientError { kind })?;
198
199        Ok(Self {
200            read_client: read_only_client,
201            public_key: None,
202            signer: NoSigner,
203        })
204    }
205}
206
207impl<S> AuditTrailClient<S> {
208    /// Creates a signing client from an existing read-only client and signer.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if the signer public key cannot be loaded.
213    pub async fn new(client: AuditTrailClientReadOnly, signer: S) -> Result<Self, Error>
214    where
215        S: Signer<IotaKeySignature>,
216    {
217        let public_key = signer
218            .public_key()
219            .await
220            .map_err(|e| Error::InvalidKey(e.to_string()))?;
221
222        Ok(AuditTrailClient {
223            read_client: client,
224            public_key: Some(public_key),
225            signer,
226        })
227    }
228
229    /// Replaces the signer used by this client.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the replacement signer public key cannot be loaded.
234    pub async fn with_signer<NewS>(self, signer: NewS) -> Result<AuditTrailClient<NewS>, secret_storage::Error>
235    where
236        NewS: Signer<IotaKeySignature>,
237    {
238        let public_key = signer.public_key().await?;
239
240        Ok(AuditTrailClient {
241            read_client: self.read_client,
242            public_key: Some(public_key),
243            signer,
244        })
245    }
246    /// Returns the underlying read-only client view.
247    pub fn read_only(&self) -> &AuditTrailClientReadOnly {
248        &self.read_client
249    }
250
251    /// Returns a typed handle bound to a specific trail object ID.
252    pub fn trail<'a>(&'a self, trail_id: ObjectID) -> AuditTrailHandle<'a, Self> {
253        AuditTrailHandle::new(self, trail_id)
254    }
255
256    /// Returns the TfComponents package ID used by this client.
257    pub fn tf_components_package_id(&self) -> ObjectID {
258        self.read_client.tf_components_package_id()
259    }
260
261    /// Creates a builder for a new audit trail.
262    ///
263    /// When the client has a signer, the builder is pre-populated with that signer's address as
264    /// the initial admin.
265    pub fn create_trail(&self) -> AuditTrailBuilder {
266        AuditTrailBuilder {
267            admin: self.public_key.as_ref().map(IotaAddress::from),
268            ..AuditTrailBuilder::default()
269        }
270    }
271}
272
273impl<S> AuditTrailClient<S>
274where
275    S: Signer<IotaKeySignature>,
276{
277    /// Returns a reference to the [PublicKey] wrapped by this client.
278    pub fn public_key(&self) -> &PublicKey {
279        self.public_key.as_ref().expect("public_key is set")
280    }
281
282    /// Returns the [IotaAddress] wrapped by this client.
283    #[inline(always)]
284    pub fn address(&self) -> IotaAddress {
285        IotaAddress::from(self.public_key())
286    }
287}
288
289#[cfg_attr(feature = "send-sync", async_trait)]
290#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
291impl<S> CoreClientReadOnly for AuditTrailClient<S> {
292    fn package_id(&self) -> ObjectID {
293        self.read_client.package_id()
294    }
295
296    fn tf_components_package_id(&self) -> Option<ObjectID> {
297        Some(self.read_client.tf_components_package_id())
298    }
299
300    fn network_name(&self) -> &NetworkName {
301        self.read_client.network()
302    }
303
304    fn client_adapter(&self) -> &IotaClientAdapter {
305        &self.read_client
306    }
307}
308
309#[cfg_attr(feature = "send-sync", async_trait)]
310#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
311impl<S> CoreClient<S> for AuditTrailClient<S>
312where
313    S: Signer<IotaKeySignature> + OptionalSync,
314{
315    fn signer(&self) -> &S {
316        &self.signer
317    }
318
319    fn sender_address(&self) -> IotaAddress {
320        IotaAddress::from(self.public_key())
321    }
322
323    fn sender_public_key(&self) -> &PublicKey {
324        self.public_key()
325    }
326}
327
328#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
329#[cfg_attr(feature = "send-sync", async_trait)]
330impl<S> AuditTrailReadOnly for AuditTrailClient<S>
331where
332    S: Signer<IotaKeySignature> + OptionalSync,
333{
334    /// Delegates read-only execution to the wrapped [`AuditTrailClientReadOnly`].
335    async fn execute_read_only_transaction<T: DeserializeOwned>(
336        &self,
337        tx: ProgrammableTransaction,
338    ) -> Result<T, Error> {
339        self.read_client.execute_read_only_transaction(tx).await
340    }
341}
342
343impl<S> AuditTrailFull for AuditTrailClient<S> where S: Signer<IotaKeySignature> + OptionalSync {}