From dada23c8f26a653b6b4bda22228c92e6ca91ef4b Mon Sep 17 00:00:00 2001 From: lemunozm Date: Wed, 25 Oct 2023 16:08:29 +0200 Subject: [PATCH 01/11] finish basic_loan_flow --- pallets/loans/src/entities/loans.rs | 8 +- .../src/generic/cases/loans.rs | 175 +++++++++++------- 2 files changed, 113 insertions(+), 70 deletions(-) diff --git a/pallets/loans/src/entities/loans.rs b/pallets/loans/src/entities/loans.rs index 340a78ed4e..180575f119 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -523,16 +523,16 @@ impl ActiveLoan { #[scale_info(skip_type_params(T))] pub struct ActiveLoanInfo { /// Related active loan - active_loan: ActiveLoan, + pub active_loan: ActiveLoan, /// Present value of the loan - present_value: T::Balance, + pub present_value: T::Balance, /// Current outstanding principal of this loan - outstanding_principal: T::Balance, + pub outstanding_principal: T::Balance, /// Current outstanding interest of this loan - outstanding_interest: T::Balance, + pub outstanding_interest: T::Balance, } impl TryFrom<(T::PoolId, ActiveLoan)> for ActiveLoanInfo { diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index 82fcf4b749..8d53262431 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -1,4 +1,4 @@ -use cfg_primitives::{Balance, CollectionId, ItemId, PoolId, SECONDS_PER_YEAR}; +use cfg_primitives::{Balance, CollectionId, ItemId, PoolId, SECONDS_PER_HOUR}; use cfg_traits::{ interest::{CompoundingSchedule, InterestRate}, Seconds, TimeAsSecs, @@ -7,7 +7,7 @@ use cfg_types::permissions::PoolRole; use frame_support::traits::Get; use pallet_loans::{ entities::{ - input::PrincipalInput, + input::{PrincipalInput, RepaidInput}, loans::LoanInfo, pricing::{ internal::{InternalPricing, MaxBorrowAmount as IntMaxBorrowAmount}, @@ -19,7 +19,9 @@ use pallet_loans::{ Maturity, PayDownSchedule, RepayRestrictions, RepaymentSchedule, }, }; -use runtime_common::apis::runtime_decl_for_PoolsApi::PoolsApiV1; +use runtime_common::apis::{ + runtime_decl_for_LoansApi::LoansApiV1, runtime_decl_for_PoolsApi::PoolsApiV1, +}; use crate::{ generic::{ @@ -50,77 +52,86 @@ const FOR_FEES: Balance = cfg(1); const EXPECTED_POOL_BALANCE: Balance = usd6(1_000_000); const COLLATERAL_VALUE: Balance = usd6(100_000); -fn initialize_state_for_loans, T: Runtime>() -> Environment { - let mut env = Environment::from_storage( - Genesis::::default() - .add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES)) - .add(genesis::assets(vec![Usd6::ID])) - .add(genesis::tokens(vec![(Usd6::ID, Usd6::ED)])) - .storage(), - ); - - env.state_mut(|| { - // Creating a pool - utils::give_balance::(POOL_ADMIN.id(), T::PoolDeposit::get()); - utils::create_empty_pool::(POOL_ADMIN.id(), POOL_A, Usd6::ID); - - // Funding a pool - let tranche_id = T::Api::tranche_id(POOL_A, 0).unwrap(); - let tranche_investor = PoolRole::TrancheInvestor(tranche_id, Seconds::MAX); - utils::give_pool_role::(INVESTOR.id(), POOL_A, tranche_investor); - utils::give_tokens::(INVESTOR.id(), Usd6::ID, EXPECTED_POOL_BALANCE); - utils::invest::(INVESTOR.id(), POOL_A, tranche_id, EXPECTED_POOL_BALANCE); - }); - - env.pass(Blocks::BySeconds(POOL_MIN_EPOCH_TIME)); - - env.state_mut(|| { - // New epoch with the investor funds available - utils::close_pool_epoch::(POOL_ADMIN.id(), POOL_A); - - // Preparing borrower - utils::give_pool_role::(BORROWER.id(), POOL_A, PoolRole::Borrower); - utils::give_nft::(BORROWER.id(), NFT_A); - }); - - env -} +mod common { + use super::*; + + pub fn initialize_state_for_loans, T: Runtime>() -> Environment { + let mut env = Environment::from_storage( + Genesis::::default() + .add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES)) + .add(genesis::assets(vec![Usd6::ID])) + .add(genesis::tokens(vec![(Usd6::ID, Usd6::ED)])) + .storage(), + ); + + env.state_mut(|| { + // Creating a pool + utils::give_balance::(POOL_ADMIN.id(), T::PoolDeposit::get()); + utils::create_empty_pool::(POOL_ADMIN.id(), POOL_A, Usd6::ID); + + // Funding a pool + let tranche_id = T::Api::tranche_id(POOL_A, 0).unwrap(); + let tranche_investor = PoolRole::TrancheInvestor(tranche_id, Seconds::MAX); + utils::give_pool_role::(INVESTOR.id(), POOL_A, tranche_investor); + utils::give_tokens::(INVESTOR.id(), Usd6::ID, EXPECTED_POOL_BALANCE); + utils::invest::(INVESTOR.id(), POOL_A, tranche_id, EXPECTED_POOL_BALANCE); + }); + + env.pass(Blocks::BySeconds(POOL_MIN_EPOCH_TIME)); + + env.state_mut(|| { + // New epoch with the investor funds available + utils::close_pool_epoch::(POOL_ADMIN.id(), POOL_A); + + // Preparing borrower + utils::give_pool_role::(BORROWER.id(), POOL_A, PoolRole::Borrower); + utils::give_nft::(BORROWER.id(), NFT_A); + }); + + env + } -fn internal_priced_loan(now: Seconds) -> LoanInfo { - LoanInfo { - schedule: RepaymentSchedule { - maturity: Maturity::Fixed { - date: now + SECONDS_PER_YEAR, - extension: SECONDS_PER_YEAR / 2, + pub fn internal_priced_loan(now: Seconds) -> LoanInfo { + LoanInfo { + schedule: RepaymentSchedule { + maturity: Maturity::Fixed { + date: now + SECONDS_PER_HOUR, + extension: SECONDS_PER_HOUR / 2, + }, + interest_payments: InterestPayments::None, + pay_down_schedule: PayDownSchedule::None, }, - interest_payments: InterestPayments::None, - pay_down_schedule: PayDownSchedule::None, - }, - interest_rate: InterestRate::Fixed { - rate_per_year: rate_from_percent(20), - compounding: CompoundingSchedule::Secondly, - }, - collateral: NFT_A, - pricing: Pricing::Internal(InternalPricing { - collateral_value: COLLATERAL_VALUE, - max_borrow_amount: IntMaxBorrowAmount::UpToTotalBorrowed { - advance_rate: rate_from_percent(100), + interest_rate: InterestRate::Fixed { + rate_per_year: rate_from_percent(20), + compounding: CompoundingSchedule::Secondly, }, - valuation_method: ValuationMethod::OutstandingDebt, - }), - restrictions: LoanRestrictions { - borrows: BorrowRestrictions::NotWrittenOff, - repayments: RepayRestrictions::None, - }, + collateral: NFT_A, + pricing: Pricing::Internal(InternalPricing { + collateral_value: COLLATERAL_VALUE, + max_borrow_amount: IntMaxBorrowAmount::UpToTotalBorrowed { + advance_rate: rate_from_percent(100), + }, + valuation_method: ValuationMethod::OutstandingDebt, + }), + restrictions: LoanRestrictions { + borrows: BorrowRestrictions::NotWrittenOff, + repayments: RepayRestrictions::None, + }, + } } } -fn borrow() { - let mut env = initialize_state_for_loans::, T>(); +/// Basic loan flow: +/// - creating +/// - borrowing +/// - repaying +/// - closing +fn basic_loan_flow() { + let mut env = common::initialize_state_for_loans::, T>(); let info = env.state(|| { let now = as TimeAsSecs>::now(); - internal_priced_loan::(now) + common::internal_priced_loan::(now) }); env.submit_now( @@ -148,6 +159,38 @@ fn borrow() { }, ) .unwrap(); + + env.pass(Blocks::BySeconds(SECONDS_PER_HOUR / 2)); + + let loan_portfolio = env.state(|| T::Api::portfolio_loan(POOL_A, loan_id).unwrap()); + + env.state_mut(|| { + // Required to be able the borrower to repay the interest accrued + utils::give_tokens::(BORROWER.id(), Usd6::ID, loan_portfolio.outstanding_interest); + }); + + env.submit_now( + BORROWER, + pallet_loans::Call::repay { + pool_id: POOL_A, + loan_id, + amount: RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), + interest: loan_portfolio.outstanding_interest, + unscheduled: 0, + }, + }, + ) + .unwrap(); + + env.submit_now( + BORROWER, + pallet_loans::Call::close { + pool_id: POOL_A, + loan_id, + }, + ) + .unwrap(); } -crate::test_for_runtimes!(all, borrow); +crate::test_for_runtimes!(all, basic_loan_flow); From 0ea63aa96b4b84bedbf255fea7509d53933a6f5f Mon Sep 17 00:00:00 2001 From: lemunozm Date: Wed, 25 Oct 2023 17:12:11 +0200 Subject: [PATCH 02/11] remove deprecated tests --- runtime/integration-tests/src/lib.rs | 1 - runtime/integration-tests/src/pools/env.rs | 122 ----- runtime/integration-tests/src/pools/mod.rs | 14 - .../src/pools/pool_starts.rs | 70 --- runtime/integration-tests/src/utils/env.rs | 31 ++ .../integration-tests/src/utils/extrinsics.rs | 87 +++ runtime/integration-tests/src/utils/loans.rs | 226 -------- runtime/integration-tests/src/utils/mod.rs | 2 - runtime/integration-tests/src/utils/pools.rs | 500 ------------------ 9 files changed, 118 insertions(+), 935 deletions(-) delete mode 100644 runtime/integration-tests/src/pools/env.rs delete mode 100644 runtime/integration-tests/src/pools/mod.rs delete mode 100644 runtime/integration-tests/src/pools/pool_starts.rs delete mode 100644 runtime/integration-tests/src/utils/loans.rs delete mode 100644 runtime/integration-tests/src/utils/pools.rs diff --git a/runtime/integration-tests/src/lib.rs b/runtime/integration-tests/src/lib.rs index 64f12522d2..bd4b09b5b6 100644 --- a/runtime/integration-tests/src/lib.rs +++ b/runtime/integration-tests/src/lib.rs @@ -17,7 +17,6 @@ mod evm; mod generic; mod liquidity_pools; -mod pools; mod rewards; mod runtime_apis; mod utils; diff --git a/runtime/integration-tests/src/pools/env.rs b/runtime/integration-tests/src/pools/env.rs deleted file mode 100644 index 0f97ffcea2..0000000000 --- a/runtime/integration-tests/src/pools/env.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2021 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. - -use codec::Encode; -use fudge::primitives::Chain; -use pallet_balances::Call as BalancesCall; -use sp_runtime::Storage; -use tokio::runtime::Handle; - -use crate::{ - chain::{ - centrifuge, - centrifuge::{Runtime, PARA_ID}, - }, - utils::{ - accounts::Keyring, - extrinsics::{nonce_centrifuge, xt_centrifuge}, - *, - }, -}; - -#[tokio::test] -async fn env_works() { - let mut env = env::test_env_default(Handle::current()); - - // FIXME: https://github.com/centrifuge/centrifuge-chain/issues/1219 - // Breaks on >= 10 for fast-runtime since session length is 5 blocks - #[cfg(feature = "fast-runtime")] - let num_blocks = 9; - #[cfg(not(feature = "fast-runtime"))] - let num_blocks = 10; - let block_before = env - .with_state(Chain::Para(PARA_ID), || { - frame_system::Pallet::::block_number() - }) - .expect("Cannot create block before"); - - frame_support::assert_ok!(env::pass_n(&mut env, num_blocks)); - - let block_after = env - .with_state(Chain::Para(PARA_ID), || { - frame_system::Pallet::::block_number() - }) - .expect("Cannot create block after"); - - assert_eq!(block_before + num_blocks as u32, block_after) -} - -#[tokio::test] -async fn extrinsics_works() { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - let mut env = env::test_env_with_centrifuge_storage(Handle::current(), genesis); - - let to: cfg_primitives::Address = Keyring::Bob.into(); - let xt = xt_centrifuge( - &env, - Keyring::Alice, - nonce_centrifuge(&env, Keyring::Alice), - centrifuge::RuntimeCall::Balances(BalancesCall::transfer { - dest: to, - value: 100 * cfg_primitives::constants::CFG, - }), - ) - .unwrap(); - env.append_extrinsic(Chain::Para(PARA_ID), xt.encode()) - .unwrap(); - - let (alice_before, bob_before) = env - .with_state(Chain::Para(PARA_ID), || { - ( - frame_system::Pallet::::account(Keyring::Alice.to_account_id()), - frame_system::Pallet::::account(Keyring::Bob.to_account_id()), - ) - }) - .unwrap(); - - env.evolve().unwrap(); - - let (alice_after, bob_after) = env - .with_state(Chain::Para(PARA_ID), || { - ( - frame_system::Pallet::::account(Keyring::Alice.to_account_id()), - frame_system::Pallet::::account(Keyring::Bob.to_account_id()), - ) - }) - .unwrap(); - - // Need to account for fees here - assert!(alice_after.data.free <= alice_before.data.free - 100 * cfg_primitives::constants::CFG); - assert_eq!( - bob_after.data.free, - bob_before.data.free + 100 * cfg_primitives::constants::CFG - ); - - env.evolve().unwrap(); - - let (alice_after, bob_after) = env - .with_state(Chain::Para(PARA_ID), || { - ( - frame_system::Pallet::::account(Keyring::Alice.to_account_id()), - frame_system::Pallet::::account(Keyring::Bob.to_account_id()), - ) - }) - .unwrap(); - - // Need to account for fees here - assert!(alice_after.data.free <= alice_before.data.free - 100 * cfg_primitives::constants::CFG); - assert_eq!( - bob_after.data.free, - bob_before.data.free + 100 * cfg_primitives::constants::CFG - ); -} diff --git a/runtime/integration-tests/src/pools/mod.rs b/runtime/integration-tests/src/pools/mod.rs deleted file mode 100644 index 8f182271d0..0000000000 --- a/runtime/integration-tests/src/pools/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2021 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. - -mod env; -mod pool_starts; diff --git a/runtime/integration-tests/src/pools/pool_starts.rs b/runtime/integration-tests/src/pools/pool_starts.rs deleted file mode 100644 index 48b9326bd0..0000000000 --- a/runtime/integration-tests/src/pools/pool_starts.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2021 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. -use cfg_primitives::{AccountId, Address, Balance, ItemId}; -use cfg_types::{fixed_point::Rate, permissions::PoolRole}; -use fudge::primitives::Chain; -use sp_runtime::{traits::AccountIdConversion, DispatchError, Storage, TokenError}; -use tokio::runtime::Handle; - -use crate::{ - chain::centrifuge::{Runtime, RuntimeCall, RuntimeEvent, PARA_ID}, - utils::{ - accounts::Keyring, - env::{ChainState, EventRange}, - loans::{borrow_call, issue_default_loan, NftManager}, - pools::{default_pool_calls, permission_call}, - time::secs::SECONDS_PER_DAY, - tokens::DECIMAL_BASE_12, - *, - }, -}; - -#[tokio::test] -async fn create_loan() { - let mut env = { - let mut genesis = Storage::default(); - genesis::default_balances::(&mut genesis); - genesis::register_default_asset::(&mut genesis); - env::test_env_with_centrifuge_storage(Handle::current(), genesis) - }; - - let mut nft_manager = NftManager::new(); - let pool_id = 0u64; - let loan_amount = 10_000 * DECIMAL_BASE_12; - let maturity = 90 * SECONDS_PER_DAY; - - env::run!( - env, - Chain::Para(PARA_ID), - RuntimeCall, - ChainState::PoolEmpty, - Keyring::Admin => default_pool_calls(Keyring::Admin.into(), pool_id, &mut nft_manager), - issue_default_loan( - Keyring::Admin.into(), - pool_id, - loan_amount, - maturity, - &mut nft_manager, - ) - ); - - env::assert_events!( - env, - Chain::Para(PARA_ID), - RuntimeEvent, - EventRange::All, - RuntimeEvent::System(frame_system::Event::ExtrinsicFailed{..}) if [count 0], - RuntimeEvent::PoolRegistry(pallet_pool_registry::Event::Registered { pool_id, .. }) if [*pool_id == 0], - RuntimeEvent::Loans(pallet_loans::Event::Created{ pool_id, loan_id, loan_info }) - if [*pool_id == 0 && *loan_id == 1 && loan_info.collateral() == (4294967296, ItemId(1))], - ); -} diff --git a/runtime/integration-tests/src/utils/env.rs b/runtime/integration-tests/src/utils/env.rs index 63bcb591ac..d36b39575a 100644 --- a/runtime/integration-tests/src/utils/env.rs +++ b/runtime/integration-tests/src/utils/env.rs @@ -935,3 +935,34 @@ pub fn pass_n(env: &mut TestEnv, n: u64) -> Result<(), ()> { Ok(()) } + +mod tests { + use super::*; + + #[tokio::test] + async fn env_works() { + let mut env = test_env_default(Handle::current()); + + // FIXME: https://github.com/centrifuge/centrifuge-chain/issues/1219 + // Breaks on >= 10 for fast-runtime since session length is 5 blocks + #[cfg(feature = "fast-runtime")] + let num_blocks = 9; + #[cfg(not(feature = "fast-runtime"))] + let num_blocks = 10; + let block_before = env + .with_state(Chain::Para(PARA_ID), || { + frame_system::Pallet::::block_number() + }) + .expect("Cannot create block before"); + + frame_support::assert_ok!(pass_n(&mut env, num_blocks)); + + let block_after = env + .with_state(Chain::Para(PARA_ID), || { + frame_system::Pallet::::block_number() + }) + .expect("Cannot create block after"); + + assert_eq!(block_before + num_blocks as u32, block_after) + } +} diff --git a/runtime/integration-tests/src/utils/extrinsics.rs b/runtime/integration-tests/src/utils/extrinsics.rs index b479896e28..e20c3b1611 100644 --- a/runtime/integration-tests/src/utils/extrinsics.rs +++ b/runtime/integration-tests/src/utils/extrinsics.rs @@ -218,3 +218,90 @@ where { frame_system::Pallet::::account_nonce(who.into()).into() } + +mod tests { + use codec::Encode; + use fudge::primitives::Chain; + use pallet_balances::Call as BalancesCall; + use sp_runtime::Storage; + use tokio::runtime::Handle; + + use super::{nonce_centrifuge, xt_centrifuge}; + use crate::{ + chain::{ + centrifuge, + centrifuge::{Runtime, PARA_ID}, + }, + utils::{accounts::Keyring, env, genesis}, + }; + + #[tokio::test] + async fn extrinsics_works() { + let mut genesis = Storage::default(); + genesis::default_balances::(&mut genesis); + let mut env = env::test_env_with_centrifuge_storage(Handle::current(), genesis); + + let to: cfg_primitives::Address = Keyring::Bob.into(); + let xt = xt_centrifuge( + &env, + Keyring::Alice, + nonce_centrifuge(&env, Keyring::Alice), + centrifuge::RuntimeCall::Balances(BalancesCall::transfer { + dest: to, + value: 100 * cfg_primitives::constants::CFG, + }), + ) + .unwrap(); + env.append_extrinsic(Chain::Para(PARA_ID), xt.encode()) + .unwrap(); + + let (alice_before, bob_before) = env + .with_state(Chain::Para(PARA_ID), || { + ( + frame_system::Pallet::::account(Keyring::Alice.to_account_id()), + frame_system::Pallet::::account(Keyring::Bob.to_account_id()), + ) + }) + .unwrap(); + + env.evolve().unwrap(); + + let (alice_after, bob_after) = env + .with_state(Chain::Para(PARA_ID), || { + ( + frame_system::Pallet::::account(Keyring::Alice.to_account_id()), + frame_system::Pallet::::account(Keyring::Bob.to_account_id()), + ) + }) + .unwrap(); + + // Need to account for fees here + assert!( + alice_after.data.free <= alice_before.data.free - 100 * cfg_primitives::constants::CFG + ); + assert_eq!( + bob_after.data.free, + bob_before.data.free + 100 * cfg_primitives::constants::CFG + ); + + env.evolve().unwrap(); + + let (alice_after, bob_after) = env + .with_state(Chain::Para(PARA_ID), || { + ( + frame_system::Pallet::::account(Keyring::Alice.to_account_id()), + frame_system::Pallet::::account(Keyring::Bob.to_account_id()), + ) + }) + .unwrap(); + + // Need to account for fees here + assert!( + alice_after.data.free <= alice_before.data.free - 100 * cfg_primitives::constants::CFG + ); + assert_eq!( + bob_after.data.free, + bob_before.data.free + 100 * cfg_primitives::constants::CFG + ); + } +} diff --git a/runtime/integration-tests/src/utils/loans.rs b/runtime/integration-tests/src/utils/loans.rs deleted file mode 100644 index 89f107b310..0000000000 --- a/runtime/integration-tests/src/utils/loans.rs +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2021 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. - -//! Utilities around the loans pallet -use std::{collections::HashMap, time::Duration}; - -use cfg_primitives::{ - AccountId, Address, Balance, CollectionId, ItemId, LoanId, PoolId, SECONDS_PER_YEAR, -}; -use cfg_traits::{ - interest::{CompoundingSchedule, InterestRate}, - Seconds, -}; -use cfg_types::fixed_point::Rate; -use pallet_loans::{ - entities::{ - input::{PrincipalInput, RepaidInput}, - loans::LoanInfo, - pricing::{ - internal::{InternalPricing, MaxBorrowAmount}, - Pricing, - }, - }, - types::{ - valuation::{DiscountedCashFlow, ValuationMethod}, - BorrowRestrictions, InterestPayments, LoanRestrictions, Maturity, PayDownSchedule, - RepaidAmount, RepayRestrictions, RepaymentSchedule, - }, - Call as LoansCall, -}; -use pallet_uniques::Call as UniquesCall; -use sp_runtime::{traits::One, FixedPointNumber}; - -use crate::{ - chain::centrifuge::{Runtime, RuntimeCall}, - utils::tokens::rate_from_percent, -}; - -type Asset = (CollectionId, ItemId); - -/// Structure that manages collateral and loan nft ids -pub struct NftManager { - collaterals: HashMap, - loans: HashMap, -} - -impl NftManager { - pub fn new() -> Self { - Self { - collaterals: HashMap::new(), - loans: HashMap::new(), - } - } - - /// Currently simply maps pool_id = loan_class_id for a pool - pub fn loan_class_id(&self, pool_id: PoolId) -> CollectionId { - pool_id - } - - /// Maps (pool_id + 1) << 32 = collateral_class id - /// - /// panics if pool_id >= u32::MAX - 1 as this would result in an overflow - /// during shifting. - pub fn collateral_class_id(&self, pool_id: PoolId) -> CollectionId { - assert!( - pool_id < u32::MAX.into(), - "Pool-id must be smaller u32::MAX for testing. To ensure no-clashes in NFT class-ids" - ); - let id = (pool_id + 1) << 32; - id - } - - pub fn curr_loan_id(&mut self, pool_id: PoolId) -> ItemId { - self.loans.entry(pool_id).or_insert(ItemId(1)).clone() - } - - fn next_loan_id(&mut self, pool_id: PoolId) -> ItemId { - let id = self.loans.entry(pool_id).or_insert(ItemId(1)); - let next = id.clone(); - *id = ItemId(id.0); - next - } - - pub fn curr_collateral_id(&mut self, pool_id: PoolId) -> ItemId { - self.loans.entry(pool_id).or_insert(ItemId(1)).clone() - } - - fn next_collateral_id(&mut self, pool_id: PoolId) -> ItemId { - let id = self.collaterals.entry(pool_id).or_insert(ItemId(1)); - let next = id.clone(); - *id = ItemId(id.0); - next - } -} - -/// Issues a default loan with the following properties -/// * 15% APR -/// * value with amount -/// * maturity as given -/// * Type: DiscountedCashFlow with UpToTotalBorrowed -/// * advance_rate: 90%, -/// * probability_of_default: 5%, -/// * loss_given_default: 50%, -/// * discount_rate: 4% , -pub fn issue_default_loan( - owner: AccountId, - pool_id: PoolId, - amount: Balance, - maturity: Seconds, - manager: &mut NftManager, -) -> Vec { - let loan_info = LoanInfo { - schedule: RepaymentSchedule { - maturity: Maturity::fixed(maturity), - interest_payments: InterestPayments::None, - pay_down_schedule: PayDownSchedule::None, - }, - collateral: ( - manager.collateral_class_id(pool_id), - manager.next_collateral_id(pool_id), - ), - interest_rate: InterestRate::Fixed { - rate_per_year: rate_from_percent(15), - compounding: CompoundingSchedule::Secondly, - }, - pricing: Pricing::Internal(InternalPricing { - collateral_value: amount, - max_borrow_amount: MaxBorrowAmount::UpToTotalBorrowed { - advance_rate: rate_from_percent(90), - }, - valuation_method: ValuationMethod::DiscountedCashFlow(DiscountedCashFlow { - probability_of_default: rate_from_percent(5), - loss_given_default: rate_from_percent(50), - discount_rate: InterestRate::Fixed { - rate_per_year: rate_from_percent(4), - compounding: CompoundingSchedule::Secondly, - }, - }), - }), - restrictions: LoanRestrictions { - borrows: BorrowRestrictions::NotWrittenOff, - repayments: RepayRestrictions::None, - }, - }; - - issue_loan(owner, pool_id, loan_info, manager) -} - -/// Issues a loan. -/// Should always be used instead of manually issuing a loan as this keeps the -/// `NftManager` in sync. -/// -/// * owner should also be `PricingAdmin` -/// * owner should be owner of `CollateralClass` -/// -/// Does create the following calls: -/// * mint collateral nft -/// * creates a new loan with this collateral -/// * prices the loan accordingly to input -pub fn issue_loan( - owner: AccountId, - pool_id: PoolId, - loan_info: LoanInfo, - manager: &mut NftManager, -) -> Vec { - let mut calls = Vec::new(); - calls.push(mint_nft_call( - manager.collateral_class_id(pool_id), - manager.next_collateral_id(pool_id), - owner, - )); - calls.push(create_loan_call(pool_id, loan_info)); - calls -} - -pub fn create_loan_call(pool_id: PoolId, info: LoanInfo) -> RuntimeCall { - RuntimeCall::Loans(LoansCall::create { pool_id, info }) -} - -pub fn borrow_call( - pool_id: PoolId, - loan_id: LoanId, - amount: PrincipalInput, -) -> RuntimeCall { - RuntimeCall::Loans(LoansCall::borrow { - pool_id, - loan_id, - amount, - }) -} - -pub fn repay_call(pool_id: PoolId, loan_id: LoanId, amount: RepaidInput) -> RuntimeCall { - RuntimeCall::Loans(LoansCall::repay { - pool_id, - loan_id, - amount, - }) -} - -pub fn close_loan_call(pool_id: PoolId, loan_id: LoanId) -> RuntimeCall { - RuntimeCall::Loans(LoansCall::close { pool_id, loan_id }) -} - -pub fn create_nft_call(admin: AccountId, collection: CollectionId) -> RuntimeCall { - RuntimeCall::Uniques(UniquesCall::create { - admin: Address::Id(admin), - collection, - }) -} - -pub fn mint_nft_call(collection: CollectionId, item: ItemId, owner: AccountId) -> RuntimeCall { - RuntimeCall::Uniques(UniquesCall::mint { - collection, - item, - owner: Address::Id(owner), - }) -} diff --git a/runtime/integration-tests/src/utils/mod.rs b/runtime/integration-tests/src/utils/mod.rs index 686713f73b..d92663d9fb 100644 --- a/runtime/integration-tests/src/utils/mod.rs +++ b/runtime/integration-tests/src/utils/mod.rs @@ -20,9 +20,7 @@ pub mod evm; pub mod extrinsics; pub mod genesis; pub mod liquidity_pools_gateway; -pub mod loans; pub mod logs; -pub mod pools; pub mod preimage; pub mod time; pub mod tokens; diff --git a/runtime/integration-tests/src/utils/pools.rs b/runtime/integration-tests/src/utils/pools.rs deleted file mode 100644 index 23420e787c..0000000000 --- a/runtime/integration-tests/src/utils/pools.rs +++ /dev/null @@ -1,500 +0,0 @@ -// Copyright 2021 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. - -//! Utilities around creating a pool - -use cfg_primitives::{AccountId, Balance, PoolId, TrancheId}; -use cfg_traits::Permissions as PermissionsT; -use cfg_types::{ - consts::pools::*, - fixed_point::Rate, - permissions::{PermissionScope, PoolRole, Role}, - pools::TrancheMetadata, - tokens::CurrencyId, -}; -use codec::Encode; -use frame_support::{Blake2_128, StorageHasher}; -use fudge::primitives::Chain; -use pallet_permissions::Call as PermissionsCall; -use pallet_pool_registry::Call as PoolRegistryCall; -use pallet_pool_system::{ - tranches::{TrancheIndex, TrancheInput, TrancheType}, - Call as PoolSystemCall, -}; -use sp_runtime::{traits::One, BoundedVec, FixedPointNumber, Perquintill}; - -use crate::{ - chain::centrifuge::{ - Loans, OrmlTokens, Permissions, PoolSystem, RuntimeCall, Timestamp, PARA_ID, - }, - utils::{ - accounts::Keyring, - env::TestEnv, - loans::NftManager, - time::secs::*, - tokens, - tokens::{DECIMAL_BASE_12, YEAR_RATE}, - AUSD_CURRENCY_ID, - }, -}; - -/// Creates a default pool. -/// -/// This will also inject the extrinsics needed for this. Furthermore, it -/// progresses the chain to a point where all extrinsics are included in the -/// state. -/// -/// Given keyring will be the origin that dispatches the calls and the admin of -/// the pool and its collateral and loan nft classes. -pub fn default_pool( - env: &mut TestEnv, - nfts: &mut NftManager, - admin: Keyring, - pool_id: PoolId, -) -> Result<(), ()> { - let calls: Vec> = default_pool_calls(admin.to_account_id(), pool_id, nfts) - .into_iter() - .map(|call| call.encode()) - .collect(); - env.batch_sign_and_submit(Chain::Para(PARA_ID), admin, calls) -} - -/// Creates a custom pool. -/// -/// This will also inject the extrinsics needed for this. Furthermore, it -/// progresses the chain to a point where all extrinsics are included in the -/// state. -/// -/// Given keyring will be the origin that dispatches the calls and the admin of -/// the pool and its collateral and loan nft classes. -pub fn custom_pool( - env: &mut TestEnv, - nfts: &mut NftManager, - admin: Keyring, - pool_id: PoolId, - currency: CurrencyId, - max_reserve: Balance, - tranche_inputs: Vec>, -) -> Result<(), ()> { - let calls: Vec> = pool_setup_calls( - admin.to_account_id(), - pool_id, - currency, - max_reserve, - tranche_inputs, - nfts, - ) - .into_iter() - .map(|call| call.encode()) - .collect(); - env.batch_sign_and_submit(Chain::Para(PARA_ID), admin, calls) -} - -/// Creates a default pool. -/// -/// This will also inject the extrinsics needed for this. Furthermore, it -/// progresses the chain to a point where all extrinsics are included in the -/// state. - -/// Creates the necessary calls for initialising a pool. -/// This includes: -/// * creating a pool -/// * whitelisting investors -/// * initialising the loans pallet for the given pool -/// -/// Extrinsics are returned and must be submitted to the transaction pool -/// in order to be included into the next block. -/// -/// * Pool id as given -/// * Admin as provided (also owner of pool, and owner of nft-classes for -/// collateral and loans) -/// * 5 Tranches -/// * 0: Junior Tranche -/// * 1: 10% APR, 5% Risk buffer -/// * 2: 7% APR, 5% Risk buffer -/// * 3: 5% APR, 10% Risk buffer -/// * 4: 3% APR, 25% Risk buffer -/// * Whitelistings -/// * Keyring::TrancheInvestor(index) accounts with index 0 - 9 for tranche -/// with id 0 -/// * Keyring::TrancheInvestor(index) accounts with index 10 - 19 for tranche -/// with id 1 -/// * Keyring::TrancheInvestor(index) accounts with index 20 - 29 for tranche -/// with id 2 -/// * Keyring::TrancheInvestor(index) accounts with index 30 - 39 for tranche -/// with id 3 -/// * Keyring::TrancheInvestor(index) accounts with index 40 - 49 for tranche -/// with id 4 -/// * Currency: AUSD (Acala USD), -/// * MaxReserve: 100_000 AUSD -pub fn default_pool_calls( - admin: AccountId, - pool_id: PoolId, - nfts: &mut NftManager, -) -> Vec { - pool_setup_calls( - admin, - pool_id, - AUSD_CURRENCY_ID, - 100_000 * DECIMAL_BASE_12, - create_tranche_input( - vec![None, Some(10), Some(7), Some(5), Some(3)], - vec![None, Some(5), Some(5), Some(10), Some(25)], - None, - ), - nfts, - ) -} - -/// Creates the necessary calls for setting up a pool. Given the input. -/// Per default there will be 10 investors whitelisted per tranche. -/// Order goes like follow `whitelist_10_for_each_tranche_calls` docs explain. -/// Furthermore, it will create the necessary calls for creating the -/// collateral-nft and loan-nft classes in the Uniques pallet. -pub fn pool_setup_calls( - admin: AccountId, - pool_id: PoolId, - currency: CurrencyId, - max_reserve: Balance, - tranche_input: Vec>, - nfts: &mut NftManager, -) -> Vec { - let mut calls = Vec::new(); - let num_tranches = tranche_input.len(); - calls.push(create_pool_call( - admin.clone(), - pool_id, - currency, - max_reserve, - tranche_input, - )); - calls.extend(whitelist_admin(admin.clone(), pool_id)); - calls.extend(whitelist_10_for_each_tranche_calls( - pool_id, - num_tranches as u32, - )); - - let collateral_class = nfts.collateral_class_id(pool_id); - calls.push(super::loans::create_nft_call(admin, collateral_class)); - calls -} - -/// Creates a TrancheInput vector given the input. -/// The given input data MUST be sorted from residual-to-non-residual tranches. -/// -/// DOES NOT check whether the length of the vectors match. It will simply zip -/// starting with rates. -pub fn create_tranche_input( - rates: Vec>, - risk_buffs: Vec>, - seniorities: Option>>, -) -> Vec> { - let interest_rates = rates - .into_iter() - .map(|rate| { - if let Some(rate) = rate { - Some(tokens::rate_from_percent(rate) / *YEAR_RATE + One::one()) - } else { - None - } - }) - .collect::>>(); - - let risk_buffs = risk_buffs - .into_iter() - .map(|buff| { - if let Some(buff) = buff { - Some(Perquintill::from_percent(buff)) - } else { - None - } - }) - .collect::>>(); - - let seniority = if let Some(seniorites) = seniorities { - seniorites - } else { - risk_buffs.iter().map(|_| None).collect() - }; - - interest_rates - .into_iter() - .zip(risk_buffs) - .zip(seniority) - .map(|((rate, buff), seniority)| { - if let (Some(interest_rate_per_sec), Some(min_risk_buffer)) = (rate, buff) { - TrancheInput { - tranche_type: TrancheType::NonResidual { - interest_rate_per_sec, - min_risk_buffer, - }, - seniority, - metadata: TrancheMetadata { - token_name: BoundedVec::default(), - token_symbol: BoundedVec::default(), - }, - } - } else { - TrancheInput { - tranche_type: TrancheType::Residual, - seniority, - metadata: TrancheMetadata { - token_name: BoundedVec::default(), - token_symbol: BoundedVec::default(), - }, - } - } - }) - .collect() -} - -/// Enables permission for all existing `PoolRole` variants -/// (except for PoolRole::TrancheInvestor) for the given account -pub fn whitelist_admin(admin: AccountId, pool_id: PoolId) -> Vec { - let mut calls = Vec::new(); - calls.push(permission_call( - PoolRole::PoolAdmin, - admin.clone(), - pool_id, - PoolRole::Borrower, - )); - calls.push(permission_call( - PoolRole::PoolAdmin, - admin.clone(), - pool_id, - PoolRole::LiquidityAdmin, - )); - calls.push(permission_call( - PoolRole::PoolAdmin, - admin.clone(), - pool_id, - PoolRole::LoanAdmin, - )); - calls.push(permission_call( - PoolRole::PoolAdmin, - admin.clone(), - pool_id, - PoolRole::InvestorAdmin, - )); - calls.push(permission_call( - PoolRole::PoolAdmin, - admin.clone(), - pool_id, - PoolRole::PricingAdmin, - )); - - calls -} - -/// This should only be used at start-up of a pool -/// The function generated xts for whitelisting 10 -/// investors per tranche. -/// -/// Note: -/// * Tranche-ids are calcualted as if no tranches were removed or added -> -/// tranche-id for residual tranche blake2_128::hash((0, pool_id)) -/// * Investor accounts whitelisted for respective tranche like -/// * Investors whitelisted for tranche 0 -/// * Keyring::TrancheInvestor(1) -/// * Keyring::TrancheInvestor(2) -/// * Keyring::TrancheInvestor(3) -/// * Keyring::TrancheInvestor(4) -/// * Keyring::TrancheInvestor(5) -/// * Keyring::TrancheInvestor(6) -/// * Keyring::TrancheInvestor(7) -/// * Keyring::TrancheInvestor(8) -/// * Keyring::TrancheInvestor(9) -/// * Keyring::TrancheInvestor(10) -/// * Investors whitelisted for tranche 1 -/// * Keyring::TrancheInvestor(11) -/// * Keyring::TrancheInvestor(12) -/// * Keyring::TrancheInvestor(13) -/// * Keyring::TrancheInvestor(14) -/// * Keyring::TrancheInvestor(15) -/// * Keyring::TrancheInvestor(16) -/// * Keyring::TrancheInvestor(17) -/// * Keyring::TrancheInvestor(18) -/// * Keyring::TrancheInvestor(19) -/// * Keyring::TrancheInvestor(20) -pub fn whitelist_10_for_each_tranche_calls(pool: PoolId, num_tranches: u32) -> Vec { - let mut calls = Vec::with_capacity(10 * num_tranches as usize); - - let mut x: u32 = 0; - while x < num_tranches { - for id in 1..11 { - calls.push(whitelist_investor_call( - pool, - Keyring::TrancheInvestor((x * 10) + id), - tranche_id(pool, x as u64), - )); - } - x += 1; - } - - calls -} - -/// Whitelist a given investor for a fiven pool and tranche for 1 year of time -pub fn whitelist_investor_call(pool: PoolId, investor: Keyring, tranche: TrancheId) -> RuntimeCall { - permission_call( - PoolRole::InvestorAdmin, - investor.to_account_id(), - pool, - PoolRole::TrancheInvestor(tranche, SECONDS_PER_YEAR), - ) -} - -/// Creates a permission xt with the given input -pub fn permission_call( - with_role: PoolRole, - to: AccountId, - pool_id: PoolId, - role: PoolRole, -) -> RuntimeCall { - RuntimeCall::Permissions(PermissionsCall::add { - to, - scope: PermissionScope::Pool(pool_id), - with_role: Role::PoolRole(with_role), - role: Role::PoolRole(role), - }) -} - -pub fn create_pool_call( - admin: AccountId, - pool_id: PoolId, - currency: CurrencyId, - max_reserve: Balance, - tranche_inputs: Vec>, -) -> RuntimeCall { - RuntimeCall::PoolRegistry(PoolRegistryCall::register { - admin, - pool_id, - tranche_inputs, - currency, - max_reserve, - metadata: None, - write_off_policy: BoundedVec::default(), - }) -} - -/// Calculates the tranche-id for pools at start-up. Makes it easier -/// to whitelist. -/// -/// Logic: Blake2_128::hash((tranche_index, pool_id)) -fn tranche_id(pool: PoolId, index: TrancheIndex) -> TrancheId { - Blake2_128::hash((index, pool).encode().as_slice()).into() -} - -/// A module where all calls need to be called within an -/// externalities provided environment. -mod with_ext { - use cfg_traits::PoolNAV; - - use super::*; - - /// Whitelists 10 tranche-investors per tranche. - /// - /// **Needs: Mut Externalities to persist** - /// ------------------------------- - /// E.g.: num_tranches = 2 - /// * Investors whitelisted for tranche 0 - /// * Keyring::TrancheInvestor(1) - /// * Keyring::TrancheInvestor(2) - /// * Keyring::TrancheInvestor(3) - /// * Keyring::TrancheInvestor(4) - /// * Keyring::TrancheInvestor(5) - /// * Keyring::TrancheInvestor(6) - /// * Keyring::TrancheInvestor(7) - /// * Keyring::TrancheInvestor(8) - /// * Keyring::TrancheInvestor(9) - /// * Keyring::TrancheInvestor(10) - /// * Investors whitelisted for tranche 1 - /// * Keyring::TrancheInvestor(11) - /// * Keyring::TrancheInvestor(12) - /// * Keyring::TrancheInvestor(13) - /// * Keyring::TrancheInvestor(14) - /// * Keyring::TrancheInvestor(15) - /// * Keyring::TrancheInvestor(16) - /// * Keyring::TrancheInvestor(17) - /// * Keyring::TrancheInvestor(18) - /// * Keyring::TrancheInvestor(19) - /// * Keyring::TrancheInvestor(20) - pub fn whitelist_investors(pool_id: PoolId, num_tranches: u32) { - let mut x: u32 = 0; - while x < num_tranches { - for id in 1..11 { - let id = (x * 10) + id; - permit_investor(id, pool_id, tranche_id(pool_id, x as u64)); - } - x += 1; - } - } - - /// Retrieves the token prices of a pool at the state that - /// this is called with. - /// - /// **Needs: Externalities** - /// - /// NOTE: - /// * update_portfolio_valuation() is called with Keyring::Admin as calley - pub fn get_tranche_prices(pool: PoolId) -> Vec { - let now = Timestamp::now(); - let mut details = PoolSystem::pool(pool).expect("POOLS: Getting pool failed."); - Loans::update_portfolio_valuation(Keyring::Admin.into(), pool) - .expect("LOANS: UpdatingNav failed"); - let (epoch_nav, _) = - >::nav(pool).expect("LOANS: Getting NAV failed"); - - let total_assets = details.reserve.total + epoch_nav; - - details - .tranches - .calculate_prices::<_, OrmlTokens, _>(total_assets, now) - .expect("POOLS: Calculating tranche-prices failed") - } - - /// Add a permission for who, at pool with role. - /// - /// **Needs: Mut Externalities to persist** - pub fn permission_for(who: AccountId, pool_id: PoolId, role: PoolRole) { - >::add( - PermissionScope::Pool(pool_id), - who, - Role::PoolRole(role), - ) - .expect("ESSENTIAL: Adding a permission for a role should not fail."); - } - - /// Adds all roles that `PoolRole`s currently provides to the Keyring::Admin - /// account - /// - /// **Needs: Mut Externalities to persist** - pub fn permit_admin(id: PoolId) { - permission_for(Keyring::Admin.into(), id, PoolRole::PricingAdmin); - permission_for(Keyring::Admin.into(), id, PoolRole::LiquidityAdmin); - permission_for(Keyring::Admin.into(), id, PoolRole::LoanAdmin); - permission_for(Keyring::Admin.into(), id, PoolRole::InvestorAdmin); - permission_for(Keyring::Admin.into(), id, PoolRole::Borrower); - } - - /// Add a `PoolRole::TrancheInvestor to a Keyring::TrancheInvestor(u32) - /// account. Role is permitted for 1 year. - /// - /// **Needs: Mut Externalities to persist** - pub fn permit_investor(investor: u32, pool: PoolId, tranche: TrancheId) { - permission_for( - Keyring::TrancheInvestor(investor).into(), - pool, - PoolRole::TrancheInvestor(tranche, SECONDS_PER_YEAR), - ) - } -} From fb0c847352931cfbe4d626131f7263c9e56590ce Mon Sep 17 00:00:00 2001 From: lemunozm Date: Wed, 25 Oct 2023 17:28:28 +0200 Subject: [PATCH 03/11] reorganization of the generic module --- .../src/generic/cases/example.rs | 4 +- .../src/generic/cases/loans.rs | 4 +- .../src/generic/{runtime.rs => config.rs} | 0 .../src/generic/{environment.rs => env.rs} | 2 +- .../src/generic/envs/fudge_env.rs | 6 +- .../src/generic/envs/fudge_env/handle.rs | 2 +- .../src/generic/envs/runtime_env.rs | 6 +- .../integration-tests/src/generic/impls.rs | 129 +++++++++++++++++ runtime/integration-tests/src/generic/mod.rs | 135 +----------------- .../src/generic/utils/genesis.rs | 2 +- .../src/generic/utils/mod.rs | 2 +- 11 files changed, 147 insertions(+), 145 deletions(-) rename runtime/integration-tests/src/generic/{runtime.rs => config.rs} (100%) rename runtime/integration-tests/src/generic/{environment.rs => env.rs} (98%) create mode 100644 runtime/integration-tests/src/generic/impls.rs diff --git a/runtime/integration-tests/src/generic/cases/example.rs b/runtime/integration-tests/src/generic/cases/example.rs index 61e32ef89d..694e94a25a 100644 --- a/runtime/integration-tests/src/generic/cases/example.rs +++ b/runtime/integration-tests/src/generic/cases/example.rs @@ -4,12 +4,12 @@ use sp_api::runtime_decl_for_Core::CoreV4; use crate::{ generic::{ - environment::{Blocks, Env}, + config::Runtime, + env::{Blocks, Env}, envs::{ fudge_env::{FudgeEnv, FudgeSupport}, runtime_env::RuntimeEnv, }, - runtime::Runtime, utils::genesis::Genesis, }, utils::accounts::Keyring, diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index 8d53262431..eadc8a6746 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -25,9 +25,9 @@ use runtime_common::apis::{ use crate::{ generic::{ - environment::{Blocks, Env}, + config::Runtime, + env::{Blocks, Env}, envs::runtime_env::RuntimeEnv, - runtime::Runtime, utils::{ self, genesis::{ diff --git a/runtime/integration-tests/src/generic/runtime.rs b/runtime/integration-tests/src/generic/config.rs similarity index 100% rename from runtime/integration-tests/src/generic/runtime.rs rename to runtime/integration-tests/src/generic/config.rs diff --git a/runtime/integration-tests/src/generic/environment.rs b/runtime/integration-tests/src/generic/env.rs similarity index 98% rename from runtime/integration-tests/src/generic/environment.rs rename to runtime/integration-tests/src/generic/env.rs index 77ee636071..1ed610e274 100644 --- a/runtime/integration-tests/src/generic/environment.rs +++ b/runtime/integration-tests/src/generic/env.rs @@ -7,7 +7,7 @@ use sp_runtime::{ DispatchError, DispatchResult, MultiSignature, Storage, }; -use crate::{generic::runtime::Runtime, utils::accounts::Keyring}; +use crate::{generic::config::Runtime, utils::accounts::Keyring}; /// Used by Env::pass() to determine how many blocks should be passed #[derive(Clone)] diff --git a/runtime/integration-tests/src/generic/envs/fudge_env.rs b/runtime/integration-tests/src/generic/envs/fudge_env.rs index abb5e2e997..e30c1936d6 100644 --- a/runtime/integration-tests/src/generic/envs/fudge_env.rs +++ b/runtime/integration-tests/src/generic/envs/fudge_env.rs @@ -11,8 +11,8 @@ use sp_runtime::{generic::BlockId, DispatchError, DispatchResult, Storage}; use crate::{ generic::{ - environment::{utils, Env}, - runtime::Runtime, + config::Runtime, + env::{utils, Env}, }, utils::accounts::Keyring, }; @@ -116,7 +116,7 @@ mod tests { use cfg_primitives::CFG; use super::*; - use crate::generic::{environment::Blocks, utils::genesis::Genesis}; + use crate::generic::{env::Blocks, utils::genesis::Genesis}; fn correct_nonce_for_submit_later() { let mut env = FudgeEnv::::from_storage( diff --git a/runtime/integration-tests/src/generic/envs/fudge_env/handle.rs b/runtime/integration-tests/src/generic/envs/fudge_env/handle.rs index 59b0d4fa1d..40da6557ec 100644 --- a/runtime/integration-tests/src/generic/envs/fudge_env/handle.rs +++ b/runtime/integration-tests/src/generic/envs/fudge_env/handle.rs @@ -27,7 +27,7 @@ use sp_runtime::Storage; use sp_transaction_pool::runtime_api::TaggedTransactionQueue; use tokio::runtime::Handle; -use crate::{generic::runtime::Runtime, utils::time::START_DATE}; +use crate::{generic::config::Runtime, utils::time::START_DATE}; type InherentCreator = Box< dyn CreateInherentDataProviders< diff --git a/runtime/integration-tests/src/generic/envs/runtime_env.rs b/runtime/integration-tests/src/generic/envs/runtime_env.rs index fca028b81c..887fd92de0 100644 --- a/runtime/integration-tests/src/generic/envs/runtime_env.rs +++ b/runtime/integration-tests/src/generic/envs/runtime_env.rs @@ -18,8 +18,8 @@ use sp_timestamp::Timestamp; use crate::{ generic::{ - environment::{utils, Env}, - runtime::Runtime, + config::Runtime, + env::{utils, Env}, }, utils::accounts::Keyring, }; @@ -197,7 +197,7 @@ mod tests { use cfg_primitives::CFG; use super::*; - use crate::generic::{environment::Blocks, utils::genesis::Genesis}; + use crate::generic::{env::Blocks, utils::genesis::Genesis}; fn correct_nonce_for_submit_now() { let mut env = RuntimeEnv::::from_storage( diff --git a/runtime/integration-tests/src/generic/impls.rs b/runtime/integration-tests/src/generic/impls.rs new file mode 100644 index 0000000000..a58d2e47bd --- /dev/null +++ b/runtime/integration-tests/src/generic/impls.rs @@ -0,0 +1,129 @@ +/// Implements the `Runtime` trait for a runtime +macro_rules! impl_runtime { + ($runtime_path:ident, $kind:ident) => { + const _: () = { + use crate::generic::config::{Runtime, RuntimeKind}; + + impl Runtime for $runtime_path::Runtime { + type Api = Self; + type Block = $runtime_path::Block; + type MaxTranchesExt = $runtime_path::MaxTranches; + type RuntimeCallExt = $runtime_path::RuntimeCall; + type RuntimeEventExt = $runtime_path::RuntimeEvent; + + const KIND: RuntimeKind = RuntimeKind::$kind; + } + }; + }; +} + +impl_runtime!(development_runtime, Development); +impl_runtime!(altair_runtime, Altair); +impl_runtime!(centrifuge_runtime, Centrifuge); + +/// Implements fudge support for a runtime +macro_rules! impl_fudge_support { + ( + $fudge_companion_type:ident, + $relay_path:ident, + $parachain_path:ident, + $parachain_id:literal + ) => { + const _: () = { + use fudge::primitives::Chain; + use polkadot_core_primitives::Block as RelayBlock; + use sp_api::ConstructRuntimeApi; + use sp_runtime::Storage; + + use crate::generic::envs::fudge_env::{ + handle::{ + FudgeHandle, ParachainBuilder, ParachainClient, RelayClient, RelaychainBuilder, + }, + FudgeSupport, + }; + + #[fudge::companion] + pub struct $fudge_companion_type { + #[fudge::relaychain] + pub relay: RelaychainBuilder<$relay_path::RuntimeApi, $relay_path::Runtime>, + + #[fudge::parachain($parachain_id)] + pub parachain: + ParachainBuilder<$parachain_path::Block, $parachain_path::RuntimeApi>, + } + + // Implement for T only one time when fudge::companion + // supports generic in the struct signature. + impl FudgeHandle<$parachain_path::Runtime> for $fudge_companion_type { + type ParachainApi = <$parachain_path::RuntimeApi as ConstructRuntimeApi< + $parachain_path::Block, + ParachainClient<$parachain_path::Block, Self::ParachainConstructApi>, + >>::RuntimeApi; + type ParachainConstructApi = $parachain_path::RuntimeApi; + type RelayApi = <$relay_path::RuntimeApi as ConstructRuntimeApi< + RelayBlock, + RelayClient, + >>::RuntimeApi; + type RelayConstructApi = $relay_path::RuntimeApi; + type RelayRuntime = $relay_path::Runtime; + + const PARACHAIN_CODE: Option<&'static [u8]> = $parachain_path::WASM_BINARY; + const PARA_ID: u32 = $parachain_id; + const RELAY_CODE: Option<&'static [u8]> = $relay_path::WASM_BINARY; + + fn new(relay_storage: Storage, parachain_storage: Storage) -> Self { + let relay = Self::new_relay_builder(relay_storage); + let parachain = Self::new_parachain_builder(&relay, parachain_storage); + + Self::new(relay, parachain).unwrap() + } + + fn relay(&self) -> &RelaychainBuilder { + &self.relay + } + + fn relay_mut( + &mut self, + ) -> &mut RelaychainBuilder { + &mut self.relay + } + + fn parachain( + &self, + ) -> &ParachainBuilder<$parachain_path::Block, Self::ParachainConstructApi> { + &self.parachain + } + + fn parachain_mut( + &mut self, + ) -> &mut ParachainBuilder<$parachain_path::Block, Self::ParachainConstructApi> { + &mut self.parachain + } + + fn append_extrinsic(&mut self, chain: Chain, extrinsic: Vec) -> Result<(), ()> { + self.append_extrinsic(chain, extrinsic) + } + + fn with_state(&self, chain: Chain, f: impl FnOnce() -> R) -> R { + self.with_state(chain, f).unwrap() + } + + fn with_mut_state(&mut self, chain: Chain, f: impl FnOnce() -> R) -> R { + self.with_mut_state(chain, f).unwrap() + } + + fn evolve(&mut self) { + self.evolve().unwrap() + } + } + + impl FudgeSupport for $parachain_path::Runtime { + type FudgeHandle = $fudge_companion_type; + } + }; + }; +} + +impl_fudge_support!(FudgeDevelopment, rococo_runtime, development_runtime, 2000); +impl_fudge_support!(FudgeAltair, kusama_runtime, altair_runtime, 2088); +impl_fudge_support!(CentrifugeAltair, polkadot_runtime, centrifuge_runtime, 2031); diff --git a/runtime/integration-tests/src/generic/mod.rs b/runtime/integration-tests/src/generic/mod.rs index 8d62d5ee0a..5ead789c2e 100644 --- a/runtime/integration-tests/src/generic/mod.rs +++ b/runtime/integration-tests/src/generic/mod.rs @@ -3,12 +3,13 @@ // Allow dead code for utilities not used yet #![allow(dead_code)] -pub mod environment; +pub mod env; pub mod envs { pub mod fudge_env; pub mod runtime_env; } -pub mod runtime; +pub mod config; +mod impls; pub mod utils; // Test cases @@ -17,13 +18,11 @@ mod cases { mod loans; } -use runtime::{Runtime, RuntimeKind}; - /// Generate tests for the specified runtimes or all runtimes. /// Usage /// /// ```rust -/// use crate::generic::runtime::Runtime; +/// use crate::generic::config::Runtime; /// /// fn foo { /// /// Your test here... @@ -70,129 +69,3 @@ macro_rules! test_for_runtimes { $crate::test_for_runtimes!([development, altair, centrifuge], $test_name); }; } - -/// Implements the `Runtime` trait for a runtime -macro_rules! impl_runtime { - ($runtime_path:ident, $kind:ident) => { - impl Runtime for $runtime_path::Runtime { - type Api = Self; - type Block = $runtime_path::Block; - type MaxTranchesExt = $runtime_path::MaxTranches; - type RuntimeCallExt = $runtime_path::RuntimeCall; - type RuntimeEventExt = $runtime_path::RuntimeEvent; - - const KIND: RuntimeKind = RuntimeKind::$kind; - } - }; -} - -impl_runtime!(development_runtime, Development); -impl_runtime!(altair_runtime, Altair); -impl_runtime!(centrifuge_runtime, Centrifuge); - -/// Implements fudge support for a runtime -macro_rules! impl_fudge_support { - ( - $fudge_companion_type:ident, - $relay_path:ident, - $parachain_path:ident, - $parachain_id:literal - ) => { - const _: () = { - use fudge::primitives::Chain; - use polkadot_core_primitives::Block as RelayBlock; - use sp_api::ConstructRuntimeApi; - use sp_runtime::Storage; - - use crate::generic::envs::fudge_env::{ - handle::{ - FudgeHandle, ParachainBuilder, ParachainClient, RelayClient, RelaychainBuilder, - }, - FudgeSupport, - }; - - #[fudge::companion] - pub struct $fudge_companion_type { - #[fudge::relaychain] - pub relay: RelaychainBuilder<$relay_path::RuntimeApi, $relay_path::Runtime>, - - #[fudge::parachain($parachain_id)] - pub parachain: - ParachainBuilder<$parachain_path::Block, $parachain_path::RuntimeApi>, - } - - // Implement for T only one time when fudge::companion - // supports generic in the struct signature. - impl FudgeHandle<$parachain_path::Runtime> for $fudge_companion_type { - type ParachainApi = <$parachain_path::RuntimeApi as ConstructRuntimeApi< - $parachain_path::Block, - ParachainClient<$parachain_path::Block, Self::ParachainConstructApi>, - >>::RuntimeApi; - type ParachainConstructApi = $parachain_path::RuntimeApi; - type RelayApi = <$relay_path::RuntimeApi as ConstructRuntimeApi< - RelayBlock, - RelayClient, - >>::RuntimeApi; - type RelayConstructApi = $relay_path::RuntimeApi; - type RelayRuntime = $relay_path::Runtime; - - const PARACHAIN_CODE: Option<&'static [u8]> = $parachain_path::WASM_BINARY; - const PARA_ID: u32 = $parachain_id; - const RELAY_CODE: Option<&'static [u8]> = $relay_path::WASM_BINARY; - - fn new(relay_storage: Storage, parachain_storage: Storage) -> Self { - let relay = Self::new_relay_builder(relay_storage); - let parachain = Self::new_parachain_builder(&relay, parachain_storage); - - Self::new(relay, parachain).unwrap() - } - - fn relay(&self) -> &RelaychainBuilder { - &self.relay - } - - fn relay_mut( - &mut self, - ) -> &mut RelaychainBuilder { - &mut self.relay - } - - fn parachain( - &self, - ) -> &ParachainBuilder<$parachain_path::Block, Self::ParachainConstructApi> { - &self.parachain - } - - fn parachain_mut( - &mut self, - ) -> &mut ParachainBuilder<$parachain_path::Block, Self::ParachainConstructApi> { - &mut self.parachain - } - - fn append_extrinsic(&mut self, chain: Chain, extrinsic: Vec) -> Result<(), ()> { - self.append_extrinsic(chain, extrinsic) - } - - fn with_state(&self, chain: Chain, f: impl FnOnce() -> R) -> R { - self.with_state(chain, f).unwrap() - } - - fn with_mut_state(&mut self, chain: Chain, f: impl FnOnce() -> R) -> R { - self.with_mut_state(chain, f).unwrap() - } - - fn evolve(&mut self) { - self.evolve().unwrap() - } - } - - impl FudgeSupport for $parachain_path::Runtime { - type FudgeHandle = $fudge_companion_type; - } - }; - }; -} - -impl_fudge_support!(FudgeDevelopment, rococo_runtime, development_runtime, 2000); -impl_fudge_support!(FudgeAltair, kusama_runtime, altair_runtime, 2088); -impl_fudge_support!(CentrifugeAltair, polkadot_runtime, centrifuge_runtime, 2031); diff --git a/runtime/integration-tests/src/generic/utils/genesis.rs b/runtime/integration-tests/src/generic/utils/genesis.rs index e37fe3df17..d035f4e5aa 100644 --- a/runtime/integration-tests/src/generic/utils/genesis.rs +++ b/runtime/integration-tests/src/generic/utils/genesis.rs @@ -6,7 +6,7 @@ use codec::Encode; use frame_support::traits::GenesisBuild; use sp_runtime::Storage; -use crate::{generic::runtime::Runtime, utils::accounts::default_accounts}; +use crate::{generic::config::Runtime, utils::accounts::default_accounts}; pub struct Genesis { storage: Storage, diff --git a/runtime/integration-tests/src/generic/utils/mod.rs b/runtime/integration-tests/src/generic/utils/mod.rs index 8bbd9c6920..9b13b7b80f 100644 --- a/runtime/integration-tests/src/generic/utils/mod.rs +++ b/runtime/integration-tests/src/generic/utils/mod.rs @@ -15,7 +15,7 @@ use frame_support::BoundedVec; use pallet_pool_system::tranches::{TrancheInput, TrancheType}; use sp_runtime::{traits::One, Perquintill}; -use crate::generic::runtime::{Runtime, RuntimeKind}; +use crate::generic::config::{Runtime, RuntimeKind}; pub const POOL_MIN_EPOCH_TIME: Seconds = 24; From 693d9e38906115ef34bf7f702f786971121129bd Mon Sep 17 00:00:00 2001 From: lemunozm Date: Wed, 25 Oct 2023 17:49:13 +0200 Subject: [PATCH 04/11] add Readme --- .../integration-tests/src/generic/README.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 runtime/integration-tests/src/generic/README.md diff --git a/runtime/integration-tests/src/generic/README.md b/runtime/integration-tests/src/generic/README.md new file mode 100644 index 0000000000..1fc390dd91 --- /dev/null +++ b/runtime/integration-tests/src/generic/README.md @@ -0,0 +1,23 @@ +# Runtime Generic tests + +The aim of this module is to make integration-tests independently for all runtimes at once. + +You can choose the environment you for your each of your use cases: + - `RuntimeEnv`: Simple environment that acts as a wrapper over the runtime + - `FudgeEnv`: Advanced environment that use a client and connect the runtime to a relay chain. + +Both environment uses the same interface so jumping from one to the another should be something smooth. + +## Where I start? +- Create a new file in `cases/` for the use case you want to test. +- Maybe you need to update the `Runtime` trait in `config.rs` file with extra information from your new pallet. + This could imply: + - Adding bounds to the `Runtime` trait with your new pallet. + - Adding bounds to `T::RuntimeCallExt` to support calls from your pallet. + - Adding bounds to `T::EventExt` to support events from your pallet. + - Adding bounds to `T::Api` to support new api calls. +- You can add `GenerisBuild` builders for setting the initial state of your pallet for others in `utils/genesis.rs`. + Please be as generic and simple as possible to leave others to compose its own requirement using your method, + without hidden initializations. +- You can add any utility that helps to initialize states for others under `utils` folder. + Again, focus in simplity but without side effects or hidden / non-obvious state changes. From f8be6d84efb881fcf9d62a3262fdfddf993ac657 Mon Sep 17 00:00:00 2001 From: lemunozm Date: Wed, 25 Oct 2023 20:28:12 +0200 Subject: [PATCH 05/11] support for external priced loans --- Cargo.lock | 1 + libs/types/src/fixed_point.rs | 5 + runtime/integration-tests/Cargo.toml | 1 + .../src/generic/cases/loans.rs | 105 +++++++++++++----- .../integration-tests/src/generic/config.rs | 10 +- .../src/generic/utils/genesis.rs | 8 +- .../src/generic/utils/mod.rs | 6 + 7 files changed, 107 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 663175b873..d66666807b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11085,6 +11085,7 @@ dependencies = [ "liquidity-pools-gateway-routers", "node-primitives", "orml-asset-registry", + "orml-oracle", "orml-tokens", "orml-traits", "orml-xtokens", diff --git a/libs/types/src/fixed_point.rs b/libs/types/src/fixed_point.rs index ee9f8e6e75..377232128a 100644 --- a/libs/types/src/fixed_point.rs +++ b/libs/types/src/fixed_point.rs @@ -623,6 +623,11 @@ impl FixedU128
{ pub const fn from_inner(inner: u128) -> Self { Self(inner) } + + /// const version of `FixedPointNumber::saturating_from_integer`. + pub const fn from_integer(n: u128) -> Self { + Self::from_inner(n * DIV) + } } impl Saturating for FixedU128
{ diff --git a/runtime/integration-tests/Cargo.toml b/runtime/integration-tests/Cargo.toml index e1857235bc..052b81c2cf 100644 --- a/runtime/integration-tests/Cargo.toml +++ b/runtime/integration-tests/Cargo.toml @@ -80,6 +80,7 @@ orml-asset-registry = { git = "https://github.com/open-web3-stack/open-runtime-m orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.38" } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.38" } orml-xtokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.38" } +orml-oracle = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.38" } # Misc xcm-emulator = { git = "https://github.com/shaunxw/xcm-simulator", rev = "754f3b90ecc65af735a6c9a2e1792c5253926ff6" } diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index eadc8a6746..bb0d47b54a 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -1,15 +1,18 @@ -use cfg_primitives::{Balance, CollectionId, ItemId, PoolId, SECONDS_PER_HOUR}; +use cfg_primitives::{conversion, Balance, CollectionId, ItemId, LoanId, PoolId, SECONDS_PER_HOUR}; use cfg_traits::{ interest::{CompoundingSchedule, InterestRate}, Seconds, TimeAsSecs, }; -use cfg_types::permissions::PoolRole; +use cfg_types::{ + fixed_point::Quantity, oracles::OracleKey, permissions::PoolRole, tokens::CurrencyId, +}; use frame_support::traits::Get; use pallet_loans::{ entities::{ input::{PrincipalInput, RepaidInput}, loans::LoanInfo, pricing::{ + external::{ExternalPricing, MaxBorrowAmount as ExtMaxBorrowAmount}, internal::{InternalPricing, MaxBorrowAmount as IntMaxBorrowAmount}, Pricing, }, @@ -32,7 +35,7 @@ use crate::{ self, genesis::{ self, - currency::{cfg, usd6, CurrencyInfo, Usd6}, + currency::{self, cfg, usd6, CurrencyInfo, Usd6}, Genesis, }, POOL_MIN_EPOCH_TIME, @@ -47,16 +50,19 @@ const BORROWER: Keyring = Keyring::Bob; const POOL_A: PoolId = 23; const NFT_A: (CollectionId, ItemId) = (1, ItemId(10)); +const PRICE_A: OracleKey = OracleKey::Isin(*b"INE123456AB1"); +const PRICE_A_VALUE: Quantity = Quantity::from_integer(1_000); const FOR_FEES: Balance = cfg(1); const EXPECTED_POOL_BALANCE: Balance = usd6(1_000_000); const COLLATERAL_VALUE: Balance = usd6(100_000); +const QUANTITY: Quantity = Quantity::from_integer(100); mod common { use super::*; - pub fn initialize_state_for_loans, T: Runtime>() -> Environment { - let mut env = Environment::from_storage( + pub fn initialize_state_for_loans, T: Runtime>() -> E { + let mut env = E::from_storage( Genesis::::default() .add(genesis::balances(T::ExistentialDeposit::get() + FOR_FEES)) .add(genesis::assets(vec![Usd6::ID])) @@ -91,7 +97,15 @@ mod common { env } - pub fn internal_priced_loan(now: Seconds) -> LoanInfo { + pub fn last_loan_id, T: Runtime>(env: &E) -> LoanId { + env.find_event(|e| match e { + pallet_loans::Event::::Created { loan_id, .. } => Some(loan_id), + _ => None, + }) + .unwrap() + } + + pub fn default_loan(now: Seconds, pricing: Pricing) -> LoanInfo { LoanInfo { schedule: RepaymentSchedule { maturity: Maturity::Fixed { @@ -106,32 +120,49 @@ mod common { compounding: CompoundingSchedule::Secondly, }, collateral: NFT_A, - pricing: Pricing::Internal(InternalPricing { - collateral_value: COLLATERAL_VALUE, - max_borrow_amount: IntMaxBorrowAmount::UpToTotalBorrowed { - advance_rate: rate_from_percent(100), - }, - valuation_method: ValuationMethod::OutstandingDebt, - }), + pricing: pricing, restrictions: LoanRestrictions { borrows: BorrowRestrictions::NotWrittenOff, repayments: RepayRestrictions::None, }, } } + + pub fn default_internal_pricing() -> Pricing { + Pricing::Internal(InternalPricing { + collateral_value: COLLATERAL_VALUE, + max_borrow_amount: IntMaxBorrowAmount::UpToTotalBorrowed { + advance_rate: rate_from_percent(100), + }, + valuation_method: ValuationMethod::OutstandingDebt, + }) + } + + pub fn default_external_pricing(curency_id: CurrencyId) -> Pricing { + Pricing::External(ExternalPricing { + price_id: PRICE_A, + max_borrow_amount: ExtMaxBorrowAmount::Quantity(QUANTITY), + notional: conversion::fixed_point_to_balance( + PRICE_A_VALUE, + currency::find_metadata(curency_id).decimals as usize, + ) + .unwrap(), + max_price_variation: rate_from_percent(0), + }) + } } -/// Basic loan flow: -/// - creating -/// - borrowing -/// - repaying -/// - closing +/// Test the basic loan flow, which consist in: +/// - create a loan +/// - borrow from the loan +/// - fully repay the loan until +/// - close the loan fn basic_loan_flow() { let mut env = common::initialize_state_for_loans::, T>(); let info = env.state(|| { let now = as TimeAsSecs>::now(); - common::internal_priced_loan::(now) + common::default_loan::(now, common::default_internal_pricing()) }); env.submit_now( @@ -143,12 +174,7 @@ fn basic_loan_flow() { ) .unwrap(); - let loan_id = env - .find_event(|e| match e { - pallet_loans::Event::::Created { loan_id, .. } => Some(loan_id), - _ => None, - }) - .unwrap(); + let loan_id = common::last_loan_id(&env); env.submit_now( BORROWER, @@ -165,7 +191,8 @@ fn basic_loan_flow() { let loan_portfolio = env.state(|| T::Api::portfolio_loan(POOL_A, loan_id).unwrap()); env.state_mut(|| { - // Required to be able the borrower to repay the interest accrued + // Give required tokens to the borrower to be able to repay the interest accrued + // until this moment utils::give_tokens::(BORROWER.id(), Usd6::ID, loan_portfolio.outstanding_interest); }); @@ -193,4 +220,30 @@ fn basic_loan_flow() { .unwrap(); } +/// Test using oracles to price the loan +fn oracle_priced_loan() { + let mut env = common::initialize_state_for_loans::, T>(); + + env.state_mut(|| utils::feed_oracle::(vec![(PRICE_A, PRICE_A_VALUE)])); + + let info = env.state(|| { + let now = as TimeAsSecs>::now(); + common::default_loan::(now, common::default_external_pricing(Usd6::ID)) + }); + + env.submit_now( + BORROWER, + pallet_loans::Call::create { + pool_id: POOL_A, + info, + }, + ) + .unwrap(); + + let loan_id = common::last_loan_id(&env); + + // TODO: in progress +} + crate::test_for_runtimes!(all, basic_loan_flow); +crate::test_for_runtimes!(all, oracle_priced_loan); diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index 1ef6afbd35..9d455aa59e 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -7,6 +7,7 @@ use cfg_primitives::{ use cfg_traits::Millis; use cfg_types::{ fixed_point::{Quantity, Rate}, + oracles::OracleKey, permissions::{PermissionScope, Role}, tokens::{CurrencyId, CustomMetadata, TrancheCurrency}, }; @@ -70,6 +71,8 @@ pub trait Runtime: CollectionId = CollectionId, ItemId = ItemId, Rate = Rate, + Quantity = Quantity, + PriceId = OracleKey, > + orml_tokens::Config + orml_asset_registry::Config< AssetId = CurrencyId, @@ -88,6 +91,7 @@ pub trait Runtime: Balance = Balance, NativeFungible = pallet_balances::Pallet, > + cumulus_pallet_parachain_system::Config + + orml_oracle::Config { /// Just the RuntimeCall type, but redefined with extra bounds. /// You can add `From` bounds in order to convert pallet calls to @@ -103,7 +107,8 @@ pub trait Runtime: + From> + From> + From> - + From>; + + From> + + From>; /// Just the RuntimeEvent type, but redefined with extra bounds. /// You can add `TryInto` and `From` bounds in order to convert pallet @@ -120,7 +125,8 @@ pub trait Runtime: + From> + From> + From> - + From>; + + From> + + From>; /// Block used by the runtime type Block: Block< diff --git a/runtime/integration-tests/src/generic/utils/genesis.rs b/runtime/integration-tests/src/generic/utils/genesis.rs index d035f4e5aa..62247e02bd 100644 --- a/runtime/integration-tests/src/generic/utils/genesis.rs +++ b/runtime/integration-tests/src/generic/utils/genesis.rs @@ -4,7 +4,7 @@ use cfg_primitives::{Balance, CFG}; use cfg_types::tokens::{AssetMetadata, CrossChainTransferability, CurrencyId, CustomMetadata}; use codec::Encode; use frame_support::traits::GenesisBuild; -use sp_runtime::Storage; +use sp_runtime::{FixedPointNumber, Storage}; use crate::{generic::config::Runtime, utils::accounts::default_accounts}; @@ -69,6 +69,8 @@ pub fn assets(currency_ids: Vec) -> impl GenesisBuild } pub mod currency { + use cfg_primitives::conversion; + use super::*; pub const fn cfg(amount: Balance) -> Balance { @@ -98,6 +100,10 @@ pub mod currency { }, } } + + fn fixed_point_as_balance>(value: N) -> Balance { + conversion::fixed_point_to_balance(value, Self::DECIMALS as usize).unwrap() + } } pub struct Usd6; diff --git a/runtime/integration-tests/src/generic/utils/mod.rs b/runtime/integration-tests/src/generic/utils/mod.rs index 9b13b7b80f..dedb5a0eef 100644 --- a/runtime/integration-tests/src/generic/utils/mod.rs +++ b/runtime/integration-tests/src/generic/utils/mod.rs @@ -3,6 +3,8 @@ use cfg_primitives::{AccountId, Balance, CollectionId, ItemId, PoolId, TrancheId}; use cfg_traits::{investments::TrancheCurrency as _, Seconds}; use cfg_types::{ + fixed_point::Quantity, + oracles::OracleKey, permissions::{PermissionScope, PoolRole, Role}, tokens::{CurrencyId, TrancheCurrency}, }; @@ -134,3 +136,7 @@ pub fn invest( ) .unwrap(); } + +pub fn feed_oracle(values: Vec<(OracleKey, Quantity)>) { + orml_oracle::Pallet::::feed_values(RawOrigin::Root.into(), values).unwrap(); +} From eff5303b31c5cd9dc888635eb0639ee082d9b2a7 Mon Sep 17 00:00:00 2001 From: lemunozm Date: Thu, 26 Oct 2023 13:27:03 +0200 Subject: [PATCH 06/11] oracle example working --- .../src/generic/cases/loans.rs | 165 +++++++++++++++--- .../src/generic/utils/currency.rs | 102 +++++++++++ .../src/generic/utils/genesis.rs | 96 +--------- .../src/generic/utils/mod.rs | 15 +- 4 files changed, 255 insertions(+), 123 deletions(-) create mode 100644 runtime/integration-tests/src/generic/utils/currency.rs diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index bb0d47b54a..a088348edd 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -1,18 +1,16 @@ -use cfg_primitives::{conversion, Balance, CollectionId, ItemId, LoanId, PoolId, SECONDS_PER_HOUR}; +use cfg_primitives::{Balance, CollectionId, ItemId, LoanId, PoolId, SECONDS_PER_MINUTE}; use cfg_traits::{ interest::{CompoundingSchedule, InterestRate}, Seconds, TimeAsSecs, }; -use cfg_types::{ - fixed_point::Quantity, oracles::OracleKey, permissions::PoolRole, tokens::CurrencyId, -}; +use cfg_types::{fixed_point::Quantity, oracles::OracleKey, permissions::PoolRole}; use frame_support::traits::Get; use pallet_loans::{ entities::{ input::{PrincipalInput, RepaidInput}, loans::LoanInfo, pricing::{ - external::{ExternalPricing, MaxBorrowAmount as ExtMaxBorrowAmount}, + external::{ExternalAmount, ExternalPricing, MaxBorrowAmount as ExtMaxBorrowAmount}, internal::{InternalPricing, MaxBorrowAmount as IntMaxBorrowAmount}, Pricing, }, @@ -33,11 +31,8 @@ use crate::{ envs::runtime_env::RuntimeEnv, utils::{ self, - genesis::{ - self, - currency::{self, cfg, usd6, CurrencyInfo, Usd6}, - Genesis, - }, + currency::{self, cfg, usd6, CurrencyInfo, Usd6}, + genesis::{self, Genesis}, POOL_MIN_EPOCH_TIME, }, }, @@ -51,7 +46,8 @@ const BORROWER: Keyring = Keyring::Bob; const POOL_A: PoolId = 23; const NFT_A: (CollectionId, ItemId) = (1, ItemId(10)); const PRICE_A: OracleKey = OracleKey::Isin(*b"INE123456AB1"); -const PRICE_A_VALUE: Quantity = Quantity::from_integer(1_000); +const PRICE_VALUE_A: Quantity = Quantity::from_integer(1_000); +const PRICE_VALUE_B: Quantity = Quantity::from_integer(500); const FOR_FEES: Balance = cfg(1); const EXPECTED_POOL_BALANCE: Balance = usd6(1_000_000); @@ -109,8 +105,8 @@ mod common { LoanInfo { schedule: RepaymentSchedule { maturity: Maturity::Fixed { - date: now + SECONDS_PER_HOUR, - extension: SECONDS_PER_HOUR / 2, + date: now + SECONDS_PER_MINUTE, + extension: SECONDS_PER_MINUTE / 2, }, interest_payments: InterestPayments::None, pay_down_schedule: PayDownSchedule::None, @@ -138,26 +134,90 @@ mod common { }) } - pub fn default_external_pricing(curency_id: CurrencyId) -> Pricing { + pub fn default_external_pricing() -> Pricing { Pricing::External(ExternalPricing { price_id: PRICE_A, max_borrow_amount: ExtMaxBorrowAmount::Quantity(QUANTITY), - notional: conversion::fixed_point_to_balance( - PRICE_A_VALUE, - currency::find_metadata(curency_id).decimals as usize, - ) - .unwrap(), + notional: currency::price_to_currency(PRICE_VALUE_A, Usd6::ID), max_price_variation: rate_from_percent(0), }) } } +mod call { + use super::*; + + pub fn create(info: LoanInfo) -> pallet_loans::Call { + pallet_loans::Call::create { + pool_id: POOL_A, + info, + } + } + + pub fn borrow_internal(loan_id: LoanId) -> pallet_loans::Call { + pallet_loans::Call::borrow { + pool_id: POOL_A, + loan_id, + amount: PrincipalInput::Internal(COLLATERAL_VALUE / 2), + } + } + + pub fn borrow_external(loan_id: LoanId) -> pallet_loans::Call { + pallet_loans::Call::borrow { + pool_id: POOL_A, + loan_id, + amount: PrincipalInput::External(ExternalAmount { + quantity: Quantity::from_integer(50), + settlement_price: currency::price_to_currency(PRICE_VALUE_A, Usd6::ID), + }), + } + } + + pub fn repay_internal(loan_id: LoanId, interest: Balance) -> pallet_loans::Call { + pallet_loans::Call::repay { + pool_id: POOL_A, + loan_id, + amount: RepaidInput { + principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), + interest, + unscheduled: 0, + }, + } + } + + pub fn repay_external( + loan_id: LoanId, + interest: Balance, + settlement_price: Quantity, + ) -> pallet_loans::Call { + pallet_loans::Call::repay { + pool_id: POOL_A, + loan_id, + amount: RepaidInput { + principal: PrincipalInput::External(ExternalAmount { + quantity: Quantity::from_integer(50), + settlement_price: currency::price_to_currency(settlement_price, Usd6::ID), + }), + interest, + unscheduled: 0, + }, + } + } + + pub fn close(loan_id: LoanId) -> pallet_loans::Call { + pallet_loans::Call::close { + pool_id: POOL_A, + loan_id, + } + } +} + /// Test the basic loan flow, which consist in: /// - create a loan /// - borrow from the loan /// - fully repay the loan until /// - close the loan -fn basic_loan_flow() { +fn internal_priced() { let mut env = common::initialize_state_for_loans::, T>(); let info = env.state(|| { @@ -186,10 +246,9 @@ fn basic_loan_flow() { ) .unwrap(); - env.pass(Blocks::BySeconds(SECONDS_PER_HOUR / 2)); + env.pass(Blocks::BySeconds(SECONDS_PER_MINUTE / 2)); let loan_portfolio = env.state(|| T::Api::portfolio_loan(POOL_A, loan_id).unwrap()); - env.state_mut(|| { // Give required tokens to the borrower to be able to repay the interest accrued // until this moment @@ -210,6 +269,7 @@ fn basic_loan_flow() { ) .unwrap(); + // Closing the loan succesfully means that the loan has been fully repaid env.submit_now( BORROWER, pallet_loans::Call::close { @@ -221,14 +281,14 @@ fn basic_loan_flow() { } /// Test using oracles to price the loan -fn oracle_priced_loan() { +fn oracle_priced() { let mut env = common::initialize_state_for_loans::, T>(); - env.state_mut(|| utils::feed_oracle::(vec![(PRICE_A, PRICE_A_VALUE)])); + env.state_mut(|| utils::feed_oracle::(vec![(PRICE_A, PRICE_VALUE_A)])); let info = env.state(|| { let now = as TimeAsSecs>::now(); - common::default_loan::(now, common::default_external_pricing(Usd6::ID)) + common::default_loan::(now, common::default_external_pricing()) }); env.submit_now( @@ -242,8 +302,57 @@ fn oracle_priced_loan() { let loan_id = common::last_loan_id(&env); - // TODO: in progress + env.submit_now( + BORROWER, + pallet_loans::Call::borrow { + pool_id: POOL_A, + loan_id, + amount: PrincipalInput::External(ExternalAmount { + quantity: Quantity::from_integer(50), + settlement_price: currency::price_to_currency(PRICE_VALUE_A, Usd6::ID), + }), + }, + ) + .unwrap(); + + env.pass(Blocks::BySeconds(SECONDS_PER_MINUTE / 2)); + + env.state_mut(|| utils::feed_oracle::(vec![(PRICE_A, PRICE_VALUE_B)])); + + let loan_portfolio = env.state(|| T::Api::portfolio_loan(POOL_A, loan_id).unwrap()); + env.state_mut(|| { + // Give required tokens to the borrower to be able to repay the interest accrued + // until this moment + utils::give_tokens::(BORROWER.id(), Usd6::ID, loan_portfolio.outstanding_interest); + }); + + env.submit_now( + BORROWER, + pallet_loans::Call::repay { + pool_id: POOL_A, + loan_id, + amount: RepaidInput { + principal: PrincipalInput::External(ExternalAmount { + quantity: Quantity::from_integer(50), + settlement_price: currency::price_to_currency(PRICE_VALUE_B, Usd6::ID), + }), + interest: loan_portfolio.outstanding_interest, + unscheduled: 0, + }, + }, + ) + .unwrap(); + + // Closing the loan succesfully means that the loan has been fully repaid + env.submit_now( + BORROWER, + pallet_loans::Call::close { + pool_id: POOL_A, + loan_id, + }, + ) + .unwrap(); } -crate::test_for_runtimes!(all, basic_loan_flow); -crate::test_for_runtimes!(all, oracle_priced_loan); +crate::test_for_runtimes!(all, internal_priced); +crate::test_for_runtimes!(all, oracle_priced); diff --git a/runtime/integration-tests/src/generic/utils/currency.rs b/runtime/integration-tests/src/generic/utils/currency.rs new file mode 100644 index 0000000000..3a0c180785 --- /dev/null +++ b/runtime/integration-tests/src/generic/utils/currency.rs @@ -0,0 +1,102 @@ +use cfg_primitives::{conversion, Balance, CFG}; +use cfg_types::tokens::{AssetMetadata, CrossChainTransferability, CurrencyId, CustomMetadata}; +use sp_runtime::FixedPointNumber; + +pub const fn cfg(amount: Balance) -> Balance { + amount * CFG +} + +pub trait CurrencyInfo { + const ID: CurrencyId; + const DECIMALS: u32; + const UNIT: Balance = 10u128.pow(Self::DECIMALS); + const SYMBOL: &'static str; + const NAME: &'static str = Self::SYMBOL; + const LOCATION: Option = None; + const CUSTOM: CustomMetadata; + const ED: Balance = 0; + + fn metadata() -> AssetMetadata { + AssetMetadata { + decimals: Self::DECIMALS, + name: Self::NAME.as_bytes().to_vec(), + symbol: Self::SYMBOL.as_bytes().to_vec(), + existential_deposit: Self::ED, + location: None, + additional: CustomMetadata { + pool_currency: true, + ..Default::default() + }, + } + } +} + +/// Matches default() but for const support +const CONST_DEFAULT_CUSTOM: CustomMetadata = CustomMetadata { + transferability: CrossChainTransferability::None, + mintable: false, + permissioned: false, + pool_currency: false, +}; + +pub fn find_metadata(currency_id: CurrencyId) -> AssetMetadata { + match currency_id { + Usd6::ID => Usd6::metadata(), + Usd12::ID => Usd12::metadata(), + Usd18::ID => Usd18::metadata(), + _ => panic!("Unsupported currency {currency_id:?}"), + } +} + +pub fn price_to_currency>( + price: N, + currency_id: CurrencyId, +) -> Balance { + let decimals = find_metadata(currency_id).decimals; + conversion::fixed_point_to_balance(price, decimals as usize).unwrap() +} + +pub struct Usd6; +impl CurrencyInfo for Usd6 { + const CUSTOM: CustomMetadata = CustomMetadata { + pool_currency: true, + ..CONST_DEFAULT_CUSTOM + }; + const DECIMALS: u32 = 6; + const ID: CurrencyId = CurrencyId::ForeignAsset(1); + const SYMBOL: &'static str = "USD6"; +} + +pub const fn usd6(amount: Balance) -> Balance { + amount * Usd6::UNIT +} + +pub struct Usd12; +impl CurrencyInfo for Usd12 { + const CUSTOM: CustomMetadata = CustomMetadata { + pool_currency: true, + ..CONST_DEFAULT_CUSTOM + }; + const DECIMALS: u32 = 12; + const ID: CurrencyId = CurrencyId::ForeignAsset(2); + const SYMBOL: &'static str = "USD12"; +} + +pub const fn usd12(amount: Balance) -> Balance { + amount * Usd12::UNIT +} + +pub struct Usd18; +impl CurrencyInfo for Usd18 { + const CUSTOM: CustomMetadata = CustomMetadata { + pool_currency: true, + ..CONST_DEFAULT_CUSTOM + }; + const DECIMALS: u32 = 18; + const ID: CurrencyId = CurrencyId::ForeignAsset(3); + const SYMBOL: &'static str = "USD12"; +} + +pub const fn usd18(amount: Balance) -> Balance { + amount * Usd18::UNIT +} diff --git a/runtime/integration-tests/src/generic/utils/genesis.rs b/runtime/integration-tests/src/generic/utils/genesis.rs index 62247e02bd..99dbd9ef71 100644 --- a/runtime/integration-tests/src/generic/utils/genesis.rs +++ b/runtime/integration-tests/src/generic/utils/genesis.rs @@ -1,12 +1,15 @@ use std::marker::PhantomData; -use cfg_primitives::{Balance, CFG}; -use cfg_types::tokens::{AssetMetadata, CrossChainTransferability, CurrencyId, CustomMetadata}; +use cfg_primitives::Balance; +use cfg_types::tokens::CurrencyId; use codec::Encode; use frame_support::traits::GenesisBuild; -use sp_runtime::{FixedPointNumber, Storage}; +use sp_runtime::Storage; -use crate::{generic::config::Runtime, utils::accounts::default_accounts}; +use crate::{ + generic::{config::Runtime, utils::currency}, + utils::accounts::default_accounts, +}; pub struct Genesis { storage: Storage, @@ -67,88 +70,3 @@ pub fn assets(currency_ids: Vec) -> impl GenesisBuild last_asset_id: Default::default(), // It seems deprecated } } - -pub mod currency { - use cfg_primitives::conversion; - - use super::*; - - pub const fn cfg(amount: Balance) -> Balance { - amount * CFG - } - - pub trait CurrencyInfo { - const ID: CurrencyId; - const DECIMALS: u32; - const UNIT: Balance = 10u128.pow(Self::DECIMALS); - const SYMBOL: &'static str; - const NAME: &'static str = Self::SYMBOL; - const LOCATION: Option = None; - const CUSTOM: CustomMetadata; - const ED: Balance = 0; - - fn metadata() -> AssetMetadata { - AssetMetadata { - decimals: Self::DECIMALS, - name: Self::NAME.as_bytes().to_vec(), - symbol: Self::SYMBOL.as_bytes().to_vec(), - existential_deposit: Self::ED, - location: None, - additional: CustomMetadata { - pool_currency: true, - ..Default::default() - }, - } - } - - fn fixed_point_as_balance>(value: N) -> Balance { - conversion::fixed_point_to_balance(value, Self::DECIMALS as usize).unwrap() - } - } - - pub struct Usd6; - impl CurrencyInfo for Usd6 { - const CUSTOM: CustomMetadata = CustomMetadata { - pool_currency: true, - ..CONST_DEFAULT_CUSTOM - }; - const DECIMALS: u32 = 6; - const ID: CurrencyId = CurrencyId::ForeignAsset(1); - const SYMBOL: &'static str = "USD6"; - } - - pub const fn usd6(amount: Balance) -> Balance { - amount * Usd6::UNIT - } - - pub struct Usd12; - impl CurrencyInfo for Usd12 { - const CUSTOM: CustomMetadata = CustomMetadata { - pool_currency: true, - ..CONST_DEFAULT_CUSTOM - }; - const DECIMALS: u32 = 12; - const ID: CurrencyId = CurrencyId::ForeignAsset(2); - const SYMBOL: &'static str = "USD12"; - } - - pub const fn usd12(amount: Balance) -> Balance { - amount * Usd12::UNIT - } - - /// Matches default() but for const support - const CONST_DEFAULT_CUSTOM: CustomMetadata = CustomMetadata { - transferability: CrossChainTransferability::None, - mintable: false, - permissioned: false, - pool_currency: false, - }; - - pub fn find_metadata(currency: CurrencyId) -> AssetMetadata { - match currency { - Usd6::ID => Usd6::metadata(), - Usd12::ID => Usd12::metadata(), - _ => panic!("Unsupported currency {currency:?}"), - } - } -} diff --git a/runtime/integration-tests/src/generic/utils/mod.rs b/runtime/integration-tests/src/generic/utils/mod.rs index dedb5a0eef..dff37029fe 100644 --- a/runtime/integration-tests/src/generic/utils/mod.rs +++ b/runtime/integration-tests/src/generic/utils/mod.rs @@ -1,21 +1,24 @@ // Divide this utilties into files when it grows +pub mod currency; +pub mod genesis; + use cfg_primitives::{AccountId, Balance, CollectionId, ItemId, PoolId, TrancheId}; use cfg_traits::{investments::TrancheCurrency as _, Seconds}; use cfg_types::{ fixed_point::Quantity, oracles::OracleKey, permissions::{PermissionScope, PoolRole, Role}, + pools::TrancheMetadata, tokens::{CurrencyId, TrancheCurrency}, }; -use frame_system::RawOrigin; -use sp_runtime::traits::StaticLookup; -pub mod genesis; - -use cfg_types::pools::TrancheMetadata; use frame_support::BoundedVec; +use frame_system::RawOrigin; use pallet_pool_system::tranches::{TrancheInput, TrancheType}; -use sp_runtime::{traits::One, Perquintill}; +use sp_runtime::{ + traits::{One, StaticLookup}, + Perquintill, +}; use crate::generic::config::{Runtime, RuntimeKind}; From c434bf7a92300b045b11f034483dd8ee8fd4b095 Mon Sep 17 00:00:00 2001 From: lemunozm Date: Thu, 26 Oct 2023 13:31:12 +0200 Subject: [PATCH 07/11] refactor using calls --- .../src/generic/cases/loans.rs | 101 ++++-------------- 1 file changed, 20 insertions(+), 81 deletions(-) diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index a088348edd..bd600e00fb 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -54,6 +54,7 @@ const EXPECTED_POOL_BALANCE: Balance = usd6(1_000_000); const COLLATERAL_VALUE: Balance = usd6(100_000); const QUANTITY: Quantity = Quantity::from_integer(100); +/// Common utilities for loan use cases mod common { use super::*; @@ -101,7 +102,7 @@ mod common { .unwrap() } - pub fn default_loan(now: Seconds, pricing: Pricing) -> LoanInfo { + pub fn default_loan_info(now: Seconds, pricing: Pricing) -> LoanInfo { LoanInfo { schedule: RepaymentSchedule { maturity: Maturity::Fixed { @@ -144,13 +145,14 @@ mod common { } } +/// Predefined loan calls for use cases mod call { use super::*; - pub fn create(info: LoanInfo) -> pallet_loans::Call { + pub fn create(info: &LoanInfo) -> pallet_loans::Call { pallet_loans::Call::create { pool_id: POOL_A, - info, + info: info.clone(), } } @@ -222,29 +224,15 @@ fn internal_priced() { let info = env.state(|| { let now = as TimeAsSecs>::now(); - common::default_loan::(now, common::default_internal_pricing()) + common::default_loan_info::(now, common::default_internal_pricing()) }); - env.submit_now( - BORROWER, - pallet_loans::Call::create { - pool_id: POOL_A, - info, - }, - ) - .unwrap(); + env.submit_now(BORROWER, call::create(&info)).unwrap(); let loan_id = common::last_loan_id(&env); - env.submit_now( - BORROWER, - pallet_loans::Call::borrow { - pool_id: POOL_A, - loan_id, - amount: PrincipalInput::Internal(COLLATERAL_VALUE / 2), - }, - ) - .unwrap(); + env.submit_now(BORROWER, call::borrow_internal(loan_id)) + .unwrap(); env.pass(Blocks::BySeconds(SECONDS_PER_MINUTE / 2)); @@ -257,27 +245,12 @@ fn internal_priced() { env.submit_now( BORROWER, - pallet_loans::Call::repay { - pool_id: POOL_A, - loan_id, - amount: RepaidInput { - principal: PrincipalInput::Internal(COLLATERAL_VALUE / 2), - interest: loan_portfolio.outstanding_interest, - unscheduled: 0, - }, - }, + call::repay_internal(loan_id, loan_portfolio.outstanding_interest), ) .unwrap(); // Closing the loan succesfully means that the loan has been fully repaid - env.submit_now( - BORROWER, - pallet_loans::Call::close { - pool_id: POOL_A, - loan_id, - }, - ) - .unwrap(); + env.submit_now(BORROWER, call::close(loan_id)).unwrap(); } /// Test using oracles to price the loan @@ -288,70 +261,36 @@ fn oracle_priced() { let info = env.state(|| { let now = as TimeAsSecs>::now(); - common::default_loan::(now, common::default_external_pricing()) + common::default_loan_info::(now, common::default_external_pricing()) }); - env.submit_now( - BORROWER, - pallet_loans::Call::create { - pool_id: POOL_A, - info, - }, - ) - .unwrap(); + env.submit_now(BORROWER, call::create(&info)).unwrap(); let loan_id = common::last_loan_id(&env); - env.submit_now( - BORROWER, - pallet_loans::Call::borrow { - pool_id: POOL_A, - loan_id, - amount: PrincipalInput::External(ExternalAmount { - quantity: Quantity::from_integer(50), - settlement_price: currency::price_to_currency(PRICE_VALUE_A, Usd6::ID), - }), - }, - ) - .unwrap(); + env.submit_now(BORROWER, call::borrow_external(loan_id)) + .unwrap(); env.pass(Blocks::BySeconds(SECONDS_PER_MINUTE / 2)); - env.state_mut(|| utils::feed_oracle::(vec![(PRICE_A, PRICE_VALUE_B)])); - let loan_portfolio = env.state(|| T::Api::portfolio_loan(POOL_A, loan_id).unwrap()); env.state_mut(|| { // Give required tokens to the borrower to be able to repay the interest accrued // until this moment utils::give_tokens::(BORROWER.id(), Usd6::ID, loan_portfolio.outstanding_interest); + + // Oracle modify the value + utils::feed_oracle::(vec![(PRICE_A, PRICE_VALUE_B)]) }); env.submit_now( BORROWER, - pallet_loans::Call::repay { - pool_id: POOL_A, - loan_id, - amount: RepaidInput { - principal: PrincipalInput::External(ExternalAmount { - quantity: Quantity::from_integer(50), - settlement_price: currency::price_to_currency(PRICE_VALUE_B, Usd6::ID), - }), - interest: loan_portfolio.outstanding_interest, - unscheduled: 0, - }, - }, + call::repay_external(loan_id, loan_portfolio.outstanding_interest, PRICE_VALUE_B), ) .unwrap(); // Closing the loan succesfully means that the loan has been fully repaid - env.submit_now( - BORROWER, - pallet_loans::Call::close { - pool_id: POOL_A, - loan_id, - }, - ) - .unwrap(); + env.submit_now(BORROWER, call::close(loan_id)).unwrap(); } crate::test_for_runtimes!(all, internal_priced); From d699e1c6e0f343f4f07bba2a23fa2d9e8616dd6c Mon Sep 17 00:00:00 2001 From: lemunozm Date: Thu, 26 Oct 2023 14:54:50 +0200 Subject: [PATCH 08/11] add maturity extension changed test --- pallets/loans/docs/types.md | 2 +- .../src/generic/cases/loans.rs | 91 +++++++++++++++++-- .../integration-tests/src/generic/config.rs | 2 + runtime/integration-tests/src/generic/env.rs | 6 +- .../src/generic/utils/currency.rs | 3 + .../src/generic/utils/genesis.rs | 5 + .../src/generic/utils/mod.rs | 6 +- 7 files changed, 100 insertions(+), 15 deletions(-) diff --git a/pallets/loans/docs/types.md b/pallets/loans/docs/types.md index 36b336e6ae..ee5ef6bc33 100644 --- a/pallets/loans/docs/types.md +++ b/pallets/loans/docs/types.md @@ -99,7 +99,7 @@ package policy { } enum WriteOffTrigger { - PrincipalOverdueDays, + PrincipalOverdue, PriceOutdated, } diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index bd600e00fb..40ccbe6dfc 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -3,10 +3,15 @@ use cfg_traits::{ interest::{CompoundingSchedule, InterestRate}, Seconds, TimeAsSecs, }; -use cfg_types::{fixed_point::Quantity, oracles::OracleKey, permissions::PoolRole}; -use frame_support::traits::Get; +use cfg_types::{ + fixed_point::{Quantity, Rate}, + oracles::OracleKey, + permissions::PoolRole, +}; +use frame_support::{assert_err, traits::Get}; use pallet_loans::{ entities::{ + changes::LoanMutation, input::{PrincipalInput, RepaidInput}, loans::LoanInfo, pricing::{ @@ -16,8 +21,8 @@ use pallet_loans::{ }, }, types::{ - valuation::ValuationMethod, BorrowRestrictions, InterestPayments, LoanRestrictions, - Maturity, PayDownSchedule, RepayRestrictions, RepaymentSchedule, + valuation::ValuationMethod, BorrowLoanError, BorrowRestrictions, InterestPayments, + LoanRestrictions, Maturity, PayDownSchedule, RepayRestrictions, RepaymentSchedule, }, }; use runtime_common::apis::{ @@ -42,6 +47,8 @@ use crate::{ const POOL_ADMIN: Keyring = Keyring::Admin; const INVESTOR: Keyring = Keyring::Alice; const BORROWER: Keyring = Keyring::Bob; +const LOAN_ADMIN: Keyring = Keyring::Charlie; +const ANY: Keyring = Keyring::Dave; const POOL_A: PoolId = 23; const NFT_A: (CollectionId, ItemId) = (1, ItemId(10)); @@ -72,6 +79,13 @@ mod common { utils::give_balance::(POOL_ADMIN.id(), T::PoolDeposit::get()); utils::create_empty_pool::(POOL_ADMIN.id(), POOL_A, Usd6::ID); + // Setting borrower + utils::give_pool_role::(BORROWER.id(), POOL_A, PoolRole::Borrower); + utils::give_nft::(BORROWER.id(), NFT_A); + + // Setting a loan admin + utils::give_pool_role::(LOAN_ADMIN.id(), POOL_A, PoolRole::LoanAdmin); + // Funding a pool let tranche_id = T::Api::tranche_id(POOL_A, 0).unwrap(); let tranche_investor = PoolRole::TrancheInvestor(tranche_id, Seconds::MAX); @@ -85,10 +99,6 @@ mod common { env.state_mut(|| { // New epoch with the investor funds available utils::close_pool_epoch::(POOL_ADMIN.id(), POOL_A); - - // Preparing borrower - utils::give_pool_role::(BORROWER.id(), POOL_A, PoolRole::Borrower); - utils::give_nft::(BORROWER.id(), NFT_A); }); env @@ -102,6 +112,14 @@ mod common { .unwrap() } + pub fn last_change_id, T: Runtime>(env: &E) -> T::Hash { + env.find_event(|e| match e { + pallet_pool_system::Event::::ProposedChange { change_id, .. } => Some(change_id), + _ => None, + }) + .unwrap() + } + pub fn default_loan_info(now: Seconds, pricing: Pricing) -> LoanInfo { LoanInfo { schedule: RepaymentSchedule { @@ -212,6 +230,24 @@ mod call { loan_id, } } + + pub fn propose_loan_mutation( + loan_id: LoanId, + mutation: LoanMutation, + ) -> pallet_loans::Call { + pallet_loans::Call::propose_loan_mutation { + pool_id: POOL_A, + loan_id, + mutation, + } + } + + pub fn apply_loan_mutation(change_id: T::Hash) -> pallet_loans::Call { + pallet_loans::Call::apply_loan_mutation { + pool_id: POOL_A, + change_id, + } + } } /// Test the basic loan flow, which consist in: @@ -226,7 +262,6 @@ fn internal_priced() { let now = as TimeAsSecs>::now(); common::default_loan_info::(now, common::default_internal_pricing()) }); - env.submit_now(BORROWER, call::create(&info)).unwrap(); let loan_id = common::last_loan_id(&env); @@ -263,7 +298,6 @@ fn oracle_priced() { let now = as TimeAsSecs>::now(); common::default_loan_info::(now, common::default_external_pricing()) }); - env.submit_now(BORROWER, call::create(&info)).unwrap(); let loan_id = common::last_loan_id(&env); @@ -293,5 +327,42 @@ fn oracle_priced() { env.submit_now(BORROWER, call::close(loan_id)).unwrap(); } +fn update_maturity_extension() { + let mut env = common::initialize_state_for_loans::, T>(); + + let info = env.state(|| { + let now = as TimeAsSecs>::now(); + common::default_loan_info::(now, common::default_internal_pricing()) + }); + env.submit_now(BORROWER, call::create(&info)).unwrap(); + + let loan_id = common::last_loan_id(&env); + + env.submit_now(BORROWER, call::borrow_internal(loan_id)) + .unwrap(); + + env.pass(Blocks::BySeconds(SECONDS_PER_MINUTE)); + + // Loan at this point is overdue and trying to borrow it will fail + assert_err!( + env.submit_now(BORROWER, call::borrow_internal(loan_id)), + pallet_loans::Error::::BorrowLoanError(BorrowLoanError::MaturityDatePassed), + ); + + env.submit_now( + LOAN_ADMIN, + call::propose_loan_mutation(loan_id, LoanMutation::MaturityExtension(12 /* seconds */)), + ) + .unwrap(); + + let change_id = common::last_change_id(&env); + env.submit_now(LOAN_ADMIN, call::apply_loan_mutation(change_id)) + .unwrap(); + + // Now the loan is no longer overdue and can be borrowed again + env.submit_now(ANY, call::borrow_internal(loan_id)).unwrap(); +} + crate::test_for_runtimes!(all, internal_priced); crate::test_for_runtimes!(all, oracle_priced); +crate::test_for_runtimes!(all, update_maturity_extension); diff --git a/runtime/integration-tests/src/generic/config.rs b/runtime/integration-tests/src/generic/config.rs index 9d455aa59e..d3dd98b8c4 100644 --- a/runtime/integration-tests/src/generic/config.rs +++ b/runtime/integration-tests/src/generic/config.rs @@ -122,10 +122,12 @@ pub trait Runtime: + TryInto> + TryInto> + TryInto> + + TryInto> + From> + From> + From> + From> + + From> + From>; /// Block used by the runtime diff --git a/runtime/integration-tests/src/generic/env.rs b/runtime/integration-tests/src/generic/env.rs index 1ed610e274..8dee7b01ec 100644 --- a/runtime/integration-tests/src/generic/env.rs +++ b/runtime/integration-tests/src/generic/env.rs @@ -46,9 +46,9 @@ pub trait Env { /// Pass any number of blocks fn pass(&mut self, blocks: Blocks) { let (next, end_block) = self.state(|| { - let next = frame_system::Pallet::::block_number() + 1; + let current = frame_system::Pallet::::block_number(); - let end_block = next + let end_block = current + match blocks { Blocks::ByNumber(n) => n, Blocks::BySeconds(secs) => { @@ -62,7 +62,7 @@ pub trait Env { Blocks::UntilEvent { limit, .. } => limit, }; - (next, end_block) + (current + 1, end_block) }); for i in next..end_block { diff --git a/runtime/integration-tests/src/generic/utils/currency.rs b/runtime/integration-tests/src/generic/utils/currency.rs index 3a0c180785..a84c633838 100644 --- a/runtime/integration-tests/src/generic/utils/currency.rs +++ b/runtime/integration-tests/src/generic/utils/currency.rs @@ -1,3 +1,6 @@ +//! PLEASE be as much generic as possible because no domain or use cases are +//! considered at this level. + use cfg_primitives::{conversion, Balance, CFG}; use cfg_types::tokens::{AssetMetadata, CrossChainTransferability, CurrencyId, CustomMetadata}; use sp_runtime::FixedPointNumber; diff --git a/runtime/integration-tests/src/generic/utils/genesis.rs b/runtime/integration-tests/src/generic/utils/genesis.rs index 99dbd9ef71..8f46ab24a2 100644 --- a/runtime/integration-tests/src/generic/utils/genesis.rs +++ b/runtime/integration-tests/src/generic/utils/genesis.rs @@ -1,3 +1,6 @@ +//! PLEASE be as much generic as possible because no domain or use cases are +//! considered at this level. + use std::marker::PhantomData; use cfg_primitives::Balance; @@ -36,6 +39,8 @@ impl Genesis { } } +// Add GenesisBuild functions for initialize your pallets + pub fn balances(balance: Balance) -> impl GenesisBuild { pallet_balances::GenesisConfig:: { balances: default_accounts() diff --git a/runtime/integration-tests/src/generic/utils/mod.rs b/runtime/integration-tests/src/generic/utils/mod.rs index dff37029fe..bb9a28abb7 100644 --- a/runtime/integration-tests/src/generic/utils/mod.rs +++ b/runtime/integration-tests/src/generic/utils/mod.rs @@ -1,4 +1,8 @@ -// Divide this utilties into files when it grows +//! PLEASE be as much generic as possible because no domain or use cases are +//! considered at util level and below modules. If you need utilities related to +//! your use case, add them under `cases/`. +//! +//! Divide this utilities into files when it grows pub mod currency; pub mod genesis; From 4ab64126aaa1817fa720d7c78d7591c8f44aa507 Mon Sep 17 00:00:00 2001 From: lemunozm Date: Thu, 26 Oct 2023 16:43:43 +0200 Subject: [PATCH 09/11] minor doc fixes --- runtime/integration-tests/src/generic/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/integration-tests/src/generic/README.md b/runtime/integration-tests/src/generic/README.md index 1fc390dd91..403192b0dc 100644 --- a/runtime/integration-tests/src/generic/README.md +++ b/runtime/integration-tests/src/generic/README.md @@ -2,11 +2,11 @@ The aim of this module is to make integration-tests independently for all runtimes at once. -You can choose the environment you for your each of your use cases: +You can choose the environment for each of your use cases: - `RuntimeEnv`: Simple environment that acts as a wrapper over the runtime - `FudgeEnv`: Advanced environment that use a client and connect the runtime to a relay chain. -Both environment uses the same interface so jumping from one to the another should be something smooth. +Both environment uses the same interface so jumping from one to the another should be something "smooth". ## Where I start? - Create a new file in `cases/` for the use case you want to test. @@ -16,7 +16,7 @@ Both environment uses the same interface so jumping from one to the another shou - Adding bounds to `T::RuntimeCallExt` to support calls from your pallet. - Adding bounds to `T::EventExt` to support events from your pallet. - Adding bounds to `T::Api` to support new api calls. -- You can add `GenerisBuild` builders for setting the initial state of your pallet for others in `utils/genesis.rs`. +- You can add `GenesisBuild` builders for setting the initial state of your pallet for others in `utils/genesis.rs`. Please be as generic and simple as possible to leave others to compose its own requirement using your method, without hidden initializations. - You can add any utility that helps to initialize states for others under `utils` folder. From 5b264e035cfad1f325992beb562da10fb4a8c29c Mon Sep 17 00:00:00 2001 From: lemunozm Date: Thu, 26 Oct 2023 18:28:10 +0200 Subject: [PATCH 10/11] fix block by seconds issue --- .../src/generic/cases/loans.rs | 5 +- runtime/integration-tests/src/generic/env.rs | 81 +++++++++++++------ 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/runtime/integration-tests/src/generic/cases/loans.rs b/runtime/integration-tests/src/generic/cases/loans.rs index 40ccbe6dfc..cab0d66112 100644 --- a/runtime/integration-tests/src/generic/cases/loans.rs +++ b/runtime/integration-tests/src/generic/cases/loans.rs @@ -356,11 +356,12 @@ fn update_maturity_extension() { .unwrap(); let change_id = common::last_change_id(&env); - env.submit_now(LOAN_ADMIN, call::apply_loan_mutation(change_id)) + env.submit_now(ANY, call::apply_loan_mutation(change_id)) .unwrap(); // Now the loan is no longer overdue and can be borrowed again - env.submit_now(ANY, call::borrow_internal(loan_id)).unwrap(); + env.submit_now(BORROWER, call::borrow_internal(loan_id)) + .unwrap(); } crate::test_for_runtimes!(all, internal_priced); diff --git a/runtime/integration-tests/src/generic/env.rs b/runtime/integration-tests/src/generic/env.rs index 8dee7b01ec..d8dd5a48e4 100644 --- a/runtime/integration-tests/src/generic/env.rs +++ b/runtime/integration-tests/src/generic/env.rs @@ -6,25 +6,45 @@ use sp_runtime::{ traits::{Block, Extrinsic}, DispatchError, DispatchResult, MultiSignature, Storage, }; +use sp_std::ops::Range; use crate::{generic::config::Runtime, utils::accounts::Keyring}; /// Used by Env::pass() to determine how many blocks should be passed #[derive(Clone)] -pub enum Blocks { +pub enum Blocks { /// Pass X blocks ByNumber(BlockNumber), /// Pass a number of blocks enough to emulate the given passage of time. /// i.e. choosing 1 sec would pass 1 block to emulate such change in the /// time. + /// See the test below for an example BySeconds(Seconds), /// Pass a number of block until find an event or reach the limit - UntilEvent { - event: T::RuntimeEventExt, - limit: BlockNumber, - }, + UntilEvent { event: Event, limit: BlockNumber }, +} + +impl Blocks { + fn range_for(&self, current: BlockNumber, slot_duration: Seconds) -> Range { + let blocks = match self { + Blocks::ByNumber(n) => *n, + Blocks::BySeconds(secs) => { + let n = secs / slot_duration; + if secs % slot_duration != 0 { + n as BlockNumber + 1 + } else { + n as BlockNumber + } + } + Blocks::UntilEvent { limit, .. } => *limit, + }; + + dbg!(blocks); + + (current + 1)..(current + 1 + blocks) + } } /// Define an environment behavior @@ -44,28 +64,15 @@ pub trait Env { fn submit_later(&mut self, who: Keyring, call: impl Into) -> DispatchResult; /// Pass any number of blocks - fn pass(&mut self, blocks: Blocks) { - let (next, end_block) = self.state(|| { - let current = frame_system::Pallet::::block_number(); - - let end_block = current - + match blocks { - Blocks::ByNumber(n) => n, - Blocks::BySeconds(secs) => { - let n = secs / pallet_aura::Pallet::::slot_duration().into_seconds(); - if n % pallet_aura::Pallet::::slot_duration() != 0 { - n as BlockNumber + 1 - } else { - n as BlockNumber - } - } - Blocks::UntilEvent { limit, .. } => limit, - }; - - (current + 1, end_block) + fn pass(&mut self, blocks: Blocks) { + let (current, slot) = self.state(|| { + ( + frame_system::Pallet::::block_number(), + pallet_aura::Pallet::::slot_duration().into_seconds(), + ) }); - for i in next..end_block { + for i in blocks.range_for(current, slot) { self.__priv_build_block(i); if let Blocks::UntilEvent { event, .. } = blocks.clone() { @@ -149,3 +156,27 @@ pub mod utils { ::Extrinsic::new(runtime_call, Some(multi_address)).unwrap() } } + +mod tests { + use super::*; + struct MockEnv; + + const SLOT_DURATION: Seconds = 12; + + fn blocks_from(current: BlockNumber, blocks: Blocks<()>) -> Vec { + blocks + .range_for(current, SLOT_DURATION) + .into_iter() + .collect() + } + + #[test] + fn by_seconds() { + assert_eq!(blocks_from(0, Blocks::BySeconds(0)), [] as [BlockNumber; 0]); + assert_eq!(blocks_from(0, Blocks::BySeconds(1)), [1]); + assert_eq!(blocks_from(0, Blocks::BySeconds(12)), [1]); + assert_eq!(blocks_from(5, Blocks::BySeconds(0)), [] as [BlockNumber; 0]); + assert_eq!(blocks_from(5, Blocks::BySeconds(12)), [6]); + assert_eq!(blocks_from(5, Blocks::BySeconds(60)), [6, 7, 8, 9, 10]); + } +} From 83a50952be82965e1b554e854f11668941f5912d Mon Sep 17 00:00:00 2001 From: lemunozm Date: Thu, 26 Oct 2023 19:31:11 +0200 Subject: [PATCH 11/11] Support jumping to future blocks --- .../src/generic/cases/example.rs | 17 ++++++- runtime/integration-tests/src/generic/env.rs | 50 +++++++++++++------ .../src/generic/envs/fudge_env.rs | 6 ++- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/runtime/integration-tests/src/generic/cases/example.rs b/runtime/integration-tests/src/generic/cases/example.rs index 694e94a25a..6e4bd4310a 100644 --- a/runtime/integration-tests/src/generic/cases/example.rs +++ b/runtime/integration-tests/src/generic/cases/example.rs @@ -1,4 +1,5 @@ -use cfg_primitives::{Balance, CFG}; +use cfg_primitives::{Balance, CFG, SECONDS_PER_YEAR}; +use cfg_traits::IntoSeconds; use frame_support::traits::Get; use sp_api::runtime_decl_for_Core::CoreV4; @@ -156,7 +157,21 @@ fn fudge_call_api() { }) } +fn pass_time_one_block() { + let mut env = RuntimeEnv::::from_storage(Default::default()); + + let before = env.state(|| pallet_timestamp::Pallet::::get()); + + // Not supported in fudge + env.pass(Blocks::JumpBySeconds(SECONDS_PER_YEAR)); + + let after = env.state(|| pallet_timestamp::Pallet::::get()); + + assert_eq!((after - before).into_seconds(), SECONDS_PER_YEAR) +} + crate::test_for_runtimes!([development, altair, centrifuge], transfer_balance); crate::test_for_runtimes!(all, call_api); crate::test_for_runtimes!(all, fudge_transfer_balance); crate::test_for_runtimes!(all, fudge_call_api); +crate::test_for_runtimes!(all, pass_time_one_block); diff --git a/runtime/integration-tests/src/generic/env.rs b/runtime/integration-tests/src/generic/env.rs index d8dd5a48e4..1d37a6b0aa 100644 --- a/runtime/integration-tests/src/generic/env.rs +++ b/runtime/integration-tests/src/generic/env.rs @@ -24,26 +24,37 @@ pub enum Blocks { /// Pass a number of block until find an event or reach the limit UntilEvent { event: Event, limit: BlockNumber }, + + /// Jumps to a block in the future to reach the requested time. + /// Only one real block is created in the process. + /// This can be used to emulate passing time during long periods + /// computationally very fast. + /// (i.e. years) + JumpBySeconds(Seconds), } impl Blocks { fn range_for(&self, current: BlockNumber, slot_duration: Seconds) -> Range { - let blocks = match self { - Blocks::ByNumber(n) => *n, + let next = current + 1; + let (from, to) = match self { + Blocks::ByNumber(n) => (next, next + *n), Blocks::BySeconds(secs) => { - let n = secs / slot_duration; + let mut blocks = (secs / slot_duration) as BlockNumber; if secs % slot_duration != 0 { - n as BlockNumber + 1 - } else { - n as BlockNumber - } + blocks += 1 + }; + (next, next + blocks) + } + Blocks::UntilEvent { limit, .. } => (next, next + *limit), + Blocks::JumpBySeconds(secs) => { + let mut blocks = (secs / slot_duration) as BlockNumber; + if secs % slot_duration != 0 { + blocks += 1 + }; + (next + blocks.saturating_sub(1), next + blocks) } - Blocks::UntilEvent { limit, .. } => *limit, }; - - dbg!(blocks); - - (current + 1)..(current + 1 + blocks) + from..to } } @@ -162,6 +173,7 @@ mod tests { struct MockEnv; const SLOT_DURATION: Seconds = 12; + const EMPTY: [BlockNumber; 0] = []; fn blocks_from(current: BlockNumber, blocks: Blocks<()>) -> Vec { blocks @@ -172,11 +184,21 @@ mod tests { #[test] fn by_seconds() { - assert_eq!(blocks_from(0, Blocks::BySeconds(0)), [] as [BlockNumber; 0]); + assert_eq!(blocks_from(0, Blocks::BySeconds(0)), EMPTY); assert_eq!(blocks_from(0, Blocks::BySeconds(1)), [1]); assert_eq!(blocks_from(0, Blocks::BySeconds(12)), [1]); - assert_eq!(blocks_from(5, Blocks::BySeconds(0)), [] as [BlockNumber; 0]); + assert_eq!(blocks_from(5, Blocks::BySeconds(0)), EMPTY); assert_eq!(blocks_from(5, Blocks::BySeconds(12)), [6]); assert_eq!(blocks_from(5, Blocks::BySeconds(60)), [6, 7, 8, 9, 10]); } + + #[test] + fn by_seconds_fast() { + assert_eq!(blocks_from(0, Blocks::JumpBySeconds(0)), EMPTY); + assert_eq!(blocks_from(0, Blocks::JumpBySeconds(1)), [1]); + assert_eq!(blocks_from(0, Blocks::JumpBySeconds(12)), [1]); + assert_eq!(blocks_from(5, Blocks::JumpBySeconds(0)), EMPTY); + assert_eq!(blocks_from(5, Blocks::JumpBySeconds(12)), [6]); + assert_eq!(blocks_from(5, Blocks::JumpBySeconds(60)), [10]); + } } diff --git a/runtime/integration-tests/src/generic/envs/fudge_env.rs b/runtime/integration-tests/src/generic/envs/fudge_env.rs index e30c1936d6..76db6147c1 100644 --- a/runtime/integration-tests/src/generic/envs/fudge_env.rs +++ b/runtime/integration-tests/src/generic/envs/fudge_env.rs @@ -76,7 +76,11 @@ impl Env for FudgeEnv { self.handle.parachain().with_state(f).unwrap() } - fn __priv_build_block(&mut self, _i: BlockNumber) { + fn __priv_build_block(&mut self, i: BlockNumber) { + let current = self.state(|| frame_system::Pallet::::block_number()); + if i > current + 1 { + panic!("Jump to future blocks is unsupported in fudge (maybe you've used Blocks::BySecondsFast?)"); + } self.handle.evolve(); } }