diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index c647ec9..0b0ea6e 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -1702,9 +1702,16 @@ impl Escrow { } /// Admin sets the allowlist status for a specific agent. + /// + /// Emits an `agt_alw` event with `(agent, allowed)` after the storage + /// write so indexers can observe every per-agent allowlist change + /// on-chain, mirroring the `cfg_set` event already emitted by + /// [`Self::set_allowlist_enabled`] for the master toggle. pub fn set_agent_allowed(env: Env, agent: Address, allowed: bool) { require_admin(&env); - write_flag(&env, &DataKey::AgentAllowed(agent), allowed); + write_flag(&env, &DataKey::AgentAllowed(agent.clone()), allowed); + env.events() + .publish((symbol_short!("agt_alw"),), (agent, allowed)); } /// Read whether an agent is on the blocklist (false for never-set). @@ -1716,9 +1723,15 @@ impl Escrow { /// agent is rejected by `record_usage` with `AgentBlocked`, /// independent of the allowlist and taking precedence over it: an /// agent that is both allow-listed and blocked is still rejected. + /// + /// Emits an `agt_blk` event with `(agent, blocked)` after the storage + /// write so indexers can observe every per-agent blocklist change + /// on-chain. pub fn set_agent_blocked(env: Env, agent: Address, blocked: bool) { require_admin(&env); - write_flag(&env, &DataKey::AgentBlocked(agent), blocked); + write_flag(&env, &DataKey::AgentBlocked(agent.clone()), blocked); + env.events() + .publish((symbol_short!("agt_blk"),), (agent, blocked)); } /// Admin sets the per-call lower bound on `requests` for batched diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index acc83e0..49bd5c7 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -1101,16 +1101,21 @@ fn test_transfer_service_ownership_genuine_transfer_emits_event() { let new_owner = Address::generate(&env); let desc = String::from_str(&env, "inference service"); client.set_service_metadata(&svc, &desc, &owner); - // Capture event count before transfer. - let events_before = env.events().all(); - let count_before = events_before.len(); // Perform genuine transfer. client.transfer_service_ownership(&owner, &svc, &new_owner); - // Exactly one new event (owner_chg). + // env.events().all() reflects only the most recent contract invocation + // (confirmed by every other event test in this file, e.g. + // assert_usage_event_count re-checking a count of 1 after each of + // several sequential record_usage calls), not a running total since + // the start of the test. The prior `set_service_metadata` call's + // `meta_set` event is therefore not present here — only this + // transfer's own event is. Comparing against a pre-call count of + // events from a *different* invocation was the bug; checking this + // call's own event count directly is the fix. let events_after = env.events().all(); assert_eq!( events_after.len(), - count_before + 1, + 1, "genuine transfer must emit exactly one event" ); let (_addr, topics, data) = events_after.last().unwrap(); @@ -2525,6 +2530,109 @@ fn test_set_agent_blocked_requires_admin_auth() { client.set_agent_blocked(&agent, &true); } #[test] +fn test_set_agent_allowed_emits_agt_alw_event() { + let env = Env::default(); + let (client, admin) = setup_initialized(&env); + let agent = Address::generate(&env); + + client.set_agent_allowed(&agent, &true); + + let events = env.events().all(); + let (_addr, topics, data) = events.last().unwrap(); + let expected_topics: soroban_sdk::Vec = + (symbol_short!("agt_alw"),).into_val(&env); + assert_eq!(topics, expected_topics); + let decoded: (Address, bool) = data.into_val(&env); + assert_eq!(decoded, (agent, true)); +} +#[test] +fn test_set_agent_allowed_emits_event_on_every_toggle() { + let env = Env::default(); + let (client, admin) = setup_initialized(&env); + let agent = Address::generate(&env); + let expected_topics: soroban_sdk::Vec = + (symbol_short!("agt_alw"),).into_val(&env); + + client.set_agent_allowed(&agent, &true); + let (_, topics, data) = env.events().all().last().unwrap(); + assert_eq!(topics, expected_topics); + let decoded: (Address, bool) = data.into_val(&env); + assert_eq!(decoded, (agent.clone(), true)); + + // A second, independent call emits its own event carrying the new + // value; each call's event is checked immediately after that call. + client.set_agent_allowed(&agent, &false); + let (_, topics, data) = env.events().all().last().unwrap(); + assert_eq!(topics, expected_topics); + let decoded: (Address, bool) = data.into_val(&env); + assert_eq!(decoded, (agent, false)); +} +#[test] +fn test_set_agent_blocked_emits_agt_blk_event() { + let env = Env::default(); + let (client, admin) = setup_initialized(&env); + let agent = Address::generate(&env); + + client.set_agent_blocked(&agent, &true); + + let events = env.events().all(); + let (_addr, topics, data) = events.last().unwrap(); + let expected_topics: soroban_sdk::Vec = + (symbol_short!("agt_blk"),).into_val(&env); + assert_eq!(topics, expected_topics); + let decoded: (Address, bool) = data.into_val(&env); + assert_eq!(decoded, (agent, true)); +} +#[test] +fn test_set_agent_blocked_emits_event_on_every_toggle() { + let env = Env::default(); + let (client, admin) = setup_initialized(&env); + let agent = Address::generate(&env); + let expected_topics: soroban_sdk::Vec = + (symbol_short!("agt_blk"),).into_val(&env); + + client.set_agent_blocked(&agent, &true); + let (_, topics, data) = env.events().all().last().unwrap(); + assert_eq!(topics, expected_topics); + let decoded: (Address, bool) = data.into_val(&env); + assert_eq!(decoded, (agent.clone(), true)); + + client.set_agent_blocked(&agent, &false); + let (_, topics, data) = env.events().all().last().unwrap(); + assert_eq!(topics, expected_topics); + let decoded: (Address, bool) = data.into_val(&env); + assert_eq!(decoded, (agent, false)); +} +#[test] +fn test_agt_alw_and_agt_blk_topics_do_not_collide() { + let env = Env::default(); + let (client, admin) = setup_initialized(&env); + let agent = Address::generate(&env); + let alw_topics: soroban_sdk::Vec = (symbol_short!("agt_alw"),).into_val(&env); + let blk_topics: soroban_sdk::Vec = (symbol_short!("agt_blk"),).into_val(&env); + + client.set_agent_allowed(&agent, &true); + let events = env.events().all(); + assert_eq!( + events.iter().filter(|(_, t, _)| t == &alw_topics).count(), + 1 + ); + assert_eq!( + events.iter().filter(|(_, t, _)| t == &blk_topics).count(), + 0 + ); + + client.set_agent_blocked(&agent, &true); + let events = env.events().all(); + assert_eq!( + events.iter().filter(|(_, t, _)| t == &blk_topics).count(), + 1 + ); + // The distinct topic ensures a blocklist write is never mistaken for an + // allowlist write by a listener subscribed to only one of the two. + assert!(alw_topics != blk_topics); +} +#[test] fn test_remove_service_price_clears_price() { let env = Env::default(); let (client, admin) = setup_initialized(&env);