Skip to content
Open
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
6 changes: 6 additions & 0 deletions contract/contracts/hello-world/src/base/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,10 @@ pub enum Error {
/// Triggered when a notification has already been delivered and cannot be recalled.
NotificationDelivered = 30,
InvalidLimit = 30,
/// Triggered when referencing a template ID that does not exist in the registry.
TemplateNotFound = 31,
/// Triggered when a template name exceeds the maximum allowed length.
TemplateNameTooLong = 32,
/// Triggered when a template content field is empty.
TemplateContentEmpty = 33,
}
41 changes: 41 additions & 0 deletions contract/contracts/hello-world/src/base/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,44 @@ pub struct NotificationAcknowledged {
pub priority: NotificationPriority,
pub timestamp: u64,
}

// ============================================================================
// Template Registry (Issue #352)
// ============================================================================

/// Emitted when a new notification template is registered on-chain.
///
/// Off-chain indexers key off `template_id` to track registered templates.
/// The owner's address is published as an indexed topic for creator-based
/// filtering.
#[contractevent(data_format = "single-value")]
#[derive(Clone)]
pub struct TemplateRegistered {
/// Address that created and owns the template.
#[topic]
pub owner: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
/// Unique identifier of the newly registered template.
pub template_id: BytesN<32>,
}

/// Emitted when an existing notification template is updated by its owner.
///
/// Off-chain indexers should use `template_id` to invalidate any cached
/// versions of the template and re-fetch the updated content.
#[contractevent(data_format = "single-value")]
#[derive(Clone)]
pub struct TemplateUpdated {
/// Address that owns (and updated) the template.
#[topic]
pub owner: Address,
#[topic]
pub category: NotificationCategory,
#[topic]
pub priority: NotificationPriority,
/// Unique identifier of the updated template.
pub template_id: BytesN<32>,
}
26 changes: 26 additions & 0 deletions contract/contracts/hello-world/src/base/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ pub struct AuditRecord {
pub timestamp: u64,
}

// ============================================================================
// Template Registry (Issue #352)
// ============================================================================

/// A reusable notification template stored on-chain.
///
/// Senders can register a template once and reference its `id` in subsequent
/// notifications instead of resending identical payload data, reducing
/// transaction size and gas cost.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NotificationTemplate {
/// Unique identifier for this template.
pub id: BytesN<32>,
/// Address that created and owns the template. Only the owner may update it.
pub owner: Address,
/// Human-readable name for the template (max 100 characters).
pub name: String,
/// The reusable notification content/payload stored with this template.
pub content: String,
/// Ledger timestamp (seconds) when the template was first registered.
pub created_at: u64,
/// Ledger timestamp (seconds) when the template was last updated, if ever.
pub updated_at: Option<u64>,
}

