Skip to content

Commit 4aea04e

Browse files
committed
feat: Implement audit trail metadata update and migration operations
- Added `update_metadata` function in `metadata.rs` to allow updating metadata for an audit trail. - Introduced `migrate` function in `migrate.rs` for migrating audit trails. - Created `mod.rs` to organize audit trail operations, including create, locking, metadata, migrate, and records. - Implemented record management functions in `records.rs` for adding, deleting, and retrieving records. - Developed `CreateTrail` and `AddRecord` transaction types to facilitate trail creation and record addition. - Enhanced `Data` type in `record.rs` to support both bytes and text, with serialization and deserialization logic. - Updated various core types and error handling to reflect new functionalities. - Added end-to-end tests for creating trails and managing records.
1 parent 398b515 commit 4aea04e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1850
-63
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
# Copyright 2020-2026 IOTA Stiftung
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
script_dir=$(cd "$(dirname $0)" && pwd)
7+
package_dir=$script_dir/..
8+
9+
RESPONSE=$(iota client publish --with-unpublished-dependencies --silence-warnings --json --gas-budget 500000000 $package_dir)
10+
{ # try
11+
PACKAGE_ID=$(echo $RESPONSE | jq --raw-output '.objectChanges[] | select(.type | contains("published")) | .packageId')
12+
} || { # catch
13+
echo $RESPONSE
14+
}
15+
16+
export IOTA_AUDIT_TRAIL_PKG_ID=$PACKAGE_ID
17+
echo "${IOTA_AUDIT_TRAIL_PKG_ID}"

