From 2d05ba0437faf2c602b07105020f816a83ff8ae0 Mon Sep 17 00:00:00 2001 From: Itodo-S Date: Sun, 28 Jun 2026 11:58:52 +0100 Subject: [PATCH] feat(agent-vault): emit events on asset whitelist add/remove (#37) add_asset() and remove_asset() updated persistent storage with no on-chain event, so indexers/clients had to poll is_supported_asset() to detect whitelist changes. - Add AddAssetEvent and RemoveAssetEvent (#[contractevent], asset as #[topic], plus admin). - add_asset() emits AddAssetEvent after writing to storage. - remove_asset() emits RemoveAssetEvent only when the asset key existed (the remove is otherwise a no-op). - Tests verify both events via env.events().all() (exact topics + data reconstructed from the generated Event impl), and that removing a non-whitelisted asset emits nothing. cargo test (52 passing) and cargo clippy --all-targets -- -D warnings pass. --- contracts/agent-vault/src/lib.rs | 30 ++++++++++- contracts/agent-vault/src/tests.rs | 80 ++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/contracts/agent-vault/src/lib.rs b/contracts/agent-vault/src/lib.rs index 1a25344..9483693 100644 --- a/contracts/agent-vault/src/lib.rs +++ b/contracts/agent-vault/src/lib.rs @@ -82,6 +82,20 @@ pub struct TaskDoneEvent { pub refund: i128, } +#[contractevent] +pub struct AddAssetEvent { + #[topic] + pub asset: Address, + pub admin: Address, +} + +#[contractevent] +pub struct RemoveAssetEvent { + #[topic] + pub asset: Address, + pub admin: Address, +} + #[contracterror] #[derive(Clone, Copy, PartialEq, Eq)] pub enum VaultError { @@ -270,6 +284,13 @@ impl AgentVault { Self::extend_persistent_ttl(&env, &asset_key); log!(&env, "Asset added to whitelist: {}", asset); + + AddAssetEvent { + asset: asset.clone(), + admin: admin.clone(), + } + .publish(&env); + Ok(()) } @@ -288,9 +309,16 @@ impl AgentVault { let asset_key = DataKey::AssetSupported(asset.clone()); if env.storage().persistent().has(&asset_key) { env.storage().persistent().remove(&asset_key); + + log!(&env, "Asset removed from whitelist: {}", asset); + + RemoveAssetEvent { + asset: asset.clone(), + admin: admin.clone(), + } + .publish(&env); } - log!(&env, "Asset removed from whitelist: {}", asset); Ok(()) } diff --git a/contracts/agent-vault/src/tests.rs b/contracts/agent-vault/src/tests.rs index 0824186..208e733 100644 --- a/contracts/agent-vault/src/tests.rs +++ b/contracts/agent-vault/src/tests.rs @@ -1,6 +1,6 @@ -use crate::{AgentVault, AgentVaultClient, DataKey, VaultError}; -use soroban_sdk::testutils::{Address as _, Ledger as _}; -use soroban_sdk::{token, Address, Env}; +use crate::{AddAssetEvent, AgentVault, AgentVaultClient, DataKey, RemoveAssetEvent, VaultError}; +use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; +use soroban_sdk::{token, vec, Address, Env, Event as _}; struct TestEnv { env: Env, @@ -1242,3 +1242,77 @@ fn test_get_user_tasks_separate_users() { assert_eq!(t.client.get_user_tasks(&user2).len(), 0); assert_eq!(t.client.get_user_tasks(&user1).len(), 1); } + +// Asset whitelist events (#37) + +#[test] +fn test_add_asset_emits_event() { + let t = setup_test(); + t.client.init(&t.admin, &t.usdc_sac); + + let asset = Address::generate(&t.env); + t.client.add_asset(&t.admin, &asset); + + // The only event so far should be the AddAssetEvent for this asset/admin. + let expected = AddAssetEvent { + asset: asset.clone(), + admin: t.admin.clone(), + }; + assert_eq!( + t.env.events().all(), + vec![ + &t.env, + ( + t.contract_id.clone(), + expected.topics(&t.env), + expected.data(&t.env), + ), + ], + ); + + assert!(t.client.is_supported_asset(&asset)); +} + +#[test] +fn test_remove_asset_emits_event() { + let t = setup_test(); + t.client.init(&t.admin, &t.usdc_sac); + + let asset = Address::generate(&t.env); + t.client.add_asset(&t.admin, &asset); + + // `env.events().all()` reflects the most recent invocation, so calling + // remove_asset last leaves exactly its RemoveAssetEvent in the buffer. + t.client.remove_asset(&t.admin, &asset); + + let expected = RemoveAssetEvent { + asset: asset.clone(), + admin: t.admin.clone(), + }; + assert_eq!( + t.env.events().all(), + vec![ + &t.env, + ( + t.contract_id.clone(), + expected.topics(&t.env), + expected.data(&t.env), + ), + ], + ); + + assert!(!t.client.is_supported_asset(&asset)); +} + +#[test] +fn test_remove_nonexistent_asset_emits_no_event() { + let t = setup_test(); + t.client.init(&t.admin, &t.usdc_sac); + + let asset = Address::generate(&t.env); + + // Asset was never whitelisted: remove is a no-op and must not emit an event. + t.client.remove_asset(&t.admin, &asset); + + assert!(t.env.events().all().events().is_empty()); +}