diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..3f2baa11 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[alias] +docs-notarization = "doc -p notarization" +docs-audit-trail = "doc -p audit_trail" diff --git a/README.md b/README.md index 0efb11a3..b4593d6f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@

Introduction ◈ + Where To Start ◈ + ToolkitsDocumentation & ResourcesBindingsContributing @@ -16,41 +18,105 @@ --- -# IOTA Notarization +# IOTA Notarization And Audit Trail ## Introduction -IOTA Notarization enables the creation of immutable, on-chain records for any arbitrary data. This is achieved by storing the data, or a hash of it, inside a dedicated Move object on the IOTA ledger. This process provides a verifiable, timestamped proof of the data's existence and integrity at a specific point in time. +This repository contains two complementary IOTA ledger toolkits for verifiable on-chain data workflows: -IOTA Notarization is composed of two primary components: +- **IOTA Notarization** + Best when you want a proof object for arbitrary data, documents, hashes, or latest-state notarization flows. +- **IOTA Audit Trail** + Best when you want shared audit records with sequential entries, role-based access control, locking, and tagging. -- **Notarization Move Package**: The on-chain smart contracts that define the behavior and structure of notarization objects. -- **Notarization Library (Rust/Wasm)**: A client-side library that provides developers with convenient functions to create, manage, and verify `Notarization` objects on the network. +Each toolkit is available as: -## Documentation and Resources +- a **Move package** for the on-chain contracts +- a **Rust SDK** for typed client access and transaction builders +- **wasm bindings** for JavaScript and TypeScript integrations -- [Notarization Documentation Pages](https://docs.iota.org/developer/iota-notarization): Supplementing documentation with context around notarization and simple examples on library usage. -- API References: - - [Rust API Reference](https://iotaledger.github.io/notarization/notarization/index.html): Package documentation (cargo docs). +## Where To Start - +### I want to notarize data -- Examples: - - [Rust Examples](https://github.com/iotaledger/notarization/tree/main/examples/README.md): Practical code snippets to get you started with the library in Rust. - - [Wasm Examples](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/notarization_wasm/examples/README.md): Practical code snippets to get you started with the library in TypeScript/JavaScript. +Use **IOTA Notarization** when your main need is proving the existence, integrity, or latest state of data on-chain. + +- [Notarization Rust SDK](./notarization-rs) +- [Notarization Move Package](./notarization-move) +- [Notarization Wasm SDK](./bindings/wasm/notarization_wasm) +- [Notarization examples](./bindings/wasm/notarization_wasm/examples/README.md) + +### I want audit records + +Use **IOTA Audit Trail** when you need shared audit records with permissions, capabilities, tagging, and write or delete controls. + +- [Audit Trail Rust SDK](./audit-trail-rs) +- [Audit Trail Move Package](./audit-trail-move) +- [Audit Trail Wasm SDK](./bindings/wasm/audit_trail_wasm) +- [Audit Trail examples](./bindings/wasm/audit_trail_wasm/examples/README.md) + +### I want the on-chain contracts + +- [Notarization Move](./notarization-move) +- [Audit Trail Move](./audit-trail-move) + +### I want application SDKs + +- [Notarization Rust](./notarization-rs) +- [Audit Trail Rust](./audit-trail-rs) +- [Notarization Wasm](./bindings/wasm/notarization_wasm) +- [Audit Trail Wasm](./bindings/wasm/audit_trail_wasm) + +## Toolkits + +| Toolkit | Best for | Move Package | Rust SDK | Wasm SDK | +| ------------ | ------------------------------------------------------------------------ | ------------------------------------------ | -------------------------------------- | -------------------------------------------------------- | +| Notarization | Proof objects for documents, hashes, and updatable notarized state | [`notarization-move`](./notarization-move) | [`notarization-rs`](./notarization-rs) | [`notarization_wasm`](./bindings/wasm/notarization_wasm) | +| Audit Trail | Shared sequential records with roles, capabilities, tagging, and locking | [`audit-trail-move`](./audit-trail-move) | [`audit-trail-rs`](./audit-trail-rs) | [`audit_trail_wasm`](./bindings/wasm/audit_trail_wasm) | + +### Which one should I use? + +| Need | Best fit | +| ------------------------------------------------------------------------- | ------------ | +| Immutable or updatable proof object for arbitrary data | Notarization | +| Simple proof-of-existence or latest-state notarization flow | Notarization | +| Shared sequential records with roles, capabilities, and record tag policy | Audit Trail | +| Team or system audit log with governance and operational controls | Audit Trail | + +## Documentation And Resources + +### IOTA Notarization + +- [Notarization Rust SDK README](./notarization-rs/README.md) +- [Notarization Move Package README](./notarization-move/README.md) +- [Notarization Wasm README](./bindings/wasm/notarization_wasm/README.md) +- [Notarization examples](./bindings/wasm/notarization_wasm/examples/README.md) +- [IOTA Notarization Docs Portal](https://docs.iota.org/developer/iota-notarization) + +### IOTA Audit Trail + +- [Audit Trail Rust SDK README](./audit-trail-rs/README.md) +- [Audit Trail Move Package README](./audit-trail-move/README.md) +- [Audit Trail Wasm README](./bindings/wasm/audit_trail_wasm/README.md) +- [Audit Trail examples](./bindings/wasm/audit_trail_wasm/examples/README.md) + +### Shared + +- [Repository examples](./examples/README.md) ## Bindings -[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface) Bindings of this [Rust](https://www.rust-lang.org/) library to other programming languages: +[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface) bindings available in this repository: -- [Web Assembly](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/notarization_wasm) (JavaScript/TypeScript) +- [Web Assembly for IOTA Notarization](./bindings/wasm/notarization_wasm) +- [Web Assembly for IOTA Audit Trail](./bindings/wasm/audit_trail_wasm) ## Contributing -We would love to have you help us with the development of IOTA Notarization. Each and every contribution is greatly valued! +We would love to have you help us with the development of IOTA Notarization and Audit Trail. Each and every contribution is greatly valued. Please review the [contribution](https://docs.iota.org/developer/iota-notarization/contribute) sections in the [IOTA Docs Portal](https://docs.iota.org/developer/iota-notarization/). -To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included! +To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included. -The best place to get involved in discussions about this library or to look for support at is the `#notarization` channel on the [IOTA Discord](https://discord.gg/iota-builders). You can also ask questions on our [Stack Exchange](https://iota.stackexchange.com/). +The best place to get involved in discussions about these libraries or to look for support at is the `#notarization` channel on the [IOTA Discord](https://discord.gg/iota-builders). You can also ask questions on our [Stack Exchange](https://iota.stackexchange.com/). diff --git a/audit-trail-move/README.md b/audit-trail-move/README.md new file mode 100644 index 00000000..41ab23fd --- /dev/null +++ b/audit-trail-move/README.md @@ -0,0 +1,90 @@ +![banner](https://github.com/iotaledger/notarization/raw/HEAD/.github/banner_notarization.png) + +

+ StackExchange + Discord + Apache 2.0 license +

+ +

+ Introduction ◈ + Modules ◈ + Development & Testing ◈ + Related Libraries ◈ + Contributing +

+ +--- + +# IOTA Audit Trail Move Package + +## Introduction + +`audit-trail-move` is the on-chain Move package behind IOTA Audit Trail. + +It defines the shared `AuditTrail` object and the supporting types needed for: + +- sequential record storage +- role-based access control through capabilities +- trail-wide locking for writes and deletions +- record tags and role tag restrictions +- immutable and updatable trail metadata +- emitted events for trail and record lifecycle changes + +The package depends on `TfComponents` for reusable capability, role-map, and timelock primitives. + +## Modules + +- `audit_trail::main` + Core shared object, events, trail lifecycle, record mutation, metadata updates, roles, and capabilities. +- `audit_trail::record` + Record payloads, initial records, and correction metadata. +- `audit_trail::locking` + Locking configuration and lock evaluation helpers. +- `audit_trail::permission` + Permission constructors and admin permission presets. +- `audit_trail::record_tags` + Tag registry and role tag helpers. + +## Development And Testing + +Build the Move package: + +```bash +cd audit-trail-move +iota move build +``` + +Run the Move test suite: + +```bash +cd audit-trail-move +iota move test +``` + +Publish locally: + +```bash +cd audit-trail-move +./scripts/publish_package.sh +``` + +The publish script prints `IOTA_AUDIT_TRAIL_PKG_ID` and, on `localnet`, also exports `IOTA_TF_COMPONENTS_PKG_ID`. + +The package history files [`Move.lock`](./Move.lock) and [`Move.history.json`](./Move.history.json) are used by the Rust SDK to resolve and track deployed package versions. + +## Related Libraries + +- [Rust SDK](https://github.com/iotaledger/notarization/tree/main/audit-trail-rs/README.md) +- [Wasm SDK](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/audit_trail_wasm/README.md) +- [Repository Root](https://github.com/iotaledger/notarization/tree/main/README.md) + +## Contributing + +We would love to have you help us with the development of IOTA Audit Trail. Each and every contribution is greatly valued. + +Please review the [contribution](https://docs.iota.org/developer/iota-notarization/contribute) sections in the [IOTA Docs Portal](https://docs.iota.org/developer/iota-notarization/). + +To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included. + +The best place to get involved in discussions about this package or to look for support at is the `#notarization` channel on the [IOTA Discord](https://discord.gg/iota-builders). You can also ask questions on our [Stack Exchange](https://iota.stackexchange.com/). diff --git a/audit-trail-rs/README.md b/audit-trail-rs/README.md index 97a329c2..f02ca754 100644 --- a/audit-trail-rs/README.md +++ b/audit-trail-rs/README.md @@ -1 +1,61 @@ -# IOTA Audit Trail (WIP) +![banner](https://github.com/iotaledger/notarization/raw/HEAD/.github/banner_notarization.png) + +

+ StackExchange + Discord + Apache 2.0 license +

+ +

+ Introduction ◈ + Documentation & Resources ◈ + Bindings ◈ + Contributing +

