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
17 changes: 17 additions & 0 deletions dongle-smartcontract/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub struct ProjectOwnershipTransferredEvent {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProjectArchivedEvent {
pub project_id: u64,
pub archived_by: Address,
pub owner: Address,
pub timestamp: u64,
}
Expand Down Expand Up @@ -243,6 +244,22 @@ pub fn publish_project_updated_event(env: &Env, project_id: u64, owner: Address)
);
}

pub fn publish_project_archived_event(env: &Env, project_id: u64, archived_by: Address) {
let event_data = ProjectArchivedEvent {
project_id,
archived_by,
timestamp: env.ledger().timestamp(),
};
env.events().publish(
(
symbol_short!("PROJECT"),
symbol_short!("ARCHIVED"),
project_id,
),
event_data,
);
}

// ── Fee events ────────────────────────────────────────────────────────────────

pub fn publish_fee_paid_event(env: &Env, project_id: u64, payer: Address, amount: u128) {
Expand Down
36 changes: 36 additions & 0 deletions dongle-smartcontract/src/project_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::constants::MAX_PROJECTS_PER_USER;
use crate::errors::ContractError;
use crate::events::{
publish_ownership_transferred_event, publish_project_archived_event,
publish_project_registered_event, publish_project_updated_event,
publish_project_reactivated_event, publish_project_registered_event,
publish_project_updated_event,
};
Expand Down Expand Up @@ -104,6 +105,7 @@ impl ProjectRegistry {
logo_cid: params.logo_cid,
metadata_cid: params.metadata_cid,
verification_status: VerificationStatus::Unverified,
is_archived: false,
created_at: now,
updated_at: now,
tags: params.tags.clone(),
Expand Down Expand Up @@ -571,6 +573,8 @@ impl ProjectRegistry {
return projects;
}

let mut collected: u32 = 0;
for id in first..=count {
let end = core::cmp::min(
first.saturating_add(effective_limit as u64),
count.saturating_add(1),
Expand All @@ -582,6 +586,7 @@ impl ProjectRegistry {
break;
}
if let Some(project) = Self::get_project(env, id) {
if !project.is_archived {
if !project.archived {
projects.push_back(project);
collected += 1;
Expand Down Expand Up @@ -615,6 +620,8 @@ impl ProjectRegistry {
return projects;
}

let mut collected: u32 = 0;
for i in start_id..len {
let end = core::cmp::min(start_id.saturating_add(effective_limit), len);

let mut collected: u32 = 0;
Expand All @@ -624,6 +631,7 @@ impl ProjectRegistry {
}
if let Some(id) = category_projects.get(i) {
if let Some(project) = Self::get_project(env, id) {
if !project.is_archived {
if !project.archived {
projects.push_back(project);
collected += 1;
Expand Down Expand Up @@ -757,6 +765,34 @@ impl ProjectRegistry {
Ok(())
}

/// Archive a project. The owner can archive their own project; admins can force-archive any project.
pub fn archive_project(
env: &Env,
project_id: u64,
caller: Address,
) -> Result<(), ContractError> {
let mut project =
Self::get_project(env, project_id).ok_or(ContractError::ProjectNotFound)?;

caller.require_auth();

let is_owner = project.owner == caller;
let is_admin = crate::admin_manager::AdminManager::is_admin(env, &caller);

if !is_owner && !is_admin {
return Err(ContractError::Unauthorized);
}

project.is_archived = true;
project.updated_at = env.ledger().timestamp();
env.storage()
.persistent()
.set(&StorageKey::Project(project_id), &project);

StorageManager::extend_project_ttl(env, project_id);

publish_project_archived_event(env, project_id, caller);
Ok(())
/// List projects by tag - Issue #125
pub fn list_projects_by_tag(
env: &Env,
Expand Down
93 changes: 93 additions & 0 deletions dongle-smartcontract/src/tests/archival.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Tests for project archival feature (issue #121).

use crate::errors::ContractError;
use crate::tests::fixtures::{create_test_project, setup_contract};
use soroban_sdk::{testutils::Address as _, Address, Env};

#[test]
fn test_owner_can_archive_project() {
let env = Env::default();
env.mock_all_auths();
let (client, _admin) = setup_contract(&env);
let owner = Address::generate(&env);

let project_id = create_test_project(&client, &owner, "MyProject");
client.archive_project(&project_id, &owner);

let project = client.get_project(&project_id).unwrap();
assert!(project.is_archived);
}

#[test]
fn test_unauthorized_cannot_archive_project() {
let env = Env::default();
env.mock_all_auths();
let (client, _admin) = setup_contract(&env);
let owner = Address::generate(&env);
let stranger = Address::generate(&env);

let project_id = create_test_project(&client, &owner, "MyProject");

let result = client.try_archive_project(&project_id, &stranger);
assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
}

#[test]
fn test_admin_can_force_archive_project() {
let env = Env::default();
env.mock_all_auths();
let (client, admin) = setup_contract(&env);
let owner = Address::generate(&env);

let project_id = create_test_project(&client, &owner, "MyProject");
client.archive_project(&project_id, &admin);

let project = client.get_project(&project_id).unwrap();
assert!(project.is_archived);
}

#[test]
fn test_archived_project_excluded_from_list_projects() {
let env = Env::default();
env.mock_all_auths();
let (client, _admin) = setup_contract(&env);
let owner = Address::generate(&env);

let id1 = create_test_project(&client, &owner, "ProjectA");
let id2 = create_test_project(&client, &owner, "ProjectB");

client.archive_project(&id1, &owner);

let projects = client.list_projects(&0, &10);
assert_eq!(projects.len(), 1);
assert_eq!(projects.get(0).unwrap().id, id2);
}

#[test]
fn test_archived_project_excluded_from_list_by_category() {
let env = Env::default();
env.mock_all_auths();
let (client, _admin) = setup_contract(&env);
let owner = Address::generate(&env);

let id1 = create_test_project(&client, &owner, "ProjectA");
let id2 = create_test_project(&client, &owner, "ProjectB");

client.archive_project(&id1, &owner);

let category = soroban_sdk::String::from_str(&env, "DeFi");
let projects = client.list_projects_by_category(&category, &0, &10);
assert_eq!(projects.len(), 1);
assert_eq!(projects.get(0).unwrap().id, id2);
}

#[test]
fn test_archive_nonexistent_project_fails() {
let env = Env::default();
env.mock_all_auths();
let (client, _admin) = setup_contract(&env);
let caller = Address::generate(&env);

let result = client.try_archive_project(&999, &caller);
assert_eq!(result, Err(Ok(ContractError::ProjectNotFound)));
}
2 changes: 2 additions & 0 deletions dongle-smartcontract/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

// Existing test modules
mod admin;
mod archival;
mod error_handling_tests;
mod fee;
mod indexer;
mod review;
Expand Down
1 change: 1 addition & 0 deletions dongle-smartcontract/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub struct Project {
pub logo_cid: Option<String>,
pub metadata_cid: Option<String>,
pub verification_status: VerificationStatus,
pub is_archived: bool,
pub created_at: u64,
pub updated_at: u64,
pub tags: Option<Vec<String>>,
Expand Down
Loading