diff --git a/contract/contracts/hello-world/src/autoshare_logic.rs b/contract/contracts/hello-world/src/autoshare_logic.rs index af2fd88..75c8dc7 100644 --- a/contract/contracts/hello-world/src/autoshare_logic.rs +++ b/contract/contracts/hello-world/src/autoshare_logic.rs @@ -1,5 +1,10 @@ use crate::base::errors::Error; use crate::base::events::{ + AdminTransferred, AuthorizationFailure, AutoshareCreated, AutoshareUpdated, CategoryRegistered, + ContractPaused, ContractUnpaused, GroupActivated, GroupDeactivated, NotificationCategory, + NotificationDelivered, NotificationExpired, NotificationExtended, NotificationLimitsConfigured, + NotificationPriority, NotificationRecalled, NotificationRevoked, NotificationScheduled, + ScheduledNotificationCancelled, Withdrawal, AdminTransferred, AuthorizationFailure, AutoshareCreated, AutoshareUpdated, ContractPaused, ContractUnpaused, GroupActivated, GroupDeactivated, NotificationAcknowledged, NotificationCategory, NotificationExpired, NotificationPriority, NotificationRevoked, @@ -1013,6 +1018,10 @@ pub fn schedule_notification( expires_at, revoked_by: None, revoked_at: None, + delivered: false, + delivered_at: None, + recalled_by: None, + recalled_at: None, title, }; env.storage().persistent().set(&key, ¬ification); @@ -1276,6 +1285,110 @@ pub fn batch_schedule_notifications( /// /// Revoked notifications maintain their state for transparency and auditing: /// they can still be queried but cannot be cancelled or expired. +pub fn confirm_notification_delivery( + env: Env, + notification_id: BytesN<32>, + caller: Address, +) -> Result<(), Error> { + caller.require_auth(); + + if get_paused_status(&env) { + return Err(Error::ContractPaused); + } + + let key = DataKey::ScheduledNotification(notification_id.clone()); + let mut notification = load_notification(&env, ¬ification_id).ok_or(Error::NotFound)?; + + if is_revoked(¬ification) { + return Err(Error::NotificationRevoked); + } + + if is_expired(&env, ¬ification) { + return Err(Error::NotificationExpired); + } + + if notification.delivered { + return Err(Error::NotificationDelivered); + } + + let admin = get_admin(env.clone()).ok(); + let is_creator = caller == notification.creator; + let is_admin = admin.as_ref().map_or(false, |a| caller == *a); + + if !is_creator && !is_admin { + return Err(Error::Unauthorized); + } + + let delivered_at = env.ledger().timestamp(); + notification.delivered = true; + notification.delivered_at = Some(delivered_at); + + env.storage().persistent().set(&key, ¬ification); + + NotificationDelivered { + notification_id, + delivered_by: caller, + category: NotificationCategory::Notification, + priority: NotificationPriority::High, + delivered_at, + } + .publish(&env); + + Ok(()) +} + +pub fn recall_notification( + env: Env, + notification_id: BytesN<32>, + caller: Address, +) -> Result<(), Error> { + caller.require_auth(); + + if get_paused_status(&env) { + return Err(Error::ContractPaused); + } + + let key = DataKey::ScheduledNotification(notification_id.clone()); + let mut notification = load_notification(&env, ¬ification_id).ok_or(Error::NotFound)?; + + if is_revoked(¬ification) { + return Err(Error::NotificationRevoked); + } + + if is_expired(&env, ¬ification) { + return Err(Error::NotificationExpired); + } + + if notification.delivered { + return Err(Error::NotificationDelivered); + } + + let admin = get_admin(env.clone()).ok(); + let is_creator = caller == notification.creator; + let is_admin = admin.as_ref().map_or(false, |a| caller == *a); + + if !is_creator && !is_admin { + return Err(Error::Unauthorized); + } + + let recalled_at = env.ledger().timestamp(); + notification.recalled_by = Some(caller.clone()); + notification.recalled_at = Some(recalled_at); + + env.storage().persistent().set(&key, ¬ification); + + NotificationRecalled { + notification_id, + recalled_by: caller, + category: NotificationCategory::Notification, + priority: NotificationPriority::High, + recalled_at, + } + .publish(&env); + + Ok(()) +} + pub fn revoke_notification( env: Env, notification_id: BytesN<32>, diff --git a/contract/contracts/hello-world/src/base/errors.rs b/contract/contracts/hello-world/src/base/errors.rs index 5cf99ca..4de5713 100644 --- a/contract/contracts/hello-world/src/base/errors.rs +++ b/contract/contracts/hello-world/src/base/errors.rs @@ -68,5 +68,8 @@ pub enum Error { NotAuthorizedToAcknowledge = 29, AlreadyRevoked = 29, /// Triggered when an invalid limit configuration is provided. + InvalidLimit = 29, + /// Triggered when a notification has already been delivered and cannot be recalled. + NotificationDelivered = 30, InvalidLimit = 30, } diff --git a/contract/contracts/hello-world/src/base/events.rs b/contract/contracts/hello-world/src/base/events.rs index 4a916d2..b4c5416 100644 --- a/contract/contracts/hello-world/src/base/events.rs +++ b/contract/contracts/hello-world/src/base/events.rs @@ -201,6 +201,36 @@ pub struct ScheduledNotificationCancelled { pub notification_id: BytesN<32>, } +/// Emitted when a notification is confirmed as delivered to its intended recipient. +#[contractevent(data_format = "single-value")] +#[derive(Clone)] +pub struct NotificationDelivered { + #[topic] + pub notification_id: BytesN<32>, + #[topic] + pub delivered_by: Address, + #[topic] + pub category: NotificationCategory, + #[topic] + pub priority: NotificationPriority, + pub delivered_at: u64, +} + +/// Emitted when a sender recalls a scheduled notification before delivery confirmation. +#[contractevent(data_format = "single-value")] +#[derive(Clone)] +pub struct NotificationRecalled { + #[topic] + pub notification_id: BytesN<32>, + #[topic] + pub recalled_by: Address, + #[topic] + pub category: NotificationCategory, + #[topic] + pub priority: NotificationPriority, + pub recalled_at: u64, +} + /// Emitted when a notification is scheduled on-chain with a bounded lifetime. /// /// Off-chain consumers can use this to track the notification's existence and diff --git a/contract/contracts/hello-world/src/base/types.rs b/contract/contracts/hello-world/src/base/types.rs index 0e94870..21bd86d 100644 --- a/contract/contracts/hello-world/src/base/types.rs +++ b/contract/contracts/hello-world/src/base/types.rs @@ -43,6 +43,14 @@ pub struct ScheduledNotification { pub revoked_by: Option
, /// Ledger timestamp (seconds) at which the notification was revoked, if revoked. pub revoked_at: Option