+ +--- + +# IOTA Audit Trail Rust SDK + +## Introduction + +`audit_trail` is the Rust SDK for reading and writing audit trails on the IOTA ledger. + +An audit trail is a shared on-chain object that stores a sequential series of records together with: + +- role-based access control backed by capabilities +- trail-level locking rules for writes and deletions +- tag registries for record categorization +- immutable creation metadata and optional updatable metadata + +The crate provides: + +- read-only and signing client wrappers for the on-chain audit-trail package +- typed trail handles for records, locking, access control, and tags +- serializable Rust representations of on-chain objects and emitted events +- transaction builders that integrate with the shared `product_common` transaction flow + +## Documentation And Resources + +- [Audit Trail Move Package](https://github.com/iotaledger/notarization/tree/main/audit-trail-move): On-chain contract package that defines the shared object model, permissions, locking, and events. +- [Wasm SDK](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/audit_trail_wasm): JavaScript and TypeScript bindings for browser and Node.js integrations. +- [Wasm Examples](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/audit_trail_wasm/examples/README.md): Runnable audit-trail examples for JS and TS consumers. +- [Repository Examples](https://github.com/iotaledger/notarization/tree/main/examples/README.md): End-to-end examples across the broader repository. + +This README is also used as the crate-level rustdoc entry point, while the source files provide detailed API documentation for all public types and methods. + +## Bindings + +[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface) bindings of this Rust SDK to other programming languages: + +- [Web Assembly](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/audit_trail_wasm) (JavaScript/TypeScript) + +## Contributing + +We would love to have you help us with the development of IOTA Audit Trail. Each and every contribution is greatly valued. + +Please review the [contribution](https://docs.iota.org/developer/iota-notarization/contribute) sections in the [IOTA Docs Portal](https://docs.iota.org/developer/iota-notarization/). + +To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included. + +The best place to get involved in discussions about this library or to look for support at is the `#notarization` channel on the [IOTA Discord](https://discord.gg/iota-builders). You can also ask questions on our [Stack Exchange](https://iota.stackexchange.com/). diff --git a/audit-trail-rs/src/client/full_client.rs b/audit-trail-rs/src/client/full_client.rs index cf8f1d2d..3c77523c 100644 --- a/audit-trail-rs/src/client/full_client.rs +++ b/audit-trail-rs/src/client/full_client.rs @@ -1,9 +1,81 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! A full client wrapper for audit trail interactions. +//! # Audit Trail Client //! -//! This client includes signing capabilities for executing transactions. +//! The full client extends [`AuditTrailClientReadOnly`] with signing support and write +//! transaction builders. +//! +//! ## Transaction Flow +//! +//! Write APIs return a [`TransactionBuilder`](product_common::transaction::transaction_builder::TransactionBuilder) +//! that you can configure before signing and submitting: +//! +//! ```rust,no_run +//! # use audit_trail::AuditTrailClient; +//! # use audit_trail::core::types::Data; +//! # async fn example( +//! # client: &AuditTrailClient< +//! # impl secret_storage::Signer + iota_interaction::OptionalSync, +//! # >, +//! # ) -> Result<(), Box> { +//! let created = client +//! .create_trail() +//! .with_initial_record_parts(Data::text("Initial record"), None, None) +//! .finish() +//! .with_gas_budget(1_000_000) +//! .build_and_execute(client) +//! .await?; +//! +//! let trail_id = created.output.trail_id; +//! +//! client +//! .trail(trail_id) +//! .records() +//! .add(Data::text("Follow-up record"), None, None) +//! .build_and_execute(client) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Example Workflow +//! +//! ```rust,no_run +//! # use audit_trail::AuditTrailClient; +//! # use audit_trail::core::types::{Data, PermissionSet, RoleTags}; +//! # async fn example( +//! # client: &AuditTrailClient< +//! # impl secret_storage::Signer + iota_interaction::OptionalSync, +//! # >, +//! # ) -> Result<(), Box> { +//! let created = client +//! .create_trail() +//! .with_initial_record_parts(Data::text("Initial record"), None, None) +//! .with_record_tags(["finance"]) +//! .finish() +//! .build_and_execute(client) +//! .await?; +//! +//! let trail_id = created.output.trail_id; +//! +//! client +//! .trail(trail_id) +//! .access() +//! .for_role("TaggedWriter") +//! .create(PermissionSet::record_admin_permissions(), Some(RoleTags::new(["finance"]))) +//! .build_and_execute(client) +//! .await?; +//! +//! client +//! .trail(trail_id) +//! .records() +//! .add(Data::text("Budget approved"), None, Some("finance".to_string())) +//! .build_and_execute(client) +//! .await?; +//! # Ok(()) +//! # } +//! ``` use std::ops::Deref; @@ -32,7 +104,7 @@ use crate::iota_interaction_adapter::IotaClientAdapter; #[non_exhaustive] pub struct NoSigner; -/// The error that results from a failed attempt at creating an [AuditTrailClient] +/// The error that results from a failed attempt at creating an [`AuditTrailClient`] /// from a given [IotaClient]. #[derive(Debug, thiserror::Error)] #[error("failed to create an 'AuditTrailClient' from the given 'IotaClient'")] @@ -43,23 +115,33 @@ pub struct FromIotaClientError { pub kind: FromIotaClientErrorKind, } -/// Types of failure for [FromIotaClientError]. +/// Categories of failure for [`FromIotaClientError`]. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum FromIotaClientErrorKind { /// A package ID is required, but was not supplied. - #[error("an IOTA Identity package ID must be supplied when connecting to an unofficial IOTA network")] + #[error("an audit-trail package ID must be supplied when connecting to an unofficial IOTA network")] MissingPackageId, /// Network ID resolution through an RPC call failed. #[error("failed to resolve the network the given client is connected to")] NetworkResolution(#[source] Box), } -/// A full client that wraps the read-only client and hosts write operations. +/// A client for creating and managing audit trails on the IOTA blockchain. +/// +/// This client combines read-only capabilities with transaction signing, +/// enabling full interaction with audit trails. +/// +/// ## Type Parameter +/// +/// - `S`: The signer type that implements [`Signer`] #[derive(Clone)] pub struct AuditTrailClient { + /// The underlying read-only client used for executing read-only operations. pub(super) read_client: AuditTrailClientReadOnly, + /// The public key associated with the signer, if any. pub(super) public_key: Option, + /// The signer used for signing transactions, or `NoSigner` if the client is read-only. pub(super) signer: S, } @@ -71,15 +153,16 @@ impl Deref for AuditTrailClient { } impl AuditTrailClient { - /// Creates a new [AuditTrailClient], with **no** signing capabilities, from the given [IotaClient]. + /// Creates a new client with no signing capabilities from an IOTA client. /// /// # Warning - /// Passing `package_overrides` is **only** required when connecting to a custom IOTA network - /// or when testing against explicitly deployed package pairs. /// - /// Relying on a custom Audit Trail package when connected to an official IOTA network is **highly - /// discouraged** and is sure to result in compatibility issues when interacting with other official - /// IOTA Trust Framework's products. + /// Passing `package_overrides` is only needed when connecting to a custom IOTA network or + /// when testing against explicitly deployed package pairs. + /// + /// Relying on a custom audit-trail package while connected to an official IOTA network is + /// strongly discouraged and can lead to compatibility problems with other official IOTA Trust + /// Framework products. /// /// # Examples /// ```rust,ignore @@ -122,7 +205,11 @@ impl AuditTrailClient { } impl AuditTrailClient { - /// Creates a new client with signing capabilities from an existing read-only client. + /// Creates a signing client from an existing read-only client and signer. + /// + /// # Errors + /// + /// Returns an error if the signer public key cannot be loaded. pub async fn new(client: AuditTrailClientReadOnly, signer: S) -> Result where S: Signer, @@ -139,7 +226,11 @@ impl AuditTrailClient { }) } - /// Sets a new signer for this client. + /// Replaces the signer used by this client. + /// + /// # Errors + /// + /// Returns an error if the replacement signer public key cannot be loaded. pub async fn with_signer(self, signer: NewS) -> Result, secret_storage::Error> where NewS: Signer, @@ -152,10 +243,12 @@ impl AuditTrailClient { signer, }) } + /// Returns the underlying read-only client view. pub fn read_only(&self) -> &AuditTrailClientReadOnly { &self.read_client } + /// Returns a typed handle bound to a specific trail object ID. pub fn trail<'a>(&'a self, trail_id: ObjectID) -> AuditTrailHandle<'a, Self> { AuditTrailHandle::new(self, trail_id) } @@ -165,17 +258,16 @@ impl AuditTrailClient { self.read_client.tf_components_package_id() } - /// Creates a builder for an audit trail. + /// Creates a builder for a new audit trail. + /// + /// When the client has a signer, the builder is pre-populated with that signer's address as + /// the initial admin. pub fn create_trail(&self) -> AuditTrailBuilder { AuditTrailBuilder { admin: self.public_key.as_ref().map(IotaAddress::from), ..AuditTrailBuilder::default() } } - - pub async fn delete_trail(&self, _trail_id: ObjectID) -> Result<(), Error> { - Err(Error::NotImplemented("AuditTrailClient::delete_trail")) - } } impl AuditTrailClient @@ -239,6 +331,7 @@ impl AuditTrailReadOnly for AuditTrailClient where S: Signer + OptionalSync, { + /// Delegates read-only execution to the wrapped [`AuditTrailClientReadOnly`]. async fn execute_read_only_transaction( &self, tx: ProgrammableTransaction, diff --git a/audit-trail-rs/src/client/mod.rs b/audit-trail-rs/src/client/mod.rs index 79c06024..feef288f 100644 --- a/audit-trail-rs/src/client/mod.rs +++ b/audit-trail-rs/src/client/mod.rs @@ -1,7 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Client implementations for interacting with audit trails on the IOTA blockchain. +//! Client implementations for interacting with audit trails on the IOTA ledger. +//! +//! [`AuditTrailClientReadOnly`] is the entry point for read-only inspection and typed trail handles. +//! [`AuditTrailClient`] wraps a read-only client together with a signer so it can build write +//! transactions through the shared transaction infrastructure. use iota_interaction::IotaClientTrait; use product_common::network_name::NetworkName; @@ -9,13 +13,15 @@ use product_common::network_name::NetworkName; use crate::error::Error; use crate::iota_interaction_adapter::IotaClientAdapter; +/// A signing client that can create audit-trail transaction builders. pub mod full_client; +/// A read-only client that resolves package IDs and executes inspected calls. pub mod read_only; pub use full_client::*; pub use read_only::*; -/// Returns the network-id also known as chain-identifier provided by the specified iota_client +/// Resolves the network name reported by the given IOTA client. async fn network_id(iota_client: &IotaClientAdapter) -> Result { let network_id = iota_client .read_api() diff --git a/audit-trail-rs/src/client/read_only.rs b/audit-trail-rs/src/client/read_only.rs index 46c8ab0f..3b765a4d 100644 --- a/audit-trail-rs/src/client/read_only.rs +++ b/audit-trail-rs/src/client/read_only.rs @@ -1,7 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! A read-only client for interacting with IOTA Audit Trail module objects. +//! Read-only client support for audit-trail interactions. +//! +//! [`AuditTrailClientReadOnly`] resolves the deployed package IDs for the connected network, exposes +//! typed trail handles, and provides the internal read-only execution primitive used by the handle +//! APIs. use std::ops::Deref; @@ -22,14 +26,25 @@ use crate::error::Error; use crate::iota_interaction_adapter::IotaClientAdapter; use crate::package; -/// Optional package ID overrides used when constructing an audit trail client. +/// Explicit package-ID overrides used when constructing an audit-trail client. +/// +/// Use this when talking to custom deployments, local test networks, or any environment where the +/// package registry does not yet know the relevant package IDs. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct PackageOverrides { - pub audit_trail_package_id: Option, - pub tf_components_package_id: Option, + /// Override for the audit-trail package itself. + pub audit_trail: Option, + /// Override for the `tf_components` package used by time locks and capabilities. + pub tf_component: Option, } -/// A read-only client for interacting with audit trail module objects on a specific network. +/// A read-only client for interacting with audit-trail objects on a specific network. +/// +/// This is the main entry point for applications that only need package resolution and typed read +/// helpers. Once constructed, use [`Self::trail`] to create lightweight handles scoped to a single +/// trail object. +/// +/// For write flows, wrap this client in [`crate::AuditTrailClient`]. #[derive(Clone)] pub struct AuditTrailClientReadOnly { /// The underlying IOTA client adapter used for communication. @@ -63,6 +78,8 @@ impl AuditTrailClientReadOnly { } /// Returns the package ID used by this client. + /// + /// This is the deployed audit-trail Move package ID, not a trail object ID. pub fn package_id(&self) -> ObjectID { self.audit_trail_pkg_id } @@ -77,14 +94,24 @@ impl AuditTrailClientReadOnly { &self.iota_client } - /// Returns a typed handle bound to a trail id. + /// Returns a typed handle bound to a specific trail object ID. + /// + /// Creating the handle is cheap. Reads only happen when you call methods on the returned + /// [`AuditTrailHandle`], such as [`AuditTrailHandle::get`]. pub fn trail<'a>(&'a self, trail_id: ObjectID) -> AuditTrailHandle<'a, Self> { AuditTrailHandle::new(self, trail_id) } - /// Attempts to create a new [`AuditTrailClientReadOnly`] from a given IOTA client. + /// Creates a new read-only client from an IOTA client. + /// + /// The package IDs are resolved from the internal registry using the connected network name. + /// This is the recommended constructor when connecting to official deployments whose package + /// history is already tracked by the crate. + /// + /// # Errors /// - /// This resolves the package ID from the internal registry based on the network. + /// Returns an error if the network cannot be resolved or if the package IDs for that network + /// cannot be determined. pub async fn new( #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient, #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient, @@ -111,11 +138,18 @@ impl AuditTrailClientReadOnly { }) } - /// Creates a new [`AuditTrailClientReadOnly`] with explicit package overrides. + /// Creates a new read-only client with explicit package-ID overrides. /// - /// This function allows overriding the package ID lookup from the registry, - /// which is useful for local testing or custom deployments where the package - /// IDs are known ahead of time. + /// This bypasses the default package-registry lookup for any IDs provided in + /// [`PackageOverrides`]. + /// + /// Prefer this constructor when talking to custom deployments, local networks, or preview + /// environments whose package IDs are not yet part of the built-in registry. + /// + /// # Errors + /// + /// Returns an error if the network cannot be resolved or if the resulting package-ID + /// configuration is invalid. pub async fn new_with_package_overrides( #[cfg(target_arch = "wasm32")] iota_client: WasmIotaClient, #[cfg(not(target_arch = "wasm32"))] iota_client: IotaClient, @@ -150,6 +184,10 @@ impl CoreClientReadOnly for AuditTrailClientReadOnly { #[cfg_attr(not(feature = "send-sync"), async_trait::async_trait(?Send))] #[cfg_attr(feature = "send-sync", async_trait::async_trait)] impl AuditTrailReadOnly for AuditTrailClientReadOnly { + /// Executes a programmable transaction through `dev_inspect` and decodes the first return + /// value as `T`. + /// + /// This is primarily used by the typed read-only handle APIs. async fn execute_read_only_transaction( &self, tx: ProgrammableTransaction, diff --git a/audit-trail-rs/src/core/access/mod.rs b/audit-trail-rs/src/core/access/mod.rs index b2ef3b81..7f33fe55 100644 --- a/audit-trail-rs/src/core/access/mod.rs +++ b/audit-trail-rs/src/core/access/mod.rs @@ -1,6 +1,15 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Role and capability management APIs for audit trails. +//! +//! This module is the Rust-facing wrapper around the access-control state integrated into each audit trail. +//! Roles grant [`PermissionSet`] values, while capability objects bind one role to one trail and may add +//! optional address or time restrictions. +//! +//! Additional record-tag constraints are represented as [`RoleTags`]. They narrow which tagged records a role +//! may operate on, but they do not replace the underlying permission checks enforced by the Move package. + use iota_interaction::types::base_types::ObjectID; use iota_interaction::{IotaKeySignature, OptionalSync}; use product_common::core_client::CoreClient; @@ -18,6 +27,10 @@ pub use transactions::{ IssueCapability, RevokeCapability, RevokeInitialAdminCapability, UpdateRole, }; +/// Access-control API scoped to a specific trail. +/// +/// This handle exposes role-management and capability-management operations for one trail. All authorization is +/// still enforced against the capability supplied during transaction construction. #[derive(Debug, Clone)] pub struct TrailAccess<'a, C> { pub(crate) client: &'a C, @@ -40,15 +53,18 @@ impl<'a, C> TrailAccess<'a, C> { self } - /// Returns a handle bound to a specific role name. + /// Returns a role-scoped handle for the given role name. + /// + /// The returned handle only identifies the role. Existence and authorization are checked when the + /// resulting transaction is built and executed. pub fn for_role(&self, name: impl Into) -> RoleHandle<'a, C> { RoleHandle::new(self.client, self.trail_id, name.into(), self.selected_capability_id) } /// Revokes an issued capability. /// - /// Pass the capability's `valid_until` value when it is known so the denylist entry matches the on-chain cleanup - /// model. + /// Revocation adds the capability ID to the trail's denylist. Pass the capability's `valid_until` value + /// when it is known so later cleanup keeps the same expiry semantics. pub fn revoke_capability( &self, capability_id: ObjectID, @@ -69,6 +85,9 @@ impl<'a, C> TrailAccess<'a, C> { } /// Destroys a capability object. + /// + /// This consumes the owned capability object itself. It uses the generic capability-destruction path and + /// therefore must not be used for initial-admin capabilities. pub fn destroy_capability(&self, capability_id: ObjectID) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -83,7 +102,10 @@ impl<'a, C> TrailAccess<'a, C> { )) } - /// Destroys an initial admin capability (self-service, no auth cap required). + /// Destroys an initial-admin capability without presenting another authorization capability. + /// + /// Initial-admin capability IDs are tracked separately, so they cannot be removed through the generic + /// destroy path. pub fn destroy_initial_admin_capability( &self, capability_id: ObjectID, @@ -95,10 +117,10 @@ impl<'a, C> TrailAccess<'a, C> { TransactionBuilder::new(DestroyInitialAdminCapability::new(self.trail_id, capability_id)) } - /// Revokes an initial admin capability by ID. + /// Revokes an initial-admin capability by ID. /// - /// Pass the capability's `valid_until` value when it is known so the denylist entry matches the on-chain cleanup - /// model. + /// Like [`TrailAccess::revoke_capability`], this writes to the denylist. The dedicated entry point exists + /// because initial-admin capability IDs are protected separately. pub fn revoke_initial_admin_capability( &self, capability_id: ObjectID, @@ -119,6 +141,9 @@ impl<'a, C> TrailAccess<'a, C> { } /// Removes expired entries from the revoked-capability denylist. + /// + /// Only entries whose stored expiry has passed are removed. Revocations without an expiry remain until + /// they are explicitly destroyed or the trail is deleted. pub fn cleanup_revoked_capabilities(&self) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -133,6 +158,10 @@ impl<'a, C> TrailAccess<'a, C> { } } +/// Role-scoped access-control API. +/// +/// A `RoleHandle` identifies one role name inside the trail's access-control state and builds transactions that +/// act on that role. #[derive(Debug, Clone)] pub struct RoleHandle<'a, C> { pub(crate) client: &'a C, @@ -162,11 +191,17 @@ impl<'a, C> RoleHandle<'a, C> { self } + /// Returns the role name represented by this handle. pub fn name(&self) -> &str { &self.name } - /// Creates this role with the provided permissions and optional role-tag access rules. + /// Creates this role with the provided permissions and optional role-tag + /// access rules. + /// + /// Any supplied [`RoleTags`] must already exist in the trail-owned tag + /// registry. The tag list is stored as + /// role data on the Move side and is later used for tag-aware record authorization. pub fn create(&self, permissions: PermissionSet, role_tags: Option) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -184,6 +219,12 @@ impl<'a, C> RoleHandle<'a, C> { } /// Issues a capability for this role using optional restrictions. + /// + /// The resulting capability always targets this trail and grants exactly + /// this role. `issued_to`, + /// `valid_from_ms`, and `valid_until_ms` only configure restrictions on + /// the issued object; enforcement + /// happens on-chain when the capability is later used. pub fn issue_capability(&self, options: CapabilityIssueOptions) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -200,6 +241,9 @@ impl<'a, C> RoleHandle<'a, C> { } /// Updates permissions and role-tag access rules for this role. + /// + /// As with [`RoleHandle::create`], any supplied [`RoleTags`] must already + /// exist in the trail tag registry. pub fn update_permissions( &self, permissions: PermissionSet, @@ -221,6 +265,8 @@ impl<'a, C> RoleHandle<'a, C> { } /// Deletes this role. + /// + /// The reserved initial-admin role cannot be deleted. pub fn delete(&self) -> TransactionBuilder where C: AuditTrailFull + CoreClient, diff --git a/audit-trail-rs/src/core/access/operations.rs b/audit-trail-rs/src/core/access/operations.rs index 574cc6b3..dba653e2 100644 --- a/audit-trail-rs/src/core/access/operations.rs +++ b/audit-trail-rs/src/core/access/operations.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal access-control helpers that build role and capability transactions. +//! +//! These helpers encode Rust-side access inputs into the exact Move call shapes expected by the audit-trail +//! package and apply the lightweight preflight checks that are cheaper to surface before submission. + use iota_interaction::types::base_types::{IotaAddress, ObjectID}; use iota_interaction::types::transaction::{ObjectArg, ProgrammableTransaction}; use iota_interaction::{OptionalSync, ident_str}; @@ -10,9 +15,19 @@ use crate::core::internal::{trail as trail_reader, tx}; use crate::core::types::{CapabilityIssueOptions, Permission, PermissionSet, RoleTags}; use crate::error::Error; +/// Internal namespace for role and capability transaction construction. +/// +/// Each helper selects the required authorization permission, prepares +/// Move-compatible arguments, and then +/// delegates to the shared trail transaction builders in [`crate::core::internal::tx`]. pub(super) struct AccessOps; impl AccessOps { + /// Builds the `create_role` call. + /// + /// `role_tags`, when present, are validated against the trail tag registry + /// before PTB construction so the + /// Rust side fails early with `Error::InvalidArgument` instead of relying on a later Move abort. pub(super) async fn create_role( client: &C, trail_id: ObjectID, @@ -62,6 +77,10 @@ impl AccessOps { .await } + /// Builds the `update_role_permissions` call. + /// + /// The same tag-registry precondition as [`AccessOps::create_role`] applies because role-tag data is stored + /// on-chain as part of the role definition. pub(super) async fn update_role( client: &C, trail_id: ObjectID, @@ -112,6 +131,10 @@ impl AccessOps { .await } + /// Builds the `delete_role` call. + /// + /// The PTB only carries the role name and clock reference. Protection of the initial-admin role remains an + /// access-control invariant enforced by the Move package. pub(super) async fn delete_role( client: &C, trail_id: ObjectID, @@ -139,6 +162,10 @@ impl AccessOps { .await } + /// Builds the `new_capability` call for a role. + /// + /// Optional restrictions are serialized exactly as provided. Validation of `issued_to`, `valid_from`, and + /// `valid_until` semantics remains on-chain. pub(super) async fn issue_capability( client: &C, trail_id: ObjectID, @@ -170,6 +197,10 @@ impl AccessOps { .await } + /// Builds the generic `revoke_capability` call. + /// + /// `capability_valid_until` is forwarded to the Move layer so the denylist can later be cleaned up without + /// losing the capability's original expiry boundary. pub(super) async fn revoke_capability( client: &C, trail_id: ObjectID, @@ -199,6 +230,10 @@ impl AccessOps { .await } + /// Builds the generic `destroy_capability` call. + /// + /// This resolves the capability object reference up front because the Move entry point consumes the owned + /// capability object rather than only its ID. pub(super) async fn destroy_capability( client: &C, trail_id: ObjectID, @@ -230,6 +265,10 @@ impl AccessOps { .await } + /// Builds the dedicated `destroy_initial_admin_capability` call. + /// + /// Initial-admin capability IDs are tracked separately, so they cannot be destroyed through the generic + /// capability path. pub(super) async fn destroy_initial_admin_capability( client: &C, trail_id: ObjectID, @@ -249,6 +288,10 @@ impl AccessOps { .await } + /// Builds the dedicated `revoke_initial_admin_capability` call. + /// + /// This keeps the same denylist-expiry behavior as [`AccessOps::revoke_capability`] while using the + /// separate Move entry point reserved for tracked initial-admin IDs. pub(super) async fn revoke_initial_admin_capability( client: &C, trail_id: ObjectID, @@ -278,6 +321,10 @@ impl AccessOps { .await } + /// Builds the `cleanup_revoked_capabilities` call. + /// + /// Cleanup only prunes denylist entries whose stored expiry has elapsed. It does not change capability + /// objects and does not revoke any additional IDs. pub(super) async fn cleanup_revoked_capabilities( client: &C, trail_id: ObjectID, @@ -303,6 +350,10 @@ impl AccessOps { } } +/// Verifies that every requested role tag already exists in the trail tag registry. +/// +/// Roles may only reference tags that are defined on the trail itself so later record-tag checks +/// stay consistent with the registry stored on-chain. async fn assert_role_tags_defined(client: &C, trail_id: ObjectID, role_tags: &Option) -> Result<(), Error> where C: CoreClientReadOnly + OptionalSync, diff --git a/audit-trail-rs/src/core/access/transactions.rs b/audit-trail-rs/src/core/access/transactions.rs index 1d7dee43..7380dd4f 100644 --- a/audit-trail-rs/src/core/access/transactions.rs +++ b/audit-trail-rs/src/core/access/transactions.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Transaction payloads for audit-trail role and capability administration. +//! +//! These types cache the generated programmable transaction, delegate PTB construction to +//! [`super::operations::AccessOps`], and decode the matching Move events into typed Rust outputs. + use async_trait::async_trait; use iota_interaction::OptionalSync; use iota_interaction::rpc_types::{IotaTransactionBlockEffects, IotaTransactionBlockEvents}; @@ -19,6 +24,10 @@ use crate::error::Error; // ===== CreateRole ===== +/// Transaction that creates a role on a trail. +/// +/// This maps to the audit-trail `create_role` Move entry point and therefore requires an authorization +/// capability with `AddRoles`. #[derive(Debug, Clone)] pub struct CreateRole { trail_id: ObjectID, @@ -31,6 +40,9 @@ pub struct CreateRole { } impl CreateRole { + /// Creates a `CreateRole` transaction builder payload. + /// + /// `role_tags`, when present, are serialized as Move `record_tags::RoleTags` role data. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -106,6 +118,10 @@ impl Transaction for CreateRole { } } +/// Transaction that updates an existing role. +/// +/// This updates both the permission set and the optional role-tag data stored for the role. The entry point +/// requires `UpdateRoles`. #[derive(Debug, Clone)] pub struct UpdateRole { trail_id: ObjectID, @@ -118,6 +134,7 @@ pub struct UpdateRole { } impl UpdateRole { + /// Creates an `UpdateRole` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -193,6 +210,9 @@ impl Transaction for UpdateRole { } } +/// Transaction that deletes a role. +/// +/// The reserved initial-admin role cannot be deleted even if the caller holds `DeleteRoles`. #[derive(Debug, Clone)] pub struct DeleteRole { trail_id: ObjectID, @@ -203,6 +223,7 @@ pub struct DeleteRole { } impl DeleteRole { + /// Creates a `DeleteRole` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, name: String, selected_capability_id: Option) -> Self { Self { trail_id, @@ -267,6 +288,10 @@ impl Transaction for DeleteRole { } } +/// Transaction that issues a capability for a role. +/// +/// This mints a new capability object for `role` against `trail_id`. Optional issuance restrictions are +/// copied into the capability object and later enforced on-chain. #[derive(Debug, Clone)] pub struct IssueCapability { trail_id: ObjectID, @@ -278,6 +303,7 @@ pub struct IssueCapability { } impl IssueCapability { + /// Creates an `IssueCapability` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -350,6 +376,10 @@ impl Transaction for IssueCapability { } } +/// Transaction that revokes a capability. +/// +/// Revocation writes the capability ID into the trail's revoked-capability denylist. Supplying +/// `capability_valid_until` preserves the same expiry boundary later used by denylist cleanup. #[derive(Debug, Clone)] pub struct RevokeCapability { trail_id: ObjectID, @@ -361,6 +391,7 @@ pub struct RevokeCapability { } impl RevokeCapability { + /// Creates a `RevokeCapability` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -433,6 +464,10 @@ impl Transaction for RevokeCapability { } } +/// Transaction that destroys a capability object. +/// +/// This path is for ordinary capabilities. Initial-admin capabilities must use +/// [`DestroyInitialAdminCapability`] instead. #[derive(Debug, Clone)] pub struct DestroyCapability { trail_id: ObjectID, @@ -443,6 +478,7 @@ pub struct DestroyCapability { } impl DestroyCapability { + /// Creates a `DestroyCapability` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -514,6 +550,9 @@ impl Transaction for DestroyCapability { // ===== DestroyInitialAdminCapability ===== +/// Transaction that destroys an initial-admin capability without an auth capability. +/// +/// Initial-admin capability IDs are tracked separately and cannot be removed through the generic destroy path. #[derive(Debug, Clone)] pub struct DestroyInitialAdminCapability { trail_id: ObjectID, @@ -522,6 +561,7 @@ pub struct DestroyInitialAdminCapability { } impl DestroyInitialAdminCapability { + /// Creates a `DestroyInitialAdminCapability` transaction builder payload. pub fn new(trail_id: ObjectID, capability_id: ObjectID) -> Self { Self { trail_id, @@ -579,6 +619,9 @@ impl Transaction for DestroyInitialAdminCapability { // ===== RevokeInitialAdminCapability ===== +/// Transaction that revokes an initial-admin capability. +/// +/// This is the dedicated revoke path for capability IDs recognized as active initial-admin capabilities. #[derive(Debug, Clone)] pub struct RevokeInitialAdminCapability { trail_id: ObjectID, @@ -590,6 +633,7 @@ pub struct RevokeInitialAdminCapability { } impl RevokeInitialAdminCapability { + /// Creates a `RevokeInitialAdminCapability` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -662,6 +706,10 @@ impl Transaction for RevokeInitialAdminCapability { } } +/// Transaction that cleans up expired revoked-capability entries. +/// +/// This does not revoke additional capabilities. It only prunes denylist entries whose stored expiry has +/// already elapsed. #[derive(Debug, Clone)] pub struct CleanupRevokedCapabilities { trail_id: ObjectID, @@ -671,6 +719,7 @@ pub struct CleanupRevokedCapabilities { } impl CleanupRevokedCapabilities { + /// Creates a `CleanupRevokedCapabilities` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, selected_capability_id: Option) -> Self { Self { trail_id, diff --git a/audit-trail-rs/src/core/builder.rs b/audit-trail-rs/src/core/builder.rs index f143c176..3194b0fe 100644 --- a/audit-trail-rs/src/core/builder.rs +++ b/audit-trail-rs/src/core/builder.rs @@ -1,7 +1,7 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Audit trail builder for creation transactions. +//! Builder for trail-creation transactions. use std::collections::HashSet; @@ -12,18 +12,30 @@ use super::types::{Data, ImmutableMetadata, InitialRecord, LockingConfig}; use crate::core::create::CreateTrail; /// Builder for creating an audit trail. +/// +/// The builder collects the full create-time configuration before it is normalized into the Move `create` +/// call. Any tag list configured here becomes the trail-owned registry that later role-tag and record-tag +/// checks refer to. #[derive(Debug, Clone, Default)] pub struct AuditTrailBuilder { + /// Initial admin address that should receive the initial admin capability. pub admin: Option, + /// Optional initial record created together with the trail. pub initial_record: Option, + /// Locking rules to apply at creation time. pub locking_config: LockingConfig, + /// Immutable metadata stored once at creation time. pub trail_metadata: Option, + /// Mutable metadata stored on the trail object. pub updatable_metadata: Option, + /// Canonical list of record tags owned by the trail. pub record_tags: HashSet, } impl AuditTrailBuilder { /// Sets the full initial record input used during trail creation. + /// + /// When present, the initial record is created as sequence number `0`. pub fn with_initial_record(mut self, initial_record: InitialRecord) -> Self { self.initial_record = Some(initial_record); self @@ -41,12 +53,16 @@ impl AuditTrailBuilder { } /// Sets the locking configuration for the trail. + /// + /// This replaces the entire create-time locking configuration. pub fn with_locking_config(mut self, config: LockingConfig) -> Self { self.locking_config = config; self } /// Sets immutable metadata for the trail. + /// + /// Immutable metadata is stored once during creation and cannot be updated later. pub fn with_trail_metadata(mut self, metadata: ImmutableMetadata) -> Self { self.trail_metadata = Some(metadata); self @@ -62,12 +78,16 @@ impl AuditTrailBuilder { } /// Sets updatable metadata for the trail. + /// + /// This seeds the mutable metadata field that later `update_metadata` calls can replace or clear. pub fn with_updatable_metadata(mut self, metadata: impl Into) -> Self { self.updatable_metadata = Some(metadata.into()); self } /// Sets the canonical list of tags that may be used on records in this trail. + /// + /// The list is deduplicated into the trail-owned tag registry during creation. pub fn with_record_tags(mut self, tags: I) -> Self where I: IntoIterator, @@ -77,13 +97,13 @@ impl AuditTrailBuilder { self } - /// Sets the admin address that receives the initial admin capability. + /// Sets the admin address that receives the initial-admin capability. pub fn with_admin(mut self, admin: IotaAddress) -> Self { self.admin = Some(admin); self } - /// Finalizes the builder and creates a transaction builder. + /// Finalizes the builder and creates the trail-creation transaction builder. pub fn finish(self) -> TransactionBuilder { TransactionBuilder::new(CreateTrail::new(self)) } diff --git a/audit-trail-rs/src/core/create/mod.rs b/audit-trail-rs/src/core/create/mod.rs index 7365c88c..7dace9ff 100644 --- a/audit-trail-rs/src/core/create/mod.rs +++ b/audit-trail-rs/src/core/create/mod.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Trail-creation transaction types. + mod operations; mod transactions; diff --git a/audit-trail-rs/src/core/create/operations.rs b/audit-trail-rs/src/core/create/operations.rs index d330e0b1..30132c81 100644 --- a/audit-trail-rs/src/core/create/operations.rs +++ b/audit-trail-rs/src/core/create/operations.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal helpers that turn validated builder state into the trail-creation Move call. + use std::collections::HashSet; use iota_interaction::ident_str; @@ -12,20 +14,36 @@ use crate::core::internal::tx; use crate::core::types::{Data, ImmutableMetadata, InitialRecord, LockingConfig}; use crate::error::Error; +/// Internal namespace for trail-creation transaction construction. pub(super) struct CreateOps; +/// Normalized inputs required to build the `main::create` programmable transaction. +/// +/// This keeps the public builder layer separate from the low-level PTB encoding logic. pub(super) struct CreateTrailArgs { + /// Audit-trail package used for generic type tags and Move calls. pub audit_trail_package_id: ObjectID, + /// TfComponents package used by locking and capability-related values. pub tf_components_package_id: ObjectID, + /// Address that should receive the initial admin capability. pub admin: IotaAddress, + /// Optional first record inserted into the newly created trail. pub initial_record: Option, + /// Initial locking rules for the trail. pub locking_config: LockingConfig, + /// Immutable metadata stored at trail creation time. pub trail_metadata: Option, + /// Mutable metadata slot initialized together with the trail. pub updatable_metadata: Option, + /// Canonical set of record tags that may be used on the trail. pub record_tags: HashSet, } impl CreateOps { + /// Builds the programmable transaction that creates a new audit trail. + /// + /// Record tags are sorted before serialization so the resulting wire format is stable across + /// equivalent `HashSet` inputs. pub(super) fn create_trail(args: CreateTrailArgs) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); let CreateTrailArgs { diff --git a/audit-trail-rs/src/core/create/transactions.rs b/audit-trail-rs/src/core/create/transactions.rs index e13daa8a..551e2bd7 100644 --- a/audit-trail-rs/src/core/create/transactions.rs +++ b/audit-trail-rs/src/core/create/transactions.rs @@ -16,15 +16,23 @@ use crate::core::internal::trail as trail_reader; use crate::core::types::{AuditTrailCreated, Event, OnChainAuditTrail}; use crate::error::Error; -/// Output of a create trail transaction. +/// Output of a successful trail-creation transaction. #[derive(Debug, Clone)] pub struct TrailCreated { + /// Newly created trail object ID. pub trail_id: ObjectID, + /// Address that created the trail. pub creator: IotaAddress, + /// Millisecond timestamp emitted by the creation event. pub timestamp: u64, } impl TrailCreated { + /// Loads the newly created trail object from the ledger. + /// + /// # Errors + /// + /// Returns an error if the trail cannot be fetched or deserialized. pub async fn fetch_audit_trail(&self, client: &C) -> Result where C: CoreClientReadOnly + OptionalSync, @@ -34,6 +42,9 @@ impl TrailCreated { } /// A transaction that creates a new audit trail. +/// +/// The builder state is normalized into the exact Move `create` call shape, including tag-registry setup, +/// optional initial-record creation, and initial-admin capability assignment. #[derive(Debug, Clone)] pub struct CreateTrail { builder: AuditTrailBuilder, diff --git a/audit-trail-rs/src/core/internal/capability.rs b/audit-trail-rs/src/core/internal/capability.rs index 9b721e34..eeb1ca33 100644 --- a/audit-trail-rs/src/core/internal/capability.rs +++ b/audit-trail-rs/src/core/internal/capability.rs @@ -1,6 +1,7 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Capability discovery helpers used by internal transaction builders. use std::collections::HashSet; use iota_interaction::move_types::language_storage::StructTag; @@ -18,14 +19,10 @@ use super::{linked_table, tx}; use crate::core::types::{Capability, OnChainAuditTrail, Permission}; use crate::error::Error; -/// Finds an owned capability that grants `permission` on `trail_id`. +/// Finds an owned capability object that grants `permission` for `trail_id` and returns its object +/// reference. /// -/// This is the standard lookup path used by most trail operations. It derives -/// the set of role names that grant the requested permission from the current -/// on-chain trail state, then delegates the actual owned-object scan to -/// [`find_owned_capability`]. The selected capability is returned as an -/// [`ObjectRef`] because transaction construction needs the live object -/// reference, not just the parsed capability payload. +/// The lookup is restricted to roles on `trail` that include the requested permission. pub(crate) async fn find_capable_cap( client: &C, owner: IotaAddress, @@ -59,16 +56,10 @@ where tx::get_object_ref_by_id(client, &object_id).await } -/// Finds the first owned capability that survives common local filtering. +/// Searches the owner's capability objects and returns the first one matching `predicate`. /// -/// This helper is the generic capability scanner used by the more specific -/// permission-based and tag-aware lookup functions below. It handles: -/// - fetching owned capability objects page by page, -/// - excluding revoked capability IDs recorded on the trail, and -/// - enforcing any `issued_to` address restriction locally. -/// -/// The caller supplies the remaining policy via `predicate`, typically matching -/// the target trail and one or more allowed role names. +/// Revoked capabilities are filtered out before the predicate is applied to the remaining +/// candidates. pub(crate) async fn find_owned_capability( client: &C, owner: IotaAddress, @@ -126,10 +117,10 @@ where Ok(None) } -/// Loads the current revoked-capability denylist from the trail's linked table. +/// Traverses the revoked-capabilities linked table and collects every revoked capability ID. /// -/// The resulting set is used during local capability selection so revoked -/// capabilities are ignored before transaction construction. +/// The traversal validates that the linked-table shape is acyclic and that the number of visited +/// entries matches the size recorded on-chain. async fn revoked_capability_ids(client: &C, trail: &OnChainAuditTrail) -> Result, Error> where C: CoreClientReadOnly + OptionalSync, @@ -169,13 +160,10 @@ where Ok(keys) } -/// Applies the shared local capability filters. +/// Returns whether a capability is a usable match for the current owner and predicate. /// -/// A capability is considered usable locally when: -/// - the caller-specific predicate matches, -/// - the capability ID is not present in the trail's revoked-capability set, and -/// - any `issued_to` restriction matches the current owner address, and -/// - the current local time falls within the capability's validity window. +/// A capability only matches when it satisfies the caller-provided predicate, has not been +/// revoked, and is either unbound or explicitly issued to `owner`. fn capability_matches

