diff --git a/soroban-contracts/contracts/vault_factory/src/lib.rs b/soroban-contracts/contracts/vault_factory/src/lib.rs index f8fde8a..63892f6 100644 --- a/soroban-contracts/contracts/vault_factory/src/lib.rs +++ b/soroban-contracts/contracts/vault_factory/src/lib.rs @@ -31,6 +31,10 @@ const MAX_BATCH_SIZE: u32 = 10; /// Maximum page size for status-filtered vault list queries. const MAX_STATUS_PAGE_SIZE: u32 = 50; +/// Maximum number of entries to scan when building the recent list. +/// Bounds runtime even if many historic vaults have been removed. +const MAX_RECENT_SCAN: u32 = 200; + // ───────────────────────────────────────────────────────────────────────────── // Contract // ───────────────────────────────────────────────────────────────────────────── @@ -468,6 +472,40 @@ impl VaultFactory { result } + /// Returns the most recently created vaults (newest-first). + /// + /// Deterministic ordering is based on the factory's monotonic deploy counter, + /// not the live registry index (which uses swap-remove on deletion). + /// + /// `limit` is capped to 50. Returns an empty vec when `limit == 0` or when + /// no vaults exist. Removed vaults are skipped. + pub fn list_recent_vaults(e: &Env, limit: u32) -> Vec
{ + let capped = limit.min(MAX_STATUS_PAGE_SIZE); + let mut result: Vec
= Vec::new(e); + if capped == 0 { + return result; + } + + let mut deploy_id = get_vault_deploy_counter(e); + if deploy_id == 0 { + return result; + } + + let mut scanned: u32 = 0; + while deploy_id > 0 && result.len() < capped && scanned < MAX_RECENT_SCAN { + if let Some(vault) = get_vault_by_deploy_id(e, deploy_id) { + // Skip removed/unregistered vaults. + if get_vault_info(e, &vault).is_some() { + result.push_back(vault); + } + } + deploy_id -= 1; + scanned += 1; + } + + result + } + /// Returns a page of *active* vault addresses. /// /// `offset` is zero-based within the active-vault list. Returns an empty @@ -896,6 +934,8 @@ impl VaultFactory { }; put_vault_info(e, &vault_addr, info); register_vault(e, vault_addr.clone()); + // Persist deploy ordering for recent-vault queries. + put_vault_by_deploy_id(e, counter, &vault_addr); push_vaults_by_asset(e, &vault_asset, vault_addr.clone()); emit_vault_created( diff --git a/soroban-contracts/contracts/vault_factory/src/storage.rs b/soroban-contracts/contracts/vault_factory/src/storage.rs index dc4bf5a..d97e216 100644 --- a/soroban-contracts/contracts/vault_factory/src/storage.rs +++ b/soroban-contracts/contracts/vault_factory/src/storage.rs @@ -40,6 +40,11 @@ pub enum DataKey { VaultInfo(Address), VaultCount, VaultDeployCounter, + /// Monotonic deploy id → vault address mapping (persistent). + /// + /// This preserves deterministic "most recent vaults" ordering even when + /// vaults are removed from the indexed registry (which uses swap-remove). + VaultByDeployId(u32), VaultsByAsset(Address), DefaultFeeBps, } @@ -268,6 +273,16 @@ pub fn increment_vault_deploy_counter(e: &Env) -> u32 { count } +pub fn get_vault_by_deploy_id(e: &Env, id: u32) -> Option
{ + e.storage().persistent().get(&DataKey::VaultByDeployId(id)) +} + +pub fn put_vault_by_deploy_id(e: &Env, id: u32, vault: &Address) { + let key = DataKey::VaultByDeployId(id); + e.storage().persistent().set(&key, vault); + bump_persist(e, &key); +} + // ───────────────────────────────────────────────────────────────────────────── // VaultInfo (Persistent, keyed by vault address) // ───────────────────────────────────────────────────────────────────────────── diff --git a/soroban-contracts/contracts/vault_factory/src/tests.rs b/soroban-contracts/contracts/vault_factory/src/tests.rs index 43b3975..6520cb8 100644 --- a/soroban-contracts/contracts/vault_factory/src/tests.rs +++ b/soroban-contracts/contracts/vault_factory/src/tests.rs @@ -6,7 +6,10 @@ use soroban_sdk::{ }; use crate::{ - storage::{get_vault_count, get_vault_info, put_vault_info, register_vault}, + storage::{ + get_vault_count, get_vault_info, increment_vault_deploy_counter, put_vault_by_deploy_id, + put_vault_info, register_vault, + }, types::{VaultInfo, VaultType}, VaultFactory, VaultFactoryClient, }; @@ -122,6 +125,15 @@ fn inject_vault(e: &Env, factory_id: &Address, active: bool) -> Address { vault } +fn inject_vault_with_deploy_id(e: &Env, factory_id: &Address, active: bool) -> Address { + let vault = inject_vault(e, factory_id, active); + e.as_contract(factory_id, || { + let id = increment_vault_deploy_counter(e); + put_vault_by_deploy_id(e, id, &vault); + }); + vault +} + /// `VaultInfo.asset` is stored in the registry and returned by `get_vault_info` so /// indexers can resolve the underlying asset without N+1 vault calls. #[test] @@ -1110,3 +1122,32 @@ fn test_get_all_vaults_returns_vaults_in_creation_order() { // Verify vault count matches assert_eq!(client.get_vault_count(), 4); } + +#[test] +fn test_list_recent_vaults_returns_newest_first() { + let e = Env::default(); + e.mock_all_auths(); + let (factory_id, _admin) = setup_factory(&e); + let client = VaultFactoryClient::new(&e, &factory_id); + + let v1 = inject_vault_with_deploy_id(&e, &factory_id, true); + let v2 = inject_vault_with_deploy_id(&e, &factory_id, true); + let v3 = inject_vault_with_deploy_id(&e, &factory_id, true); + let v4 = inject_vault_with_deploy_id(&e, &factory_id, true); + let v5 = inject_vault_with_deploy_id(&e, &factory_id, true); + + let recent = client.list_recent_vaults(&3); + assert_eq!(recent.len(), 3); + assert_eq!(recent.get(0).unwrap(), v5); + assert_eq!(recent.get(1).unwrap(), v4); + assert_eq!(recent.get(2).unwrap(), v3); + + // Asking for more than exist returns all (up to cap). + let all_recent = client.list_recent_vaults(&10); + assert_eq!(all_recent.len(), 5); + assert_eq!(all_recent.get(4).unwrap(), v1); + assert_eq!(all_recent.get(0).unwrap(), v5); + assert_eq!(all_recent.get(1).unwrap(), v4); + assert_eq!(all_recent.get(2).unwrap(), v3); + assert_eq!(all_recent.get(3).unwrap(), v2); +}