diff --git a/EVENT_SCHEMA.md b/EVENT_SCHEMA.md index 53bc423..7c22605 100644 --- a/EVENT_SCHEMA.md +++ b/EVENT_SCHEMA.md @@ -388,6 +388,30 @@ Emitted when existing offering metadata is replaced. --- +### `metadata_removed` + +Emitted when the owner deletes a stale offering's metadata from instance storage. + +| Index | Location | Type | Description | +|---------|----------|---------|---------------------| +| topic 0 | topics | Symbol | `"metadata_removed"` | +| topic 1 | topics | String | offering_id | +| topic 2 | topics | Address | caller (owner) | +| data | data | () | empty | + +```json +{ + "topics": ["metadata_removed", "offering-001", "GOWNER..."], + "data": null +} +``` + +**Indexer note:** After this event, `get_metadata(offering_id)` returns `None`. +The call is idempotent — removing a key that was never set (or was already removed) +emits the event and returns `Ok(())` without error. + +--- + ### `set_authorized_caller` Emitted when the owner updates the authorized caller address. @@ -808,6 +832,7 @@ Emitted by `set_vault()` when the admin updates the registered vault address. | `set_authorized_caller` | vault | `set_authorized_caller()` | | `metadata_set` | vault | `set_metadata()` | | `metadata_updated` | vault | `update_metadata()` | +| `metadata_removed` | vault | `remove_metadata()` | | `distribute` | vault | `distribute()` | | `init` | revenue-pool | `init()` | | `admin_changed` | revenue-pool | `set_admin()` | @@ -829,6 +854,7 @@ Emitted by `set_vault()` when the admin updates the registered vault address. |---------|---------------|--------------------------------------------------------------| | 0.0.1 | vault | Initial vault events | | 0.0.1 | vault | Added `set_authorized_caller` event with old/new value payload (Issue #256) | +| 0.0.1 | vault | Added `metadata_removed` event on `remove_metadata()` for stale-entry cleanup | | 0.0.1 | revenue-pool | Full revenue pool event suite with JSON examples | | 0.0.1 | revenue-pool | Added `admin_changed` event on `set_admin` for explicit old/new admin intent | | 0.1.0 | settlement | `payment_received`, `balance_credited` | diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index bfbef2f..f85aaae 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -879,6 +879,34 @@ impl CalloraVault { Ok(metadata) } + /// Remove stored offering metadata (owner only). + /// + /// Deletes the `Metadata(offering_id)` storage key from instance storage. + /// Silently succeeds if the key does not exist (idempotent). + /// + /// # Errors + /// - `VaultError::Unauthorized` — caller is not the vault owner. + /// - `VaultError::OfferingIdTooLong` — `offering_id` exceeds `MAX_OFFERING_ID_LEN`. + pub fn remove_metadata( + env: Env, + caller: Address, + offering_id: String, + ) -> Result<(), VaultError> { + caller.require_auth(); + Self::require_owner(env.clone(), caller.clone())?; + if offering_id.len() > MAX_OFFERING_ID_LEN { + return Err(VaultError::OfferingIdTooLong); + } + env.storage() + .instance() + .remove(&StorageKey::Metadata(offering_id.clone())); + env.events().publish( + (Symbol::new(&env, "metadata_removed"), offering_id, caller), + (), + ); + Ok(()) + } + /// Admin-gated contract upgrade. /// /// Only the current admin may call. This will instruct the host to update diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index 4233a40..2ff5d3e 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -2008,6 +2008,93 @@ fn metadata_remains_after_ownership_transfer() { ); } +// --------------------------------------------------------------------------- +// remove_metadata tests +// --------------------------------------------------------------------------- + +#[test] +fn remove_metadata_clears_entry() { + let env = Env::default(); + let owner = Address::generate(&env); + let (_, client) = create_vault(&env); + let (usdc, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc, &None, &None, &None, &None, &None); + + let offering_id = String::from_str(&env, "offering-rm-001"); + let metadata = String::from_str(&env, "ipfs://bafybeig"); + + client.set_metadata(&owner, &offering_id, &metadata); + assert_eq!(client.get_metadata(&offering_id), Some(metadata)); + + client.remove_metadata(&owner, &offering_id); + assert_eq!(client.get_metadata(&offering_id), None); +} + +#[test] +fn remove_metadata_emits_event() { + let env = Env::default(); + let owner = Address::generate(&env); + let (vault_address, client) = create_vault(&env); + let (usdc, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc, &None, &None, &None, &None, &None); + + let offering_id = String::from_str(&env, "offering-rm-002"); + client.set_metadata(&owner, &offering_id, &String::from_str(&env, "ipfs://cid")); + client.remove_metadata(&owner, &offering_id); + + let events = env.events().all(); + let ev = events.last().expect("expected metadata_removed event"); + + assert_eq!(ev.0, vault_address); + let topics = &ev.1; + assert_eq!(topics.len(), 3); + + let topic0: Symbol = topics.get(0).unwrap().into_val(&env); + let topic1: String = topics.get(1).unwrap().into_val(&env); + let topic2: Address = topics.get(2).unwrap().into_val(&env); + + assert_eq!(topic0, Symbol::new(&env, "metadata_removed")); + assert_eq!(topic1, offering_id); + assert_eq!(topic2, owner); +} + +#[test] +#[should_panic(expected = "unauthorized: owner only")] +fn unauthorized_cannot_remove_metadata() { + let env = Env::default(); + let owner = Address::generate(&env); + let unauthorized = Address::generate(&env); + let (_, client) = create_vault(&env); + let (usdc, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc, &None, &None, &None, &None, &None); + + let offering_id = String::from_str(&env, "offering-rm-003"); + client.set_metadata(&owner, &offering_id, &String::from_str(&env, "ipfs://cid")); + client.remove_metadata(&unauthorized, &offering_id); +} + +#[test] +fn remove_metadata_nonexistent_is_noop() { + let env = Env::default(); + let owner = Address::generate(&env); + let (_, client) = create_vault(&env); + let (usdc, _, _) = create_usdc(&env, &owner); + + env.mock_all_auths(); + client.init(&owner, &usdc, &None, &None, &None, &None, &None); + + let offering_id = String::from_str(&env, "offering-rm-never-set"); + // Should not panic even when the key was never written + client.remove_metadata(&owner, &offering_id); + assert_eq!(client.get_metadata(&offering_id), None); +} + // --------------------------------------------------------------------------- // Full lifecycle test // ---------------------------------------------------------------------------