( cap: &Capability, owner: IotaAddress, @@ -272,8 +260,8 @@ mod tests { let valid_roles = HashSet::from(["Writer".to_string()]); let revoked_ids = HashSet::from([revoked_cap_id]); - let revoked_cap = make_capability(revoked_cap_id, trail_id, "Writer", None); - let valid_cap = make_capability(valid_cap_id, trail_id, "Writer", None); + let revoked_cap = make_capability(revoked_cap_id, trail_id, "Writer", None, None, None); + let valid_cap = make_capability(valid_cap_id, trail_id, "Writer", None, None, None); assert!(!capability_matches(&revoked_cap, owner, 0, &revoked_ids, &|cap| cap .matches_target_and_role(trail_id, &valid_roles))); @@ -287,7 +275,7 @@ mod tests { let other_owner = IotaAddress::random_for_testing_only(); let trail_id = dbg_object_id(4); let valid_roles = HashSet::from(["Writer".to_string()]); - let cap = make_capability(dbg_object_id(5), trail_id, "Writer", Some(other_owner)); + let cap = make_capability(dbg_object_id(5), trail_id, "Writer", Some(other_owner), None, None); assert!(!capability_matches(&cap, owner, 0, &HashSet::new(), &|candidate| { candidate.matches_target_and_role(trail_id, &valid_roles) @@ -299,8 +287,7 @@ mod tests { let owner = IotaAddress::random_for_testing_only(); let trail_id = dbg_object_id(6); let valid_roles = HashSet::from(["Writer".to_string()]); - let mut cap = make_capability(dbg_object_id(7), trail_id, "Writer", None); - cap.valid_from = Some(2_000); + let cap = make_capability(dbg_object_id(7), trail_id, "Writer", None, Some(2_000), None); assert!(!capability_matches(&cap, owner, 1_999, &HashSet::new(), &|candidate| { candidate.matches_target_and_role(trail_id, &valid_roles) @@ -315,8 +302,7 @@ mod tests { let owner = IotaAddress::random_for_testing_only(); let trail_id = dbg_object_id(8); let valid_roles = HashSet::from(["Writer".to_string()]); - let mut cap = make_capability(dbg_object_id(9), trail_id, "Writer", None); - cap.valid_until = Some(2_000); + let cap = make_capability(dbg_object_id(9), trail_id, "Writer", None, None, Some(2_000)); assert!(capability_matches(&cap, owner, 2_000, &HashSet::new(), &|candidate| { candidate.matches_target_and_role(trail_id, &valid_roles) @@ -326,14 +312,68 @@ mod tests { })); } - fn make_capability(id: ObjectID, trail_id: ObjectID, role: &str, issued_to: Option) -> Capability { + #[test] + fn capability_matches_accepts_unbound_capability_for_matching_role() { + let owner = IotaAddress::random_for_testing_only(); + let trail_id = dbg_object_id(6); + let valid_roles = HashSet::from(["Writer".to_string()]); + let cap = make_capability(dbg_object_id(7), trail_id, "Writer", None, None, None); + + assert!(capability_matches(&cap, owner, 0, &HashSet::new(), &|candidate| { + candidate.matches_target_and_role(trail_id, &valid_roles) + })); + } + + #[test] + fn capability_matches_rejects_non_matching_role() { + let owner = IotaAddress::random_for_testing_only(); + let trail_id = dbg_object_id(8); + let valid_roles = HashSet::from(["Writer".to_string()]); + let cap = make_capability(dbg_object_id(9), trail_id, "Reader", None, None, None); + + assert!(!capability_matches(&cap, owner, 0, &HashSet::new(), &|candidate| { + candidate.matches_target_and_role(trail_id, &valid_roles) + })); + } + + #[test] + fn capability_matches_honors_time_constraints() { + let owner = IotaAddress::random_for_testing_only(); + let trail_id = dbg_object_id(10); + let valid_roles = HashSet::from(["Writer".to_string()]); + let cap = make_capability( + dbg_object_id(11), + trail_id, + "Writer", + Some(owner), + Some(1_700_000_000_000), + Some(1_700_000_005_000), + ); + + assert!(capability_matches( + &cap, + owner, + 1_700_000_000_000, + &HashSet::new(), + &|candidate| { candidate.matches_target_and_role(trail_id, &valid_roles) } + )); + } + + fn make_capability( + id: ObjectID, + trail_id: ObjectID, + role: &str, + issued_to: Option, + valid_from: Option, + valid_until: Option, + ) -> Capability { Capability { id: UID::new(id), target_key: trail_id, role: role.to_string(), issued_to, - valid_from: None, - valid_until: None, + valid_from, + valid_until, } } } diff --git a/audit-trail-rs/src/core/internal/linked_table.rs b/audit-trail-rs/src/core/internal/linked_table.rs index 2ec47849..7f3f4c85 100644 --- a/audit-trail-rs/src/core/internal/linked_table.rs +++ b/audit-trail-rs/src/core/internal/linked_table.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Helpers for reading Move `LinkedTable` nodes through dynamic fields. + use iota_interaction::rpc_types::{IotaData as _, IotaObjectDataOptions}; use iota_interaction::types::base_types::ObjectID; use iota_interaction::types::collection_types::LinkedTableNode; @@ -11,6 +13,10 @@ use serde::de::DeserializeOwned; use crate::error::Error; +/// Fetches and decodes a single linked-table node stored as a dynamic field under `table_id`. +/// +/// The caller provides the fully encoded Move field name so this helper can stay generic over the +/// linked-table key and value types. pub(crate) async fn fetch_node( client: &C, table_id: ObjectID, diff --git a/audit-trail-rs/src/core/internal/mod.rs b/audit-trail-rs/src/core/internal/mod.rs index 44b8c792..c4409bcb 100644 --- a/audit-trail-rs/src/core/internal/mod.rs +++ b/audit-trail-rs/src/core/internal/mod.rs @@ -1,8 +1,16 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal helpers used to bridge public audit-trail APIs to low-level IOTA object access and +//! programmable transaction construction. + +/// Capability lookup helpers for trail-scoped permission checks. pub(crate) mod capability; +/// Linked-table decoding helpers for traversing on-chain Move collections. pub(crate) mod linked_table; +/// Serde adapters for Move collection types that are exposed as standard Rust collections. pub(crate) mod move_collections; +/// Raw trail fetch and decode helpers. pub(crate) mod trail; +/// Common programmable-transaction building helpers. pub(crate) mod tx; diff --git a/audit-trail-rs/src/core/internal/move_collections.rs b/audit-trail-rs/src/core/internal/move_collections.rs index 9df1cc84..ba31be21 100644 --- a/audit-trail-rs/src/core/internal/move_collections.rs +++ b/audit-trail-rs/src/core/internal/move_collections.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Serde adapters for decoding Move collection wrappers into standard Rust collections. + use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::hash::Hash; @@ -8,6 +10,10 @@ use std::hash::Hash; use iota_interaction::types::collection_types::{VecMap, VecSet}; use serde::{Deserialize, Deserializer}; +/// Deserializes a Move `VecMap` into a Rust [`HashMap`]. +/// +/// This adapter is used on public domain types that expose map-like data as idiomatic Rust +/// collections while preserving the on-chain wire format. pub(crate) fn deserialize_vec_map<'de, D, K, V>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -22,6 +28,7 @@ where .collect()) } +/// Deserializes a Move `VecSet` into a Rust [`HashSet`]. pub(crate) fn deserialize_vec_set<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, diff --git a/audit-trail-rs/src/core/internal/trail.rs b/audit-trail-rs/src/core/internal/trail.rs index cd1ddee2..d90861b8 100644 --- a/audit-trail-rs/src/core/internal/trail.rs +++ b/audit-trail-rs/src/core/internal/trail.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Helpers for fetching and decoding the shared on-chain audit-trail object. + use iota_interaction::rpc_types::{IotaData as _, IotaObjectDataOptions}; use iota_interaction::types::base_types::ObjectID; use iota_interaction::{IotaClientTrait, OptionalSync}; @@ -9,6 +11,7 @@ use product_common::core_client::CoreClientReadOnly; use crate::core::types::OnChainAuditTrail; use crate::error::Error; +/// Loads the shared audit-trail object and decodes it into [`OnChainAuditTrail`]. pub(crate) async fn get_audit_trail(trail_id: ObjectID, client: &C) -> Result where C: CoreClientReadOnly + OptionalSync, diff --git a/audit-trail-rs/src/core/internal/tx.rs b/audit-trail-rs/src/core/internal/tx.rs index 32fcb1db..8680c4d0 100644 --- a/audit-trail-rs/src/core/internal/tx.rs +++ b/audit-trail-rs/src/core/internal/tx.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Shared transaction-building helpers used by the internal audit-trail operations. + use std::str::FromStr; use iota_interaction::rpc_types::IotaObjectDataOptions; @@ -21,6 +23,7 @@ use super::{capability, trail as trail_reader}; use crate::core::types::Permission; use crate::error::Error; +/// Returns the canonical immutable clock object argument. pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { ptb.obj(ObjectArg::SharedObject { id: IOTA_CLOCK_OBJECT_ID, @@ -30,6 +33,8 @@ pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { .expect("network has a singleton clock instantiated") } +/// Serializes a pure programmable-transaction argument and annotates serialization failures with +/// the logical argument name. pub(crate) fn ptb_pure(ptb: &mut Ptb, name: &str, value: T) -> Result where T: Serialize + core::fmt::Debug, @@ -41,6 +46,7 @@ where }) } +/// Wraps an optional argument into the corresponding Move `std::option::Option` value. pub(crate) fn option_to_move( option: Option, tag: TypeTag, @@ -67,6 +73,8 @@ pub(crate) fn option_to_move( Ok(arg) } +/// Builds a writable trail transaction after resolving both the trail object and a matching +/// capability for `owner`. pub(crate) async fn build_trail_transaction( client: &C, trail_id: ObjectID, @@ -89,6 +97,8 @@ where build_trail_transaction_with_cap_ref(client, trail_id, cap_ref, method, additional_args).await } +/// Builds a writable trail transaction when the caller already has the capability object +/// reference. pub(crate) async fn build_trail_transaction_with_cap_ref( client: &C, trail_id: ObjectID, @@ -123,6 +133,7 @@ where Ok(ptb.finish()) } +/// Builds a read-only trail transaction that borrows the shared trail object immutably. pub(crate) async fn build_read_only_transaction( client: &C, trail_id: ObjectID, @@ -153,6 +164,10 @@ where Ok(ptb.finish()) } +/// Extracts the generic record payload type from the on-chain trail object type. +/// +/// Audit-trail Move entry points are generic over the record payload type, so transaction builders +/// need this type tag to invoke the correct specialization. pub(crate) async fn get_type_tag(client: &C, object_id: &ObjectID) -> Result where C: CoreClientReadOnly + OptionalSync, @@ -179,6 +194,7 @@ where .map_err(|e| Error::FailedToParseTag(format!("Failed to parse tag '{type_param_str}': {e}"))) } +/// Extracts the innermost generic type parameter from a full Move object type string. fn parse_type(full_type: &str) -> Result { if let (Some(start), Some(end)) = (full_type.find('<'), full_type.rfind('>')) { Ok(full_type[start + 1..end].to_string()) @@ -189,6 +205,7 @@ fn parse_type(full_type: &str) -> Result { } } +/// Fetches the current object reference for `object_id`. pub(crate) async fn get_object_ref_by_id( client: &impl CoreClientReadOnly, object_id: &ObjectID, @@ -207,6 +224,10 @@ pub(crate) async fn get_object_ref_by_id( Ok(data.object_ref()) } +/// Resolves a shared object argument for use in a programmable transaction. +/// +/// This validates that the fetched object is shared and returns the appropriate mutability flag for +/// the planned call. pub(crate) async fn get_shared_object_arg( client: &impl CoreClientReadOnly, object_id: &ObjectID, diff --git a/audit-trail-rs/src/core/locking/mod.rs b/audit-trail-rs/src/core/locking/mod.rs index e9f91d04..3e8cde62 100644 --- a/audit-trail-rs/src/core/locking/mod.rs +++ b/audit-trail-rs/src/core/locking/mod.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Locking configuration APIs for audit trails. + use iota_interaction::types::base_types::ObjectID; use iota_interaction::{IotaKeySignature, OptionalSync}; use product_common::core_client::CoreClient; @@ -18,6 +20,10 @@ pub use transactions::{UpdateDeleteRecordWindow, UpdateDeleteTrailLock, UpdateLo use self::operations::LockingOps; +/// Locking API scoped to a specific trail. +/// +/// This handle updates the trail's locking configuration and queries whether an individual record is currently +/// locked against deletion. #[derive(Debug, Clone)] pub struct TrailLocking<'a, C> { pub(crate) client: &'a C, @@ -40,6 +46,10 @@ impl<'a, C> TrailLocking<'a, C> { self } + /// Replaces the full locking configuration for the trail. + /// + /// This overwrites all three locking dimensions at once: record delete window, trail delete lock, and + /// write lock. pub fn update(&self, config: LockingConfig) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -54,6 +64,7 @@ impl<'a, C> TrailLocking<'a, C> { )) } + /// Updates only the delete-record window. pub fn update_delete_record_window(&self, window: LockingWindow) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -68,6 +79,7 @@ impl<'a, C> TrailLocking<'a, C> { )) } + /// Updates only the delete-trail time lock. pub fn update_delete_trail_lock(&self, lock: TimeLock) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -82,6 +94,7 @@ impl<'a, C> TrailLocking<'a, C> { )) } + /// Updates only the write lock. pub fn update_write_lock(&self, lock: TimeLock) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -96,6 +109,11 @@ impl<'a, C> TrailLocking<'a, C> { )) } + /// Returns `true` when the given record is currently locked against deletion. + /// + /// # Errors + /// + /// Returns an error if the lock state cannot be computed from the current on-chain state. pub async fn is_record_locked(&self, sequence_number: u64) -> Result where C: AuditTrailReadOnly, diff --git a/audit-trail-rs/src/core/locking/operations.rs b/audit-trail-rs/src/core/locking/operations.rs index ed726be3..c34b81af 100644 --- a/audit-trail-rs/src/core/locking/operations.rs +++ b/audit-trail-rs/src/core/locking/operations.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal helpers that build locking-related programmable transactions. +//! +//! These helpers serialize locking values into the Move shapes used by the trail package and select the +//! corresponding locking-update permissions. + use iota_interaction::OptionalSync; use iota_interaction::types::base_types::{IotaAddress, ObjectID}; use iota_interaction::types::transaction::ProgrammableTransaction; @@ -10,9 +15,11 @@ use crate::core::internal::tx; use crate::core::types::{LockingConfig, LockingWindow, Permission, TimeLock}; use crate::error::Error; +/// Internal namespace for locking transaction construction. pub(super) struct LockingOps; impl LockingOps { + /// Builds the `update_locking_config` call. pub(super) async fn update_locking_config( client: &C, trail_id: ObjectID, @@ -44,6 +51,7 @@ impl LockingOps { .await } + /// Builds the `update_delete_record_window` call. pub(super) async fn update_delete_record_window( client: &C, trail_id: ObjectID, @@ -71,6 +79,7 @@ impl LockingOps { .await } + /// Builds the `update_delete_trail_lock` call. pub(super) async fn update_delete_trail_lock( client: &C, trail_id: ObjectID, @@ -101,6 +110,7 @@ impl LockingOps { .await } + /// Builds the `update_write_lock` call. pub(super) async fn update_write_lock( client: &C, trail_id: ObjectID, @@ -131,6 +141,7 @@ impl LockingOps { .await } + /// Builds the read-only `is_record_locked` call. pub(super) async fn is_record_locked( client: &C, trail_id: ObjectID, diff --git a/audit-trail-rs/src/core/locking/transactions.rs b/audit-trail-rs/src/core/locking/transactions.rs index a1690eb0..22e4e886 100644 --- a/audit-trail-rs/src/core/locking/transactions.rs +++ b/audit-trail-rs/src/core/locking/transactions.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Transaction payloads for locking updates. + use async_trait::async_trait; use iota_interaction::OptionalSync; use iota_interaction::rpc_types::IotaTransactionBlockEffects; @@ -14,6 +16,9 @@ use super::operations::LockingOps; use crate::core::types::{LockingConfig, LockingWindow, TimeLock}; use crate::error::Error; +/// Transaction that replaces the full locking configuration. +/// +/// This writes the full `LockingConfig` object and therefore updates all locking dimensions in one call. #[derive(Debug, Clone)] pub struct UpdateLockingConfig { trail_id: ObjectID, @@ -24,6 +29,7 @@ pub struct UpdateLockingConfig { } impl UpdateLockingConfig { + /// Creates an `UpdateLockingConfig` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -75,6 +81,9 @@ impl Transaction for UpdateLockingConfig { } } +/// Transaction that updates the delete-record window. +/// +/// This updates only the rule that governs when individual records may be deleted. #[derive(Debug, Clone)] pub struct UpdateDeleteRecordWindow { trail_id: ObjectID, @@ -85,6 +94,7 @@ pub struct UpdateDeleteRecordWindow { } impl UpdateDeleteRecordWindow { + /// Creates an `UpdateDeleteRecordWindow` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -136,6 +146,9 @@ impl Transaction for UpdateDeleteRecordWindow { } } +/// Transaction that updates the delete-trail lock. +/// +/// This updates only the time lock guarding deletion of the entire trail object. #[derive(Debug, Clone)] pub struct UpdateDeleteTrailLock { trail_id: ObjectID, @@ -146,6 +159,7 @@ pub struct UpdateDeleteTrailLock { } impl UpdateDeleteTrailLock { + /// Creates an `UpdateDeleteTrailLock` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -197,6 +211,9 @@ impl Transaction for UpdateDeleteTrailLock { } } +/// Transaction that updates the write lock. +/// +/// This updates only the time lock guarding future record writes. #[derive(Debug, Clone)] pub struct UpdateWriteLock { trail_id: ObjectID, @@ -207,6 +224,7 @@ pub struct UpdateWriteLock { } impl UpdateWriteLock { + /// Creates an `UpdateWriteLock` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, diff --git a/audit-trail-rs/src/core/mod.rs b/audit-trail-rs/src/core/mod.rs index 36584101..53f08953 100644 --- a/audit-trail-rs/src/core/mod.rs +++ b/audit-trail-rs/src/core/mod.rs @@ -1,14 +1,33 @@ // Copyright 2020-2025 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Core types and builders for audit trail. +//! Core handles, builders, transactions, and domain types for audit trails. +//! +//! This namespace contains the main trail-facing Rust API: +//! +//! - [`crate::core::access`] exposes role and capability management +//! - [`crate::core::builder`] configures trail creation +//! - [`crate::core::create`] contains the creation transaction types +//! - [`crate::core::locking`] manages trail locking rules +//! - [`crate::core::records`] reads and mutates trail records +//! - [`crate::core::tags`] manages the trail-owned record-tag registry +//! - [`crate::core::trail`] provides the high-level typed handle bound to a specific trail +//! - [`crate::core::types`] contains serializable value types shared across the crate +/// Role and capability management APIs. pub mod access; +/// Builder used to configure trail creation. pub mod builder; +/// Trail-creation transaction types. pub mod create; pub(crate) mod internal; +/// Locking configuration APIs. pub mod locking; +/// Record read and mutation APIs. pub mod records; +/// Trail-scoped record-tag management APIs. pub mod tags; +/// High-level trail handle types. pub mod trail; +/// Shared domain and event types. pub mod types; diff --git a/audit-trail-rs/src/core/records/mod.rs b/audit-trail-rs/src/core/records/mod.rs index 02b117ce..0e32ec6f 100644 --- a/audit-trail-rs/src/core/records/mod.rs +++ b/audit-trail-rs/src/core/records/mod.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Record read and mutation APIs for audit trails. + use std::collections::{BTreeMap, HashMap}; use iota_interaction::move_core_types::annotated_value::MoveValue; @@ -29,6 +31,9 @@ use self::operations::RecordsOps; const MAX_LIST_PAGE_LIMIT: usize = 1_000; +/// Record API scoped to a specific trail. +/// +/// This handle builds record-oriented transactions and loads record data from the trail's linked-table storage. #[derive(Debug, Clone)] pub struct TrailRecords<'a, C, D = Data> { pub(crate) client: &'a C, @@ -53,6 +58,11 @@ impl<'a, C, D> TrailRecords<'a, C, D> { self } + /// Loads a single record by sequence number. + /// + /// # Errors + /// + /// Returns an error if the record cannot be loaded or deserialized. pub async fn get(&self, sequence_number: u64) -> Result, Error> where C: AuditTrailReadOnly, @@ -62,6 +72,10 @@ impl<'a, C, D> TrailRecords<'a, C, D> { self.client.execute_read_only_transaction(tx).await } + /// Builds a transaction that appends a record to the trail. + /// + /// Tagged writes must reference a tag already defined on the trail. They also require a capability whose + /// role allows both `AddRecord` and the requested tag. pub fn add(&self, data: D, metadata: Option, tag: Option) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -79,6 +93,9 @@ impl<'a, C, D> TrailRecords<'a, C, D> { )) } + /// Builds a transaction that deletes a single record. + /// + /// Deletion remains subject to record locking rules and tag-based access restrictions enforced on-chain. pub fn delete(&self, sequence_number: u64) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -93,6 +110,9 @@ impl<'a, C, D> TrailRecords<'a, C, D> { )) } + /// Builds a transaction that deletes up to `limit` records in one operation. + /// + /// Batch deletion removes records from the front of the trail and requires `DeleteAllRecords`. pub fn delete_records_batch(&self, limit: u64) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -107,6 +127,11 @@ impl<'a, C, D> TrailRecords<'a, C, D> { )) } + /// Placeholder for a future correction helper. + /// + /// # Errors + /// + /// Always returns [`Error::NotImplemented`]. pub async fn correct(&self, _replaces: Vec, _data: D, _metadata: Option) -> Result<(), Error> where C: AuditTrailFull, @@ -114,6 +139,11 @@ impl<'a, C, D> TrailRecords<'a, C, D> { Err(Error::NotImplemented("TrailRecords::correct")) } + /// Returns the number of records currently stored in the trail. + /// + /// # Errors + /// + /// Returns an error if the count cannot be computed from the current on-chain state. pub async fn record_count(&self) -> Result where C: AuditTrailReadOnly, @@ -122,7 +152,7 @@ impl<'a, C, D> TrailRecords<'a, C, D> { self.client.execute_read_only_transaction(tx).await } - /// List all records into a [`HashMap`]. + /// Lists all records into a [`HashMap`]. /// /// This traverses the full on-chain linked table and can be expensive for large trails. /// For paginated access, use [`list_page`](Self::list_page). @@ -135,7 +165,7 @@ impl<'a, C, D> TrailRecords<'a, C, D> { list_linked_table::<_, Record>(self.client, &records_table, None).await } - /// List all records with a hard cap to protect against expensive traversals. + /// Lists all records with a hard cap to protect against expensive traversals. pub async fn list_with_limit(&self, max_entries: usize) -> Result>, Error> where C: AuditTrailReadOnly, @@ -145,7 +175,7 @@ impl<'a, C, D> TrailRecords<'a, C, D> { list_linked_table::<_, Record>(self.client, &records_table, Some(max_entries)).await } - /// List one page of linked-table records starting from `cursor`. + /// Lists one page of linked-table records starting from `cursor`. /// /// Pass `None` for the first page; use `next_cursor` for subsequent pages. pub async fn list_page(&self, cursor: Option, limit: usize) -> Result, Error> @@ -190,6 +220,7 @@ where C: CoreClientReadOnly + OptionalSync, V: DeserializeOwned, { + // Preserve linked-table order while exposing a page as a stable Rust map keyed by sequence number. if limit == 0 { return Ok((BTreeMap::new(), start_key.or(table.head))); } @@ -233,6 +264,7 @@ where C: CoreClientReadOnly + OptionalSync, V: DeserializeOwned, { + // Full traversal is only allowed when the caller explicitly accepts the current linked-table size. let expected = table.size as usize; let cap = max_entries.unwrap_or(expected); diff --git a/audit-trail-rs/src/core/records/operations.rs b/audit-trail-rs/src/core/records/operations.rs index 50412ac1..781e6909 100644 --- a/audit-trail-rs/src/core/records/operations.rs +++ b/audit-trail-rs/src/core/records/operations.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal record-operation helpers that build trail-scoped programmable transactions. +//! +//! These helpers enforce the Rust-side preflight checks around record tags and then encode the exact Move call +//! arguments expected by the trail package. + use iota_interaction::OptionalSync; use iota_interaction::types::base_types::{IotaAddress, ObjectID}; use iota_interaction::types::transaction::ProgrammableTransaction; @@ -11,9 +16,14 @@ use crate::core::internal::{trail as trail_reader, tx}; use crate::core::types::{Data, Permission}; use crate::error::Error; +/// Internal namespace for record-related transaction construction. pub(super) struct RecordsOps; impl RecordsOps { + /// Builds the `add_record` call. + /// + /// Tagged writes are prevalidated against the trail tag registry and require a capability whose role allows + /// both `AddRecord` and the requested tag. pub(super) async fn add_record( client: &C, trail_id: ObjectID, @@ -72,6 +82,9 @@ impl RecordsOps { } } + /// Builds the `delete_record` call. + /// + /// Authorization and locking remain enforced by the Move entry point. pub(super) async fn delete_record( client: &C, trail_id: ObjectID, @@ -98,6 +111,9 @@ impl RecordsOps { .await } + /// Builds the `delete_records_batch` call. + /// + /// Batch deletion requires `DeleteAllRecords` and deletes from the front of the trail. pub(super) async fn delete_records_batch( client: &C, trail_id: ObjectID, @@ -124,6 +140,7 @@ impl RecordsOps { .await } + /// Builds the read-only `get_record` call. pub(super) async fn get_record( client: &C, trail_id: ObjectID, @@ -139,6 +156,7 @@ impl RecordsOps { .await } + /// Builds the read-only `record_count` call. pub(super) async fn record_count(client: &C, trail_id: ObjectID) -> Result where C: CoreClientReadOnly + OptionalSync, diff --git a/audit-trail-rs/src/core/records/transactions.rs b/audit-trail-rs/src/core/records/transactions.rs index a09fed4e..ab4c4a63 100644 --- a/audit-trail-rs/src/core/records/transactions.rs +++ b/audit-trail-rs/src/core/records/transactions.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Transaction payloads for record writes and deletions. +//! +//! These types cache the generated programmable transaction, delegate PTB construction to +//! [`super::operations::RecordsOps`], and decode record events into typed Rust outputs. + use async_trait::async_trait; use iota_interaction::OptionalSync; use iota_interaction::rpc_types::{IotaTransactionBlockEffects, IotaTransactionBlockEvents}; @@ -16,18 +21,29 @@ use crate::error::Error; // ===== AddRecord ===== +/// Transaction that appends a record to a trail. +/// +/// Tagged writes require the tag to exist in the trail registry and a capability whose role explicitly allows +/// that tag in addition to `AddRecord`. #[derive(Debug, Clone)] pub struct AddRecord { + /// Trail object ID that will receive the record. pub trail_id: ObjectID, + /// Address authorizing the write. pub owner: IotaAddress, + /// Record payload to append. pub data: Data, + /// Optional application-defined metadata. pub metadata: Option, + /// Optional trail-owned tag to attach to the record. pub tag: Option, + /// Explicit capability to use instead of auto-selecting one from the owner's wallet. pub selected_capability_id: Option, cached_ptb: OnceCell, } impl AddRecord { + /// Creates an `AddRecord` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -105,16 +121,25 @@ impl Transaction for AddRecord { // ===== DeleteRecord ===== +/// Transaction that deletes a single record. +/// +/// This uses the single-record delete entry point, which remains subject to record-locking and tag-aware +/// authorization checks. #[derive(Debug, Clone)] pub struct DeleteRecord { + /// Trail object ID containing the record. pub trail_id: ObjectID, + /// Address authorizing the deletion. pub owner: IotaAddress, + /// Sequence number of the record to delete. pub sequence_number: u64, + /// Explicit capability to use instead of auto-selecting one from the owner's wallet. pub selected_capability_id: Option, cached_ptb: OnceCell, } impl DeleteRecord { + /// Creates a `DeleteRecord` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -186,16 +211,25 @@ impl Transaction for DeleteRecord { // ===== DeleteRecordsBatch ===== +/// Transaction that deletes multiple records in a batch operation. +/// +/// The Move entry point deletes records from the front of the trail up to `limit` and reports the number of +/// deleted records through the emitted `RecordDeleted` events. #[derive(Debug, Clone)] pub struct DeleteRecordsBatch { + /// Trail object ID containing the records. pub trail_id: ObjectID, + /// Address authorizing the deletion. pub owner: IotaAddress, + /// Maximum number of records to delete in this batch. pub limit: u64, + /// Explicit capability to use instead of auto-selecting one from the owner's wallet. pub selected_capability_id: Option, cached_ptb: OnceCell, } impl DeleteRecordsBatch { + /// Creates a `DeleteRecordsBatch` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, limit: u64, selected_capability_id: Option) -> Self { Self { trail_id, diff --git a/audit-trail-rs/src/core/tags/mod.rs b/audit-trail-rs/src/core/tags/mod.rs index 2d51a943..c1862a79 100644 --- a/audit-trail-rs/src/core/tags/mod.rs +++ b/audit-trail-rs/src/core/tags/mod.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Record-tag registry APIs for audit trails. + use iota_interaction::types::base_types::ObjectID; use iota_interaction::{IotaKeySignature, OptionalSync}; use product_common::core_client::CoreClient; @@ -14,6 +16,9 @@ mod transactions; pub use transactions::{AddRecordTag, RemoveRecordTag}; +/// Tag-registry API scoped to a specific trail. +/// +/// The registry defines the canonical set of tags that records and role-tag restrictions may reference. #[derive(Debug, Clone)] pub struct TrailTags<'a, C> { pub(crate) client: &'a C, @@ -37,6 +42,8 @@ impl<'a, C> TrailTags<'a, C> { } /// Adds a tag to the trail-owned record-tag registry. + /// + /// Added tags become available to future tagged record writes and role-tag restrictions. pub fn add(&self, tag: impl Into) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -52,6 +59,8 @@ impl<'a, C> TrailTags<'a, C> { } /// Removes a tag from the trail-owned record-tag registry. + /// + /// Removal fails on-chain while the tag is still referenced by existing records or role-tag policies. pub fn remove(&self, tag: impl Into) -> TransactionBuilder where C: AuditTrailFull + CoreClient, diff --git a/audit-trail-rs/src/core/tags/operations.rs b/audit-trail-rs/src/core/tags/operations.rs index 36bc1980..ba86c650 100644 --- a/audit-trail-rs/src/core/tags/operations.rs +++ b/audit-trail-rs/src/core/tags/operations.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal helpers that build record-tag registry transactions. +//! +//! These helpers encode updates to the trail-owned tag registry and select the corresponding tag-management +//! permissions. + use iota_interaction::OptionalSync; use iota_interaction::types::base_types::{IotaAddress, ObjectID}; use iota_interaction::types::transaction::ProgrammableTransaction; @@ -10,9 +15,11 @@ use crate::core::internal::tx; use crate::core::types::Permission; use crate::error::Error; +/// Internal namespace for tag-registry transaction construction. pub(super) struct TagsOps; impl TagsOps { + /// Builds the `add_record_tag` call. pub(super) async fn add_record_tag( client: &C, trail_id: ObjectID, @@ -39,6 +46,7 @@ impl TagsOps { .await } + /// Builds the `remove_record_tag` call. pub(super) async fn remove_record_tag( client: &C, trail_id: ObjectID, diff --git a/audit-trail-rs/src/core/tags/transactions.rs b/audit-trail-rs/src/core/tags/transactions.rs index 1c4727de..171074a0 100644 --- a/audit-trail-rs/src/core/tags/transactions.rs +++ b/audit-trail-rs/src/core/tags/transactions.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Transaction payloads for tag-registry updates. + use async_trait::async_trait; use iota_interaction::OptionalSync; use iota_interaction::rpc_types::IotaTransactionBlockEffects; @@ -13,6 +15,9 @@ use tokio::sync::OnceCell; use super::operations::TagsOps; use crate::error::Error; +/// Transaction that adds a record tag to the trail registry. +/// +/// This extends the canonical tag registry owned by the trail. #[derive(Debug, Clone)] pub struct AddRecordTag { trail_id: ObjectID, @@ -23,6 +28,7 @@ pub struct AddRecordTag { } impl AddRecordTag { + /// Creates an `AddRecordTag` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, tag: String, selected_capability_id: Option) -> Self { Self { trail_id, @@ -69,6 +75,9 @@ impl Transaction for AddRecordTag { } } +/// Transaction that removes a record tag from the trail registry. +/// +/// Removal only succeeds when the tag is no longer used by records or role-tag restrictions. #[derive(Debug, Clone)] pub struct RemoveRecordTag { trail_id: ObjectID, @@ -79,6 +88,7 @@ pub struct RemoveRecordTag { } impl RemoveRecordTag { + /// Creates a `RemoveRecordTag` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, tag: String, selected_capability_id: Option) -> Self { Self { trail_id, diff --git a/audit-trail-rs/src/core/trail.rs b/audit-trail-rs/src/core/trail.rs index 593c3ac2..8e034a50 100644 --- a/audit-trail-rs/src/core/trail.rs +++ b/audit-trail-rs/src/core/trail.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! High-level trail handles and trail-scoped transactions. + use iota_interaction::types::base_types::ObjectID; use iota_interaction::types::transaction::ProgrammableTransaction; use iota_interaction::{IotaKeySignature, OptionalSync}; @@ -22,20 +24,24 @@ mod transactions; pub use transactions::{DeleteAuditTrail, Migrate, UpdateMetadata}; -/// Marker trait for read-only audit trail clients. +/// Marker trait for read-only audit-trail clients. #[doc(hidden)] #[cfg_attr(not(feature = "send-sync"), async_trait::async_trait(?Send))] #[cfg_attr(feature = "send-sync", async_trait::async_trait)] pub trait AuditTrailReadOnly: CoreClientReadOnly + OptionalSync { + /// Executes a read-only programmable transaction and decodes the first return value. async fn execute_read_only_transaction(&self, tx: ProgrammableTransaction) -> Result; } -/// Marker trait for full (read-write) audit trail clients. +/// Marker trait for full audit-trail clients. #[doc(hidden)] pub trait AuditTrailFull: AuditTrailReadOnly {} -/// A typed handle bound to a specific audit trail and client. +/// A typed handle bound to one trail ID and one client. +/// +/// This is the main trail-scoped entry point. It keeps the trail identity together with the client so record, +/// locking, access, tag, migration, and metadata operations all share one typed handle. #[derive(Debug, Clone)] pub struct AuditTrailHandle<'a, C> { pub(crate) client: &'a C, @@ -59,6 +65,8 @@ impl<'a, C> AuditTrailHandle<'a, C> { } /// Loads the full on-chain audit trail object. + /// + /// Each call fetches a fresh snapshot from chain state rather than reusing cached client-side data. pub async fn get(&self) -> Result where C: AuditTrailReadOnly, @@ -66,7 +74,9 @@ impl<'a, C> AuditTrailHandle<'a, C> { trail_reader::get_audit_trail(self.trail_id, self.client).await } - /// Updates the trail's updatable metadata. + /// Updates the trail's mutable metadata field. + /// + /// Passing `None` clears the field on-chain. pub fn update_metadata(&self, metadata: Option) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -81,7 +91,7 @@ impl<'a, C> AuditTrailHandle<'a, C> { )) } - /// Migrates the trail to the latest package version. + /// Migrates the trail to the latest package version supported by this crate. pub fn migrate(&self) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -91,9 +101,9 @@ impl<'a, C> AuditTrailHandle<'a, C> { TransactionBuilder::new(Migrate::new(self.trail_id, owner, self.selected_capability_id)) } - /// Deletes the audit trail object. + /// Deletes the trail object. /// - /// The trail must be empty before deletion. + /// Deletion requires the trail to be empty and to satisfy the trail-delete lock rules. pub fn delete_audit_trail(&self) -> TransactionBuilder where C: AuditTrailFull + CoreClient, @@ -103,18 +113,30 @@ impl<'a, C> AuditTrailHandle<'a, C> { TransactionBuilder::new(DeleteAuditTrail::new(self.trail_id, owner, self.selected_capability_id)) } + /// Returns the record API scoped to this trail. + /// + /// Use this for record reads, appends, and deletions. pub fn records(&self) -> TrailRecords<'a, C, Data> { TrailRecords::new(self.client, self.trail_id, self.selected_capability_id) } + /// Returns the locking API scoped to this trail. + /// + /// Use this for inspecting lock state and updating locking rules. pub fn locking(&self) -> TrailLocking<'a, C> { TrailLocking::new(self.client, self.trail_id, self.selected_capability_id) } + /// Returns the access-control API scoped to this trail. + /// + /// Use this for roles, capabilities, and access-policy updates. pub fn access(&self) -> TrailAccess<'a, C> { TrailAccess::new(self.client, self.trail_id, self.selected_capability_id) } + /// Returns the tag-registry API scoped to this trail. + /// + /// Use this for managing the canonical tag registry that record writes and role tags must reference. pub fn tags(&self) -> TrailTags<'a, C> { TrailTags::new(self.client, self.trail_id, self.selected_capability_id) } diff --git a/audit-trail-rs/src/core/trail/operations.rs b/audit-trail-rs/src/core/trail/operations.rs index 3b003914..e3ebfa0c 100644 --- a/audit-trail-rs/src/core/trail/operations.rs +++ b/audit-trail-rs/src/core/trail/operations.rs @@ -1,6 +1,11 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Internal helpers that build trail-level programmable transactions. +//! +//! These helpers select the required trail-level permission and encode the corresponding metadata, migration, +//! and deletion calls. + use iota_interaction::OptionalSync; use iota_interaction::types::base_types::{IotaAddress, ObjectID}; use iota_interaction::types::transaction::ProgrammableTransaction; @@ -10,9 +15,11 @@ use crate::core::internal::tx; use crate::core::types::Permission; use crate::error::Error; +/// Internal namespace for trail-level transaction construction. pub(super) struct TrailOps; impl TrailOps { + /// Builds the `migrate` call. pub(super) async fn migrate( client: &C, trail_id: ObjectID, @@ -37,6 +44,7 @@ impl TrailOps { .await } + /// Builds the `update_metadata` call. pub(super) async fn update_metadata( client: &C, trail_id: ObjectID, @@ -63,6 +71,7 @@ impl TrailOps { .await } + /// Builds the `delete_audit_trail` call. pub(super) async fn delete_audit_trail( client: &C, trail_id: ObjectID, diff --git a/audit-trail-rs/src/core/trail/transactions.rs b/audit-trail-rs/src/core/trail/transactions.rs index 47859145..8148b385 100644 --- a/audit-trail-rs/src/core/trail/transactions.rs +++ b/audit-trail-rs/src/core/trail/transactions.rs @@ -1,6 +1,8 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Transaction payloads for trail-level metadata, migration, and deletion operations. + use async_trait::async_trait; use iota_interaction::OptionalSync; use iota_interaction::rpc_types::{IotaTransactionBlockEffects, IotaTransactionBlockEvents}; @@ -14,6 +16,10 @@ use super::operations::TrailOps; use crate::core::types::{AuditTrailDeleted, Event}; use crate::error::Error; +/// Transaction that migrates a trail to the latest package version supported by this crate. +/// +/// This requires `Migrate` on the trail and succeeds only when the on-chain package version is older than the +/// current supported version. #[derive(Debug, Clone)] pub struct Migrate { trail_id: ObjectID, @@ -23,6 +29,7 @@ pub struct Migrate { } impl Migrate { + /// Creates a `Migrate` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, selected_capability_id: Option) -> Self { Self { trail_id, @@ -61,6 +68,9 @@ impl Transaction for Migrate { } } +/// Transaction that updates mutable trail metadata. +/// +/// Passing `None` clears the mutable metadata field. #[derive(Debug, Clone)] pub struct UpdateMetadata { trail_id: ObjectID, @@ -71,6 +81,7 @@ pub struct UpdateMetadata { } impl UpdateMetadata { + /// Creates an `UpdateMetadata` transaction builder payload. pub fn new( trail_id: ObjectID, owner: IotaAddress, @@ -122,6 +133,10 @@ impl Transaction for UpdateMetadata { } } +/// Transaction that deletes an empty trail. +/// +/// Deletion still depends on the trail-delete permission, an empty record set, and the configured trail-delete +/// lock. #[derive(Debug, Clone)] pub struct DeleteAuditTrail { trail_id: ObjectID, @@ -131,6 +146,7 @@ pub struct DeleteAuditTrail { } impl DeleteAuditTrail { + /// Creates a `DeleteAuditTrail` transaction builder payload. pub fn new(trail_id: ObjectID, owner: IotaAddress, selected_capability_id: Option) -> Self { Self { trail_id, diff --git a/audit-trail-rs/src/core/types/audit_trail.rs b/audit-trail-rs/src/core/types/audit_trail.rs index 2cc95e50..962a3218 100644 --- a/audit-trail-rs/src/core/types/audit_trail.rs +++ b/audit-trail-rs/src/core/types/audit_trail.rs @@ -19,46 +19,36 @@ use crate::core::internal::move_collections::deserialize_vec_map; use crate::core::internal::tx; use crate::error::Error; -/// Registry of record tags configured for an audit trail. -/// -/// Each entry maps a tag name to its current usage count across role definitions and records. -/// -/// `TagRegistry` maintains a combined usage count per tag that is -/// incremented every time a record or role references the tag. A tag cannot be -/// removed from the registry while its usage count is greater than zero. +/// Registry of trail-owned record tags. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TagRegistry { - /// Mapping from a human-readable tag name to how many times it is used in role definitions and records. + /// Mapping from tag name to usage count. #[serde(deserialize_with = "deserialize_vec_map")] pub tag_map: HashMap, } impl TagRegistry { - /// Returns the number of tags currently registered. + /// Returns the number of registered tags. pub fn len(&self) -> usize { self.tag_map.len() } - /// Returns `true` if the registry contains no tags. + /// Returns `true` when no tags are registered. pub fn is_empty(&self) -> bool { self.tag_map.is_empty() } - /// Returns `true` if a tag with the given name exists in the registry. - /// - /// - `tag`: The tag name to look up. + /// Returns `true` when the registry contains the given tag. pub fn contains_key(&self, tag: &str) -> bool { self.tag_map.contains_key(tag) } - /// Returns the current usage count associated with a tag name. - /// - /// - `tag`: The tag name to look up. + /// Returns the usage count for a tag. pub fn get(&self, tag: &str) -> Option<&u64> { self.tag_map.get(tag) } - /// Iterates over all registered tag names and their usage counts. + /// Iterates over tag names and usage counts. pub fn iter(&self) -> impl Iterator { self.tag_map.iter() } @@ -67,31 +57,27 @@ impl TagRegistry { /// An audit trail stored on-chain. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OnChainAuditTrail { - /// Unique object id of the audit trail. + /// Unique object ID of the trail. pub id: UID, - /// Address that originally created the trail. + /// Address that created the trail. pub creator: IotaAddress, - /// Unix timestamp in milliseconds when the trail was created. + /// Millisecond timestamp at which the trail was created. pub created_at: u64, - /// Monotonically increasing number. - /// Can be interpreted as total number of created `Record` instances by this audit trai. - /// Will be used as identifier for the next record added to the trail, starting at 0 for the first record. + /// Current record sequence number cursor. pub sequence_number: u64, - /// Contains the trail's records keyed by `sequence_number`. + /// Linked table containing the trail records. pub records: LinkedTable, - /// Registry of tag names tracked with their current usage counts. - /// Tag names can be added to records to restrict record-access to users having capabilities - /// granting access to this tag. Tag specific access can be defined by adding tags to [`Role`] definitions. + /// Registry of allowed record tags. pub tags: TagRegistry, - /// Active write/delete locking rules for this trail. + /// Active locking rules for the trail. pub locking_config: LockingConfig, - /// [`Role`] definitions and permissions configured for the trail. + /// Role and capability configuration for the trail. pub roles: RoleMap, - /// Immutable metadata set at creation time, if present. + /// Metadata fixed at creation time. pub immutable_metadata: Option, - /// Mutable metadata string that can be updated after creation, if present. + /// Metadata that can be updated after creation. pub updatable_metadata: Option, - /// On-chain schema or object version maintained by the Move package. + /// On-chain package version of the trail object. pub version: u64, } @@ -100,22 +86,16 @@ pub struct OnChainAuditTrail { pub struct ImmutableMetadata { /// Human-readable trail name. pub name: String, - /// Optional longer description explaining the trail's purpose. + /// Optional human-readable description. pub description: Option, } impl ImmutableMetadata { - /// Creates immutable metadata for a new trail. - /// - /// - `name`: The human-readable name to store on the trail. - /// - `description`: An optional longer description stored alongside the name. + /// Creates immutable metadata for a trail. pub fn new(name: String, description: Option) -> Self { Self { name, description } } - /// Returns the Move type tag for `main::ImmutableMetadata` in the given package. - /// - /// - `package_id`: The published audit-trail Move package id. pub(in crate::core) fn tag(package_id: ObjectID) -> TypeTag { TypeTag::from_str(&format!("{package_id}::main::ImmutableMetadata")) .expect("invalid TypeTag for ImmutableMetadata") @@ -124,9 +104,6 @@ impl ImmutableMetadata { /// Creates a new `Argument` from the `ImmutableMetadata`. /// /// To be used when creating a new `ImmutableMetadata` object on the ledger. - /// - /// - `ptb`: The programmable transaction builder the argument should be added to. - /// - `package_id`: The published audit-trail Move package id. pub(in crate::core) fn to_ptb(&self, ptb: &mut Ptb, package_id: ObjectID) -> Result { let name = tx::ptb_pure(ptb, "name", &self.name)?; let description = tx::ptb_pure(ptb, "description", &self.description)?; diff --git a/audit-trail-rs/src/core/types/event.rs b/audit-trail-rs/src/core/types/event.rs index d52fa828..988f43bf 100644 --- a/audit-trail-rs/src/core/types/event.rs +++ b/audit-trail-rs/src/core/types/event.rs @@ -13,102 +13,157 @@ use super::{Permission, PermissionSet, RoleTags}; /// Generic wrapper for audit trail events. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Event { + /// Parsed event payload. #[serde(flatten)] pub data: D, } +/// Event emitted when a trail is created. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AuditTrailCreated { + /// Newly created trail object ID. pub trail_id: ObjectID, + /// Address that created the trail. pub creator: IotaAddress, + /// Millisecond event timestamp. #[serde(deserialize_with = "deserialize_number_from_string")] pub timestamp: u64, } +/// Event emitted when a trail is deleted. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AuditTrailDeleted { + /// Deleted trail object ID. pub trail_id: ObjectID, + /// Millisecond event timestamp. #[serde(deserialize_with = "deserialize_number_from_string")] pub timestamp: u64, } +/// Event emitted when a record is added. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RecordAdded { + /// Trail object ID receiving the new record. pub trail_id: ObjectID, + /// Sequence number assigned to the new record. #[serde(deserialize_with = "deserialize_number_from_string")] pub sequence_number: u64, + /// Address that added the record. pub added_by: IotaAddress, + /// Millisecond event timestamp. #[serde(deserialize_with = "deserialize_number_from_string")] pub timestamp: u64, } +/// Event emitted when a record is deleted. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RecordDeleted { + /// Trail object ID from which the record was deleted. pub trail_id: ObjectID, + /// Sequence number of the deleted record. #[serde(deserialize_with = "deserialize_number_from_string")] pub sequence_number: u64, + /// Address that deleted the record. pub deleted_by: IotaAddress, + /// Millisecond event timestamp. #[serde(deserialize_with = "deserialize_number_from_string")] pub timestamp: u64, } +/// Event emitted when a capability is issued. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CapabilityIssued { + /// Trail object ID protected by the capability. pub target_key: ObjectID, + /// Newly created capability object ID. pub capability_id: ObjectID, + /// Role granted by the capability. pub role: String, + /// Address receiving the capability, if one is assigned. pub issued_to: Option, + /// Millisecond timestamp at which the capability becomes valid. #[serde(deserialize_with = "deserialize_option_number_from_string")] pub valid_from: Option, + /// Millisecond timestamp at which the capability expires. #[serde(deserialize_with = "deserialize_option_number_from_string")] pub valid_until: Option, } +/// Event emitted when a capability object is destroyed. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CapabilityDestroyed { + /// Trail object ID protected by the capability. pub target_key: ObjectID, + /// Destroyed capability object ID. pub capability_id: ObjectID, + /// Role granted by the capability. pub role: String, + /// Address that held the capability, if any. pub issued_to: Option, + /// Millisecond timestamp at which the capability became valid. #[serde(deserialize_with = "deserialize_option_number_from_string")] pub valid_from: Option, + /// Millisecond timestamp at which the capability expired. #[serde(deserialize_with = "deserialize_option_number_from_string")] pub valid_until: Option, } +/// Event emitted when a capability is revoked. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CapabilityRevoked { + /// Trail object ID protected by the capability. pub target_key: ObjectID, + /// Revoked capability object ID. pub capability_id: ObjectID, + /// Millisecond timestamp retained for denylist cleanup. #[serde(deserialize_with = "deserialize_number_from_string")] pub valid_until: u64, } +/// Event emitted when a role is created. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct RoleCreated { + /// Trail object ID that owns the role. pub trail_id: ObjectID, + /// Role name. pub role: String, + /// Permissions granted by the new role. pub permissions: PermissionSet, + /// Optional record-tag restrictions stored as role data. pub data: Option, + /// Address that created the role. pub created_by: IotaAddress, + /// Millisecond event timestamp. pub timestamp: u64, } +/// Event emitted when a role is updated. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct RoleUpdated { + /// Trail object ID that owns the role. pub trail_id: ObjectID, + /// Role name. pub role: String, + /// Updated permissions for the role. pub permissions: PermissionSet, + /// Updated record-tag restrictions, if any. pub data: Option, + /// Address that updated the role. pub updated_by: IotaAddress, + /// Millisecond event timestamp. pub timestamp: u64, } +/// Event emitted when a role is deleted. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct RoleDeleted { + /// Trail object ID that owned the role. pub trail_id: ObjectID, + /// Role name. pub role: String, + /// Address that deleted the role. pub deleted_by: IotaAddress, + /// Millisecond event timestamp. pub timestamp: u64, } diff --git a/audit-trail-rs/src/core/types/locking.rs b/audit-trail-rs/src/core/types/locking.rs index 1986c811..eb03205a 100644 --- a/audit-trail-rs/src/core/types/locking.rs +++ b/audit-trail-rs/src/core/types/locking.rs @@ -13,8 +13,11 @@ use crate::error::Error; /// Locking configuration for the audit trail. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct LockingConfig { + /// Delete-window policy applied to individual records. pub delete_record_window: LockingWindow, + /// Time lock that gates deletion of the entire trail. pub delete_trail_lock: TimeLock, + /// Time lock that gates record writes. pub write_lock: TimeLock, } @@ -47,10 +50,15 @@ impl LockingConfig { /// Must match `tf_components::timelock::TimeLock` variant order for BCS compatibility. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum TimeLock { + /// Unlocks at the given Unix timestamp in seconds. UnlockAt(u32), + /// Unlocks at the given Unix timestamp in milliseconds. UnlockAtMs(u64), + /// Remains locked until the protected object is explicitly destroyed. UntilDestroyed, + /// Represents an always-locked state. Infinite, + /// Disables the time lock. #[default] None, } @@ -110,12 +118,17 @@ impl TimeLock { /// Defines a locking window (none, time based, or count based). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum LockingWindow { + /// No delete window is enforced. #[default] None, + /// Records may be deleted only within the given number of seconds since creation. TimeBased { + /// Window size in seconds. seconds: u64, }, + /// Records may be deleted only within the first `count` subsequent records. CountBased { + /// Number of subsequent records after which deletion is no longer allowed. count: u64, }, } diff --git a/audit-trail-rs/src/core/types/mod.rs b/audit-trail-rs/src/core/types/mod.rs index ef9dfdcc..486299f2 100644 --- a/audit-trail-rs/src/core/types/mod.rs +++ b/audit-trail-rs/src/core/types/mod.rs @@ -1,13 +1,22 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Core data types for audit trail. +//! Shared serializable domain types for audit trails. +//! +//! These types stay close to the on-chain data model so they can deserialize ledger state and events while also +//! serving as the typed inputs and outputs of the Rust client API. +/// On-chain trail metadata types. pub mod audit_trail; +/// Event payload types emitted by audit-trail transactions. pub mod event; +/// Locking configuration types. pub mod locking; +/// Permission and permission-set types. pub mod permission; +/// Record payload and pagination types. pub mod record; +/// Role, capability, and role-tag types. pub mod role_map; pub use audit_trail::*; diff --git a/audit-trail-rs/src/core/types/permission.rs b/audit-trail-rs/src/core/types/permission.rs index a7b49ac7..57a50906 100644 --- a/audit-trail-rs/src/core/types/permission.rs +++ b/audit-trail-rs/src/core/types/permission.rs @@ -13,27 +13,46 @@ use serde::{Deserialize, Serialize}; use crate::error::Error; -/// Permission enum matching the Move permission module. +/// Audit-trail permission variants mirrored from the Move permission module. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum Permission { + /// Allows deleting the entire trail. DeleteAuditTrail, + /// Allows deleting all records in batch form. DeleteAllRecords, + /// Allows adding records. AddRecord, + /// Allows deleting individual records. DeleteRecord, + /// Allows creating correction records. CorrectRecord, + /// Allows updating the full locking configuration. UpdateLockingConfig, + /// Allows updating the delete-record window. UpdateLockingConfigForDeleteRecord, + /// Allows updating the delete-trail time lock. UpdateLockingConfigForDeleteTrail, + /// Allows updating the write lock. UpdateLockingConfigForWrite, + /// Allows creating roles. AddRoles, + /// Allows updating roles. UpdateRoles, + /// Allows deleting roles. DeleteRoles, + /// Allows issuing capabilities. AddCapabilities, + /// Allows revoking capabilities. RevokeCapabilities, + /// Allows updating mutable metadata. UpdateMetadata, + /// Allows deleting mutable metadata. DeleteMetadata, + /// Allows migrating the trail to a newer package version. Migrate, + /// Allows adding trail-owned record tags. AddRecordTags, + /// Allows deleting trail-owned record tags. DeleteRecordTags, } @@ -75,9 +94,10 @@ impl Permission { } } -/// Convenience wrapper for permission sets. +/// Convenience wrapper around a set of [`Permission`] values. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct PermissionSet { + /// Permissions granted by this set. pub permissions: HashSet, } @@ -92,6 +112,7 @@ impl PermissionSet { Ok(ptb.command(Command::MakeMoveVec(Some(permission_type.into()), permission_args))) } + /// Returns the recommended role-administration permissions. pub fn admin_permissions() -> Self { Self { permissions: HashSet::from([ @@ -106,6 +127,7 @@ impl PermissionSet { } } + /// Returns the permissions needed to administer records. pub fn record_admin_permissions() -> Self { Self { permissions: HashSet::from([ @@ -116,6 +138,7 @@ impl PermissionSet { } } + /// Returns the permissions needed to administer locking rules. pub fn locking_admin_permissions() -> Self { Self { permissions: HashSet::from([ @@ -127,24 +150,28 @@ impl PermissionSet { } } + /// Returns the permissions needed to administer roles. pub fn role_admin_permissions() -> Self { Self { permissions: HashSet::from([Permission::AddRoles, Permission::UpdateRoles, Permission::DeleteRoles]), } } + /// Returns the permissions needed to administer record tags. pub fn tag_admin_permissions() -> Self { Self { permissions: HashSet::from([Permission::AddRecordTags, Permission::DeleteRecordTags]), } } + /// Returns the permissions needed to issue and revoke capabilities. pub fn cap_admin_permissions() -> Self { Self { permissions: HashSet::from_iter(vec![Permission::AddCapabilities, Permission::RevokeCapabilities]), } } + /// Returns the permissions needed to administer mutable metadata. pub fn metadata_admin_permissions() -> Self { Self { permissions: HashSet::from_iter(vec![Permission::UpdateMetadata, Permission::DeleteMetadata]), diff --git a/audit-trail-rs/src/core/types/record.rs b/audit-trail-rs/src/core/types/record.rs index d20f743d..e18e1ff2 100644 --- a/audit-trail-rs/src/core/types/record.rs +++ b/audit-trail-rs/src/core/types/record.rs @@ -17,32 +17,62 @@ use crate::error::Error; /// Page of records loaded through linked-table traversal. #[derive(Debug, Clone)] pub struct PaginatedRecord { + /// Records included in the current page, keyed by sequence number. pub records: BTreeMap>, + /// Cursor to pass to the next [`TrailRecords::list_page`](crate::core::records::TrailRecords::list_page) call. pub next_cursor: Option, + /// Indicates whether another page may be available. pub has_next_page: bool, } /// A single record in the audit trail. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Record { + /// Record payload stored on-chain. pub data: D, + /// Optional application-defined metadata. pub metadata: Option, + /// Optional trail-owned tag attached to the record. pub tag: Option, + /// Monotonic record sequence number inside the trail. pub sequence_number: u64, + /// Address that added the record. pub added_by: IotaAddress, + /// Millisecond timestamp at which the record was added. pub added_at: u64, + /// Correction relationships for this record. pub correction: RecordCorrection, } /// Input used when creating a trail with an initial record. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct InitialRecord { + /// Initial payload to store in the trail. pub data: D, + /// Optional application-defined metadata. pub metadata: Option, + /// Optional initial tag from the trail-owned registry. pub tag: Option, } impl InitialRecord { + /// Creates a new initial record. + /// + /// # Examples + /// + /// ```rust + /// use audit_trail::core::types::{Data, InitialRecord}; + /// + /// let record = InitialRecord::new( + /// Data::text("hello"), + /// Some("seed".to_string()), + /// Some("inbox".to_string()), + /// ); + /// + /// assert_eq!(record.data, Data::text("hello")); + /// assert_eq!(record.metadata.as_deref(), Some("seed")); + /// assert_eq!(record.tag.as_deref(), Some("inbox")); + /// ``` pub fn new(data: impl Into, metadata: Option, tag: Option) -> Self { Self { data: data.into(), @@ -78,11 +108,14 @@ impl InitialRecord { /// Bidirectional correction tracking for audit records. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RecordCorrection { + /// Sequence numbers that this record supersedes. pub replaces: HashSet, + /// Sequence number of the record that supersedes this one, if any. pub is_replaced_by: Option, } impl RecordCorrection { + /// Creates a correction value that replaces the given sequence numbers. pub fn with_replaces(replaces: HashSet) -> Self { Self { replaces, @@ -90,10 +123,24 @@ impl RecordCorrection { } } + /// Returns `true` when this record supersedes at least one earlier record. + /// + /// # Examples + /// + /// ```rust + /// use std::collections::HashSet; + /// + /// use audit_trail::core::types::RecordCorrection; + /// + /// let correction = RecordCorrection::with_replaces(HashSet::from([1, 2])); + /// + /// assert!(correction.is_correction()); + /// ``` pub fn is_correction(&self) -> bool { !self.replaces.is_empty() } + /// Returns `true` when this record has itself been replaced by a later record. pub fn is_replaced(&self) -> bool { self.is_replaced_by.is_some() } @@ -102,7 +149,9 @@ impl RecordCorrection { /// Supported record data types. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Data { + /// Arbitrary binary payload. Bytes(Vec), + /// UTF-8 text payload. Text(String), } @@ -153,11 +202,27 @@ impl Data { } /// Creates a new `Data` from bytes. + /// + /// # Examples + /// + /// ```rust + /// use audit_trail::core::types::Data; + /// + /// assert_eq!(Data::bytes([1_u8, 2, 3]), Data::Bytes(vec![1, 2, 3])); + /// ``` pub fn bytes(data: impl Into>) -> Self { Self::Bytes(data.into()) } /// Creates a new `Data` from text. + /// + /// # Examples + /// + /// ```rust + /// use audit_trail::core::types::Data; + /// + /// assert_eq!(Data::text("hello"), Data::Text("hello".to_string())); + /// ``` pub fn text(data: impl Into) -> Self { Self::Text(data.into()) } diff --git a/audit-trail-rs/src/core/types/role_map.rs b/audit-trail-rs/src/core/types/role_map.rs index fb3d9cc3..29e45c41 100644 --- a/audit-trail-rs/src/core/types/role_map.rs +++ b/audit-trail-rs/src/core/types/role_map.rs @@ -19,227 +19,88 @@ use crate::core::internal::move_collections::{deserialize_vec_map, deserialize_v use crate::core::internal::tx; use crate::error::Error; -/// The role and capability registry attached to an audit trail. +/// Role and capability configuration stored on a trail. /// -/// A [`RoleMap`] stores every named role defined on the trail, tracks which -/// capabilities have been revoked, and records the administrative permission -/// requirements for role and capability management. -/// -/// ## Roles and capabilities -/// -/// Each entry in [`roles`](RoleMap::roles) maps a role name to a [`Role`] that -/// holds a set of [`Permission`]s and [`RoleTags`]. -/// Each [`Capability`] is associated with exactly one [`Role`] and belongs to a specific [`AuditTrail`] -/// instance which is identified by the [`target_key`](RoleMap::target_key). -/// -/// ## What are Roles -/// -/// A role is a named set of [`Permission`]s, optionally paired with a [`RoleTags`] allowlist. -/// -/// Roles are identified by a unique string name within a trail (e.g., `"RecordAdmin"`, -/// `"Auditor"`, `"LegalReviewer"`). The same role definition can back many independent -/// [`Capability`] objects — to be owned and used by users or system components that should share the same -/// access level. A capability holder may exercise only the permissions of the role it was -/// issued for. -/// -/// ## Initial admin role and capability -/// -/// When a trail is created the Move runtime mints an *initial admin* -/// capability and transfers it to the creator (or the address supplied via -/// `with_admin`). -/// -/// The *initial admin* role name is indicated by [`initial_admin_role_name`]. -/// The role grants permissions specified by [`role_admin_permissions`] and [`capability_admin_permissions`], -/// which are required to manage additional roles and capabilities. -/// -/// ## Create, Delete and Update Roles -/// -/// All three operations are gated by the permissions stored in -/// [`role_admin_permissions`](RoleMap::role_admin_permissions): -/// -/// | Operation | Required permission | Additional constraints | -/// |-----------|--------------------------------------|-----------------------------------------------------------------------------------------------------------------| -/// | Create | `role_admin_permissions.add` | Any [`RoleTags`] specified must be registered in the trail's tag registry. | -/// | Delete | `role_admin_permissions.delete` | The initial admin role (see [`initial_admin_role_name`](RoleMap::initial_admin_role_name)) cannot be deleted. | -/// | Update | `role_admin_permissions.update` | Updating the initial admin role requires the new permission set to still include all configured admin permissions.| -/// -/// The caller supplies a [`Capability`] that is validated before the operation proceeds. -/// An `ECapabilityPermissionDenied` error is returned if the capability's role does not -/// carry the required permission. -/// -/// ## Issue, Revoke, and Destroy Capabilities -/// -/// **Issuing** a capability requires the `capability_admin_permissions.add` permission. -/// [`CapabilityIssueOptions`] allow restricting a newly minted capability further: -/// - `issued_to` — binds the capability to a specific wallet address; the Move runtime rejects use by any other sender. -/// - `valid_from_ms` / `valid_until_ms` — a Unix-millisecond validity window; use outside this range is rejected. -/// -/// **Revoking** a capability requires the `capability_admin_permissions.revoke` permission. -/// Revocation adds the capability's ID to the [`revoked_capabilities`](RoleMap::revoked_capabilities) -/// denylist; the object itself continues to exist on-chain but is refused by -/// `assert_capability_valid`. The caller must provide: -/// - the capability's object ID, and -/// - optionally its `valid_until` value, which allows the denylist entry to be cleaned up automatically once it expires -/// via [`AuditTrailHandle::access().cleanup_revoked_capabilities`]. -/// -/// Because the `RoleMap` uses a denylist (not an allowlist), it does **not** track all -/// issued capabilities on-chain. Callers are responsible for maintaining an off-chain -/// record of issued capability IDs and their validity constraints so that the correct ID -/// can be supplied at revocation time. -/// -/// **Destroying** a capability permanently removes it from the chain. Any holder may -/// destroy their own capability without needing any admin permission — this is intentional -/// so that users can always clean up capabilities they no longer need. Destroying a -/// revoked capability also removes it from the denylist. -/// -/// ## Managing the initial admin role and its capabilities -/// -/// The initial admin role is the only role that exists when a trail is first created. -/// It carries all permissions required to manage roles and capabilities -/// (i.e. everything in [`role_admin_permissions`](RoleMap::role_admin_permissions) and -/// [`capability_admin_permissions`](RoleMap::capability_admin_permissions)). -/// -/// Two invariants protect it from accidental lock-out: -/// - The initial admin **role** can never be deleted. -/// - Updating its permissions is only permitted if the new permission set still includes all configured role and -/// capability admin permissions. -/// -/// Initial admin **capabilities** are tracked separately in -/// [`initial_admin_cap_ids`](RoleMap::initial_admin_cap_ids) and must be managed through -/// dedicated entry-points: -/// - `revoke_initial_admin_capability` — adds the cap to the denylist. -/// - `destroy_initial_admin_capability` — permanently removes the cap from the chain. -/// -/// Attempting to use the generic `revoke_capability` or `destroy_capability` on an initial -/// admin capability returns `EInitialAdminCapabilityMustBeExplicitlyDestroyed`. -/// -/// ## Using Tags -/// Tags are string labels managed by the audit trail using a [`TagRegistry`](super::audit_trail::TagRegistry). -/// The registry acts as a controlled vocabulary: a tag must be registered on the -/// trail before it can be attached to a record or referenced by a role. -/// -/// Each record may carry at most one immutable tag. Tagged Records can only be accessed -/// by users having Capabilities with Roles that allow the tag in their RoleTags. -/// This allows for flexible access control policies based on record tags, -/// i.e. to allow access to specific records only for users in specific departments. -/// -/// Each role may optionally include a [`RoleTags`] allowlist that grants the holders of that -/// role's capability access to records tagged with that specific tag. +/// This mirrors the access-control state maintained by the Move package, including the reserved initial-admin +/// role, the revoked-capability denylist, and the role data used for tag-aware authorization. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoleMap { - /// The object ID of the audit trail this role map belongs to. + /// Trail object ID that this role map protects. pub target_key: ObjectID, - /// All named roles defined on the trail, keyed by role name. + /// Role definitions keyed by role name. #[serde(deserialize_with = "deserialize_vec_map")] pub roles: HashMap, - /// Name of the built-in admin role created automatically at trail creation - /// (typically `"Admin"`). + /// Reserved role name used for initial-admin capabilities. pub initial_admin_role_name: String, - /// Set of capability IDs that have been revoked and must no longer be - /// accepted by the Move runtime. + /// Denylist of revoked capability IDs. pub revoked_capabilities: LinkedTable, - /// Object IDs of the initial admin capabilities minted at trail creation. - /// These require dedicated revoke/destroy entry-points. + /// Capability IDs currently recognized as initial-admin capabilities. #[serde(deserialize_with = "deserialize_vec_set")] pub initial_admin_cap_ids: HashSet, - /// Permissions required to add, update, and delete roles on this trail. + /// Permissions required to administer roles. pub role_admin_permissions: RoleAdminPermissions, - /// Permissions required to issue and revoke capabilities on this trail. + /// Permissions required to administer capabilities. pub capability_admin_permissions: CapabilityAdminPermissions, } -/// A single role definition within a [`RoleMap`]. -/// -/// Each role combines a permission set that governs what operations holders may -/// perform, and optional [`RoleTags`] data that restricts which tagged records -/// those holders may interact with. +/// Role definition stored in the trail role map. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Role { - /// The set of [`Permission`]s granted to any [`Capability`] issued for this role. + /// Permissions granted by the role. #[serde(deserialize_with = "deserialize_vec_set")] pub permissions: HashSet, - /// Optional tag allowlist. When present, a capability holder for this role may only - /// add or access records whose tag is contained in this set. When `None`, the role - /// does not impose any tag-based restriction (but untagged-record permissions still - /// apply). + /// Optional role-scoped record-tag restrictions. pub data: Option, } -/// Defines the permissions required to administer roles in this [`RoleMap`]. -/// -/// When a capability holder attempts to create, delete, or update a role, the -/// `RoleMap` checks that the holder's role includes the corresponding permission -/// listed here. +/// Permissions required to administer roles in the trail's access-control state. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoleAdminPermissions { - /// The [`Permission`] a capability must carry to create a new role - /// (typically [`Permission::AddRoles`]). + /// Permission required to create roles. pub add: Permission, - /// The [`Permission`] a capability must carry to delete an existing role - /// (typically [`Permission::DeleteRoles`]). + /// Permission required to delete roles. pub delete: Permission, - /// The [`Permission`] a capability must carry to update the permissions or - /// tags of an existing role (typically [`Permission::UpdateRoles`]). + /// Permission required to update roles. pub update: Permission, } -/// Defines the permissions required to administer capabilities in this [`RoleMap`]. -/// -/// When a capability holder attempts to issue or revoke a capability, the -/// `RoleMap` checks that the holder's role includes the corresponding permission -/// listed here. +/// Permissions required to administer capabilities in the trail's access-control state. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CapabilityAdminPermissions { - /// The [`Permission`] a capability must carry to issue (mint) a new capability - /// (typically [`Permission::AddCapabilities`]). + /// Permission required to issue capabilities. pub add: Permission, - /// The [`Permission`] a capability must carry to revoke an existing capability - /// or to clean up the revoked-capabilities denylist - /// (typically [`Permission::RevokeCapabilities`]). + /// Permission required to revoke capabilities. pub revoke: Permission, } -/// Options for constraining a newly issued [`Capability`]. +/// Capability issuance options used by the role-based API. /// -/// All fields default to `None` (no restriction). Use [`Default::default()`] -/// to issue an unrestricted capability, or populate individual fields to add -/// constraints. +/// These fields only configure restrictions on the issued capability object. Matching against the current +/// caller and timestamp happens when the capability is later used. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct CapabilityIssueOptions { - /// If set, only the specified address may present the capability. The Move - /// runtime rejects any transaction from a different sender with - /// `ECapabilityIssuedToMismatch`. + /// Address that should own the capability, if any. pub issued_to: Option, - /// If set, the capability is not valid before this Unix timestamp - /// (milliseconds since epoch). Transactions submitted before this time - /// are rejected with `ECapabilityTimeConstraintsNotMet`. + /// Millisecond timestamp at which the capability becomes valid. pub valid_from_ms: Option, - /// If set, the capability expires after this Unix timestamp (milliseconds - /// since epoch). Transactions submitted after this time are rejected with - /// `ECapabilityTimeConstraintsNotMet`. + /// Millisecond timestamp at which the capability expires. pub valid_until_ms: Option, } -/// An allowlist of record tag names that may be attached to a [`Role`]. +/// Allowlisted record tags stored as role data. /// -/// When a role carries a `RoleTags` value, capability holders for that role may -/// only add or interact with records whose tag is contained in [`tags`](RoleTags::tags). -/// This maps to the Move `record_tags::RoleTags` type. +/// The Rust name stays `RecordTags` for API continuity, but it maps to Move `record_tags::RoleTags`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RoleTags { - /// The set of tag names this role is allowed to use. Every tag listed here - /// must be registered in the trail's tag registry before the role is created - /// or updated. + /// Allowlisted record tags for the role. #[serde(deserialize_with = "deserialize_vec_set")] pub tags: HashSet, } impl RoleTags { - /// Creates a new [`RoleTags`] from any iterator of string-like items. - /// - /// # Arguments + /// Creates role-tag restrictions from an iterator of tag names. /// - /// * `tags` — an iterator of tag names (e.g., `["finance", "legal"]`). + /// The set is deduplicated, and PTB encoding later sorts the tags for deterministic serialization. pub fn new(tags: I) -> Self where I: IntoIterator, @@ -250,11 +111,7 @@ impl RoleTags { } } - /// Returns `true` if `tag` is present in this allowlist. - /// - /// # Arguments - /// - /// * `tag` — the record tag name to check. + /// Returns `true` when the given tag is allowed for the role. pub fn allows(&self, tag: &str) -> bool { self.tags.contains(tag) } @@ -278,49 +135,33 @@ impl RoleTags { } } -/// An on-chain capability object deserialized from the Move `capability::Capability` type. +/// Capability data returned by the Move capability module. /// -/// A capability grants its holder the permissions of the [`Role`] identified by -/// [`role`](Capability::role) on the trail identified by -/// [`target_key`](Capability::target_key). The `RoleMap` validates all fields -/// of the capability before allowing any operation to proceed. +/// A capability grants exactly one role against exactly one trail and may additionally restrict who may use it +/// and during which time window it is valid. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Capability { - /// The unique on-chain object ID of this capability. + /// Capability object ID. pub id: UID, - /// The object ID of the audit trail this capability is valid for. - /// Must match the [`RoleMap::target_key`] of the trail being accessed. + /// Trail object ID protected by the capability. pub target_key: ObjectID, - /// The name of the role this capability was issued for (e.g., `"Admin"`, - /// `"RecordAdmin"`). Determines the set of [`Permission`]s the holder may - /// exercise. + /// Role granted by the capability. pub role: String, - /// Optional address binding. When set, only the specified address may - /// present this capability; any other sender is rejected. + /// Capability holder, if the capability is assigned to an address. pub issued_to: Option, - /// Optional start of the validity window (Unix milliseconds). The - /// capability is rejected before this timestamp. + /// Millisecond timestamp at which the capability becomes valid. #[serde(deserialize_with = "deserialize_option_number_from_string")] pub valid_from: Option, - /// Optional end of the validity window (Unix milliseconds). The capability - /// is rejected after this timestamp. + /// Millisecond timestamp at which the capability expires. #[serde(deserialize_with = "deserialize_option_number_from_string")] pub valid_until: Option, } impl Capability { - /// Returns the Move `TypeTag` for `capability::Capability` in the given package. pub(crate) fn type_tag(package_id: ObjectID) -> TypeTag { TypeTag::from_str(format!("{package_id}::capability::Capability").as_str()).expect("failed to create type tag") } - /// Returns `true` if this capability targets the given trail and its role is - /// contained in `valid_roles`. - /// - /// # Arguments - /// - /// * `trail_id` — the object ID of the trail to match against. - /// * `valid_roles` — the set of role names considered acceptable. pub(crate) fn matches_target_and_role(&self, trail_id: ObjectID, valid_roles: &HashSet) -> bool { self.target_key == trail_id && valid_roles.contains(&self.role) } @@ -331,3 +172,53 @@ impl MoveType for Capability { Self::type_tag(package) } } + +#[cfg(test)] +mod tests { + use iota_interaction::types::base_types::{IotaAddress, dbg_object_id}; + use iota_interaction::types::id::UID; + use serde_json::json; + + use super::Capability; + + #[test] + fn capability_deserializes_string_encoded_time_constraints() { + let issued_to = IotaAddress::random_for_testing_only(); + let capability = Capability { + id: UID::new(dbg_object_id(1)), + target_key: dbg_object_id(2), + role: "Writer".to_string(), + issued_to: Some(issued_to), + valid_from: None, + valid_until: None, + }; + + let mut value = serde_json::to_value(capability).expect("capability serializes"); + value["valid_from"] = json!("1700000000000"); + value["valid_until"] = json!("1700000005000"); + + let decoded: Capability = serde_json::from_value(value).expect("capability deserializes"); + + assert_eq!(decoded.valid_from, Some(1_700_000_000_000)); + assert_eq!(decoded.valid_until, Some(1_700_000_005_000)); + assert_eq!(decoded.issued_to, Some(issued_to)); + } + + #[test] + fn capability_deserializes_absent_time_constraints() { + let capability = Capability { + id: UID::new(dbg_object_id(4)), + target_key: dbg_object_id(5), + role: "Writer".to_string(), + issued_to: None, + valid_from: None, + valid_until: None, + }; + + let value = serde_json::to_value(capability).expect("capability serializes"); + let decoded: Capability = serde_json::from_value(value).expect("capability deserializes"); + + assert_eq!(decoded.valid_from, None); + assert_eq!(decoded.valid_until, None); + } +} diff --git a/audit-trail-rs/src/error.rs b/audit-trail-rs/src/error.rs index 79f75834..af81958a 100644 --- a/audit-trail-rs/src/error.rs +++ b/audit-trail-rs/src/error.rs @@ -1,34 +1,36 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Error types returned by the audit-trail public API. + use crate::iota_interaction_adapter::AdapterError; -/// Errors that can occur when managing Audit Trails +/// Errors that can occur when reading or mutating audit trails. #[derive(Debug, thiserror::Error, strum::IntoStaticStr)] #[non_exhaustive] pub enum Error { - /// Caused by invalid keys. + /// Returned when a signer key or public key cannot be derived or validated. #[error("invalid key: {0}")] InvalidKey(String), - /// Config is invalid. + /// Returned when client configuration or package-ID configuration is invalid. #[error("invalid config: {0}")] InvalidConfig(String), - /// An error caused by either a connection issue or an invalid RPC call. + /// Returned when an RPC request fails. #[error("RPC error: {0}")] RpcError(String), - /// The provided IOTA Client returned an error + /// Error returned by the underlying IOTA client adapter. #[error("IOTA client error: {0}")] IotaClient(#[from] AdapterError), - /// Generic error + /// Generic catch-all error for crate-specific failures that do not fit a narrower variant. #[error("{0}")] GenericError(String), /// Placeholder for unimplemented API surface. #[error("not implemented: {0}")] NotImplemented(&'static str), - /// Failed to parse tag + /// Returned when a Move tag cannot be parsed. #[error("Failed to parse tag: {0}")] FailedToParseTag(String), - /// Invalid argument + /// Returned when an argument is semantically invalid. #[error("Invalid argument: {0}")] InvalidArgument(String), /// The response from the IOTA node API was not in the expected format. @@ -37,7 +39,7 @@ pub enum Error { /// Failed to deserialize data using BCS. #[error("BCS deserialization error: {0}")] DeserializationError(#[from] bcs::Error), - /// The response from the IOTA node API was not in the expected format. + /// The transaction response from the IOTA node API was not in the expected format. #[error("unexpected transaction response: {0}")] TransactionUnexpectedResponse(String), } diff --git a/audit-trail-rs/src/iota_interaction_adapter.rs b/audit-trail-rs/src/iota_interaction_adapter.rs index ec0d9f0a..c2db171b 100644 --- a/audit-trail-rs/src/iota_interaction_adapter.rs +++ b/audit-trail-rs/src/iota_interaction_adapter.rs @@ -1,9 +1,10 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -// The following platform compile switch provides all the -// ...Adapter types from iota_interaction_rust or iota_interaction_ts -// like IotaClientAdapter, TransactionBuilderAdapter ... and so on +//! Platform-dependent adapter re-exports for the underlying IOTA interaction layer. +//! +//! This keeps the rest of the crate generic over native and wasm targets by exposing the same +//! adapter names from either `iota_interaction_rust` or `iota_interaction_ts`. #[cfg(not(target_arch = "wasm32"))] pub(crate) use iota_interaction_rust::*; diff --git a/audit-trail-rs/src/lib.rs b/audit-trail-rs/src/lib.rs index 2c8f8644..82f6f73e 100644 --- a/audit-trail-rs/src/lib.rs +++ b/audit-trail-rs/src/lib.rs @@ -1,13 +1,21 @@ // Copyright 2020-2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#![doc = include_str!("../README.md")] +#![warn(missing_docs, rustdoc::all)] + +/// Client wrappers for read-only and signing access to audit trails. pub mod client; +/// Core handles, builders, transactions, and domain types. pub mod core; +/// Error types returned by the public API. pub mod error; pub(crate) mod iota_interaction_adapter; pub(crate) mod package; +/// A signing audit-trail client that can build write transactions. pub use client::full_client::AuditTrailClient; +/// Read-only client types and package override configuration. pub use client::read_only::{AuditTrailClientReadOnly, PackageOverrides}; /// HTTP utilities to implement the trait [HttpClient](product_common::http_client::HttpClient). #[cfg(feature = "gas-station")] diff --git a/audit-trail-rs/src/package.rs b/audit-trail-rs/src/package.rs index 4818f56b..0d443bb7 100644 --- a/audit-trail-rs/src/package.rs +++ b/audit-trail-rs/src/package.rs @@ -85,7 +85,7 @@ pub(crate) async fn resolve_package_ids( let chain_id = network.as_ref().to_string(); let package_registry = audit_trail_package_registry().await; let audit_trail_package_id = package_overrides - .audit_trail_package_id + .audit_trail .or_else(|| package_registry.package_id(network)) .ok_or_else(|| { Error::InvalidConfig(format!( @@ -105,12 +105,12 @@ pub(crate) async fn resolve_package_ids( drop(package_registry); let env = Env::new_with_alias(chain_id.clone(), resolved_network.as_ref()); - if let Some(audit_trail_package_id) = package_overrides.audit_trail_package_id { + if let Some(audit_trail_package_id) = package_overrides.audit_trail { audit_trail_package_registry_mut() .await .insert_env_history(env.clone(), vec![audit_trail_package_id]); } - if let Some(tf_components_package_id) = package_overrides.tf_components_package_id { + if let Some(tf_components_package_id) = package_overrides.tf_component { tf_components_override_registry_mut() .await .insert_env_history(env, vec![tf_components_package_id]); diff --git a/audit-trail-rs/tests/e2e/access.rs b/audit-trail-rs/tests/e2e/access.rs index cfcff048..94b4e2d0 100644 --- a/audit-trail-rs/tests/e2e/access.rs +++ b/audit-trail-rs/tests/e2e/access.rs @@ -73,6 +73,89 @@ async fn update_role_permissions_then_issue_capability() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn delegated_role_and_capability_admins_can_enable_record_writes() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let role_admin = get_funded_test_client().await?; + let cap_admin = get_funded_test_client().await?; + let record_admin = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("delegated-access-flow")).await?; + + admin + .create_role( + trail_id, + "RoleAdmin", + PermissionSet::role_admin_permissions().permissions, + None, + ) + .await?; + admin + .create_role( + trail_id, + "CapAdmin", + PermissionSet::cap_admin_permissions().permissions, + None, + ) + .await?; + admin + .issue_cap( + trail_id, + "RoleAdmin", + CapabilityIssueOptions { + issued_to: Some(role_admin.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + admin + .issue_cap( + trail_id, + "CapAdmin", + CapabilityIssueOptions { + issued_to: Some(cap_admin.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + role_admin + .create_role( + trail_id, + "RecordAdmin", + PermissionSet::record_admin_permissions().permissions, + None, + ) + .await?; + cap_admin + .issue_cap( + trail_id, + "RecordAdmin", + CapabilityIssueOptions { + issued_to: Some(record_admin.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + let added = record_admin + .trail(trail_id) + .records() + .add(Data::text("delegated write"), None, None) + .build_and_execute(&record_admin) + .await? + .output; + + assert_eq!(added.trail_id, trail_id); + assert_eq!(added.sequence_number, 1); + + let record = admin.trail(trail_id).records().get(1).await?; + assert_eq!(record.sequence_number, 1); + assert_eq!(record.added_by, record_admin.sender_address()); + assert_eq!(record.data, Data::text("delegated write")); + + Ok(()) +} + #[tokio::test] async fn create_role_rejects_undefined_role_tags() -> anyhow::Result<()> { let client = get_funded_test_client().await?; @@ -129,6 +212,122 @@ async fn update_role_permissions_rejects_undefined_role_tags() -> anyhow::Result Ok(()) } +#[tokio::test] +async fn issue_capability_for_nonexistent_role_fails() -> anyhow::Result<()> { + let client = get_funded_test_client().await?; + let trail_id = client.create_test_trail(Data::text("missing-role-cap")).await?; + + let issued = client + .trail(trail_id) + .access() + .for_role("NonExistentRole") + .issue_capability(CapabilityIssueOptions::default()) + .build_and_execute(&client) + .await; + + assert!(issued.is_err(), "issuing a capability for a missing role must fail"); + + Ok(()) +} + +#[tokio::test] +async fn issue_capability_requires_add_capabilities_permission() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let operator = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("missing-cap-permission")).await?; + + admin + .create_role(trail_id, "NoCapPerm", vec![Permission::AddRecord], None) + .await?; + admin + .create_role( + trail_id, + "RecordAdmin", + PermissionSet::record_admin_permissions().permissions, + None, + ) + .await?; + admin + .issue_cap( + trail_id, + "NoCapPerm", + CapabilityIssueOptions { + issued_to: Some(operator.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + let issued = operator + .trail(trail_id) + .access() + .for_role("RecordAdmin") + .issue_capability(CapabilityIssueOptions::default()) + .build_and_execute(&operator) + .await; + + assert!( + issued.is_err(), + "issuing a capability without AddCapabilities permission must fail" + ); + + Ok(()) +} + +#[tokio::test] +async fn revoke_capability_requires_revoke_capabilities_permission() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let no_revoke = get_funded_test_client().await?; + let target = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("missing-revoke-permission")).await?; + + admin + .create_role(trail_id, "NoRevokePerm", vec![Permission::AddRecord], None) + .await?; + admin + .create_role( + trail_id, + "RecordAdmin", + PermissionSet::record_admin_permissions().permissions, + None, + ) + .await?; + admin + .issue_cap( + trail_id, + "NoRevokePerm", + CapabilityIssueOptions { + issued_to: Some(no_revoke.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + let target_cap = admin + .issue_cap( + trail_id, + "RecordAdmin", + CapabilityIssueOptions { + issued_to: Some(target.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + let revoked = no_revoke + .trail(trail_id) + .access() + .revoke_capability(target_cap.capability_id, target_cap.valid_until) + .build_and_execute(&no_revoke) + .await; + + assert!( + revoked.is_err(), + "revoking a capability without RevokeCapabilities permission must fail" + ); + + Ok(()) +} + #[tokio::test] async fn delete_role_prevents_new_capability_issuance() -> anyhow::Result<()> { let client = get_funded_test_client().await?; diff --git a/audit-trail-rs/tests/e2e/client.rs b/audit-trail-rs/tests/e2e/client.rs index 2b88f6d0..0bca63b4 100644 --- a/audit-trail-rs/tests/e2e/client.rs +++ b/audit-trail-rs/tests/e2e/client.rs @@ -172,8 +172,8 @@ impl TestClient { let client = AuditTrailClient::from_iota_client( iota_client.clone(), Some(PackageOverrides { - audit_trail_package_id: Some(package_ids.audit_trail_package_id), - tf_components_package_id: package_ids.tf_components_package_id, + audit_trail: Some(package_ids.audit_trail_package_id), + tf_component: package_ids.tf_components_package_id, }), ) .await?; diff --git a/audit-trail-rs/tests/e2e/records.rs b/audit-trail-rs/tests/e2e/records.rs index 46bab339..ea798b9c 100644 --- a/audit-trail-rs/tests/e2e/records.rs +++ b/audit-trail-rs/tests/e2e/records.rs @@ -9,6 +9,7 @@ use audit_trail::core::types::{ use audit_trail::error::Error; use iota_interaction::types::base_types::ObjectID; use product_common::core_client::CoreClient; +use tokio::time::{Duration, sleep}; use crate::client::{TestClient, get_funded_test_client}; @@ -174,6 +175,38 @@ async fn add_tagged_record_requires_trail_defined_tag() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn add_record_requires_add_record_permission() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let writer = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("records-add-permission")).await?; + let records = writer.trail(trail_id).records(); + + admin + .create_role(trail_id, "NoAddRecord", [Permission::DeleteRecord], None) + .await?; + admin + .issue_cap( + trail_id, + "NoAddRecord", + CapabilityIssueOptions { + issued_to: Some(writer.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + let denied = records + .add(Data::text("should fail"), None, None) + .build_and_execute(&writer) + .await; + + assert!(denied.is_err(), "adding without AddRecord permission must fail"); + assert_eq!(admin.trail(trail_id).records().record_count().await?, 1); + + Ok(()) +} + #[tokio::test] async fn add_record_selector_skips_revoked_capability_when_valid_one_exists() -> anyhow::Result<()> { let client = get_funded_test_client().await?; @@ -212,7 +245,52 @@ async fn add_record_selector_skips_revoked_capability_when_valid_one_exists() -> assert_eq!(added.sequence_number, 1); assert_text_data(records.get(1).await?.data, "writer record"); - // Tagged record flow. + Ok(()) +} + +#[tokio::test] +async fn revoked_capability_cannot_add_record_without_fallback() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let writer = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("records-revoked-hard-fail")).await?; + let records = writer.trail(trail_id).records(); + let role_name = "RecordWriter"; + + admin + .create_role(trail_id, role_name, [Permission::AddRecord], None) + .await?; + let issued = admin + .issue_cap( + trail_id, + role_name, + CapabilityIssueOptions { + issued_to: Some(writer.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + admin + .trail(trail_id) + .access() + .revoke_capability(issued.capability_id, issued.valid_until) + .build_and_execute(&admin) + .await?; + + let denied = records + .add(Data::text("should fail"), None, None) + .build_and_execute(&writer) + .await; + + assert!(denied.is_err(), "revoked capabilities must not authorize writes"); + assert_eq!(admin.trail(trail_id).records().record_count().await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn add_tagged_record_skips_revoked_capability_when_valid_one_exists() -> anyhow::Result<()> { + let client = get_funded_test_client().await?; let tagged_trail_id = client .create_test_trail_with_tags(Data::text("records-revoked-tagged"), ["finance"]) .await?; @@ -477,6 +555,98 @@ async fn add_record_using_capability_uses_selected_capability_without_fallback() Ok(()) } +#[tokio::test] +async fn add_record_respects_valid_from_constraint() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let writer = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("records-valid-from")).await?; + let records = writer.trail(trail_id).records(); + let role_name = "RecordWriter"; + + admin + .create_role(trail_id, role_name, [Permission::AddRecord], None) + .await?; + let valid_from_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_millis() as u64 + + 15_000; + admin + .issue_cap( + trail_id, + role_name, + CapabilityIssueOptions { + issued_to: Some(writer.sender_address()), + valid_from_ms: Some(valid_from_ms), + valid_until_ms: None, + }, + ) + .await?; + + let denied = records + .add(Data::text("too early"), None, None) + .build_and_execute(&writer) + .await; + assert!(denied.is_err(), "writes before valid_from must fail"); + + sleep(Duration::from_secs(16)).await; + + let added = records + .add(Data::text("on time"), None, None) + .build_and_execute(&writer) + .await? + .output; + + assert_eq!(added.sequence_number, 1); + assert_text_data(records.get(1).await?.data, "on time"); + + Ok(()) +} + +#[tokio::test] +async fn add_record_respects_valid_until_constraint() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let writer = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("records-valid-until")).await?; + let records = writer.trail(trail_id).records(); + let role_name = "RecordWriter"; + + admin + .create_role(trail_id, role_name, [Permission::AddRecord], None) + .await?; + let valid_until_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_millis() as u64 + + 15_000; + admin + .issue_cap( + trail_id, + role_name, + CapabilityIssueOptions { + issued_to: Some(writer.sender_address()), + valid_from_ms: None, + valid_until_ms: Some(valid_until_ms), + }, + ) + .await?; + + let added = records + .add(Data::text("before expiry"), None, None) + .build_and_execute(&writer) + .await? + .output; + assert_eq!(added.sequence_number, 1); + + sleep(Duration::from_secs(16)).await; + + let denied = records + .add(Data::text("after expiry"), None, None) + .build_and_execute(&writer) + .await; + assert!(denied.is_err(), "writes after valid_until must fail"); + + Ok(()) +} + #[tokio::test] async fn add_record_allows_mixed_data_variants() -> anyhow::Result<()> { let client = get_funded_test_client().await?; @@ -850,6 +1020,37 @@ async fn delete_records_batch_respects_limit_and_deletes_oldest_first() -> anyho Ok(()) } +#[tokio::test] +async fn delete_records_batch_requires_delete_all_records_permission() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let operator = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("batch-delete-permission")).await?; + let records = operator.trail(trail_id).records(); + + admin + .create_role(trail_id, "TrailDeleteOnly", [Permission::DeleteAuditTrail], None) + .await?; + admin + .issue_cap( + trail_id, + "TrailDeleteOnly", + CapabilityIssueOptions { + issued_to: Some(operator.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + let denied = records.delete_records_batch(10).build_and_execute(&operator).await; + assert!( + denied.is_err(), + "batch deletion must require DeleteAllRecords permission" + ); + assert_eq!(admin.trail(trail_id).records().record_count().await?, 1); + + Ok(()) +} + #[tokio::test] async fn delete_records_batch_requires_matching_role_tag_access() -> anyhow::Result<()> { let client = get_funded_test_client().await?; diff --git a/audit-trail-rs/tests/e2e/trail.rs b/audit-trail-rs/tests/e2e/trail.rs index 5506416d..4ade501b 100644 --- a/audit-trail-rs/tests/e2e/trail.rs +++ b/audit-trail-rs/tests/e2e/trail.rs @@ -347,6 +347,80 @@ async fn update_metadata_does_not_affect_immutable_metadata() -> anyhow::Result< Ok(()) } +#[tokio::test] +async fn update_metadata_requires_permission() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let metadata_user = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("trail-update-meta-denied")).await?; + + admin + .create_role(trail_id, "NoMetadataPerm", vec![Permission::AddRecord], None) + .await?; + admin + .issue_cap( + trail_id, + "NoMetadataPerm", + CapabilityIssueOptions { + issued_to: Some(metadata_user.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + let updated = metadata_user + .trail(trail_id) + .update_metadata(Some("should fail".to_string())) + .build_and_execute(&metadata_user) + .await; + + assert!( + updated.is_err(), + "updating metadata without UpdateMetadata permission must fail" + ); + assert_eq!(admin.trail(trail_id).get().await?.updatable_metadata, None); + + Ok(()) +} + +#[tokio::test] +async fn revoked_capability_cannot_update_metadata() -> anyhow::Result<()> { + let admin = get_funded_test_client().await?; + let metadata_user = get_funded_test_client().await?; + let trail_id = admin.create_test_trail(Data::text("trail-update-meta-revoked")).await?; + + admin + .create_role(trail_id, "MetadataAdmin", vec![Permission::UpdateMetadata], None) + .await?; + let issued = admin + .issue_cap( + trail_id, + "MetadataAdmin", + CapabilityIssueOptions { + issued_to: Some(metadata_user.sender_address()), + ..CapabilityIssueOptions::default() + }, + ) + .await?; + + admin + .trail(trail_id) + .access() + .revoke_capability(issued.capability_id, issued.valid_until) + .build_and_execute(&admin) + .await?; + + let updated = metadata_user + .trail(trail_id) + .update_metadata(Some("should fail".to_string())) + .build_and_execute(&metadata_user) + .await; + + assert!(updated.is_err(), "revoked capabilities must not update metadata"); + assert_eq!(admin.trail(trail_id).get().await?.updatable_metadata, None); + + Ok(()) +} + #[tokio::test] async fn delete_audit_trail_fails_when_records_exist() -> anyhow::Result<()> { let client = get_funded_test_client().await?; diff --git a/bindings/wasm/audit_trail_wasm/README.md b/bindings/wasm/audit_trail_wasm/README.md index b06586e7..7a9fcf75 100644 --- a/bindings/wasm/audit_trail_wasm/README.md +++ b/bindings/wasm/audit_trail_wasm/README.md @@ -1,22 +1,66 @@ -# IOTA Audit Trail WASM Library +# `audit_trail_wasm` -`audit_trail_wasm` provides the Rust-to-WASM bindings for the `audit_trail` crate and is published to JavaScript consumers as `@iota/audit-trail`. +`audit_trail_wasm` exposes the `audit_trail` Rust SDK to JavaScript and TypeScript consumers through `wasm-bindgen`. -The current MVP surface includes: +It is designed for browser and other `wasm32` environments that need: + +- read-only and signing audit-trail clients +- typed wrappers for trail handles, records, locking, access control, and tags +- serializable value and event types that map cleanly into JS/TS +- transaction wrappers that integrate with the shared `product_common` wasm transaction helpers + +## Main entry points + +- `AuditTrailClientReadOnly` for reads and inspected transactions +- `AuditTrailClient` for signed write flows +- `AuditTrailBuilder` for creating new trails +- `AuditTrailHandle` for trail-scoped APIs +- `TrailRecords`, `TrailLocking`, `TrailAccess`, and `TrailTags` for subsystem-specific operations + +## Choosing an entry point + +- Use `AuditTrailClientReadOnly` when you need reads, package resolution, or inspected transactions. +- Use `AuditTrailClient` when you also need typed write transaction builders. +- Use `AuditTrailHandle` after you already know the trail object ID and want to stay scoped to that trail. +- Use `AuditTrailBuilder` when you are preparing a create-trail transaction. + +## Data model wrappers + +The bindings expose JS-friendly wrappers for the most important Rust value types: -- `AuditTrailClientReadOnly` -- `AuditTrailClient` -- `AuditTrailBuilder` -- `AuditTrailHandle` -- `TrailRecords` - `Data` -- `Record` -- `PaginatedRecord` -- `OnChainAuditTrail` -- `ImmutableMetadata` -- `LockingConfig` -- `LockingWindow` -- `TimeLock` +- `Permission` and `PermissionSet` +- `RoleTags`, `RoleMap`, and `CapabilityIssueOptions` +- `TimeLock`, `LockingWindow`, and `LockingConfig` +- `Record`, `PaginatedRecord`, and `OnChainAuditTrail` +- event payloads such as `RecordAdded`, `RoleCreated`, and `CapabilityIssued` + +## Typical read flow + +1. Create an `AuditTrailClientReadOnly` or `AuditTrailClient`. +2. Resolve a trail handle with `.trail(trailId)`. +3. Read state with `.get()`, `.records().get(...)`, `.records().listPage(...)`, or `.locking().isRecordLocked(...)`. + +## Typical write flow + +1. Create an `AuditTrailClient` with a transaction signer. +2. Build a transaction from `client.createTrail()`, `client.trail(trailId)`, or one of the trail subsystem handles. +3. Convert that transaction wrapper into programmable transaction bytes. +4. Submit it through your surrounding JS transaction flow and feed the effects and events back into the typed `applyWithEvents(...)` helper. + +The bindings intentionally separate transaction construction from submission so browser apps, wallet integrations, and server-side signing flows can keep transport and execution policy outside the SDK. + +## Minimal TypeScript shape + +```ts +import { AuditTrailClientReadOnly } from "@iota/audit-trail-wasm"; + +const client = await AuditTrailClientReadOnly.create(iotaClient); +const trail = client.trail(trailId); +const state = await trail.get(); + +console.log(state.sequenceNumber); +``` ## Build @@ -27,4 +71,4 @@ npm run build ## Examples -See [examples/README.md](./examples/README.md) for the node example flows. +See [examples/README.md](./examples/README.md) for runnable node and web example flows. diff --git a/bindings/wasm/audit_trail_wasm/src/builder.rs b/bindings/wasm/audit_trail_wasm/src/builder.rs index c79b18f8..0db57672 100644 --- a/bindings/wasm/audit_trail_wasm/src/builder.rs +++ b/bindings/wasm/audit_trail_wasm/src/builder.rs @@ -11,16 +11,19 @@ use wasm_bindgen::prelude::*; use crate::trail::WasmCreateTrail; use crate::types::WasmLockingConfig; +/// Trail-creation builder exposed to wasm consumers. #[wasm_bindgen(js_name = AuditTrailBuilder, inspectable)] pub struct WasmAuditTrailBuilder(pub(crate) AuditTrailBuilder); #[wasm_bindgen(js_class = AuditTrailBuilder)] impl WasmAuditTrailBuilder { + /// Sets the initial record using a UTF-8 string payload. #[wasm_bindgen(js_name = withInitialRecordString)] pub fn with_initial_record_string(self, data: String, metadata: Option, tag: Option) -> Self { Self(self.0.with_initial_record_parts(data, metadata, tag)) } + /// Sets the initial record using raw bytes. #[wasm_bindgen(js_name = withInitialRecordBytes)] pub fn with_initial_record_bytes( self, @@ -31,32 +34,38 @@ impl WasmAuditTrailBuilder { Self(self.0.with_initial_record_parts(data.to_vec(), metadata, tag)) } + /// Sets immutable metadata for the trail. #[wasm_bindgen(js_name = withTrailMetadata)] pub fn with_trail_metadata(self, name: String, description: Option) -> Self { Self(self.0.with_trail_metadata_parts(name, description)) } + /// Sets mutable metadata for the trail. #[wasm_bindgen(js_name = withUpdatableMetadata)] pub fn with_updatable_metadata(self, metadata: String) -> Self { Self(self.0.with_updatable_metadata(metadata)) } + /// Sets the locking configuration for the trail. #[wasm_bindgen(js_name = withLockingConfig)] pub fn with_locking_config(self, config: WasmLockingConfig) -> Self { Self(self.0.with_locking_config(config.into())) } + /// Sets the canonical list of record tags owned by the trail. #[wasm_bindgen(js_name = withRecordTags)] pub fn with_record_tags(self, tags: Vec) -> Self { Self(self.0.with_record_tags(tags)) } + /// Sets the initial admin address. #[wasm_bindgen(js_name = withAdmin)] pub fn with_admin(self, admin: WasmIotaAddress) -> Result { let admin = parse_wasm_iota_address(&admin)?; Ok(Self(self.0.with_admin(admin))) } + /// Finalizes the builder into a transaction wrapper. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn finish(self) -> Result { Ok(into_transaction_builder(WasmCreateTrail::new(self))) diff --git a/bindings/wasm/audit_trail_wasm/src/client.rs b/bindings/wasm/audit_trail_wasm/src/client.rs index a09a5ea1..acc65f13 100644 --- a/bindings/wasm/audit_trail_wasm/src/client.rs +++ b/bindings/wasm/audit_trail_wasm/src/client.rs @@ -14,12 +14,17 @@ use crate::builder::WasmAuditTrailBuilder; use crate::client_read_only::{WasmAuditTrailClientReadOnly, WasmPackageOverrides}; use crate::trail_handle::WasmAuditTrailHandle; +/// Signing audit-trail client exposed to wasm consumers. +/// +/// This wraps the read-only client with a transaction signer so JS/TS consumers can build typed +/// write transactions while keeping submission and execution outside the SDK. #[derive(Clone)] #[wasm_bindgen(js_name = AuditTrailClient)] pub struct WasmAuditTrailClient(pub(crate) AuditTrailClient); #[wasm_bindgen(js_class = AuditTrailClient)] impl WasmAuditTrailClient { + /// Creates a signing client from an existing read-only client and signer. #[wasm_bindgen(js_name = create)] pub async fn new( client: WasmAuditTrailClientReadOnly, @@ -29,6 +34,10 @@ impl WasmAuditTrailClient { Ok(Self(client)) } + /// Creates a signing client directly from an IOTA client and signer. + /// + /// Pass `package_id` when connecting to a custom deployment that is not known to the package + /// registry. #[wasm_bindgen(js_name = createFromIotaClient)] pub async fn create_from_iota_client( iota_client: WasmIotaClient, @@ -40,8 +49,8 @@ impl WasmAuditTrailClient { AuditTrailClientReadOnly::new_with_package_overrides( iota_client, PackageOverrides { - audit_trail_package_id: Some(package_id), - tf_components_package_id: None, + audit_trail: Some(package_id), + tf_component: None, }, ) .await @@ -54,6 +63,7 @@ impl WasmAuditTrailClient { Ok(Self(client)) } + /// Creates a signing client directly from an IOTA client, signer, and full package overrides. #[wasm_bindgen(js_name = createFromIotaClientWithPackageOverrides)] pub async fn create_from_iota_client_with_package_overrides( iota_client: WasmIotaClient, @@ -73,36 +83,43 @@ impl WasmAuditTrailClient { Ok(Self(client)) } + /// Returns the sender public key associated with the signer. #[wasm_bindgen(js_name = senderPublicKey)] pub fn sender_public_key(&self) -> Result { self.0.public_key().try_into() } + /// Returns the sender address associated with the signer. #[wasm_bindgen(js_name = senderAddress)] pub fn sender_address(&self) -> String { self.0.address().to_string() } + /// Returns the connected network name. #[wasm_bindgen] pub fn network(&self) -> String { self.0.network().to_string() } + /// Returns the connected chain ID. #[wasm_bindgen(js_name = chainId)] pub fn chain_id(&self) -> String { self.0.chain_id().to_string() } + /// Returns the audit-trail package ID used by this client. #[wasm_bindgen(js_name = packageId)] pub fn package_id(&self) -> String { self.0.package_id().to_string() } + /// Returns the `tf_components` package ID used by this client. #[wasm_bindgen(js_name = tfComponentsPackageId)] pub fn tf_components_package_id(&self) -> String { self.0.tf_components_package_id().to_string() } + /// Returns the resolved audit-trail package history as stringified object IDs. #[wasm_bindgen(js_name = packageHistory)] pub fn package_history(&self) -> Vec { self.0 @@ -112,16 +129,19 @@ impl WasmAuditTrailClient { .collect() } + /// Returns the underlying IOTA client wrapper. #[wasm_bindgen(js_name = iotaClient)] pub fn iota_client(&self) -> WasmIotaClient { self.0.read_only().iota_client().clone().into_inner() } + /// Returns the signer used by this client. #[wasm_bindgen] pub fn signer(&self) -> WasmTransactionSigner { self.0.signer().clone() } + /// Replaces the signer used by this client. #[wasm_bindgen(js_name = withSigner)] pub async fn with_signer(self, signer: WasmTransactionSigner) -> Result { let client = self @@ -132,16 +152,27 @@ impl WasmAuditTrailClient { Ok(Self(client)) } + /// Returns the read-only view of this client. + /// + /// This is useful when a caller wants to pass the client into code that only needs read + /// capabilities. #[wasm_bindgen(js_name = readOnly)] pub fn read_only(&self) -> WasmAuditTrailClientReadOnly { WasmAuditTrailClientReadOnly(self.0.read_only().clone()) } + /// Creates a builder for a new audit trail. + /// + /// The builder is pre-populated with the signer address as the initial admin when available. #[wasm_bindgen(js_name = createTrail)] pub fn create_trail(&self) -> WasmAuditTrailBuilder { WasmAuditTrailBuilder(self.0.create_trail()) } + /// Returns a trail-scoped handle for the given trail object ID. + /// + /// Creating the handle is cheap. Network reads and transaction building happen on the returned + /// handle and its subsystem wrappers. pub fn trail(&self, trail_id: WasmObjectID) -> Result { let trail_id = parse_wasm_object_id(&trail_id)?; Ok(WasmAuditTrailHandle::from_full(self.0.clone(), trail_id)) diff --git a/bindings/wasm/audit_trail_wasm/src/client_read_only.rs b/bindings/wasm/audit_trail_wasm/src/client_read_only.rs index aaa8a716..77d9db49 100644 --- a/bindings/wasm/audit_trail_wasm/src/client_read_only.rs +++ b/bindings/wasm/audit_trail_wasm/src/client_read_only.rs @@ -11,17 +11,21 @@ use wasm_bindgen::prelude::*; use crate::trail_handle::WasmAuditTrailHandle; +/// Package-ID overrides exposed to JavaScript and TypeScript consumers. #[derive(Clone)] #[wasm_bindgen(js_name = PackageOverrides, getter_with_clone, inspectable)] pub struct WasmPackageOverrides { + /// Override for the audit-trail package ID. #[wasm_bindgen(js_name = auditTrailPackageId)] pub audit_trail_package_id: Option, + /// Override for the `tf_components` package ID. #[wasm_bindgen(js_name = tfComponentsPackageId)] pub tf_components_package_id: Option, } #[wasm_bindgen(js_class = PackageOverrides)] impl WasmPackageOverrides { + /// Creates package overrides for custom deployments. #[wasm_bindgen(constructor)] pub fn new( audit_trail_package_id: Option, @@ -39,12 +43,12 @@ impl TryFrom for PackageOverrides { fn try_from(value: WasmPackageOverrides) -> std::result::Result { Ok(Self { - audit_trail_package_id: value + audit_trail: value .audit_trail_package_id .as_ref() .map(parse_wasm_object_id) .transpose()?, - tf_components_package_id: value + tf_component: value .tf_components_package_id .as_ref() .map(parse_wasm_object_id) @@ -53,18 +57,31 @@ impl TryFrom for PackageOverrides { } } +/// Read-only audit-trail client exposed to wasm consumers. +/// +/// This is the main JS/TS entry point for package resolution and typed reads. Use [`Self::trail`] +/// to get an [`AuditTrailHandle`](crate::trail_handle::WasmAuditTrailHandle) bound to one trail +/// object. #[derive(Clone)] #[wasm_bindgen(js_name = AuditTrailClientReadOnly)] pub struct WasmAuditTrailClientReadOnly(pub(crate) AuditTrailClientReadOnly); #[wasm_bindgen(js_class = AuditTrailClientReadOnly)] impl WasmAuditTrailClientReadOnly { + /// Creates a read-only client by resolving package IDs from the connected network. + /// + /// This is the recommended constructor for official deployments tracked by the built-in + /// package registry. #[wasm_bindgen(js_name = create)] pub async fn new(iota_client: WasmIotaClient) -> Result { let client = AuditTrailClientReadOnly::new(iota_client).await.wasm_result()?; Ok(Self(client)) } + /// Creates a read-only client with explicit package overrides. + /// + /// Prefer this when your JS/TS app talks to a local deployment, preview environment, or any + /// package pair that is not yet part of the registry baked into the SDK. #[wasm_bindgen(js_name = createWithPackageOverrides)] pub async fn new_with_package_overrides( iota_client: WasmIotaClient, @@ -77,6 +94,10 @@ impl WasmAuditTrailClientReadOnly { Ok(Self(client)) } + /// Creates a read-only client while overriding only the audit-trail package ID. + /// + /// This is a compatibility helper for existing callers that only need a single package + /// override. #[wasm_bindgen(js_name = createWithPkgId)] pub async fn new_with_pkg_id( iota_client: WasmIotaClient, @@ -86,8 +107,8 @@ impl WasmAuditTrailClientReadOnly { let client = AuditTrailClientReadOnly::new_with_package_overrides( iota_client, PackageOverrides { - audit_trail_package_id: Some(package_id), - tf_components_package_id: None, + audit_trail: Some(package_id), + tf_component: None, }, ) .await @@ -95,16 +116,19 @@ impl WasmAuditTrailClientReadOnly { Ok(Self(client)) } + /// Returns the audit-trail package ID used by this client. #[wasm_bindgen(js_name = packageId)] pub fn package_id(&self) -> String { self.0.package_id().to_string() } + /// Returns the `tf_components` package ID used by this client. #[wasm_bindgen(js_name = tfComponentsPackageId)] pub fn tf_components_package_id(&self) -> String { self.0.tf_components_package_id().to_string() } + /// Returns the resolved audit-trail package history as stringified object IDs. #[wasm_bindgen(js_name = packageHistory)] pub fn package_history(&self) -> Vec { self.0 @@ -114,21 +138,28 @@ impl WasmAuditTrailClientReadOnly { .collect() } + /// Returns the connected network name. #[wasm_bindgen] pub fn network(&self) -> String { self.0.network().to_string() } + /// Returns the connected chain ID. #[wasm_bindgen(js_name = chainId)] pub fn chain_id(&self) -> String { self.0.chain_id().to_string() } + /// Returns the underlying IOTA client wrapper. #[wasm_bindgen(js_name = iotaClient)] pub fn iota_client(&self) -> WasmIotaClient { self.0.iota_client().clone().into_inner() } + /// Returns a trail-scoped handle for the given trail object ID. + /// + /// Creating the handle is cheap. Reads only happen when you call methods on the returned + /// handle. pub fn trail(&self, trail_id: WasmObjectID) -> Result { let trail_id = parse_wasm_object_id(&trail_id)?; Ok(WasmAuditTrailHandle::from_read_only(self.0.clone(), trail_id)) diff --git a/bindings/wasm/audit_trail_wasm/src/lib.rs b/bindings/wasm/audit_trail_wasm/src/lib.rs index fa475db9..8e19f317 100644 --- a/bindings/wasm/audit_trail_wasm/src/lib.rs +++ b/bindings/wasm/audit_trail_wasm/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#![doc = include_str!("../README.md")] +#![warn(rustdoc::all)] #![allow(deprecated)] #![allow(clippy::upper_case_acronyms)] #![allow(clippy::drop_non_drop)] @@ -17,8 +19,10 @@ mod trail; pub(crate) mod trail_handle; pub(crate) mod types; +/// Shared wasm bindings re-exported from `product_common`. pub use product_common::bindings::*; +/// Installs the panic hook used by the wasm bindings. #[wasm_bindgen(start)] pub fn start() -> std::result::Result<(), JsValue> { console_error_panic_hook::set_once(); diff --git a/bindings/wasm/audit_trail_wasm/src/trail.rs b/bindings/wasm/audit_trail_wasm/src/trail.rs index 3b63d874..dc9c995d 100644 --- a/bindings/wasm/audit_trail_wasm/src/trail.rs +++ b/bindings/wasm/audit_trail_wasm/src/trail.rs @@ -30,6 +30,7 @@ use crate::types::{ WasmRoleCreated, WasmRoleDeleted, WasmRoleMap, WasmRoleUpdated, }; +/// Read-only view of an on-chain audit trail for wasm consumers. #[wasm_bindgen(js_name = OnChainAuditTrail, inspectable)] #[derive(Clone)] pub struct WasmOnChainAuditTrail(pub(crate) OnChainAuditTrail); @@ -40,36 +41,43 @@ impl WasmOnChainAuditTrail { Self(trail) } + /// Returns the trail object ID. #[wasm_bindgen(getter)] pub fn id(&self) -> String { self.0.id.id.to_string() } + /// Returns the creator address. #[wasm_bindgen(getter)] pub fn creator(&self) -> String { self.0.creator.to_string() } + /// Returns the creation timestamp in milliseconds. #[wasm_bindgen(js_name = createdAt, getter)] pub fn created_at(&self) -> u64 { self.0.created_at } + /// Returns the current record sequence counter. #[wasm_bindgen(js_name = sequenceNumber, getter)] pub fn sequence_number(&self) -> u64 { self.0.sequence_number } + /// Returns the active locking configuration. #[wasm_bindgen(js_name = lockingConfig, getter)] pub fn locking_config(&self) -> WasmLockingConfig { self.0.locking_config.clone().into() } + /// Returns the record linked-table metadata. #[wasm_bindgen(getter)] pub fn records(&self) -> WasmLinkedTable { self.0.records.clone().into() } + /// Returns the trail-owned record tags together with usage counts. #[wasm_bindgen(getter)] pub fn tags(&self) -> Vec { let mut tags: Vec = self @@ -82,21 +90,25 @@ impl WasmOnChainAuditTrail { tags } + /// Returns the trail role map. #[wasm_bindgen(getter)] pub fn roles(&self) -> WasmRoleMap { self.0.roles.clone().into() } + /// Returns immutable metadata when present. #[wasm_bindgen(js_name = immutableMetadata, getter)] pub fn immutable_metadata(&self) -> Option { self.0.immutable_metadata.clone().map(Into::into) } + /// Returns mutable metadata when present. #[wasm_bindgen(js_name = updatableMetadata, getter)] pub fn updatable_metadata(&self) -> Option { self.0.updatable_metadata.clone() } + /// Returns the on-chain version of the trail object. #[wasm_bindgen(getter)] pub fn version(&self) -> u64 { self.0.version @@ -121,21 +133,25 @@ async fn apply_trail_created( Ok(trail.into()) } +/// Transaction wrapper for trail creation. #[wasm_bindgen(js_name = CreateTrail, inspectable)] pub struct WasmCreateTrail(pub(crate) CreateTrail); #[wasm_bindgen(js_class = CreateTrail)] impl WasmCreateTrail { + /// Creates a transaction wrapper from an [`AuditTrailBuilder`](crate::builder::WasmAuditTrailBuilder). #[wasm_bindgen(constructor)] pub fn new(builder: WasmAuditTrailBuilder) -> Self { Self(CreateTrail::new(builder.0)) } + /// Builds the programmable transaction bytes for submission. #[wasm_bindgen(js_name = buildProgrammableTransaction)] pub async fn build_programmable_transaction(&self, client: &WasmCoreClientReadOnly) -> Result> { build_programmable_transaction(&self.0, client).await } + /// Applies transaction effects and events and then fetches the created trail object. #[wasm_bindgen(js_name = applyWithEvents)] pub async fn apply_with_events( self, @@ -147,6 +163,7 @@ impl WasmCreateTrail { } } +/// Transaction wrapper for mutable-metadata updates. #[wasm_bindgen(js_name = UpdateMetadata, inspectable)] pub struct WasmUpdateMetadata(pub(crate) UpdateMetadata); @@ -168,6 +185,7 @@ impl WasmUpdateMetadata { } } +/// Transaction wrapper for trail migration. #[wasm_bindgen(js_name = Migrate, inspectable)] pub struct WasmMigrate(pub(crate) Migrate); @@ -189,6 +207,7 @@ impl WasmMigrate { } } +/// Transaction wrapper for deleting a trail. #[wasm_bindgen(js_name = DeleteAuditTrail, inspectable)] pub struct WasmDeleteAuditTrail(pub(crate) DeleteAuditTrail); @@ -211,6 +230,7 @@ impl WasmDeleteAuditTrail { } } +/// Transaction wrapper for replacing the full locking configuration. #[wasm_bindgen(js_name = UpdateLockingConfig, inspectable)] pub struct WasmUpdateLockingConfig(pub(crate) UpdateLockingConfig); @@ -232,6 +252,7 @@ impl WasmUpdateLockingConfig { } } +/// Transaction wrapper for updating the delete-record window. #[wasm_bindgen(js_name = UpdateDeleteRecordWindow, inspectable)] pub struct WasmUpdateDeleteRecordWindow(pub(crate) UpdateDeleteRecordWindow); @@ -253,6 +274,7 @@ impl WasmUpdateDeleteRecordWindow { } } +/// Transaction wrapper for updating the delete-trail lock. #[wasm_bindgen(js_name = UpdateDeleteTrailLock, inspectable)] pub struct WasmUpdateDeleteTrailLock(pub(crate) UpdateDeleteTrailLock); @@ -274,6 +296,7 @@ impl WasmUpdateDeleteTrailLock { } } +/// Transaction wrapper for updating the write lock. #[wasm_bindgen(js_name = UpdateWriteLock, inspectable)] pub struct WasmUpdateWriteLock(pub(crate) UpdateWriteLock); @@ -295,6 +318,7 @@ impl WasmUpdateWriteLock { } } +/// Transaction wrapper for creating a role. #[wasm_bindgen(js_name = CreateRole, inspectable)] pub struct WasmCreateRole(pub(crate) CreateRole); @@ -317,6 +341,7 @@ impl WasmCreateRole { } } +/// Transaction wrapper for updating a role. #[wasm_bindgen(js_name = UpdateRole, inspectable)] pub struct WasmUpdateRole(pub(crate) UpdateRole); @@ -339,6 +364,7 @@ impl WasmUpdateRole { } } +/// Transaction wrapper for deleting a role. #[wasm_bindgen(js_name = DeleteRole, inspectable)] pub struct WasmDeleteRole(pub(crate) DeleteRole); @@ -361,6 +387,7 @@ impl WasmDeleteRole { } } +/// Transaction wrapper for issuing a capability. #[wasm_bindgen(js_name = IssueCapability, inspectable)] pub struct WasmIssueCapability(pub(crate) IssueCapability); @@ -383,6 +410,7 @@ impl WasmIssueCapability { } } +/// Transaction wrapper for revoking a capability. #[wasm_bindgen(js_name = RevokeCapability, inspectable)] pub struct WasmRevokeCapability(pub(crate) RevokeCapability); @@ -405,6 +433,7 @@ impl WasmRevokeCapability { } } +/// Transaction wrapper for destroying a capability. #[wasm_bindgen(js_name = DestroyCapability, inspectable)] pub struct WasmDestroyCapability(pub(crate) DestroyCapability); @@ -427,6 +456,7 @@ impl WasmDestroyCapability { } } +/// Transaction wrapper for destroying an initial-admin capability. #[wasm_bindgen(js_name = DestroyInitialAdminCapability, inspectable)] pub struct WasmDestroyInitialAdminCapability(pub(crate) DestroyInitialAdminCapability); @@ -449,6 +479,7 @@ impl WasmDestroyInitialAdminCapability { } } +/// Transaction wrapper for revoking an initial-admin capability. #[wasm_bindgen(js_name = RevokeInitialAdminCapability, inspectable)] pub struct WasmRevokeInitialAdminCapability(pub(crate) RevokeInitialAdminCapability); @@ -471,6 +502,7 @@ impl WasmRevokeInitialAdminCapability { } } +/// Transaction wrapper for cleaning up expired revoked-capability entries. #[wasm_bindgen(js_name = CleanupRevokedCapabilities, inspectable)] pub struct WasmCleanupRevokedCapabilities(pub(crate) CleanupRevokedCapabilities); @@ -492,6 +524,7 @@ impl WasmCleanupRevokedCapabilities { } } +/// Transaction wrapper for adding a record. #[wasm_bindgen(js_name = AddRecord, inspectable)] pub struct WasmAddRecord(pub(crate) AddRecord); @@ -514,6 +547,7 @@ impl WasmAddRecord { } } +/// Transaction wrapper for deleting a single record. #[wasm_bindgen(js_name = DeleteRecord, inspectable)] pub struct WasmDeleteRecord(pub(crate) DeleteRecord); @@ -536,6 +570,7 @@ impl WasmDeleteRecord { } } +/// Transaction wrapper for deleting records in batch form. #[wasm_bindgen(js_name = DeleteRecordsBatch, inspectable)] pub struct WasmDeleteRecordsBatch(pub(crate) DeleteRecordsBatch); @@ -557,6 +592,7 @@ impl WasmDeleteRecordsBatch { } } +/// Transaction wrapper for adding a record tag to the trail registry. #[wasm_bindgen(js_name = AddRecordTag, inspectable)] pub struct WasmAddRecordTag(pub(crate) AddRecordTag); @@ -578,6 +614,7 @@ impl WasmAddRecordTag { } } +/// Transaction wrapper for removing a record tag from the trail registry. #[wasm_bindgen(js_name = RemoveRecordTag, inspectable)] pub struct WasmRemoveRecordTag(pub(crate) RemoveRecordTag); diff --git a/bindings/wasm/audit_trail_wasm/src/trail_handle/access.rs b/bindings/wasm/audit_trail_wasm/src/trail_handle/access.rs index 6b5d1862..76e1ce2b 100644 --- a/bindings/wasm/audit_trail_wasm/src/trail_handle/access.rs +++ b/bindings/wasm/audit_trail_wasm/src/trail_handle/access.rs @@ -18,6 +18,7 @@ use crate::trail::{ }; use crate::types::{WasmCapabilityIssueOptions, WasmPermissionSet, WasmRoleTags}; +/// Access-control API scoped to a specific trail. #[derive(Clone)] #[wasm_bindgen(js_name = TrailAccess, inspectable)] pub struct WasmTrailAccess { @@ -40,6 +41,7 @@ impl WasmTrailAccess { #[wasm_bindgen(js_class = TrailAccess)] impl WasmTrailAccess { + /// Returns a role-scoped handle for the given role name. #[wasm_bindgen(js_name = forRole)] pub fn for_role(&self, name: String) -> WasmRoleHandle { WasmRoleHandle { @@ -49,6 +51,7 @@ impl WasmTrailAccess { } } + /// Builds a capability-revocation transaction. #[wasm_bindgen(js_name = revokeCapability, unchecked_return_type = "TransactionBuilder")] pub fn revoke_capability( &self, @@ -65,6 +68,7 @@ impl WasmTrailAccess { Ok(into_transaction_builder(WasmRevokeCapability(tx))) } + /// Builds a capability-destruction transaction. #[wasm_bindgen(js_name = destroyCapability, unchecked_return_type = "TransactionBuilder")] pub fn destroy_capability(&self, capability_id: WasmObjectID) -> Result { let capability_id = parse_wasm_object_id(&capability_id)?; @@ -77,6 +81,7 @@ impl WasmTrailAccess { Ok(into_transaction_builder(WasmDestroyCapability(tx))) } + /// Builds an initial-admin-capability destruction transaction. #[wasm_bindgen(js_name = destroyInitialAdminCapability, unchecked_return_type = "TransactionBuilder")] pub fn destroy_initial_admin_capability(&self, capability_id: WasmObjectID) -> Result { let capability_id = parse_wasm_object_id(&capability_id)?; @@ -89,6 +94,7 @@ impl WasmTrailAccess { Ok(into_transaction_builder(WasmDestroyInitialAdminCapability(tx))) } + /// Builds an initial-admin-capability revocation transaction. #[wasm_bindgen(js_name = revokeInitialAdminCapability, unchecked_return_type = "TransactionBuilder")] pub fn revoke_initial_admin_capability( &self, @@ -105,6 +111,7 @@ impl WasmTrailAccess { Ok(into_transaction_builder(WasmRevokeInitialAdminCapability(tx))) } + /// Builds a cleanup transaction for expired revoked-capability entries. #[wasm_bindgen(js_name = cleanupRevokedCapabilities, unchecked_return_type = "TransactionBuilder")] pub fn cleanup_revoked_capabilities(&self) -> Result { let tx = self @@ -117,6 +124,7 @@ impl WasmTrailAccess { } } +/// Role-scoped access-control API. #[derive(Clone)] #[wasm_bindgen(js_name = RoleHandle, inspectable)] pub struct WasmRoleHandle { @@ -140,11 +148,13 @@ impl WasmRoleHandle { #[wasm_bindgen(js_class = RoleHandle)] impl WasmRoleHandle { + /// Returns the role name represented by this handle. #[wasm_bindgen(getter)] pub fn name(&self) -> String { self.name.clone() } + /// Builds a role-creation transaction. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn create( &self, @@ -161,6 +171,7 @@ impl WasmRoleHandle { Ok(into_transaction_builder(WasmCreateRole(tx))) } + /// Builds a capability-issuance transaction for this role. #[wasm_bindgen(js_name = issueCapability, unchecked_return_type = "TransactionBuilder")] pub fn issue_capability(&self, options: WasmCapabilityIssueOptions) -> Result { let tx = self @@ -173,6 +184,7 @@ impl WasmRoleHandle { Ok(into_transaction_builder(WasmIssueCapability(tx))) } + /// Builds a role-update transaction for this role. #[wasm_bindgen(js_name = updatePermissions, unchecked_return_type = "TransactionBuilder")] pub fn update_permissions( &self, @@ -189,6 +201,7 @@ impl WasmRoleHandle { Ok(into_transaction_builder(WasmUpdateRole(tx))) } + /// Builds a role-deletion transaction for this role. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn delete(&self) -> Result { let tx = self diff --git a/bindings/wasm/audit_trail_wasm/src/trail_handle/locking.rs b/bindings/wasm/audit_trail_wasm/src/trail_handle/locking.rs index 0ed6fb9e..6c56d997 100644 --- a/bindings/wasm/audit_trail_wasm/src/trail_handle/locking.rs +++ b/bindings/wasm/audit_trail_wasm/src/trail_handle/locking.rs @@ -15,6 +15,7 @@ use crate::trail::{ }; use crate::types::{WasmLockingConfig, WasmLockingWindow, WasmTimeLock}; +/// Locking API scoped to a specific trail. #[derive(Clone)] #[wasm_bindgen(js_name = TrailLocking, inspectable)] pub struct WasmTrailLocking { @@ -38,6 +39,7 @@ impl WasmTrailLocking { #[wasm_bindgen(js_class = TrailLocking)] impl WasmTrailLocking { + /// Builds a transaction that replaces the full locking configuration. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn update(&self, config: WasmLockingConfig) -> Result { let tx = self @@ -49,6 +51,7 @@ impl WasmTrailLocking { Ok(into_transaction_builder(WasmUpdateLockingConfig(tx))) } + /// Builds a transaction that updates only the delete-record window. #[wasm_bindgen(js_name = updateDeleteRecordWindow, unchecked_return_type = "TransactionBuilder")] pub fn update_delete_record_window(&self, window: WasmLockingWindow) -> Result { let tx = self @@ -60,6 +63,7 @@ impl WasmTrailLocking { Ok(into_transaction_builder(WasmUpdateDeleteRecordWindow(tx))) } + /// Builds a transaction that updates only the delete-trail lock. #[wasm_bindgen(js_name = updateDeleteTrailLock, unchecked_return_type = "TransactionBuilder")] pub fn update_delete_trail_lock(&self, lock: WasmTimeLock) -> Result { let tx = self @@ -71,6 +75,7 @@ impl WasmTrailLocking { Ok(into_transaction_builder(WasmUpdateDeleteTrailLock(tx))) } + /// Builds a transaction that updates only the write lock. #[wasm_bindgen(js_name = updateWriteLock, unchecked_return_type = "TransactionBuilder")] pub fn update_write_lock(&self, lock: WasmTimeLock) -> Result { let tx = self @@ -82,6 +87,7 @@ impl WasmTrailLocking { Ok(into_transaction_builder(WasmUpdateWriteLock(tx))) } + /// Returns whether a record is currently locked against deletion. #[wasm_bindgen(js_name = isRecordLocked)] pub async fn is_record_locked(&self, sequence_number: u64) -> Result { self.read_only diff --git a/bindings/wasm/audit_trail_wasm/src/trail_handle/mod.rs b/bindings/wasm/audit_trail_wasm/src/trail_handle/mod.rs index 7a71477a..a18a91fd 100644 --- a/bindings/wasm/audit_trail_wasm/src/trail_handle/mod.rs +++ b/bindings/wasm/audit_trail_wasm/src/trail_handle/mod.rs @@ -1,6 +1,8 @@ // Copyright 2026 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +//! Trail-scoped wasm handle wrappers. + mod access; mod locking; mod records; @@ -21,6 +23,10 @@ use wasm_bindgen::prelude::*; use crate::trail::{WasmDeleteAuditTrail, WasmMigrate, WasmOnChainAuditTrail, WasmUpdateMetadata}; +/// Handle bound to a specific audit-trail object. +/// +/// `AuditTrailHandle` keeps one trail ID together with the originating client so all trail-scoped +/// reads and transaction builders can be discovered from a single JS/TS value. #[derive(Clone)] #[wasm_bindgen(js_name = AuditTrailHandle, inspectable)] pub struct WasmAuditTrailHandle { @@ -60,17 +66,22 @@ impl WasmAuditTrailHandle { #[wasm_bindgen(js_class = AuditTrailHandle)] impl WasmAuditTrailHandle { + /// Loads the full on-chain trail object. + /// + /// Each call fetches a fresh snapshot from chain state. pub async fn get(&self) -> Result { let trail = self.read_only.trail(self.trail_id).get().await.wasm_result()?; Ok(trail.into()) } + /// Builds a migration transaction for this trail. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn migrate(&self) -> Result { let tx = self.require_write()?.trail(self.trail_id).migrate().into_inner(); Ok(into_transaction_builder(WasmMigrate(tx))) } + /// Builds a delete transaction for this trail. #[wasm_bindgen(js_name = deleteAuditTrail, unchecked_return_type = "TransactionBuilder")] pub fn delete_audit_trail(&self) -> Result { let tx = self @@ -81,6 +92,7 @@ impl WasmAuditTrailHandle { Ok(into_transaction_builder(WasmDeleteAuditTrail(tx))) } + /// Builds a mutable-metadata update transaction for this trail. #[wasm_bindgen(js_name = updateMetadata, unchecked_return_type = "TransactionBuilder")] pub fn update_metadata(&self, metadata: Option) -> Result { let tx = self @@ -91,6 +103,7 @@ impl WasmAuditTrailHandle { Ok(into_transaction_builder(WasmUpdateMetadata(tx))) } + /// Returns the record API scoped to this trail. pub fn records(&self) -> WasmTrailRecords { WasmTrailRecords { read_only: self.read_only.clone(), @@ -99,6 +112,7 @@ impl WasmAuditTrailHandle { } } + /// Returns the access-control API scoped to this trail. pub fn access(&self) -> WasmTrailAccess { WasmTrailAccess { full: self.full.clone(), @@ -106,6 +120,7 @@ impl WasmAuditTrailHandle { } } + /// Returns the locking API scoped to this trail. pub fn locking(&self) -> WasmTrailLocking { WasmTrailLocking { read_only: self.read_only.clone(), @@ -114,6 +129,7 @@ impl WasmAuditTrailHandle { } } + /// Returns the tag-registry API scoped to this trail. pub fn tags(&self) -> WasmTrailTags { WasmTrailTags { read_only: self.read_only.clone(), diff --git a/bindings/wasm/audit_trail_wasm/src/trail_handle/records.rs b/bindings/wasm/audit_trail_wasm/src/trail_handle/records.rs index 03d67538..d4b646b2 100644 --- a/bindings/wasm/audit_trail_wasm/src/trail_handle/records.rs +++ b/bindings/wasm/audit_trail_wasm/src/trail_handle/records.rs @@ -12,8 +12,9 @@ use product_common::bindings::utils::into_transaction_builder; use wasm_bindgen::prelude::*; use crate::trail::{WasmAddRecord, WasmDeleteRecord, WasmDeleteRecordsBatch}; -use crate::types::{WasmData, WasmPaginatedRecord, WasmRecord}; +use crate::types::{WasmData, WasmEmpty, WasmPaginatedRecord, WasmRecord}; +/// Record API scoped to a specific trail. #[derive(Clone)] #[wasm_bindgen(js_name = TrailRecords, inspectable)] pub struct WasmTrailRecords { @@ -37,6 +38,7 @@ impl WasmTrailRecords { #[wasm_bindgen(js_class = TrailRecords)] impl WasmTrailRecords { + /// Loads one record by sequence number. pub async fn get(&self, sequence_number: u64) -> Result { let record = self .read_only @@ -48,6 +50,7 @@ impl WasmTrailRecords { Ok(record.into()) } + /// Returns the number of records currently stored in the trail. #[wasm_bindgen(js_name = recordCount)] pub async fn record_count(&self) -> Result { self.read_only @@ -58,6 +61,7 @@ impl WasmTrailRecords { .wasm_result() } + /// Lists all records in sequence-number order. pub async fn list(&self) -> Result> { let mut records: Vec<_> = self .read_only @@ -72,6 +76,7 @@ impl WasmTrailRecords { Ok(records.into_iter().map(|(_, record)| record.into()).collect()) } + /// Lists all records while enforcing a maximum number of entries. #[wasm_bindgen(js_name = listWithLimit)] pub async fn list_with_limit(&self, max_entries: usize) -> Result> { let mut records: Vec<_> = self @@ -87,6 +92,7 @@ impl WasmTrailRecords { Ok(records.into_iter().map(|(_, record)| record.into()).collect()) } + /// Loads one page of records starting at `cursor`. #[wasm_bindgen(js_name = listPage)] pub async fn list_page(&self, cursor: Option, limit: usize) -> Result { let page = self @@ -99,6 +105,18 @@ impl WasmTrailRecords { Ok(page.into()) } + /// Executes the correction helper for a record payload. + pub async fn correct(&self, replaces: Vec, data: WasmData, metadata: Option) -> Result { + self.require_write()? + .trail(self.trail_id) + .records() + .correct(replaces, data.into(), metadata) + .await + .wasm_result()?; + Ok(WasmEmpty) + } + + /// Builds a record-add transaction. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn add(&self, data: WasmData, metadata: Option, tag: Option) -> Result { let tx = self @@ -110,6 +128,7 @@ impl WasmTrailRecords { Ok(into_transaction_builder(WasmAddRecord(tx))) } + /// Builds a single-record delete transaction. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn delete(&self, sequence_number: u64) -> Result { let tx = self @@ -121,6 +140,7 @@ impl WasmTrailRecords { Ok(into_transaction_builder(WasmDeleteRecord(tx))) } + /// Builds a batched record-delete transaction. #[wasm_bindgen(js_name = deleteBatch, unchecked_return_type = "TransactionBuilder")] pub fn delete_batch(&self, limit: u64) -> Result { let tx = self diff --git a/bindings/wasm/audit_trail_wasm/src/trail_handle/tags.rs b/bindings/wasm/audit_trail_wasm/src/trail_handle/tags.rs index 4bfa4552..2b545938 100644 --- a/bindings/wasm/audit_trail_wasm/src/trail_handle/tags.rs +++ b/bindings/wasm/audit_trail_wasm/src/trail_handle/tags.rs @@ -13,6 +13,7 @@ use wasm_bindgen::prelude::*; use crate::trail::{WasmAddRecordTag, WasmRemoveRecordTag}; use crate::types::WasmRecordTagEntry; +/// Tag-registry API scoped to a specific trail. #[derive(Clone)] #[wasm_bindgen(js_name = TrailTags, inspectable)] pub struct WasmTrailTags { @@ -22,6 +23,7 @@ pub struct WasmTrailTags { } impl WasmTrailTags { + /// Returns the writable client for tag mutations. fn require_write(&self) -> Result<&AuditTrailClient> { self.full.as_ref().ok_or_else(|| { wasm_error(anyhow!( @@ -44,12 +46,14 @@ impl WasmTrailTags { Ok(tags) } + /// Builds a transaction that adds a tag to the trail registry. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn add(&self, tag: String) -> Result { let tx = self.require_write()?.trail(self.trail_id).tags().add(tag).into_inner(); Ok(into_transaction_builder(WasmAddRecordTag(tx))) } + /// Builds a transaction that removes a tag from the trail registry. #[wasm_bindgen(unchecked_return_type = "TransactionBuilder")] pub fn remove(&self, tag: String) -> Result { let tx = self diff --git a/bindings/wasm/audit_trail_wasm/src/types.rs b/bindings/wasm/audit_trail_wasm/src/types.rs index da65fd76..52cc9586 100644 --- a/bindings/wasm/audit_trail_wasm/src/types.rs +++ b/bindings/wasm/audit_trail_wasm/src/types.rs @@ -16,6 +16,7 @@ use product_common::bindings::WasmIotaAddress; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; +/// Placeholder wrapper used for transaction outputs that carry no value. #[wasm_bindgen(js_name = Empty, inspectable)] pub struct WasmEmpty; @@ -25,12 +26,14 @@ impl From<()> for WasmEmpty { } } +/// JS-friendly wrapper for audit-trail record payloads. #[wasm_bindgen(js_name = Data, inspectable)] #[derive(Clone)] pub struct WasmData(pub(crate) Data); #[wasm_bindgen(js_class = Data)] impl WasmData { + /// Returns the underlying payload as either a string or `Uint8Array`. #[wasm_bindgen(getter)] pub fn value(&self) -> JsValue { match &self.0 { @@ -39,6 +42,7 @@ impl WasmData { } } + /// Returns the payload converted to a string. #[wasm_bindgen(js_name = toString)] pub fn to_string(&self) -> String { match &self.0 { @@ -47,6 +51,7 @@ impl WasmData { } } + /// Returns the payload converted to raw bytes. #[wasm_bindgen(js_name = toBytes)] pub fn to_bytes(&self) -> Vec { match &self.0 { @@ -55,11 +60,13 @@ impl WasmData { } } + /// Creates a text payload. #[wasm_bindgen(js_name = fromString)] pub fn from_string(data: String) -> Self { Self(Data::text(data)) } + /// Creates a binary payload. #[wasm_bindgen(js_name = fromBytes)] pub fn from_bytes(data: Uint8Array) -> Self { Self(Data::bytes(data.to_vec())) @@ -137,6 +144,7 @@ fn sorted_role_entries(roles: HashMap) -> Vec for Permission { } } +/// JS-friendly wrapper for a set of permissions. #[wasm_bindgen(js_name = PermissionSet, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmPermissionSet { + /// Permissions granted by this set. pub permissions: Vec, } #[wasm_bindgen(js_class = PermissionSet)] impl WasmPermissionSet { + /// Creates a permission set from an explicit list of permissions. #[wasm_bindgen(constructor)] pub fn new(permissions: Vec) -> Self { Self { permissions } } + /// Returns the recommended role-administration permission set. #[wasm_bindgen(js_name = adminPermissions)] pub fn admin_permissions() -> Self { PermissionSet::admin_permissions().into() } + /// Returns the permissions needed to administer records. #[wasm_bindgen(js_name = recordAdminPermissions)] pub fn record_admin_permissions() -> Self { PermissionSet::record_admin_permissions().into() } + /// Returns the permissions needed to administer locking rules. #[wasm_bindgen(js_name = lockingAdminPermissions)] pub fn locking_admin_permissions() -> Self { PermissionSet::locking_admin_permissions().into() } + /// Returns the permissions needed to administer roles. #[wasm_bindgen(js_name = roleAdminPermissions)] pub fn role_admin_permissions() -> Self { PermissionSet::role_admin_permissions().into() } + /// Returns the permissions needed to issue and revoke capabilities. #[wasm_bindgen(js_name = capAdminPermissions)] pub fn cap_admin_permissions() -> Self { PermissionSet::cap_admin_permissions().into() } + /// Returns the permissions needed to administer mutable metadata. #[wasm_bindgen(js_name = metadataAdminPermissions)] pub fn metadata_admin_permissions() -> Self { PermissionSet::metadata_admin_permissions().into() } + /// Returns the permissions needed to administer record tags. #[wasm_bindgen(js_name = tagAdminPermissions)] pub fn tag_admin_permissions() -> Self { PermissionSet::tag_admin_permissions().into() @@ -278,12 +296,17 @@ impl From for PermissionSet { } } +/// Linked-table metadata for record storage. #[wasm_bindgen(js_name = LinkedTable, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmLinkedTable { + /// Linked-table object ID. pub id: String, + /// Declared number of entries in the table. pub size: u64, + /// Sequence number of the first entry, if any. pub head: Option, + /// Sequence number of the last entry, if any. pub tail: Option, } @@ -298,6 +321,7 @@ impl From> for WasmLinkedTable { } } +/// Permission requirements for role administration. #[wasm_bindgen(js_name = RoleAdminPermissions, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmRoleAdminPermissions { @@ -316,6 +340,7 @@ impl From for WasmRoleAdminPermissions { } } +/// Permission requirements for capability administration. #[wasm_bindgen(js_name = CapabilityAdminPermissions, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmCapabilityAdminPermissions { @@ -332,6 +357,7 @@ impl From for WasmCapabilityAdminPermissions { } } +/// Flattened role entry exposed inside [`WasmRoleMap`]. #[wasm_bindgen(js_name = RolePermissionsEntry, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRolePermissionsEntry { @@ -341,14 +367,17 @@ pub struct WasmRolePermissionsEntry { pub role_tags: Option, } +/// Allowlisted record tags stored on a role. #[wasm_bindgen(js_name = RoleTags, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmRoleTags { + /// Sorted tag names allowed by the role. pub tags: Vec, } #[wasm_bindgen(js_class = RoleTags)] impl WasmRoleTags { + /// Creates role-tag restrictions from a list of tag names. #[wasm_bindgen(constructor)] pub fn new(tags: Vec) -> Self { let mut tags = tags; @@ -372,6 +401,7 @@ impl From for RoleTags { } } +/// Trail-owned record tag plus its usage count. #[wasm_bindgen(js_name = RecordTagEntry, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmRecordTagEntry { @@ -386,6 +416,7 @@ impl From<(String, u64)> for WasmRecordTagEntry { } } +/// JS-friendly view of the trail role map. #[wasm_bindgen(js_name = RoleMap, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRoleMap { @@ -418,6 +449,7 @@ impl From for WasmRoleMap { } } +/// Linked-table metadata keyed by object IDs. #[wasm_bindgen(js_name = ObjectIdLinkedTable, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmObjectIdLinkedTable { @@ -438,6 +470,7 @@ impl From> for WasmObjectIdLinkedTable { } } +/// Capability issuance options exposed to wasm consumers. #[wasm_bindgen(js_name = CapabilityIssueOptions, getter_with_clone, inspectable)] #[derive(Clone, Default, Serialize, Deserialize)] pub struct WasmCapabilityIssueOptions { @@ -451,6 +484,7 @@ pub struct WasmCapabilityIssueOptions { #[wasm_bindgen(js_class = CapabilityIssueOptions)] impl WasmCapabilityIssueOptions { + /// Creates capability issuance options. #[wasm_bindgen(constructor)] pub fn new(issued_to: Option, valid_from_ms: Option, valid_until_ms: Option) -> Self { Self { @@ -481,6 +515,7 @@ impl From for CapabilityIssueOptions { } } +/// Capability data returned to wasm consumers. #[wasm_bindgen(js_name = Capability, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmCapability { @@ -509,6 +544,7 @@ impl From for WasmCapability { } } +/// Event payload emitted when a trail is created. #[wasm_bindgen(js_name = AuditTrailCreated, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmAuditTrailCreated { @@ -528,6 +564,7 @@ impl From for WasmAuditTrailCreated { } } +/// Event payload emitted when a trail is deleted. #[wasm_bindgen(js_name = AuditTrailDeleted, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmAuditTrailDeleted { @@ -545,6 +582,7 @@ impl From for WasmAuditTrailDeleted { } } +/// Event payload emitted when a record is added. #[wasm_bindgen(js_name = RecordAdded, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRecordAdded { @@ -568,6 +606,7 @@ impl From for WasmRecordAdded { } } +/// Event payload emitted when a record is deleted. #[wasm_bindgen(js_name = RecordDeleted, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRecordDeleted { @@ -591,6 +630,7 @@ impl From for WasmRecordDeleted { } } +/// Event payload emitted when a capability is issued. #[wasm_bindgen(js_name = CapabilityIssued, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmCapabilityIssued { @@ -620,6 +660,7 @@ impl From for WasmCapabilityIssued { } } +/// Event payload emitted when a capability is destroyed. #[wasm_bindgen(js_name = CapabilityDestroyed, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmCapabilityDestroyed { @@ -649,6 +690,7 @@ impl From for WasmCapabilityDestroyed { } } +/// Event payload emitted when a capability is revoked. #[wasm_bindgen(js_name = CapabilityRevoked, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmCapabilityRevoked { @@ -670,6 +712,7 @@ impl From for WasmCapabilityRevoked { } } +/// Event payload emitted when a role is created. #[wasm_bindgen(js_name = RoleCreated, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRoleCreated { @@ -697,6 +740,7 @@ impl From for WasmRoleCreated { } } +/// Event payload emitted when a role is updated. #[wasm_bindgen(js_name = RoleUpdated, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRoleUpdated { @@ -724,6 +768,7 @@ impl From for WasmRoleUpdated { } } +/// Event payload emitted when a role is deleted. #[wasm_bindgen(js_name = RoleDeleted, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRoleDeleted { @@ -746,6 +791,7 @@ impl From for WasmRoleDeleted { } } +/// Discriminant for the shape stored inside [`WasmTimeLock`]. #[wasm_bindgen(js_name = TimeLockType)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WasmTimeLockType { @@ -756,37 +802,44 @@ pub enum WasmTimeLockType { Infinite, } +/// JS-friendly wrapper for time locks. #[wasm_bindgen(js_name = TimeLock, inspectable)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmTimeLock(pub(crate) TimeLock); #[wasm_bindgen(js_class = TimeLock)] impl WasmTimeLock { + /// Creates a lock that unlocks at a Unix timestamp in seconds. #[wasm_bindgen(js_name = withUnlockAt)] pub fn with_unlock_at(time_sec: u32) -> Self { Self(TimeLock::UnlockAt(time_sec)) } + /// Creates a lock that unlocks at a Unix timestamp in milliseconds. #[wasm_bindgen(js_name = withUnlockAtMs)] pub fn with_unlock_at_ms(time_ms: u64) -> Self { Self(TimeLock::UnlockAtMs(time_ms)) } + /// Creates a lock that stays active until the protected object is destroyed. #[wasm_bindgen(js_name = withUntilDestroyed)] pub fn with_until_destroyed() -> Self { Self(TimeLock::UntilDestroyed) } + /// Creates a lock that never unlocks. #[wasm_bindgen(js_name = withInfinite)] pub fn with_infinite() -> Self { Self(TimeLock::Infinite) } + /// Creates a disabled lock. #[wasm_bindgen(js_name = withNone)] pub fn with_none() -> Self { Self(TimeLock::None) } + /// Returns the lock variant. #[wasm_bindgen(js_name = "type", getter)] pub fn lock_type(&self) -> WasmTimeLockType { match self.0 { @@ -798,6 +851,7 @@ impl WasmTimeLock { } } + /// Returns the lock argument for parameterized variants. #[wasm_bindgen(js_name = "args", getter)] pub fn args(&self) -> JsValue { match self.0 { @@ -820,6 +874,7 @@ impl From for TimeLock { } } +/// Discriminant for the shape stored inside [`WasmLockingWindow`]. #[wasm_bindgen(js_name = LockingWindowType)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WasmLockingWindowType { @@ -828,27 +883,32 @@ pub enum WasmLockingWindowType { CountBased, } +/// JS-friendly wrapper for delete windows. #[wasm_bindgen(js_name = LockingWindow, inspectable)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WasmLockingWindow(pub(crate) LockingWindow); #[wasm_bindgen(js_class = LockingWindow)] impl WasmLockingWindow { + /// Creates a disabled delete window. #[wasm_bindgen(js_name = withNone)] pub fn with_none() -> Self { Self(LockingWindow::None) } + /// Creates a time-based delete window. #[wasm_bindgen(js_name = withTimeBased)] pub fn with_time_based(seconds: u64) -> Self { Self(LockingWindow::TimeBased { seconds }) } + /// Creates a count-based delete window. #[wasm_bindgen(js_name = withCountBased)] pub fn with_count_based(count: u64) -> Self { Self(LockingWindow::CountBased { count }) } + /// Returns the window variant. #[wasm_bindgen(js_name = "type", getter)] pub fn window_type(&self) -> WasmLockingWindowType { match self.0 { @@ -858,6 +918,7 @@ impl WasmLockingWindow { } } + /// Returns the window argument for parameterized variants. #[wasm_bindgen(js_name = "args", getter)] pub fn args(&self) -> JsValue { match self.0 { @@ -880,6 +941,7 @@ impl From for LockingWindow { } } +/// Full locking configuration exposed to wasm consumers. #[wasm_bindgen(js_name = LockingConfig, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmLockingConfig { @@ -893,6 +955,7 @@ pub struct WasmLockingConfig { #[wasm_bindgen(js_class = LockingConfig)] impl WasmLockingConfig { + /// Creates a locking configuration. #[wasm_bindgen(constructor)] pub fn new( delete_record_window: WasmLockingWindow, @@ -927,6 +990,7 @@ impl From for LockingConfig { } } +/// Immutable trail metadata exposed to wasm consumers. #[wasm_bindgen(js_name = ImmutableMetadata, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmImmutableMetadata { @@ -952,6 +1016,7 @@ impl From for ImmutableMetadata { } } +/// Correction metadata attached to a record. #[wasm_bindgen(js_name = RecordCorrection, getter_with_clone, inspectable)] #[derive(Clone, Serialize, Deserialize)] pub struct WasmRecordCorrection { @@ -980,6 +1045,7 @@ impl From for RecordCorrection { } } +/// Single audit-trail record exposed to wasm consumers. #[wasm_bindgen(js_name = Record, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmRecord { @@ -1009,6 +1075,7 @@ impl From> for WasmRecord { } } +/// One page of records returned by `TrailRecords.listPage(...)`. #[wasm_bindgen(js_name = PaginatedRecord, getter_with_clone, inspectable)] #[derive(Clone)] pub struct WasmPaginatedRecord { diff --git a/examples/utils/utils.rs b/examples/utils/utils.rs index 28ea0d9b..a3af6aec 100644 --- a/examples/utils/utils.rs +++ b/examples/utils/utils.rs @@ -62,8 +62,8 @@ pub async fn get_funded_audit_trail_client() -> Result + StackExchange + Discord + Apache 2.0 license +

+ +

+ Introduction ◈ + Modules ◈ + Development & Testing ◈ + Related Libraries ◈ + Contributing +

+ +--- + +# IOTA Notarization Move Package + +## Introduction + +`notarization-move` is the on-chain Move package behind IOTA Notarization. + +It defines the core `Notarization` object and the supporting modules for: + +- dynamic notarization flows +- locked notarization flows +- immutable creation metadata +- optional updatable metadata +- state updates, transfer rules, and destruction checks +- emitted events for notarization lifecycle changes + +The package depends on `TfComponents` for shared timelock primitives. + +## Modules + +- `iota_notarization::notarization` + Core object, state model, metadata, lock metadata, updates, and destruction logic. +- `iota_notarization::dynamic_notarization` + Dynamic notarization creation and transfer flows. +- `iota_notarization::locked_notarization` + Locked notarization creation flows with timelock controls. +- `iota_notarization::method` + Method discriminator helpers for dynamic and locked variants. + +## Development And Testing + +Build the Move package: + +```bash +cd notarization-move +iota move build +``` + +Run the Move test suite: + +```bash +cd notarization-move +iota move test +``` + +Publish locally: + +```bash +cd notarization-move +./scripts/publish_package.sh +``` + +The package history files [`Move.lock`](./Move.lock) and [`Move.history.json`](./Move.history.json) are used by the Rust SDK to resolve and track deployed package versions. + +## Related Libraries + +- [Rust SDK](https://github.com/iotaledger/notarization/tree/main/notarization-rs/README.md) +- [Wasm SDK](https://github.com/iotaledger/notarization/tree/main/bindings/wasm/notarization_wasm/README.md) +- [Repository Root](https://github.com/iotaledger/notarization/tree/main/README.md) + +## Contributing + +We would love to have you help us with the development of IOTA Notarization. Each and every contribution is greatly valued. + +Please review the [contribution](https://docs.iota.org/developer/iota-notarization/contribute) sections in the [IOTA Docs Portal](https://docs.iota.org/developer/iota-notarization/). + +To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included. + +The best place to get involved in discussions about this package or to look for support at is the `#notarization` channel on the [IOTA Discord](https://discord.gg/iota-builders). You can also ask questions on our [Stack Exchange](https://iota.stackexchange.com/). diff --git a/notarization-rs/README.md b/notarization-rs/README.md index e7faf0a9..75be16ba 100644 --- a/notarization-rs/README.md +++ b/notarization-rs/README.md @@ -6,37 +6,6 @@ instance, which is mapped to the Notarization object on the ledger and can be us You can find the full IOTA Notarization documentation [here](https://docs.iota.org/developer/iota-notarization). -Following Notarization methods are currently provided: - -- Dynamic Notarization -- Locked Notarization - -These Notarization methods are implemented using a single Notarization Move object, stored on the IOTA Ledger. -The Method specific behavior is achieved via configuration of this object. - -To minimize the need for config settings, the Notarization methods reduce the number of available configuration -parameters while using method specific fixed settings for several parameters, resulting in the typical method -specific behaviour. Here, Notarization methods can be seen as prepared configuration sets to facilitate -Notarization usage for often needed use cases. - -Here is an overview of the most important configuration parameters for each of these methods: - -| Method | Locking exists | delete_lock* | update_lock | transfer_lock | -| ------- | --------------- | ---------------- | ----------------------- | ----------------------- | -| Dynamic | Optional [conf] | None [static] | None [static] | Optional [conf] | -| Locked | Yes [static] | Optional* [conf] | UntilDestroyed [static] | UntilDestroyed [static] | - -Explanation of terms and symbols for the table above: - -- [conf]: Configurable parameter. -- [static]: Fixed or static parameter. -- Optional: - - Locks: The lock can be set to UnlockAt or UntilDestroyed. - - Locking exists: If no locking is used, there will be no [`LockMetadata`] stored with the Notarization object - Otherwise [`LockMetadata`] will be created automatically. If no [`LockMetadata`] exist, the behaviour is - equivalent to existing [`LockMetadata`] with all locks set to [`None`]. - - *: delete_lock can not be set to `UntilDestroyed`. - ## Process Flows The following workflows demonstrate how NotarizationBuilder and Notarization instances can be used to create, update and diff --git a/notarization-rs/src/lib.rs b/notarization-rs/src/lib.rs index 611fae81..25512ddd 100644 --- a/notarization-rs/src/lib.rs +++ b/notarization-rs/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2020-2025 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#![doc = include_str!("../README.md")] + pub mod client; pub mod core; pub mod error;