diff --git a/contract/contracts/hello-world/src/autoshare_logic.rs b/contract/contracts/hello-world/src/autoshare_logic.rs index 814e71f..cdc00a1 100644 --- a/contract/contracts/hello-world/src/autoshare_logic.rs +++ b/contract/contracts/hello-world/src/autoshare_logic.rs @@ -76,6 +76,7 @@ pub fn create_autoshare( id: id.clone(), name, creator: creator.clone(), + priority: NotificationPriority::Standard, usage_count, total_usages_paid: usage_count, members: Vec::new(&env), @@ -111,6 +112,7 @@ pub fn create_autoshare( AutoshareCreated { creator: creator.clone(), + priority: details.priority, category: NotificationCategory::Group, priority: NotificationPriority::Medium, id: id.clone(), @@ -262,6 +264,7 @@ pub fn initialize_admin(env: Env, admin: Address) { fn publish_authorization_failure(env: &Env, caller: &Address, action: &str) { AuthorizationFailure { caller: caller.clone(), + priority: NotificationPriority::High, category: NotificationCategory::Admin, priority: NotificationPriority::Critical, action: String::from_str(env, action), @@ -299,6 +302,7 @@ pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) -> R env.storage().persistent().set(&DataKey::Admin, &new_admin); AdminTransferred { old_admin: current_admin, + priority: NotificationPriority::High, category: NotificationCategory::Admin, priority: NotificationPriority::Critical, new_admin, @@ -324,6 +328,7 @@ pub fn pause(env: Env, admin: Address) -> Result<(), Error> { env.storage().persistent().set(&pause_key, &true); ContractPaused { + priority: NotificationPriority::High, category: NotificationCategory::Admin, priority: NotificationPriority::High, } @@ -344,6 +349,7 @@ pub fn unpause(env: Env, admin: Address) -> Result<(), Error> { env.storage().persistent().set(&pause_key, &false); ContractUnpaused { + priority: NotificationPriority::High, category: NotificationCategory::Admin, priority: NotificationPriority::High, } @@ -682,6 +688,7 @@ pub fn update_members( AutoshareUpdated { updater: caller, + priority: details.priority, category: NotificationCategory::Group, priority: NotificationPriority::Medium, id: id.clone(), @@ -719,6 +726,7 @@ pub fn deactivate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), GroupDeactivated { creator: caller, + priority: details.priority, category: NotificationCategory::Group, priority: NotificationPriority::Low, id: id.clone(), @@ -756,6 +764,7 @@ pub fn activate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), E GroupActivated { creator: caller, + priority: details.priority, category: NotificationCategory::Group, priority: NotificationPriority::Low, id: id.clone(), @@ -805,6 +814,7 @@ pub fn withdraw( Withdrawal { token, recipient, + priority: NotificationPriority::Critical, category: NotificationCategory::Financial, priority: NotificationPriority::High, amount, diff --git a/contract/contracts/hello-world/src/base/events.rs b/contract/contracts/hello-world/src/base/events.rs index 807be85..86104c6 100644 --- a/contract/contracts/hello-world/src/base/events.rs +++ b/contract/contracts/hello-world/src/base/events.rs @@ -1,5 +1,15 @@ use soroban_sdk::{contractevent, contracttype, Address, BytesN, String}; +/// Priority metadata attached to notifications emitted by the contract. +#[contracttype] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum NotificationPriority { + Low = 0, + Standard = 1, + High = 2, + Critical = 3, +} + /// High-level notification category attached to every emitted event. /// /// Off-chain consumers (listeners, indexers, dashboards) often only care about a @@ -62,6 +72,8 @@ pub struct AutoshareCreated { #[topic] pub creator: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, @@ -72,6 +84,8 @@ pub struct AutoshareCreated { #[contractevent] #[derive(Clone)] pub struct ContractPaused { + #[topic] + pub priority: NotificationPriority, #[topic] pub category: NotificationCategory, #[topic] @@ -82,6 +96,8 @@ pub struct ContractPaused { #[contractevent] #[derive(Clone)] pub struct ContractUnpaused { + #[topic] + pub priority: NotificationPriority, #[topic] pub category: NotificationCategory, #[topic] @@ -95,6 +111,8 @@ pub struct AutoshareUpdated { #[topic] pub updater: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, @@ -108,6 +126,8 @@ pub struct GroupDeactivated { #[topic] pub creator: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, @@ -121,6 +141,8 @@ pub struct GroupActivated { #[topic] pub creator: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, @@ -134,6 +156,8 @@ pub struct AdminTransferred { #[topic] pub old_admin: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, @@ -149,6 +173,8 @@ pub struct Withdrawal { #[topic] pub recipient: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, @@ -162,6 +188,8 @@ pub struct AuthorizationFailure { #[topic] pub caller: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, #[topic] pub priority: NotificationPriority, diff --git a/contract/contracts/hello-world/src/base/types.rs b/contract/contracts/hello-world/src/base/types.rs index a2d1d04..73abe7a 100644 --- a/contract/contracts/hello-world/src/base/types.rs +++ b/contract/contracts/hello-world/src/base/types.rs @@ -1,3 +1,4 @@ +use crate::base::events::NotificationPriority; use soroban_sdk::{contracttype, Address, BytesN, String, Vec}; #[contracttype] @@ -6,6 +7,7 @@ pub struct AutoShareDetails { pub id: BytesN<32>, pub name: String, pub creator: Address, + pub priority: NotificationPriority, pub usage_count: u32, pub total_usages_paid: u32, pub members: Vec, diff --git a/contract/contracts/hello-world/src/tests/notification_test.rs b/contract/contracts/hello-world/src/tests/notification_test.rs index 7cf68f8..f435f44 100644 --- a/contract/contracts/hello-world/src/tests/notification_test.rs +++ b/contract/contracts/hello-world/src/tests/notification_test.rs @@ -1,5 +1,10 @@ //! Tests for notification category metadata attached to emitted events. //! +//! Every event the contract publishes carries notification metadata so off-chain +//! consumers can route by category and urgency. These tests verify: +//! - each action emits the expected category and priority, and +//! - the change is backward compatible: the event name remains the first topic, +//! the category remains the trailing topic, and payload data is unchanged. //! Every event the contract publishes now carries a [`NotificationCategory`] as //! its trailing topic so off-chain consumers can subscribe to / filter by whole //! categories. These tests verify: @@ -58,6 +63,16 @@ fn priority_of(env: &soroban_sdk::Env, event_name: &str) -> Option Option { + let topics = topics_of(env, event_name)?; + if topics.len() < 2 { + return None; + } + let priority = topics.get(topics.len() - 2)?; + NotificationPriority::try_from_val(env, &priority).ok() +} + /// Returns the category of the most recently emitted event — i.e. the metadata a /// streaming consumer would read off the event as it arrives. /// @@ -73,6 +88,15 @@ fn latest_category(env: &soroban_sdk::Env) -> Option { NotificationCategory::try_from_val(env, &category_topic).ok() } +fn latest_priority(env: &soroban_sdk::Env) -> Option { + let (_addr, topics, _data) = env.events().all().last()?; + if topics.len() < 2 { + return None; + } + let priority = topics.get(topics.len() - 2)?; + NotificationPriority::try_from_val(env, &priority).ok() +} + #[test] fn test_created_event_has_group_category() { let test_env = setup_test_env(); @@ -94,6 +118,28 @@ fn test_created_event_has_group_category() { ); assert_eq!( priority_of(&test_env.env, "autoshare_created"), + Some(NotificationPriority::Standard) + ); +} + +#[test] +fn test_created_group_stores_standard_priority() { + let test_env = setup_test_env(); + let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); + let creator = test_env.users.get(0).unwrap().clone(); + let token = test_env.mock_tokens.get(0).unwrap().clone(); + + let id = create_test_group( + &test_env.env, + &test_env.autoshare_contract, + &creator, + &Vec::new(&test_env.env), + 1, + &token, + ); + + let details = client.get(&id); + assert_eq!(details.priority, NotificationPriority::Standard); Some(NotificationPriority::Medium) ); } @@ -127,6 +173,7 @@ fn test_updated_event_has_group_category() { ); assert_eq!( priority_of(&test_env.env, "autoshare_updated"), + Some(NotificationPriority::Standard) Some(NotificationPriority::Medium) ); } @@ -154,6 +201,7 @@ fn test_deactivate_and_activate_events_have_group_category() { ); assert_eq!( priority_of(&test_env.env, "group_deactivated"), + Some(NotificationPriority::Standard) Some(NotificationPriority::Low) ); @@ -164,6 +212,7 @@ fn test_deactivate_and_activate_events_have_group_category() { ); assert_eq!( priority_of(&test_env.env, "group_activated"), + Some(NotificationPriority::Standard) Some(NotificationPriority::Low) ); } @@ -207,6 +256,7 @@ fn test_admin_transfer_event_has_admin_category() { ); assert_eq!( priority_of(&test_env.env, "admin_transferred"), + Some(NotificationPriority::High) Some(NotificationPriority::Critical) ); } @@ -236,6 +286,7 @@ fn test_withdrawal_event_has_financial_category() { ); assert_eq!( priority_of(&test_env.env, "withdrawal"), + Some(NotificationPriority::Critical) Some(NotificationPriority::High) ); } @@ -280,6 +331,10 @@ fn test_events_can_be_filtered_by_category() { latest_category(&test_env.env), Some(NotificationCategory::Group) ); + assert_eq!( + latest_priority(&test_env.env), + Some(NotificationPriority::Standard) + ); route(); // Admin event -> skipped by this subscriber. @@ -288,6 +343,10 @@ fn test_events_can_be_filtered_by_category() { latest_category(&test_env.env), Some(NotificationCategory::Admin) ); + assert_eq!( + latest_priority(&test_env.env), + Some(NotificationPriority::High) + ); route(); client.unpause(&test_env.admin); @@ -298,6 +357,10 @@ fn test_events_can_be_filtered_by_category() { latest_category(&test_env.env), Some(NotificationCategory::Financial) ); + assert_eq!( + latest_priority(&test_env.env), + Some(NotificationPriority::Critical) + ); route(); assert_eq!(processed, 2); // Group + Financial @@ -493,6 +556,7 @@ fn test_created_event_backward_compatible_shape() { ); let topics = topics_of(&test_env.env, "autoshare_created").expect("event emitted"); + // [0] event name, [1] creator (unchanged), [2] priority, [3] category. // [0] event name, [1] creator (unchanged), [2] category (now second-to-last), // [3] priority (new trailing topic). assert_eq!(topics.len(), 4); @@ -503,8 +567,12 @@ fn test_created_event_backward_compatible_shape() { let topic_creator = Address::try_from_val(&test_env.env, &topics.get(1).unwrap()).unwrap(); assert_eq!(topic_creator, creator); + let priority = + NotificationPriority::try_from_val(&test_env.env, &topics.get(2).unwrap()).unwrap(); + assert_eq!(priority, NotificationPriority::Standard); + let category = - NotificationCategory::try_from_val(&test_env.env, &topics.get(2).unwrap()).unwrap(); + NotificationCategory::try_from_val(&test_env.env, &topics.get(3).unwrap()).unwrap(); assert_eq!(category, NotificationCategory::Group); // The newly added trailing topic is the priority.