From 12c1adce6e92ab4903ddb55afc0072f6886d0bff Mon Sep 17 00:00:00 2001 From: William Freudenberger Date: Wed, 15 Nov 2023 21:30:50 +0100 Subject: [PATCH] feat: add InvestmentPortfolio RtAPI --- Cargo.lock | 1 + libs/primitives/src/lib.rs | 2 +- libs/traits/src/investments.rs | 20 --- libs/types/src/fixed_point.rs | 2 +- libs/types/src/investments.rs | 19 +++ pallets/investments/src/lib.rs | 42 +---- runtime/altair/src/lib.rs | 7 +- runtime/centrifuge/Cargo.toml | 1 + runtime/centrifuge/src/lib.rs | 7 +- runtime/common/Cargo.toml | 4 + runtime/common/src/apis/investments.rs | 8 +- runtime/common/src/lib.rs | 151 +++++++++++++----- runtime/development/src/lib.rs | 7 +- .../src/runtime_apis/investments.rs | 14 ++ .../integration-tests/src/runtime_apis/mod.rs | 1 + 15 files changed, 171 insertions(+), 115 deletions(-) create mode 100644 runtime/integration-tests/src/runtime_apis/investments.rs diff --git a/Cargo.lock b/Cargo.lock index 89fa577626..2d42b85eb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11386,6 +11386,7 @@ dependencies = [ "log", "orml-asset-registry", "orml-oracle", + "orml-tokens", "orml-traits", "pallet-anchors", "pallet-authorship", diff --git a/libs/primitives/src/lib.rs b/libs/primitives/src/lib.rs index b7e5be88ba..2c4d027c83 100644 --- a/libs/primitives/src/lib.rs +++ b/libs/primitives/src/lib.rs @@ -222,7 +222,7 @@ pub mod constants { /// We allow for 0.5 seconds of compute with a 6 second average block time. pub const MAXIMUM_BLOCK_WEIGHT: Weight = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0) .saturating_div(2) - .set_proof_size(MAX_POV_SIZE as u64); + .set_proof_size(MAX_POV_SIZE); pub const MICRO_CFG: Balance = 1_000_000_000_000; // 10−6 0.000001 pub const MILLI_CFG: Balance = 1_000 * MICRO_CFG; // 10−3 0.001 diff --git a/libs/traits/src/investments.rs b/libs/traits/src/investments.rs index 905966de61..5bab8c4a57 100644 --- a/libs/traits/src/investments.rs +++ b/libs/traits/src/investments.rs @@ -219,26 +219,6 @@ pub trait InvestmentAccountant { ) -> Result<(), Self::Error>; } -/// Trait to handle Investment Portfolios for accounts -pub trait InvestmentsPortfolio { - type InvestmentId; - type CurrencyId; - type Balance; - type Error; - type AccountInvestmentPortfolio; - - /// Get the payment currency for an investment. - fn get_investment_currency_id( - investment_id: Self::InvestmentId, - ) -> Result; - - /// Get the investments and associated payment currencies and balances for - /// an account. - fn get_account_investments_currency( - who: &Account, - ) -> Result; -} - /// Trait to handle investments in (presumably) foreign currencies, i.e., other /// currencies than the pool currency. /// diff --git a/libs/types/src/fixed_point.rs b/libs/types/src/fixed_point.rs index 377232128a..96eab4e55c 100644 --- a/libs/types/src/fixed_point.rs +++ b/libs/types/src/fixed_point.rs @@ -550,7 +550,7 @@ impl FixedPointNumberExtension for FixedU128
{ multiply_by_rational_with_rounding( lhs.value, - Self::DIV as u128, + Self::DIV, rhs.value, Rounding::from_signed(r, negative), ) diff --git a/libs/types/src/investments.rs b/libs/types/src/investments.rs index 57b127d782..26c0500ac7 100644 --- a/libs/types/src/investments.rs +++ b/libs/types/src/investments.rs @@ -239,3 +239,22 @@ pub struct ExecutedForeignCollect { /// pool currency) pub amount_remaining: Balance, } + +/// A representation of an investment portfolio consisting of free, pending and +/// claimable pool currency as well as tranche tokens. +#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + +pub struct InvestmentPortfolio { + /// The unprocessed invest order amount in pool currency + pub pending_invest_currency: Balance, + /// The amount of tranche tokens which can be collected for an invest order + pub claimable_tranche_tokens: Balance, + /// The amount of tranche tokens which can be transferred + pub free_tranche_tokens: Balance, + /// The amount of tranche tokens which cannot be transferred + pub locked_tranche_tokens: Balance, + /// The unprocessed redeem order amount in tranche tokens + pub pending_redeem_tranche_tokens: Balance, + /// The amount of pool currency which can be collected for a redeem order + pub claimable_currency: Balance, +} diff --git a/pallets/investments/src/lib.rs b/pallets/investments/src/lib.rs index 99bbd87fed..5f606f8a1b 100644 --- a/pallets/investments/src/lib.rs +++ b/pallets/investments/src/lib.rs @@ -15,9 +15,7 @@ use cfg_primitives::OrderId; use cfg_traits::{ - investments::{ - Investment, InvestmentAccountant, InvestmentCollector, InvestmentsPortfolio, OrderManager, - }, + investments::{Investment, InvestmentAccountant, InvestmentCollector, OrderManager}, PreConditions, StatusNotificationHook, }; use cfg_types::{ @@ -47,6 +45,7 @@ use sp_std::{ convert::TryInto, vec::Vec, }; + pub mod weights; pub use weights::WeightInfo; @@ -60,12 +59,6 @@ mod tests; type CurrencyOf = <::Tokens as Inspect<::AccountId>>::AssetId; -type AccountInvestmentPortfolioOf = Vec<( - ::InvestmentId, - CurrencyOf, - ::Amount, -)>; - /// The enum we parse to `PreConditions` so the runtime /// can make an educated decision about this investment #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)] @@ -1100,37 +1093,6 @@ impl Pallet { } } -impl InvestmentsPortfolio for Pallet { - type AccountInvestmentPortfolio = AccountInvestmentPortfolioOf; - type Balance = T::Amount; - type CurrencyId = CurrencyOf; - type Error = DispatchError; - type InvestmentId = T::InvestmentId; - - /// Get the payment currency for an investment. - fn get_investment_currency_id( - investment_id: T::InvestmentId, - ) -> Result, DispatchError> { - let info = T::Accountant::info(investment_id).map_err(|_| Error::::UnknownInvestment)?; - Ok(info.payment_currency) - } - - /// Get the investments and associated payment currencies and balances for - /// an account. - fn get_account_investments_currency( - who: &T::AccountId, - ) -> Result { - let mut investments_currency: Vec<(Self::InvestmentId, Self::CurrencyId, Self::Balance)> = - Vec::new(); - >::iter_key_prefix(who).try_for_each(|i| { - let currency = Self::get_investment_currency_id(i)?; - let balance = T::Accountant::balance(i, who); - investments_currency.push((i, currency, balance)); - Ok::<(), DispatchError>(()) - })?; - Ok(investments_currency) - } -} impl Investment for Pallet { type Amount = T::Amount; type CurrencyId = CurrencyOf; diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 2e1ab06714..db15223cf6 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -31,6 +31,7 @@ use cfg_types::{ fee_keys::FeeKey, fixed_point::{Quantity, Rate, Ratio}, ids::PRICE_ORACLE_PALLET_ID, + investments::InvestmentPortfolio, oracles::OracleKey, permissions::{PermissionRoles, PermissionScope, PermissionedCurrencyRole, PoolRole, Role}, time::TimeProvider, @@ -2164,9 +2165,9 @@ impl_runtime_apis! { // Investment Runtime APIs - impl runtime_common::apis::InvestmentsApi for Runtime { - fn investment_portfolio(account_id: AccountId) -> Option> { - runtime_common::investment_portfolios::get_portfolios::(account_id) + impl runtime_common::apis::InvestmentsApi> for Runtime { + fn investment_portfolio(account_id: AccountId) -> Vec<(TrancheCurrency, InvestmentPortfolio)> { + runtime_common::investment_portfolios::get_account_portfolio::(account_id) } } diff --git a/runtime/centrifuge/Cargo.toml b/runtime/centrifuge/Cargo.toml index ff9040e52a..1d63f1f64a 100644 --- a/runtime/centrifuge/Cargo.toml +++ b/runtime/centrifuge/Cargo.toml @@ -420,3 +420,4 @@ on-chain-release-build = [ # Set timing constants (e.g. session period) to faster versions to speed up testing. fast-runtime = [] + diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index de69ab4264..dfa10fa83f 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -30,6 +30,7 @@ use cfg_types::{ fee_keys::FeeKey, fixed_point::{Quantity, Rate, Ratio}, ids::PRICE_ORACLE_PALLET_ID, + investments::InvestmentPortfolio, oracles::OracleKey, permissions::{ PermissionRoles, PermissionScope, PermissionedCurrencyRole, PoolRole, Role, UNION, @@ -2201,9 +2202,9 @@ impl_runtime_apis! { } // Investment Runtime APIs - impl runtime_common::apis::InvestmentsApi for Runtime { - fn investment_portfolio(account_id: AccountId) -> Option> { - runtime_common::investment_portfolios::get_portfolios::(account_id) + impl runtime_common::apis::InvestmentsApi> for Runtime { + fn investment_portfolio(account_id: AccountId) -> Vec<(TrancheCurrency, InvestmentPortfolio)> { + runtime_common::investment_portfolios::get_account_portfolio::(account_id) } } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 37f58a69ed..d1b5893e99 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -35,6 +35,7 @@ xcm-executor = { git = "https://github.com/paritytech/polkadot", default-feature # ORML dependencies orml-asset-registry = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } orml-oracle = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } +orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false, branch = "polkadot-v0.9.43" } # Frontier dependencies @@ -97,6 +98,7 @@ std = [ "log/std", "orml-asset-registry/std", "orml-oracle/std", + "orml-tokens/std", "orml-traits/std", "pallet-anchors/std", "pallet-authorship/std", @@ -143,6 +145,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "orml-asset-registry/runtime-benchmarks", + "orml-tokens/runtime-benchmarks", "pallet-anchors/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-data-collector/runtime-benchmarks", @@ -176,6 +179,7 @@ try-runtime = [ "frame-system/try-runtime", "orml-asset-registry/try-runtime", "orml-oracle/try-runtime", + "orml-tokens/try-runtime", "pallet-anchors/try-runtime", "pallet-authorship/try-runtime", "pallet-balances/try-runtime", diff --git a/runtime/common/src/apis/investments.rs b/runtime/common/src/apis/investments.rs index d07b45f00a..16257e429a 100644 --- a/runtime/common/src/apis/investments.rs +++ b/runtime/common/src/apis/investments.rs @@ -16,14 +16,12 @@ use sp_std::vec::Vec; decl_runtime_apis! { /// Runtime API for investments - pub trait InvestmentsApi + pub trait InvestmentsApi where AccountId: Codec, InvestmentId: Codec, - PoolId: Codec, - CurrencyId: Codec, - Balance: Codec, + InvestmentPortfolio: Codec, { - fn investment_portfolio(account_id: AccountId) -> Option>; + fn investment_portfolio(account_id: AccountId) -> Vec<(InvestmentId, InvestmentPortfolio)>; } } diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 8e47762d35..57d6762a2f 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -334,7 +334,6 @@ pub mod changes { use pallet_pool_system::pool_types::changes::Requirement; use super::*; - const SECONDS_PER_WEEK: u32 = 60; #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -373,50 +372,124 @@ pub mod changes { /// Module for investment portfolio common to all runtimes pub mod investment_portfolios { - - use cfg_traits::investments::{InvestmentsPortfolio, TrancheCurrency}; - use sp_std::vec::Vec; + use cfg_primitives::{Balance, PoolId, TrancheId}; + use cfg_traits::{ + investments::{InvestmentCollector, TrancheCurrency}, + PoolInspect, Seconds, + }; + use cfg_types::{investments::InvestmentPortfolio, tokens::CurrencyId}; + use sp_core::crypto::AccountId32; + use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; /// Get the PoolId, CurrencyId, InvestmentId, and Balance for all /// investments for an account. - pub fn get_portfolios< - Runtime, - AccountId, - TrancheId, - Investments, - InvestmentId, - CurrencyId, - PoolId, - Balance, - >( - account_id: AccountId, - ) -> Option> + pub fn get_account_portfolio( + investor: ::AccountId, + // TODO: Add limit for iterations + ) -> Vec<( + ::InvestmentId, + InvestmentPortfolio, + )> where - Investments: InvestmentsPortfolio< - AccountId, - AccountInvestmentPortfolio = Vec<(InvestmentId, CurrencyId, Balance)>, - InvestmentId = InvestmentId, - CurrencyId = CurrencyId, - Balance = Balance, + T: frame_system::Config + + pallet_investments::Config + + pallet_balances::Config + + orml_tokens::Config, + ::InvestmentId: + TrancheCurrency + Into<::CurrencyId> + Ord, + AccountId32: From<::AccountId>, + CurrencyId: From<::CurrencyId> + + From<::InvestmentId>, + Balance: From<::Balance> + + From<::Amount> + + From<::Balance>, + PoolInspector: PoolInspect< + ::AccountId, + ::CurrencyId, + PoolId = PoolId, + TrancheId = TrancheId, + Moment = Seconds, >, - AccountId: Into<::AccountId>, - InvestmentId: TrancheCurrency, - Runtime: frame_system::Config, { - let account_investments: Vec<(InvestmentId, CurrencyId, Balance)> = - Investments::get_account_investments_currency(&account_id).ok()?; - // Pool getting defined in runtime - // as opposed to pallet helper method - // as getting pool id in investments pallet - // would force tighter coupling of investments - // and pool pallets. - let portfolio: Vec<(PoolId, CurrencyId, InvestmentId, Balance)> = account_investments - .into_iter() - .map(|(investment_id, currency_id, balance)| { - (investment_id.of_pool(), currency_id, investment_id, balance) - }) - .collect(); - Some(portfolio) + let mut portfolio = BTreeMap::< + ::InvestmentId, + InvestmentPortfolio, + >::new(); + + // Denote current tranche token balances before dry running collecting + orml_tokens::Accounts::::iter_key_prefix(&investor).for_each(|currency| { + if let CurrencyId::Tranche(pool_id, tranche_id) = CurrencyId::from(currency) { + let balance = orml_tokens::Accounts::::get(&investor, currency); + portfolio + .entry(TrancheCurrency::generate(pool_id, tranche_id)) + .and_modify(|p| { + p.free_tranche_tokens = balance.free.into(); + p.locked_tranche_tokens = balance.frozen.into(); + }); + } + }); + + // Set pending invest currency and claimable tranche tokens + pallet_investments::InvestOrders::::iter_key_prefix(&investor).for_each(|invest_id| { + let amount = pallet_investments::InvestOrders::::get(&investor, invest_id) + .map(|order| order.amount()) + .unwrap_or_default(); + + // Collect such that we can determine claimable tranche tokens + // NOTE: Does not modify storage since RtAPI is readonly + let _ = + pallet_investments::Pallet::::collect_investment(investor.clone(), invest_id); + + let free_tranche_tokens_new = + orml_tokens::Accounts::::get(&investor, invest_id.into()) + .free + .into(); + portfolio.entry(invest_id).and_modify(|p| { + p.pending_invest_currency = amount.into(); + if p.free_tranche_tokens < free_tranche_tokens_new { + p.claimable_tranche_tokens = + free_tranche_tokens_new.saturating_sub(p.free_tranche_tokens); + } + }); + }); + + // Sett pending tranche tokens and claimable invest currency + pallet_investments::InvestOrders::::iter_key_prefix(&investor).for_each(|invest_id| { + let amount = pallet_investments::RedeemOrders::::get(&investor, invest_id) + .map(|order| order.amount()) + .unwrap_or_default(); + + let pool_currency = PoolInspector::currency_for(invest_id.of_pool()); + let balance_before: Balance = pool_currency + .map(|p_currency| { + orml_tokens::Accounts::::get(&investor, p_currency) + .free + .into() + }) + .unwrap_or_default(); + + // Collect such that we can determine claimable invest currency + // NOTE: Does not modify storage since RtAPI is readonly + let _ = + pallet_investments::Pallet::::collect_redemption(investor.clone(), invest_id); + + let balance_after: Balance = pool_currency + .map(|p_currency| { + orml_tokens::Accounts::::get(&investor, p_currency) + .free + .into() + }) + .unwrap_or_default(); + + portfolio.entry(invest_id).and_modify(|p| { + p.pending_redeem_tranche_tokens = amount.into(); + if balance_before < balance_after { + p.claimable_currency = balance_after.saturating_sub(balance_before); + } + }); + }); + + portfolio.into_iter().collect() } } diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index a7126c20b3..7269b6f376 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -33,6 +33,7 @@ use cfg_types::{ fee_keys::FeeKey, fixed_point::{Quantity, Rate, Ratio}, ids::PRICE_ORACLE_PALLET_ID, + investments::InvestmentPortfolio, locations::Location, oracles::OracleKey, permissions::{ @@ -2294,9 +2295,9 @@ impl_runtime_apis! { } // Investment Runtime APIs - impl runtime_common::apis::InvestmentsApi for Runtime { - fn investment_portfolio(account_id: AccountId) -> Option> { - runtime_common::investment_portfolios::get_portfolios::(account_id) + impl runtime_common::apis::InvestmentsApi> for Runtime { + fn investment_portfolio(account_id: AccountId) -> Vec<(TrancheCurrency, InvestmentPortfolio)> { + runtime_common::investment_portfolios::get_account_portfolio::(account_id) } } diff --git a/runtime/integration-tests/src/runtime_apis/investments.rs b/runtime/integration-tests/src/runtime_apis/investments.rs new file mode 100644 index 0000000000..d5b09601a2 --- /dev/null +++ b/runtime/integration-tests/src/runtime_apis/investments.rs @@ -0,0 +1,14 @@ +// Copyright 2023 Centrifuge Foundation (centrifuge.io). +// +// This file is part of the Centrifuge chain project. +// Centrifuge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version (see http://www.gnu.org/licenses). +// Centrifuge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +#[tokio::test] +async fn investment_portfolio_api_works() {} diff --git a/runtime/integration-tests/src/runtime_apis/mod.rs b/runtime/integration-tests/src/runtime_apis/mod.rs index 26e584e939..a1af10decc 100644 --- a/runtime/integration-tests/src/runtime_apis/mod.rs +++ b/runtime/integration-tests/src/runtime_apis/mod.rs @@ -9,6 +9,7 @@ // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. +mod investments; mod rewards; use std::sync::Arc;