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
18 changes: 17 additions & 1 deletion contract/contracts/hello-world/src/autoshare_logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ pub fn create_autoshare(
return Err(Error::ContractPaused);
}

if !is_category_registered(env.clone(), NotificationCategory::Group) {
return Err(Error::CategoryNotRegistered);
}

let key = DataKey::AutoShare(id.clone());

// Check if it already exists to prevent overwriting
Expand Down Expand Up @@ -330,7 +334,11 @@ fn seed_default_categories(env: &Env) {
return;
}

let categories: Vec<NotificationCategory> = Vec::new(env);
let mut categories: Vec<NotificationCategory> = Vec::new(env);
categories.push_back(NotificationCategory::Group);
categories.push_back(NotificationCategory::Admin);
categories.push_back(NotificationCategory::Financial);
categories.push_back(NotificationCategory::Notification);
env.storage().persistent().set(&key, &categories);
}

Expand Down Expand Up @@ -1200,6 +1208,10 @@ pub fn schedule_notification(
return Err(Error::ContractPaused);
}

if !is_category_registered(env.clone(), NotificationCategory::Notification) {
return Err(Error::CategoryNotRegistered);
}

if ttl_seconds == 0 {
return Err(Error::InvalidExpirationDuration);
}
Expand Down Expand Up @@ -1390,6 +1402,10 @@ pub fn batch_schedule_notifications(
return Err(Error::ContractPaused);
}

if !is_category_registered(env.clone(), NotificationCategory::Notification) {
return Err(Error::CategoryNotRegistered);
}

let count = ids.len();

// Must have at least one notification.
Expand Down
18 changes: 9 additions & 9 deletions contract/contracts/hello-world/src/base/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,19 @@ pub enum Error {
/// Triggered when the caller is not authorized to revoke a notification.
NotAuthorizedToRevoke = 28,
/// Triggered when attempting to revoke a notification that is already revoked.
AlreadyRevoked = 28,
AlreadyRevoked = 29,
/// Triggered when a transfer is attempted to the zero address.
ZeroAddressTransfer = 29,
ZeroAddressTransfer = 30,
/// Triggered when `accept_ownership` is called but no pending transfer is queued.
NoPendingOwnershipTransfer = 30,
NoPendingOwnershipTransfer = 31,
/// Triggered when a caller other than the pending owner calls `accept_ownership`.
NotPendingOwner = 31,
NotPendingOwner = 32,
/// Triggered when the caller is not authorized to acknowledge a notification.
NotAuthorizedToAcknowledge = 29,
AlreadyRevoked = 29,
NotAuthorizedToAcknowledge = 33,
/// Triggered when an invalid limit configuration is provided.
InvalidLimit = 29,
InvalidLimit = 34,
/// Triggered when a notification has already been delivered and cannot be recalled.
NotificationDelivered = 30,
InvalidLimit = 30,
NotificationDelivered = 35,
/// Triggered when a notification category is not registered.
CategoryNotRegistered = 36,
}
2 changes: 2 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,8 @@ pub enum NotificationCategory {
Financial = 2,
/// Scheduled notification operations: scheduling, expiry, cancellation.
Notification = 3,
/// System testing category
System = 4,
}

/// Severity level attached to every emitted event alongside its category.
Expand Down
3 changes: 2 additions & 1 deletion contract/contracts/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ impl AutoShareContract {
/// Acknowledges multiple scheduled notifications in a single batch.
pub fn acknowledge_notifications(env: Env, caller: Address, notification_ids: Vec<BytesN<32>>) {
autoshare_logic::acknowledge_notifications(env, caller, notification_ids).unwrap();
}
/// Extends the expiration period of a scheduled notification by `extension_seconds`.
///
/// Only the notification creator or the contract admin can extend it.
Expand Down Expand Up @@ -646,7 +647,7 @@ impl AutoShareContract {

#[cfg(test)]
pub mod test_utils {
#[path = "tests/test_utils.rs"]
#[path = "../tests/test_utils.rs"]
mod inner;
pub use inner::*;
}
Expand Down
31 changes: 15 additions & 16 deletions contract/contracts/hello-world/src/tests/category_registry_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,34 @@ fn latest_event_topics(env: &soroban_sdk::Env, event_name: &str) -> Option<sorob
}

#[test]
fn test_default_registry_starts_empty() {
fn test_default_registry_has_defaults() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);

let categories = client.get_registered_categories();
assert_eq!(categories.len(), 0);
assert!(!client.is_category_registered(&NotificationCategory::Group));
assert_eq!(categories.len(), 4);
assert!(client.is_category_registered(&NotificationCategory::Group));
}

