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