diff --git a/audit-trail-move/sources/audit_trail.move b/audit-trail-move/sources/audit_trail.move index da99e4a..4bcf6f0 100644 --- a/audit-trail-move/sources/audit_trail.move +++ b/audit-trail-move/sources/audit_trail.move @@ -8,35 +8,22 @@ module audit_trail::main; use audit_trail::{ - capability::{Self, Capability}, + capability::Capability, locking::{Self, LockingConfig, LockingWindow, set_delete_record_lock}, permission::{Self, Permission}, - record::{Self, Record} -}; -use iota::{ - clock::{Self, Clock}, - event, - linked_table::{Self, LinkedTable}, - vec_map::{Self, VecMap}, - vec_set::{Self, VecSet} + record::{Self, Record}, + role_map::{Self, RoleMap} }; +use iota::{clock::{Self, Clock}, event, linked_table::{Self, LinkedTable}}; use std::string::String; // ===== Errors ===== #[error] const ERecordNotFound: vector = b"Record not found at the given sequence number"; #[error] -const ERoleDoesNotExist: vector = b"The specified role does not exist in the `roles` map"; -#[error] const EPermissionDenied: vector = b"The role associated with the provided capability does not have the required permission"; #[error] -const ECapabilityHasBeenRevoked: vector = - b"The provided capability has been revoked and is no longer valid"; -#[error] -const ETrailIdNotCorrect: vector = - b"The trail ID associated with the provided capability does not match the audit trail"; -#[error] const ERecordLocked: vector = b"The record is locked and cannot be deleted"; // ===== Constants ===== @@ -68,14 +55,12 @@ public struct AuditTrail has key, store { records: LinkedTable>, /// Deletion locking rules locking_config: LockingConfig, - /// Map of role names to permission sets. - roles: VecMap>, + /// A list of role definitions consisting of a unique role specifier and a list of associated permissions + roles: RoleMap, /// Set at creation, cannot be changed immutable_metadata: Option, /// Can be updated by holders of MetadataUpdate permission updatable_metadata: Option, - /// Whitelist of valid capability IDs - issued_capabilities: VecSet, } // ===== Events ===== @@ -110,16 +95,6 @@ public struct RecordDeleted has copy, drop { timestamp: u64, } -/// Emitted when a capability is issued -public struct CapabilityIssued has copy, drop { - trail_id: ID, - capability_id: ID, - role: String, - issued_to: address, - issued_by: address, - timestamp: u64, -} - // ===== Constructors ===== /// Create immutable trail metadata @@ -187,16 +162,25 @@ public fun create( initial_data.destroy_none(); }; - let mut roles = vec_map::empty>(); - roles.insert(initial_admin_role_name(), permission::admin_permissions()); + let role_admin_permissions = role_map::new_role_admin_permissions( + permission::add_roles(), + permission::delete_roles(), + permission::update_roles(), + ); + + let capability_admin_permissions = role_map::new_capability_admin_permissions( + permission::add_capabilities(), + permission::revoke_capabilities(), + ); - let admin_cap = capability::new_capability( - initial_admin_role_name(), + let (roles, admin_cap) = role_map::new( trail_id, + initial_admin_role_name(), + permission::admin_permissions(), + role_admin_permissions, + capability_admin_permissions, ctx, ); - let mut issued_capabilities = vec_set::empty(); - issued_capabilities.insert(admin_cap.id()); let trail = AuditTrail { id: trail_uid, @@ -208,7 +192,6 @@ public fun create( roles, immutable_metadata: trail_metadata, updatable_metadata, - issued_capabilities, }; transfer::share_object(trail); @@ -240,7 +223,17 @@ public fun add_record( clock: &Clock, ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::add_record()), EPermissionDenied); + assert!( + trail + .roles + .is_capability_valid( + cap, + &permission::add_record(), + clock, + ctx, + ), + EPermissionDenied, + ); let caller = ctx.sender(); let timestamp = clock::timestamp_ms(clock); @@ -277,7 +270,17 @@ public fun delete_record( clock: &Clock, ctx: &mut TxContext, ) { - assert!(trail.has_capability_permission(cap, &permission::delete_record()), EPermissionDenied); + assert!( + trail + .roles + .is_capability_valid( + cap, + &permission::delete_record(), + clock, + ctx, + ), + EPermissionDenied, + ); assert!(linked_table::contains(&trail.records, sequence_number), ERecordNotFound); assert!(!trail.is_record_locked(sequence_number, clock), ERecordLocked); @@ -324,10 +327,18 @@ public fun update_locking_config( trail: &mut AuditTrail, cap: &Capability, new_config: LockingConfig, - _: &mut TxContext, + clock: &Clock, + ctx: &TxContext, ) { assert!( - trail.has_capability_permission(cap, &permission::update_locking_config()), + trail + .roles + .is_capability_valid( + cap, + &permission::update_locking_config(), + clock, + ctx, + ), EPermissionDenied, ); trail.locking_config = new_config; @@ -338,13 +349,18 @@ public fun update_locking_config_for_delete_record( trail: &mut AuditTrail, cap: &Capability, new_delete_record_lock: LockingWindow, - _: &mut TxContext, + clock: &Clock, + ctx: &TxContext, ) { assert!( - trail.has_capability_permission( - cap, - &permission::update_locking_config_for_delete_record(), - ), + trail + .roles + .is_capability_valid( + cap, + &permission::update_locking_config_for_delete_record(), + clock, + ctx, + ), EPermissionDenied, ); set_delete_record_lock(&mut trail.locking_config, new_delete_record_lock); @@ -355,10 +371,18 @@ public fun update_metadata( trail: &mut AuditTrail, cap: &Capability, new_metadata: Option, - _: &mut TxContext, + clock: &Clock, + ctx: &TxContext, ) { assert!( - trail.has_capability_permission(cap, &permission::update_metadata()), + trail + .roles + .is_capability_valid( + cap, + &permission::update_metadata(), + clock, + ctx, + ), EPermissionDenied, ); trail.updatable_metadata = new_metadata; @@ -442,130 +466,14 @@ public fun has_record(trail: &AuditTrail, sequence_number: u public fun records(trail: &AuditTrail): &LinkedTable> { &trail.records } +// ===== Role and Capability Functions ===== -// ===== Role related Functions ===== - -/// Get the permissions associated with a specific role. -/// Aborts with ERoleDoesNotExist if the role does not exist. -public fun get_role_permissions( - trail: &AuditTrail, - role: &String, -): &VecSet { - assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); - vec_map::get(&trail.roles, role) -} - -/// Create a new role consisting of a role name and associated permissions -public fun create_role( - trail: &mut AuditTrail, - cap: &Capability, - role: String, - permissions: VecSet, - _: &mut TxContext, -) { - assert!(trail.has_capability_permission(cap, &permission::add_roles()), EPermissionDenied); - vec_map::insert(&mut trail.roles, role, permissions); -} - -/// Delete an existing role -public fun delete_role( - trail: &mut AuditTrail, - cap: &Capability, - role: &String, - _: &mut TxContext, -) { - assert!(trail.has_capability_permission(cap, &permission::delete_roles()), EPermissionDenied); - vec_map::remove(&mut trail.roles, role); -} - -/// Update permissions associated with an existing role -public fun update_role_permissions( - trail: &mut AuditTrail, - cap: &Capability, - role: &String, - new_permissions: VecSet, - _: &mut TxContext, -) { - assert!(trail.has_capability_permission(cap, &permission::update_roles()), EPermissionDenied); - assert!(vec_map::contains(&trail.roles, role), ERoleDoesNotExist); - vec_map::remove(&mut trail.roles, role); - vec_map::insert(&mut trail.roles, *role, new_permissions); -} - -/// Returns the roles defined in the audit trail -public fun roles(trail: &AuditTrail): &VecMap> { +/// Returns a reference the RoleMap managing the roles and capabilities used in the audit trail +public fun roles(trail: &AuditTrail): &RoleMap { &trail.roles } -/// Indicates if the specified role exists in the audit trail -public fun has_role(trail: &AuditTrail, role: &String): bool { - vec_map::contains(&trail.roles, role) -} - -// ===== Capability related Functions ===== - -/// Indicates if a provided capability has a specific permission. -public fun has_capability_permission( - trail: &AuditTrail, - cap: &Capability, - permission: &Permission, -): bool { - assert!(trail.id() == cap.trail_id(), ETrailIdNotCorrect); - assert!(trail.issued_capabilities.contains(&cap.id()), ECapabilityHasBeenRevoked); - let permissions = trail.get_role_permissions(cap.role()); - vec_set::contains(permissions, permission) -} - -/// Create a new capability with a specific role -/// Aborts with ERoleDoesNotExist if the role does not exist. -public fun new_capability( - trail: &mut AuditTrail, - cap: &Capability, - role: &String, - ctx: &mut TxContext, -): Capability { - assert!( - trail.has_capability_permission(cap, &permission::add_capabilities()), - EPermissionDenied, - ); - assert!(trail.roles.contains(role), ERoleDoesNotExist); - let new_cap = capability::new_capability( - *role, - trail.id(), - ctx, - ); - trail.issued_capabilities.insert(new_cap.id()); - new_cap -} - -/// Destroy an existing capability -/// Every owner of a capability is allowed to destroy it when no longer needed. -/// TODO: Clarify if we need to restrict access with the `CapabilitiesRevoke` permission here. -/// If yes, we also need a destroy function for Admin capabilities (without the need of another Admin capability). -/// Otherwise the last Admin capability holder will block the trail forever by not being able to destroy it. -public fun destroy_capability( - trail: &mut AuditTrail, - cap_to_destroy: Capability, -) { - assert!(trail.id() == cap_to_destroy.trail_id(), ETrailIdNotCorrect); - trail.issued_capabilities.remove(&cap_to_destroy.id()); - cap_to_destroy.destroy(); -} - -/// Revoke a capability. Requires `CapabilitiesRevoke` permission. -public fun revoke_capability( - trail: &mut AuditTrail, - cap: &Capability, - cap_to_revoke: ID, -) { - assert!( - trail.has_capability_permission(cap, &permission::revoke_capabilities()), - EPermissionDenied, - ); - trail.issued_capabilities.remove(&cap_to_revoke); -} - -/// Get the capabilities issued for this trail -public fun issued_capabilities(trail: &AuditTrail): &VecSet { - &trail.issued_capabilities +/// Returns a mutable reference to the RoleMap managing the roles and capabilities used in the audit trail +public fun roles_mut(trail: &mut AuditTrail): &mut RoleMap { + &mut trail.roles } diff --git a/audit-trail-move/sources/capability.move b/audit-trail-move/sources/capability.move index 87ca0ce..d793a06 100644 --- a/audit-trail-move/sources/capability.move +++ b/audit-trail-move/sources/capability.move @@ -4,72 +4,200 @@ /// Role-based access control capabilities for audit trails module audit_trail::capability; +use iota::clock::{Self, Clock}; use std::string::String; +// ===== Errors ===== + +#[error] +const EValidityPeriodInconsistent: vector = + b"Validity period is inconsistent: valid_from must be before valid_until"; + // ===== Core Structures ===== -/// Capability granting role-based access to an audit trail +/// Capability granting role-based access to a managed onchain object (i.e. an audit trail) public struct Capability has key, store { id: UID, - trail_id: ID, + /// The ID of the onchain object this capability applies to + security_vault_id: ID, + /// The role granted by this capability + /// Arbitrary string specifying a role contained in the `role_map::RoleMap` mapping + role: String, + /// For whom has this capability been issued + /// * If Some(address), the capability is bound to that specific address + /// * If None, the capability is not bound to a specific address + issued_to: Option
, + /// Optional validity period start timestamp (in seconds since Unix epoch) + /// * The specified timestamp is included in the validity period + /// * If None, the capability is valid from creation time + valid_from: Option, + /// Optional validity period end timestamp (in seconds since Unix epoch) + /// * The specified timestamp is excluded in the validity period + /// * If None, the capability does not expire + valid_until: Option, +} + +/// Create a new capability with a specific role and all available optional restrictions +/// +/// Parameters: +/// * role: The role granted by this capability +/// * security_vault_id: The ID of onchain object (i.e. an audit trail) this capability applies to +/// * issued_to: Optional address restriction; if Some(address), the capability is bound to that specific address +/// * valid_from: Optional validity period start timestamp (in seconds since Unix epoch); if Some(ts), the capability is valid from that timestamp onwards +/// * valid_until: Optional validity period end timestamp (in seconds since Unix epoch); if Some(ts), the capability is valid until that timestamp +/// * ctx: The transaction context +/// +/// Returns: The newly created Capability +/// +/// Errors: +/// * EValidityPeriodInconsistent: If both valid_from and valid_until are provided and valid_from >= valid_until +public(package) fun new_capability( + role: String, + security_vault_id: ID, + issued_to: Option
, + valid_from: Option, + valid_until: Option, + ctx: &mut TxContext, +): Capability { + if (valid_from.is_some() && valid_until.is_some()) { + let from = valid_from.borrow(); + let until = valid_until.borrow(); + assert!(*from < *until, EValidityPeriodInconsistent); + }; + Capability { + id: object::new(ctx), + role, + security_vault_id, + issued_to, + valid_from, + valid_until, + } +} + +/// Create a new unrestricted capability with a specific role +public(package) fun new_capability_without_restrictions( role: String, + security_vault_id: ID, + ctx: &mut TxContext, +): Capability { + Capability { + id: object::new(ctx), + role, + security_vault_id, + issued_to: std::option::none(), + valid_from: std::option::none(), + valid_until: std::option::none(), + } } -/// Create a new capability with a specific role -public(package) fun new_capability(role: String, trail_id: ID, ctx: &mut TxContext): Capability { +/// Create a new capability with a specific role and validity period, valid until the given timestamp +public(package) fun new_capability_valid_until( + role: String, + security_vault_id: ID, + valid_until: u64, + ctx: &mut TxContext, +): Capability { Capability { id: object::new(ctx), role, - trail_id, + security_vault_id, + issued_to: std::option::none(), + valid_from: std::option::none(), + valid_until: std::option::some(valid_until), } } -// TODO: Is this needed? What is a setup capability? -// -// /// Create a setup capability for trail initialization -// public fun new_setup_cap(ctx: &mut TxContext): Capability { -// Capability { -// id: object::new(ctx), -// } -// } +/// Create a new capability with a specific role, exclusively usable by a specific address and an optional +/// validity period, valid until the given timestamp +public(package) fun new_capability_for_address( + role: String, + security_vault_id: ID, + issued_to: address, + valid_until: Option, + ctx: &mut TxContext, +): Capability { + Capability { + id: object::new(ctx), + role, + security_vault_id, + issued_to: std::option::some(issued_to), + valid_from: std::option::none(), + valid_until, + } +} /// Get the capability's ID -public fun cap_id(cap: &Capability): ID { +public fun id(cap: &Capability): ID { object::uid_to_inner(&cap.id) } /// Get the capability's role -public fun cap_role(cap: &Capability): &String { +public fun role(cap: &Capability): &String { &cap.role } -/// Get the capability's trail ID -public fun cap_trail_id(cap: &Capability): ID { - cap.trail_id +/// Get the capability's security_vault_id +public fun security_vault_id(cap: &Capability): ID { + cap.security_vault_id } /// Check if the capability has a specific role -public fun cap_has_role(cap: &Capability, role: &String): bool { +public fun has_role(cap: &Capability, role: &String): bool { &cap.role == role } -/// Destroy a capability -public(package) fun cap_destroy(cap: Capability) { - let Capability { id, role: _role, trail_id: _trail_id } = cap; - object::delete(id); +// Get the capability's issued_to address +public fun issued_to(cap: &Capability): &Option
{ + &cap.issued_to } -#[test_only] -public fun cap_destroy_for_testing(cap: Capability) { - cap_destroy(cap); +// Get the capability's valid_from timestamp +public fun valid_from(cap: &Capability): &Option { + &cap.valid_from +} + +// Get the capability's valid_until timestamp +public fun valid_until(cap: &Capability): &Option { + &cap.valid_until } -// ===== public use statements ===== +// Check if the capability is currently valid for `clock::timestamp_ms(clock)` +public fun is_currently_valid(cap: &Capability, clock: &Clock): bool { + let current_ts = clock::timestamp_ms(clock) / 1000; // convert to seconds + cap.is_valid_for_timestamp(current_ts) +} + +// Check if the capability is valid for a specific timestamp (in seconds since Unix epoch) +public fun is_valid_for_timestamp(cap: &Capability, timestamp_secs: u64): bool { + let valid_from_ok = if (cap.valid_from.is_some()) { + let from = cap.valid_from.borrow(); + timestamp_secs >= *from + } else { + true + }; + let valid_until_ok = if (cap.valid_until.is_some()) { + let until = cap.valid_until.borrow(); + timestamp_secs < *until + } else { + true + }; + valid_from_ok && valid_until_ok +} + +/// Destroy a capability +public(package) fun destroy(cap: Capability) { + let Capability { + id, + role: _role, + security_vault_id: _trail_id, + issued_to: _issued_to, + valid_from: _valid_from, + valid_until: _valid_until, + } = cap; + object::delete(id); +} -public use fun cap_id as Capability.id; -public use fun cap_role as Capability.role; -public use fun cap_trail_id as Capability.trail_id; -public use fun cap_has_role as Capability.has_role; -public use fun cap_destroy as Capability.destroy; #[test_only] -public use fun cap_destroy_for_testing as Capability.destroy_for_testing; +public fun destroy_for_testing(cap: Capability) { + destroy(cap); +} diff --git a/audit-trail-move/sources/role_map.move b/audit-trail-move/sources/role_map.move new file mode 100644 index 0000000..5416969 --- /dev/null +++ b/audit-trail-move/sources/role_map.move @@ -0,0 +1,601 @@ +// Copyright (c) 2026 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// A role-based access control helper mapping unique role identifiers to their associated permissions. +/// +/// Provides the following functionalities: +/// - Define an initial role with a custom set of permissions (i.e. an Admin role). +/// - Use custom permission types defined by the integrating module using the generic parameter `P`. +/// - Create, delete, and update roles and their permissions +/// - Issue, revoke, and destroy `audit_trail::capability`s associated with a specific role. +/// - Validate `audit_trail::capability`s against the defined roles to facilitate proper access control by other modules +/// (function `RoleMap.is_capability_valid()`) +/// - All functions are access restricted by custom permissions defined during `RoleMap` instantiation. +/// +/// Examples: +/// - audit_trail::main module uses `RoleMap` to manage access to the audit trail records and their operations. + +module audit_trail::role_map; + +use audit_trail::capability::{Self, Capability}; +use iota::{clock::Clock, event, vec_map::{Self, VecMap}, vec_set::{Self, VecSet}}; +use std::string::String; + +// =============== Errors ========================================================== + +#[error] +const EPermissionDenied: vector = + b"The role associated with the provided capability does not have the required permission"; +#[error] +const ERoleDoesNotExist: vector = + b"The specified role, directly specified or specified by a capability, does not exist in the `RoleMap` mapping"; +#[error] +const ECapabilityHasBeenRevoked: vector = + b"The provided capability has been revoked and is no longer valid"; +#[error] +const ECapabilitySecurityVaultIdMismatch: vector = + b"The security_vault_id associated with the provided capability does not match the security_vault_id of the `RoleMap`"; +#[error] +const ECapabilityTimeConstraintsNotMet: vector = + b"The capability's time constraints are not currently met either due to `valid_from` or `valid_until` restrictions"; +#[error] +const ECapabilityIssuedToMismatch: vector = + b"The capability is restricted to a specific address which does not match the caller's address"; +#[error] +const ECapabilityPermissionDenied: vector = + b"The role associated with provided capability does not have the required permission"; + +// =============== Events ========================================================== + +/// Emitted when a capability is issued +public struct CapabilityIssued has copy, drop { + security_vault_id: ID, + capability_id: ID, + role: String, + issued_to: Option
, + valid_from: Option, + valid_until: Option, +} + +/// Emitted when a capability is destroyed +public struct CapabilityDestroyed has copy, drop { + security_vault_id: ID, + capability_id: ID, + role: String, + issued_to: Option
, + valid_from: Option, + valid_until: Option, +} + +/// Emitted when a capability is revoked or destroyed +public struct CapabilityRevoked has copy, drop { + security_vault_id: ID, + capability_id: ID, +} + +// TODO: Add event for Role creation, removing, updating, etc. + +// =============== Core Types ====================================================== + +/// Defines the permissions required to administer roles in this RoleMap +public struct RoleAdminPermissions has copy, drop, store { + /// Permission required to add a new role + add: P, + /// Permission required to delete an existing role + delete: P, + /// Permission required to update permissions associated with an existing role + update: P, +} + +/// Defines the permissions required to administer capabilities in this RoleMap +public struct CapabilityAdminPermissions has copy, drop, store { + /// Permission required to add (issue) a new capability + add: P, + /// Permission required to revoke an existing capability + revoke: P, +} + +/// The RoleMap structure mapping role names to their associated permissions +/// Generic parameter P defines the permission type used by the integrating module +/// (i.e. audit_trail::Permission) +public struct RoleMap has copy, drop, store { + /// The ObjectID of the onchain object integrating this RoleMap + security_vault_id: ID, + /// Mapping of role names to their associated permissions + roles: VecMap>, + /// Whitelist of all issued capability IDs + issued_capabilities: VecSet, + /// Permissions required to administer roles in this RoleMap + role_admin_permissions: RoleAdminPermissions