#[test]
fn test_admin_can_register_category() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);

client.register_category(&test_env.admin, &NotificationCategory::Group);
client.register_category(&test_env.admin, &NotificationCategory::System);

assert!(client.is_category_registered(&NotificationCategory::Group));
assert!(client.is_category_registered(&NotificationCategory::System));
let categories = client.get_registered_categories();
assert_eq!(categories.len(), 1);
assert_eq!(categories.get(0).unwrap(), NotificationCategory::Group);
assert_eq!(categories.len(), 5);
assert_eq!(categories.get(4).unwrap(), NotificationCategory::System);
}

#[test]
fn test_register_category_emits_event() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);

client.register_category(&test_env.admin, &NotificationCategory::Admin);
client.register_category(&test_env.admin, &NotificationCategory::System);

let topics = latest_event_topics(&test_env.env, "category_registered")
.expect("category_registered event");
Expand All @@ -60,7 +60,7 @@ fn test_register_category_emits_event() {
);
assert_eq!(
NotificationCategory::try_from_val(&test_env.env, &topics.get(2).unwrap()).unwrap(),
NotificationCategory::Admin
NotificationCategory::System
);
assert_eq!(
NotificationPriority::try_from_val(&test_env.env, &topics.get(3).unwrap()).unwrap(),
Expand All @@ -74,8 +74,8 @@ fn test_duplicate_category_registration_rejected() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);

client.register_category(&test_env.admin, &NotificationCategory::Financial);
client.register_category(&test_env.admin, &NotificationCategory::Financial);
client.register_category(&test_env.admin, &NotificationCategory::System);
client.register_category(&test_env.admin, &NotificationCategory::System);
}

#[test]
Expand All @@ -85,20 +85,19 @@ fn test_non_admin_cannot_register_category() {
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);
let non_admin = Address::generate(&test_env.env);

client.register_category(&non_admin, &NotificationCategory::Notification);
client.register_category(&non_admin, &NotificationCategory::System);
}

#[test]
fn test_registry_queries_multiple_categories() {
let test_env = setup_test_env();
let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract);

client.register_category(&test_env.admin, &NotificationCategory::Group);
client.register_category(&test_env.admin, &NotificationCategory::Notification);
client.register_category(&test_env.admin, &NotificationCategory::System);

let categories = client.get_registered_categories();
assert_eq!(categories.len(), 2);
assert_eq!(categories.len(), 5);
assert!(client.is_category_registered(&NotificationCategory::Group));
assert!(client.is_category_registered(&NotificationCategory::Notification));
assert!(!client.is_category_registered(&NotificationCategory::Admin));
assert!(client.is_category_registered(&NotificationCategory::System));
}
2 changes: 0 additions & 2 deletions dashboard/src/pages/NotificationSearchPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,6 @@ describe('NotificationSearchPage loading skeletons', () => {
expect(screen.getByLabelText(/filter by delivery status/i)).toHaveValue('');
expect(screen.getByLabelText(/filter from date/i)).toHaveValue('');
expect(screen.queryByRole('button', { name: /clear all filters/i })).not.toBeInTheDocument();
});
});

