From 79c6593dae5488a94c0c1ec78e1e2e74f666f111 Mon Sep 17 00:00:00 2001 From: CleanDev-Fix <219162456+CleanDev-Fix@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:42:25 -0400 Subject: [PATCH] feat: add notification priority levels --- .../hello-world/src/autoshare_logic.rs | 13 ++- .../contracts/hello-world/src/base/events.rs | 28 +++++ .../contracts/hello-world/src/base/types.rs | 2 + .../src/tests/notification_test.rs | 108 ++++++++++++++++-- 4 files changed, 139 insertions(+), 12 deletions(-) diff --git a/contract/contracts/hello-world/src/autoshare_logic.rs b/contract/contracts/hello-world/src/autoshare_logic.rs index d392945..91940e5 100644 --- a/contract/contracts/hello-world/src/autoshare_logic.rs +++ b/contract/contracts/hello-world/src/autoshare_logic.rs @@ -1,7 +1,8 @@ use crate::base::errors::Error; use crate::base::events::{ AdminTransferred, AuthorizationFailure, AutoshareCreated, AutoshareUpdated, ContractPaused, - ContractUnpaused, GroupActivated, GroupDeactivated, NotificationCategory, Withdrawal, + ContractUnpaused, GroupActivated, GroupDeactivated, NotificationCategory, NotificationPriority, + Withdrawal, }; use crate::base::types::{AutoShareDetails, GroupMember, PaymentHistory}; use soroban_sdk::{contracttype, token, Address, BytesN, Env, String, Vec}; @@ -73,6 +74,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), @@ -108,6 +110,7 @@ pub fn create_autoshare( AutoshareCreated { creator: creator.clone(), + priority: details.priority, category: NotificationCategory::Group, id: id.clone(), } @@ -258,6 +261,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, action: String::from_str(env, action), } @@ -294,6 +298,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, new_admin, } @@ -318,6 +323,7 @@ pub fn pause(env: Env, admin: Address) -> Result<(), Error> { env.storage().persistent().set(&pause_key, &true); ContractPaused { + priority: NotificationPriority::High, category: NotificationCategory::Admin, } .publish(&env); @@ -337,6 +343,7 @@ pub fn unpause(env: Env, admin: Address) -> Result<(), Error> { env.storage().persistent().set(&pause_key, &false); ContractUnpaused { + priority: NotificationPriority::High, category: NotificationCategory::Admin, } .publish(&env); @@ -674,6 +681,7 @@ pub fn update_members( AutoshareUpdated { updater: caller, + priority: details.priority, category: NotificationCategory::Group, id: id.clone(), } @@ -710,6 +718,7 @@ pub fn deactivate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), GroupDeactivated { creator: caller, + priority: details.priority, category: NotificationCategory::Group, id: id.clone(), } @@ -746,6 +755,7 @@ pub fn activate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), E GroupActivated { creator: caller, + priority: details.priority, category: NotificationCategory::Group, id: id.clone(), } @@ -794,6 +804,7 @@ pub fn withdraw( Withdrawal { token, recipient, + priority: NotificationPriority::Critical, category: NotificationCategory::Financial, amount, } diff --git a/contract/contracts/hello-world/src/base/events.rs b/contract/contracts/hello-world/src/base/events.rs index 9c97285..5d18c54 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 @@ -32,6 +42,8 @@ pub struct AutoshareCreated { #[topic] pub creator: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub id: BytesN<32>, } @@ -40,6 +52,8 @@ pub struct AutoshareCreated { #[contractevent] #[derive(Clone)] pub struct ContractPaused { + #[topic] + pub priority: NotificationPriority, #[topic] pub category: NotificationCategory, } @@ -48,6 +62,8 @@ pub struct ContractPaused { #[contractevent] #[derive(Clone)] pub struct ContractUnpaused { + #[topic] + pub priority: NotificationPriority, #[topic] pub category: NotificationCategory, } @@ -59,6 +75,8 @@ pub struct AutoshareUpdated { #[topic] pub updater: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub id: BytesN<32>, } @@ -70,6 +88,8 @@ pub struct GroupDeactivated { #[topic] pub creator: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub id: BytesN<32>, } @@ -81,6 +101,8 @@ pub struct GroupActivated { #[topic] pub creator: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub id: BytesN<32>, } @@ -92,6 +114,8 @@ pub struct AdminTransferred { #[topic] pub old_admin: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub new_admin: Address, } @@ -105,6 +129,8 @@ pub struct Withdrawal { #[topic] pub recipient: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub amount: i128, } @@ -116,6 +142,8 @@ pub struct AuthorizationFailure { #[topic] pub caller: Address, #[topic] + pub priority: NotificationPriority, + #[topic] pub category: NotificationCategory, pub action: String, } 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 607ce0c..7f043fe 100644 --- a/contract/contracts/hello-world/src/tests/notification_test.rs +++ b/contract/contracts/hello-world/src/tests/notification_test.rs @@ -1,13 +1,12 @@ //! Tests for notification category metadata attached to emitted events. //! -//! 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: -//! - each action emits the expected category, and -//! - the change is backward compatible: the event name remains the first topic -//! and the previously defined topics/data are unchanged. - -use crate::base::events::NotificationCategory; +//! 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. + +use crate::base::events::{NotificationCategory, NotificationPriority}; use crate::test_utils::{create_test_group, setup_test_env}; use crate::AutoShareContractClient; @@ -42,6 +41,16 @@ fn category_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. fn latest_category(env: &soroban_sdk::Env) -> Option { @@ -50,6 +59,15 @@ fn latest_category(env: &soroban_sdk::Env) -> Option { NotificationCategory::try_from_val(env, &last).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(); @@ -69,6 +87,30 @@ fn test_created_event_has_group_category() { category_of(&test_env.env, "autoshare_created"), Some(NotificationCategory::Group) ); + 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); } #[test] @@ -98,6 +140,10 @@ fn test_updated_event_has_group_category() { category_of(&test_env.env, "autoshare_updated"), Some(NotificationCategory::Group) ); + assert_eq!( + priority_of(&test_env.env, "autoshare_updated"), + Some(NotificationPriority::Standard) + ); } #[test] @@ -121,12 +167,20 @@ fn test_deactivate_and_activate_events_have_group_category() { category_of(&test_env.env, "group_deactivated"), Some(NotificationCategory::Group) ); + assert_eq!( + priority_of(&test_env.env, "group_deactivated"), + Some(NotificationPriority::Standard) + ); client.activate_group(&id, &creator); assert_eq!( category_of(&test_env.env, "group_activated"), Some(NotificationCategory::Group) ); + assert_eq!( + priority_of(&test_env.env, "group_activated"), + Some(NotificationPriority::Standard) + ); } #[test] @@ -139,12 +193,20 @@ fn test_pause_and_unpause_events_have_admin_category() { category_of(&test_env.env, "contract_paused"), Some(NotificationCategory::Admin) ); + assert_eq!( + priority_of(&test_env.env, "contract_paused"), + Some(NotificationPriority::High) + ); client.unpause(&test_env.admin); assert_eq!( category_of(&test_env.env, "contract_unpaused"), Some(NotificationCategory::Admin) ); + assert_eq!( + priority_of(&test_env.env, "contract_unpaused"), + Some(NotificationPriority::High) + ); } #[test] @@ -158,6 +220,10 @@ fn test_admin_transfer_event_has_admin_category() { category_of(&test_env.env, "admin_transferred"), Some(NotificationCategory::Admin) ); + assert_eq!( + priority_of(&test_env.env, "admin_transferred"), + Some(NotificationPriority::High) + ); } #[test] @@ -183,6 +249,10 @@ fn test_withdrawal_event_has_financial_category() { category_of(&test_env.env, "withdrawal"), Some(NotificationCategory::Financial) ); + assert_eq!( + priority_of(&test_env.env, "withdrawal"), + Some(NotificationPriority::Critical) + ); } /// Models an off-chain subscriber that only wants a subset of categories. As @@ -225,6 +295,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. @@ -233,6 +307,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); @@ -243,6 +321,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 @@ -268,8 +350,8 @@ 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] category (new trailing topic) - assert_eq!(topics.len(), 3); + // [0] event name, [1] creator (unchanged), [2] priority, [3] category. + assert_eq!(topics.len(), 4); let name = Symbol::try_from_val(&test_env.env, &topics.get(0).unwrap()).unwrap(); assert_eq!(name, Symbol::new(&test_env.env, "autoshare_created")); @@ -277,8 +359,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); // Data payload is still the group id.