Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion contract/contracts/hello-world/src/autoshare_logic.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -109,6 +110,7 @@ pub fn create_autoshare(
AutoshareCreated {
creator: creator.clone(),
category: NotificationCategory::Group,
priority: NotificationPriority::Medium,
id: id.clone(),
}
.publish(&env);
Expand Down Expand Up @@ -259,6 +261,7 @@ fn publish_authorization_failure(env: &Env, caller: &Address, action: &str) {
AuthorizationFailure {
caller: caller.clone(),
category: NotificationCategory::Admin,
priority: NotificationPriority::Critical,
action: String::from_str(env, action),
}
.publish(env);
Expand Down Expand Up @@ -295,6 +298,7 @@ pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) -> R
AdminTransferred {
old_admin: current_admin,
category: NotificationCategory::Admin,
priority: NotificationPriority::Critical,
new_admin,
}
.publish(&env);
Expand All @@ -319,6 +323,7 @@ pub fn pause(env: Env, admin: Address) -> Result<(), Error> {
env.storage().persistent().set(&pause_key, &true);
ContractPaused {
category: NotificationCategory::Admin,
priority: NotificationPriority::High,
}
.publish(&env);
Ok(())
Expand All @@ -338,6 +343,7 @@ pub fn unpause(env: Env, admin: Address) -> Result<(), Error> {
env.storage().persistent().set(&pause_key, &false);
ContractUnpaused {
category: NotificationCategory::Admin,
priority: NotificationPriority::High,
}
.publish(&env);
Ok(())
Expand Down Expand Up @@ -675,6 +681,7 @@ pub fn update_members(
AutoshareUpdated {
updater: caller,
category: NotificationCategory::Group,
priority: NotificationPriority::Medium,
id: id.clone(),
}
.publish(&env);
Expand Down Expand Up @@ -711,6 +718,7 @@ pub fn deactivate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(),
GroupDeactivated {
creator: caller,
category: NotificationCategory::Group,
priority: NotificationPriority::Low,
id: id.clone(),
}
.publish(&env);
Expand Down Expand Up @@ -747,6 +755,7 @@ pub fn activate_group(env: Env, id: BytesN<32>, caller: Address) -> Result<(), E
GroupActivated {
creator: caller,
category: NotificationCategory::Group,
priority: NotificationPriority::Low,
id: id.clone(),
}
.publish(&env);
Expand Down Expand Up @@ -795,6 +804,7 @@ pub fn withdraw(
token,
recipient,
category: NotificationCategory::Financial,
priority: NotificationPriority::High,
amount,
}
.publish(&env);
Expand Down
46 changes: 46 additions & 0 deletions contract/contracts/hello-world/src/base/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ pub enum NotificationCategory {
Financial = 2,
}

/// Severity level attached to every emitted event alongside its category.
///
/// Off-chain consumers (alerting, dashboards, paging) often route notifications
/// by priority rather than (or in addition to) category. Each event carries its
/// priority as a trailing, indexed event topic so consumers can subscribe to —
/// or page on — high-priority notifications without decoding the payload.
///
/// # Backward compatibility
///
/// The priority is published as the *last* topic of every event, after the
/// event name, the previously defined topics, and the category. Existing
/// listeners that only read the event name (the first topic), the prior topics,
/// or the category will continue to work unchanged: the extra trailing topic is
/// simply ignored by consumers that don't look for it.
#[contracttype]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum NotificationPriority {
/// Informational: routine lifecycle events. No action required.
Low = 0,
/// Standard: day-to-day operational events worth tracking.
Medium = 1,
/// Elevated: events the operator should review promptly.
High = 2,
/// Urgent: security-relevant or funds-moving events that demand
/// immediate attention (e.g. admin transfer, authorization failure).
Critical = 3,
}

/// Emitted when a new AutoShare group is created.
#[contractevent(data_format = "single-value")]
#[derive(Clone)]
Expand All @@ -33,6 +61,8 @@ pub struct AutoshareCreated {
pub creator: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub id: BytesN<32>,
}

Expand All @@ -42,6 +72,8 @@ pub struct AutoshareCreated {
pub struct ContractPaused {
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
}

/// Emitted when the contract is unpaused by the admin.
Expand All @@ -50,6 +82,8 @@ pub struct ContractPaused {
pub struct ContractUnpaused {
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
}

/// Emitted when an AutoShare group's member list is updated.
Expand All @@ -60,6 +94,8 @@ pub struct AutoshareUpdated {
pub updater: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub id: BytesN<32>,
}

Expand All @@ -71,6 +107,8 @@ pub struct GroupDeactivated {
pub creator: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub id: BytesN<32>,
}

Expand All @@ -82,6 +120,8 @@ pub struct GroupActivated {
pub creator: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub id: BytesN<32>,
}

Expand All @@ -93,6 +133,8 @@ pub struct AdminTransferred {
pub old_admin: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub new_admin: Address,
}

Expand All @@ -106,6 +148,8 @@ pub struct Withdrawal {
pub recipient: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub amount: i128,
}

Expand All @@ -117,5 +161,7 @@ pub struct AuthorizationFailure {
pub caller: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
pub action: String,
}
77 changes: 69 additions & 8 deletions contract/contracts/hello-world/src/tests/notification_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! - 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;
use crate::base::events::{NotificationCategory, NotificationPriority};
use crate::test_utils::{create_test_group, setup_test_env};
use crate::AutoShareContractClient;

Expand All @@ -34,20 +34,43 @@ fn topics_of(env: &soroban_sdk::Env, event_name: &str) -> Option<Vec<Val>> {
found
}

/// Extracts the notification category (the trailing topic) for the latest event
/// named `event_name`.
/// Extracts the notification category for the latest event named `event_name`.
///
/// Events now carry **two** trailing topics: the category (the previously-trailing
/// topic) followed by the priority (added with the `NotificationPriority` change).
/// This helper reads the *second-to-last* topic, i.e. the category, so existing
/// subscribers keyed off category keep working unchanged.
fn category_of(env: &soroban_sdk::Env, event_name: &str) -> Option<NotificationCategory> {
let topics = topics_of(env, event_name)?;
let n = topics.len();
if n < 2 {
return None;
}
let category_topic = topics.get(n - 2)?;
NotificationCategory::try_from_val(env, &category_topic).ok()
}

/// Extracts the notification priority (the trailing topic) for the latest event
/// named `event_name`.
fn priority_of(env: &soroban_sdk::Env, event_name: &str) -> Option<NotificationPriority> {
let topics = topics_of(env, event_name)?;
let last = topics.last()?;
NotificationCategory::try_from_val(env, &last).ok()
NotificationPriority::try_from_val(env, &last).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.
///
/// Categories are now the **second-to-last** topic (priority is the trailing
/// topic), so we read from the back accordingly.
fn latest_category(env: &soroban_sdk::Env) -> Option<NotificationCategory> {
let (_addr, topics, _data) = env.events().all().last()?;
let last = topics.last()?;
NotificationCategory::try_from_val(env, &last).ok()
let n = topics.len();
if n < 2 {
return None;
}
let category_topic = topics.get(n - 2)?;
NotificationCategory::try_from_val(env, &category_topic).ok()
}

#[test]
Expand All @@ -69,6 +92,10 @@ 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::Medium)
);
}

#[test]
Expand Down Expand Up @@ -98,6 +125,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::Medium)
);
}

#[test]
Expand All @@ -121,12 +152,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::Low)
);

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::Low)
);
}

#[test]
Expand All @@ -139,12 +178,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]
Expand All @@ -158,6 +205,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::Critical)
);
}

#[test]
Expand All @@ -183,6 +234,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::High)
);
}

/// Models an off-chain subscriber that only wants a subset of categories. As
Expand Down Expand Up @@ -268,8 +323,9 @@ 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] category (now second-to-last),
// [3] priority (new trailing topic).
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"));
Expand All @@ -281,6 +337,11 @@ fn test_created_event_backward_compatible_shape() {
NotificationCategory::try_from_val(&test_env.env, &topics.get(2).unwrap()).unwrap();
assert_eq!(category, NotificationCategory::Group);

// The newly added trailing topic is the priority.
let priority =
NotificationPriority::try_from_val(&test_env.env, &topics.get(3).unwrap()).unwrap();
assert_eq!(priority, NotificationPriority::Medium);

// Data payload is still the group id.
let data = test_env
.env
Expand Down
Loading