Skip to content

Commit d7ff478

Browse files
authored
Merge pull request #248 from iotaledger/chore/add-return-values-for-at
chore: enhance audit trail API with return values
2 parents 1f13c12 + 49a7f58 commit d7ff478

15 files changed

Lines changed: 191 additions & 53 deletions

File tree

audit-trail-move/sources/audit_trail.move

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ public struct RecordDeleted has copy, drop {
125125
timestamp: u64,
126126
}
127127

128+
/// Emitted when expired revoked-capability entries are removed from the denylist
129+
public struct RevokedCapabilitiesCleanedUp has copy, drop {
130+
trail_id: ID,
131+
cleaned_count: u64,
132+
cleaned_by: address,
133+
timestamp: u64,
134+
}
135+
136+
/// Returned when a capability is issued through the audit-trail API
137+
public struct CapabilityIssuedReceipt has copy, drop {
138+
target_key: ID,
139+
capability_id: ID,
140+
role: String,
141+
issued_to: Option<address>,
142+
valid_from: Option<u64>,
143+
valid_until: Option<u64>,
144+
}
145+
128146
// ===== Constructors =====
129147

130148
/// Create immutable trail metadata
@@ -277,6 +295,7 @@ fun assert_record_tag_allowed<D: store + copy>(
277295
/// Add a record to the trail
278296
///
279297
/// Records are added sequentially with auto-assigned sequence numbers.
298+
/// Returns the same receipt that is emitted as the `RecordAdded` event.
280299
public fun add_record<D: store + copy>(
281300
self: &mut AuditTrail<D>,
282301
cap: &Capability,
@@ -285,7 +304,7 @@ public fun add_record<D: store + copy>(
285304
record_tag: Option<String>,
286305
clock: &Clock,
287306
ctx: &mut TxContext,
288-
) {
307+
): RecordAdded {
289308
assert!(self.version == PACKAGE_VERSION, EPackageVersionMismatch);
290309
self
291310
.roles
@@ -320,12 +339,15 @@ public fun add_record<D: store + copy>(
320339
linked_table::push_back(&mut self.records, seq, record);
321340
self.sequence_number = self.sequence_number + 1;
322341

323-
event::emit(RecordAdded {
342+
let output = RecordAdded {
324343
trail_id,
325344
sequence_number: seq,
326345
added_by: caller,
327346
timestamp,
328-
});
347+
};
348+
349+
event::emit(copy output);
350+
output
329351
}
330352

331353
/// Delete a record from the trail by sequence number
@@ -377,14 +399,14 @@ public fun delete_record<D: store + copy + drop>(
377399
/// Delete up to `limit` records from the front of the trail.
378400
///
379401
/// Requires `DeleteAllRecords` permission. Locked records are skipped.
380-
/// Returns the number of records deleted in this batch.
402+
/// Returns the sequence numbers deleted in this batch, in deletion order.
381403
public fun delete_records_batch<D: store + copy + drop>(
382404
self: &mut AuditTrail<D>,
383405
cap: &Capability,
384406
limit: u64,
385407
clock: &Clock,
386408
ctx: &mut TxContext,
387-
): u64 {
409+
): vector<u64> {
388410
assert!(self.version == PACKAGE_VERSION, EPackageVersionMismatch);
389411
self
390412
.roles
@@ -396,6 +418,7 @@ public fun delete_records_batch<D: store + copy + drop>(
396418
);
397419

398420
let mut deleted = 0;
421+
let mut deleted_sequence_numbers = vector::empty<u64>();
399422
let caller = ctx.sender();
400423
let timestamp = clock.timestamp_ms();
401424
let trail_id = self.id();
@@ -431,11 +454,12 @@ public fun delete_records_batch<D: store + copy + drop>(
431454
deleted_by: caller,
432455
timestamp,
433456
});
457+
vector::push_back(&mut deleted_sequence_numbers, sequence_number);
434458

435459
deleted = deleted + 1;
436460
};
437461

438-
deleted
462+
deleted_sequence_numbers
439463
}
440464

441465
/// Delete an empty audit trail.
@@ -754,6 +778,7 @@ public fun delete_role<D: store + copy>(
754778
/// Issues a new capability for an existing role.
755779
///
756780
/// The capability object is transferred to `issued_to` if provided, otherwise to the caller.
781+
/// Returns the same receipt that is emitted as the `CapabilityIssued` event.
757782
public fun new_capability<D: store + copy>(
758783
self: &mut AuditTrail<D>,
759784
cap: &Capability,
@@ -763,7 +788,7 @@ public fun new_capability<D: store + copy>(
763788
valid_until: Option<u64>,
764789
clock: &Clock,
765790
ctx: &mut TxContext,
766-
) {
791+
): CapabilityIssuedReceipt {
767792
assert!(self.version == PACKAGE_VERSION, EPackageVersionMismatch);
768793

769794
let recipient = if (issued_to.is_some()) {
@@ -783,7 +808,16 @@ public fun new_capability<D: store + copy>(
783808
clock,
784809
ctx,
785810
);
811+
let output = CapabilityIssuedReceipt {
812+
target_key: self.id(),
813+
capability_id: new_cap.id(),
814+
role: *new_cap.role(),
815+
issued_to: *new_cap.issued_to(),
816+
valid_from: *new_cap.valid_from(),
817+
valid_until: *new_cap.valid_until(),
818+
};
786819
transfer::public_transfer(new_cap, recipient);
820+
output
787821
}
788822

789823
/// Revokes an issued capability by ID.
@@ -878,6 +912,8 @@ public fun revoke_initial_admin_capability<D: store + copy>(
878912
/// Entries with `valid_until == 0` (i.e. capabilities that had no expiry) are kept,
879913
/// since they remain potentially valid and must stay on the denylist.
880914
///
915+
/// Returns the same receipt that is emitted as the `RevokedCapabilitiesCleanedUp` event.
916+
///
881917
/// Parameters
882918
/// ----------
883919
/// - cap: Reference to the capability used to authorize this operation.
@@ -892,15 +928,25 @@ public fun cleanup_revoked_capabilities<D: store + copy>(
892928
cap: &Capability,
893929
clock: &Clock,
894930
ctx: &TxContext,
895-
) {
931+
): RevokedCapabilitiesCleanedUp {
896932
assert!(self.version == PACKAGE_VERSION, EPackageVersionMismatch);
933+
let revoked_count_before = linked_table::length(role_map::revoked_capabilities(self.access()));
897934
self
898935
.access_mut()
899936
.cleanup_revoked_capabilities(
900937
cap,
901938
clock,
902939
ctx,
903940
);
941+
let revoked_count_after = linked_table::length(role_map::revoked_capabilities(self.access()));
942+
let output = RevokedCapabilitiesCleanedUp {
943+
trail_id: self.id(),
944+
cleaned_count: revoked_count_before - revoked_count_after,
945+
cleaned_by: ctx.sender(),
946+
timestamp: clock::timestamp_ms(clock),
947+
};
948+
event::emit(copy output);
949+
output
904950
}
905951

906952
// ===== Trail Query Functions =====

audit-trail-move/tests/locking_tests.move

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,11 +1018,13 @@ fun test_delete_records_batch_skips_locked_records() {
10181018
&clock,
10191019
ts::ctx(&mut scenario),
10201020
);
1021-
assert!(deleted == 2, 0);
1022-
assert!(trail.record_count() == 1, 1);
1023-
assert!(!trail.has_record(0), 2);
1024-
assert!(!trail.has_record(1), 3);
1025-
assert!(trail.has_record(2), 4);
1021+
assert!(vector::length(&deleted) == 2, 0);
1022+
assert!(*vector::borrow(&deleted, 0) == 0, 1);
1023+
assert!(*vector::borrow(&deleted, 1) == 1, 2);
1024+
assert!(trail.record_count() == 1, 3);
1025+
assert!(!trail.has_record(0), 4);
1026+
assert!(!trail.has_record(1), 5);
1027+
assert!(trail.has_record(2), 6);
10261028

10271029
record_maintenance_cap.destroy_for_testing();
10281030
cleanup_capability_trail_and_clock(&scenario, admin_cap, trail, clock);
@@ -1144,8 +1146,9 @@ fun test_delete_audit_trail_after_batch_cleanup() {
11441146
&clock,
11451147
ts::ctx(&mut scenario),
11461148
);
1147-
assert!(deleted == 1, 0);
1148-
assert!(trail.record_count() == 0, 1);
1149+
assert!(vector::length(&deleted) == 1, 0);
1150+
assert!(*vector::borrow(&deleted, 0) == 0, 1);
1151+
assert!(trail.record_count() == 0, 2);
11491152

11501153
main::delete_audit_trail(trail, &delete_maintenance_cap, &clock, ts::ctx(&mut scenario));
11511154

audit-trail-move/tests/record_tests.move

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,9 @@ fun test_delete_records_batch_with_matching_role_tags() {
309309
);
310310

311311
let deleted = trail.delete_records_batch(&cap, 10, &clock, ts::ctx(&mut scenario));
312-
assert!(deleted == 1, 0);
313-
assert!(trail.record_count() == 0, 1);
312+
assert!(vector::length(&deleted) == 1, 0);
313+
assert!(*vector::borrow(&deleted, 0) == 0, 1);
314+
assert!(trail.record_count() == 0, 2);
314315

315316
cleanup_capability_trail_and_clock(&scenario, cap, trail, clock);
316317
};

audit-trail-rs/src/core/access/transactions.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ use tokio::sync::OnceCell;
1818
use super::operations::AccessOps;
1919
use crate::core::types::{
2020
CapabilityDestroyed, CapabilityIssueOptions, CapabilityIssued, CapabilityRevoked, Event, PermissionSet,
21-
RawRoleCreated, RawRoleDeleted, RawRoleUpdated, RoleCreated, RoleDeleted, RoleTags, RoleUpdated,
21+
RawRoleCreated, RawRoleDeleted, RawRoleUpdated, RevokedCapabilitiesCleanedUp, RoleCreated, RoleDeleted, RoleTags,
22+
RoleUpdated,
2223
};
2324
use crate::error::Error;
2425

@@ -709,7 +710,7 @@ impl Transaction for RevokeInitialAdminCapability {
709710
/// Transaction that cleans up expired revoked-capability entries.
710711
///
711712
/// This does not revoke additional capabilities. It only prunes denylist entries whose stored expiry has
712-
/// already elapsed.
713+
/// already elapsed and returns the typed cleanup receipt emitted by the Move package.
713714
#[derive(Debug, Clone)]
714715
pub struct CleanupRevokedCapabilities {
715716
trail_id: ObjectID,
@@ -741,7 +742,7 @@ impl CleanupRevokedCapabilities {
741742
#[cfg_attr(feature = "send-sync", async_trait)]
742743
impl Transaction for CleanupRevokedCapabilities {
743744
type Error = Error;
744-
type Output = ();
745+
type Output = RevokedCapabilitiesCleanedUp;
745746

746747
async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
747748
where
@@ -750,10 +751,30 @@ impl Transaction for CleanupRevokedCapabilities {
750751
self.cached_ptb.get_or_try_init(|| self.make_ptb(client)).await.cloned()
751752
}
752753

754+
async fn apply_with_events<C>(
755+
self,
756+
_: &mut IotaTransactionBlockEffects,
757+
events: &mut IotaTransactionBlockEvents,
758+
_: &C,
759+
) -> Result<Self::Output, Self::Error>
760+
where
761+
C: CoreClientReadOnly + OptionalSync,
762+
{
763+
let event = events
764+
.data
765+
.iter()
766+
.find_map(|data| {
767+
serde_json::from_value::<Event<RevokedCapabilitiesCleanedUp>>(data.parsed_json.clone()).ok()
768+
})
769+
.ok_or_else(|| Error::UnexpectedApiResponse("RevokedCapabilitiesCleanedUp event not found".to_string()))?;
770+
771+
Ok(event.data)
772+
}
773+
753774
async fn apply<C>(self, _: &mut IotaTransactionBlockEffects, _: &C) -> Result<Self::Output, Self::Error>
754775
where
755776
C: CoreClientReadOnly + OptionalSync,
756777
{
757-
Ok(())
778+
unreachable!("RevokedCapabilitiesCleanedUp output requires transaction events")
758779
}
759780
}

audit-trail-rs/src/core/records/transactions.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,9 @@ impl Transaction for DeleteRecord {
213213

214214
/// Transaction that deletes multiple records in a batch operation.
215215
///
216-
/// The Move entry point skips locked records, deletes up to `limit` unlocked records in trail order, and reports
217-
/// the number of deleted records through the emitted `RecordDeleted` events.
216+
/// The Move entry point skips locked records, deletes up to `limit` unlocked records in trail order, and returns
217+
/// the deleted sequence numbers. The Rust implementation mirrors that output by collecting the matching
218+
/// `RecordDeleted` events in order.
218219
#[derive(Debug, Clone)]
219220
pub struct DeleteRecordsBatch {
220221
/// Trail object ID containing the records.
@@ -259,7 +260,7 @@ impl DeleteRecordsBatch {
259260
#[cfg_attr(feature = "send-sync", async_trait)]
260261
impl Transaction for DeleteRecordsBatch {
261262
type Error = Error;
262-
type Output = u64;
263+
type Output = Vec<u64>;
263264

264265
async fn build_programmable_transaction<C>(&self, client: &C) -> Result<ProgrammableTransaction, Self::Error>
265266
where
@@ -281,7 +282,8 @@ impl Transaction for DeleteRecordsBatch {
281282
.data
282283
.iter()
283284
.filter_map(|data| serde_json::from_value::<Event<RecordDeleted>>(data.parsed_json.clone()).ok())
284-
.count() as u64;
285+
.map(|event| event.data.sequence_number)
286+
.collect();
285287

286288
Ok(deleted)
287289
}

audit-trail-rs/src/core/types/event.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ pub struct CapabilityRevoked {
120120
pub valid_until: u64,
121121
}
122122

123+
/// Event emitted when expired revoked-capability denylist entries are removed.
124+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125+
pub struct RevokedCapabilitiesCleanedUp {
126+
/// Trail object ID whose denylist was pruned.
127+
pub trail_id: ObjectID,
128+
/// Number of expired entries removed by this cleanup call.
129+
#[serde(deserialize_with = "deserialize_number_from_string")]
130+
pub cleaned_count: u64,
131+
/// Address that triggered the cleanup.
132+
pub cleaned_by: IotaAddress,
133+
/// Millisecond event timestamp.
134+
#[serde(deserialize_with = "deserialize_number_from_string")]
135+
pub timestamp: u64,
136+
}
137+
123138
/// Event emitted when a role is created.
124139
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
125140
pub struct RoleCreated {

audit-trail-rs/tests/e2e/access.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,16 @@ async fn cleanup_revoked_capabilities_removes_expired_entries() -> anyhow::Resul
568568
let before_cleanup = trail.get().await?;
569569
assert_eq!(before_cleanup.roles.revoked_capabilities.size, 1);
570570

571-
access.cleanup_revoked_capabilities().build_and_execute(&client).await?;
571+
let cleaned = access
572+
.cleanup_revoked_capabilities()
573+
.build_and_execute(&client)
574+
.await?
575+
.output;
576+
577+
assert_eq!(cleaned.trail_id, trail_id);
578+
assert_eq!(cleaned.cleaned_count, 1);
579+
assert_eq!(cleaned.cleaned_by, client.sender_address());
580+
assert!(cleaned.timestamp > 0);
572581

573582
let after_cleanup = trail.get().await?;
574583
assert_eq!(after_cleanup.roles.revoked_capabilities.size, 0);

audit-trail-rs/tests/e2e/records.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,11 @@ async fn delete_records_batch_respects_limit_and_deletes_oldest_first() -> anyho
993993
assert_eq!(records.record_count().await?, 3);
994994

995995
let deleted_two = records.delete_records_batch(2).build_and_execute(&client).await?.output;
996-
assert_eq!(deleted_two, 2, "batch delete should stop at the provided limit");
996+
assert_eq!(
997+
deleted_two,
998+
vec![0, 1],
999+
"batch delete should return the deleted sequence numbers"
1000+
);
9971001
assert_eq!(records.record_count().await?, 1);
9981002
assert!(records.get(0).await.is_err(), "oldest record should be removed first");
9991003
assert!(
@@ -1007,15 +1011,18 @@ async fn delete_records_batch_respects_limit_and_deletes_oldest_first() -> anyho
10071011
.build_and_execute(&client)
10081012
.await?
10091013
.output;
1010-
assert_eq!(deleted_last, 1, "remaining record should be deleted");
1014+
assert_eq!(deleted_last, vec![2], "remaining record should be deleted");
10111015
assert_eq!(records.record_count().await?, 0);
10121016

10131017
let deleted_empty = records
10141018
.delete_records_batch(10)
10151019
.build_and_execute(&client)
10161020
.await?
10171021
.output;
1018-
assert_eq!(deleted_empty, 0, "deleting from an empty trail should return zero");
1022+
assert!(
1023+
deleted_empty.is_empty(),
1024+
"deleting from an empty trail should return no sequence numbers"
1025+
);
10191026

10201027
Ok(())
10211028
}
@@ -1057,7 +1064,11 @@ async fn delete_records_batch_skips_locked_records() -> anyhow::Result<()> {
10571064
.build_and_execute(&client)
10581065
.await?
10591066
.output;
1060-
assert_eq!(deleted, 2, "batch delete should skip the count-locked tail record");
1067+
assert_eq!(
1068+
deleted,
1069+
vec![0, 1],
1070+
"batch delete should skip the count-locked tail record"
1071+
);
10611072
assert_eq!(records.record_count().await?, 1);
10621073
assert!(
10631074
records.get(0).await.is_err(),
@@ -1185,7 +1196,7 @@ async fn delete_records_batch_with_matching_role_tag_access_succeeds() -> anyhow
11851196
.build_and_execute(&client)
11861197
.await?
11871198
.output;
1188-
assert_eq!(deleted, 2);
1199+
assert_eq!(deleted, vec![0, 1]);
11891200
assert_eq!(records.record_count().await?, 0);
11901201
assert!(records.get(1).await.is_err());
11911202

0 commit comments

Comments
 (0)