/// Protocol-level configurable limits for notifications.
/// Allows administrators to set boundaries on notification sizes,
/// expiration periods, and batch operation sizes.
Expand Down
53 changes: 53 additions & 0 deletions contract/contracts/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod interfaces {
mod autoshare_logic;
mod preferences_logic;
mod reputation_logic;
mod template_registry_logic;

#[cfg(test)]
pub mod mock_token;
Expand Down Expand Up @@ -562,6 +563,55 @@ impl AutoShareContract {
reputation_logic::get_reputation_tier(&env, &sender).unwrap_or(0)
}

// ============================================================================
// Template Registry (Issue #352)
// ============================================================================

/// Registers a new reusable notification template on-chain.
///
/// `id` must be unique — reverts with `AlreadyExists` if it is already taken.
/// `creator` becomes the sole owner and is the only address that can update it.
/// Emits a `TemplateRegistered` event on success.
pub fn register_template(
env: Env,
id: BytesN<32>,
creator: Address,
name: String,
content: String,
) {
template_registry_logic::register_template(env, id, creator, name, content).unwrap();
}

/// Updates the `name` and `content` of an existing template.
///
/// Only the original owner (creator) may call this function.
/// Reverts with `Unauthorized` for any other caller.
/// Reverts with `TemplateNotFound` if `id` is not registered.
/// Emits a `TemplateUpdated` event on success.
pub fn update_template(
env: Env,
id: BytesN<32>,
caller: Address,
name: String,
content: String,
) {
template_registry_logic::update_template(env, id, caller, name, content).unwrap();
}

/// Returns the full `NotificationTemplate` record for `id`.
///
/// Reverts with `TemplateNotFound` if `id` is not registered.
pub fn get_template(env: Env, id: BytesN<32>) -> base::types::NotificationTemplate {
template_registry_logic::get_template(env, id).unwrap()
}

/// Returns `true` if a template is registered under `id`, `false` otherwise.
///
/// This is a pure view function and never reverts.
pub fn template_exists(env: Env, id: BytesN<32>) -> bool {
template_registry_logic::template_exists(env, id)
}

// ============================================================================
// Schema Version Tracking (Issue #309)
// ============================================================================
Expand Down Expand Up @@ -659,4 +709,7 @@ mod tests {

#[path = "../tests/access_log_test.rs"]
mod access_log_test;

#[path = "../tests/template_registry_test.rs"]
mod template_registry_test;
}
217 changes: 217 additions & 0 deletions contract/contracts/hello-world/src/template_registry_logic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/// Template Registry Logic — Issue #352
///
/// Provides register/update/query operations for reusable notification templates.
/// Templates are stored in persistent storage keyed by a caller-supplied
/// `BytesN<32>` ID. Only the original owner (creator) of a template may update
/// it. Attempting to reference a non-existent template ID reverts with a clear
/// error.
use crate::base::errors::Error;
use crate::base::events::{
AuthorizationFailure, NotificationCategory, NotificationPriority, TemplateRegistered,
TemplateUpdated,
};
use crate::base::types::NotificationTemplate;
use soroban_sdk::{contracttype, Address, BytesN, Env, String};

// ============================================================================
// Limits
// ============================================================================

/// Maximum allowed byte-length for a template name.
const MAX_TEMPLATE_NAME_LEN: u32 = 100;

// ============================================================================
// Storage keys
// ============================================================================

/// Persistent-storage key variants used by the template registry.
///
/// Keys are separate from `DataKey` in `autoshare_logic` to avoid the two
/// enums merging into a single namespace and to keep each module's storage
/// footprint explicit. Both enums live in persistent storage and are
/// distinguished by their variant discriminant at the serialisation layer.
#[contracttype]
pub enum TemplateKey {
/// Full `NotificationTemplate` record keyed by template ID.
Template(BytesN<32>),
}

// ============================================================================
// Public API
// ============================================================================

/// Register a new notification template on-chain.
///
/// The caller becomes the owner of the template and is the only address
/// authorised to update it in the future.
///
/// # Arguments
/// * `id` – caller-chosen unique identifier; reverts with `AlreadyExists`
/// if it is already taken.
/// * `creator` – address that will own the template (must authorise the call).
/// * `name` – human-readable label (max 100 bytes); reverts with
/// `TemplateNameTooLong` if exceeded.
/// * `content` – notification payload/body; must not be empty, reverts with
/// `TemplateContentEmpty` otherwise.
///
/// # Errors
/// - `AlreadyExists` – a template with this `id` is already registered.
/// - `TemplateNameTooLong` – `name` exceeds `MAX_TEMPLATE_NAME_LEN` bytes.
/// - `TemplateContentEmpty` – `content` is an empty string.
///
/// # Events
/// Emits [`TemplateRegistered`] on success.
pub fn register_template(
env: Env,
id: BytesN<32>,
creator: Address,
name: String,
content: String,
) -> Result<(), Error> {
creator.require_auth();

let key = TemplateKey::Template(id.clone());

// Prevent overwriting an existing template.
if env.storage().persistent().has(&key) {
return Err(Error::AlreadyExists);
}

validate_name(&name)?;
validate_content(&content)?;

let template = NotificationTemplate {
id: id.clone(),
owner: creator.clone(),
name,
content,
created_at: env.ledger().timestamp(),
updated_at: None,
};

env.storage().persistent().set(&key, &template);

TemplateRegistered {
owner: creator,
category: NotificationCategory::Notification,
priority: NotificationPriority::Medium,
template_id: id,
}
.publish(&env);

Ok(())
}

/// Update the `name` and/or `content` of an existing template.
///
/// Only the original owner of the template is permitted to call this function.
/// Any other caller reverts with `Unauthorized`.
///
/// # Arguments
/// * `id` – identifier of the template to update.
/// * `caller` – must match the template's stored `owner`; must authorise the call.
/// * `name` – replacement name (max 100 bytes).
/// * `content`– replacement content; must not be empty.
///
/// # Errors
/// - `TemplateNotFound` – no template is registered under `id`.
/// - `Unauthorized` – `caller` is not the template owner.
/// - `TemplateNameTooLong` – replacement `name` exceeds `MAX_TEMPLATE_NAME_LEN`.
/// - `TemplateContentEmpty` – replacement `content` is empty.
///
/// # Events
/// Emits [`TemplateUpdated`] on success.
pub fn update_template(
env: Env,
id: BytesN<32>,
caller: Address,
name: String,
content: String,
) -> Result<(), Error> {
caller.require_auth();

let key = TemplateKey::Template(id.clone());

let mut template: NotificationTemplate = env
.storage()
.persistent()
.get(&key)
.ok_or(Error::TemplateNotFound)?;

// Only the original owner may update.
if template.owner != caller {
publish_authorization_failure(&env, &caller, "update_template");
return Err(Error::Unauthorized);
}

validate_name(&name)?;
validate_content(&content)?;

template.name = name;
template.content = content;
template.updated_at = Some(env.ledger().timestamp());

env.storage().persistent().set(&key, &template);

TemplateUpdated {
owner: caller,
category: NotificationCategory::Notification,
priority: NotificationPriority::Medium,
template_id: id,
}
.publish(&env);

Ok(())
}

/// Return the full record for a registered template.
///
/// # Errors
/// - `TemplateNotFound` – no template is registered under `id`.
pub fn get_template(env: Env, id: BytesN<32>) -> Result<NotificationTemplate, Error> {
let key = TemplateKey::Template(id);
env.storage()
.persistent()
.get(&key)
.ok_or(Error::TemplateNotFound)
}

/// Return `true` if a template with the given `id` exists, `false` otherwise.
///
/// This is a pure view function and never reverts.
pub fn template_exists(env: Env, id: BytesN<32>) -> bool {
let key = TemplateKey::Template(id);
env.storage().persistent().has(&key)
}

// ============================================================================
// Validation helpers
// ============================================================================

fn validate_name(name: &String) -> Result<(), Error> {
if name.len() > MAX_TEMPLATE_NAME_LEN {
return Err(Error::TemplateNameTooLong);
}
Ok(())
}

fn validate_content(content: &String) -> Result<(), Error> {
if content.len() == 0 {
return Err(Error::TemplateContentEmpty);
}
Ok(())
}

// ============================================================================
// Auth helpers
// ============================================================================

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);
}
Loading
Loading