, + /// Permissions required to administer capabilities in this RoleMap + capability_admin_permissions: CapabilityAdminPermissions

, +} + +// =============== Role & Capability AdminPermissions Functions ==================== + +public fun new_role_admin_permissions( + add: P, + delete: P, + update: P, +): RoleAdminPermissions

{ + RoleAdminPermissions { + add, + delete, + update, + } +} + +public fun new_capability_admin_permissions( + add: P, + revoke: P, +): CapabilityAdminPermissions

{ + CapabilityAdminPermissions { + add, + revoke, + } +} + +// =============== RoleMap Functions =============================================== + +/// Create a new RoleMap with an initial admin role +/// The initial admin role is created with the specified name and permissions +/// An initial admin capability is created and returned alongside the RoleMap +/// The initial admin capability has no restrictions (no address, valid_from, or valid_until) +/// The security_vault_id is associated with both the RoleMap and the initial admin capability +/// Returns the newly created RoleMap and the initial admin capability +/// +/// Parameters +/// ---------- +/// - security_vault_id: +/// The security_vault_id to associate this RoleMap with the initial admin capability +/// and all other created capabilities. Set this to the ID of the onchain object that integrates the RoleMap. +/// - initial_admin_role_name: +/// The name of the initial admin role +/// - initial_admin_role_permissions: +/// The permissions associated with the initial admin role +/// - role_admin_permissions: +/// The permissions required to administer roles in this RoleMap +/// - capability_admin_permissions: +/// The permissions required to administer capabilities in this RoleMap +/// - ctx: +/// The transaction context for capability creation +public fun new( + security_vault_id: ID, + initial_admin_role_name: String, + initial_admin_role_permissions: VecSet

, + role_admin_permissions: RoleAdminPermissions

, + capability_admin_permissions: CapabilityAdminPermissions

, + ctx: &mut TxContext, +): (RoleMap

, Capability) { + let mut roles = vec_map::empty>(); + roles.insert(initial_admin_role_name, initial_admin_role_permissions); + + let admin_cap = capability::new_capability_without_restrictions( + initial_admin_role_name, + security_vault_id, + ctx, + ); + let mut issued_capabilities = vec_set::empty(); + issued_capabilities.insert(admin_cap.id()); + let role_map = RoleMap { + roles, + role_admin_permissions, + capability_admin_permissions, + security_vault_id, + issued_capabilities, + }; + + (role_map, admin_cap) +} + +/// Get the permissions associated with a specific role. +/// Aborts with ERoleDoesNotExist if the role does not exist. +public fun get_role_permissions(role_map: &RoleMap

, role: &String): &VecSet

{ + assert!(vec_map::contains(&role_map.roles, role), ERoleDoesNotExist); + vec_map::get(&role_map.roles, role) +} + +/// Create a new role consisting of a role name and associated permissions +public fun create_role( + role_map: &mut RoleMap

, + cap: &Capability, + role: String, + permissions: VecSet

, + clock: &Clock, + ctx: &TxContext, +) { + assert!( + role_map.is_capability_valid( + cap, + &role_map.role_admin_permissions.add, + clock, + ctx, + ), + EPermissionDenied, + ); + + vec_map::insert(&mut role_map.roles, role, permissions); +} + +/// Delete an existing role +public fun delete_role( + role_map: &mut RoleMap

, + cap: &Capability, + role: &String, + clock: &Clock, + ctx: &TxContext, +) { + assert!( + role_map.is_capability_valid( + cap, + &role_map.role_admin_permissions.delete, + clock, + ctx, + ), + EPermissionDenied, + ); + + vec_map::remove(&mut role_map.roles, role); +} + +/// Update permissions associated with an existing role +public fun update_role_permissions( + role_map: &mut RoleMap

, + cap: &Capability, + role: &String, + new_permissions: VecSet

, + clock: &Clock, + ctx: &TxContext, +) { + assert!( + role_map.is_capability_valid( + cap, + &role_map.role_admin_permissions.update, + clock, + ctx, + ), + EPermissionDenied, + ); + + assert!(vec_map::contains(&role_map.roles, role), ERoleDoesNotExist); + vec_map::remove(&mut role_map.roles, role); + vec_map::insert(&mut role_map.roles, *role, new_permissions); +} + +/// Indicates if the specified role exists in the role_map +public fun has_role(role_map: &RoleMap

, role: &String): bool { + vec_map::contains(&role_map.roles, role) +} + +// =============== Capability related Functions ==================================== + +/// Indicates if a provided capability is valid. +/// +/// A capability is considered valid if: +/// - The capability's security_vault_id matches the RoleMap's security_vault_id. +/// Aborts with ECapabilitySecurityVaultIdMismatch if not matching. +/// - The role value specified by the capability exists in the `RoleMap` mapping. +/// Aborts with ERoleDoesNotExist if the role does not exist. +/// - The role associated with the capability contains the permission specified by the `permission` argument. +/// Aborts with ECapabilityPermissionDenied if the permission is not granted by the role. +/// - The capability has not been revoked (is included in the `issued_capabilities` set). +/// Aborts with ECapabilityHasBeenRevoked if revoked. +/// - The capability is currently active, based on its time restrictions (if any). +/// Aborts with ECapabilityTimeConstraintsNotMet, if the current time is outside the valid_from and valid_until range. +/// - If the capability is restricted to a specific address, the caller's address matches the sender of the transaction. +/// Aborts with ECapabilityIssuedToMismatch if the addresses do not match. +/// +/// Parameters +/// ---------- +/// - role_map: Reference to the `RoleMap` mapping. +/// - cap: Reference to the capability to be validated. +/// - permission: The permission to check against the capability's role. +/// - clock: Reference to a Clock instance for time-based validation. +/// - ctx: Reference to the transaction context for accessing the caller's address. +/// +/// Returns +/// ------- +/// - bool: true if the capability is valid, otherwise aborts with the relevant error. +public fun is_capability_valid( + role_map: &RoleMap

, + cap: &Capability, + permission: &P, + clock: &Clock, + ctx: &TxContext, +): bool { + assert!( + role_map.security_vault_id == cap.security_vault_id(), + ECapabilitySecurityVaultIdMismatch, + ); + + let permissions = role_map.get_role_permissions(cap.role()); + assert!(vec_set::contains(permissions, permission), ECapabilityPermissionDenied); + + assert!(role_map.issued_capabilities.contains(&cap.id()), ECapabilityHasBeenRevoked); + + if (cap.valid_from().is_some() || cap.valid_until().is_some()) { + assert!(cap.is_currently_valid(clock), ECapabilityTimeConstraintsNotMet); + }; + + if (cap.issued_to().is_some()) { + let caller = ctx.sender(); + let issued_to_addr = cap.issued_to().borrow(); + assert!(*issued_to_addr == caller, ECapabilityIssuedToMismatch); + }; + + true +} + +/// Create a new capability +/// +/// Parameters +/// ---------- +/// - role_map: Reference to the `RoleMap` mapping. +/// - cap: Reference to the capability used to authorize the creation of the new capability. +/// - role: The role to be assigned to the new capability. +/// - issued_to: Optional address restriction for the new capability. +/// - valid_from: Optional start time (in seconds since Unix epoch) for the new capability. +/// - valid_until: Optional end time (in seconds since Unix epoch) for the new capability. +/// - clock: Reference to a Clock instance for time-based validation. +/// - ctx: Reference to the transaction context. +/// +/// Returns the newly created capability. +/// +/// Sends a CapabilityIssued event upon successful creation. +/// +/// Errors: +/// - Aborts with EPermissionDenied if the provided capability does not have the permission specified with `CapabilityAdminPermissions::add`. +/// - Aborts with ERoleDoesNotExist if the specified role does not exist in the role_map. +/// - Aborts with audit_trail::capability::EValidityPeriodInconsistent if the provided valid_from and valid_until are inconsistent. +public fun new_capability( + role_map: &mut RoleMap

, + cap: &Capability, + role: &String, + issued_to: Option

