diff --git a/contracts/provider/external-staking/src/contract.rs b/contracts/provider/external-staking/src/contract.rs index c9bf2c07..25391957 100644 --- a/contracts/provider/external-staking/src/contract.rs +++ b/contracts/provider/external-staking/src/contract.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ coin, coins, ensure, ensure_eq, from_binary, Addr, BankMsg, Binary, Coin, Decimal, DepsMut, - Env, Order, Response, StdResult, Storage, Uint128, Uint256, WasmMsg, + Env, Order, Response, StdError, StdResult, Storage, Uint128, Uint256, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::{Bounder, Item, Map}; @@ -25,9 +25,9 @@ use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; use crate::error::ContractError; use crate::msg::{ - AllTxsResponse, AuthorizedEndpointResponse, ConfigResponse, IbcChannelResponse, - ListRemoteValidatorsResponse, PendingRewards, ReceiveVirtualStake, StakeInfo, StakesResponse, - TxResponse, + AllPendingRewards, AllTxsResponse, AuthorizedEndpointResponse, ConfigResponse, + IbcChannelResponse, ListRemoteValidatorsResponse, PendingRewards, ReceiveVirtualStake, + StakeInfo, StakesResponse, TxResponse, ValidatorPendingReward, }; use crate::state::{Config, Distribution, Stake}; use mesh_sync::Tx; @@ -800,6 +800,47 @@ impl ExternalStakingContract<'_> { Ok(resp) } + /// Returns how much rewards are to be withdrawn by particular user, iterating over all validators. + /// This is like stakes is to stake query, but for rewards. + #[msg(query)] + pub fn all_pending_rewards( + &self, + ctx: QueryCtx, + user: String, + start_after: Option, + limit: Option, + ) -> Result { + let limit: usize = clamp_page_limit(limit); + let user = ctx.deps.api.addr_validate(&user)?; + + let bound = start_after.as_deref().and_then(Bounder::exclusive_bound); + + let config = self.config.load(ctx.deps.storage)?; + + let rewards: Vec<_> = self + .stakes + .prefix(&user) + .range(ctx.deps.storage, bound, None, Order::Ascending) + .take(limit) + .map(|item| { + let (validator, stake_lock) = item?; + let stake = stake_lock.read()?; + let distribution = self + .distribution + .may_load(ctx.deps.storage, &validator)? + .unwrap_or_default(); + let amount = Self::calculate_reward(stake, &distribution)?; + Ok::<_, ContractError>(ValidatorPendingReward::new( + validator, + amount.u128(), + &config.rewards_denom, + )) + }) + .collect::>()?; + + Ok(AllPendingRewards { rewards }) + } + /// Calculates reward for the user basing on the `Stake` he want to withdraw rewards from, and /// the corresponding validator `Distribution`. // @@ -807,10 +848,7 @@ impl ExternalStakingContract<'_> { // could be enforced by taking user and validator in arguments, then fetching data, but // sometimes data are used also for different calculations so we want to avoid double // fetching. - fn calculate_reward( - stake: &Stake, - distribution: &Distribution, - ) -> Result { + fn calculate_reward(stake: &Stake, distribution: &Distribution) -> Result { let points = distribution.points_per_stake * Uint256::from(stake.stake); let points = stake.points_alignment.align(points); diff --git a/contracts/provider/external-staking/src/msg.rs b/contracts/provider/external-staking/src/msg.rs index bcb7cf42..1eb661c8 100644 --- a/contracts/provider/external-staking/src/msg.rs +++ b/contracts/provider/external-staking/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, IbcChannel, Uint128}; +use cosmwasm_std::{coin, Coin, IbcChannel, Uint128}; use crate::{error::ContractError, state::Config}; @@ -89,12 +89,33 @@ pub struct UsersResponse { pub users: Vec, } -/// Response for penging rewards query +/// Response for pending rewards query on one validator #[cw_serde] pub struct PendingRewards { pub amount: Coin, } +/// Response for pending rewards query on all validator +#[cw_serde] +pub struct AllPendingRewards { + pub rewards: Vec, +} + +#[cw_serde] +pub struct ValidatorPendingReward { + pub validator: String, + pub amount: Coin, +} + +impl ValidatorPendingReward { + pub fn new(validator: impl Into, amount: u128, denom: impl Into) -> Self { + Self { + amount: coin(amount, denom), + validator: validator.into(), + } + } +} + pub type TxResponse = mesh_sync::Tx; #[cw_serde] diff --git a/contracts/provider/external-staking/src/multitest.rs b/contracts/provider/external-staking/src/multitest.rs index 76837a86..507c8711 100644 --- a/contracts/provider/external-staking/src/multitest.rs +++ b/contracts/provider/external-staking/src/multitest.rs @@ -16,7 +16,7 @@ use sylvia::multitest::App; use crate::contract::cross_staking::test_utils::CrossStakingApi; use crate::contract::multitest_utils::{CodeId, ExternalStakingContractProxy}; use crate::error::ContractError; -use crate::msg::{AuthorizedEndpoint, ReceiveVirtualStake, StakeInfo}; +use crate::msg::{AuthorizedEndpoint, ReceiveVirtualStake, StakeInfo, ValidatorPendingReward}; const OSMO: &str = "osmo"; const STAR: &str = "star"; @@ -761,7 +761,7 @@ fn distribution() { .unwrap(); // Only users[0] stakes on validators[1] - // 30 tokens for users[1] + // 30 tokens for users[0] contract .distribute_rewards(validators[1].to_owned()) .with_funds(&coins(30, STAR)) @@ -789,6 +789,22 @@ fn distribution() { .unwrap(); assert_eq!(rewards.amount.amount.u128(), 0); + // Show all rewards skips validators that were never staked on + let all_rewards = contract + .all_pending_rewards(users[0].to_owned(), None, None) + .unwrap(); + let expected = vec![ + ValidatorPendingReward::new(validators[0], 20, STAR), + ValidatorPendingReward::new(validators[1], 30, STAR), + ]; + assert_eq!(all_rewards.rewards, expected); + + let all_rewards = contract + .all_pending_rewards(users[1].to_owned(), None, None) + .unwrap(); + let expected = vec![ValidatorPendingReward::new(validators[0], 30, STAR)]; + assert_eq!(all_rewards.rewards, expected); + // Distributed funds should be on the staking contract assert_eq!( app.app() @@ -1210,6 +1226,24 @@ fn distribution() { .unwrap(); assert_eq!(rewards.amount.amount.u128(), 37); + let all_rewards = contract + .all_pending_rewards(users[0].to_owned(), None, None) + .unwrap(); + let expected = vec![ + ValidatorPendingReward::new(validators[0], 6, STAR), + ValidatorPendingReward::new(validators[1], 2, STAR), + ]; + assert_eq!(all_rewards.rewards, expected); + + let all_rewards = contract + .all_pending_rewards(users[1].to_owned(), None, None) + .unwrap(); + let expected = vec![ + ValidatorPendingReward::new(validators[0], 33, STAR), + ValidatorPendingReward::new(validators[1], 37, STAR), + ]; + assert_eq!(all_rewards.rewards, expected); + // And try to withdraw all, previous balances: contract .withdraw_rewards(validators[0].to_string())