Skip to content

Commit

Permalink
tests: investment portfolio single tranche
Browse files Browse the repository at this point in the history
  • Loading branch information
wischli committed Nov 16, 2023
1 parent 12c1adc commit 0e05be5
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 51 deletions.
36 changes: 36 additions & 0 deletions libs/types/src/investments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,39 @@ pub struct InvestmentPortfolio<Balance> {
/// The amount of pool currency which can be collected for a redeem order
pub claimable_currency: Balance,
}

impl<Balance: Default> InvestmentPortfolio<Balance> {
pub fn new() -> Self {
Default::default()
}

pub fn with_pending_invest_currency(mut self, amount: Balance) -> Self {
self.pending_invest_currency = amount;
self
}

pub fn with_free_tranche_tokens(mut self, amount: Balance) -> Self {
self.free_tranche_tokens = amount;
self
}

pub fn with_locked_tranche_tokens(mut self, amount: Balance) -> Self {
self.locked_tranche_tokens = amount;
self
}

pub fn with_claimable_tranche_tokens(mut self, amount: Balance) -> Self {
self.claimable_tranche_tokens = amount;
self
}

pub fn with_pending_redeem_tranche_tokens(mut self, amount: Balance) -> Self {
self.pending_redeem_tranche_tokens = amount;
self
}

pub fn with_claimable_currency(mut self, amount: Balance) -> Self {
self.claimable_currency = amount;
self
}
}
85 changes: 49 additions & 36 deletions runtime/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,14 @@ pub mod investment_portfolios {
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.
///
/// NOTE: Moving inner scope to any pallet would introduce tight(er)
/// coupling due to requirement of iterating over storage maps which in turn
/// require the pallet's Config trait.
pub fn get_account_portfolio<T, PoolInspector>(
investor: <T as frame_system::Config>::AccountId,
// TODO: Add limit for iterations
Expand All @@ -391,17 +394,11 @@ pub mod investment_portfolios {
InvestmentPortfolio<Balance>,
)>
where
T: frame_system::Config
+ pallet_investments::Config
+ pallet_balances::Config
+ orml_tokens::Config,
T: frame_system::Config + pallet_investments::Config + orml_tokens::Config,
<T as pallet_investments::Config>::InvestmentId:
TrancheCurrency<PoolId, TrancheId> + Into<<T as orml_tokens::Config>::CurrencyId> + Ord,
AccountId32: From<<T as frame_system::Config>::AccountId>,
CurrencyId: From<<T as orml_tokens::Config>::CurrencyId>
+ From<<T as pallet_investments::Config>::InvestmentId>,
Balance: From<<T as pallet_balances::Config>::Balance>
+ From<<T as pallet_investments::Config>::Amount>
CurrencyId: From<<T as orml_tokens::Config>::CurrencyId>,
Balance: From<<T as pallet_investments::Config>::Amount>
+ From<<T as orml_tokens::Config>::Balance>,
PoolInspector: PoolInspect<
<T as frame_system::Config>::AccountId,
Expand All @@ -425,40 +422,47 @@ pub mod investment_portfolios {
.and_modify(|p| {
p.free_tranche_tokens = balance.free.into();
p.locked_tranche_tokens = balance.frozen.into();
});
})
.or_insert(
InvestmentPortfolio::<Balance>::new()
.with_free_tranche_tokens(balance.free.into())
.with_locked_tranche_tokens(balance.frozen.into()),
);
}
});

// Set pending invest currency and claimable tranche tokens
pallet_investments::InvestOrders::<T>::iter_key_prefix(&investor).for_each(|invest_id| {
let amount = pallet_investments::InvestOrders::<T>::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::<T>::collect_investment(investor.clone(), invest_id);

let amount = pallet_investments::InvestOrders::<T>::get(&investor, invest_id)
.map(|order| order.amount())
.unwrap_or_default();
let free_tranche_tokens_new =
orml_tokens::Accounts::<T>::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::<T>::iter_key_prefix(&investor).for_each(|invest_id| {
let amount = pallet_investments::RedeemOrders::<T>::get(&investor, invest_id)
.map(|order| order.amount())
.unwrap_or_default();
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);
}
})
.or_insert(
InvestmentPortfolio::<Balance>::new()
.with_pending_invest_currency(amount.into())
.with_claimable_tranche_tokens(free_tranche_tokens_new),
);
});

