diff --git a/Cargo.lock b/Cargo.lock index 656f9828..6c6b8b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "account-history-contract" +version = "0.0.0" +dependencies = [ + "account-history-contract", + "anyhow", + "cargo-husky", + "chrono", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils", + "cw2", + "elys-bindings", + "elys-bindings-test", + "schemars", + "semver", + "serde", + "serde_json", + "thiserror", + "trade_shield_contract", +] + [[package]] name = "ahash" version = "0.7.8" @@ -19,6 +43,12 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + [[package]] name = "base16ct" version = "0.2.0" @@ -91,6 +121,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "num-traits", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -527,6 +566,15 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.19.0" diff --git a/bindings/src/account_history/mod.rs b/bindings/src/account_history/mod.rs new file mode 100644 index 00000000..c413ff51 --- /dev/null +++ b/bindings/src/account_history/mod.rs @@ -0,0 +1,2 @@ +pub mod msg; +pub mod types; diff --git a/bindings/src/account_history/msg/execute.rs b/bindings/src/account_history/msg/execute.rs new file mode 100644 index 00000000..4af4ee62 --- /dev/null +++ b/bindings/src/account_history/msg/execute.rs @@ -0,0 +1,18 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub enum ExecuteMsg { + AddUserAddressToQueue { + user_address: String, + }, + ChangeParams { + update_account_enabled: Option, + processed_account_per_block: Option, + delete_old_data_enabled: Option, + delete_epoch: Option, + }, + CleanStorage { + limit: u64, + }, + CleanStorageBulk {}, +} diff --git a/bindings/src/account_history/msg/instantiate.rs b/bindings/src/account_history/msg/instantiate.rs new file mode 100644 index 00000000..a7e7a15f --- /dev/null +++ b/bindings/src/account_history/msg/instantiate.rs @@ -0,0 +1,9 @@ +use cosmwasm_schema::cw_serde; +use cw_utils::Expiration; + +#[cw_serde] +pub struct InstantiateMsg { + pub limit: Option, + pub expiration: Option, + pub trade_shield_address: Option, +} diff --git a/bindings/src/account_history/msg/migration.rs b/bindings/src/account_history/msg/migration.rs new file mode 100644 index 00000000..9718fcf8 --- /dev/null +++ b/bindings/src/account_history/msg/migration.rs @@ -0,0 +1,7 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct MigrationMsg { + pub limit: Option, + pub trade_shield_address: Option, +} diff --git a/bindings/src/account_history/msg/mod.rs b/bindings/src/account_history/msg/mod.rs new file mode 100644 index 00000000..6e6323c0 --- /dev/null +++ b/bindings/src/account_history/msg/mod.rs @@ -0,0 +1,60 @@ +mod execute; +mod instantiate; +mod migration; +mod query; +mod sudo; + +pub use execute::ExecuteMsg; +pub use instantiate::InstantiateMsg; +pub use migration::MigrationMsg; +pub use query::QueryMsg; +pub use sudo::SudoMsg; + +pub mod query_resp { + mod get_all_resp; + mod get_rewards_resp; + mod get_storage_size; + mod liquid_asset; + mod membership_tier_response; + mod params_resp; + mod total_value_per_asset_resp; + mod user_value_response; + + pub use get_all_resp::GetAllResp; + pub use get_rewards_resp::GetRewardsResp; + pub use get_storage_size::StorageSizeResp; + pub use liquid_asset::LiquidAsset; + pub use membership_tier_response::MembershipTierResponse; + pub use params_resp::ParamsResp; + pub use total_value_per_asset_resp::GetLiquidAssetsResp; + pub use user_value_response::UserValueResponse; + + mod staked_assets_response; + pub use staked_assets_response::StakeAssetBalanceBreakdown; + pub use staked_assets_response::StakedAssetsResponse; + + pub mod earn { + mod get_eden_boost_earn_details_resp; + pub use get_eden_boost_earn_details_resp::GetEdenBoostEarnProgramResp; + mod get_eden_earn_details_resp; + pub use get_eden_earn_details_resp::GetEdenEarnProgramResp; + mod get_elys_earn_details_resp; + pub use get_elys_earn_details_resp::GetElysEarnProgramResp; + mod get_usdc_earn_details_resp; + pub use get_usdc_earn_details_resp::GetUsdcEarnProgramResp; + } + + pub mod estaking { + mod get_estaking_rewards_response; + pub use get_estaking_rewards_response::GetEstakingRewardsResponse; + } + + pub mod masterchef { + mod get_masterchef_pending_rewards; + mod get_masterchef_pool_apr_response; + mod get_masterchef_stable_stake_response; + pub use get_masterchef_pending_rewards::GetMasterchefUserPendingRewardResponse; + pub use get_masterchef_pool_apr_response::MasterChefPoolAprResponse; + pub use get_masterchef_stable_stake_response::StableStakeAprResponse; + } +} diff --git a/bindings/src/account_history/msg/query.rs b/bindings/src/account_history/msg/query.rs new file mode 100644 index 00000000..7c4d7d38 --- /dev/null +++ b/bindings/src/account_history/msg/query.rs @@ -0,0 +1,169 @@ +#[allow(unused_imports)] +use super::super::types::{PerpetualAssets, PortfolioBalanceSnapshot}; +#[allow(unused_imports)] +use super::query_resp::earn::*; +#[allow(unused_imports)] +use super::query_resp::estaking::*; +#[allow(unused_imports)] +use super::query_resp::masterchef::*; + +#[allow(unused_imports)] +use super::query_resp::*; +#[allow(unused_imports)] +use crate::query_resp::QueryStableStakeAprResponse; +#[allow(unused_imports)] +use crate::query_resp::{ + AuthAddressesResponse, BalanceBorrowed, MasterchefParamsResponse, MasterchefPoolInfoResponse, + PoolFilterType, QueryAprsResponse, QueryEarnPoolResponse, QueryExitPoolEstimationResponse, + QueryJoinPoolEstimationResponse, QueryPoolAssetEstimationResponse, QueryStakedPositionResponse, + QueryUnstakedPositionResponse, QueryUserPoolResponse, QueryVestingInfoResponse, + StableStakeParamsData, StakedAvailable, +}; +#[allow(unused_imports)] +use crate::types::{BalanceAvailable, PageRequest}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +#[allow(unused_imports)] +use cosmwasm_std::Uint128; +#[cfg(feature = "debug")] +use cosmwasm_std::{Coin, DecCoin, Decimal}; +use cw2::ContractVersion; + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(AuthAddressesResponse)] + Accounts { pagination: Option }, + #[returns(GetLiquidAssetsResp)] + GetLiquidAssets { user_address: String }, + #[returns(StakedAssetsResponse)] + GetStakedAssets { user_address: Option }, + #[returns(QueryUserPoolResponse)] + GetPoolBalances { user_address: String }, + #[returns(GetRewardsResp)] + GetRewards { user_address: String }, + #[returns(MembershipTierResponse)] + GetMembershipTier { user_address: String }, + #[returns(PerpetualAssets)] + GetPerpetualAssets { user_address: String }, + #[returns(Decimal)] + GetAssetPrice { asset: String }, + #[returns(Decimal)] + GetAssetPriceFromDenomInToDenomOut { denom_in: String, denom_out: String }, + + #[returns(QueryEarnPoolResponse)] + GetLiquidityPools { + pool_ids: Option>, + filter_type: PoolFilterType, + pagination: Option, + }, + + #[returns(QueryJoinPoolEstimationResponse)] + JoinPoolEstimation { pool_id: u64, amounts_in: Vec }, + + #[returns(QueryExitPoolEstimationResponse)] + ExitPoolEstimation { + pool_id: u64, + exit_fiat_amount: Decimal, + }, + + #[returns(QueryPoolAssetEstimationResponse)] + PoolAssetEstimation { pool_id: u64, amount: DecCoin }, + + #[returns(GetEstakingRewardsResponse)] + GetEstakingRewards { address: String }, + + #[returns(MasterchefParamsResponse)] + GetMasterchefParams {}, + + #[returns(MasterchefPoolInfoResponse)] + GetMasterchefPoolInfo { pool_id: u64 }, + + #[returns(GetMasterchefUserPendingRewardResponse)] + GetMasterchefPendingRewards { address: String }, + + #[returns(QueryStableStakeAprResponse)] + GetMasterchefStableStakeApr { denom: String }, + + #[returns(MasterChefPoolAprResponse)] + GetMasterChefPoolApr { pool_ids: Vec }, + // debug only + #[cfg(feature = "debug")] + #[returns(ParamsResp)] + Params {}, + + #[cfg(feature = "debug")] + #[returns(PortfolioBalanceSnapshot)] + LastSnapshot { user_address: String }, + + #[cfg(feature = "debug")] + #[returns(UserValueResponse)] + UserValue { user_address: String }, + + #[cfg(feature = "debug")] + #[returns(Vec<(String, Vec)>)] + All { pagination: Option }, + + #[cfg(feature = "debug")] + #[returns(Vec)] + UserSnapshots { user_address: String }, + + #[cfg(feature = "debug")] + #[returns(QueryStakedPositionResponse)] + CommitmentStakedPositions { delegator_address: String }, + + #[cfg(feature = "debug")] + #[returns(QueryUnstakedPositionResponse)] + CommitmentUnStakedPositions { delegator_address: String }, + + #[cfg(feature = "debug")] + #[returns(StakedAvailable)] + CommitmentStakedBalanceOfDenom { address: String, denom: String }, + + #[cfg(feature = "debug")] + #[returns(BalanceBorrowed)] + StableStakeBalanceOfBorrow {}, + + #[cfg(feature = "debug")] + #[returns(StableStakeParamsData)] + StableStakeParams {}, + + #[cfg(feature = "debug")] + #[returns(QueryVestingInfoResponse)] + CommitmentVestingInfo { address: String }, + + #[cfg(feature = "debug")] + #[returns(BalanceAvailable)] + Balance { address: String, denom: String }, + + #[cfg(feature = "debug")] + #[returns(Decimal)] + AmmPriceByDenom { token_in: Coin, discount: Decimal }, + + #[cfg(feature = "debug")] + #[returns(GetEdenEarnProgramResp)] + GetEdenEarnProgramDetails { address: String }, + + #[cfg(feature = "debug")] + #[returns(GetEdenBoostEarnProgramResp)] + GetEdenBoostEarnProgramDetails { address: String }, + + #[cfg(feature = "debug")] + #[returns(GetElysEarnProgramResp)] + GetElysEarnProgramDetails { address: String }, + + #[cfg(feature = "debug")] + #[returns(GetUsdcEarnProgramResp)] + GetUsdcEarnProgramDetails { address: String }, + + #[cfg(feature = "debug")] + #[returns(QueryAprsResponse)] + IncentiveAprs {}, + + #[cfg(feature = "debug")] + #[returns(StorageSizeResp)] + StorageSize {}, + + #[cfg(feature = "debug")] + #[returns(ContractVersion)] + Version {}, +} diff --git a/bindings/src/account_history/msg/query_resp/earn/get_eden_boost_earn_details_resp.rs b/bindings/src/account_history/msg/query_resp/earn/get_eden_boost_earn_details_resp.rs new file mode 100644 index 00000000..1e0206b0 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/earn/get_eden_boost_earn_details_resp.rs @@ -0,0 +1,9 @@ +use crate::account_history::types::earn_program::eden_boost_earn::EdenBoostEarnProgram; + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +#[derive(Default)] +pub struct GetEdenBoostEarnProgramResp { + pub data: EdenBoostEarnProgram, +} diff --git a/bindings/src/account_history/msg/query_resp/earn/get_eden_earn_details_resp.rs b/bindings/src/account_history/msg/query_resp/earn/get_eden_earn_details_resp.rs new file mode 100644 index 00000000..9d69ef09 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/earn/get_eden_earn_details_resp.rs @@ -0,0 +1,9 @@ +use cosmwasm_schema::cw_serde; + +use crate::account_history::types::earn_program::eden_earn::EdenEarnProgram; + +#[cw_serde] +#[derive(Default)] +pub struct GetEdenEarnProgramResp { + pub data: EdenEarnProgram, +} diff --git a/bindings/src/account_history/msg/query_resp/earn/get_elys_earn_details_resp.rs b/bindings/src/account_history/msg/query_resp/earn/get_elys_earn_details_resp.rs new file mode 100644 index 00000000..b2bead57 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/earn/get_elys_earn_details_resp.rs @@ -0,0 +1,8 @@ +use crate::account_history::types::earn_program::elys_earn::ElysEarnProgram; +use cosmwasm_schema::cw_serde; + +#[cw_serde] +#[derive(Default)] +pub struct GetElysEarnProgramResp { + pub data: ElysEarnProgram, +} diff --git a/bindings/src/account_history/msg/query_resp/earn/get_usdc_earn_details_resp.rs b/bindings/src/account_history/msg/query_resp/earn/get_usdc_earn_details_resp.rs new file mode 100644 index 00000000..ca156b16 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/earn/get_usdc_earn_details_resp.rs @@ -0,0 +1,8 @@ +use crate::account_history::types::earn_program::usdc_earn::UsdcEarnProgram; +use cosmwasm_schema::cw_serde; + +#[cw_serde] +#[derive(Default)] +pub struct GetUsdcEarnProgramResp { + pub data: UsdcEarnProgram, +} diff --git a/bindings/src/account_history/msg/query_resp/estaking/get_estaking_rewards_response.rs b/bindings/src/account_history/msg/query_resp/estaking/get_estaking_rewards_response.rs new file mode 100644 index 00000000..ee98b493 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/estaking/get_estaking_rewards_response.rs @@ -0,0 +1,16 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Coin; + +use crate::trade_shield::types::CoinValue; + +#[cw_serde] +pub struct GetEstakingRewardsResponse { + pub rewards: Vec, + pub total: Vec, +} + +#[cw_serde] +pub struct DelegationDelegatorReward { + pub validator_address: String, + pub reward: Vec, +} diff --git a/bindings/src/account_history/msg/query_resp/get_all_resp.rs b/bindings/src/account_history/msg/query_resp/get_all_resp.rs new file mode 100644 index 00000000..6f18ecc2 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/get_all_resp.rs @@ -0,0 +1,9 @@ +use crate::account_history::types::PortfolioBalanceSnapshot; +use crate::types::PageResponse; +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct GetAllResp { + pub snapshot_list: Vec<(String, PortfolioBalanceSnapshot)>, + pub pagination: Option, +} diff --git a/bindings/src/account_history/msg/query_resp/get_rewards_resp.rs b/bindings/src/account_history/msg/query_resp/get_rewards_resp.rs new file mode 100644 index 00000000..0682c5ed --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/get_rewards_resp.rs @@ -0,0 +1,10 @@ +use crate::account_history::types::Reward; +use crate::trade_shield::types::CoinValue; + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct GetRewardsResp { + pub rewards_map: Reward, + pub rewards: Vec, +} diff --git a/bindings/src/account_history/msg/query_resp/get_storage_size.rs b/bindings/src/account_history/msg/query_resp/get_storage_size.rs new file mode 100644 index 00000000..dff0900c --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/get_storage_size.rs @@ -0,0 +1,8 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct StorageSizeResp { + pub user_address_queue_data_size: u128, + pub history_data_size: u128, + pub old_history_2_data_size: u128, +} diff --git a/bindings/src/account_history/msg/query_resp/liquid_asset.rs b/bindings/src/account_history/msg/query_resp/liquid_asset.rs new file mode 100644 index 00000000..1e264d33 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/liquid_asset.rs @@ -0,0 +1,14 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; + +#[cw_serde] +pub struct LiquidAsset { + pub denom: String, + pub price: Decimal, + pub available_amount: Decimal, + pub available_value: Decimal, + pub in_order_amount: Decimal, + pub in_order_value: Decimal, + pub total_amount: Decimal, + pub total_value: Decimal, +} diff --git a/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_pending_rewards.rs b/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_pending_rewards.rs new file mode 100644 index 00000000..31596dd9 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_pending_rewards.rs @@ -0,0 +1,12 @@ +use std::collections::HashMap; + +use cosmwasm_schema::cw_serde; + +use crate::trade_shield::types::CoinValue; + +#[cw_serde] +#[derive(Default)] +pub struct GetMasterchefUserPendingRewardResponse { + pub rewards: HashMap>, + pub total_rewards: Vec, +} diff --git a/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_pool_apr_response.rs b/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_pool_apr_response.rs new file mode 100644 index 00000000..e523edfc --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_pool_apr_response.rs @@ -0,0 +1,8 @@ +use cosmwasm_schema::cw_serde; + +use crate::query_resp::PoolApr; + +#[cw_serde] +pub struct MasterChefPoolAprResponse { + pub data: Vec, +} diff --git a/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_stable_stake_response.rs b/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_stable_stake_response.rs new file mode 100644 index 00000000..21fd032d --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/masterchef/get_masterchef_stable_stake_response.rs @@ -0,0 +1,7 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Int128; + +#[cw_serde] +pub struct StableStakeAprResponse { + pub apr: Int128, +} diff --git a/bindings/src/account_history/msg/query_resp/membership_tier_response.rs b/bindings/src/account_history/msg/query_resp/membership_tier_response.rs new file mode 100644 index 00000000..dee6a005 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/membership_tier_response.rs @@ -0,0 +1,51 @@ +use std::str::FromStr; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal, Decimal256}; + +// Tier fee discount is +// Bronze tier: standard ( no discount) +// Silver tier: > $50k balance ( 10% discount) +// Gold tier: > $250K balance ( 20% discount) +// Platinum tier: > $500K balance ( 30% discount) + +#[cw_serde] +pub struct MembershipTierResponse { + pub identifier: String, + pub name: String, + pub discount: Decimal, +} + +impl MembershipTierResponse { + pub fn zero() -> Self { + Self::calc(Decimal256::zero()) + } + + pub fn calc(balance: Decimal256) -> Self { + if balance > Decimal256::from_str("500000").unwrap() { + Self { + identifier: "platinum".to_string(), + name: "Platinum".to_string(), + discount: Decimal::from_str("0.3").unwrap(), + } + } else if balance > Decimal256::from_str("250000").unwrap() { + Self { + identifier: "gold".to_string(), + name: "Gold".to_string(), + discount: Decimal::from_str("0.2").unwrap(), + } + } else if balance > Decimal256::from_str("50000").unwrap() { + Self { + identifier: "silver".to_string(), + name: "Silver".to_string(), + discount: Decimal::from_str("0.1").unwrap(), + } + } else { + Self { + identifier: "bronze".to_string(), + name: "Bronze".to_string(), + discount: Decimal::zero(), + } + } + } +} diff --git a/bindings/src/account_history/msg/query_resp/params_resp.rs b/bindings/src/account_history/msg/query_resp/params_resp.rs new file mode 100644 index 00000000..71390d85 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/params_resp.rs @@ -0,0 +1,14 @@ +use crate::account_history::types::Metadata; +use cosmwasm_schema::cw_serde; +use cw_utils::Expiration; + +#[cw_serde] +pub struct ParamsResp { + pub expiration: Expiration, + pub processed_account_per_block: u64, + pub trade_shield_address: Option, + pub update_account_enabled: bool, + pub metadata: Metadata, + pub delete_old_data_enabled: bool, + pub delete_epoch: u64, +} diff --git a/bindings/src/account_history/msg/query_resp/staked_assets_response.rs b/bindings/src/account_history/msg/query_resp/staked_assets_response.rs new file mode 100644 index 00000000..4c03f2f4 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/staked_assets_response.rs @@ -0,0 +1,34 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DecCoin, Decimal}; + +use crate::account_history::types::StakedAssets; + +#[cw_serde] +#[derive(Default)] +pub struct StakeAssetBalanceBreakdown { + pub staked: Decimal, + pub unstaking: Decimal, + pub vesting: Decimal, +} + +#[cw_serde] +pub struct StakedAssetsResponse { + pub total_staked_balance: DecCoin, + pub staked_assets: StakedAssets, + pub total_balance: Decimal, + pub balance_break_down: StakeAssetBalanceBreakdown, +} + +impl StakeAssetBalanceBreakdown { + pub fn total(&self) -> Decimal { + let total = self + .staked + .checked_add(self.unstaking) + .and_then(|sum| sum.checked_add(self.vesting)); + + match total { + Ok(result) => result, + Err(_) => Decimal::zero(), + } + } +} diff --git a/bindings/src/account_history/msg/query_resp/total_value_per_asset_resp.rs b/bindings/src/account_history/msg/query_resp/total_value_per_asset_resp.rs new file mode 100644 index 00000000..ad2182cd --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/total_value_per_asset_resp.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::DecCoin; + +use super::LiquidAsset; + +#[cw_serde] +pub struct GetLiquidAssetsResp { + pub liquid_assets: Vec, + pub total_liquid_asset_balance: DecCoin, +} diff --git a/bindings/src/account_history/msg/query_resp/user_value_response.rs b/bindings/src/account_history/msg/query_resp/user_value_response.rs new file mode 100644 index 00000000..1300dad6 --- /dev/null +++ b/bindings/src/account_history/msg/query_resp/user_value_response.rs @@ -0,0 +1,7 @@ +use crate::account_history::types::PortfolioBalanceSnapshot; +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct UserValueResponse { + pub value: PortfolioBalanceSnapshot, +} diff --git a/bindings/src/account_history/msg/sudo.rs b/bindings/src/account_history/msg/sudo.rs new file mode 100644 index 00000000..ea31f9fd --- /dev/null +++ b/bindings/src/account_history/msg/sudo.rs @@ -0,0 +1,6 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub enum SudoMsg { + ClockEndBlock {}, +} diff --git a/bindings/src/account_history/types/account_snapshot.rs b/bindings/src/account_history/types/account_snapshot.rs new file mode 100644 index 00000000..5a86c6ad --- /dev/null +++ b/bindings/src/account_history/types/account_snapshot.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::cw_serde; +use cw_utils::Expiration; + +use super::{LiquidAsset, PerpetualAssets, PoolBalances, Reward, StakedAssets, TotalBalance}; + +#[cw_serde] +pub struct AccountSnapshot { + pub date: Expiration, + pub total_balance: TotalBalance, + pub reward: Reward, + pub pool_balances: PoolBalances, + pub liquid_asset: LiquidAsset, + pub staked_assets: StakedAssets, + pub perpetual_assets: PerpetualAssets, +} + +impl AccountSnapshot { + pub fn zero(value_denom: &String) -> Self { + Self { + date: Expiration::Never {}, + total_balance: TotalBalance::zero(value_denom), + reward: Reward::default(), + pool_balances: PoolBalances::default(), + liquid_asset: LiquidAsset::zero(value_denom), + staked_assets: StakedAssets::default(), + perpetual_assets: PerpetualAssets::default(), + } + } +} + +// implement default +impl Default for AccountSnapshot { + fn default() -> Self { + Self { + date: Expiration::Never {}, + total_balance: TotalBalance::default(), + reward: Reward::default(), + pool_balances: PoolBalances::default(), + liquid_asset: LiquidAsset::default(), + staked_assets: StakedAssets::default(), + perpetual_assets: PerpetualAssets::default(), + } + } +} diff --git a/bindings/src/account_history/types/coin_value.rs b/bindings/src/account_history/types/coin_value.rs new file mode 100644 index 00000000..f45ae0ca --- /dev/null +++ b/bindings/src/account_history/types/coin_value.rs @@ -0,0 +1,120 @@ +use crate::{query_resp::OracleAssetInfoResponse, types::OracleAssetInfo, ElysQuerier}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Coin, Decimal, StdError, StdResult}; + +use super::ElysDenom; + +#[cw_serde] +#[derive(Default)] +pub struct CoinValue { + pub denom: String, + pub amount_token: Decimal, + pub price: Decimal, + pub amount_usd: Decimal, +} + +impl CoinValue { + pub fn new(denom: String, amount_token: Decimal, price: Decimal, amount_usd: Decimal) -> Self { + Self { + denom, + amount_token, + price, + amount_usd, + } + } + + pub fn from_price_and_coin( + balance: &Coin, + price_and_decimal_point: (Decimal, u64), + ) -> StdResult { + let amount_token = Decimal::from_atomics(balance.amount, price_and_decimal_point.1 as u32) + .map_err(|e| { + StdError::generic_err(format!("failed to convert amount to Decimal: {}", e)) + })?; + + let amount_usd = price_and_decimal_point + .0 + .clone() + .checked_mul( + Decimal::from_atomics(balance.amount, price_and_decimal_point.1 as u32).map_err( + |e| { + StdError::generic_err(format!( + "failed to convert amount_usd_base to Decimal: {}", + e + )) + }, + )?, + ) + .map_err(|e| { + StdError::generic_err(format!( + "failed to convert amount_usd_base to Decimal: {}", + e + )) + })?; + + Ok(Self { + denom: balance.denom.clone(), + amount_token, + price: price_and_decimal_point.0, + amount_usd, + }) + } + + pub fn from_coin(balance: &Coin, querier: &ElysQuerier<'_>) -> StdResult { + let OracleAssetInfoResponse { asset_info } = querier + .asset_info(balance.denom.clone()) + .unwrap_or(OracleAssetInfoResponse { + asset_info: OracleAssetInfo { + denom: balance.denom.clone(), + display: balance.denom.clone(), + band_ticker: balance.denom.clone(), + elys_ticker: balance.denom.clone(), + decimal: 6, + }, + }); + let decimal_point_usd = asset_info.decimal; + + let amount_token = Decimal::from_atomics(balance.amount, decimal_point_usd as u32) + .map_err(|e| { + StdError::generic_err(format!("failed to convert amount to Decimal: {}", e)) + })?; + + // Eden boost does not have Fiat valuation. + if balance.denom == ElysDenom::EdenBoost.as_str() { + return Ok(Self { + amount_token, + denom: balance.denom.clone(), + amount_usd: Decimal::zero(), + price: Decimal::zero(), + }); + } + + let price = querier + .get_asset_price(balance.denom.clone()) + .map_err(|e| StdError::generic_err(format!("failed to get_asset_price: {}", e)))?; + + let amount_usd = price + .clone() + .checked_mul( + Decimal::from_atomics(balance.amount, decimal_point_usd as u32).map_err(|e| { + StdError::generic_err(format!( + "failed to convert amount_usd_base to Decimal: {}", + e + )) + })?, + ) + .map_err(|e| { + StdError::generic_err(format!( + "failed to convert amount_usd_base to Decimal: {}", + e + )) + })?; + + Ok(Self { + denom: balance.denom.clone(), + amount_token, + price, + amount_usd, + }) + } +} diff --git a/bindings/src/account_history/types/denom.rs b/bindings/src/account_history/types/denom.rs new file mode 100644 index 00000000..f7e00013 --- /dev/null +++ b/bindings/src/account_history/types/denom.rs @@ -0,0 +1,33 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub enum ElysDenom { + // Elys + Elys, + // Eden + Eden, + // Eden Boost + EdenBoost, + // Usdc + Usdc, + // USDC + USDC, + // ElysSource + ElysSource, + // AnySource + AnySource, +} + +impl ElysDenom { + pub fn as_str(&self) -> &'static str { + match self { + ElysDenom::Elys => "uelys", + ElysDenom::Eden => "ueden", + ElysDenom::EdenBoost => "uedenb", + ElysDenom::Usdc => "uusdc", + ElysDenom::USDC => "USDC", + ElysDenom::ElysSource => "elys", + ElysDenom::AnySource => "", + } + } +} diff --git a/bindings/src/account_history/types/earn_detail/earn_detail.rs b/bindings/src/account_history/types/earn_detail/earn_detail.rs new file mode 100644 index 00000000..dae43827 --- /dev/null +++ b/bindings/src/account_history/types/earn_detail/earn_detail.rs @@ -0,0 +1,117 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal, Int128, Uint128}; + +#[cw_serde] +#[derive(Default)] +pub struct AprEdenBoost { + pub uusdc: Uint128, + pub ueden: Uint128, +} +#[cw_serde] +pub struct AprUsdc { + pub uusdc: Int128, + pub ueden: Int128, +} + +// implement default +impl Default for AprUsdc { + fn default() -> Self { + Self { + uusdc: Int128::zero(), + ueden: Int128::zero(), + } + } +} + +#[cw_serde] +pub struct AprElys { + pub uusdc: Uint128, + pub ueden: Uint128, + pub uedenb: Uint128, +} + +// implement default +impl Default for AprElys { + fn default() -> Self { + Self { + uusdc: Uint128::zero(), + ueden: Uint128::zero(), + uedenb: Uint128::zero(), + } + } +} + +#[cw_serde] +pub struct BalanceBorrowed { + pub usd_amount: Decimal, + pub percentage: Decimal, +} + +// implement default +impl Default for BalanceBorrowed { + fn default() -> Self { + Self { + usd_amount: Decimal::zero(), + percentage: Decimal::zero(), + } + } +} + +#[cw_serde] +pub struct QueryAprResponse { + pub apr: Uint128, +} + +// implement default +impl Default for QueryAprResponse { + fn default() -> Self { + Self { + apr: Uint128::zero(), + } + } +} + +#[cw_serde] +pub struct BalanceReward { + pub asset: String, + pub amount: Uint128, + pub usd_amount: Option, +} + +// implement default +impl Default for BalanceReward { + fn default() -> Self { + Self { + asset: "".to_string(), + amount: Uint128::zero(), + usd_amount: None, + } + } +} + +#[cw_serde] +pub struct StakingValidator { + // The validator identity. + pub id: String, + // The validator address. + pub address: String, + // The validator name. + pub name: String, + // Voting power percentage for this validator. + pub voting_power: Decimal, + // commission percentage for the validator. + pub commission: Decimal, +} + +// implement default +impl Default for StakingValidator { + fn default() -> Self { + Self { + id: "".to_string(), + address: "".to_string(), + name: "".to_string(), + voting_power: Decimal::zero(), + commission: Decimal::zero(), + } + } +} diff --git a/bindings/src/account_history/types/earn_program/eden_boost_earn.rs b/bindings/src/account_history/types/earn_program/eden_boost_earn.rs new file mode 100644 index 00000000..768bae3f --- /dev/null +++ b/bindings/src/account_history/types/earn_program/eden_boost_earn.rs @@ -0,0 +1,36 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; + +use crate::account_history::types::earn_detail::earn_detail::AprEdenBoost; +use crate::trade_shield::types::CoinValue; + +#[cw_serde] +pub struct EdenBoostEarnProgram { + // should be 0 initially. In days + pub bonding_period: u64, + // The APR For the Eden Boost Earn Program. + pub apr: AprEdenBoost, + // available should be the user Eden Boost liquid balance on Elys and returned + // only if address is included in the request object. + pub available: Option, + // it should return how much Eden Boost the user has staked in this program ONLY. + // it should only be included if address is in the request object. + pub staked: Option, + // The rewards the user currently has on the Eden Boost Earn Program. + // It should be in the response only if the address is in the request object. + // rewards are either USDC or EDEN. + pub rewards: Option>, +} + +// implement default +impl Default for EdenBoostEarnProgram { + fn default() -> Self { + Self { + bonding_period: 0, + apr: AprEdenBoost::default(), + available: None, + staked: None, + rewards: None, + } + } +} diff --git a/bindings/src/account_history/types/earn_program/eden_earn.rs b/bindings/src/account_history/types/earn_program/eden_earn.rs new file mode 100644 index 00000000..07e708e3 --- /dev/null +++ b/bindings/src/account_history/types/earn_program/eden_earn.rs @@ -0,0 +1,65 @@ +use crate::{ + account_history::types::{AprElys, ElysDenom}, + query_resp::StakedAvailable, + trade_shield::types::BalanceAvailable, + trade_shield::types::CoinValue, + types::VestingDetail, +}; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Coin, Decimal, Uint128}; + +#[cw_serde] +pub struct EdenEarnProgram { + // should be 0 initially. In days + pub bonding_period: u64, + // The APR For the EDEN Earn Program. + pub apr: AprElys, + // available should be the user EDEN liquid balance on Elys and returned + // only if address is included in the request object. + pub available: Option, + // it should return how much EDEN the user has staked in this program ONLY. + // it should only be included if address is in the request object. + pub staked: Option, + // The rewards the user currently has on the EDEN Earn Program. + // It should be in the response only if the address is in the request object. + // rewards are either USDC, EDEN or EDEN Boost. + // Eden Boost doesnt have USD amount. + pub rewards: Option>, + // The sum of all the total_vest. + pub vesting: BalanceAvailable, + // A list of all the vesting details for the EDEN program. + // it should only be included if address is in the request object. + pub vesting_details: Option>, +} + +impl EdenEarnProgram { + fn to_coin(&self, amount: Uint128) -> Coin { + Coin::new(u128::from(amount), ElysDenom::Eden.as_str()) + } + + pub fn to_coin_available(&self) -> Coin { + self.to_coin(self.available.clone().unwrap_or_default().amount) + } + + pub fn to_coin_staked(&self) -> Coin { + self.to_coin(self.staked.clone().unwrap_or_default().amount) + } +} +// implement default +impl Default for EdenEarnProgram { + fn default() -> Self { + Self { + bonding_period: 0, + apr: AprElys::default(), + available: None, + staked: None, + rewards: None, + vesting: BalanceAvailable { + amount: Uint128::zero(), + usd_amount: Decimal::zero(), + }, + vesting_details: None, + } + } +} diff --git a/bindings/src/account_history/types/earn_program/elys_earn.rs b/bindings/src/account_history/types/earn_program/elys_earn.rs new file mode 100644 index 00000000..f73824f5 --- /dev/null +++ b/bindings/src/account_history/types/earn_program/elys_earn.rs @@ -0,0 +1,47 @@ +use crate::{ + account_history::types::AprElys, + query_resp::StakedAvailable, + trade_shield::types::CoinValue, + types::{BalanceAvailable, StakedPosition, UnstakedPosition}, +}; + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct ElysEarnProgram { + // should be 0 initially. In days + pub bonding_period: u64, + // The APR For the Elys Earn Program. + pub apr: AprElys, + // available should be the user Elys liquid balance on Elys and returned + // only if address is included in the request object. + pub available: Option, + // it should return how much Elys the user has staked in this program ONLY. + // it should only be included if address is in the request object. + pub staked: Option, + // The rewards the user currently has on the Elys Earn Program. + // It should be in the response only if the address is in the request object. + // rewards are either USDC, EDEN or EDEN Boost. + pub rewards: Option>, + // All the positions the user has staked on the ELYS program. + // It should be in the response only if the address is in the request object. + pub staked_positions: Option>, + // The positions the user has decided to unstake. + // It should be in the response only if the address is in the request object. + pub unstaked_positions: Option>, +} + +// implement default +impl Default for ElysEarnProgram { + fn default() -> Self { + Self { + bonding_period: 14, + apr: AprElys::default(), + available: None, + staked: None, + rewards: None, + staked_positions: None, + unstaked_positions: None, + } + } +} diff --git a/bindings/src/account_history/types/earn_program/usdc_earn.rs b/bindings/src/account_history/types/earn_program/usdc_earn.rs new file mode 100644 index 00000000..2eb2d686 --- /dev/null +++ b/bindings/src/account_history/types/earn_program/usdc_earn.rs @@ -0,0 +1,42 @@ +use crate::{ + account_history::types::AprUsdc, + query_resp::{BalanceBorrowed, StakedAvailable}, + trade_shield::types::CoinValue, + types::BalanceAvailable, +}; + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct UsdcEarnProgram { + // should be 0 initially. In days + pub bonding_period: u64, + // The APR For the USDC Earn Program. + pub apr: AprUsdc, + // available should be the user USDC liquid balance on Elys and returned + // only if address is included in the request object. + pub available: Option, + // it should return how much USDC the user has staked in this program ONLY. + // it should only be included if address is in the request object. + pub staked: Option, + // The rewards the user currently has on the USDC Earn Program. + // It should be in the response only if the address is in the request object. + // rewards are either USDC or EDEN. + pub rewards: Option>, + // The amount that has been borrowed from the user staked positions. + pub borrowed: Option, +} + +// implement default +impl Default for UsdcEarnProgram { + fn default() -> Self { + Self { + bonding_period: 0, + apr: AprUsdc::default(), + available: None, + staked: None, + rewards: None, + borrowed: None, + } + } +} diff --git a/bindings/src/account_history/types/liquid_asset.rs b/bindings/src/account_history/types/liquid_asset.rs new file mode 100644 index 00000000..a20a13c9 --- /dev/null +++ b/bindings/src/account_history/types/liquid_asset.rs @@ -0,0 +1,35 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DecCoin, Decimal256}; + +use crate::trade_shield::types::CoinValue; + +#[cw_serde] +pub struct LiquidAsset { + pub total_liquid_asset_balance: DecCoin, + pub total_available_balance: DecCoin, + pub total_in_orders_balance: DecCoin, + pub available_asset_balance: Vec, + pub in_orders_asset_balance: Vec, + pub total_value_per_asset: Vec, +} + +// implement zero +impl LiquidAsset { + pub fn zero(value_denom: &String) -> Self { + Self { + total_liquid_asset_balance: DecCoin::new(Decimal256::zero(), value_denom.to_string()), + total_available_balance: DecCoin::new(Decimal256::zero(), value_denom.to_string()), + total_in_orders_balance: DecCoin::new(Decimal256::zero(), value_denom.to_string()), + available_asset_balance: vec![], + in_orders_asset_balance: vec![], + total_value_per_asset: vec![], + } + } +} + +// implement default +impl Default for LiquidAsset { + fn default() -> Self { + Self::zero(&"usdc".to_string()) + } +} diff --git a/bindings/src/account_history/types/metadata.rs b/bindings/src/account_history/types/metadata.rs new file mode 100644 index 00000000..2cd89353 --- /dev/null +++ b/bindings/src/account_history/types/metadata.rs @@ -0,0 +1,144 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; + +use crate::{query_resp::QueryAprResponse, ElysQuerier}; + +use super::ElysDenom; + +#[cw_serde] +pub struct Metadata { + pub usdc_denom: String, + pub usdc_base_denom: String, + pub usdc_display_denom: String, + pub usdc_decimal: u64, + pub eden_decimal: u64, + pub usdc_apr_usdc: QueryAprResponse, + pub eden_apr_usdc: QueryAprResponse, + pub usdc_apr_edenb: QueryAprResponse, + pub eden_apr_edenb: QueryAprResponse, + pub usdc_apr_eden: QueryAprResponse, + pub eden_apr_eden: QueryAprResponse, + pub edenb_apr_eden: QueryAprResponse, + pub usdc_apr_elys: QueryAprResponse, + pub eden_apr_elys: QueryAprResponse, + pub edenb_apr_elys: QueryAprResponse, + pub uusdc_usd_price: Decimal, + pub uelys_price_in_uusdc: Decimal, +} + +impl Metadata { + pub fn update_prices(&self, querier: &ElysQuerier) -> StdResult { + let usdc_oracle_price = querier + .get_oracle_price( + self.usdc_display_denom.clone(), + ElysDenom::AnySource.as_str().to_string(), + 0, + ) + .map_err(|_| StdError::generic_err("an error occurred while getting usdc price"))?; + let uusdc_usd_price = usdc_oracle_price + .price + .price + .checked_div( + Decimal::from_atomics(Uint128::new(self.usdc_decimal as u128), 0) + .map_or(Decimal::zero(), |res| res), + ) + .map_or(Decimal::zero(), |res| res); + let uelys_price_in_uusdc = querier.get_asset_price(ElysDenom::Elys.as_str())?; + + Ok(Self { + uusdc_usd_price, + uelys_price_in_uusdc, + ..self.clone() + }) + } + + pub fn collect(querier: &ElysQuerier) -> StdResult { + let usdc_denom_entry = querier + .get_asset_profile(ElysDenom::Usdc.as_str().to_string()) + .map_err(|_| StdError::generic_err("an error occurred while getting usdc denom"))?; + let usdc_denom = usdc_denom_entry.entry.denom; + let usdc_base_denom = usdc_denom_entry.entry.base_denom; + let usdc_display_denom = usdc_denom_entry.entry.display_name; + let usdc_decimal = + u64::checked_pow(10, usdc_denom_entry.entry.decimals as u32).map_or(0, |res| res); + + let eden_denom_entry = querier + .get_asset_profile(ElysDenom::Eden.as_str().to_string()) + .map_err(|_| StdError::generic_err("an error occurred while getting eden denom"))?; + + let aprs = querier.get_incentive_aprs().unwrap_or_default(); + + Ok(Self { + usdc_denom, + usdc_base_denom, + usdc_display_denom, + usdc_decimal, + eden_decimal: u64::checked_pow(10, eden_denom_entry.entry.decimals as u32) + .map_or(0, |res| res), + + // APR section + usdc_apr_usdc: QueryAprResponse { + apr: aprs.usdc_apr_usdc, + }, + eden_apr_usdc: QueryAprResponse { + apr: aprs.eden_apr_usdc, + }, + + usdc_apr_edenb: QueryAprResponse { + apr: aprs.usdc_apr_edenb, + }, + eden_apr_edenb: QueryAprResponse { + apr: aprs.eden_apr_edenb, + }, + + usdc_apr_eden: QueryAprResponse { + apr: aprs.usdc_apr_eden, + }, + eden_apr_eden: QueryAprResponse { + apr: aprs.eden_apr_eden, + }, + edenb_apr_eden: QueryAprResponse { + apr: aprs.edenb_apr_eden, + }, + + usdc_apr_elys: QueryAprResponse { + apr: aprs.usdc_apr_elys, + }, + eden_apr_elys: QueryAprResponse { + apr: aprs.eden_apr_elys, + }, + edenb_apr_elys: QueryAprResponse { + apr: aprs.edenb_apr_elys, + }, + + // prices + uusdc_usd_price: Decimal::zero(), + uelys_price_in_uusdc: Decimal::zero(), + }) + } +} + +// default +impl Default for Metadata { + fn default() -> Self { + Metadata { + usdc_denom: "usdc".to_string(), + usdc_base_denom: "uusdc".to_string(), + usdc_display_denom: "USDC".to_string(), + usdc_decimal: 6, + eden_decimal: 6, + usdc_apr_usdc: QueryAprResponse::default(), + eden_apr_usdc: QueryAprResponse::default(), + usdc_apr_edenb: QueryAprResponse::default(), + eden_apr_edenb: QueryAprResponse::default(), + usdc_apr_eden: QueryAprResponse::default(), + eden_apr_eden: QueryAprResponse::default(), + edenb_apr_eden: QueryAprResponse::default(), + usdc_apr_elys: QueryAprResponse::default(), + eden_apr_elys: QueryAprResponse::default(), + edenb_apr_elys: QueryAprResponse::default(), + uusdc_usd_price: Decimal::zero(), + uelys_price_in_uusdc: Decimal::zero(), + } + } +} diff --git a/bindings/src/account_history/types/mod.rs b/bindings/src/account_history/types/mod.rs new file mode 100644 index 00000000..55ab0d64 --- /dev/null +++ b/bindings/src/account_history/types/mod.rs @@ -0,0 +1,47 @@ +mod account_snapshot; +mod coin_value; +mod liquid_asset; +mod metadata; +mod perpetual_assets; +mod pool_balances; +mod portfolio_balance_snapshot; +mod reward; +mod staked_assets; +mod total_balance; + +pub use account_snapshot::AccountSnapshot; +pub use coin_value::CoinValue; +pub use metadata::Metadata; +pub use portfolio_balance_snapshot::PortfolioBalanceSnapshot; + +pub mod earn_detail { + pub mod earn_detail; +} +pub use earn_detail::earn_detail::{ + AprElys, AprUsdc, BalanceBorrowed, BalanceReward, QueryAprResponse, StakingValidator, +}; + +pub mod earn_program { + pub mod eden_boost_earn; + pub use eden_boost_earn::EdenBoostEarnProgram; + + pub mod eden_earn; + pub use eden_earn::EdenEarnProgram; + + pub mod elys_earn; + pub use elys_earn::ElysEarnProgram; + + pub mod usdc_earn; + pub use usdc_earn::UsdcEarnProgram; +} + +pub mod denom; +pub use denom::ElysDenom; + +pub use total_balance::TotalBalance; + +pub use liquid_asset::LiquidAsset; +pub use perpetual_assets::*; +pub use pool_balances::PoolBalances; +pub use reward::Reward; +pub use staked_assets::StakedAssets; diff --git a/bindings/src/account_history/types/perpetual_assets.rs b/bindings/src/account_history/types/perpetual_assets.rs new file mode 100644 index 00000000..86b27933 --- /dev/null +++ b/bindings/src/account_history/types/perpetual_assets.rs @@ -0,0 +1,118 @@ +use crate::trade_shield::types::PerpetualPositionPlus; +use crate::types::PerpetualPosition; +use crate::ElysQuerier; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DecCoin, Decimal, Decimal256, SignedDecimal, StdError, StdResult, Uint128}; + +#[cw_serde] +pub struct PerpetualAssets { + pub total_perpetual_asset_balance: DecCoin, + pub perpetual_asset: Vec, +} + +#[cw_serde] +pub struct PerpetualAsset { + pub id: u64, + pub denom: String, + pub position: PerpetualPosition, + pub pnl: SignedDecimal, + pub collateral: DecCoin, + pub leverage: SignedDecimal, + pub size: DecCoin, + pub order_price: SignedDecimal, + pub liquidation: SignedDecimal, + pub health: SignedDecimal, + pub profit_price: DecCoin, + pub stop_loss: Option, + pub fees: Decimal, +} + +impl PerpetualAsset { + pub fn new( + mtp: PerpetualPositionPlus, + usdc_denom: String, + querier: &ElysQuerier<'_>, + ) -> StdResult { + let collateral_info = querier.asset_info(mtp.mtp.collateral_asset.clone())?; + let trading_asset_info = querier.asset_info(mtp.mtp.trading_asset.clone())?; + + Ok(PerpetualAsset { + id: mtp.mtp.id, + denom: mtp.mtp.collateral_asset.clone(), + position: PerpetualPosition::try_from_i32(mtp.mtp.position)?, + pnl: mtp.unrealized_pnl, + collateral: DecCoin { + denom: mtp.mtp.collateral_asset.clone(), + amount: Decimal256::from( + Decimal::from_atomics( + Uint128::new(mtp.mtp.collateral.i128() as u128), + collateral_info.asset_info.decimal as u32, + ) + .map_err(|e| { + StdError::generic_err(format!( + "failed to convert collateral to Decimal256: {}", + e + )) + })?, + ), + }, + leverage: mtp.mtp.leverage, + size: DecCoin { + denom: mtp.mtp.trading_asset.clone(), + amount: Decimal256::from( + Decimal::from_atomics( + Uint128::new(mtp.mtp.custody.i128() as u128), + trading_asset_info.asset_info.decimal as u32, + ) + .map_err(|e| { + StdError::generic_err(format!( + "failed to convert custody to Decimal256: {}", + e + )) + })?, + ), + }, + order_price: mtp.mtp.open_price, + liquidation: mtp.liquidation_price, + health: mtp.mtp.mtp_health, + profit_price: DecCoin { + denom: mtp.mtp.collateral_asset.clone(), + amount: Decimal256::try_from(mtp.mtp.take_profit_price).map_err(|e| { + StdError::generic_err(format!( + "failed to convert take_profit_price to Decimal256: {}", + e + )) + })?, + }, + stop_loss: match mtp.stop_loss_price { + Some(stop_loss) => Some(DecCoin { + amount: Decimal256::from(stop_loss.rate), + denom: usdc_denom.to_owned(), + }), + None => None, + }, + fees: Decimal::from_atomics( + Uint128::new(mtp.mtp.borrow_interest_paid_collateral.i128() as u128), + collateral_info.asset_info.decimal as u32, + ) + .map_err(|e| { + StdError::generic_err(format!( + "failed to convert borrow_interest_paid_collateral to Decimal256: {}", + e + )) + })?, + }) + } +} + +impl PerpetualAssets { + pub fn default() -> Self { + Self { + total_perpetual_asset_balance: DecCoin { + denom: "uusd".to_owned(), + amount: Decimal256::zero(), + }, + perpetual_asset: vec![], + } + } +} diff --git a/bindings/src/account_history/types/pool_balances.rs b/bindings/src/account_history/types/pool_balances.rs new file mode 100644 index 00000000..6fd886fa --- /dev/null +++ b/bindings/src/account_history/types/pool_balances.rs @@ -0,0 +1,13 @@ +use crate::query_resp::UserPoolResp; +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct PoolBalances { + pub balances: Vec, +} + +impl Default for PoolBalances { + fn default() -> Self { + Self { balances: vec![] } + } +} diff --git a/bindings/src/account_history/types/portfolio.rs b/bindings/src/account_history/types/portfolio.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/bindings/src/account_history/types/portfolio.rs @@ -0,0 +1 @@ + diff --git a/bindings/src/account_history/types/portfolio_balance_snapshot.rs b/bindings/src/account_history/types/portfolio_balance_snapshot.rs new file mode 100644 index 00000000..10908f7a --- /dev/null +++ b/bindings/src/account_history/types/portfolio_balance_snapshot.rs @@ -0,0 +1,24 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal256; +use cw_utils::Expiration; + +#[cw_serde] +pub struct PortfolioBalanceSnapshot { + pub date: Expiration, + pub total_balance_usd: Decimal256, +} + +impl PortfolioBalanceSnapshot { + pub fn zero() -> Self { + Self { + date: Expiration::Never {}, + total_balance_usd: Decimal256::zero(), + } + } +} + +impl Default for PortfolioBalanceSnapshot { + fn default() -> Self { + Self::zero() + } +} diff --git a/bindings/src/account_history/types/reward.rs b/bindings/src/account_history/types/reward.rs new file mode 100644 index 00000000..086fbd43 --- /dev/null +++ b/bindings/src/account_history/types/reward.rs @@ -0,0 +1,24 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; + +#[cw_serde] +pub struct Reward { + pub usdc_usd: Decimal, + pub eden_usd: Decimal, + pub eden_boost: Decimal, + pub other_usd: Decimal, + pub total_usd: Decimal, +} + +// implement default +impl Default for Reward { + fn default() -> Self { + Self { + usdc_usd: Decimal::zero(), + eden_usd: Decimal::zero(), + eden_boost: Decimal::zero(), + other_usd: Decimal::zero(), + total_usd: Decimal::zero(), + } + } +} diff --git a/bindings/src/account_history/types/staked_asset.rs b/bindings/src/account_history/types/staked_asset.rs new file mode 100644 index 00000000..85f9d003 --- /dev/null +++ b/bindings/src/account_history/types/staked_asset.rs @@ -0,0 +1,13 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; +use elys_bindings::types::EarnType; + +#[cw_serde] +pub struct StakedAsset { + pub program: EarnType, + pub bonding_period: u64, + pub apr: Decimal, + pub available: Decimal, + pub staked: Decimal, + pub rewards: Decimal, +} diff --git a/bindings/src/account_history/types/staked_assets.rs b/bindings/src/account_history/types/staked_assets.rs new file mode 100644 index 00000000..c74ba7fa --- /dev/null +++ b/bindings/src/account_history/types/staked_assets.rs @@ -0,0 +1,25 @@ +use cosmwasm_schema::cw_serde; + +use super::earn_program::{ + EdenBoostEarnProgram, EdenEarnProgram, ElysEarnProgram, UsdcEarnProgram, +}; + +#[cw_serde] +pub struct StakedAssets { + pub eden_boost_earn_program: EdenBoostEarnProgram, + pub eden_earn_program: EdenEarnProgram, + pub elys_earn_program: ElysEarnProgram, + pub usdc_earn_program: UsdcEarnProgram, +} + +// implement default +impl Default for StakedAssets { + fn default() -> Self { + Self { + eden_boost_earn_program: EdenBoostEarnProgram::default(), + eden_earn_program: EdenEarnProgram::default(), + elys_earn_program: ElysEarnProgram::default(), + usdc_earn_program: UsdcEarnProgram::default(), + } + } +} diff --git a/bindings/src/account_history/types/total_balance.rs b/bindings/src/account_history/types/total_balance.rs new file mode 100644 index 00000000..0362484f --- /dev/null +++ b/bindings/src/account_history/types/total_balance.rs @@ -0,0 +1,27 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal256; + +#[cw_serde] +pub struct TotalBalance { + pub total_balance: Decimal256, + pub portfolio_usd: Decimal256, + pub reward_usd: Decimal256, +} + +// implement zero +impl TotalBalance { + pub fn zero(_value_denom: &String) -> Self { + Self { + total_balance: Decimal256::zero(), + portfolio_usd: Decimal256::zero(), + reward_usd: Decimal256::zero(), + } + } +} + +// implement default +impl Default for TotalBalance { + fn default() -> Self { + Self::zero(&"usdc".to_string()) + } +} diff --git a/bindings/src/lib.rs b/bindings/src/lib.rs index ef75216a..84c479a4 100644 --- a/bindings/src/lib.rs +++ b/bindings/src/lib.rs @@ -12,4 +12,6 @@ pub use msg::*; pub use querier::ElysQuerier; pub use query::*; +pub mod account_history; + pub mod trade_shield; diff --git a/contracts/account-history-contract/.cargo/config b/contracts/account-history-contract/.cargo/config new file mode 100644 index 00000000..01cb7727 --- /dev/null +++ b/contracts/account-history-contract/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/account-history-contract/Cargo.toml b/contracts/account-history-contract/Cargo.toml new file mode 100644 index 00000000..405e9973 --- /dev/null +++ b/contracts/account-history-contract/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "account-history-contract" +version = "0.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["debug"] +debug = [] + +[dependencies] +cosmwasm-schema = { version = "1.1.4", default-features = false } +cosmwasm-std = { version = "1.1.4", features = [ + "staking", +], default-features = false } +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +cw-storage-plus = { version = "1.2.0", default-features = false, features = [ + "iterator", +] } +thiserror = { version = "1", default-features = false } +schemars = { default-features = false, version = "0.8.1" } +cw-utils = { version = "0.13", default-features = false } +cw2 = { version = "1.0.1" } +elys-bindings = { path = "../../bindings" } +chrono = { version = "0.4.33", default-features = false, features = ["alloc"] } +semver = "1.0" + +[dev-dependencies] +trade_shield_contract = { path = "../trade-shield-contract" } +cw-multi-test = "0.13.4" +serde_json = "1.0.107" +elys-bindings = { path = "../../bindings", features = ["testing"] } +elys-bindings-test = { path = "../../bindings-test" } +anyhow = "1" +account-history-contract = { path = ".", features = ["debug"] } +cargo-husky.workspace = true diff --git a/contracts/account-history-contract/src/action/execute/add_user_address_to_queue.rs b/contracts/account-history-contract/src/action/execute/add_user_address_to_queue.rs new file mode 100644 index 00000000..314299df --- /dev/null +++ b/contracts/account-history-contract/src/action/execute/add_user_address_to_queue.rs @@ -0,0 +1,9 @@ +use crate::states::USER_ADDRESS_QUEUE; +use cosmwasm_std::{DepsMut, StdResult}; +use elys_bindings::ElysQuery; + +pub fn add_user_address_to_queue(deps: DepsMut, user_address: String) -> StdResult<()> { + USER_ADDRESS_QUEUE.push_front(deps.storage, &user_address)?; + + Ok(()) +} diff --git a/contracts/account-history-contract/src/action/execute/clean_up_storage.rs b/contracts/account-history-contract/src/action/execute/clean_up_storage.rs new file mode 100644 index 00000000..98a93352 --- /dev/null +++ b/contracts/account-history-contract/src/action/execute/clean_up_storage.rs @@ -0,0 +1,20 @@ +use cosmwasm_std::{DepsMut, Response, StdResult}; + +use crate::states::{HISTORY, OLD_HISTORY_2, USER_ADDRESS_QUEUE}; +use elys_bindings::{ElysMsg, ElysQuery}; + +pub fn clean_up_storage(deps: &mut DepsMut, limit: u64) -> StdResult> { + // Delete history values + for _ in 0..limit { + if let Some(val) = HISTORY.first(deps.storage)? { + HISTORY.remove(deps.storage, &val.0); + } + if let Some(val) = OLD_HISTORY_2.first(deps.storage)? { + OLD_HISTORY_2.remove(deps.storage, &val.0); + } + if !USER_ADDRESS_QUEUE.is_empty(deps.storage).unwrap() { + let _ = USER_ADDRESS_QUEUE.pop_front(deps.storage); + } + } + Ok(Response::default()) +} diff --git a/contracts/account-history-contract/src/action/mod.rs b/contracts/account-history-contract/src/action/mod.rs new file mode 100644 index 00000000..641ed9de --- /dev/null +++ b/contracts/account-history-contract/src/action/mod.rs @@ -0,0 +1,74 @@ +pub mod query { + mod get_liquid_assets; + use crate::error::ContractError; + mod exit_pool_estimation; + mod get_estaking_rewards; + mod get_masterchef_pending_rewards; + mod get_masterchef_pool_apr; + mod get_masterchef_stable_stake_apr; + mod get_membership_tier; + mod get_perpetual_asset; + mod get_pool_balances; + mod get_pools; + mod get_rewards; + mod join_pool_estimation; + mod pool_asset_estimation; + + #[cfg(feature = "debug")] + mod all; + #[cfg(feature = "debug")] + mod last_snapshot; + #[cfg(feature = "debug")] + mod params; + #[cfg(feature = "debug")] + mod user_snapshots; + #[cfg(feature = "debug")] + mod user_value; + + #[cfg(feature = "debug")] + pub use all::all; + #[cfg(feature = "debug")] + pub use last_snapshot::last_snapshot; + #[cfg(feature = "debug")] + pub use params::params; + #[cfg(feature = "debug")] + pub use user_snapshots::user_snapshots; + #[cfg(feature = "debug")] + pub use user_value::user_value; + + pub use exit_pool_estimation::exit_pool_estimation; + pub use get_pool_balances::get_pool_balances; + pub use get_pools::get_pools; + pub use join_pool_estimation::join_pool_estimation; + pub use pool_asset_estimation::pool_asset_estimation; + mod get_eden_boost_earn_program_details; + pub use get_eden_boost_earn_program_details::get_eden_boost_earn_program_details; + pub use get_liquid_assets::get_liquid_assets; + mod get_eden_earn_program_details; + pub use get_eden_earn_program_details::get_eden_earn_program_details; + mod get_elys_earn_program_details; + pub use get_elys_earn_program_details::get_elys_earn_program_details; + mod get_usdc_earn_program_details; + pub use get_usdc_earn_program_details::get_usdc_earn_program_details; + mod get_staked_assets; + pub use get_estaking_rewards::get_estaking_rewards; + pub use get_masterchef_pending_rewards::get_masterchef_pending_rewards; + pub use get_masterchef_pool_apr::get_masterchef_pool_apr; + pub use get_masterchef_stable_stake_apr::get_masterchef_stable_stake_apr; + pub use get_membership_tier::get_membership_tier; + pub use get_perpetual_asset::get_perpetuals_assets; + pub use get_rewards::get_rewards; + pub use get_staked_assets::get_staked_assets; +} + +pub mod execute { + mod add_user_address_to_queue; + mod clean_up_storage; + pub use add_user_address_to_queue::add_user_address_to_queue; + pub use clean_up_storage::clean_up_storage; +} + +pub mod sudo { + mod update_metadata_prices; + pub use update_metadata_prices::update_metadata_prices; +} diff --git a/contracts/account-history-contract/src/action/query/all.rs b/contracts/account-history-contract/src/action/query/all.rs new file mode 100644 index 00000000..cfab3810 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/all.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::{Deps, Order, StdResult}; +use elys_bindings::{ + account_history::{msg::query_resp::GetAllResp, types::PortfolioBalanceSnapshot}, + types::PageRequest, + ElysQuery, +}; + +use crate::states::HISTORY; + +pub fn all(deps: Deps, pagination: Option) -> StdResult { + let snapshot_list: Vec<(String, PortfolioBalanceSnapshot)> = HISTORY + .prefix_range(deps.storage, None, None, Order::Ascending) + .filter_map(|res| res.ok()) + .map(|(key, value)| (key, value)) + .collect(); + + let (snapshot_list, page_response) = match pagination { + Some(pagination) => { + let (snapshot_list, page_resp) = pagination.filter(snapshot_list)?; + (snapshot_list, Some(page_resp)) + } + None => (snapshot_list, None), + }; + + Ok(GetAllResp { + snapshot_list, + pagination: page_response, + }) +} diff --git a/contracts/account-history-contract/src/action/query/exit_pool_estimation.rs b/contracts/account-history-contract/src/action/query/exit_pool_estimation.rs new file mode 100644 index 00000000..4638e508 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/exit_pool_estimation.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::{Decimal, Deps, StdError, StdResult, Uint128}; +use elys_bindings::query_resp::{PoolFilterType, QueryExitPoolEstimationResponse}; +use elys_bindings::{ElysQuerier, ElysQuery}; + +/** + * Given a pool id and a fiat amount, determine the assets the user will + * get in return. + */ +pub fn exit_pool_estimation( + deps: Deps, + pool_id: u64, + exit_fiat_amount: Decimal, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let pool_response = + querier.get_all_pools(Some(vec![pool_id]), PoolFilterType::FilterAll as i32, None)?; + let pool = pool_response + .pools + .ok_or(StdError::generic_err("Failed to fetch pool"))? + .first() + .ok_or(StdError::generic_err("Pool not found"))? + .clone(); + let share_price = pool + .share_usd_price + .ok_or(StdError::generic_err("Unable to get share price"))?; + let share_amount = exit_fiat_amount + .checked_div(share_price) + .unwrap_or_default(); + + Ok(querier + .exit_pool_estimation(pool_id, Uint128::new(share_amount.atomics().into())) + .map_err(|err| { + StdError::generic_err(format!( + "exit_pool_estimation: Error getting estimation from chain:{:?}", + err + )) + })?) +} diff --git a/contracts/account-history-contract/src/action/query/get_eden_boost_earn_program_details.rs b/contracts/account-history-contract/src/action/query/get_eden_boost_earn_program_details.rs new file mode 100644 index 00000000..8562f2fc --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_eden_boost_earn_program_details.rs @@ -0,0 +1,55 @@ +use super::*; +use crate::msg::query_resp::earn::GetEdenBoostEarnProgramResp; +use cosmwasm_std::Deps; +use elys_bindings::{ + account_history::types::{ + earn_detail::earn_detail::AprEdenBoost, earn_program::EdenBoostEarnProgram, ElysDenom, + }, + query_resp::{QueryAprResponse, Validator}, + ElysQuerier, ElysQuery, +}; + +pub fn get_eden_boost_earn_program_details( + deps: &Deps, + address: Option, + asset: String, + usdc_apr: QueryAprResponse, + eden_apr: QueryAprResponse, +) -> Result { + let bonding_period = 0; + let denom = ElysDenom::EdenBoost.as_str(); + if asset != denom.to_string() { + return Err(ContractError::AssetDenomError {}); + } + + let querier = ElysQuerier::new(&deps.querier); + + let mut data = EdenBoostEarnProgram::default(); + + data.bonding_period = bonding_period; + data.apr = AprEdenBoost { + uusdc: usdc_apr.apr, + ueden: eden_apr.apr, + }; + + if let Some(addr) = address { + let all_rewards = querier + .get_estaking_rewards(addr.clone()) + .unwrap_or_default(); + let program_rewards = all_rewards + .get_validator_rewards(Validator::EdenBoost) + .to_coin_values(&querier) + .unwrap_or_default() + .into_iter() + .map(|coin| coin.1) + .collect(); + let available = querier.get_balance(addr.clone(), asset.clone())?; + let staked = querier.get_staked_balance(addr, asset)?; + + data.available = Some(available.amount); + data.staked = Some(staked.amount); + data.rewards = Some(program_rewards); + } + + Ok(GetEdenBoostEarnProgramResp { data }) +} diff --git a/contracts/account-history-contract/src/action/query/get_eden_earn_program_details.rs b/contracts/account-history-contract/src/action/query/get_eden_earn_program_details.rs new file mode 100644 index 00000000..24197a9f --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_eden_earn_program_details.rs @@ -0,0 +1,71 @@ +use super::*; +use crate::msg::query_resp::earn::GetEdenEarnProgramResp; +use cosmwasm_std::{Decimal, Deps}; +use elys_bindings::account_history::types::earn_program::EdenEarnProgram; +use elys_bindings::account_history::types::{AprElys, ElysDenom}; +use elys_bindings::query_resp::Validator; +use elys_bindings::{query_resp::QueryAprResponse, ElysQuerier, ElysQuery}; + +pub fn get_eden_earn_program_details( + deps: &Deps, + address: Option, + asset: String, + uusdc_usd_price: Decimal, + uelys_price_in_uusdc: Decimal, + usdc_apr: QueryAprResponse, + eden_apr: QueryAprResponse, + edenb_apr: QueryAprResponse, +) -> Result { + let bonding_period = 0; + let denom = ElysDenom::Eden.as_str(); + if asset != denom.to_string() { + return Err(ContractError::AssetDenomError {}); + } + + let querier = ElysQuerier::new(&deps.querier); + + let mut data = EdenEarnProgram::default(); + + data.bonding_period = bonding_period; + data.apr = AprElys { + uusdc: usdc_apr.apr, + ueden: eden_apr.apr, + uedenb: edenb_apr.apr, + }; + + if let Some(addr) = address { + let all_rewards = querier + .get_estaking_rewards(addr.clone()) + .unwrap_or_default(); + let program_rewards = all_rewards + .get_validator_rewards(Validator::Eden) + .to_coin_values(&querier) + .unwrap_or_default() + .into_iter() + .map(|coin| coin.1) + .collect(); + + let mut available = querier.get_balance(addr.clone(), asset.clone())?; + let staked = querier.get_staked_balance(addr.clone(), asset.clone())?; + let vesting_info = querier.get_vesting_info(addr)?; + + // have value in usd + let mut available_in_usd = uelys_price_in_uusdc + .checked_mul( + Decimal::from_atomics(available.amount, 0).map_or(Decimal::zero(), |res| res), + ) + .unwrap_or_default(); + available_in_usd = available_in_usd + .checked_mul(uusdc_usd_price) + .unwrap_or_default(); + available.usd_amount = available_in_usd; + + data.available = Some(available); + data.staked = Some(staked); + data.rewards = Some(program_rewards); + data.vesting = vesting_info.vesting; + data.vesting_details = vesting_info.vesting_details; + } + + Ok(GetEdenEarnProgramResp { data }) +} diff --git a/contracts/account-history-contract/src/action/query/get_elys_earn_program_details.rs b/contracts/account-history-contract/src/action/query/get_elys_earn_program_details.rs new file mode 100644 index 00000000..95534488 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_elys_earn_program_details.rs @@ -0,0 +1,72 @@ +use super::*; +use crate::msg::query_resp::earn::GetElysEarnProgramResp; +use cosmwasm_std::{Decimal, Deps}; +use elys_bindings::{ + account_history::types::{earn_program::ElysEarnProgram, AprElys, ElysDenom}, + query_resp::QueryAprResponse, + ElysQuerier, ElysQuery, +}; + +pub fn get_elys_earn_program_details( + deps: &Deps, + address: Option, + asset: String, + uusdc_usd_price: Decimal, + uelys_price_in_uusdc: Decimal, + usdc_apr: QueryAprResponse, + eden_apr: QueryAprResponse, + edenb_apr: QueryAprResponse, +) -> Result { + let bonding_period = 14; + let denom = ElysDenom::Elys.as_str(); + if asset != denom.to_string() { + return Err(ContractError::AssetDenomError {}); + } + + let querier = ElysQuerier::new(&deps.querier); + + let mut data = ElysEarnProgram::default(); + + data.bonding_period = bonding_period; + data.apr = AprElys { + uusdc: usdc_apr.apr, + ueden: eden_apr.apr, + uedenb: edenb_apr.apr, + }; + + if let Some(addr) = address { + let all_rewards = querier + .get_estaking_rewards(addr.clone()) + .unwrap_or_default(); + let program_rewards = all_rewards + .get_elys_validators() + .to_coin_values(&querier) + .unwrap_or_default() + .into_iter() + .map(|coin| coin.1) + .collect(); + + let mut available = querier.get_balance(addr.clone(), asset.clone())?; + let staked = querier.get_staked_balance(addr.clone(), asset)?; + + let staked_positions = querier.get_staked_positions(addr.clone())?; + let unstaked_positions = querier.get_unstaked_positions(addr)?; + + // have value in usd + let mut available_in_usd = uelys_price_in_uusdc + .checked_mul(Decimal::from_atomics(available.amount, 0).unwrap_or_default()) + .unwrap_or_default(); + available_in_usd = available_in_usd + .checked_mul(uusdc_usd_price) + .unwrap_or_default(); + available.usd_amount = available_in_usd; + + data.available = Some(available); + data.staked = Some(staked); + data.rewards = Some(program_rewards); + data.staked_positions = staked_positions.staked_position; + data.unstaked_positions = unstaked_positions.unstaked_position; + } + + Ok(GetElysEarnProgramResp { data }) +} diff --git a/contracts/account-history-contract/src/action/query/get_estaking_rewards.rs b/contracts/account-history-contract/src/action/query/get_estaking_rewards.rs new file mode 100644 index 00000000..dac85c21 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_estaking_rewards.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::account_history::msg::query_resp::estaking::GetEstakingRewardsResponse; +use elys_bindings::trade_shield::types::CoinValue; +use elys_bindings::{ElysQuerier, ElysQuery}; + +/** + * Given a user address, gets the Estaking rewards available. + */ +pub fn get_estaking_rewards( + deps: Deps, + address: String, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let response = querier.get_estaking_rewards(address).unwrap_or_default(); + + let fiat_coins = response.to_coin_values(&querier); + let rewards = fiat_coins + .unwrap_or_default() + .into_iter() + .map(|(_, v)| v.clone()) + .collect::>(); + + Ok(GetEstakingRewardsResponse { + rewards, + total: response.total, + }) +} diff --git a/contracts/account-history-contract/src/action/query/get_liquid_assets.rs b/contracts/account-history-contract/src/action/query/get_liquid_assets.rs new file mode 100644 index 00000000..cfc8c23c --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_liquid_assets.rs @@ -0,0 +1,53 @@ +use cosmwasm_std::{Decimal, Deps, Env, StdResult}; +use elys_bindings::{trade_shield::types::CoinValue, ElysQuerier, ElysQuery}; + +use crate::{ + msg::query_resp::{GetLiquidAssetsResp, LiquidAsset}, + types::AccountSnapshotGenerator, +}; + +pub fn get_liquid_assets( + deps: Deps, + user_address: String, + _env: Env, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let generator = AccountSnapshotGenerator::new(&deps)?; + + let liquid_asset = generator.get_liquid_assets(&deps, &querier, &user_address)?; + + let mut liquid_assets: Vec = vec![]; + + for total in liquid_asset.total_value_per_asset.clone() { + let (available_amount, available_value) = + get_info(&liquid_asset.available_asset_balance, &total.denom); + let (in_order_amount, in_order_value) = + get_info(&liquid_asset.in_orders_asset_balance, &total.denom); + + liquid_assets.push(LiquidAsset { + denom: total.denom, + price: total.price, + available_amount, + available_value, + in_order_amount, + in_order_value, + total_amount: total.amount_token, + total_value: total.amount_usd, + }); + } + + Ok(GetLiquidAssetsResp { + liquid_assets, + total_liquid_asset_balance: liquid_asset.total_liquid_asset_balance.clone(), + }) +} + +fn get_info(list_info: &Vec, denom: &String) -> (Decimal, Decimal) { + list_info + .iter() + .find(|info| &info.denom == denom) + .map_or((Decimal::zero(), Decimal::zero()), |data| { + (data.amount_token, data.amount_usd) + }) +} diff --git a/contracts/account-history-contract/src/action/query/get_masterchef_pending_rewards.rs b/contracts/account-history-contract/src/action/query/get_masterchef_pending_rewards.rs new file mode 100644 index 00000000..4b995663 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_masterchef_pending_rewards.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::account_history::msg::query_resp::masterchef::GetMasterchefUserPendingRewardResponse; +use elys_bindings::{ElysQuerier, ElysQuery}; + +pub fn get_masterchef_pending_rewards( + deps: Deps, + address: String, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let resp = querier.get_masterchef_pending_rewards(address)?; + + let (rewards, total_rewards) = resp.to_coin_values(&querier)?; + + Ok(GetMasterchefUserPendingRewardResponse { + rewards, + total_rewards, + }) +} diff --git a/contracts/account-history-contract/src/action/query/get_masterchef_pool_apr.rs b/contracts/account-history-contract/src/action/query/get_masterchef_pool_apr.rs new file mode 100644 index 00000000..2af95c40 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_masterchef_pool_apr.rs @@ -0,0 +1,17 @@ +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::{ + account_history::msg::query_resp::masterchef::MasterChefPoolAprResponse, ElysQuerier, ElysQuery, +}; + +pub fn get_masterchef_pool_apr( + deps: Deps, + pool_ids: Vec, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let resp = querier.get_masterchef_pool_apr(pool_ids)?; + + Ok(MasterChefPoolAprResponse { + data: resp.to_decimal(), + }) +} diff --git a/contracts/account-history-contract/src/action/query/get_masterchef_stable_stake_apr.rs b/contracts/account-history-contract/src/action/query/get_masterchef_stable_stake_apr.rs new file mode 100644 index 00000000..5c4129ac --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_masterchef_stable_stake_apr.rs @@ -0,0 +1,13 @@ +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::{query_resp::QueryStableStakeAprResponse, ElysQuerier, ElysQuery}; + +pub fn get_masterchef_stable_stake_apr( + deps: Deps, + denom: String, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let resp = querier.get_masterchef_stable_stake_apr(denom)?; + + Ok(resp) +} diff --git a/contracts/account-history-contract/src/action/query/get_membership_tier.rs b/contracts/account-history-contract/src/action/query/get_membership_tier.rs new file mode 100644 index 00000000..49ca73e8 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_membership_tier.rs @@ -0,0 +1,23 @@ +use crate::msg::query_resp::MembershipTierResponse; +use cosmwasm_std::{Deps, Env, StdResult}; +use elys_bindings::ElysQuery; + +use super::user_snapshots; + +pub fn get_membership_tier( + env: Env, + deps: Deps, + user_address: String, +) -> StdResult { + let user_snapshots_list = user_snapshots(env, deps, user_address)?; + + match user_snapshots_list + .iter() + .min_by_key(|&snapshot| snapshot.total_balance_usd) + { + Some(snapshot) => Ok(MembershipTierResponse::calc( + snapshot.total_balance_usd.to_owned(), + )), + None => return Ok(MembershipTierResponse::zero()), + } +} diff --git a/contracts/account-history-contract/src/action/query/get_perpetual_asset.rs b/contracts/account-history-contract/src/action/query/get_perpetual_asset.rs new file mode 100644 index 00000000..172bf2fe --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_perpetual_asset.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::{Deps, Env, StdResult}; +use elys_bindings::{account_history::types::PerpetualAssets, ElysQuery}; + +use crate::types::AccountSnapshotGenerator; + +pub fn get_perpetuals_assets( + deps: Deps, + address: String, + _env: Env, +) -> StdResult { + let generator = AccountSnapshotGenerator::new(&deps)?; + + generator.get_perpetuals(&deps, &address) +} diff --git a/contracts/account-history-contract/src/action/query/get_pool_balances.rs b/contracts/account-history-contract/src/action/query/get_pool_balances.rs new file mode 100644 index 00000000..9cc73d5b --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_pool_balances.rs @@ -0,0 +1,16 @@ +use crate::types::AccountSnapshotGenerator; +use cosmwasm_std::{Deps, Env, StdResult}; +use elys_bindings::query_resp::QueryUserPoolResponse; +use elys_bindings::ElysQuery; + +pub fn get_pool_balances( + deps: Deps, + address: String, + _env: Env, +) -> StdResult { + let generator = AccountSnapshotGenerator::new(&deps)?; + + let pool_balances_response = generator.get_pool_balances(&deps, &address)?; + + Ok(pool_balances_response) +} diff --git a/contracts/account-history-contract/src/action/query/get_pools.rs b/contracts/account-history-contract/src/action/query/get_pools.rs new file mode 100644 index 00000000..86771758 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_pools.rs @@ -0,0 +1,16 @@ +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::{ + query_resp::PoolFilterType, query_resp::QueryEarnPoolResponse, types::PageRequest, ElysQuerier, + ElysQuery, +}; + +pub fn get_pools( + deps: Deps, + pool_ids: Option>, + filter_type: PoolFilterType, + pagination: Option, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + let resp = querier.get_all_pools(pool_ids, filter_type as i32, pagination)?; + Ok(resp) +} diff --git a/contracts/account-history-contract/src/action/query/get_pools_apr.rs b/contracts/account-history-contract/src/action/query/get_pools_apr.rs new file mode 100644 index 00000000..91324daf --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_pools_apr.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::{query_resp::QueryIncentivePoolAprsResponse, ElysQuerier, ElysQuery}; + +pub fn get_pools_apr( + deps: Deps, + pool_ids: Option>, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + let resp = querier.get_pools_apr(pool_ids)?; + Ok(resp) +} diff --git a/contracts/account-history-contract/src/action/query/get_portfolio.rs b/contracts/account-history-contract/src/action/query/get_portfolio.rs new file mode 100644 index 00000000..84466d68 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_portfolio.rs @@ -0,0 +1,11 @@ +use crate::{msg::query_resp::GetPortfolioResp, types::AccountSnapshotGenerator}; +use cosmwasm_std::{Deps, Env, SignedDecimal256, StdResult}; +use elys_bindings::{ElysQuerier, ElysQuery}; + +pub fn get_portfolio( + deps: Deps, + user_address: String, + env: Env, +) -> StdResult { + unimplemented!() +} diff --git a/contracts/account-history-contract/src/action/query/get_rewards.rs b/contracts/account-history-contract/src/action/query/get_rewards.rs new file mode 100644 index 00000000..ec1e1aad --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_rewards.rs @@ -0,0 +1,22 @@ +use crate::{msg::query_resp::GetRewardsResp, types::AccountSnapshotGenerator}; + +use cosmwasm_std::{Deps, Env, StdResult}; + +use elys_bindings::ElysQuery; + +pub fn get_rewards( + deps: Deps, + user_address: String, + _env: Env, +) -> StdResult { + let generator = AccountSnapshotGenerator::new(&deps)?; + + let rewards_response = generator.get_rewards(&deps, &user_address)?; + + let resp = GetRewardsResp { + rewards_map: rewards_response.rewards_map.clone(), + rewards: rewards_response.rewards.clone(), + }; + + Ok(resp) +} diff --git a/contracts/account-history-contract/src/action/query/get_staked_assets.rs b/contracts/account-history-contract/src/action/query/get_staked_assets.rs new file mode 100644 index 00000000..c898c1a4 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_staked_assets.rs @@ -0,0 +1,24 @@ +use crate::msg::query_resp::StakedAssetsResponse; +use crate::types::AccountSnapshotGenerator; +use cosmwasm_std::{DecCoin, Decimal256, Deps, Env, StdResult}; +use elys_bindings::ElysQuery; + +pub fn get_staked_assets( + deps: Deps, + address: Option, + _env: Env, +) -> StdResult { + let generator = AccountSnapshotGenerator::new(&deps)?; + + let staked_assets_response = generator.get_staked_assets(&deps, address)?; + + Ok(StakedAssetsResponse { + total_staked_balance: DecCoin::new( + Decimal256::from(staked_assets_response.total_staked_balance.amount), + generator.metadata.usdc_denom, + ), + staked_assets: staked_assets_response.staked_assets.to_owned(), + total_balance: staked_assets_response.total_balance, + balance_break_down: staked_assets_response.balance_break_down, + }) +} diff --git a/contracts/account-history-contract/src/action/query/get_usdc_earn_program_details.rs b/contracts/account-history-contract/src/action/query/get_usdc_earn_program_details.rs new file mode 100644 index 00000000..a6de98f1 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/get_usdc_earn_program_details.rs @@ -0,0 +1,66 @@ +use super::*; +use crate::msg::query_resp::earn::GetUsdcEarnProgramResp; +use cosmwasm_std::{Decimal, Deps}; +use elys_bindings::{ + account_history::types::{earn_program::UsdcEarnProgram, AprUsdc, ElysDenom}, + ElysQuerier, ElysQuery, +}; + +pub fn get_usdc_earn_program_details( + deps: &Deps, + address: Option, + usdc_denom: String, + usdc_base_denom: String, + uusdc_usd_price: Decimal, +) -> Result { + let pool_id = 32767u64; + let bonding_period = 0; + + let querier = ElysQuerier::new(&deps.querier); + + let eden_apr = querier + .get_masterchef_stable_stake_apr(ElysDenom::Eden.as_str().to_string()) + .unwrap_or_default(); + let usdc_apr = querier + .get_masterchef_stable_stake_apr(ElysDenom::Usdc.as_str().to_string()) + .unwrap_or_default(); + + let mut data = UsdcEarnProgram::default(); + + data.bonding_period = bonding_period; + data.apr = AprUsdc { + uusdc: usdc_apr.apr, + ueden: eden_apr.apr, + }; + + if let Some(addr) = address { + let rewards = querier + .get_masterchef_pending_rewards(addr.clone()) + .unwrap_or_default(); + let coin_values_rewards = rewards.to_coin_values(&querier).unwrap_or_default(); + let pool_rewards = coin_values_rewards.0[&pool_id].clone(); + + let mut available = querier.get_balance(addr.clone(), usdc_denom)?; + available.usd_amount = available + .usd_amount + .checked_mul(uusdc_usd_price) + .unwrap_or_default(); + + let mut staked = querier.get_staked_balance(addr, usdc_base_denom)?; + + let mut borrowed = querier.get_borrowed_balance()?; + borrowed.usd_amount = borrowed + .usd_amount + .checked_mul(uusdc_usd_price) + .unwrap_or_default(); + + staked.lockups = None; + + data.available = Some(available); + data.staked = Some(staked); + data.rewards = Some(pool_rewards); + data.borrowed = Some(borrowed); + } + + Ok(GetUsdcEarnProgramResp { data }) +} diff --git a/contracts/account-history-contract/src/action/query/join_pool_estimation.rs b/contracts/account-history-contract/src/action/query/join_pool_estimation.rs new file mode 100644 index 00000000..105bf633 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/join_pool_estimation.rs @@ -0,0 +1,12 @@ +use cosmwasm_std::{Coin, Deps, StdResult}; +use elys_bindings::{query_resp::QueryJoinPoolEstimationResponse, ElysQuerier, ElysQuery}; + +pub fn join_pool_estimation( + deps: Deps, + pool_id: u64, + amounts_in: Vec, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + let resp = querier.join_pool_estimation(pool_id, amounts_in)?; + Ok(resp) +} diff --git a/contracts/account-history-contract/src/action/query/last_snapshot.rs b/contracts/account-history-contract/src/action/query/last_snapshot.rs new file mode 100644 index 00000000..93b3fe40 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/last_snapshot.rs @@ -0,0 +1,20 @@ +use cosmwasm_std::{Deps, Env, StdResult}; +use elys_bindings::{account_history::types::PortfolioBalanceSnapshot, ElysQuery}; + +use crate::{states::HISTORY, utils::get_today}; + +pub fn last_snapshot( + deps: Deps, + user_address: String, + env: Env, +) -> StdResult { + let today = get_today(&env.block); + let key = today + &user_address; + + let snapshot = match HISTORY.may_load(deps.storage, &key)? { + Some(snapshot) => snapshot.clone(), + None => PortfolioBalanceSnapshot::default(), + }; + + Ok(snapshot) +} diff --git a/contracts/account-history-contract/src/action/query/params.rs b/contracts/account-history-contract/src/action/query/params.rs new file mode 100644 index 00000000..96c89388 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/params.rs @@ -0,0 +1,29 @@ +use crate::{ + msg::query_resp::ParamsResp, + states::{ + DELETE_EPOCH, DELETE_OLD_DATA_ENABLED, EXPIRATION, METADATA, PROCESSED_ACCOUNT_PER_BLOCK, + TRADE_SHIELD_ADDRESS, UPDATE_ACCOUNT_ENABLED, + }, +}; +use cosmwasm_std::{Deps, StdResult}; +use elys_bindings::ElysQuery; + +pub fn params(deps: Deps) -> StdResult { + let expiration = EXPIRATION.load(deps.storage)?; + let processed_account_per_block = PROCESSED_ACCOUNT_PER_BLOCK.load(deps.storage)?; + let trade_shield_address = TRADE_SHIELD_ADDRESS.load(deps.storage)?; + let update_account_enabled = UPDATE_ACCOUNT_ENABLED.load(deps.storage)?; + let metadata = METADATA.load(deps.storage)?; + let delete_old_data_enabled = DELETE_OLD_DATA_ENABLED.load(deps.storage)?; + let delete_epoch = DELETE_EPOCH.load(deps.storage)?; + + Ok(ParamsResp { + expiration, + processed_account_per_block, + update_account_enabled, + trade_shield_address, + metadata, + delete_old_data_enabled, + delete_epoch, + }) +} diff --git a/contracts/account-history-contract/src/action/query/pool_asset_estimation.rs b/contracts/account-history-contract/src/action/query/pool_asset_estimation.rs new file mode 100644 index 00000000..7312c293 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/pool_asset_estimation.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::{DecCoin, Decimal, Decimal256, Deps, StdError, StdResult}; +use elys_bindings::query_resp::{PoolFilterType, QueryPoolAssetEstimationResponse}; +use elys_bindings::{ElysQuerier, ElysQuery}; +use std::collections::HashMap; + +/** + * Given an asset and a pool, determine the quantity of every other asset in the pool + * needed to keep the pool balanced. + * Useful to use in FE forms before calling join pool. + */ +pub fn pool_asset_estimation( + deps: Deps, + pool_id: u64, + asset: DecCoin, +) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + let asset_denom = asset.denom.to_string(); + + let pool_response = + querier.get_all_pools(Some(vec![pool_id]), PoolFilterType::FilterAll as i32, None)?; + let pool = match pool_response.pools { + Some(pools) => { + if let Some(pool) = pools.first() { + pool.clone() + } else { + return Err(StdError::generic_err("Pool not found")); + } + } + None => return Err(StdError::generic_err("Failed to fetch pool")), + }; + + let asset_usd_price = querier + .get_asset_price(asset.denom) + .unwrap_or(Decimal::zero()); + let asset_in_usd = asset.amount * Decimal256::from(asset_usd_price); + + // Ensure the current_pool_ratio is populated + let current_pool_ratio = match &pool.current_pool_ratio { + Some(ratio) => ratio, + None => return Err(StdError::generic_err("Current pool ratio is not populated")), + }; + + let asset_ratio = Decimal256::from(current_pool_ratio.get(&asset_denom).unwrap().clone()); + let total_in_usd = asset_in_usd / asset_ratio; + + let mut estimations = HashMap::new(); + for (denom, _) in current_pool_ratio.iter() { + if denom.to_string() != asset_denom { + let usd_price = querier.get_asset_price(denom).unwrap_or(Decimal::zero()); + let dec_price = Decimal256::from(usd_price); + let ratio = Decimal256::from(current_pool_ratio.get(denom).unwrap().clone()); + + let quantity = (total_in_usd * ratio) / dec_price; + + estimations.insert(denom.clone(), quantity); + } + } + + // Return the result + Ok(QueryPoolAssetEstimationResponse { + amounts: estimations, + }) +} diff --git a/contracts/account-history-contract/src/action/query/user_snapshots.rs b/contracts/account-history-contract/src/action/query/user_snapshots.rs new file mode 100644 index 00000000..a8d9c4ac --- /dev/null +++ b/contracts/account-history-contract/src/action/query/user_snapshots.rs @@ -0,0 +1,46 @@ +use chrono::NaiveDateTime; +use cosmwasm_std::{Deps, Env, StdResult, Timestamp}; +use cw_utils::Expiration; +use elys_bindings::{account_history::types::PortfolioBalanceSnapshot, ElysQuery}; + +use crate::{states::HISTORY, types::AccountSnapshotGenerator}; + +pub fn user_snapshots( + env: Env, + deps: Deps, + user_address: String, +) -> StdResult> { + let generator = AccountSnapshotGenerator::new(&deps)?; + let expiration = match generator.expiration { + Expiration::AtHeight(h) => Timestamp::from_seconds(h * 3), // since a block is created every 3 seconds + Expiration::AtTime(t) => t.clone(), + _ => panic!("never expire"), + }; + + let mut day_date = if env.block.time.seconds() < expiration.seconds() { + Timestamp::from_seconds(0) + } else { + env.block + .time + .minus_seconds(expiration.seconds()) + .plus_days(1) + }; + + let mut user_snapshots_list: Vec = vec![]; + + while day_date <= env.block.time { + let date = NaiveDateTime::from_timestamp_opt(day_date.seconds() as i64, 0) + .expect("Failed to convert block time to date") + .format("%Y-%m-%d") + .to_string(); + let key = date + &user_address; + + if let Some(portfolio) = HISTORY.may_load(deps.storage, &key)? { + user_snapshots_list.push(portfolio.to_owned()) + } + + day_date = day_date.plus_days(1); + } + + Ok(user_snapshots_list) +} diff --git a/contracts/account-history-contract/src/action/query/user_value.rs b/contracts/account-history-contract/src/action/query/user_value.rs new file mode 100644 index 00000000..6d253c81 --- /dev/null +++ b/contracts/account-history-contract/src/action/query/user_value.rs @@ -0,0 +1,23 @@ +use crate::msg::query_resp::UserValueResponse; +use cosmwasm_std::{Deps, Env, StdError, StdResult}; +use elys_bindings::ElysQuery; + +use super::user_snapshots; + +pub fn user_value( + env: Env, + deps: Deps, + user_address: String, +) -> StdResult { + let user_snapshots_list = user_snapshots(env, deps, user_address.clone())?; + + match user_snapshots_list + .iter() + .min_by_key(|&portfolio| portfolio.total_balance_usd) + { + Some(portfolio) => Ok(UserValueResponse { + value: portfolio.to_owned(), + }), + None => Err(StdError::not_found(format!("user :{user_address}"))), + } +} diff --git a/contracts/account-history-contract/src/action/sudo/update_metadata_prices.rs b/contracts/account-history-contract/src/action/sudo/update_metadata_prices.rs new file mode 100644 index 00000000..3d5293d5 --- /dev/null +++ b/contracts/account-history-contract/src/action/sudo/update_metadata_prices.rs @@ -0,0 +1,14 @@ +use crate::states::METADATA; +use cosmwasm_std::{DepsMut, Response, StdResult}; +use elys_bindings::{ElysMsg, ElysQuerier, ElysQuery}; + +pub fn update_metadata_prices(deps: DepsMut) -> StdResult> { + let querier = ElysQuerier::new(&deps.querier); + + // update metadata prices + let mut metadata = METADATA.load(deps.storage)?; + metadata = metadata.update_prices(&querier)?; + METADATA.save(deps.storage, &metadata)?; + + Ok(Response::default()) +} diff --git a/contracts/account-history-contract/src/bin/account-history-contract-schema.rs b/contracts/account-history-contract/src/bin/account-history-contract-schema.rs new file mode 100644 index 00000000..509eeeef --- /dev/null +++ b/contracts/account-history-contract/src/bin/account-history-contract-schema.rs @@ -0,0 +1,11 @@ +use account_history_contract::msg::{InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +use cosmwasm_std::Empty; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: Empty, + query: QueryMsg + } +} diff --git a/contracts/account-history-contract/src/entry_point/execute.rs b/contracts/account-history-contract/src/entry_point/execute.rs new file mode 100644 index 00000000..de7a742b --- /dev/null +++ b/contracts/account-history-contract/src/entry_point/execute.rs @@ -0,0 +1,76 @@ +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response, StdError, StdResult}; +use elys_bindings::{account_history::msg::ExecuteMsg, ElysMsg, ElysQuery}; + +use crate::{ + action::execute::{add_user_address_to_queue, clean_up_storage}, + states::{ + DELETE_EPOCH, DELETE_OLD_DATA_ENABLED, HISTORY, OLD_HISTORY_2, PARAMS_ADMIN, + PROCESSED_ACCOUNT_PER_BLOCK, TRADE_SHIELD_ADDRESS, UPDATE_ACCOUNT_ENABLED, + }, +}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + mut deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> StdResult> { + match msg { + ExecuteMsg::AddUserAddressToQueue { user_address } => { + let trade_shield_address = match TRADE_SHIELD_ADDRESS.load(deps.storage)? { + Some(addr) => addr, + None => return Err(StdError::generic_err("Unauthorized")), + }; + if trade_shield_address.as_str() != info.sender.as_str() { + return Err(StdError::generic_err("Unauthorized")); + } + add_user_address_to_queue(deps, user_address)?; + Ok(Response::new()) + } + ExecuteMsg::ChangeParams { + update_account_enabled, + processed_account_per_block, + delete_old_data_enabled, + delete_epoch, + } => { + let params_admin = PARAMS_ADMIN.load(deps.storage)?; + + if params_admin.as_str() != info.sender.as_str() { + return Err(StdError::generic_err("Unauthorized")); + } + + if let Some(processed_account_per_block) = processed_account_per_block { + PROCESSED_ACCOUNT_PER_BLOCK.save(deps.storage, &processed_account_per_block)?; + } + + if let Some(update_account_enabled) = update_account_enabled { + UPDATE_ACCOUNT_ENABLED.save(deps.storage, &update_account_enabled)?; + } + + if let Some(delete_old_data_enabled) = delete_old_data_enabled { + DELETE_OLD_DATA_ENABLED.save(deps.storage, &delete_old_data_enabled)?; + } + + if let Some(delete_epoch) = delete_epoch { + DELETE_EPOCH.save(deps.storage, &delete_epoch)?; + } + Ok(Response::new()) + } + ExecuteMsg::CleanStorage { limit } => { + if info.sender != PARAMS_ADMIN.load(deps.storage)? { + return Err(StdError::generic_err("Unauthorized")); + } + let resp = clean_up_storage(&mut deps, limit)?; + Ok(resp) + } + ExecuteMsg::CleanStorageBulk {} => { + if info.sender != PARAMS_ADMIN.load(deps.storage)? { + return Err(StdError::generic_err("Unauthorized")); + } + HISTORY.clear(deps.storage); + OLD_HISTORY_2.clear(deps.storage); + Ok(Response::new()) + } + } +} diff --git a/contracts/account-history-contract/src/entry_point/instantiate.rs b/contracts/account-history-contract/src/entry_point/instantiate.rs new file mode 100644 index 00000000..0eaa7550 --- /dev/null +++ b/contracts/account-history-contract/src/entry_point/instantiate.rs @@ -0,0 +1,63 @@ +use cw2::set_contract_version; +use cw_utils::Expiration; +use elys_bindings::account_history::types::Metadata; + +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response, StdResult, Timestamp}; +use elys_bindings::trade_shield::states::PARAMS_ADMIN; +use elys_bindings::{ElysMsg, ElysQuerier, ElysQuery}; + +use crate::msg::InstantiateMsg; +use crate::states::{ + DELETE_EPOCH, DELETE_OLD_DATA_ENABLED, EXPIRATION, METADATA, PROCESSED_ACCOUNT_PER_BLOCK, + TRADE_SHIELD_ADDRESS, UPDATE_ACCOUNT_ENABLED, +}; + +// Version info, for migration info +pub const CONTRACT_NAME: &str = "account-history"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // EXPIRATION + match msg.expiration { + Some(expiration) => EXPIRATION.save(deps.storage, &expiration)?, + None => EXPIRATION.save( + deps.storage, + &Expiration::AtTime(Timestamp::from_seconds(3 * 24 * 60 * 60)), + )?, + }; + + // PROCESSED_ACCOUNT_PER_BLOCK + let limit = match msg.limit { + Some(limit) => limit, + None => 1, + }; + + PARAMS_ADMIN.save(deps.storage, &info.sender.to_string())?; + + PROCESSED_ACCOUNT_PER_BLOCK.save(deps.storage, &limit)?; + + // TRADESHIELD ADDRESS + TRADE_SHIELD_ADDRESS.save(deps.storage, &msg.trade_shield_address)?; + + // METADATA + let querier = ElysQuerier::new(&deps.querier); + + let metadata = Metadata::collect(&querier)?; + + METADATA.save(deps.storage, &metadata)?; + + UPDATE_ACCOUNT_ENABLED.save(deps.storage, &true)?; + + DELETE_OLD_DATA_ENABLED.save(deps.storage, &true)?; + DELETE_EPOCH.save(deps.storage, &1000u64)?; + + // RESPONSE + Ok(Response::new()) +} diff --git a/contracts/account-history-contract/src/entry_point/migrate.rs b/contracts/account-history-contract/src/entry_point/migrate.rs new file mode 100644 index 00000000..ea48b240 --- /dev/null +++ b/contracts/account-history-contract/src/entry_point/migrate.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{entry_point, DepsMut, Env, Response, StdResult, Timestamp}; +use cw_utils::Expiration; +use elys_bindings::account_history::msg::MigrationMsg; +// use elys_bindings::account_history::types::Metadata; +use elys_bindings::{ElysMsg, ElysQuery}; + +use crate::states::{DELETE_EPOCH, DELETE_OLD_DATA_ENABLED, PROCESSED_ACCOUNT_PER_BLOCK}; +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + msg: MigrationMsg, +) -> StdResult> { + // PROCESSED_ACCOUNT_PER_BLOCK + let limit = match msg.limit { + Some(limit) => limit, + None => 1, + }; + + PROCESSED_ACCOUNT_PER_BLOCK.save(deps.storage, &limit)?; + DELETE_OLD_DATA_ENABLED.save(deps.storage, &true)?; + DELETE_EPOCH.save(deps.storage, &1000u64)?; + + // METADATA + // let querier = ElysQuerier::new(&deps.querier); + + // let metadata = Metadata::collect(&querier)?; + + // METADATA.save(deps.storage, &metadata)?; + + // RESPONSE + + Ok(Response::new()) +} diff --git a/contracts/account-history-contract/src/entry_point/mod.rs b/contracts/account-history-contract/src/entry_point/mod.rs new file mode 100644 index 00000000..db29648a --- /dev/null +++ b/contracts/account-history-contract/src/entry_point/mod.rs @@ -0,0 +1,11 @@ +mod execute; +mod instantiate; +mod migrate; +mod query; +mod sudo; + +pub use execute::execute; +pub use instantiate::instantiate; +pub use migrate::migrate; +pub use query::query; +pub use sudo::sudo; diff --git a/contracts/account-history-contract/src/entry_point/query.rs b/contracts/account-history-contract/src/entry_point/query.rs new file mode 100644 index 00000000..a629aaf2 --- /dev/null +++ b/contracts/account-history-contract/src/entry_point/query.rs @@ -0,0 +1,276 @@ +use crate::{ + action::query::{ + get_eden_boost_earn_program_details, get_eden_earn_program_details, + get_elys_earn_program_details, get_estaking_rewards, get_liquid_assets, + get_masterchef_pending_rewards, get_masterchef_pool_apr, get_masterchef_stable_stake_apr, + get_membership_tier, get_perpetuals_assets, get_pool_balances, get_rewards, + get_staked_assets, get_usdc_earn_program_details, + }, + states::{HISTORY, OLD_HISTORY_2, USER_ADDRESS_QUEUE}, + types::AccountSnapshotGenerator, +}; + +#[cfg(feature = "debug")] +use crate::action::query::{ + all, exit_pool_estimation, get_pools, join_pool_estimation, last_snapshot, params, + pool_asset_estimation, user_snapshots, user_value, +}; + +use cosmwasm_std::{entry_point, to_json_binary, Binary, Deps, Env, StdResult}; +use cw2::CONTRACT; +use elys_bindings::{ + account_history::{msg::query_resp::StorageSizeResp, types::ElysDenom}, + query_resp::QueryAprResponse, + ElysQuerier, ElysQuery, +}; + +use crate::msg::QueryMsg; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + use QueryMsg::*; + + match msg { + Accounts { pagination } => to_json_binary(&{ + let querrier = ElysQuerier::new(&deps.querier); + + let resp = querrier.accounts(pagination)?; + resp + }), + + GetLiquidAssets { user_address } => { + to_json_binary(&get_liquid_assets(deps, user_address, env)?) + } + GetStakedAssets { user_address } => { + to_json_binary(&get_staked_assets(deps, user_address, env)?) + } + GetPoolBalances { user_address } => { + to_json_binary(&get_pool_balances(deps, user_address, env)?) + } + GetRewards { user_address } => to_json_binary(&get_rewards(deps, user_address, env)?), + + GetMembershipTier { user_address } => { + to_json_binary(&get_membership_tier(env, deps, user_address)?) + } + GetPerpetualAssets { user_address } => { + to_json_binary(&get_perpetuals_assets(deps, user_address, env)?) + } + GetAssetPrice { asset } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_asset_price(asset)?) + } + + GetLiquidityPools { + pool_ids, + filter_type, + pagination, + } => to_json_binary(&get_pools(deps, pool_ids, filter_type, pagination)?), + + JoinPoolEstimation { + pool_id, + amounts_in, + } => to_json_binary(&join_pool_estimation(deps, pool_id, amounts_in)?), + + PoolAssetEstimation { pool_id, amount } => { + to_json_binary(&pool_asset_estimation(deps, pool_id, amount)?) + } + + ExitPoolEstimation { + pool_id, + exit_fiat_amount, + } => to_json_binary(&exit_pool_estimation(deps, pool_id, exit_fiat_amount)?), + + GetAssetPriceFromDenomInToDenomOut { + denom_in, + denom_out, + } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary( + &querier.get_asset_price_from_denom_in_to_denom_out(denom_in, denom_out)?, + ) + } + + GetEstakingRewards { address } => to_json_binary(&get_estaking_rewards(deps, address)?), + + GetMasterchefParams {} => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.masterchef_params()?) + } + + GetMasterchefPoolInfo { pool_id } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.masterchef_pool_info(pool_id)?) + } + + GetMasterchefPendingRewards { address } => { + to_json_binary(&get_masterchef_pending_rewards(deps, address)?) + } + + GetMasterChefPoolApr { pool_ids } => { + to_json_binary(&get_masterchef_pool_apr(deps, pool_ids)?) + } + + GetMasterchefStableStakeApr { denom } => { + to_json_binary(&get_masterchef_stable_stake_apr(deps, denom)?) + } + + // debug only + #[cfg(feature = "debug")] + Params {} => to_json_binary(¶ms(deps)?), + #[cfg(feature = "debug")] + All { pagination } => to_json_binary(&all(deps, pagination)?), + #[cfg(feature = "debug")] + UserSnapshots { user_address } => to_json_binary(&user_snapshots(env, deps, user_address)?), + #[cfg(feature = "debug")] + LastSnapshot { user_address } => to_json_binary(&last_snapshot(deps, user_address, env)?), + #[cfg(feature = "debug")] + UserValue { user_address } => to_json_binary(&user_value(env, deps, user_address)?), + #[cfg(feature = "debug")] + CommitmentStakedPositions { delegator_address } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_staked_positions(delegator_address)?) + } + #[cfg(feature = "debug")] + CommitmentUnStakedPositions { delegator_address } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_unstaked_positions(delegator_address)?) + } + #[cfg(feature = "debug")] + CommitmentStakedBalanceOfDenom { address, denom } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_staked_balance(address, denom)?) + } + #[cfg(feature = "debug")] + StableStakeBalanceOfBorrow {} => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_borrowed_balance()?) + } + #[cfg(feature = "debug")] + StableStakeParams {} => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_stable_stake_params()?) + } + #[cfg(feature = "debug")] + CommitmentVestingInfo { address } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_vesting_info(address)?) + } + #[cfg(feature = "debug")] + Balance { address, denom } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_balance(address, denom)?) + } + #[cfg(feature = "debug")] + AmmPriceByDenom { token_in, discount } => { + let querier = ElysQuerier::new(&deps.querier); + to_json_binary(&querier.get_amm_price_by_denom(token_in, discount)?) + } + #[cfg(feature = "debug")] + GetEdenEarnProgramDetails { address } => { + let querier = ElysQuerier::new(&deps.querier); + let aprs = querier.get_incentive_aprs().unwrap_or_default(); + + let generator = AccountSnapshotGenerator::new(&deps)?; + let program = get_eden_earn_program_details( + &deps, + Some(address.to_owned()), + ElysDenom::Eden.as_str().to_string(), + generator.metadata.uusdc_usd_price, + generator.metadata.uelys_price_in_uusdc, + QueryAprResponse { + apr: aprs.usdc_apr_eden, + }, + QueryAprResponse { + apr: aprs.eden_apr_eden, + }, + QueryAprResponse { + apr: aprs.edenb_apr_eden, + }, + ) + .unwrap_or_default(); + to_json_binary(&program) + } + #[cfg(feature = "debug")] + GetEdenBoostEarnProgramDetails { address } => { + let querier = ElysQuerier::new(&deps.querier); + let aprs = querier.get_incentive_aprs().unwrap_or_default(); + let program = get_eden_boost_earn_program_details( + &deps, + Some(address.to_owned()), + ElysDenom::EdenBoost.as_str().to_string(), + QueryAprResponse { + apr: aprs.usdc_apr_edenb, + }, + QueryAprResponse { + apr: aprs.eden_apr_edenb, + }, + ) + .unwrap_or_default(); + + to_json_binary(&program) + } + #[cfg(feature = "debug")] + GetElysEarnProgramDetails { address } => { + let querier = ElysQuerier::new(&deps.querier); + let aprs = querier.get_incentive_aprs().unwrap_or_default(); + + let generator = AccountSnapshotGenerator::new(&deps)?; + let program = get_elys_earn_program_details( + &deps, + Some(address.to_owned()), + ElysDenom::Elys.as_str().to_string(), + generator.metadata.uusdc_usd_price, + generator.metadata.uelys_price_in_uusdc, + QueryAprResponse { + apr: aprs.usdc_apr_elys, + }, + QueryAprResponse { + apr: aprs.eden_apr_elys, + }, + QueryAprResponse { + apr: aprs.edenb_apr_elys, + }, + ) + .unwrap_or_default(); + + to_json_binary(&program) + } + #[cfg(feature = "debug")] + GetUsdcEarnProgramDetails { address } => { + let generator = AccountSnapshotGenerator::new(&deps)?; + let program = get_usdc_earn_program_details( + &deps, + Some(address.to_owned()), + generator.metadata.usdc_denom.to_owned(), + generator.metadata.usdc_base_denom.to_owned(), + generator.metadata.uusdc_usd_price, + ) + .unwrap_or_default(); + + to_json_binary(&program) + } + #[cfg(feature = "debug")] + IncentiveAprs { .. } => { + let querier = ElysQuerier::new(&deps.querier); + let response = querier.get_incentive_aprs().unwrap_or_default(); + + to_json_binary(&response) + } + StorageSize {} => { + let user_address_queue_data_size = USER_ADDRESS_QUEUE.len(deps.storage)? as u128; + let history_data_size = HISTORY + .keys(deps.storage, None, None, cosmwasm_std::Order::Descending) + .count() as u128; + let old_history_2_data_size = OLD_HISTORY_2 + .keys(deps.storage, None, None, cosmwasm_std::Order::Descending) + .count() as u128; + + to_json_binary(&StorageSizeResp { + user_address_queue_data_size, + history_data_size, + old_history_2_data_size, + }) + } + Version {} => to_json_binary(&CONTRACT.load(deps.storage)?), + } +} diff --git a/contracts/account-history-contract/src/entry_point/sudo.rs b/contracts/account-history-contract/src/entry_point/sudo.rs new file mode 100644 index 00000000..7d8d69d8 --- /dev/null +++ b/contracts/account-history-contract/src/entry_point/sudo.rs @@ -0,0 +1,18 @@ +use crate::action::execute::clean_up_storage; +use crate::states::DELETE_OLD_DATA_ENABLED; +use crate::{msg::SudoMsg, states::DELETE_EPOCH}; +use cosmwasm_std::{entry_point, DepsMut, Env, Response, StdResult}; +use elys_bindings::{ElysMsg, ElysQuery}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo(mut deps: DepsMut, _env: Env, msg: SudoMsg) -> StdResult> { + match msg { + SudoMsg::ClockEndBlock {} => { + let epoch = DELETE_EPOCH.load(deps.storage)?; + if DELETE_OLD_DATA_ENABLED.load(deps.storage)? == true { + clean_up_storage(&mut deps, epoch)?; + } + Ok(Response::new()) + } + } +} diff --git a/contracts/account-history-contract/src/error.rs b/contracts/account-history-contract/src/error.rs new file mode 100644 index 00000000..3672bd62 --- /dev/null +++ b/contracts/account-history-contract/src/error.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[allow(dead_code)] +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + StdError(#[from] StdError), + #[error("PortfolioError")] + PortfolioError {}, + #[error("RewardError")] + RewardError {}, + #[error("TotalBalanceError")] + TotalBalanceError {}, + #[error("AssetDenomError")] + AssetDenomError {}, + #[error("{balance} is smaller than {amount}")] + InsufficientBalanceError { balance: u128, amount: u64 }, +} diff --git a/contracts/account-history-contract/src/lib.rs b/contracts/account-history-contract/src/lib.rs new file mode 100644 index 00000000..1705184a --- /dev/null +++ b/contracts/account-history-contract/src/lib.rs @@ -0,0 +1,12 @@ +pub mod entry_point; + +pub use elys_bindings::account_history::msg; +pub use elys_bindings::account_history::types as bindings_account_history_types; + +pub mod utils; + +mod action; + +mod error; +mod states; +mod types; diff --git a/contracts/account-history-contract/src/states/admin_address.rs b/contracts/account-history-contract/src/states/admin_address.rs new file mode 100644 index 00000000..e20f975e --- /dev/null +++ b/contracts/account-history-contract/src/states/admin_address.rs @@ -0,0 +1,3 @@ +use cw_storage_plus::Item; + +pub const PARAMS_ADMIN: Item = Item::new("params admin"); diff --git a/contracts/account-history-contract/src/states/enable_update_account.rs b/contracts/account-history-contract/src/states/enable_update_account.rs new file mode 100644 index 00000000..bc90f13c --- /dev/null +++ b/contracts/account-history-contract/src/states/enable_update_account.rs @@ -0,0 +1,3 @@ +use cw_storage_plus::Item; + +pub const UPDATE_ACCOUNT_ENABLED: Item = Item::new("update_account_enabled"); diff --git a/contracts/account-history-contract/src/states/expiration.rs b/contracts/account-history-contract/src/states/expiration.rs new file mode 100644 index 00000000..371be223 --- /dev/null +++ b/contracts/account-history-contract/src/states/expiration.rs @@ -0,0 +1,4 @@ +use cw_storage_plus::Item; +use cw_utils::Expiration; + +pub const EXPIRATION: Item = Item::new("expiration"); diff --git a/contracts/account-history-contract/src/states/history.rs b/contracts/account-history-contract/src/states/history.rs new file mode 100644 index 00000000..11717429 --- /dev/null +++ b/contracts/account-history-contract/src/states/history.rs @@ -0,0 +1,14 @@ +use std::collections::HashMap; + +use cw_storage_plus::{Item, Map}; +use elys_bindings::account_history::types::PortfolioBalanceSnapshot; + +pub const OLD_HISTORY_2: Map<&str, HashMap> = + Map::new("history_portfolio_balance_snapshot_2"); + +pub const HISTORY: Map<&str, PortfolioBalanceSnapshot> = + Map::new("history_portfolio_balance_snapshot_3"); + +pub const DELETE_OLD_DATA_ENABLED: Item = Item::new("delete_old_data_enabled"); + +pub const DELETE_EPOCH: Item = Item::new("delete_data_epoch"); diff --git a/contracts/account-history-contract/src/states/metadata.rs b/contracts/account-history-contract/src/states/metadata.rs new file mode 100644 index 00000000..b1f3aafc --- /dev/null +++ b/contracts/account-history-contract/src/states/metadata.rs @@ -0,0 +1,4 @@ +use cw_storage_plus::Item; +use elys_bindings::account_history::types::Metadata; + +pub const METADATA: Item = Item::new("metadata"); diff --git a/contracts/account-history-contract/src/states/mod.rs b/contracts/account-history-contract/src/states/mod.rs new file mode 100644 index 00000000..16595276 --- /dev/null +++ b/contracts/account-history-contract/src/states/mod.rs @@ -0,0 +1,17 @@ +mod admin_address; +mod enable_update_account; +mod expiration; +mod history; +mod metadata; +mod processed_account_per_block; +mod trade_shield_address; +mod user_address_queue; + +pub use admin_address::PARAMS_ADMIN; +pub use enable_update_account::UPDATE_ACCOUNT_ENABLED; +pub use expiration::EXPIRATION; +pub use history::{DELETE_EPOCH, DELETE_OLD_DATA_ENABLED, HISTORY, OLD_HISTORY_2}; +pub use metadata::METADATA; +pub use processed_account_per_block::PROCESSED_ACCOUNT_PER_BLOCK; +pub use trade_shield_address::TRADE_SHIELD_ADDRESS; +pub use user_address_queue::USER_ADDRESS_QUEUE; diff --git a/contracts/account-history-contract/src/states/processed_account_per_block.rs b/contracts/account-history-contract/src/states/processed_account_per_block.rs new file mode 100644 index 00000000..c169f7ce --- /dev/null +++ b/contracts/account-history-contract/src/states/processed_account_per_block.rs @@ -0,0 +1,3 @@ +use cw_storage_plus::Item; + +pub const PROCESSED_ACCOUNT_PER_BLOCK: Item = Item::new("processed account per block"); diff --git a/contracts/account-history-contract/src/states/trade_shield_address.rs b/contracts/account-history-contract/src/states/trade_shield_address.rs new file mode 100644 index 00000000..854103c7 --- /dev/null +++ b/contracts/account-history-contract/src/states/trade_shield_address.rs @@ -0,0 +1,3 @@ +use cw_storage_plus::Item; + +pub const TRADE_SHIELD_ADDRESS: Item> = Item::new("trade_shield_address2"); diff --git a/contracts/account-history-contract/src/states/user_address_queue.rs b/contracts/account-history-contract/src/states/user_address_queue.rs new file mode 100644 index 00000000..e598a898 --- /dev/null +++ b/contracts/account-history-contract/src/states/user_address_queue.rs @@ -0,0 +1,3 @@ +use cw_storage_plus::Deque; + +pub const USER_ADDRESS_QUEUE: Deque = Deque::new("user address queue 1"); diff --git a/contracts/account-history-contract/src/types/account_snapshot_generator.rs b/contracts/account-history-contract/src/types/account_snapshot_generator.rs new file mode 100644 index 00000000..cc53b331 --- /dev/null +++ b/contracts/account-history-contract/src/types/account_snapshot_generator.rs @@ -0,0 +1,777 @@ +use std::collections::HashMap; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + Coin, DecCoin, Decimal, Decimal256, Deps, Env, QuerierWrapper, StdError, StdResult, Uint128, +}; +use cw_utils::Expiration; +use elys_bindings::{ + account_history::{ + msg::query_resp::{GetRewardsResp, StakeAssetBalanceBreakdown, StakedAssetsResponse}, + types::{ + AccountSnapshot, ElysDenom, LiquidAsset, Metadata, PerpetualAsset, PerpetualAssets, + PoolBalances, PortfolioBalanceSnapshot, Reward, StakedAssets, TotalBalance, + }, + }, + query_resp::{ + CommittedTokens, OracleAssetInfoResponse, PoolFilterType, PoolResp, QueryAprResponse, + QueryUserPoolResponse, UserPoolResp, + }, + trade_shield::{ + msg::{ + query_resp::{ + GetPerpetualOrdersResp, GetPerpetualPositionsForAddressResp, GetSpotOrdersResp, + }, + QueryMsg::{GetPerpetualOrders, GetSpotOrders, PerpetualGetPositionsForAddress}, + }, + types::{ + CoinValue, OracleAssetInfo, PerpetualOrder, PerpetualOrderPlus, PerpetualOrderType, + SpotOrder, Status, + }, + }, + ElysQuerier, ElysQuery, +}; + +use crate::{ + action::query::{ + get_eden_boost_earn_program_details, get_eden_earn_program_details, + get_elys_earn_program_details, get_pools, get_usdc_earn_program_details, + }, + states::{EXPIRATION, METADATA, TRADE_SHIELD_ADDRESS}, +}; + +#[cw_serde] +pub struct AccountSnapshotGenerator { + pub trade_shield_address: Option, + pub expiration: Expiration, + pub metadata: Metadata, +} + +impl AccountSnapshotGenerator { + pub fn new(deps: &Deps) -> StdResult { + let expiration = EXPIRATION.load(deps.storage)?; + let trade_shield_address = TRADE_SHIELD_ADDRESS.load(deps.storage)?; + let metadata = METADATA.load(deps.storage)?; + + Ok(Self { + trade_shield_address, + expiration, + metadata, + }) + } + + pub fn generate_portfolio_balance_snapshot_for_address( + &self, + querier: &ElysQuerier, + deps: &Deps, + env: &Env, + address: &String, + ) -> StdResult { + let snapshot = self.generate_account_snapshot_for_address(querier, deps, env, address)?; + + Ok(PortfolioBalanceSnapshot { + date: snapshot.date, + total_balance_usd: snapshot.total_balance.total_balance.clone(), + }) + } + + pub fn generate_account_snapshot_for_address( + &self, + querier: &ElysQuerier, + deps: &Deps, + env: &Env, + address: &String, + ) -> StdResult { + let liquid_assets_response = self.get_liquid_assets(&deps, querier, &address)?; // ✅ Fixme + let staked_assets_response = self.get_staked_assets(&deps, Some(address.clone()))?; + let rewards_response = self.get_rewards(&deps, &address)?; + let perpetual_response = self.get_perpetuals(&deps, &address)?; + let pool_balances_response = self.get_pool_balances(&deps, &address)?; + + let date = match self.expiration { + Expiration::AtHeight(_) => Expiration::AtHeight(env.block.height), + Expiration::AtTime(_) => Expiration::AtTime(env.block.time), + Expiration::Never {} => panic!("never expire"), + }; + + let mut total_liquidity_position_balance = Decimal256::zero(); + for pool in pool_balances_response.pools.iter() { + total_liquidity_position_balance = + total_liquidity_position_balance.checked_add(Decimal256::from(pool.available))?; + } + + let reward = rewards_response.rewards_map; + let portfolio_usd = liquid_assets_response + .total_liquid_asset_balance + .amount + .checked_add(Decimal256::from(staked_assets_response.total_balance))? + .checked_add( + perpetual_response + .total_perpetual_asset_balance + .amount + .clone(), + )? + .checked_add(total_liquidity_position_balance)?; + + let reward_usd = Decimal256::from(reward.total_usd.clone()); + let total_balance = portfolio_usd.checked_add(reward_usd.clone())?; + + // Adds the records all the time as we should return data to the FE even if it is 0 balanced. + Ok(AccountSnapshot { + date, + total_balance: TotalBalance { + total_balance, + portfolio_usd: portfolio_usd.clone(), + reward_usd, + }, + reward, + pool_balances: PoolBalances { + balances: pool_balances_response.pools, + }, + liquid_asset: liquid_assets_response, + staked_assets: staked_assets_response.staked_assets, + perpetual_assets: perpetual_response, + }) + } + + pub fn get_pools_user_rewards( + &self, + deps: &Deps, + address: &String, + pools: Vec, + ) -> ( + Decimal, + HashMap, + HashMap>, + ) { + let querier = ElysQuerier::new(&deps.querier); + + let all_rewards = querier + .get_masterchef_pending_rewards(address.clone()) + .unwrap_or_default(); + let rewards_per_pool = all_rewards + .rewards_to_coin_values(&querier) + .unwrap_or_default(); + + let mut total_rewards = Decimal::zero(); + let mut total_breakdown: HashMap = HashMap::new(); + pools.iter().for_each(|pool| { + let pool_rewards = rewards_per_pool.get(&(pool.pool_id as u64)); + + match pool_rewards { + Some(rewards) => { + rewards.iter().for_each(|reward| { + total_rewards = total_rewards + .checked_add(reward.amount_usd) + .unwrap_or_default(); + + if let Some(breakdown_reward) = total_breakdown.get_mut(&reward.denom) { + // Update the amounts + breakdown_reward.amount_token = breakdown_reward + .amount_token + .checked_add(reward.amount_token) + .unwrap_or_default(); + breakdown_reward.amount_usd = breakdown_reward + .amount_usd + .checked_add(reward.amount_usd) + .unwrap_or_default(); + } else { + // Create a new default reward if it doesn't exist + let default_reward = CoinValue::new( + reward.denom.clone(), + Decimal::zero(), + reward.price, + Decimal::zero(), + ); + let breakdown_reward = total_breakdown + .entry(reward.denom.clone()) + .or_insert(default_reward); + + // Update the amounts + breakdown_reward.amount_token = breakdown_reward + .amount_token + .checked_add(reward.amount_token) + .unwrap_or_default(); + breakdown_reward.amount_usd = breakdown_reward + .amount_usd + .checked_add(reward.amount_usd) + .unwrap_or_default(); + } + }); + } + None => {} + }; + }); + + (total_rewards, total_breakdown, rewards_per_pool) + } + + pub fn get_pool_balances( + &self, + deps: &Deps, + address: &String, + ) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + let commitments = querier.get_commitments(address.clone())?.commitments; + + struct IdSortedPoolBalance { + pub id: u64, + pub balance: CommittedTokens, + } + + let pool_balances: Vec = + commitments.committed_tokens.map_or(vec![], |res| { + res.iter() + .filter(|coin| coin.denom.starts_with("amm/pool/")) + .cloned() + .collect() + }); + + let pool_data: Vec = pool_balances + .iter() + .map(|coin| { + let id = coin + .denom + .split("/") + .last() + .unwrap_or("0") + .parse::() + .unwrap_or(0u64); + IdSortedPoolBalance { + id, + balance: coin.clone(), + } + }) + .collect(); + + // For each pool_data, fetch the pool with that ID + let mut pool_resp: Vec = Vec::new(); + for user_pool in pool_data { + let pool_id = user_pool.id; + let pool = get_pools(*deps, Some(vec![pool_id]), PoolFilterType::FilterAll, None)?; + let pool = pool + .pools + .unwrap_or_default() + .first() + .map_or(PoolResp::default(), |pool| pool.clone()); + + let balance_uint = Uint128::new(user_pool.balance.amount.i128() as u128); + let share_price = pool.share_usd_price.or(Some(Decimal::zero())).unwrap(); + + // Assumes that pool.assets are in the desired displaying sort order. + let balance_breakdown = pool + .assets + .clone() + .into_iter() + .map(|asset| { + pool.current_pool_ratio.clone().map_or(None, |ratios| { + let denom = asset.token.denom.clone(); + let ratio = ratios.get(&denom); + let asset_price = querier.get_asset_price(denom.clone()); + + match (asset_price, ratio) { + (Ok(price), Some(ratio)) => { + let asset_shares = + Decimal::from_atomics(balance_uint, 18).unwrap() * ratio; + let shares_usd = asset_shares * share_price; + let asset_amount = shares_usd / price; + + Some(CoinValue::new(denom, asset_amount, price, shares_usd)) + } + (_, _) => None, + } + }) + }) + .collect(); + + pool_resp.push(UserPoolResp { + pool, + balance: user_pool.balance, + available: Decimal::from_atomics(balance_uint, 18).unwrap() * share_price, + balance_breakdown, + }); + } + + let pools: Vec = pool_resp + .iter() + .map(|user_pool| user_pool.pool.clone()) + .collect(); + + let (total_rewards, total_rewards_breakdown, rewards_per_pool) = + self.get_pools_user_rewards(&deps, address, pools); + + Ok(QueryUserPoolResponse { + pools: pool_resp, + total_rewards, + total_rewards_breakdown, + rewards_per_pool, + }) + } + + pub fn get_liquid_assets( + &self, + deps: &Deps, + querier: &ElysQuerier, + address: &String, + ) -> StdResult { + let mut account_balances = deps.querier.query_all_balances(address)?; + let orders_balances = + self.get_all_orders(&deps.querier, &self.trade_shield_address, &address)?; + + let eden_program = get_eden_earn_program_details( + deps, + Some(address.to_owned()), + ElysDenom::Eden.as_str().to_string(), + self.metadata.uusdc_usd_price, + self.metadata.uelys_price_in_uusdc, + self.metadata.usdc_apr_eden.to_owned(), + self.metadata.eden_apr_eden.to_owned(), + self.metadata.edenb_apr_eden.to_owned(), + ) + .unwrap_or_default(); + + let available = eden_program.data.to_coin_available(); + if available.amount > Uint128::zero() { + account_balances.push(available); + } + + // `total_value_per_asset` that maps references to denoms (`&String`) to values of type + // `Coin`. it is used to store the coin of unique denom. + let mut total_value_per_asset: HashMap<&String, Coin> = HashMap::new(); + // `price_per_asset` that maps references to denoms to tuples containing a `Decimal` and an unsigned 64-bit integer. + let mut price_per_asset: HashMap<&String, (Decimal, u64)> = HashMap::new(); + + for available in account_balances.iter() { + total_value_per_asset + .entry(&available.denom) + .and_modify(|e| e.amount += available.amount) + .or_insert_with(|| available.clone()); + } + + for in_order in orders_balances.iter() { + total_value_per_asset + .entry(&in_order.denom) + .and_modify(|e| e.amount += in_order.amount) + .or_insert_with(|| in_order.clone()); + } + + // memoization of price and decimal point for each unique denom in `total_value_per_asset` + for (key, _) in total_value_per_asset.iter() { + let val = Self::get_price_and_decimal_point(key.to_string(), querier); + if let Ok(v) = val { + price_per_asset.insert(key, v); + } + } + + // closure for converting coin to coinvalue using from_price_and_coin + let coin_to_coin_value = |coin: &Coin| { + let price_and_point = price_per_asset.get(&coin.denom); + if price_and_point.is_none() { + return None; + } + CoinValue::from_price_and_coin(coin, price_and_point.unwrap().clone()).ok() + }; + + let available_asset_balance: Vec = account_balances + .iter() + .filter_map(coin_to_coin_value) + .collect(); + + let in_orders_asset_balance: Vec = orders_balances + .iter() + .filter_map(coin_to_coin_value) + .collect(); + + let mut total_available_balance = + DecCoin::new(Decimal256::zero(), &self.metadata.usdc_denom); + let mut total_in_orders_balance = + DecCoin::new(Decimal256::zero(), &self.metadata.usdc_denom); + + for balance in &available_asset_balance { + total_available_balance.amount = total_available_balance + .amount + .checked_add(Decimal256::from(balance.amount_usd.clone()))? + } + + for balance in &in_orders_asset_balance { + total_in_orders_balance.amount = total_in_orders_balance + .amount + .checked_add(Decimal256::from(balance.amount_usd.clone()))? + } + + let total_value_per_asset: Vec = total_value_per_asset + .values() + .filter_map(coin_to_coin_value) + .collect(); + + // calculating total liquid assets + let total_liquid_asset_balance = DecCoin::new( + total_available_balance.amount + total_in_orders_balance.amount, + &self.metadata.usdc_denom, + ); + + Ok(LiquidAsset { + total_liquid_asset_balance, + total_available_balance, + total_in_orders_balance, + available_asset_balance, + in_orders_asset_balance, + total_value_per_asset, + }) + } + + pub fn get_staked_assets( + &self, + deps: &Deps, + address: Option, + ) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + let aprs = querier.get_incentive_aprs().unwrap_or_default(); + + // create staked_assets variable that is a StakedAssets struct + let mut staked_assets = StakedAssets::default(); + let mut total_staked_balance = Decimal::zero(); + + // usdc program + let usdc_details = get_usdc_earn_program_details( + deps, + address.clone(), + self.metadata.usdc_denom.to_owned(), + self.metadata.usdc_base_denom.to_owned(), + self.metadata.uusdc_usd_price, + ) + .unwrap_or_default(); + + let staked_asset_usdc = usdc_details.data; + total_staked_balance = total_staked_balance + .checked_add( + staked_asset_usdc + .clone() + .staked + .unwrap_or_default() + .usd_amount, + ) + .unwrap_or_default(); + staked_assets.usdc_earn_program = staked_asset_usdc; + + // elys program + let elys_details = get_elys_earn_program_details( + deps, + address.clone(), + ElysDenom::Elys.as_str().to_string(), + self.metadata.uusdc_usd_price, + self.metadata.uelys_price_in_uusdc, + QueryAprResponse { + apr: aprs.usdc_apr_elys, + }, + QueryAprResponse { + apr: aprs.eden_apr_elys, + }, + QueryAprResponse { + apr: aprs.edenb_apr_elys, + }, + ) + .unwrap_or_default(); + + let staked_asset_elys = elys_details.data; + total_staked_balance = total_staked_balance + .checked_add( + staked_asset_elys + .clone() + .staked + .unwrap_or_default() + .usd_amount, + ) + .unwrap_or_default(); + staked_assets.elys_earn_program = staked_asset_elys.clone(); + + let unstaking = staked_asset_elys + .unstaked_positions + .map_or(Decimal::zero(), |x| { + x.iter().fold(Decimal::zero(), |acc, position| { + acc.checked_add(position.unstaked.usd_amount) + .unwrap_or_default() + }) + }); + + // eden program + let eden_details = get_eden_earn_program_details( + deps, + address.clone(), + ElysDenom::Eden.as_str().to_string(), + self.metadata.uusdc_usd_price, + self.metadata.uelys_price_in_uusdc, + QueryAprResponse { + apr: aprs.usdc_apr_eden, + }, + QueryAprResponse { + apr: aprs.eden_apr_eden, + }, + QueryAprResponse { + apr: aprs.edenb_apr_eden, + }, + ) + .unwrap_or_default(); + + let staked_asset_eden = eden_details.data; + total_staked_balance = total_staked_balance + .checked_add( + staked_asset_eden + .clone() + .staked + .unwrap_or_default() + .usd_amount, + ) + .unwrap_or_default(); + let vesting = staked_asset_eden.vesting.usd_amount; + + staked_assets.eden_earn_program = staked_asset_eden; + + let edenb_details = get_eden_boost_earn_program_details( + deps, + address, + ElysDenom::EdenBoost.as_str().to_string(), + QueryAprResponse { + apr: aprs.usdc_apr_edenb, + }, + QueryAprResponse { + apr: aprs.eden_apr_edenb, + }, + ) + .unwrap_or_default(); + + let staked_asset_edenb = edenb_details.data; + staked_assets.eden_boost_earn_program = staked_asset_edenb; + let balance_break_down = StakeAssetBalanceBreakdown { + staked: Decimal::from(total_staked_balance), + unstaking, + vesting, + }; + + Ok(StakedAssetsResponse { + staked_assets, + total_staked_balance: DecCoin::new( + Decimal256::from(total_staked_balance), + self.metadata.usdc_denom.to_owned(), + ), + total_balance: balance_break_down.total(), + balance_break_down, + }) + } + + pub fn get_all_orders( + &self, + querier: &QuerierWrapper, + trade_shield_address: &Option, + owner: &String, + ) -> StdResult> { + let trade_shield_address = match trade_shield_address { + Some(trade_shield_address) => trade_shield_address, + None => return Ok(vec![]), + }; + + let spot_order: GetSpotOrdersResp = querier + .query_wasm_smart( + trade_shield_address, + &GetSpotOrders { + pagination: None, + order_owner: Some(owner.clone()), + order_type: None, + order_status: Some(Status::Pending), + }, + ) + .map_err(|e| StdError::generic_err(format!("GetSpotOrders failed {}", e)))?; + let perpetual_order: GetPerpetualOrdersResp = querier + .query_wasm_smart( + trade_shield_address, + &GetPerpetualOrders { + pagination: None, + order_owner: Some(owner.clone()), + order_type: Some(PerpetualOrderType::LimitOpen), + order_status: Some(Status::Pending), + }, + ) + .map_err(|e| StdError::generic_err(format!("GetPerpetualOrders failed {}", e)))?; + let mut map: HashMap = HashMap::new(); + + for SpotOrder { order_amount, .. } in spot_order.orders { + map.entry(order_amount.denom) + .and_modify(|e| *e += order_amount.amount) + .or_insert(order_amount.amount); + } + for PerpetualOrderPlus { + order: PerpetualOrder { collateral, .. }, + .. + } in perpetual_order.orders + { + map.entry(collateral.denom) + .and_modify(|e| *e += collateral.amount) + .or_insert(collateral.amount); + } + + let consolidated_coins: Vec = map + .into_iter() + .map(|(denom, amount)| Coin { denom, amount }) + .collect(); + Ok(consolidated_coins) + } + + pub fn get_perpetuals( + &self, + deps: &Deps, + address: &String, + ) -> StdResult { + let trade_shield_address = match self.trade_shield_address.clone() { + Some(trade_shield_address) => trade_shield_address, + None => return Ok(PerpetualAssets::default()), + }; + + let GetPerpetualPositionsForAddressResp { mtps, .. } = deps + .querier + .query_wasm_smart( + trade_shield_address, + &PerpetualGetPositionsForAddress { + address: address.to_string(), + pagination: None, + }, + ) + .map_err(|_| StdError::generic_err("an error occurred while getting perpetuals"))?; + let mut perpetual_vec: Vec = vec![]; + let querier = ElysQuerier::new(&deps.querier); + + for mtp in mtps { + match PerpetualAsset::new(mtp, self.metadata.usdc_denom.to_owned(), &querier) { + Ok(perpetual_asset) => perpetual_vec.push(perpetual_asset), + Err(_) => continue, + } + } + + let total_perpetual_asset_balance_amount = perpetual_vec + .iter() + .map(|perpetual| perpetual.size.amount) + .fold(Decimal256::zero(), |acc, item| acc + item); + let total_perpetual_asset_balance = DecCoin::new( + total_perpetual_asset_balance_amount, + self.metadata.usdc_denom.to_owned(), + ); + + Ok(PerpetualAssets { + total_perpetual_asset_balance, + perpetual_asset: perpetual_vec, + }) + } + + pub fn get_rewards( + &self, + deps: &Deps, + address: &String, + ) -> StdResult { + let querier = ElysQuerier::new(&deps.querier); + + // Elys Eden and Eden Boost Program rewards + let estaking_rewards = querier + .get_estaking_rewards(address.clone()) + .unwrap_or_default(); + // All pool rewards including USDC program rewards + let masterchef_rewards = querier.get_masterchef_pending_rewards(address.to_string())?; + + // Concatenate all staking reward vectors into one + let all_staking_rewards: Vec<&Coin> = estaking_rewards + .total + .iter() + .chain(&masterchef_rewards.total_rewards) + .collect(); + + // Accumulate amounts for each denomination + let mut denom_amounts: HashMap = HashMap::new(); + all_staking_rewards.iter().for_each(|coin| { + denom_amounts + .entry(coin.denom.clone()) + .and_modify(|amount| { + *amount += coin.amount; + }) + .or_insert(coin.amount); + }); + + // Convert accumulated amounts to CoinValue instances + let mut reward_map: HashMap = HashMap::new(); + for (denom, amount) in denom_amounts { + let dec_coin_value = CoinValue::from_coin( + &Coin { + denom: denom.clone(), + amount, + }, + &querier, + ) + .unwrap_or_default(); + + reward_map.insert(denom, dec_coin_value); + } + + let total_usd: Decimal = reward_map.values().map(|v| v.amount_usd).sum(); + + // Calculate other_usd as the sum of all amount_usd values in reward_map + // excluding USDC, Eden, and EdenBoost + let mut other_usd: Decimal = Decimal::zero(); + for (denom, value) in &reward_map { + if denom != &self.metadata.usdc_denom + && denom != &ElysDenom::Eden.as_str().to_string() + && denom != &ElysDenom::EdenBoost.as_str().to_string() + { + other_usd += value.amount_usd; + } + } + + let reward = Reward { + usdc_usd: reward_map + .entry(self.metadata.usdc_denom.clone()) + .or_default() + .amount_usd, + eden_usd: reward_map + .entry(ElysDenom::Eden.as_str().to_string()) + .or_default() + .amount_usd, + eden_boost: reward_map + .entry(ElysDenom::EdenBoost.as_str().to_string()) + .or_default() + .amount_token, + other_usd, + total_usd, + }; + + // Construct rewards_vec as the values of all rewards_map entries + let rewards_vec: Vec = reward_map.into_iter().map(|(_, v)| v).collect(); + + let resp = GetRewardsResp { + rewards_map: reward, + rewards: rewards_vec, + }; + + Ok(resp) + } + + fn get_price_and_decimal_point( + denom: String, + querier: &ElysQuerier<'_>, + ) -> StdResult<(Decimal, u64)> { + let OracleAssetInfoResponse { asset_info } = + querier + .asset_info(denom.clone()) + .unwrap_or(OracleAssetInfoResponse { + asset_info: OracleAssetInfo { + denom: denom.clone(), + display: denom.clone(), + band_ticker: denom.clone(), + elys_ticker: denom.clone(), + decimal: 6, + }, + }); + + let price = querier + .get_asset_price(denom.clone()) + .map_err(|e| StdError::generic_err(format!("failed to get_asset_price: {}", e)))?; + + Ok((price, asset_info.decimal)) + } +} diff --git a/contracts/account-history-contract/src/types/mod.rs b/contracts/account-history-contract/src/types/mod.rs new file mode 100644 index 00000000..3f77e314 --- /dev/null +++ b/contracts/account-history-contract/src/types/mod.rs @@ -0,0 +1,3 @@ +mod account_snapshot_generator; + +pub use account_snapshot_generator::AccountSnapshotGenerator; diff --git a/contracts/account-history-contract/src/utils.rs b/contracts/account-history-contract/src/utils.rs new file mode 100644 index 00000000..2fafc0f4 --- /dev/null +++ b/contracts/account-history-contract/src/utils.rs @@ -0,0 +1,11 @@ +use chrono::NaiveDateTime; +use cosmwasm_std::BlockInfo; + +pub fn get_raw_today(block_info: &BlockInfo) -> NaiveDateTime { + NaiveDateTime::from_timestamp_opt(block_info.time.seconds() as i64, 0) + .expect("Failed to convert block time to date") +} + +pub fn get_today(block_info: &BlockInfo) -> String { + get_raw_today(block_info).format("%Y-%m-%d").to_string() +} diff --git a/scripts/build.sh b/scripts/build.sh index 177ffd23..bf741744 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,6 +4,7 @@ if [ "${CI:-}" = "true" ]; then echo "CI is true, running the block of commands..." export VERSION=$(git describe --tags --match 'v*' --abbrev=0 | sed 's/^v//') + sed -i "s/^version = .*/version = \"$VERSION\"/" contracts/account-history-contract/Cargo.toml sed -i "s/^version = .*/version = \"$VERSION\"/" contracts/trade-shield-contract/Cargo.toml cargo update fi diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 2c3a4073..ddb5fb02 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -20,10 +20,11 @@ if [ -n "$CI" ]; then # contract addresses TS_CONTRACT_ADDRESS=elys1m3hduhk4uzxn8mxuvpz02ysndxfwgy5mq60h4c34qqn67xud584qeee3m4 + AH_CONTRACT_ADDRESS=elys1s37xz7tzrru2cpl96juu9lfqrsd4jh73j9slyv440q5vttx2uyesetjpne # set elysd path ELYSD=/tmp/elysd - URL=https://github.com/elys-network/elys/releases/download/v0.39.0/elysd-v0.39.0-linux-amd64 + URL=https://github.com/elys-network/elys/releases/download/v0.29.26/elysd-v0.29.26-linux-amd64 # download elysd and binary to path wget $URL -O $ELYSD @@ -97,8 +98,35 @@ wait_for_tx $txhash export ts_contract_address=$(elysd q tx $txhash --node $NODE | extract_contract_address) echo "ts_contract_address: $ts_contract_address" +# store and init/migrate account history contract +txhash=$(elysd tx wasm store artifacts/account_history_contract.wasm $OPTIONS --sequence $(($sequence + 5)) | extract_txhash) +echo "ah store txhash: $txhash" +wait_for_tx $txhash +codeid=$(elysd q tx $txhash --node $NODE | extract_code_id) +echo "ah code id: $codeid" +if [ -n "$AH_CONTRACT_ADDRESS" ]; then + txhash=$(elysd tx wasm migrate $OPTIONS --sequence $(($sequence + 6)) $AH_CONTRACT_ADDRESS $codeid '{ + "trade_shield_address": "'"$TS_CONTRACT_ADDRESS"'", + "limit": 50 + }' | extract_txhash) + echo "ah migrate txhash: $txhash" +else + txhash=$(elysd tx wasm init $OPTIONS --sequence $(($sequence + 6)) --label "ah" --admin $NAME $codeid '{ + "limit": 300, + "expiration": { + "at_time": "604800000000000" + }, + "trade_shield_address": "'"$ts_contract_address"'" + }' | extract_txhash) + echo "ah init txhash: $txhash" +fi +wait_for_tx $txhash +ah_contract_address=$(elysd q tx $txhash --node $NODE | extract_contract_address) +echo "ah_contract_address: $ah_contract_address" + # print environment variables to set printf "\nset those environment variables to use the contracts:\n\n" printf "export NODE=%s\n" "$NODE" printf "export NAME=%s\n" "$NAME" -printf "export TS_CONTRACT_ADDRESS=%s\n" "$ts_contract_address" \ No newline at end of file +printf "export TS_CONTRACT_ADDRESS=%s\n" "$ts_contract_address" +printf "export AH_CONTRACT_ADDRESS=%s\n" "$ah_contract_address" \ No newline at end of file diff --git a/scripts/test_history.sh b/scripts/test_history.sh new file mode 100755 index 00000000..6594df03 --- /dev/null +++ b/scripts/test_history.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# set -x +extract_txhash() { awk -F 'txhash: ' '/txhash:/{print $2; exit}'; } +extract_code_id() { awk -F 'key: code_id|value: ' '/key: code_id/ { getline; gsub(/"/, "", $2); print $2; exit }'; } +extract_contract_address() { awk -F 'key: _contract_address|value: ' '/key: _contract_address/ { getline; gsub(/"/, "", $2); print $2; exit }'; } + +OPTIONS="--from validator --gas auto --gas-adjustment=1.3 --fees 100000uelys -b sync -y --keyring-backend=test --chain-id=elystestnet-1" + +docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/workspace-optimizer:0.14.0 + +# store and init trade shield contract +txhash=$(elysd tx wasm store artifacts/trade_shield_contract.wasm $OPTIONS | extract_txhash) +sleep 10 +codeid=$(elysd q tx $txhash | extract_code_id) +txhash=$(elysd tx wasm init $codeid '{}' $OPTIONS --label "Contract" --admin validator | extract_txhash) +sleep 10 +addr=$(elysd q tx $txhash | extract_contract_address) + +echo tradeshield : $addr + + +# store and init account history contract +txhash=$(elysd tx wasm store artifacts/account_history_contract.wasm $OPTIONS | extract_txhash) +sleep 10 +codeid=$(elysd q tx $txhash | extract_code_id) +msg=$(echo '{"limit" : 300, "expiration": {"at_time":"604800000000000"}, "trade_shield_address" :"'$addr'"}') +txhash=$(elysd tx wasm init $codeid "$msg" $OPTIONS --label "Contract" --admin validator | extract_txhash) +sleep 10 +addr=$(elysd q tx $txhash | extract_contract_address) +echo history : $addr +elysd tx wasm exec $addr '{}' --from validator --gas-prices 0.25uelys --gas auto --gas-adjustment 1.3 -b sync -y --keyring-backend=test --chain-id=elystestnet-1 +elysd q wasm contract-state smart $addr '{"all" : {}}' +# elysd q wasm contract-state smart $addr2 '{"get_liquid_assets" : {"user_address" : "WRITE THE USER ADDRESS"}}' \ No newline at end of file