audit-trail-move/sources/record.move

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ use std::string::String;
1313
/// A single record in the audit trail
1414
public struct Record<D: store + copy> has store {
1515
/// Arbitrary data stored on-chain
16-
stored_data: D,
16+
data: D,
1717
/// Optional metadata for this specific record
18-
record_metadata: Option<String>,
18+
metadata: Option<String>,
1919
/// Position in the trail (0-indexed, never reused)
2020
sequence_number: u64,
2121
/// Who added this record
@@ -30,16 +30,16 @@ public struct Record<D: store + copy> has store {
3030

3131
/// Create a new record
3232
public(package) fun new<D: store + copy>(
33-
stored_data: D,
34-
record_metadata: Option<String>,
33+
data: D,
34+
metadata: Option<String>,
3535
sequence_number: u64,
3636
added_by: address,
3737
added_at: u64,
3838
correction: RecordCorrection,
3939
): Record<D> {
4040
Record {
41-
stored_data,
42-
record_metadata,
41+
data,
42+
metadata,
4343
sequence_number,
4444
added_by,
4545
added_at,
@@ -51,12 +51,12 @@ public(package) fun new<D: store + copy>(
5151

5252
/// Get the stored data from a record
5353
public fun data<D: store + copy>(record: &Record<D>): &D {
54-
&record.stored_data
54+
&record.data
5555
}
5656

5757
/// Get the record metadata
5858
public fun metadata<D: store + copy>(record: &Record<D>): &Option<String> {
59-
&record.record_metadata
59+
&record.metadata
6060
}
6161

6262
/// Get the record sequence number
@@ -82,8 +82,8 @@ public fun correction<D: store + copy>(record: &Record<D>): &RecordCorrection {
8282
/// Destroy a record
8383
public(package) fun destroy<D: store + copy + drop>(record: Record<D>) {
8484
let Record {
85-
stored_data: _,
86-
record_metadata: _,
85+
data: _,
86+
metadata: _,
8787
sequence_number: _,
8888
added_by: _,
8989
added_at: _,
Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,125 @@
1-
// Copyright 2020-2025 IOTA Stiftung
1+
// Copyright 2020-2026 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

4-
//! A minimal full client wrapper for audit trail interactions.
4+
//! A full client wrapper for audit trail interactions.
55
//!
6-
//! This is a scaffold that will be extended with transaction-building capabilities
7-
//! once the Move contract API is finalized.
6+
//! This client includes signing capabilities for executing transactions.
87
98
use std::ops::Deref;
109

1110
use crate::client::read_only::AuditTrailClientReadOnly;
11+
use crate::core::builder::AuditTrailBuilder;
12+
use crate::core::handler::{AuditTrailFull, AuditTrailHandle, AuditTrailReadOnly};
13+
use crate::error::Error;
14+
use iota_interaction::types::base_types::ObjectID;
15+
use iota_interaction::types::transaction::ProgrammableTransaction;
16+
use iota_interaction::{IotaKeySignature, OptionalSync};
17+
use iota_sdk::types::crypto::PublicKey;
18+
use product_common::core_client::{CoreClient, CoreClientReadOnly};
19+
use product_common::network_name::NetworkName;
20+
use secret_storage::Signer;
21+
use serde::de::DeserializeOwned;
1222

13-
/// A full client that wraps the read-only client and will host write operations.
23+
/// A full client that wraps the read-only client and hosts write operations.
1424
#[derive(Clone)]
15-
pub struct AuditTrailClient {
25+
pub struct AuditTrailClient<S> {
1626
read_client: AuditTrailClientReadOnly,
27+
public_key: PublicKey,
28+
signer: S,
1729
}
1830

19-
impl Deref for AuditTrailClient {
31+
impl<S> Deref for AuditTrailClient<S> {
2032
type Target = AuditTrailClientReadOnly;
2133
fn deref(&self) -> &Self::Target {
2234
&self.read_client
2335
}
2436
}
2537

26-
impl AuditTrailClient {
27-
/// Creates a new full client from an existing read-only client.
28-
pub fn new(read_client: AuditTrailClientReadOnly) -> Self {
29-
Self { read_client }
38+
impl<S> AuditTrailClient<S>
39+
where
40+
S: Signer<IotaKeySignature>,
41+
{
42+
pub async fn new(client: AuditTrailClientReadOnly, signer: S) -> Result<Self, Error> {
43+
let public_key = signer
44+
.public_key()
45+
.await
46+
.map_err(|e| Error::InvalidKey(e.to_string()))?;
47+
48+
Ok(Self {
49+
public_key,
50+
read_client: client,
51+
signer,
52+
})
3053
}
54+
}
3155

32-
/// Returns a reference to the underlying read-only client.
33-
pub const fn read_only(&self) -> &AuditTrailClientReadOnly {
56+
impl<S> AuditTrailClient<S> {
57+
pub fn read_only(&self) -> &AuditTrailClientReadOnly {
3458
&self.read_client
3559
}
60+
61+
pub fn trail<'a>(&'a self, trail_id: ObjectID) -> AuditTrailHandle<'a, Self> {
62+
AuditTrailHandle::new(self, trail_id)
63+
}
64+
65+
/// Creates a builder for an audit trail.
66+
pub fn create_trail(&self) -> AuditTrailBuilder {
67+
AuditTrailBuilder::new()
68+
}
69+
70+
pub async fn migrate(&self, _trail_id: ObjectID, _cap_id: ObjectID) -> Result<(), Error> {
71+
Err(Error::NotImplemented("AuditTrailClient::migrate"))
72+
}
73+
74+
pub async fn delete_trail(&self, _trail_id: ObjectID, _cap_id: ObjectID) -> Result<(), Error> {
75+
Err(Error::NotImplemented("AuditTrailClient::delete_trail"))
76+
}
77+
}
78+
79+
#[async_trait::async_trait]
80+
impl<S> CoreClientReadOnly for AuditTrailClient<S> {
81+
fn package_id(&self) -> ObjectID {
82+
self.read_client.package_id()
83+
}
84+
85+
fn network_name(&self) -> &NetworkName {
86+
self.read_client.network()
87+
}
88+
89+
fn client_adapter(&self) -> &crate::iota_interaction_adapter::IotaClientAdapter {
90+
self.read_client.iota_client()
91+
}
92+
}
93+
94+
#[async_trait::async_trait]
95+
impl<S> CoreClient<S> for AuditTrailClient<S>
96+
where
97+
S: Signer<IotaKeySignature> + OptionalSync,
98+
{
99+
fn signer(&self) -> &S {
100+
&self.signer
101+
}
102+
103+
fn sender_address(&self) -> iota_interaction::types::base_types::IotaAddress {
104+
iota_interaction::types::base_types::IotaAddress::from(&self.public_key)
105+
}
106+
107+
fn sender_public_key(&self) -> &iota_interaction::types::crypto::PublicKey {
108+
&self.public_key
109+
}
36110
}
111+
112+
#[async_trait::async_trait]
113+
impl<S> AuditTrailReadOnly for AuditTrailClient<S>
114+
where
115+
S: Signer<IotaKeySignature> + OptionalSync,
116+
{
117+
async fn execute_read_only_transaction<T: DeserializeOwned>(
118+
&self,
119+
tx: ProgrammableTransaction,
120+
) -> Result<T, Error> {
121+
self.read_client.execute_read_only_transaction(tx).await
122+
}
123+
}
124+
125+
impl<S> AuditTrailFull for AuditTrailClient<S> where S: Signer<IotaKeySignature> + OptionalSync {}

audit-trail-rs/src/client/mod.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
// Copyright 2020-2025 IOTA Stiftung
1+
// Copyright 2020-2026 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

44
//! Client implementations for interacting with audit trails on the IOTA blockchain.
5-
//!
6-
//! This module provides two client types:
7-
//! - [`read_only`]: Read-only access to audit trail data
8-
//! - [`full_client`]: Full read-write access with transaction capabilities
95
106
use iota_interaction::IotaClientTrait;
117
use product_common::network_name::NetworkName;

audit-trail-rs/src/client/read_only.rs

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
// Copyright 2020-2025 IOTA Stiftung
1+
// Copyright 2020-2026 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

44
//! A read-only client for interacting with IOTA Audit Trail module objects.
5-
//!
6-
//! This client provides minimal setup to resolve the audit trail package ID
7-
//! and basic access to the underlying IOTA client adapter.
85
96
use std::ops::Deref;
107

118
#[cfg(not(target_arch = "wasm32"))]
129
use iota_interaction::IotaClient;
13-
use iota_interaction::types::base_types::ObjectID;
10+
use iota_interaction::IotaClientTrait;
11+
use iota_interaction::types::base_types::{IotaAddress, ObjectID};
12+
use iota_interaction::types::transaction::{ProgrammableTransaction, TransactionKind};
1413
#[cfg(target_arch = "wasm32")]
1514
use iota_interaction_ts::bindings::WasmIotaClient;
15+
use product_common::core_client::CoreClientReadOnly;
1616
use product_common::network_name::NetworkName;
1717
use product_common::package_registry::Env;
18+
use serde::de::DeserializeOwned;
1819

1920
use super::network_id;
21+
use crate::core::handler::{AuditTrailHandle, AuditTrailReadOnly};
2022
use crate::error::Error;
2123
use crate::iota_interaction_adapter::IotaClientAdapter;
2224
use crate::package;
@@ -62,6 +64,11 @@ impl AuditTrailClientReadOnly {
6264
&self.iota_client
6365
}
6466

67+
/// Returns a typed handle bound to a trail id.
68+
pub fn trail<'a>(&'a self, trail_id: ObjectID) -> AuditTrailHandle<'a, Self> {
69+
AuditTrailHandle::new(self, trail_id)
70+
}
71+
6572
/// Attempts to create a new [`AuditTrailClientReadOnly`] from a given IOTA client.
6673
///
6774
/// This resolves the package ID from the internal registry based on the network.
@@ -126,3 +133,48 @@ impl AuditTrailClientReadOnly {
126133
Self::new_internal(client, network).await
127134
}
128135
}
136+
137+
#[async_trait::async_trait]
138+
impl CoreClientReadOnly for AuditTrailClientReadOnly {
139+
fn package_id(&self) -> ObjectID {
140+
self.audit_trail_pkg_id
141+
}
142+
143+
fn network_name(&self) -> &NetworkName {
144+
&self.network
145+
}
146+
147+
fn client_adapter(&self) -> &IotaClientAdapter {
148+
&self.iota_client
149+
}
150+
}
151+
152+
#[async_trait::async_trait]
153+
impl AuditTrailReadOnly for AuditTrailClientReadOnly {
154+
async fn execute_read_only_transaction<T: DeserializeOwned>(
155+
&self,
156+
tx: ProgrammableTransaction,
157+
) -> Result<T, Error> {
158+
let inspection_result = self
159+
.iota_client
160+
.read_api()
161+
.dev_inspect_transaction_block(IotaAddress::ZERO, TransactionKind::programmable(tx), None, None, None)
162+
.await
163+
.map_err(|err| Error::UnexpectedApiResponse(format!("Failed to inspect transaction block: {err}")))?;
164+
165+
let execution_results = inspection_result
166+
.results
167+
.ok_or_else(|| Error::UnexpectedApiResponse("DevInspectResults missing 'results' field".to_string()))?;
168+
169+
let (return_value_bytes, _) = execution_results
170+
.first()
171+
.ok_or_else(|| Error::UnexpectedApiResponse("Execution results list is empty".to_string()))?
172+
.return_values
173+
.first()
174+
.ok_or_else(|| Error::InvalidArgument("should have at least one return value".to_string()))?;
175+
176+
let deserialized_output = bcs::from_bytes::<T>(return_value_bytes)?;
177+
178+
Ok(deserialized_output)
179+
}
180+
}

audit-trail-rs/src/core/builder.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2020-2026 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Audit trail builder for creation transactions.
5+
6+
use product_common::transaction::transaction_builder::TransactionBuilder;
7+
8+
use super::transactions::CreateTrail;
9+
use super::types::{Data, ImmutableMetadata, LockingConfig};
10+
use crate::error::Error;
11+
12+
/// Builder for creating an audit trail.
13+
#[derive(Debug, Clone)]
14+
pub struct AuditTrailBuilder {
15+
pub initial_data: Option<Data>,
16+
pub initial_record_metadata: Option<String>,
17+
pub locking_config: LockingConfig,
18+
pub trail_metadata: Option<ImmutableMetadata>,
19+
pub updatable_metadata: Option<String>,
20+
}
21+
22+
impl AuditTrailBuilder {
23+
/// Creates a new builder.
24+
pub fn new() -> Self {
25+
Self {
26+
initial_data: None,
27+
initial_record_metadata: None,
28+
locking_config: LockingConfig::none(),
29+
trail_metadata: None,
30+
updatable_metadata: None,
31+
}
32+
}
33+
34+
/// Sets the initial record data and optional record metadata.
35+
pub fn with_initial_record(mut self, data: Data, metadata: Option<String>) -> Self {
36+
self.initial_data = Some(data);
37+
self.initial_record_metadata = metadata;
38+
self
39+
}
40+
41+
/// Sets the locking configuration for the trail.
42+
pub fn with_locking_config(mut self, config: LockingConfig) -> Self {
43+
self.locking_config = config;
44+
self
45+
}
46+
47+
/// Sets immutable metadata for the trail.
48+
pub fn with_trail_metadata(mut self, metadata: ImmutableMetadata) -> Self {
49+
self.trail_metadata = Some(metadata);
50+
self
51+
}
52+
53+
/// Sets immutable metadata by parts.
54+
pub fn with_trail_metadata_parts(mut self, name: impl Into<String>, description: Option<String>) -> Self {
55+
self.trail_metadata = Some(ImmutableMetadata {
56+
name: name.into(),
57+
description,
58+
});
59+
self
60+
}
61+
62+
/// Sets updatable metadata for the trail.
63+
pub fn with_updatable_metadata(mut self, metadata: impl Into<String>) -> Self {
64+
self.updatable_metadata = Some(metadata.into());
65+
self
66+
}
67+
68+
/// Finalizes the builder and creates a transaction builder.
69+
pub fn finish(self) -> Result<TransactionBuilder<CreateTrail>, Error> {
70+
Ok(TransactionBuilder::new(CreateTrail::new(self)))
71+
}
72+
}

0 commit comments

Comments
 (0)