// Set pending tranche tokens and claimable invest currency
pallet_investments::RedeemOrders::<T>::iter_key_prefix(&investor).for_each(|invest_id| {
let pool_currency = PoolInspector::currency_for(invest_id.of_pool());
let balance_before: Balance = pool_currency
.map(|p_currency| {
Expand All @@ -472,7 +476,9 @@ pub mod investment_portfolios {
// NOTE: Does not modify storage since RtAPI is readonly
let _ =
pallet_investments::Pallet::<T>::collect_redemption(investor.clone(), invest_id);

let amount = pallet_investments::RedeemOrders::<T>::get(&investor, invest_id)
.map(|order| order.amount())
.unwrap_or_default();
let balance_after: Balance = pool_currency
.map(|p_currency| {
orml_tokens::Accounts::<T>::get(&investor, p_currency)
Expand All @@ -481,12 +487,19 @@ pub mod investment_portfolios {
})
.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
.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);
}
})
.or_insert(
InvestmentPortfolio::<Balance>::new()
.with_pending_redeem_tranche_tokens(amount.into())
.with_claimable_currency(balance_after),
);
});

portfolio.into_iter().collect()
Expand Down
164 changes: 164 additions & 0 deletions runtime/integration-tests/src/generic/cases/investments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use cfg_primitives::{Balance, PoolId};
use cfg_traits::{investments::TrancheCurrency as _, Seconds};
use cfg_types::{investments::InvestmentPortfolio, permissions::PoolRole, tokens::TrancheCurrency};
use runtime_common::apis::{
runtime_decl_for_investments_api::InvestmentsApiV1, runtime_decl_for_pools_api::PoolsApiV1,
};
use sp_core::Get;

use crate::{
generic::{
config::Runtime,
env::{Blocks, Env},
envs::runtime_env::RuntimeEnv,
utils::{
self,
currency::{cfg, usd6, CurrencyInfo, Usd6},
genesis::{self, Genesis},
POOL_MIN_EPOCH_TIME,
},
},
utils::accounts::Keyring,
};

const POOL_ADMIN: Keyring = Keyring::Admin;
const INVESTOR: Keyring = Keyring::Alice;
const POOL_A: PoolId = 23;
const EXPECTED_POOL_BALANCE: Balance = usd6(1_000_000);
const REDEEM_AMOUNT: Balance = EXPECTED_POOL_BALANCE / 2;
const FOR_FEES: Balance = cfg(1);

mod common {
use super::*;

pub fn initialize_state_for_investments<E: Env<T>, T: Runtime>() -> E {
let mut env = E::from_storage(
Genesis::<T>::default()
.add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES))
.add(genesis::assets(vec![Usd6::ID]))
.add(genesis::tokens(vec![(Usd6::ID, Usd6::ED)]))
.storage(),
Genesis::<T>::default().storage(),
);

env.parachain_state_mut(|| {
// Create a pool
utils::give_balance::<T>(POOL_ADMIN.id(), T::PoolDeposit::get());
utils::create_empty_pool::<T>(POOL_ADMIN.id(), POOL_A, Usd6::ID);

// Grant permissions
let tranche_id = T::Api::tranche_id(POOL_A, 0).unwrap();
let tranche_investor = PoolRole::TrancheInvestor(tranche_id, Seconds::MAX);
utils::give_pool_role::<T>(INVESTOR.id(), POOL_A, tranche_investor);
});

env
}
}

