From 9811ee4778232636656aa96c0af6aa402466f627 Mon Sep 17 00:00:00 2001 From: dunnie99 Date: Sun, 27 Sep 2026 22:55:56 +0100 Subject: [PATCH 1/4] test(factory): cover TTL bump on empty pool pages --- soroban/contracts/factory/src/test.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/soroban/contracts/factory/src/test.rs b/soroban/contracts/factory/src/test.rs index a5b6aa9..6998f01 100644 --- a/soroban/contracts/factory/src/test.rs +++ b/soroban/contracts/factory/src/test.rs @@ -4,8 +4,9 @@ use super::*; use soroban_sdk::{ symbol_short, testutils::{ - storage::Persistent as _, Address as _, AuthorizedFunction, AuthorizedInvocation, - Events as _, Ledger, MockAuth, MockAuthInvoke, + storage::{Instance as _, Persistent as _}, + Address as _, AuthorizedFunction, AuthorizedInvocation, Events as _, Ledger, MockAuth, + MockAuthInvoke, }, vec, Address, BytesN, Env, IntoVal, Symbol, }; @@ -170,6 +171,10 @@ fn pool_record_ttl(env: &Env, factory_addr: &Address, pool_id: u32) -> u32 { }) } +fn factory_instance_ttl(env: &Env, factory_addr: &Address) -> u32 { + env.as_contract(factory_addr, || env.storage().instance().get_ttl()) +} + #[test] fn test_initialize_sets_admin() { let t = setup(); @@ -1371,6 +1376,18 @@ fn test_list_pools_bumps_pool_record_ttl() { assert_eq!(pool_record_ttl(&t.env, &t.factory_addr, id), TTL_EXTEND_TO); } +#[test] +fn test_list_pools_empty_page_bumps_factory_instance_ttl() { + let t = setup(); + + advance_ledgers(&t.env, TTL_EXTEND_TO - TTL_THRESHOLD + 1); + assert!(factory_instance_ttl(&t.env, &t.factory_addr) < TTL_THRESHOLD); + + let page = t.client.list_pools(&0, &10); + assert!(page.records.is_empty()); + assert_eq!(factory_instance_ttl(&t.env, &t.factory_addr), TTL_EXTEND_TO); +} + #[test] fn test_refresh_pool_ttls_restores_ttl_for_unqueried_pool() { let t = setup(); From 827d8fd8cc55834b569c843f4d7b3636839e1c3f Mon Sep 17 00:00:00 2001 From: dunnie99 Date: Sun, 27 Sep 2026 22:59:03 +0100 Subject: [PATCH 2/4] feat(farming-pool): add paginated staker query --- soroban/contracts/farming-pool/src/lib.rs | 24 ++++++++++++++++++++++ soroban/contracts/farming-pool/src/test.rs | 20 ++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 4a06d0e..02cba18 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -2590,6 +2590,30 @@ impl FarmingPool { Self::whitelist_count(env) } + /// Return a zero-based page of distinct users with an active stake or locked position. + pub fn get_stakers(env: Env, start: u32, limit: u32) -> Result, PoolError> { + require_initialized(&env)?; + bump_instance(&env); + + let mut all = get_staked_users_list(&env); + for user in get_locked_users_list(&env).iter() { + if !all.contains(&user) { + all.push_back(user); + } + } + + let mut page = Vec::new(&env); + let mut index = start; + let mut count = 0u32; + while index < all.len() && count < limit { + page.push_back(all.get(index).unwrap()); + index += 1; + count += 1; + } + + Ok(page) + } + /// Return a paginated list of all users with active stakes. /// /// `offset`: zero-based index of the first user to return. diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index 7498744..e7269d6 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -1942,6 +1942,26 @@ fn test_get_user_position_none_after_full_unlock() { assert!(t.client.get_user_position(&t.user).is_none()); } +#[test] +fn test_get_stakers_pages_distinct_flexible_and_locked_users() { + let t = setup(1, 1); + let locked_only_user = Address::generate(&t.env); + t.token_sac.mint(&locked_only_user, &1_000_000); + + t.client.stake(&t.user, &1_000); + t.client.lock_assets(&t.user, &500); + t.client.lock_assets(&locked_only_user, &500); + + let first_page = t.client.get_stakers(&0, &1); + assert_eq!(first_page.len(), 1); + assert_eq!(first_page.get(0), Some(t.user.clone())); + + let second_page = t.client.get_stakers(&1, &1); + assert_eq!(second_page.len(), 1); + assert_eq!(second_page.get(0), Some(locked_only_user)); + assert!(t.client.get_stakers(&2, &10).is_empty()); +} + // ── pause / unpause tests ───────────────────────────────────────────────────── #[test] From 01fc0a9eba7d0680daef36189a865e055b907689 Mon Sep 17 00:00:00 2001 From: dunnie99 Date: Sun, 27 Sep 2026 23:00:03 +0100 Subject: [PATCH 3/4] feat(farming-pool): expose total earned credits --- soroban/contracts/farming-pool/src/lib.rs | 8 ++++++++ soroban/contracts/farming-pool/src/test.rs | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 02cba18..49c617c 100644 --- a/soroban/contracts/farming-pool/src/lib.rs +++ b/soroban/contracts/farming-pool/src/lib.rs @@ -2406,6 +2406,14 @@ impl FarmingPool { Self::total_distributed_credits(env) } + /// Return the total credits earned across users and staking systems. + /// + /// Accrual is included when a user's stake or position is checkpointed; + /// uncheckpointed accrual remains available through the per-user queries. + pub fn get_total_earned(env: Env) -> Result { + Self::total_distributed_credits(env) + } + /// Aggregate pool overview in a single invocation (Issue #395). /// /// A pool dashboard previously needed up to six separate reads diff --git a/soroban/contracts/farming-pool/src/test.rs b/soroban/contracts/farming-pool/src/test.rs index e7269d6..dff03c3 100644 --- a/soroban/contracts/farming-pool/src/test.rs +++ b/soroban/contracts/farming-pool/src/test.rs @@ -246,6 +246,19 @@ fn test_total_distributed_credits_starts_at_zero() { assert_eq!(t.client.total_distributed_credits(), 0); } +#[test] +fn test_get_total_earned_counts_credits_when_checkpointed() { + let t = setup(2, 1); + t.client.stake(&t.user, &1_000); + advance_ledgers(&t.env, 10); + + assert_eq!(t.client.get_credits(&t.user), 10_000); + assert_eq!(t.client.get_total_earned(), 0); + + t.client.unstake(&t.user, &1_000); + assert_eq!(t.client.get_total_earned(), 10_000); +} + #[test] fn test_total_distributed_credits_counts_banked_stake_accrual_on_checkpoint() { let t = setup(2, 1); From 0483edd5c646bcf669d08a51d336f4fd444b321c Mon Sep 17 00:00:00 2001 From: dunnie99 Date: Sun, 27 Sep 2026 23:00:28 +0100 Subject: [PATCH 4/4] feat(vesting-wallet): expose full release status --- soroban/contracts/vesting-wallet/src/lib.rs | 7 +++++++ soroban/contracts/vesting-wallet/src/test.rs | 3 +++ 2 files changed, 10 insertions(+) diff --git a/soroban/contracts/vesting-wallet/src/lib.rs b/soroban/contracts/vesting-wallet/src/lib.rs index e7cf788..f5a85b5 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -355,6 +355,13 @@ impl VestingWallet { Ok(get_released(&env)) } + /// Return whether the beneficiary has received the entire scheduled amount. + pub fn is_fully_released(env: Env) -> Result { + require_initialized(&env)?; + bump_instance(&env); + Ok(get_released(&env) == get_total_amount(&env)) + } + /// Return the amount currently available to release (vested minus released). pub fn releasable(env: Env) -> Result { require_initialized(&env)?; diff --git a/soroban/contracts/vesting-wallet/src/test.rs b/soroban/contracts/vesting-wallet/src/test.rs index 9fae3ea..7944ee8 100644 --- a/soroban/contracts/vesting-wallet/src/test.rs +++ b/soroban/contracts/vesting-wallet/src/test.rs @@ -358,12 +358,15 @@ fn test_release_nothing_before_cliff() { #[test] fn test_release_full_amount_after_end() { let t = setup(0, 100, 1_000); + assert!(!t.client.is_fully_released()); advance_ledgers(&t.env, 200); // past end + assert!(!t.client.is_fully_released()); let amount = t.client.release(); assert_eq!(amount, 1_000); assert_eq!(t.token.balance(&t.beneficiary), 1_000); assert_eq!(t.client.releasable(), 0); + assert!(t.client.is_fully_released()); } #[test]