describe('searchNotifications query params', () => {
const originalFetch = global.fetch;
Expand Down
1 change: 0 additions & 1 deletion dashboard/src/pages/NotificationSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const NOTIFICATION_TYPE_OPTIONS = [
];
const API_BASE = getEventsApiBaseUrl();

const STATUS_OPTIONS = ['', 'PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'CANCELLED', 'PROCESSED'];

export function NotificationSearchPage() {
const [query, setQuery] = useState('');
Expand Down
116 changes: 116 additions & 0 deletions listener/fix-merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const fs = require('fs');
const path = require('path');

const listenerDir = path.join(__dirname, 'src');

function fixFile(relativePath, replacer) {
const filePath = path.join(listenerDir, relativePath);
if (!fs.existsSync(filePath)) {
console.log('File not found:', relativePath);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
let newContent = replacer(content);
if (newContent !== content) {
fs.writeFileSync(filePath, newContent, 'utf8');
console.log('Fixed', relativePath);
}
}

// 1. config.ts
fixFile('config.ts', (c) => {
let lines = c.split('\n');
if (lines[0].includes('import { Config') && lines[1].includes('import { Config')) {
lines.splice(0, 1); // remove duplicate line 1
}
return lines.join('\n');
});

// 2. index.ts
fixFile('index.ts', (c) => {
let res = c;
res = res.replace(/ notificationAPI,\n templateService,\n rateLimit:/g, ' rateLimit:');
return res;
});

// 3. batch-validation-service.ts
fixFile('services/batch-validation-service.ts', (c) => {
return c.replace(/ error: validation.error \?\? '',\n error: validation.error \?\? 'Invalid notification item',/g, " error: validation.error ?? 'Invalid notification item',");
});

// 4. event-processing-queue.ts
fixFile('services/event-processing-queue.ts', (c) => {
return c.replace(/ \/\/ Metrics\n private metrics = \{\n totalEnqueued: 0,\n totalProcessed: 0,\n totalSucceeded: 0,\n totalFailed: 0,\n processingTimes: \[\] as number\[\],\n \};\n\n \/\/ Metrics/g, ' // Metrics');
});

// 5. notification-retry-queue.ts
fixFile('services/notification-retry-queue.ts', (c) => {
let res = c.replace(/ \/\/ Metrics\n private metrics = \{\n totalEnqueued: 0,\n totalProcessed: 0,\n totalSucceeded: 0,\n totalFailed: 0,\n processingTimes: \[\] as number\[\],\n \};\n\n \/\/ Metrics/g, ' // Metrics');
res = res.replace(/ this.queue.push\(\{ event, contractConfig, retryCount: 0, nextRetryAt, requestId \}\);\n this.metrics.totalEnqueued\+\+;\n this.queue.push\(\{ event, contractConfig, retryCount: 0, nextRetryAt, requestId, priority, enqueuedAt: Date.now\(\) \}\);/g, ' this.metrics.totalEnqueued++;\n this.queue.push({ event, contractConfig, retryCount: 0, nextRetryAt, requestId, priority, enqueuedAt: Date.now() });');
return res;
});

// 6. scheduled-notification-repository.ts
fixFile('services/scheduled-notification-repository.ts', (c) => {
return c.replace(/ payload: decompressPayload\(row\.payload\),\n payload: row\.payload,/g, ' payload: decompressPayload(row.payload),');
});

// 7. notification-api.ts
fixFile('services/notification-api.ts', (c) => {
return c.replace(/validatePayloadSize\(input\.payload, this\.maxPayloadSizeBytes\);/g, '// validatePayloadSize(input.payload, this.maxPayloadSizeBytes);');
});

// 8. database/migration-system.ts
fixFile('database/migration-system.ts', (c) => {
return c.replace(/const rows = await this\.db\.all<\{ id: string \}>\(\n 'SELECT id FROM migrations ORDER BY applied_at'\n \);\n return rows\.map\(\(row\) => row\.id\);/g, "const rows = await this.db.all<{ id: string }>(\n 'SELECT id FROM migrations ORDER BY applied_at'\n );\n return (rows as any[]).map((row) => row.id);");
});

// 9. template-repository.ts duplicate functions
fixFile('store/event-registry.ts', (c) => {
return c.replace(/ setTtlMs\(ttlMs: number\): void \{\n this\.ttlMs = ttlMs;\n \}\n\n startCleanup\(intervalMs = 60_000\): void \{\n if \(this\.cleanupTimer\) return;\n this\.cleanupTimer = setInterval\(\(\) => this\.pruneExpired\(\), intervalMs\);\n \}\n\n setTtlMs\(ms: number\): void \{\n this\.ttlMs = ms;\n \}/g, ' setTtlMs(ms: number): void {\n this.ttlMs = ms;\n }\n\n startCleanup(intervalMs = 60_000): void {\n if (this.cleanupTimer) return;\n this.cleanupTimer = setInterval(() => this.pruneExpired(), intervalMs);\n }');
});

// 10. notification-template-service.ts duplicate functions
fixFile('services/notification-template-service.ts', (c) => {
let res = c;
res = res.replace(/ async delete\(templateId: string\): Promise<void> \{\n await this\.repository\.delete\(templateId\);\n this\.cache\.invalidate\(templateId\);\n \}/g, '');
res = res.replace(/ async getAll\(\): Promise<NotificationTemplate\[\]> \{\n return this\.repository\.listAll\(\);\n \}/g, '');
return res;
});

// 11. Rename duplicated NotificationTemplate types
fixFile('types/notification-template.ts', (c) => {
let res = c;
res = res.replace(/export interface NotificationTemplateRow \{\n id: string;/g, 'export interface NotificationTemplateRowOld {\n id: string;');
res = res.replace(/export interface NotificationTemplate \{\n id: string;/g, 'export interface NotificationTemplateOld {\n id: string;');
res = res.replace(/export interface CreateNotificationTemplateInput \{/g, 'export interface CreateNotificationTemplateInputOld {');
res = res.replace(/export interface UpdateNotificationTemplateInput \{/g, 'export interface UpdateNotificationTemplateInputOld {');
return res;
});

// 12. Update files using the OLD template types
const oldFiles = [
'services/notification-template-service.ts',
'services/notification-template-repository.ts',
'api/template-routes.ts',
'api/templates-api.test.ts',
'services/notification-template-service.test.ts',
'api/events-server.ts'
];

oldFiles.forEach(f => {
fixFile(f, (c) => {
let res = c;
// Replace imports and usages
res = res.replace(/NotificationTemplate,/g, 'NotificationTemplateOld,');
res = res.replace(/NotificationTemplate /g, 'NotificationTemplateOld ');
res = res.replace(/NotificationTemplate>/g, 'NotificationTemplateOld>');
res = res.replace(/NotificationTemplate\[\]/g, 'NotificationTemplateOld[]');
res = res.replace(/: NotificationTemplate/g, ': NotificationTemplateOld');

res = res.replace(/NotificationTemplateRow/g, 'NotificationTemplateRowOld');
res = res.replace(/CreateNotificationTemplateInput/g, 'CreateNotificationTemplateInputOld');
res = res.replace(/UpdateNotificationTemplateInput/g, 'UpdateNotificationTemplateInputOld');
return res;
});
});
67 changes: 67 additions & 0 deletions listener/fix-merge2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const fs = require('fs');
const path = require('path');

const listenerDir = path.join(__dirname, 'src');

function fixFile(relativePath, replacer) {
const filePath = path.join(listenerDir, relativePath);
if (!fs.existsSync(filePath)) {
console.log('File not found:', relativePath);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
let newContent = replacer(content);
if (newContent !== content) {
fs.writeFileSync(filePath, newContent, 'utf8');
console.log('Fixed', relativePath);
}
}

// Fix missing brace in types/notification-template.ts
fixFile('types/notification-template.ts', (c) => {
let res = c;
res = res.replace(/ updated_by: string \| null;\nexport interface NotificationTemplate \{/g, ' updated_by: string | null;\n}\nexport interface NotificationTemplate {');

// Rename duplicate types to Old*
res = res.replace(/export interface NotificationTemplateRow \{\n id: string;\n name: string;/g, 'export interface NotificationTemplateRowOld {\n id: string;\n name: string;');
res = res.replace(/export interface NotificationTemplate \{\n id: string;\n name: string;/g, 'export interface NotificationTemplateOld {\n id: string;\n name: string;');
res = res.replace(/export interface CreateNotificationTemplateInput \{\n id: string;\n name: string;/g, 'export interface CreateNotificationTemplateInputOld {\n id: string;\n name: string;');
res = res.replace(/export interface UpdateNotificationTemplateInput \{\n name\?: string;/g, 'export interface UpdateNotificationTemplateInputOld {\n name?: string;');
return res;
});

// Update files using the OLD template types
const oldFiles = [
'services/notification-template-service.ts',
'services/notification-template-repository.ts',
'api/template-routes.ts',
'api/templates-api.test.ts',
'services/notification-template-service.test.ts',
'api/events-server.ts'
];

oldFiles.forEach(f => {
fixFile(f, (c) => {
let res = c;
// Replace imports and usages carefully
// We only want to replace NotificationTemplate with NotificationTemplateOld, avoiding double-renaming
// and we only want to replace it as an identifier (word boundary)
res = res.replace(/\bNotificationTemplate\b/g, 'NotificationTemplateOld');
res = res.replace(/\bNotificationTemplateRow\b/g, 'NotificationTemplateRowOld');
res = res.replace(/\bCreateNotificationTemplateInput\b/g, 'CreateNotificationTemplateInputOld');
res = res.replace(/\bUpdateNotificationTemplateInput\b/g, 'UpdateNotificationTemplateInputOld');

// Fix the class names which were also renamed by the above regex!
res = res.replace(/\bNotificationTemplateOldService\b/g, 'NotificationTemplateService');
res = res.replace(/\bNotificationTemplateOldRepository\b/g, 'NotificationTemplateRepository');
res = res.replace(/\bNotificationTemplateOldCache\b/g, 'NotificationTemplateCache');

// Also in notification-template-service.ts, remove duplicate methods
if (f === 'services/notification-template-service.ts') {
res = res.replace(/ async delete\(templateId: string\): Promise<void> \{\n await this\.repository\.delete\(templateId\);\n this\.cache\.invalidate\(templateId\);\n \}/g, '');
res = res.replace(/ async getAll\(\): Promise<NotificationTemplateOld\[\]> \{\n return this\.repository\.listAll\(\);\n \}/g, '');
}

return res;
});
});
Loading