fn investment_portfolio_single_tranche<T: Runtime>() {
let mut env = common::initialize_state_for_investments::<RuntimeEnv<T>, T>();

let tranche_id = env.parachain_state(|| T::Api::tranche_id(POOL_A, 0).unwrap());
let invest_id = TrancheCurrency::generate(POOL_A, tranche_id);

let mut investment_portfolio =
env.parachain_state(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(investment_portfolio, vec![]);

// Invest to have pending pool currency
env.parachain_state_mut(|| {
utils::give_tokens::<T>(INVESTOR.id(), Usd6::ID, EXPECTED_POOL_BALANCE);
utils::invest::<T>(INVESTOR.id(), POOL_A, tranche_id, EXPECTED_POOL_BALANCE);
assert_eq!(
pallet_investments::InvestOrders::<T>::get(INVESTOR.id(), invest_id)
.unwrap()
.amount(),
EXPECTED_POOL_BALANCE
);
});
investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(
investment_portfolio,
vec![(
invest_id,
InvestmentPortfolio::<Balance>::new()
.with_pending_invest_currency(EXPECTED_POOL_BALANCE)
)]
);

// Execute epoch to move pending to claimable pool currency
env.pass(Blocks::BySeconds(POOL_MIN_EPOCH_TIME));
env.parachain_state_mut(|| {
utils::close_pool_epoch::<T>(POOL_ADMIN.id(), POOL_A);
});
investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(
investment_portfolio,
vec![(
invest_id,
InvestmentPortfolio::<Balance>::new()
.with_claimable_tranche_tokens(EXPECTED_POOL_BALANCE)
)]
);

// Collect to move claimable pool currency to free tranche tokens
env.parachain_state_mut(|| {
utils::collect_investments::<T>(INVESTOR.id(), POOL_A, tranche_id);
});
investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(
investment_portfolio,
vec![(
invest_id,
InvestmentPortfolio::<Balance>::new().with_free_tranche_tokens(EXPECTED_POOL_BALANCE)
)]
);

// Redeem to move free tranche tokens to partially pending
env.parachain_state_mut(|| {
utils::redeem::<T>(INVESTOR.id(), POOL_A, tranche_id, REDEEM_AMOUNT);
});
investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(
investment_portfolio,
vec![(
invest_id,
InvestmentPortfolio::<Balance>::new()
.with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT)
.with_pending_redeem_tranche_tokens(REDEEM_AMOUNT)
)]
);

// Execute epoch to move pending tranche tokens to claimable pool currency
env.pass(Blocks::BySeconds(POOL_MIN_EPOCH_TIME));
env.parachain_state_mut(|| {
utils::close_pool_epoch::<T>(POOL_ADMIN.id(), POOL_A);
});
investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(
investment_portfolio,
vec![(
invest_id,
InvestmentPortfolio::<Balance>::new()
.with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT)
.with_claimable_currency(REDEEM_AMOUNT)
)]
);

// Collect redemption to clear claimable pool currency
env.parachain_state_mut(|| {
utils::collect_redemptions::<T>(INVESTOR.id(), POOL_A, tranche_id);
});
investment_portfolio = env.parachain_state_mut(|| T::Api::investment_portfolio(INVESTOR.id()));
assert_eq!(
investment_portfolio,
vec![(
invest_id,
InvestmentPortfolio::<Balance>::new()
.with_free_tranche_tokens(EXPECTED_POOL_BALANCE - REDEEM_AMOUNT)
)]
);
}

crate::test_for_runtimes!(all, investment_portfolio_single_tranche);
7 changes: 7 additions & 0 deletions runtime/integration-tests/src/generic/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use cfg_primitives::{
use cfg_traits::Millis;
use cfg_types::{
fixed_point::{Quantity, Rate},
investments::InvestmentPortfolio,
oracles::OracleKey,
permissions::{PermissionScope, Role},
tokens::{CurrencyId, CustomMetadata, TrancheCurrency},
Expand Down Expand Up @@ -128,6 +129,7 @@ pub trait Runtime:
+ TryInto<pallet_pool_system::Event<Self>>
+ From<frame_system::Event<Self>>
+ From<pallet_balances::Event<Self>>
+ From<pallet_investments::Event<Self>>
+ From<pallet_transaction_payment::Event<Self>>
+ From<pallet_loans::Event<Self>>
+ From<pallet_pool_system::Event<Self>>
Expand Down Expand Up @@ -170,6 +172,11 @@ pub trait Runtime:
CurrencyId,
Quantity,
Self::MaxTranchesExt,
> + apis::runtime_decl_for_investments_api::InvestmentsApiV1<
Self::Block,
AccountId,
TrancheCurrency,
InvestmentPortfolio<Balance>,
>;

type MaxTranchesExt: Codec + Get<u32> + Member + PartialOrd + TypeInfo;
Expand Down
1 change: 1 addition & 0 deletions runtime/integration-tests/src/generic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod utils;
// Test cases
mod cases {
mod example;
mod investments;
mod liquidity_pools;
mod loans;
}
Expand Down
Loading

0 comments on commit 0e05be5

Please sign in to comment.