, + valid_from: Option, + valid_until: Option, + clock: &Clock, + ctx: &mut TxContext, +): Capability { + assert!( + role_map.is_capability_valid( + cap, + &role_map.capability_admin_permissions.add, + clock, + ctx, + ), + EPermissionDenied, + ); + + assert!(role_map.roles.contains(role), ERoleDoesNotExist); + let new_cap = capability::new_capability( + *role, + role_map.security_vault_id, + issued_to, + valid_from, + valid_until, + ctx, + ); + register_new_capability(role_map, &new_cap); + new_cap +} + +/// Create a new unrestricted capability with a specific role without any +/// address, valid_from, or valid_until restrictions. +/// +/// Returns the newly created capability. +/// +/// Sends a CapabilityIssued event upon successful creation. +/// +/// Errors: +/// - Aborts with EPermissionDenied if the provided capability does not have the permission specified with `CapabilityAdminPermissions::add`. +/// - Aborts with ERoleDoesNotExist if the specified role does not exist in the role_map. +public fun new_capability_without_restrictions( + role_map: &mut RoleMap

, + cap: &Capability, + role: &String, + clock: &Clock, + ctx: &mut TxContext, +): Capability { + assert!( + role_map.is_capability_valid( + cap, + &role_map.capability_admin_permissions.add, + clock, + ctx, + ), + EPermissionDenied, + ); + + assert!(role_map.roles.contains(role), ERoleDoesNotExist); + let new_cap = capability::new_capability_without_restrictions( + *role, + role_map.security_vault_id, + ctx, + ); + + register_new_capability(role_map, &new_cap); + new_cap +} + +/// Create a new capability with a specific role that expires at a given timestamp (seconds since Unix epoch). +/// +/// Returns the newly created capability. +/// +/// Sends a CapabilityIssued event upon successful creation. +/// +/// Errors: +/// - Aborts with EPermissionDenied if the provided capability does not have the permission specified with `CapabilityAdminPermissions::add`. +/// - Aborts with ERoleDoesNotExist if the specified role does not exist in the role_map. +public fun new_capability_valid_until( + role_map: &mut RoleMap

, + cap: &Capability, + role: &String, + valid_until: u64, + clock: &Clock, + ctx: &mut TxContext, +): Capability { + assert!( + role_map.is_capability_valid( + cap, + &role_map.capability_admin_permissions.add, + clock, + ctx, + ), + EPermissionDenied, + ); + + assert!(role_map.roles.contains(role), ERoleDoesNotExist); + let new_cap = capability::new_capability_valid_until( + *role, + role_map.security_vault_id, + valid_until, + ctx, + ); + + register_new_capability(role_map, &new_cap); + new_cap +} + +/// Create a new capability with a specific role restricted to an address. +/// Optionally set an expiration time (seconds since Unix epoch). +/// +/// Returns the newly created capability. +/// +/// Sends a CapabilityIssued event upon successful creation. +/// +/// Errors: +/// - Aborts with EPermissionDenied if the provided capability does not have the permission specified with `CapabilityAdminPermissions::add`. +/// - Aborts with ERoleDoesNotExist if the specified role does not exist in the role_map. +public fun new_capability_for_address( + role_map: &mut RoleMap

, + cap: &Capability, + role: &String, + issued_to: address, + valid_until: Option, + clock: &Clock, + ctx: &mut TxContext, +): Capability { + assert!( + role_map.is_capability_valid( + cap, + &role_map.capability_admin_permissions.add, + clock, + ctx, + ), + EPermissionDenied, + ); + + assert!(role_map.roles.contains(role), ERoleDoesNotExist); + let new_cap = capability::new_capability_for_address( + *role, + role_map.security_vault_id, + issued_to, + valid_until, + ctx, + ); + + register_new_capability(role_map, &new_cap); + new_cap +} + +/// Destroy an existing capability +/// Every owner of a capability is allowed to destroy it when no longer needed. +/// +/// Sends a CapabilityDestroyed event upon successful destruction. +/// +/// TODO: Clarify if we need to restrict access with the `CapabilitiesRevoke` permission here. +/// If yes, we also need a destroy function for Admin capabilities (without the need of another Admin capability). +/// Otherwise the last Admin capability holder will block the role_map forever by not being able to destroy it. +public fun destroy_capability( + role_map: &mut RoleMap

, + cap_to_destroy: Capability, +) { + assert!( + role_map.security_vault_id == cap_to_destroy.security_vault_id(), + ECapabilitySecurityVaultIdMismatch, + ); + + if (role_map.issued_capabilities.contains(&cap_to_destroy.id())) { + // Capability has not been revoked before destroying, so let's remove it now + role_map.issued_capabilities.remove(&cap_to_destroy.id()); + }; + + event::emit(CapabilityDestroyed { + security_vault_id: role_map.security_vault_id, + capability_id: cap_to_destroy.id(), + role: *cap_to_destroy.role(), + issued_to: *cap_to_destroy.issued_to(), + valid_from: *cap_to_destroy.valid_from(), + valid_until: *cap_to_destroy.valid_until(), + }); + + cap_to_destroy.destroy(); +} + +/// Revoke an existing capability +/// +/// Sends a CapabilityRevoked event upon successful revocation. +/// +/// Errors: +/// - Aborts with EPermissionDenied if the provided capability does not have the permission specified with `CapabilityAdminPermissions::revoke`. +/// - Aborts with ERoleDoesNotExist if the specified role does not exist in the `RoleMap.issued_capabilities()` list. +public fun revoke_capability( + role_map: &mut RoleMap

, + cap: &Capability, + cap_to_revoke: ID, + clock: &Clock, + ctx: &TxContext, +) { + assert!( + role_map.is_capability_valid( + cap, + &role_map.capability_admin_permissions.revoke, + clock, + ctx, + ), + EPermissionDenied, + ); + + assert!(role_map.issued_capabilities.contains(&cap_to_revoke), ERoleDoesNotExist); + role_map.issued_capabilities.remove(&cap_to_revoke); + + event::emit(CapabilityRevoked { + security_vault_id: role_map.security_vault_id, + capability_id: cap_to_revoke, + }); +} + +fun register_new_capability(role_map: &mut RoleMap

, new_cap: &Capability) { + role_map.issued_capabilities.insert(new_cap.id()); + + event::emit(CapabilityIssued { + security_vault_id: role_map.security_vault_id, + capability_id: new_cap.id(), + role: *new_cap.role(), + issued_to: *new_cap.issued_to(), + valid_from: *new_cap.valid_from(), + valid_until: *new_cap.valid_until(), + }); +} + +// =============== Getter Functions ================================================ + +/// Returns the size of the role_map, the number of managed roles +public fun size(role_map: &RoleMap

): u64 { + vec_map::size(&role_map.roles) +} + +/// Returns the security_vault_id associated with the role_map +public fun security_vault_id(role_map: &RoleMap

): ID { + role_map.security_vault_id +} + +//Returns the role admin permissions associated with the role_map +public fun role_admin_permissions(role_map: &RoleMap

): &RoleAdminPermissions

