diff --git a/Cargo.lock b/Cargo.lock index 3bf1916ebb..f91bd4bdfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1723,15 +1723,6 @@ dependencies = [ "windows-targets 0.52.4", ] -[[package]] -name = "chronoutil" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa531c9c2b0e6168a6e4c5023cd38e8d5ab009d3a10459cd3e7baecd68fc3715" -dependencies = [ - "chrono", -] - [[package]] name = "cid" version = "0.9.0" @@ -8104,7 +8095,6 @@ dependencies = [ "cfg-types", "cfg-utils", "chrono", - "chronoutil", "frame-benchmarking", "frame-support", "frame-system", diff --git a/Cargo.toml b/Cargo.toml index cd58c59256..bafc1e0612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,6 @@ impl-trait-for-tuples = "0.2.1" num-traits = { version = "0.2", default-features = false } num_enum = { version = "0.5.3", default-features = false } chrono = { version = "0.4", default-features = false } -chronoutil = "0.2" # Cumulus cumulus-pallet-aura-ext = { git = "https://github.com/paritytech/polkadot-sdk", default-features = false, branch = "release-polkadot-v1.1.0" } diff --git a/pallets/loans/Cargo.toml b/pallets/loans/Cargo.toml index e4008ece4d..f505a74dbe 100644 --- a/pallets/loans/Cargo.toml +++ b/pallets/loans/Cargo.toml @@ -16,7 +16,6 @@ targets = ["x86_64-unknown-linux-gnu"] parity-scale-codec = { workspace = true } scale-info = { workspace = true } chrono = { workspace = true } -chronoutil = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } diff --git a/pallets/loans/src/entities/loans.rs b/pallets/loans/src/entities/loans.rs index dbe3b55c3e..6a99dd20a4 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -14,7 +14,7 @@ use sp_runtime::{ }, DispatchError, }; -use sp_std::collections::btree_map::BTreeMap; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; use crate::{ entities::{ diff --git a/pallets/loans/src/types/cashflow.rs b/pallets/loans/src/types/cashflow.rs index 4a50157382..af5353d6e1 100644 --- a/pallets/loans/src/types/cashflow.rs +++ b/pallets/loans/src/types/cashflow.rs @@ -12,8 +12,7 @@ // GNU General Public License for more details. use cfg_traits::{interest::InterestRate, Seconds}; -use chrono::{DateTime, NaiveDate}; -use chronoutil::DateRule; +use chrono::{DateTime, Datelike, NaiveDate}; use frame_support::pallet_prelude::RuntimeDebug; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -23,6 +22,13 @@ use sp_runtime::{ }, ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, }; +use sp_std::{cmp::min, vec, vec::Vec}; + +// By now only "day 1" of the month is supported for monthly cashflows. +// Modifying this value would make `monthly_dates()` and +// `monthly_dates_intervals()` to no longer work as expected. +// Supporting more reference dates will imply more logic related to dates. +const REFERENCE_DAY_1: u32 = 1; /// Specify the expected repayments date #[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] @@ -78,6 +84,15 @@ fn date_to_seconds(date: NaiveDate) -> Result { .ensure_into()?) } +fn next_month_with_day(date: NaiveDate, day: u32) -> Option { + let (month, year) = match date.month() { + 12 => (1, date.year() + 1), + n => (n + 1, date.year()), + }; + + NaiveDate::from_ymd_opt(year, month, day) +} + /// Interest payment periods #[derive(Encode, Decode, Clone, PartialEq, Eq, TypeInfo, RuntimeDebug, MaxEncodedLen)] pub enum InterestPayments { @@ -161,30 +176,37 @@ fn monthly_dates( end_date: NaiveDate, reference_day: u32, ) -> Result, DispatchError> { - if start_date > end_date { + if start_date >= end_date { return Err(DispatchError::Other("Cashflow must start before it ends")); } - let mut dates = DateRule::monthly(start_date) - .with_end(end_date) - .with_rolling_day(reference_day) - .map_err(|_| DispatchError::Other("Paydown day must be less than 31 days"))? - .into_iter() - .collect::>(); - - // We want to skip the first month if the date is before the starting date - if let Some(first) = dates.first() { - if *first <= start_date { - dates.remove(0); - } + if reference_day != REFERENCE_DAY_1 { + return Err(DispatchError::Other( + "Only day 1 as reference is supported by now", + )); } - // We want to add the end date as last date if the previous last date is before - // end date - if let Some(last) = dates.last() { - if *last < end_date { - dates.push(end_date); + let first_date = + next_month_with_day(start_date, REFERENCE_DAY_1).ok_or("must be a correct date, qed")?; + + let mut dates = vec![min(first_date, end_date)]; + + loop { + let last = dates + .last() + .ok_or(DispatchError::Other("must be a last date, qed"))?; + + let next = + next_month_with_day(*last, REFERENCE_DAY_1).ok_or("must be a correct date, qed")?; + + if next >= end_date { + if *last < end_date { + dates.push(end_date); + } + break; } + + dates.push(next); } Ok(dates) @@ -204,13 +226,16 @@ fn monthly_dates_intervals( .enumerate() .map(|(i, date)| { let days = match i { - 0 => (date - start_date).num_days(), + 0 if last_index == 0 => end_date.day() - REFERENCE_DAY_1, + 0 if start_date.day() == REFERENCE_DAY_1 => 30, + 0 => (date - start_date).num_days().ensure_into()?, + n if n == last_index && end_date.day() == REFERENCE_DAY_1 => 30, n if n == last_index => { let prev_date = monthly_dates .get(n.ensure_sub(1)?) .ok_or(DispatchError::Other("n > 1. qed"))?; - (date - *prev_date).num_days() + (date - *prev_date).num_days().ensure_into()? } _ => 30, }; @@ -276,50 +301,40 @@ mod tests { (from_ymd(2022, 4, 20), Rate::from((19, 30))), ] ); - - assert_ok!( - monthly_dates_intervals(from_ymd(2022, 1, 20), from_ymd(2022, 4, 20), 31), - vec![ - (from_ymd(2022, 1, 31), Rate::from((11, 30))), - (from_ymd(2022, 2, 28), Rate::one()), - (from_ymd(2022, 3, 31), Rate::one()), - (from_ymd(2022, 4, 20), Rate::from((20, 30))), - ] - ); } #[test] fn one_day() { assert_err!( - monthly_dates(from_ymd(2022, 1, 20), from_ymd(2022, 1, 20), 31), - DispatchError::Arithmetic(ArithmeticError::Underflow), + monthly_dates(from_ymd(2022, 1, 20), from_ymd(2022, 1, 20), 1), + DispatchError::Other("Cashflow must start before it ends") ); } #[test] - fn same_month() { - assert_ok!( - monthly_dates_intervals(from_ymd(2022, 1, 10), from_ymd(2022, 1, 15), 20), - vec![(from_ymd(2022, 1, 15), Rate::from((5, 30)))] + fn unsupported_reference_day() { + assert_err!( + monthly_dates(from_ymd(2022, 1, 20), from_ymd(2022, 4, 20), 2), + DispatchError::Other("Only day 1 as reference is supported by now") ); + } + #[test] + fn start_and_end_same_day_as_reference_day() { assert_ok!( - monthly_dates_intervals(from_ymd(2022, 1, 10), from_ymd(2022, 1, 20), 15), + monthly_dates_intervals(from_ymd(2022, 1, 1), from_ymd(2022, 3, 1), 1), vec![ - (from_ymd(2022, 1, 15), Rate::from((5, 30))), - (from_ymd(2022, 1, 20), Rate::from((5, 30))), + (from_ymd(2022, 2, 1), Rate::one()), + (from_ymd(2022, 3, 1), Rate::one()), ] ); } #[test] - fn same_day_as_paydown_day() { + fn same_month() { assert_ok!( - monthly_dates_intervals(from_ymd(2022, 1, 15), from_ymd(2022, 3, 15), 15), - vec![ - (from_ymd(2022, 2, 15), Rate::one()), - (from_ymd(2022, 3, 15), Rate::one()), - ] + monthly_dates_intervals(from_ymd(2022, 1, 1), from_ymd(2022, 1, 15), 1), + vec![(from_ymd(2022, 1, 15), Rate::from((14, 30)))] ); } } diff --git a/runtime/altair/src/lib.rs b/runtime/altair/src/lib.rs index 4aa5e3bf40..f41605438c 100644 --- a/runtime/altair/src/lib.rs +++ b/runtime/altair/src/lib.rs @@ -121,7 +121,7 @@ use sp_runtime::{ ApplyExtrinsicResult, DispatchError, DispatchResult, FixedI128, Perbill, Permill, Perquintill, }; use sp_staking::currency_to_vote::U128CurrencyToVote; -use sp_std::{marker::PhantomData, prelude::*}; +use sp_std::{marker::PhantomData, prelude::*, vec::Vec}; use sp_version::RuntimeVersion; use staging_xcm_executor::XcmExecutor; use static_assertions::const_assert; @@ -2350,6 +2350,10 @@ impl_runtime_apis! { ) -> Result { Ok(runtime_common::update_nav_with_input(pool_id, input_prices)?.nav_aum) } + + fn cashflow(pool_id: PoolId, loan_id: LoanId) -> Result, DispatchError> { + Loans::cashflow(pool_id, loan_id) + } } diff --git a/runtime/centrifuge/src/lib.rs b/runtime/centrifuge/src/lib.rs index f31ab882d1..c372a136f3 100644 --- a/runtime/centrifuge/src/lib.rs +++ b/runtime/centrifuge/src/lib.rs @@ -124,7 +124,7 @@ use sp_runtime::{ ApplyExtrinsicResult, FixedI128, Perbill, Permill, Perquintill, }; use sp_staking::currency_to_vote::U128CurrencyToVote; -use sp_std::{marker::PhantomData, prelude::*}; +use sp_std::{marker::PhantomData, prelude::*, vec::Vec}; use sp_version::RuntimeVersion; use staging_xcm_executor::XcmExecutor; use static_assertions::const_assert; @@ -2398,6 +2398,10 @@ impl_runtime_apis! { ) -> Result { Ok(runtime_common::update_nav_with_input(pool_id, input_prices)?.nav_aum) } + + fn cashflow(pool_id: PoolId, loan_id: LoanId) -> Result, DispatchError> { + Loans::cashflow(pool_id, loan_id) + } } // Investment Runtime APIs diff --git a/runtime/development/src/lib.rs b/runtime/development/src/lib.rs index 3f85a22f85..eb2412a37b 100644 --- a/runtime/development/src/lib.rs +++ b/runtime/development/src/lib.rs @@ -128,7 +128,7 @@ use sp_runtime::{ ApplyExtrinsicResult, FixedI128, Perbill, Permill, Perquintill, }; use sp_staking::currency_to_vote::U128CurrencyToVote; -use sp_std::{marker::PhantomData, prelude::*}; +use sp_std::{marker::PhantomData, prelude::*, vec::Vec}; use sp_version::RuntimeVersion; use staging_xcm_executor::XcmExecutor; use static_assertions::const_assert; @@ -2437,6 +2437,10 @@ impl_runtime_apis! { ) -> Result { Ok(runtime_common::update_nav_with_input(pool_id, input_prices)?.nav_aum) } + + fn cashflow(pool_id: PoolId, loan_id: LoanId) -> Result, DispatchError> { + Loans::cashflow(pool_id, loan_id) + } } // Investment Runtime APIs