diff --git a/contracts/view-facade/README.md b/contracts/view-facade/README.md index 0e4820f31..aebc87931 100644 --- a/contracts/view-facade/README.md +++ b/contracts/view-facade/README.md @@ -92,6 +92,28 @@ Emitted once when `init` succeeds. **Topic:** `("facade", "init")` +### `Register` + +Emitted when a contract is added to the registry via `register`. + +| Field | Type | Description | +|---|---|---| +| `address` | `Address` | On-chain address of the registered contract | +| `kind` | `ContractKind` | Role of the contract | +| `version` | `u32` | Version number at registration time | + +**Topic:** `("facade", "register")` + +### `Deregister` + +Emitted when a contract is removed from the registry via `deregister`. + +| Field | Type | Description | +|---|---|---| +| `address` | `Address` | On-chain address of the removed contract | + +**Topic:** `("facade", "deregstr")` + ## Error Codes | Code | Value | Meaning | @@ -117,10 +139,11 @@ cargo test Expected output: ``` -running 14 tests +running 17 tests test test::test_contract_count_initially_zero ... ok test test::test_deregister_before_init_rejected ... ok test test::test_deregister_contract ... ok +test test::test_deregister_emits_event ... ok test test::test_deregister_nonexistent_is_noop ... ok test test::test_double_init_rejected ... ok test test::test_get_admin_before_init_returns_none ... ok @@ -132,8 +155,10 @@ test test::test_list_and_count_contracts ... ok test test::test_register_all_contract_kinds ... ok test test::test_register_and_lookup_contract ... ok test test::test_register_before_init_rejected ... ok +test test::test_register_deregister_stability ... ok +test test::test_register_emits_event ... ok -test result: ok. 14 passed; 0 failed; 0 ignored +test result: ok. 17 passed; 0 failed; 0 ignored ``` ## Building diff --git a/contracts/view-facade/src/lib.rs b/contracts/view-facade/src/lib.rs index 5a797f55b..2dcca0aca 100644 --- a/contracts/view-facade/src/lib.rs +++ b/contracts/view-facade/src/lib.rs @@ -268,13 +268,19 @@ impl ViewFacade { .unwrap_or(Vec::new(&env)); registry.push_back(RegisteredContract { - address, - kind, + address: address.clone(), + kind: kind.clone(), version, }); env.storage().instance().set(&DataKey::Registry, ®istry); + // Emit register event for off-chain indexers + env.events().publish( + (symbol_short!("facade"), symbol_short!("register")), + (address, kind, version), + ); + Ok(()) } @@ -316,6 +322,12 @@ impl ViewFacade { env.storage().instance().set(&DataKey::Registry, &updated); + // Emit deregister event for off-chain indexers + env.events().publish( + (symbol_short!("facade"), symbol_short!("deregstr")), + address, + ); + Ok(()) } diff --git a/contracts/view-facade/src/test.rs b/contracts/view-facade/src/test.rs index 83cec7d6a..54f6f99cf 100644 --- a/contracts/view-facade/src/test.rs +++ b/contracts/view-facade/src/test.rs @@ -275,3 +275,106 @@ fn test_deregister_before_init_rejected() { let result = facade.try_deregister(&addr); assert_eq!(result, Err(Ok(FacadeError::NotInitialized))); } + +// --------------------------------------------------------------------------- +// Registry — events +// --------------------------------------------------------------------------- + +/// `register` emits a `("facade", "register")` event with (address, kind, version). +#[test] +fn test_register_emits_event() { + use soroban_sdk::{symbol_short, testutils::Events as _, vec, IntoVal}; + + let (env, facade, admin) = setup(); + let contract = Address::generate(&env); + + facade.init(&admin); + facade.register(&contract, &ContractKind::BountyEscrow, &1u32); + + let facade_id = facade.address.clone(); + let events = env.events().all(); + + let found = events.iter().any(|(cid, topics, _data)| { + if cid != facade_id { + return false; + } + let expected = vec![ + &env, + symbol_short!("facade").into_val(&env), + symbol_short!("register").into_val(&env), + ]; + topics == expected + }); + + assert!(found, "register event must be emitted"); +} + +/// `deregister` emits a `("facade", "deregstr")` event with the removed address. +#[test] +fn test_deregister_emits_event() { + use soroban_sdk::{symbol_short, testutils::Events as _, vec, IntoVal}; + + let (env, facade, admin) = setup(); + let contract = Address::generate(&env); + + facade.init(&admin); + facade.register(&contract, &ContractKind::SorobanEscrow, &2u32); + facade.deregister(&contract); + + let facade_id = facade.address.clone(); + let events = env.events().all(); + + let found = events.iter().any(|(cid, topics, _data)| { + if cid != facade_id { + return false; + } + let expected = vec![ + &env, + symbol_short!("facade").into_val(&env), + symbol_short!("deregstr").into_val(&env), + ]; + topics == expected + }); + + assert!(found, "deregister event must be emitted"); +} + +// --------------------------------------------------------------------------- +// Registry — stability +// --------------------------------------------------------------------------- + +/// Registering and deregistering multiple contracts leaves the registry in +/// a consistent state — only the expected entries remain. +#[test] +fn test_register_deregister_stability() { + let (env, facade, admin) = setup(); + + facade.init(&admin); + + let c1 = Address::generate(&env); + let c2 = Address::generate(&env); + let c3 = Address::generate(&env); + + facade.register(&c1, &ContractKind::BountyEscrow, &1); + facade.register(&c2, &ContractKind::ProgramEscrow, &2); + facade.register(&c3, &ContractKind::GrainlifyCore, &3); + assert_eq!(facade.contract_count(), 3); + + // Remove the middle one + facade.deregister(&c2); + assert_eq!(facade.contract_count(), 2); + + // c1 and c3 should remain in order + let list = facade.list_contracts(); + assert_eq!(list.get(0).unwrap().address, c1); + assert_eq!(list.get(1).unwrap().address, c3); + + // c2 should be gone + assert_eq!(facade.get_contract(&c2), None); + + // Re-register c2 — should appear at end + facade.register(&c2, &ContractKind::ProgramEscrow, &4); + assert_eq!(facade.contract_count(), 3); + assert_eq!(facade.list_contracts().get(2).unwrap().address, c2); + assert_eq!(facade.list_contracts().get(2).unwrap().version, 4); +}