11use crate :: base:: errors:: Error ;
22use crate :: base:: events:: {
3- AdminTransferred , AuthorizationFailure , AutoshareCreated , AutoshareUpdated , ContractPaused ,
4- ContractUnpaused , GroupActivated , GroupDeactivated , NotificationCategory , NotificationExpired ,
5- NotificationPriority , NotificationScheduled , ScheduledNotificationCancelled , Withdrawal ,
3+ AdminTransferred , AuditAction , AuditRecordAppended , AuthorizationFailure , AutoshareCreated ,
4+ AutoshareUpdated , BatchNotificationsCreated , ContractPaused , ContractUnpaused , GroupActivated ,
5+ GroupDeactivated , NotificationCategory , NotificationExpired , NotificationPriority ,
6+ NotificationScheduled , ScheduledNotificationCancelled , Withdrawal ,
7+ } ;
8+ use crate :: base:: types:: {
9+ AuditRecord , AutoShareDetails , GroupMember , PaymentHistory , ScheduledNotification ,
610} ;
7- use crate :: base:: types:: { AutoShareDetails , GroupMember , PaymentHistory , ScheduledNotification } ;
811use soroban_sdk:: { contracttype, token, Address , BytesN , Env , String , Vec } ;
912
1013/// Maximum allowed length for AutoShare group names.
@@ -24,6 +27,10 @@ pub enum DataKey {
2427 GroupMembers ( BytesN < 32 > ) ,
2528 IsPaused ,
2629 ScheduledNotification ( BytesN < 32 > ) ,
30+ /// Monotonically increasing counter for audit record sequence numbers.
31+ AuditSeq ,
32+ /// All audit records stored in a single Vec for full-scan queries.
33+ AuditLog ,
2734}
2835
2936pub fn create_autoshare (
@@ -899,6 +906,13 @@ pub fn schedule_notification(
899906 } ;
900907 env. storage ( ) . persistent ( ) . set ( & key, & notification) ;
901908
909+ append_audit_record (
910+ & env,
911+ notification_id. clone ( ) ,
912+ AuditAction :: Created ,
913+ creator. clone ( ) ,
914+ ) ;
915+
902916 NotificationScheduled {
903917 creator,
904918 category : NotificationCategory :: Notification ,
@@ -944,6 +958,13 @@ pub fn expire_notification(env: Env, notification_id: BytesN<32>) -> Result<(),
944958
945959 env. storage ( ) . persistent ( ) . remove ( & key) ;
946960
961+ append_audit_record (
962+ & env,
963+ notification_id. clone ( ) ,
964+ AuditAction :: Expired ,
965+ env. current_contract_address ( ) ,
966+ ) ;
967+
947968 NotificationExpired {
948969 notification_id,
949970 category : NotificationCategory :: Notification ,
@@ -984,6 +1005,13 @@ pub fn cancel_notification(
9841005 . remove ( & DataKey :: ScheduledNotification ( notification_id. clone ( ) ) ) ;
9851006 }
9861007
1008+ append_audit_record (
1009+ & env,
1010+ notification_id. clone ( ) ,
1011+ AuditAction :: Cancelled ,
1012+ caller. clone ( ) ,
1013+ ) ;
1014+
9871015 ScheduledNotificationCancelled {
9881016 caller,
9891017 category : NotificationCategory :: Notification ,
@@ -994,3 +1022,252 @@ pub fn cancel_notification(
9941022
9951023 Ok ( ( ) )
9961024}
1025+
1026+ // ============================================================================
1027+ // Batch Notification Creation
1028+ // ============================================================================
1029+
1030+ /// Maximum number of notifications that can be created in a single batch call.
1031+ const MAX_BATCH_SIZE : u32 = 50 ;
1032+
1033+ /// Creates multiple scheduled notifications in a single transaction.
1034+ ///
1035+ /// Each `ids[i]` is paired with `ttl_seconds[i]`. Both slices must have the same
1036+ /// length and must not be empty. The length must not exceed [`MAX_BATCH_SIZE`].
1037+ /// The same validation applied by [`schedule_notification`] is applied to each
1038+ /// entry; if any entry fails the entire call is rejected.
1039+ ///
1040+ /// A [`NotificationScheduled`] event is emitted for every created notification,
1041+ /// followed by a single [`BatchNotificationsCreated`] summary event carrying the
1042+ /// full list of ids and the count.
1043+ pub fn batch_schedule_notifications (
1044+ env : Env ,
1045+ ids : Vec < BytesN < 32 > > ,
1046+ creator : Address ,
1047+ ttl_seconds : Vec < u64 > ,
1048+ ) -> Result < ( ) , Error > {
1049+ creator. require_auth ( ) ;
1050+
1051+ if get_paused_status ( & env) {
1052+ return Err ( Error :: ContractPaused ) ;
1053+ }
1054+
1055+ let count = ids. len ( ) ;
1056+
1057+ // Must have at least one notification.
1058+ if count == 0 {
1059+ return Err ( Error :: InvalidInput ) ;
1060+ }
1061+
1062+ // Lengths must match.
1063+ if count != ttl_seconds. len ( ) {
1064+ return Err ( Error :: InvalidInput ) ;
1065+ }
1066+
1067+ // Enforce maximum batch size.
1068+ if count > MAX_BATCH_SIZE {
1069+ return Err ( Error :: BatchTooLarge ) ;
1070+ }
1071+
1072+ let created_at = env. ledger ( ) . timestamp ( ) ;
1073+
1074+ // Validate all entries before persisting any (all-or-nothing semantics).
1075+ // Also track ids seen within this batch to catch intra-batch duplicates.
1076+ let mut seen_in_batch: Vec < BytesN < 32 > > = Vec :: new ( & env) ;
1077+ for i in 0 ..count {
1078+ let ttl = ttl_seconds. get ( i) . unwrap ( ) ;
1079+ if ttl == 0 {
1080+ return Err ( Error :: InvalidExpirationDuration ) ;
1081+ }
1082+ let id = ids. get ( i) . unwrap ( ) ;
1083+
1084+ // Check for intra-batch duplicates.
1085+ for seen in seen_in_batch. iter ( ) {
1086+ if seen == id {
1087+ return Err ( Error :: AlreadyExists ) ;
1088+ }
1089+ }
1090+ seen_in_batch. push_back ( id. clone ( ) ) ;
1091+
1092+ let key = DataKey :: ScheduledNotification ( id. clone ( ) ) ;
1093+ if env. storage ( ) . persistent ( ) . has ( & key) {
1094+ return Err ( Error :: AlreadyExists ) ;
1095+ }
1096+ // Validate ttl doesn't overflow.
1097+ created_at
1098+ . checked_add ( ttl)
1099+ . ok_or ( Error :: InvalidExpirationDuration ) ?;
1100+ }
1101+
1102+ // Persist and emit per-notification events.
1103+ for i in 0 ..count {
1104+ let ttl = ttl_seconds. get ( i) . unwrap ( ) ;
1105+ let id = ids. get ( i) . unwrap ( ) ;
1106+ let expires_at = created_at + ttl;
1107+
1108+ let notification = ScheduledNotification {
1109+ id : id. clone ( ) ,
1110+ creator : creator. clone ( ) ,
1111+ created_at,
1112+ expires_at,
1113+ } ;
1114+ let key = DataKey :: ScheduledNotification ( id. clone ( ) ) ;
1115+ env. storage ( ) . persistent ( ) . set ( & key, & notification) ;
1116+
1117+ append_audit_record ( & env, id. clone ( ) , AuditAction :: Created , creator. clone ( ) ) ;
1118+
1119+ NotificationScheduled {
1120+ creator : creator. clone ( ) ,
1121+ category : NotificationCategory :: Notification ,
1122+ priority : NOTIFICATION_PRIORITY ,
1123+ notification_id : id. clone ( ) ,
1124+ }
1125+ . publish ( & env) ;
1126+ }
1127+
1128+ // Summary event.
1129+ BatchNotificationsCreated {
1130+ creator : creator. clone ( ) ,
1131+ category : NotificationCategory :: Notification ,
1132+ priority : NOTIFICATION_PRIORITY ,
1133+ count,
1134+ ids,
1135+ }
1136+ . publish ( & env) ;
1137+
1138+ Ok ( ( ) )
1139+ }
1140+
1141+ // ============================================================================
1142+ // Audit Logging
1143+ // ============================================================================
1144+
1145+ /// Appends an immutable [`AuditRecord`] to the on-chain audit log and emits an
1146+ /// [`AuditRecordAppended`] event. The sequence number is auto-incremented.
1147+ fn append_audit_record (
1148+ env : & Env ,
1149+ notification_id : BytesN < 32 > ,
1150+ action : AuditAction ,
1151+ actor : Address ,
1152+ ) {
1153+ // Increment sequence counter.
1154+ let seq_key = DataKey :: AuditSeq ;
1155+ let seq: u64 = env
1156+ . storage ( )
1157+ . instance ( )
1158+ . get ( & seq_key)
1159+ . unwrap_or ( 0u64 )
1160+ + 1 ;
1161+ env. storage ( ) . instance ( ) . set ( & seq_key, & seq) ;
1162+
1163+ let timestamp = env. ledger ( ) . timestamp ( ) ;
1164+
1165+ let record = AuditRecord {
1166+ seq,
1167+ notification_id : notification_id. clone ( ) ,
1168+ action,
1169+ actor : actor. clone ( ) ,
1170+ timestamp,
1171+ } ;
1172+
1173+ // Append to the full log (used for full-scan / range queries).
1174+ let log_key = DataKey :: AuditLog ;
1175+ let mut log: Vec < AuditRecord > = env
1176+ . storage ( )
1177+ . persistent ( )
1178+ . get ( & log_key)
1179+ . unwrap_or ( Vec :: new ( env) ) ;
1180+ log. push_back ( record) ;
1181+ env. storage ( ) . persistent ( ) . set ( & log_key, & log) ;
1182+
1183+ AuditRecordAppended {
1184+ notification_id,
1185+ action,
1186+ category : NotificationCategory :: Notification ,
1187+ seq,
1188+ actor,
1189+ timestamp,
1190+ }
1191+ . publish ( env) ;
1192+ }
1193+
1194+ /// Returns all audit records in creation order.
1195+ ///
1196+ /// Records are immutable and append-only; this list can only grow over time.
1197+ pub fn get_audit_log ( env : Env ) -> Vec < AuditRecord > {
1198+ env. storage ( )
1199+ . persistent ( )
1200+ . get ( & DataKey :: AuditLog )
1201+ . unwrap_or ( Vec :: new ( & env) )
1202+ }
1203+
1204+ /// Returns all audit records for a specific notification identifier.
1205+ pub fn get_audit_records_for_notification (
1206+ env : Env ,
1207+ notification_id : BytesN < 32 > ,
1208+ ) -> Vec < AuditRecord > {
1209+ let log: Vec < AuditRecord > = env
1210+ . storage ( )
1211+ . persistent ( )
1212+ . get ( & DataKey :: AuditLog )
1213+ . unwrap_or ( Vec :: new ( & env) ) ;
1214+
1215+ let mut result: Vec < AuditRecord > = Vec :: new ( & env) ;
1216+ for record in log. iter ( ) {
1217+ if record. notification_id == notification_id {
1218+ result. push_back ( record) ;
1219+ }
1220+ }
1221+ result
1222+ }
1223+
1224+ /// Records a delivery attempt for a notification in the audit log.
1225+ ///
1226+ /// This is a permissionless write so any authorised service (an off-chain
1227+ /// relay, a keeper) can record that it attempted delivery.
1228+ pub fn record_delivery_attempt (
1229+ env : Env ,
1230+ notification_id : BytesN < 32 > ,
1231+ actor : Address ,
1232+ ) -> Result < ( ) , Error > {
1233+ actor. require_auth ( ) ;
1234+
1235+ if get_paused_status ( & env) {
1236+ return Err ( Error :: ContractPaused ) ;
1237+ }
1238+
1239+ append_audit_record ( & env, notification_id, AuditAction :: DeliveryAttempt , actor) ;
1240+ Ok ( ( ) )
1241+ }
1242+
1243+ /// Records a delivery failure for a notification in the audit log.
1244+ pub fn record_delivery_failure (
1245+ env : Env ,
1246+ notification_id : BytesN < 32 > ,
1247+ actor : Address ,
1248+ ) -> Result < ( ) , Error > {
1249+ actor. require_auth ( ) ;
1250+
1251+ if get_paused_status ( & env) {
1252+ return Err ( Error :: ContractPaused ) ;
1253+ }
1254+
1255+ append_audit_record ( & env, notification_id, AuditAction :: DeliveryFailed , actor) ;
1256+ Ok ( ( ) )
1257+ }
1258+
1259+ /// Records that the recipient acknowledged a notification.
1260+ pub fn record_acknowledgment (
1261+ env : Env ,
1262+ notification_id : BytesN < 32 > ,
1263+ actor : Address ,
1264+ ) -> Result < ( ) , Error > {
1265+ actor. require_auth ( ) ;
1266+
1267+ if get_paused_status ( & env) {
1268+ return Err ( Error :: ContractPaused ) ;
1269+ }
1270+
1271+ append_audit_record ( & env, notification_id, AuditAction :: Acknowledged , actor) ;
1272+ Ok ( ( ) )
1273+ }
0 commit comments