{ + &role_map.role_admin_permissions +} + +public fun issued_capabilities(role_map: &RoleMap

): &VecSet { + &role_map.issued_capabilities +} diff --git a/audit-trail-move/tests/capability_tests.move b/audit-trail-move/tests/capability_tests.move index e93732a..9631539 100644 --- a/audit-trail-move/tests/capability_tests.move +++ b/audit-trail-move/tests/capability_tests.move @@ -4,13 +4,104 @@ module audit_trail::capability_tests; use audit_trail::{ capability::Capability, locking, - main::{Self, AuditTrail}, + main::AuditTrail, permission, - test_utils::{Self, TestData, setup_test_audit_trail} + test_utils::{ + Self, + TestData, + setup_test_audit_trail, + fetch_capability_trail_and_clock, + cleanup_capability_trail_and_clock + } }; -use iota::test_scenario as ts; +use iota::test_scenario::{Self as ts, Scenario}; use std::string; +/// Helper function to setup an audit trail with a RecordAdmin role and a capability +/// with a time window restriction transferred to the record_user. +/// Returns the trail_id. +fun setup_trail_with_record_admin_capability_and_time_window_restriction( + scenario: &mut Scenario, + admin_user: address, + record_user: address, + valid_from_secs: u64, + valid_until_secs: u64, +): ID { + // Setup + let trail_id = setup_trail_with_record_admin_role(scenario, admin_user); + + // Issue capability with time window + ts::next_tx(scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(scenario); + + let cap = trail + .roles_mut() + .new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + std::option::none(), // no address restriction + std::option::some(valid_from_secs), + std::option::some(valid_until_secs), + &clock, + ts::ctx(scenario), + ); + + // Verify capability properties + assert!(cap.issued_to().is_none(), 0); + assert!(cap.valid_from() == std::option::some(valid_from_secs), 1); + assert!(cap.valid_until() == std::option::some(valid_until_secs), 2); + + transfer::public_transfer(cap, record_user); + cleanup_capability_trail_and_clock(scenario, admin_cap, trail, clock); + }; + + trail_id +} + +/// Helper function to setup an audit trail with a RecordAdmin role. +/// Returns the trail_id. +fun setup_trail_with_record_admin_role(scenario: &mut Scenario, admin_user: address): ID { + // Setup: Create audit trail with admin capability + let trail_id = { + let locking_config = locking::new(locking::window_count_based(0)); + + let (admin_cap, trail_id) = setup_test_audit_trail( + scenario, + locking_config, + std::option::none(), + ); + + transfer::public_transfer(admin_cap, admin_user); + trail_id + }; + + // Create a custom role for testing + ts::next_tx(scenario, admin_user); + { + let admin_cap = ts::take_from_sender(scenario); + let mut trail = ts::take_shared>(scenario); + let clock = iota::clock::create_for_testing(ts::ctx(scenario)); + + let record_admin_perms = permission::record_admin_permissions(); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + record_admin_perms, + &clock, + ts::ctx(scenario), + ); + + iota::clock::destroy_for_testing(clock); + ts::return_to_sender(scenario, admin_cap); + ts::return_shared(trail); + }; + + trail_id +} + #[test] fun test_new_capability() { let admin_user = @0xAD; @@ -33,73 +124,84 @@ fun test_new_capability() { // Create a role to issue capabilities for ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); - - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); - - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Issue first capability and verify it's tracked ts::next_tx(&mut scenario, admin_user); let cap1_id = { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - let initial_cap_count = trail.issued_capabilities().size(); + // Verify initial state - only admin capability should be tracked + let initial_cap_count = trail.roles().issued_capabilities().size(); assert!(initial_cap_count == 1, 0); // Only admin cap - let cap1 = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let cap1 = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); assert!(cap1.role() == string::utf8(b"RecordAdmin"), 1); - assert!(cap1.trail_id() == trail_id, 2); + assert!(cap1.security_vault_id() == trail_id, 2); let cap1_id = object::id(&cap1); - assert!(trail.issued_capabilities().size() == initial_cap_count + 1, 3); - assert!(trail.issued_capabilities().contains(&cap1_id), 4); + + // Verify capability ID is tracked in issued_capabilities + assert!(trail.roles().issued_capabilities().size() == initial_cap_count + 1, 3); + assert!(trail.roles().issued_capabilities().contains(&cap1_id), 4); transfer::public_transfer(cap1, user1); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); cap1_id }; // Issue second capability and verify both are tracked with unique IDs ts::next_tx(&mut scenario, admin_user); - { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let _cap2_id = { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - let previous_cap_count = trail.issued_capabilities().size(); + let previous_cap_count = trail.roles().issued_capabilities().size(); - let cap2 = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let cap2 = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); let cap2_id = object::id(&cap2); - assert!(trail.issued_capabilities().size() == previous_cap_count + 1, 5); - assert!(trail.issued_capabilities().contains(&cap1_id), 6); - assert!(trail.issued_capabilities().contains(&cap2_id), 7); + // Verify both capabilities are tracked + assert!(trail.roles().issued_capabilities().size() == previous_cap_count + 1, 5); + assert!(trail.roles().issued_capabilities().contains(&cap1_id), 6); + assert!(trail.roles().issued_capabilities().contains(&cap2_id), 7); + + // Verify capabilities have unique IDs assert!(cap1_id != cap2_id, 8); transfer::public_transfer(cap2, user2); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + + cap2_id }; ts::end(scenario); @@ -113,178 +215,418 @@ fun test_revoke_capability() { let mut scenario = ts::begin(admin_user); - { - let locking_config = locking::new(locking::window_count_based(0)); - let (admin_cap, _) = setup_test_audit_trail( - &mut scenario, - locking_config, - option::none(), - ); - transfer::public_transfer(admin_cap, admin_user); + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Issue two capabilities + ts::next_tx(&mut scenario, admin_user); + let (cap1_id, cap2_id) = { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap1 = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + let cap1_id = object::id(&cap1); + transfer::public_transfer(cap1, user1); + + let cap2 = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + let cap2_id = object::id(&cap2); + transfer::public_transfer(cap2, user2); + + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + + (cap1_id, cap2_id) }; - // Create role + // Test: Revoke first capability and verify it's removed from tracking ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + let cap1 = ts::take_from_address(&scenario, user1); + + // Verify both capabilities are tracked before revocation + let cap_count_before = trail.roles().issued_capabilities().size(); + assert!(trail.roles().issued_capabilities().contains(&cap1_id), 0); + assert!(trail.roles().issued_capabilities().contains(&cap2_id), 1); + + // Revoke the capability + trail + .roles_mut() + .revoke_capability( + &admin_cap, + cap1.id(), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability was removed from tracking + assert!(trail.roles().issued_capabilities().size() == cap_count_before - 1, 2); + assert!(!trail.roles().issued_capabilities().contains(&cap1_id), 3); + // Verify other capability is still tracked + assert!(trail.roles().issued_capabilities().contains(&cap2_id), 4); + + ts::return_to_address(user1, cap1); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + // Verify revoked capability object still exists (just invalidated) + ts::next_tx(&mut scenario, user1); + { + assert!(ts::has_most_recent_for_sender(&scenario), 5); + }; - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + // Test: Revoke second capability + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + let cap2 = ts::take_from_address(&scenario, user2); + + let cap_count_before = trail.roles().issued_capabilities().size(); + + trail + .roles_mut() + .revoke_capability( + &admin_cap, + cap2.id(), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability was removed from tracking + assert!(trail.roles().issued_capabilities().size() == cap_count_before - 1, 6); + assert!(!trail.roles().issued_capabilities().contains(&cap2_id), 7); + + ts::return_to_address(user2, cap2); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; + ts::end(scenario); +} + +#[test] +fun test_destroy_capability() { + let admin_user = @0xAD; + let user1 = @0xB0B; + let user2 = @0xCAB; + + let mut scenario = ts::begin(admin_user); + + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + // Issue two capabilities ts::next_tx(&mut scenario, admin_user); let (cap1_id, cap2_id) = { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); - - let cap1 = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap1 = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); let cap1_id = object::id(&cap1); transfer::public_transfer(cap1, user1); - let cap2 = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let cap2 = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); let cap2_id = object::id(&cap2); transfer::public_transfer(cap2, user2); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); (cap1_id, cap2_id) }; - // Revoke first capability and verify it's removed from tracking + // User1 destroys their capability ts::next_tx(&mut scenario, user1); { - let admin_cap = ts::take_from_address(&scenario, admin_user); let mut trail = ts::take_shared>(&scenario); let cap1 = ts::take_from_sender(&scenario); - let cap_count_before = trail.issued_capabilities().size(); - assert!(trail.issued_capabilities().contains(&cap1_id), 0); - assert!(trail.issued_capabilities().contains(&cap2_id), 1); + // Verify both capabilities are tracked before destruction + let cap_count_before = trail.roles().issued_capabilities().size(); + assert!(trail.roles().issued_capabilities().contains(&cap1_id), 0); + assert!(trail.roles().issued_capabilities().contains(&cap2_id), 1); + + // Destroy the capability + trail.roles_mut().destroy_capability(cap1); - trail.revoke_capability(&admin_cap, cap1.id()); + // Verify capability was removed from tracking + assert!(trail.roles().issued_capabilities().size() == cap_count_before - 1, 2); + assert!(!trail.roles().issued_capabilities().contains(&cap1_id), 3); - assert!(trail.issued_capabilities().size() == cap_count_before - 1, 2); - assert!(!trail.issued_capabilities().contains(&cap1_id), 3); - assert!(trail.issued_capabilities().contains(&cap2_id), 4); + // Verify other capability is still tracked + assert!(trail.roles().issued_capabilities().contains(&cap2_id), 4); - ts::return_to_address(admin_user, admin_cap); - ts::return_to_sender(&scenario, cap1); ts::return_shared(trail); }; - // Verify revoked capability object still exists (just invalidated) + // Verify destroyed capability no longer exists ts::next_tx(&mut scenario, user1); { - assert!(ts::has_most_recent_for_sender(&scenario), 5); + assert!(!ts::has_most_recent_for_sender(&scenario), 5); + }; + + // Test: User2 destroys their own capability + ts::next_tx(&mut scenario, user2); + { + let mut trail = ts::take_shared>(&scenario); + let cap2 = ts::take_from_sender(&scenario); + + let cap_count_before = trail.roles().issued_capabilities().size(); + + trail.roles_mut().destroy_capability(cap2); + + // Verify capability was removed from tracking + assert!(trail.roles().issued_capabilities().size() == cap_count_before - 1, 6); + assert!(!trail.roles().issued_capabilities().contains(&cap2_id), 7); + + ts::return_shared(trail); + }; + + // Verify only admin capability remains + ts::next_tx(&mut scenario, admin_user); + { + let trail = ts::take_shared>(&scenario); + + // Only the initial admin capability should remain + assert!(trail.roles().issued_capabilities().size() == 1, 8); + + ts::return_shared(trail); }; ts::end(scenario); } +/// Test capability lifecycle: creation, usage, and destruction in a complete workflow. +/// +/// This test validates: +/// - Multiple capabilities can be created for different roles +/// - Capabilities can be used to perform authorized actions +/// - Capabilities can be revoked or destroyed +/// - issued_capabilities tracking remains accurate throughout the lifecycle #[test] -fun test_destroy_capability() { +fun test_capability_lifecycle() { let admin_user = @0xAD; - let user1 = @0xB0B; - let user2 = @0xCAB; + let record_admin_user = @0xB0B; + let role_admin_user = @0xCAB; let mut scenario = ts::begin(admin_user); + // Setup: Create audit trail + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Create an additional RoleAdmin role + ts::next_tx(&mut scenario, admin_user); { - let locking_config = locking::new(locking::window_count_based(0)); - let (admin_cap, _) = setup_test_audit_trail( - &mut scenario, - locking_config, - option::none(), - ); - transfer::public_transfer(admin_cap, admin_user); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + // Initially only admin cap should be tracked + assert!(trail.roles().issued_capabilities().size() == 1, 0); + + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RoleAdmin"), + permission::role_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; - // Create role + // Issue capabilities ts::next_tx(&mut scenario, admin_user); + let (record_cap_id, role_cap_id) = { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + let record_cap_id = object::id(&record_cap); + transfer::public_transfer(record_cap, record_admin_user); + + let role_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RoleAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + let role_cap_id = object::id(&role_cap); + transfer::public_transfer(role_cap, role_admin_user); + + // Verify all capabilities are tracked + assert!(trail.roles().issued_capabilities().size() == 3, 1); // admin + record + role + assert!(trail.roles().issued_capabilities().contains(&record_cap_id), 2); + assert!(trail.roles().issued_capabilities().contains(&role_cap_id), 3); + + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + + (record_cap_id, role_cap_id) + }; + + // Use RecordAdmin capability to add a record + ts::next_tx(&mut scenario, record_admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), + clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); + + let test_data = test_utils::new_test_data(1, b"Test record"); + trail.add_record( + &record_cap, + test_data, + std::option::none(), + &clock, ts::ctx(&mut scenario), ); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; - // Issue two capabilities - ts::next_tx(&mut scenario, admin_user); - let (cap1_id, cap2_id) = { - let admin_cap = ts::take_from_sender(&scenario); + // RecordAdmin destroys their capability + ts::next_tx(&mut scenario, record_admin_user); + { let mut trail = ts::take_shared>(&scenario); + let record_cap = ts::take_from_sender(&scenario); - let cap1 = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); - let cap1_id = object::id(&cap1); - transfer::public_transfer(cap1, user1); + trail.roles_mut().destroy_capability(record_cap); - let cap2 = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); - let cap2_id = object::id(&cap2); - transfer::public_transfer(cap2, user2); + // Verify capability was removed + assert!(trail.roles().issued_capabilities().size() == 2, 4); // admin + role + assert!(!trail.roles().issued_capabilities().contains(&record_cap_id), 5); - ts::return_to_sender(&scenario, admin_cap); ts::return_shared(trail); + }; - (cap1_id, cap2_id) + // Admin revokes RoleAdmin capability + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + let role_cap = ts::take_from_address(&scenario, role_admin_user); + + trail + .roles_mut() + .revoke_capability( + &admin_cap, + role_cap.id(), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability was removed + assert!(trail.roles().issued_capabilities().size() == 1, 6); // only admin remains + assert!(!trail.roles().issued_capabilities().contains(&role_cap_id), 7); + + ts::return_to_address(role_admin_user, role_cap); + + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; - // User1 destroys their capability - ts::next_tx(&mut scenario, user1); + ts::end(scenario); +} + +#[test, expected_failure(abort_code = audit_trail::role_map::ECapabilityIssuedToMismatch)] +fun test_capability_issued_to_only() { + let admin_user = @0xAD; + let authorized_user = @0xB0B; + let unauthorized_user = @0xCAB; + + let mut scenario = ts::begin(admin_user); + + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Issue capability restricted to authorized_user only + ts::next_tx(&mut scenario, admin_user); { - let mut trail = ts::take_shared>(&scenario); - let cap1 = ts::take_from_sender(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability_for_address( + &admin_cap, + &string::utf8(b"RecordAdmin"), + authorized_user, + std::option::none(), // no time restriction + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability properties + assert!(cap.issued_to() == std::option::some(authorized_user), 0); + assert!(cap.valid_from().is_none(), 1); + assert!(cap.valid_until().is_none(), 2); + + transfer::public_transfer(cap, authorized_user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; - let cap_count_before = trail.issued_capabilities().size(); - assert!(trail.issued_capabilities().contains(&cap1_id), 0); - assert!(trail.issued_capabilities().contains(&cap2_id), 1); + // Authorized user can use the capability + ts::next_tx(&mut scenario, authorized_user); + { + let (record_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.destroy_capability(cap1); + let test_data = test_utils::new_test_data(1, b"Authorized record"); + trail.add_record( + &record_cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); - assert!(trail.issued_capabilities().size() == cap_count_before - 1, 2); - assert!(!trail.issued_capabilities().contains(&cap1_id), 3); - assert!(trail.issued_capabilities().contains(&cap2_id), 4); + // Transfer the capability to he unauthorized_user to prepare the next test + transfer::public_transfer(record_cap, unauthorized_user); + // Cleanup + iota::clock::destroy_for_testing(clock); ts::return_shared(trail); }; - // Verify destroyed capability no longer exists - ts::next_tx(&mut scenario, user1); + // Unauthorized user cannot use the capability + ts::next_tx(&mut scenario, unauthorized_user); { - assert!(!ts::has_most_recent_for_sender(&scenario), 5); + let (record_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + // This should fail as unauthorized_user has the wrong address + let test_data = test_utils::new_test_data(1, b"Unauthorized record"); + trail.add_record( + &record_cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); @@ -293,7 +635,7 @@ fun test_destroy_capability() { // ===== Error Case Tests ===== #[test] -#[expected_failure(abort_code = main::ECapabilityHasBeenRevoked)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityHasBeenRevoked)] fun test_revoked_capability_cannot_be_used() { let admin_user = @0xAD; let user = @0xB0B; @@ -313,48 +655,50 @@ fun test_revoked_capability_cannot_be_used() { // Create role and issue capability to user ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); - - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); - - let user_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); + + let user_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(user_cap, user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Revoke the capability ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let user_cap = ts::take_from_address(&scenario, user); - trail.revoke_capability(&admin_cap, user_cap.id()); + trail + .roles_mut() + .revoke_capability(&admin_cap, user_cap.id(), &clock, ts::ctx(&mut scenario)); ts::return_to_address(user, user_cap); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Try to use revoked capability - should fail ts::next_tx(&mut scenario, user); { - let mut trail = ts::take_shared>(&scenario); - let user_cap = ts::take_from_sender(&scenario); + let (user_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); - let mut clock = iota::clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); trail.add_record( @@ -365,16 +709,14 @@ fun test_revoked_capability_cannot_be_used() { ts::ctx(&mut scenario), ); - iota::clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, user_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, user_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::ERoleDoesNotExist)] +#[expected_failure(abort_code = audit_trail::role_map::ERoleDoesNotExist)] fun test_new_capability_for_nonexistent_role() { let admin_user = @0xAD; @@ -392,25 +734,26 @@ fun test_new_capability_for_nonexistent_role() { ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - let bad_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NonExistentRole"), - ts::ctx(&mut scenario), - ); + let bad_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NonExistentRole"), + &clock, + ts::ctx(&mut scenario), + ); bad_cap.destroy_for_testing(); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_revoke_capability_permission_denied() { let admin_user = @0xAD; let user1 = @0xB0B; @@ -431,35 +774,50 @@ fun test_revoke_capability_permission_denied() { // Create two roles: one without revoke permission, one with record permissions ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"NoRevokePerm"), perms, ts::ctx(&mut scenario)); - - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); - - let user1_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoRevokePerm"), - ts::ctx(&mut scenario), - ); - - let user2_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoRevokePerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); + + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); + + let user1_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoRevokePerm"), + &clock, + ts::ctx(&mut scenario), + ); + + let user2_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(user1_cap, user1); transfer::public_transfer(user2_cap, user2); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // User1 (without revoke permission) tries to revoke User2's capability @@ -468,19 +826,23 @@ fun test_revoke_capability_permission_denied() { let user1_cap = ts::take_from_sender(&scenario); let mut trail = ts::take_shared>(&scenario); let user2_cap = ts::take_from_address(&scenario, user2); + let clock = iota::clock::create_for_testing(ts::ctx(&mut scenario)); - trail.revoke_capability(&user1_cap, user2_cap.id()); + trail + .roles_mut() + .revoke_capability(&user1_cap, user2_cap.id(), &clock, ts::ctx(&mut scenario)); ts::return_to_address(user2, user2_cap); ts::return_to_sender(&scenario, user1_cap); ts::return_shared(trail); + iota::clock::destroy_for_testing(clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_new_capability_permission_denied() { let admin_user = @0xAD; let user = @0xB0B; @@ -500,45 +862,681 @@ fun test_new_capability_permission_denied() { // Create role without add_capabilities permission ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"NoCapPerm"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoCapPerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); + + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); + + let user_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoCapPerm"), + &clock, + ts::ctx(&mut scenario), + ); + + transfer::public_transfer(user_cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + // User tries to issue a new capability without permission + ts::next_tx(&mut scenario, user); + { + let (user_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let new_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &user_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + + new_cap.destroy_for_testing(); + cleanup_capability_trail_and_clock(&scenario, user_cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test capability with only valid_from restriction (time-restricted from a point). +/// +/// This test validates: +/// - Capability can be used after valid_from timestamp +/// - Capability is not restricted by address or end time +/// - Capability cannot be used before valid_from timestamp +#[test, expected_failure(abort_code = audit_trail::role_map::ECapabilityTimeConstraintsNotMet)] +fun test_capability_valid_from_only() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), + let valid_from_time = test_utils::initial_time_for_testing() + 5000; + + // Setup + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Issue capability with valid_from restriction only + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + std::option::none(), // no address restriction + std::option::some(valid_from_time), + std::option::none(), // no valid_until + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability properties + assert!(cap.issued_to().is_none(), 0); + assert!(cap.valid_from() == std::option::some(valid_from_time), 1); + assert!(cap.valid_until().is_none(), 2); + + transfer::public_transfer(cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + // Use the capability after valid_from + ts::next_tx(&mut scenario, user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(test_utils::initial_time_for_testing() + 6000); + + let test_data = test_utils::new_test_data(1, b"Test record after valid_from"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, ts::ctx(&mut scenario), ); - let user_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoCapPerm"), + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + // Try to use the capability before valid_from + ts::next_tx(&mut scenario, user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); + + // This should fail as the capability is not valid yet + let test_data = test_utils::new_test_data(1, b"Test record before valid_from"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, ts::ctx(&mut scenario), ); - transfer::public_transfer(user_cap, user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); }; - // User tries to issue a new capability without permission + ts::end(scenario); +} + +/// Test capability with only valid_until restriction (time-restricted until a point). +/// +/// This test validates: +/// - Capability can be used before valid_until timestamp +/// - Capability is not restricted by address or start time +/// - Capability cannot be used after valid_until timestamp +#[test, expected_failure(abort_code = audit_trail::role_map::ECapabilityTimeConstraintsNotMet)] +fun test_capability_valid_until_only() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let valid_until_time_secs = test_utils::initial_time_for_testing() / 1000 + 10; + + // Setup + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Issue capability with valid_until restriction + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability_valid_until( + &admin_cap, + &string::utf8(b"RecordAdmin"), + valid_until_time_secs, + &clock, + ts::ctx(&mut scenario), + ); + + // Verify capability properties + assert!(cap.issued_to().is_none(), 0); + assert!(cap.valid_from().is_none(), 1); + assert!(cap.valid_until() == std::option::some(valid_until_time_secs), 2); + + transfer::public_transfer(cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + // Use the capability before valid_until ts::next_tx(&mut scenario, user); { - let user_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(valid_until_time_secs* 1000 - 1000); - let new_cap = trail.new_capability( - &user_cap, - &string::utf8(b"RecordAdmin"), + let test_data = test_utils::new_test_data(1, b"Test record before valid_until"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, ts::ctx(&mut scenario), ); - new_cap.destroy_for_testing(); - ts::return_to_sender(&scenario, user_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + // Try to use the capability after valid_until + ts::next_tx(&mut scenario, user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(valid_until_time_secs* 1000 + 1000); + + // This should fail as the capability has expired + let test_data = test_utils::new_test_data(1, b"Test record after valid_until"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test capability with valid_from and valid_until restrictions (time window). +/// +/// This test validates: +/// - Capability can be used between valid_from and valid_until +/// - Capability is not restricted by address +#[test] +fun test_capability_time_window() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let valid_from_time = test_utils::initial_time_for_testing() + 5000; + let valid_until_time = test_utils::initial_time_for_testing() + 10000; + + // Setup + let _trail_id = setup_trail_with_record_admin_capability_and_time_window_restriction( + &mut scenario, + admin_user, + user, + valid_from_time / 1000, + valid_until_time / 1000, + ); + + // Use the capability within the valid time window + ts::next_tx(&mut scenario, user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(valid_from_time + 2500); + + let test_data = test_utils::new_test_data(1, b"Test record within time window"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test capability with valid_from and valid_until restrictions (time window). +/// +/// This test validates: +/// - Capability cannot be used before valid_from +#[test, expected_failure(abort_code = audit_trail::role_map::ECapabilityTimeConstraintsNotMet)] +fun test_capability_time_window_before_valid_from() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let valid_from_time_secs = test_utils::initial_time_for_testing() / 1000 + 5; + let valid_until_time_secs = test_utils::initial_time_for_testing() / 1000 + 10; + + // Setup + let _trail_id = setup_trail_with_record_admin_capability_and_time_window_restriction( + &mut scenario, + admin_user, + user, + valid_from_time_secs, + valid_until_time_secs, + ); + + // Use the capability before valid_from + ts::next_tx(&mut scenario, user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(valid_from_time_secs* 1000 - 1000); + + let test_data = test_utils::new_test_data(1, b"Test record before valid_from"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test capability with valid_from and valid_until restrictions (time window). +/// +/// This test validates: +/// - Capability cannot be used after valid_until +#[test, expected_failure(abort_code = audit_trail::role_map::ECapabilityTimeConstraintsNotMet)] +fun test_capability_time_window_after_valid_until() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let valid_from_time_secs = test_utils::initial_time_for_testing() / 1000 + 5; + let valid_until_time_secs = test_utils::initial_time_for_testing() / 1000 + 10; + + // Setup + let _trail_id = setup_trail_with_record_admin_capability_and_time_window_restriction( + &mut scenario, + admin_user, + user, + valid_from_time_secs, + valid_until_time_secs, + ); + + // Use the capability after valid_until + ts::next_tx(&mut scenario, user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(valid_until_time_secs* 1000 + 1000); + + let test_data = test_utils::new_test_data(1, b"Test record after valid_until"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test Capability::is_valid_for_timestamp function. +/// +/// This test validates: +/// - Returns true when timestamp is within valid range +/// - Returns false when timestamp is before valid_from +/// - Returns false when timestamp is after valid_until +/// - Returns true when no time restrictions exist +#[test] +fun test_is_valid_for_timestamp() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let base_time = test_utils::initial_time_for_testing(); + let valid_from_time = base_time + 5000; + let valid_until_time = base_time + 10000; + + // Setup + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Test with time-restricted capability + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + std::option::none(), + std::option::some(valid_from_time), + std::option::some(valid_until_time), + &clock, + ts::ctx(&mut scenario), + ); + + // Before valid_from + assert!(!cap.is_valid_for_timestamp(valid_from_time - 1), 0); + + // At valid_from (inclusive) + assert!(cap.is_valid_for_timestamp(valid_from_time), 1); + + // During validity period + assert!(cap.is_valid_for_timestamp(valid_from_time + 2500), 2); + + // Before valid_until (exclusive) + assert!(cap.is_valid_for_timestamp(valid_until_time - 1), 3); + + // At valid_until (exclusive) + assert!(!cap.is_valid_for_timestamp(valid_until_time), 4); + + // After valid_until + assert!(!cap.is_valid_for_timestamp(valid_until_time + 1), 5); + + transfer::public_transfer(cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + // Test with unrestricted capability + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let unrestricted_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + + // Should be valid at any timestamp + assert!(unrestricted_cap.is_valid_for_timestamp(0), 6); + assert!(unrestricted_cap.is_valid_for_timestamp(base_time), 7); + assert!(unrestricted_cap.is_valid_for_timestamp(valid_until_time + 99999), 8); + + transfer::public_transfer(unrestricted_cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test Capability::is_currently_valid function. +/// +/// This test validates: +/// - Returns true when current time is within valid range +/// - Returns false when current time is outside valid range +/// - Works correctly with Clock object +#[test] +fun test_is_currently_valid() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let base_time = test_utils::initial_time_for_testing(); + let valid_from_time = base_time + 5000; + let valid_until_time = base_time + 10000; + + // Setup + let _trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Issue time-restricted capability + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability( + &admin_cap, + &string::utf8(b"RecordAdmin"), + std::option::none(), + std::option::some(valid_from_time / 1000), + std::option::some(valid_until_time / 1000), + &clock, + ts::ctx(&mut scenario), + ); + + transfer::public_transfer(cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + // Test before valid_from + ts::next_tx(&mut scenario, user); + { + let cap = ts::take_from_sender(&scenario); + let mut clock = iota::clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(valid_from_time - 1000); + + assert!(!cap.is_currently_valid(&clock), 0); + + iota::clock::destroy_for_testing(clock); + ts::return_to_sender(&scenario, cap); + }; + + // Test during valid period + ts::next_tx(&mut scenario, user); + { + let cap = ts::take_from_sender(&scenario); + let mut clock = iota::clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(valid_from_time + 2500); + + assert!(cap.is_currently_valid(&clock), 1); + + iota::clock::destroy_for_testing(clock); + ts::return_to_sender(&scenario, cap); + }; + + // Test after valid_until + ts::next_tx(&mut scenario, user); + { + let cap = ts::take_from_sender(&scenario); + let mut clock = iota::clock::create_for_testing(ts::ctx(&mut scenario)); + clock.set_for_testing(valid_until_time + 1000); + + assert!(!cap.is_currently_valid(&clock), 2); + + iota::clock::destroy_for_testing(clock); + ts::return_to_sender(&scenario, cap); + }; + + ts::end(scenario); +} + +/// Test Capability::new_capability_without_restrictions function. +/// +/// This test validates: +/// - Creates capability with no restrictions +/// - issued_to, valid_from, and valid_until are all None +/// - Capability can be used by anyone at any time +#[test] +fun test_new_capability_without_restrictions() { + let admin_user = @0xAD; + let any_user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + // Setup + let trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Create unrestricted capability + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); + + // Verify no restrictions + assert!(cap.issued_to().is_none(), 0); + assert!(cap.valid_from().is_none(), 1); + assert!(cap.valid_until().is_none(), 2); + assert!(cap.role() == string::utf8(b"RecordAdmin"), 3); + assert!(cap.security_vault_id() == trail_id, 4); + + transfer::public_transfer(cap, any_user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + // Verify any user can use it at any time + ts::next_tx(&mut scenario, any_user); + { + let (cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); + clock.set_for_testing(999999999); + + let test_data = test_utils::new_test_data(1, b"Test"); + trail.add_record( + &cap, + test_data, + std::option::none(), + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test Capability::new_capability_valid_until function. +/// +/// This test validates: +/// - Creates capability with only valid_until restriction +/// - issued_to and valid_from are None +/// - Capability expires at the specified timestamp +#[test] +fun test_new_capability_valid_until() { + let admin_user = @0xAD; + let user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + let valid_until_time = test_utils::initial_time_for_testing() + 10000; + + // Setup + let trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Create capability with valid_until + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability_valid_until( + &admin_cap, + &string::utf8(b"RecordAdmin"), + valid_until_time, + &clock, + ts::ctx(&mut scenario), + ); + + // Verify restrictions + assert!(cap.issued_to().is_none(), 0); + assert!(cap.valid_from().is_none(), 1); + assert!(cap.valid_until() == std::option::some(valid_until_time), 2); + assert!(cap.role() == string::utf8(b"RecordAdmin"), 3); + assert!(cap.security_vault_id() == trail_id, 4); + + transfer::public_transfer(cap, user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); + }; + + ts::end(scenario); +} + +/// Test Capability::new_capability_for_address with None for valid_until. +/// +/// This test validates: +/// - Creates capability restricted to specific address +/// - valid_until is None (no expiration) +/// - valid_from is None +#[test] +fun test_new_capability_for_address_no_expiration() { + let admin_user = @0xAD; + let authorized_user = @0xB0B; + + let mut scenario = ts::begin(admin_user); + + // Setup + let trail_id = setup_trail_with_record_admin_role(&mut scenario, admin_user); + + // Create capability for address without expiration + ts::next_tx(&mut scenario, admin_user); + { + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); + + let cap = trail + .roles_mut() + .new_capability_for_address( + &admin_cap, + &string::utf8(b"RecordAdmin"), + authorized_user, + std::option::none(), // no expiration + &clock, + ts::ctx(&mut scenario), + ); + + // Verify restrictions + assert!(cap.issued_to() == std::option::some(authorized_user), 0); + assert!(cap.valid_from().is_none(), 1); + assert!(cap.valid_until().is_none(), 2); + assert!(cap.role() == string::utf8(b"RecordAdmin"), 3); + assert!(cap.security_vault_id() == trail_id, 4); + + transfer::public_transfer(cap, authorized_user); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); diff --git a/audit-trail-move/tests/create_audit_trail_tests.move b/audit-trail-move/tests/create_audit_trail_tests.move index b860cd0..7e36153 100644 --- a/audit-trail-move/tests/create_audit_trail_tests.move +++ b/audit-trail-move/tests/create_audit_trail_tests.move @@ -2,10 +2,16 @@ module audit_trail::create_audit_trail_tests; use audit_trail::{ - capability::Capability, locking, main::{Self, AuditTrail, initial_admin_role_name}, - test_utils::{setup_test_audit_trail, new_test_data, initial_time_for_testing, TestData} + test_utils::{ + setup_test_audit_trail, + new_test_data, + initial_time_for_testing, + TestData, + fetch_capability_trail_and_clock, + cleanup_capability_trail_and_clock + } }; use iota::{clock, test_scenario as ts}; use std::string; @@ -26,7 +32,7 @@ fun test_create_without_initial_record() { // Verify capability was created assert!(admin_cap.role() == initial_admin_role_name(), 0); - assert!(admin_cap.trail_id() == trail_id, 1); + assert!(admin_cap.security_vault_id() == trail_id, 1); // Clean up admin_cap.destroy_for_testing(); @@ -64,7 +70,7 @@ fun test_create_with_initial_record() { // Verify capability assert!(admin_cap.role() == initial_admin_role_name(), 0); - assert!(admin_cap.trail_id() == trail_id, 1); + assert!(admin_cap.security_vault_id() == trail_id, 1); // Clean up admin_cap.destroy_for_testing(); @@ -223,7 +229,7 @@ fun test_create_metadata_admin_role() { // Verify admin capability was created assert!(admin_cap.role() == initial_admin_role_name(), 0); - assert!(admin_cap.trail_id() == trail_id, 1); + assert!(admin_cap.security_vault_id() == trail_id, 1); // Transfer the admin capability to the user transfer::public_transfer(admin_cap, user); @@ -232,22 +238,23 @@ fun test_create_metadata_admin_role() { // User receives the capability and creates the MetadataAdmin role ts::next_tx(&mut scenario, user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); - + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create the MetadataAdmin role using the admin capability let metadata_admin_role_name = string::utf8(b"MetadataAdmin"); let metadata_admin_perms = audit_trail::permission::metadata_admin_permissions(); - trail.create_role( - &admin_cap, - metadata_admin_role_name, - metadata_admin_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + metadata_admin_role_name, + metadata_admin_perms, + &clock, + ts::ctx(&mut scenario), + ); // Verify the role was created by fetching its permissions - let role_perms = trail.get_role_permissions(&string::utf8(b"MetadataAdmin")); + let role_perms = trail.roles().get_role_permissions(&string::utf8(b"MetadataAdmin")); // Verify the role has the correct permissions assert!( @@ -267,8 +274,7 @@ fun test_create_metadata_admin_role() { assert!(iota::vec_set::size(role_perms) == 2, 4); // Clean up - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); diff --git a/audit-trail-move/tests/locking_tests.move b/audit-trail-move/tests/locking_tests.move index 1f9ca1b..12fcc33 100644 --- a/audit-trail-move/tests/locking_tests.move +++ b/audit-trail-move/tests/locking_tests.move @@ -4,9 +4,17 @@ module audit_trail::locking_tests; use audit_trail::{ capability::Capability, locking, - main::{Self, AuditTrail}, + main::AuditTrail, permission, - test_utils::{TestData, setup_test_audit_trail, new_test_data, initial_time_for_testing} + test_utils::{ + TestData, + setup_test_audit_trail, + new_test_data, + initial_time_for_testing, + fetch_capability_trail_and_clock, + cleanup_capability_trail_and_clock, + cleanup_trail_and_clock + } }; use iota::{clock, test_scenario as ts}; use std::string; @@ -110,25 +118,30 @@ fun test_count_based_locking() { // Create RecordAdmin role and capability ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Add 5 records and verify locking @@ -254,30 +267,37 @@ fun test_update_locking_config() { // Create LockingAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::update_locking_config()]); - trail.create_role(&admin_cap, string::utf8(b"LockingAdmin"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"LockingAdmin"), + perms, + &clock, + ts::ctx(&mut scenario), + ); - let locking_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"LockingAdmin"), - ts::ctx(&mut scenario), - ); + let locking_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"LockingAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(locking_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Update from no-locking to time-based ts::next_tx(&mut scenario, admin); { - let locking_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (locking_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // Initially unlocked @@ -287,22 +307,22 @@ fun test_update_locking_config() { trail.update_locking_config( &locking_cap, locking::time_based(3600), + &clock, ts::ctx(&mut scenario), ); // Now locked assert!(trail.is_record_locked(0, &clock), 1); - clock::destroy_for_testing(clock); - locking_cap.destroy_for_testing(); - ts::return_shared(trail); + // locking_cap.destroy_for_testing(); + cleanup_capability_trail_and_clock(&scenario, locking_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_update_locking_config_permission_denied() { let admin = @0xAD; let mut scenario = ts::begin(admin); @@ -320,42 +340,45 @@ fun test_update_locking_config_permission_denied() { // Create role WITHOUT UpdateLockingConfig permission ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role( - &admin_cap, - string::utf8(b"NoLockingPerm"), - perms, - ts::ctx(&mut scenario), - ); - - let no_locking_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoLockingPerm"), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoLockingPerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); + let no_locking_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoLockingPerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(no_locking_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to update locking config - should fail ts::next_tx(&mut scenario, admin); { - let no_locking_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (no_locking_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); trail.update_locking_config( &no_locking_cap, locking::time_based(3600), + &clock, ts::ctx(&mut scenario), ); - no_locking_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, no_locking_cap, trail, clock); }; ts::end(scenario); @@ -380,37 +403,41 @@ fun test_update_locking_config_for_delete_record() { // Create role with UpdateLockingConfigForDeleteRecord permission ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[ permission::update_locking_config_for_delete_record(), ]); - trail.create_role( - &admin_cap, - string::utf8(b"DeleteLockAdmin"), - perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"DeleteLockAdmin"), + perms, + &clock, + ts::ctx(&mut scenario), + ); - let delete_lock_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"DeleteLockAdmin"), - ts::ctx(&mut scenario), - ); + let delete_lock_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"DeleteLockAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(delete_lock_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Update delete_record_lock ts::next_tx(&mut scenario, admin); { - let delete_lock_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (delete_lock_cap, mut trail, mut clock) = fetch_capability_trail_and_clock( + &mut scenario, + ); clock.set_for_testing(initial_time_for_testing() + 1000); // Initially unlocked @@ -420,22 +447,21 @@ fun test_update_locking_config_for_delete_record() { trail.update_locking_config_for_delete_record( &delete_lock_cap, locking::window_count_based(5), + &clock, ts::ctx(&mut scenario), ); // Now locked (single record, last 5 are locked) assert!(trail.is_record_locked(0, &clock), 1); - clock::destroy_for_testing(clock); - delete_lock_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, delete_lock_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_update_locking_config_for_delete_record_permission_denied() { let admin = @0xAD; let mut scenario = ts::begin(admin); @@ -453,37 +479,46 @@ fun test_update_locking_config_for_delete_record_permission_denied() { // Create role with update_locking_config but NOT update_locking_config_for_delete_record ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::update_locking_config()]); - trail.create_role(&admin_cap, string::utf8(b"WrongPerm"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"WrongPerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); - let wrong_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"WrongPerm"), - ts::ctx(&mut scenario), - ); + let wrong_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"WrongPerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(wrong_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to update delete_record_lock - should fail ts::next_tx(&mut scenario, admin); { - let wrong_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (wrong_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); trail.update_locking_config_for_delete_record( &wrong_cap, locking::window_count_based(5), + &clock, ts::ctx(&mut scenario), ); - wrong_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, wrong_cap, trail, clock); }; ts::end(scenario); @@ -508,25 +543,30 @@ fun test_delete_record_after_time_lock_expires() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Test boundary: exactly at lock expiry (should still be locked) @@ -628,24 +668,28 @@ fun test_combined_time_and_count_locking_both_lock() { // Create RecordAdmin role and add records ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Add 5 records - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(initial_time_for_testing() + 1000); let mut i = 0u64; @@ -660,10 +704,8 @@ fun test_combined_time_and_count_locking_both_lock() { i = i + 1; }; - clock::destroy_for_testing(clock); transfer::public_transfer(record_cap, admin); - admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Test: Records locked by BOTH time and count @@ -711,24 +753,28 @@ fun test_combined_locking_time_expired_but_count_locked() { // Create RecordAdmin role and add records ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Add 5 records - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(initial_time_for_testing() + 1000); let mut i = 0u64; @@ -743,10 +789,8 @@ fun test_combined_locking_time_expired_but_count_locked() { i = i + 1; }; - clock::destroy_for_testing(clock); transfer::public_transfer(record_cap, admin); - admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Test: Time lock expired, but count lock still active for last 2 records @@ -795,24 +839,28 @@ fun test_combined_locking_count_satisfied_but_time_locked() { // Create RecordAdmin role and add records ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Add 5 records - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(initial_time_for_testing() + 1000); let mut i = 0u64; @@ -827,10 +875,8 @@ fun test_combined_locking_count_satisfied_but_time_locked() { i = i + 1; }; - clock::destroy_for_testing(clock); transfer::public_transfer(record_cap, admin); - admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Test: Count lock satisfied (not in last 2), but time lock still active @@ -876,24 +922,28 @@ fun test_combined_locking_both_satisfied_can_delete() { // Create RecordAdmin role and add records ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Add 5 records - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(initial_time_for_testing() + 1000); let mut i = 0u64; @@ -908,10 +958,9 @@ fun test_combined_locking_both_satisfied_can_delete() { i = i + 1; }; - clock::destroy_for_testing(clock); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Test: Both locks satisfied - can delete diff --git a/audit-trail-move/tests/metadata_tests.move b/audit-trail-move/tests/metadata_tests.move index db2e8a5..eac3aea 100644 --- a/audit-trail-move/tests/metadata_tests.move +++ b/audit-trail-move/tests/metadata_tests.move @@ -4,9 +4,12 @@ module audit_trail::metadata_tests; use audit_trail::{ capability::Capability, locking, - main::{Self, AuditTrail}, permission, - test_utils::{TestData, setup_test_audit_trail} + test_utils::{ + setup_test_audit_trail, + fetch_capability_trail_and_clock, + cleanup_capability_trail_and_clock + } }; use iota::test_scenario as ts; use std::string; @@ -34,41 +37,45 @@ fun test_update_metadata_success() { // Create MetadataAdmin role and capability ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create MetadataAdmin role with metadata permissions let metadata_perms = permission::metadata_admin_permissions(); - trail.create_role( - &admin_cap, - string::utf8(b"MetadataAdmin"), - metadata_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"MetadataAdmin"), + metadata_perms, + &clock, + ts::ctx(&mut scenario), + ); // Issue capability to metadata admin user - let metadata_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"MetadataAdmin"), - ts::ctx(&mut scenario), - ); + let metadata_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"MetadataAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(metadata_cap, metadata_admin_user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Test: MetadataAdmin updates metadata ts::next_tx(&mut scenario, metadata_admin_user); { - let metadata_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (metadata_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Update metadata let new_metadata = std::option::some(string::utf8(b"Updated metadata value")); trail.update_metadata( &metadata_cap, new_metadata, + &clock, ts::ctx(&mut scenario), ); @@ -77,21 +84,20 @@ fun test_update_metadata_success() { assert!(current_metadata.is_some(), 0); assert!(*current_metadata.borrow() == string::utf8(b"Updated metadata value"), 1); - ts::return_to_sender(&scenario, metadata_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, metadata_cap, trail, clock); }; // Test: Update metadata again to verify multiple updates work ts::next_tx(&mut scenario, metadata_admin_user); { - let metadata_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (metadata_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Update to different value let new_metadata = std::option::some(string::utf8(b"Second update")); trail.update_metadata( &metadata_cap, new_metadata, + &clock, ts::ctx(&mut scenario), ); @@ -100,20 +106,19 @@ fun test_update_metadata_success() { assert!(current_metadata.is_some(), 2); assert!(*current_metadata.borrow() == string::utf8(b"Second update"), 3); - ts::return_to_sender(&scenario, metadata_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, metadata_cap, trail, clock); }; // Test: Set metadata to none ts::next_tx(&mut scenario, metadata_admin_user); { - let metadata_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (metadata_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Set to none trail.update_metadata( &metadata_cap, std::option::none(), + &clock, ts::ctx(&mut scenario), ); @@ -121,8 +126,7 @@ fun test_update_metadata_success() { let current_metadata = trail.metadata(); assert!(current_metadata.is_none(), 4); - ts::return_to_sender(&scenario, metadata_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, metadata_cap, trail, clock); }; ts::end(scenario); @@ -131,7 +135,7 @@ fun test_update_metadata_success() { // ===== Error Case Tests ===== #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_update_metadata_permission_denied() { let admin_user = @0xAD; let user = @0xB0B; @@ -152,51 +156,54 @@ fun test_update_metadata_permission_denied() { // Create role WITHOUT update_metadata permission ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create role with only add_record permission (no update_metadata) let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role( - &admin_cap, - string::utf8(b"NoMetadataPerm"), - perms, - ts::ctx(&mut scenario), - ); - - let user_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoMetadataPerm"), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoMetadataPerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); + + let user_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoMetadataPerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(user_cap, user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // User tries to update metadata - should fail ts::next_tx(&mut scenario, user); { - let user_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (user_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // This should fail - no update_metadata permission trail.update_metadata( &user_cap, std::option::some(string::utf8(b"Should fail")), + &clock, ts::ctx(&mut scenario), ); - ts::return_to_sender(&scenario, user_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, user_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::ECapabilityHasBeenRevoked)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityHasBeenRevoked)] fun test_update_metadata_revoked_capability() { let admin_user = @0xAD; let metadata_admin_user = @0xB0B; @@ -217,59 +224,62 @@ fun test_update_metadata_revoked_capability() { // Create MetadataAdmin role and capability ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create MetadataAdmin role let metadata_perms = permission::metadata_admin_permissions(); - trail.create_role( - &admin_cap, - string::utf8(b"MetadataAdmin"), - metadata_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"MetadataAdmin"), + metadata_perms, + &clock, + ts::ctx(&mut scenario), + ); // Issue capability - let metadata_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"MetadataAdmin"), - ts::ctx(&mut scenario), - ); + let metadata_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"MetadataAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(metadata_cap, metadata_admin_user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Revoke the capability ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let metadata_cap = ts::take_from_address(&scenario, metadata_admin_user); - trail.revoke_capability(&admin_cap, metadata_cap.id()); + trail + .roles_mut() + .revoke_capability(&admin_cap, metadata_cap.id(), &clock, ts::ctx(&mut scenario)); ts::return_to_address(metadata_admin_user, metadata_cap); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Try to use revoked capability - should fail ts::next_tx(&mut scenario, metadata_admin_user); { - let metadata_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (metadata_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // This should fail - capability has been revoked trail.update_metadata( &metadata_cap, std::option::some(string::utf8(b"Should fail")), + &clock, ts::ctx(&mut scenario), ); - ts::return_to_sender(&scenario, metadata_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, metadata_cap, trail, clock); }; ts::end(scenario); diff --git a/audit-trail-move/tests/record_tests.move b/audit-trail-move/tests/record_tests.move index 02e692a..c52dec3 100644 --- a/audit-trail-move/tests/record_tests.move +++ b/audit-trail-move/tests/record_tests.move @@ -2,7 +2,6 @@ module audit_trail::record_tests; use audit_trail::{ - capability::Capability, locking, main::{Self, AuditTrail}, permission, @@ -12,7 +11,10 @@ use audit_trail::{ new_test_data, initial_time_for_testing, test_data_value, - test_data_message + test_data_message, + fetch_capability_trail_and_clock, + cleanup_capability_trail_and_clock, + cleanup_trail_and_clock } }; use iota::{clock, test_scenario as ts}; @@ -39,34 +41,36 @@ fun test_add_record_to_empty_trail() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Add record ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let record_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // Verify initial state @@ -87,9 +91,7 @@ fun test_add_record_to_empty_trail() { assert!(!trail.is_empty(), 3); assert!(trail.has_record(0), 4); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); @@ -114,34 +116,36 @@ fun test_add_multiple_records() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Add multiple records ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let record_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // Add 3 records @@ -164,16 +168,14 @@ fun test_add_multiple_records() { assert!(trail.has_record(2), 3); assert!(!trail.has_record(3), 4); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_add_record_permission_denied() { let admin = @0xAD; let mut scenario = ts::begin(admin); @@ -192,30 +194,37 @@ fun test_add_record_permission_denied() { // Create role WITHOUT AddRecord permission ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::delete_record()]); - trail.create_role(&admin_cap, string::utf8(b"NoAddPerm"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoAddPerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); - let no_add_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoAddPerm"), - ts::ctx(&mut scenario), - ); + let no_add_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoAddPerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(no_add_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to add record - should fail ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let no_add_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (no_add_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // This should fail - no AddRecord permission @@ -227,9 +236,7 @@ fun test_add_record_permission_denied() { ts::ctx(&mut scenario), ); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, no_add_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, no_add_cap, trail, clock); }; ts::end(scenario); @@ -256,34 +263,36 @@ fun test_delete_record_success() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Delete record ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let record_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // Verify initial state @@ -297,16 +306,14 @@ fun test_delete_record_success() { assert!(trail.record_count() == 1, 2); // record_count doesn't decrease assert!(!trail.has_record(0), 3); // but record is gone - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_delete_record_permission_denied() { let admin = @0xAD; let mut scenario = ts::begin(admin); @@ -325,38 +332,43 @@ fun test_delete_record_permission_denied() { // Create role WITHOUT DeleteRecord permission ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"NoDeletePerm"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoDeletePerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); - let no_delete_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoDeletePerm"), - ts::ctx(&mut scenario), - ); + let no_delete_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoDeletePerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(no_delete_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to delete record - should fail ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let no_delete_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (no_delete_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // This should fail - no DeleteRecord permission trail.delete_record(&no_delete_cap, 0, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, no_delete_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, no_delete_cap, trail, clock); }; ts::end(scenario); @@ -382,42 +394,42 @@ fun test_delete_record_not_found() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to delete non-existent record - should fail ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let record_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // This should fail - record doesn't exist trail.delete_record(&record_cap, 999, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); @@ -443,43 +455,43 @@ fun test_delete_record_time_locked() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to delete locked record - should fail ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let record_cap = ts::take_from_sender(&scenario); - + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); // Time is only 1 second after creation - still within lock window - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(initial_time_for_testing() + 1000); // +1 second // This should fail - record is time-locked trail.delete_record(&record_cap, 0, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); @@ -505,42 +517,42 @@ fun test_delete_record_count_locked() { // Create RecordAdmin role ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(record_cap, admin); admin_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_trail_and_clock(trail, clock); }; // Try to delete locked record - should fail ts::next_tx(&mut scenario, admin); { - let mut trail = ts::take_shared>(&scenario); - let record_cap = ts::take_from_sender(&scenario); - - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); + let (record_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); clock.set_for_testing(initial_time_for_testing() + 1000); // Only 1 record exists, and last 5 are locked, so it's locked trail.delete_record(&record_cap, 0, &clock, ts::ctx(&mut scenario)); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_cap, trail, clock); }; ts::end(scenario); @@ -630,27 +642,31 @@ fun test_first_last_sequence() { // Create RecordAdmin and test sequence functions ts::next_tx(&mut scenario, admin); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, mut clock) = fetch_capability_trail_and_clock(&mut scenario); // Empty trail assert!(trail.first_sequence().is_none(), 0); assert!(trail.last_sequence().is_none(), 1); - trail.create_role( - &admin_cap, - string::utf8(b"RecordAdmin"), - permission::record_admin_permissions(), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RecordAdmin"), + permission::record_admin_permissions(), + &clock, + ts::ctx(&mut scenario), + ); - let record_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); clock.set_for_testing(initial_time_for_testing() + 1000); // Add first record @@ -689,10 +705,8 @@ fun test_first_last_sequence() { assert!(trail.first_sequence() == std::option::some(0), 6); assert!(trail.last_sequence() == std::option::some(2), 7); - clock::destroy_for_testing(clock); - admin_cap.destroy_for_testing(); record_cap.destroy_for_testing(); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); diff --git a/audit-trail-move/tests/role_tests.move b/audit-trail-move/tests/role_tests.move index eb8a6f7..b7e2de8 100644 --- a/audit-trail-move/tests/role_tests.move +++ b/audit-trail-move/tests/role_tests.move @@ -2,13 +2,18 @@ module audit_trail::role_tests; use audit_trail::{ - capability::Capability, locking, - main::{Self, AuditTrail, initial_admin_role_name}, + main::{initial_admin_role_name, AuditTrail}, permission, - test_utils::{Self, TestData, setup_test_audit_trail} + test_utils::{ + Self, + TestData, + setup_test_audit_trail, + fetch_capability_trail_and_clock, + cleanup_capability_trail_and_clock + } }; -use iota::{clock, test_scenario as ts}; +use iota::test_scenario as ts; use std::string; #[test] @@ -32,7 +37,7 @@ fun test_role_based_permission_delegation() { // Verify admin capability was created with correct role and trail reference assert!(admin_cap.role() == initial_admin_role_name(), 0); - assert!(admin_cap.trail_id() == trail_id, 1); + assert!(admin_cap.security_vault_id() == trail_id, 1); // Transfer the admin capability to the user transfer::public_transfer(admin_cap, admin_user); @@ -43,128 +48,140 @@ fun test_role_based_permission_delegation() { // Step 2: Admin creates RoleAdmin and CapAdmin roles ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Verify initial state - should only have the initial admin role assert!(trail.roles().size() == 1, 2); // Create RoleAdmin role let role_admin_perms = permission::role_admin_permissions(); - trail.create_role( - &admin_cap, - string::utf8(b"RoleAdmin"), - role_admin_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RoleAdmin"), + role_admin_perms, + &clock, + ts::ctx(&mut scenario), + ); // Create CapAdmin role let cap_admin_perms = permission::cap_admin_permissions(); - trail.create_role( - &admin_cap, - string::utf8(b"CapAdmin"), - cap_admin_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"CapAdmin"), + cap_admin_perms, + &clock, + ts::ctx(&mut scenario), + ); // Verify both roles were created assert!(trail.roles().size() == 3, 3); // Initial admin + RoleAdmin + CapAdmin - assert!(trail.has_role(&string::utf8(b"RoleAdmin")), 4); - assert!(trail.has_role(&string::utf8(b"CapAdmin")), 5); + assert!(trail.roles().has_role(&string::utf8(b"RoleAdmin")), 4); + assert!(trail.roles().has_role(&string::utf8(b"CapAdmin")), 5); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Step 3: Admin creates capability for RoleAdmin and CapAdmin and transfers to the respective users ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); - let role_admin_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"RoleAdmin"), - ts::ctx(&mut scenario), - ); + let role_admin_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"RoleAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Verify the capability was created with correct role and trail ID assert!(role_admin_cap.role() == string::utf8(b"RoleAdmin"), 6); - assert!(role_admin_cap.trail_id() == trail_id, 7); + assert!(role_admin_cap.security_vault_id() == trail_id, 7); iota::transfer::public_transfer(role_admin_cap, role_admin_user); - let cap_admin_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"CapAdmin"), - ts::ctx(&mut scenario), - ); + let cap_admin_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"CapAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Verify the capability was created with correct role and trail ID assert!(cap_admin_cap.role() == string::utf8(b"CapAdmin"), 8); - assert!(cap_admin_cap.trail_id() == trail_id, 9); + assert!(cap_admin_cap.security_vault_id() == trail_id, 9); iota::transfer::public_transfer(cap_admin_cap, cap_admin_user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // Step 5: RoleAdmin creates RecordAdmin role (demonstrating delegated role management) ts::next_tx(&mut scenario, role_admin_user); { - let mut trail = ts::take_shared>(&scenario); - let role_admin_cap = ts::take_from_sender(&scenario); + let (role_admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Verify RoleAdmin has the correct role assert!(role_admin_cap.role() == string::utf8(b"RoleAdmin"), 10); let record_admin_perms = permission::record_admin_permissions(); - trail.create_role( - &role_admin_cap, - string::utf8(b"RecordAdmin"), - record_admin_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &role_admin_cap, + string::utf8(b"RecordAdmin"), + record_admin_perms, + &clock, + ts::ctx(&mut scenario), + ); // Verify RecordAdmin role was created successfully assert!(trail.roles().size() == 4, 11); // Initial admin + RoleAdmin + CapAdmin + RecordAdmin - assert!(trail.has_role(&string::utf8(b"RecordAdmin")), 12); + assert!(trail.roles().has_role(&string::utf8(b"RecordAdmin")), 12); - ts::return_to_sender(&scenario, role_admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, role_admin_cap, trail, clock); }; // Step 6: CapAdmin creates capability for RecordAdmin and transfers to record_admin_user ts::next_tx(&mut scenario, cap_admin_user); { - let mut trail = ts::take_shared>(&scenario); - let cap_admin_cap = ts::take_from_sender(&scenario); + let (cap_admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Verify CapAdmin has the correct role assert!(cap_admin_cap.role() == string::utf8(b"CapAdmin"), 13); - let record_admin_cap = trail.new_capability( - &cap_admin_cap, - &string::utf8(b"RecordAdmin"), - ts::ctx(&mut scenario), - ); + let record_admin_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &cap_admin_cap, + &string::utf8(b"RecordAdmin"), + &clock, + ts::ctx(&mut scenario), + ); // Verify the capability was created with correct role and trail ID assert!(record_admin_cap.role() == string::utf8(b"RecordAdmin"), 14); - assert!(record_admin_cap.trail_id() == trail_id, 15); + assert!(record_admin_cap.security_vault_id() == trail_id, 15); iota::transfer::public_transfer(record_admin_cap, record_admin_user); - ts::return_to_sender(&scenario, cap_admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, cap_admin_cap, trail, clock); }; // Step 7: RecordAdmin adds a new record to the audit trail (demonstrating delegated record management) ts::next_tx(&mut scenario, record_admin_user); { - let mut trail = ts::take_shared>(&scenario); - let record_admin_cap = ts::take_from_sender(&scenario); + let (record_admin_cap, mut trail, mut clock) = fetch_capability_trail_and_clock( + &mut scenario, + ); + clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); // Verify RecordAdmin has the correct role assert!(record_admin_cap.role() == string::utf8(b"RecordAdmin"), 16); @@ -172,9 +189,6 @@ fun test_role_based_permission_delegation() { // Verify initial record count let initial_record_count = trail.records().length(); - let mut clock = clock::create_for_testing(ts::ctx(&mut scenario)); - clock.set_for_testing(test_utils::initial_time_for_testing() + 1000); - let test_data = test_utils::new_test_data(42, b"Test record added by RecordAdmin"); trail.add_record( @@ -188,9 +202,7 @@ fun test_role_based_permission_delegation() { // Verify the record was added successfully assert!(trail.records().length() == initial_record_count + 1, 17); - clock::destroy_for_testing(clock); - ts::return_to_sender(&scenario, record_admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, record_admin_cap, trail, clock); }; // Cleanup @@ -216,29 +228,42 @@ fun test_delete_role_success() { ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Verify initial state - only Admin role exists assert!(trail.roles().size() == 1, 0); // Create a role to delete let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"RoleToDelete"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RoleToDelete"), + perms, + &clock, + ts::ctx(&mut scenario), + ); // Verify the role was created assert!(trail.roles().size() == 2, 1); - assert!(trail.has_role(&string::utf8(b"RoleToDelete")), 2); + assert!(trail.roles().has_role(&string::utf8(b"RoleToDelete")), 2); // Delete the role - trail.delete_role(&admin_cap, &string::utf8(b"RoleToDelete"), ts::ctx(&mut scenario)); + trail + .roles_mut() + .delete_role( + &admin_cap, + &string::utf8(b"RoleToDelete"), + &clock, + ts::ctx(&mut scenario), + ); // Verify the role was deleted assert!(trail.roles().size() == 1, 3); - assert!(!trail.has_role(&string::utf8(b"RoleToDelete")), 4); + assert!(!trail.roles().has_role(&string::utf8(b"RoleToDelete")), 4); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); @@ -247,7 +272,7 @@ fun test_delete_role_success() { // ===== Error Case Tests ===== #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_create_role_permission_denied() { let admin_user = @0xAD; let user = @0xB0B; @@ -268,44 +293,59 @@ fun test_create_role_permission_denied() { // Create role without RolesAdd permission ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create role WITHOUT add_roles permission let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"NoRolesPerm"), perms, ts::ctx(&mut scenario)); - - let user_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoRolesPerm"), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoRolesPerm"), + perms, + &clock, + ts::ctx(&mut scenario), + ); + + let user_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoRolesPerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(user_cap, user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // User tries to create a role - should fail ts::next_tx(&mut scenario, user); { - let user_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (user_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let perms = permission::from_vec(vector[permission::add_record()]); // This should fail - no add_roles permission - trail.create_role(&user_cap, string::utf8(b"NewRole"), perms, ts::ctx(&mut scenario)); - - ts::return_to_sender(&scenario, user_cap); - ts::return_shared(trail); + trail + .roles_mut() + .create_role( + &user_cap, + string::utf8(b"NewRole"), + perms, + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, user_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_delete_role_permission_denied() { let admin_user = @0xAD; let user = @0xB0B; @@ -326,51 +366,63 @@ fun test_delete_role_permission_denied() { // Create roles ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create a role to delete let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"RoleToDelete"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RoleToDelete"), + perms, + &clock, + ts::ctx(&mut scenario), + ); // Create role WITHOUT delete_roles permission let no_delete_perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role( - &admin_cap, - string::utf8(b"NoDeleteRolePerm"), - no_delete_perms, - ts::ctx(&mut scenario), - ); - - let user_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoDeleteRolePerm"), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoDeleteRolePerm"), + no_delete_perms, + &clock, + ts::ctx(&mut scenario), + ); + + let user_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoDeleteRolePerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(user_cap, user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // User tries to delete a role - should fail ts::next_tx(&mut scenario, user); { - let user_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (user_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // This should fail - no delete_roles permission - trail.delete_role(&user_cap, &string::utf8(b"RoleToDelete"), ts::ctx(&mut scenario)); + trail + .roles_mut() + .delete_role(&user_cap, &string::utf8(b"RoleToDelete"), &clock, ts::ctx(&mut scenario)); - ts::return_to_sender(&scenario, user_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, user_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::EPermissionDenied)] +#[expected_failure(abort_code = audit_trail::role_map::ECapabilityPermissionDenied)] fun test_update_role_permissions_permission_denied() { let admin_user = @0xAD; let user = @0xB0B; @@ -391,58 +443,71 @@ fun test_update_role_permissions_permission_denied() { // Create roles ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create a role to update let perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role(&admin_cap, string::utf8(b"RoleToUpdate"), perms, ts::ctx(&mut scenario)); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"RoleToUpdate"), + perms, + &clock, + ts::ctx(&mut scenario), + ); // Create role WITHOUT update_roles permission let no_update_perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role( - &admin_cap, - string::utf8(b"NoUpdateRolePerm"), - no_update_perms, - ts::ctx(&mut scenario), - ); - - let user_cap = trail.new_capability( - &admin_cap, - &string::utf8(b"NoUpdateRolePerm"), - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"NoUpdateRolePerm"), + no_update_perms, + &clock, + ts::ctx(&mut scenario), + ); + + let user_cap = trail + .roles_mut() + .new_capability_without_restrictions( + &admin_cap, + &string::utf8(b"NoUpdateRolePerm"), + &clock, + ts::ctx(&mut scenario), + ); transfer::public_transfer(user_cap, user); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; // User tries to update a role - should fail ts::next_tx(&mut scenario, user); { - let user_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (user_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let new_perms = permission::from_vec(vector[permission::delete_record()]); // This should fail - no update_roles permission - trail.update_role_permissions( - &user_cap, - &string::utf8(b"RoleToUpdate"), - new_perms, - ts::ctx(&mut scenario), - ); - - ts::return_to_sender(&scenario, user_cap); - ts::return_shared(trail); + trail + .roles_mut() + .update_role_permissions( + &user_cap, + &string::utf8(b"RoleToUpdate"), + new_perms, + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, user_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::ERoleDoesNotExist)] +#[expected_failure(abort_code = audit_trail::role_map::ERoleDoesNotExist)] fun test_get_role_permissions_nonexistent() { let admin_user = @0xAD; @@ -463,7 +528,7 @@ fun test_get_role_permissions_nonexistent() { let trail = ts::take_shared>(&scenario); // This should fail - role doesn't exist - let _perms = trail.get_role_permissions(&string::utf8(b"NonExistentRole")); + let _perms = trail.roles().get_role_permissions(&string::utf8(b"NonExistentRole")); ts::return_shared(trail); }; @@ -489,46 +554,50 @@ fun test_update_role_permissions_success() { ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); // Create a role with add_record permission let initial_perms = permission::from_vec(vector[permission::add_record()]); - trail.create_role( - &admin_cap, - string::utf8(b"TestRole"), - initial_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .create_role( + &admin_cap, + string::utf8(b"TestRole"), + initial_perms, + &clock, + ts::ctx(&mut scenario), + ); // Verify the role was created with add_record permission - let perms = trail.get_role_permissions(&string::utf8(b"TestRole")); + let perms = trail.roles().get_role_permissions(&string::utf8(b"TestRole")); assert!(perms.contains(&permission::add_record()), 0); assert!(!perms.contains(&permission::delete_record()), 1); // Update the role to have delete_record permission instead let new_perms = permission::from_vec(vector[permission::delete_record()]); - trail.update_role_permissions( - &admin_cap, - &string::utf8(b"TestRole"), - new_perms, - ts::ctx(&mut scenario), - ); + trail + .roles_mut() + .update_role_permissions( + &admin_cap, + &string::utf8(b"TestRole"), + new_perms, + &clock, + ts::ctx(&mut scenario), + ); // Verify the permissions were updated - let updated_perms = trail.get_role_permissions(&string::utf8(b"TestRole")); + let updated_perms = trail.roles().get_role_permissions(&string::utf8(b"TestRole")); assert!(!updated_perms.contains(&permission::add_record()), 2); assert!(updated_perms.contains(&permission::delete_record()), 3); - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); } #[test] -#[expected_failure(abort_code = main::ERoleDoesNotExist)] +#[expected_failure(abort_code = audit_trail::role_map::ERoleDoesNotExist)] fun test_update_role_permissions_nonexistent() { let admin_user = @0xAD; @@ -546,21 +615,22 @@ fun test_update_role_permissions_nonexistent() { ts::next_tx(&mut scenario, admin_user); { - let admin_cap = ts::take_from_sender(&scenario); - let mut trail = ts::take_shared>(&scenario); + let (admin_cap, mut trail, clock) = fetch_capability_trail_and_clock(&mut scenario); let new_perms = permission::from_vec(vector[permission::add_record()]); // This should fail - role doesn't exist - trail.update_role_permissions( - &admin_cap, - &string::utf8(b"NonExistentRole"), - new_perms, - ts::ctx(&mut scenario), - ); - - ts::return_to_sender(&scenario, admin_cap); - ts::return_shared(trail); + trail + .roles_mut() + .update_role_permissions( + &admin_cap, + &string::utf8(b"NonExistentRole"), + new_perms, + &clock, + ts::ctx(&mut scenario), + ); + + cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock); }; ts::end(scenario); diff --git a/audit-trail-move/tests/test_utils.move b/audit-trail-move/tests/test_utils.move index 18d828b..e0f5739 100644 --- a/audit-trail-move/tests/test_utils.move +++ b/audit-trail-move/tests/test_utils.move @@ -1,8 +1,8 @@ #[test_only] module audit_trail::test_utils; -use audit_trail::{capability::Capability, locking, main}; -use iota::{clock, test_scenario::{Self as ts, Scenario}}; +use audit_trail::{capability::Capability, locking, main::{Self, AuditTrail}}; +use iota::{clock::{Self, Clock}, test_scenario::{Self as ts, Scenario}}; use std::string; const INITIAL_TIME_FOR_TESTING: u64 = 1234; @@ -63,3 +63,28 @@ public(package) fun setup_test_audit_trail( (admin_cap, trail_id) } + +public(package) fun fetch_capability_trail_and_clock( + scenario: &mut Scenario, +): (Capability, AuditTrail, Clock) { + let admin_cap = ts::take_from_sender(scenario); + let trail = ts::take_shared>(scenario); + let clock = iota::clock::create_for_testing(ts::ctx(scenario)); + (admin_cap, trail, clock) +} + +public(package) fun cleanup_capability_trail_and_clock( + scenario: &Scenario, + cap: Capability, + trail: AuditTrail, + clock: Clock, +) { + iota::clock::destroy_for_testing(clock); + ts::return_to_sender(scenario, cap); + ts::return_shared(trail); +} + +public(package) fun cleanup_trail_and_clock(trail: AuditTrail, clock: Clock) { + iota::clock::destroy_for_testing(clock); + ts::return_shared(trail); +} diff --git a/bindings/wasm/notarization_wasm/examples/src/gas-costs/01_create_update_destroy.ts b/bindings/wasm/notarization_wasm/examples/src/gas-costs/01_create_update_destroy.ts new file mode 100644 index 0000000..fa716d3 --- /dev/null +++ b/bindings/wasm/notarization_wasm/examples/src/gas-costs/01_create_update_destroy.ts @@ -0,0 +1,133 @@ +// Copyright 2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { EpochInfo, IotaTransactionBlockResponse } from "@iota/iota-sdk/client"; +import { NotarizationClient, State } from "@iota/notarization/node"; +import { getFundedClient } from "../util"; + +const STATE_METADATA: string | null = null; // "State metadata example"; +const IMMUTABLE_DESCRIPTION: string | null = null; // "This metadata will not change"; + +let REFERENCE_GAS_PRICE: bigint | null = null; + +const BILLION = 1000000000; +const MINIMUM_STORAGE_COST = 0.0029488; // Unit is IOTA not Nanos + +function print_gas_cost(transaction_type: String, flexDataSize: number, response: IotaTransactionBlockResponse) { + const gasUsed = response.effects?.gasUsed; + const referenceGasPrice = REFERENCE_GAS_PRICE ? Number(REFERENCE_GAS_PRICE) : -1; // Fallback to -1 if EpochInfo is not available + + if (gasUsed != undefined) { + const totalGasCost = parseInt(gasUsed.computationCost) + parseInt(gasUsed.storageCost) + - parseInt(gasUsed.storageRebate); + const storageCost = parseInt(gasUsed.storageCost) / BILLION; + const computationCostNanos = parseInt(gasUsed.computationCost); + const storageCostAboveMin = storageCost - MINIMUM_STORAGE_COST; + console.log( + "-------------------------------------------------------------------------------------------------------", + ); + console.log(`--- Gas cost for '${transaction_type}' transaction`); + console.log( + "-------------------------------------------------------------------------------------------------------", + ); + console.log(`referenceGasPrice: ${referenceGasPrice}`); + console.log(`computationCost: ${computationCostNanos / BILLION}`); + console.log(`Computation Units: ${computationCostNanos / referenceGasPrice}`); + console.log(`storageCost: ${storageCost}`); + console.log(`flexDataSize: ${flexDataSize}`); + console.log(`storageCost above minimum (0.0029488): ${storageCostAboveMin}`); + console.log(`storageCostAboveMin per flexDataSize: ${storageCostAboveMin / (flexDataSize - 1)}`); + console.log(`storageRebate: ${parseInt(gasUsed.storageRebate) / BILLION}`); + console.log(`totalGasCost (calculated): ${totalGasCost / BILLION}`); + console.log( + "-------------------------------------------------------------------------------------------------------", + ); + } else { + console.log("Gas used information is not available."); + } +} + +function randomString(length = 50) { + return [...Array(length + 10)].map((value) => (Math.random() * 1000000).toString(36).replace(".", "")).join("") + .substring(0, length); +} + +async function create_dynamic_notarization( + notarizationClient: NotarizationClient, + stateDataSize: number, +): Promise<{ notarization: any; response: IotaTransactionBlockResponse }> { + console.log(`Creating a dynamic notarization for state updates with ${stateDataSize} bytes of state data`); + + let stateData = randomString(stateDataSize); + + const { output: notarization, response: response } = await notarizationClient + .createDynamic() + .withStringState(stateData, STATE_METADATA) + .withImmutableDescription(IMMUTABLE_DESCRIPTION) + .finish() + .buildAndExecute(notarizationClient); + + console.log("āœ… Created dynamic notarization:", notarization.id); + const flexDataSize = stateData.length + (STATE_METADATA ? STATE_METADATA.length : 0) + + (IMMUTABLE_DESCRIPTION ? IMMUTABLE_DESCRIPTION.length : 0); + print_gas_cost("Create", flexDataSize, response); + + return { notarization, response }; +} + +/** Create, update and destroy a Dynamic Notarization to estimate gas cost */ +export async function createUpdateDestroy(): Promise { + console.log("Create, update and destroy a Dynamic Notarization to estimate gas cost"); + + const notarizationClient = await getFundedClient(); + + const iotaClient = notarizationClient.iotaClient(); + REFERENCE_GAS_PRICE = await iotaClient.getReferenceGasPrice(); + console.log( + "Successfully fetched the referenceGasPrice: ", + REFERENCE_GAS_PRICE != null ? REFERENCE_GAS_PRICE : "Not Available", + ); + + let notarization; + + // Create several dynamic notarizations with different initial state sizes. The notarization with the largest state size will be used for updates. + console.log("\nšŸ†• Creating dynamic notarizations with different initial state sizes..."); + for (let i = 1; i <= 4; i++) { + const result = await create_dynamic_notarization(notarizationClient, 10 * i * i); // 10, 40, 90, 160 bytes + notarization = result.notarization; + } + + // Perform multiple state updates + console.log("\nšŸ”„ Performing state updates..."); + + for (let i = 1; i <= 3; i++) { + console.log(`\n--- Update ${i} ---`); + + // Create new state with updated content and metadata + const newContent = randomString(i * 50); // Set this size to 138 bytes to keep total flex data size equal to the latest created notarization + const newMetadata = `Version ${i + 1}.0 - Update ${i}`; + + // Update the state + const { output: _, response: response } = await notarizationClient + .updateState( + State.fromString(newContent, newMetadata), + notarization.id, + ) + .buildAndExecute(notarizationClient); + + console.log(`āœ… State update ${i} completed`); + const flexDataSize = newContent.length + newMetadata.length; + print_gas_cost("Update", flexDataSize, response); + } + + // Destroy the dynamic notarization + try { + const { output: _, response: response } = await notarizationClient + .destroy(notarization.id) + .buildAndExecute(notarizationClient); + console.log("āœ… Successfully destroyed unlocked dynamic notarization"); + print_gas_cost("Destroy", 1, response); + } catch (e) { + console.log("āŒ Failed to destroy:", e); + } +} diff --git a/bindings/wasm/notarization_wasm/examples/src/gas-costs/README.md b/bindings/wasm/notarization_wasm/examples/src/gas-costs/README.md new file mode 100644 index 0000000..5449be1 --- /dev/null +++ b/bindings/wasm/notarization_wasm/examples/src/gas-costs/README.md @@ -0,0 +1,62 @@ +# Gas Cost Estimation Example for Notarization + +This folder contains an example to estimate the gas cost for Notarization object creation, update and destroy operations. + +It can be run like any other example. + +The log output of the example is optimized to evaluate variables and constants needed to calculate gas cost as being +described in the following sections. + +## Results of the Gas Cost Estimation + +The gas cost for creating Dynamic and Locked Notarizations only differ in the amount of needed Storage Cost. +The mimimum Byte size of a Locked Notarization is 19 bytes larger than the one of a Dynamic Notarization due to the additional +lock information stored in the Notarization object. This results in a slightly higher Storage Cost (0.0001425 IOTA) for +Locked Notarizations compared to Dynamic Notarizations when they are created with the same amount of State Data, Metadata, etc. + +**For the sake of simplicity, the following sections only describe the gas cost estimation for Dynamic Notarizations.** + +### Creating Notarizations + +The cost for creating a Notarization object can roughly be calculated by the following equation: + + `TotalCost` = `FlexDataSize` * `FlexDataByteCost` + `MinimumStorageCost` + `ComputationCost` + + `TotalCost` = F [Byte] * 0.0000076 [IOTA/Byte] + 0.00295 [IOTA] + 0.001 [IOTA] + +Where: + +| Parameter | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `FlexDataSize` | Sum of the byte sizes of State Data, State Metadata, Updatable Metadata and Immutable Metadata. The value must be reduced by 1 as the `MinimumStorageCost` uses 1 byte of State Data. | +| `FlexDataByteCost` | A constant value of 0.0000076 IOTA/Byte
This value denotes (`StorageCost` - `MinimumStorageCost`) divided by `FlexDataSize`. | +| `MinimumStorageCost` | A constant value of 0.00295 IOTA.
This value denotes the `StorageCost` for a Notarization with 1 Byte of `FlexDataSize` meaning a Notarization with 1 Byte of State Data, no meta data and no optional locks. | +| `ComputationCost` | A constant value of 0.001 IOTA.
Given the Gas Price is 1000 nano, the `ComputationCost` will always be 0.001 IOTA as creating Notarizations always consume 1000 Computation Units. | +| `TotalCost` | The amount of IOTA that would need to be paid for gas when Storage Rebate is not taken into account. The real gas cost will be lower, due to Storage Rebate, which is usually -0.0009804 IOTA when a Notarization object is created. | + +Examples: + +| `FlexDataSize` | `TotalCost` (Storage Rebate not taken into account) | +| -------------- | --------------------------------------------------- | +| 10 | 0.004026 IOTA | +| 100 | 0.00471 IOTA | +| 1000 | 0.01155 IOTA | + +### Updating Dynamic Notarizations + +The `TotalCost` for updating a Dynamic Notarization can roughly be calculated using the same equation used for creating +Notarization objects (see above). + +The value for `FlexDataByteCost` should be set to 0.00000769 IOTA/Byte. + +If the new Notarization State results in the same `FlexDataSize` as the overwritten old Notarization State, the Storage +Rebate will compensate the Storage Cost so that the real gas cost to be paid will be more or less the Computation Cost, +which is always 0.001 IOTA (presumed the Gas Price is 1000 nano). + +### Destroying a Notarization + +The `TotalCost` for destroying a Notarization is the Computation Cost which is 0.001 IOTA (presumed the Gas Price is 1000 nano). + +Due to the Storage Rebate, which depends on the size of the stored Notarization object, the real gas cost to be paid will often be negative. + +The Storage Rebate can roughly be calculated using the below equation. See above for more details about the used variables and constants. diff --git a/bindings/wasm/notarization_wasm/examples/src/main.ts b/bindings/wasm/notarization_wasm/examples/src/main.ts index 0f78ce9..b86467f 100644 --- a/bindings/wasm/notarization_wasm/examples/src/main.ts +++ b/bindings/wasm/notarization_wasm/examples/src/main.ts @@ -9,6 +9,7 @@ import { updateState } from "./05_update_state"; import { updateMetadata } from "./06_update_metadata"; import { transferNotarization } from "./07_transfer_notarization"; import { accessReadOnlyMethods } from "./08_access_read_only_methods"; +import { createUpdateDestroy } from "./gas-costs/01_create_update_destroy"; import { iotWeatherStation } from "./real-world/01_iot_weather_station"; import { legalContract } from "./real-world/02_legal_contract"; @@ -40,6 +41,8 @@ export async function main(example?: string) { return await iotWeatherStation(); case "02_real_world_legal_contract": return await legalContract(); + case "01_gas_costs_create_update_destroy": + return await createUpdateDestroy(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/notarization-move/sources/timelock.move b/notarization-move/sources/timelock.move index e287228..c6eb46f 100644 --- a/notarization-move/sources/timelock.move +++ b/notarization-move/sources/timelock.move @@ -23,7 +23,7 @@ const ETimelockNotExpired: u64 = 1; /// Represents different types of time-based locks that can be applied to /// notarizations. public enum TimeLock has store { - /// A lock that unlocks at a specific Unix timestamp (seconds since epoch) + /// A lock that unlocks at a specific Unix timestamp (seconds since Unix epoch) UnlockAt(u32), /// A permanent lock that never unlocks until the notarization object is destroyed (can't be used for `delete_lock`) UntilDestroyed,