diff --git a/soroban/contracts/factory/src/test.rs b/soroban/contracts/factory/src/test.rs index 63d32cb..75c9700 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(); @@ -1368,6 +1373,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(); diff --git a/soroban/contracts/farming-pool/src/lib.rs b/soroban/contracts/farming-pool/src/lib.rs index 0b4d268..f6f041c 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 @@ -2590,6 +2598,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 c7c1336..1f80091 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); @@ -1925,6 +1938,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] diff --git a/soroban/contracts/vesting-wallet/src/lib.rs b/soroban/contracts/vesting-wallet/src/lib.rs index 5c64867..1e2c8ef 100644 --- a/soroban/contracts/vesting-wallet/src/lib.rs +++ b/soroban/contracts/vesting-wallet/src/lib.rs @@ -368,6 +368,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 5bcf351..aab9c2e 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]