Skip to main content

audit_trails/client/
read_only.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Read-only client support for audit-trail interactions.
5//!
6//! [`AuditTrailClientReadOnly`] resolves the deployed package IDs for the connected network, exposes
7//! typed trail handles, and provides the internal read-only execution primitive used by the handle
8//! APIs.
9
10use std::ops::Deref;
11
12#[cfg(not(target_arch = "wasm32"))]
13use iota_interaction::IotaClient;
14use iota_interaction::IotaClientTrait;
15use iota_interaction::types::base_types::{IotaAddress, ObjectID};
16use iota_interaction::types::transaction::{ProgrammableTransaction, TransactionKind};
17#[cfg(target_arch = "wasm32")]
18use iota_interaction_ts::bindings::WasmIotaClient;
19use product_common::core_client::CoreClientReadOnly;
20use product_common::network_name::NetworkName;
21use serde::de::DeserializeOwned;
22
23use super::network_id;
24use crate::core::trail::{AuditTrailHandle, AuditTrailReadOnly};
25use crate::error::Error;
26use crate::iota_interaction_adapter::IotaClientAdapter;
27use crate::package;
28
29/// Explicit package-ID overrides used when constructing an audit-trail client.
30///
31/// Use this when talking to custom deployments, local test networks, or any environment where the
32/// package registry does not yet know the relevant package IDs.
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub struct PackageOverrides {
35    /// Override for the audit-trail package itself.
36    pub audit_trail: Option<ObjectID>,
37    /// Override for the `tf_components` package used by time locks and capabilities.
38    pub tf_component: Option<ObjectID>,
39}
40
41/// A read-only client for interacting with audit-trail objects on a specific network.
42///
43/// This is the main entry point for applications that only need package resolution and typed read
44/// helpers. Once constructed, use [`Self::trail`] to create lightweight handles scoped to a single
45/// trail object.
46///
47/// For write flows, wrap this client in [`crate::AuditTrailClient`].
48#[derive(Clone)]
49pub struct AuditTrailClientReadOnly {
50    /// The underlying IOTA client adapter used for communication.
51    iota_client: IotaClientAdapter,
52    /// The [`ObjectID`] of the deployed Audit Trail Package (smart contract).
53    audit_trail_pkg_id: ObjectID,
54    /// The [`ObjectID`] of the deployed TfComponents package used by Audit Trail.
55    pub(crate) tf_components_pkg_id: ObjectID,
56    /// The name of the network this client is connected to (e.g., "mainnet", "testnet").
57    network: NetworkName,
58    /// Raw chain identifier returned by the IOTA node.
59    chain_id: String,
60}
61
62impl Deref for AuditTrailClientReadOnly {
63    type Target = IotaClientAdapter;
64    fn deref(&self) -> &Self::Target {
65        &self.iota_client
66    }
67}
68
69impl AuditTrailClientReadOnly {
70    /// Returns the name of the network the client is connected to.
71    pub const fn network(&self) -> &NetworkName {
72        &self.network
73    }
74
75    /// Returns the raw chain identifier for the network this client is connected to.
76    pub fn chain_id(&self) -> &str {
77        &self.chain_id
78    }
79
80    /// Returns the package ID used by this client.
81    ///
82    /// This is the deployed audit-trail Move package ID, not a trail object ID.
83    pub fn package_id(&self) -> ObjectID {
84        self.audit_trail_pkg_id
85    }
86
87    /// Returns the TfComponents package ID used by this client.
88    pub fn tf_components_package_id(&self) -> ObjectID {
89        self.tf_components_pkg_id
90    }
91
92    /// Returns a reference to the underlying IOTA client adapter.
93    pub const fn iota_client(&self) -> &IotaClientAdapter {
94        &self.iota_client
95    }
96
97    /// Returns a typed handle bound to a specific trail object ID.
98    ///
99    /// Creating the handle is cheap. Reads only happen when you call methods on the returned
100    /// [`AuditTrailHandle`], such as [`AuditTrailHandle::get`].
101    pub fn trail<'a>(&'a self, trail_id: ObjectID) -> AuditTrailHandle<'a, Self> {
102        AuditTrailHandle::new(self, trail_id)
103    }
104
105    /// Creates a new read-only client from an IOTA client.
106    ///
107    /// The package IDs are resolved from the internal registry using the connected network name.
108    /// This is the recommended constructor when connecting to official deployments whose package
109    /// history is already tracked by the crate.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the network cannot be resolved or if the package IDs for that network
114    /// cannot be determined.
115    pub async fn new(
116        #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient,
117        #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient,
118    ) -> Result<Self, Error> {
119        let client = IotaClientAdapter::new(iota_client);
120        let network = network_id(&client).await?;
121        Self::new_internal(client, network, PackageOverrides::default()).await
122    }
123
124    async fn new_internal(
125        iota_client: IotaClientAdapter,
126        network: NetworkName,
127        package_overrides: PackageOverrides,
128    ) -> Result<Self, Error> {
129        let chain_id = network.as_ref().to_string();
130        let (network, package_ids) = package::resolve_package_ids(&network, &package_overrides).await?;
131
132        Ok(Self {
133            iota_client,
134            audit_trail_pkg_id: package_ids.audit_trail_package_id,
135            tf_components_pkg_id: package_ids.tf_components_package_id,
136            network,
137            chain_id,
138        })
139    }
140
141    /// Creates a new read-only client with explicit package-ID overrides.
142    ///
143    /// This bypasses the default package-registry lookup for any IDs provided in
144    /// [`PackageOverrides`].
145    ///
146    /// Prefer this constructor when talking to custom deployments, local networks, or preview
147    /// environments whose package IDs are not yet part of the built-in registry.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the network cannot be resolved or if the resulting package-ID
152    /// configuration is invalid.
153    pub async fn new_with_package_overrides(
154        #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient,
155        #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient,
156        package_overrides: PackageOverrides,
157    ) -> Result<Self, Error> {
158        let client = IotaClientAdapter::new(iota_client);
159        let network = network_id(&client).await?;
160        Self::new_internal(client, network, package_overrides).await
161    }
162}
163
164#[cfg_attr(not(feature = "send-sync"), async_trait::async_trait(?Send))]
165#[cfg_attr(feature = "send-sync", async_trait::async_trait)]
166impl CoreClientReadOnly for AuditTrailClientReadOnly {
167    fn package_id(&self) -> ObjectID {
168        self.audit_trail_pkg_id
169    }
170
171    fn tf_components_package_id(&self) -> Option<ObjectID> {
172        Some(self.tf_components_pkg_id)
173    }
174
175    fn network_name(&self) -> &NetworkName {
176        &self.network
177    }
178
179    fn client_adapter(&self) -> &IotaClientAdapter {
180        &self.iota_client
181    }
182}
183
184#[cfg_attr(not(feature = "send-sync"), async_trait::async_trait(?Send))]
185#[cfg_attr(feature = "send-sync", async_trait::async_trait)]
186impl AuditTrailReadOnly for AuditTrailClientReadOnly {
187    /// Executes a programmable transaction through `dev_inspect` and decodes the first return
188    /// value as `T`.
189    ///
190    /// This is primarily used by the typed read-only handle APIs.
191    async fn execute_read_only_transaction<T: DeserializeOwned>(
192        &self,
193        tx: ProgrammableTransaction,
194    ) -> Result<T, Error> {
195        let inspection_result = self
196            .iota_client
197            .read_api()
198            .dev_inspect_transaction_block(IotaAddress::ZERO, TransactionKind::Programmable(tx), None, None, None)
199            .await
200            .map_err(|err| Error::UnexpectedApiResponse(format!("Failed to inspect transaction block: {err}")))?;
201
202        let execution_results = inspection_result
203            .results
204            .ok_or_else(|| Error::UnexpectedApiResponse("DevInspectResults missing 'results' field".to_string()))?;
205
206        let (return_value_bytes, _) = execution_results
207            .first()
208            .ok_or_else(|| Error::UnexpectedApiResponse("Execution results list is empty".to_string()))?
209            .return_values
210            .first()
211            .ok_or_else(|| Error::InvalidArgument("should have at least one return value".to_string()))?;
212
213        let deserialized_output = bcs::from_bytes::<T>(return_value_bytes)?;
214
215        Ok(deserialized_output)
216    }
217}