Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions soroban-contracts/contracts/vault_factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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<Address> {
let capped = limit.min(MAX_STATUS_PAGE_SIZE);
let mut result: Vec<Address> = 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
Expand Down Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions soroban-contracts/contracts/vault_factory/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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<Address> {
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)
// ─────────────────────────────────────────────────────────────────────────────
Expand Down
43 changes: 42 additions & 1 deletion soroban-contracts/contracts/vault_factory/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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);
}
Loading