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::crypto::PublicKey;
86use iota_interaction::{IotaKeySignature, OptionalSync};
87#[cfg(target_arch = "wasm32")]
88use iota_interaction_ts::bindings::WasmIotaClient as IotaClient;
89use iota_sdk_types::{Address, ObjectId, ProgrammableTransaction};
90use product_common::core_client::{CoreClient, CoreClientReadOnly};
91use product_common::network_name::NetworkName;
92use secret_storage::Signer;
93use serde::de::DeserializeOwned;
94
95use crate::client::read_only::{AuditTrailClientReadOnly, PackageOverrides};
96use crate::core::builder::AuditTrailBuilder;
97use crate::core::trail::{AuditTrailFull, AuditTrailHandle, AuditTrailReadOnly};
98use crate::error::Error;
99use crate::iota_interaction_adapter::IotaClientAdapter;
100
101/// A marker type indicating the absence of a signer.
102#[derive(Debug, Clone, Copy)]
103#[non_exhaustive]
104pub struct NoSigner;
105
106/// The error that results from a failed attempt at creating an [`AuditTrailClient`]
107/// from a given [IotaClient].
108#[derive(Debug, thiserror::Error)]
109#[error("failed to create an 'AuditTrailClient' from the given 'IotaClient'")]
110#[non_exhaustive]
111pub struct FromIotaClientError {
112    /// Type of failure for this error.
113    #[source]
114    pub kind: FromIotaClientErrorKind,
115}
116
117/// Categories of failure for [`FromIotaClientError`].
118#[derive(Debug, thiserror::Error)]
119#[non_exhaustive]
120pub enum FromIotaClientErrorKind {
121    /// A package ID is required, but was not supplied.
122    #[error("an audit-trail package ID must be supplied when connecting to an unofficial IOTA network")]
123    MissingPackageId,
124    /// Network ID resolution through an RPC call failed.
125    #[error("failed to resolve the network the given client is connected to")]
126    NetworkResolution(#[source] Box<dyn std::error::Error + Send + Sync>),
127}
128
129/// A client for creating and managing audit trails on the IOTA blockchain.
130///
131/// This client combines read-only capabilities with transaction signing,
132/// enabling full interaction with audit trails.
133///
134/// ## Type Parameter
135///
136/// - `S`: The signer type that implements [`Signer<IotaKeySignature>`]
137#[derive(Clone)]
138pub struct AuditTrailClient<S> {
139    /// The underlying read-only client used for executing read-only operations.
140    pub(super) read_client: AuditTrailClientReadOnly,
141    /// The public key associated with the signer, if any.
142    pub(super) public_key: Option<PublicKey>,
143    /// The signer used for signing transactions, or `NoSigner` if the client is read-only.
144    pub(super) signer: S,
145}
146
147impl<S> Deref for AuditTrailClient<S> {
148    type Target = AuditTrailClientReadOnly;
149    fn deref(&self) -> &Self::Target {
150        &self.read_client
151    }
152}
153
154impl AuditTrailClient<NoSigner> {
155    /// Creates a new client with no signing capabilities from an IOTA client.
156    ///
157    /// # Warning
158    ///
159    /// Passing `package_overrides` is only needed when connecting to a custom IOTA network or
160    /// when testing against explicitly deployed package pairs.
161    ///
162    /// Relying on a custom audit-trail package while connected to an official IOTA network is
163    /// strongly discouraged and can lead to compatibility problems with other official IOTA Trust
164    /// Framework products.
165    ///
166    /// # Examples
167    /// ```rust,ignore
168    /// # use audit_trails::client::AuditTrailClient;
169    ///
170    /// # #[tokio::main]
171    /// # async fn main() -> anyhow::Result<()> {
172    /// let iota_client = iota_sdk::IotaClientBuilder::default()
173    ///     .build_testnet()
174    ///     .await?;
175    /// // No package ID is required since we are connecting to an official IOTA network.
176    /// let audit_trail_client = AuditTrailClient::from_iota_client(iota_client, None).await?;
177    /// # Ok(())
178    /// # }
179    /// ```
180    pub async fn from_iota_client(
181        iota_client: IotaClient,
182        package_overrides: impl Into<Option<PackageOverrides>>,
183    ) -> Result<Self, FromIotaClientError> {
184        let read_only_client = if let Some(package_overrides) = package_overrides.into() {
185            AuditTrailClientReadOnly::new_with_package_overrides(iota_client, package_overrides).await
186        } else {
187            AuditTrailClientReadOnly::new(iota_client).await
188        }
189        .map_err(|e| match e {
190            Error::InvalidConfig(_) => FromIotaClientErrorKind::MissingPackageId,
191            Error::RpcError(msg) => FromIotaClientErrorKind::NetworkResolution(msg.into()),
192            _ => unreachable!(
193                "'AuditTrailClientReadOnly::new' has been changed without updating error handling in 'AuditTrailClient::from_iota_client'"
194            ),
195        })
196        .map_err(|kind| FromIotaClientError { kind })?;
197
198        Ok(Self {
199            read_client: read_only_client,
200            public_key: None,
201            signer: NoSigner,
202        })
203    }
204}
205
206impl<S> AuditTrailClient<S> {
207    /// Creates a signing client from an existing read-only client and signer.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the signer public key cannot be loaded.
212    pub async fn new(client: AuditTrailClientReadOnly, signer: S) -> Result<Self, Error>
213    where
214        S: Signer<IotaKeySignature>,
215    {
216        let public_key = signer
217            .public_key()
218            .await
219            .map_err(|e| Error::InvalidKey(e.to_string()))?;
220
221        Ok(AuditTrailClient {
222            read_client: client,
223            public_key: Some(public_key),
224            signer,
225        })
226    }
227
228    /// Replaces the signer used by this client.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if the replacement signer public key cannot be loaded.
233    pub async fn with_signer<NewS>(self, signer: NewS) -> Result<AuditTrailClient<NewS>, secret_storage::Error>
234    where
235        NewS: Signer<IotaKeySignature>,
236    {
237        let public_key = signer.public_key().await?;
238
239        Ok(AuditTrailClient {
240            read_client: self.read_client,
241            public_key: Some(public_key),
242            signer,
243        })
244    }
245    /// Returns the underlying read-only client view.
246    pub fn read_only(&self) -> &AuditTrailClientReadOnly {
247        &self.read_client
248    }
249
250    /// Returns a typed handle bound to a specific trail object ID.
251    pub fn trail<'a>(&'a self, trail_id: ObjectId) -> AuditTrailHandle<'a, Self> {
252        AuditTrailHandle::new(self, trail_id)
253    }
254
255    /// Returns the TfComponents package ID used by this client.
256    pub fn tf_components_package_id(&self) -> ObjectId {
257        self.read_client.tf_components_package_id()
258    }
259
260    /// Creates a builder for a new audit trail.
261    ///
262    /// When the client has a signer, the builder is pre-populated with that signer's address as
263    /// the initial admin.
264    pub fn create_trail(&self) -> AuditTrailBuilder {
265        AuditTrailBuilder {
266            admin: self.public_key.as_ref().map(Address::from),
267            ..AuditTrailBuilder::default()
268        }
269    }
270}
271
272impl<S> AuditTrailClient<S>
273where
274    S: Signer<IotaKeySignature>,
275{
276    /// Returns a reference to the [PublicKey] wrapped by this client.
277    pub fn public_key(&self) -> &PublicKey {
278        self.public_key.as_ref().expect("public_key is set")
279    }
280
281    /// Returns the [Address] wrapped by this client.
282    #[inline(always)]
283    pub fn address(&self) -> Address {
284        Address::from(self.public_key())
285    }
286}
287
288#[cfg_attr(feature = "send-sync", async_trait)]
289#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
290impl<S> CoreClientReadOnly for AuditTrailClient<S> {
291    fn package_id(&self) -> ObjectId {
292        self.read_client.package_id()
293    }
294
295    fn tf_components_package_id(&self) -> Option<ObjectId> {
296        Some(self.read_client.tf_components_package_id())
297    }
298
299    fn network_name(&self) -> &NetworkName {
300        self.read_client.network()
301    }
302
303    fn client_adapter(&self) -> &IotaClientAdapter {
304        &self.read_client
305    }
306}
307
308#[cfg_attr(feature = "send-sync", async_trait)]
309#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
310impl<S> CoreClient<S> for AuditTrailClient<S>
311where
312    S: Signer<IotaKeySignature> + OptionalSync,
313{
314    fn signer(&self) -> &S {
315        &self.signer
316    }
317
318    fn sender_address(&self) -> Address {
319        Address::from(self.public_key())
320    }
321
322    fn sender_public_key(&self) -> &PublicKey {
323        self.public_key()
324    }
325}
326
327#[cfg_attr(not(feature = "send-sync"), async_trait(?Send))]
328#[cfg_attr(feature = "send-sync", async_trait)]
329impl<S> AuditTrailReadOnly for AuditTrailClient<S>
330where
331    S: Signer<IotaKeySignature> + OptionalSync,
332{
333    /// Delegates read-only execution to the wrapped [`AuditTrailClientReadOnly`].
334    async fn execute_read_only_transaction<T: DeserializeOwned>(
335        &self,
336        tx: ProgrammableTransaction,
337    ) -> Result<T, Error> {
338        self.read_client.execute_read_only_transaction(tx).await
339    }
340}
341
342impl<S> AuditTrailFull for AuditTrailClient<S> where S: Signer<IotaKeySignature> + OptionalSync {}