diff --git a/audit-trail-rs/src/core/create/operations.rs b/audit-trail-rs/src/core/create/operations.rs index 99d981e8..3b7da516 100644 --- a/audit-trail-rs/src/core/create/operations.rs +++ b/audit-trail-rs/src/core/create/operations.rs @@ -25,14 +25,12 @@ impl CreateOps { ) -> Result { let mut ptb = ProgrammableTransactionBuilder::new(); - let initial_data = initial_data.ok_or_else(|| { - Error::InvalidArgument( - "initial_data is required to create the default flexible trail; use `with_initial_record(...)`" - .to_string(), - ) - })?; let data_tag = Data::tag(audit_trail_package_id); - let initial_data_arg = initial_data.to_option_ptb(&mut ptb, audit_trail_package_id)?; + let initial_data_arg = match initial_data { + Some(data) => data.to_option_ptb(&mut ptb, audit_trail_package_id)?, + None => utils::option_to_move(None, data_tag.clone(), &mut ptb) + .map_err(|e| Error::InvalidArgument(format!("failed to build initial_data option: {e}")))?, + }; let initial_record_metadata = utils::ptb_pure(&mut ptb, "initial_record_metadata", initial_record_metadata)?; let locking_config = locking_config.to_ptb(&mut ptb, audit_trail_package_id, tf_components_package_id)?; diff --git a/audit-trail-rs/src/core/types/record.rs b/audit-trail-rs/src/core/types/record.rs index 647a64e1..404a0128 100644 --- a/audit-trail-rs/src/core/types/record.rs +++ b/audit-trail-rs/src/core/types/record.rs @@ -70,7 +70,7 @@ impl Data { TypeTag::from_str(&format!("{package_id}::record::Data")).expect("should be valid type tag") } - /// Creates a PTB argument for the default flexible Move `record::Data` type. + /// Creates a PTB argument for the Move `record::Data` type exposed by the Rust SDK. pub(in crate::core) fn to_ptb(self, ptb: &mut Ptb, package_id: ObjectID) -> Result { match self { Data::Bytes(bytes) => { @@ -103,7 +103,7 @@ impl Data { .map_err(|e| Error::InvalidArgument(format!("failed to build record data option: {e}"))) } - /// Validates that the on-chain trail stores the default flexible Move `record::Data` type. + /// Validates that the on-chain trail stores the Move `record::Data` type supported by the Rust SDK. pub(in crate::core) fn ensure_supported_trail_tag(expected: &TypeTag, package_id: ObjectID) -> Result<(), Error> { let supported = Self::tag(package_id); @@ -175,50 +175,3 @@ impl From<&[u8]> for Data { Data::Bytes(value.to_vec()) } } - -#[cfg(test)] -mod tests { - use super::Data; - use iota_interaction::types::TypeTag; - use iota_interaction::types::base_types::ObjectID; - use std::str::FromStr; - - fn roundtrip(value: &Data) -> Data { - let encoded = bcs::to_bytes(value).expect("failed to bcs encode Data"); - bcs::from_bytes::(&encoded).expect("failed to deserialize Data from bcs payload") - } - - #[test] - fn deserialize_text_variant_roundtrips() { - let data = roundtrip(&Data::Text("hello world".to_string())); - assert_eq!(data, Data::Text("hello world".to_string())); - } - - #[test] - fn deserialize_bytes_variant_roundtrips() { - let data = roundtrip(&Data::Bytes(vec![0xF0, 0x28, 0x8C, 0x28])); - assert_eq!(data, Data::Bytes(vec![0xF0, 0x28, 0x8C, 0x28])); - } - - #[test] - fn supported_trail_tag_accepts_record_data() { - let package_id = ObjectID::from_str("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - .expect("valid object id"); - - let expected = Data::tag(package_id); - Data::ensure_supported_trail_tag(&expected, package_id).expect("record::Data should be supported"); - } - - #[test] - fn supported_trail_tag_rejects_legacy_string_trails() { - let package_id = ObjectID::from_str("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - .expect("valid object id"); - let legacy_string = TypeTag::from_str("0x1::string::String").expect("valid string type tag"); - - let err = Data::ensure_supported_trail_tag(&legacy_string, package_id).expect_err("legacy tag should fail"); - assert!( - err.to_string().contains("unsupported trail record type"), - "unexpected error: {err}" - ); - } -} diff --git a/audit-trail-rs/tests/e2e/records.rs b/audit-trail-rs/tests/e2e/records.rs index 98ff7bff..97f4a9a7 100644 --- a/audit-trail-rs/tests/e2e/records.rs +++ b/audit-trail-rs/tests/e2e/records.rs @@ -75,7 +75,7 @@ async fn add_and_fetch_record_roundtrip() -> anyhow::Result<()> { } #[tokio::test] -async fn add_record_accepts_mixed_data_variants_in_default_trail() -> anyhow::Result<()> { +async fn add_record_accepts_text_and_bytes_variants() -> anyhow::Result<()> { let client = get_funded_test_client().await?; let trail_id = client.create_test_trail(Data::text("text-trail")).await?; let records = client.trail(trail_id).records(); @@ -98,6 +98,30 @@ async fn add_record_accepts_mixed_data_variants_in_default_trail() -> anyhow::Re Ok(()) } +#[tokio::test] +async fn add_record_to_empty_trail_created_without_initial_record() -> anyhow::Result<()> { + let client = get_funded_test_client().await?; + let trail_id = client.create_trail().finish().build_and_execute(&client).await?.output.trail_id; + let records = client.trail(trail_id).records(); + + grant_role_capability(&client, trail_id, "RecordWriter", [Permission::AddRecord]).await?; + + let added = records + .add(Data::text("first record"), Some("first metadata".to_string())) + .build_and_execute(&client) + .await? + .output; + + assert_eq!(added.sequence_number, 0); + assert_eq!(records.record_count().await?, 1); + + let record = records.get(0).await?; + assert_eq!(record.metadata, Some("first metadata".to_string())); + assert_text_data(record.data, "first record"); + + Ok(()) +} + #[tokio::test] async fn get_missing_record_fails() -> 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 d84a6448..f0c895d7 100644 --- a/audit-trail-rs/tests/e2e/trail.rs +++ b/audit-trail-rs/tests/e2e/trail.rs @@ -42,6 +42,25 @@ async fn create_trail_with_default_builder_settings() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn create_empty_trail_without_initial_record() -> anyhow::Result<()> { + let client = get_funded_test_client().await?; + + let created = client.create_trail().finish().build_and_execute(&client).await?.output; + + assert_eq!(created.creator, client.sender_address()); + + let on_chain = created.fetch_audit_trail(&client).await?; + assert_eq!(on_chain.id.object_id(), &created.trail_id); + assert_eq!(on_chain.creator, client.sender_address()); + assert_eq!(on_chain.sequence_number, 0); + assert_eq!(on_chain.locking_config, config_with_window(LockingWindow::None)); + assert!(on_chain.immutable_metadata.is_none()); + assert!(on_chain.updatable_metadata.is_none()); + + Ok(()) +} + #[tokio::test] async fn create_trail_with_metadata_and_time_lock() -> anyhow::Result<()> { let client = get_funded_test_client().await?;